@bubblebrain-ai/bubble 0.0.21 → 0.0.23

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 (65) hide show
  1. package/README.md +197 -34
  2. package/dist/agent/abort-errors.d.ts +14 -0
  3. package/dist/agent/abort-errors.js +21 -0
  4. package/dist/agent/budget-ledger.d.ts +41 -0
  5. package/dist/agent/budget-ledger.js +64 -0
  6. package/dist/agent/child-runner.d.ts +55 -0
  7. package/dist/agent/child-runner.js +312 -0
  8. package/dist/agent/internal-reminder-sanitizer.js +29 -9
  9. package/dist/agent/profiles.d.ts +8 -0
  10. package/dist/agent/profiles.js +27 -5
  11. package/dist/agent/result-integrator.d.ts +22 -0
  12. package/dist/agent/result-integrator.js +50 -0
  13. package/dist/agent/subagent-control.d.ts +31 -0
  14. package/dist/agent/subagent-control.js +27 -0
  15. package/dist/agent/subagent-lifecycle-reminder.js +11 -2
  16. package/dist/agent/subagent-scheduler.d.ts +95 -0
  17. package/dist/agent/subagent-scheduler.js +256 -0
  18. package/dist/agent/subagent-store.d.ts +41 -0
  19. package/dist/agent/subagent-store.js +149 -0
  20. package/dist/agent/subagent-summary.d.ts +30 -0
  21. package/dist/agent/subagent-summary.js +74 -0
  22. package/dist/agent/worktree.d.ts +29 -0
  23. package/dist/agent/worktree.js +73 -0
  24. package/dist/agent.d.ts +63 -5
  25. package/dist/agent.js +360 -287
  26. package/dist/approval/controller.js +9 -1
  27. package/dist/approval/tool-helper.js +2 -0
  28. package/dist/approval/types.d.ts +17 -1
  29. package/dist/config.d.ts +8 -0
  30. package/dist/config.js +17 -0
  31. package/dist/feishu/agent-host/approval-card.js +9 -0
  32. package/dist/feishu/agent-host/run-driver.js +1 -0
  33. package/dist/main.js +38 -2
  34. package/dist/model-catalog.js +6 -0
  35. package/dist/network/errors.d.ts +28 -0
  36. package/dist/network/errors.js +24 -0
  37. package/dist/orchestrator/default-hooks.js +5 -1
  38. package/dist/prompt/compose.js +3 -0
  39. package/dist/prompt/delegation.d.ts +14 -0
  40. package/dist/prompt/delegation.js +64 -0
  41. package/dist/prompt/task-reminders.d.ts +5 -1
  42. package/dist/prompt/task-reminders.js +10 -2
  43. package/dist/provider-anthropic.js +23 -0
  44. package/dist/provider-transform.js +14 -0
  45. package/dist/provider.js +23 -3
  46. package/dist/slash-commands/commands.js +29 -2
  47. package/dist/slash-commands/types.d.ts +2 -0
  48. package/dist/tools/agent-lifecycle.d.ts +29 -3
  49. package/dist/tools/agent-lifecycle.js +394 -40
  50. package/dist/tools/child-tools.d.ts +31 -0
  51. package/dist/tools/child-tools.js +106 -0
  52. package/dist/tools/index.js +1 -1
  53. package/dist/tui/run.d.ts +17 -1
  54. package/dist/tui/run.js +155 -10
  55. package/dist/tui/session-picker-data.d.ts +18 -0
  56. package/dist/tui/session-picker-data.js +21 -0
  57. package/dist/tui/trace-groups.js +41 -5
  58. package/dist/tui/wordmark.d.ts +2 -0
  59. package/dist/tui/wordmark.js +31 -4
  60. package/dist/tui-ink/approval/approval-dialog.js +10 -0
  61. package/dist/tui-opentui/approval/approval-dialog.js +10 -0
  62. package/dist/types.d.ts +17 -0
  63. package/dist/update/index.d.ts +18 -4
  64. package/dist/update/index.js +41 -19
  65. package/package.json +1 -1
package/README.md CHANGED
@@ -1,38 +1,37 @@
1
1
  # Bubble
2
2
 
