@mono-agent/agent-runtime 0.18.0 → 0.18.2

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.
@@ -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
+ }