fold sub-agent prompts via parent_tool_use_id (T-338)

In live stream-json sessions a sub-agent's spawning prompt is tagged
with parent_tool_use_id, not the transcript JSONL's isSidechain +
parentUuid. The parser ignored that field, so the prompt parsed as a
main-thread user turn and rendered as a blue "you" card above the
Activity Agent card instead of folding into it.

Carry parent_tool_use_id onto ConversationItem; its presence now marks
the item as a sidechain message. The sidechain fold resolves ownership
directly by tool-use id (no transcript-only uuid chain to walk), so the
prompt folds into its Agent card and the run nests under it as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 16:03:50 +02:00
co-authored by Claude Opus 4.8
parent 026e331146
commit dea3e70e49
7 changed files with 113 additions and 7 deletions
@@ -378,6 +378,30 @@ void main() {
expect(items[0].parentUuid, 'msg-A');
expect(items[1].parentUuid, isNull); // '' → null
});
test('stream-json parent_tool_use_id marks a sidechain message (T-338)', () {
// The stream-json wire tags sub-agent messages with parent_tool_use_id and
// NO isSidechain flag — we must treat it as a sidechain item anyway.
final raw = envelope(
type: 'user',
uuid: 'sp',
message: {'role': 'user', 'content': 'go explore the codebase'},
)..['parent_tool_use_id'] = 'toolu_task1';
final items = parseAll([raw]);
expect(items.first.isSidechain, isTrue);
expect(items.first.parentToolUseId, 'toolu_task1');
});
test('empty parent_tool_use_id normalises to null and stays main-thread (T-338)', () {
final raw = envelope(
type: 'user',
uuid: 'mt',
message: {'role': 'user', 'content': 'a normal turn'},
)..['parent_tool_use_id'] = '';
final items = parseAll([raw]);
expect(items.first.isSidechain, isFalse);
expect(items.first.parentToolUseId, isNull);
});
});
// -------------------------------------------------------------------------