Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5362,8 +5362,55 @@ export function makeClaudeAdapterV2(
context.toolCalls.delete(toolCall.nativeItemId);
}

const assistantText = assistantTextFromSdkMessage(message);
const assistantParentToolUseId = parentToolUseIdFromSdkMessage(message);
if (message.type === "assistant" && assistantParentToolUseId !== null) {
// Claude never streams subagent output, so each thinking block
// arrives whole in its own snapshot and lands in the child thread.
const thinking = message.message.content.flatMap((block) =>
block.type === "thinking" && block.thinking.trim().length > 0 ? [block.thinking] : [],
);
const subagent =
thinking.length === 0
? undefined
: yield* resolveSubagentByToolUseId(context, assistantParentToolUseId);
if (subagent !== undefined) {
const now = yield* DateTime.now;
for (const [index, text] of thinking.entries()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Adapters/ClaudeAdapterV2.ts:5378

Duplicate assistant snapshots with the same message.uuid emit the same derived reasoning item ID with a newly incremented nextChildItemOrdinal, so later text or tool items can sort before the thinking they followed. Deduplicate subagent reasoning snapshots before this loop, mirroring the root path's context.reasoning.snapshots handling.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts around line 5378:

Duplicate assistant snapshots with the same `message.uuid` emit the same derived reasoning item ID with a newly incremented `nextChildItemOrdinal`, so later text or tool items can sort before the thinking they followed. Deduplicate subagent reasoning snapshots before this loop, mirroring the root path's `context.reasoning.snapshots` handling.

const nativeItemId = `${message.uuid}:thinking:${index}`;
yield* emitProviderEvent({
type: "turn_item.updated",
driver: CLAUDE_PROVIDER,
turnItem: {
id: idAllocator.derive.turnItemFromProviderItem({
driver: CLAUDE_PROVIDER,
nativeItemId,
}),
threadId: subagent.childThreadId,
runId: null,
nodeId: subagent.childRootNodeId,
providerThreadId: null,
providerTurnId: null,
nativeItemRef: {
driver: CLAUDE_PROVIDER,
nativeId: nativeItemId,
strength: "strong",
},
parentItemId: null,
ordinal: ++subagent.nextChildItemOrdinal,
type: "reasoning",
title: "Thinking",
text,
streaming: false,
status: "completed",
startedAt: now,
completedAt: now,
updatedAt: now,
},
});
}
}
}
const assistantText = assistantTextFromSdkMessage(message);
if (
assistantText !== null &&
assistantText.text.length > 0 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,41 @@ export function assertClaudeBackgroundSubagentLifecycleOutput(
assert.isDefined(agentAChild);
assert.deepEqual(assistantTexts(agentAChild), ["A_FIRST", "A_SECOND"]);

// The recording's subagent thinking lands in each child thread, before the
// reply it led to, and never in the parent.
const thinkingOf = (child: OrchestrationV2ThreadProjection | undefined) =>
(child?.turnItems ?? []).flatMap((item) =>
item.type === "reasoning" && item.title === "Thinking" ? [item] : [],
);
const agentAThinking = thinkingOf(agentAChild);
assert.lengthOf(agentAThinking, 2, "both of Agent A's runs thought before replying");
assert.include(agentAThinking[0]?.text, '"A_FIRST"');
assert.include(agentAThinking[1]?.text, '"A_SECOND"');
for (const item of agentAThinking) {
assert.equal(item.status, "completed");
assert.isFalse(item.streaming);
assert.isNull(item.runId);
}
const agentAOrder = agentAChild.turnItems.flatMap((item) =>
item.type === "assistant_message"
? [item.text.trim()]
: item.type === "reasoning" && item.title === "Thinking"
? ["thinking"]
: [],
);
assert.deepEqual(agentAOrder, ["thinking", "A_FIRST", "thinking", "A_SECOND"]);
const agentBChild =
agentB?.childThreadId == null ? undefined : result.projections.get(agentB.childThreadId);
assert.lengthOf(thinkingOf(agentBChild), 1);
assert.include(thinkingOf(agentBChild)[0]?.text, "B_DONE");
const subagentThinking = new Set(
[...agentAThinking, ...thinkingOf(agentBChild)].map((item) => item.text),
);
assert.isFalse(
thinkingOf(projection).some((item) => subagentThinking.has(item.text)),
"subagent thinking leaked into the parent thread",
);

// Subagents appear in background_tasks_changed but never on the roster.
assert.isFalse(
result.domainEvents.some(
Expand Down
Loading