Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/auto-effort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-adaptive-thinking": minor
---

Add an opt-in `autoSelect` mode that classifies the first user request in a session and carries the chosen reasoning effort until the agent, an explicit variant, or a model change overrides it.
5 changes: 5 additions & 0 deletions .changeset/effort-recommendation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-adaptive-thinking": minor
---

Recommend a reasoning effort for each user turn and tell the agent when not to change it. The default guidance now relies on that recommendation and keeps a mid-turn reassessment clause.
5 changes: 5 additions & 0 deletions .changeset/quiet-resets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-adaptive-thinking": patch
---

Reset a temporary reasoning effort to the level in effect before the first override, so repeated changes no longer reset to an intermediate level or skip the reset. Read session history once per tool call with a bounded query, cache provider variants, merge guidance into an existing first system prompt, and remove the runtime Zod dependency.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The plugin accepts these optional settings:

- `enabled`: enable or disable the plugin without removing it from config. Defaults to `true`.
- `quiet`: suppress toast notifications for configuration errors. Defaults to `false`.
- `autoSelect`: choose an initial effort level from the first user request in a session and carry it across turns. Defaults to `false`.
- `toolName`: name of the reasoning-effort tool exposed to agents. Defaults to `set_reasoning_effort`.
- `toolDescription`: description for the reasoning-effort tool. Defaults to `Set your reasoning effort`.
- `systemPrompt`: custom adaptive-thinking guidance injected into the system prompt.
Expand Down Expand Up @@ -89,6 +90,12 @@ Example persisted change:

The system prompt lists the valid levels for the current session. Cached session state is only used when it is still valid for the current model, so switching models will not advertise or reuse an invalid effort level.

With `autoSelect` enabled, the plugin classifies the first user request in a session and sets the effort from the available variants for the current model, ignoring `none`. The chosen level is carried across turns until the agent changes it, the model changes, or a request carries an explicit variant. An explicit variant (UI picker, CLI flag, or API field) is honored and becomes the level carried forward.

When `autoSelect` is off, the plugin still classifies each user turn and adds a recommendation to the guidance. If the recommended level differs from the current one, the agent is told to call the tool with that level; if it matches, the agent is told to keep the level unless the work grows beyond it. The default guidance relies on this recommendation instead of a blanket reassessment instruction.

Adaptive-thinking guidance is merged into the first existing system prompt instead of a separate system message, so strict chat templates that allow only one system message keep working.

## Troubleshooting

