Skip to content
Open
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
12 changes: 12 additions & 0 deletions src/agent/claude/stream-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface ContentBlock {
interface ClaudeRawEvent {
type?: string;
subtype?: string;
/** Parent tool ID for nested messages; null on top-level messages. */
parent_tool_use_id?: string | null;
session_id?: string;
cwd?: string;
model?: string;
Expand All @@ -31,6 +33,9 @@ export function* translateEvent(raw: unknown): Generator<AgentEvent> {
if (!raw || typeof raw !== 'object') return;
const evt = raw as ClaudeRawEvent;

// Nested assistant/user messages are internal and must not reach Lark.
if (isNestedMessage(evt)) return;

if (evt.type === 'system' && evt.subtype === 'init') {
yield {
type: 'system',
Expand Down Expand Up @@ -83,3 +88,10 @@ export function* translateEvent(raw: unknown): Generator<AgentEvent> {
yield { type: 'done', sessionId: evt.session_id, terminationReason: 'normal' };
}
}

function isNestedMessage(evt: ClaudeRawEvent): boolean {
return (
(evt.type === 'assistant' || evt.type === 'user') &&
typeof evt.parent_tool_use_id === 'string'
);
}
64 changes: 64 additions & 0 deletions tests/unit/agent/claude-stream-json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,70 @@ describe('Claude stream-json translator', () => {
]);
});

it('filters nested assistant/user messages by their explicit parent tool-use ID', () => {
const nestedAssistant = {
type: 'assistant',
parent_tool_use_id: 'tool-agent-1',
message: {
content: [
{ type: 'text', text: 'private research report' },
{ type: 'thinking', thinking: 'private reasoning' },
{ type: 'tool_use', id: 'tool-child-1', name: 'WebSearch', input: { query: 'x' } },
],
},
};
const nestedToolResult = {
type: 'user',
parent_tool_use_id: 'tool-agent-1',
message: {
content: [
{ type: 'tool_result', tool_use_id: 'tool-child-1', content: 'private result' },
],
},
};

expect([...translateEvent(nestedAssistant)]).toEqual([]);
expect([...translateEvent(nestedToolResult)]).toEqual([]);
});

it('preserves top-level messages and lifecycle events', () => {
expect([
...translateEvent({
type: 'assistant',
parent_tool_use_id: null,
message: { content: [{ type: 'text', text: 'public synthesis' }] },
}),
]).toEqual([{ type: 'text', delta: 'public synthesis' }]);
expect([
...translateEvent({
type: 'user',
parent_tool_use_id: null,
message: {
content: [
{ type: 'tool_result', tool_use_id: 'tool-agent-1', content: 'agent report' },
],
},
}),
]).toEqual([
{ type: 'tool_result', id: 'tool-agent-1', output: 'agent report', isError: false },
]);
expect([
...translateEvent({
type: 'system',
subtype: 'init',
parent_tool_use_id: 'unexpected',
session_id: 'sess-1',
}),
]).toEqual([{ type: 'system', sessionId: 'sess-1', cwd: undefined, model: undefined }]);
expect([
...translateEvent({
type: 'result',
parent_tool_use_id: 'unexpected',
session_id: 'sess-1',
}),
]).toEqual([{ type: 'done', sessionId: 'sess-1', terminationReason: 'normal' }]);
});

it('translates user tool_result blocks including structured output and errors', () => {
expect([
...translateEvent({
Expand Down