Skip to content
Open
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/strict-templates-single-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"opencode-adaptive-thinking": patch
---

Merge adaptive-thinking guidance into the existing first system prompt instead of appending a second system message. Some strict chat templates require exactly one system message at the beginning of the conversation and fail to render when a later system message is added, which could break inference before it started. When no system prompt exists yet, the guidance is still injected as a standalone entry. Automatic reasoning-effort switching via `set_reasoning_effort` is unchanged.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ 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.

The adaptive-thinking guidance is merged into the first existing system prompt rather than added as a separate system message. This keeps the plugin compatible with providers and chat templates that require a single system message at the start of the conversation. When no system prompt exists yet, the guidance is injected on its own.

## 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
73 changes: 73 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,77 @@ describe("AdaptiveThinkingPlugin", () => {
}),
});
});

test("merges adaptive guidance into an existing system prompt", async () => {
const sessionID = "merge-existing-system-prompt";
const { client } = createClient(sessionID, [createMessage("medium")]);
const plugin = await AdaptiveThinkingPlugin({ client } as never);
const existing = "You are a helpful coding assistant.";
const system: string[] = [existing];

await plugin["experimental.chat.system.transform"]!(
{
sessionID,
model: { variants },
} as never,
{ system },
);

expect(system).toHaveLength(1);
expect(system[0]).toContain(existing);
expect(system[0]).toContain(`${existing}\n\n`);
expect(system[0]).toContain("You MUST manage reasoning effort actively");
expect(system[0]).toContain("set_reasoning_effort");
});

test("injects adaptive guidance when no system prompt exists", async () => {
const sessionID = "inject-empty-system-prompt";
const { client } = createClient(sessionID, [createMessage("medium")]);
const plugin = await AdaptiveThinkingPlugin({ client } as never);
const system: string[] = [];

await plugin["experimental.chat.system.transform"]!(
{
sessionID,
model: { variants },
} as never,
{ system },
);

expect(system).toHaveLength(1);
expect(system[0]).toContain("You MUST manage reasoning effort actively");
expect(system[0]).toContain("set_reasoning_effort");
});

test("preserves variant details and tool name when merging into an existing prompt", async () => {
const sessionID = "merge-variant-details";
const { client, toolContext } = createClient(sessionID, [createMessage("medium")]);
const plugin = await AdaptiveThinkingPlugin({ client } as never, {
toolName: "adjust_reasoning",
});
const existing = "Base system instructions.";
const system: string[] = [existing];

await setReasoningEffort(
plugin,
{ level: "high", persist: true },
toolContext,
"adjust_reasoning",
);
await plugin["experimental.chat.system.transform"]!(
{
sessionID,
model: { variants },
} as never,
{ system },
);

expect(system).toHaveLength(1);
expect(system[0]).toContain(existing);
expect(system[0]).toContain("Current reasoning effort level: high.");
expect(system[0]).toContain(
"Valid reasoning effort levels for this session: none, low, medium, high, xhigh.",
);
expect(system[0]).toContain("adjust_reasoning");
});
});
25 changes: 18 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,14 +338,25 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }, options) => {
}
}

system.push(
const adaptivePrompt =
config.systemPrompt.trim() +
" " +
(variant ? `Current reasoning effort level: ${variant}. ` : "") +
`Valid reasoning effort levels for this session: ${variants.join(", ")}. ` +
`To change your reasoning effort, use the \`${config.toolName}\` tool with one of the valid levels. ` +
"Only call it when the task complexity justifies changing levels.",
);
" " +
(variant ? `Current reasoning effort level: ${variant}. ` : "") +
`Valid reasoning effort levels for this session: ${variants.join(", ")}. ` +
`To change your reasoning effort, use the \`${config.toolName}\` tool with one of the valid levels. ` +
"Only call it when the task complexity justifies changing levels.";

// Some chat templates (e.g. strict Jinja templates) require exactly one
// system message at the very beginning of the conversation and reject any
// system message that appears later. Merge the adaptive-thinking guidance
// into the existing first system prompt instead of pushing a second one;
// only inject a standalone entry when no system prompt exists yet.
const existingSystemPrompt = system[0];
if (existingSystemPrompt === undefined) {
system.push(adaptivePrompt);
} else {
system[0] = `${existingSystemPrompt}\n\n${adaptivePrompt}`;
}
},
};
};