@bubblebrain-ai/bubble 0.0.22 → 0.0.24
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/internal-reminder-sanitizer.js +29 -9
- package/dist/goal/command.d.ts +20 -0
- package/dist/goal/command.js +71 -0
- package/dist/goal/engine.d.ts +33 -0
- package/dist/goal/engine.js +65 -0
- package/dist/goal/format.d.ts +18 -0
- package/dist/goal/format.js +82 -0
- package/dist/goal/prompts.d.ts +13 -0
- package/dist/goal/prompts.js +84 -0
- package/dist/goal/store.d.ts +61 -0
- package/dist/goal/store.js +161 -0
- package/dist/goal/tools.d.ts +10 -0
- package/dist/goal/tools.js +70 -0
- package/dist/main.js +10 -2
- package/dist/model-catalog.js +17 -0
- package/dist/provider-transform.js +31 -0
- package/dist/session-types.d.ts +3 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +2 -0
- package/dist/tui/run.d.ts +8 -0
- package/dist/tui/run.js +318 -29
- package/dist/tui/trace-groups.js +41 -5
- package/dist/tui-ink/run.d.ts +2 -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.
|
|
@@ -54,17 +54,37 @@ export function sanitizeAssistantProviderMetadata(metadata) {
|
|
|
54
54
|
if (!metadata || !anthropic || !blocks?.length)
|
|
55
55
|
return metadata;
|
|
56
56
|
let changed = false;
|
|
57
|
-
const sanitizedBlocks =
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const sanitizedBlocks = [];
|
|
58
|
+
for (const block of blocks) {
|
|
59
|
+
// Plaintext text blocks are unsigned, so rewriting them in place is safe.
|
|
60
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
61
|
+
const sanitizedText = sanitizeInternalReminderBlocks(block.text);
|
|
62
|
+
if (sanitizedText !== block.text) {
|
|
63
|
+
changed = true;
|
|
64
|
+
sanitizedBlocks.push({ ...block, text: sanitizedText });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
sanitizedBlocks.push(block);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
// Extended-thinking blocks carry an Anthropic signature over their exact
|
|
72
|
+
// text; rewriting the text would invalidate the signature and the API
|
|
73
|
+
// would reject the replayed block. So when a thinking block's text carries
|
|
74
|
+
// internal markup (e.g. an echoed system reminder), DROP the whole block
|
|
75
|
+
// rather than mutate it. Thinking text is never user-visible — the display
|
|
76
|
+
// path renders message.reasoning, not contentBlocks — so dropping loses
|
|
77
|
+
// nothing on screen; it only keeps the verbatim reminder out of the
|
|
78
|
+
// persisted metadata and the Anthropic replay payload. redacted_thinking
|
|
79
|
+
// holds encrypted `data` (no plaintext field) and cannot carry a reminder.
|
|
80
|
+
if (block.type === "thinking" && typeof block.thinking === "string") {
|
|
81
|
+
if (sanitizeInternalReminderBlocks(block.thinking) !== block.thinking) {
|
|
82
|
+
changed = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
64
85
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
86
|
+
sanitizedBlocks.push(block);
|
|
87
|
+
}
|
|
68
88
|
if (!changed)
|
|
69
89
|
return metadata;
|
|
70
90
|
return {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parser for the `/goal` slash command.
|
|
3
|
+
*
|
|
4
|
+
* Forms:
|
|
5
|
+
* /goal -> show summary
|
|
6
|
+
* /goal <objective> [--budget N] -> set a new goal
|
|
7
|
+
* /goal clear | pause | resume
|
|
8
|
+
* /goal edit <new objective>
|
|
9
|
+
*
|
|
10
|
+
* --budget accepts plain integers and k/m suffixes: 200000, 200k, 1.5m.
|
|
11
|
+
*/
|
|
12
|
+
export type GoalCommandKind = "show" | "set" | "clear" | "pause" | "resume" | "edit";
|
|
13
|
+
export interface GoalCommand {
|
|
14
|
+
kind: GoalCommandKind;
|
|
15
|
+
objective?: string;
|
|
16
|
+
tokenBudget?: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseGoalCommand(input: string): GoalCommand;
|
|
20
|
+
export declare function parseBudgetValue(raw: string): number | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parser for the `/goal` slash command.
|
|
3
|
+
*
|
|
4
|
+
* Forms:
|
|
5
|
+
* /goal -> show summary
|
|
6
|
+
* /goal <objective> [--budget N] -> set a new goal
|
|
7
|
+
* /goal clear | pause | resume
|
|
8
|
+
* /goal edit <new objective>
|
|
9
|
+
*
|
|
10
|
+
* --budget accepts plain integers and k/m suffixes: 200000, 200k, 1.5m.
|
|
11
|
+
*/
|
|
12
|
+
const SUBCOMMANDS = new Set(["clear", "pause", "resume", "edit"]);
|
|
13
|
+
export function parseGoalCommand(input) {
|
|
14
|
+
const body = input.trim().replace(/^\/goal\b/, "").trim();
|
|
15
|
+
if (!body)
|
|
16
|
+
return { kind: "show" };
|
|
17
|
+
const firstToken = body.split(/\s+/, 1)[0].toLowerCase();
|
|
18
|
+
const rest = body.slice(firstToken.length).trim();
|
|
19
|
+
if (SUBCOMMANDS.has(firstToken)) {
|
|
20
|
+
if (firstToken === "edit") {
|
|
21
|
+
if (!rest)
|
|
22
|
+
return { kind: "edit", error: "Usage: /goal edit <new objective>" };
|
|
23
|
+
const { text, tokenBudget, error } = extractBudget(rest);
|
|
24
|
+
if (error)
|
|
25
|
+
return { kind: "edit", error };
|
|
26
|
+
const objective = text.trim();
|
|
27
|
+
if (!objective)
|
|
28
|
+
return { kind: "edit", error: "Usage: /goal edit <new objective>" };
|
|
29
|
+
return { kind: "edit", objective, tokenBudget };
|
|
30
|
+
}
|
|
31
|
+
// clear / pause / resume take no arguments.
|
|
32
|
+
if (rest)
|
|
33
|
+
return { kind: firstToken, error: `/goal ${firstToken} takes no arguments` };
|
|
34
|
+
return { kind: firstToken };
|
|
35
|
+
}
|
|
36
|
+
// Anything else is a new objective.
|
|
37
|
+
const { text, tokenBudget, error } = extractBudget(body);
|
|
38
|
+
if (error)
|
|
39
|
+
return { kind: "set", error };
|
|
40
|
+
const objective = text.trim();
|
|
41
|
+
if (!objective)
|
|
42
|
+
return { kind: "set", error: "Usage: /goal <objective> [--budget N]" };
|
|
43
|
+
return { kind: "set", objective, tokenBudget };
|
|
44
|
+
}
|
|
45
|
+
function extractBudget(s) {
|
|
46
|
+
const match = s.match(/--budget(?:=|\s+)(\S+)/);
|
|
47
|
+
if (!match || match.index === undefined)
|
|
48
|
+
return { text: s };
|
|
49
|
+
const value = parseBudgetValue(match[1]);
|
|
50
|
+
if (value === undefined || value <= 0) {
|
|
51
|
+
return { text: s, error: `Invalid --budget value: "${match[1]}" (use e.g. 200000, 200k, 1.5m)` };
|
|
52
|
+
}
|
|
53
|
+
const text = (s.slice(0, match.index) + s.slice(match.index + match[0].length))
|
|
54
|
+
.replace(/\s+/g, " ")
|
|
55
|
+
.trim();
|
|
56
|
+
return { text, tokenBudget: value };
|
|
57
|
+
}
|
|
58
|
+
export function parseBudgetValue(raw) {
|
|
59
|
+
const match = raw.trim().match(/^(\d+(?:\.\d+)?)([kmKM]?)$/);
|
|
60
|
+
if (!match)
|
|
61
|
+
return undefined;
|
|
62
|
+
let value = parseFloat(match[1]);
|
|
63
|
+
if (!Number.isFinite(value))
|
|
64
|
+
return undefined;
|
|
65
|
+
const suffix = match[2].toLowerCase();
|
|
66
|
+
if (suffix === "k")
|
|
67
|
+
value *= 1_000;
|
|
68
|
+
else if (suffix === "m")
|
|
69
|
+
value *= 1_000_000;
|
|
70
|
+
return Math.round(value);
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for the goal auto-continuation loop.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the TUI so the stop conditions can be unit-tested directly. The
|
|
5
|
+
* TUI calls shouldContinueGoal() after each goal turn finishes and either fires
|
|
6
|
+
* another turn or stops with the returned reason.
|
|
7
|
+
*
|
|
8
|
+
* The agent decides when the work is done — there is intentionally NO turn-count
|
|
9
|
+
* cap (unlike a fixed iteration limit). The loop only stops when:
|
|
10
|
+
* - the model marks the goal complete/blocked (via update_goal),
|
|
11
|
+
* - the user pauses/clears it,
|
|
12
|
+
* - the run is interrupted or the provider errors (out of quota, network, …),
|
|
13
|
+
* - or a user-set token budget is exhausted.
|
|
14
|
+
* Otherwise it keeps going.
|
|
15
|
+
*/
|
|
16
|
+
import type { GoalState } from "./store.js";
|
|
17
|
+
export type GoalStopReason = "complete" | "blocked" | "paused" | "budget" | "error" | "cancelled" | "user_input" | "no_goal";
|
|
18
|
+
export interface ContinueDecisionInput {
|
|
19
|
+
goal: GoalState | null;
|
|
20
|
+
/** The last run was interrupted/cancelled by the user. */
|
|
21
|
+
cancelled?: boolean;
|
|
22
|
+
/** The last run failed with a provider/run error (quota, network, API). */
|
|
23
|
+
errored?: boolean;
|
|
24
|
+
/** Number of user inputs queued to run next (a real message preempts the goal). */
|
|
25
|
+
queuedInputs?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ContinueDecision {
|
|
28
|
+
continue: boolean;
|
|
29
|
+
reason?: GoalStopReason;
|
|
30
|
+
}
|
|
31
|
+
export declare function shouldContinueGoal(input: ContinueDecisionInput): ContinueDecision;
|
|
32
|
+
/** Human-readable one-liner explaining why auto-continuation stopped. */
|
|
33
|
+
export declare function stopReasonNotice(reason: GoalStopReason | undefined): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for the goal auto-continuation loop.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the TUI so the stop conditions can be unit-tested directly. The
|
|
5
|
+
* TUI calls shouldContinueGoal() after each goal turn finishes and either fires
|
|
6
|
+
* another turn or stops with the returned reason.
|
|
7
|
+
*
|
|
8
|
+
* The agent decides when the work is done — there is intentionally NO turn-count
|
|
9
|
+
* cap (unlike a fixed iteration limit). The loop only stops when:
|
|
10
|
+
* - the model marks the goal complete/blocked (via update_goal),
|
|
11
|
+
* - the user pauses/clears it,
|
|
12
|
+
* - the run is interrupted or the provider errors (out of quota, network, …),
|
|
13
|
+
* - or a user-set token budget is exhausted.
|
|
14
|
+
* Otherwise it keeps going.
|
|
15
|
+
*/
|
|
16
|
+
export function shouldContinueGoal(input) {
|
|
17
|
+
const { goal } = input;
|
|
18
|
+
if (!goal)
|
|
19
|
+
return { continue: false, reason: "no_goal" };
|
|
20
|
+
if (input.errored)
|
|
21
|
+
return { continue: false, reason: "error" };
|
|
22
|
+
if (input.cancelled)
|
|
23
|
+
return { continue: false, reason: "cancelled" };
|
|
24
|
+
if ((input.queuedInputs ?? 0) > 0)
|
|
25
|
+
return { continue: false, reason: "user_input" };
|
|
26
|
+
switch (goal.status) {
|
|
27
|
+
case "complete":
|
|
28
|
+
return { continue: false, reason: "complete" };
|
|
29
|
+
case "blocked":
|
|
30
|
+
return { continue: false, reason: "blocked" };
|
|
31
|
+
case "paused":
|
|
32
|
+
return { continue: false, reason: "paused" };
|
|
33
|
+
case "budget_limited":
|
|
34
|
+
return { continue: false, reason: "budget" };
|
|
35
|
+
case "active":
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
// Only an explicit, user-set token budget bounds the loop; with no budget it
|
|
39
|
+
// runs until the model finishes, the user stops it, or the provider errors.
|
|
40
|
+
if (goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget) {
|
|
41
|
+
return { continue: false, reason: "budget" };
|
|
42
|
+
}
|
|
43
|
+
return { continue: true };
|
|
44
|
+
}
|
|
45
|
+
/** Human-readable one-liner explaining why auto-continuation stopped. */
|
|
46
|
+
export function stopReasonNotice(reason) {
|
|
47
|
+
switch (reason) {
|
|
48
|
+
case "complete":
|
|
49
|
+
return "Goal complete.";
|
|
50
|
+
case "blocked":
|
|
51
|
+
return "Goal marked blocked — /goal resume to retry.";
|
|
52
|
+
case "paused":
|
|
53
|
+
return "Goal paused — /goal resume to continue.";
|
|
54
|
+
case "budget":
|
|
55
|
+
return "Goal hit its token budget — /goal resume to continue.";
|
|
56
|
+
case "error":
|
|
57
|
+
return "Goal paused — the provider errored. Fix it, then /goal resume.";
|
|
58
|
+
case "cancelled":
|
|
59
|
+
return "Goal paused (interrupted) — /goal resume to continue.";
|
|
60
|
+
case "user_input":
|
|
61
|
+
return "Goal paused for your input — it resumes after this turn.";
|
|
62
|
+
default:
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display helpers for the goal feature — shared by the goal tools (model-facing
|
|
3
|
+
* summary), the `/goal` summary command, and the TUI status-line indicator.
|
|
4
|
+
*/
|
|
5
|
+
import type { GoalState, GoalStatus } from "./store.js";
|
|
6
|
+
export declare function goalStatusLabel(status: GoalStatus): string;
|
|
7
|
+
/** Compact token count: 950, 1.2K, 63.9K, 1.5M. */
|
|
8
|
+
export declare function formatTokensCompact(tokens: number): string;
|
|
9
|
+
/** Full multi-detail summary, e.g. for the model's get_goal result. */
|
|
10
|
+
export declare function goalSummaryText(goal: GoalState): string;
|
|
11
|
+
/**
|
|
12
|
+
* Terminal notice shown when a goal finishes, with the accurate final token
|
|
13
|
+
* spend. Call only after the finishing run's tokens have been accounted (the
|
|
14
|
+
* update_goal tool can't report this — see goal/tools.ts).
|
|
15
|
+
*/
|
|
16
|
+
export declare function goalCompleteNotice(goal: GoalState): string;
|
|
17
|
+
/** Compact single-line indicator for the status line / sidebar. */
|
|
18
|
+
export declare function goalIndicatorLine(goal: GoalState, maxObjective?: number): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display helpers for the goal feature — shared by the goal tools (model-facing
|
|
3
|
+
* summary), the `/goal` summary command, and the TUI status-line indicator.
|
|
4
|
+
*/
|
|
5
|
+
export function goalStatusLabel(status) {
|
|
6
|
+
switch (status) {
|
|
7
|
+
case "active":
|
|
8
|
+
return "active";
|
|
9
|
+
case "paused":
|
|
10
|
+
return "paused";
|
|
11
|
+
case "blocked":
|
|
12
|
+
return "blocked";
|
|
13
|
+
case "budget_limited":
|
|
14
|
+
return "budget limited";
|
|
15
|
+
case "complete":
|
|
16
|
+
return "complete";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Compact token count: 950, 1.2K, 63.9K, 1.5M. */
|
|
20
|
+
export function formatTokensCompact(tokens) {
|
|
21
|
+
const n = Math.max(0, Math.round(tokens));
|
|
22
|
+
if (n < 1_000)
|
|
23
|
+
return String(n);
|
|
24
|
+
if (n < 1_000_000)
|
|
25
|
+
return `${trimZero(n / 1_000)}K`;
|
|
26
|
+
return `${trimZero(n / 1_000_000)}M`;
|
|
27
|
+
}
|
|
28
|
+
function trimZero(value) {
|
|
29
|
+
const rounded = Math.round(value * 10) / 10;
|
|
30
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
31
|
+
}
|
|
32
|
+
function tokensPart(goal) {
|
|
33
|
+
if (goal.tokenBudget !== undefined) {
|
|
34
|
+
return `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok`;
|
|
35
|
+
}
|
|
36
|
+
if (goal.tokensUsed > 0)
|
|
37
|
+
return `${formatTokensCompact(goal.tokensUsed)} tok`;
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Full multi-detail summary, e.g. for the model's get_goal result. */
|
|
41
|
+
export function goalSummaryText(goal) {
|
|
42
|
+
const parts = [
|
|
43
|
+
`Objective: ${goal.objective}`,
|
|
44
|
+
`Status: ${goalStatusLabel(goal.status)}.`,
|
|
45
|
+
`Turns: ${goal.turnsSpent}.`,
|
|
46
|
+
];
|
|
47
|
+
const tokens = tokensPart(goal);
|
|
48
|
+
if (tokens)
|
|
49
|
+
parts.push(`Tokens: ${tokens}.`);
|
|
50
|
+
if (goal.tokenBudget !== undefined) {
|
|
51
|
+
const remaining = Math.max(0, goal.tokenBudget - goal.tokensUsed);
|
|
52
|
+
parts.push(`Remaining budget: ${formatTokensCompact(remaining)} tok.`);
|
|
53
|
+
}
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Terminal notice shown when a goal finishes, with the accurate final token
|
|
58
|
+
* spend. Call only after the finishing run's tokens have been accounted (the
|
|
59
|
+
* update_goal tool can't report this — see goal/tools.ts).
|
|
60
|
+
*/
|
|
61
|
+
export function goalCompleteNotice(goal) {
|
|
62
|
+
const tokens = goal.tokenBudget !== undefined
|
|
63
|
+
? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok`
|
|
64
|
+
: `${formatTokensCompact(goal.tokensUsed)} tok`;
|
|
65
|
+
const turns = `${goal.turnsSpent} ${goal.turnsSpent === 1 ? "turn" : "turns"}`;
|
|
66
|
+
return `Goal complete — ${tokens} used over ${turns}.`;
|
|
67
|
+
}
|
|
68
|
+
/** Compact single-line indicator for the status line / sidebar. */
|
|
69
|
+
export function goalIndicatorLine(goal, maxObjective = 48) {
|
|
70
|
+
const segments = [`goal: ${goalStatusLabel(goal.status)}`, `${goal.turnsSpent} turns`];
|
|
71
|
+
const tokens = tokensPart(goal);
|
|
72
|
+
if (tokens)
|
|
73
|
+
segments.push(tokens);
|
|
74
|
+
const objective = truncateObjective(goal.objective, maxObjective);
|
|
75
|
+
return `${segments.join(" · ")} — ${objective}`;
|
|
76
|
+
}
|
|
77
|
+
function truncateObjective(objective, max) {
|
|
78
|
+
const single = objective.replace(/\s+/g, " ").trim();
|
|
79
|
+
if (single.length <= max)
|
|
80
|
+
return single;
|
|
81
|
+
return `${single.slice(0, Math.max(0, max - 1))}…`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing prompts for the autonomous `/goal` feature.
|
|
3
|
+
*
|
|
4
|
+
* Ported and trimmed from Codex's `ext/goal/templates/goals/*.md`. These are
|
|
5
|
+
* injected into the model context (wrapped as an internal context block, so
|
|
6
|
+
* they never render as a user bubble) at the start of each goal turn. The
|
|
7
|
+
* objective is treated as untrusted data: XML-escaped and fenced in
|
|
8
|
+
* <objective> so it cannot be read as higher-priority instructions.
|
|
9
|
+
*/
|
|
10
|
+
import type { GoalState } from "./store.js";
|
|
11
|
+
export declare function continuationPrompt(goal: GoalState): string;
|
|
12
|
+
export declare function initialPrompt(goal: GoalState): string;
|
|
13
|
+
export declare function budgetLimitPrompt(goal: GoalState): string;
|