pi-ultracode 0.1.2 → 0.3.0

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 CHANGED
@@ -12,7 +12,7 @@ Ultracode 保留 Pi 自身的核心循环、内存模型、工具系统、扩展
12
12
  | --- | --- |
13
13
  | `workflow` 工具 | 模型请求前按 Ultracode 状态校准;运行确定性的 JavaScript 编排脚本,并调用 `agent()`、`parallel()`、`pipeline()` 和 `workflow()`。 |
14
14
  | 独立子代理 | 每次 `agent()` 调用都会创建一条带标准编码工具的新 Pi 会话。 |
15
- | 并行执行 | `parallel()` 并发运行任务,且保留输入顺序。 |
15
+ | 并行执行 | `parallel()` 并发运行任务、保留输入顺序,并在启动前整批预留 agent slots。 |
16
16
  | 流水线 | `pipeline()` 让任务独立流经多个阶段,无需全局屏障。 |
17
17
  | 结构化输出 | 传入 JSON Schema,即可通过终止型 `structured_output` 工具获得经校验的对象。 |
18
18
  | Agent Type | 通过 `.pi/ultracode/agents/*.md` 定义角色提示词、工具白名单、模型和思考强度。 |
@@ -20,7 +20,7 @@ Ultracode 保留 Pi 自身的核心循环、内存模型、工具系统、扩展
20
20
  | Worktree 隔离 | 让写入型并行代理在临时 git worktree 中运行,并把改动安全合并回共享工作树。 |
21
21
  | 进度与详情 | 实时状态、实际模型/effort、turn/工具/token 统计,以及可流式展开的 TUI 任务详情浮层。 |
22
22
  | Ultracode 模式 | `/ultracode on` 启用主动编排提示词,并请求当前模型支持的最高思考强度。 |
23
- | 配额执行 | 可选 token 预算、代理数量限制、并发限制、脚本超时与嵌套深度限制。 |
23
+ | 执行限制 | `maxAgents`(默认 128/上限 1024)、每进程且每 session/runsDir 最多 4 个活动 workflow、并发限制与嵌套深度限制。 |
24
24
 
25
25
  ## 为什么使用工作流
26
26
 
@@ -32,7 +32,7 @@ Ultracode 保留 Pi 自身的核心循环、内存模型、工具系统、扩展
32
32
  - 哪些阶段依赖前一阶段的结果;
33
33
  - 哪些代理应返回结构化数据;
34
34
  - 何时需要共享上下文,何时需要隔离;
35
- - 预算如何限制额外探索;
35
+ - 何时停止额外探索;
36
36
  - 失败分支应如何降级。
37
37
 
38
38
  ## 安装
@@ -82,7 +82,7 @@ pi --ultracode
82
82
  工作流运行时会内联显示实时进度,例如:
83
83
 
84
84
  ```text
85
- ◆ ▶ audit_repo (4/7 done, 2 running) · 3 cached · 203k token (141k new, 62k replayed) · 41.2k/500k out
85
+ ◆ ▶ audit_repo (4/7 done, 2 running) · 3 cached · 203k token (141k new, 62k replayed)
86
86
  ✓ Survey 1/1
87
87
  #1 ✓ repo inventory
88
88
  gpt-5.6-sol • max · 15 turns · 42 tool uses · 141k token
@@ -101,11 +101,8 @@ pi --ultracode
101
101
  ```text
102
102
  /ultracode # toggle on/off
103
103
  /ultracode on # enable orchestration mode
104
- /ultracode on 500k # enable it with an output-token budget
105
104
  /ultracode off # disable the tool and restore the previous thinking level
106
- /ultracode status # show status and the configured budget
107
- /ultracode budget 500k # set a token budget
108
- /ultracode budget off # remove the budget
105
+ /ultracode status # show current status
109
106
  /workflows # open the interactive workflow detail overlay
110
107
  /workflows <runId> # open one run directly (prefix accepted)
111
108
  /workflows abort # abort active runs
