mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 17:34:51 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d3910943 | ||
|
|
2095efb07c |
@@ -607,14 +607,23 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
|
||||
expect(statuses).toContain("running");
|
||||
});
|
||||
|
||||
function taskNotification(status: string, form = "user"): string {
|
||||
const text = `<task-notification>\n<task-id>${AGENT_ID}</task-id>\n<tool-use-id>${TOOL_USE_ID}</tool-use-id>\n<status>${status}</status>\n<summary>Background task ended</summary>\n</task-notification>`;
|
||||
function taskNotification({
|
||||
status,
|
||||
form = "user",
|
||||
includeToolUseId = true,
|
||||
}: {
|
||||
status: string;
|
||||
form?: string;
|
||||
includeToolUseId?: boolean;
|
||||
}): string {
|
||||
const toolUseTag = includeToolUseId ? `<tool-use-id>${TOOL_USE_ID}</tool-use-id>\n` : "";
|
||||
const text = `<task-notification>\n<task-id>${AGENT_ID}</task-id>\n${toolUseTag}<status>${status}</status>\n<summary>Background task ended</summary>\n</task-notification>`;
|
||||
const records = {
|
||||
user: { message: { role: "user", content: [{ type: "text", text }] } },
|
||||
system: {
|
||||
subtype: "task_notification",
|
||||
task_id: AGENT_ID,
|
||||
tool_use_id: TOOL_USE_ID,
|
||||
...(includeToolUseId ? { tool_use_id: TOOL_USE_ID } : {}),
|
||||
status,
|
||||
},
|
||||
"queue-operation": { operation: "enqueue", content: text },
|
||||
@@ -636,7 +645,7 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
|
||||
`restores a %s task notification from ${form} when the child stop_reason is null`,
|
||||
async (status, expected) => {
|
||||
writeSession({
|
||||
parentLines: [...backgroundLaunch(), taskNotification(status, form)],
|
||||
parentLines: [...backgroundLaunch(), taskNotification({ status, form })],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: null })],
|
||||
});
|
||||
@@ -648,9 +657,73 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
|
||||
);
|
||||
}
|
||||
|
||||
for (const form of ["user", "system", "queue-operation"]) {
|
||||
test.each([
|
||||
["completed", "completed"],
|
||||
["failed", "failed"],
|
||||
["stopped", "canceled"],
|
||||
["unknown", "running"],
|
||||
])(`restores a task-id-only %s notification from ${form}`, async (status, expected) => {
|
||||
writeSession({
|
||||
parentLines: [
|
||||
...backgroundLaunch(),
|
||||
taskNotification({ status, form, includeToolUseId: false }),
|
||||
],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: "tool_use" })],
|
||||
});
|
||||
expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({
|
||||
id: TOOL_USE_ID,
|
||||
status: expected,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("keeps the latest outcome across task-only and tool-linked notifications", async () => {
|
||||
writeSession({
|
||||
parentLines: [
|
||||
...backgroundLaunch(),
|
||||
taskNotification({ status: "stopped", form: "queue-operation", includeToolUseId: false }),
|
||||
taskNotification({ status: "stopped", form: "user", includeToolUseId: false }),
|
||||
taskNotification({ status: "completed" }),
|
||||
],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: "tool_use" })],
|
||||
});
|
||||
expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({ status: "completed" });
|
||||
});
|
||||
|
||||
test("honors a later task-only stopped notification after a tool-linked completion", async () => {
|
||||
writeSession({
|
||||
parentLines: [
|
||||
...backgroundLaunch(),
|
||||
taskNotification({ status: "completed" }),
|
||||
taskNotification({ status: "stopped", form: "user", includeToolUseId: false }),
|
||||
],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: "tool_use" })],
|
||||
});
|
||||
expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({ status: "canceled" });
|
||||
});
|
||||
|
||||
test("does not apply an unrelated task's stopped outcome to a child", async () => {
|
||||
writeSession({
|
||||
parentLines: [
|
||||
...backgroundLaunch(),
|
||||
taskNotification({ status: "stopped", form: "user", includeToolUseId: false }).replace(
|
||||
AGENT_ID,
|
||||
"unrelated-task",
|
||||
),
|
||||
],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: "tool_use" })],
|
||||
});
|
||||
expect(upserts(await replayDescriptors()).at(-1)).toMatchObject({ status: "running" });
|
||||
});
|
||||
|
||||
test("prefers a stopped notification over the child's end_turn", async () => {
|
||||
writeSession({
|
||||
parentLines: [...backgroundLaunch(), taskNotification("stopped")],
|
||||
parentLines: [...backgroundLaunch(), taskNotification({ status: "stopped" })],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: "end_turn" })],
|
||||
});
|
||||
@@ -659,7 +732,7 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
|
||||
|
||||
test("does not invent an outcome for an unknown notification status", async () => {
|
||||
writeSession({
|
||||
parentLines: [...backgroundLaunch(), taskNotification("unknown")],
|
||||
parentLines: [...backgroundLaunch(), taskNotification({ status: "unknown" })],
|
||||
meta: JSON.stringify({ toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [sidechainEntry({ stopReason: null })],
|
||||
});
|
||||
|
||||
@@ -6435,10 +6435,9 @@ async function readClaudeReplayParentFacts(
|
||||
const linksByAgentId = new Map<string, { toolCallId: string }>();
|
||||
const outcomes: Map<string, { status: ProviderSubagentStatus }> = new Map();
|
||||
for await (const entry of entries) {
|
||||
const notification = readTaskNotificationFromHistoryRecord(entry);
|
||||
const status = mapTaskStatus(notification?.status ?? undefined);
|
||||
if (notification?.toolUseId && status) {
|
||||
outcomes.set(notification.toolUseId, { status });
|
||||
const notificationOutcome = readClaudeReplayNotificationOutcome(entry, linksByAgentId);
|
||||
if (notificationOutcome) {
|
||||
outcomes.set(notificationOutcome.toolCallId, { status: notificationOutcome.status });
|
||||
}
|
||||
const content = toObjectRecord(entry.message)?.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
@@ -6470,6 +6469,19 @@ async function readClaudeReplayParentFacts(
|
||||
};
|
||||
}
|
||||
|
||||
function readClaudeReplayNotificationOutcome(
|
||||
entry: ClaudeHistoryEntry,
|
||||
linksByAgentId: ReadonlyMap<string, { toolCallId: string }>,
|
||||
): { toolCallId: string; status: ProviderSubagentStatus } | null {
|
||||
const notification = readTaskNotificationFromHistoryRecord(entry);
|
||||
const status = mapTaskStatus(notification?.status ?? undefined);
|
||||
// Stopped tasks can omit tool-use-id; their launch result already links the native ID.
|
||||
const toolCallId =
|
||||
notification?.toolUseId ??
|
||||
(notification?.taskId ? linksByAgentId.get(notification.taskId)?.toolCallId : undefined);
|
||||
return toolCallId && status ? { toolCallId, status } : null;
|
||||
}
|
||||
|
||||
function readClaudeReplayTaskDeclaration(
|
||||
entryType: unknown,
|
||||
block: Record<string, unknown> | undefined,
|
||||
|
||||
Reference in New Issue
Block a user