@bubblebrain-ai/bubble 0.0.27 → 0.0.29

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.
Files changed (60) hide show
  1. package/README.md +21 -0
  2. package/dist/agent/categories.d.ts +2 -0
  3. package/dist/agent/categories.js +4 -0
  4. package/dist/agent/child-runner.d.ts +5 -1
  5. package/dist/agent/child-runner.js +35 -2
  6. package/dist/agent/profiles.js +3 -0
  7. package/dist/agent/structured-output.d.ts +37 -0
  8. package/dist/agent/structured-output.js +193 -0
  9. package/dist/agent/subagent-control.d.ts +3 -0
  10. package/dist/agent/subagent-scheduler.d.ts +10 -0
  11. package/dist/agent/subagent-scheduler.js +31 -0
  12. package/dist/agent/workflow/control.d.ts +37 -0
  13. package/dist/agent/workflow/control.js +20 -0
  14. package/dist/agent/workflow/errors.d.ts +16 -0
  15. package/dist/agent/workflow/errors.js +24 -0
  16. package/dist/agent/workflow/runtime.d.ts +75 -0
  17. package/dist/agent/workflow/runtime.js +237 -0
  18. package/dist/agent.d.ts +105 -0
  19. package/dist/agent.js +425 -17
  20. package/dist/context/compact-llm.d.ts +10 -1
  21. package/dist/context/compact-llm.js +13 -5
  22. package/dist/context/compact.d.ts +30 -0
  23. package/dist/context/compact.js +34 -17
  24. package/dist/network/provider-transport.d.ts +9 -0
  25. package/dist/network/provider-transport.js +19 -1
  26. package/dist/provider-anthropic.js +13 -0
  27. package/dist/provider.d.ts +14 -0
  28. package/dist/provider.js +24 -0
  29. package/dist/session.d.ts +16 -0
  30. package/dist/session.js +33 -1
  31. package/dist/slash-commands/commands.js +47 -1
  32. package/dist/slash-commands/types.d.ts +16 -1
  33. package/dist/tools/agent-lifecycle.d.ts +6 -0
  34. package/dist/tools/agent-lifecycle.js +285 -0
  35. package/dist/tools/child-tools.d.ts +10 -0
  36. package/dist/tools/child-tools.js +12 -0
  37. package/dist/tools/read.d.ts +1 -1
  38. package/dist/tools/read.js +9 -0
  39. package/dist/tui/image-display.d.ts +6 -0
  40. package/dist/tui/image-display.js +26 -1
  41. package/dist/tui-ink/app.js +84 -6
  42. package/dist/tui-ink/compaction-progress.d.ts +19 -0
  43. package/dist/tui-ink/compaction-progress.js +74 -0
  44. package/dist/tui-ink/input-box.d.ts +7 -1
  45. package/dist/tui-ink/input-box.js +48 -15
  46. package/dist/tui-ink/markdown.d.ts +18 -0
  47. package/dist/tui-ink/markdown.js +172 -16
  48. package/dist/tui-ink/message-list.js +38 -94
  49. package/dist/tui-ink/run.js +5 -0
  50. package/dist/tui-ink/subagent-inspector.d.ts +17 -0
  51. package/dist/tui-ink/subagent-inspector.js +189 -0
  52. package/dist/tui-ink/subagent-view.d.ts +47 -0
  53. package/dist/tui-ink/subagent-view.js +163 -0
  54. package/dist/tui-ink/terminal-env.d.ts +15 -0
  55. package/dist/tui-ink/terminal-env.js +22 -0
  56. package/dist/tui-ink/use-terminal-size.js +33 -6
  57. package/dist/tui-ink/width.d.ts +18 -0
  58. package/dist/tui-ink/width.js +130 -0
  59. package/dist/types.d.ts +35 -0
  60. package/package.json +2 -1
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Workflow runtime (option C) — executes an LLM-authored JS orchestration
3
+ * script in a QuickJS-wasm sandbox and returns its final value.
4
+ *
5
+ * Engine decision (proven empirically, node + bun): the SYNC variant with a
6
+ * `newPromise` deferred-promise bridge — NOT asyncify. Asyncify serializes
7
+ * parallel() and corrupts the VM on the 2nd agent failure; the sync+newPromise
8
+ * bridge gives true concurrency AND clean error propagation across many
9
+ * failures. The host drives the VM job queue via executePendingJobs().
10
+ *
11
+ * The script's only capability is agent(); it has no fs/shell/net/clock/RNG
12
+ * (determinism gating below). parallel/pipeline/phase/log/budget are a JS
13
+ * prelude over the single host function __agent plus a few host callbacks.
14
+ */
15
+ import { getQuickJS } from "quickjs-emscripten";
16
+ /**
17
+ * Async semaphore bounding how many workflow agents run concurrently — kept
18
+ * below the global scheduler cap so interactive subagents always have slots
19
+ * (option C review M2/M5). Permits are acquired/released only around a leaf
20
+ * agent dispatch, never across parallel/pipeline composition (no deadlock).
21
+ */
22
+ export class WorkflowConcurrencyGate {
23
+ capacity;
24
+ active = 0;
25
+ waiters = [];
26
+ constructor(capacity) {
27
+ this.capacity = capacity;
28
+ }
29
+ async acquire() {
30
+ if (this.active < this.capacity) {
31
+ this.active += 1;
32
+ return;
33
+ }
34
+ await new Promise((resolve) => this.waiters.push(resolve));
35
+ this.active += 1;
36
+ }
37
+ release() {
38
+ this.active -= 1;
39
+ const next = this.waiters.shift();
40
+ if (next)
41
+ next();
42
+ }
43
+ }
44
+ const DEFAULT_MAX_AGENTS = 1000;
45
+ const DEFAULT_COMPUTE_DEADLINE_MS = 10_000;
46
+ // JS prelude evaluated before the user script: defines the script API in terms
47
+ // of the minimal host surface (__agent / __phase / __log / __budget*).
48
+ const PRELUDE = `
49
+ globalThis.agent = async (prompt, opts) => {
50
+ const raw = await __agent(JSON.stringify({ prompt: String(prompt ?? ""), opts: opts || {} }));
51
+ return JSON.parse(raw);
52
+ };
53
+ globalThis.parallel = (thunks) => Promise.all((thunks || []).map((t) => {
54
+ try { return Promise.resolve(t()).catch(() => null); } catch (_e) { return Promise.resolve(null); }
55
+ }));
56
+ globalThis.pipeline = (items, ...stages) => Promise.all((items || []).map(async (item, i) => {
57
+ let v = item;
58
+ for (const stage of stages) {
59
+ try { v = await stage(v, item, i); } catch (_e) { return null; }
60
+ }
61
+ return v;
62
+ }));
63
+ globalThis.phase = (t) => __phase(String(t ?? ""));
64
+ globalThis.log = (m) => __log(String(m ?? ""));
65
+ globalThis.budget = {
66
+ get total() { return __budgetTotal(); },
67
+ spent() { return __budgetSpent(); },
68
+ remaining() { return __budgetRemaining(); },
69
+ };
70
+ `;
71
+ /** Removes ambient nondeterminism so a run is reproducible (design §4.3). */
72
+ const DETERMINISM_GATING = `
73
+ delete globalThis.Date;
74
+ delete globalThis.WeakRef;
75
+ delete globalThis.FinalizationRegistry;
76
+ Math.random = () => { throw new Error("Math.random is disabled in workflows (nondeterministic)"); };
77
+ `;
78
+ /** Turns `export const meta = …` / `export function …` into plain declarations. */
79
+ function stripExports(script) {
80
+ return script.replace(/(^|\n)\s*export\s+(const|let|var|function|class|async)\b/g, "$1$2");
81
+ }
82
+ export async function runWorkflow(options) {
83
+ const QuickJS = await getQuickJS();
84
+ const vm = QuickJS.newContext();
85
+ const pending = new Set();
86
+ const state = { disposed: false };
87
+ let agentCount = 0;
88
+ const maxAgents = options.maxAgents ?? DEFAULT_MAX_AGENTS;
89
+ // Per-compute deadline: only counts VM bytecode time, reset on each pump so a
90
+ // long run that is mostly waiting on agents is not killed.
91
+ let computeStart = Date.now();
92
+ const computeDeadline = options.computeDeadlineMs ?? DEFAULT_COMPUTE_DEADLINE_MS;
93
+ vm.runtime.setInterruptHandler(() => Date.now() - computeStart > computeDeadline);
94
+ try {
95
+ vm.unwrapResult(vm.evalCode(DETERMINISM_GATING)).dispose();
96
+ installHostFunctions(vm, options, pending, () => agentCount, () => { agentCount += 1; }, maxAgents, state);
97
+ // args as a deterministic injected global.
98
+ const argsJson = JSON.stringify(options.args ?? null);
99
+ vm.unwrapResult(vm.evalCode(`globalThis.args = ${argsJson};`)).dispose();
100
+ vm.unwrapResult(vm.evalCode(PRELUDE)).dispose();
101
+ const body = stripExports(options.script);
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");
111
+ computeStart = Date.now();
112
+ const evalResult = vm.evalCode(wrapped);
113
+ if (evalResult.error) {
114
+ const message = vm.dump(evalResult.error);
115
+ evalResult.error.dispose();
116
+ return { ok: false, error: `workflow script error: ${formatError(message)}` };
117
+ }
118
+ evalResult.value.dispose();
119
+ // Drive the VM job queue interleaved with host agent settlements.
120
+ const isDone = () => {
121
+ const h = vm.getProp(vm.global, "__wfdone");
122
+ const done = vm.dump(h) === true;
123
+ h.dispose();
124
+ return done;
125
+ };
126
+ while (!isDone()) {
127
+ if (options.signal?.aborted) {
128
+ return { ok: false, error: "workflow aborted" };
129
+ }
130
+ computeStart = Date.now();
131
+ vm.runtime.executePendingJobs();
132
+ if (isDone())
133
+ break;
134
+ if (pending.size === 0) {
135
+ if (!vm.runtime.hasPendingJob || !vm.runtime.hasPendingJob())
136
+ break; // settled or stalled
137
+ continue;
138
+ }
139
+ await Promise.race([...pending, abortRace(options.signal)]);
140
+ }
141
+ const errH = vm.getProp(vm.global, "__wferror");
142
+ const err = vm.dump(errH);
143
+ errH.dispose();
144
+ if (err != null && err !== "") {
145
+ return { ok: false, error: String(err) };
146
+ }
147
+ const resH = vm.getProp(vm.global, "__wfresult");
148
+ const value = vm.dump(resH);
149
+ resH.dispose();
150
+ return { ok: true, value };
151
+ }
152
+ catch (error) {
153
+ return { ok: false, error: error?.message || String(error) };
154
+ }
155
+ finally {
156
+ state.disposed = true;
157
+ vm.dispose();
158
+ }
159
+ }
160
+ function abortRace(signal) {
161
+ if (!signal)
162
+ return new Promise(() => { }); // never settles
163
+ if (signal.aborted)
164
+ return Promise.resolve();
165
+ return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
166
+ }
167
+ function installHostFunctions(vm, options, pending, getCount, bumpCount, maxAgents, state) {
168
+ vm.newFunction("__agent", (specHandle) => {
169
+ const spec = JSON.parse(vm.getString(specHandle));
170
+ const deferred = vm.newPromise();
171
+ // Settling the VM promise touches the context, which may have been disposed
172
+ // if the run was aborted while host work was still in flight — guard it.
173
+ const settle = (fn) => {
174
+ if (state.disposed)
175
+ return;
176
+ try {
177
+ fn();
178
+ }
179
+ catch { /* VM disposed mid-settle */ }
180
+ };
181
+ if (getCount() >= maxAgents) {
182
+ const e = vm.newString(`workflow exceeded the ${maxAgents}-agent cap`);
183
+ deferred.reject(e);
184
+ e.dispose();
185
+ }
186
+ else {
187
+ bumpCount();
188
+ const p = options.dispatchAgent(spec).then((res) => settle(() => {
189
+ if (res.ok) {
190
+ const v = vm.newString(JSON.stringify(res.value ?? null));
191
+ deferred.resolve(v);
192
+ v.dispose();
193
+ }
194
+ else {
195
+ const e = vm.newString(res.error);
196
+ deferred.reject(e);
197
+ e.dispose();
198
+ }
199
+ }), (err) => settle(() => {
200
+ const e = vm.newString(err?.message || String(err));
201
+ deferred.reject(e);
202
+ e.dispose();
203
+ })).finally(() => { pending.delete(p); });
204
+ pending.add(p);
205
+ }
206
+ deferred.settled.then(() => { if (!state.disposed)
207
+ try {
208
+ vm.runtime.executePendingJobs();
209
+ }
210
+ catch { /* disposed */ } });
211
+ return deferred.handle;
212
+ }).consume((f) => vm.setProp(vm.global, "__agent", f));
213
+ vm.newFunction("__phase", (h) => {
214
+ options.onPhase?.(vm.getString(h));
215
+ return vm.undefined;
216
+ }).consume((f) => vm.setProp(vm.global, "__phase", f));
217
+ vm.newFunction("__log", (h) => {
218
+ options.onLog?.(vm.getString(h));
219
+ return vm.undefined;
220
+ }).consume((f) => vm.setProp(vm.global, "__log", f));
221
+ vm.newFunction("__budgetTotal", () => {
222
+ const total = options.budget?.total ?? null;
223
+ return total === null ? vm.null : vm.newNumber(total);
224
+ }).consume((f) => vm.setProp(vm.global, "__budgetTotal", f));
225
+ vm.newFunction("__budgetSpent", () => vm.newNumber(options.budget?.spent() ?? 0))
226
+ .consume((f) => vm.setProp(vm.global, "__budgetSpent", f));
227
+ vm.newFunction("__budgetRemaining", () => {
228
+ const remaining = options.budget?.remaining() ?? Number.POSITIVE_INFINITY;
229
+ return vm.newNumber(Number.isFinite(remaining) ? remaining : Number.MAX_SAFE_INTEGER);
230
+ }).consume((f) => vm.setProp(vm.global, "__budgetRemaining", f));
231
+ }
232
+ function formatError(value) {
233
+ if (value && typeof value === "object" && "message" in value) {
234
+ return String(value.message);
235
+ }
236
+ return String(value);
237
+ }
package/dist/agent.d.ts CHANGED
@@ -7,6 +7,7 @@ import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Mes
7
7
  import { type TurnHooks } from "./orchestrator/hooks.js";
