mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 22:23:07 +00:00
Compare 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 () => {
|
test("replays the subagent's own transcript onto its timeline", async () => {
|
||||||
writeSession({
|
writeSession({
|
||||||
parentLines: [taskToolUse(), taskToolResult()],
|
parentLines: [taskToolUse(), taskToolResult()],
|
||||||
|
|||||||
@@ -303,6 +303,8 @@ interface ClaudeReplayOwnership {
|
|||||||
interface ClaudeSubagentCardFacts {
|
interface ClaudeSubagentCardFacts {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/** The child's tool calls, one line each. Live, the sidechain tracker streams it instead. */
|
||||||
|
log?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PersistedTimelineEntry {
|
interface PersistedTimelineEntry {
|
||||||
@@ -5016,7 +5018,12 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
const replay = {
|
const replay = {
|
||||||
restoredIds: restoredProviderSubagentIds,
|
restoredIds: restoredProviderSubagentIds,
|
||||||
toolOwners: subagentReplay.toolOwners,
|
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;
|
if (observations.length === 0) return replay;
|
||||||
|
|
||||||
@@ -5846,7 +5853,7 @@ function buildClaudeSubagentCardDetail(
|
|||||||
type: "sub_agent",
|
type: "sub_agent",
|
||||||
...(facts.title ? { subAgentType: facts.title } : {}),
|
...(facts.title ? { subAgentType: facts.title } : {}),
|
||||||
...(facts.description ? { description: facts.description } : {}),
|
...(facts.description ? { description: facts.description } : {}),
|
||||||
log: "",
|
log: facts.log ?? "",
|
||||||
actions: [],
|
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 {
|
export class ClaudeSidechainTracker {
|
||||||
private readonly activeSidechains = new Map<string, SubAgentActivityState>();
|
private readonly activeSidechains = new Map<string, SubAgentActivityState>();
|
||||||
private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
|
private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined;
|
||||||
@@ -129,11 +209,7 @@ export class ClaudeSidechainTracker {
|
|||||||
type: "sub_agent",
|
type: "sub_agent",
|
||||||
...(state.subAgentType ? { subAgentType: state.subAgentType } : {}),
|
...(state.subAgentType ? { subAgentType: state.subAgentType } : {}),
|
||||||
...(state.description ? { description: state.description } : {}),
|
...(state.description ? { description: state.description } : {}),
|
||||||
log: state.actions
|
log: formatSubAgentActionLog(state.actions),
|
||||||
.map((action) =>
|
|
||||||
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
actions: [],
|
actions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,9 +374,9 @@ export class ClaudeSidechainTracker {
|
|||||||
parentToolUseId: string,
|
parentToolUseId: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
const taskInput = this.getToolInput(parentToolUseId);
|
const taskInput = this.getToolInput(parentToolUseId);
|
||||||
const nextName = this.normalizeSubAgentText(taskInput?.name);
|
const nextName = normalizeSubAgentText(taskInput?.name);
|
||||||
const nextSubAgentType = this.normalizeSubAgentText(taskInput?.subagent_type);
|
const nextSubAgentType = normalizeSubAgentText(taskInput?.subagent_type);
|
||||||
const nextDescription = this.normalizeSubAgentText(taskInput?.description);
|
const nextDescription = normalizeSubAgentText(taskInput?.description);
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
if (nextName && nextName !== state.name) {
|
if (nextName && nextName !== state.name) {
|
||||||
@@ -318,17 +394,6 @@ export class ClaudeSidechainTracker {
|
|||||||
return changed;
|
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(
|
private extractAssistantMessageActions(
|
||||||
message: Extract<SDKMessage, { type: "assistant" }>,
|
message: Extract<SDKMessage, { type: "assistant" }>,
|
||||||
): SubAgentActionCandidate[] {
|
): SubAgentActionCandidate[] {
|
||||||
@@ -338,15 +403,7 @@ export class ClaudeSidechainTracker {
|
|||||||
}
|
}
|
||||||
const actions: SubAgentActionCandidate[] = [];
|
const actions: SubAgentActionCandidate[] = [];
|
||||||
for (const block of content) {
|
for (const block of content) {
|
||||||
if (
|
if (!isClaudeContentChunk(block) || !isToolUseChunk(block)) {
|
||||||
!isClaudeContentChunk(block) ||
|
|
||||||
!(
|
|
||||||
block.type === "tool_use" ||
|
|
||||||
block.type === "mcp_tool_use" ||
|
|
||||||
block.type === "server_tool_use"
|
|
||||||
) ||
|
|
||||||
typeof block.name !== "string"
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const key = readTrimmedString(block.id) ?? `assistant:${block.name}:${actions.length}`;
|
const key = readTrimmedString(block.id) ?? `assistant:${block.name}:${actions.length}`;
|
||||||
@@ -367,15 +424,7 @@ export class ClaudeSidechainTracker {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
|
const block = isClaudeContentChunk(event.content_block) ? event.content_block : null;
|
||||||
if (
|
if (!block || !isToolUseChunk(block)) {
|
||||||
!block ||
|
|
||||||
!(
|
|
||||||
block.type === "tool_use" ||
|
|
||||||
block.type === "mcp_tool_use" ||
|
|
||||||
block.type === "server_tool_use"
|
|
||||||
) ||
|
|
||||||
typeof block.name !== "string"
|
|
||||||
) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const key =
|
const key =
|
||||||
@@ -420,7 +469,7 @@ export class ClaudeSidechainTracker {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = this.deriveSubAgentActionSummary(normalizedToolName, candidate.input);
|
const summary = deriveSubAgentActionSummary(normalizedToolName, candidate.input);
|
||||||
const existingIndex = state.actionIndexByKey.get(candidate.key);
|
const existingIndex = state.actionIndexByKey.get(candidate.key);
|
||||||
|
|
||||||
if (existingIndex !== undefined) {
|
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 { normalizeProviderReplayTimestamp } from "../../../provider-history-timestamps.js";
|
||||||
import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
|
import type { ProviderSubagentStatus } from "../../../provider-subagents/store.js";
|
||||||
import { resolveObservedClaudeModelId } from "../models.js";
|
import { resolveObservedClaudeModelId } from "../models.js";
|
||||||
|
import { readClaudeSubagentActionLog } from "../sidechain-tracker.js";
|
||||||
import type { SubagentObservation } from "./observation.js";
|
import type { SubagentObservation } from "./observation.js";
|
||||||
import { buildClaudeSubagentSubtitle, type ClaudeSubagentUsage } from "./presentation.js";
|
import { buildClaudeSubagentSubtitle, type ClaudeSubagentUsage } from "./presentation.js";
|
||||||
|
|
||||||
@@ -337,9 +338,15 @@ export function observeReplaySubagents(input: {
|
|||||||
subagents: readonly ClaudeReplaySubagentInput[];
|
subagents: readonly ClaudeReplaySubagentInput[];
|
||||||
parent: ClaudeReplayParentFacts;
|
parent: ClaudeReplayParentFacts;
|
||||||
convertEntry: (entry: ClaudeReplayEntry) => AgentTimelineItem[];
|
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 observations: SubagentObservation[] = [];
|
||||||
const toolOwners = new Map<string, string>();
|
const toolOwners = new Map<string, string>();
|
||||||
|
const actionLogs = new Map<string, string>();
|
||||||
const unresolved = [...input.subagents].sort(
|
const unresolved = [...input.subagents].sort(
|
||||||
(left, right) => (left.meta?.spawnDepth ?? 1) - (right.meta?.spawnDepth ?? 1),
|
(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.
|
// Only proven descendants may own notifications; ambient sidecars cannot claim them.
|
||||||
recordReplayToolOwners(toolOwners, subagent.entries, link.id);
|
recordReplayToolOwners(toolOwners, subagent.entries, link.id);
|
||||||
|
if (!ownerId) actionLogs.set(link.id, readClaudeSubagentActionLog(subagent.entries));
|
||||||
observations.push(...observeSubagent(subagent, parent, input.convertEntry, ownerId));
|
observations.push(...observeSubagent(subagent, parent, input.convertEntry, ownerId));
|
||||||
if (subagent.parentFacts) resolvedParents.set(link.id, subagent.parentFacts);
|
if (subagent.parentFacts) resolvedParents.set(link.id, subagent.parentFacts);
|
||||||
unresolved.splice(index, 1);
|
unresolved.splice(index, 1);
|
||||||
madeProgress = true;
|
madeProgress = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { observations, toolOwners };
|
return { observations, toolOwners, actionLogs };
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveReplayOwner(
|
function resolveReplayOwner(
|
||||||
|
|||||||
Reference in New Issue
Block a user