Fix Claude subagent narration leaking into chat (#1807)
* fix(claude): keep subagent text out of parent transcript Sidechain tool activity is folded into the parent Task/Agent row, but sidechain assistant text was still going through the top-level text assembler. That made the chat show subagent narration without its matching tool rows. * test(claude): simplify subagent sidechain assertion
This commit is contained in:
@@ -211,6 +211,20 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
|
||||
parent_tool_use_id: "task-call-1",
|
||||
elapsed_time_seconds: 1,
|
||||
},
|
||||
{
|
||||
type: "assistant",
|
||||
parent_tool_use_id: "task-call-1",
|
||||
message: {
|
||||
id: "subagent-message-1",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Sub-agent narration belongs inside the Task row, not the parent transcript.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "assistant",
|
||||
parent_tool_use_id: null,
|
||||
@@ -295,6 +309,46 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
|
||||
expect(projectedTaskCalls).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("keeps sidechain assistant text out of the parent transcript", async () => {
|
||||
const session = await new ClaudeAgentClient({
|
||||
logger,
|
||||
queryFactory,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
}).createSession({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
const events = await collectUntilTerminal(streamSession(session, "delegate work"));
|
||||
await session.close();
|
||||
|
||||
const visibleAssistantText = events
|
||||
.flatMap((event) =>
|
||||
event.type === "timeline" && event.item.type === "assistant_message"
|
||||
? [event.item.text]
|
||||
: [],
|
||||
)
|
||||
.join("");
|
||||
|
||||
expect(visibleAssistantText).not.toContain("Sub-agent narration");
|
||||
|
||||
const latestSubAgentUpdate = events
|
||||
.filter(
|
||||
(event): event is Extract<AgentStreamEvent, { type: "timeline" }> =>
|
||||
event.type === "timeline" &&
|
||||
event.item.type === "tool_call" &&
|
||||
event.item.callId === "task-call-1" &&
|
||||
event.item.detail.type === "sub_agent",
|
||||
)
|
||||
.map((event) => event.item)
|
||||
.at(-1);
|
||||
|
||||
expect(latestSubAgentUpdate?.detail).toMatchObject({
|
||||
type: "sub_agent",
|
||||
log: expect.stringContaining("[Read] README.md"),
|
||||
});
|
||||
});
|
||||
|
||||
test("tails sub-agent actions instead of dropping latest entries at cap", async () => {
|
||||
queryFactory.mockImplementation(() => buildQueryMock(buildTailScenarioEvents(205)));
|
||||
|
||||
|
||||
@@ -1731,6 +1731,14 @@ function isClaudeSubagentToolName(name: string | undefined): boolean {
|
||||
return name === "Task" || name === "Agent";
|
||||
}
|
||||
|
||||
function readClaudeParentToolUseId(message: SDKMessage): string | null {
|
||||
if (!("parent_tool_use_id" in message)) {
|
||||
return null;
|
||||
}
|
||||
const parentToolUseId = (message as { parent_tool_use_id?: unknown }).parent_tool_use_id;
|
||||
return typeof parentToolUseId === "string" && parentToolUseId.length > 0 ? parentToolUseId : null;
|
||||
}
|
||||
|
||||
class ClaudeContextUsageState {
|
||||
private contextWindowMaxTokens: number | undefined;
|
||||
private streamRequestInputTokens: number | undefined;
|
||||
@@ -3495,20 +3503,22 @@ class ClaudeAgentSession implements AgentSession {
|
||||
suppressAssistantText: true,
|
||||
suppressReasoning: true,
|
||||
});
|
||||
const assistantTimelineEvents = this.timelineAssembler
|
||||
.consume({
|
||||
message,
|
||||
runId: turnId,
|
||||
messageIdHint,
|
||||
})
|
||||
.map(
|
||||
(item) =>
|
||||
({
|
||||
type: "timeline",
|
||||
item,
|
||||
provider: "claude",
|
||||
}) satisfies AgentStreamEvent,
|
||||
);
|
||||
const assistantTimelineEvents = readClaudeParentToolUseId(message)
|
||||
? []
|
||||
: this.timelineAssembler
|
||||
.consume({
|
||||
message,
|
||||
runId: turnId,
|
||||
messageIdHint,
|
||||
})
|
||||
.map(
|
||||
(item) =>
|
||||
({
|
||||
type: "timeline",
|
||||
item,
|
||||
provider: "claude",
|
||||
}) satisfies AgentStreamEvent,
|
||||
);
|
||||
|
||||
return [...messageEvents, ...assistantTimelineEvents];
|
||||
}
|
||||
@@ -3582,10 +3592,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
suppressReasoning?: boolean;
|
||||
},
|
||||
): AgentStreamEvent[] {
|
||||
const parentToolUseId =
|
||||
"parent_tool_use_id" in message
|
||||
? (message as { parent_tool_use_id: string | null }).parent_tool_use_id
|
||||
: null;
|
||||
const parentToolUseId = readClaudeParentToolUseId(message);
|
||||
if (parentToolUseId) {
|
||||
return this.sidechainTracker.handleMessage(message, parentToolUseId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user