diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 53396241f..910e8b6fc 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -347,10 +347,12 @@ async function streamAssistantResponse( (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey; const dynamicToolChoice = config.getToolChoice?.(); + const dynamicReasoning = config.getReasoning?.(); const response = await streamFunction(config.model, llmContext, { ...config, apiKey: resolvedApiKey, toolChoice: dynamicToolChoice ?? config.toolChoice, + reasoning: dynamicReasoning ?? config.reasoning, signal, }); diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 5fbdd5ba0..202758668 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -784,6 +784,7 @@ export class Agent { intentTracing: this.#intentTracing, onAssistantMessageEvent: this.#onAssistantMessageEvent, getToolChoice, + getReasoning: () => this.#state.thinkingLevel, getSteeringMessages: async () => { if (skipInitialSteeringPoll) { skipInitialSteeringPoll = false; diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 7c55a5427..63c414387 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -145,6 +145,14 @@ export interface AgentLoopConfig extends SimpleStreamOptions { * When set and returns a value, overrides the static `toolChoice`. */ getToolChoice?: () => ToolChoice | undefined; + + /** + * Dynamic reasoning effort override, resolved per LLM call. + * When set and returns a value, overrides the static `reasoning` captured + * at run-loop start. Use this so mid-run thinking-level changes apply on + * the next model call instead of waiting for the next prompt. + */ + getReasoning?: () => Effort | undefined; } export interface ToolCallContext { diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index df02cd092..ecd9d5ce1 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -379,4 +379,52 @@ describe("Agent", () => { provider: "openai", }); }); + + it("re-reads thinking level for each model call within a run", async () => { + const toolSchema = Type.Object({ value: Type.String() }); + type Details = { value: string }; + const alphaTool: AgentTool = { + name: "alpha", + label: "Alpha", + description: "Alpha tool", + parameters: toolSchema, + async execute(_toolCallId, params) { + return { content: [{ type: "text", text: `alpha:${params.value}` }], details: { value: params.value } }; + }, + }; + + let callIndex = 0; + const reasoningPerCall: Array = []; + + const agent = new Agent({ + initialState: { + model: getBundledModel("openai", "gpt-4o-mini"), + thinkingLevel: ThinkingLevel.Low, + tools: [alphaTool], + messages: [], + }, + streamFn: (_model, _context, options) => { + reasoningPerCall.push(options?.reasoning); + const stream = new MockAssistantStream(); + queueMicrotask(() => { + pushAlphaThenDoneEvent(stream, callIndex, createAssistantMessage); + callIndex += 1; + }); + return stream; + }, + }); + + // Bump thinking level mid-run, after the first assistant turn finishes + // and before the second model call (which follows the tool result). + const unsubscribe = agent.subscribe(event => { + if (event.type === "message_end" && event.message.role === "toolResult") { + agent.setThinkingLevel(ThinkingLevel.High); + } + }); + + await agent.prompt("run"); + unsubscribe(); + + expect(reasoningPerCall).toEqual([ThinkingLevel.Low, ThinkingLevel.High]); + }); });