@bubblebrain-ai/bubble 0.0.4 → 0.0.6
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/budget-ledger.d.ts +20 -0
- package/dist/agent/budget-ledger.js +51 -0
- package/dist/agent/execution-governor.js +1 -1
- 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-size.d.ts +9 -0
- package/dist/agent/task-size.js +33 -0
- package/dist/agent/tool-intent.d.ts +1 -0
- package/dist/agent/tool-intent.js +1 -1
- package/dist/agent.d.ts +60 -1
- package/dist/agent.js +648 -55
- 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.js +12 -5
- package/dist/mcp/manager.js +1 -0
- package/dist/orchestrator/default-hooks.js +85 -35
- package/dist/orchestrator/hooks.d.ts +5 -3
- package/dist/prompt/compose.d.ts +1 -0
- package/dist/prompt/compose.js +11 -1
- package/dist/prompt/environment.js +23 -2
- package/dist/prompt/provider-prompts/deepseek.js +1 -2
- package/dist/prompt/provider-prompts/kimi.js +1 -2
- package/dist/prompt/reminders.d.ts +21 -2
- package/dist/prompt/reminders.js +53 -8
- package/dist/prompt/runtime.d.ts +1 -1
- package/dist/prompt/runtime.js +17 -23
- package/dist/provider-artifacts.d.ts +7 -0
- package/dist/provider-artifacts.js +60 -0
- package/dist/provider.d.ts +16 -8
- package/dist/provider.js +149 -34
- package/dist/session-log.js +3 -1
- 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.d.ts +2 -1
- package/dist/tools/bash.js +3 -1
- package/dist/tools/edit-apply.d.ts +25 -0
- package/dist/tools/edit-apply.js +228 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +75 -56
- 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/file-state.d.ts +25 -0
- package/dist/tools/file-state.js +52 -0
- package/dist/tools/glob.js +1 -0
- package/dist/tools/grep.js +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +9 -7
- 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.d.ts +2 -1
- package/dist/tools/read.js +6 -1
- 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.d.ts +4 -3
- package/dist/tools/write.js +135 -54
- package/dist/tui/display-history.d.ts +10 -1
- 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 +811 -274
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- 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 +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/types.d.ts +105 -10
- package/package.json +1 -1
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,22 +325,51 @@ 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.argumentsCorrupt) {
|
|
342
|
+
currentToolCall.argsCorrupt = true;
|
|
343
|
+
}
|
|
344
|
+
if (chunk.arguments) {
|
|
345
|
+
yield {
|
|
346
|
+
type: "tool_call_delta",
|
|
347
|
+
id: currentToolCall.id,
|
|
348
|
+
name: currentToolCall.name,
|
|
349
|
+
argumentsDelta: chunk.arguments,
|
|
350
|
+
arguments: currentToolCall.args,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
314
353
|
}
|
|
315
354
|
if (chunk.isEnd && currentToolCall) {
|
|
316
355
|
assistantMsg.toolCalls.push({
|
|
317
356
|
id: currentToolCall.id,
|
|
318
357
|
name: currentToolCall.name,
|
|
319
358
|
arguments: currentToolCall.args,
|
|
359
|
+
...(currentToolCall.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
320
360
|
});
|
|
321
|
-
|
|
361
|
+
yield {
|
|
362
|
+
type: "tool_call_end",
|
|
363
|
+
id: currentToolCall.id,
|
|
364
|
+
name: currentToolCall.name,
|
|
365
|
+
arguments: currentToolCall.args,
|
|
366
|
+
};
|
|
367
|
+
streamingToolCalls.delete(chunk.id);
|
|
322
368
|
}
|
|
323
369
|
break;
|
|
324
370
|
case "usage":
|
|
325
371
|
turnUsage = chunk.usage;
|
|
372
|
+
this.budgetLedger?.recordUsage(chunk.usage, this.budgetSource);
|
|
326
373
|
this.lastInputTokens = chunk.usage.promptTokens;
|
|
327
374
|
this.lastAnchorMessageCount = this.messages.length;
|
|
328
375
|
if (hookState.taskBudget) {
|
|
@@ -333,7 +380,10 @@ export class Agent {
|
|
|
333
380
|
}
|
|
334
381
|
break;
|
|
335
382
|
}
|
|
383
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
384
|
+
yield update;
|
|
336
385
|
}
|
|
386
|
+
throwIfAborted(abortSignal);
|
|
337
387
|
this.appendMessage(assistantMsg);
|
|
338
388
|
assistantAppended = true;
|
|
339
389
|
}
|
|
@@ -359,10 +409,14 @@ export class Agent {
|
|
|
359
409
|
for (let index = 0; index < assistantMsg.toolCalls.length; index++) {
|
|
360
410
|
const tc = assistantMsg.toolCalls[index];
|
|
361
411
|
try {
|
|
362
|
-
parsedCalls.push({
|
|
412
|
+
parsedCalls.push({
|
|
413
|
+
...tc,
|
|
414
|
+
parsedArgs: JSON.parse(tc.arguments),
|
|
415
|
+
...(tc.argsCorrupt ? { argsCorrupt: true } : {}),
|
|
416
|
+
});
|
|
363
417
|
}
|
|
364
418
|
catch {
|
|
365
|
-
parsedCalls.push({ ...tc, parsedArgs: {} });
|
|
419
|
+
parsedCalls.push({ ...tc, parsedArgs: {}, argsCorrupt: true });
|
|
366
420
|
}
|
|
367
421
|
}
|
|
368
422
|
const executedResults = [];
|
|
@@ -395,7 +449,41 @@ export class Agent {
|
|
|
395
449
|
yield { type: "tool_start", id: tc.id, name: tc.name, args: tc.parsedArgs };
|
|
396
450
|
const todosVersionBefore = this._todosVersion;
|
|
397
451
|
const modeVersionBefore = this._modeVersion;
|
|
398
|
-
|
|
452
|
+
const updateQueue = createUpdateQueue();
|
|
453
|
+
let result;
|
|
454
|
+
if (blockedResult) {
|
|
455
|
+
result = blockedResult;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const toolExecution = this.executeTool(tc, cwd, abortSignal, (update) => updateQueue.push(update));
|
|
459
|
+
let settled = false;
|
|
460
|
+
let resolved;
|
|
461
|
+
let rejected;
|
|
462
|
+
void toolExecution
|
|
463
|
+
.then((value) => {
|
|
464
|
+
resolved = value;
|
|
465
|
+
})
|
|
466
|
+
.catch((error) => {
|
|
467
|
+
rejected = error;
|
|
468
|
+
})
|
|
469
|
+
.finally(() => {
|
|
470
|
+
settled = true;
|
|
471
|
+
updateQueue.wake();
|
|
472
|
+
});
|
|
473
|
+
while (!settled || updateQueue.hasItems()) {
|
|
474
|
+
for (const update of updateQueue.drain()) {
|
|
475
|
+
yield { type: "tool_update", id: tc.id, name: tc.name, update };
|
|
476
|
+
}
|
|
477
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
478
|
+
yield update;
|
|
479
|
+
if (!settled) {
|
|
480
|
+
await updateQueue.wait();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (rejected)
|
|
484
|
+
throw rejected;
|
|
485
|
+
result = resolved ?? { content: `Error: Tool "${tc.name}" returned no result`, isError: true };
|
|
486
|
+
}
|
|
399
487
|
throwIfAborted(abortSignal);
|
|
400
488
|
await hookBus.runAfterToolCall({
|
|
401
489
|
agent: this,
|
|
@@ -422,6 +510,8 @@ export class Agent {
|
|
|
422
510
|
this.onToolResult?.(tc.name, result);
|
|
423
511
|
executedResults.push(result);
|
|
424
512
|
yield { type: "tool_end", id: tc.id, name: tc.name, result };
|
|
513
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
514
|
+
yield update;
|
|
425
515
|
if (this._todosVersion !== todosVersionBefore) {
|
|
426
516
|
yield { type: "todos_updated", todos: this.getTodos() };
|
|
427
517
|
}
|
|
@@ -466,6 +556,8 @@ export class Agent {
|
|
|
466
556
|
}
|
|
467
557
|
break;
|
|
468
558
|
}
|
|
559
|
+
for (const update of this.drainSubagentToolUpdates())
|
|
560
|
+
yield update;
|
|
469
561
|
yield { type: "agent_end" };
|
|
470
562
|
}
|
|
471
563
|
async recoverFromOverflow(attempt) {
|
|
@@ -507,62 +599,424 @@ export class Agent {
|
|
|
507
599
|
}
|
|
508
600
|
async runSubtask(input, cwd, options) {
|
|
509
601
|
const subtaskType = options?.subtaskType;
|
|
510
|
-
const
|
|
511
|
-
|
|
602
|
+
const profile = builtinAgentProfiles().find((item) => item.subtaskType === (subtaskType ?? "general_readonly"))
|
|
603
|
+
?? builtinAgentProfiles().find((item) => item.subtaskType === "general_readonly");
|
|
604
|
+
const run = await this.runSubAgent(input, cwd, {
|
|
605
|
+
profile,
|
|
606
|
+
runId: randomUUID(),
|
|
607
|
+
subAgentId: randomUUID(),
|
|
608
|
+
parentToolCallId: "task",
|
|
609
|
+
description: options?.description,
|
|
610
|
+
});
|
|
611
|
+
const lines = [
|
|
612
|
+
"Note: task is deprecated. Use spawn_agent with a named profile instead.",
|
|
613
|
+
`Subtask type: ${profile.subtaskType ?? "general_readonly"}`,
|
|
614
|
+
];
|
|
615
|
+
if (options?.description) {
|
|
616
|
+
lines.push(`Subtask description: ${options.description}`);
|
|
617
|
+
}
|
|
618
|
+
if (run.summary) {
|
|
619
|
+
lines.push("", "Subtask summary:", run.summary);
|
|
620
|
+
}
|
|
621
|
+
if (run.toolNotes.length > 0) {
|
|
622
|
+
lines.push("", "Subtask tools:", ...run.toolNotes.slice(0, 8).map((note) => `- ${note}`));
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
content: lines.join("\n"),
|
|
626
|
+
status: getSubtaskPolicy(subtaskType).resultStatus,
|
|
627
|
+
isError: run.status !== "completed",
|
|
628
|
+
metadata: {
|
|
629
|
+
kind: "subagent",
|
|
630
|
+
reason: `Subtask (${profile.subtaskType ?? "general_readonly"}) investigation completed.`,
|
|
631
|
+
subagents: [run],
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
async runSubAgent(input, cwd, options) {
|
|
636
|
+
const record = this.createSubagentThreadRecord({
|
|
637
|
+
profile: options.profile,
|
|
638
|
+
task: typeof input === "string" ? input : "(multimodal task)",
|
|
639
|
+
runId: options.runId,
|
|
640
|
+
agentId: options.subAgentId,
|
|
641
|
+
parentToolCallId: options.parentToolCallId,
|
|
642
|
+
parentToolName: "subagent",
|
|
643
|
+
nickname: options.nickname,
|
|
644
|
+
});
|
|
645
|
+
await this.runSubagentThread(record, input, cwd, {
|
|
646
|
+
approval: options.approval ?? options.profile.approval,
|
|
647
|
+
abortSignal: options.abortSignal,
|
|
648
|
+
forkContext: options.forkContext,
|
|
649
|
+
directEmit: options.emitUpdate,
|
|
650
|
+
});
|
|
651
|
+
return subagentResultFromThread(record);
|
|
652
|
+
}
|
|
653
|
+
async spawnSubAgent(input, cwd, options) {
|
|
654
|
+
const record = this.createSubagentThreadRecord({
|
|
655
|
+
profile: options.profile,
|
|
656
|
+
task: typeof input === "string" ? input : "(multimodal task)",
|
|
657
|
+
parentToolCallId: options.parentToolCallId,
|
|
658
|
+
parentToolName: "spawn_agent",
|
|
659
|
+
});
|
|
660
|
+
this.subagentThreads.set(record.agentId, record);
|
|
661
|
+
this.queueSubagentUpdate(record, "queued", undefined, `Queued ${record.nickname} (${record.profile.name})`);
|
|
662
|
+
record.promise = this.runSubagentThread(record, input, cwd, {
|
|
663
|
+
approval: options.approval ?? record.profile.approval,
|
|
664
|
+
abortSignal: options.abortSignal,
|
|
665
|
+
forkContext: options.forkContext,
|
|
666
|
+
queueUpdates: true,
|
|
667
|
+
});
|
|
668
|
+
void record.promise.finally(() => this.notifySubagentWaiters(record));
|
|
669
|
+
return snapshotSubagentThread(record);
|
|
670
|
+
}
|
|
671
|
+
async waitSubAgents(options = {}) {
|
|
672
|
+
const targets = this.resolveSubagentTargets(options.agentIds);
|
|
673
|
+
if (targets.length === 0)
|
|
674
|
+
return [];
|
|
675
|
+
const completed = targets.filter((record) => isFinalSubagentStatus(record.status));
|
|
676
|
+
if (completed.length > 0)
|
|
677
|
+
return completed.map(snapshotSubagentThread);
|
|
678
|
+
const timeoutMs = normalizeWaitTimeout(options.timeoutMs);
|
|
679
|
+
let waiter;
|
|
680
|
+
await Promise.race([
|
|
681
|
+
new Promise((resolve) => {
|
|
682
|
+
waiter = resolve;
|
|
683
|
+
for (const record of targets) {
|
|
684
|
+
record.waiters.add(resolve);
|
|
685
|
+
}
|
|
686
|
+
}).finally(() => {
|
|
687
|
+
if (waiter) {
|
|
688
|
+
for (const record of targets) {
|
|
689
|
+
record.waiters.delete(waiter);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}),
|
|
693
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
694
|
+
]);
|
|
695
|
+
const finished = targets.filter((record) => isFinalSubagentStatus(record.status));
|
|
696
|
+
return (finished.length > 0 ? finished : targets).map(snapshotSubagentThread);
|
|
697
|
+
}
|
|
698
|
+
async sendSubAgentInput(agentId, input, cwd, options = {}) {
|
|
699
|
+
const record = this.subagentThreads.get(agentId);
|
|
700
|
+
if (!record) {
|
|
701
|
+
throw new Error(`Unknown subagent: ${agentId}`);
|
|
702
|
+
}
|
|
703
|
+
if (record.status === "running" || record.status === "queued") {
|
|
704
|
+
if (!options.interrupt) {
|
|
705
|
+
throw new Error(`Subagent ${agentId} is still running. Call wait_agent first or pass interrupt:true.`);
|
|
706
|
+
}
|
|
707
|
+
record.abortController.abort(new AgentAbortError(`Subagent ${agentId} interrupted.`));
|
|
708
|
+
await record.promise?.catch(() => undefined);
|
|
709
|
+
record.abortController = new AbortController();
|
|
710
|
+
}
|
|
711
|
+
if (record.status === "closed") {
|
|
712
|
+
throw new Error(`Subagent ${agentId} is closed.`);
|
|
713
|
+
}
|
|
714
|
+
record.parentToolCallId = options.parentToolCallId ?? record.parentToolCallId;
|
|
715
|
+
record.parentToolName = "send_input";
|
|
716
|
+
record.task = typeof input === "string" ? input : "(multimodal task)";
|
|
717
|
+
record.summary = "";
|
|
718
|
+
record.toolNotes = [];
|
|
719
|
+
record.usage = undefined;
|
|
720
|
+
record.error = undefined;
|
|
721
|
+
record.updatedAt = Date.now();
|
|
722
|
+
record.promise = this.runSubagentThread(record, input, cwd, {
|
|
723
|
+
approval: record.profile.approval,
|
|
724
|
+
abortSignal: options.abortSignal,
|
|
725
|
+
queueUpdates: true,
|
|
726
|
+
reuseAgent: true,
|
|
727
|
+
});
|
|
728
|
+
void record.promise.finally(() => this.notifySubagentWaiters(record));
|
|
729
|
+
return snapshotSubagentThread(record);
|
|
730
|
+
}
|
|
731
|
+
async closeSubAgent(agentId) {
|
|
732
|
+
const record = this.subagentThreads.get(agentId);
|
|
733
|
+
if (!record) {
|
|
734
|
+
throw new Error(`Unknown subagent: ${agentId}`);
|
|
735
|
+
}
|
|
736
|
+
if (!isFinalSubagentStatus(record.status)) {
|
|
737
|
+
record.abortController.abort(new AgentAbortError(`Subagent ${agentId} closed.`));
|
|
738
|
+
await record.promise?.catch(() => undefined);
|
|
739
|
+
}
|
|
740
|
+
record.status = "closed";
|
|
741
|
+
record.updatedAt = Date.now();
|
|
742
|
+
this.queueSubagentUpdate(record, "cancelled", undefined, `${record.nickname} closed`);
|
|
743
|
+
this.notifySubagentWaiters(record);
|
|
744
|
+
return snapshotSubagentThread(record);
|
|
745
|
+
}
|
|
746
|
+
listSubAgents() {
|
|
747
|
+
return [...this.subagentThreads.values()].map(snapshotSubagentThread);
|
|
748
|
+
}
|
|
749
|
+
createSubagentThreadRecord(options) {
|
|
750
|
+
const now = Date.now();
|
|
751
|
+
const nickname = options.nickname ?? assignAgentNickname(options.profile, this.activeSubagentNicknames());
|
|
752
|
+
return {
|
|
753
|
+
agentId: options.agentId ?? randomUUID(),
|
|
754
|
+
runId: options.runId ?? randomUUID(),
|
|
755
|
+
nickname,
|
|
756
|
+
profile: options.profile,
|
|
757
|
+
parentToolCallId: options.parentToolCallId,
|
|
758
|
+
parentToolName: options.parentToolName,
|
|
759
|
+
status: "queued",
|
|
760
|
+
task: options.task,
|
|
761
|
+
summary: "",
|
|
762
|
+
toolNotes: [],
|
|
763
|
+
createdAt: now,
|
|
764
|
+
updatedAt: now,
|
|
765
|
+
abortController: new AbortController(),
|
|
766
|
+
waiters: new Set(),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async runSubagentThread(record, input, cwd, options) {
|
|
770
|
+
const emit = (status, event, message) => {
|
|
771
|
+
const update = this.buildSubagentUpdate(record, status, event, message);
|
|
772
|
+
options.directEmit?.(update);
|
|
773
|
+
if (options.queueUpdates) {
|
|
774
|
+
this.pendingSubagentUpdates.push({ id: record.parentToolCallId, name: record.parentToolName, update });
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const allTools = [...this.tools.values()];
|
|
778
|
+
const diagnostics = validateAgentProfileTools(allTools, record.profile, options.approval);
|
|
779
|
+
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
780
|
+
for (const diagnostic of diagnostics.filter((item) => item.severity === "warning")) {
|
|
781
|
+
record.toolNotes.push(`profile: ${diagnostic.message}`);
|
|
782
|
+
}
|
|
783
|
+
if (blockingDiagnostics.length > 0) {
|
|
784
|
+
record.status = "blocked";
|
|
785
|
+
record.error = blockingDiagnostics.map((diagnostic) => diagnostic.message).join("\n");
|
|
786
|
+
record.updatedAt = Date.now();
|
|
787
|
+
emit("blocked", undefined, record.error);
|
|
788
|
+
this.notifySubagentWaiters(record);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const tools = selectToolsForAgentProfile(allTools, record.profile, options.approval);
|
|
792
|
+
const subAgent = options.reuseAgent && record.agent
|
|
793
|
+
? record.agent
|
|
794
|
+
: this.createSubAgentInstance(record, tools, cwd, options.forkContext);
|
|
795
|
+
record.agent = subAgent;
|
|
796
|
+
record.status = "running";
|
|
797
|
+
record.updatedAt = Date.now();
|
|
798
|
+
emit("running", undefined, `Running ${record.nickname} (${record.profile.name})...`);
|
|
799
|
+
let turnSummaryBuffer = "";
|
|
800
|
+
let turnHadToolCall = false;
|
|
801
|
+
let executedAnyTool = false;
|
|
802
|
+
try {
|
|
803
|
+
const childAbortSignal = composeAbortSignals([
|
|
804
|
+
options.abortSignal,
|
|
805
|
+
record.abortController.signal,
|
|
806
|
+
]);
|
|
807
|
+
for await (const event of subAgent.run(input, cwd, { abortSignal: childAbortSignal })) {
|
|
808
|
+
if (event.type === "text_delta") {
|
|
809
|
+
turnSummaryBuffer += event.content;
|
|
810
|
+
}
|
|
811
|
+
if (event.type === "tool_call_start"
|
|
812
|
+
|| event.type === "tool_call_delta"
|
|
813
|
+
|| event.type === "tool_call_end"
|
|
814
|
+
|| event.type === "tool_start") {
|
|
815
|
+
turnHadToolCall = true;
|
|
816
|
+
}
|
|
817
|
+
if (event.type === "tool_end") {
|
|
818
|
+
executedAnyTool = true;
|
|
819
|
+
record.toolNotes.push(`${event.name}: ${summarizeSubagentToolEnd(event)}`);
|
|
820
|
+
}
|
|
821
|
+
if (event.type === "turn_end" && event.usage) {
|
|
822
|
+
record.usage = mergeUsage(record.usage, event.usage);
|
|
823
|
+
}
|
|
824
|
+
if (event.type === "turn_end") {
|
|
825
|
+
const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
|
|
826
|
+
if (!turnHadToolCall && turnSummary) {
|
|
827
|
+
// Only the latest tool-free assistant turn is a candidate for the summary;
|
|
828
|
+
// earlier ones are intermediate "I'll do X next" reasoning, not the final answer.
|
|
829
|
+
record.summary = turnSummary;
|
|
830
|
+
}
|
|
831
|
+
turnSummaryBuffer = "";
|
|
832
|
+
turnHadToolCall = false;
|
|
833
|
+
}
|
|
834
|
+
record.updatedAt = Date.now();
|
|
835
|
+
emit("running", event);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
|
|
840
|
+
record.status = cancelled ? "cancelled" : "failed";
|
|
841
|
+
record.summary = sanitizeSubagentSummary(record.summary);
|
|
842
|
+
record.error = error?.message || String(error);
|
|
843
|
+
record.updatedAt = Date.now();
|
|
844
|
+
emit(record.status, undefined, record.error);
|
|
845
|
+
this.notifySubagentWaiters(record);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
record.summary = sanitizeSubagentSummary(record.summary);
|
|
849
|
+
if (needsExplicitFinalSummary(record, executedAnyTool)) {
|
|
850
|
+
await this.runSubagentFinalSummaryTurn(record, subAgent, cwd, options.abortSignal, emit);
|
|
851
|
+
}
|
|
852
|
+
record.status = "completed";
|
|
853
|
+
record.summary = sanitizeSubagentSummary(record.summary);
|
|
854
|
+
record.updatedAt = Date.now();
|
|
855
|
+
emit("completed", undefined, record.summary || `${record.nickname} completed`);
|
|
856
|
+
this.notifySubagentWaiters(record);
|
|
857
|
+
}
|
|
858
|
+
async runSubagentFinalSummaryTurn(record, subAgent, cwd, abortSignal, emit) {
|
|
859
|
+
const prompt = [
|
|
860
|
+
"Produce the final subagent summary now.",
|
|
861
|
+
"Do not call tools. Do not announce next steps or plans.",
|
|
862
|
+
"Use the evidence already gathered in this child thread.",
|
|
863
|
+
"Return concise findings with concrete file paths and explicit uncertainty.",
|
|
864
|
+
"Your entire response will be returned to the parent as the subagent's answer.",
|
|
865
|
+
].join("\n");
|
|
866
|
+
subAgent.injectSystemReminder([
|
|
867
|
+
"Subagent final-summary mode is active.",
|
|
868
|
+
"Do not call tools. Do not announce next steps.",
|
|
869
|
+
"Use only the evidence already gathered in this child thread.",
|
|
870
|
+
"Return the final concise summary as your complete response.",
|
|
871
|
+
].join("\n"));
|
|
872
|
+
let finalBuffer = "";
|
|
873
|
+
let finalHadToolCall = false;
|
|
874
|
+
const finalAbortSignal = composeAbortSignals([abortSignal, record.abortController.signal]);
|
|
875
|
+
for await (const event of subAgent.run(prompt, cwd, { abortSignal: finalAbortSignal })) {
|
|
876
|
+
if (event.type === "text_delta") {
|
|
877
|
+
finalBuffer += event.content;
|
|
878
|
+
}
|
|
879
|
+
if (event.type === "tool_call_start"
|
|
880
|
+
|| event.type === "tool_call_delta"
|
|
881
|
+
|| event.type === "tool_call_end"
|
|
882
|
+
|| event.type === "tool_start") {
|
|
883
|
+
finalHadToolCall = true;
|
|
884
|
+
}
|
|
885
|
+
if (event.type === "turn_end" && event.usage) {
|
|
886
|
+
record.usage = mergeUsage(record.usage, event.usage);
|
|
887
|
+
}
|
|
888
|
+
emit("running", event);
|
|
889
|
+
}
|
|
890
|
+
const finalSummary = sanitizeSubagentSummary(finalBuffer);
|
|
891
|
+
if (!finalHadToolCall && finalSummary) {
|
|
892
|
+
record.summary = finalSummary;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
createSubAgentInstance(record, tools, cwd, forkContext) {
|
|
896
|
+
const childToolNames = tools.map((tool) => tool.name);
|
|
897
|
+
const childSystemPrompt = buildSystemPrompt({
|
|
898
|
+
agentName: "Bubble",
|
|
899
|
+
configuredProvider: this.providerId || "none",
|
|
900
|
+
configuredModel: this.model || "none",
|
|
901
|
+
configuredModelId: this.model || "none",
|
|
902
|
+
thinkingLevel: this.thinkingLevel,
|
|
903
|
+
mode: "plan",
|
|
904
|
+
workingDir: cwd,
|
|
905
|
+
tools: childToolNames,
|
|
906
|
+
skills: childToolNames.includes("skill") ? this.skillSummaries : undefined,
|
|
907
|
+
memoryPrompt: childToolNames.some((name) => name === "memory_search" || name === "memory_read_summary")
|
|
908
|
+
? this.memoryPrompt
|
|
909
|
+
: undefined,
|
|
910
|
+
agentProfilePrompt: [
|
|
911
|
+
`You are subagent ${record.nickname}. Your agent profile is ${record.profile.name}.`,
|
|
912
|
+
record.profile.prompt,
|
|
913
|
+
].filter(Boolean).join("\n\n"),
|
|
914
|
+
});
|
|
512
915
|
const subAgent = new Agent({
|
|
513
916
|
provider: this.provider,
|
|
514
917
|
providerId: this.providerId,
|
|
515
|
-
model: this.model,
|
|
918
|
+
model: record.profile.model && record.profile.model !== "inherit" ? record.profile.model : this.model,
|
|
516
919
|
tools,
|
|
517
920
|
temperature: this.temperature,
|
|
518
921
|
thinkingLevel: this.thinkingLevel,
|
|
519
922
|
mode: "plan",
|
|
520
|
-
maxTurns:
|
|
521
|
-
|
|
522
|
-
|
|
923
|
+
maxTurns: record.profile.maxTurns,
|
|
924
|
+
budgetLedger: this.budgetLedger,
|
|
925
|
+
budgetSource: { runId: record.runId, subAgentId: record.agentId },
|
|
926
|
+
systemPrompt: childSystemPrompt,
|
|
523
927
|
hooks: this.hookDefinitions,
|
|
524
928
|
});
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const toolNotes = [];
|
|
528
|
-
for await (const event of subAgent.run(input, cwd)) {
|
|
529
|
-
if (event.type === "text_delta") {
|
|
530
|
-
summary += event.content;
|
|
531
|
-
}
|
|
532
|
-
if (event.type === "tool_end") {
|
|
533
|
-
const detail = event.result.metadata?.reason
|
|
534
|
-
|| event.result.content.split("\n").find((line) => line.trim())?.trim()
|
|
535
|
-
|| "completed";
|
|
536
|
-
toolNotes.push(`${event.name}: ${detail}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const lines = [];
|
|
540
|
-
const trimmedSummary = summary.trim();
|
|
541
|
-
lines.push(`Subtask type: ${policy.type}`);
|
|
542
|
-
if (options?.description) {
|
|
543
|
-
lines.push(`Subtask description: ${options.description}`);
|
|
929
|
+
if (forkContext) {
|
|
930
|
+
subAgent.messages = this.forkMessagesForSubagent(childSystemPrompt);
|
|
544
931
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
932
|
+
return subAgent;
|
|
933
|
+
}
|
|
934
|
+
forkMessagesForSubagent(childSystemPrompt) {
|
|
935
|
+
const forked = this.messages
|
|
936
|
+
.filter((message) => {
|
|
937
|
+
if (message.role === "system" || message.role === "meta")
|
|
938
|
+
return false;
|
|
939
|
+
if (message.role === "assistant" && message.toolCalls?.some((call) => isSubagentLifecycleTool(call.name))) {
|
|
940
|
+
return false;
|
|
552
941
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
942
|
+
if (message.role === "tool" && message.metadata?.kind === "subagent") {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
})
|
|
947
|
+
.slice(-20);
|
|
948
|
+
return [{ role: "system", content: childSystemPrompt }, ...forked];
|
|
949
|
+
}
|
|
950
|
+
buildSubagentUpdate(record, status, event, message) {
|
|
557
951
|
return {
|
|
558
|
-
|
|
559
|
-
|
|
952
|
+
type: "subagent_update",
|
|
953
|
+
parentToolCallId: record.parentToolCallId,
|
|
954
|
+
runId: record.runId,
|
|
955
|
+
subAgentId: record.agentId,
|
|
956
|
+
agentName: record.profile.name,
|
|
957
|
+
nickname: record.nickname,
|
|
958
|
+
status,
|
|
959
|
+
childEvent: event,
|
|
960
|
+
summaryDelta: event?.type === "text_delta" ? event.content : undefined,
|
|
961
|
+
toolName: "name" in (event ?? {}) ? event.name : undefined,
|
|
962
|
+
toolCallId: "id" in (event ?? {}) ? event.id : undefined,
|
|
963
|
+
message,
|
|
560
964
|
metadata: {
|
|
561
|
-
kind: "
|
|
562
|
-
|
|
965
|
+
kind: "subagent",
|
|
966
|
+
runId: record.runId,
|
|
967
|
+
subagents: [{
|
|
968
|
+
subAgentId: record.agentId,
|
|
969
|
+
agentName: record.profile.name,
|
|
970
|
+
nickname: record.nickname,
|
|
971
|
+
status,
|
|
972
|
+
profileSource: record.profile.source,
|
|
973
|
+
task: record.task,
|
|
974
|
+
summary: record.summary,
|
|
975
|
+
toolNotes: record.toolNotes,
|
|
976
|
+
usage: record.usage,
|
|
977
|
+
error: record.error,
|
|
978
|
+
}],
|
|
563
979
|
},
|
|
564
980
|
};
|
|
565
981
|
}
|
|
982
|
+
queueSubagentUpdate(record, status, event, message) {
|
|
983
|
+
this.pendingSubagentUpdates.push({
|
|
984
|
+
id: record.parentToolCallId,
|
|
985
|
+
name: record.parentToolName,
|
|
986
|
+
update: this.buildSubagentUpdate(record, status, event, message),
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
drainSubagentToolUpdates() {
|
|
990
|
+
return this.pendingSubagentUpdates.splice(0, this.pendingSubagentUpdates.length)
|
|
991
|
+
.map((pending) => ({
|
|
992
|
+
type: "tool_update",
|
|
993
|
+
id: pending.id,
|
|
994
|
+
name: pending.name,
|
|
995
|
+
update: pending.update,
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
activeSubagentNicknames() {
|
|
999
|
+
return [...this.subagentThreads.values()]
|
|
1000
|
+
.filter((record) => !isFinalSubagentStatus(record.status))
|
|
1001
|
+
.map((record) => record.nickname);
|
|
1002
|
+
}
|
|
1003
|
+
resolveSubagentTargets(agentIds) {
|
|
1004
|
+
if (!agentIds || agentIds.length === 0) {
|
|
1005
|
+
return [...this.subagentThreads.values()].filter((record) => record.status !== "closed");
|
|
1006
|
+
}
|
|
1007
|
+
return agentIds.map((id) => {
|
|
1008
|
+
const record = this.subagentThreads.get(id);
|
|
1009
|
+
if (!record) {
|
|
1010
|
+
throw new Error(`Unknown subagent: ${id}`);
|
|
1011
|
+
}
|
|
1012
|
+
return record;
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
notifySubagentWaiters(record) {
|
|
1016
|
+
for (const waiter of record.waiters) {
|
|
1017
|
+
waiter();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
566
1020
|
maybeCompactResidentHistory() {
|
|
567
1021
|
if (this.messages.length === 0) {
|
|
568
1022
|
return;
|
|
@@ -608,7 +1062,7 @@ export class Agent {
|
|
|
608
1062
|
this.messages.push(message);
|
|
609
1063
|
this.onMessageAppend?.(message);
|
|
610
1064
|
}
|
|
611
|
-
async executeTool(toolCall, cwd, abortSignal) {
|
|
1065
|
+
async executeTool(toolCall, cwd, abortSignal, emitUpdate) {
|
|
612
1066
|
throwIfAborted(abortSignal);
|
|
613
1067
|
if (toolCall.name === "exit_plan_mode" && this._mode !== "plan") {
|
|
614
1068
|
return {
|
|
@@ -626,7 +1080,7 @@ export class Agent {
|
|
|
626
1080
|
if (this._mode === "plan" && !tool.readOnly) {
|
|
627
1081
|
return {
|
|
628
1082
|
content: `Error: Tool "${toolCall.name}" is not allowed in plan mode. ` +
|
|
629
|
-
`In plan mode you may only use read-only tools (read, glob, grep, lsp, web_search, web_fetch,
|
|
1083
|
+
`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). ` +
|
|
630
1084
|
`To modify files or run commands, present your proposal and call exit_plan_mode so the user can review and approve it.`,
|
|
631
1085
|
isError: true,
|
|
632
1086
|
};
|
|
@@ -638,6 +1092,25 @@ export class Agent {
|
|
|
638
1092
|
isError: true,
|
|
639
1093
|
};
|
|
640
1094
|
}
|
|
1095
|
+
if (toolCall.argsCorrupt) {
|
|
1096
|
+
return {
|
|
1097
|
+
content: `Error: The arguments for "${toolCall.name}" failed to parse as JSON, indicating the tool call was truncated or malformed mid-stream. ` +
|
|
1098
|
+
`Re-issue the call with valid JSON arguments; do not assume the previous attempt ran.`,
|
|
1099
|
+
isError: true,
|
|
1100
|
+
status: "blocked",
|
|
1101
|
+
metadata: { kind: "security", reason: "args_corrupt" },
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
const missingRequired = findMissingRequiredArgs(tool.parameters, toolCall.parsedArgs);
|
|
1105
|
+
if (missingRequired.length > 0) {
|
|
1106
|
+
return {
|
|
1107
|
+
content: `Error: Tool "${toolCall.name}" was called without required argument${missingRequired.length === 1 ? "" : "s"}: ${missingRequired.map((name) => `"${name}"`).join(", ")}. ` +
|
|
1108
|
+
`Re-issue the call with all required fields filled. Do not assume the previous attempt ran with default values.`,
|
|
1109
|
+
isError: true,
|
|
1110
|
+
status: "blocked",
|
|
1111
|
+
metadata: { kind: "security", reason: "missing_required_args", missing: missingRequired },
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
641
1114
|
try {
|
|
642
1115
|
return await tool.execute(toolCall.parsedArgs, {
|
|
643
1116
|
cwd,
|
|
@@ -645,6 +1118,7 @@ export class Agent {
|
|
|
645
1118
|
abortSignal,
|
|
646
1119
|
toolCall: { id: toolCall.id, name: toolCall.name },
|
|
647
1120
|
agent: this,
|
|
1121
|
+
emitUpdate,
|
|
648
1122
|
});
|
|
649
1123
|
}
|
|
650
1124
|
catch (err) {
|
|
@@ -655,11 +1129,29 @@ export class Agent {
|
|
|
655
1129
|
}
|
|
656
1130
|
}
|
|
657
1131
|
}
|
|
1132
|
+
function findMissingRequiredArgs(schema, args) {
|
|
1133
|
+
const required = schema?.required;
|
|
1134
|
+
if (!required || required.length === 0)
|
|
1135
|
+
return [];
|
|
1136
|
+
const missing = [];
|
|
1137
|
+
for (const name of required) {
|
|
1138
|
+
const value = args ? args[name] : undefined;
|
|
1139
|
+
// Empty strings/arrays are intentionally allowed — writing an empty file
|
|
1140
|
+
// or passing an empty list can be legitimate. Only undefined/null counts
|
|
1141
|
+
// as "missing", because the observed failure mode is `finalArgs: "{}"`
|
|
1142
|
+
// where the field is entirely absent.
|
|
1143
|
+
if (value === undefined || value === null) {
|
|
1144
|
+
missing.push(name);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return missing;
|
|
1148
|
+
}
|
|
658
1149
|
function estimateResidentChars(messages) {
|
|
659
1150
|
let total = 0;
|
|
660
1151
|
for (const message of messages) {
|
|
661
1152
|
switch (message.role) {
|
|
662
1153
|
case "system":
|
|
1154
|
+
case "meta":
|
|
663
1155
|
total += message.content.length;
|
|
664
1156
|
break;
|
|
665
1157
|
case "tool":
|
|
@@ -694,6 +1186,34 @@ function throwIfAborted(signal) {
|
|
|
694
1186
|
throw reason;
|
|
695
1187
|
throw new AgentAbortError(typeof reason === "string" ? reason : undefined);
|
|
696
1188
|
}
|
|
1189
|
+
function createUpdateQueue() {
|
|
1190
|
+
const items = [];
|
|
1191
|
+
let waiter;
|
|
1192
|
+
return {
|
|
1193
|
+
push(item) {
|
|
1194
|
+
items.push(item);
|
|
1195
|
+
this.wake();
|
|
1196
|
+
},
|
|
1197
|
+
drain() {
|
|
1198
|
+
return items.splice(0, items.length);
|
|
1199
|
+
},
|
|
1200
|
+
hasItems() {
|
|
1201
|
+
return items.length > 0;
|
|
1202
|
+
},
|
|
1203
|
+
wait() {
|
|
1204
|
+
if (items.length > 0)
|
|
1205
|
+
return Promise.resolve();
|
|
1206
|
+
return new Promise((resolve) => {
|
|
1207
|
+
waiter = resolve;
|
|
1208
|
+
});
|
|
1209
|
+
},
|
|
1210
|
+
wake() {
|
|
1211
|
+
const resolve = waiter;
|
|
1212
|
+
waiter = undefined;
|
|
1213
|
+
resolve?.();
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
697
1217
|
function estimateToolPayloadChars(messages) {
|
|
698
1218
|
return messages.reduce((sum, message) => {
|
|
699
1219
|
if (message.role !== "tool") {
|
|
@@ -703,7 +1223,7 @@ function estimateToolPayloadChars(messages) {
|
|
|
703
1223
|
}, 0);
|
|
704
1224
|
}
|
|
705
1225
|
function countUserTurns(messages) {
|
|
706
|
-
return messages.reduce((count, message) => count + (message.role === "user"
|
|
1226
|
+
return messages.reduce((count, message) => count + (message.role === "user" ? 1 : 0), 0);
|
|
707
1227
|
}
|
|
708
1228
|
function getCurrentHeapUsed() {
|
|
709
1229
|
try {
|
|
@@ -713,3 +1233,76 @@ function getCurrentHeapUsed() {
|
|
|
713
1233
|
return 0;
|
|
714
1234
|
}
|
|
715
1235
|
}
|
|
1236
|
+
function isFinalSubagentStatus(status) {
|
|
1237
|
+
return status === "completed"
|
|
1238
|
+
|| status === "failed"
|
|
1239
|
+
|| status === "blocked"
|
|
1240
|
+
|| status === "cancelled"
|
|
1241
|
+
|| status === "closed";
|
|
1242
|
+
}
|
|
1243
|
+
function normalizeWaitTimeout(value) {
|
|
1244
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1245
|
+
return 30_000;
|
|
1246
|
+
return Math.max(100, Math.min(3_600_000, Math.floor(value)));
|
|
1247
|
+
}
|
|
1248
|
+
function isSubagentLifecycleTool(name) {
|
|
1249
|
+
return name === "subagent"
|
|
1250
|
+
|| name === "spawn_agent"
|
|
1251
|
+
|| name === "wait_agent"
|
|
1252
|
+
|| name === "send_input"
|
|
1253
|
+
|| name === "close_agent";
|
|
1254
|
+
}
|
|
1255
|
+
function sanitizeSubagentSummary(value) {
|
|
1256
|
+
return stripProviderProtocolArtifacts(value).trim();
|
|
1257
|
+
}
|
|
1258
|
+
function needsExplicitFinalSummary(record, executedAnyTool) {
|
|
1259
|
+
// If the subagent actually invoked any tool, always solicit an explicit final
|
|
1260
|
+
// summary. We cannot tell from the stream alone whether a tool-free trailing
|
|
1261
|
+
// turn was the real answer or mid-thought narration ("Let me try X next:").
|
|
1262
|
+
// Asking the model to restate its findings is cheap and yields predictable,
|
|
1263
|
+
// clean output. (Profile-validation notes in `toolNotes` do not count as
|
|
1264
|
+
// actual tool executions.)
|
|
1265
|
+
if (executedAnyTool)
|
|
1266
|
+
return true;
|
|
1267
|
+
if (!record.summary)
|
|
1268
|
+
return false;
|
|
1269
|
+
if (isOnlyProviderProtocolArtifacts(record.summary))
|
|
1270
|
+
return true;
|
|
1271
|
+
if (/<\/?[||][^<>]*>/.test(record.summary))
|
|
1272
|
+
return true;
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
function summarizeSubagentToolEnd(event) {
|
|
1276
|
+
const metadata = (event.result.metadata ?? {});
|
|
1277
|
+
const reason = readString(metadata.reason);
|
|
1278
|
+
if (reason)
|
|
1279
|
+
return reason;
|
|
1280
|
+
const summary = readString(metadata.summary);
|
|
1281
|
+
if (summary)
|
|
1282
|
+
return summary;
|
|
1283
|
+
if (event.result.isError) {
|
|
1284
|
+
const firstLine = event.result.content.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1285
|
+
return firstLine ? truncateForNote(firstLine) : "failed";
|
|
1286
|
+
}
|
|
1287
|
+
const matches = readNumber(metadata.matches);
|
|
1288
|
+
const pattern = readString(metadata.pattern);
|
|
1289
|
+
const path = readString(metadata.path);
|
|
1290
|
+
if (matches !== undefined) {
|
|
1291
|
+
const target = pattern ? ` for ${pattern}` : "";
|
|
1292
|
+
const within = path ? ` in ${path}` : "";
|
|
1293
|
+
return `${matches} match${matches === 1 ? "" : "es"}${target}${within}`;
|
|
1294
|
+
}
|
|
1295
|
+
const kind = readString(metadata.kind);
|
|
1296
|
+
if (path)
|
|
1297
|
+
return kind ? `${kind} ${path}` : path;
|
|
1298
|
+
return event.result.status ?? "completed";
|
|
1299
|
+
}
|
|
1300
|
+
function readString(value) {
|
|
1301
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1302
|
+
}
|
|
1303
|
+
function readNumber(value) {
|
|
1304
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1305
|
+
}
|
|
1306
|
+
function truncateForNote(value, max = 200) {
|
|
1307
|
+
return value.length <= max ? value : `${value.slice(0, max - 3)}...`;
|
|
1308
|
+
}
|