@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.
- package/README.md +197 -34
- package/dist/agent/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/internal-reminder-sanitizer.js +29 -9
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +63 -5
- package/dist/agent.js +360 -287
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +1 -0
- package/dist/main.js +38 -2
- package/dist/model-catalog.js +6 -0
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider-transform.js +14 -0
- package/dist/provider.js +23 -3
- package/dist/slash-commands/commands.js +29 -2
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/index.js +1 -1
- package/dist/tui/run.d.ts +17 -1
- package/dist/tui/run.js +155 -10
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.js +41 -5
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- package/dist/tui-opentui/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/dist/update/index.d.ts +18 -4
- package/dist/update/index.js +41 -19
- 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
|
|
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
|
|
8
|
-
- Bun
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
## Install
|
|
19
23
|
|
|
20
24
|
```bash
|
|
21
25
|
npm install -g @bubblebrain-ai/bubble
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
To install from a local tarball:
|
|
25
29
|
|
|
26
30
|
```bash
|
|
27
|
-
npm install -g ./bubblebrain-ai-bubble
|
|
31
|
+
npm install -g ./bubblebrain-ai-bubble-<version>.tgz
|
|
28
32
|
```
|
|
29
33
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
Resume your last conversation:
|
|
50
56
|
|
|
51
57
|
```bash
|
|
52
|
-
bubble --
|
|
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
|
-
##
|
|
132
|
+
## What Bubble can do
|
|
56
133
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
Run `bubble update` at any time to upgrade to the latest published version.
|
|
203
|
+
|
|
204
|
+
## Configuration and storage
|
|
64
205
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
254
|
+
## Feishu host (optional)
|
|
90
255
|
|
|
91
|
-
Bubble
|
|
256
|
+
Bubble can also run as a Feishu (Lark) bot host:
|
|
92
257
|
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
/memory search <query>
|
|
96
|
-
/memory refresh
|
|
258
|
+
```bash
|
|
259
|
+
bubble serve --feishu --setup
|
|
97
260
|
```
|
|
98
261
|
|
|
99
|
-
|
|
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;
|