@@ -155,14 +152,15 @@ export const meta = {
155
152
  脚本可使用下列全局变量:
156
153
 
157
154
  - `agent(prompt, options)`
158
- - `parallel(thunks)`
155
+ - `parallel(thunks, options?)`
159
156
  - `pipeline(items, ...stages)`
160
157
  - `phase(title)`
161
158
  - `log(message)`
162
159
  - `workflow(nameOrRef, args)`
163
160
  - `args`
164
161
  - `cwd`
165
- - `budget`
162
+
163
+ workflow 工具本身支持 `maxAgents?: integer`:默认 128,范围 1–1024。它限制同一 run 在首次执行及所有 resume 中累计获准启动的 live agent;cache replay 不重复占用额度。它不是 token budget,token/cost 仍只用于观测展示。
166
164
 
167
165
  `agent()` 的主要选项:
168
166
 
@@ -170,16 +168,20 @@ export const meta = {
170
168
  | --- | --- |
171
169
  | `label` | 进度中显示的 2–5 个词短名称。 |
172
170
  | `phase` | 显式指定进度阶段。 |
173
- | `schema` | 普通 JSON Schema;成功时返回经 `structured_output` 校验的对象。 |
171
+ | `schema` | 内联的有界 JSON Schema 子集(≤256 KiB、深度≤64);成功时返回经 `structured_output` 校验且≤2 MiB 的对象。 |
174
172
  | `model` | 按模式覆盖模型,也可带 `:off`、`:high`、`:xhigh` 或 `:max`。 |
175
173
  | `agentType` | 使用内置或自定义角色。 |
176
174
  | `isolation: "worktree"` | 在临时 git worktree 中运行写入型代理。 |
177
175
 
178
- 为保证可恢复和确定性,脚本不提供 Node.js 模块加载能力,也不允许直接使用 `fs`、网络、`Date.now()`、`new Date()` 或 `Math.random()`。
176
+ 为保证可恢复和确定性,脚本不提供 Node.js 模块加载能力,也不允许直接使用 `fs`、网络、`Date.now()`、`new Date()` 或 `Math.random()`。Worker 使用 context-realm bridge,禁用字符串/Wasm code generation,并设置 128 MiB V8 old-generation heap 上限;Intl/Temporal、ArrayBuffer、SharedArrayBuffer、typed arrays 与 WebAssembly 不向脚本开放。控制流另受循环/函数 checkpoint、host-call fuel、AST 结构限制、同步 stall watchdog 和无 host RPC 时的 idle-progress watchdog 约束。这些是确定性与 liveness guard,不是安全沙箱,也不宣称与 Grok Build 的 Rhai operation fuel 完全等价。所有 `agent()`、`parallel()`、`pipeline()` 和 `workflow()` 返回值都必须直接被 await 或 return;原生 `.then/.catch/.finally` 链、动态 method call(`value[key](...)`)和 `Promise.all/allSettled/race/any` 会在执行前被拒绝(本地异步仅保留 `new Promise(...)` 与 `Promise.resolve(...)`)。并发编排必须使用 `parallel()`/`pipeline()`,以保持 call path 可恢复且不受完成顺序影响;同 scope 的 sibling async orchestration 及未观察或仍 pending 的编排 promise 也会使 workflow 失败。
177
+
178
+ 含编排调用的 helper 也属于可恢复身份的一部分,因此只支持直接声明的函数/函数变量,或声明对象/已赋给标识符的 class instance 上的静态 method call。helper alias、事后赋值 method、`this.otherMethod()` 转发、临时/awaited factory receiver 与高阶 callable 会在执行前 fail closed;请先改写为一个直接声明的 helper,再从明确的源码调用点调用它。编译器会把源码调用点、词法 loop iteration、helper caller scope、请求指纹和同请求 occurrence 组合成 durable identity,避免条件分支、循环、并行完成顺序或 resume 重试改变 cache 对应关系。
179
+
180
+ 资源输入输出同样有硬上限:`args` 1 MiB、schema 256 KiB/64 层、每个 agent/workflow output 2 MiB、journal 64 MiB。args/schema/output 必须是严格 JSON tree(只允许可持久化的有限 number〔不含 `-0`〕、string、boolean、null、array 与 plain object;拒绝 binary objects、accessor、稀疏数组以及循环/重复对象引用)。schema 只接受明确支持的子集:类型、object/array 结构、`enum`/`const`、`anyOf`/`allOf`、长度和数值约束及注解;未知关键字会直接拒绝。`$ref` 家族、`oneOf`、`format` 与所有 regex 关键字(包括 `pattern`/`patternProperties`)均不支持;JavaScript regex validation 无可靠执行上限。journal 在每次 append 前检查上限并校验记录语义;单进程恢复时只会修复 EOF 处的 torn JSONL 尾记录,文件中部损坏仍 fail closed。
179
181
 
180
182
  ### 取消与清理
181
183
 
182
- 按 `Esc` 取消工作流时,取消信号会覆盖子代理初始化、异步 preflight 和流式执行窗口。运行器会等待进行中的 `abort()` 完成后再释放会话;清理失败不会覆盖原始 prompt 或取消错误。兼容旧版 Pi 时创建的临时 fallback 会话也遵循同一清理规则。
184
+ 按 `Esc` 取消工作流时,取消信号会覆盖子代理初始化、异步 preflight 和流式执行窗口。运行器会等待进行中的 `abort()`,但 cleanup 最多等待 25 秒;即使 provider 忽略取消,workflow lease 也会在有界清理后释放。兼容旧版 Pi 时创建的临时 fallback 会话同样遵循该规则。
183
185
 
184
186
  ### 直接调用
185
187
 
@@ -194,12 +196,12 @@ const result = await agent(
194
196
  { label: "mode review" },
195
197
  );
196
198
 
197
- export default result;
199
+ return result;
198
200
  ```
199
201
 
200
202
  ### 并行检视
201
203
 
202
- `parallel()` 接收 thunk,而不是已经启动的 Promise
204
+ `parallel()` 接收 thunk,而不是已经启动的 Promise。每个 panel 在任意 thunk 启动前原子预留 `options.reserveAgents ?? thunks.length` 个 agent slot;`reserveAgents` 不能小于 thunk 数。每个 thunk 保有一个独立基础 slot,其余显式预留量由 panel 共享;调用超过预留量会使整个 workflow 失败,不会临时挪用未预留的 `maxAgents` 容量。
203
205
 
204
206
  ```js
205
207
  export const meta = {
@@ -215,9 +217,10 @@ const tasks = [
215
217
 
216
218
  const results = await parallel(
217
219
  tasks.map(([label, prompt]) => () => agent(prompt, { label })),
220
+ { reserveAgents: tasks.length },
218
221
  );
219
222
 
220
- export default results.filter(Boolean);
223
+ return results.filter(Boolean);
221
224
  ```
222
225
 
223
226
  ### 流水线
@@ -244,12 +247,12 @@ const results = await pipeline(
244
247
  ),
245
248
  );
246
249
 
247
- export default results.filter(Boolean);
250
+ return results.filter(Boolean);
248
251
  ```
249
252
 
250
253
  ### 结构化输出
251
254
 
252
- 不传 `schema` 时,`agent()` 返回子代理最后一段非空 assistant 文本;传入普通 JSON Schema 时,则返回经校验的对象:
255
+ 不传 `schema` 时,`agent()` 返回子代理最后一段非空 assistant 文本;传入支持的 JSON Schema 子集时,则返回经校验的对象。两种返回值都必须是≤2 MiB 的严格 JSON value:
253
256
 
254
257
  ```js
255
258
  export const meta = {
@@ -273,7 +276,7 @@ const finding = await agent(
273
276
  },
274
277
  );
275
278
 
276
- export default finding;
279
+ return finding;
277
280
  ```
278
281
 
279
282
  使用 schema 的代理必须调用 `structured_output`。否则该 `agent()` 分支会失败并返回 `null`;工作流本身仍可继续,因此脚本应过滤或显式处理失败分支,而不会把自由文本伪装成结构化结果。
@@ -339,7 +342,7 @@ await agent("Analyze this failure.", {
339
342
  });
340
343
  ```
341
344
 
342
- 若未指定 model,则子代理继承工作流工具创建时的 Pi 当前模型。Ultracode 启用时,默认子代理强度同样请求 `max`,并按每个代理实际选择的模型独立钳制。Ultracode 不会自动启用 GPT-5.6 的 `pro` 模式,也不会隐式设置 token 预算。
345
+ 若未指定 model,则子代理继承工作流工具创建时的 Pi 当前模型。Ultracode 启用时,默认子代理强度同样请求 `max`,并按每个代理实际选择的模型独立钳制。Ultracode 不会自动启用 GPT-5.6 的 `pro` 模式。
343
346
 
344
347
  ## Worktree 隔离
345
348
 
@@ -381,7 +384,7 @@ await agent("Implement the requested change and run focused tests.", {
381
384
  await workflow("saved_workflow_name", { target: "src" });
382
385
  ```
383
386
 
384
- 嵌套限制为一层,且共享父运行的并发上限、代理计数与 token 预算。
387
+ 嵌套限制为一层,且共享父运行的并发上限、`maxAgents`、当前 parallel reservations 与代理计数。由 workflow 启动的 child Pi session 会显式排除 `workflow` 工具,不能再创建一条拥有独立额度的新 workflow 链。
385
388
 
386
389
  每次运行都会把脚本和 JSONL journal 保存到:
387
390
 
@@ -390,36 +393,38 @@ await workflow("saved_workflow_name", { target: "src" });
390
393
  <sessionDir>/ultracode-runs/<runId>.jsonl
391
394
  ```
392
395
 
393
- 暂停、终止或修改脚本后,可再次调用 `workflow` 工具并传入 `resumeFromRunId`。最长的未变 `agent()` 调用前缀会立即返回缓存结果;第一个变化或新增的调用及其后续调用会实时执行。
394
-
395
- ## 预算与限制
396
-
397
- 默认 token 预算为无限。只有显式配置后,`budget.total` 才是有限值。
398
-
399
- 动态探索必须同时检查预算是否存在:
396
+ 没有 sessionDir 的独立调用使用用户目录下按 cwd 哈希隔离的 `~/.pi/ultracode-runs/<cwd-hash>/`,不会写入仓库可控制的 `.pi` symlink。run script、journal、details 和 transcript 会拒绝 symlink/non-regular artifacts,并使用 no-follow/独占或原子写入。与 Pi 和 Grok Build 一样,session 采用单进程 owner 模型;同一 Pi 进程内由 process-local lease 阻止重复运行同一 runId,但不支持两个 Pi 进程并发写同一个 session。
400
397
 
401
- ```js
402
- while (budget.total && budget.remaining() > 50_000) {
403
- // additional bounded exploration
404
- break;
405
- }
406
- ```
398
+ 已有 run 可再次调用 `workflow` 并传入 `resumeFromRunId`。resume 是 immutable:顶层 script/args、实际解析到的 Agent Type 定义、effective model/thinking,以及每个 nested workflow call path 的源码/args 都必须和原 run 完全一致;args 的可观察对象键序也属于 identity。需要修改工作时必须启动新 run。成功的 agent 结果按 Worker 生成的稳定结构 call path replay,因此不受 parallel/pipeline 完成顺序影响。live admission 会在 runner side effect 前持久化,并在整个 run 生命周期累计;恢复时省略 `maxAgents` 会继承当前上限,显式值只能提高、不能降低。不存在或格式不受支持的 runId 会明确失败。
407
399
 
408
- 若不检查 `budget.total`,无限预算下的 `remaining()` 是 `Infinity`,循环可能一直运行到代理数量上限。
400
+ ## 限制
409
401
 
410
- 其他限制:
402
+ 运行时限制:
411
403
 
412
- - 最大代理数;
413
- - 最大并发数;
414
- - 工作流脚本超时;
415
- - 嵌套深度;
404
+ - run-lifetime `maxAgents` live admission(默认 128,绝对上限 1024;cache replay 免费);
405
+ - 同一进程、同一 session/runsDir 最多 4 个活动顶层 workflow;
406
+ - 每个 run 最大并发 16;
407
+ - 最多一层嵌套 workflow;
408
+ - 10,000 次 host-call fuel、AST/checkpoint、stall 与 idle-progress 限制;
409
+ - Worker V8 old-generation heap 128 MiB,并禁用 string/Wasm code generation 与二进制内存构造器;
410
+ - args/schema/structured output/journal 字节与结构上限;
411
+ - 取消后的 25 秒 cleanup deadline;
412
+ - child session 显式排除 `workflow` 工具;
416
413
  - 结构化输出校验;
417
- - 静态禁止不确定性与危险全局变量。
414
+ - 静态及 context-realm 运行期确定性 guard。
418
415
 
419
416
  ## 会话与兼容性
420
417
 
421
418
  Ultracode 模式状态通过自定义 session entry 持久化。恢复、reload、fork 和 `/tree` 导航都会按当前 branch 重新读取状态;被丢弃分支中的 entry 不会错误启用模式。旧版 Pi 或旧模型会把 `max` 兼容回退为 `xhigh`,而不会把未知值静默变成 `off`。
422
419
 
420
+ ### 0.3 迁移说明
421
+
422
+ 0.3 引入 Grok Build 式结构控制:`maxAgents` 默认为 128(范围 1–1024),并在同一 run 的所有 resume 中累计 live admissions;cache replay 不重复占用。`parallel()` 会在启动前按 panel 原子预留 slots,`reserveAgents < thunks.length`、调用超过预留量或超过 `maxAgents` 都是 fatal policy failure。一个 thunk 会调用多个 agent 时,必须用 `reserveAgents` 显式覆盖全部潜在调用。resume 改为 immutable script/args,并按稳定结构 call path replay;旧的“修改脚本后最长前缀恢复”不再支持。同一进程、同一 session/runsDir 同时最多 4 个顶层 workflow,取消清理有 25 秒上限,run artifacts 拒绝 symlink。
423
+
424
+ ### 0.2 迁移说明
425
+
426
+ 0.2 移除了 token budget 功能:`/ultracode budget` 命令、workflow tool 的 `budget` 参数,以及 workflow 脚本中的全局 `budget` 对象都不再存在。旧的 saved workflow 如果引用 `budget.total`、`budget.remaining()` 或 `budget.spent()`,需要删除这些引用,改为显式的 agent 数量、循环条件或输入列表边界。
427
+
423
428
  扩展清理只管理自己的详情浮层、status 与运行状态,不会调用 Pi 的全局 `ui.clear()`,因此不会清除其他扩展的 UI。
424
429
 
425
430
  ## 开发
@@ -457,7 +462,7 @@ npm 发布由 [`.github/workflows/publish-npm.yml`](.github/workflows/publish-np
457
462
  同时在 GitHub 仓库中创建名为 `npm` 的 Environment;可以按需增加审批人。正常发布无需打开 Actions 页面:
458
463
 
459
464
  ```bash
460
- npm version patch # or: minor / major / 0.2.0-beta.1
465
+ npm version patch # or: minor / major / x.y.z-beta.1
461
466
  git push origin main --follow-tags
462
467
  ```
463
468
 
@@ -44,8 +44,11 @@ const VERDICT = {
44
44
  const seen = new Set()
45
45
  const confirmed = []
46
46
  let dry = 0
47
+ let roundsRun = 0
48
+ const MAX_ROUNDS = 2
47
49
 
48
- while (dry < 2) {
50
+ while (dry < 2 && roundsRun < MAX_ROUNDS) {
51
+ roundsRun++
49
52
  const rounds = await parallel(
50
53
  FINDERS.map((prompt, i) => () => agent(prompt, { label: 'find ' + i, phase: 'Find', schema: BUGS })),
51
54
  )
@@ -59,16 +62,23 @@ while (dry < 2) {
59
62
  for (const b of fresh) seen.add(b.file + ':' + b.desc)
60
63
 
61
64
  const judged = await parallel(
62
- fresh.map((b) => () =>
63
- parallel(
65
+ fresh.map((b) => async () => {
66
+ const votes = await parallel(
64
67
  ['correctness', 'security', 'does-it-reproduce'].map((lens) => () =>
65
68
  agent('Judge via the ' + lens + ' lens — is this real? "' + b.desc + '" (' + b.file + '). Default to real:false if unsure.',
66
69
  { label: 'verify ' + lens, phase: 'Verify', agentType: 'code-reviewer', schema: VERDICT }),
67
70
  ),
68
- ).then((votes) => ({ bug: b, real: votes.filter(Boolean).filter((v) => v.real).length >= 2 })),
69
- ),
71
+ )
72
+ return { bug: b, real: votes.filter(Boolean).filter((v) => v.real).length >= 2 }
73
+ }),
70
74
  )
71
75
  confirmed.push(...judged.filter((j) => j.real).map((j) => j.bug))
72
76
  }
73
77
 
74
- return { confirmed, totalSeen: seen.size }
78
+ const complete = dry >= 2
79
+ const truncated = !complete
80
+ if (truncated) {
81
+ log('Stopped after MAX_ROUNDS=' + MAX_ROUNDS + ' before reaching two consecutive dry rounds; returning truncated results.')
82
+ }
83
+
84
+ return { confirmed, totalSeen: seen.size, roundsRun, complete, truncated }
@@ -39,7 +39,6 @@ export default function extension(pi: ExtensionAPI, extraDeps: UltracodeExtensio
39
39
  } = extraDeps;
40
40
 
41
41
  const workflowTool = createWorkflowTool({
42
- getDefaultBudget: () => mode.getBudget(),
43
42
  getThinkingLevel: () => mode.getSubagentThinkingLevel(),
44
43
  ...workflowDeps,
45
44
  isExecutionAllowed: () => mode.isEnforcing(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-ultracode",
3
- "version": "0.1.2",
4
- "description": "Claude-Code-style \"ultracode\" for Pi: an effort mode that defaults to deterministic multi-agent workflow orchestration, with worktree isolation, per-agent model overrides, custom agent types, nested workflows, token budgets, resumable runs, and a /workflows manager.",
3
+ "version": "0.3.0",
4
+ "description": "Claude-Code-style \"ultracode\" for Pi: an effort mode that defaults to deterministic multi-agent workflow orchestration, with worktree isolation, per-agent model overrides, custom agent types, nested workflows, resumable runs, and a /workflows manager.",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package",
@@ -53,12 +53,12 @@
53
53
  "acorn": "^8.11.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@earendil-works/pi-agent-core": "^0.80.10",
57
- "@earendil-works/pi-ai": "^0.80.10",
58
- "@earendil-works/pi-coding-agent": "^0.80.10",
59
- "@earendil-works/pi-tui": "^0.80.10",
56
+ "@earendil-works/pi-agent-core": "^0.83.0",
57
+ "@earendil-works/pi-ai": "^0.83.0",
58
+ "@earendil-works/pi-coding-agent": "^0.83.0",
59
+ "@earendil-works/pi-tui": "^0.83.0",
60
60
  "@types/node": "^24.12.4",
61
- "typebox": "1.1.38",
61
+ "typebox": "1.3.7",
62
62
  "typescript": "^5.9.3"
63
63
  },
64
64
  "peerDependencies": {
package/src/commands.ts CHANGED
@@ -3,16 +3,16 @@
3
3
  */
4
4
 
5
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
- import { parseBudget, type UltracodeMode } from "./mode.ts";
6
+ import type { UltracodeMode } from "./mode.ts";
7
7
  import { getRegistry } from "./workflow/registry.ts";
8
8
  import { workflowRunsDir } from "./workflow/tool.ts";
9
9
  import { openWorkflowOverlay } from "./workflow/workflow-overlay.ts";
10
10
 
11
11
  export function registerCommands(pi: ExtensionAPI, mode: UltracodeMode): void {
12
12
  pi.registerCommand("ultracode", {
13
- description: "Toggle ultracode mode (max thinking + default workflow orchestration). Bare /ultracode toggles; subcommands: on|off|status|budget <n>",
13
+ description: "Toggle ultracode mode (max thinking + default workflow orchestration). Bare /ultracode toggles; subcommands: on|off|status",
14
14
  getArgumentCompletions(prefix: string) {
15
- return ["on", "off", "status", "budget"]
15
+ return ["on", "off", "status"]
16
16
  .filter((s) => s.startsWith(prefix))
17
17
  .map((value) => ({ value, label: value }));
18
18
  },
@@ -34,11 +34,19 @@ export function registerCommands(pi: ExtensionAPI, mode: UltracodeMode): void {
34
34
  }
35
35
 
36
36
  if (sub === "status") {
37
+ if (parts.length > 1) {
38
+ ctx.ui.notify(ultracodeUsage(), "error");
39
+ return;
40
+ }
37
41
  ctx.ui.notify(mode.statusLine(), "info");
38
42
  return;
39
43
  }
40
44
 
41
45
  if (sub === "off") {
46
+ if (parts.length > 1) {
47
+ ctx.ui.notify(ultracodeUsage(), "error");
48
+ return;
49
+ }
42
50
  mode.disable(pi);
43
51
  await mode.flushThinkingPreference();
44
52
  ctx.ui.notify("Ultracode off — thinking restored and workflow tool disabled.", "info");
@@ -46,30 +54,14 @@ export function registerCommands(pi: ExtensionAPI, mode: UltracodeMode): void {
46
54
  return;
47
55
  }
48
56
 
49
- if (sub === "budget") {
50
- const budget = parts[1] ? parseBudget(parts[1]) : null;
51
- mode.setBudget(pi, budget);
52
- ctx.ui.notify(
53
- budget ? `Ultracode token budget set to ~${budget} output tokens.` : "Ultracode token budget cleared.",
54
- "info",
55
- );
56
- ctx.ui.setStatus("ultracode", mode.statusLine());
57
+ if (sub !== "on" || parts.length > 1) {
58
+ ctx.ui.notify(ultracodeUsage(), "error");
57
59
  return;
58
60
  }
59
61
 
60
- // "on", "on 500k", "500k", "+500k"
61
- let budget: number | null | undefined;
62
- const budgetToken = sub === "on" ? parts[1] : sub;
63
- if (budgetToken) {
64
- const parsed = parseBudget(budgetToken);
65
- if (parsed) budget = parsed;
66
- }
67
- mode.enable(pi, budget !== undefined ? { budget } : {});
62
+ mode.enable(pi);
68
63
  await mode.flushThinkingPreference();
69
- ctx.ui.notify(
70
- `Ultracode on — ${mode.statusLine()}${budget ? ` (budget ~${budget} tokens)` : ""}`,
71
- "info",
72
- );
64
+ ctx.ui.notify(`Ultracode on — ${mode.statusLine()}`, "info");
73
65
  ctx.ui.setStatus("ultracode", mode.statusLine());
74
66
  },
75
67
  });
@@ -107,3 +99,7 @@ export function registerCommands(pi: ExtensionAPI, mode: UltracodeMode): void {
107
99
  handler: async (ctx) => openWorkflows(ctx),
108
100
  });
109
101
  }
102
+
103
+ function ultracodeUsage(): string {
104
+ return "Usage: /ultracode [on|off|status]";
105
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Public API of pi-ultracode, for reuse and testing. */
2
2
 
3
3
  export { default } from "../extensions/ultracode.ts";
4
- export { UltracodeMode, parseBudget, MODE_ENTRY_TYPE } from "./mode.ts";
4
+ export { UltracodeMode, MODE_ENTRY_TYPE } from "./mode.ts";
5
5
  export { registerCommands } from "./commands.ts";
6
6
  export {
7
7
  ULTRACODE_TAGLINE,
package/src/mode.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * - raises the thinking level to the model's maximum (remembering the previous level),
6
6
  * - keeps the `workflow` tool active,
7
7
  * - injects a standing "author and run a workflow by default" system block on
8
- * every turn, plus an optional token budget,
9
- * - persists its on/off + budget state in session custom entries so it survives
8
+ * every turn,
9
+ * - persists its on/off state in session custom entries so it survives
10
10
  * reload, resume, fork, and compaction.
11
11
  */
12
12
 
@@ -25,7 +25,6 @@ export const MODE_ENTRY_TYPE = "ultracode-mode";
25
25
 
26
26
  interface PersistedModeState {
27
27
  enabled: boolean;
28
- budgetTotal: number | null;
29
28
  previousThinking?: ThinkingLevel;
30
29
  /** `null` records that the setting was originally absent (Pi defaults to medium). */
31
30
  previousDefaultThinking?: ThinkingLevel | null;
@@ -50,7 +49,6 @@ export interface ThinkingPreferenceStore {
50
49
  export class UltracodeMode {
51
50
  private enabled = false;
52
51
  private suspended = false;
53
- private budgetTotal: number | null = null;
54
52
  private previousThinking: ThinkingLevel | undefined;
55
53
  private previousDefaultThinking: ThinkingLevel | null | undefined;
56
54
  /** Restore a level later if the current non-reasoning model clamps it to off. */
@@ -98,12 +96,12 @@ export class UltracodeMode {
98
96
  }
99
97
 
100
98
  /** Enable if off, disable if on. Returns the new enabled state. */
101
- toggle(pi: ExtensionAPI, opts: { budget?: number | null } = {}): boolean {
99
+ toggle(pi: ExtensionAPI): boolean {
102
100
  if (this.enabled) {
103
101
  this.disable(pi);
104
102
  return false;
105
103
  }
106
- this.enable(pi, opts);
104
+ this.enable(pi);
107
105
  return true;
108
106
  }
109
107
 
@@ -191,9 +189,6 @@ export class UltracodeMode {
191
189
  return this.suspended;
192
190
  }
193
191
 
194
- getBudget(): number | null {
195
- return this.budgetTotal;
196
- }
197
192
 
198
193
  /** Keep tool availability aligned with the current mode state. */
199
194
  syncWorkflowTool(pi: ExtensionAPI): void {
@@ -206,8 +201,7 @@ export class UltracodeMode {
206
201
  }
207
202
 
208
203
  /** Turn ultracode on. Idempotent. */
209
- enable(pi: ExtensionAPI, opts: { budget?: number | null } = {}): void {
210
- if (opts.budget !== undefined) this.budgetTotal = opts.budget;
204
+ enable(pi: ExtensionAPI): void {
211
205
  this.suspended = false;
212
206
  this.pendingPreviousThinking = undefined;
213
207
  this.pendingClearGeneration++;
@@ -253,10 +247,6 @@ export class UltracodeMode {
253
247
  this.persist(pi);
254
248
  }
255
249
 
256
- setBudget(pi: ExtensionAPI, budget: number | null): void {
257
- this.budgetTotal = budget;
258
- this.persist(pi);
259
- }
260
250
 
261
251
  /** Restore mode state from the active session branch. */
262
252
  restore(
@@ -298,7 +288,6 @@ export class UltracodeMode {
298
288
  ) as ThinkingLevel | undefined;
299
289
  this.enabled = false;
300
290
  this.suspended = false;
301
- this.budgetTotal = null;
302
291
  this.pendingPreviousThinking = undefined;
303
292
  this.previousThinking = target;
304
293
  this.previousDefaultThinking = globalPreference;
@@ -318,7 +307,6 @@ export class UltracodeMode {
318
307
 
319
308
  this.suspended = false;
320
309
  this.enabled = latest.enabled;
321
- this.budgetTotal = latest.budgetTotal;
322
310
  this.syncWorkflowTool(pi);
323
311
  const maxIsUnknownToRuntime = !this.runtimeSupportsMaxThinking
324
312
  && preference.effective === ULTRACODE_THINKING_LEVEL;
@@ -392,7 +380,7 @@ export class UltracodeMode {
392
380
  */
393
381
  beforeAgentStart(event: { systemPrompt: string }): { systemPrompt: string } | undefined {
394
382
  if (!this.isEnforcing()) return undefined;
395
- const block = ultracodeSystemBlock({ budgetTotal: this.budgetTotal });
383
+ const block = ultracodeSystemBlock();
396
384
  return { systemPrompt: `${event.systemPrompt}\n\n${block}\n\n${ULTRACODE_ACTIVE_REMINDER}` };
397
385
  }
398
386
 
@@ -401,7 +389,6 @@ export class UltracodeMode {
401
389
  const parts = ["ultracode: on"];
402
390
  // Show the level that actually applied, including compatibility/model fallback.
403
391
  if (this.appliedThinking) parts.push(this.appliedThinking);
404
- if (this.budgetTotal) parts.push(`budget ~${formatTokens(this.budgetTotal)}`);
405
392
  return parts.join(" · ");
406
393
  }
407
394
 
@@ -542,7 +529,6 @@ export class UltracodeMode {
542
529
  private persist(pi: ExtensionAPI): void {
543
530
  const state: PersistedModeState = {
544
531
  enabled: this.enabled,
545
- budgetTotal: this.budgetTotal,
546
532
  previousThinking: this.previousThinking,
547
533
  previousDefaultThinking: this.legacyDefaultMigrationPending
548
534
  ? undefined
@@ -560,11 +546,9 @@ export class UltracodeMode {
560
546
  function parsePersistedModeState(data: unknown): PersistedModeState | undefined {
561
547
  if (!data || typeof data !== "object") return undefined;
562
548
  const value = data as Record<string, unknown>;
563
- const budget = value.budgetTotal;
564
549
  const previousDefault = value.previousDefaultThinking;
565
550
  return {
566
551
  enabled: value.enabled === true,
567
- budgetTotal: typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? budget : null,
568
552
  previousThinking: isThinkingLevel(value.previousThinking) ? value.previousThinking : undefined,
569
553
  previousDefaultThinking: previousDefault === null || isThinkingLevel(previousDefault)
570
554
  ? previousDefault
@@ -587,21 +571,3 @@ function safeGetThinking(pi: ExtensionAPI): ThinkingLevel | undefined {
587
571
  return undefined;
588
572
  }
589
573
  }
590
-
591
- function formatTokens(n: number): string {
592
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
593
- if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
594
- return String(n);
595
- }
596
-
597
- /** Parse a budget token like "500k", "1m", "250000", "+500k". */
598
- export function parseBudget(input: string): number | null {
599
- const match = input.trim().match(/^\+?\s*([0-9][0-9_.]*)\s*([kmKM])?$/);
600
- if (!match) return null;
601
- const value = Number(match[1].replace(/_/g, ""));
602
- if (!Number.isFinite(value)) return null;
603
- const unit = match[2]?.toLowerCase();
604
- if (unit === "k") return Math.round(value * 1_000);
605
- if (unit === "m") return Math.round(value * 1_000_000);
606
- return Math.round(value);
607
- }