8
8
  import type { ExternalHookController } from "./hooks/controller.js";
9
9
  import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "./agent/categories.js";
10
+ import { type WorkflowRunSnapshot } from "./agent/workflow/control.js";
10
11
  import { BudgetLedger } from "./agent/budget-ledger.js";
11
12
  import { type AgentProfile, type SubagentRunResult } from "./agent/profiles.js";
12
13
  import { type SubagentThreadSnapshot } from "./agent/subagent-control.js";
@@ -23,6 +24,8 @@ export interface AgentSubagentRuntimeConfig {
23
24
  launchIntervalMs?: number;
24
25
  rateLimitMaxAttempts?: number;
25
26
  rateLimitBackoffMs?: number[];
27
+ transportRetryMaxAttempts?: number;
28
+ transportRetryBackoffMs?: number[];
26
29
  /**
27
30
  * Directory for persisted child state (design §7). Defaults to
28
31
  * `<session>.subagents` next to the session file when a session exists.
@@ -112,6 +115,10 @@ export declare class Agent {
112
115
  private readonly subagentScheduler;
113
116
  private readonly childRunner;
114
117
  private readonly resultIntegrator;
118
+ /** Background dynamic-workflow runs (option C Phase 4), keyed by runId. */
119
+ private readonly workflowRuns;
120
+ /** runIds whose completed result should be ingested at the next turn. */
121
+ private readonly pendingWorkflowDeliveries;
115
122
  private subagentsConfig;
116
123
  private readonly rateLimitPolicy?;
117
124
  private pendingSubagentUpdates;
@@ -164,6 +171,17 @@ export declare class Agent {
164
171
  private recoverFromOverflow;
165
172
  compactResidentHistory(): void;
166
173
  private maybeCompactWithLLM;
174
+ /**
175
+ * Stream a 9-section handoff summary of `oldMessages` from the session model.
176
+ * Powers the manual `/compact` command: streaming (rather than `complete()`)
177
+ * is what lets the TUI show live progress as the summary is produced.
178
+ *
179
+ * `onDelta` receives the full accumulated text and the latest delta on each
180
+ * chunk. Returns the trimmed summary, or "" if the model produced nothing
181
+ * (the caller falls back to heuristic compaction in that case). Throws only
182
+ * if the provider stream itself errors.
183
+ */
184
+ summarizeForCompaction(oldMessages: Message[], onDelta?: (full: string, delta: string) => void, abortSignal?: AbortSignal): Promise<string>;
167
185
  runSubtask(input: string | ContentPart[], cwd: string, options?: {
168
186
  subtaskType?: string;
169
187
  description?: string;
@@ -186,6 +204,8 @@ export declare class Agent {
186
204
  profile: AgentProfile;
187
205
  parentToolCallId: string;
188
206
  category?: string;
207
+ model?: string;
208
+ effort?: ThinkingLevel;
189
209
  route?: ResolvedSubagentRoute;
190
210
  approval?: "fail" | "disabled";
191
211
  description?: string;
@@ -211,6 +231,8 @@ export declare class Agent {
211
231
  runAgentTeam(cwd: string, options: {
212
232
  profile: AgentProfile;
213
233
  category?: string;
234
+ model?: string;
235
+ effort?: ThinkingLevel;
214
236
  promptTemplate: string;
215
237
  items: string[];
216
238
  parentToolCallId: string;
@@ -218,6 +240,83 @@ export declare class Agent {
218
240
  abortSignal?: AbortSignal;
219
241
  approval?: "fail" | "disabled";
220
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
+ /**
268
+ * Dynamic workflow (option C): runs an LLM-authored JS orchestration script in
269
+ * a QuickJS sandbox. Each agent() call in the script becomes a real scheduled
270
+ * subagent (same route resolution, ChildRunner, scheduler, schema validation
271
+ * as spawn_agent), so the script expresses deterministic control flow while
272
+ * the runtime keeps owning concurrency/budget/retry.
273
+ *
274
+ * Foreground entry point (used by `-p`/headless and tests): awaits to
275
+ * completion and returns the result. Background runs go through startWorkflow.
276
+ */
277
+ runWorkflow(cwd: string, options: {
278
+ script: string;
279
+ args?: unknown;
280
+ parentToolCallId: string;
281
+ emitUpdate?: (update: ToolUpdate) => void;
282
+ abortSignal?: AbortSignal;
283
+ }): Promise<{
284
+ result: {
285
+ ok: true;
286
+ value: unknown;
287
+ } | {
288
+ ok: false;
289
+ error: string;
290
+ };
291
+ agentCount: number;
292
+ logs: string[];
293
+ snapshots: SubagentThreadSnapshot[];
294
+ }>;
295
+ /**
296
+ * Starts a workflow in the BACKGROUND (option C Phase 4): returns a runId
297
+ * immediately; the script runs detached, its agents stream progress through
298
+ * the queued channel (drained at turn boundaries like spawn_agent), and its
299
+ * result is ingested at the next turn. Collect explicitly with waitWorkflow.
300
+ */
301
+ startWorkflow(cwd: string, options: {
302
+ script: string;
303
+ args?: unknown;
304
+ title?: string;
305
+ parentToolCallId: string;
306
+ abortSignal?: AbortSignal;
307
+ }): {
308
+ runId: string;
309
+ title: string;
310
+ };
311
+ /** Blocks until a background workflow reaches a final state (or times out). */
312
+ waitWorkflow(runId: string, timeoutMs?: number): Promise<WorkflowRunSnapshot | undefined>;
313
+ /** Cancels a running background workflow. */
314
+ closeWorkflow(runId: string): WorkflowRunSnapshot | undefined;
315
+ listWorkflows(): WorkflowRunSnapshot[];
316
+ private snapshotWorkflow;
317
+ /** Injects completed background-workflow results before the next turn (§5 analog). */
318
+ private flushWorkflowDeliveries;
319
+ private executeWorkflow;
221
320
  /** Marks a child's full summary as delivered to parent context (design §3.3). */
222
321
  markSubagentDelivered(agentId: string): void;
223
322
  private snapshotSubagent;
@@ -234,6 +333,12 @@ export declare class Agent {
234
333
  private dispatchSubagentRun;
235
334
  private emitSubagentLifecycle;
236
335
  private runSubagentLifecycleHookFor;
336
+ /**
337
+ * Resolves a child's model route. Priority, highest first (design v2 §1.1):
338
+ * call-site override (model/effort) > profile.model > category > inherit parent.
339
+ * The call-site override is what lets the model say "opus for this reviewer,
340
+ * haiku for these twenty scouts" per spawn/batch member at request time.
341
+ */
237
342
  private resolveRouteForSubagent;
238
343
  private createSubagentThreadRecord;
239
344
  private runSubagentThread;