mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 06:02:56 +00:00
Merge branch 'main' into update-zh-i18n-upstream-pr
This commit is contained in:
@@ -634,6 +634,72 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
|
||||
expect(events[0]).toMatchObject({ id: TOOL_USE_ID });
|
||||
});
|
||||
|
||||
test.each(["Agent", "Task"] as const)(
|
||||
"labels a background %s call's card with its type and task after a restart",
|
||||
async (toolName) => {
|
||||
writeSession({
|
||||
parentLines: [
|
||||
parentEntry([
|
||||
{
|
||||
type: "tool_use",
|
||||
id: TOOL_USE_ID,
|
||||
name: toolName,
|
||||
input: {
|
||||
subagent_type: "general-purpose",
|
||||
description: "Count files here",
|
||||
prompt: "Run ls and reply with the number of entries.",
|
||||
},
|
||||
},
|
||||
]),
|
||||
JSON.stringify({
|
||||
type: "user",
|
||||
sessionId: "replay-session",
|
||||
timestamp: "2026-07-26T06:27:48.000Z",
|
||||
message: {
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: TOOL_USE_ID,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Async agent launched successfully.\nagentId: ${AGENT_ID}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
toolUseResult: { isAsync: true, status: "async_launched", agentId: AGENT_ID },
|
||||
}),
|
||||
],
|
||||
meta: JSON.stringify({
|
||||
agentType: "general-purpose",
|
||||
description: "Count files here",
|
||||
toolUseId: TOOL_USE_ID,
|
||||
spawnDepth: 1,
|
||||
requestShape: "background",
|
||||
}),
|
||||
});
|
||||
|
||||
const cards = (await replayEvents()).flatMap((event) =>
|
||||
event.type === "timeline" &&
|
||||
event.item.type === "tool_call" &&
|
||||
event.item.callId === TOOL_USE_ID
|
||||
? [event.item]
|
||||
: [],
|
||||
);
|
||||
expect(cards.at(-1)).toMatchObject({
|
||||
status: "completed",
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
subAgentType: "general-purpose",
|
||||
description: "Count files here",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("replays the subagent's own transcript onto its timeline", async () => {
|
||||
writeSession({
|
||||
parentLines: [taskToolUse(), taskToolResult()],
|
||||
|
||||
@@ -130,6 +130,7 @@ import {
|
||||
type ProviderCatalog,
|
||||
type ProviderRefreshContext,
|
||||
type ResolveAgentDefaultModeInput,
|
||||
type ToolCallDetail,
|
||||
} from "../../agent-sdk-types.js";
|
||||
import { importSessionFromPersistence } from "../../provider-session-import.js";
|
||||
import { runProviderRefreshActivity } from "../../provider-refresh-deadline.js";
|
||||
@@ -295,6 +296,13 @@ interface AsyncMessageInput<T> {
|
||||
interface ClaudeReplayOwnership {
|
||||
restoredIds: ReadonlySet<string>;
|
||||
toolOwners: ReadonlyMap<string, string>;
|
||||
/** The parent's Task calls, keyed by tool-call id, so their cards replay labeled as they were live. */
|
||||
subagentToolCalls: ReadonlyMap<string, ClaudeSubagentCardFacts>;
|
||||
}
|
||||
|
||||
interface ClaudeSubagentCardFacts {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface PersistedTimelineEntry {
|
||||
@@ -4249,16 +4257,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
return {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
...toolCall,
|
||||
detail: {
|
||||
type: "sub_agent",
|
||||
...(declaration.title ? { subAgentType: declaration.title } : {}),
|
||||
...(declaration.description ? { description: declaration.description } : {}),
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
},
|
||||
item: { ...toolCall, detail: buildClaudeSubagentCardDetail(declaration) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4977,6 +4976,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const sidechainEntries = [parentContent, ...sidechains.contents]
|
||||
.flatMap(parseClaudeHistoryRecords)
|
||||
.filter((entry) => entry.isSidechain === true && typeof entry.agentId === "string");
|
||||
const parentFacts = readClaudeReplayParentFacts(parentEntries);
|
||||
|
||||
// Replay produces the same observations the live task protocol produces, then folds them
|
||||
// with the same function, so identity and status are derived once for both paths.
|
||||
@@ -4987,7 +4987,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
entries,
|
||||
parentFacts: readClaudeReplayParentFacts(entries as ClaudeHistoryEntry[]),
|
||||
})),
|
||||
parent: readClaudeReplayParentFacts(parentEntries),
|
||||
parent: parentFacts,
|
||||
convertEntry: (entry) => this.convertHistoryEntry(entry as ClaudeHistoryEntry),
|
||||
});
|
||||
const observations = [
|
||||
@@ -5016,6 +5016,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const replay = {
|
||||
restoredIds: restoredProviderSubagentIds,
|
||||
toolOwners: subagentReplay.toolOwners,
|
||||
subagentToolCalls: parentFacts.toolCalls,
|
||||
};
|
||||
if (observations.length === 0) return replay;
|
||||
|
||||
@@ -5082,7 +5083,12 @@ class ClaudeAgentSession implements AgentSession {
|
||||
return;
|
||||
}
|
||||
const taskSnapshot = this.taskState.observe(entry);
|
||||
const items = [...(taskSnapshot ? [taskSnapshot] : []), ...this.convertHistoryEntry(entry)];
|
||||
const items = [
|
||||
...(taskSnapshot ? [taskSnapshot] : []),
|
||||
...this.convertHistoryEntry(entry).map((item) =>
|
||||
labelReplayedSubagentCard(item, replay.subagentToolCalls),
|
||||
),
|
||||
];
|
||||
const isVisibleUserEntry =
|
||||
entry.type === "user" &&
|
||||
typeof entry.uuid === "string" &&
|
||||
@@ -5832,6 +5838,33 @@ function parseClaudeHistoryRecords(content: string): ClaudeHistoryEntry[] {
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** The parent's card for a subagent, labeled with its type and task. */
|
||||
function buildClaudeSubagentCardDetail(
|
||||
facts: ClaudeSubagentCardFacts,
|
||||
): Extract<ToolCallDetail, { type: "sub_agent" }> {
|
||||
return {
|
||||
type: "sub_agent",
|
||||
...(facts.title ? { subAgentType: facts.title } : {}),
|
||||
...(facts.description ? { description: facts.description } : {}),
|
||||
log: "",
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Live, a Task call's card is built from the task protocol's declaration. Replay has no task
|
||||
* protocol, so the generic tool mapper leaves the card unlabeled; label it from the same facts.
|
||||
*/
|
||||
function labelReplayedSubagentCard(
|
||||
item: AgentTimelineItem,
|
||||
subagentToolCalls: ReadonlyMap<string, ClaudeSubagentCardFacts>,
|
||||
): AgentTimelineItem {
|
||||
if (item.type !== "tool_call") return item;
|
||||
const facts = subagentToolCalls.get(item.callId);
|
||||
if (!facts) return item;
|
||||
return { ...item, detail: buildClaudeSubagentCardDetail(facts) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect what the parent transcript knows about its Task calls, in the shape the shared replay
|
||||
* source consumes. The agentId scrape is kept only as a fallback for sessions recorded before
|
||||
|
||||
Reference in New Issue
Block a user