@bubblebrain-ai/bubble 0.0.3 → 0.0.5
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 +8 -3
- package/dist/agent/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.d.ts +14 -0
- package/dist/agent/execution-governor.js +172 -14
- package/dist/agent/profiles.d.ts +59 -0
- package/dist/agent/profiles.js +460 -0
- package/dist/agent/subagent-control.d.ts +52 -0
- package/dist/agent/subagent-control.js +38 -0
- package/dist/agent/task-classifier.d.ts +1 -1
- package/dist/agent/task-classifier.js +60 -0
- package/dist/agent/tool-intent.d.ts +14 -0
- package/dist/agent/tool-intent.js +125 -1
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +606 -53
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +45 -0
- package/dist/context/budget.js +1 -0
- package/dist/context/compact-llm.js +7 -6
- package/dist/context/compact.js +6 -6
- package/dist/context/projector.d.ts +3 -3
- package/dist/context/projector.js +32 -18
- package/dist/context/prune.d.ts +2 -2
- package/dist/context/prune.js +1 -4
- package/dist/main.d.ts +1 -1
- package/dist/main.js +13 -6
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +92 -1
- package/dist/orchestrator/hooks.d.ts +10 -0
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +20 -1
- package/dist/prompt/environment.js +21 -2
- package/dist/prompt/provider-prompts/deepseek.d.ts +1 -0
- package/dist/prompt/provider-prompts/deepseek.js +8 -0
- package/dist/prompt/provider-prompts/glm.d.ts +1 -0
- package/dist/prompt/provider-prompts/glm.js +7 -0
- package/dist/prompt/provider-prompts/kimi.d.ts +1 -0
- package/dist/prompt/provider-prompts/kimi.js +7 -0
- package/dist/prompt/reminders.d.ts +5 -1
- package/dist/prompt/reminders.js +51 -6
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +16 -3
- package/dist/prompt/task-reminders.d.ts +2 -0
- package/dist/prompt/task-reminders.js +56 -0
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +6 -7
- package/dist/provider.js +77 -15
- package/dist/session-log.js +3 -1
- package/dist/slash-commands/commands.js +2 -3
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +355 -0
- package/dist/tools/bash.js +12 -7
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +197 -0
- package/dist/tools/edit.js +64 -52
- package/dist/tools/exit-plan-mode.js +3 -1
- package/dist/tools/file-mutation-queue.d.ts +1 -0
- package/dist/tools/file-mutation-queue.js +32 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/lsp.js +2 -0
- package/dist/tools/memory.js +2 -0
- package/dist/tools/question.js +2 -0
- package/dist/tools/read.js +1 -0
- package/dist/tools/skill.js +1 -0
- package/dist/tools/task.js +1 -0
- package/dist/tools/todo.js +1 -0
- package/dist/tools/tool-search.js +2 -1
- package/dist/tools/web-fetch.js +1 -0
- package/dist/tools/web-search.js +1 -0
- package/dist/tools/write.js +10 -1
- package/dist/tui/display-history.d.ts +8 -1
- package/dist/tui/image-paste.d.ts +41 -0
- package/dist/tui/image-paste.js +217 -0
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.js +814 -269
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +114 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +22 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +82 -0
- package/dist/types.d.ts +90 -10
- package/package.json +3 -3
package/dist/agent.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* It maintains message state, calls the LLM, executes tools, and auto-continues.
|
|
4
4
|
*/
|
|
5
5
|
import { compactMessages } from "./context/compact.js";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { compactMessagesWithLLM } from "./context/compact-llm.js";
|
|
7
8
|
import { getContextBudget } from "./context/budget.js";
|
|
8
9
|
import { isContextOverflowError } from "./context/overflow.js";
|
|
@@ -11,7 +12,12 @@ import { aggressivePruneMessages } from "./context/prune.js";
|
|
|
11
12
|
import { buildDeferredToolsReminder, buildToolFreezeReminder, isPermissionModeReminder, reminderForMode } from "./prompt/reminders.js";
|
|
12
13
|
import { HookBus } from "./orchestrator/hooks.js";
|
|
13
14
|
import { createDefaultHooks } from "./orchestrator/default-hooks.js";
|
|
14
|
-
import {
|
|
15
|
+
import { getSubtaskPolicy } from "./agent/subtask-policy.js";
|
|
16
|
+
import { composeAbortSignals } from "./agent/budget-ledger.js";
|
|
17
|
+
import { assignAgentNickname, builtinAgentProfiles, mergeUsage, selectToolsForAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
|
|
18
|
+
import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
|
|
19
|
+
import { buildSystemPrompt } from "./system-prompt.js";
|
|
20
|
+
import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "./provider-artifacts.js";
|
|
15
21
|
const MAX_CONSECUTIVE_OVERFLOW_RECOVERIES = 3;
|
|
16
22
|
const RESIDENT_HISTORY_KEEP_RECENT_TURNS = 3;
|
|
17
23
|
const RESIDENT_HISTORY_MESSAGE_LIMIT = 160;
|
|
@@ -46,6 +52,12 @@ export class Agent {
|
|
|
46
52
|
hookDefinitions;
|
|
47
53
|
maxTurns;
|
|
48
54
|
taskBudget;
|
|
55
|
+
budgetLedger;
|
|
56
|
+
budgetSource;
|
|
57
|
+
skillSummaries;
|
|
58
|
+
memoryPrompt;
|
|
59
|
+
subagentThreads = new Map();
|
|
60
|
+
pendingSubagentUpdates = [];
|
|
49
61
|
lastInputTokens = null;
|
|
50
62
|
lastAnchorMessageCount = null;
|
|
51
63
|
constructor(options) {
|
|
@@ -64,6 +76,10 @@ export class Agent {
|
|
|
64
76
|
this.hookDefinitions = options.hooks ?? [];
|
|
65
77
|
this.maxTurns = options.maxTurns ?? options.steps;
|
|
66
78
|
this.taskBudget = options.taskBudget;
|
|
79
|
+
this.budgetLedger = options.budgetLedger;
|
|
80
|
+
this.budgetSource = options.budgetSource ?? { runId: this.sessionID ?? "agent" };
|
|
81
|
+
this.skillSummaries = options.skills ?? [];
|
|
82
|
+
this.memoryPrompt = options.memoryPrompt;
|
|
67
83
|
if (options.systemPrompt) {
|
|
68
84
|
this.messages.push({ role: "system", content: options.systemPrompt });
|
|
69
85
|
}
|
|
@@ -101,11 +117,11 @@ export class Agent {
|
|
|
101
117
|
return !!tool?.deferred && !this.unlockedDeferred.has(name);
|
|
102
118
|
}
|
|
103
119
|
injectSystemReminder(content) {
|
|
104
|
-
this.appendMessage({ role: "
|
|
120
|
+
this.appendMessage({ role: "meta", kind: "system-reminder", content });
|
|
105
121
|
}
|
|
106
122
|
injectModeReminder() {
|
|
107
|
-
this.messages = this.messages.filter((message) => !(message.role === "
|
|
108
|
-
&& message.
|
|
123
|
+
this.messages = this.messages.filter((message) => !(message.role === "meta"
|
|
124
|
+
&& message.kind === "system-reminder"
|
|
109
125
|
&& isPermissionModeReminder(message.content)));
|
|
110
126
|
this.injectSystemReminder(reminderForMode(this._mode));
|
|
111
127
|
}
|
|
@@ -131,7 +147,7 @@ export class Agent {
|
|
|
131
147
|
this.provider = provider;
|
|
132
148
|
}
|
|
133
149
|
complete(messages, options) {
|
|
134
|
-
return this.provider.complete(messages, {
|
|
150
|
+
return this.provider.complete(projectMessages(messages), {
|
|
135
151
|
model: options?.model ?? this.apiModel,
|
|
136
152
|
temperature: options?.temperature ?? this.temperature,
|
|
137
153
|
thinkingLevel: options?.thinkingLevel ?? this.thinkingLevel,
|
|
@@ -189,7 +205,7 @@ export class Agent {
|
|
|
189
205
|
this.messages.unshift(systemMessage);
|
|
190
206
|
}
|
|
191
207
|
async *run(userInput, cwd, options = {}) {
|
|
192
|
-
const abortSignal = options.abortSignal;
|
|
208
|
+
const abortSignal = composeAbortSignals([options.abortSignal, this.budgetLedger?.signal]);
|
|
193
209
|
throwIfAborted(abortSignal);
|
|
194
210
|
const hookBus = new HookBus();
|
|
195
211
|
for (const hooks of createDefaultHooks()) {
|
|
@@ -227,6 +243,8 @@ export class Agent {
|
|
|
227
243
|
while (true) {
|
|
228
244
|
throwIfAborted(abortSignal);
|
|
229
245
|
flushGovernorReminders();
|
|
246
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
247
|
+
yield update;
|
|
230
248
|
yield { type: "turn_start" };
|
|
231
249
|
step += 1;
|
|
232
250
|
hookState.turnCount = step;
|
|
@@ -250,7 +268,7 @@ export class Agent {
|
|
|
250
268
|
reasoning: "",
|
|
251
269
|
toolCalls: [],
|
|
252
270
|
};
|
|
253
|
-
|
|
271
|
+
const streamingToolCalls = new Map();
|
|
254
272
|
let turnUsage;
|
|
255
273
|
let assistantAppended = false;
|
|
256
274
|
let toolEntries = Array.from(this.tools.values())
|
|
@@ -307,10 +325,28 @@ export class Agent {
|
|
|
307
325
|
break;
|
|
308
326
|
case "tool_call":
|
|
309
327
|
if (chunk.isStart) {
|
|
310
|
-
|
|
328
|
+
streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
|
|
329
|
+
yield { type: "tool_call_start", id: chunk.id, name: chunk.name };
|
|
311
330
|
}
|
|
331
|
+
if (!streamingToolCalls.has(chunk.id)) {
|
|
332
|
+
streamingToolCalls.set(chunk.id, { id: chunk.id, name: chunk.name, args: "" });
|
|
333
|
+
}
|
|
334
|
+
const currentToolCall = streamingToolCalls.get(chunk.id);
|
|
312
335
|
if (currentToolCall) {
|
|
336
|
+
currentToolCall.name = chunk.name || currentToolCall.name;
|
|
313
337
|
currentToolCall.args += chunk.arguments;
|
|
338
|
+
if (chunk.argumentsFull !== undefined) {
|
|
339
|
+
currentToolCall.args = chunk.argumentsFull;
|
|
340
|
+
}
|
|
341
|
+
if (chunk.arguments) {
|
|
342
|
+
yield {
|
|
343
|
+
type: "tool_call_delta",
|
|
344
|
+
id: currentToolCall.id,
|
|
345
|
+
name: currentToolCall.name,
|
|
346
|
+
argumentsDelta: chunk.arguments,
|
|
347
|
+
arguments: currentToolCall.args,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
314
350
|
}
|
|
315
351
|
if (chunk.isEnd && currentToolCall) {
|
|
316
352
|
assistantMsg.toolCalls.push({
|
|
@@ -318,11 +354,18 @@ export class Agent {
|
|
|
318
354
|
name: currentToolCall.name,
|
|
319
355
|
arguments: currentToolCall.args,
|
|
320
356
|
});
|
|
321
|
-
|
|
357
|
+
yield {
|
|
358
|
+
type: "tool_call_end",
|
|
359
|
+
id: currentToolCall.id,
|
|
360
|
+
name: currentToolCall.name,
|
|
361
|
+
arguments: currentToolCall.args,
|
|
362
|
+
};
|
|
363
|
+
streamingToolCalls.delete(chunk.id);
|
|
322
364
|
}
|
|
323
365
|
break;
|
|
324
366
|
case "usage":
|
|
325
367
|
turnUsage = chunk.usage;
|
|
368
|
+
this.budgetLedger?.recordUsage(chunk.usage, this.budgetSource);
|
|
326
369
|
this.lastInputTokens = chunk.usage.promptTokens;
|
|
327
370
|
this.lastAnchorMessageCount = this.messages.length;
|
|
328
371
|
if (hookState.taskBudget) {
|
|
@@ -333,7 +376,10 @@ export class Agent {
|
|
|
333
376
|
}
|
|
334
377
|
break;
|
|
335
378
|
}
|
|
379
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
380
|
+
yield update;
|
|
336
381
|
}
|
|
382
|
+
throwIfAborted(abortSignal);
|
|
337
383
|
this.appendMessage(assistantMsg);
|
|
338
384
|
assistantAppended = true;
|
|
339
385
|
}
|
|
@@ -395,7 +441,41 @@ export class Agent {
|
|
|
395
441
|
yield { type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs };
|
|
396
442
|
const todosVersionBefore = this._todosVersion;
|
|
397
443
|
const modeVersionBefore = this._modeVersion;
|
|
398
|
-
|
|
444
|
+
const updateQueue = createUpdateQueue();
|
|
445
|
+
let result;
|
|
446
|
+
if (blockedResult) {
|
|
447
|
+
result = blockedResult;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const toolExecution = this.executeTool(tc, cwd, abortSignal, (update) => updateQueue.push(update));
|
|
451
|
+
let settled = false;
|
|
452
|
+
let resolved;
|
|
453
|
+
let rejected;
|
|
454
|
+
void toolExecution
|
|
455
|
+
.then((value) => {
|
|
456
|
+
resolved = value;
|
|
457
|
+
})
|
|
458
|
+
.catch((error) => {
|
|
459
|
+
rejected = error;
|
|
460
|
+
})
|
|
461
|
+
.finally(() => {
|
|
462
|
+
settled = true;
|
|
463
|
+
updateQueue.wake();
|
|
464
|
+
});
|
|
465
|
+
while (!settled || updateQueue.hasItems()) {
|
|
466
|
+
for (const update of updateQueue.drain()) {
|
|
467
|
+
yield { type: "tool_update", id: tc.id, name: tc.name, update };
|
|
468
|
+
}
|
|
469
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
470
|
+
yield update;
|
|
471
|
+
if (!settled) {
|
|
472
|
+
await updateQueue.wait();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (rejected)
|
|
476
|
+
throw rejected;
|
|
477
|
+
result = resolved ?? { content: `Error: Tool "${tc.name}" returned no result`, isError: true };
|
|
478
|
+
}
|
|
399
479
|
throwIfAborted(abortSignal);
|
|
400
480
|
await hookBus.runAfterToolCall({
|
|
401
481
|
agent: this,
|
|
@@ -422,6 +502,8 @@ export class Agent {
|
|
|
422
502
|
this.onToolResult?.(tc.name, result);
|
|
423
503
|
executedResults.push(result);
|
|
424
504
|
yield { type: "tool_end", id: tc.id, name: tc.name, result };
|
|
505
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
506
|
+
yield update;
|
|
425
507
|
if (this._todosVersion !== todosVersionBefore) {
|
|
426
508
|
yield { type: "todos_updated", todos: this.getTodos() };
|
|
427
509
|
}
|
|
@@ -460,8 +542,14 @@ export class Agent {
|
|
|
460
542
|
});
|
|
461
543
|
flushGovernorReminders();
|
|
462
544
|
yield { type: "turn_end", usage: turnUsage };
|
|
545
|
+
if (hookState.forceContinuationReason) {
|
|
546
|
+
delete hookState.forceContinuationReason;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
463
549
|
break;
|
|
464
550
|
}
|
|
551
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
552
|
+
yield update;
|
|
465
553
|
yield { type: "agent_end" };
|
|
466
554
|
}
|
|
467
555
|
async recoverFromOverflow(attempt) {
|
|
@@ -503,62 +591,424 @@ export class Agent {
|
|
|
503
591
|
}
|
|
504
592
|
async runSubtask(input, cwd, options) {
|
|
505
593
|
const subtaskType = options?.subtaskType;
|
|
506
|
-
const
|
|
507
|
-
|
|
594
|
+
const profile = builtinAgentProfiles().find((item) => item.subtaskType === (subtaskType ?? "general_readonly"))
|
|
595
|
+
?? builtinAgentProfiles().find((item) => item.subtaskType === "general_readonly");
|
|
596
|
+
const run = await this.runSubAgent(input, cwd, {
|
|
597
|
+
profile,
|
|
598
|
+
runId: randomUUID(),
|
|
599
|
+
subAgentId: randomUUID(),
|
|
600
|
+
parentToolCallId: "task",
|
|
601
|
+
description: options?.description,
|
|
602
|
+
});
|
|
603
|
+
const lines = [
|
|
604
|
+
"Note: task is deprecated. Use spawn_agent with a named profile instead.",
|
|
605
|
+
`Subtask type: ${profile.subtaskType ?? "general_readonly"}`,
|
|
606
|
+
];
|
|
607
|
+
if (options?.description) {
|
|
608
|
+
lines.push(`Subtask description: ${options.description}`);
|
|
609
|
+
}
|
|
610
|
+
if (run.summary) {
|
|
611
|
+
lines.push("", "Subtask summary:", run.summary);
|
|
612
|
+
}
|
|
613
|
+
if (run.toolNotes.length > 0) {
|
|
614
|
+
lines.push("", "Subtask tools:", ...run.toolNotes.slice(0, 8).map((note) => `- ${note}`));
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
content: lines.join("\n"),
|
|
618
|
+
status: getSubtaskPolicy(subtaskType).resultStatus,
|
|
619
|
+
isError: run.status !== "completed",
|
|
620
|
+
metadata: {
|
|
621
|
+
kind: "subagent",
|
|
622
|
+
reason: `Subtask (${profile.subtaskType ?? "general_readonly"}) investigation completed.`,
|
|
623
|
+
subagents: [run],
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
async runSubAgent(input, cwd, options) {
|
|
628
|
+
const record = this.createSubagentThreadRecord({
|
|
629
|
+
profile: options.profile,
|
|
630
|
+
task: typeof input === "string" ? input : "(multimodal task)",
|
|
631
|
+
runId: options.runId,
|
|
632
|
+
agentId: options.subAgentId,
|
|
633
|
+
parentToolCallId: options.parentToolCallId,
|
|
634
|
+
parentToolName: "subagent",
|
|
635
|
+
nickname: options.nickname,
|
|
636
|
+
});
|
|
637
|
+
await this.runSubagentThread(record, input, cwd, {
|
|
638
|
+
approval: options.approval ?? options.profile.approval,
|
|
639
|
+
abortSignal: options.abortSignal,
|
|
640
|
+
forkContext: options.forkContext,
|
|
641
|
+
directEmit: options.emitUpdate,
|
|
642
|
+
});
|
|
643
|
+
return subagentResultFromThread(record);
|
|
644
|
+
}
|
|
645
|
+
async spawnSubAgent(input, cwd, options) {
|
|
646
|
+
const record = this.createSubagentThreadRecord({
|
|
647
|
+
profile: options.profile,
|
|
648
|
+
task: typeof input === "string" ? input : "(multimodal task)",
|
|
649
|
+
parentToolCallId: options.parentToolCallId,
|
|
650
|
+
parentToolName: "spawn_agent",
|
|
651
|
+
});
|
|
652
|
+
this.subagentThreads.set(record.agentId, record);
|
|
653
|
+
this.queueSubagentUpdate(record, "queued", undefined, `Queued ${record.nickname} (${record.profile.name})`);
|
|
654
|
+
record.promise = this.runSubagentThread(record, input, cwd, {
|
|
655
|
+
approval: options.approval ?? record.profile.approval,
|
|
656
|
+
abortSignal: options.abortSignal,
|
|
657
|
+
forkContext: options.forkContext,
|
|
658
|
+
queueUpdates: true,
|
|
659
|
+
});
|
|
660
|
+
void record.promise.finally(() => this.notifySubagentWaiters(record));
|
|
661
|
+
return snapshotSubagentThread(record);
|
|
662
|
+
}
|
|
663
|
+
async waitSubAgents(options = {}) {
|
|
664
|
+
const targets = this.resolveSubagentTargets(options.agentIds);
|
|
665
|
+
if (targets.length === 0)
|
|
666
|
+
return [];
|
|
667
|
+
const completed = targets.filter((record) => isFinalSubagentStatus(record.status));
|
|
668
|
+
if (completed.length > 0)
|
|
669
|
+
return completed.map(snapshotSubagentThread);
|
|
670
|
+
const timeoutMs = normalizeWaitTimeout(options.timeoutMs);
|
|
671
|
+
let waiter;
|
|
672
|
+
await Promise.race([
|
|
673
|
+
new Promise((resolve) => {
|
|
674
|
+
waiter = resolve;
|
|
675
|
+
for (const record of targets) {
|
|
676
|
+
record.waiters.add(resolve);
|
|
677
|
+
}
|
|
678
|
+
}).finally(() => {
|
|
679
|
+
if (waiter) {
|
|
680
|
+
for (const record of targets) {
|
|
681
|
+
record.waiters.delete(waiter);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}),
|
|
685
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
686
|
+
]);
|
|
687
|
+
const finished = targets.filter((record) => isFinalSubagentStatus(record.status));
|
|
688
|
+
return (finished.length > 0 ? finished : targets).map(snapshotSubagentThread);
|
|
689
|
+
}
|
|
690
|
+
async sendSubAgentInput(agentId, input, cwd, options = {}) {
|
|
691
|
+
const record = this.subagentThreads.get(agentId);
|
|
692
|
+
if (!record) {
|
|
693
|
+
throw new Error(`Unknown subagent: ${agentId}`);
|
|
694
|
+
}
|
|
695
|
+
if (record.status === "running" || record.status === "queued") {
|
|
696
|
+
if (!options.interrupt) {
|
|
697
|
+
throw new Error(`Subagent ${agentId} is still running. Call wait_agent first or pass interrupt:true.`);
|
|
698
|
+
}
|
|
699
|
+
record.abortController.abort(new AgentAbortError(`Subagent ${agentId} interrupted.`));
|
|
700
|
+
await record.promise?.catch(() => undefined);
|
|
701
|
+
record.abortController = new AbortController();
|
|
702
|
+
}
|
|
703
|
+
if (record.status === "closed") {
|
|
704
|
+
throw new Error(`Subagent ${agentId} is closed.`);
|
|
705
|
+
}
|
|
706
|
+
record.parentToolCallId = options.parentToolCallId ?? record.parentToolCallId;
|
|
707
|
+
record.parentToolName = "send_input";
|
|
708
|
+
record.task = typeof input === "string" ? input : "(multimodal task)";
|
|
709
|
+
record.summary = "";
|
|
710
|
+
record.toolNotes = [];
|
|
711
|
+
record.usage = undefined;
|
|
712
|
+
record.error = undefined;
|
|
713
|
+
record.updatedAt = Date.now();
|
|
714
|
+
record.promise = this.runSubagentThread(record, input, cwd, {
|
|
715
|
+
approval: record.profile.approval,
|
|
716
|
+
abortSignal: options.abortSignal,
|
|
717
|
+
queueUpdates: true,
|
|
718
|
+
reuseAgent: true,
|
|
719
|
+
});
|
|
720
|
+
void record.promise.finally(() => this.notifySubagentWaiters(record));
|
|
721
|
+
return snapshotSubagentThread(record);
|
|
722
|
+
}
|
|
723
|
+
async closeSubAgent(agentId) {
|
|
724
|
+
const record = this.subagentThreads.get(agentId);
|
|
725
|
+
if (!record) {
|
|
726
|
+
throw new Error(`Unknown subagent: ${agentId}`);
|
|
727
|
+
}
|
|
728
|
+
if (!isFinalSubagentStatus(record.status)) {
|
|
729
|
+
record.abortController.abort(new AgentAbortError(`Subagent ${agentId} closed.`));
|
|
730
|
+
await record.promise?.catch(() => undefined);
|
|
731
|
+
}
|
|
732
|
+
record.status = "closed";
|
|
733
|
+
record.updatedAt = Date.now();
|
|
734
|
+
this.queueSubagentUpdate(record, "cancelled", undefined, `${record.nickname} closed`);
|
|
735
|
+
this.notifySubagentWaiters(record);
|
|
736
|
+
return snapshotSubagentThread(record);
|
|
737
|
+
}
|
|
738
|
+
listSubAgents() {
|
|
739
|
+
return [...this.subagentThreads.values()].map(snapshotSubagentThread);
|
|
740
|
+
}
|
|
741
|
+
createSubagentThreadRecord(options) {
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
const nickname = options.nickname ?? assignAgentNickname(options.profile, this.activeSubagentNicknames());
|
|
744
|
+
return {
|
|
745
|
+
agentId: options.agentId ?? randomUUID(),
|
|
746
|
+
runId: options.runId ?? randomUUID(),
|
|
747
|
+
nickname,
|
|
748
|
+
profile: options.profile,
|
|
749
|
+
parentToolCallId: options.parentToolCallId,
|
|
750
|
+
parentToolName: options.parentToolName,
|
|
751
|
+
status: "queued",
|
|
752
|
+
task: options.task,
|
|
753
|
+
summary: "",
|
|
754
|
+
toolNotes: [],
|
|
755
|
+
createdAt: now,
|
|
756
|
+
updatedAt: now,
|
|
757
|
+
abortController: new AbortController(),
|
|
758
|
+
waiters: new Set(),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
async runSubagentThread(record, input, cwd, options) {
|
|
762
|
+
const emit = (status, event, message) => {
|
|
763
|
+
const update = this.buildSubagentUpdate(record, status, event, message);
|
|
764
|
+
options.directEmit?.(update);
|
|
765
|
+
if (options.queueUpdates) {
|
|
766
|
+
this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
const allTools = [...this.tools.values()];
|
|
770
|
+
const diagnostics = validateAgentProfileTools(allTools, record.profile, options.approval);
|
|
771
|
+
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
772
|
+
for (const diagnostic of diagnostics.filter((item) => item.severity === "warning")) {
|
|
773
|
+
record.toolNotes.push(`profile: ${diagnostic.message}`);
|
|
774
|
+
}
|
|
775
|
+
if (blockingDiagnostics.length > 0) {
|
|
776
|
+
record.status = "blocked";
|
|
777
|
+
record.error = blockingDiagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
778
|
+
record.updatedAt = Date.now();
|
|
779
|
+
emit("blocked", undefined, record.error);
|
|
780
|
+
this.notifySubagentWaiters(record);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const tools = selectToolsForAgentProfile(allTools, record.profile, options.approval);
|
|
784
|
+
const subAgent = options.reuseAgent && record.agent
|
|
785
|
+
? record.agent
|
|
786
|
+
: this.createSubAgentInstance(record, tools, cwd, options.forkContext);
|
|
787
|
+
record.agent = subAgent;
|
|
788
|
+
record.status = "running";
|
|
789
|
+
record.updatedAt = Date.now();
|
|
790
|
+
emit("running", undefined, `Running ${record.nickname} (${record.profile.name})...`);
|
|
791
|
+
let turnSummaryBuffer = "";
|
|
792
|
+
let turnHadToolCall = false;
|
|
793
|
+
let executedAnyTool = false;
|
|
794
|
+
try {
|
|
795
|
+
const childAbortSignal = composeAbortSignals([
|
|
796
|
+
options.abortSignal,
|
|
797
|
+
record.abortController.signal,
|
|
798
|
+
]);
|
|
799
|
+
for await (const event of subAgent.run(input, cwd, { abortSignal: childAbortSignal })) {
|
|
800
|
+
if (event.type === "text_delta") {
|
|
801
|
+
turnSummaryBuffer += event.content;
|
|
802
|
+
}
|
|
803
|
+
if (event.type === "tool_call_start"
|
|
804
|
+
|| event.type === "tool_call_delta"
|
|
805
|
+
|| event.type === "tool_call_end"
|
|
806
|
+
|| event.type === "tool_start") {
|
|
807
|
+
turnHadToolCall = true;
|
|
808
|
+
}
|
|
809
|
+
if (event.type === "tool_end") {
|
|
810
|
+
executedAnyTool = true;
|
|
811
|
+
record.toolNotes.push(`${event.name}: ${summarizeSubagentToolEnd(event)}`);
|
|
812
|
+
}
|
|
813
|
+
if (event.type === "turn_end" && event.usage) {
|
|
814
|
+
record.usage = mergeUsage(record.usage, event.usage);
|
|
815
|
+
}
|
|
816
|
+
if (event.type === "turn_end") {
|
|
817
|
+
const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
|
|
818
|
+
if (!turnHadToolCall && turnSummary) {
|
|
819
|
+
// Only the latest tool-free assistant turn is a candidate for the summary;
|
|
820
|
+
// earlier ones are intermediate "I'll do X next" reasoning, not the final answer.
|
|
821
|
+
record.summary = turnSummary;
|
|
822
|
+
}
|
|
823
|
+
turnSummaryBuffer = "";
|
|
824
|
+
turnHadToolCall = false;
|
|
825
|
+
}
|
|
826
|
+
record.updatedAt = Date.now();
|
|
827
|
+
emit("running", event);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
|
|
832
|
+
record.status = cancelled ? "cancelled" : "failed";
|
|
833
|
+
record.summary = sanitizeSubagentSummary(record.summary);
|
|
834
|
+
record.error = error?.message || String(error);
|
|
835
|
+
record.updatedAt = Date.now();
|
|
836
|
+
emit(record.status, undefined, record.error);
|
|
837
|
+
this.notifySubagentWaiters(record);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
record.summary = sanitizeSubagentSummary(record.summary);
|
|
841
|
+
if (needsExplicitFinalSummary(record, executedAnyTool)) {
|
|
842
|
+
await this.runSubagentFinalSummaryTurn(record, subAgent, cwd, options.abortSignal, emit);
|
|
843
|
+
}
|
|
844
|
+
record.status = "completed";
|
|
845
|
+
record.summary = sanitizeSubagentSummary(record.summary);
|
|
846
|
+
record.updatedAt = Date.now();
|
|
847
|
+
emit("completed", undefined, record.summary || `${record.nickname} completed`);
|
|
848
|
+
this.notifySubagentWaiters(record);
|
|
849
|
+
}
|
|
850
|
+
async runSubagentFinalSummaryTurn(record, subAgent, cwd, abortSignal, emit) {
|
|
851
|
+
const prompt = [
|
|
852
|
+
"Produce the final subagent summary now.",
|
|
853
|
+
"Do not call tools. Do not announce next steps or plans.",
|
|
854
|
+
"Use the evidence already gathered in this child thread.",
|
|
855
|
+
"Return concise findings with concrete file paths and explicit uncertainty.",
|
|
856
|
+
"Your entire response will be returned to the parent as the subagent's answer.",
|
|
857
|
+
].join("\n");
|
|
858
|
+
subAgent.injectSystemReminder([
|
|
859
|
+
"Subagent final-summary mode is active.",
|
|
860
|
+
"Do not call tools. Do not announce next steps.",
|
|
861
|
+
"Use only the evidence already gathered in this child thread.",
|
|
862
|
+
"Return the final concise summary as your complete response.",
|
|
863
|
+
].join("\n"));
|
|
864
|
+
let finalBuffer = "";
|
|
865
|
+
let finalHadToolCall = false;
|
|
866
|
+
const finalAbortSignal = composeAbortSignals([abortSignal, record.abortController.signal]);
|
|
867
|
+
for await (const event of subAgent.run(prompt, cwd, { abortSignal: finalAbortSignal })) {
|
|
868
|
+
if (event.type === "text_delta") {
|
|
869
|
+
finalBuffer += event.content;
|
|
870
|
+
}
|
|
871
|
+
if (event.type === "tool_call_start"
|
|
872
|
+
|| event.type === "tool_call_delta"
|
|
873
|
+
|| event.type === "tool_call_end"
|
|
874
|
+
|| event.type === "tool_start") {
|
|
875
|
+
finalHadToolCall = true;
|
|
876
|
+
}
|
|
877
|
+
if (event.type === "turn_end" && event.usage) {
|
|
878
|
+
record.usage = mergeUsage(record.usage, event.usage);
|
|
879
|
+
}
|
|
880
|
+
emit("running", event);
|
|
881
|
+
}
|
|
882
|
+
const finalSummary = sanitizeSubagentSummary(finalBuffer);
|
|
883
|
+
if (!finalHadToolCall && finalSummary) {
|
|
884
|
+
record.summary = finalSummary;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
createSubAgentInstance(record, tools, cwd, forkContext) {
|
|
888
|
+
const childToolNames = tools.map((tool) => tool.name);
|
|
889
|
+
const childSystemPrompt = buildSystemPrompt({
|
|
890
|
+
agentName: "Bubble",
|
|
891
|
+
configuredProvider: this.providerId || "none",
|
|
892
|
+
configuredModel: this.model || "none",
|
|
893
|
+
configuredModelId: this.model || "none",
|
|
894
|
+
thinkingLevel: this.thinkingLevel,
|
|
895
|
+
mode: "plan",
|
|
896
|
+
workingDir: cwd,
|
|
897
|
+
tools: childToolNames,
|
|
898
|
+
skills: childToolNames.includes("skill") ? this.skillSummaries : undefined,
|
|
899
|
+
memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
|
|
900
|
+
? this.memoryPrompt
|
|
901
|
+
: undefined,
|
|
902
|
+
agentProfilePrompt: [
|
|
903
|
+
`You are subagent ${record.nickname}. Your agent profile is ${record.profile.name}.`,
|
|
904
|
+
record.profile.prompt,
|
|
905
|
+
].filter(Boolean).join("\n\n"),
|
|
906
|
+
});
|
|
508
907
|
const subAgent = new Agent({
|
|
509
908
|
provider: this.provider,
|
|
510
909
|
providerId: this.providerId,
|
|
511
|
-
model: this.model,
|
|
910
|
+
model: record.profile.model && record.profile.model !== "inherit" ? record.profile.model : this.model,
|
|
512
911
|
tools,
|
|
513
912
|
temperature: this.temperature,
|
|
514
913
|
thinkingLevel: this.thinkingLevel,
|
|
515
914
|
mode: "plan",
|
|
516
|
-
maxTurns:
|
|
517
|
-
|
|
518
|
-
|
|
915
|
+
maxTurns: record.profile.maxTurns,
|
|
916
|
+
budgetLedger: this.budgetLedger,
|
|
917
|
+
budgetSource: { runId: record.runId, subAgentId: record.agentId },
|
|
918
|
+
systemPrompt: childSystemPrompt,
|
|
519
919
|
hooks: this.hookDefinitions,
|
|
520
920
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const toolNotes = [];
|
|
524
|
-
for await (const event of subAgent.run(input, cwd)) {
|
|
525
|
-
if (event.type === "text_delta") {
|
|
526
|
-
summary += event.content;
|
|
527
|
-
}
|
|
528
|
-
if (event.type === "tool_end") {
|
|
529
|
-
const detail = event.result.metadata?.reason
|
|
530
|
-
|| event.result.content.split("\n").find((line) => line.trim())?.trim()
|
|
531
|
-
|| "completed";
|
|
532
|
-
toolNotes.push(`${event.name}: ${detail}`);
|
|
533
|
-
}
|
|
921
|
+
if (forkContext) {
|
|
922
|
+
subAgent.messages = this.forkMessagesForSubagent(childSystemPrompt);
|
|
534
923
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (toolNotes.length > 0) {
|
|
545
|
-
lines.push("", "Subtask tools:");
|
|
546
|
-
for (const note of toolNotes.slice(0, 8)) {
|
|
547
|
-
lines.push(`- ${note}`);
|
|
924
|
+
return subAgent;
|
|
925
|
+
}
|
|
926
|
+
forkMessagesForSubagent(childSystemPrompt) {
|
|
927
|
+
const forked = this.messages
|
|
928
|
+
.filter((message) => {
|
|
929
|
+
if (message.role === "system" || message.role === "meta")
|
|
930
|
+
return false;
|
|
931
|
+
if (message.role === "assistant" && message.toolCalls?.some((call) => isSubagentLifecycleTool(call.name))) {
|
|
932
|
+
return false;
|
|
548
933
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
934
|
+
if (message.role === "tool" && message.metadata?.kind === "subagent") {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
return true;
|
|
938
|
+
})
|
|
939
|
+
.slice(-20);
|
|
940
|
+
return [{ role: "system", content: childSystemPrompt }, ...forked];
|
|
941
|
+
}
|
|
942
|
+
buildSubagentUpdate(record, status, event, message) {
|
|
553
943
|
return {
|
|
554
|
-
|
|
555
|
-
|
|
944
|
+
type: "subagent_update",
|
|
945
|
+
parentToolCallId: record.parentToolCallId,
|
|
946
|
+
runId: record.runId,
|
|
947
|
+
subAgentId: record.agentId,
|
|
948
|
+
agentName: record.profile.name,
|
|
949
|
+
nickname: record.nickname,
|
|
950
|
+
status,
|
|
951
|
+
childEvent: event,
|
|
952
|
+
summaryDelta: event?.type === "text_delta" ? event.content : undefined,
|
|
953
|
+
toolName: "name" in (event ?? {}) ? event.name : undefined,
|
|
954
|
+
toolCallId: "id" in (event ?? {}) ? event.id : undefined,
|
|
955
|
+
message,
|
|
556
956
|
metadata: {
|
|
557
|
-
kind: "
|
|
558
|
-
|
|
957
|
+
kind: "subagent",
|
|
958
|
+
runId: record.runId,
|
|
959
|
+
subagents: [{
|
|
960
|
+
subAgentId: record.agentId,
|
|
961
|
+
agentName: record.profile.name,
|
|
962
|
+
nickname: record.nickname,
|
|
963
|
+
status,
|
|
964
|
+
profileSource: record.profile.source,
|
|
965
|
+
task: record.task,
|
|
966
|
+
summary: record.summary,
|
|
967
|
+
toolNotes: record.toolNotes,
|
|
968
|
+
usage: record.usage,
|
|
969
|
+
error: record.error,
|
|
970
|
+
}],
|
|
559
971
|
},
|
|
560
972
|
};
|
|
561
973
|
}
|
|
974
|
+
queueSubagentUpdate(record, status, event, message) {
|
|
975
|
+
this.pendingSubagentUpdates.push({
|
|
976
|
+
id: record.parentToolCallId,
|
|
977
|
+
name: record.parentToolName,
|
|
978
|
+
update: this.buildSubagentUpdate(record, status, event, message),
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
drainSubagentToolUpdates() {
|
|
982
|
+
return this.pendingSubagentUpdates.splice(0, this.pendingSubagentUpdates.length)
|
|
983
|
+
.map((pending) => ({
|
|
984
|
+
type: "tool_update",
|
|
985
|
+
id: pending.id,
|
|
986
|
+
name: pending.name,
|
|
987
|
+
update: pending.update,
|
|
988
|
+
}));
|
|
989
|
+
}
|
|
990
|
+
activeSubagentNicknames() {
|
|
991
|
+
return [...this.subagentThreads.values()]
|
|
992
|
+
.filter((record) => !isFinalSubagentStatus(record.status))
|
|
993
|
+
.map((record) => record.nickname);
|
|
994
|
+
}
|
|
995
|
+
resolveSubagentTargets(agentIds) {
|
|
996
|
+
if (!agentIds || agentIds.length === 0) {
|
|
997
|
+
return [...this.subagentThreads.values()].filter((record) => record.status !== "closed");
|
|
998
|
+
}
|
|
999
|
+
return agentIds.map((id) => {
|
|
1000
|
+
const record = this.subagentThreads.get(id);
|
|
1001
|
+
if (!record) {
|
|
1002
|
+
throw new Error(`Unknown subagent: ${id}`);
|
|
1003
|
+
}
|
|
1004
|
+
return record;
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
notifySubagentWaiters(record) {
|
|
1008
|
+
for (const waiter of record.waiters) {
|
|
1009
|
+
waiter();
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
562
1012
|
maybeCompactResidentHistory() {
|
|
563
1013
|
if (this.messages.length === 0) {
|
|
564
1014
|
return;
|
|
@@ -604,7 +1054,7 @@ export class Agent {
|
|
|
604
1054
|
this.messages.push(message);
|
|
605
1055
|
this.onMessageAppend?.(message);
|
|
606
1056
|
}
|
|
607
|
-
async executeTool(toolCall, cwd, abortSignal) {
|
|
1057
|
+
async executeTool(toolCall, cwd, abortSignal, emitUpdate) {
|
|
608
1058
|
throwIfAborted(abortSignal);
|
|
609
1059
|
if (toolCall.name === "exit_plan_mode" && this._mode !== "plan") {
|
|
610
1060
|
return {
|
|
@@ -622,7 +1072,7 @@ export class Agent {
|
|
|
622
1072
|
if (this._mode === "plan" && !tool.readOnly) {
|
|
623
1073
|
return {
|
|
624
1074
|
content: `Error: Tool "${toolCall.name}" is not allowed in plan mode. ` +
|
|
625
|
-
`In plan mode you may only use read-only tools (read, glob, grep, lsp, web_search, web_fetch,
|
|
1075
|
+
`In plan mode you may only use read-only tools (read, glob, grep, lsp, web_search, web_fetch, spawn_agent, wait_agent, send_input, close_agent, skill, todo_write, tool_search, question, exit_plan_mode). ` +
|
|
626
1076
|
`To modify files or run commands, present your proposal and call exit_plan_mode so the user can review and approve it.`,
|
|
627
1077
|
isError: true,
|
|
628
1078
|
};
|
|
@@ -641,6 +1091,7 @@ export class Agent {
|
|
|
641
1091
|
abortSignal,
|
|
642
1092
|
toolCall: { id: toolCall.id, name: toolCall.name },
|
|
643
1093
|
agent: this,
|
|
1094
|
+
emitUpdate,
|
|
644
1095
|
});
|
|
645
1096
|
}
|
|
646
1097
|
catch (err) {
|
|
@@ -656,6 +1107,7 @@ function estimateResidentChars(messages) {
|
|
|
656
1107
|
for (const message of messages) {
|
|
657
1108
|
switch (message.role) {
|
|
658
1109
|
case "system":
|
|
1110
|
+
case "meta":
|
|
659
1111
|
total += message.content.length;
|
|
660
1112
|
break;
|
|
661
1113
|
case "tool":
|
|
@@ -690,6 +1142,34 @@ function throwIfAborted(signal) {
|
|
|
690
1142
|
throw reason;
|
|
691
1143
|
throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
|
|
692
1144
|
}
|
|
1145
|
+
function createUpdateQueue() {
|
|
1146
|
+
const items = [];
|
|
1147
|
+
let waiter;
|
|
1148
|
+
return {
|
|
1149
|
+
push(item) {
|
|
1150
|
+
items.push(item);
|
|
1151
|
+
this.wake();
|
|
1152
|
+
},
|
|
1153
|
+
drain() {
|
|
1154
|
+
return items.splice(0, items.length);
|
|
1155
|
+
},
|
|
1156
|
+
hasItems() {
|
|
1157
|
+
return items.length > 0;
|
|
1158
|
+
},
|
|
1159
|
+
wait() {
|
|
1160
|
+
if (items.length > 0)
|
|
1161
|
+
return Promise.resolve();
|
|
1162
|
+
return new Promise((resolve) => {
|
|
1163
|
+
waiter = resolve;
|
|
1164
|
+
});
|
|
1165
|
+
},
|
|
1166
|
+
wake() {
|
|
1167
|
+
const resolve = waiter;
|
|
1168
|
+
waiter = undefined;
|
|
1169
|
+
resolve?.();
|
|
1170
|
+
},
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
693
1173
|
function estimateToolPayloadChars(messages) {
|
|
694
1174
|
return messages.reduce((sum, message) => {
|
|
695
1175
|
if (message.role !== "tool") {
|
|
@@ -699,7 +1179,7 @@ function estimateToolPayloadChars(messages) {
|
|
|
699
1179
|
}, 0);
|
|
700
1180
|
}
|
|
701
1181
|
function countUserTurns(messages) {
|
|
702
|
-
return messages.reduce((count, message) => count + (message.role === "user"
|
|
1182
|
+
return messages.reduce((count, message) => count + (message.role === "user" ? 1 : 0), 0);
|
|
703
1183
|
}
|
|
704
1184
|
function getCurrentHeapUsed() {
|
|
705
1185
|
try {
|
|
@@ -709,3 +1189,76 @@ function getCurrentHeapUsed() {
|
|
|
709
1189
|
return 0;
|
|
710
1190
|
}
|
|
711
1191
|
}
|
|
1192
|
+
function isFinalSubagentStatus(status) {
|
|
1193
|
+
return status === "completed"
|
|
1194
|
+
|| status === "failed"
|
|
1195
|
+
|| status === "blocked"
|
|
1196
|
+
|| status === "cancelled"
|
|
1197
|
+
|| status === "closed";
|
|
1198
|
+
}
|
|
1199
|
+
function normalizeWaitTimeout(value) {
|
|
1200
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1201
|
+
return 30_000;
|
|
1202
|
+
return Math.max(100, Math.min(3_600_000, Math.floor(value)));
|
|
1203
|
+
}
|
|
1204
|
+
function isSubagentLifecycleTool(name) {
|
|
1205
|
+
return name === "subagent"
|
|
1206
|
+
|| name === "spawn_agent"
|
|
1207
|
+
|| name === "wait_agent"
|
|
1208
|
+
|| name === "send_input"
|
|
1209
|
+
|| name === "close_agent";
|
|
1210
|
+
}
|
|
1211
|
+
function sanitizeSubagentSummary(value) {
|
|
1212
|
+
return stripProviderProtocolArtifacts(value).trim();
|
|
1213
|
+
}
|
|
1214
|
+
function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
1215
|
+
// If the subagent actually invoked any tool, always solicit an explicit final
|
|
1216
|
+
// summary. We cannot tell from the stream alone whether a tool-free trailing
|
|
1217
|
+
// turn was the real answer or mid-thought narration ("Let me try X next:").
|
|
1218
|
+
// Asking the model to restate its findings is cheap and yields predictable,
|
|
1219
|
+
// clean output. (Profile-validation notes in `toolNotes` do not count as
|
|
1220
|
+
// actual tool executions.)
|
|
1221
|
+
if (executedAnyTool)
|
|
1222
|
+
return true;
|
|
1223
|
+
if (!record.summary)
|
|
1224
|
+
return false;
|
|
1225
|
+
if (isOnlyProviderProtocolArtifacts(record.summary))
|
|
1226
|
+
return true;
|
|
1227
|
+
if (/<\/?[||][^<>]*>/.test(record.summary))
|
|
1228
|
+
return true;
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
function summarizeSubagentToolEnd(event) {
|
|
1232
|
+
const metadata = (event.result.metadata ?? {});
|
|
1233
|
+
const reason = readString(metadata.reason);
|
|
1234
|
+
if (reason)
|
|
1235
|
+
return reason;
|
|
1236
|
+
const summary = readString(metadata.summary);
|
|
1237
|
+
if (summary)
|
|
1238
|
+
return summary;
|
|
1239
|
+
if (event.result.isError) {
|
|
1240
|
+
const firstLine = event.result.content.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1241
|
+
return firstLine ? truncateForNote(firstLine) : "failed";
|
|
1242
|
+
}
|
|
1243
|
+
const matches = readNumber(metadata.matches);
|
|
1244
|
+
const pattern = readString(metadata.pattern);
|
|
1245
|
+
const path = readString(metadata.path);
|
|
1246
|
+
if (matches !== undefined) {
|
|
1247
|
+
const target = pattern ? ` for ${pattern}` : "";
|
|
1248
|
+
const within = path ? ` in ${path}` : "";
|
|
1249
|
+
return `${matches} match${matches === 1 ? "" : "es"}${target}${within}`;
|
|
1250
|
+
}
|
|
1251
|
+
const kind = readString(metadata.kind);
|
|
1252
|
+
if (path)
|
|
1253
|
+
return kind ? `${kind} ${path}` : path;
|
|
1254
|
+
return event.result.status ?? "completed";
|
|
1255
|
+
}
|
|
1256
|
+
function readString(value) {
|
|
1257
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1258
|
+
}
|
|
1259
|
+
function readNumber(value) {
|
|
1260
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1261
|
+
}
|
|
1262
|
+
function truncateForNote(value, max = 200) {
|
|
1263
|
+
return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
|
|
1264
|
+
}
|