@mono-agent/agent-runtime 0.18.1 → 0.18.3
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/MIGRATION.md +32 -0
- package/README.md +50 -0
- package/package.json +1 -1
- package/src/agent/tools/agent-tool.js +16 -4
- package/src/agent/tools/web-controller.js +97 -7
- package/src/agent/tools/web-search.js +296 -60
- package/src/ai/providers/claude-cli.js +77 -22
- package/src/ai/providers/claude-sdk.js +54 -11
- package/src/ai/providers/claude-subagent-activity.js +719 -0
- package/src/ai/providers/codex-app.js +1039 -105
- package/src/ai/providers/pi-native/result-builder.js +11 -0
- package/src/ai/providers/pi-native.js +22 -2
- package/src/ai/providers/transport-errors.js +154 -0
- package/src/ai/runtime/router.js +42 -0
- package/src/ai/types.js +98 -10
- package/types/agent/tools/web-controller.d.ts +5 -1
- package/types/agent/tools/web-search.d.ts +13 -0
- package/types/ai/providers/claude-subagent-activity.d.ts +53 -0
- package/types/ai/providers/transport-errors.d.ts +44 -0
- package/types/ai/runtime/router.d.ts +9 -0
- package/types/ai/types.d.ts +231 -20
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
// Claude Code 2.1.220+ forwards native-agent lifecycle plus child messages on
|
|
2
|
+
// the live stream. Both the CLI and Agent SDK expose the same durable shapes:
|
|
3
|
+
//
|
|
4
|
+
// system/task_started task_id, tool_use_id, task_type, subagent_type
|
|
5
|
+
// assistant|user parent_tool_use_id, message.content[]
|
|
6
|
+
// system/task_notification task_id, tool_use_id, status, summary, usage
|
|
7
|
+
//
|
|
8
|
+
// Normalizing that stream directly avoids filesystem transcript replay and,
|
|
9
|
+
// importantly, lets the provider bridge remove child prose from the parent's
|
|
10
|
+
// answer while still preserving the child's nested activity for operators.
|
|
11
|
+
|
|
12
|
+
/** Matches the in-process Agent collector's per-payload wire cap. */
|
|
13
|
+
const WIRE_CONTENT_MAX_CHARS = 2_000;
|
|
14
|
+
const AGENT_TASK_TYPE = /(^|[_-])(?:sub)?agent($|[_-])/i;
|
|
15
|
+
|
|
16
|
+
/** @param {unknown} value */
|
|
17
|
+
function boundedText(value) {
|
|
18
|
+
if (typeof value !== "string") return undefined;
|
|
19
|
+
return value.length > WIRE_CONTENT_MAX_CHARS
|
|
20
|
+
? `${value.slice(0, WIRE_CONTENT_MAX_CHARS)}…`
|
|
21
|
+
: value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {unknown} value */
|
|
25
|
+
function wireContent(value) {
|
|
26
|
+
if (typeof value === "string") return boundedText(value);
|
|
27
|
+
if (value === undefined) return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return boundedText(JSON.stringify(value));
|
|
30
|
+
} catch {
|
|
31
|
+
return boundedText(String(value));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {unknown} value */
|
|
36
|
+
function nonEmptyString(value) {
|
|
37
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {Record<string, any>} raw */
|
|
41
|
+
function isAgentTask(raw) {
|
|
42
|
+
if (raw?.skip_transcript === true) return false;
|
|
43
|
+
if (nonEmptyString(raw?.subagent_type)) return true;
|
|
44
|
+
const taskType = nonEmptyString(raw?.task_type);
|
|
45
|
+
return taskType !== undefined && AGENT_TASK_TYPE.test(taskType);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {unknown} value */
|
|
49
|
+
function isAgentToolName(value) {
|
|
50
|
+
return value === "Agent" || value === "Task";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {unknown} value */
|
|
54
|
+
function isExplicitBackgroundLaunchAcknowledgement(value) {
|
|
55
|
+
const content = typeof value === "string" ? value.trim() : "";
|
|
56
|
+
return /^Async agent launched successfully\. The agent is working in the background\.?$/iu.test(content);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {Record<string, any>} raw */
|
|
60
|
+
function rawEventKey(raw) {
|
|
61
|
+
const uuid = nonEmptyString(raw?.uuid);
|
|
62
|
+
if (uuid) return `${raw.type ?? "?"}:${raw.subtype ?? ""}:${uuid}`;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify([
|
|
65
|
+
raw?.type,
|
|
66
|
+
raw?.subtype,
|
|
67
|
+
raw?.task_id,
|
|
68
|
+
raw?.tool_use_id,
|
|
69
|
+
raw?.parent_tool_use_id,
|
|
70
|
+
raw?.message,
|
|
71
|
+
raw?.status,
|
|
72
|
+
]);
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @param {string} nativeId */
|
|
79
|
+
function orphanId(nativeId) {
|
|
80
|
+
return `claude-task:${nativeId}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Correlate Claude's live native-agent stream into provider-neutral
|
|
85
|
+
* `subagent_activity` events.
|
|
86
|
+
*
|
|
87
|
+
* `observe()` returns `consumed: true` for every event owned by a native
|
|
88
|
+
* subagent. Provider bridges must not additionally forward or interpret those
|
|
89
|
+
* records as parent events. Non-agent background tasks are consumed without
|
|
90
|
+
* creating activity.
|
|
91
|
+
*/
|
|
92
|
+
export function createClaudeSubagentActivityNormalizer() {
|
|
93
|
+
/** @type {Map<string, any>} */
|
|
94
|
+
const byParentToolUseId = new Map();
|
|
95
|
+
/** @type {Map<string, any>} */
|
|
96
|
+
const byNativeId = new Map();
|
|
97
|
+
/** @type {Set<any>} */
|
|
98
|
+
const active = new Set();
|
|
99
|
+
/** @type {Set<any>} */
|
|
100
|
+
const ambiguousCohorts = new Set();
|
|
101
|
+
const ignoredNativeIds = new Set();
|
|
102
|
+
const seenRawEvents = new Set();
|
|
103
|
+
const usedNames = new Set();
|
|
104
|
+
let callIndex = 0;
|
|
105
|
+
let fallbackMessageIndex = 0;
|
|
106
|
+
|
|
107
|
+
/** @param {any} entry */
|
|
108
|
+
function subagentFor(entry) {
|
|
109
|
+
return {
|
|
110
|
+
id: entry.id,
|
|
111
|
+
name: entry.name,
|
|
112
|
+
callIndex: entry.callIndex,
|
|
113
|
+
...(entry.nativeId === undefined || entry.ambiguousNativeIdentity ? {} : { nativeId: entry.nativeId }),
|
|
114
|
+
...(entry.label === undefined ? {} : { label: entry.label }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @param {any} entry @param {Record<string, unknown>} event */
|
|
119
|
+
function wrap(entry, event) {
|
|
120
|
+
return { type: "subagent_activity", subagent: subagentFor(entry), ...event };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @param {any} entry */
|
|
124
|
+
function startedEvent(entry) {
|
|
125
|
+
return wrap(entry, {
|
|
126
|
+
phase: "agent_started",
|
|
127
|
+
id: `agent:${entry.id}`,
|
|
128
|
+
name: `Agent(${entry.name})`,
|
|
129
|
+
arguments: {
|
|
130
|
+
name: entry.name,
|
|
131
|
+
...(entry.label === undefined ? {} : { description: entry.label }),
|
|
132
|
+
...(entry.prompt === undefined ? {} : { prompt: entry.prompt }),
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {any} entry */
|
|
138
|
+
function noteName(entry) {
|
|
139
|
+
if (entry.name !== "subagent" && entry.name !== entry.nativeId) usedNames.add(entry.name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @param {any} entry */
|
|
143
|
+
function authoredName(entry) {
|
|
144
|
+
return entry.name === "subagent" || entry.name === entry.nativeId
|
|
145
|
+
? undefined
|
|
146
|
+
: entry.name;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {any[]} entries */
|
|
150
|
+
function ambiguousCohortFor(entries) {
|
|
151
|
+
const existing = [...new Set(entries.map((entry) => entry.ambiguousCohort).filter(Boolean))];
|
|
152
|
+
const cohort = existing[0] ?? { entries: new Set(), outcomes: new Map() };
|
|
153
|
+
ambiguousCohorts.add(cohort);
|
|
154
|
+
for (const duplicateCohort of existing.slice(1)) {
|
|
155
|
+
for (const member of duplicateCohort.entries) {
|
|
156
|
+
cohort.entries.add(member);
|
|
157
|
+
member.ambiguousCohort = cohort;
|
|
158
|
+
}
|
|
159
|
+
for (const [member, outcome] of duplicateCohort.outcomes) {
|
|
160
|
+
cohort.outcomes.set(member, outcome);
|
|
161
|
+
}
|
|
162
|
+
ambiguousCohorts.delete(duplicateCohort);
|
|
163
|
+
}
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
cohort.entries.add(entry);
|
|
166
|
+
entry.ambiguousCohort = cohort;
|
|
167
|
+
entry.ambiguousNativeIdentity = true;
|
|
168
|
+
}
|
|
169
|
+
return cohort;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Claude can announce a native task before it includes the parent tool-use id
|
|
174
|
+
* that is the public delegation identity. Match the two one-sided records by
|
|
175
|
+
* their authored metadata, then by arrival order when Claude omitted it. The
|
|
176
|
+
* latter is deliberately limited to entries missing the opposite identity;
|
|
177
|
+
* a resolved task is never eligible for a second parent.
|
|
178
|
+
*
|
|
179
|
+
* @param {{parentToolUseId?: string, nativeId?: string, name?: string, label?: string, prompt?: string}} input
|
|
180
|
+
*/
|
|
181
|
+
function pendingCorrelation(input) {
|
|
182
|
+
const candidates = [...active].filter((entry) => {
|
|
183
|
+
if (entry.terminal) return false;
|
|
184
|
+
if (input.parentToolUseId !== undefined && input.nativeId === undefined) {
|
|
185
|
+
return entry.parentToolUseId === undefined && entry.nativeId !== undefined;
|
|
186
|
+
}
|
|
187
|
+
if (input.nativeId !== undefined && input.parentToolUseId === undefined) {
|
|
188
|
+
return entry.nativeId === undefined && entry.parentToolUseId !== undefined;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
192
|
+
if (candidates.length === 0) return undefined;
|
|
193
|
+
|
|
194
|
+
const scored = candidates.map((entry) => {
|
|
195
|
+
let score = 0;
|
|
196
|
+
const entryName = authoredName(entry);
|
|
197
|
+
if (input.name !== undefined && entryName !== undefined) {
|
|
198
|
+
if (input.name !== entryName) return { entry, score: -1 };
|
|
199
|
+
score += 8;
|
|
200
|
+
}
|
|
201
|
+
if (input.label !== undefined && entry.label !== undefined) {
|
|
202
|
+
if (input.label !== entry.label) return { entry, score: -1 };
|
|
203
|
+
score += 4;
|
|
204
|
+
}
|
|
205
|
+
if (input.prompt !== undefined && entry.prompt !== undefined) {
|
|
206
|
+
if (input.prompt !== entry.prompt) return { entry, score: -1 };
|
|
207
|
+
score += 2;
|
|
208
|
+
}
|
|
209
|
+
return { entry, score };
|
|
210
|
+
}).filter(({ score }) => score >= 0);
|
|
211
|
+
scored.sort((left, right) => right.score - left.score
|
|
212
|
+
|| left.entry.callIndex - right.entry.callIndex);
|
|
213
|
+
const bestScore = scored[0]?.score;
|
|
214
|
+
const tied = bestScore === undefined
|
|
215
|
+
? []
|
|
216
|
+
: scored.filter(({ score }) => score === bestScore).map(({ entry }) => entry);
|
|
217
|
+
if (tied.length > 1) ambiguousCohortFor(tied);
|
|
218
|
+
return scored[0]?.entry;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @param {any} entry @param {any} duplicate */
|
|
222
|
+
function mergeEntries(entry, duplicate) {
|
|
223
|
+
if (entry === duplicate) return entry;
|
|
224
|
+
if (entry.ambiguousCohort || duplicate.ambiguousCohort) {
|
|
225
|
+
const cohort = ambiguousCohortFor([entry, duplicate]);
|
|
226
|
+
const duplicateOutcome = cohort.outcomes.get(duplicate);
|
|
227
|
+
if (duplicateOutcome !== undefined && !cohort.outcomes.has(entry)) {
|
|
228
|
+
cohort.outcomes.set(entry, duplicateOutcome);
|
|
229
|
+
}
|
|
230
|
+
cohort.outcomes.delete(duplicate);
|
|
231
|
+
cohort.entries.delete(duplicate);
|
|
232
|
+
cohort.entries.add(entry);
|
|
233
|
+
}
|
|
234
|
+
active.delete(duplicate);
|
|
235
|
+
for (const [toolId, tool] of duplicate.openTools) {
|
|
236
|
+
if (!entry.openTools.has(toolId)) entry.openTools.set(toolId, tool);
|
|
237
|
+
}
|
|
238
|
+
for (const toolId of duplicate.settledToolIds) entry.settledToolIds.add(toolId);
|
|
239
|
+
entry.toolCount += duplicate.toolCount;
|
|
240
|
+
entry.backgroundRequested ||= duplicate.backgroundRequested;
|
|
241
|
+
entry.label ??= duplicate.label;
|
|
242
|
+
entry.prompt ??= duplicate.prompt;
|
|
243
|
+
if (authoredName(entry) === undefined && authoredName(duplicate) !== undefined) {
|
|
244
|
+
entry.name = duplicate.name;
|
|
245
|
+
}
|
|
246
|
+
if (duplicate.parentToolUseId !== undefined) {
|
|
247
|
+
entry.parentToolUseId ??= duplicate.parentToolUseId;
|
|
248
|
+
byParentToolUseId.set(duplicate.parentToolUseId, entry);
|
|
249
|
+
}
|
|
250
|
+
if (duplicate.nativeId !== undefined) {
|
|
251
|
+
entry.nativeId ??= duplicate.nativeId;
|
|
252
|
+
byNativeId.set(duplicate.nativeId, entry);
|
|
253
|
+
}
|
|
254
|
+
return entry;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** @param {any} parentEntry @param {any} nativeEntry */
|
|
258
|
+
function correctAmbiguousPair(parentEntry, nativeEntry) {
|
|
259
|
+
const cohort = parentEntry.ambiguousCohort;
|
|
260
|
+
const previousParentNativeId = parentEntry.nativeId;
|
|
261
|
+
const requestedNativeId = nativeEntry.nativeId;
|
|
262
|
+
const previousParentOutcome = cohort.outcomes.get(parentEntry);
|
|
263
|
+
const requestedNativeOutcome = cohort.outcomes.get(nativeEntry);
|
|
264
|
+
cohort.outcomes.delete(parentEntry);
|
|
265
|
+
cohort.outcomes.delete(nativeEntry);
|
|
266
|
+
|
|
267
|
+
parentEntry.nativeId = requestedNativeId;
|
|
268
|
+
nativeEntry.nativeId = previousParentNativeId;
|
|
269
|
+
if (requestedNativeId !== undefined) byNativeId.set(requestedNativeId, parentEntry);
|
|
270
|
+
if (previousParentNativeId !== undefined) byNativeId.set(previousParentNativeId, nativeEntry);
|
|
271
|
+
if (requestedNativeOutcome !== undefined) cohort.outcomes.set(parentEntry, requestedNativeOutcome);
|
|
272
|
+
if (previousParentOutcome !== undefined) cohort.outcomes.set(nativeEntry, previousParentOutcome);
|
|
273
|
+
return parentEntry;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {{parentToolUseId?: string, nativeId?: string, name?: string, label?: string, prompt?: string, backgroundRequested?: boolean}} input
|
|
278
|
+
*/
|
|
279
|
+
function entryFor(input) {
|
|
280
|
+
const parentEntry = input.parentToolUseId === undefined
|
|
281
|
+
? undefined
|
|
282
|
+
: byParentToolUseId.get(input.parentToolUseId);
|
|
283
|
+
const nativeEntry = input.nativeId === undefined
|
|
284
|
+
? undefined
|
|
285
|
+
: byNativeId.get(input.nativeId);
|
|
286
|
+
let entry = parentEntry ?? nativeEntry;
|
|
287
|
+
if (parentEntry && nativeEntry && parentEntry !== nativeEntry) {
|
|
288
|
+
if (parentEntry.ambiguousCohort
|
|
289
|
+
&& parentEntry.ambiguousCohort === nativeEntry.ambiguousCohort) {
|
|
290
|
+
// A later notification carrying both ids is authoritative. Correct the
|
|
291
|
+
// provisional pair without merging away the other canonical group.
|
|
292
|
+
entry = correctAmbiguousPair(parentEntry, nativeEntry);
|
|
293
|
+
} else {
|
|
294
|
+
// Prefer the parent-keyed entry: its id is already the public canonical
|
|
295
|
+
// id, while native-only entries have deliberately emitted no lifecycle.
|
|
296
|
+
entry = mergeEntries(parentEntry, nativeEntry);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!entry) entry = pendingCorrelation(input);
|
|
300
|
+
if (!entry) {
|
|
301
|
+
const id = input.parentToolUseId ?? orphanId(input.nativeId ?? `unknown-${callIndex}`);
|
|
302
|
+
entry = {
|
|
303
|
+
id,
|
|
304
|
+
parentToolUseId: input.parentToolUseId,
|
|
305
|
+
nativeId: input.nativeId,
|
|
306
|
+
name: input.name ?? input.nativeId ?? "subagent",
|
|
307
|
+
label: input.label,
|
|
308
|
+
prompt: input.prompt,
|
|
309
|
+
backgroundRequested: input.backgroundRequested === true,
|
|
310
|
+
callIndex: callIndex++,
|
|
311
|
+
started: false,
|
|
312
|
+
terminal: false,
|
|
313
|
+
toolCount: 0,
|
|
314
|
+
openTools: new Map(),
|
|
315
|
+
settledToolIds: new Set(),
|
|
316
|
+
};
|
|
317
|
+
active.add(entry);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (input.parentToolUseId !== undefined) {
|
|
321
|
+
entry.parentToolUseId ??= input.parentToolUseId;
|
|
322
|
+
// Native-only task_started records remain silent until this correlation,
|
|
323
|
+
// so changing the provisional orphan id here cannot invalidate an event
|
|
324
|
+
// already sent to consumers.
|
|
325
|
+
if (!entry.started) entry.id = entry.parentToolUseId;
|
|
326
|
+
byParentToolUseId.set(input.parentToolUseId, entry);
|
|
327
|
+
}
|
|
328
|
+
if (input.nativeId !== undefined) {
|
|
329
|
+
entry.nativeId ??= input.nativeId;
|
|
330
|
+
byNativeId.set(input.nativeId, entry);
|
|
331
|
+
}
|
|
332
|
+
if (input.name !== undefined && (entry.name === "subagent" || entry.name === entry.nativeId)) {
|
|
333
|
+
entry.name = input.name;
|
|
334
|
+
}
|
|
335
|
+
entry.label ??= input.label;
|
|
336
|
+
entry.prompt ??= input.prompt;
|
|
337
|
+
entry.backgroundRequested ||= input.backgroundRequested === true;
|
|
338
|
+
noteName(entry);
|
|
339
|
+
return entry;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @param {any} entry */
|
|
343
|
+
function ensureStarted(entry) {
|
|
344
|
+
if (entry.started || entry.terminal) return [];
|
|
345
|
+
entry.started = true;
|
|
346
|
+
return [startedEvent(entry)];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** @param {any} entry @param {string} reason */
|
|
350
|
+
function drainTools(entry, reason) {
|
|
351
|
+
const events = [];
|
|
352
|
+
for (const [toolId, tool] of entry.openTools) {
|
|
353
|
+
if (entry.settledToolIds.has(toolId)) continue;
|
|
354
|
+
entry.settledToolIds.add(toolId);
|
|
355
|
+
events.push(wrap(entry, {
|
|
356
|
+
phase: "completed",
|
|
357
|
+
id: `agent:${entry.id}:${toolId}`,
|
|
358
|
+
name: `${entry.name}▸${tool.name}`,
|
|
359
|
+
isError: true,
|
|
360
|
+
content: reason,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
entry.openTools.clear();
|
|
364
|
+
return events;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* @param {any} entry
|
|
369
|
+
* @param {{status?: string, summary?: string, usage?: Record<string, unknown>, reason?: string}} outcome
|
|
370
|
+
*/
|
|
371
|
+
function finish(entry, outcome) {
|
|
372
|
+
if (entry.terminal) return [];
|
|
373
|
+
const status = outcome.status ?? "ended";
|
|
374
|
+
const reason = outcome.reason ?? "subagent ended before this tool returned";
|
|
375
|
+
const events = [
|
|
376
|
+
...ensureStarted(entry),
|
|
377
|
+
...drainTools(entry, reason),
|
|
378
|
+
];
|
|
379
|
+
entry.terminal = true;
|
|
380
|
+
active.delete(entry);
|
|
381
|
+
const usage = outcome.usage && typeof outcome.usage === "object" ? outcome.usage : {};
|
|
382
|
+
const reportedToolUses = Number(usage.tool_uses);
|
|
383
|
+
const toolUses = Number.isFinite(reportedToolUses) ? reportedToolUses : entry.toolCount;
|
|
384
|
+
events.push(wrap(entry, {
|
|
385
|
+
phase: "agent_completed",
|
|
386
|
+
id: `agent:${entry.id}`,
|
|
387
|
+
name: `Agent(${entry.name})`,
|
|
388
|
+
isError: status !== "completed",
|
|
389
|
+
...(Number.isFinite(Number(usage.duration_ms)) ? { executionMs: Number(usage.duration_ms) } : {}),
|
|
390
|
+
content: boundedText(outcome.summary) ?? `${status} · ${toolUses} tool call${toolUses === 1 ? "" : "s"}`,
|
|
391
|
+
...(Number.isFinite(Number(usage.total_tokens)) ? { totalTokens: Number(usage.total_tokens) } : {}),
|
|
392
|
+
}));
|
|
393
|
+
return events;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Multiple metadata-identical native starts cannot be safely attributed when
|
|
398
|
+
* their child frames omit task_description. Keep each canonical parent group
|
|
399
|
+
* open until the whole tied cohort settles, then close every parent exactly
|
|
400
|
+
* once with an aggregate outcome instead of attaching the wrong native
|
|
401
|
+
* terminal to whichever child frame happened to arrive first.
|
|
402
|
+
*
|
|
403
|
+
* @param {any} cohort
|
|
404
|
+
* @param {string} [fallbackReason]
|
|
405
|
+
*/
|
|
406
|
+
function settleAmbiguousCohort(cohort, fallbackReason) {
|
|
407
|
+
const nativeEntries = [...cohort.entries].filter((entry) => entry.nativeId !== undefined);
|
|
408
|
+
if (fallbackReason !== undefined) {
|
|
409
|
+
for (const entry of nativeEntries) {
|
|
410
|
+
if (!cohort.outcomes.has(entry)) {
|
|
411
|
+
cohort.outcomes.set(entry, {
|
|
412
|
+
status: "stopped",
|
|
413
|
+
summary: fallbackReason,
|
|
414
|
+
reason: fallbackReason,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (nativeEntries.some((entry) => !cohort.outcomes.has(entry))) return [];
|
|
420
|
+
|
|
421
|
+
const outcomes = nativeEntries.map((entry) => cohort.outcomes.get(entry));
|
|
422
|
+
const statuses = outcomes.map((outcome) => outcome?.status ?? "ended");
|
|
423
|
+
const allCompleted = statuses.every((status) => status === "completed");
|
|
424
|
+
const status = allCompleted ? "completed" : statuses.find((value) => value !== "completed") ?? "ended";
|
|
425
|
+
const count = nativeEntries.length;
|
|
426
|
+
const summary = allCompleted
|
|
427
|
+
? `${count} concurrent subagent${count === 1 ? "" : "s"} completed`
|
|
428
|
+
: fallbackReason ?? `concurrent subagents settled: ${[...new Set(statuses)].join(", ")}`;
|
|
429
|
+
const events = [];
|
|
430
|
+
for (const entry of cohort.entries) {
|
|
431
|
+
if (entry.parentToolUseId !== undefined) {
|
|
432
|
+
events.push(...finish(entry, { status, summary, reason: fallbackReason }));
|
|
433
|
+
} else {
|
|
434
|
+
// The native-only placeholder never published a lifecycle. Discard it
|
|
435
|
+
// rather than inventing an orphan group alongside the canonical ones.
|
|
436
|
+
entry.terminal = true;
|
|
437
|
+
active.delete(entry);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
ambiguousCohorts.delete(cohort);
|
|
441
|
+
return events;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** @param {Record<string, any>} raw @param {any} entry */
|
|
445
|
+
function childMessageEvents(raw, entry) {
|
|
446
|
+
const events = [...ensureStarted(entry)];
|
|
447
|
+
const blocks = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
448
|
+
const messageKey = nonEmptyString(raw.uuid) ?? `event-${fallbackMessageIndex++}`;
|
|
449
|
+
|
|
450
|
+
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
|
451
|
+
const block = blocks[blockIndex];
|
|
452
|
+
if (block?.type === "tool_use" && nonEmptyString(block.id)) {
|
|
453
|
+
const toolId = nonEmptyString(block.id);
|
|
454
|
+
if (entry.openTools.has(toolId) || entry.settledToolIds.has(toolId)) continue;
|
|
455
|
+
const name = nonEmptyString(block.name) ?? "?";
|
|
456
|
+
entry.openTools.set(toolId, { name });
|
|
457
|
+
entry.toolCount += 1;
|
|
458
|
+
events.push(wrap(entry, {
|
|
459
|
+
phase: "started",
|
|
460
|
+
id: `agent:${entry.id}:${toolId}`,
|
|
461
|
+
name: `${entry.name}▸${name}`,
|
|
462
|
+
arguments: block.input,
|
|
463
|
+
}));
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (block?.type === "tool_result" && nonEmptyString(block.tool_use_id)) {
|
|
468
|
+
const toolId = nonEmptyString(block.tool_use_id);
|
|
469
|
+
if (entry.settledToolIds.has(toolId)) continue;
|
|
470
|
+
const openTool = entry.openTools.get(toolId);
|
|
471
|
+
entry.openTools.delete(toolId);
|
|
472
|
+
entry.settledToolIds.add(toolId);
|
|
473
|
+
events.push(wrap(entry, {
|
|
474
|
+
phase: "completed",
|
|
475
|
+
id: `agent:${entry.id}:${toolId}`,
|
|
476
|
+
name: `${entry.name}▸${openTool?.name ?? "?"}`,
|
|
477
|
+
isError: block.is_error === true,
|
|
478
|
+
...(wireContent(block.content) === undefined ? {} : { content: wireContent(block.content) }),
|
|
479
|
+
}));
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const kind = block?.type === "thinking" || block?.type === "redacted_thinking"
|
|
484
|
+
? "thinking"
|
|
485
|
+
: block?.type === "text"
|
|
486
|
+
? "text"
|
|
487
|
+
: undefined;
|
|
488
|
+
if (kind === undefined) continue;
|
|
489
|
+
const content = kind === "thinking"
|
|
490
|
+
? boundedText(block.thinking ?? block.text)
|
|
491
|
+
: boundedText(block.text);
|
|
492
|
+
if (!content) continue;
|
|
493
|
+
events.push(wrap(entry, {
|
|
494
|
+
phase: "message",
|
|
495
|
+
id: `agent:${entry.id}:message:${messageKey}:${blockIndex}`,
|
|
496
|
+
name: `${entry.name}▸${kind}`,
|
|
497
|
+
kind,
|
|
498
|
+
role: raw.type === "user" ? "user" : "assistant",
|
|
499
|
+
content,
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
return events;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** @param {Record<string, any>} raw */
|
|
506
|
+
function observe(raw) {
|
|
507
|
+
if (!raw || typeof raw !== "object") return { consumed: false, events: [] };
|
|
508
|
+
|
|
509
|
+
// Partial child deltas are intentionally not projected. Their finalized
|
|
510
|
+
// assistant message follows with the same content, and emitting both would
|
|
511
|
+
// violate the exactly-once activity contract.
|
|
512
|
+
if (raw.type === "stream_event" && nonEmptyString(raw.parent_tool_use_id)) {
|
|
513
|
+
return { consumed: true, events: [] };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Foreground Task/Agent calls have no task_started/task_notification
|
|
517
|
+
// bookends in Claude 2.1.220. Seed their identity from the parent's tool_use
|
|
518
|
+
// so otherwise-metadata-free child frames still get the authored name and
|
|
519
|
+
// label. Keep the parent event on the normal stream.
|
|
520
|
+
if (raw.type === "assistant" && !nonEmptyString(raw.parent_tool_use_id)) {
|
|
521
|
+
for (const block of Array.isArray(raw?.message?.content) ? raw.message.content : []) {
|
|
522
|
+
if (block?.type !== "tool_use" || !isAgentToolName(block.name) || !nonEmptyString(block.id)) continue;
|
|
523
|
+
const input = block.input && typeof block.input === "object" ? block.input : {};
|
|
524
|
+
entryFor({
|
|
525
|
+
parentToolUseId: nonEmptyString(block.id),
|
|
526
|
+
name: nonEmptyString(input.subagent_type ?? input.name),
|
|
527
|
+
label: nonEmptyString(input.description),
|
|
528
|
+
prompt: boundedText(input.prompt),
|
|
529
|
+
backgroundRequested: input.run_in_background === true,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return { consumed: false, events: [] };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// A synchronous native child settles through the parent's Task tool_result,
|
|
536
|
+
// not task_notification. Background launches also produce a parent result,
|
|
537
|
+
// but it is launch metadata rather than completion. Consume a pure launch
|
|
538
|
+
// acknowledgement so ordinary tool-call consumers cannot close the group;
|
|
539
|
+
// the task notification (or terminal drain) owns that lifecycle transition.
|
|
540
|
+
if (raw.type === "user" && !nonEmptyString(raw.parent_tool_use_id)) {
|
|
541
|
+
const events = [];
|
|
542
|
+
const blocks = Array.isArray(raw?.message?.content) ? raw.message.content : [];
|
|
543
|
+
const forwardedBlocks = [];
|
|
544
|
+
let launchAcknowledgements = 0;
|
|
545
|
+
for (const block of blocks) {
|
|
546
|
+
if (block?.type !== "tool_result" || !nonEmptyString(block.tool_use_id)) {
|
|
547
|
+
forwardedBlocks.push(block);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const entry = byParentToolUseId.get(nonEmptyString(block.tool_use_id));
|
|
551
|
+
const launchOnly = entry !== undefined && (entry.backgroundRequested
|
|
552
|
+
|| isExplicitBackgroundLaunchAcknowledgement(block.content));
|
|
553
|
+
if (launchOnly && block.is_error !== true) {
|
|
554
|
+
launchAcknowledgements += 1;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
forwardedBlocks.push(block);
|
|
558
|
+
if (!entry || entry.terminal) continue;
|
|
559
|
+
const isError = block.is_error === true;
|
|
560
|
+
events.push(...finish(entry, {
|
|
561
|
+
status: isError ? "failed" : "completed",
|
|
562
|
+
summary: wireContent(block.content),
|
|
563
|
+
reason: isError
|
|
564
|
+
? "subagent tool failed before returning"
|
|
565
|
+
: "subagent ended before this tool returned",
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
if (launchAcknowledgements === 0) return { consumed: false, events };
|
|
569
|
+
if (forwardedBlocks.length === 0) return { consumed: true, events };
|
|
570
|
+
return {
|
|
571
|
+
consumed: false,
|
|
572
|
+
events,
|
|
573
|
+
forwarded: {
|
|
574
|
+
...raw,
|
|
575
|
+
message: { ...raw.message, content: forwardedBlocks },
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if ((raw.type === "assistant" || raw.type === "user") && nonEmptyString(raw.parent_tool_use_id)) {
|
|
581
|
+
const key = rawEventKey(raw);
|
|
582
|
+
if (key !== undefined && seenRawEvents.has(key)) return { consumed: true, events: [] };
|
|
583
|
+
if (key !== undefined) seenRawEvents.add(key);
|
|
584
|
+
const entry = entryFor({
|
|
585
|
+
parentToolUseId: nonEmptyString(raw.parent_tool_use_id),
|
|
586
|
+
name: nonEmptyString(raw.subagent_type),
|
|
587
|
+
label: nonEmptyString(raw.task_description),
|
|
588
|
+
});
|
|
589
|
+
if (entry.terminal) return { consumed: true, events: [] };
|
|
590
|
+
return { consumed: true, events: childMessageEvents(raw, entry) };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (raw.type === "tool_progress" && nonEmptyString(raw.parent_tool_use_id)) {
|
|
594
|
+
// The finalized tool_use/tool_result blocks carry the durable activity.
|
|
595
|
+
return { consumed: true, events: [] };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (raw.type !== "system") return { consumed: false, events: [] };
|
|
599
|
+
|
|
600
|
+
if (raw.subtype === "background_tasks_changed") {
|
|
601
|
+
for (const task of Array.isArray(raw.tasks) ? raw.tasks : []) {
|
|
602
|
+
const nativeId = nonEmptyString(task?.task_id);
|
|
603
|
+
if (nativeId && !isAgentTask(task)) ignoredNativeIds.add(nativeId);
|
|
604
|
+
}
|
|
605
|
+
return { consumed: true, events: [] };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const nativeId = nonEmptyString(raw.task_id);
|
|
609
|
+
if (!nativeId) return { consumed: false, events: [] };
|
|
610
|
+
|
|
611
|
+
if (raw.subtype === "task_started") {
|
|
612
|
+
const key = rawEventKey(raw);
|
|
613
|
+
if (key !== undefined && seenRawEvents.has(key)) return { consumed: true, events: [] };
|
|
614
|
+
if (key !== undefined) seenRawEvents.add(key);
|
|
615
|
+
if (!isAgentTask(raw)) {
|
|
616
|
+
ignoredNativeIds.add(nativeId);
|
|
617
|
+
return { consumed: true, events: [] };
|
|
618
|
+
}
|
|
619
|
+
ignoredNativeIds.delete(nativeId);
|
|
620
|
+
const entry = entryFor({
|
|
621
|
+
nativeId,
|
|
622
|
+
parentToolUseId: nonEmptyString(raw.tool_use_id),
|
|
623
|
+
name: nonEmptyString(raw.subagent_type),
|
|
624
|
+
label: nonEmptyString(raw.description),
|
|
625
|
+
prompt: boundedText(raw.prompt),
|
|
626
|
+
});
|
|
627
|
+
// A native id is diagnostic metadata, not the public delegation key.
|
|
628
|
+
// Wait for a parent frame (or a terminal orphan fallback) instead of
|
|
629
|
+
// publishing a second, provisional group that cannot later be renamed.
|
|
630
|
+
return {
|
|
631
|
+
consumed: true,
|
|
632
|
+
events: entry.parentToolUseId === undefined ? [] : ensureStarted(entry),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (ignoredNativeIds.has(nativeId)) return { consumed: true, events: [] };
|
|
637
|
+
|
|
638
|
+
if (raw.subtype === "task_progress") {
|
|
639
|
+
let entry = byNativeId.get(nativeId);
|
|
640
|
+
if (!entry && nonEmptyString(raw.subagent_type)) {
|
|
641
|
+
entry = entryFor({
|
|
642
|
+
nativeId,
|
|
643
|
+
parentToolUseId: nonEmptyString(raw.tool_use_id),
|
|
644
|
+
name: nonEmptyString(raw.subagent_type),
|
|
645
|
+
label: nonEmptyString(raw.description),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (!entry) return { consumed: false, events: [] };
|
|
649
|
+
return {
|
|
650
|
+
consumed: true,
|
|
651
|
+
events: entry.parentToolUseId === undefined ? [] : ensureStarted(entry),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (raw.subtype === "task_updated") {
|
|
656
|
+
const entry = byNativeId.get(nativeId);
|
|
657
|
+
if (!entry) return { consumed: false, events: [] };
|
|
658
|
+
entry.label ??= nonEmptyString(raw.patch?.description);
|
|
659
|
+
return { consumed: true, events: [] };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (raw.subtype !== "task_notification") return { consumed: false, events: [] };
|
|
663
|
+
if (!byNativeId.has(nativeId)
|
|
664
|
+
&& !nonEmptyString(raw.subagent_type)
|
|
665
|
+
&& raw.output_file === "") {
|
|
666
|
+
// Current background Bash notifications omit task_type but carry an empty
|
|
667
|
+
// output_file. Native agents carry a transcript path even though this
|
|
668
|
+
// bridge never reads it.
|
|
669
|
+
ignoredNativeIds.add(nativeId);
|
|
670
|
+
return { consumed: true, events: [] };
|
|
671
|
+
}
|
|
672
|
+
if (raw.skip_transcript === true && !byNativeId.has(nativeId)) {
|
|
673
|
+
ignoredNativeIds.add(nativeId);
|
|
674
|
+
return { consumed: true, events: [] };
|
|
675
|
+
}
|
|
676
|
+
const key = rawEventKey(raw);
|
|
677
|
+
if (key !== undefined && seenRawEvents.has(key)) return { consumed: true, events: [] };
|
|
678
|
+
if (key !== undefined) seenRawEvents.add(key);
|
|
679
|
+
const entry = entryFor({
|
|
680
|
+
nativeId,
|
|
681
|
+
parentToolUseId: nonEmptyString(raw.tool_use_id),
|
|
682
|
+
name: nonEmptyString(raw.subagent_type),
|
|
683
|
+
label: nonEmptyString(raw.description),
|
|
684
|
+
});
|
|
685
|
+
const outcome = {
|
|
686
|
+
status: nonEmptyString(raw.status),
|
|
687
|
+
summary: nonEmptyString(raw.summary),
|
|
688
|
+
usage: raw.usage,
|
|
689
|
+
};
|
|
690
|
+
if (entry.ambiguousCohort) {
|
|
691
|
+
entry.ambiguousCohort.outcomes.set(entry, outcome);
|
|
692
|
+
return {
|
|
693
|
+
consumed: true,
|
|
694
|
+
events: settleAmbiguousCohort(entry.ambiguousCohort),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
consumed: true,
|
|
699
|
+
events: finish(entry, outcome),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
observe,
|
|
705
|
+
/** Close every still-open child and its tools exactly once. */
|
|
706
|
+
drain(reason = "subagent stream closed before completion") {
|
|
707
|
+
const events = [];
|
|
708
|
+
for (const cohort of [...ambiguousCohorts]) {
|
|
709
|
+
events.push(...settleAmbiguousCohort(cohort, reason));
|
|
710
|
+
}
|
|
711
|
+
for (const entry of [...active]) {
|
|
712
|
+
events.push(...finish(entry, { status: "stopped", reason, summary: reason }));
|
|
713
|
+
}
|
|
714
|
+
return events;
|
|
715
|
+
},
|
|
716
|
+
subagentInvoked: () => callIndex > 0,
|
|
717
|
+
nativeSubagentsUsed: () => [...usedNames],
|
|
718
|
+
};
|
|
719
|
+
}
|