Skip to content

Commit 4ec2727

Browse files
fix: collect tool results from subagent messages with absent isMeta field (#23)
User messages in subagent JSONLs lack the isMeta field, defaulting to false. An unconditional `continue` in the !isMeta branch skipped tool result collection for these messages, causing all subagent tools to show "No result received". Now we check for tool_result blocks before continuing, allowing them to fall through to the result collection logic.
1 parent 94f722d commit 4ec2727

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

src/renderer/utils/displayItemBuilder.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,16 +423,20 @@ export function buildDisplayItemsFromMessages(
423423
}
424424
continue;
425425
}
426-
// Plain-text user message (subagent input prompt)
427-
if (rawText.trim()) {
426+
// Only treat as subagent input if there are NO tool_result blocks in this message
427+
const hasToolResults =
428+
Array.isArray(msg.content) &&
429+
msg.content.some((b) => b.type === 'tool_result');
430+
if (rawText.trim() && !hasToolResults) {
428431
displayItems.push({
429432
type: 'subagent_input',
430433
content: rawText.trim(),
431434
timestamp: msgTimestamp,
432435
tokenCount: estimateTokens(rawText),
433436
});
437+
continue;
434438
}
435-
continue;
439+
// Fall through to tool result processing below if message has tool_results
436440
}
437441

438442
if (msg.type === 'assistant' && Array.isArray(msg.content)) {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { buildDisplayItemsFromMessages } from '../../../src/renderer/utils/displayItemBuilder';
3+
import type { ParsedMessage } from '../../../src/main/types/messages';
4+
5+
/**
6+
* Helper to create a minimal ParsedMessage for testing.
7+
*/
8+
function makeMessage(overrides: Partial<ParsedMessage> & Pick<ParsedMessage, 'type' | 'content'>): ParsedMessage {
9+
return {
10+
uuid: `msg-${Math.random().toString(36).slice(2, 8)}`,
11+
parentUuid: null,
12+
timestamp: new Date('2025-01-01T00:00:00Z'),
13+
isMeta: false,
14+
isSidechain: false,
15+
toolCalls: [],
16+
toolResults: [],
17+
...overrides,
18+
} as ParsedMessage;
19+
}
20+
21+
describe('buildDisplayItemsFromMessages', () => {
22+
describe('subagent tool results with isMeta=false', () => {
23+
it('should collect tool results from user messages without isMeta field', () => {
24+
// Simulates real subagent JSONL where user messages with tool_result
25+
// blocks have isMeta absent (defaults to false after parsing).
26+
const toolUseId = 'toolu_test123';
27+
28+
const assistantMsg = makeMessage({
29+
uuid: 'assistant-1',
30+
type: 'assistant',
31+
content: [
32+
{
33+
type: 'tool_use',
34+
id: toolUseId,
35+
name: 'Bash',
36+
input: { command: 'echo hello' },
37+
},
38+
],
39+
timestamp: new Date('2025-01-01T00:00:00Z'),
40+
});
41+
42+
// This is the key scenario: user message with tool_result but isMeta: false
43+
// (simulating subagent JSONL where isMeta field is absent)
44+
const toolResultMsg = makeMessage({
45+
uuid: 'user-result-1',
46+
type: 'user',
47+
isMeta: false,
48+
content: [
49+
{
50+
type: 'tool_result',
51+
tool_use_id: toolUseId,
52+
content: 'hello\n',
53+
is_error: false,
54+
},
55+
],
56+
toolResults: [
57+
{
58+
toolUseId: toolUseId,
59+
content: 'hello\n',
60+
isError: false,
61+
},
62+
],
63+
timestamp: new Date('2025-01-01T00:00:01Z'),
64+
});
65+
66+
const items = buildDisplayItemsFromMessages([assistantMsg, toolResultMsg], []);
67+
68+
const toolItems = items.filter((item) => item.type === 'tool');
69+
expect(toolItems).toHaveLength(1);
70+
71+
const tool = toolItems[0];
72+
if (tool.type !== 'tool') throw new Error('Expected tool item');
73+
74+
// The critical assertion: result must be present, not orphaned
75+
expect(tool.tool.isOrphaned).toBe(false);
76+
expect(tool.tool.result).toBeDefined();
77+
expect(tool.tool.result?.content).toBe('hello\n');
78+
expect(tool.tool.name).toBe('Bash');
79+
});
80+
81+
it('should still render subagent_input for plain text user messages without tool results', () => {
82+
const userMsg = makeMessage({
83+
uuid: 'user-input-1',
84+
type: 'user',
85+
isMeta: false,
86+
content: 'Please run the tests',
87+
toolResults: [],
88+
timestamp: new Date('2025-01-01T00:00:00Z'),
89+
});
90+
91+
const items = buildDisplayItemsFromMessages([userMsg], []);
92+
93+
const inputItems = items.filter((item) => item.type === 'subagent_input');
94+
expect(inputItems).toHaveLength(1);
95+
if (inputItems[0].type !== 'subagent_input') throw new Error('Expected subagent_input');
96+
expect(inputItems[0].content).toBe('Please run the tests');
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)