talon-agent 1.45.0 → 1.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -21,6 +21,13 @@ import {
|
|
|
21
21
|
export interface HandleEventContext {
|
|
22
22
|
state: StreamState;
|
|
23
23
|
seenToolCallIds: Set<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Tool item ids whose `item.started` was reported via [onToolStart] and
|
|
26
|
+
* still await their completion — the completion path must close them via
|
|
27
|
+
* [onToolEnd] (same id) instead of the collapsed [onToolUse] fallback.
|
|
28
|
+
* Owned by the caller so it spans the whole turn, like [seenToolCallIds].
|
|
29
|
+
*/
|
|
30
|
+
startedToolIds: Set<string>;
|
|
24
31
|
codexToolMetrics: { count: number };
|
|
25
32
|
onTextBlock?: (text: string) => Promise<void>;
|
|
26
33
|
onToolUse?: (
|
|
@@ -28,6 +35,16 @@ export interface HandleEventContext {
|
|
|
28
35
|
input: Record<string, unknown>,
|
|
29
36
|
meta?: { failed?: boolean },
|
|
30
37
|
) => void;
|
|
38
|
+
onToolStart?: (
|
|
39
|
+
callId: string,
|
|
40
|
+
toolName: string,
|
|
41
|
+
input: Record<string, unknown>,
|
|
42
|
+
) => void;
|
|
43
|
+
onToolEnd?: (
|
|
44
|
+
callId: string,
|
|
45
|
+
toolName: string,
|
|
46
|
+
meta?: { failed?: boolean },
|
|
47
|
+
) => void;
|
|
31
48
|
chatId: string;
|
|
32
49
|
}
|
|
33
50
|
|
|
@@ -36,12 +53,105 @@ export interface HandleEventContext {
|
|
|
36
53
|
*
|
|
37
54
|
* Synchronous — keeps the for-await loop simple. The shared
|
|
38
55
|
* `routeDelivery` step at end-of-turn handles the final emit.
|
|
56
|
+
*
|
|
57
|
+
* `item.started` is observed ONLY to open the live tool window for
|
|
58
|
+
* consumers ([onToolStart] → UI spinner + real durations); every
|
|
59
|
+
* side effect that matters — metrics, terminator detection, delivered-
|
|
60
|
+
* text capture — stays strictly on `item.completed`, preserving the
|
|
61
|
+
* in_progress race fix documented on [handleMcpToolCall].
|
|
39
62
|
*/
|
|
40
63
|
export function handleEvent(event: ThreadEvent, ctx: HandleEventContext): void {
|
|
64
|
+
if (event.type === "item.started") {
|
|
65
|
+
handleItemStarted(event.item, ctx);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
41
68
|
if (event.type !== "item.completed") return;
|
|
42
69
|
handleItem(event.item, ctx);
|
|
43
70
|
}
|
|
44
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Map a tool-shaped [ThreadItem] to its fleet-vocabulary name + input,
|
|
74
|
+
* or null for non-tool items (agent_message / reasoning / todo_list /
|
|
75
|
+
* error). Single source of truth for the started + completed paths so
|
|
76
|
+
* the pair always reports the same name.
|
|
77
|
+
*/
|
|
78
|
+
function toolShape(
|
|
79
|
+
item: ThreadItem,
|
|
80
|
+
): { name: string; input: Record<string, unknown> } | null {
|
|
81
|
+
switch (item.type) {
|
|
82
|
+
case "mcp_tool_call":
|
|
83
|
+
return {
|
|
84
|
+
name: item.tool,
|
|
85
|
+
input:
|
|
86
|
+
item.arguments && typeof item.arguments === "object"
|
|
87
|
+
? (item.arguments as Record<string, unknown>)
|
|
88
|
+
: {},
|
|
89
|
+
};
|
|
90
|
+
case "command_execution":
|
|
91
|
+
return { name: "Bash", input: { command: item.command } };
|
|
92
|
+
case "file_change":
|
|
93
|
+
return {
|
|
94
|
+
name: fileChangeToolName(item.changes),
|
|
95
|
+
input: { changes: item.changes },
|
|
96
|
+
};
|
|
97
|
+
case "web_search":
|
|
98
|
+
return { name: "WebSearch", input: { query: item.query } };
|
|
99
|
+
case "agent_message":
|
|
100
|
+
case "reasoning":
|
|
101
|
+
case "todo_list":
|
|
102
|
+
case "error":
|
|
103
|
+
return null;
|
|
104
|
+
default: {
|
|
105
|
+
const type =
|
|
106
|
+
typeof (item as { type?: unknown }).type === "string"
|
|
107
|
+
? (item as { type: string }).type
|
|
108
|
+
: "unknown";
|
|
109
|
+
return { name: type, input: nativeItemPayload(item) };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleItemStarted(item: ThreadItem, ctx: HandleEventContext): void {
|
|
115
|
+
if (!ctx.onToolStart) return;
|
|
116
|
+
const shape = toolShape(item);
|
|
117
|
+
if (!shape || ctx.startedToolIds.has(item.id)) return;
|
|
118
|
+
ctx.startedToolIds.add(item.id);
|
|
119
|
+
try {
|
|
120
|
+
ctx.onToolStart(item.id, shape.name, shape.input);
|
|
121
|
+
} catch {
|
|
122
|
+
/* non-fatal */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Close the live tool window for [item] if its start was reported,
|
|
128
|
+
* falling back to the collapsed one-shot [onToolUse] when it wasn't
|
|
129
|
+
* (older CLIs that never emit `item.started`, or no start listener).
|
|
130
|
+
*/
|
|
131
|
+
function reportToolTerminal(
|
|
132
|
+
ctx: HandleEventContext,
|
|
133
|
+
item: { id: string },
|
|
134
|
+
toolName: string,
|
|
135
|
+
input: Record<string, unknown>,
|
|
136
|
+
meta?: { failed?: boolean },
|
|
137
|
+
): void {
|
|
138
|
+
if (ctx.startedToolIds.delete(item.id) && ctx.onToolEnd) {
|
|
139
|
+
try {
|
|
140
|
+
ctx.onToolEnd(item.id, toolName, meta);
|
|
141
|
+
} catch {
|
|
142
|
+
/* non-fatal */
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (ctx.onToolUse) {
|
|
147
|
+
try {
|
|
148
|
+
ctx.onToolUse(toolName, input, meta);
|
|
149
|
+
} catch {
|
|
150
|
+
/* non-fatal */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
45
155
|
function handleItem(item: ThreadItem, ctx: HandleEventContext): void {
|
|
46
156
|
switch (item.type) {
|
|
47
157
|
case "agent_message":
|
|
@@ -56,7 +166,7 @@ function handleItem(item: ThreadItem, ctx: HandleEventContext): void {
|
|
|
56
166
|
// backend instead of splitting the same activity across
|
|
57
167
|
// `tool_calls.command_execution` vs `tool_calls.Bash`.
|
|
58
168
|
case "command_execution":
|
|
59
|
-
handleNativeCodexTool(ctx, "Bash", {
|
|
169
|
+
handleNativeCodexTool(ctx, item, "Bash", {
|
|
60
170
|
command: item.command,
|
|
61
171
|
status: item.status,
|
|
62
172
|
...(typeof item.exit_code === "number"
|
|
@@ -65,13 +175,13 @@ function handleItem(item: ThreadItem, ctx: HandleEventContext): void {
|
|
|
65
175
|
});
|
|
66
176
|
return;
|
|
67
177
|
case "file_change":
|
|
68
|
-
handleNativeCodexTool(ctx, fileChangeToolName(item.changes), {
|
|
178
|
+
handleNativeCodexTool(ctx, item, fileChangeToolName(item.changes), {
|
|
69
179
|
status: item.status,
|
|
70
180
|
changes: item.changes,
|
|
71
181
|
});
|
|
72
182
|
return;
|
|
73
183
|
case "web_search":
|
|
74
|
-
handleNativeCodexTool(ctx, "WebSearch", { query: item.query });
|
|
184
|
+
handleNativeCodexTool(ctx, item, "WebSearch", { query: item.query });
|
|
75
185
|
return;
|
|
76
186
|
case "reasoning":
|
|
77
187
|
case "todo_list":
|
|
@@ -88,7 +198,7 @@ function handleItem(item: ThreadItem, ctx: HandleEventContext): void {
|
|
|
88
198
|
typeof (item as { type?: unknown }).type === "string"
|
|
89
199
|
? (item as { type: string }).type
|
|
90
200
|
: "unknown";
|
|
91
|
-
handleNativeCodexTool(ctx, type, nativeItemPayload(item));
|
|
201
|
+
handleNativeCodexTool(ctx, item, type, nativeItemPayload(item));
|
|
92
202
|
return;
|
|
93
203
|
}
|
|
94
204
|
}
|
|
@@ -150,25 +260,12 @@ function handleMcpToolCall(
|
|
|
150
260
|
// nothing was delivered and the turn is not terminated, so skip
|
|
151
261
|
// the stream-state mutations (delivered-text capture / terminator
|
|
152
262
|
// flip) that assume a successful call.
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
ctx.onToolUse(toolName, input, { failed: true });
|
|
156
|
-
} catch {
|
|
157
|
-
/* non-fatal */
|
|
158
|
-
}
|
|
159
|
-
}
|
|
263
|
+
reportToolTerminal(ctx, item, toolName, input, { failed: true });
|
|
160
264
|
return;
|
|
161
265
|
}
|
|
162
266
|
|
|
163
267
|
recordToolUse(ctx.state, toolName, input);
|
|
164
|
-
|
|
165
|
-
if (ctx.onToolUse) {
|
|
166
|
-
try {
|
|
167
|
-
ctx.onToolUse(toolName, input);
|
|
168
|
-
} catch {
|
|
169
|
-
/* non-fatal */
|
|
170
|
-
}
|
|
171
|
-
}
|
|
268
|
+
reportToolTerminal(ctx, item, toolName, input);
|
|
172
269
|
|
|
173
270
|
if (!ctx.state.turnTerminated && isTurnTerminator(toolName, input)) {
|
|
174
271
|
ctx.state.turnTerminated = true;
|
|
@@ -192,17 +289,12 @@ function recordCodexToolMetric(
|
|
|
192
289
|
|
|
193
290
|
function handleNativeCodexTool(
|
|
194
291
|
ctx: HandleEventContext,
|
|
292
|
+
item: { id: string },
|
|
195
293
|
toolName: string,
|
|
196
294
|
input: Record<string, unknown>,
|
|
197
295
|
): void {
|
|
198
296
|
recordCodexToolMetric(ctx, toolName);
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
ctx.onToolUse(toolName, input);
|
|
202
|
-
} catch {
|
|
203
|
-
/* non-fatal */
|
|
204
|
-
}
|
|
205
|
-
}
|
|
297
|
+
reportToolTerminal(ctx, item, toolName, input);
|
|
206
298
|
}
|
|
207
299
|
|
|
208
300
|
/**
|
|
@@ -203,6 +203,8 @@ export async function handleMessage(
|
|
|
203
203
|
messageId,
|
|
204
204
|
onTextBlock,
|
|
205
205
|
onToolUse,
|
|
206
|
+
onToolStart,
|
|
207
|
+
onToolEnd,
|
|
206
208
|
} = params;
|
|
207
209
|
const t0 = Date.now();
|
|
208
210
|
const session = getSession(chatId);
|
|
@@ -316,6 +318,7 @@ export async function handleMessage(
|
|
|
316
318
|
// into the live-turn overlay — /status updates while the turn runs.
|
|
317
319
|
const streamState = createStreamState(chatId);
|
|
318
320
|
const seenToolCallIds = new Set<string>();
|
|
321
|
+
const startedToolIds = new Set<string>();
|
|
319
322
|
const codexToolMetrics = { count: 0 };
|
|
320
323
|
const abortController = new AbortController();
|
|
321
324
|
activeAborts.set(chatId, abortController);
|
|
@@ -440,9 +443,12 @@ export async function handleMessage(
|
|
|
440
443
|
handleEvent(event, {
|
|
441
444
|
state: streamState,
|
|
442
445
|
seenToolCallIds,
|
|
446
|
+
startedToolIds,
|
|
443
447
|
codexToolMetrics,
|
|
444
448
|
onTextBlock,
|
|
445
449
|
onToolUse,
|
|
450
|
+
onToolStart,
|
|
451
|
+
onToolEnd,
|
|
446
452
|
chatId,
|
|
447
453
|
});
|
|
448
454
|
|
|
@@ -63,6 +63,12 @@ export async function* handlerToEvents(
|
|
|
63
63
|
r?.();
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
+
// Tool calls announced via `onToolStart` that have not yet reported an
|
|
67
|
+
// `onToolEnd` — flushed with an error result if the handler settles with
|
|
68
|
+
// any still open (abort/crash mid-tool), preserving the call→result
|
|
69
|
+
// pairing contract for every consumer.
|
|
70
|
+
const openToolCalls = new Map<string, string>();
|
|
71
|
+
|
|
66
72
|
// Handler `onStreamDelta` delivers the FULL accumulated text so far,
|
|
67
73
|
// not the new chunk. `AgentEvent.text_delta.text` carries the delta —
|
|
68
74
|
// event-native consumers (the frontends, via the dispatcher's
|
|
@@ -109,7 +115,7 @@ export async function* handlerToEvents(
|
|
|
109
115
|
.toString(36)
|
|
110
116
|
.slice(2, 8)}`;
|
|
111
117
|
emit({ type: "tool_call", id, name: toolName, input });
|
|
112
|
-
// Callback backends (
|
|
118
|
+
// Callback backends (OpenCode, Kilo, OpenAI Agents) surface a
|
|
113
119
|
// tool call only at terminal status — by the time onToolUse fires,
|
|
114
120
|
// the tool has already finished. Emit the matching `tool_result`
|
|
115
121
|
// immediately so consumers see the same call→result contract the
|
|
@@ -123,6 +129,23 @@ export async function* handlerToEvents(
|
|
|
123
129
|
...(meta?.failed ? { error: "tool call failed" } : {}),
|
|
124
130
|
});
|
|
125
131
|
},
|
|
132
|
+
// Live tool lifecycle (Codex `item.started` → `item.completed`):
|
|
133
|
+
// `tool_call` goes out the moment the SDK dispatches the tool and the
|
|
134
|
+
// matching `tool_result` when it settles, so consumers see a real
|
|
135
|
+
// running window instead of the collapsed 0ms call+result above.
|
|
136
|
+
onToolStart: (callId, toolName, input) => {
|
|
137
|
+
openToolCalls.set(callId, toolName);
|
|
138
|
+
emit({ type: "tool_call", id: callId, name: toolName, input });
|
|
139
|
+
},
|
|
140
|
+
onToolEnd: (callId, toolName, meta) => {
|
|
141
|
+
if (!openToolCalls.delete(callId)) return; // unknown/duplicate id
|
|
142
|
+
emit({
|
|
143
|
+
type: "tool_result",
|
|
144
|
+
id: callId,
|
|
145
|
+
name: toolName,
|
|
146
|
+
...(meta?.failed ? { error: "tool call failed" } : {}),
|
|
147
|
+
});
|
|
148
|
+
},
|
|
126
149
|
};
|
|
127
150
|
|
|
128
151
|
let result: QueryResult | undefined;
|
|
@@ -158,6 +181,13 @@ export async function* handlerToEvents(
|
|
|
158
181
|
}
|
|
159
182
|
await handlerPromise;
|
|
160
183
|
|
|
184
|
+
// Resolve any tool that started but never settled (abort or crash killed
|
|
185
|
+
// the subprocess mid-call) so spinners close instead of hanging forever.
|
|
186
|
+
for (const [id, name] of openToolCalls) {
|
|
187
|
+
yield { type: "tool_result", id, name, error: "tool did not complete" };
|
|
188
|
+
}
|
|
189
|
+
openToolCalls.clear();
|
|
190
|
+
|
|
161
191
|
if (error) {
|
|
162
192
|
yield { type: "error", error: classifiedToAgentError(classify(error)) };
|
|
163
193
|
return;
|
|
@@ -48,12 +48,34 @@ export type QueryParams = {
|
|
|
48
48
|
* their SDKs surface it at (or after) terminal status, with no separate
|
|
49
49
|
* start/finish pair. `meta.failed` marks a call whose terminal status was
|
|
50
50
|
* an error, so consumers can render it as failed rather than successful.
|
|
51
|
+
*
|
|
52
|
+
* Backends whose SDK exposes a live start signal should prefer the
|
|
53
|
+
* `onToolStart` / `onToolEnd` pair below and only fall back to this
|
|
54
|
+
* for calls whose start was never observed.
|
|
51
55
|
*/
|
|
52
56
|
onToolUse?: (
|
|
53
57
|
toolName: string,
|
|
54
58
|
input: Record<string, unknown>,
|
|
55
59
|
meta?: { failed?: boolean },
|
|
56
60
|
) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Live tool lifecycle (backends with start signals — Codex emits
|
|
63
|
+
* `item.started`). `callId` is the SDK's stable item id: the same id
|
|
64
|
+
* must be passed to `onToolEnd` so consumers can pair the two and
|
|
65
|
+
* measure a real duration. Without this pair, `onToolUse` collapses
|
|
66
|
+
* call+result into one instant and every tool renders as 0ms.
|
|
67
|
+
*/
|
|
68
|
+
onToolStart?: (
|
|
69
|
+
callId: string,
|
|
70
|
+
toolName: string,
|
|
71
|
+
input: Record<string, unknown>,
|
|
72
|
+
) => void;
|
|
73
|
+
/** Terminal counterpart to [onToolStart]; ignored for unknown ids. */
|
|
74
|
+
onToolEnd?: (
|
|
75
|
+
callId: string,
|
|
76
|
+
toolName: string,
|
|
77
|
+
meta?: { failed?: boolean },
|
|
78
|
+
) => void;
|
|
57
79
|
};
|
|
58
80
|
|
|
59
81
|
/** Result of a backend AI query. */
|