From ee341909f57b3fcee54f0a44ba90f591c9783e10 Mon Sep 17 00:00:00 2001 From: Dividesbyzer0 Date: Mon, 31 Aug 2026 12:49:17 -0400 Subject: [PATCH] fix: merge adaptive guidance into existing system prompt The experimental.chat.system.transform hook appended its guidance with system.push(...), creating a second system message. Strict chat templates require exactly one system message at the start of the conversation and fail to render otherwise (e.g. llama.cpp: "System message must be at the beginning."), breaking template rendering before inference starts. Merge the guidance into the first existing system prompt instead, and only inject a standalone entry when no system prompt exists yet. Automatic reasoning-effort switching via set_reasoning_effort is unchanged. --- .changeset/strict-templates-single-system.md | 5 ++ README.md | 2 + src/index.test.ts | 73 ++++++++++++++++++++ src/index.ts | 25 +++++-- 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 .changeset/strict-templates-single-system.md diff --git a/.changeset/strict-templates-single-system.md b/.changeset/strict-templates-single-system.md new file mode 100644 index 0000000..965994b --- /dev/null +++ b/.changeset/strict-templates-single-system.md @@ -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. diff --git a/README.md b/README.md index c4c1b76..67fd7f6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/index.test.ts b/src/index.test.ts index 991f46a..f2ada2d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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"); + }); }); diff --git a/src/index.ts b/src/index.ts index 3b21c89..7f00658 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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}`; + } }, }; };