3
- Bubble is a terminal coding agent for working inside local project folders. It can read and edit files, run commands with approval controls, use project skills, connect MCP tools, and keep persistent memory across sessions.
3
+ Bubble is a terminal coding agent. It works inside a local project folder: it reads and edits files, runs commands behind configurable approval controls, searches and navigates code with language-server intelligence, browses the web, loads reusable skills, connects MCP tools, fans work out to subagents, and keeps persistent memory across sessions.
4
+
5
+ It is provider-agnostic. Bring an API key for OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Zhipu, Z.AI, MiniMax, Groq, Together, Fireworks, a local OpenAI-compatible endpoint, and more — or sign in to ChatGPT with OAuth and drive the Codex models directly.
6
+
7
+ ---
4
8
 
5
9
  ## Requirements
6
10
 
7
- - Node.js 20+ and npm for installation
8
- - Bun for running Bubble
11
+ - Node.js 20 or newer (used to install the launcher)
12
+ - [Bun](https://bun.sh) (used to run the agent)
9
13
 
10
- Install Bun if it is not already available:
14
+ Install Bun if you do not already have it:
11
15
 
12
16
  ```bash
13
17
  curl -fsSL https://bun.sh/install | bash
14
18
  ```
15
19
 
16
- ## Install
20
+ The `npm install` step puts a small Node.js launcher named `bubble` on your PATH. When you run `bubble`, the launcher locates Bun and starts the real runtime under it. If Bun is missing, it prints the install command above instead of failing with a low-level error.
17
21
 
18
- From npm:
22
+ ## Install
19
23
 
20
24
  ```bash
21
25
  npm install -g @bubblebrain-ai/bubble
22
26
  ```
23
27
 
24
- From a local package tarball:
28
+ To install from a local tarball:
25
29
 
26
30
  ```bash
27
- npm install -g ./bubblebrain-ai-bubble-0.0.3.tgz
31
+ npm install -g ./bubblebrain-ai-bubble-<version>.tgz
28
32
  ```
29
33
 
30
- The npm command installs a small Node.js launcher named `bubble`. When you run
31
- `bubble`, the launcher checks for Bun and starts the real Bubble runtime with
32
- `bun`. If Bun is missing, it prints the install command above instead of failing
33
- with a low-level runtime error.
34
-
35
- ## Usage
34
+ ## Quick start
36
35
 
37
36
  Start Bubble in the current directory:
38
37
 
@@ -40,31 +39,192 @@ Start Bubble in the current directory:
40
39
  bubble
41
40
  ```
42
41
 
43
- Start Bubble for a specific project:
42
+ On first launch, connect a model:
43
+
44
+ - Run `/login` to sign in to ChatGPT (OAuth) and use the Codex models, or
45
+ - Run `/provider` to add any other provider with an API key.
46
+
47
+ Then just type what you want done. Bubble plans, edits files, and runs commands, asking for approval where the current permission mode requires it.
48
+
49
+ Point Bubble at a different project:
44
50
 
45
51
  ```bash
46
52
  bubble --cwd /path/to/project
47
53
  ```
48
54
 
49
- Show CLI options:
55
+ Resume your last conversation:
50
56
 
51
57
  ```bash
52
- bubble --help
58
+ bubble --resume
59
+ ```
60
+
61
+ ## Model providers
62
+
63
+ Bubble ships with a catalog of built-in providers. Configure them inside the app — no environment variables required.
64
+
65
+ | How | What it does |
66
+ | --- | --- |
67
+ | `/login` | OAuth sign-in for ChatGPT; unlocks the OpenAI Codex models without an API key. |
68
+ | `/provider` | Open a picker to connect, switch, add, or remove a provider. |
69
+ | `/key <provider> <key>` | Set the API key for a provider. |
70
+ | `/model` | Pick the active model and reasoning effort. |
71
+
72
+ Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and international), Kimi for Coding, Zhipu AI, Z.AI, Alibaba DashScope, MiniMax, StepFun, Groq, Together AI, Fireworks, and a `local` profile for any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.).
73
+
74
+ ### Custom providers and models
75
+
76
+ For full control — custom base URLs, self-hosted gateways, extra models, or pinning a protocol — define providers in `~/.bubble/models.json`:
77
+
78
+ ```json
79
+ {
80
+ "providers": {
81
+ "my-gateway": {
82
+ "baseURL": "https://gateway.internal/v1",
83
+ "apiKey": "sk-...",
84
+ "protocol": "openai-chat",
85
+ "models": [
86
+ { "id": "my-model", "name": "My Model" }
87
+ ]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ `protocol` accepts `openai-chat` (default) or `anthropic-messages`. Entries in `models.json` take precedence over the built-in catalog.
94
+
95
+ ### Reasoning effort
96
+
97
+ Models that support it expose a reasoning-effort control: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Set it with `/model <id> --reasoning-effort <level>` in the app, or at launch with `--reasoning` (medium) or `--reasoning-effort <level>`.
98
+
99
+ ## Permission modes
100
+
101
+ Bubble gates risky actions behind a permission mode. Press `Tab` to cycle modes during a session, or set a default with `--plan` / `--dangerously-skip-permissions` / project settings.
102
+
103
+ | Mode | Behavior |
104
+ | --- | --- |
105
+ | Default (Build) | File edits and writes auto-approve; bash and other tools prompt unless covered by an allow rule. |
106
+ | Plan | Read-only investigation. The agent proposes a plan and waits for your approval before making changes. |
107
+ | Bypass | Auto-approves every tool and disables all safety prompts. Enable deliberately with `--dangerously-skip-permissions`. |
108
+
109
+ Allow/deny rules are configured per scope and persisted across sessions. Manage them in-app with `/permissions`, or edit the settings files directly:
110
+
111
+ - `~/.bubble/settings.json` — user scope (applies everywhere)
112
+ - `<project>/.bubble/settings.json` — project scope (commit to share with your team)
113
+ - `<project>/.bubble/settings.local.json` — local overrides (gitignore)
114
+
115
+ Rules use a simple pattern syntax, for example:
116
+
117
+ ```json
118
+ {
119
+ "permissions": {
120
+ "defaultMode": "default",
121
+ "allow": [
122
+ "Bash(git status)",
123
+ "Bash(npm run:*)",
124
+ "Read(./src/**)",
125
+ "WebFetch(domain:github.com)"
126
+ ],
127
+ "deny": ["Read(~/.ssh/**)"]
128
+ }
129
+ }
53
130
  ```
54
131
 
55
- ## Configuration
132
+ ## What Bubble can do
56
133
 
57
- Bubble stores user configuration, sessions, permissions, skills, and memory under:
134
+ **Files and code.** Read, write, and make targeted edits; find files by glob; search contents with ripgrep; and navigate with language-server operations (go-to-definition, find references, hover, document/workspace symbols, call hierarchy).
135
+
136
+ **Shell and dev servers.** Run bounded bash commands with streaming output, and start/stop/inspect long-running dev servers (`npm run dev`, Vite, Next, etc.) with readiness checks and captured logs.
137
+
138
+ **Web.** Search the web and fetch/extract page contents on demand.
139
+
140
+ **Skills.** Drop reusable instructions and assets into a `SKILL.md`-based directory and invoke them with `/<skill-name>`. Bubble discovers skills from `~/.bubble/skills`, `~/.agents/skills`, `~/.claude/skills`, and the project's `.bubble/skills`. Browse them with `/skills`.
141
+
142
+ **MCP tools.** Connect Model Context Protocol servers (stdio, HTTP, or SSE) under the `mcpServers` key of any settings file. Their tools and prompts become available to the agent; manage connections with `/mcp`.
143
+
144
+ **Subagents.** Bubble can spawn background subagents with independent context, send them follow-ups, wait on their results, and fan a task out across a team — with concurrency limits and token budgets. Define custom agent profiles in `~/.bubble/agents` or a project's `.bubble/agents`.
145
+
146
+ **Persistent memory.** A background pipeline distills durable facts, preferences, and decisions from past sessions and recalls them later. Inspect and maintain it with `/memory status`, `/memory search <query>`, and `/memory refresh`.
147
+
148
+ **Sessions.** Every conversation is saved. Resume the latest with `bubble --resume`, or browse and switch sessions in-app with `/session`. Use `/rewind` to roll the conversation — and optionally your file edits — back to an earlier point.
149
+
150
+ ## Useful slash commands
151
+
152
+ | Command | Description |
153
+ | --- | --- |
154
+ | `/help` | List available commands. |
155
+ | `/model` | Switch model and reasoning effort. |
156
+ | `/provider`, `/login`, `/logout`, `/key` | Connect and manage providers. |
157
+ | `/session`, `/rewind`, `/clear` | Manage conversation history. |
158
+ | `/skills` | Open the searchable skills picker. |
159
+ | `/mcp` | List or reconnect MCP servers. |
160
+ | `/memory` | Inspect and maintain persistent memory. |
161
+ | `/permissions` | View or edit allow/deny rules. |
162
+ | `/context`, `/stats`, `/compact` | Inspect context usage, model stats, and compact the session. |
163
+ | `/lsp`, `/hooks` | Manage language servers and lifecycle hooks. |
164
+ | `/theme`, `/sidebar` | Adjust the interface. |
165
+ | `/feedback` | Send feedback or report a bug. |
166
+
167
+ ## Non-interactive mode
168
+
169
+ Run a single prompt and print the result — useful for scripts and pipelines:
170
+
171
+ ```bash
172
+ bubble --print "summarize what this repo does"
173
+ echo "fix the failing test" | bubble --print
174
+ ```
175
+
176
+ Combine with `--model`, `--cwd`, and `--plan` as needed.
177
+
178
+ ## CLI reference
58
179
 
59
180
  ```text
60
- ~/.bubble
181
+ Usage:
182
+ bubble [options] [prompt] Start interactive TUI
183
+ bubble update [--check] Update to the latest version (alias: upgrade)
184
+ bubble serve --feishu [options] Run as a Feishu bot host
185
+
186
+ Options (default):
187
+ -m, --model <model> Model to use
188
+ --cwd <dir> Working directory (default: current)
189
+ -k, --api-key <key> API key for the active provider
190
+ -r, --resume Resume a previous session (latest by default)
191
+ --session <name> Session name to create or resume
192
+ --reasoning Enable reasoning mode at medium effort
193
+ --reasoning-effort <l> Set reasoning effort: off|minimal|low|medium|high|xhigh|max
194
+ --plan Start in plan mode (read-only investigation; propose before executing)
195
+ --dangerously-skip-permissions
196
+ Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
197
+ -p, --print Non-interactive mode (single prompt)
198
+ -v, --version Print the installed version and exit
199
+ -h, --help Show this help
61
200
  ```
62
201
 
63
- In the app, use `/login` or provider commands to configure model access.
202
+ Run `bubble update` at any time to upgrade to the latest published version.
203
+
204
+ ## Configuration and storage
64
205
 
65
- ### ChatGPT Network Configuration
206
+ Bubble keeps everything under `~/.bubble`:
207
+
208
+ ```text
209
+ ~/.bubble/
210
+ config.json User preferences (theme, default model, recent models)
211
+ models.json Custom providers and models
212
+ auth.json OAuth credentials (file mode 0600)
213
+ settings.json User-scope permissions, MCP servers, hooks
214
+ sessions/ Saved conversations, grouped by project directory
215
+ memories/ Persistent memory store
216
+ skills/ User skills
217
+ agents/ Custom subagent profiles
218
+ ```
66
219
 
67
- ChatGPT OAuth and GPT/Codex requests respect standard proxy variables:
220
+ Environment variables:
221
+
222
+ - `BUBBLE_HOME` — override the data directory (defaults to `~/.bubble`).
223
+ - `BUBBLE_DEV=1` — use `~/.bubble-dev` instead, for development.
224
+
225
+ ### Network configuration
226
+
227
+ ChatGPT OAuth and GPT/Codex requests respect the standard proxy variables:
68
228
 
69
229
  ```bash
70
230
  export HTTPS_PROXY=http://proxy.example.com:8080
@@ -72,28 +232,31 @@ export HTTP_PROXY=http://proxy.example.com:8080
72
232
  export NO_PROXY=localhost,127.0.0.1
73
233
  ```
74
234
 
75
- If your network uses a corporate or custom HTTPS CA, start Bubble with:
235
+ If your network uses a corporate or custom HTTPS CA:
76
236
 
77
237
  ```bash
78
238
  NODE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
79
239
  ```
80
240
 
81
- You can also use `BUBBLE_EXTRA_CA_CERTS` for Bubble's ChatGPT requests:
241
+ `BUBBLE_EXTRA_CA_CERTS` applies the same trust to Bubble's ChatGPT requests specifically. Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
242
+
243
+ ## Development
82
244
 
83
245
  ```bash
84
- BUBBLE_EXTRA_CA_CERTS=/absolute/path/to/ca.pem bubble
246
+ bun install # install dependencies
247
+ npm run build # compile TypeScript to dist/
248
+ npm test # run the test suite (vitest)
249
+ npm start # run the built agent
85
250
  ```
86
251
 
87
- Do not disable TLS verification with `NODE_TLS_REJECT_UNAUTHORIZED=0`.
252
+ `npm run dev` compiles and launches in one step. The TUI is built on [OpenTUI](https://github.com/anomalyco/opentui) and Solid.
88
253
 
89
- ## Memory
254
+ ## Feishu host (optional)
90
255
 
91
- Bubble maintains persistent memory automatically from prior sessions. Useful commands:
256
+ Bubble can also run as a Feishu (Lark) bot host:
92
257
 
93
- ```text
94
- /memory status
95
- /memory search <query>
96
- /memory refresh
258
+ ```bash
259
+ bubble serve --feishu --setup
97
260
  ```
98
261
 
99
- Memory is maintained by a background pipeline and can be refreshed manually when you want new session information to be indexed immediately.
262
+ `--setup` runs the binding wizard, `--kill-old` replaces a conflicting instance for the same App ID, and `--dry-run` connects once and exits as a smoke test.
@@ -0,0 +1,14 @@
1
+ export declare class AgentAbortError extends Error {
2
+ constructor(message?: string);
3
+ }
4
+ /**
5
+ * Abort tagged with why the runtime stopped a child, so finalization can map
6
+ * it to a SubagentFinalReason (design doc §3.1) instead of guessing from
7
+ * message strings.
8
+ */
9
+ export declare class SubagentAbortError extends AgentAbortError {
10
+ readonly subagentReason: "interrupt" | "user_close" | "budget";
11
+ constructor(message: string, subagentReason: "interrupt" | "user_close" | "budget");
12
+ }
13
+ /** Shown when the model produced no user-visible content despite recovery attempts. */
14
+ export declare const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
@@ -0,0 +1,21 @@
1
+ export class AgentAbortError extends Error {
2
+ constructor(message = "Agent run cancelled.") {
3
+ super(message);
4
+ this.name = "AgentAbortError";
5
+ }
6
+ }
7
+ /**
8
+ * Abort tagged with why the runtime stopped a child, so finalization can map
9
+ * it to a SubagentFinalReason (design doc §3.1) instead of guessing from
10
+ * message strings.
11
+ */
12
+ export class SubagentAbortError extends AgentAbortError {
13
+ subagentReason;
14
+ constructor(message, subagentReason) {
15
+ super(message);
16
+ this.subagentReason = subagentReason;
17
+ this.name = "SubagentAbortError";
18
+ }
19
+ }
20
+ /** Shown when the model produced no user-visible content despite recovery attempts. */
21
+ export const EMPTY_ASSISTANT_FALLBACK = "The model returned no user-visible response. Please retry, or switch models if this keeps happening.";
@@ -1,4 +1,5 @@
1
1
  import type { TokenUsage } from "../types.js";
2
+ import type { SubagentTokenCap } from "./subagent-control.js";
2
3
  export interface BudgetUsageSource {
3
4
  runId: string;
4
5
  subAgentId?: string;
@@ -8,13 +9,53 @@ export interface BudgetSnapshot {
8
9
  limit?: number;
9
10
  exhausted: boolean;
10
11
  }
12
+ /**
13
+ * 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.
18
+ */
11
19
  export declare class BudgetLedger {
12
20
  private readonly limit?;
13
21
  private spent;
22
+ private readonly spentBySource;
14
23
  private readonly controller;
15
24
  constructor(limit?: number | undefined);
16
25
  get signal(): AbortSignal;
17
26
  recordUsage(usage: TokenUsage, source: BudgetUsageSource): void;
27
+ /** Tokens attributed to one child (or the parent when subAgentId is omitted). */
28
+ 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;
18
32
  snapshot(): BudgetSnapshot;
19
33
  }
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;
20
61
  export declare function composeAbortSignals(signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
@@ -1,6 +1,15 @@
1
+ const PARENT_SOURCE_KEY = "__parent__";
2
+ /**
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.
8
+ */
1
9
  export class BudgetLedger {
2
10
  limit;
3
11
  spent = 0;
12
+ spentBySource = new Map();
4
13
  controller = new AbortController();
5
14
  constructor(limit) {
6
15
  this.limit = limit;
@@ -11,10 +20,25 @@ export class BudgetLedger {
11
20
  recordUsage(usage, source) {
12
21
  const delta = usage.promptTokens + usage.completionTokens;
13
22
  this.spent += delta;
23
+ const key = source.subAgentId ?? PARENT_SOURCE_KEY;
24
+ this.spentBySource.set(key, (this.spentBySource.get(key) ?? 0) + delta);
14
25
  if (this.limit !== undefined && this.spent >= this.limit && !this.controller.signal.aborted) {
15
26
  this.controller.abort(budgetAbortError("Budget exhausted"));
16
27
  }
17
28
  }
29
+ /** Tokens attributed to one child (or the parent when subAgentId is omitted). */
30
+ spentBy(subAgentId) {
31
+ return this.spentBySource.get(subAgentId ?? PARENT_SOURCE_KEY) ?? 0;
32
+ }
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;
41
+ }
18
42
  snapshot() {
19
43
  return {
20
44
  spent: this.spent,
@@ -23,6 +47,46 @@ export class BudgetLedger {
23
47
  };
24
48
  }
25
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
+ }
26
90
  function budgetAbortError(message) {
27
91
  const error = new Error(message);
28
92
  error.name = "AbortError";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * ChildRunner — executes one logical run of a subagent thread and reports the
3
+ * outcome to the scheduler (design doc §2, extracted in Phase 3).
4
+ *
5
+ * A logical run spans dispatch → final state; a rate-limit re-entry is the
6
+ * same logical run (no second SubagentStart), while a send_input restart is a
7
+ * new one. The runner owns: tool validation defense, instance reuse,
8
+ * turn-boundary budget enforcement, the handoff completeness guard, and the
9
+ * mapping of failures to SubagentFinalReason.
10
+ */
11
+ import { BudgetLedger } from "./budget-ledger.js";
12
+ import type { SubagentRunOutcome } from "./subagent-scheduler.js";
13
+ import type { SubagentFinalReason, SubagentThreadRecord } from "./subagent-control.js";
14
+ import type { AgentEvent, Message, ToolRegistryEntry, ToolUpdate } from "../types.js";
15
+ export interface ChildRunOptions {
16
+ approval: "fail" | "disabled";
17
+ abortSignal?: AbortSignal;
18
+ forkContext?: boolean;
19
+ directEmit?: (update: ToolUpdate) => void;
20
+ queueUpdates?: boolean;
21
+ reuseAgent?: boolean;
22
+ /** 1-based scheduler attempt; >1 means rate-limit re-entry of the same logical run. */
23
+ attempt?: number;
24
+ }
25
+ export interface ChildRunnerHost {
26
+ allTools(): ToolRegistryEntry[];
27
+ budgetLedger(): BudgetLedger | undefined;
28
+ emit(record: SubagentThreadRecord, options: ChildRunOptions, status: ToolUpdate["status"], event?: AgentEvent, message?: string): void;
29
+ runLifecycleHook(record: SubagentThreadRecord, cwd: string, eventName: "SubagentStart" | "SubagentStop", status?: string, error?: string, abortSignal?: AbortSignal): Promise<void>;
30
+ finalizeBlocked(record: SubagentThreadRecord, error: string, options: ChildRunOptions): void;
31
+ createInstance(record: SubagentThreadRecord, tools: ToolRegistryEntry[], cwd: string, forkContext?: boolean): Promise<NonNullable<SubagentThreadRecord["agent"]>>;
32
+ notifyWaiters(record: SubagentThreadRecord): void;
33
+ /** Called on every final state so background results can be ingested (§5). */
34
+ onFinal(record: SubagentThreadRecord, options: ChildRunOptions): void;
35
+ }
36
+ export declare class ChildRunner {
37
+ private readonly host;
38
+ constructor(host: ChildRunnerHost);
39
+ run(record: SubagentThreadRecord, input: string | import("../types.js").ContentPart[], cwd: string, options: ChildRunOptions): Promise<SubagentRunOutcome>;
40
+ private runFinalSummaryTurn;
41
+ }
42
+ export declare function sanitizeSubagentSummary(value: string): string;
43
+ /**
44
+ * Handoff completeness guard (design §3.2): a deterministic CJK-aware token
45
+ * floor and a cheap intermediate-narration prefix check run in parallel.
46
+ * Both only apply after the child actually used tools — a short direct answer
47
+ * to a trivial question is a complete handoff.
48
+ */
49
+ export declare function needsExplicitFinalSummary(record: SubagentThreadRecord, executedAnyTool: boolean): boolean;
50
+ export declare function classifySubagentAbortReason(reason: unknown, parentSignal: AbortSignal | undefined, ledger: BudgetLedger | undefined): SubagentFinalReason;
51
+ /**
52
+ * Drops trailing "[model request interrupted ...]" boundary messages so a
53
+ * rate-limit re-entry resumes from clean history (design §4.5).
54
+ */
55
+ export declare function stripTrailingModelInterruptedBoundary(messages: Message[]): void;