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 +47 -42
- package/examples/workflows/loop-until-dry-bugs.workflow.js +16 -6
- package/extensions/ultracode.ts +0 -1
- package/package.json +7 -7
- package/src/commands.ts +19 -23
- package/src/index.ts +1 -1
- package/src/mode.ts +6 -40
- package/src/prompts.ts +18 -28
- package/src/workflow/admission.ts +318 -0
- package/src/workflow/agent-runner.ts +12 -0
- package/src/workflow/display.ts +17 -13
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/json-schema.ts +39 -17
- package/src/workflow/leases.ts +60 -0
- package/src/workflow/parser.ts +809 -5
- package/src/workflow/registry.ts +39 -22
- package/src/workflow/run-artifacts.ts +231 -0
- package/src/workflow/run-details.ts +106 -28
- package/src/workflow/runtime.ts +640 -303
- package/src/workflow/script-executor.ts +317 -0
- package/src/workflow/script-worker.mjs +678 -0
- package/src/workflow/structured-output.ts +2 -0
- package/src/workflow/tool.ts +207 -74
- package/src/workflow/value-limits.ts +363 -0
- package/src/workflow/worktree.ts +5 -3
- package/types/workflow.d.ts +60 -22
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
|
-
|
|
|
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)
|
|
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
|
|
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
|
-
|
|
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` |
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
+
return results.filter(Boolean);
|
|
248
251
|
```
|
|
249
252
|
|
|
250
253
|
### 结构化输出
|
|
251
254
|
|
|
252
|
-
不传 `schema` 时,`agent()` 返回子代理最后一段非空 assistant
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 /
|
|
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
|
-
)
|
|
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
|
-
|
|
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 }
|
package/extensions/ultracode.ts
CHANGED
|
@@ -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.
|
|
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,
|
|
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.
|
|
57
|
-
"@earendil-works/pi-ai": "^0.
|
|
58
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
59
|
-
"@earendil-works/pi-tui": "^0.
|
|
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.
|
|
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 {
|
|
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
|
|
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"
|
|
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
|
|
50
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
9
|
-
* - persists its on/off
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
}
|