From 73508941956bdc61df7f8e6d27753b5f8ee47c3b Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Sat, 25 Apr 2026 07:55:34 -0400 Subject: [PATCH 1/3] fix: harden reasoning effort state handling --- .../facts/preference/reasoning_effort.md | 52 ++++ .../project/reasoning_effort_behavior.md | 62 ++++ .../project/reasoning_effort_confirmation.md | 34 +++ .../reasoning_effort_reset_behavior.md | 42 +++ .../project/reasoning_effort_reset_message.md | 39 +++ .../project/reasoning_effort_reset_notice.md | 36 +++ ...easoning_effort_reset_notice_visibility.md | 33 +++ .../reasoning_effort_state_handling.md | 31 ++ src/index.test.ts | 164 +++++++++++ src/index.ts | 269 ++++++++++++++++-- 10 files changed, 742 insertions(+), 20 deletions(-) create mode 100644 .brv/context-tree/facts/preference/reasoning_effort.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_behavior.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_confirmation.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_reset_behavior.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_reset_message.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_reset_notice.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_reset_notice_visibility.md create mode 100644 .brv/context-tree/facts/project/reasoning_effort_state_handling.md create mode 100644 src/index.test.ts diff --git a/.brv/context-tree/facts/preference/reasoning_effort.md b/.brv/context-tree/facts/preference/reasoning_effort.md new file mode 100644 index 0000000..77d480a --- /dev/null +++ b/.brv/context-tree/facts/preference/reasoning_effort.md @@ -0,0 +1,52 @@ +--- +title: Reasoning Effort +summary: Reasoning effort preference is set to low. +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T10:20:14.913Z' +updatedAt: '2026-04-25T11:55:01.662Z' +--- +## Reason +User set the reasoning effort preference to low. + +## Raw Concept +**Task:** +Record the user's reasoning effort preference + +**Changes:** +- Set reasoning effort to low +- Confirmed the setting is set to low +- Clarified that the low setting persists beyond a single turn until explicitly changed again +- Set reasoning effort to medium +- User set reasoning effort to low +- Assistant confirmed it was not persisted +- Set thinking level to low without persisting it +- Reasoning effort set to low +- Captured instruction to actively manage reasoning effort + +**Flow:** +User preference stated -> preference recorded + +**Timestamp:** 2026-04-25 + +**Author:** user + +## Narrative +### Structure +A simple preference entry under facts/preference. + +### Dependencies +Applies when deciding how much internal reasoning to use for the current turn. + +### Highlights +The user explicitly set reasoning effort to low. + +## Facts +- **reasoning_effort**: Reasoning effort is set to low [preference] + +--- + +## Cross-reference notes +- This entry is the canonical source for the reasoning effort preference. +- The abstract and overview files are derivative summaries and should point here if retained. diff --git a/.brv/context-tree/facts/project/reasoning_effort_behavior.md b/.brv/context-tree/facts/project/reasoning_effort_behavior.md new file mode 100644 index 0000000..1c56464 --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_behavior.md @@ -0,0 +1,62 @@ +--- +title: Reasoning Effort Behavior +summary: Temporary low reasoning effort can be set in a live session, invalid levels are rejected, and the next idle transition should reset to medium once. +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:53:01.433Z' +updatedAt: '2026-04-25T11:54:28.539Z' +--- +## Reason +Persist lasting knowledge about reasoning effort behavior, validation, and checks + +## Raw Concept +**Task:** +Document reasoning effort session behavior and verification outcomes + +**Changes:** +- Split state into currentVariant, persistedVariant, and temporaryResetVariant +- Temporary reasoning effort now resets only once on session.idle +- Reset prompts are ignored so they do not remain visible in-context +- Persisted reasoning effort no longer triggers idle reset +- Invalid levels are rejected before calling promptAsync or mutating state +- System guidance now uses the transform hook model variants for new sessions +- System prompt guidance is now one consolidated entry +- Removed v2 SDK type imports and @ts-expect-error usage around variant +- Set temporary reasoning effort to low +- Confirmed invalid reasoning effort level is rejected before mutation +- Observed expected one-time idle reset behavior to medium +- Verified tests, typecheck, lint, format:check, and build passed + +**Files:** +- src/index.ts +- src/index.test.ts + +**Flow:** +set reasoning effort -> validate level -> run checks -> idle transition resets to medium once + +**Timestamp:** 2026-04-25T11:54:23.547Z + +**Author:** assistant + +## Narrative +### Structure +This entry records a live-session reasoning-effort state change and the expected follow-up idle reset behavior. + +### Dependencies +The behavior depends on the session state machine and validation for allowed reasoning effort levels. + +### Highlights +A temporary low setting was accepted, an invalid level was rejected, and the next idle transition should restore medium only once. + +### Rules +Temporary reasoning effort now resets only once on session.idle. Reset prompts are ignored and should not remain visible in-context. Invalid levels are rejected before calling promptAsync or mutating state. + +### Examples +Checks that passed included pnpm run test, pnpm run typecheck, pnpm run lint, pnpm run format:check, and pnpm run build. + +## Facts +- **reasoning_effort**: Reasoning effort was set to low in a live session. [project] +- **reasoning_effort_validation**: Invalid reasoning effort levels are rejected before mutation. [project] +- **reasoning_effort_reset_behavior**: The temporary low reasoning effort should reset to medium once on idle, then stop repeating. [project] +- **verification_status**: Automated checks passed: test, typecheck, lint, format:check, and build. [project] diff --git a/.brv/context-tree/facts/project/reasoning_effort_confirmation.md b/.brv/context-tree/facts/project/reasoning_effort_confirmation.md new file mode 100644 index 0000000..85aaaad --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_confirmation.md @@ -0,0 +1,34 @@ +--- +title: Reasoning Effort Confirmation +summary: The only visible confirmation after the tool call was "Reasoning effort set to low". +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:06:39.753Z' +updatedAt: '2026-04-25T11:08:43.379Z' +--- +## Reason +Preserve the confirmed visible tool output from the conversation. + +## Raw Concept +**Task:** +Document the visible confirmation message related to reasoning effort. + +**Changes:** +- Recorded the exact confirmation text that was visible in the conversation +- Captured the confirmation text shown after the tool call. + +**Flow:** +tool call -> visible confirmation -> observation recorded + +**Timestamp:** 2026-04-25 + +## Narrative +### Structure +A single confirmation message was observed in the chat. + +### Highlights +No separate reset message was visible; only "Reasoning effort set to low" appeared. + +## Facts +- **reasoning_effort_confirmation**: The only visible confirmation after the tool call was "Reasoning effort set to low". [project] diff --git a/.brv/context-tree/facts/project/reasoning_effort_reset_behavior.md b/.brv/context-tree/facts/project/reasoning_effort_reset_behavior.md new file mode 100644 index 0000000..b35733c --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_reset_behavior.md @@ -0,0 +1,42 @@ +--- +title: Reasoning Effort Reset Behavior +summary: Reasoning effort can be reset to medium, and the reset notice states that it has been reset to medium. +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:09:11.082Z' +updatedAt: '2026-04-25T11:54:47.414Z' +--- +## Reason +Document persistent reasoning effort reset behavior from conversation + +## Raw Concept +**Task:** +Document reasoning effort reset behavior + +**Changes:** +- Added set_reasoning_effort tool for session variant control +- Implemented session.idle reset attempt based on cached variant +- Observed undefined priorState.variant in the reset prompt body +- Reset notice indicates reasoning effort reset to medium + +**Files:** +- src/index.ts + +**Flow:** +reset notice -> reasoning effort set to medium + +**Timestamp:** 2026-04-25 + +## Narrative +### Structure +The note records a reset-state confirmation for reasoning effort. + +### Dependencies +Depends on client.session.messages, client.app.agents, and client.session.promptAsync for variant resolution and state changes. + +### Highlights +The reset notice explicitly says: Reasoning effort reset to medium. + +## Facts +- **reasoning_effort_reset**: Reasoning effort was reset to medium. [project] diff --git a/.brv/context-tree/facts/project/reasoning_effort_reset_message.md b/.brv/context-tree/facts/project/reasoning_effort_reset_message.md new file mode 100644 index 0000000..137c963 --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_reset_message.md @@ -0,0 +1,39 @@ +--- +title: Reasoning Effort Reset Message +summary: Reasoning effort was reset to medium, with no separate tool-generated reset confirmation observed beyond an earlier low setting message. +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:08:33.219Z' +updatedAt: '2026-04-25T11:11:38.940Z' +--- +## Reason +Persist the observed reset message and prior state from the conversation. + +## Raw Concept +**Task:** +Document reasoning effort reset message behavior observed in conversation + +**Changes:** +- Captured the only visible reset-related message from the conversation +- Observed a reset message indicating reasoning effort reset to medium +- Noted there was no separate tool-generated reset confirmation + +**Flow:** +user asks about reset message -> assistant confirms message content -> assistant notes lack of separate tool-generated confirmation + +**Timestamp:** 2026-04-25T11:11:28.548Z + +## Narrative +### Structure +The conversation centers on whether a reset message is visible and what it says. + +### Dependencies +The assistant distinguishes between the user-visible reset message and a separate tool-generated confirmation. + +### Highlights +The reset message states medium, and the assistant references an earlier low setting message as prior context. + +## Facts +- **reasoning_effort**: Reasoning effort reset to medium. [project] +- **reasoning_effort**: An earlier message said reasoning effort was set to low. [project] diff --git a/.brv/context-tree/facts/project/reasoning_effort_reset_notice.md b/.brv/context-tree/facts/project/reasoning_effort_reset_notice.md new file mode 100644 index 0000000..a752a43 --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_reset_notice.md @@ -0,0 +1,36 @@ +--- +title: Reasoning Effort Reset Notice +summary: Reasoning effort reset notice is visible and states that reasoning effort reset to medium. +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:44:48.223Z' +updatedAt: '2026-04-25T11:44:48.223Z' +--- +## Reason +Capture lasting project behavior about reasoning effort reset notice visibility + +## Raw Concept +**Task:** +Document the observed reasoning effort reset notice and message + +**Changes:** +- Confirmed the reset notice is visible +- Recorded the reset message as medium + +**Flow:** +user asks about reset -> assistant confirms visible notice -> message indicates medium reset + +## Narrative +### Structure +A brief exchange confirming the presence of a reasoning effort reset notice and its displayed message. + +### Highlights +The notice is visible and explicitly reports a reset to medium. + +### Examples +User asked, "And do you see the reset?" and the assistant replied that it saw the reset notice. + +## Facts +- **reasoning_effort_reset_notice_visibility**: A reasoning effort reset notice is visible. [project] +- **reasoning_effort_reset_message**: The reset notice says "Reasoning effort reset to medium." [project] diff --git a/.brv/context-tree/facts/project/reasoning_effort_reset_notice_visibility.md b/.brv/context-tree/facts/project/reasoning_effort_reset_notice_visibility.md new file mode 100644 index 0000000..888de52 --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_reset_notice_visibility.md @@ -0,0 +1,33 @@ +--- +title: Reasoning Effort Reset Notice Visibility +summary: The reset notice is visible as a user-level message rather than a separate tool confirmation. +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:12:05.181Z' +updatedAt: '2026-04-25T11:12:05.181Z' +--- +## Reason +Capture lasting note about reset notice being visible to the user + +## Raw Concept +**Task:** +Document the visibility of the reasoning effort reset notice + +**Changes:** +- Confirmed the reset notice appears as a user-level message + +**Flow:** +reset action -> notice displayed -> user sees confirmation message + +**Timestamp:** 2026-04-25T11:12:01.205Z + +## Narrative +### Structure +A brief confirmation that the reset notice is exposed directly to the user in chat. + +### Highlights +The assistant explicitly confirmed the reset notice is not a separate tool confirmation. + +## Facts +- **reset_notice_visibility**: The reset notice is visible as a user-level message rather than a separate tool confirmation. [project] diff --git a/.brv/context-tree/facts/project/reasoning_effort_state_handling.md b/.brv/context-tree/facts/project/reasoning_effort_state_handling.md new file mode 100644 index 0000000..d55cc79 --- /dev/null +++ b/.brv/context-tree/facts/project/reasoning_effort_state_handling.md @@ -0,0 +1,31 @@ +--- +title: Reasoning Effort State Handling +summary: x +tags: [] +related: [] +keywords: [] +createdAt: '2026-04-25T11:48:50.291Z' +updatedAt: '2026-04-25T11:48:50.291Z' +--- +## Reason +x + +## Raw Concept +**Task:** +x + +**Changes:** +- x + +**Flow:** +x + +## Narrative +### Structure +x + +### Dependencies +x + +### Highlights +x diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..7861811 --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, test, vi } from "vitest"; +import { AdaptiveThinkingPlugin } from "./index"; + +const variants = { + none: {}, + low: {}, + medium: {}, + high: {}, + xhigh: {}, +}; + +const createClient = (sessionID: string, messages: Array = []) => { + const promptAsync = vi.fn(async () => ({ error: undefined })); + + return { + client: { + session: { + messages: vi.fn(async () => ({ data: messages, error: undefined })), + promptAsync, + }, + provider: { + list: vi.fn(async () => ({ + data: { + all: [ + { + id: "provider", + models: { + model: { variants }, + }, + }, + ], + }, + error: undefined, + })), + }, + app: { + agents: vi.fn(async () => ({ data: [], error: undefined })), + log: vi.fn(), + }, + }, + toolContext: { + sessionID, + messageID: "message", + agent: "agent", + directory: "/tmp", + worktree: "/tmp", + abort: new AbortController().signal, + metadata: vi.fn(), + ask: vi.fn(), + }, + }; +}; + +const createMessage = (variant = "medium") => ({ + info: { + id: "message", + role: "user", + agent: "agent", + model: { + providerID: "provider", + modelID: "model", + variant, + }, + }, + parts: [], +}); + +const setReasoningEffort = async ( + plugin: Awaited>, + args: { level: string; persist: boolean }, + context: unknown, +) => plugin.tool!["set_reasoning_effort"]!.execute(args, context as never); + +describe("AdaptiveThinkingPlugin", () => { + test("resets a temporary reasoning effort once and keeps the reset prompt ignored", async () => { + const sessionID = "temporary-reset"; + const { client, toolContext } = createClient(sessionID, [createMessage("medium")]); + const plugin = await AdaptiveThinkingPlugin({ client } as never); + + await setReasoningEffort(plugin, { level: "low", persist: false }, toolContext); + await plugin.event!({ + event: { type: "session.idle", properties: { sessionID } }, + } as never); + await plugin.event!({ + event: { type: "session.idle", properties: { sessionID } }, + } as never); + + expect(client.session.promptAsync).toHaveBeenCalledTimes(2); + const promptCalls = client.session.promptAsync.mock.calls as Array>; + const secondPrompt = promptCalls[1]?.[0]; + expect(secondPrompt).toMatchObject({ + body: { + variant: "medium", + parts: [{ ignored: true, synthetic: true }], + }, + }); + }); + + test("does not reset persisted reasoning effort on idle", async () => { + const sessionID = "persisted-effort"; + const { client, toolContext } = createClient(sessionID, [createMessage("medium")]); + const plugin = await AdaptiveThinkingPlugin({ client } as never); + + await setReasoningEffort(plugin, { level: "high", persist: true }, toolContext); + await plugin.event!({ + event: { type: "session.idle", properties: { sessionID } }, + } as never); + + expect(client.session.promptAsync).toHaveBeenCalledTimes(1); + }); + + test("rejects invalid reasoning effort levels before prompting", async () => { + const sessionID = "invalid-level"; + const { client, toolContext } = createClient(sessionID, [createMessage("medium")]); + const plugin = await AdaptiveThinkingPlugin({ client } as never); + + const result = await setReasoningEffort( + plugin, + { level: "extreme", persist: true }, + toolContext, + ); + + expect(result).toContain("Invalid reasoning effort level"); + expect(client.session.promptAsync).not.toHaveBeenCalled(); + }); + + test("uses the transform model to inject guidance for a new session", async () => { + const sessionID = "new-session"; + const { client } = createClient(sessionID, []); + 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("Valid reasoning effort levels"); + expect(system[0]).toContain("low, medium, high"); + }); + + test("adds a single non-duplicative system guidance entry", async () => { + const sessionID = "system-guidance"; + 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("Current reasoning effort level: medium"); + expect(system[0]).not.toContain("Remember to adjust your reasoning effort"); + }); +}); diff --git a/src/index.ts b/src/index.ts index 24bc6c2..1461d7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,36 +2,265 @@ import { tool, type Plugin } from "@opencode-ai/plugin"; const z = tool.schema; +type MessageInfo = { + agent?: string; + model?: { + providerID: string; + modelID: string; + variant?: string; + }; +}; + +type SessionMessage = { + info: MessageInfo; + parts: unknown[]; +}; + +type ModelWithVariants = { + variants?: Record; +}; + +type AgentWithVariant = { + name: string; + variant?: string; +}; + +const state = { + currentVariant: new Map(), + persistedVariant: new Map(), + temporaryResetVariant: new Map(), +}; + export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { + type PromptAsyncOptions = Parameters[0]; + type PromptAsyncBody = NonNullable & { variant: string }; + + const getModelVariants = (model: unknown): string[] => { + const variants = (model as ModelWithVariants | undefined)?.variants; + if (!variants) return []; + return Object.keys(variants); + }; + + const sendVariantPrompt = async ( + sessionID: string, + variant: string, + text: string, + ignored = true, + ) => { + const body: PromptAsyncBody = { + noReply: true, + parts: [ + { + type: "text", + text, + synthetic: true, + ignored, + }, + ], + variant, + }; + + return client.session.promptAsync({ + path: { id: sessionID }, + body, + }); + }; + + const resolveValidVariants = async (sessionID: string, model?: unknown): Promise => { + const inputModelVariants = getModelVariants(model); + if (inputModelVariants.length > 0) return inputModelVariants; + + const messagesResponse = await client.session.messages({ + path: { id: sessionID }, + }); + if (messagesResponse.error) { + client.app.log({ + body: { + service: "opencode-adaptive-thinking", + level: "error", + message: `Failed to retrieve messages for session ${sessionID}: ${JSON.stringify(messagesResponse.error.data)}`, + }, + }); + return []; + } + let modelInfo: { providerID: string; modelID: string } | undefined; + const messages = messagesResponse.data as SessionMessage[]; + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]!; + if (message.info.model) { + modelInfo = message.info.model; + break; + } + } + + if (!modelInfo) return []; + + const providers = await client.provider.list(); + if (providers.error) { + client.app.log({ + body: { + service: "opencode-adaptive-thinking", + level: "error", + message: `Failed to retrieve providers for session ${sessionID}: ${JSON.stringify(providers.error)}`, + }, + }); + return []; + } + const provider = providers.data?.all.find((p) => p.id === modelInfo.providerID) as + | { models: Record } + | undefined; + if (!provider) return []; + + return getModelVariants(provider.models[modelInfo.modelID]); + }; + + const resolveCurrentVariant = async (sessionID: string): Promise => { + const messagesResponse = await client.session.messages({ + path: { id: sessionID }, + }); + if (messagesResponse.error) { + client.app.log({ + body: { + service: "opencode-adaptive-thinking", + level: "error", + message: `Failed to retrieve messages for session ${sessionID}: ${JSON.stringify(messagesResponse.error.data)}`, + }, + }); + return; + } + let agentName: string | undefined; + const messages = messagesResponse.data as SessionMessage[]; + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]!; + if (message.info.model?.variant) { + return message.info.model.variant; + } + agentName = message.info.agent; + } + + if (!agentName) return; + + const agentsResponse = await client.app.agents(); + if (agentsResponse.error) { + client.app.log({ + body: { + service: "opencode-adaptive-thinking", + level: "error", + message: `Failed to retrieve agents for session ${sessionID}: ${JSON.stringify(agentsResponse.error)}`, + }, + }); + return; + } + const agents = agentsResponse.data; + const agent = agents?.find((a) => a.name === agentName) as AgentWithVariant | undefined; + return agent?.variant; + }; + return { tool: { - set_readoning_effort: tool({ + set_reasoning_effort: tool({ description: "Set your reasoning effort", args: { - level: z.enum(["none", "low", "medium", "high", "xhigh"]), + level: z + .string() + .describe( + "The level of reasoning effort to apply. Higher levels may result in more accurate and thoughtful responses, but may also take more time and resources.", + ), + persist: z + .boolean() + .optional() + .default(false) + .describe( + "Whether to persist the setting for this session, otherwise it will only apply for the remainder of the current turn", + ), }, - execute: async ({ level }, { sessionID }) => { - const sendMessageResponse = await client.session.promptAsync({ - path: { id: sessionID }, - body: { - noReply: true, - parts: [ - { - type: "text", - text: `Reasoning effort set to ${level}`, - synthetic: true, - }, - ], - // @ts-expect-error - Variant is a valid property - variant: level, - }, - }); - if (sendMessageResponse.error) { - return `Failed to set reasoning effort: ${JSON.stringify(sendMessageResponse.error.data)}`; + execute: async ({ level, persist }, { sessionID }) => { + const validVariants = await resolveValidVariants(sessionID); + if (validVariants.length === 0) { + return "Failed to set reasoning effort: no valid reasoning effort levels are available for this session"; + } + if (!validVariants.includes(level)) { + return `Invalid reasoning effort level: ${level}. Valid levels: ${validVariants.join(", ")}.`; + } + + const resetVariant = persist + ? undefined + : (state.persistedVariant.get(sessionID) ?? (await resolveCurrentVariant(sessionID))); + + const promptResponse = await sendVariantPrompt( + sessionID, + level, + `Reasoning effort set to ${level}`, + ); + if (promptResponse.error) { + return `Failed to set reasoning effort: ${JSON.stringify(promptResponse.error.data)}`; } + + state.currentVariant.set(sessionID, level); + if (persist) { + state.persistedVariant.set(sessionID, level); + state.temporaryResetVariant.delete(sessionID); + } else if (resetVariant && resetVariant !== level) { + state.temporaryResetVariant.set(sessionID, resetVariant); + } else { + state.temporaryResetVariant.delete(sessionID); + } + return `Reasoning effort set to ${level}`; }, }), }, + event: async ({ event }) => { + if (event.type === "session.idle") { + const sessionID = event.properties.sessionID; + const resetVariant = state.temporaryResetVariant.get(sessionID); + if (!resetVariant) return; + + const promptResponse = await sendVariantPrompt( + sessionID, + resetVariant, + `Reasoning effort reset to ${resetVariant}.`, + ); + if (promptResponse.error) { + client.app.log({ + body: { + service: "opencode-adaptive-thinking", + level: "error", + message: `Failed to reset reasoning effort on session idle: ${JSON.stringify(promptResponse.error.data)}`, + }, + }); + return; + } + + state.currentVariant.set(sessionID, resetVariant); + state.temporaryResetVariant.delete(sessionID); + return; + } + }, + "experimental.chat.system.transform": async ({ sessionID, model }, { system }) => { + if (!sessionID) return; + + const variants = await resolveValidVariants(sessionID, model); + if (variants.length === 0) return; + + let variant = state.currentVariant.get(sessionID) ?? state.persistedVariant.get(sessionID); + if (!variant) { + variant = await resolveCurrentVariant(sessionID); + if (variant && variants.includes(variant)) { + state.currentVariant.set(sessionID, variant); + } + } + + system.push( + "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. " + + (variant ? `Current reasoning effort level: ${variant}. ` : "") + + `Valid reasoning effort levels for this session: ${variants.join(", ")}. ` + + `To change your reasoning effort, use the \`set_reasoning_effort\` tool with one of the valid levels. ` + + "Only call it when the task complexity justifies changing levels.", + ); + }, }; }; From 94d4e11c26e5b44edc3c477facd7db3e25e1a5fa Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Sat, 25 Apr 2026 08:12:56 -0400 Subject: [PATCH 2/3] fix: restore SDK types for reasoning effort state --- src/index.ts | 46 ++++++++++++---------------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1461d7c..371ea83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,8 @@ import { tool, type Plugin } from "@opencode-ai/plugin"; +import type { Agent, Message, Model, Part, Provider } from "@opencode-ai/sdk/v2"; const z = tool.schema; -type MessageInfo = { - agent?: string; - model?: { - providerID: string; - modelID: string; - variant?: string; - }; -}; - -type SessionMessage = { - info: MessageInfo; - parts: unknown[]; -}; - -type ModelWithVariants = { - variants?: Record; -}; - -type AgentWithVariant = { - name: string; - variant?: string; -}; - const state = { currentVariant: new Map(), persistedVariant: new Map(), @@ -35,8 +13,8 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { type PromptAsyncOptions = Parameters[0]; type PromptAsyncBody = NonNullable & { variant: string }; - const getModelVariants = (model: unknown): string[] => { - const variants = (model as ModelWithVariants | undefined)?.variants; + const getModelVariants = (model: Model | undefined): string[] => { + const variants = model?.variants; if (!variants) return []; return Object.keys(variants); }; @@ -66,7 +44,7 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { }); }; - const resolveValidVariants = async (sessionID: string, model?: unknown): Promise => { + const resolveValidVariants = async (sessionID: string, model?: Model): Promise => { const inputModelVariants = getModelVariants(model); if (inputModelVariants.length > 0) return inputModelVariants; @@ -84,10 +62,10 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { return []; } let modelInfo: { providerID: string; modelID: string } | undefined; - const messages = messagesResponse.data as SessionMessage[]; + const messages = messagesResponse.data as Array<{ info: Message; parts: Array }>; for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]!; - if (message.info.model) { + if ("model" in message.info) { modelInfo = message.info.model; break; } @@ -107,11 +85,11 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { return []; } const provider = providers.data?.all.find((p) => p.id === modelInfo.providerID) as - | { models: Record } + | Provider | undefined; if (!provider) return []; - return getModelVariants(provider.models[modelInfo.modelID]); + return getModelVariants(provider.models[modelInfo.modelID] as Model | undefined); }; const resolveCurrentVariant = async (sessionID: string): Promise => { @@ -129,10 +107,10 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { return; } let agentName: string | undefined; - const messages = messagesResponse.data as SessionMessage[]; + const messages = messagesResponse.data as Array<{ info: Message; parts: Array }>; for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]!; - if (message.info.model?.variant) { + if ("model" in message.info && message.info.model.variant) { return message.info.model.variant; } agentName = message.info.agent; @@ -152,7 +130,7 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { return; } const agents = agentsResponse.data; - const agent = agents?.find((a) => a.name === agentName) as AgentWithVariant | undefined; + const agent = agents?.find((a) => a.name === agentName) as Agent | undefined; return agent?.variant; }; @@ -240,7 +218,7 @@ export const AdaptiveThinkingPlugin: Plugin = async ({ client }) => { "experimental.chat.system.transform": async ({ sessionID, model }, { system }) => { if (!sessionID) return; - const variants = await resolveValidVariants(sessionID, model); + const variants = await resolveValidVariants(sessionID, model as Model); if (variants.length === 0) return; let variant = state.currentVariant.get(sessionID) ?? state.persistedVariant.get(sessionID); From 5b2fc72cee061a1f5372800e74e93ac90529fdb5 Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Sat, 25 Apr 2026 08:17:19 -0400 Subject: [PATCH 3/3] chore: add reasoning effort changeset --- .changeset/clean-ravens-reset.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-ravens-reset.md diff --git a/.changeset/clean-ravens-reset.md b/.changeset/clean-ravens-reset.md new file mode 100644 index 0000000..5d5bea4 --- /dev/null +++ b/.changeset/clean-ravens-reset.md @@ -0,0 +1,5 @@ +--- +"opencode-adaptive-thinking": patch +--- + +Harden reasoning effort state handling so temporary settings reset once, persisted settings do not reset on idle, invalid levels are rejected before mutation, and system guidance is added as one consolidated prompt entry.