When a language model calls a tool, it does not hand you the call in one piece. It streams events: first that a tool named search is starting, then the argument JSON a few characters at a time, then a signal that the arguments are complete, and finally — after your code runs the tool — a result. Several tool calls can stream at once, interleaved, each tagged with its own id. Your job is to track them: route each event to the right call by id, accumulate its argument text, and move it through a small state machine.
Implement toolCallStreamingStateMachine(). It returns a controller with apply(event), get(id), and list(). Each tool call is { id, name, argsText, args, status, result, error } and moves through the states pending (arguments still streaming) → running (arguments complete, tool executing) → done (result in), with error as an escape hatch.
The events:
start { id, name } — announce a new call; it begins pending.delta { id, argsTextDelta } — append a fragment to that call's argsText.ready { id } — arguments are complete; parse argsText and move to running.result { id, result } — the tool returned; move to done.error { id, error } — the call failed.type ToolCall = { id: string; name: string; argsText: string; args?: unknown; status: 'pending' | 'running' | 'done' | 'error'; result?: unknown; error?: unknown };
function toolCallStreamingStateMachine(): {
apply(event: { type: string; id: string; name?: string; argsTextDelta?: string; result?: unknown; error?: unknown }): void;
get(id: string): ToolCall | undefined;
list(): ToolCall[];
};
const m = toolCallStreamingStateMachine();
m.apply({ type: 'start', id: 't1', name: 'search' });
m.apply({ type: 'delta', id: 't1', argsTextDelta: '{"q":' });
m.apply({ type: 'delta', id: 't1', argsTextDelta: '"hi"}' });
m.apply({ type: 'ready', id: 't1' });
m.get('t1'); // { id:'t1', name:'search', argsText:'{"q":"hi"}', args:{ q:'hi' }, status:'running' }
// Out-of-order events are ignored, not fatal.
m.apply({ type: 'start', id: 't2', name: 'now' });
m.apply({ type: 'result', id: 't2', result: 42 }); // too early — still pending
m.get('t2').status; // 'pending'
id; use it to find the right call. Concurrent calls must not mix.argsTextDelta as raw text while pending; only JSON.parse on ready.delta after ready, a result while pending, or an error after done should be ignored.argsText is not valid JSON on ready, the call becomes error.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
When a language model calls a tool, it does not hand you the call in one piece. It streams events: first that a tool named search is starting, then the argument JSON a few characters at a time, then a signal that the arguments are complete, and finally — after your code runs the tool — a result. Several tool calls can stream at once, interleaved, each tagged with its own id. Your job is to track them: route each event to the right call by id, accumulate its argument text, and move it through a small state machine.
Implement toolCallStreamingStateMachine(). It returns a controller with apply(event), get(id), and list(). Each tool call is { id, name, argsText, args, status, result, error } and moves through the states pending (arguments still streaming) → running (arguments complete, tool executing) → done (result in), with error as an escape hatch.
The events:
start { id, name } — announce a new call; it begins pending.delta { id, argsTextDelta } — append a fragment to that call's argsText.ready { id } — arguments are complete; parse argsText and move to running.result { id, result } — the tool returned; move to done.error { id, error } — the call failed.type ToolCall = { id: string; name: string; argsText: string; args?: unknown; status: 'pending' | 'running' | 'done' | 'error'; result?: unknown; error?: unknown };
function toolCallStreamingStateMachine(): {
apply(event: { type: string; id: string; name?: string; argsTextDelta?: string; result?: unknown; error?: unknown }): void;
get(id: string): ToolCall | undefined;
list(): ToolCall[];
};
const m = toolCallStreamingStateMachine();
m.apply({ type: 'start', id: 't1', name: 'search' });
m.apply({ type: 'delta', id: 't1', argsTextDelta: '{"q":' });
m.apply({ type: 'delta', id: 't1', argsTextDelta: '"hi"}' });
m.apply({ type: 'ready', id: 't1' });
m.get('t1'); // { id:'t1', name:'search', argsText:'{"q":"hi"}', args:{ q:'hi' }, status:'running' }
// Out-of-order events are ignored, not fatal.
m.apply({ type: 'start', id: 't2', name: 'now' });
m.apply({ type: 'result', id: 't2', result: 42 }); // too early — still pending
m.get('t2').status; // 'pending'
id; use it to find the right call. Concurrent calls must not mix.argsTextDelta as raw text while pending; only JSON.parse on ready.delta after ready, a result while pending, or an error after done should be ignored.argsText is not valid JSON on ready, the call becomes error.The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.