@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.
Files changed (61) hide show
  1. package/dist/agent/abort-errors.d.ts +2 -2
  2. package/dist/agent/budget-ledger.d.ts +4 -45
  3. package/dist/agent/budget-ledger.js +5 -75
  4. package/dist/agent/child-runner.d.ts +1 -3
  5. package/dist/agent/child-runner.js +6 -37
  6. package/dist/agent/profiles.d.ts +0 -2
  7. package/dist/agent/profiles.js +0 -3
  8. package/dist/agent/subagent-control.d.ts +5 -12
  9. package/dist/agent/subagent-control.js +0 -2
  10. package/dist/agent/subagent-store.d.ts +1 -1
  11. package/dist/agent/subagent-store.js +1 -3
  12. package/dist/agent/workflow/control.d.ts +7 -0
  13. package/dist/agent/workflow/control.js +22 -1
  14. package/dist/agent/workflow/runtime.d.ts +15 -2
  15. package/dist/agent/workflow/runtime.js +112 -30
  16. package/dist/agent.d.ts +7 -43
  17. package/dist/agent.js +50 -176
  18. package/dist/config.d.ts +1 -3
  19. package/dist/config.js +6 -5
  20. package/dist/context/overflow.js +2 -0
  21. package/dist/main.js +9 -1
  22. package/dist/model-catalog.d.ts +1 -1
  23. package/dist/model-catalog.js +25 -18
  24. package/dist/orchestrator/default-hooks.js +7 -1
  25. package/dist/prompt/delegation.d.ts +1 -1
  26. package/dist/prompt/delegation.js +21 -7
  27. package/dist/prompt/task-reminders.d.ts +1 -0
  28. package/dist/prompt/task-reminders.js +24 -1
  29. package/dist/provider-ai-sdk.d.ts +56 -0
  30. package/dist/provider-ai-sdk.js +518 -0
  31. package/dist/provider-registry.js +49 -3
  32. package/dist/provider-transform.d.ts +1 -1
  33. package/dist/provider-transform.js +8 -13
  34. package/dist/provider.js +4 -0
  35. package/dist/slash-commands/commands.js +3 -7
  36. package/dist/slash-commands/types.d.ts +1 -1
  37. package/dist/tools/agent-lifecycle.d.ts +1 -9
  38. package/dist/tools/agent-lifecycle.js +35 -263
  39. package/dist/tui-ink/app.js +51 -13
  40. package/dist/tui-ink/approval/approval-dialog.js +1 -1
  41. package/dist/tui-ink/approval/diff-view.js +4 -4
  42. package/dist/tui-ink/approval/select.js +2 -2
  43. package/dist/tui-ink/feedback-dialog.js +2 -2
  44. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  45. package/dist/tui-ink/input-box.d.ts +1 -6
  46. package/dist/tui-ink/input-box.js +14 -35
  47. package/dist/tui-ink/markdown.js +11 -9
  48. package/dist/tui-ink/message-list.d.ts +5 -1
  49. package/dist/tui-ink/message-list.js +15 -2
  50. package/dist/tui-ink/model-picker.d.ts +3 -1
  51. package/dist/tui-ink/model-picker.js +25 -14
  52. package/dist/tui-ink/plan-confirm.js +1 -1
  53. package/dist/tui-ink/subagent-view.d.ts +3 -1
  54. package/dist/tui-ink/theme.d.ts +19 -2
  55. package/dist/tui-ink/theme.js +25 -8
  56. package/dist/tui-ink/welcome.d.ts +3 -2
  57. package/dist/tui-ink/welcome.js +10 -6
  58. package/dist/types.d.ts +16 -30
  59. package/dist/variant/variant-resolver.d.ts +13 -0
  60. package/dist/variant/variant-resolver.js +26 -0
  61. package/package.json +9 -3
