mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-17 23:02:36 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38a68a5b5b |
+78
@@ -0,0 +1,78 @@
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import type { AgentSession, AgentTimelineItem } from "../../agent-sdk-types.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
|
||||
const sessions: AgentSession[] = [];
|
||||
const directories: string[] = [];
|
||||
afterEach(async () => {
|
||||
for (const session of sessions.splice(0)) await session.close();
|
||||
for (const cwd of directories.splice(0)) rmSync(cwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("a marked progress update stays assistant text when the session is reopened", async () => {
|
||||
const markedTexts = new Set<string>();
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: pino(
|
||||
{ level: "trace" },
|
||||
{
|
||||
write(line: string) {
|
||||
const record = JSON.parse(line);
|
||||
const event = record.rawEvent;
|
||||
if (event?.type !== "assistant") return;
|
||||
for (const index of event.narration_block_indexes ?? []) {
|
||||
const text = event.message.content[index]?.thinking;
|
||||
if (text) markedTexts.add(text);
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "paseo-progress-replay-"));
|
||||
directories.push(cwd);
|
||||
for (const [name, text] of [
|
||||
["a", "Plan A\nCost: 3 credits\nTime: 2 days\nRisk: needs a manual review step\n"],
|
||||
["b", "Plan B\nCost: 5 credits\nTime: 1 day\nRisk: none known\n"],
|
||||
["c", "Plan C\nCost: 2 credits\nTime: 4 days\nRisk: depends on an external vendor\n"],
|
||||
])
|
||||
writeFileSync(path.join(cwd, `plan-${name}.txt`), text);
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd,
|
||||
model: "claude-opus-5-5",
|
||||
thinkingOptionId: "xhigh",
|
||||
modeId: "bypassPermissions",
|
||||
});
|
||||
sessions.push(session);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "turn_failed") reject(new Error(event.error));
|
||||
if (event.type === "permission_requested" || event.type === "turn_completed") resolve();
|
||||
});
|
||||
void session
|
||||
.startTurn(
|
||||
"There are three plan files in this directory: plan-a.txt, plan-b.txt and plan-c.txt. Read each one, tell me how they compare on cost, time and risk, and then ask me which plan to pick using your AskUserQuestion tool. Keep me posted as you go.",
|
||||
)
|
||||
.catch(reject);
|
||||
});
|
||||
expect(
|
||||
markedTexts.size,
|
||||
"provider must produce a marked update for this reproduction",
|
||||
).toBeGreaterThan(0);
|
||||
const persistence = session.describePersistence();
|
||||
expect(persistence).not.toBeNull();
|
||||
await session.close();
|
||||
sessions.splice(sessions.indexOf(session), 1);
|
||||
const resumed = await client.resumeSession(persistence!, { cwd });
|
||||
sessions.push(resumed);
|
||||
const items: AgentTimelineItem[] = [];
|
||||
for await (const event of resumed.streamHistory()) {
|
||||
if (event.type === "timeline") items.push(event.item);
|
||||
}
|
||||
for (const text of markedTexts) {
|
||||
expect(items).toContainEqual(expect.objectContaining({ type: "assistant_message", text }));
|
||||
}
|
||||
}, 280_000);
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
|
||||
import type { AgentSession, AgentStreamEvent, AgentTimelineItem } from "../../agent-sdk-types.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
|
||||
// Reproduces #6199 through the real Claude provider. Claude Opus 5.5 writes the
|
||||
// text it means for the user between tool calls as a "progress update": a
|
||||
// thinking block that Claude Code marks with `narration_block_indexes`.
|
||||
const PROMPT =
|
||||
"There are three plan files in this directory: plan-a.txt, plan-b.txt and plan-c.txt. Read each one, tell me how they compare on cost, time and risk, and then ask me which plan to pick using your AskUserQuestion tool. Keep me posted as you go.";
|
||||
|
||||
const handles: Array<{ cwd: string; session: AgentSession }> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const handle of handles.splice(0)) {
|
||||
await handle.session.close().catch(() => undefined);
|
||||
rmSync(handle.cwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function writePlans(cwd: string): void {
|
||||
writeFileSync(
|
||||
path.join(cwd, "plan-a.txt"),
|
||||
"Plan A\nCost: 3 credits\nTime: 2 days\nRisk: needs a manual review step\n",
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(cwd, "plan-b.txt"),
|
||||
"Plan B\nCost: 5 credits\nTime: 1 day\nRisk: none known\n",
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(cwd, "plan-c.txt"),
|
||||
"Plan C\nCost: 2 credits\nTime: 4 days\nRisk: depends on an external vendor\n",
|
||||
);
|
||||
}
|
||||
|
||||
test("the comparison written before AskUserQuestion reaches the timeline as assistant text", async () => {
|
||||
const cwd = mkdtempSync(path.join(tmpdir(), "claude-progress-updates-"));
|
||||
writePlans(cwd);
|
||||
const client = new ClaudeAgentClient({ logger: pino({ level: "silent" }) });
|
||||
const session = await client.createSession({
|
||||
provider: "claude",
|
||||
cwd,
|
||||
model: "claude-opus-5-5",
|
||||
thinkingOptionId: "xhigh",
|
||||
modeId: "bypassPermissions",
|
||||
});
|
||||
handles.push({ cwd, session });
|
||||
|
||||
const items: AgentTimelineItem[] = [];
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
session.subscribe((event: AgentStreamEvent) => {
|
||||
if (event.type === "timeline") items.push(event.item);
|
||||
if (event.type === "turn_failed") reject(new Error(event.error));
|
||||
if (
|
||||
event.type === "permission_requested" ||
|
||||
event.type === "turn_completed" ||
|
||||
event.type === "turn_failed"
|
||||
) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
void session.startTurn(PROMPT).catch(reject);
|
||||
});
|
||||
|
||||
const lastReadIndex = items.findLastIndex(
|
||||
(item) => item.type === "tool_call" && item.name !== "AskUserQuestion",
|
||||
);
|
||||
const askIndex = items.findIndex(
|
||||
(item) => item.type === "tool_call" && item.name === "AskUserQuestion",
|
||||
);
|
||||
expect(askIndex).toBeGreaterThan(lastReadIndex);
|
||||
const between = items.slice(lastReadIndex + 1, askIndex);
|
||||
const assistantText = between
|
||||
.flatMap((item) => (item.type === "assistant_message" ? [item.text] : []))
|
||||
.join("");
|
||||
expect(assistantText).toMatch(/plan/i);
|
||||
}, 280_000);
|
||||
Reference in New Issue
Block a user