@bubblebrain-ai/bubble 0.0.33 → 0.0.35
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 +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/profiles.d.ts +0 -2
- package/dist/agent/profiles.js +0 -3
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +7 -0
- package/dist/agent/workflow/control.js +22 -1
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +7 -43
- package/dist/agent.js +50 -176
- package/dist/config.d.ts +1 -3
- package/dist/config.js +6 -5
- package/dist/context/overflow.js +2 -0
- package/dist/main.js +9 -1
- package/dist/model-catalog.d.ts +1 -1
- package/dist/model-catalog.js +25 -18
- package/dist/orchestrator/default-hooks.js +7 -1
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +21 -7
- package/dist/prompt/task-reminders.d.ts +1 -0
- package/dist/prompt/task-reminders.js +24 -1
- package/dist/provider-ai-sdk.d.ts +56 -0
- package/dist/provider-ai-sdk.js +518 -0
- package/dist/provider-registry.js +49 -3
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +8 -13
- package/dist/provider.js +4 -0
- package/dist/slash-commands/commands.js +3 -7
- package/dist/slash-commands/types.d.ts +1 -1
- package/dist/tools/agent-lifecycle.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +35 -263
- package/dist/tui-ink/app.js +51 -13
- package/dist/tui-ink/approval/approval-dialog.js +1 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- package/dist/tui-ink/input-box.d.ts +1 -6
- package/dist/tui-ink/input-box.js +14 -35
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +3 -1
- package/dist/tui-ink/model-picker.js +25 -14
- package/dist/tui-ink/plan-confirm.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +3 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +16 -30
- package/dist/variant/variant-resolver.d.ts +13 -0
- package/dist/variant/variant-resolver.js +26 -0
- package/package.json +9 -3
|
@@ -50,16 +50,28 @@ globalThis.agent = async (prompt, opts) => {
|
|
|
50
50
|
const raw = await __agent(JSON.stringify({ prompt: String(prompt ?? ""), opts: opts || {} }));
|
|
51
51
|
return JSON.parse(raw);
|
|
52
52
|
};
|
|
53
|
-
globalThis.parallel = (thunks) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
let v = item;
|
|
58
|
-
for (const stage of stages) {
|
|
59
|
-
try { v = await stage(v, item, i); } catch (_e) { return null; }
|
|
53
|
+
globalThis.parallel = (thunks) => {
|
|
54
|
+
if (!Array.isArray(thunks)) throw new TypeError("parallel() expects an array of functions");
|
|
55
|
+
if (thunks.some((t) => typeof t !== "function")) {
|
|
56
|
+
throw new TypeError("parallel() expects an array of functions, not promises. Wrap each call: () => agent(...)");
|
|
60
57
|
}
|
|
61
|
-
return
|
|
62
|
-
}));
|
|
58
|
+
return Promise.all(thunks.map((t) => {
|
|
59
|
+
try { return Promise.resolve(t()).catch(() => null); } catch (_e) { return Promise.resolve(null); }
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
globalThis.pipeline = (items, ...stages) => {
|
|
63
|
+
if (!Array.isArray(items)) throw new TypeError("pipeline() expects an array as the first argument");
|
|
64
|
+
if (stages.some((s) => typeof s !== "function")) {
|
|
65
|
+
throw new TypeError("pipeline() stages must be functions: pipeline(items, item => ..., result => ...)");
|
|
66
|
+
}
|
|
67
|
+
return Promise.all(items.map(async (item, i) => {
|
|
68
|
+
let v = item;
|
|
69
|
+
for (const stage of stages) {
|
|
70
|
+
try { v = await stage(v, item, i); } catch (_e) { return null; }
|
|
71
|
+
}
|
|
72
|
+
return v;
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
63
75
|
globalThis.phase = (t) => __phase(String(t ?? ""));
|
|
64
76
|
globalThis.log = (m) => __log(String(m ?? ""));
|
|
65
77
|
globalThis.budget = {
|
|
@@ -71,18 +83,68 @@ globalThis.budget = {
|
|
|
71
83
|
/** Removes ambient nondeterminism so a run is reproducible (design §4.3). */
|
|
72
84
|
const DETERMINISM_GATING = `
|
|
73
85
|
delete globalThis.Date;
|
|
86
|
+
Object.defineProperty(globalThis, "Date", {
|
|
87
|
+
configurable: false,
|
|
88
|
+
get() { throw new Error("Workflow scripts must be deterministic: Date.now()/new Date() are unavailable — pass timestamps in via args"); },
|
|
89
|
+
});
|
|
74
90
|
delete globalThis.WeakRef;
|
|
75
91
|
delete globalThis.FinalizationRegistry;
|
|
76
|
-
Math.random = () => { throw new Error("
|
|
92
|
+
Math.random = () => { throw new Error("Workflow scripts must be deterministic: Math.random() is unavailable"); };
|
|
77
93
|
`;
|
|
78
94
|
/** Turns `export const meta = …` / `export function …` into plain declarations. */
|
|
79
95
|
function stripExports(script) {
|
|
80
96
|
return script.replace(/(^|\n)\s*export\s+(const|let|var|function|class|async)\b/g, "$1$2");
|
|
81
97
|
}
|
|
98
|
+
/** Lines injected before the script body in the wrapped source (keep in sync with buildWrappedWorkflowSource). */
|
|
99
|
+
const WRAPPER_LINE_OFFSET = 2;
|
|
100
|
+
/** The EXACT source runWorkflow executes — the syntax probe must compile this, not the raw script. */
|
|
101
|
+
export function buildWrappedWorkflowSource(script) {
|
|
102
|
+
return [
|
|
103
|
+
"globalThis.__wfdone = false; globalThis.__wfresult = null; globalThis.__wferror = null;",
|
|
104
|
+
"(async () => {",
|
|
105
|
+
stripExports(script),
|
|
106
|
+
"})().then(",
|
|
107
|
+
" (r) => { globalThis.__wfresult = r === undefined ? null : r; globalThis.__wfdone = true; },",
|
|
108
|
+
" (e) => { globalThis.__wferror = (e && e.message) ? String(e.message) : String(e); globalThis.__wfdone = true; }",
|
|
109
|
+
");",
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Submit-time syntax check: compiles the exact transformed source with
|
|
114
|
+
* QuickJS's compile-only flag (same engine, zero grammar divergence) so the
|
|
115
|
+
* model gets a teaching error as the immediate tool result instead of one
|
|
116
|
+
* full turn later through the background-run delivery path.
|
|
117
|
+
*/
|
|
118
|
+
export async function precompileWorkflowScript(script) {
|
|
119
|
+
const QuickJS = await getQuickJS();
|
|
120
|
+
const vm = QuickJS.newContext();
|
|
121
|
+
try {
|
|
122
|
+
const result = vm.evalCode(buildWrappedWorkflowSource(script), "workflow.js", { compileOnly: true });
|
|
123
|
+
if (result.error) {
|
|
124
|
+
const message = vm.dump(result.error);
|
|
125
|
+
result.error.dispose();
|
|
126
|
+
return { ok: false, error: `workflow script error: ${formatError(message)}` };
|
|
127
|
+
}
|
|
128
|
+
result.value.dispose();
|
|
129
|
+
return { ok: true };
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
try {
|
|
133
|
+
vm.dispose();
|
|
134
|
+
}
|
|
135
|
+
catch { /* teardown must not mask the verdict */ }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
82
138
|
export async function runWorkflow(options) {
|
|
83
139
|
const QuickJS = await getQuickJS();
|
|
84
140
|
const vm = QuickJS.newContext();
|
|
85
141
|
const pending = new Set();
|
|
142
|
+
// Deferreds for in-flight agent() calls. If the run ends while agents are
|
|
143
|
+
// still flying (user abort, stall, compute deadline), these were never
|
|
144
|
+
// settled and their VM-side handles are still alive — disposing the VM
|
|
145
|
+
// with them alive trips QuickJS's JS_FreeRuntime assertion and Aborts the
|
|
146
|
+
// (process-wide, cached) wasm module, poisoning every later workflow.
|
|
147
|
+
const liveDeferreds = new Set();
|
|
86
148
|
const state = { disposed: false };
|
|
87
149
|
let agentCount = 0;
|
|
88
150
|
const maxAgents = options.maxAgents ?? DEFAULT_MAX_AGENTS;
|
|
@@ -93,23 +155,14 @@ export async function runWorkflow(options) {
|
|
|
93
155
|
vm.runtime.setInterruptHandler(() => Date.now() - computeStart > computeDeadline);
|
|
94
156
|
try {
|
|
95
157
|
vm.unwrapResult(vm.evalCode(DETERMINISM_GATING)).dispose();
|
|
96
|
-
installHostFunctions(vm, options, pending, () => agentCount, () => { agentCount += 1; }, maxAgents, state);
|
|
158
|
+
installHostFunctions(vm, options, pending, () => agentCount, () => { agentCount += 1; }, maxAgents, state, liveDeferreds);
|
|
97
159
|
// args as a deterministic injected global.
|
|
98
160
|
const argsJson = JSON.stringify(options.args ?? null);
|
|
99
161
|
vm.unwrapResult(vm.evalCode(`globalThis.args = ${argsJson};`)).dispose();
|
|
100
162
|
vm.unwrapResult(vm.evalCode(PRELUDE)).dispose();
|
|
101
|
-
const
|
|
102
|
-
const wrapped = [
|
|
103
|
-
"globalThis.__wfdone = false; globalThis.__wfresult = null; globalThis.__wferror = null;",
|
|
104
|
-
"(async () => {",
|
|
105
|
-
body,
|
|
106
|
-
"})().then(",
|
|
107
|
-
" (r) => { globalThis.__wfresult = r === undefined ? null : r; globalThis.__wfdone = true; },",
|
|
108
|
-
" (e) => { globalThis.__wferror = (e && e.message) ? String(e.message) : String(e); globalThis.__wfdone = true; }",
|
|
109
|
-
");",
|
|
110
|
-
].join("\n");
|
|
163
|
+
const wrapped = buildWrappedWorkflowSource(options.script);
|
|
111
164
|
computeStart = Date.now();
|
|
112
|
-
const evalResult = vm.evalCode(wrapped);
|
|
165
|
+
const evalResult = vm.evalCode(wrapped, "workflow.js");
|
|
113
166
|
if (evalResult.error) {
|
|
114
167
|
const message = vm.dump(evalResult.error);
|
|
115
168
|
evalResult.error.dispose();
|
|
@@ -154,7 +207,17 @@ export async function runWorkflow(options) {
|
|
|
154
207
|
}
|
|
155
208
|
finally {
|
|
156
209
|
state.disposed = true;
|
|
157
|
-
|
|
210
|
+
for (const deferred of liveDeferreds) {
|
|
211
|
+
try {
|
|
212
|
+
deferred.dispose();
|
|
213
|
+
}
|
|
214
|
+
catch { /* already settled/disposed */ }
|
|
215
|
+
}
|
|
216
|
+
liveDeferreds.clear();
|
|
217
|
+
try {
|
|
218
|
+
vm.dispose();
|
|
219
|
+
}
|
|
220
|
+
catch { /* a dispose failure must not mask the run's result */ }
|
|
158
221
|
}
|
|
159
222
|
}
|
|
160
223
|
function abortRace(signal) {
|
|
@@ -164,10 +227,11 @@ function abortRace(signal) {
|
|
|
164
227
|
return Promise.resolve();
|
|
165
228
|
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
166
229
|
}
|
|
167
|
-
function installHostFunctions(vm, options, pending, getCount, bumpCount, maxAgents, state) {
|
|
230
|
+
function installHostFunctions(vm, options, pending, getCount, bumpCount, maxAgents, state, liveDeferreds) {
|
|
168
231
|
vm.newFunction("__agent", (specHandle) => {
|
|
169
232
|
const spec = JSON.parse(vm.getString(specHandle));
|
|
170
233
|
const deferred = vm.newPromise();
|
|
234
|
+
liveDeferreds.add(deferred);
|
|
171
235
|
// Settling the VM promise touches the context, which may have been disposed
|
|
172
236
|
// if the run was aborted while host work was still in flight — guard it.
|
|
173
237
|
const settle = (fn) => {
|
|
@@ -203,11 +267,14 @@ function installHostFunctions(vm, options, pending, getCount, bumpCount, maxAgen
|
|
|
203
267
|
})).finally(() => { pending.delete(p); });
|
|
204
268
|
pending.add(p);
|
|
205
269
|
}
|
|
206
|
-
deferred.settled.then(() => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
270
|
+
deferred.settled.then(() => {
|
|
271
|
+
liveDeferreds.delete(deferred);
|
|
272
|
+
if (!state.disposed)
|
|
273
|
+
try {
|
|
274
|
+
vm.runtime.executePendingJobs();
|
|
275
|
+
}
|
|
276
|
+
catch { /* disposed */ }
|
|
277
|
+
});
|
|
211
278
|
return deferred.handle;
|
|
212
279
|
}).consume((f) => vm.setProp(vm.global, "__agent", f));
|
|
213
280
|
vm.newFunction("__phase", (h) => {
|
|
@@ -231,7 +298,22 @@ function installHostFunctions(vm, options, pending, getCount, bumpCount, maxAgen
|
|
|
231
298
|
}
|
|
232
299
|
function formatError(value) {
|
|
233
300
|
if (value && typeof value === "object" && "message" in value) {
|
|
234
|
-
|
|
301
|
+
const obj = value;
|
|
302
|
+
const message = String(obj.message);
|
|
303
|
+
const line = extractScriptLine(obj);
|
|
304
|
+
return line === undefined ? message : `${message} (script line ${line})`;
|
|
235
305
|
}
|
|
236
306
|
return String(value);
|
|
237
307
|
}
|
|
308
|
+
/** Maps a QuickJS error location back onto the raw script's line numbering. */
|
|
309
|
+
function extractScriptLine(obj) {
|
|
310
|
+
if (typeof obj.lineNumber === "number" && Number.isFinite(obj.lineNumber)) {
|
|
311
|
+
return Math.max(1, obj.lineNumber - WRAPPER_LINE_OFFSET);
|
|
312
|
+
}
|
|
313
|
+
if (typeof obj.stack === "string") {
|
|
314
|
+
const match = obj.stack.match(/workflow\.js:(\d+)/);
|
|
315
|
+
if (match)
|
|
316
|
+
return Math.max(1, Number(match[1]) - WRAPPER_LINE_OFFSET);
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
package/dist/agent.d.ts
CHANGED
|
@@ -16,10 +16,9 @@ import type { SkillSummary } from "./skills/types.js";
|
|
|
16
16
|
import type { FileStateTracker } from "./tools/file-state.js";
|
|
17
17
|
export { AgentAbortError, SubagentAbortError } from "./agent/abort-errors.js";
|
|
18
18
|
export declare const INTERRUPTED_ASSISTANT_CONTENT = "Interrupted by user. The prior request was stopped and should not be resumed unless the user asks.";
|
|
19
|
-
/** Runtime tuning for the subagent scheduler
|
|
19
|
+
/** Runtime tuning for the subagent scheduler. */
|
|
20
20
|
export interface AgentSubagentRuntimeConfig {
|
|
21
21
|
maxActiveSubagents?: number;
|
|
22
|
-
childTokenCap?: number;
|
|
23
22
|
launchBurst?: number;
|
|
24
23
|
launchIntervalMs?: number;
|
|
25
24
|
rateLimitMaxAttempts?: number;
|
|
@@ -223,47 +222,6 @@ export declare class Agent {
|
|
|
223
222
|
}): Promise<SubagentThreadSnapshot>;
|
|
224
223
|
closeSubAgent(agentId: string): Promise<SubagentThreadSnapshot>;
|
|
225
224
|
listSubAgents(): SubagentThreadSnapshot[];
|
|
226
|
-
/**
|
|
227
|
-
* Homogeneous map fan-out (design §1.2): one profile, one template, N items.
|
|
228
|
-
* Every member goes through the same admission and the same scheduler
|
|
229
|
-
* dispatch as spawn_agent; the tool blocks until all members are final.
|
|
230
|
-
*/
|
|
231
|
-
runAgentTeam(cwd: string, options: {
|
|
232
|
-
profile: AgentProfile;
|
|
233
|
-
category?: string;
|
|
234
|
-
model?: string;
|
|
235
|
-
effort?: ThinkingLevel;
|
|
236
|
-
promptTemplate: string;
|
|
237
|
-
items: string[];
|
|
238
|
-
parentToolCallId: string;
|
|
239
|
-
emitUpdate?: (update: ToolUpdate) => void;
|
|
240
|
-
abortSignal?: AbortSignal;
|
|
241
|
-
approval?: "fail" | "disabled";
|
|
242
|
-
}): Promise<SubagentThreadSnapshot[]>;
|
|
243
|
-
/**
|
|
244
|
-
* Heterogeneous fan-out (design v2 §1.3): N independent specs, each with its
|
|
245
|
-
* own task, profile, and per-call model/effort, dispatched concurrently as a
|
|
246
|
-
* SINGLE tool call. Unlike runAgentTeam (one template over N items), members
|
|
247
|
-
* differ. Like the team, every member goes through the same scheduler
|
|
248
|
-
* dispatch and the tool blocks until all are final, returning in spec order.
|
|
249
|
-
* Keeping fan-out inside one tool call (rather than N parallel spawn_agent
|
|
250
|
-
* tool_calls) avoids the provider parallel-tool_call bug (Kimi 400 / lost
|
|
251
|
-
* responses).
|
|
252
|
-
*/
|
|
253
|
-
runAgentBatch(cwd: string, options: {
|
|
254
|
-
specs: Array<{
|
|
255
|
-
task: string;
|
|
256
|
-
profile: AgentProfile;
|
|
257
|
-
category?: string;
|
|
258
|
-
model?: string;
|
|
259
|
-
effort?: ThinkingLevel;
|
|
260
|
-
outputSchema?: unknown;
|
|
261
|
-
}>;
|
|
262
|
-
parentToolCallId: string;
|
|
263
|
-
emitUpdate?: (update: ToolUpdate) => void;
|
|
264
|
-
abortSignal?: AbortSignal;
|
|
265
|
-
approval?: "fail" | "disabled";
|
|
266
|
-
}): Promise<SubagentThreadSnapshot[]>;
|
|
267
225
|
/**
|
|
268
226
|
* Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
|
|
269
227
|
* a QuickJS sandbox. Each agent() call in the script becomes a real scheduled
|
|
@@ -280,6 +238,9 @@ export declare class Agent {
|
|
|
280
238
|
parentToolCallId: string;
|
|
281
239
|
emitUpdate?: (update: ToolUpdate) => void;
|
|
282
240
|
abortSignal?: AbortSignal;
|
|
241
|
+
ensureProfileTrusted?: (profile: AgentProfile) => Promise<{
|
|
242
|
+
content: string | unknown;
|
|
243
|
+
} | undefined>;
|
|
283
244
|
}): Promise<{
|
|
284
245
|
result: {
|
|
285
246
|
ok: true;
|
|
@@ -304,6 +265,9 @@ export declare class Agent {
|
|
|
304
265
|
title?: string;
|
|
305
266
|
parentToolCallId: string;
|
|
306
267
|
abortSignal?: AbortSignal;
|
|
268
|
+
ensureProfileTrusted?: (profile: AgentProfile) => Promise<{
|
|
269
|
+
content: string | unknown;
|
|
270
|
+
} | undefined>;
|
|
307
271
|
}): {
|
|
308
272
|
runId: string;
|
|
309
273
|
title: string;
|
package/dist/agent.js
CHANGED
|
@@ -21,7 +21,7 @@ import { appendOutputSchemaInstructions, buildSchemaCorrectionPrompt, validateSt
|
|
|
21
21
|
import { runWorkflow, WorkflowConcurrencyGate } from "./agent/workflow/runtime.js";
|
|
22
22
|
import { buildWorkflowDeliveryNotice } from "./agent/workflow/control.js";
|
|
23
23
|
import { getSubtaskPolicy } from "./agent/subtask-policy.js";
|
|
24
|
-
import { composeAbortSignals
|
|
24
|
+
import { composeAbortSignals } from "./agent/budget-ledger.js";
|
|
25
25
|
import { assignAgentNickname, builtinAgentProfiles, discoverAgentProfiles, findAgentProfile, validateAgentProfileTools } from "./agent/profiles.js";
|
|
26
26
|
import { snapshotSubagentThread, subagentResultFromThread } from "./agent/subagent-control.js";
|
|
27
27
|
import { SubagentStore } from "./agent/subagent-store.js";
|
|
@@ -179,7 +179,6 @@ export class Agent {
|
|
|
179
179
|
});
|
|
180
180
|
this.childRunner = new ChildRunner({
|
|
181
181
|
allTools: () => [...this.tools.values()],
|
|
182
|
-
budgetLedger: () => this.budgetLedger,
|
|
183
182
|
emit: (record, options, status, event, message) => this.emitSubagentLifecycle(record, options, status, event, message),
|
|
184
183
|
runLifecycleHook: (record, cwd, eventName, status, error, abortSignal) => this.runSubagentLifecycleHookFor(record, cwd, eventName, status, error, abortSignal),
|
|
185
184
|
finalizeBlocked: (record, error, options) => this.finalizeSubagentBlocked(record, error, options),
|
|
@@ -388,7 +387,7 @@ export class Agent {
|
|
|
388
387
|
this.messages.unshift(systemMessage);
|
|
389
388
|
}
|
|
390
389
|
async *run(userInput, cwd, options = {}) {
|
|
391
|
-
const abortSignal =
|
|
390
|
+
const abortSignal = options.abortSignal;
|
|
392
391
|
const inputController = options.inputController;
|
|
393
392
|
const traceContext = {
|
|
394
393
|
cwd,
|
|
@@ -911,14 +910,14 @@ export class Agent {
|
|
|
911
910
|
}
|
|
912
911
|
let tc = parsedCalls[index];
|
|
913
912
|
let blockedResult;
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
if (tc.name === "
|
|
913
|
+
// run_workflow must be the only tool call in its response: it starts
|
|
914
|
+
// a background orchestration whose result lands next turn, and
|
|
915
|
+
// sibling calls racing it defeat the serialized fan-out contract.
|
|
916
|
+
if (tc.name === "run_workflow" && parsedCalls.length > 1) {
|
|
918
917
|
blockedResult = {
|
|
919
918
|
content: [
|
|
920
|
-
"
|
|
921
|
-
"Re-issue
|
|
919
|
+
"run_workflow must be the only tool call in your response.",
|
|
920
|
+
"Re-issue run_workflow alone — one call, nothing else in the same message — and run other tools after it returns.",
|
|
922
921
|
].join(" "),
|
|
923
922
|
isError: true,
|
|
924
923
|
status: "blocked",
|
|
@@ -1580,141 +1579,6 @@ export class Agent {
|
|
|
1580
1579
|
.filter((record) => !record.workflowInternal)
|
|
1581
1580
|
.map((record) => this.snapshotSubagent(record));
|
|
1582
1581
|
}
|
|
1583
|
-
/**
|
|
1584
|
-
* Homogeneous map fan-out (design §1.2): one profile, one template, N items.
|
|
1585
|
-
* Every member goes through the same admission and the same scheduler
|
|
1586
|
-
* dispatch as spawn_agent; the tool blocks until all members are final.
|
|
1587
|
-
*/
|
|
1588
|
-
async runAgentTeam(cwd, options) {
|
|
1589
|
-
// Team budget pre-check (§6): items × per-member cap must fit in what
|
|
1590
|
-
// remains of a limited pool after the parent's reserve. On limit-free
|
|
1591
|
-
// hosts the only bound is the items cap enforced by the tool.
|
|
1592
|
-
const limit = this.budgetLedger?.poolLimit;
|
|
1593
|
-
if (this.budgetLedger && limit !== undefined) {
|
|
1594
|
-
const reserve = Math.floor(limit * PARENT_POOL_RESERVE_RATIO);
|
|
1595
|
-
const available = Math.max(0, (this.budgetLedger.remaining() ?? 0) - reserve);
|
|
1596
|
-
const memberCap = Math.min(this.subagentsConfig.childTokenCap ?? DEFAULT_CHILD_TOKEN_CAP, options.profile.maxTokens ?? Number.POSITIVE_INFINITY);
|
|
1597
|
-
const affordable = Math.floor(available / memberCap);
|
|
1598
|
-
if (options.items.length > affordable) {
|
|
1599
|
-
throw new Error([
|
|
1600
|
-
`agent_team rejected: the remaining token budget affords at most ${affordable} member${affordable === 1 ? "" : "s"}`,
|
|
1601
|
-
`but ${options.items.length} were requested. Reduce items or run smaller batches sequentially.`,
|
|
1602
|
-
].join(" "));
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
const approval = options.approval ?? options.profile.approval;
|
|
1606
|
-
const route = this.resolveRouteForSubagent(options.profile, options.category, { model: options.model, effort: options.effort });
|
|
1607
|
-
const records = options.items.map((item) => this.createSubagentThreadRecord({
|
|
1608
|
-
profile: options.profile,
|
|
1609
|
-
task: options.promptTemplate.split("{{item}}").join(item),
|
|
1610
|
-
parentToolCallId: options.parentToolCallId,
|
|
1611
|
-
parentToolName: "agent_team",
|
|
1612
|
-
route,
|
|
1613
|
-
}));
|
|
1614
|
-
const promises = records.map((record) => {
|
|
1615
|
-
this.subagentStore.set(record);
|
|
1616
|
-
const admissionError = this.admitSubagentProfile(record, approval);
|
|
1617
|
-
if (admissionError) {
|
|
1618
|
-
this.finalizeSubagentBlocked(record, admissionError, { directEmit: options.emitUpdate });
|
|
1619
|
-
return Promise.resolve();
|
|
1620
|
-
}
|
|
1621
|
-
// Member events flow through the team tool's own emitUpdate
|
|
1622
|
-
// (directEmit): while a foreground tool runs, the parent loop blocks in
|
|
1623
|
-
// updateQueue.wait and the queued channel is not drained until the tool
|
|
1624
|
-
// settles — queueUpdates would freeze the TUI for the whole team (§1.2).
|
|
1625
|
-
record.promise = this.dispatchSubagentRun(record, record.task, cwd, {
|
|
1626
|
-
approval,
|
|
1627
|
-
abortSignal: options.abortSignal,
|
|
1628
|
-
directEmit: options.emitUpdate,
|
|
1629
|
-
});
|
|
1630
|
-
void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
|
|
1631
|
-
return record.promise;
|
|
1632
|
-
});
|
|
1633
|
-
await Promise.all(promises);
|
|
1634
|
-
// The aggregated reply carries every member's full summary.
|
|
1635
|
-
for (const record of records)
|
|
1636
|
-
this.subagentStore.markDelivered(record.agentId);
|
|
1637
|
-
return records.map((record) => this.snapshotSubagent(record));
|
|
1638
|
-
}
|
|
1639
|
-
/**
|
|
1640
|
-
* Heterogeneous fan-out (design v2 §1.3): N independent specs, each with its
|
|
1641
|
-
* own task, profile, and per-call model/effort, dispatched concurrently as a
|
|
1642
|
-
* SINGLE tool call. Unlike runAgentTeam (one template over N items), members
|
|
1643
|
-
* differ. Like the team, every member goes through the same scheduler
|
|
1644
|
-
* dispatch and the tool blocks until all are final, returning in spec order.
|
|
1645
|
-
* Keeping fan-out inside one tool call (rather than N parallel spawn_agent
|
|
1646
|
-
* tool_calls) avoids the provider parallel-tool_call bug (Kimi 400 / lost
|
|
1647
|
-
* responses).
|
|
1648
|
-
*/
|
|
1649
|
-
async runAgentBatch(cwd, options) {
|
|
1650
|
-
// Budget pre-check mirrors runAgentTeam: members × per-member cap must fit
|
|
1651
|
-
// in what remains of a limited pool after the parent's reserve.
|
|
1652
|
-
const limit = this.budgetLedger?.poolLimit;
|
|
1653
|
-
if (this.budgetLedger && limit !== undefined) {
|
|
1654
|
-
const reserve = Math.floor(limit * PARENT_POOL_RESERVE_RATIO);
|
|
1655
|
-
const available = Math.max(0, (this.budgetLedger.remaining() ?? 0) - reserve);
|
|
1656
|
-
const memberCap = this.subagentsConfig.childTokenCap ?? DEFAULT_CHILD_TOKEN_CAP;
|
|
1657
|
-
const affordable = Math.floor(available / memberCap);
|
|
1658
|
-
if (options.specs.length > affordable) {
|
|
1659
|
-
throw new Error([
|
|
1660
|
-
`agent_batch rejected: the remaining token budget affords at most ${affordable} member${affordable === 1 ? "" : "s"}`,
|
|
1661
|
-
`but ${options.specs.length} were requested. Reduce specs or run smaller batches sequentially.`,
|
|
1662
|
-
].join(" "));
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
const records = options.specs.map((spec) => {
|
|
1666
|
-
const approval = options.approval ?? spec.profile.approval;
|
|
1667
|
-
const task = spec.outputSchema !== undefined
|
|
1668
|
-
? appendOutputSchemaInstructions(spec.task, spec.outputSchema)
|
|
1669
|
-
: spec.task;
|
|
1670
|
-
const record = this.createSubagentThreadRecord({
|
|
1671
|
-
profile: spec.profile,
|
|
1672
|
-
task,
|
|
1673
|
-
parentToolCallId: options.parentToolCallId,
|
|
1674
|
-
parentToolName: "agent_batch",
|
|
1675
|
-
route: this.resolveRouteForSubagent(spec.profile, spec.category, { model: spec.model, effort: spec.effort }),
|
|
1676
|
-
});
|
|
1677
|
-
return { record, approval, spec };
|
|
1678
|
-
});
|
|
1679
|
-
const promises = records.map(({ record, approval }) => {
|
|
1680
|
-
this.subagentStore.set(record);
|
|
1681
|
-
const admissionError = this.admitSubagentProfile(record, approval);
|
|
1682
|
-
if (admissionError) {
|
|
1683
|
-
this.finalizeSubagentBlocked(record, admissionError, { directEmit: options.emitUpdate });
|
|
1684
|
-
return Promise.resolve();
|
|
1685
|
-
}
|
|
1686
|
-
record.promise = this.dispatchSubagentRun(record, record.task, cwd, {
|
|
1687
|
-
approval,
|
|
1688
|
-
abortSignal: options.abortSignal,
|
|
1689
|
-
directEmit: options.emitUpdate,
|
|
1690
|
-
});
|
|
1691
|
-
void record.promise.finally(() => this.subagentStore.notifyWaiters(record));
|
|
1692
|
-
return record.promise;
|
|
1693
|
-
});
|
|
1694
|
-
await Promise.all(promises);
|
|
1695
|
-
// Structured-output validation + one corrective retry (design v2 §1.2):
|
|
1696
|
-
// a member whose schema'd summary does not validate gets a single
|
|
1697
|
-
// send_input correction, reusing the existing resume path.
|
|
1698
|
-
for (const { record, spec } of records) {
|
|
1699
|
-
if (spec.outputSchema === undefined)
|
|
1700
|
-
continue;
|
|
1701
|
-
if (record.status !== "completed")
|
|
1702
|
-
continue;
|
|
1703
|
-
if (validateStructuredSummary(record.summary, spec.outputSchema).ok)
|
|
1704
|
-
continue;
|
|
1705
|
-
const correction = buildSchemaCorrectionPrompt(spec.outputSchema, record.summary);
|
|
1706
|
-
try {
|
|
1707
|
-
await this.sendSubAgentInput(record.agentId, correction, cwd, { abortSignal: options.abortSignal });
|
|
1708
|
-
await record.promise?.catch(() => undefined);
|
|
1709
|
-
}
|
|
1710
|
-
catch {
|
|
1711
|
-
// resume failed; leave the original summary and surface the mismatch below
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
for (const { record } of records)
|
|
1715
|
-
this.subagentStore.markDelivered(record.agentId);
|
|
1716
|
-
return records.map(({ record }) => this.snapshotSubagent(record));
|
|
1717
|
-
}
|
|
1718
1582
|
/**
|
|
1719
1583
|
* Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
|
|
1720
1584
|
* a QuickJS sandbox. Each agent() call in the script becomes a real scheduled
|
|
@@ -1732,6 +1596,7 @@ export class Agent {
|
|
|
1732
1596
|
parentToolCallId: options.parentToolCallId,
|
|
1733
1597
|
abortSignal: options.abortSignal,
|
|
1734
1598
|
directEmit: options.emitUpdate,
|
|
1599
|
+
ensureProfileTrusted: options.ensureProfileTrusted,
|
|
1735
1600
|
});
|
|
1736
1601
|
}
|
|
1737
1602
|
/**
|
|
@@ -1766,6 +1631,7 @@ export class Agent {
|
|
|
1766
1631
|
parentToolCallId: options.parentToolCallId,
|
|
1767
1632
|
abortSignal: abortController.signal,
|
|
1768
1633
|
queueUpdates: true,
|
|
1634
|
+
ensureProfileTrusted: options.ensureProfileTrusted,
|
|
1769
1635
|
}).then((out) => {
|
|
1770
1636
|
record.agentCount = out.agentCount;
|
|
1771
1637
|
record.snapshots = out.snapshots;
|
|
@@ -1842,22 +1708,13 @@ export class Agent {
|
|
|
1842
1708
|
const profiles = discoverAgentProfiles(cwd, "both").profiles;
|
|
1843
1709
|
const runRecords = [];
|
|
1844
1710
|
const logs = [];
|
|
1845
|
-
// Per-run isolation (option C review): a
|
|
1846
|
-
//
|
|
1847
|
-
// a workflow can't starve interactive subagents.
|
|
1848
|
-
const poolLimit = this.budgetLedger?.poolLimit;
|
|
1849
|
-
const runTokenCeiling = poolLimit !== undefined
|
|
1850
|
-
? Math.max(1, Math.floor(poolLimit * (1 - PARENT_POOL_RESERVE_RATIO)))
|
|
1851
|
-
: Number.POSITIVE_INFINITY;
|
|
1852
|
-
const runSpent = () => runRecords.reduce((sum, r) => sum + (r.usage ? r.usage.promptTokens + r.usage.completionTokens : 0), 0);
|
|
1711
|
+
// Per-run isolation (option C review): a concurrency sub-cap below the
|
|
1712
|
+
// global limit so a workflow can't starve interactive subagents.
|
|
1853
1713
|
const interactiveReserve = 2;
|
|
1854
1714
|
const globalCap = Math.max(1, this.subagentsConfig.maxActiveSubagents ?? 8);
|
|
1855
1715
|
const workflowConcurrency = Math.max(1, globalCap - interactiveReserve);
|
|
1856
1716
|
const gate = new WorkflowConcurrencyGate(workflowConcurrency);
|
|
1857
1717
|
const dispatchAgent = async (spec) => {
|
|
1858
|
-
if (runSpent() >= runTokenCeiling) {
|
|
1859
|
-
return { ok: false, error: "workflow token ceiling reached; not launching more agents" };
|
|
1860
|
-
}
|
|
1861
1718
|
const baseProfile = findAgentProfile(profiles, spec.opts.agentType ?? "default")
|
|
1862
1719
|
?? findAgentProfile(profiles, "default");
|
|
1863
1720
|
if (!baseProfile)
|
|
@@ -1895,8 +1752,25 @@ export class Agent {
|
|
|
1895
1752
|
route,
|
|
1896
1753
|
workflowInternal: true,
|
|
1897
1754
|
});
|
|
1755
|
+
record.expectsStructuredOutput = spec.opts.schema !== undefined;
|
|
1756
|
+
const memberLabel = typeof spec.opts.label === "string" ? spec.opts.label.trim().slice(0, 40) : "";
|
|
1757
|
+
if (memberLabel)
|
|
1758
|
+
record.nickname = memberLabel;
|
|
1898
1759
|
runRecords.push(record);
|
|
1899
1760
|
this.subagentStore.set(record);
|
|
1761
|
+
// Project-local profiles pass the same first-use trust gate as
|
|
1762
|
+
// spawn_agent: a .bubble/agents profile must never gain a side door
|
|
1763
|
+
// into execution just because a script named it (Codex review on #58).
|
|
1764
|
+
// Checked AFTER the record exists so a rejected member still shows up
|
|
1765
|
+
// in the run's counts/snapshots as blocked instead of vanishing.
|
|
1766
|
+
if (options.ensureProfileTrusted) {
|
|
1767
|
+
const blocked = await options.ensureProfileTrusted(baseProfile);
|
|
1768
|
+
if (blocked) {
|
|
1769
|
+
const message = typeof blocked.content === "string" ? blocked.content : `profile "${baseProfile.name}" requires user approval`;
|
|
1770
|
+
this.finalizeSubagentBlocked(record, message, { directEmit: options.directEmit, queueUpdates: options.queueUpdates });
|
|
1771
|
+
return { ok: false, error: message };
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1900
1774
|
const admissionError = this.admitSubagentProfile(record, profile.approval);
|
|
1901
1775
|
if (admissionError) {
|
|
1902
1776
|
this.finalizeSubagentBlocked(record, admissionError, { directEmit: options.directEmit, queueUpdates: options.queueUpdates });
|
|
@@ -1925,8 +1799,11 @@ export class Agent {
|
|
|
1925
1799
|
return { ok: true, value: record.summary };
|
|
1926
1800
|
}
|
|
1927
1801
|
// Structured output: validate, one corrective retry, then fall back to raw.
|
|
1802
|
+
// The schema: log lines are the telemetry that decides whether the
|
|
1803
|
+
// deferred terminate-style structured-output tool ever gets reopened.
|
|
1928
1804
|
let validated = validateStructuredSummary(record.summary, spec.opts.schema);
|
|
1929
1805
|
if (!validated.ok) {
|
|
1806
|
+
logs.push(`schema: ${record.nickname} first output failed validation; sending one corrective retry`);
|
|
1930
1807
|
try {
|
|
1931
1808
|
await this.sendSubAgentInput(record.agentId, buildSchemaCorrectionPrompt(spec.opts.schema, record.summary), cwd, { abortSignal: options.abortSignal });
|
|
1932
1809
|
await record.promise?.catch(() => undefined);
|
|
@@ -1935,6 +1812,9 @@ export class Agent {
|
|
|
1935
1812
|
catch {
|
|
1936
1813
|
// resume failed; fall through to raw summary
|
|
1937
1814
|
}
|
|
1815
|
+
logs.push(validated.ok
|
|
1816
|
+
? `schema: ${record.nickname} corrective retry produced valid output`
|
|
1817
|
+
: `schema: ${record.nickname} corrective retry still invalid; returning raw summary`);
|
|
1938
1818
|
}
|
|
1939
1819
|
return { ok: true, value: validated.ok ? validated.value : record.summary };
|
|
1940
1820
|
};
|
|
@@ -1945,14 +1825,11 @@ export class Agent {
|
|
|
1945
1825
|
onLog: (message) => logs.push(message),
|
|
1946
1826
|
onPhase: (title) => logs.push(`— phase: ${title} —`),
|
|
1947
1827
|
budget: {
|
|
1948
|
-
|
|
1828
|
+
// The ledger is pure accounting (no pool limit); scripts see an
|
|
1829
|
+
// unlimited budget unless a future host contract reintroduces one.
|
|
1830
|
+
total: null,
|
|
1949
1831
|
spent: () => runRecords.reduce((sum, r) => sum + (r.usage ? r.usage.promptTokens + r.usage.completionTokens : 0), 0),
|
|
1950
|
-
remaining: () =>
|
|
1951
|
-
const limit = this.budgetLedger?.poolLimit;
|
|
1952
|
-
if (limit === undefined)
|
|
1953
|
-
return Number.POSITIVE_INFINITY;
|
|
1954
|
-
return Math.max(0, (this.budgetLedger?.remaining() ?? 0));
|
|
1955
|
-
},
|
|
1832
|
+
remaining: () => Number.POSITIVE_INFINITY,
|
|
1956
1833
|
},
|
|
1957
1834
|
signal: options.abortSignal,
|
|
1958
1835
|
});
|
|
@@ -2013,13 +1890,6 @@ export class Agent {
|
|
|
2013
1890
|
dispatchSubagentRun(record, input, cwd, options) {
|
|
2014
1891
|
record.status = "queued";
|
|
2015
1892
|
record.updatedAt = Date.now();
|
|
2016
|
-
record.tokenCap = computeChildTokenCap({
|
|
2017
|
-
ledger: this.budgetLedger,
|
|
2018
|
-
subAgentId: record.agentId,
|
|
2019
|
-
activeChildren: this.subagentScheduler.activeCount(),
|
|
2020
|
-
configCap: this.subagentsConfig.childTokenCap,
|
|
2021
|
-
profileMaxTokens: record.profile.maxTokens,
|
|
2022
|
-
});
|
|
2023
1893
|
const queueSignal = composeAbortSignals([options.abortSignal, record.abortController.signal]);
|
|
2024
1894
|
return this.subagentScheduler.dispatch({
|
|
2025
1895
|
agentId: record.agentId,
|
|
@@ -2028,7 +1898,7 @@ export class Agent {
|
|
|
2028
1898
|
run: (ctx) => this.runSubagentThread(record, input, cwd, { ...options, attempt: ctx.attempt }),
|
|
2029
1899
|
onCancelledWhileQueued: (reason) => {
|
|
2030
1900
|
record.status = "cancelled";
|
|
2031
|
-
record.finalReason = classifySubagentAbortReason(reason, options.abortSignal
|
|
1901
|
+
record.finalReason = classifySubagentAbortReason(reason, options.abortSignal);
|
|
2032
1902
|
record.error = reason instanceof Error ? reason.message : reason ? String(reason) : "Cancelled while queued.";
|
|
2033
1903
|
record.updatedAt = Date.now();
|
|
2034
1904
|
// The run never started, so no SubagentStart fired and no SubagentStop follows.
|
|
@@ -2555,13 +2425,11 @@ function estimateResidentChars(messages) {
|
|
|
2555
2425
|
return total;
|
|
2556
2426
|
}
|
|
2557
2427
|
function appendProviderContentBlock(message, provider, block) {
|
|
2558
|
-
|
|
2559
|
-
return;
|
|
2560
|
-
const current = message.providerMetadata?.anthropic?.contentBlocks ?? [];
|
|
2428
|
+
const current = message.providerMetadata?.[provider]?.contentBlocks ?? [];
|
|
2561
2429
|
message.providerMetadata = {
|
|
2562
2430
|
...message.providerMetadata,
|
|
2563
|
-
|
|
2564
|
-
...message.providerMetadata?.
|
|
2431
|
+
[provider]: {
|
|
2432
|
+
...message.providerMetadata?.[provider],
|
|
2565
2433
|
contentBlocks: [...current, cloneProviderRawContentBlock(block)],
|
|
2566
2434
|
},
|
|
2567
2435
|
};
|
|
@@ -2782,5 +2650,11 @@ function isSubagentLifecycleTool(name) {
|
|
|
2782
2650
|
|| name === "send_input"
|
|
2783
2651
|
|| name === "close_agent"
|
|
2784
2652
|
|| name === "list_agents"
|
|
2785
|
-
|| name === "
|
|
2653
|
+
|| name === "run_workflow"
|
|
2654
|
+
|| name === "wait_workflow"
|
|
2655
|
+
// Legacy names: still present in transcripts recorded before the tools
|
|
2656
|
+
// were removed (2026-07-06); forked children must not inherit their
|
|
2657
|
+
// dangling tool_calls either.
|
|
2658
|
+
|| name === "agent_team"
|
|
2659
|
+
|| name === "agent_batch";
|
|
2786
2660
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -36,8 +36,6 @@ export interface UserConfigData {
|
|
|
36
36
|
export interface SubagentsUserConfig {
|
|
37
37
|
/** Global cap on concurrently running children. Default 8. */
|
|
38
38
|
maxActiveSubagents?: number;
|
|
39
|
-
/** Absolute per-child soft token cap. Default 200000. */
|
|
40
|
-
childTokenCap?: number;
|
|
41
39
|
}
|
|
42
40
|
export declare class UserConfig {
|
|
43
41
|
private data;
|
|
@@ -66,7 +64,7 @@ export declare class UserConfig {
|
|
|
66
64
|
getAgentCategories(): AgentCategoriesConfig;
|
|
67
65
|
getSubagents(): SubagentsUserConfig;
|
|
68
66
|
}
|
|
69
|
-
export declare function shouldProbeTerminalTheme(
|
|
67
|
+
export declare function shouldProbeTerminalTheme(_config: ThemeConfig): boolean;
|
|
70
68
|
export declare function effectiveThemeModeForTerminal(config: ThemeConfig, detectedTheme: Exclude<ThemeMode, "auto">): ThemeMode;
|
|
71
69
|
/** Mask an API key for safe display. */
|
|
72
70
|
export declare function maskKey(key: string): string;
|