If the tool returns `no valid reasoning effort levels are available for this session`, check that the active model exposes variants in OpenCode. Some providers or models may not support reasoning-effort levels.
Expand Down
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,6 @@
"verify": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build",
"version-packages": "changeset version"
},
"dependencies": {
"zod": "^4.4.3"
},
"devDependencies": {
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.31.0",
Expand Down
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions src/complexity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from "vitest";
import { classifyPrompt, pickVariant } from "./complexity";

test("classifies prompt complexity", () => {
expect(classifyPrompt("fix typo in the README")).toBe("simple");
expect(classifyPrompt("add a test for the parser")).toBe("normal");
expect(classifyPrompt("debug the flaky test")).toBe("complex");
expect(classifyPrompt("debug the flaky test and trace the root cause across modules")).toBe(
"deep",
);
});

test("maps complexity to an available variant", () => {
const all = ["none", "low", "medium", "high", "xhigh"];
expect(pickVariant(all, "simple")).toBe("low");
expect(pickVariant(all, "normal")).toBe("medium");
expect(pickVariant(all, "complex")).toBe("high");
expect(pickVariant(all, "deep")).toBe("xhigh");
expect(pickVariant(["low"], "deep")).toBe("low");
expect(pickVariant([], "normal")).toBeUndefined();
});
54 changes: 54 additions & 0 deletions src/complexity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export type Complexity = "simple" | "normal" | "complex" | "deep";

const signals: Array<[RegExp, number]> = [
[/\b(debug|investigate|trace)\b/i, 2],
[/\b(root cause|flaky|race|deadlock|leak)\b/i, 2],
[
/\b(architecture|architect|design|plan|refactor|migrat\w*|review|audit|security|performance|optimi[sz]e)\b/i,
2,
],
[/\b(error|exception|failing|fails?|failure|broken|bug|why)\b/i, 2],
[/\b(test|tests|coverage|benchmark)\b/i, 1],
[/\b(multi[- ]file|across|module|modules|system|end[- ]to[- ]end)\b/i, 1],
[/[?][\s\S]*[?]/, 1],
[/```/, 1],
];

const simpleSignals = /\b(typo|rename|format|lint|thanks|thank you|hello|hi|hey)\b/i;

export const classifyPrompt = (text: string): Complexity => {
const input = text.trim();
if (input.length === 0) return "normal";

let score = signals.reduce(
(total, [pattern, weight]) => total + (pattern.test(input) ? weight : 0),
0,
);
if (input.length > 400) score += 1;
if (input.length > 1200) score += 2;
if (input.length < 200 && simpleSignals.test(input)) score -= 3;

if (score >= 6) return "deep";
if (score >= 3) return "complex";
if (score <= 0 && input.length < 160) return "simple";
return "normal";
};

export const pickVariant = (
variants: Array<string>,
complexity: Complexity,
): string | undefined => {
const usable = variants.filter((variant) => !/^(none|off|disabled)$/i.test(variant));
if (usable.length === 0) return undefined;
if (usable.length === 1) return usable[0];

const last = usable.length - 1;
const index = {
simple: 0,
normal: Math.floor(last / 2),
complex: Math.round(last * 0.75),
deep: last,
}[complexity];

return usable[index];
};
55 changes: 38 additions & 17 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
import * as z from "zod/v4";

export const defaultSystemPrompt =
"You MUST manage reasoning effort actively. " +
"Lower it before trivial or routine turns; raise it for ambiguity, debugging, risky changes, or multi-step synthesis. " +
"Reassess at turn start, after meaningful new evidence, and when the task shifts. " +
"NEVER leave the current level unchanged by inertia, and NEVER reply to a trivial turn before considering a downshift.";
"Match reasoning effort to the work: lower it for routine or trivial turns, raise it for ambiguity, debugging, risky changes, or multi-step synthesis. " +
"Use the per-turn recommendation when one is provided, and reassess when new evidence changes the work; otherwise call the tool only when the current level no longer fits the task.";

export type Config = {
enabled: boolean;
quiet: boolean;
autoSelect: boolean;
toolName: string;
toolDescription: string;
systemPrompt: string;
};

export const configDefaults = {
const defaults: Config = {
enabled: true,
quiet: false,
autoSelect: false,
toolName: "set_reasoning_effort",
toolDescription: "Set your reasoning effort",
systemPrompt: defaultSystemPrompt,
};

export const ConfigSchema = z
.object({
enabled: z.boolean().default(configDefaults.enabled),
quiet: z.boolean().default(configDefaults.quiet),
toolName: z.string().default(configDefaults.toolName),
toolDescription: z.string().default(configDefaults.toolDescription),
systemPrompt: z.string().default(configDefaults.systemPrompt),
})
.optional()
.default(configDefaults);
export const parseConfig = (options: unknown): { config: Config } | { error: string } => {
if (options === undefined) return { config: { ...defaults } };
if (typeof options !== "object" || options === null || Array.isArray(options)) {
return { error: "options must be an object" };
}

const input = options as Record<string, unknown>;
const config = { ...defaults };

for (const key of ["enabled", "quiet", "autoSelect"] as const) {
const value = input[key];
if (value === undefined) continue;
if (typeof value !== "boolean") return { error: `${key} must be a boolean` };
config[key] = value;
}

for (const key of ["toolName", "toolDescription", "systemPrompt"] as const) {
const value = input[key];
if (value === undefined) continue;
if (typeof value !== "string") return { error: `${key} must be a string` };
config[key] = value;
}

return { config };
};
Loading