mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-14 13:22:37 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2bbac1a0 | ||
|
|
8a4abfedfb |
@@ -700,6 +700,60 @@ describe("ClaudeAgentSession persisted subagent replay", () => {
|
||||
},
|
||||
);
|
||||
|
||||
test("keeps a foreground subagent card's action log after a restart", async () => {
|
||||
writeSession({
|
||||
parentLines: [
|
||||
parentEntry([
|
||||
{
|
||||
type: "tool_use",
|
||||
id: TOOL_USE_ID,
|
||||
name: "Agent",
|
||||
input: {
|
||||
subagent_type: "general-purpose",
|
||||
description: "Count files foreground",
|
||||
prompt: "Run ls and reply with the number of entries.",
|
||||
run_in_background: false,
|
||||
},
|
||||
},
|
||||
]),
|
||||
taskToolResult(),
|
||||
],
|
||||
meta: JSON.stringify({ agentType: "general-purpose", toolUseId: TOOL_USE_ID }),
|
||||
sidechainLines: [
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
isSidechain: true,
|
||||
agentId: AGENT_ID,
|
||||
sessionId: "replay-session",
|
||||
timestamp: "2026-07-26T06:27:49.000Z",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "toolu_child_bash",
|
||||
name: "Bash",
|
||||
input: { command: "ls -1 | wc -l", description: "Count entries" },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
sidechainEntry({ stopReason: "end_turn" }),
|
||||
],
|
||||
});
|
||||
|
||||
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({
|
||||
detail: { type: "sub_agent", log: "[Bash] ls -1 | wc -l" },
|
||||
});
|
||||
});
|
||||
|
||||
test("replays the subagent's own transcript onto its timeline", async () => {
|
||||
writeSession({
|
||||
parentLines: [taskToolUse(), taskToolResult()],
|
||||
|
||||
@@ -303,6 +303,8 @@ interface ClaudeReplayOwnership {
|
||||
interface ClaudeSubagentCardFacts {
|
||||
title?: string;
|
||||
description?: string;
|
||||
/** The child's tool calls, one line each. Live, the sidechain tracker streams it instead. */
|
||||
log?: string;
|
||||
}
|
||||
|
||||
interface PersistedTimelineEntry {
|
||||
@@ -5016,7 +5018,12 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const replay = {
|
||||
restoredIds: restoredProviderSubagentIds,
|
||||
toolOwners: subagentReplay.toolOwners,
|
||||
subagentToolCalls: parentFacts.toolCalls,
|
||||
subagentToolCalls: new Map(
|
||||
[...parentFacts.toolCalls].map(([id, facts]) => {
|
||||
const log = subagentReplay.actionLogs.get(id);
|
||||
return [id, log ? { ...facts, log } : facts];
|
||||
}),
|
||||
),
|
||||
};
|
||||
if (observations.length === 0) return replay;
|
||||
|
||||
@@ -5846,7 +5853,7 @@ function buildClaudeSubagentCardDetail(
|
||||
type: "sub_agent",
|
||||
...(facts.title ? { subAgentType: facts.title } : {}),
|
||||
...(facts.description ? { description: facts.description } : {}),
|
||||
log: "",
|
||||
log: facts.log ?? "",
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +55,86 @@ function isClaudeContentChunk(value: unknown): value is ClaudeContentChunk {
|
||||
);
|
||||
}
|
||||
|
||||
function isToolUseChunk(block: ClaudeContentChunk): block is ClaudeContentChunk & { name: string } {
|
||||
return (
|
||||
(block.type === "tool_use" ||
|
||||
block.type === "mcp_tool_use" ||
|
||||
block.type === "server_tool_use") &&
|
||||
typeof block.name === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSubAgentText(value: unknown): string | undefined {
|
||||
const normalized = readTrimmedString(value)?.replace(/\s+/g, " ");
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized.length <= MAX_SUB_AGENT_SUMMARY_CHARS) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, MAX_SUB_AGENT_SUMMARY_CHARS)}...`;
|
||||
}
|
||||
|
||||
function deriveSubAgentActionSummary(toolName: string, input: unknown): string | undefined {
|
||||
const runningToolCall = mapClaudeRunningToolCall({
|
||||
name: toolName,
|
||||
callId: `sub-agent-summary-${toolName}`,
|
||||
input,
|
||||
output: null,
|
||||
});
|
||||
if (!runningToolCall) {
|
||||
return undefined;
|
||||
}
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: runningToolCall.name,
|
||||
status: runningToolCall.status,
|
||||
error: runningToolCall.error,
|
||||
detail: runningToolCall.detail,
|
||||
metadata: runningToolCall.metadata,
|
||||
});
|
||||
return normalizeSubAgentText(display.summary);
|
||||
}
|
||||
|
||||
/** The action log on the parent's card for a subagent, one line per tool the child called. */
|
||||
function formatSubAgentActionLog(
|
||||
actions: readonly { toolName: string; summary?: string }[],
|
||||
): string {
|
||||
return actions
|
||||
.map((action) =>
|
||||
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild a subagent card's action log from the child's persisted transcript, matching the log
|
||||
* the live tracker builds from the same tool calls as they stream.
|
||||
*/
|
||||
export function readClaudeSubagentActionLog(
|
||||
entries: readonly { type?: unknown; message?: { content?: unknown } }[],
|
||||
): string {
|
||||
const callsById = new Map<string, { toolName: string; input: unknown }>();
|
||||
for (const entry of entries) {
|
||||
const content = entry.type === "assistant" ? entry.message?.content : undefined;
|
||||
if (!Array.isArray(content)) continue;
|
||||
for (const block of content) {
|
||||
if (!isClaudeContentChunk(block) || !isToolUseChunk(block)) continue;
|
||||
const toolName = readTrimmedString(block.name);
|
||||
if (!toolName) continue;
|
||||
const key = readTrimmedString(block.id) ?? `assistant:${toolName}:${callsById.size}`;
|
||||
if (callsById.has(key)) continue;
|
||||
callsById.set(key, { toolName, input: block.input ?? null });
|
||||
}
|
||||
}
|
||||
const actions = [...callsById.values()]
|
||||
.slice(-MAX_SUB_AGENT_LOG_ENTRIES)
|
||||
.map(({ toolName, input }) => ({
|
||||
toolName,
|
||||
summary: deriveSubAgentActionSummary(toolName, input),
|
||||
}));
|
||||
return formatSubAgentActionLog(actions);
|
||||
}
|
||||
|
||||
export class ClaudeSidechainTracker {
|
||||
private readonly activeSidechains = new Map<string, SubAgentActivityState>();
|
||||
private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
|
||||
@@ -129,11 +209,7 @@ export class ClaudeSidechainTracker {
|
||||
type: "sub_agent",
|
||||
...(state.subAgentType ? { subAgentType: state.subAgentType } : {}),
|
||||
...(state.description ? { description: state.description } : {}),
|
||||
log: state.actions
|
||||
.map((action) =>
|
||||
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||
)
|
||||
.join("\n"),
|
||||
log: formatSubAgentActionLog(state.actions),
|
||||
actions: [],
|
||||
};
|
||||
|
||||
@@ -298,9 +374,9 @@ export class ClaudeSidechainTracker {
|
||||
parentToolUseId: string,
|
||||
): boolean {
|
||||
const taskInput = this.getToolInput(parentToolUseId);
|
||||
const nextName = this.normalizeSubAgentText(taskInput?.name);
|
||||
const nextSubAgentType = this.normalizeSubAgentText(taskInput?.subagent_type);
|
||||
const nextDescription = this.normalizeSubAgentText(taskInput?.description);
|
||||
const nextName = normalizeSubAgentText(taskInput?.name);
|
||||
const nextSubAgentType = normalizeSubAgentText(taskInput?.subagent_type);
|
||||
const nextDescription = normalizeSubAgentText(taskInput?.description);
|
||||
|
||||
let changed = false;
|
||||
if (nextName && nextName !== state.name) {
|
||||
@@ -318,17 +394,6 @@ export class ClaudeSidechainTracker {
|
||||
return changed;
|
||||
}
|
||||
|
||||
private normalizeSubAgentText(value: unknown): string | undefined {
|
||||
const normalized = readTrimmedString(value)?.replace(/\s+/g, " ");
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
if (normalized.length <= MAX_SUB_AGENT_SUMMARY_CHARS) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, MAX_SUB_AGENT_SUMMARY_CHARS)}...`;
|
||||
}
|
||||
|
||||
private extractAssistantMessageActions(
|
||||
message: Extract<SDKMessage, { type: "assistant" }>,
|
||||
): SubAgentActionCandidate[] {
|
||||
@@ -338,15 +403,7 @@ export class ClaudeSidechainTracker {
|
||||
}
|
||||
const actions: SubAgentActionCandidate[] = [];
|
||||
for (const block of content) {
|
||||
if (
|
||||
!isClaudeContentChunk(block) ||
|
||||
!(
|
||||
block.type === "tool_use" ||
|
||||
block.type === "mcp_tool_use" ||
|
||||
block.type === "server_tool_use"
|
||||
) ||
|
||||
typeof block.name !== "string"
|
||||
) {
|
||||
if (!isClaudeContentChunk(block) || !isToolUseChunk(block)) {
|
||||
continue;
|
||||
}
|
||||
const key = readTrimmedString(block.id) ?? `assistant:${block.name}:${actions.length}`;
|
||||
@@ -367,15 +424,7 @@ export class ClaudeSidechainTracker {
|
||||
return [];
|
||||
}
|
||||
const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
|
||||
if (
|
||||
!block ||
|
||||
!(
|
||||
block.type === "tool_use" ||
|
||||
block.type === "mcp_tool_use" ||
|
||||
block.type === "server_tool_use"
|
||||
) ||
|
||||
typeof block.name !== "string"
|
||||
) {
|
||||
if (!block || !isToolUseChunk(block)) {
|
||||
return [];
|
||||
}
|
||||
const key =
|
||||
@@ -420,7 +469,7 @@ export class ClaudeSidechainTracker {
|
||||
return false;
|
||||
}
|
||||
|
||||
const summary = this.deriveSubAgentActionSummary(normalizedToolName, candidate.input);
|
||||
const summary = deriveSubAgentActionSummary(normalizedToolName, candidate.input);
|
||||
const existingIndex = state.actionIndexByKey.get(candidate.key);
|
||||
|
||||
if (existingIndex !== undefined) {
|
||||
@@ -471,24 +520,4 @@ export class ClaudeSidechainTracker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private deriveSubAgentActionSummary(toolName: string, input: unknown): string | undefined {
|
||||
const runningToolCall = mapClaudeRunningToolCall({
|
||||
name: toolName,
|
||||
callId: `sub-agent-summary-${toolName}`,
|
||||
input,
|
||||
output: null,
|
||||
});
|
||||
if (!runningToolCall) {
|
||||
return undefined;
|
||||
}
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: runningToolCall.name,
|
||||
status: runningToolCall.status,
|
||||
error: runningToolCall.error,
|
||||
detail: runningToolCall.detail,
|
||||
metadata: runningToolCall.metadata,
|
||||
});
|
||||
return this.normalizeSubAgentText(display.summary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AgentTimelineItem } from "../../../agent-sdk-types.js";
|
||||
import { normalizeProviderReplayTimestamp } from "../../../provider-history-timestamps.js";
|
||||
import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
|
||||
import { resolveObservedClaudeModelId } from "../models.js";
|
||||
import { readClaudeSubagentActionLog } from "../sidechain-tracker.js";
|
||||
import type { SubagentObservation } from "./observation.js";
|
||||
import { buildClaudeSubagentSubtitle, type ClaudeSubagentUsage } from "./presentation.js";
|
||||
|
||||
@@ -337,9 +338,15 @@ export function observeReplaySubagents(input: {
|
||||
subagents: readonly ClaudeReplaySubagentInput[];
|
||||
parent: ClaudeReplayParentFacts;
|
||||
convertEntry: (entry: ClaudeReplayEntry) => AgentTimelineItem[];
|
||||
}): { observations: SubagentObservation[]; toolOwners: ReadonlyMap<string, string> } {
|
||||
}): {
|
||||
observations: SubagentObservation[];
|
||||
toolOwners: ReadonlyMap<string, string>;
|
||||
/** Root subagent id -> the action log its card in the parent transcript shows. */
|
||||
actionLogs: ReadonlyMap<string, string>;
|
||||
} {
|
||||
const observations: SubagentObservation[] = [];
|
||||
const toolOwners = new Map<string, string>();
|
||||
const actionLogs = new Map<string, string>();
|
||||
const unresolved = [...input.subagents].sort(
|
||||
(left, right) => (left.meta?.spawnDepth ?? 1) - (right.meta?.spawnDepth ?? 1),
|
||||
);
|
||||
@@ -360,13 +367,14 @@ export function observeReplaySubagents(input: {
|
||||
|
||||
// Only proven descendants may own notifications; ambient sidecars cannot claim them.
|
||||
recordReplayToolOwners(toolOwners, subagent.entries, link.id);
|
||||
if (!ownerId) actionLogs.set(link.id, readClaudeSubagentActionLog(subagent.entries));
|
||||
observations.push(...observeSubagent(subagent, parent, input.convertEntry, ownerId));
|
||||
if (subagent.parentFacts) resolvedParents.set(link.id, subagent.parentFacts);
|
||||
unresolved.splice(index, 1);
|
||||
madeProgress = true;
|
||||
}
|
||||
}
|
||||
return { observations, toolOwners };
|
||||
return { observations, toolOwners, actionLogs };
|
||||
}
|
||||
|
||||
function resolveReplayOwner(
|
||||
|
||||
Reference in New Issue
Block a user