@bubblebrain-ai/bubble 0.0.20 → 0.0.22

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.
Files changed (98) hide show
  1. package/dist/agent/abort-errors.d.ts +14 -0
  2. package/dist/agent/abort-errors.js +21 -0
  3. package/dist/agent/budget-ledger.d.ts +41 -0
  4. package/dist/agent/budget-ledger.js +64 -0
  5. package/dist/agent/child-runner.d.ts +55 -0
  6. package/dist/agent/child-runner.js +312 -0
  7. package/dist/agent/profiles.d.ts +8 -0
  8. package/dist/agent/profiles.js +27 -5
  9. package/dist/agent/result-integrator.d.ts +22 -0
  10. package/dist/agent/result-integrator.js +50 -0
  11. package/dist/agent/subagent-control.d.ts +31 -0
  12. package/dist/agent/subagent-control.js +27 -0
  13. package/dist/agent/subagent-lifecycle-reminder.js +11 -2
  14. package/dist/agent/subagent-scheduler.d.ts +95 -0
  15. package/dist/agent/subagent-scheduler.js +256 -0
  16. package/dist/agent/subagent-store.d.ts +41 -0
  17. package/dist/agent/subagent-store.js +149 -0
  18. package/dist/agent/subagent-summary.d.ts +30 -0
  19. package/dist/agent/subagent-summary.js +74 -0
  20. package/dist/agent/worktree.d.ts +29 -0
  21. package/dist/agent/worktree.js +73 -0
  22. package/dist/agent.d.ts +64 -5
  23. package/dist/agent.js +365 -288
  24. package/dist/approval/controller.js +9 -1
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +17 -1
  27. package/dist/checkpoints.d.ts +57 -0
  28. package/dist/checkpoints.js +0 -0
  29. package/dist/config.d.ts +8 -0
  30. package/dist/config.js +17 -0
  31. package/dist/feishu/agent-host/approval-card.js +9 -0
  32. package/dist/feishu/agent-host/run-driver.js +2 -0
  33. package/dist/main.js +88 -13
  34. package/dist/network/errors.d.ts +28 -0
  35. package/dist/network/errors.js +24 -0
  36. package/dist/orchestrator/default-hooks.js +5 -1
  37. package/dist/prompt/compose.js +3 -0
  38. package/dist/prompt/delegation.d.ts +14 -0
  39. package/dist/prompt/delegation.js +64 -0
  40. package/dist/prompt/task-reminders.d.ts +5 -1
  41. package/dist/prompt/task-reminders.js +10 -2
  42. package/dist/provider-anthropic.js +23 -0
  43. package/dist/provider.js +23 -3
  44. package/dist/session.d.ts +31 -0
  45. package/dist/session.js +69 -0
  46. package/dist/slash-commands/commands.js +109 -2
  47. package/dist/slash-commands/types.d.ts +6 -0
  48. package/dist/tools/agent-lifecycle.d.ts +29 -3
  49. package/dist/tools/agent-lifecycle.js +394 -40
  50. package/dist/tools/bash.js +4 -0
  51. package/dist/tools/child-tools.d.ts +31 -0
  52. package/dist/tools/child-tools.js +106 -0
  53. package/dist/tools/edit.d.ts +2 -1
  54. package/dist/tools/edit.js +2 -1
  55. package/dist/tools/index.d.ts +7 -0
  56. package/dist/tools/index.js +3 -3
  57. package/dist/tools/write.d.ts +2 -1
  58. package/dist/tools/write.js +2 -1
  59. package/dist/tui/image-paste.d.ts +18 -0
  60. package/dist/tui/image-paste.js +60 -0
  61. package/dist/tui/run.d.ts +11 -1
  62. package/dist/tui/run.js +399 -71
  63. package/dist/tui/session-picker-data.d.ts +18 -0
  64. package/dist/tui/session-picker-data.js +21 -0
  65. package/dist/tui/trace-groups.d.ts +16 -0
  66. package/dist/tui/trace-groups.js +42 -1
  67. package/dist/tui/transcript-scroll.d.ts +25 -0
  68. package/dist/tui/transcript-scroll.js +20 -0
  69. package/dist/tui/wordmark.d.ts +2 -0
  70. package/dist/tui/wordmark.js +31 -4
  71. package/dist/tui-ink/app.d.ts +4 -1
  72. package/dist/tui-ink/app.js +301 -247
  73. package/dist/tui-ink/approval/approval-dialog.js +10 -0
  74. package/dist/tui-ink/display-history.d.ts +16 -1
  75. package/dist/tui-ink/display-history.js +50 -21
  76. package/dist/tui-ink/footer.d.ts +6 -12
  77. package/dist/tui-ink/footer.js +10 -29
  78. package/dist/tui-ink/image-paste.d.ts +59 -0
  79. package/dist/tui-ink/image-paste.js +277 -0
  80. package/dist/tui-ink/input-box.d.ts +26 -1
  81. package/dist/tui-ink/input-box.js +171 -41
  82. package/dist/tui-ink/message-list.d.ts +1 -1
  83. package/dist/tui-ink/message-list.js +46 -29
  84. package/dist/tui-ink/run.d.ts +7 -2
  85. package/dist/tui-ink/run.js +73 -23
  86. package/dist/tui-ink/terminal-mouse.d.ts +1 -0
  87. package/dist/tui-ink/terminal-mouse.js +4 -0
  88. package/dist/tui-ink/trace-groups.d.ts +16 -0
  89. package/dist/tui-ink/trace-groups.js +50 -2
  90. package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
  91. package/dist/tui-ink/transcript-viewport-math.js +17 -0
  92. package/dist/tui-ink/transcript-viewport.d.ts +24 -0
  93. package/dist/tui-ink/transcript-viewport.js +83 -0
  94. package/dist/tui-ink/welcome.d.ts +9 -7
  95. package/dist/tui-ink/welcome.js +7 -33
  96. package/dist/tui-opentui/approval/approval-dialog.js +10 -0
  97. package/dist/types.d.ts +17 -0
  98. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -16,15 +16,21 @@ import { buildDeferredToolsReminder, buildToolFreezeReminder, reminderForMode }
16
16
  import { HookBus } from "./orchestrator/hooks.js";
