test(agent): added tests for aside timing and stale-yield queue draining
- Added an agent loop test proving aside messages are delivered after tool results, before the next model request, without interrupting tool execution. - Added a yield-queue test confirming stale entries are excluded, the queue is cleared, and re-draining yields no messages.
This commit is contained in:
@@ -768,6 +768,79 @@ describe("agentLoop with AgentMessage", () => {
|
||||
);
|
||||
expect(sawInterruptInContext).toBe(true);
|
||||
});
|
||||
|
||||
it("injects aside messages at the step boundary without interrupting tools", async () => {
|
||||
const toolSchema = z.object({ value: z.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] };
|
||||
const mock = createMockModel({
|
||||
responses: [
|
||||
{
|
||||
content: [
|
||||
{ type: "toolCall", id: "tool-1", name: "echo", arguments: { value: "first" } },
|
||||
{ type: "toolCall", id: "tool-2", name: "echo", arguments: { value: "second" } },
|
||||
],
|
||||
},
|
||||
{ content: ["done"] },
|
||||
],
|
||||
});
|
||||
|
||||
const asideMessage = createUserMessage("bg-job-complete");
|
||||
let asideDelivered = false;
|
||||
const config: AgentLoopConfig = {
|
||||
model: mock.model,
|
||||
convertToLlm: identityConverter,
|
||||
interruptMode: "immediate",
|
||||
getAsideMessages: async () => {
|
||||
if (!asideDelivered && executed.length >= 1) {
|
||||
asideDelivered = true;
|
||||
return [asideMessage];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
const stream = agentLoop([createUserMessage("start")], context, config, undefined, mock.stream);
|
||||
for await (const event of stream) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
// Asides are non-interrupting: BOTH tools in the batch run (steering would skip the 2nd).
|
||||
expect(executed).toEqual(["first", "second"]);
|
||||
|
||||
// The aside lands after the tool results, before the next model call.
|
||||
const seq = events.flatMap(event => {
|
||||
if (event.type !== "message_start") return [];
|
||||
if (event.message.role === "toolResult") return [`tool:${event.message.toolCallId}`];
|
||||
if (event.message.role === "user" && typeof event.message.content === "string") {
|
||||
return [event.message.content];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
expect(seq).toContain("bg-job-complete");
|
||||
expect(seq.indexOf("tool:tool-2")).toBeLessThan(seq.indexOf("bg-job-complete"));
|
||||
|
||||
// The model saw it on the very next request — delivered mid-run, no yield required.
|
||||
const sawAsideInContext = mock.calls[1]?.context.messages.some(
|
||||
m => m.role === "user" && typeof m.content === "string" && m.content === "bg-job-complete",
|
||||
);
|
||||
expect(sawAsideInContext).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes tools and system prompt between same-turn model calls", async () => {
|
||||
|
||||
@@ -151,4 +151,23 @@ describe("YieldQueue", () => {
|
||||
|
||||
expect(harness.streamingMessages.map(messageText)).toEqual(["second", "first"]);
|
||||
});
|
||||
|
||||
test("drainMessages builds non-stale entries, clears the queue, returns nothing on re-drain", async () => {
|
||||
const harness = createHarness(true);
|
||||
harness.queue.register<Entry>("items", {
|
||||
isStale: entry => entry.stale === true,
|
||||
build: entries => userMessage(entries.map(entry => entry.id).join(",")),
|
||||
});
|
||||
|
||||
harness.queue.enqueue("items", { id: "keep" });
|
||||
harness.queue.enqueue("items", { id: "drop", stale: true });
|
||||
|
||||
const drained = harness.queue.drainMessages();
|
||||
expect(drained.map(messageText)).toEqual(["keep"]);
|
||||
// Pull-based drain has no injection side effects and empties the queue.
|
||||
expect(harness.streamingMessages).toHaveLength(0);
|
||||
expect(harness.idleBatches).toHaveLength(0);
|
||||
expect(harness.queue.has()).toBe(false);
|
||||
expect(harness.queue.drainMessages()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user