Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions packages/acp-agent/src/agent-prompt-cancellation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,98 @@ describe("FledglingAgent prompt cancellation", () => {
);
});

it("includes prior successful tool calls and results in later model turns", async () => {
const { agent, sessionId, streamText } = await createTestAgent();
const capturedMessages: unknown[] = [];
streamText.mockImplementation((request: { readonly messages: unknown }) => {
capturedMessages.push(JSON.parse(JSON.stringify(request.messages)));
return capturedMessages.length === 1
? createImmediateStream([
{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_read", input: { path: "README.md" } },
{ type: "tool-call", toolCallId: "call-2", toolName: "workspace_list", input: { path: "." } },
{ type: "tool-result", toolCallId: "call-1", output: { content: "read ok" } },
{ type: "tool-result", toolCallId: "call-2", output: { entries: ["README.md"] } },
{ type: "text-delta", text: "done" }
])
: createImmediateStream([]);
});

await agent.prompt({ sessionId, prompt: [{ type: "text", text: "inspect" }] });
await agent.prompt({ sessionId, prompt: [{ type: "text", text: "continue" }] });

expect(capturedMessages[1]).toEqual([
{ role: "user", content: "inspect" },
{
role: "assistant",
content: [
{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_read", input: { path: "README.md" } },
{ type: "tool-call", toolCallId: "call-2", toolName: "workspace_list", input: { path: "." } }
]
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "workspace_read",
output: { type: "json", value: { content: "read ok" } }
}
]
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-2",
toolName: "workspace_list",
output: { type: "json", value: { entries: ["README.md"] } }
}
]
},
{ role: "assistant", content: "done" },
{ role: "user", content: "continue" }
]);
});

it("includes prior failed tool results in later model turns", async () => {
const { agent, sessionId, streamText } = await createTestAgent();
const capturedMessages: unknown[] = [];
streamText.mockImplementation((request: { readonly messages: unknown }) => {
capturedMessages.push(JSON.parse(JSON.stringify(request.messages)));
return capturedMessages.length === 1
? createImmediateStream([
{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_write", input: { path: "x" } },
{ type: "tool-error", toolCallId: "call-1", error: new Error("write failed") }
])
: createImmediateStream([]);
});

await agent.prompt({ sessionId, prompt: [{ type: "text", text: "write" }] });
await agent.prompt({ sessionId, prompt: [{ type: "text", text: "what failed" }] });

expect(capturedMessages[1]).toEqual([
{ role: "user", content: "write" },
{
role: "assistant",
content: [{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_write", input: { path: "x" } }]
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "workspace_write",
output: { type: "error-text", value: "write failed" }
}
]
},
{ role: "user", content: "what failed" }
]);
});

it("does not use ACP host filesystem or permission methods during prompt flow", async () => {
const { manager: sessionManager, sessionFile, tempDir: createdTempDir } = await createTempSessionManager();
const sessionUpdates: FakeSessionUpdate[] = [];
Expand Down Expand Up @@ -458,6 +550,81 @@ describe("FledglingAgent prompt cancellation", () => {
]);
});

it("keeps streamed tool history available after a durable stream error", async () => {
const { agent, sessionId, streamText } = await createTestAgent();
const capturedMessages: unknown[] = [];
streamText.mockImplementation((request: { readonly messages: unknown }) => {
capturedMessages.push(JSON.parse(JSON.stringify(request.messages)));
return capturedMessages.length === 1
? createFailingStream(
[
{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_read", input: { path: "README.md" } },
{ type: "tool-result", toolCallId: "call-1", output: { content: "partial context" } },
{ type: "text-delta", text: "partial" }
],
new Error("boom")
)
: createImmediateStream([]);
});

await expect(agent.prompt({ sessionId, prompt: [{ type: "text", text: "inspect" }] })).rejects.toThrow(
"Fledgling model stream failed: boom"
);
await agent.prompt({ sessionId, prompt: [{ type: "text", text: "continue" }] });

expect(capturedMessages[1]).toEqual([
{ role: "user", content: "inspect" },
{
role: "assistant",
content: [{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_read", input: { path: "README.md" } }]
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "workspace_read",
output: { type: "json", value: { content: "partial context" } }
}
]
},
{ role: "assistant", content: "partial" },
{ role: "user", content: "continue" }
]);
});

it("falls back to text when tool output is not JSON-safe", async () => {
const { agent, sessionId, streamText } = await createTestAgent();
const capturedMessages: unknown[] = [];
const circular: { self?: unknown } = {};
circular.self = circular;
streamText.mockImplementation((request: { readonly messages: unknown }) => {
capturedMessages.push(JSON.parse(JSON.stringify(request.messages)));
return capturedMessages.length === 1
? createImmediateStream([
{ type: "tool-call", toolCallId: "call-1", toolName: "workspace_read", input: { path: "README.md" } },
{ type: "tool-result", toolCallId: "call-1", output: circular }
])
: createImmediateStream([]);
});

await agent.prompt({ sessionId, prompt: [{ type: "text", text: "inspect" }] });
await agent.prompt({ sessionId, prompt: [{ type: "text", text: "continue" }] });

expect(capturedMessages[1]).toContainEqual({
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call-1",
toolName: "workspace_read",
output: { type: "json", value: "[object Object]" }
}
]
});
});

it("does not persist an empty assistant message when streams fail before text", async () => {
const { agent, sessionId, streamText, sessionFile, sessionUpdates } = await createTestAgent();
streamText.mockReturnValueOnce(createFailingStream([], new Error("stream failed")));
Expand Down
Loading
Loading