17
17
  import { normalizeHookInput, truncateHookText, } from "./hooks/index.js";
18
18
  import { createDefaultHooks } from "./orchestrator/default-hooks.js";
19
- import { resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
19
+ import { mergeAgentCategories, resolveModelRoute, resolveSubagentRoute } from "./agent/categories.js";
20
20
  import { getSubtaskPolicy } from "./agent/subtask-policy.js";
21
- import { composeAbortSignals } from "./agent/budget-ledger.js";
22
- import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
21
+ import { composeAbortSignals, computeChildTokenCap, DEFAULT_CHILD_TOKEN_CAP, PARENT_POOL_RESERVE_RATIO } from "./agent/budget-ledger.js";
22
+ import { assignAgentNickname, builtinAgentProfiles, validateAgentProfileTools } from "./agent/profiles.js";
23
23
  import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
24
+ import { SubagentStore } from "./agent/subagent-store.js";
25
+ import { SubagentScheduler } from "./agent/subagent-scheduler.js";
26
+ import { ChildRunner, classifySubagentAbortReason } from "./agent/child-runner.js";
27
+ import { ResultIntegrator } from "./agent/result-integrator.js";
28
+ import { AgentAbortError, EMPTY_ASSISTANT_FALLBACK, SubagentAbortError } from "./agent/abort-errors.js";
29
+ import { createSubagentWorktree, finalizeSubagentWorktree } from "./agent/worktree.js";
30
+ import { createWorktreeChildTools } from "./tools/child-tools.js";
24
31
  import { isHiddenToolResult } from "./agent/discovery-barrier.js";
25
32
  import { createStreamingInternalReminderSanitizer, sanitizeAssistantProviderMetadata, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "./agent/internal-reminder-sanitizer.js";
26
33
  import { buildSystemPrompt } from "./system-prompt.js";
27
- import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
28
34
  import { debugReasoningStream, summarizeDebugText } from "./reasoning-debug.js";
29
35
  import { buildToolPromptOptions } from "./tools/prompt-metadata.js";
30
36
  import { stopAutoServersForSession } from "./tools/server-manager.js";
@@ -39,8 +45,12 @@ const MAX_EMPTY_ASSISTANT_RECOVERIES = 1;
39
45
  const EMPTY_ASSISTANT_RECOVERY_REMINDER = "The previous model response contained no user-visible assistant content and no tool calls. " +
40
46
  "Respond now with a concise, user-visible answer, or call an available tool if more work is required. " +
41
47
  "Do not put the final answer only in hidden reasoning.";
42
- const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
43
- const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
48
+ export { AgentAbortError, SubagentAbortError } from "./agent/abort-errors.js";
49
+ // Model-facing interruption boundary. Persisted into the transcript so the
50
+ // next turn sees an explicit stop instead of a dangling request — but it must
51
+ // never render in the UI as if the assistant said it (the TUIs strip it and
52
+ // show their own interrupt indicator instead).
53
+ export const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
44
54
  function agentEventFromHookProgress(event) {
45
55
  const source = `${event.source.scope}:${event.source.index}`;
46
56
  if (event.type === "hook_start") {
@@ -73,12 +83,6 @@ function agentEventFromHookProgress(event) {
73
83
  error: event.error ?? "Hook failed.",
74
84
  };
75
85
  }
76
- export class AgentAbortError extends Error {
77
- constructor(message = "Agent run cancelled.") {
78
- super(message);
79
- this.name = "AgentAbortError";
80
- }
81
- }
82
86
  export class Agent {
83
87
  messages = [];
84
88
  provider;
@@ -110,7 +114,12 @@ export class Agent {
110
114
  fileStateTracker;
111
115
  agentCategories;
112
116
  providerFactory;
113
- subagentThreads = new Map();
117
+ subagentStore;
118
+ subagentScheduler;
119
+ childRunner;
120
+ resultIntegrator = new ResultIntegrator();
121
+ subagentsConfig;
122
+ rateLimitPolicy;
114
123
  pendingSubagentUpdates = [];
115
124
  lastInputTokens = null;
116
125
  lastAnchorMessageCount = null;
@@ -140,6 +149,46 @@ export class Agent {
140
149
  this.fileStateTracker = options.fileStateTracker;
141
150
  this.agentCategories = options.agentCategories ?? {};
142
151
  this.providerFactory = options.providerFactory;
152
+ this.subagentsConfig = options.subagents ?? {};
153
+ this.rateLimitPolicy = options.rateLimitPolicy;
154
+ // Children persist next to the session file so a later process can
155
+ // resume them via send_input (design §7). Child agents themselves
156
+ // (agentRole "subagent") never persist children — no recursion exists.
157
+ const persistDir = this.agentRole === "parent"
158
+ ? this.subagentsConfig.persistDir
159
+ ?? (this.sessionID?.endsWith(".jsonl") ? this.sessionID.replace(/\.jsonl$/, ".subagents") : undefined)
160
+ : undefined;
161
+ this.subagentStore = new SubagentStore(persistDir);
162
+ this.subagentStore.loadPersisted();
163
+ this.subagentScheduler = new SubagentScheduler({
164
+ maxActiveSubagents: this.subagentsConfig.maxActiveSubagents,
165
+ launchBurst: this.subagentsConfig.launchBurst,
166
+ launchIntervalMs: this.subagentsConfig.launchIntervalMs,
167
+ rateLimitMaxAttempts: this.subagentsConfig.rateLimitMaxAttempts,
168
+ rateLimitBackoffMs: this.subagentsConfig.rateLimitBackoffMs,
169
+ getCategoryLimit: (category) => mergeAgentCategories(this.agentCategories)[category]?.maxConcurrent,
170
+ });
171
+ this.childRunner = new ChildRunner({
172
+ allTools: () => [...this.tools.values()],
173
+ budgetLedger: () => this.budgetLedger,
174
+ emit: (record, options, status, event, message) => this.emitSubagentLifecycle(record, options, status, event, message),
175
+ runLifecycleHook: (record, cwd, eventName, status, error, abortSignal) => this.runSubagentLifecycleHookFor(record, cwd, eventName, status, error, abortSignal),
176
+ finalizeBlocked: (record, error, options) => this.finalizeSubagentBlocked(record, error, options),
177
+ createInstance: (record, tools, cwd, forkContext) => this.createSubAgentInstance(record, tools, cwd, forkContext),
178
+ notifyWaiters: (record) => this.subagentStore.notifyWaiters(record),
179
+ onFinal: (record, options) => {
180
+ if (record.worktree) {
181
+ // Inspect and clean up the worktree: unchanged → removed; changed →
182
+ // kept for the parent to review, with a diff stat in the handoff (§8).
183
+ finalizeSubagentWorktree(record.worktree);
184
+ if (record.worktree.changed) {
185
+ record.toolNotes.push(`worktree: changes left in ${record.worktree.path} — review the diff before applying`);
186
+ }
187
+ }
188
+ this.subagentStore.persist(record);
189
+ this.maybeEnqueueIngestion(record, options);
190
+ },
191
+ });
143
192
  if (options.systemPrompt) {
144
193
  this.messages.push({ role: "system", content: options.systemPrompt });
145
194
  }
@@ -191,6 +240,10 @@ export class Agent {
191
240
  this.injectSystemReminder(`[Hook ${result.eventName}] ${context}`);
192
241
  }
193
242
  }
243
+ /** Whether a tool is registered on this agent (e.g. delegation tools on parents). */
244
+ hasToolAvailable(name) {
245
+ return this.tools.has(name);
246
+ }
194
247
  /** Unlock a list of deferred tools so they're included in subsequent turns. */
195
248
  unlockDeferredTools(names) {
196
249
  for (const n of names) {
@@ -428,27 +481,29 @@ export class Agent {
428
481
  this.setTodos([]);
429
482
  yield emit({ type: "todos_updated", todos: [] });
430
483
  }
431
- const promptHook = await this.runExternalHook({
432
- eventName: "UserPromptSubmit",
433
- cwd,
434
- runId,
435
- target: typeof userInput === "string" ? userInput : "content_parts",
436
- payload: normalizeHookInput(userInput),
437
- fullPayload: { prompt: userInput },
438
- }, abortSignal);
439
- for (const event of promptHook.events)
440
- yield emit(event);
441
- if (promptHook.result.decision === "deny") {
442
- const message = promptHook.result.reason
443
- ?? `Prompt blocked by hook ${promptHook.result.sourceHookId ?? "<unknown>"}.`;
444
- yield emit({ type: "turn_start" });
445
- yield emit({ type: "text_delta", content: message });
446
- yield emit({ type: "turn_end", willContinue: false });
447
- yield emit({ type: "agent_end" });
448
- return;
484
+ if (!options.resumeWithoutInput) {
485
+ const promptHook = await this.runExternalHook({
486
+ eventName: "UserPromptSubmit",
487
+ cwd,
488
+ runId,
489
+ target: typeof userInput === "string" ? userInput : "content_parts",
490
+ payload: normalizeHookInput(userInput),
491
+ fullPayload: { prompt: userInput },
492
+ }, abortSignal);
493
+ for (const event of promptHook.events)
494
+ yield emit(event);
495
+ if (promptHook.result.decision === "deny") {
496
+ const message = promptHook.result.reason
497
+ ?? `Prompt blocked by hook ${promptHook.result.sourceHookId ?? "<unknown>"}.`;
498
+ yield emit({ type: "turn_start" });
499
+ yield emit({ type: "text_delta", content: message });
500
+ yield emit({ type: "turn_end", willContinue: false });
501
+ yield emit({ type: "agent_end" });
502
+ return;
503
+ }
504
+ this.injectHookModelContext(promptHook.result);
505
+ this.appendMessage({ role: "user", content: userInput });
449
506
  }
450
- this.injectHookModelContext(promptHook.result);
451
- this.appendMessage({ role: "user", content: userInput });
452
507
  await hookBus.runBeforeTurn({
453
508
  agent: this,
454
509
  cwd,
@@ -475,6 +530,9 @@ export class Agent {
475
530
  while (true) {
476
531
  throwIfAborted(abortSignal);
477
532
  flushGovernorReminders();
533
+ // Background child completions surface before the next inference turn
534
+ // without requiring a wait_agent call (design §5).
535
+ this.flushSubagentIngestions();
478
536
  for (const update of this.drainSubagentToolUpdates())
479
537
  yield emit(update);
480
538
  for (const event of await applyPendingInputs())
@@ -589,6 +647,7 @@ export class Agent {
589
647
  temperature: this.temperature,
590
648
  thinkingLevel: this.thinkingLevel,
591
649
  abortSignal,
650
+ rateLimitPolicy: this.rateLimitPolicy,
592
651
  });
593
652
  for await (const chunk of stream) {
594
653
  throwIfAborted(abortSignal);
@@ -838,6 +897,19 @@ export class Agent {
838
897
  }
839
898
  let tc = parsedCalls[index];
840
899
  let blockedResult;
900
+ // agent_team must be the only tool call in its response (design
901
+ // §1.2): concurrent calls race the scheduler and the aggregated
902
+ // reply; the rejection teaches the model how to re-issue.
903
+ if (tc.name === "agent_team" && parsedCalls.length > 1) {
904
+ blockedResult = {
905
+ content: [
906
+ "agent_team must be the only tool call in your response.",
907
+ "Re-issue agent_team alone — one call, nothing else in the same message — and run any other tools or additional teams sequentially after it returns.",
908
+ ].join(" "),
909
+ isError: true,
910
+ status: "blocked",
911
+ };
912
+ }
841
913
  await hookBus.runBeforeToolCall({
842
914
  agent: this,
843
915
  cwd,
@@ -1326,12 +1398,20 @@ export class Agent {
1326
1398
  nickname: options.nickname,
1327
1399
  route: options.route ?? this.resolveRouteForSubagent(options.profile, options.category),
1328
1400
  });
1329
- await this.runSubagentThread(record, input, cwd, {
1330
- approval: options.approval ?? options.profile.approval,
1401
+ this.subagentStore.set(record);
1402
+ const approval = options.approval ?? options.profile.approval;
1403
+ const admissionError = this.admitSubagentProfile(record, approval);
1404
+ if (admissionError) {
1405
+ this.finalizeSubagentBlocked(record, admissionError, { directEmit: options.emitUpdate });
1406
+ return subagentResultFromThread(record);
1407
+ }
1408
+ record.promise = this.dispatchSubagentRun(record, input, cwd, {
1409
+ approval,
1331
1410
  abortSignal: options.abortSignal,
1332
1411
  forkContext: options.forkContext,
1333
1412
  directEmit: options.emitUpdate,
1334
1413
  });
1414
+ await record.promise;
1335
1415
  return subagentResultFromThread(record);
1336
1416
  }
1337
1417
  async spawnSubAgent(input, cwd, options) {
@@ -1342,24 +1422,35 @@ export class Agent {
1342
1422
  parentToolName: "spawn_agent",
1343
1423
  route: options.route ?? this.resolveRouteForSubagent(options.profile, options.category),
1344
1424
  });
1345
- this.subagentThreads.set(record.agentId, record);
1425
+ this.subagentStore.set(record);
1426
+ const approval = options.approval ?? record.profile.approval;
1427
+ // Admission validation runs before queueing (design §4.2): a request that
1428
+ // would block never consumes a queue slot.
1429
+ const admissionError = this.admitSubagentProfile(record, approval);
1430
+ if (admissionError) {
1431
+ this.finalizeSubagentBlocked(record, admissionError, { queueUpdates: true });
1432
+ return this.snapshotSubagent(record);
1433
+ }
1346
1434
  this.queueSubagentUpdate(record, "queued", undefined, `Queued ${record.nickname} (${record.profile.name})`);
1347
- record.promise = this.runSubagentThread(record, input, cwd, {
1348
- approval: options.approval ?? record.profile.approval,
1435
+ record.promise = this.dispatchSubagentRun(record, input, cwd, {
1436
+ approval,
1349
1437
  abortSignal: options.abortSignal,
1350
1438
  forkContext: options.forkContext,
1351
1439
  queueUpdates: true,
1352
1440
  });
1353
- void record.promise.finally(() => this.notifySubagentWaiters(record));
1354
- return snapshotSubagentThread(record);
1441
+ void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
1442
+ return this.snapshotSubagent(record);
1355
1443
  }
1356
1444
  async waitSubAgents(options = {}) {
1357
1445
  const targets = this.resolveSubagentTargets(options.agentIds);
1358
1446
  if (targets.length === 0)
1359
1447
  return [];
1360
1448
  const completed = targets.filter((record) => isFinalSubagentStatus(record.status));
1361
- if (completed.length > 0)
1362
- return completed.map(snapshotSubagentThread);
1449
+ if (completed.length > 0) {
1450
+ for (const record of completed)
1451
+ this.subagentStore.markDelivered(record.agentId);
1452
+ return completed.map((record) => this.snapshotSubagent(record));
1453
+ }
1363
1454
  const timeoutMs = normalizeWaitTimeout(options.timeoutMs);
1364
1455
  let waiter;
1365
1456
  await Promise.race([
@@ -1378,10 +1469,12 @@ export class Agent {
1378
1469
  new Promise((resolve) => setTimeout(resolve, timeoutMs)),
1379
1470
  ]);
1380
1471
  const finished = targets.filter((record) => isFinalSubagentStatus(record.status));
1381
- return (finished.length > 0 ? finished : targets).map(snapshotSubagentThread);
1472
+ for (const record of finished)
1473
+ this.subagentStore.markDelivered(record.agentId);
1474
+ return (finished.length > 0 ? finished : targets).map((record) => this.snapshotSubagent(record));
1382
1475
  }
1383
1476
  async sendSubAgentInput(agentId, input, cwd, options = {}) {
1384
- const record = this.subagentThreads.get(agentId);
1477
+ const record = this.subagentStore.get(agentId);
1385
1478
  if (!record) {
1386
1479
  throw new Error(`Unknown subagent: ${agentId}`);
1387
1480
  }
@@ -1389,7 +1482,7 @@ export class Agent {
1389
1482
  if (!options.interrupt) {
1390
1483
  throw new Error(`Subagent ${agentId} is still running. Call wait_agent first or pass interrupt:true.`);
1391
1484
  }
1392
- record.abortController.abort(new AgentAbortError(`Subagent ${agentId} interrupted.`));
1485
+ record.abortController.abort(new SubagentAbortError(`Subagent ${agentId} interrupted.`, "interrupt"));
1393
1486
  await record.promise?.catch(() => undefined);
1394
1487
  record.abortController = new AbortController();
1395
1488
  }
@@ -1403,33 +1496,210 @@ export class Agent {
1403
1496
  record.toolNotes = [];
1404
1497
  record.usage = undefined;
1405
1498
  record.error = undefined;
1499
+ record.finalReason = undefined;
1500
+ record.deliveredAt = undefined;
1406
1501
  record.updatedAt = Date.now();
1407
- record.promise = this.runSubagentThread(record, input, cwd, {
1502
+ // A send_input restart is a launch like any other: it goes through the
1503
+ // scheduler's dispatch point and is subject to the same admission limits
1504
+ // (design §4.1) — batch-resuming team members cannot bypass concurrency caps.
1505
+ record.promise = this.dispatchSubagentRun(record, input, cwd, {
1408
1506
  approval: record.profile.approval,
1409
1507
  abortSignal: options.abortSignal,
1410
1508
  queueUpdates: true,
1411
1509
  reuseAgent: true,
1412
1510
  });
1413
- void record.promise.finally(() => this.notifySubagentWaiters(record));
1414
- return snapshotSubagentThread(record);
1511
+ void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
1512
+ return this.snapshotSubagent(record);
1415
1513
  }
1416
1514
  async closeSubAgent(agentId) {
1417
- const record = this.subagentThreads.get(agentId);
1515
+ const record = this.subagentStore.get(agentId);
1418
1516
  if (!record) {
1419
1517
  throw new Error(`Unknown subagent: ${agentId}`);
1420
1518
  }
1421
1519
  if (!isFinalSubagentStatus(record.status)) {
1422
- record.abortController.abort(new AgentAbortError(`Subagent ${agentId} closed.`));
1520
+ record.abortController.abort(new SubagentAbortError(`Subagent ${agentId} closed.`, "user_close"));
1423
1521
  await record.promise?.catch(() => undefined);
1424
1522
  }
1425
1523
  record.status = "closed";
1524
+ record.finalReason = record.finalReason ?? "cancelled_user";
1426
1525
  record.updatedAt = Date.now();
1427
1526
  this.queueSubagentUpdate(record, "cancelled", undefined, `${record.nickname} closed`);
1428
- this.notifySubagentWaiters(record);
1429
- return snapshotSubagentThread(record);
1527
+ this.subagentStore.persist(record);
1528
+ this.subagentStore.notifyWaiters(record);
1529
+ return this.snapshotSubagent(record);
1430
1530
  }
1431
1531
  listSubAgents() {
1432
- return [...this.subagentThreads.values()].map(snapshotSubagentThread);
1532
+ return this.subagentStore.values().map((record) => this.snapshotSubagent(record));
1533
+ }
1534
+ /**
1535
+ * Homogeneous map fan-out (design §1.2): one profile, one template, N items.
1536
+ * Every member goes through the same admission and the same scheduler
1537
+ * dispatch as spawn_agent; the tool blocks until all members are final.
1538
+ */
1539
+ async runAgentTeam(cwd, options) {
1540
+ // Team budget pre-check (§6): items × per-member cap must fit in what
1541
+ // remains of a limited pool after the parent's reserve. On limit-free
1542
+ // hosts the only bound is the items cap enforced by the tool.
1543
+ const limit = this.budgetLedger?.poolLimit;
1544
+ if (this.budgetLedger && limit !== undefined) {
1545
+ const reserve = Math.floor(limit * PARENT_POOL_RESERVE_RATIO);
1546
+ const available = Math.max(0, (this.budgetLedger.remaining() ?? 0) - reserve);
1547
+ const memberCap = Math.min(this.subagentsConfig.childTokenCap ?? DEFAULT_CHILD_TOKEN_CAP, options.profile.maxTokens ?? Number.POSITIVE_INFINITY);
1548
+ const affordable = Math.floor(available / memberCap);
1549
+ if (options.items.length > affordable) {
1550
+ throw new Error([
1551
+ `agent_team rejected: the remaining token budget affords at most ${affordable} member${affordable === 1 ? "" : "s"}`,
1552
+ `but ${options.items.length} were requested. Reduce items or run smaller batches sequentially.`,
1553
+ ].join(" "));
1554
+ }
1555
+ }
1556
+ const approval = options.approval ?? options.profile.approval;
1557
+ const route = this.resolveRouteForSubagent(options.profile, options.category);
1558
+ const records = options.items.map((item) => this.createSubagentThreadRecord({
1559
+ profile: options.profile,
1560
+ task: options.promptTemplate.split("{{item}}").join(item),
1561
+ parentToolCallId: options.parentToolCallId,
1562
+ parentToolName: "agent_team",
1563
+ route,
1564
+ }));
1565
+ const promises = records.map((record) => {
1566
+ this.subagentStore.set(record);
1567
+ const admissionError = this.admitSubagentProfile(record, approval);
1568
+ if (admissionError) {
1569
+ this.finalizeSubagentBlocked(record, admissionError, { directEmit: options.emitUpdate });
1570
+ return Promise.resolve();
1571
+ }
1572
+ // Member events flow through the team tool's own emitUpdate
1573
+ // (directEmit): while a foreground tool runs, the parent loop blocks in
1574
+ // updateQueue.wait and the queued channel is not drained until the tool
1575
+ // settles — queueUpdates would freeze the TUI for the whole team (§1.2).
1576
+ record.promise = this.dispatchSubagentRun(record, record.task, cwd, {
1577
+ approval,
1578
+ abortSignal: options.abortSignal,
1579
+ directEmit: options.emitUpdate,
1580
+ });
1581
+ void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
1582
+ return record.promise;
1583
+ });
1584
+ await Promise.all(promises);
1585
+ // The aggregated reply carries every member's full summary.
1586
+ for (const record of records)
1587
+ this.subagentStore.markDelivered(record.agentId);
1588
+ return records.map((record) => this.snapshotSubagent(record));
1589
+ }
1590
+ /** Marks a child's full summary as delivered to parent context (design §3.3). */
1591
+ markSubagentDelivered(agentId) {
1592
+ this.subagentStore.markDelivered(agentId);
1593
+ }
1594
+ snapshotSubagent(record) {
1595
+ const snapshot = snapshotSubagentThread(record);
1596
+ if (record.status === "queued") {
1597
+ const queuePosition = this.subagentScheduler.queuePosition(record.agentId);
1598
+ if (queuePosition !== undefined)
1599
+ return { ...snapshot, queuePosition };
1600
+ }
1601
+ return snapshot;
1602
+ }
1603
+ /** Returns the blocking diagnostic message when the profile cannot run, else undefined. */
1604
+ admitSubagentProfile(record, approval) {
1605
+ const diagnostics = validateAgentProfileTools([...this.tools.values()], record.profile, approval);
1606
+ const blocking = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
1607
+ if (blocking.length === 0)
1608
+ return undefined;
1609
+ return blocking.map((diagnostic) => diagnostic.message).join("\n");
1610
+ }
1611
+ /**
1612
+ * Background children (queueUpdates) get their results ingested before the
1613
+ * parent's next inference turn (design §5); foreground children (team,
1614
+ * legacy task) deliver through their tool result instead.
1615
+ */
1616
+ maybeEnqueueIngestion(record, options) {
1617
+ if (options.queueUpdates) {
1618
+ this.resultIntegrator.enqueue(record.agentId);
1619
+ }
1620
+ }
1621
+ flushSubagentIngestions() {
1622
+ if (!this.resultIntegrator.hasPending())
1623
+ return;
1624
+ for (const notice of this.resultIntegrator.drainNotices(this.subagentStore)) {
1625
+ this.injectSystemReminder(notice);
1626
+ }
1627
+ }
1628
+ finalizeSubagentBlocked(record, error, emitOptions) {
1629
+ record.status = "blocked";
1630
+ record.finalReason = "blocked";
1631
+ record.error = error;
1632
+ record.updatedAt = Date.now();
1633
+ this.emitSubagentLifecycle(record, emitOptions, "blocked", undefined, error);
1634
+ this.subagentStore.persist(record);
1635
+ this.subagentStore.notifyWaiters(record);
1636
+ }
1637
+ dispatchSubagentRun(record, input, cwd, options) {
1638
+ record.status = "queued";
1639
+ record.updatedAt = Date.now();
1640
+ record.tokenCap = computeChildTokenCap({
1641
+ ledger: this.budgetLedger,
1642
+ subAgentId: record.agentId,
1643
+ activeChildren: this.subagentScheduler.activeCount(),
1644
+ configCap: this.subagentsConfig.childTokenCap,
1645
+ profileMaxTokens: record.profile.maxTokens,
1646
+ });
1647
+ const queueSignal = composeAbortSignals([options.abortSignal, record.abortController.signal]);
1648
+ return this.subagentScheduler.dispatch({
1649
+ agentId: record.agentId,
1650
+ category: record.category,
1651
+ signal: queueSignal,
1652
+ run: (ctx) => this.runSubagentThread(record, input, cwd, { ...options, attempt: ctx.attempt }),
1653
+ onCancelledWhileQueued: (reason) => {
1654
+ record.status = "cancelled";
1655
+ record.finalReason = classifySubagentAbortReason(reason, options.abortSignal, this.budgetLedger);
1656
+ record.error = reason instanceof Error ? reason.message : reason ? String(reason) : "Cancelled while queued.";
1657
+ record.updatedAt = Date.now();
1658
+ // The run never started, so no SubagentStart fired and no SubagentStop follows.
1659
+ this.emitSubagentLifecycle(record, options, "cancelled", undefined, record.error);
1660
+ this.subagentStore.persist(record);
1661
+ this.subagentStore.notifyWaiters(record);
1662
+ this.maybeEnqueueIngestion(record, options);
1663
+ },
1664
+ onRateLimitExhausted: (attempts) => {
1665
+ record.status = "failed";
1666
+ record.finalReason = "rate_limited_exhausted";
1667
+ record.error = `Provider rate limit persisted after ${attempts} attempts.`;
1668
+ record.updatedAt = Date.now();
1669
+ void this.runSubagentLifecycleHookFor(record, cwd, "SubagentStop", record.status, record.error);
1670
+ this.emitSubagentLifecycle(record, options, "failed", undefined, record.error);
1671
+ this.subagentStore.persist(record);
1672
+ this.subagentStore.notifyWaiters(record);
1673
+ this.maybeEnqueueIngestion(record, options);
1674
+ },
1675
+ });
1676
+ }
1677
+ emitSubagentLifecycle(record, options, status, event, message) {
1678
+ const update = this.buildSubagentUpdate(record, status, event, message);
1679
+ options.directEmit?.(update);
1680
+ if (options.queueUpdates) {
1681
+ this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
1682
+ }
1683
+ }
1684
+ async runSubagentLifecycleHookFor(record, cwd, eventName, status, error, abortSignal) {
1685
+ try {
1686
+ await this.runExternalHook({
1687
+ eventName,
1688
+ cwd,
1689
+ runId: record.runId,
1690
+ target: record.profile.name,
1691
+ payload: {
1692
+ agentId: record.agentId,
1693
+ nickname: record.nickname,
1694
+ profile: record.profile.name,
1695
+ status,
1696
+ error,
1697
+ },
1698
+ }, abortSignal);
1699
+ }
1700
+ catch {
1701
+ // Subagent lifecycle hooks are observe-only; never fail the subagent.
1702
+ }
1433
1703
  }
1434
1704
  resolveRouteForSubagent(profile, category) {
1435
1705
  const parentRoute = {
@@ -1478,169 +1748,23 @@ export class Agent {
1478
1748
  waiters: new Set(),
1479
1749
  };
1480
1750
  }
1481
- async runSubagentThread(record, input, cwd, options) {
1482
- const emit = (status, event, message) => {
1483
- const update = this.buildSubagentUpdate(record, status, event, message);
1484
- options.directEmit?.(update);
1485
- if (options.queueUpdates) {
1486
- this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
1487
- }
1488
- };
1489
- const runSubagentLifecycleHook = async (eventName, status, error) => {
1490
- try {
1491
- await this.runExternalHook({
1492
- eventName,
1493
- cwd,
1494
- runId: record.runId,
1495
- target: record.profile.name,
1496
- payload: {
1497
- agentId: record.agentId,
1498
- nickname: record.nickname,
1499
- profile: record.profile.name,
1500
- status,
1501
- error,
1502
- },
1503
- }, options.abortSignal);
1504
- }
1505
- catch {
1506
- // Subagent lifecycle hooks are observe-only; never fail the subagent.
1507
- }
1508
- };
1509
- const allTools = [...this.tools.values()];
1510
- const diagnostics = validateAgentProfileTools(allTools, record.profile, options.approval);
1511
- const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
1512
- for (const diagnostic of diagnostics.filter((item) => item.severity === "warning")) {
1513
- record.toolNotes.push(`profile: ${diagnostic.message}`);
1514
- }
1515
- if (blockingDiagnostics.length > 0) {
1516
- record.status = "blocked";
1517
- record.error = blockingDiagnostics.map((diagnostic) => diagnostic.message).join("\n");
1518
- record.updatedAt = Date.now();
1519
- await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
1520
- emit("blocked", undefined, record.error);
1521
- this.notifySubagentWaiters(record);
1522
- return;
1523
- }
1524
- const tools = selectToolsForAgentProfile(allTools, record.profile, options.approval);
1525
- let subAgent;
1526
- try {
1527
- subAgent = options.reuseAgent && record.agent
1528
- ? record.agent
1529
- : await this.createSubAgentInstance(record, tools, cwd, options.forkContext);
1530
- }
1531
- catch (error) {
1532
- record.status = "blocked";
1533
- record.error = error?.message || String(error);
1534
- record.updatedAt = Date.now();
1535
- await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
1536
- emit("blocked", undefined, record.error);
1537
- this.notifySubagentWaiters(record);
1538
- return;
1539
- }
1540
- record.agent = subAgent;
1541
- record.status = "running";
1542
- record.updatedAt = Date.now();
1543
- await runSubagentLifecycleHook("SubagentStart", record.status);
1544
- emit("running", undefined, `Running ${record.nickname} (${record.profile.name})...`);
1545
- let turnSummaryBuffer = "";
1546
- let turnHadToolCall = false;
1547
- let executedAnyTool = false;
1548
- try {
1549
- const childAbortSignal = composeAbortSignals([
1550
- options.abortSignal,
1551
- record.abortController.signal,
1552
- ]);
1553
- for await (const event of subAgent.run(input, cwd, { abortSignal: childAbortSignal })) {
1554
- if (event.type === "text_delta") {
1555
- turnSummaryBuffer += event.content;
1556
- }
1557
- if (event.type === "tool_call_start"
1558
- || event.type === "tool_call_delta"
1559
- || event.type === "tool_call_end"
1560
- || event.type === "tool_start") {
1561
- turnHadToolCall = true;
1562
- }
1563
- if (event.type === "tool_end") {
1564
- executedAnyTool = true;
1565
- record.toolNotes.push(`${event.name}: ${summarizeSubagentToolEnd(event)}`);
1566
- }
1567
- if (event.type === "turn_end" && event.usage) {
1568
- record.usage = mergeUsage(record.usage, event.usage);
1569
- }
1570
- if (event.type === "turn_end") {
1571
- const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
1572
- if (!turnHadToolCall && turnSummary) {
1573
- // Only the latest tool-free assistant turn is a candidate for the summary;
1574
- // earlier ones are intermediate "I'll do X next" reasoning, not the final answer.
1575
- record.summary = turnSummary;
1576
- }
1577
- turnSummaryBuffer = "";
1578
- turnHadToolCall = false;
1579
- }
1580
- record.updatedAt = Date.now();
1581
- emit("running", event);
1582
- }
1583
- }
1584
- catch (error) {
1585
- const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
1586
- record.status = cancelled ? "cancelled" : "failed";
1587
- record.summary = sanitizeSubagentSummary(record.summary);
1588
- record.error = error?.message || String(error);
1589
- record.updatedAt = Date.now();
1590
- await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
1591
- emit(record.status, undefined, record.error);
1592
- this.notifySubagentWaiters(record);
1593
- return;
1594
- }
1595
- record.summary = sanitizeSubagentSummary(record.summary);
1596
- if (needsExplicitFinalSummary(record, executedAnyTool)) {
1597
- await this.runSubagentFinalSummaryTurn(record, subAgent, cwd, options.abortSignal, emit);
1598
- }
1599
- record.status = "completed";
1600
- record.summary = sanitizeSubagentSummary(record.summary);
1601
- record.updatedAt = Date.now();
1602
- await runSubagentLifecycleHook("SubagentStop", record.status);
1603
- emit("completed", undefined, record.summary || `${record.nickname} completed`);
1604
- this.notifySubagentWaiters(record);
1605
- }
1606
- async runSubagentFinalSummaryTurn(record, subAgent, cwd, abortSignal, emit) {
1607
- const prompt = [
1608
- "Produce the final subagent summary now.",
1609
- "Do not call tools. Do not announce next steps or plans.",
1610
- "Use the evidence already gathered in this child thread.",
1611
- "Return concise findings with concrete file paths and explicit uncertainty.",
1612
- "Your entire response will be returned to the parent as the subagent's answer.",
1613
- ].join("\n");
1614
- subAgent.injectSystemReminder([
1615
- "Subagent final-summary mode is active.",
1616
- "Do not call tools. Do not announce next steps.",
1617
- "Use only the evidence already gathered in this child thread.",
1618
- "Return the final concise summary as your complete response.",
1619
- ].join("\n"));
1620
- let finalBuffer = "";
1621
- let finalHadToolCall = false;
1622
- const finalAbortSignal = composeAbortSignals([abortSignal, record.abortController.signal]);
1623
- for await (const event of subAgent.run(prompt, cwd, { abortSignal: finalAbortSignal })) {
1624
- if (event.type === "text_delta") {
1625
- finalBuffer += event.content;
1626
- }
1627
- if (event.type === "tool_call_start"
1628
- || event.type === "tool_call_delta"
1629
- || event.type === "tool_call_end"
1630
- || event.type === "tool_start") {
1631
- finalHadToolCall = true;
1632
- }
1633
- if (event.type === "turn_end" && event.usage) {
1634
- record.usage = mergeUsage(record.usage, event.usage);
1635
- }
1636
- emit("running", event);
1637
- }
1638
- const finalSummary = sanitizeSubagentSummary(finalBuffer);
1639
- if (!finalHadToolCall && finalSummary) {
1640
- record.summary = finalSummary;
1641
- }
1751
+ runSubagentThread(record, input, cwd, options) {
1752
+ return this.childRunner.run(record, input, cwd, options);
1642
1753
  }
1643
1754
  async createSubAgentInstance(record, tools, cwd, forkContext) {
1755
+ let childCwd = cwd;
1756
+ let childMode = "plan";
1757
+ if (record.profile.mode === "write_worktree") {
1758
+ // Write children work in a runtime-allocated worktree with fresh tool
1759
+ // instances bound to it (design §8): the parent tree is never touched,
1760
+ // and the tools' own workspace fence enforces containment in code.
1761
+ if (!record.worktree) {
1762
+ record.worktree = createSubagentWorktree(cwd, record.agentId);
1763
+ }
1764
+ childCwd = record.worktree.path;
1765
+ childMode = "default";
1766
+ tools = createWorktreeChildTools(childCwd, record.profile.tools.include);
1767
+ }
1644
1768
  const childToolNames = tools.map((tool) => tool.name);
1645
1769
  const route = record.route ?? {
1646
1770
  providerId: this.providerId,
@@ -1655,14 +1779,21 @@ export class Agent {
1655
1779
  configuredModel: route.model || "none",
1656
1780
  configuredModelId: route.providerId && route.model ? `${route.providerId}:${route.model}` : route.model || "none",
1657
1781
  thinkingLevel: route.thinkingLevel,
1658
- mode: "plan",
1659
- workingDir: cwd,
1782
+ mode: childMode,
1783
+ workingDir: childCwd,
1660
1784
  ...buildToolPromptOptions(tools),
1661
1785
  memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
1662
1786
  ? this.memoryPrompt
1663
1787
  : undefined,
1664
1788
  agentProfilePrompt: [
1665
1789
  `You are subagent ${record.nickname}. Your agent profile is ${record.profile.name}.`,
1790
+ record.profile.mode === "write_worktree"
1791
+ ? [
1792
+ "You work inside an isolated git worktree; the parent reviews your diff after you finish.",
1793
+ "Make your changes, verify them (run tests where possible), and end with a handoff that lists the files you changed and how you verified them.",
1794
+ "Do not commit, push, or touch anything outside this worktree.",
1795
+ ].join(" ")
1796
+ : "",
1666
1797
  record.profile.prompt,
1667
1798
  ].filter(Boolean).join("\n\n"),
1668
1799
  });
@@ -1673,7 +1804,7 @@ export class Agent {
1673
1804
  tools,
1674
1805
  temperature: this.temperature,
1675
1806
  thinkingLevel: route.thinkingLevel,
1676
- mode: "plan",
1807
+ mode: childMode,
1677
1808
  maxTurns: record.profile.maxTurns,
1678
1809
  budgetLedger: this.budgetLedger,
1679
1810
  budgetSource: { runId: record.runId, subAgentId: record.agentId },
@@ -1684,8 +1815,18 @@ export class Agent {
1684
1815
  subAgentId: record.agentId,
1685
1816
  agentCategories: this.agentCategories,
1686
1817
  providerFactory: this.providerFactory,
1818
+ // The scheduler owns 429 backoff for children; the transport must not
1819
+ // stack its own retries on top (design §4.5).
1820
+ rateLimitPolicy: "defer",
1687
1821
  });
1688
- if (forkContext) {
1822
+ if (record.messages && record.messages.length > 0) {
1823
+ // Cross-restart resume (design §7): rebuild the child from its
1824
+ // persisted history — including its original system prompt — so
1825
+ // send_input continues with context intact.
1826
+ subAgent.messages = record.messages.map((message) => ({ ...message }));
1827
+ record.messages = undefined;
1828
+ }
1829
+ else if (forkContext) {
1689
1830
  subAgent.messages = this.forkMessagesForSubagent(childSystemPrompt);
1690
1831
  }
1691
1832
  return subAgent;
@@ -1771,27 +1912,20 @@ export class Agent {
1771
1912
  }));
1772
1913
  }
1773
1914
  activeSubagentNicknames() {
1774
- return [...this.subagentThreads.values()]
1775
- .filter((record) => !isFinalSubagentStatus(record.status))
1776
- .map((record) => record.nickname);
1915
+ return this.subagentStore.active().map((record) => record.nickname);
1777
1916
  }
1778
1917
  resolveSubagentTargets(agentIds) {
1779
1918
  if (!agentIds || agentIds.length === 0) {
1780
- return [...this.subagentThreads.values()].filter((record) => record.status !== "closed");
1919
+ return this.subagentStore.values().filter((record) => record.status !== "closed");
1781
1920
  }
1782
1921
  return agentIds.map((id) => {
1783
- const record = this.subagentThreads.get(id);
1922
+ const record = this.subagentStore.get(id);
1784
1923
  if (!record) {
1785
1924
  throw new Error(`Unknown subagent: ${id}`);
1786
1925
  }
1787
1926
  return record;
1788
1927
  });
1789
1928
  }
1790
- notifySubagentWaiters(record) {
1791
- for (const waiter of record.waiters) {
1792
- waiter();
1793
- }
1794
- }
1795
1929
  maybeCompactResidentHistory() {
1796
1930
  if (this.messages.length === 0) {
1797
1931
  return;
@@ -2238,64 +2372,7 @@ function isSubagentLifecycleTool(name) {
2238
2372
  || name === "spawn_agent"
2239
2373
  || name === "wait_agent"
2240
2374
  || name === "send_input"
2241
- || name === "close_agent";
2242
- }
2243
- function sanitizeSubagentSummary(value) {
2244
- return stripProviderProtocolArtifacts(value).trim();
2245
- }
2246
- function needsExplicitFinalSummary(record, executedAnyTool) {
2247
- if (!record.summary)
2248
- return executedAnyTool;
2249
- if (isOnlyProviderProtocolArtifacts(record.summary))
2250
- return true;
2251
- if (/<\/?[||][^<>]*>/.test(record.summary))
2252
- return true;
2253
- if (!executedAnyTool)
2254
- return false;
2255
- if (record.summary === EMPTY_ASSISTANT_FALLBACK)
2256
- return true;
2257
- return isLikelyIntermediateSubagentSummary(record.summary);
2258
- }
2259
- function isLikelyIntermediateSubagentSummary(value) {
2260
- const normalized = value.trim().replace(/\s+/g, " ").toLowerCase();
2261
- if (!normalized)
2262
- return false;
2263
- if (/^(let me|i'll|i will|i need to|i should|i'm going to|now i'll|now i will)\b/.test(normalized)) {
2264
- return true;
2265
- }
2266
- return /:\s*$/.test(normalized) && /\b(read|inspect|check|look|search|try|open)\b/.test(normalized);
2267
- }
2268
- function summarizeSubagentToolEnd(event) {
2269
- const metadata = (event.result.metadata ?? {});
2270
- const reason = readString(metadata.reason);
2271
- if (reason)
2272
- return reason;
2273
- const summary = readString(metadata.summary);
2274
- if (summary)
2275
- return summary;
2276
- if (event.result.isError) {
2277
- const firstLine = event.result.content.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
2278
- return firstLine ? truncateForNote(firstLine) : "failed";
2279
- }
2280
- const matches = readNumber(metadata.matches);
2281
- const pattern = readString(metadata.pattern);
2282
- const path = readString(metadata.path);
2283
- if (matches !== undefined) {
2284
- const target = pattern ? ` for ${pattern}` : "";
2285
- const within = path ? ` in ${path}` : "";
2286
- return `${matches} match${matches === 1 ? "" : "es"}${target}${within}`;
2287
- }
2288
- const kind = readString(metadata.kind);
2289
- if (path)
2290
- return kind ? `${kind} ${path}` : path;
2291
- return event.result.status ?? "completed";
2292
- }
2293
- function readString(value) {
2294
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
2295
- }
2296
- function readNumber(value) {
2297
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
2298
- }
2299
- function truncateForNote(value, max = 200) {
2300
- return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
2375
+ || name === "close_agent"
2376
+ || name === "list_agents"
2377
+ || name === "agent_team";
2301
2378
  }