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:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -784,6 +784,7 @@ export class Agent {
|
||||
intentTracing: this.#intentTracing,
|
||||
onAssistantMessageEvent: this.#onAssistantMessageEvent,
|
||||
getToolChoice,
|
||||
getReasoning: () => this.#state.thinkingLevel,
|
||||
getSteeringMessages: async () => {
|
||||
if (skipInitialSteeringPoll) {
|
||||
skipInitialSteeringPoll = false;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user