@bubblebrain-ai/bubble 0.0.21 → 0.0.23
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/README.md +197 -34
- package/dist/agent/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/internal-reminder-sanitizer.js +29 -9
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +63 -5
- package/dist/agent.js +360 -287
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +1 -0
- package/dist/main.js +38 -2
- package/dist/model-catalog.js +6 -0
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider-transform.js +14 -0
- package/dist/provider.js +23 -3
- package/dist/slash-commands/commands.js +29 -2
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/index.js +1 -1
- package/dist/tui/run.d.ts +17 -1
- package/dist/tui/run.js +155 -10
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.js +41 -5
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/dist/update/index.d.ts +18 -4
- package/dist/update/index.js +41 -19
- 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,
|
|
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,7 +45,7 @@ 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
|
-
|
|
48
|
+
export { AgentAbortError, SubagentAbortError } from "./agent/abort-errors.js";
|
|
43
49
|
// Model-facing interruption boundary. Persisted into the transcript so the
|
|
44
50
|
// next turn sees an explicit stop instead of a dangling request — but it must
|
|
45
51
|
// never render in the UI as if the assistant said it (the TUIs strip it and
|
|
@@ -77,12 +83,6 @@ function agentEventFromHookProgress(event) {
|
|
|
77
83
|
error: event.error ?? "Hook failed.",
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
|
-
export class AgentAbortError extends Error {
|
|
81
|
-
constructor(message = "Agent run cancelled.") {
|
|
82
|
-
super(message);
|
|
83
|
-
this.name = "AgentAbortError";
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
86
|
export class Agent {
|
|
87
87
|
messages = [];
|
|
88
88
|
provider;
|
|
@@ -114,7 +114,12 @@ export class Agent {
|
|
|
114
114
|
fileStateTracker;
|
|
115
115
|
agentCategories;
|
|
116
116
|
providerFactory;
|
|
117
|
-
|
|
117
|
+
subagentStore;
|
|
118
|
+
subagentScheduler;
|
|
119
|
+
childRunner;
|
|
120
|
+
resultIntegrator = new ResultIntegrator();
|
|
121
|
+
subagentsConfig;
|
|
122
|
+
rateLimitPolicy;
|
|
118
123
|
pendingSubagentUpdates = [];
|
|
119
124
|
lastInputTokens = null;
|
|
120
125
|
lastAnchorMessageCount = null;
|
|
@@ -144,6 +149,46 @@ export class Agent {
|
|
|
144
149
|
this.fileStateTracker = options.fileStateTracker;
|
|
145
150
|
this.agentCategories = options.agentCategories ?? {};
|
|
146
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
|
+
});
|
|
147
192
|
if (options.systemPrompt) {
|
|
148
193
|
this.messages.push({ role: "system", content: options.systemPrompt });
|
|
149
194
|
}
|
|
@@ -195,6 +240,10 @@ export class Agent {
|
|
|
195
240
|
this.injectSystemReminder(`[Hook ${result.eventName}] ${context}`);
|
|
196
241
|
}
|
|
197
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
|
+
}
|
|
198
247
|
/** Unlock a list of deferred tools so they're included in subsequent turns. */
|
|
199
248
|
unlockDeferredTools(names) {
|
|
200
249
|
for (const n of names) {
|
|
@@ -432,27 +481,29 @@ export class Agent {
|
|
|
432
481
|
this.setTodos([]);
|
|
433
482
|
yield emit({ type: "todos_updated", todos: [] });
|
|
434
483
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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 });
|
|
453
506
|
}
|
|
454
|
-
this.injectHookModelContext(promptHook.result);
|
|
455
|
-
this.appendMessage({ role: "user", content: userInput });
|
|
456
507
|
await hookBus.runBeforeTurn({
|
|
457
508
|
agent: this,
|
|
458
509
|
cwd,
|
|
@@ -479,6 +530,9 @@ export class Agent {
|
|
|
479
530
|
while (true) {
|
|
480
531
|
throwIfAborted(abortSignal);
|
|
481
532
|
flushGovernorReminders();
|
|
533
|
+
// Background child completions surface before the next inference turn
|
|
534
|
+
// without requiring a wait_agent call (design §5).
|
|
535
|
+
this.flushSubagentIngestions();
|
|
482
536
|
for (const update of this.drainSubagentToolUpdates())
|
|
483
537
|
yield emit(update);
|
|
484
538
|
for (const event of await applyPendingInputs())
|
|
@@ -593,6 +647,7 @@ export class Agent {
|
|
|
593
647
|
temperature: this.temperature,
|
|
594
648
|
thinkingLevel: this.thinkingLevel,
|
|
595
649
|
abortSignal,
|
|
650
|
+
rateLimitPolicy: this.rateLimitPolicy,
|
|
596
651
|
});
|
|
597
652
|
for await (const chunk of stream) {
|
|
598
653
|
throwIfAborted(abortSignal);
|
|
@@ -842,6 +897,19 @@ export class Agent {
|
|
|
842
897
|
}
|
|
843
898
|
let tc = parsedCalls[index];
|
|
844
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
|
+
}
|
|
845
913
|
await hookBus.runBeforeToolCall({
|
|
846
914
|
agent: this,
|
|
847
915
|
cwd,
|
|
@@ -1330,12 +1398,20 @@ export class Agent {
|
|
|
1330
1398
|
nickname: options.nickname,
|
|
1331
1399
|
route: options.route ?? this.resolveRouteForSubagent(options.profile, options.category),
|
|
1332
1400
|
});
|
|
1333
|
-
|
|
1334
|
-
|
|
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,
|
|
1335
1410
|
abortSignal: options.abortSignal,
|
|
1336
1411
|
forkContext: options.forkContext,
|
|
1337
1412
|
directEmit: options.emitUpdate,
|
|
1338
1413
|
});
|
|
1414
|
+
await record.promise;
|
|
1339
1415
|
return subagentResultFromThread(record);
|
|
1340
1416
|
}
|
|
1341
1417
|
async spawnSubAgent(input, cwd, options) {
|
|
@@ -1346,24 +1422,35 @@ export class Agent {
|
|
|
1346
1422
|
parentToolName: "spawn_agent",
|
|
1347
1423
|
route: options.route ?? this.resolveRouteForSubagent(options.profile, options.category),
|
|
1348
1424
|
});
|
|
1349
|
-
this.
|
|
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
|
+
}
|
|
1350
1434
|
this.queueSubagentUpdate(record, "queued", undefined, `Queued ${record.nickname} (${record.profile.name})`);
|
|
1351
|
-
record.promise = this.
|
|
1352
|
-
approval
|
|
1435
|
+
record.promise = this.dispatchSubagentRun(record, input, cwd, {
|
|
1436
|
+
approval,
|
|
1353
1437
|
abortSignal: options.abortSignal,
|
|
1354
1438
|
forkContext: options.forkContext,
|
|
1355
1439
|
queueUpdates: true,
|
|
1356
1440
|
});
|
|
1357
|
-
void record.promise.finally(() => this.
|
|
1358
|
-
return
|
|
1441
|
+
void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
|
|
1442
|
+
return this.snapshotSubagent(record);
|
|
1359
1443
|
}
|
|
1360
1444
|
async waitSubAgents(options = {}) {
|
|
1361
1445
|
const targets = this.resolveSubagentTargets(options.agentIds);
|
|
1362
1446
|
if (targets.length === 0)
|
|
1363
1447
|
return [];
|
|
1364
1448
|
const completed = targets.filter((record) => isFinalSubagentStatus(record.status));
|
|
1365
|
-
if (completed.length > 0)
|
|
1366
|
-
|
|
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
|
+
}
|
|
1367
1454
|
const timeoutMs = normalizeWaitTimeout(options.timeoutMs);
|
|
1368
1455
|
let waiter;
|
|
1369
1456
|
await Promise.race([
|
|
@@ -1382,10 +1469,12 @@ export class Agent {
|
|
|
1382
1469
|
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
1383
1470
|
]);
|
|
1384
1471
|
const finished = targets.filter((record) => isFinalSubagentStatus(record.status));
|
|
1385
|
-
|
|
1472
|
+
for (const record of finished)
|
|
1473
|
+
this.subagentStore.markDelivered(record.agentId);
|
|
1474
|
+
return (finished.length > 0 ? finished : targets).map((record) => this.snapshotSubagent(record));
|
|
1386
1475
|
}
|
|
1387
1476
|
async sendSubAgentInput(agentId, input, cwd, options = {}) {
|
|
1388
|
-
const record = this.
|
|
1477
|
+
const record = this.subagentStore.get(agentId);
|
|
1389
1478
|
if (!record) {
|
|
1390
1479
|
throw new Error(`Unknown subagent: ${agentId}`);
|
|
1391
1480
|
}
|
|
@@ -1393,7 +1482,7 @@ export class Agent {
|
|
|
1393
1482
|
if (!options.interrupt) {
|
|
1394
1483
|
throw new Error(`Subagent ${agentId} is still running. Call wait_agent first or pass interrupt:true.`);
|
|
1395
1484
|
}
|
|
1396
|
-
record.abortController.abort(new
|
|
1485
|
+
record.abortController.abort(new SubagentAbortError(`Subagent ${agentId} interrupted.`, "interrupt"));
|
|
1397
1486
|
await record.promise?.catch(() => undefined);
|
|
1398
1487
|
record.abortController = new AbortController();
|
|
1399
1488
|
}
|
|
@@ -1407,33 +1496,210 @@ export class Agent {
|
|
|
1407
1496
|
record.toolNotes = [];
|
|
1408
1497
|
record.usage = undefined;
|
|
1409
1498
|
record.error = undefined;
|
|
1499
|
+
record.finalReason = undefined;
|
|
1500
|
+
record.deliveredAt = undefined;
|
|
1410
1501
|
record.updatedAt = Date.now();
|
|
1411
|
-
|
|
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, {
|
|
1412
1506
|
approval: record.profile.approval,
|
|
1413
1507
|
abortSignal: options.abortSignal,
|
|
1414
1508
|
queueUpdates: true,
|
|
1415
1509
|
reuseAgent: true,
|
|
1416
1510
|
});
|
|
1417
|
-
void record.promise.finally(() => this.
|
|
1418
|
-
return
|
|
1511
|
+
void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
|
|
1512
|
+
return this.snapshotSubagent(record);
|
|
1419
1513
|
}
|
|
1420
1514
|
async closeSubAgent(agentId) {
|
|
1421
|
-
const record = this.
|
|
1515
|
+
const record = this.subagentStore.get(agentId);
|
|
1422
1516
|
if (!record) {
|
|
1423
1517
|
throw new Error(`Unknown subagent: ${agentId}`);
|
|
1424
1518
|
}
|
|
1425
1519
|
if (!isFinalSubagentStatus(record.status)) {
|
|
1426
|
-
record.abortController.abort(new
|
|
1520
|
+
record.abortController.abort(new SubagentAbortError(`Subagent ${agentId} closed.`, "user_close"));
|
|
1427
1521
|
await record.promise?.catch(() => undefined);
|
|
1428
1522
|
}
|
|
1429
1523
|
record.status = "closed";
|
|
1524
|
+
record.finalReason = record.finalReason ?? "cancelled_user";
|
|
1430
1525
|
record.updatedAt = Date.now();
|
|
1431
1526
|
this.queueSubagentUpdate(record, "cancelled", undefined, `${record.nickname} closed`);
|
|
1432
|
-
this.
|
|
1433
|
-
|
|
1527
|
+
this.subagentStore.persist(record);
|
|
1528
|
+
this.subagentStore.notifyWaiters(record);
|
|
1529
|
+
return this.snapshotSubagent(record);
|
|
1434
1530
|
}
|
|
1435
1531
|
listSubAgents() {
|
|
1436
|
-
return
|
|
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
|
+
}
|
|
1437
1703
|
}
|
|
1438
1704
|
resolveRouteForSubagent(profile, category) {
|
|
1439
1705
|
const parentRoute = {
|
|
@@ -1482,169 +1748,23 @@ export class Agent {
|
|
|
1482
1748
|
waiters: new Set(),
|
|
1483
1749
|
};
|
|
1484
1750
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const update = this.buildSubagentUpdate(record, status, event, message);
|
|
1488
|
-
options.directEmit?.(update);
|
|
1489
|
-
if (options.queueUpdates) {
|
|
1490
|
-
this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
|
|
1491
|
-
}
|
|
1492
|
-
};
|
|
1493
|
-
const runSubagentLifecycleHook = async (eventName, status, error) => {
|
|
1494
|
-
try {
|
|
1495
|
-
await this.runExternalHook({
|
|
1496
|
-
eventName,
|
|
1497
|
-
cwd,
|
|
1498
|
-
runId: record.runId,
|
|
1499
|
-
target: record.profile.name,
|
|
1500
|
-
payload: {
|
|
1501
|
-
agentId: record.agentId,
|
|
1502
|
-
nickname: record.nickname,
|
|
1503
|
-
profile: record.profile.name,
|
|
1504
|
-
status,
|
|
1505
|
-
error,
|
|
1506
|
-
},
|
|
1507
|
-
}, options.abortSignal);
|
|
1508
|
-
}
|
|
1509
|
-
catch {
|
|
1510
|
-
// Subagent lifecycle hooks are observe-only; never fail the subagent.
|
|
1511
|
-
}
|
|
1512
|
-
};
|
|
1513
|
-
const allTools = [...this.tools.values()];
|
|
1514
|
-
const diagnostics = validateAgentProfileTools(allTools, record.profile, options.approval);
|
|
1515
|
-
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
1516
|
-
for (const diagnostic of diagnostics.filter((item) => item.severity === "warning")) {
|
|
1517
|
-
record.toolNotes.push(`profile: ${diagnostic.message}`);
|
|
1518
|
-
}
|
|
1519
|
-
if (blockingDiagnostics.length > 0) {
|
|
1520
|
-
record.status = "blocked";
|
|
1521
|
-
record.error = blockingDiagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
1522
|
-
record.updatedAt = Date.now();
|
|
1523
|
-
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1524
|
-
emit("blocked", undefined, record.error);
|
|
1525
|
-
this.notifySubagentWaiters(record);
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
const tools = selectToolsForAgentProfile(allTools, record.profile, options.approval);
|
|
1529
|
-
let subAgent;
|
|
1530
|
-
try {
|
|
1531
|
-
subAgent = options.reuseAgent && record.agent
|
|
1532
|
-
? record.agent
|
|
1533
|
-
: await this.createSubAgentInstance(record, tools, cwd, options.forkContext);
|
|
1534
|
-
}
|
|
1535
|
-
catch (error) {
|
|
1536
|
-
record.status = "blocked";
|
|
1537
|
-
record.error = error?.message || String(error);
|
|
1538
|
-
record.updatedAt = Date.now();
|
|
1539
|
-
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1540
|
-
emit("blocked", undefined, record.error);
|
|
1541
|
-
this.notifySubagentWaiters(record);
|
|
1542
|
-
return;
|
|
1543
|
-
}
|
|
1544
|
-
record.agent = subAgent;
|
|
1545
|
-
record.status = "running";
|
|
1546
|
-
record.updatedAt = Date.now();
|
|
1547
|
-
await runSubagentLifecycleHook("SubagentStart", record.status);
|
|
1548
|
-
emit("running", undefined, `Running ${record.nickname} (${record.profile.name})...`);
|
|
1549
|
-
let turnSummaryBuffer = "";
|
|
1550
|
-
let turnHadToolCall = false;
|
|
1551
|
-
let executedAnyTool = false;
|
|
1552
|
-
try {
|
|
1553
|
-
const childAbortSignal = composeAbortSignals([
|
|
1554
|
-
options.abortSignal,
|
|
1555
|
-
record.abortController.signal,
|
|
1556
|
-
]);
|
|
1557
|
-
for await (const event of subAgent.run(input, cwd, { abortSignal: childAbortSignal })) {
|
|
1558
|
-
if (event.type === "text_delta") {
|
|
1559
|
-
turnSummaryBuffer += event.content;
|
|
1560
|
-
}
|
|
1561
|
-
if (event.type === "tool_call_start"
|
|
1562
|
-
|| event.type === "tool_call_delta"
|
|
1563
|
-
|| event.type === "tool_call_end"
|
|
1564
|
-
|| event.type === "tool_start") {
|
|
1565
|
-
turnHadToolCall = true;
|
|
1566
|
-
}
|
|
1567
|
-
if (event.type === "tool_end") {
|
|
1568
|
-
executedAnyTool = true;
|
|
1569
|
-
record.toolNotes.push(`${event.name}: ${summarizeSubagentToolEnd(event)}`);
|
|
1570
|
-
}
|
|
1571
|
-
if (event.type === "turn_end" && event.usage) {
|
|
1572
|
-
record.usage = mergeUsage(record.usage, event.usage);
|
|
1573
|
-
}
|
|
1574
|
-
if (event.type === "turn_end") {
|
|
1575
|
-
const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
|
|
1576
|
-
if (!turnHadToolCall && turnSummary) {
|
|
1577
|
-
// Only the latest tool-free assistant turn is a candidate for the summary;
|
|
1578
|
-
// earlier ones are intermediate "I'll do X next" reasoning, not the final answer.
|
|
1579
|
-
record.summary = turnSummary;
|
|
1580
|
-
}
|
|
1581
|
-
turnSummaryBuffer = "";
|
|
1582
|
-
turnHadToolCall = false;
|
|
1583
|
-
}
|
|
1584
|
-
record.updatedAt = Date.now();
|
|
1585
|
-
emit("running", event);
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
catch (error) {
|
|
1589
|
-
const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
|
|
1590
|
-
record.status = cancelled ? "cancelled" : "failed";
|
|
1591
|
-
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1592
|
-
record.error = error?.message || String(error);
|
|
1593
|
-
record.updatedAt = Date.now();
|
|
1594
|
-
await runSubagentLifecycleHook("SubagentStop", record.status, record.error);
|
|
1595
|
-
emit(record.status, undefined, record.error);
|
|
1596
|
-
this.notifySubagentWaiters(record);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1600
|
-
if (needsExplicitFinalSummary(record, executedAnyTool)) {
|
|
1601
|
-
await this.runSubagentFinalSummaryTurn(record, subAgent, cwd, options.abortSignal, emit);
|
|
1602
|
-
}
|
|
1603
|
-
record.status = "completed";
|
|
1604
|
-
record.summary = sanitizeSubagentSummary(record.summary);
|
|
1605
|
-
record.updatedAt = Date.now();
|
|
1606
|
-
await runSubagentLifecycleHook("SubagentStop", record.status);
|
|
1607
|
-
emit("completed", undefined, record.summary || `${record.nickname} completed`);
|
|
1608
|
-
this.notifySubagentWaiters(record);
|
|
1609
|
-
}
|
|
1610
|
-
async runSubagentFinalSummaryTurn(record, subAgent, cwd, abortSignal, emit) {
|
|
1611
|
-
const prompt = [
|
|
1612
|
-
"Produce the final subagent summary now.",
|
|
1613
|
-
"Do not call tools. Do not announce next steps or plans.",
|
|
1614
|
-
"Use the evidence already gathered in this child thread.",
|
|
1615
|
-
"Return concise findings with concrete file paths and explicit uncertainty.",
|
|
1616
|
-
"Your entire response will be returned to the parent as the subagent's answer.",
|
|
1617
|
-
].join("\n");
|
|
1618
|
-
subAgent.injectSystemReminder([
|
|
1619
|
-
"Subagent final-summary mode is active.",
|
|
1620
|
-
"Do not call tools. Do not announce next steps.",
|
|
1621
|
-
"Use only the evidence already gathered in this child thread.",
|
|
1622
|
-
"Return the final concise summary as your complete response.",
|
|
1623
|
-
].join("\n"));
|
|
1624
|
-
let finalBuffer = "";
|
|
1625
|
-
let finalHadToolCall = false;
|
|
1626
|
-
const finalAbortSignal = composeAbortSignals([abortSignal, record.abortController.signal]);
|
|
1627
|
-
for await (const event of subAgent.run(prompt, cwd, { abortSignal: finalAbortSignal })) {
|
|
1628
|
-
if (event.type === "text_delta") {
|
|
1629
|
-
finalBuffer += event.content;
|
|
1630
|
-
}
|
|
1631
|
-
if (event.type === "tool_call_start"
|
|
1632
|
-
|| event.type === "tool_call_delta"
|
|
1633
|
-
|| event.type === "tool_call_end"
|
|
1634
|
-
|| event.type === "tool_start") {
|
|
1635
|
-
finalHadToolCall = true;
|
|
1636
|
-
}
|
|
1637
|
-
if (event.type === "turn_end" && event.usage) {
|
|
1638
|
-
record.usage = mergeUsage(record.usage, event.usage);
|
|
1639
|
-
}
|
|
1640
|
-
emit("running", event);
|
|
1641
|
-
}
|
|
1642
|
-
const finalSummary = sanitizeSubagentSummary(finalBuffer);
|
|
1643
|
-
if (!finalHadToolCall && finalSummary) {
|
|
1644
|
-
record.summary = finalSummary;
|
|
1645
|
-
}
|
|
1751
|
+
runSubagentThread(record, input, cwd, options) {
|
|
1752
|
+
return this.childRunner.run(record, input, cwd, options);
|
|
1646
1753
|
}
|
|
1647
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
|
+
}
|
|
1648
1768
|
const childToolNames = tools.map((tool) => tool.name);
|
|
1649
1769
|
const route = record.route ?? {
|
|
1650
1770
|
providerId: this.providerId,
|
|
@@ -1659,14 +1779,21 @@ export class Agent {
|
|
|
1659
1779
|
configuredModel: route.model || "none",
|
|
1660
1780
|
configuredModelId: route.providerId && route.model ? `${route.providerId}:${route.model}` : route.model || "none",
|
|
1661
1781
|
thinkingLevel: route.thinkingLevel,
|
|
1662
|
-
mode:
|
|
1663
|
-
workingDir:
|
|
1782
|
+
mode: childMode,
|
|
1783
|
+
workingDir: childCwd,
|
|
1664
1784
|
...buildToolPromptOptions(tools),
|
|
1665
1785
|
memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
|
|
1666
1786
|
? this.memoryPrompt
|
|
1667
1787
|
: undefined,
|
|
1668
1788
|
agentProfilePrompt: [
|
|
1669
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
|
+
: "",
|
|
1670
1797
|
record.profile.prompt,
|
|
1671
1798
|
].filter(Boolean).join("\n\n"),
|
|
1672
1799
|
});
|
|
@@ -1677,7 +1804,7 @@ export class Agent {
|
|
|
1677
1804
|
tools,
|
|
1678
1805
|
temperature: this.temperature,
|
|
1679
1806
|
thinkingLevel: route.thinkingLevel,
|
|
1680
|
-
mode:
|
|
1807
|
+
mode: childMode,
|
|
1681
1808
|
maxTurns: record.profile.maxTurns,
|
|
1682
1809
|
budgetLedger: this.budgetLedger,
|
|
1683
1810
|
budgetSource: { runId: record.runId, subAgentId: record.agentId },
|
|
@@ -1688,8 +1815,18 @@ export class Agent {
|
|
|
1688
1815
|
subAgentId: record.agentId,
|
|
1689
1816
|
agentCategories: this.agentCategories,
|
|
1690
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",
|
|
1691
1821
|
});
|
|
1692
|
-
if (
|
|
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) {
|
|
1693
1830
|
subAgent.messages = this.forkMessagesForSubagent(childSystemPrompt);
|
|
1694
1831
|
}
|
|
1695
1832
|
return subAgent;
|
|
@@ -1775,27 +1912,20 @@ export class Agent {
|
|
|
1775
1912
|
}));
|
|
1776
1913
|
}
|
|
1777
1914
|
activeSubagentNicknames() {
|
|
1778
|
-
return
|
|
1779
|
-
.filter((record) => !isFinalSubagentStatus(record.status))
|
|
1780
|
-
.map((record) => record.nickname);
|
|
1915
|
+
return this.subagentStore.active().map((record) => record.nickname);
|
|
1781
1916
|
}
|
|
1782
1917
|
resolveSubagentTargets(agentIds) {
|
|
1783
1918
|
if (!agentIds || agentIds.length === 0) {
|
|
1784
|
-
return
|
|
1919
|
+
return this.subagentStore.values().filter((record) => record.status !== "closed");
|
|
1785
1920
|
}
|
|
1786
1921
|
return agentIds.map((id) => {
|
|
1787
|
-
const record = this.
|
|
1922
|
+
const record = this.subagentStore.get(id);
|
|
1788
1923
|
if (!record) {
|
|
1789
1924
|
throw new Error(`Unknown subagent: ${id}`);
|
|
1790
1925
|
}
|
|
1791
1926
|
return record;
|
|
1792
1927
|
});
|
|
1793
1928
|
}
|
|
1794
|
-
notifySubagentWaiters(record) {
|
|
1795
|
-
for (const waiter of record.waiters) {
|
|
1796
|
-
waiter();
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
1929
|
maybeCompactResidentHistory() {
|
|
1800
1930
|
if (this.messages.length === 0) {
|
|
1801
1931
|
return;
|
|
@@ -2242,64 +2372,7 @@ function isSubagentLifecycleTool(name) {
|
|
|
2242
2372
|
|| name === "spawn_agent"
|
|
2243
2373
|
|| name === "wait_agent"
|
|
2244
2374
|
|| name === "send_input"
|
|
2245
|
-
|| name === "close_agent"
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
return stripProviderProtocolArtifacts(value).trim();
|
|
2249
|
-
}
|
|
2250
|
-
function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
2251
|
-
if (!record.summary)
|
|
2252
|
-
return executedAnyTool;
|
|
2253
|
-
if (isOnlyProviderProtocolArtifacts(record.summary))
|
|
2254
|
-
return true;
|
|
2255
|
-
if (/<\/?[||][^<>]*>/.test(record.summary))
|
|
2256
|
-
return true;
|
|
2257
|
-
if (!executedAnyTool)
|
|
2258
|
-
return false;
|
|
2259
|
-
if (record.summary === EMPTY_ASSISTANT_FALLBACK)
|
|
2260
|
-
return true;
|
|
2261
|
-
return isLikelyIntermediateSubagentSummary(record.summary);
|
|
2262
|
-
}
|
|
2263
|
-
function isLikelyIntermediateSubagentSummary(value) {
|
|
2264
|
-
const normalized = value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
2265
|
-
if (!normalized)
|
|
2266
|
-
return false;
|
|
2267
|
-
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)) {
|
|
2268
|
-
return true;
|
|
2269
|
-
}
|
|
2270
|
-
return /:\s*$/.test(normalized) && /\b(read|inspect|check|look|search|try|open)\b/.test(normalized);
|
|
2271
|
-
}
|
|
2272
|
-
function summarizeSubagentToolEnd(event) {
|
|
2273
|
-
const metadata = (event.result.metadata ?? {});
|
|
2274
|
-
const reason = readString(metadata.reason);
|
|
2275
|
-
if (reason)
|
|
2276
|
-
return reason;
|
|
2277
|
-
const summary = readString(metadata.summary);
|
|
2278
|
-
if (summary)
|
|
2279
|
-
return summary;
|
|
2280
|
-
if (event.result.isError) {
|
|
2281
|
-
const firstLine = event.result.content.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
2282
|
-
return firstLine ? truncateForNote(firstLine) : "failed";
|
|
2283
|
-
}
|
|
2284
|
-
const matches = readNumber(metadata.matches);
|
|
2285
|
-
const pattern = readString(metadata.pattern);
|
|
2286
|
-
const path = readString(metadata.path);
|
|
2287
|
-
if (matches !== undefined) {
|
|
2288
|
-
const target = pattern ? ` for ${pattern}` : "";
|
|
2289
|
-
const within = path ? ` in ${path}` : "";
|
|
2290
|
-
return `${matches} match${matches === 1 ? "" : "es"}${target}${within}`;
|
|
2291
|
-
}
|
|
2292
|
-
const kind = readString(metadata.kind);
|
|
2293
|
-
if (path)
|
|
2294
|
-
return kind ? `${kind} ${path}` : path;
|
|
2295
|
-
return event.result.status ?? "completed";
|
|
2296
|
-
}
|
|
2297
|
-
function readString(value) {
|
|
2298
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
2299
|
-
}
|
|
2300
|
-
function readNumber(value) {
|
|
2301
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2302
|
-
}
|
|
2303
|
-
function truncateForNote(value, max = 200) {
|
|
2304
|
-
return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
|
|
2375
|
+
|| name === "close_agent"
|
|
2376
|
+
|| name === "list_agents"
|
|
2377
|
+
|| name === "agent_team";
|
|
2305
2378
|
}
|