@@ -7,8 +7,8 @@ export declare class AgentAbortError extends Error {
7
7
  * message strings.
8
8
  */
9
9
  export declare class SubagentAbortError extends AgentAbortError {
10
- readonly subagentReason: "interrupt" | "user_close" | "budget";
11
- constructor(message: string, subagentReason: "interrupt" | "user_close" | "budget");
10
+ readonly subagentReason: "interrupt" | "user_close";
11
+ constructor(message: string, subagentReason: "interrupt" | "user_close");
12
12
  }
13
13
  /** Shown when the model produced no user-visible content despite recovery attempts. */
14
14
  export declare const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
@@ -1,61 +1,20 @@
1
1
  import type { TokenUsage } from "../types.js";
2
- import type { SubagentTokenCap } from "./subagent-control.js";
3
2
  export interface BudgetUsageSource {
4
3
  runId: string;
5
4
  subAgentId?: string;
6
5
  }
7
- export interface BudgetSnapshot {
8
- spent: number;
9
- limit?: number;
10
- exhausted: boolean;
11
- }
12
6
  /**
13
7
  * Shared token ledger for a parent and all of its children, with per-source
14
- * accounting so the runtime can enforce per-child caps (design doc §6).
15
- * The shared pool limit is optional — both production hosts construct the
16
- * ledger without one — so per-child caps must never be derived solely from
17
- * "pool remaining"; see computeChildTokenCap.
8
+ * accounting. Pure bookkeeping: nothing is ever stopped for token usage —
9
+ * a child's only resource bound is the model context window, absorbed by
10
+ * compaction (design doc §6, revised 2026-07-06).
18
11
  */
19
12
  export declare class BudgetLedger {
20
- private readonly limit?;
21
13
  private spent;
22
14
  private readonly spentBySource;
23
- private readonly controller;
24
- constructor(limit?: number | undefined);
25
- get signal(): AbortSignal;
26
15
  recordUsage(usage: TokenUsage, source: BudgetUsageSource): void;
27
16
  /** Tokens attributed to one child (or the parent when subAgentId is omitted). */
28
17
  spentBy(subAgentId?: string): number;
29
- /** Pool tokens remaining, or undefined when the pool has no limit. */
30
- remaining(): number | undefined;
31
- get poolLimit(): number | undefined;
32
- snapshot(): BudgetSnapshot;
18
+ totalSpent(): number;
33
19
  }
34
- /** Default absolute per-child soft cap; applies even on limit-free hosts. */
35
- export declare const DEFAULT_CHILD_TOKEN_CAP = 200000;
36
- /** Share of a limited pool reserved for the parent's own turns. */
37
- export declare const PARENT_POOL_RESERVE_RATIO = 0.2;
38
- /** Hard cap sits at least this many tokens above the soft cap (≈ 2 turns). */
39
- export declare const CHILD_HARD_CAP_FLOOR = 20000;
40
- /**
41
- * Per-child token cap, fixed at dispatch (design doc §6). The soft cap is an
42
- * absolute number (config default 200k) so it is effective on limit-free
43
- * hosts; when the pool *is* limited, the fair share of what remains after the
44
- * parent's reserve further bounds it. The cap never shrinks mid-run because
45
- * siblings spawned later.
46
- */
47
- export declare function computeChildTokenCap(options: {
48
- ledger?: BudgetLedger;
49
- subAgentId: string;
50
- activeChildren: number;
51
- configCap?: number;
52
- profileMaxTokens?: number;
53
- }): SubagentTokenCap;
54
- /**
55
- * Hard cap recomputed at each turn-boundary check: at least ~2 of this
56
- * child's average turns above the soft cap, never below the absolute floor
57
- * (design doc §6 — replaces the fixed 25% ratio that could be smaller than a
58
- * single turn).
59
- */
60
- export declare function childHardCap(soft: number, avgTurnTokens: number): number;
61
20
  export declare function composeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
@@ -1,96 +1,26 @@
1
1
  const PARENT_SOURCE_KEY = "__parent__";
2
2
  /**
3
3
  * Shared token ledger for a parent and all of its children, with per-source
4
- * accounting so the runtime can enforce per-child caps (design doc §6).
5
- * The shared pool limit is optional — both production hosts construct the
6
- * ledger without one — so per-child caps must never be derived solely from
7
- * "pool remaining"; see computeChildTokenCap.
4
+ * accounting. Pure bookkeeping: nothing is ever stopped for token usage —
5
+ * a child's only resource bound is the model context window, absorbed by
6
+ * compaction (design doc §6, revised 2026-07-06).
8
7
  */
9
8
  export class BudgetLedger {
10
- limit;
11
9
  spent = 0;
12
10
  spentBySource = new Map();
13
- controller = new AbortController();
14
- constructor(limit) {
15
- this.limit = limit;
16
- }
17
- get signal() {
18
- return this.controller.signal;
19
- }
20
11
  recordUsage(usage, source) {
21
12
  const delta = usage.promptTokens + usage.completionTokens;
22
13
  this.spent += delta;
23
14
  const key = source.subAgentId ?? PARENT_SOURCE_KEY;
24
15
  this.spentBySource.set(key, (this.spentBySource.get(key) ?? 0) + delta);
25
- if (this.limit !== undefined && this.spent >= this.limit && !this.controller.signal.aborted) {
26
- this.controller.abort(budgetAbortError("Budget exhausted"));
27
- }
28
16
  }
29
17
  /** Tokens attributed to one child (or the parent when subAgentId is omitted). */
30
18
  spentBy(subAgentId) {
31
19
  return this.spentBySource.get(subAgentId ?? PARENT_SOURCE_KEY) ?? 0;
32
20
  }
33
- /** Pool tokens remaining, or undefined when the pool has no limit. */
34
- remaining() {
35
- if (this.limit === undefined)
36
- return undefined;
37
- return Math.max(0, this.limit - this.spent);
38
- }
39
- get poolLimit() {
40
- return this.limit;
21
+ totalSpent() {
22
+ return this.spent;
41
23
  }
42
- snapshot() {
43
- return {
44
- spent: this.spent,
45
- limit: this.limit,
46
- exhausted: this.limit !== undefined && this.spent >= this.limit,
47
- };
48
- }
49
- }
50
- /** Default absolute per-child soft cap; applies even on limit-free hosts. */
51
- export const DEFAULT_CHILD_TOKEN_CAP = 200_000;
52
- /** Share of a limited pool reserved for the parent's own turns. */
53
- export const PARENT_POOL_RESERVE_RATIO = 0.2;
54
- /** Hard cap sits at least this many tokens above the soft cap (≈ 2 turns). */
55
- export const CHILD_HARD_CAP_FLOOR = 20_000;
56
- /**
57
- * Per-child token cap, fixed at dispatch (design doc §6). The soft cap is an
58
- * absolute number (config default 200k) so it is effective on limit-free
59
- * hosts; when the pool *is* limited, the fair share of what remains after the
60
- * parent's reserve further bounds it. The cap never shrinks mid-run because
61
- * siblings spawned later.
62
- */
63
- export function computeChildTokenCap(options) {
64
- let soft = options.configCap ?? DEFAULT_CHILD_TOKEN_CAP;
65
- if (options.profileMaxTokens !== undefined && options.profileMaxTokens > 0) {
66
- soft = Math.min(soft, options.profileMaxTokens);
67
- }
68
- const limit = options.ledger?.poolLimit;
69
- if (options.ledger && limit !== undefined) {
70
- const reserve = Math.floor(limit * PARENT_POOL_RESERVE_RATIO);
71
- const available = Math.max(0, (options.ledger.remaining() ?? 0) - reserve);
72
- const share = Math.floor(available / (options.activeChildren + 1));
73
- soft = Math.max(1, Math.min(soft, share));
74
- }
75
- return {
76
- soft,
77
- hard: soft + CHILD_HARD_CAP_FLOOR,
78
- baseline: options.ledger?.spentBy(options.subAgentId) ?? 0,
79
- };
80
- }
81
- /**
82
- * Hard cap recomputed at each turn-boundary check: at least ~2 of this
83
- * child's average turns above the soft cap, never below the absolute floor
84
- * (design doc §6 — replaces the fixed 25% ratio that could be smaller than a
85
- * single turn).
86
- */
87
- export function childHardCap(soft, avgTurnTokens) {
88
- return soft + Math.max(CHILD_HARD_CAP_FLOOR, Math.ceil(avgTurnTokens * 2));
89
- }
90
- function budgetAbortError(message) {
91
- const error = new Error(message);
92
- error.name = "AbortError";
93
- return error;
94
24
  }
95
25
  export function composeAbortSignals(signals) {
96
26
  const active = signals.filter((signal) => !!signal);
@@ -8,7 +8,6 @@
8
8
  * turn-boundary budget enforcement, the handoff completeness guard, and the
9
9
  * mapping of failures to SubagentFinalReason.
10
10
  */
11
- import { BudgetLedger } from "./budget-ledger.js";
12
11
  import type { SubagentRunOutcome } from "./subagent-scheduler.js";
13
12
  import type { SubagentFinalReason, SubagentThreadRecord } from "./subagent-control.js";
14
13
  import type { AgentEvent, Message, ToolRegistryEntry, ToolResult, ToolUpdate } from "../types.js";
@@ -24,7 +23,6 @@ export interface ChildRunOptions {
24
23
  }
25
24
  export interface ChildRunnerHost {
26
25
  allTools(): ToolRegistryEntry[];
27
- budgetLedger(): BudgetLedger | undefined;
28
26
  emit(record: SubagentThreadRecord, options: ChildRunOptions, status: ToolUpdate["status"], event?: AgentEvent, message?: string): void;
29
27
  runLifecycleHook(record: SubagentThreadRecord, cwd: string, eventName: "SubagentStart" | "SubagentStop", status?: string, error?: string, abortSignal?: AbortSignal): Promise<void>;
30
28
  finalizeBlocked(record: SubagentThreadRecord, error: string, options: ChildRunOptions): void;
@@ -47,7 +45,7 @@ export declare function sanitizeSubagentSummary(value: string): string;
47
45
  * to a trivial question is a complete handoff.
48
46
  */
49
47
  export declare function needsExplicitFinalSummary(record: SubagentThreadRecord, executedAnyTool: boolean): boolean;
50
- export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined, ledger: BudgetLedger | undefined): SubagentFinalReason;
48
+ export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined): SubagentFinalReason;
51
49
  /**
52
50
  * Drops trailing "[model request interrupted ...]" boundary messages so a
53
51
  * rate-limit re-entry resumes from clean history (design §4.5).
@@ -9,7 +9,7 @@
9
9
  * mapping of failures to SubagentFinalReason.
10
10
  */
11
11
  import { AgentAbortError, EMPTY_ASSISTANT_FALLBACK, SubagentAbortError } from "./abort-errors.js";
12
- import { childHardCap, composeAbortSignals } from "./budget-ledger.js";
12
+ import { composeAbortSignals } from "./budget-ledger.js";
13
13
  import { isOnlyProviderProtocolArtifacts, stripProviderProtocolArtifacts } from "../provider-artifacts.js";
14
14
  import { isRateLimitError } from "../network/errors.js";
15
15
  import { isProviderTransportError } from "../network/provider-transport.js";
@@ -68,13 +68,6 @@ export class ChildRunner {
68
68
  let turnSummaryBuffer = "";
69
69
  let turnHadToolCall = false;
70
70
  let executedAnyTool = false;
71
- // Per-child budget enforcement happens at turn boundaries (design §6):
72
- // turn_end already carries usage, and a reminder injected here is seen by
73
- // the very next provider call — unlike chunk-level aborts.
74
- const cap = record.tokenCap;
75
- let runTokens = 0;
76
- let runTurns = 0;
77
- let budgetSoftWarned = false;
78
71
  // Re-entry after a rate limit: the input was applied on attempt 1, so the
79
72
  // child history must not gain a second copy, and any stale interruption
80
73
  // boundary from the failed call is stripped (design §4.5).
@@ -110,21 +103,6 @@ export class ChildRunner {
110
103
  }
111
104
  if (event.type === "turn_end" && event.usage) {
112
105
  record.usage = mergeUsage(record.usage, event.usage);
113
- runTokens += event.usage.promptTokens + event.usage.completionTokens;
114
- runTurns += 1;
115
- if (cap) {
116
- if (!budgetSoftWarned && runTokens >= cap.soft) {
117
- budgetSoftWarned = true;
118
- // The hard cap is fixed when the warning fires: soft + ~2 of
119
- // this child's average turns (absolute floor), so the child
120
- // gets a real chance to wrap up before the kill (design §6).
121
- cap.hard = childHardCap(cap.soft, runTokens / Math.max(1, runTurns));
122
- subAgent.injectSystemReminder(buildChildBudgetWrapUpReminder(runTokens, cap.soft));
123
- }
124
- else if (budgetSoftWarned && runTokens >= cap.hard) {
125
- record.abortController.abort(new SubagentAbortError(`Subagent ${record.agentId} exceeded its hard token cap (${cap.hard}).`, "budget"));
126
- }
127
- }
128
106
  }
129
107
  if (event.type === "turn_end") {
130
108
  const turnSummary = stripProviderProtocolArtifacts(turnSummaryBuffer).trim();
@@ -176,7 +154,7 @@ export class ChildRunner {
176
154
  const cancelled = error instanceof AgentAbortError || error?.name === "AbortError";
177
155
  record.status = cancelled ? "cancelled" : "failed";
178
156
  record.finalReason = cancelled
179
- ? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal, this.host.budgetLedger())
157
+ ? classifySubagentAbortReason(record.abortController.signal.aborted ? record.abortController.signal.reason : error, options.abortSignal)
180
158
  : "failed_transient";
181
159
  record.summary = sanitizeSubagentSummary(record.summary);
182
160
  record.error = error?.message || String(error);
@@ -267,23 +245,21 @@ export function needsExplicitFinalSummary(record, executedAnyTool) {
267
245
  return false;
268
246
  if (record.summary === EMPTY_ASSISTANT_FALLBACK)
269
247
  return true;
270
- if (estimateHandoffTokens(record.summary) < HANDOFF_TOKEN_FLOOR)
248
+ // A schema-bearing child's handoff is a compact JSON value; short is
249
+ // complete by construction, and a restate turn would risk breaking the JSON.
250
+ if (!record.expectsStructuredOutput && estimateHandoffTokens(record.summary) < HANDOFF_TOKEN_FLOOR)
271
251
  return true;
272
252
  return isIntermediateHandoff(record.summary);
273
253
  }
274
- export function classifySubagentAbortReason(reason, parentSignal, ledger) {
254
+ export function classifySubagentAbortReason(reason, parentSignal) {
275
255
  if (reason instanceof SubagentAbortError) {
276
256
  switch (reason.subagentReason) {
277
257
  case "interrupt":
278
258
  return "cancelled_interrupt";
279
259
  case "user_close":
280
260
  return "cancelled_user";
281
- case "budget":
282
- return "cancelled_budget";
283
261
  }
284
262
  }
285
- if (ledger?.snapshot().exhausted)
286
- return "cancelled_budget";
287
263
  if (parentSignal?.aborted)
288
264
  return "cancelled_parent_abort";
289
265
  return "cancelled_user";
@@ -302,13 +278,6 @@ export function stripTrailingModelInterruptedBoundary(messages) {
302
278
  break;
303
279
  }
304
280
  }
305
- function buildChildBudgetWrapUpReminder(spentTokens, softCap) {
306
- return [
307
- `Token budget notice: this subagent has used ~${Math.round(spentTokens)} tokens, crossing its ${softCap}-token budget.`,
308
- "Wrap up now: stop opening new lines of investigation and produce your complete final handoff",
309
- "(findings, conclusions, unfinished items) in your next message.",
310
- ].join(" ");
311
- }
312
281
  export function summarizeSubagentToolEnd(event) {
313
282
  const metadata = (event.result.metadata ?? {});
314
283
  const reason = readString(metadata.reason);
@@ -20,8 +20,6 @@ export interface AgentProfile {
20
20
  category?: string;
21
21
  tools: AgentProfileTools;
22
22
  maxTurns?: number;
23
- /** Optional per-child token cap declared by the profile (may only lower the runtime default). */
24
- maxTokens?: number;
25
23
  approval: AgentProfileApproval;
26
24
  nicknameCandidates?: string[];
27
25
  prompt: string;
@@ -23,8 +23,6 @@ const SUBAGENT_DENY_TOOLS = new Set([
23
23
  "send_input",
24
24
  "close_agent",
25
25
  "list_agents",
26
- "agent_team",
27
- "agent_batch",
28
26
  "run_workflow",
29
27
  "wait_workflow",
30
28
  ]);
@@ -323,7 +321,6 @@ function parseAgentProfileFile(raw, source, filePath) {
323
321
  category: stringValue(frontmatter.category),
324
322
  tools: toolsValue(frontmatter.tools),
325
323
  maxTurns: numberValue(frontmatter.maxTurns),
326
- maxTokens: numberValue(frontmatter.maxTokens),
327
324
  approval: approvalValue(frontmatter.approval),
328
325
  nicknameCandidates: stringArray(frontmatter.nicknameCandidates) ?? stringArray(frontmatter.nicknames),
329
326
  prompt: parsed.body.trim(),
@@ -8,17 +8,8 @@ export type SubagentThreadStatus = "queued" | "running" | "completed" | "failed"
8
8
  * rendered in lifecycle tool replies (design doc §3.1) — a resume hint is
9
9
  * emitted iff the runtime judged the run resumable, never as a blanket string.
10
10
  */
11
- export type SubagentFinalReason = "completed" | "failed_transient" | "failed_fatal" | "rate_limited_exhausted" | "blocked" | "cancelled_interrupt" | "cancelled_user" | "cancelled_budget" | "cancelled_parent_abort";
11
+ export type SubagentFinalReason = "completed" | "failed_transient" | "failed_fatal" | "rate_limited_exhausted" | "blocked" | "cancelled_interrupt" | "cancelled_user" | "cancelled_parent_abort";
12
12
  export declare function isResumableReason(reason: SubagentFinalReason): boolean;
13
- /** Per-child token budget, fixed at dispatch time (design doc §6). */
14
- export interface SubagentTokenCap {
15
- /** Soft cap: crossing it injects a wrap-up reminder into the child. */
16
- soft: number;
17
- /** Hard cap: crossing it aborts this child only. Updated at turn checks. */
18
- hard: number;
19
- /** Ledger tokens already attributed to this child when the run started. */
20
- baseline: number;
21
- }
22
13
  export interface SubagentThreadSnapshot {
23
14
  agentId: string;
24
15
  runId: string;
@@ -40,7 +31,6 @@ export interface SubagentThreadSnapshot {
40
31
  deliveredAt?: number;
41
32
  /** 1-based position in the scheduler queue while status is "queued". */
42
33
  queuePosition?: number;
43
- tokenCap?: SubagentTokenCap;
44
34
  /** Present for write_worktree children: where the isolated checkout lives. */
45
35
  worktree?: SubagentWorktree;
46
36
  }
@@ -63,8 +53,11 @@ export interface SubagentThreadRecord {
63
53
  createdAt: number;
64
54
  updatedAt: number;
65
55
  deliveredAt?: number;
66
- tokenCap?: SubagentTokenCap;
67
56
  worktree?: SubagentWorktree;
57
+ /** True when the child was asked for schema-validated structured output:
58
+ * a compact JSON handoff is complete by construction, so the token-floor
59
+ * restate turn must not fire on it. */
60
+ expectsStructuredOutput?: boolean;
68
61
  /** True for agents spawned inside a run_workflow: kept out of list_agents /
69
62
  * wait_agent and not persisted (design v2 appendix; option C). */
70
63
  workflowInternal?: boolean;
@@ -9,7 +9,6 @@ export function isResumableReason(reason) {
9
9
  case "completed":
10
10
  case "failed_fatal":
11
11
  case "blocked":
12
- case "cancelled_budget":
13
12
  return false;
14
13
  }
15
14
  }
@@ -33,7 +32,6 @@ export function snapshotSubagentThread(record) {
33
32
  createdAt: record.createdAt,
34
33
  updatedAt: record.updatedAt,
35
34
  deliveredAt: record.deliveredAt,
36
- tokenCap: record.tokenCap ? { ...record.tokenCap } : undefined,
37
35
  worktree: record.worktree ? { ...record.worktree } : undefined,
38
36
  };
39
37
  }
@@ -7,7 +7,7 @@
7
7
  * Persistence (design §7): final-state children are written to
8
8
  * `<persistDir>/<agentId>.json` as snapshot + compacted message history, so a
9
9
  * later process can resume them via send_input. The on-disk schema carries
10
- * `finalReason` / `resumable` / `deliveredAt` / `tokenCap` — the fields the
10
+ * `finalReason` / `resumable` / `deliveredAt` — the fields the
11
11
  * reply protocol and delivery dedup depend on. Child transcripts never mix
12
12
  * into the parent transcript.
13
13
  */
@@ -7,7 +7,7 @@
7
7
  * Persistence (design §7): final-state children are written to
8
8
  * `<persistDir>/<agentId>.json` as snapshot + compacted message history, so a
9
9
  * later process can resume them via send_input. The on-disk schema carries
10
- * `finalReason` / `resumable` / `deliveredAt` / `tokenCap` — the fields the
10
+ * `finalReason` / `resumable` / `deliveredAt` — the fields the
11
11
  * reply protocol and delivery dedup depend on. Child transcripts never mix
12
12
  * into the parent transcript.
13
13
  */
@@ -84,7 +84,6 @@ export class SubagentStore {
84
84
  createdAt: record.createdAt,
85
85
  updatedAt: record.updatedAt,
86
86
  deliveredAt: record.deliveredAt,
87
- tokenCap: record.tokenCap,
88
87
  messages: record.agent?.messages ?? record.messages,
89
88
  };
90
89
  writeFileSync(join(this.persistDir, `${record.agentId}.json`), JSON.stringify(payload));
@@ -135,7 +134,6 @@ export class SubagentStore {
135
134
  createdAt: parsed.createdAt,
136
135
  updatedAt: parsed.updatedAt,
137
136
  deliveredAt: parsed.deliveredAt,
138
- tokenCap: parsed.tokenCap,
139
137
  abortController: new AbortController(),
140
138
  waiters: new Set(),
141
139
  messages: parsed.messages,
@@ -34,4 +34,11 @@ export interface WorkflowRunSnapshot {
34
34
  snapshots: SubagentThreadSnapshot[];
35
35
  }
36
36
  /** System-reminder injected at the next turn when a background workflow finishes. */
37
+ /**
38
+ * Failed members must surface in EVERY channel that renders a finished
39
+ * workflow (delivery notice and wait_workflow): agent() converts failures to
40
+ * null inside the script, so without this line a "completed" workflow could
41
+ * silently omit items whose agents never delivered.
42
+ */
43
+ export declare function workflowMemberWarning(snapshot: WorkflowRunSnapshot): string | undefined;
37
44
  export declare function buildWorkflowDeliveryNotice(snapshot: WorkflowRunSnapshot): string;
@@ -1,8 +1,24 @@
1
1
  /** Background dynamic-workflow run state + parent-facing snapshot (option C Phase 4). */
2
2
  /** System-reminder injected at the next turn when a background workflow finishes. */
3
+ /**
4
+ * Failed members must surface in EVERY channel that renders a finished
5
+ * workflow (delivery notice and wait_workflow): agent() converts failures to
6
+ * null inside the script, so without this line a "completed" workflow could
7
+ * silently omit items whose agents never delivered.
8
+ */
9
+ export function workflowMemberWarning(snapshot) {
10
+ const failed = snapshot.snapshots.filter((agent) => agent.status !== "completed" && agent.status !== "closed");
11
+ if (failed.length === 0)
12
+ return undefined;
13
+ const names = failed.map((agent) => `${agent.nickname} (${agent.status}${agent.error ? `: ${truncate(agent.error, 120)}` : ""})`).join("; ");
14
+ return `warning: ${failed.length} of ${snapshot.agentCount} agents did not complete — ${names}. Their agent() calls returned null; treat those items as missing, not done.`;
15
+ }
3
16
  export function buildWorkflowDeliveryNotice(snapshot) {
4
17
  const head = `workflow "${snapshot.title}" (${snapshot.runId}) ${snapshot.status} — ${snapshot.agentCount} agent${snapshot.agentCount === 1 ? "" : "s"}.`;
5
18
  const lines = [head];
19
+ const warning = workflowMemberWarning(snapshot);
20
+ if (warning)
21
+ lines.push(warning);
6
22
  if (snapshot.result?.ok) {
7
23
  const rendered = typeof snapshot.result.value === "string"
8
24
  ? snapshot.result.value
@@ -12,7 +28,12 @@ export function buildWorkflowDeliveryNotice(snapshot) {
12
28
  else if (snapshot.result && !snapshot.result.ok) {
13
29
  lines.push(`error: ${snapshot.result.error}`);
14
30
  }
15
- lines.push("Do not re-run this workflow; integrate its result.");
31
+ if (snapshot.result && !snapshot.result.ok) {
32
+ lines.push("The workflow failed. If the error is in the script, fix it and issue a corrected run_workflow; do not integrate partial results as if complete.");
33
+ }
34
+ else {
35
+ lines.push("Do not re-run this workflow; integrate its result.");
36
+ }
16
37
  return lines.join("\n");
17
38
  }
18
39
  function truncate(value, max) {
@@ -18,9 +18,8 @@ export interface WorkflowAgentOpts {
18
18
  category?: string;
19
19
  agentType?: string;
20
20
  schema?: unknown;
21
+ /** Display name for this member in progress views (wired to the record nickname). */
21
22
  label?: string;
22
- phase?: string;
23
- isolation?: string;
24
23
  }
25
24
  export interface WorkflowAgentSpec {
26
25
  prompt: string;
@@ -72,4 +71,18 @@ export declare class WorkflowConcurrencyGate {
72
71
  acquire(): Promise<void>;
73
72
  release(): void;
74
73
  }
74
+ /** The EXACT source runWorkflow executes — the syntax probe must compile this, not the raw script. */
75
+ export declare function buildWrappedWorkflowSource(script: string): string;
76
+ /**
77
+ * Submit-time syntax check: compiles the exact transformed source with
78
+ * QuickJS's compile-only flag (same engine, zero grammar divergence) so the
79
+ * model gets a teaching error as the immediate tool result instead of one
80
+ * full turn later through the background-run delivery path.
81
+ */
82
+ export declare function precompileWorkflowScript(script: string): Promise<{
83
+ ok: true;
84
+ } | {
85
+ ok: false;
86
+ error: string;
87
+ }>;
75
88
  export declare function runWorkflow(options: RunWorkflowOptions): Promise<RunWorkflowResult>;