import type { ConnectorTool } from "@rakazo/adapter-kit"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const fakeAgentState = vi.hoisted(() => ({ mode: "dispatch" as | "dispatch" | "empty" | "two-boundaries" | "silent-continuation" | "subagent-limit" | "parent-limit" | "parent-parallel" | "ask-pause" | "nested-ask-pause", emitFinalAfterFollowUp: true, abortCount: 0, tools: [] as Array<{ name: string; prepareArguments?: (args: unknown) => Record; execute: (toolCallId: string, params: Record) => Promise; }>, invoke: { name: "destination_write", args: { collection: "notes", title: "Result", body: "Done" } as Record, }, preparedMessages: [] as unknown[], steeredMessages: [] as unknown[], followUpMessages: [] as unknown[], initialMessages: [] as unknown[], promptInputs: [] as string[], promptImages: [] as unknown[][], toolResult: undefined as unknown, })); vi.mock("@earendil-works/pi-agent-core", () => ({ Agent: class { state = { errorMessage: undefined as string | undefined, messages: [] as unknown[] }; private readonly tools: typeof fakeAgentState.tools; private readonly listeners: Array<(event: Record) => void> = []; private readonly prepareNextTurnWithContext?: (input: { context: { messages: unknown[] }; }) => Promise<{ context?: { messages: unknown[] } } | undefined>; private aborted = false; constructor(options: { initialState: { tools: typeof fakeAgentState.tools; messages: unknown[] }; prepareNextTurnWithContext?: (input: { context: { messages: unknown[] }; }) => Promise<{ context?: { messages: unknown[] } } | undefined>; }) { this.tools = options.initialState.tools; this.prepareNextTurnWithContext = options.prepareNextTurnWithContext; fakeAgentState.tools = this.tools; fakeAgentState.initialMessages = options.initialState.messages; } subscribe(listener: (event: Record) => void) { this.listeners.push(listener); } async prompt(prompt: string, images?: unknown[]) { fakeAgentState.promptInputs.push(prompt); fakeAgentState.promptImages.push(images ?? []); if (fakeAgentState.mode === "empty" || fakeAgentState.mode === "two-boundaries") { await this.prepareNextTurnWithContext?.({ context: { messages: [] } }); if (fakeAgentState.mode === "two-boundaries") { await this.prepareNextTurnWithContext?.({ context: { messages: [] } }); } fakeAgentState.preparedMessages = [...fakeAgentState.steeredMessages]; return; } if (fakeAgentState.mode === "dispatch") { const target = this.tools.find((tool) => tool.name === fakeAgentState.invoke.name) ?? this.tools[0]; if (!target) throw new Error("expected tool was not exposed"); const rawArgs = fakeAgentState.invoke.args; const args = target.prepareArguments?.(rawArgs) ?? rawArgs; this.emit({ type: "tool_execution_start", toolName: target.name, args }); fakeAgentState.toolResult = await target.execute("call-1", args); return; } if (fakeAgentState.mode === "silent-continuation") { const target = this.tools.find((tool) => tool.name === fakeAgentState.invoke.name) ?? this.tools[0]; if (!target) throw new Error("expected tool was not exposed"); const rawArgs = fakeAgentState.invoke.args; const args = target.prepareArguments?.(rawArgs) ?? rawArgs; const narration = "I will check the destination first."; this.emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: narration }, }); this.emit({ type: "tool_execution_start", toolName: target.name, args }); await target.execute("call-1", args); this.emit({ type: "turn_end", message: { role: "assistant", content: [ { type: "text", text: narration }, { type: "toolCall", id: "call-1", name: target.name, arguments: args, }, ], }, toolResults: [{ toolCallId: "call-1", result: { ok: true } }], }); this.emit({ type: "turn_end", message: { role: "assistant", content: [] }, toolResults: [], }); if (fakeAgentState.followUpMessages.length > 0 && fakeAgentState.emitFinalAfterFollowUp) { const text = "The final answer."; this.emit({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: text }, }); this.emit({ type: "message_end", message: { role: "assistant", content: [{ type: "text", text }] }, }); this.emit({ type: "turn_end", message: { role: "assistant", content: [{ type: "text", text }] }, toolResults: [], }); } return; } if (fakeAgentState.mode === "parent-limit") { const shell = this.tools.find((tool) => tool.name === "shell"); if (!shell) throw new Error("shell was not exposed"); for (let index = 0; index < 100; index += 1) { const args = { command: `echo ${index}` }; this.emit({ type: "tool_execution_start", toolName: shell.name, args }); if (this.aborted) break; await shell.execute(`shell-${index}`, args); } return; } if (fakeAgentState.mode === "parent-parallel") { const shell = this.tools.find((tool) => tool.name === "shell"); if (!shell) throw new Error("shell was not exposed"); const batch = Array.from({ length: 4 }, (_, index) => ({ args: { command: `echo ${index}` }, })); for (const item of batch) { this.emit({ type: "tool_execution_start", toolName: shell.name, args: item.args }); } await Promise.all( batch.map((item, index) => this.aborted ? Promise.resolve() : shell.execute(`shell-${index}`, item.args), ), ); return; } if (fakeAgentState.mode === "ask-pause" || fakeAgentState.mode === "nested-ask-pause") { if (fakeAgentState.mode === "nested-ask-pause") { const delegation = this.tools.find((tool) => tool.name === "run_subagent"); if (delegation) { const args = { name: "helper", task: "ask the user" }; this.emit({ type: "tool_execution_start", toolName: delegation.name, args }); await delegation.execute("delegate-1", args); return; } } const shell = this.tools.find((tool) => tool.name === "shell"); const askUser = this.tools.find((tool) => tool.name === "ask_user"); if (!shell) throw new Error("shell was not exposed"); if (!askUser) throw new Error("ask_user was not exposed"); for (let index = 0; index < 2; index += 1) { const args = { command: `echo ${index}` }; this.emit({ type: "tool_execution_start", toolName: shell.name, args }); if (this.aborted) return; await shell.execute(`shell-pause-${index}`, args); } const askArgs = { question: "Which option?", options: ["Keep going", "Stop here"], }; this.emit({ type: "tool_execution_start", toolName: askUser.name, args: askArgs }); if (!this.aborted) await askUser.execute("ask-1", askArgs); return; } const delegation = this.tools.find((tool) => tool.name === "run_subagent"); if (delegation) { const args = { name: "loop", task: "keep calling shell" }; this.emit({ type: "tool_execution_start", toolName: delegation.name, args }); await delegation.execute("delegate-1", args); return; } const shell = this.tools.find((tool) => tool.name === "shell"); if (!shell) throw new Error("shell was not exposed to the subagent"); for (let index = 0; index < 100; index += 1) { const args = { command: `echo ${index}` }; this.emit({ type: "tool_execution_start", toolName: shell.name, args }); if (this.aborted) break; await shell.execute(`shell-${index}`, args); } } async waitForIdle() {} steer(message: unknown) { fakeAgentState.steeredMessages.push(message); } followUp(message: unknown) { fakeAgentState.followUpMessages.push(message); } abort() { this.aborted = true; // Mirror pi-agent-core: abort settles the run with an errorMessage that // would otherwise make PiAgentRuntime throw and skip a final reply. this.state.errorMessage = "Request aborted by user"; fakeAgentState.abortCount += 1; } private emit(event: Record) { for (const listener of this.listeners) listener(event); } }, })); vi.mock("@earendil-works/pi-ai/providers/all", () => ({ builtinModels: () => ({ getModel: (_provider: string, modelId: string) => modelId === "dispatch-test-model" ? { provider: "test", id: modelId } : undefined, streamSimple: () => { throw new Error("the fake agent must not call a provider"); }, }), })); vi.mock("./pi-local-provider.js", () => ({ registerLocalProvider: (models: unknown) => models, })); vi.mock("./pi-openai-compatible-provider.js", () => ({ OPENAI_COMPATIBLE_PROVIDER_ID: "openai-compatible", registerOpenAiCompatibleCatalog: (models: unknown) => models, registerOpenAiCompatibleRuntime: (models: unknown) => models, })); import { maxToolCallsPerTurn, PiAgentRuntime } from "./pi-runtime.js"; import { TOOL_RESULT_TEXT_LIMIT } from "./pi-runtime-limits.js"; const destinationTool: ConnectorTool = { name: "destination.write", description: "Write a record to the connected destination", inputSchema: { type: "object", properties: { collection: { type: "string" }, title: { type: "string" }, body: { type: "string" }, }, }, route: { connectorId: "destination", toolName: "destination.write" }, }; const writeFileTool: ConnectorTool = { name: "write_file", description: "Write a UTF-8 file into this bot's home.", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" }, }, }, }; const shellTool = { name: "shell", description: "Run a command", inputSchema: { type: "object", properties: { command: { type: "string" } } }, }; const previousMaxToolCalls = process.env.MAX_TOOL_CALLS_PER_TURN; describe("Pi connector tool dispatch", () => { beforeEach(() => { fakeAgentState.mode = "dispatch"; fakeAgentState.abortCount = 0; fakeAgentState.tools = []; fakeAgentState.preparedMessages = []; fakeAgentState.steeredMessages = []; fakeAgentState.followUpMessages = []; fakeAgentState.emitFinalAfterFollowUp = true; fakeAgentState.initialMessages = []; fakeAgentState.promptInputs = []; fakeAgentState.promptImages = []; fakeAgentState.toolResult = undefined; fakeAgentState.invoke = { name: "destination_write", args: { collection: "notes", title: "Result", body: "Done" }, }; delete process.env.MAX_TOOL_CALLS_PER_TURN; }); it("injects durable steering at Pi's next safe turn boundary", async () => { fakeAgentState.mode = "empty"; let claimCount = 0; const claimSteering = vi.fn(async () => { claimCount += 1; return claimCount === 1 ? [] : [ { id: "steering-1", messageId: "message-1", text: "Use the newer customer totals." }, { id: "steering-2", messageId: "message-2", text: "Keep the original date range." }, ]; }); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "r", prompt: "prepare the report", instructions: "Follow the user's instructions.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, claimSteering, }, { signal: new AbortController().signal }, )) { // Exhaust the runtime event stream. } expect(claimSteering).toHaveBeenNthCalledWith(1, []); expect(claimSteering).toHaveBeenNthCalledWith(2, []); expect(fakeAgentState.preparedMessages).toEqual([ expect.objectContaining({ role: "user", content: "Use the newer customer totals." }), expect.objectContaining({ role: "user", content: "Keep the original date range." }), ]); }); it("defers steering arriving during a continuation to the following boundary", async () => { fakeAgentState.mode = "two-boundaries"; let claimCount = 0; const claimSteering = vi.fn(async () => { claimCount += 1; if (claimCount === 1) return []; return claimCount === 2 ? [{ id: "steering-1", messageId: "message-1", text: "First boundary context." }] : [{ id: "steering-2", messageId: "message-2", text: "Next boundary context." }]; }); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "r", prompt: "continue", instructions: "Follow the user's instructions.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, claimSteering, }, { signal: new AbortController().signal }, )) { // Exhaust the runtime event stream. } expect(claimSteering).toHaveBeenNthCalledWith(1, []); expect(claimSteering).toHaveBeenNthCalledWith(2, []); expect(claimSteering).toHaveBeenNthCalledWith(3, ["steering-1"]); expect(fakeAgentState.preparedMessages).toEqual([ expect.objectContaining({ content: "First boundary context." }), expect.objectContaining({ content: "Next boundary context." }), ]); }); it("consumes pending continuation steering once in the first prompt", async () => { fakeAgentState.mode = "empty"; const steering = [ { id: "steering-1", messageId: "message-1", text: "Use the newer customer totals." }, { id: "steering-2", messageId: "message-2", text: "Keep the original date range." }, ]; const claimSteering = vi.fn(async (seenIds: string[]) => (seenIds.length ? [] : steering)); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "continuation", prompt: "Respond to the user's steering context.", instructions: "Follow the user's instructions.", history: [ { id: steering[0]!.messageId, role: "user", content: steering[0]!.text }, { id: steering[1]!.messageId, role: "user", content: steering[1]!.text }, { role: "assistant", content: "Here is the first result." }, ], tools: [], model: { provider: "test", id: "dispatch-test-model" }, claimSteering, }, { signal: new AbortController().signal }, )) { // Exhaust the runtime event stream. } expect(fakeAgentState.promptInputs).toHaveLength(1); for (const item of steering) { expect(fakeAgentState.promptInputs[0]?.split(item.text)).toHaveLength(2); expect(JSON.stringify(fakeAgentState.initialMessages)).not.toContain(item.text); } expect(fakeAgentState.steeredMessages).toEqual([]); }); it("delivers images attached to initial and boundary steering", async () => { fakeAgentState.mode = "empty"; let claimCount = 0; const claimSteering = vi.fn(async () => { claimCount += 1; if (claimCount === 1) { return [ { id: "initial-image", messageId: "initial-image-message", text: "Inspect the first image.", images: [ { name: "first.png", mimeType: "image/png" as const, data: new Uint8Array([1, 2, 3]), }, ], }, ]; } return [ { id: "boundary-image", messageId: "boundary-image-message", text: "Compare the second image.", images: [ { name: "second.png", mimeType: "image/png" as const, data: new Uint8Array([4, 5, 6]) }, ], }, ]; }); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "image-steering", prompt: "continue", instructions: "Inspect attached images.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, claimSteering, }, { signal: new AbortController().signal }, )) { // Exhaust the runtime event stream. } expect(fakeAgentState.promptImages).toEqual([ [{ type: "image", data: "AQID", mimeType: "image/png" }], ]); expect(fakeAgentState.steeredMessages).toContainEqual({ role: "user", content: [ { type: "text", text: "Compare the second image." }, { type: "image", data: "BAUG", mimeType: "image/png" }, ], timestamp: expect.any(Number), }); }); afterEach(() => { if (previousMaxToolCalls === undefined) delete process.env.MAX_TOOL_CALLS_PER_TURN; else process.env.MAX_TOOL_CALLS_PER_TURN = previousMaxToolCalls; }); it.each([undefined, "", ".", "/home/rakazo", "subdir"])( "preserves executor-owned shell cwd defaults (%j)", async (cwd) => { const command = "printf WORKSPACE_OK > roundtrip.txt"; fakeAgentState.invoke = { name: "shell", args: cwd === undefined ? { command } : { command, cwd }, }; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "r", prompt: "write the workspace test file", instructions: "Use shell.", history: [], tools: [shellTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { signal: new AbortController().signal }, )) { // Exercise Pi argument preparation and execution, not just path helpers. } expect(executeTool.mock.calls).toStrictEqual([ ["shell", cwd ? { command, cwd } : { command }, "call-1"], ]); }, ); it("exposes a provider-safe name while executing the original connector name", async () => { const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "r", prompt: "write the result", instructions: "Use destination_write for connected destination records.", history: [], tools: [destinationTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "1", traceId: "1", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { // Exhaust the runtime event stream so tool execution completes. } expect(fakeAgentState.tools.map((tool) => tool.name)).toEqual(["destination_write"]); expect(executeTool).toHaveBeenCalledWith( "destination.write", { collection: "notes", title: "Result", body: "Done" }, "call-1", { connectorId: "destination", toolName: "destination.write" }, ); }); it("does not claim a tool-only turn finished work the model never described", async () => { const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "tool-only", prompt: "send the update", instructions: "Use the destination tool.", history: [], tools: [destinationTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool: vi.fn(async () => ({ ok: true })), }, { operationId: "tool-only", traceId: "tool-only", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(events).toContainEqual({ type: "progress", text: "Using destination_write", activity: true, }); expect(events).not.toContainEqual({ type: "text", text: "I finished the work." }); expect(events.at(-1)).toEqual({ type: "done" }); }); it("continues after narration is followed by a silent tool result turn", async () => { fakeAgentState.mode = "silent-continuation"; const runtime = new PiAgentRuntime(); const events: unknown[] = []; const onToolCompleted = vi.fn(); for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "silent-continuation", prompt: "send the update and report back", instructions: "Use the destination tool, then tell the user what happened.", history: [], tools: [destinationTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool: vi.fn(async () => ({ ok: true })), onToolCompleted, }, { operationId: "silent-continuation", traceId: "silent-continuation", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(fakeAgentState.followUpMessages).toHaveLength(1); expect(fakeAgentState.followUpMessages[0]).toEqual( expect.objectContaining({ role: "user", content: expect.stringContaining("Continue"), }), ); expect(onToolCompleted).toHaveBeenCalledWith( expect.objectContaining({ name: "destination.write", executionId: "call-1", result: expect.objectContaining({ content: expect.any(Array) }), durationMs: expect.any(Number), }), ); expect(events).toContainEqual({ type: "text", text: "The final answer." }); expect(events.at(-1)).toEqual({ type: "done", text: "I will check the destination first.The final answer.", }); }); it("does not wait for a slow tool completion hook", async () => { let resolveAudit!: () => void; const audit = new Promise((resolve) => { resolveAudit = resolve; }); const onToolCompleted = vi.fn(() => audit); let settled = false; const runtime = new PiAgentRuntime(); const run = (async () => { for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "non-blocking-audit", prompt: "send the update", instructions: "Use the destination tool.", history: [], tools: [destinationTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool: vi.fn(async () => ({ ok: true })), onToolCompleted, }, { operationId: "non-blocking-audit", traceId: "non-blocking-audit", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { // Exhaust the runtime event stream. } })().finally(() => { settled = true; }); try { await new Promise((resolve) => setTimeout(resolve, 10)); expect(settled).toBe(true); expect(onToolCompleted).toHaveBeenCalledOnce(); } finally { resolveAudit(); await run; } }); it("makes an unfinished tool turn visible instead of completing silently", async () => { fakeAgentState.mode = "silent-continuation"; fakeAgentState.emitFinalAfterFollowUp = false; const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "silent-fallback", prompt: "send the update and report back", instructions: "Use the destination tool, then tell the user what happened.", history: [], tools: [destinationTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool: vi.fn(async () => ({ ok: true })), }, { operationId: "silent-fallback", traceId: "silent-fallback", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(events).toContainEqual({ type: "text", text: "I completed the tool step but could not produce a final response. Please ask me to continue.", }); expect(events.at(-1)).toEqual({ type: "done", text: "I completed the tool step but could not produce a final response. Please ask me to continue.", }); }); it("keeps FYI bot-message wakes silent when the model produces nothing", async () => { fakeAgentState.mode = "empty"; const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "bot-wake-silent", prompt: "[bot] FYI only", instructions: "Stay silent when there is nothing to do.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, allowSilentEmpty: true, executeTool: vi.fn(async () => ({ ok: true })), }, { operationId: "bot-wake-silent", traceId: "bot-wake-silent", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(events).not.toContainEqual({ type: "text", text: "No response. Try again." }); expect(events).toEqual([{ type: "done" }]); }); it("still asks the user to retry when a normal empty turn produces nothing", async () => { fakeAgentState.mode = "empty"; const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "user-empty", prompt: "hello", instructions: "Reply to the user.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, executeTool: vi.fn(async () => ({ ok: true })), }, { operationId: "user-empty", traceId: "user-empty", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(events).toContainEqual({ type: "text", text: "No response. Try again." }); expect(events.at(-1)).toEqual({ type: "done", text: "No response. Try again." }); }); it("surfaces a contextual peer fallback when that run produces nothing", async () => { fakeAgentState.mode = "empty"; const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "peer-result-empty", prompt: "[bot] result", instructions: "Summarize the result.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, emptyResponseText: "Update from Researcher: The answer is 42.", executeTool: vi.fn(async () => ({ ok: true })), }, { operationId: "peer-result-empty", traceId: "peer-result-empty", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(events).toContainEqual({ type: "text", text: "Update from Researcher: The answer is 42.", }); expect(events.at(-1)).toEqual({ type: "done", text: "Update from Researcher: The answer is 42.", }); }); it("normalizes a blank contextual fallback", async () => { fakeAgentState.mode = "empty"; const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "peer-result-blank", prompt: "[bot] result", instructions: "Summarize the result.", history: [], tools: [], model: { provider: "test", id: "dispatch-test-model" }, emptyResponseText: " ", executeTool: vi.fn(async () => ({ ok: true })), }, { operationId: "peer-result-blank", traceId: "peer-result-blank", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(events.at(-1)).toEqual({ type: "done", text: "No response. Try again." }); }); it("allows more than 80 tool calls by default when no fuse is configured", async () => { fakeAgentState.mode = "parent-limit"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "unlimited", prompt: "keep going", instructions: "Use shell.", history: [], tools: [shellTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "2a", traceId: "2a", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(100); expect(fakeAgentState.abortCount).toBe(0); expect(events).not.toContainEqual( expect.objectContaining({ type: "progress", text: expect.stringContaining("tool calls in one turn"), }), ); }); it("soft-stops with a final message when MAX_TOOL_CALLS_PER_TURN is set", async () => { process.env.MAX_TOOL_CALLS_PER_TURN = "80"; fakeAgentState.mode = "parent-limit"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "parent-limited", prompt: "keep going", instructions: "Use shell.", history: [], tools: [shellTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "2b", traceId: "2b", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(80); expect(fakeAgentState.abortCount).toBeGreaterThanOrEqual(1); expect(events).toContainEqual({ type: "progress", text: "Stopped: more than 80 tool calls in one turn.", }); expect(events).toContainEqual({ type: "text", text: "I stopped after reaching the limit of 80 tool calls in this turn. Send another message to continue.", }); expect(events.at(-1)).toEqual({ type: "done", text: "I stopped after reaching the limit of 80 tool calls in this turn. Send another message to continue.", }); }); it("shares an optional tool-call fuse with subagents and still emits a final message", async () => { process.env.MAX_TOOL_CALLS_PER_TURN = "80"; fakeAgentState.mode = "subagent-limit"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "limited", prompt: "delegate the loop", instructions: "Use run_subagent.", history: [], tools: [ { name: "run_subagent", description: "Delegate work", inputSchema: { type: "object", properties: {} }, }, shellTool, ], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "2", traceId: "2", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(79); expect(fakeAgentState.abortCount).toBeGreaterThanOrEqual(1); expect(events).toContainEqual({ type: "progress", text: "Stopped: more than 80 tool calls in one turn.", }); expect(events).toContainEqual({ type: "text", text: "I stopped after reaching the limit of 80 tool calls in this turn. Send another message to continue.", }); expect(events).toContainEqual({ type: "done", text: "I stopped after reaching the limit of 80 tool calls in this turn. Send another message to continue.", }); const subagentEvents = events.filter( (event): event is { type: "subagent"; status: string; result?: string } => typeof event === "object" && event !== null && "type" in event && (event as { type?: string }).type === "subagent", ); expect(subagentEvents.some((event) => event.status === "failed")).toBe(false); expect(subagentEvents).toContainEqual( expect.objectContaining({ type: "subagent", status: "completed", result: "I stopped after reaching the limit of 80 tool calls in this turn. Send another message to continue.", }), ); }); it("treats unset, empty, and non-positive MAX_TOOL_CALLS_PER_TURN as unlimited", () => { expect(maxToolCallsPerTurn({})).toBe(0); expect(maxToolCallsPerTurn({ MAX_TOOL_CALLS_PER_TURN: "" })).toBe(0); expect(maxToolCallsPerTurn({ MAX_TOOL_CALLS_PER_TURN: "0" })).toBe(0); expect(maxToolCallsPerTurn({ MAX_TOOL_CALLS_PER_TURN: "-5" })).toBe(0); expect(maxToolCallsPerTurn({ MAX_TOOL_CALLS_PER_TURN: "abc" })).toBe(0); expect(maxToolCallsPerTurn({ MAX_TOOL_CALLS_PER_TURN: "80" })).toBe(80); expect(maxToolCallsPerTurn({ MAX_TOOL_CALLS_PER_TURN: " 12.9 " })).toBe(12); }); it("lets a parallel tool batch finish before the optional fuse aborts", async () => { process.env.MAX_TOOL_CALLS_PER_TURN = "2"; fakeAgentState.mode = "parent-parallel"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "parallel-fuse", prompt: "run them together", instructions: "Use shell.", history: [], tools: [shellTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "2c", traceId: "2c", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(2); expect(fakeAgentState.abortCount).toBeGreaterThanOrEqual(1); expect(events).toContainEqual({ type: "progress", text: "Stopped: more than 2 tool calls in one turn.", }); }); it("keeps an optional tool-call fuse across a second run() for the same runId", async () => { process.env.MAX_TOOL_CALLS_PER_TURN = "5"; fakeAgentState.mode = "parent-limit"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const abort = new AbortController(); let started = 0; executeTool.mockImplementation(async () => { started += 1; if (started === 2) abort.abort(); return { ok: true }; }); await expect(async () => { for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "fuse-resume", prompt: "keep going", instructions: "Use shell.", history: [], tools: [shellTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "2d", traceId: "2d", spaceId: "w", userId: "u", signal: abort.signal, }, )) { // Exhaust until abort fails the turn. } }).rejects.toThrow(); executeTool.mockImplementation(async () => ({ ok: true })); const events: unknown[] = []; for await (const event of runtime.run( { botId: "b", threadId: "t", runId: "fuse-resume", prompt: "keep going", instructions: "Use shell.", history: [], tools: [shellTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "2e", traceId: "2e", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(5); expect(events).toContainEqual({ type: "progress", text: "Stopped: more than 5 tool calls in one turn.", }); }); it("keeps an optional tool-call fuse across ask_user pause and continueRun", async () => { process.env.MAX_TOOL_CALLS_PER_TURN = "4"; fakeAgentState.mode = "ask-pause"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const events: unknown[] = []; const request = { botId: "b", threadId: "t", runId: "fuse-ask-pause", prompt: "keep going", instructions: "Use shell, then ask.", history: [], tools: [ shellTool, { name: "ask_user", description: "Ask a short multiple-choice question", inputSchema: { type: "object", properties: {} }, }, ], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }; for await (const event of runtime.run(request, { operationId: "2f", traceId: "2f", spaceId: "w", userId: "u", signal: new AbortController().signal, })) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(2); expect(events).toContainEqual( expect.objectContaining({ type: "ask", text: "Which option?", }), ); fakeAgentState.mode = "parent-limit"; const resumed: unknown[] = []; for await (const event of runtime.run( { ...request, tools: [shellTool], }, { operationId: "2g", traceId: "2g", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { resumed.push(event); } expect(executeTool).toHaveBeenCalledTimes(3); expect(resumed).toContainEqual({ type: "progress", text: "Stopped: more than 4 tool calls in one turn.", }); }); it("keeps an optional tool-call fuse across nested ask_user pause and continueRun", async () => { process.env.MAX_TOOL_CALLS_PER_TURN = "4"; fakeAgentState.mode = "nested-ask-pause"; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); const events: unknown[] = []; const request = { botId: "b", threadId: "t", runId: "fuse-nested-ask-pause", prompt: "delegate then ask", instructions: "Use a subagent.", history: [], tools: [ { name: "run_subagent", description: "Delegate work", inputSchema: { type: "object", properties: {} }, }, shellTool, { name: "ask_user", description: "Ask a short multiple-choice question", inputSchema: { type: "object", properties: {} }, }, ], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }; for await (const event of runtime.run(request, { operationId: "2h", traceId: "2h", spaceId: "w", userId: "u", signal: new AbortController().signal, })) { events.push(event); } expect(executeTool).toHaveBeenCalledTimes(2); expect(events).toContainEqual( expect.objectContaining({ type: "ask", text: "Which option?", }), ); fakeAgentState.mode = "parent-limit"; const resumed: unknown[] = []; for await (const event of runtime.run( { ...request, tools: [shellTool], }, { operationId: "2i", traceId: "2i", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { resumed.push(event); } expect(executeTool).toHaveBeenCalledTimes(2); expect(resumed).toContainEqual({ type: "progress", text: "Stopped: more than 4 tool calls in one turn.", }); }); it("clips structured tool results to one aggregate text budget", async () => { const first = "a".repeat(TOOL_RESULT_TEXT_LIMIT - 5); const image = { type: "image" as const, data: "iVBORw0KGgo=", mimeType: "image/png" as const }; fakeAgentState.invoke = { name: "destination_write", args: { collection: "notes", title: "Result", body: "Done" }, }; const executeTool = vi.fn(async () => ({ kind: "agent_tool_result", content: [ { type: "text" as const, text: first }, image, { type: "text" as const, text: "bbbbbbbbbb" }, { type: "text" as const, text: "omit me" }, ], details: { ok: true }, })); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "clip-multipart", prompt: "write it", instructions: "Use the destination.", history: [], tools: [destinationTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "clip", traceId: "2h", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { // Exhaust so the fake agent executes the tool. } const result = fakeAgentState.toolResult as { content: Array<{ type: string; text?: string }>; }; expect(result.content).toHaveLength(3); expect(result.content[0]).toEqual({ type: "text", text: first }); expect(result.content[1]).toEqual(image); expect(result.content[2]).toEqual({ type: "text", text: "bbbbb…" }); }); it("serialises object content instead of writing [object Object] for write_file", async () => { fakeAgentState.invoke = { name: "write_file", args: { path: "shared/state.json", content: { last_run: 1_787_648_953 } }, }; const executeTool = vi.fn(async () => ({ ok: true })); const runtime = new PiAgentRuntime(); for await (const _event of runtime.run( { botId: "b", threadId: "t", runId: "r", prompt: "save the state file", instructions: "Use write_file to save state.", history: [], tools: [writeFileTool], model: { provider: "test", id: "dispatch-test-model" }, executeTool, }, { operationId: "3", traceId: "3", spaceId: "w", userId: "u", signal: new AbortController().signal, }, )) { // Exhaust the runtime event stream so tool execution completes. } expect(executeTool).toHaveBeenCalledWith( "write_file", { path: "shared/state.json", content: '{\n "last_run": 1787648953\n}', }, "call-1", ); }); });