@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.
- 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/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 +64 -5
- package/dist/agent.js +365 -288
- 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/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- 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 +2 -0
- package/dist/main.js +88 -13
- 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.js +23 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +109 -2
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/bash.js +4 -0
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +3 -3
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/run.d.ts +11 -1
- package/dist/tui/run.js +399 -71
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +42 -1
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +301 -247
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +50 -2
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- 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,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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
1330
|
-
|
|
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.
|
|
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.
|
|
1348
|
-
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.
|
|
1354
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
1414
|
-
return
|
|
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.
|
|
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
|
|
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.
|
|
1429
|
-
|
|
1527
|
+
this.subagentStore.persist(record);
|
|
1528
|
+
this.subagentStore.notifyWaiters(record);
|
|
1529
|
+
return this.snapshotSubagent(record);
|
|
1430
1530
|
}
|
|
1431
1531
|
listSubAgents() {
|
|
1432
|
-
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
|
+
}
|
|
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
|
-
|
|
1482
|
-
|
|
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:
|
|
1659
|
-
workingDir:
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
|
1919
|
+
return this.subagentStore.values().filter((record) => record.status !== "closed");
|
|
1781
1920
|
}
|
|
1782
1921
|
return agentIds.map((id) => {
|
|
1783
|
-
const record = this.
|
|
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
|
-
|
|
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
|
}
|