feat(agent): add post-turn stop callback

This commit is contained in:
Mario Zechner
2026-05-02 00:49:22 +02:00
parent a44622670f
commit 73b7b2c564
5 changed files with 152 additions and 1 deletions

View File

@@ -894,6 +894,103 @@ describe("agentLoop with AgentMessage", () => {
expect(parallelObserved).toBe(true);
});
it("should stop after the current turn when shouldStopAfterTurn returns true", async () => {
const toolSchema = Type.Object({ value: Type.String() });
const executed: string[] = [];
const tool: AgentTool<typeof toolSchema, { value: string }> = {
name: "echo",
label: "Echo",
description: "Echo tool",
parameters: toolSchema,
async execute(_toolCallId, params) {
executed.push(params.value);
return {
content: [{ type: "text", text: `echoed: ${params.value}` }],
details: { value: params.value },
};
},
};
const context: AgentContext = {
systemPrompt: "",
messages: [],
tools: [tool],
};
let steeringPolls = 0;
let followUpPolls = 0;
let callbackToolResultIds: string[] = [];
let callbackContextRoles: string[] = [];
const config: AgentLoopConfig = {
model: createModel(),
convertToLlm: identityConverter,
getSteeringMessages: async () => {
steeringPolls++;
return [];
},
getFollowUpMessages: async () => {
followUpPolls++;
return [createUserMessage("follow up should stay queued")];
},
shouldStopAfterTurn: async ({ message, toolResults, context }) => {
expect(message.role).toBe("assistant");
callbackToolResultIds = toolResults.map((toolResult) => toolResult.toolCallId);
callbackContextRoles = context.messages.map((contextMessage) => contextMessage.role);
return true;
},
};
let llmCalls = 0;
const stream = agentLoop([createUserMessage("echo something")], context, config, undefined, () => {
llmCalls++;
const mockStream = new MockAssistantStream();
queueMicrotask(() => {
if (llmCalls === 1) {
const message = createAssistantMessage(
[{ type: "toolCall", id: "tool-1", name: "echo", arguments: { value: "hello" } }],
"toolUse",
);
mockStream.push({ type: "done", reason: "toolUse", message });
} else {
mockStream.push({
type: "done",
reason: "stop",
message: createAssistantMessage([{ type: "text", text: "should not run" }]),
});
}
});
return mockStream;
});
const events: AgentEvent[] = [];
for await (const event of stream) {
events.push(event);
}
const messages = await stream.result();
expect(llmCalls).toBe(1);
expect(executed).toEqual(["hello"]);
expect(steeringPolls).toBe(1);
expect(followUpPolls).toBe(0);
expect(callbackToolResultIds).toEqual(["tool-1"]);
expect(callbackContextRoles).toEqual(["user", "assistant", "toolResult"]);
expect(messages.map((message) => message.role)).toEqual(["user", "assistant", "toolResult"]);
expect(events.map((event) => event.type)).toEqual([
"agent_start",
"turn_start",
"message_start",
"message_end",
"message_start",
"message_end",
"tool_execution_start",
"tool_execution_end",
"message_start",
"message_end",
"turn_end",
"agent_end",
]);
});
it("should stop after a tool batch when every tool result sets terminate=true", async () => {
const toolSchema = Type.Object({ value: Type.String() });
const tool: AgentTool<typeof toolSchema, { value: string }> = {