evOo-local/packages/adapters/src/pi-runtime-tool-dispatch.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

1380 lines
44 KiB
TypeScript

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<string, unknown>;
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
}>,
invoke: {
name: "destination_write",
args: { collection: "notes", title: "Result", body: "Done" } as Record<string, unknown>,
},
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<string, unknown>) => 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<string, unknown>) => 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<string, unknown>) {
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<void>((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",
);
});
});