fix: stop a stream the fold cannot use from claiming the run pane

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-17 10:31:06 +02:00
co-authored by Claude Opus 5
parent e1ab81ba72
commit 7e5809acd2
2 changed files with 27 additions and 14 deletions
@@ -86,6 +86,9 @@ describe('agent stream', () => {
// A provider that does not filter its empty deltas opens with one, and the
// run is an agent's all the same.
expect(isAgentStream('{"type":"token_delta","content":""}\n')).toBe(true)
// What the fold cannot use must not claim the pane either, or the script's
// own output is replaced by a view with nothing to draw.
expect(isAgentStream('{"type":"tool_call","function_name":"q"}\n')).toBe(false)
})
it('folds the token deltas into the answer so far', () => {
+24 -14
View File
@@ -235,6 +235,24 @@ const TOOL_TURN_STARTED: AgentStreamEvent['type'][] = [
'tool_execution'
]
/**
* Whether an event carries what its type declares: a delta its text, a tool event
* the `call_id` its row is keyed on and the name that labels it. `result_stream` is
* whatever the job wrote, so detection and the fold ask the same question — were
* they to differ, a stream would be claimed and then render nothing.
*/
function isWellFormedEvent(event: AgentStreamEvent): boolean {
if (event.type === 'token_delta' || event.type === 'reasoning_token_delta') {
return typeof event.content === 'string'
}
return (
typeof event.call_id === 'string' &&
event.call_id !== '' &&
typeof event.function_name === 'string' &&
event.function_name !== ''
)
}
/**
* Whether `result_stream` is an agent's event stream rather than something a
* script printed. Reads only the first complete line, because it runs on every
@@ -250,7 +268,7 @@ export function isAgentStream(raw: string): boolean {
}
const line = raw.slice(start, end)
if (line.trim() !== '') {
return parseStreamEvents(line).length > 0
return parseStreamEvents(line).some(isWellFormedEvent)
}
start = end + 1
}
@@ -276,24 +294,16 @@ export function advanceAgentStream(
return previous
}
const stream: AgentStream = { ...previous.stream, entries: [...previous.stream.entries] }
// `result_stream` is whatever the job wrote and the parser keeps any line whose
// `type` it knows, so an event can arrive without the fields its type promises.
// A delta with no text would append "undefined", and a tool event keyed on
// nothing would draw an unlabelled card every later nameless event joins.
for (const event of parseStreamEvents(raw.slice(previous.consumed, complete))) {
if (!isWellFormedEvent(event)) {
continue
}
if (event.type === 'token_delta') {
if (typeof event.content === 'string') {
stream.current += event.content
}
stream.current += event.content
continue
}
if (event.type === 'reasoning_token_delta') {
if (typeof event.content === 'string') {
stream.reasoning += event.content
}
continue
}
if (typeof event.call_id !== 'string' || typeof event.function_name !== 'string') {
stream.reasoning += event.content
continue
}
if (TOOL_TURN_STARTED.includes(event.type)) {