feat(agent): enabled dynamic reasoning override per model call

- Added an optional `getReasoning` callback to `AgentLoopConfig` to resolve reasoning effort dynamically for each LLM call.
- Updated the agent loop to resolve reasoning via `getReasoning` and use it in place of static `reasoning` when provided.
- Added a test confirming a run re-reads the thinking level between consecutive model calls when it changes mid-run.
This commit is contained in:
can1357
2026-05-03 06:13:09 +02:00
parent ce2a109c09
commit a60e492fba
4 changed files with 59 additions and 0 deletions
+2
View File
@@ -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,
});
+1
View File
@@ -784,6 +784,7 @@ export class Agent {
intentTracing: this.#intentTracing,
onAssistantMessageEvent: this.#onAssistantMessageEvent,
getToolChoice,
getReasoning: () => this.#state.thinkingLevel,
getSteeringMessages: async () => {
if (skipInitialSteeringPoll) {
skipInitialSteeringPoll = false;
+8
View File
@@ -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 {
+48
View File
@@ -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<typeof toolSchema, Details> = {
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<SimpleStreamOptions["reasoning"]> = [];
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]);
});
});