pi-ultracode 0.3.0 → 0.3.2
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 +73 -401
- package/README.zh-CN.md +155 -0
- package/package.json +5 -5
- package/src/prompts.ts +1 -1
- package/src/workflow/agent-runner.ts +88 -22
- package/src/workflow/workflow-overlay.ts +17 -7
package/README.md
CHANGED
|
@@ -1,433 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-ultracode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Deterministic multi-agent workflows for [Pi](https://github.com/earendil-works/pi).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**English** · [简体中文](https://github.com/ekil1100/pi-ultracode/blob/main/README.zh-CN.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`pi-ultracode` adds an opt-in high-effort mode to Pi. When a task benefits from delegation, the main agent can run a bounded JavaScript workflow with parallel subagents, isolated worktrees, structured output, durable resume, and live progress.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The project combines the terminal-first agentic experience of [Claude Code](https://github.com/anthropics/claude-code) with the explicit workflow and structural-control ideas found in [Grok Build](https://github.com/xai-org/grok-build), while keeping Pi as the runtime.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
| --- | --- |
|
|
13
|
-
| `workflow` 工具 | 模型请求前按 Ultracode 状态校准;运行确定性的 JavaScript 编排脚本,并调用 `agent()`、`parallel()`、`pipeline()` 和 `workflow()`。 |
|
|
14
|
-
| 独立子代理 | 每次 `agent()` 调用都会创建一条带标准编码工具的新 Pi 会话。 |
|
|
15
|
-
| 并行执行 | `parallel()` 并发运行任务、保留输入顺序,并在启动前整批预留 agent slots。 |
|
|
16
|
-
| 流水线 | `pipeline()` 让任务独立流经多个阶段,无需全局屏障。 |
|
|
17
|
-
| 结构化输出 | 传入 JSON Schema,即可通过终止型 `structured_output` 工具获得经校验的对象。 |
|
|
18
|
-
| Agent Type | 通过 `.pi/ultracode/agents/*.md` 定义角色提示词、工具白名单、模型和思考强度。 |
|
|
19
|
-
| 模型覆盖 | 每次 `agent()` 调用都可指定模型模式,例如 `anthropic/claude-sonnet-4`。 |
|
|
20
|
-
| Worktree 隔离 | 让写入型并行代理在临时 git worktree 中运行,并把改动安全合并回共享工作树。 |
|
|
21
|
-
| 进度与详情 | 实时状态、实际模型/effort、turn/工具/token 统计,以及可流式展开的 TUI 任务详情浮层。 |
|
|
22
|
-
| Ultracode 模式 | `/ultracode on` 启用主动编排提示词,并请求当前模型支持的最高思考强度。 |
|
|
23
|
-
| 执行限制 | `maxAgents`(默认 128/上限 1024)、每进程且每 session/runsDir 最多 4 个活动 workflow、并发限制与嵌套深度限制。 |
|
|
24
|
-
|
|
25
|
-
## 为什么使用工作流
|
|
26
|
-
|
|
27
|
-
普通的多代理提示通常依赖模型临场决定如何分解与组合任务。Ultracode 采用另一种方式:**模型编写确定性的编排脚本,运行时执行该脚本**。
|
|
28
|
-
|
|
29
|
-
这样可以清晰表达:
|
|
30
|
-
|
|
31
|
-
- 哪些任务可以并行;
|
|
32
|
-
- 哪些阶段依赖前一阶段的结果;
|
|
33
|
-
- 哪些代理应返回结构化数据;
|
|
34
|
-
- 何时需要共享上下文,何时需要隔离;
|
|
35
|
-
- 何时停止额外探索;
|
|
36
|
-
- 失败分支应如何降级。
|
|
37
|
-
|
|
38
|
-
## 安装
|
|
11
|
+
## Install
|
|
39
12
|
|
|
40
13
|
```bash
|
|
41
|
-
pi install npm:pi-ultracode
|
|
14
|
+
pi install npm:pi-ultracode
|
|
42
15
|
```
|
|
43
16
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
安装后在 Pi 中运行 `/reload`。
|
|
47
|
-
|
|
48
|
-
开发仓库也可直接加载:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pi -e ./extensions/ultracode.ts
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
仓库中提供两个兼容入口:
|
|
55
|
-
|
|
56
|
-
- `extensions/ultracode.ts`:推荐的 Pi 扩展入口;
|
|
57
|
-
- `src/index.ts`:兼容入口,重新导出同一个扩展。
|
|
58
|
-
|
|
59
|
-
## 快速开始
|
|
60
|
-
|
|
61
|
-
在 Pi 中启用模式:
|
|
17
|
+
Then reload Pi and toggle Ultracode on:
|
|
62
18
|
|
|
63
19
|
```text
|
|
64
|
-
/
|
|
20
|
+
/reload
|
|
21
|
+
/ultracode
|
|
65
22
|
```
|
|
66
23
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```text
|
|
70
|
-
Review the current branch, identify correctness and compatibility risks,
|
|
71
|
-
fix the justified issues, and verify the result.
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
启用后,主代理会被要求在并行检视、迁移、审计或大范围修改明显有益时编写并运行工作流,而不是把所有工作塞进一个上下文。
|
|
75
|
-
|
|
76
|
-
也可以在启动时启用:
|
|
24
|
+
You can also enable it at startup:
|
|
77
25
|
|
|
78
26
|
```bash
|
|
79
27
|
pi --ultracode
|
|
80
28
|
```
|
|
81
29
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```text
|
|
85
|
-
◆ ▶ audit_repo (4/7 done, 2 running) · 3 cached · 203k token (141k new, 62k replayed)
|
|
86
|
-
✓ Survey 1/1
|
|
87
|
-
#1 ✓ repo inventory
|
|
88
|
-
gpt-5.6-sol • max · 15 turns · 42 tool uses · 141k token
|
|
89
|
-
▶ Review 3/4 · 1 running
|
|
90
|
-
#2 ✓ auth module
|
|
91
|
-
#3 ✓ db layer
|
|
92
|
-
#4 ● payments module
|
|
93
|
-
claude-sonnet-4 • high · 6 turns · 18 tool uses · 52.3k+ token · partial
|
|
94
|
-
▶ Verify 0/2 · 2 running
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
紧凑 token 数始终是 input + output;cache read/write 与成本只出现在任务详情中。主对话会折叠较早任务并明确显示省略数量,按 `Ctrl+O` 可展开全部,或用 `F6` 打开详情。按 `Esc` 可取消;运行中的子代理会被终止并显示为 cancelled,同时保留已有输出和部分用量。
|
|
98
|
-
|
|
99
|
-
## 命令
|
|
30
|
+
> `pi-ultracode` registers a tool named `workflow`. If `pi-dynamic-workflows` is installed, remove it first with `pi remove npm:pi-dynamic-workflows`.
|
|
100
31
|
|
|
101
|
-
|
|
102
|
-
/ultracode # toggle on/off
|
|
103
|
-
/ultracode on # enable orchestration mode
|
|
104
|
-
/ultracode off # disable the tool and restore the previous thinking level
|
|
105
|
-
/ultracode status # show current status
|
|
106
|
-
/workflows # open the interactive workflow detail overlay
|
|
107
|
-
/workflows <runId> # open one run directly (prefix accepted)
|
|
108
|
-
/workflows abort # abort active runs
|
|
109
|
-
F6 # open the workflow detail overlay
|
|
110
|
-
```
|
|
32
|
+
Current releases are tested with Pi 0.84.
|
|
111
33
|
|
|
112
|
-
|
|
34
|
+
## Core features
|
|
113
35
|
|
|
114
|
-
-
|
|
115
|
-
- `
|
|
116
|
-
-
|
|
36
|
+
- **Opt-in high-effort mode** — requests the highest thinking level supported by the current model and restores the previous level when disabled.
|
|
37
|
+
- **Explicit workflows** — compose work with `agent()`, `parallel()`, `pipeline()`, and nested `workflow()` calls.
|
|
38
|
+
- **Independent subagents** — each agent gets its own Pi session, context, tools, model selection, and optional role.
|
|
39
|
+
- **Parallel worktree isolation** — writing agents can work in temporary git worktrees before their patches are integrated.
|
|
40
|
+
- **Structured output** — validate agent results with a bounded JSON Schema subset.
|
|
41
|
+
- **Durable resume** — replay completed calls by stable structural identity without rerunning successful work.
|
|
42
|
+
- **Live observability** — inspect status, model, thinking level, turns, tool use, token usage, retries, and transcripts in the TUI.
|
|
43
|
+
- **Bounded execution** — lifetime agent admission, concurrency limits, strict JSON limits, worker watchdogs, and bounded cleanup.
|
|
117
44
|
|
|
118
|
-
|
|
45
|
+
## Quick start
|
|
119
46
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
状态行会显示实际生效的思考强度,例如:
|
|
47
|
+
Toggle Ultracode on and describe a substantial task:
|
|
123
48
|
|
|
124
49
|
```text
|
|
125
|
-
ultracode
|
|
126
|
-
ultracode: on · xhigh
|
|
127
|
-
ultracode: on · high
|
|
128
|
-
ultracode: on · off
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Ultracode 请求的是**当前模型支持的最高强度**,不是固定标签。对支持 `max` 的模型(包括 GPT-5.6 Sol)使用 `max`;能力较低的模型由 Pi 正常钳制。对不认识 `max` 的旧版 Pi,Ultracode 会自动回退到 `xhigh`。
|
|
132
|
-
|
|
133
|
-
显式覆盖始终优先:
|
|
134
|
-
|
|
135
|
-
- 每次调用的模型后缀,例如 `model: "openai/gpt-5.6-sol:high"`;
|
|
136
|
-
- Agent Type frontmatter 中的 `thinking:`;
|
|
137
|
-
- 显式 `:off`、`:high`、`:xhigh` 或 `:max`。
|
|
138
|
-
|
|
139
|
-
模型切换或手动降低强度时,只要 Ultracode 仍启用,它就会重新请求该模型的最高可用强度。关闭或退出时,会恢复启用前的会话强度与原始默认偏好。若当前模型暂时无法表示原始强度,恢复意图会保留到后续兼容模型;中间模型的钳制结果不会覆盖原始意图。
|
|
50
|
+
/ultracode
|
|
140
51
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
扩展注册单个 `workflow` 工具,并在 input preflight 与 `before_agent_start` 边界按 Ultracode 状态校准 active tools,同时保留其他 active tools。关闭或暂停时,`tool_call` 与实际执行入口会双重 fail-closed 拒绝残留调用。工具输入是 JavaScript 源码,其中必须包含纯字面量 `meta` 导出:
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
export const meta = {
|
|
147
|
-
name: "review_and_fix",
|
|
148
|
-
description: "Review independent areas, then verify the combined result",
|
|
149
|
-
};
|
|
52
|
+
Review the current branch in parallel, verify every finding,
|
|
53
|
+
fix the justified issues, and run the relevant tests.
|
|
150
54
|
```
|
|
151
55
|
|
|
152
|
-
|
|
56
|
+
Pi decides whether a workflow is useful. Small tasks can still use the normal single-agent loop.
|
|
153
57
|
|
|
154
|
-
|
|
155
|
-
- `parallel(thunks, options?)`
|
|
156
|
-
- `pipeline(items, ...stages)`
|
|
157
|
-
- `phase(title)`
|
|
158
|
-
- `log(message)`
|
|
159
|
-
- `workflow(nameOrRef, args)`
|
|
160
|
-
- `args`
|
|
161
|
-
- `cwd`
|
|
58
|
+
Useful commands:
|
|
162
59
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
`agent()` 的主要选项:
|
|
166
|
-
|
|
167
|
-
| 选项 | 作用 |
|
|
60
|
+
| Command | Action |
|
|
168
61
|
| --- | --- |
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
|
|
|
175
|
-
|
|
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 对应关系。
|
|
62
|
+
| `/ultracode` | Toggle Ultracode on or off |
|
|
63
|
+
| `/ultracode on` | Explicitly enable it (optional alias) |
|
|
64
|
+
| `/ultracode off` | Disable it and restore the previous thinking level |
|
|
65
|
+
| `/ultracode status` | Show the current mode and effective thinking level |
|
|
66
|
+
| `/workflows` or `F6` | Open the workflow browser |
|
|
67
|
+
| `/workflows <runId>` | Open a specific run |
|
|
68
|
+
| `/workflows abort` | Abort active runs |
|
|
179
69
|
|
|
180
|
-
|
|
70
|
+
Press `Esc` to cancel a running workflow. In Pi's fullscreen TUI, use `Ctrl+PageUp`, `Ctrl+PageDown`, and `Ctrl+End` inside workflow details.
|
|
181
71
|
|
|
182
|
-
|
|
72
|
+
## Workflow example
|
|
183
73
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
### 直接调用
|
|
187
|
-
|
|
188
|
-
```js
|
|
189
|
-
export const meta = {
|
|
190
|
-
name: "single_review",
|
|
191
|
-
description: "Run one focused reviewer",
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const result = await agent(
|
|
195
|
-
"Inspect src/mode.ts for lifecycle bugs. Return concise findings.",
|
|
196
|
-
{ label: "mode review" },
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
return result;
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### 并行检视
|
|
203
|
-
|
|
204
|
-
`parallel()` 接收 thunk,而不是已经启动的 Promise。每个 panel 在任意 thunk 启动前原子预留 `options.reserveAgents ?? thunks.length` 个 agent slot;`reserveAgents` 不能小于 thunk 数。每个 thunk 保有一个独立基础 slot,其余显式预留量由 panel 共享;调用超过预留量会使整个 workflow 失败,不会临时挪用未预留的 `maxAgents` 容量。
|
|
74
|
+
The main agent normally writes workflows for you. Reusable workflows can also be saved under `.pi/ultracode/workflows/*.workflow.js` or `~/.pi/ultracode/workflows/*.workflow.js`.
|
|
205
75
|
|
|
206
76
|
```js
|
|
207
77
|
export const meta = {
|
|
208
78
|
name: "parallel_review",
|
|
209
|
-
description: "Review
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const tasks = [
|
|
213
|
-
["correctness", "Review correctness and lifecycle behavior."],
|
|
214
|
-
["tests", "Review test coverage and missing regressions."],
|
|
215
|
-
["docs", "Review README and parity documentation."],
|
|
216
|
-
];
|
|
217
|
-
|
|
218
|
-
const results = await parallel(
|
|
219
|
-
tasks.map(([label, prompt]) => () => agent(prompt, { label })),
|
|
220
|
-
{ reserveAgents: tasks.length },
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
return results.filter(Boolean);
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### 流水线
|
|
227
|
-
|
|
228
|
-
除非后续阶段确实需要所有前置结果,否则优先使用 `pipeline()`。不同条目可独立推进:
|
|
229
|
-
|
|
230
|
-
```js
|
|
231
|
-
export const meta = {
|
|
232
|
-
name: "review_pipeline",
|
|
233
|
-
description: "Inspect and verify several modules independently",
|
|
79
|
+
description: "Review correctness and tests in parallel",
|
|
234
80
|
};
|
|
235
81
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
async (file) => agent(
|
|
241
|
-
`Inspect ${file} and report concrete risks.`,
|
|
242
|
-
{ label: `inspect ${file}` },
|
|
82
|
+
const findings = await parallel([
|
|
83
|
+
() => agent(
|
|
84
|
+
"Review the current branch for correctness and lifecycle bugs.",
|
|
85
|
+
{ label: "correctness review" },
|
|
243
86
|
),
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{ label:
|
|
87
|
+
() => agent(
|
|
88
|
+
"Review the current branch for missing or weak regression tests.",
|
|
89
|
+
{ label: "test review" },
|
|
247
90
|
),
|
|
248
|
-
);
|
|
91
|
+
]);
|
|
249
92
|
|
|
250
|
-
return
|
|
93
|
+
return findings.filter(Boolean);
|
|
251
94
|
```
|
|
252
95
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
不传 `schema` 时,`agent()` 返回子代理最后一段非空 assistant 文本;传入支持的 JSON Schema 子集时,则返回经校验的对象。两种返回值都必须是≤2 MiB 的严格 JSON value:
|
|
96
|
+
For editor IntelliSense, add:
|
|
256
97
|
|
|
257
98
|
```js
|
|
258
|
-
|
|
259
|
-
name: "structured_audit",
|
|
260
|
-
description: "Collect machine-readable findings",
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const finding = await agent(
|
|
264
|
-
"Audit the mode lifecycle and return one finding.",
|
|
265
|
-
{
|
|
266
|
-
label: "lifecycle audit",
|
|
267
|
-
schema: {
|
|
268
|
-
type: "object",
|
|
269
|
-
properties: {
|
|
270
|
-
severity: { enum: ["low", "medium", "high"] },
|
|
271
|
-
summary: { type: "string" },
|
|
272
|
-
},
|
|
273
|
-
required: ["severity", "summary"],
|
|
274
|
-
additionalProperties: false,
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
return finding;
|
|
99
|
+
/// <reference types="pi-ultracode/workflow" />
|
|
280
100
|
```
|
|
281
101
|
|
|
282
|
-
|
|
102
|
+
The complete workflow contract is documented in [`types/workflow.d.ts`](types/workflow.d.ts). A larger example is available in [`examples/workflows`](examples/workflows).
|
|
283
103
|
|
|
284
|
-
##
|
|
104
|
+
## Roles, models, and worktrees
|
|
285
105
|
|
|
286
|
-
|
|
106
|
+
Custom agent roles live in:
|
|
287
107
|
|
|
288
108
|
```text
|
|
289
109
|
.pi/ultracode/agents/*.md
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
用户级角色定义放在:
|
|
293
|
-
|
|
294
|
-
```text
|
|
295
110
|
~/.pi/ultracode/agents/*.md
|
|
296
111
|
```
|
|
297
112
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
示例:
|
|
301
|
-
|
|
302
|
-
```md
|
|
303
|
-
---
|
|
304
|
-
name: reviewer
|
|
305
|
-
description: Read-only reviewer focused on correctness and regressions
|
|
306
|
-
tools: read, bash
|
|
307
|
-
model: anthropic/claude-sonnet-4
|
|
308
|
-
thinking: high
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
Inspect the requested area. Do not modify files. Report only evidence-backed findings.
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
工作流中的用法:
|
|
113
|
+
A call can select a role, model, thinking level, or worktree isolation:
|
|
315
114
|
|
|
316
115
|
```js
|
|
317
|
-
await agent("
|
|
318
|
-
label: "
|
|
319
|
-
agentType: "
|
|
320
|
-
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
内置类型包括 `claude`、`general-purpose`、`Explore`、`Plan` 和 `code-reviewer`。
|
|
324
|
-
|
|
325
|
-
Agent Type 可控制:
|
|
326
|
-
|
|
327
|
-
- 角色提示词;
|
|
328
|
-
- 工具白名单;
|
|
329
|
-
- 默认模型;
|
|
330
|
-
- 默认思考强度。
|
|
331
|
-
|
|
332
|
-
每次调用的选项优先于 Agent Type 默认值。
|
|
333
|
-
|
|
334
|
-
## 模型选择
|
|
335
|
-
|
|
336
|
-
`model` 接受 provider/model 模式,并可带思考强度后缀:
|
|
337
|
-
|
|
338
|
-
```js
|
|
339
|
-
await agent("Analyze this failure.", {
|
|
340
|
-
label: "failure analysis",
|
|
341
|
-
model: "openai/gpt-5.6-sol:max",
|
|
342
|
-
});
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
若未指定 model,则子代理继承工作流工具创建时的 Pi 当前模型。Ultracode 启用时,默认子代理强度同样请求 `max`,并按每个代理实际选择的模型独立钳制。Ultracode 不会自动启用 GPT-5.6 的 `pro` 模式。
|
|
346
|
-
|
|
347
|
-
## Worktree 隔离
|
|
348
|
-
|
|
349
|
-
并行写入同一工作树容易产生竞态。对会修改文件的代理使用:
|
|
350
|
-
|
|
351
|
-
```js
|
|
352
|
-
await agent("Implement the requested change and run focused tests.", {
|
|
353
|
-
label: "implementation",
|
|
116
|
+
await agent("Implement and test the fix.", {
|
|
117
|
+
label: "implement fix",
|
|
118
|
+
agentType: "general-purpose",
|
|
119
|
+
model: "anthropic/claude-sonnet-4:high",
|
|
354
120
|
isolation: "worktree",
|
|
355
121
|
});
|
|
356
122
|
```
|
|
357
123
|
|
|
358
|
-
|
|
124
|
+
Worktree isolation requires a git repository with at least one commit. If setup fails, Ultracode logs the fallback and runs that agent in the shared working directory; avoid parallel writers in that case. A clean working tree is recommended for predictable patch integration.
|
|
359
125
|
|
|
360
|
-
|
|
361
|
-
2. 在隔离目录运行代理;
|
|
362
|
-
3. 捕获 worktree diff;
|
|
363
|
-
4. 检查共享工作树是否漂移;
|
|
364
|
-
5. 通过 `git apply --3way` 应用补丁;
|
|
365
|
-
6. 在失败时保存 rescue patch;
|
|
366
|
-
7. 成功且安全时清理临时 worktree。
|
|
126
|
+
## Guardrails
|
|
367
127
|
|
|
368
|
-
|
|
128
|
+
A workflow defaults to `maxAgents: 128`, supports at most 16 concurrent agent calls, and allows one level of nested workflows. The lifetime agent limit is preserved across resumes; cached replay does not consume it again.
|
|
369
129
|
|
|
370
|
-
|
|
130
|
+
Workflow agent sessions retain project context and ordinary skills, but do not initialize ambient Pi extensions or expose parent orchestration tools and skills (`workflow`, `subagent`, `subagent_wait`, or `pi-subagents`). This keeps orchestration at the parent boundary and allows `pi-ultracode` and `pi-subagents` to coexist in the main session.
|
|
371
131
|
|
|
372
|
-
|
|
132
|
+
Resume is intentionally immutable: the script, arguments, agent definitions, effective models, and call structure must still match. Changed work starts a new run.
|
|
373
133
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
```text
|
|
377
|
-
.pi/ultracode/workflows/<name>.workflow.js
|
|
378
|
-
~/.pi/ultracode/workflows/<name>.workflow.js
|
|
379
|
-
```
|
|
134
|
+
Token and cost data are reported for observability, not enforced as a budget. Worker and VM restrictions are determinism and liveness guards, not a security sandbox.
|
|
380
135
|
|
|
381
|
-
|
|
136
|
+
## Design inspiration
|
|
382
137
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
138
|
+
- **Claude Code** — natural-language, terminal-first agentic coding and focused delegation.
|
|
139
|
+
- **Grok Build** — explicit workflows, durable journals, bounded admission, and structural execution controls.
|
|
140
|
+
- **Pi** — the model runtime, sessions, tools, extension API, and TUI remain native Pi behavior.
|
|
386
141
|
|
|
387
|
-
|
|
142
|
+
`pi-ultracode` is an independent Pi extension and is not affiliated with Anthropic or xAI.
|
|
388
143
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
```text
|
|
392
|
-
<sessionDir>/ultracode-runs/<runId>.workflow.js
|
|
393
|
-
<sessionDir>/ultracode-runs/<runId>.jsonl
|
|
394
|
-
```
|
|
395
|
-
|
|
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。
|
|
397
|
-
|
|
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 会明确失败。
|
|
399
|
-
|
|
400
|
-
## 限制
|
|
401
|
-
|
|
402
|
-
运行时限制:
|
|
403
|
-
|
|
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` 工具;
|
|
413
|
-
- 结构化输出校验;
|
|
414
|
-
- 静态及 context-realm 运行期确定性 guard。
|
|
415
|
-
|
|
416
|
-
## 会话与兼容性
|
|
417
|
-
|
|
418
|
-
Ultracode 模式状态通过自定义 session entry 持久化。恢复、reload、fork 和 `/tree` 导航都会按当前 branch 重新读取状态;被丢弃分支中的 entry 不会错误启用模式。旧版 Pi 或旧模型会把 `max` 兼容回退为 `xhigh`,而不会把未知值静默变成 `off`。
|
|
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
|
-
|
|
428
|
-
扩展清理只管理自己的详情浮层、status 与运行状态,不会调用 Pi 的全局 `ui.clear()`,因此不会清除其他扩展的 UI。
|
|
429
|
-
|
|
430
|
-
## 开发
|
|
144
|
+
## Development
|
|
431
145
|
|
|
432
146
|
```bash
|
|
433
147
|
npm ci
|
|
@@ -436,48 +150,6 @@ npm test
|
|
|
436
150
|
npm pack --dry-run
|
|
437
151
|
```
|
|
438
152
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
- `extensions/ultracode.ts`:扩展入口和 Pi 生命周期接线;
|
|
442
|
-
- `src/mode.ts`:Ultracode 模式状态、恢复与强度管理;
|
|
443
|
-
- `src/thinking.ts`:共享思考强度词汇与兼容性检测;
|
|
444
|
-
- `src/workflow/`:脚本验证、运行时、子代理执行、worktree 和进度显示;
|
|
445
|
-
- `test/`:单元、扩展和回归测试;
|
|
446
|
-
- `docs/parity.md`:与原生 Pi 能力边界的说明。
|
|
447
|
-
|
|
448
|
-
开发依赖会安装 TypeScript 和用于测试的 Pi peer 包;通过 `pi install` 加载正式包时,Pi 仍会提供这些 peer 依赖。
|
|
449
|
-
|
|
450
|
-
## 发布
|
|
451
|
-
|
|
452
|
-
npm 发布由 [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml) 完成,使用 npm Trusted Publishing(OIDC),无需保存长期 `NPM_TOKEN`。
|
|
453
|
-
|
|
454
|
-
首次使用前,在 npm 的 `pi-ultracode` 包设置中添加 GitHub Actions Trusted Publisher:
|
|
455
|
-
|
|
456
|
-
- Organization or user:`ekil1100`
|
|
457
|
-
- Repository:`pi-ultracode`
|
|
458
|
-
- Workflow filename:`publish-npm.yml`
|
|
459
|
-
- Environment name:`npm`
|
|
460
|
-
- Allowed action:`npm publish`
|
|
461
|
-
|
|
462
|
-
同时在 GitHub 仓库中创建名为 `npm` 的 Environment;可以按需增加审批人。正常发布无需打开 Actions 页面:
|
|
463
|
-
|
|
464
|
-
```bash
|
|
465
|
-
npm version patch # or: minor / major / x.y.z-beta.1
|
|
466
|
-
git push origin main --follow-tags
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
`npm version` 会同步更新 `package.json` 和 `package-lock.json`,创建版本提交与 `v<version>` Tag;推送 Tag 后 CI 自动发布。稳定版本发布到 `latest`,预发布版本发布到 `next`。
|
|
470
|
-
|
|
471
|
-
需要重试或指定其他 npm dist-tag 时,也可以在 Actions 中手动运行工作流;填写完全匹配 `package.json` 的版本,且只允许从 `main` 运行。
|
|
472
|
-
|
|
473
|
-
工作流会校验 Tag 与包版本完全一致、确认版本尚未发布,并在正式发布前执行干净安装、测试和类型检查。
|
|
474
|
-
|
|
475
|
-
## 设计原则
|
|
476
|
-
|
|
477
|
-
Ultracode 的目标不是“尽可能多地启动代理”,而是:
|
|
478
|
-
|
|
479
|
-
> 在任务确实适合拆分、并行、独立验证或跨上下文扩展时,使用确定性编排获得更高置信度;否则继续使用 Pi 简洁直接的单代理循环。
|
|
480
|
-
|
|
481
|
-
## 许可证
|
|
153
|
+
## License
|
|
482
154
|
|
|
483
155
|
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# pi-ultracode
|
|
2
|
+
|
|
3
|
+
为 [Pi](https://github.com/earendil-works/pi) 提供确定性的多代理工作流。
|
|
4
|
+
|
|
5
|
+
[English](README.md) · **简体中文**
|
|
6
|
+
|
|
7
|
+
`pi-ultracode` 为 Pi 增加一个可选的高强度执行模式。当任务适合拆分时,主代理可以运行有边界的 JavaScript 工作流,使用并行子代理、隔离 worktree、结构化输出、持久恢复和实时进度。
|
|
8
|
+
|
|
9
|
+
项目结合了 [Claude Code](https://github.com/anthropics/claude-code) 的终端 Agentic 体验,以及 [Grok Build](https://github.com/xai-org/grok-build) 的显式工作流和结构控制思路,同时继续使用 Pi 作为运行时。
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install npm:pi-ultracode
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
然后重新加载 Pi,并用主命令切换到 Ultracode:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
/reload
|
|
21
|
+
/ultracode
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
也可以在启动时直接启用:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi --ultracode
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> `pi-ultracode` 会注册名为 `workflow` 的工具。如果已安装 `pi-dynamic-workflows`,请先运行 `pi remove npm:pi-dynamic-workflows`。
|
|
31
|
+
|
|
32
|
+
当前版本已在 Pi 0.84 上验证。
|
|
33
|
+
|
|
34
|
+
## 核心特性
|
|
35
|
+
|
|
36
|
+
- **可选高强度模式**:请求当前模型支持的最高思考强度,关闭时恢复之前的强度。
|
|
37
|
+
- **显式工作流**:通过 `agent()`、`parallel()`、`pipeline()` 和嵌套 `workflow()` 组合任务。
|
|
38
|
+
- **独立子代理**:每个代理拥有独立的 Pi 会话、上下文、工具、模型选择和可选角色。
|
|
39
|
+
- **并行 worktree 隔离**:写入型代理可在临时 git worktree 中工作,再集成补丁。
|
|
40
|
+
- **结构化输出**:使用有边界的 JSON Schema 子集校验代理结果。
|
|
41
|
+
- **持久恢复**:按照稳定的结构身份复用已完成调用,不重复执行成功任务。
|
|
42
|
+
- **实时可观测性**:在 TUI 中查看状态、模型、思考强度、turn、工具、token、重试和 transcript。
|
|
43
|
+
- **有界执行**:限制累计代理数、并发、JSON 大小、Worker 运行和取消清理时间。
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
用主命令开启 Ultracode,然后描述一个较复杂的任务:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
/ultracode
|
|
51
|
+
|
|
52
|
+
Review the current branch in parallel, verify every finding,
|
|
53
|
+
fix the justified issues, and run the relevant tests.
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pi 会自行判断工作流是否有帮助。小任务仍可继续使用普通的单代理循环。
|
|
57
|
+
|
|
58
|
+
常用命令:
|
|
59
|
+
|
|
60
|
+
| 命令 | 作用 |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `/ultracode` | 切换 Ultracode 的开启或关闭状态 |
|
|
63
|
+
| `/ultracode on` | 显式启用(可选别名) |
|
|
64
|
+
| `/ultracode off` | 关闭并恢复之前的思考强度 |
|
|
65
|
+
| `/ultracode status` | 查看当前模式和实际思考强度 |
|
|
66
|
+
| `/workflows` 或 `F6` | 打开工作流浏览器 |
|
|
67
|
+
| `/workflows <runId>` | 打开指定运行 |
|
|
68
|
+
| `/workflows abort` | 中止活动运行 |
|
|
69
|
+
|
|
70
|
+
按 `Esc` 可取消正在运行的工作流。在 Pi 的 fullscreen TUI 中,请在工作流详情内使用 `Ctrl+PageUp`、`Ctrl+PageDown` 和 `Ctrl+End`。
|
|
71
|
+
|
|
72
|
+
## 工作流示例
|
|
73
|
+
|
|
74
|
+
通常由主代理自动编写工作流。需要复用时,可以保存到 `.pi/ultracode/workflows/*.workflow.js` 或 `~/.pi/ultracode/workflows/*.workflow.js`。
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
export const meta = {
|
|
78
|
+
name: "parallel_review",
|
|
79
|
+
description: "Review correctness and tests in parallel",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const findings = await parallel([
|
|
83
|
+
() => agent(
|
|
84
|
+
"Review the current branch for correctness and lifecycle bugs.",
|
|
85
|
+
{ label: "correctness review" },
|
|
86
|
+
),
|
|
87
|
+
() => agent(
|
|
88
|
+
"Review the current branch for missing or weak regression tests.",
|
|
89
|
+
{ label: "test review" },
|
|
90
|
+
),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
return findings.filter(Boolean);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
如需编辑器类型提示,请添加:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
/// <reference types="pi-ultracode/workflow" />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
完整工作流接口见 [`types/workflow.d.ts`](types/workflow.d.ts),更多示例见 [`examples/workflows`](examples/workflows)。
|
|
103
|
+
|
|
104
|
+
## 角色、模型与 worktree
|
|
105
|
+
|
|
106
|
+
自定义代理角色放在:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
.pi/ultracode/agents/*.md
|
|
110
|
+
~/.pi/ultracode/agents/*.md
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
每次调用都可以选择角色、模型、思考强度或 worktree 隔离:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
await agent("Implement and test the fix.", {
|
|
117
|
+
label: "implement fix",
|
|
118
|
+
agentType: "general-purpose",
|
|
119
|
+
model: "anthropic/claude-sonnet-4:high",
|
|
120
|
+
isolation: "worktree",
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Worktree 隔离只能在至少有一个 commit 的 Git 仓库中使用。如果创建失败,Ultracode 会记录回退信息,并让该代理在共享工作目录中运行;此时应避免并行写入。为保证补丁集成可预测,建议保持工作区干净。
|
|
125
|
+
|
|
126
|
+
## 执行边界
|
|
127
|
+
|
|
128
|
+
工作流默认使用 `maxAgents: 128`,最多并发运行 16 个代理,并只允许一层嵌套工作流。累计代理上限会跨 resume 保留,缓存回放不会再次占用额度。
|
|
129
|
+
|
|
130
|
+
Workflow 子代理会保留项目上下文和普通 skills,但不会初始化环境中的 Pi 扩展,也不会暴露父级编排工具与 skill(`workflow`、`subagent`、`subagent_wait` 或 `pi-subagents`)。这样可把编排限制在父会话边界,同时允许 `pi-ultracode` 与 `pi-subagents` 在主会话中共存。
|
|
131
|
+
|
|
132
|
+
Resume 刻意保持不可变:脚本、参数、代理定义、实际模型和调用结构都必须匹配。工作内容发生变化时,应启动新的 run。
|
|
133
|
+
|
|
134
|
+
Token 和成本只用于可观测性,不作为执行预算。Worker 与 VM 限制用于确定性和存活性保护,不是安全沙箱。
|
|
135
|
+
|
|
136
|
+
## 设计参考
|
|
137
|
+
|
|
138
|
+
- **Claude Code**:自然语言驱动、终端优先的 Agentic 编码体验和任务委派。
|
|
139
|
+
- **Grok Build**:显式工作流、持久 journal、有界 admission 和结构执行控制。
|
|
140
|
+
- **Pi**:模型运行时、会话、工具、扩展 API 和 TUI 都继续使用 Pi 原生能力。
|
|
141
|
+
|
|
142
|
+
`pi-ultracode` 是独立的 Pi 扩展,与 Anthropic 或 xAI 没有关联。
|
|
143
|
+
|
|
144
|
+
## 开发
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm ci
|
|
148
|
+
npm run typecheck
|
|
149
|
+
npm test
|
|
150
|
+
npm pack --dry-run
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 许可证
|
|
154
|
+
|
|
155
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ultracode",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
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": [
|
|
@@ -53,10 +53,10 @@
|
|
|
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.84.0",
|
|
57
|
+
"@earendil-works/pi-ai": "^0.84.0",
|
|
58
|
+
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
59
|
+
"@earendil-works/pi-tui": "^0.84.0",
|
|
60
60
|
"@types/node": "^24.12.4",
|
|
61
61
|
"typebox": "1.3.7",
|
|
62
62
|
"typescript": "^5.9.3"
|
package/src/prompts.ts
CHANGED
|
@@ -64,7 +64,7 @@ export const WORKFLOW_GUIDELINES: string[] = [
|
|
|
64
64
|
"For the workflow tool, if agent() needs machine-readable output pass an inline JSON Schema via opts.schema; agent() then returns the validated object. Use only the bounded subset: types, object/array structure, enum/const, anyOf/allOf, length/numeric constraints, and annotations. Unknown keywords, $ref variants, oneOf, format, pattern, and patternProperties are rejected. Keep schemas under 256 KiB / 64 levels and every agent/workflow output under 2 MiB. Use JSON Schema, not TypeScript or TypeBox constructors.",
|
|
65
65
|
"For the workflow tool, when agent() is called WITHOUT a schema, its return value is the subagent's final assistant text (the last text the subagent produced). With a schema it returns the validated structured_output object. Prefer a schema for machine-readable results; use the text form only for prose summaries.",
|
|
66
66
|
"For the workflow tool, agent opts also accept: model (override the subagent model by pattern), agentType (use a custom subagent role/system-prompt), isolation:'worktree' (run the agent in an isolated git worktree — use ONLY when agents mutate files in parallel and would conflict), and phase (assign the agent to a progress group explicitly inside parallel()/pipeline()).",
|
|
67
|
-
"For the workflow tool, workflow(nameOrRef, args) runs a saved workflow (by name) or a scriptPath inline as a sub-step, sharing this run's concurrency, maxAgents, active reservations, and agent counter. Nesting is one level only. Child subagent sessions
|
|
67
|
+
"For the workflow tool, workflow(nameOrRef, args) runs a saved workflow (by name) or a scriptPath inline as a sub-step, sharing this run's concurrency, maxAgents, active reservations, and agent counter. Nesting is one level only. Child subagent sessions do not load ambient extensions or expose workflow/subagent orchestration tools, so they cannot start an independent recursive workflow chain.",
|
|
68
68
|
"For workflow resumeFromRunId, reuse the exact same script and args. Resume is immutable, successful calls replay by stable structural call path, and maxAgents may only stay the same or increase; changed work must start a new run.",
|
|
69
69
|
"For the workflow tool, failed agent()/parallel()/pipeline() branches return null and log the failure (unless the whole run is aborted or a policy limit is hit). Check for nulls before synthesizing conclusions, and prefer a final synthesis/assertion agent that returns a compact JSON-serializable verdict.",
|
|
70
70
|
"For the workflow tool, directly await or return every orchestration promise (agent/parallel/pipeline/workflow). Native .then/.catch/.finally chains and Promise.all/allSettled/race/any are rejected; use parallel() or pipeline() so call identity stays deterministic. Unobserved, pending, or native same-scope concurrent orchestration is fatal.",
|
|
@@ -14,6 +14,7 @@ import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
import {
|
|
15
15
|
createAgentSession,
|
|
16
16
|
createCodingTools,
|
|
17
|
+
DefaultResourceLoader,
|
|
17
18
|
getAgentDir,
|
|
18
19
|
SessionManager,
|
|
19
20
|
SettingsManager,
|
|
@@ -45,6 +46,42 @@ import type { AgentTypeDef } from "./agent-types.ts";
|
|
|
45
46
|
|
|
46
47
|
export type { ThinkingLevel } from "../thinking.ts";
|
|
47
48
|
|
|
49
|
+
const CHILD_ORCHESTRATION_TOOLS = ["workflow", "subagent", "subagent_wait"];
|
|
50
|
+
const PARENT_ONLY_CHILD_SKILLS = new Set(["pi-subagents"]);
|
|
51
|
+
|
|
52
|
+
export interface WorkflowChildResourceLoaderOptions {
|
|
53
|
+
cwd: string;
|
|
54
|
+
agentDir: string;
|
|
55
|
+
settingsManager?: SettingsManager;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the sealed resource view used by workflow agent sessions.
|
|
60
|
+
*
|
|
61
|
+
* Ambient extension factories execute during discovery, so filtering an already
|
|
62
|
+
* loaded extension result is too late: another in-process orchestrator may have
|
|
63
|
+
* already replaced process-global lifecycle state. Disable ambient extensions at
|
|
64
|
+
* the loader boundary while retaining project context and ordinary skills.
|
|
65
|
+
*/
|
|
66
|
+
export async function createWorkflowChildResourceLoader(
|
|
67
|
+
options: WorkflowChildResourceLoaderOptions,
|
|
68
|
+
): Promise<DefaultResourceLoader> {
|
|
69
|
+
const settingsManager = options.settingsManager
|
|
70
|
+
?? SettingsManager.create(options.cwd, options.agentDir);
|
|
71
|
+
const loader = new DefaultResourceLoader({
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
agentDir: options.agentDir,
|
|
74
|
+
settingsManager,
|
|
75
|
+
noExtensions: true,
|
|
76
|
+
skillsOverride: ({ skills, diagnostics }) => ({
|
|
77
|
+
skills: skills.filter((skill) => !PARENT_ONLY_CHILD_SKILLS.has(skill.name)),
|
|
78
|
+
diagnostics,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
await loader.reload();
|
|
82
|
+
return loader;
|
|
83
|
+
}
|
|
84
|
+
|
|
48
85
|
/** A minimal structural view of a Pi model (avoids importing the heavy generic type). */
|
|
49
86
|
export interface ModelLike {
|
|
50
87
|
provider: string;
|
|
@@ -192,7 +229,10 @@ export interface WorkflowAgentRunnerOptions {
|
|
|
192
229
|
model?: ModelLike;
|
|
193
230
|
/** Default thinking level for subagents. */
|
|
194
231
|
thinkingLevel?: ThinkingLevel;
|
|
195
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Test seam for session construction and initialization races. Custom
|
|
234
|
+
* factories own resource isolation; production uses the sealed Pi loader.
|
|
235
|
+
*/
|
|
196
236
|
createSession?: AgentSessionFactory;
|
|
197
237
|
/** Test/compatibility seam for async ModelRuntime initialization. */
|
|
198
238
|
createModelRuntime?: ModelRuntimeFactory;
|
|
@@ -243,6 +283,7 @@ export class WorkflowAgentRunner {
|
|
|
243
283
|
private readonly defaultModel?: ModelLike;
|
|
244
284
|
private readonly defaultThinking?: ThinkingLevel;
|
|
245
285
|
private readonly createSession: AgentSessionFactory;
|
|
286
|
+
private readonly usesPiSessionFactory: boolean;
|
|
246
287
|
private readonly createModelRuntime?: ModelRuntimeFactory;
|
|
247
288
|
private modelRuntimePromise?: Promise<ModelRuntimeLike | undefined>;
|
|
248
289
|
private readonly runtimeSupportsMaxThinking: boolean;
|
|
@@ -253,10 +294,10 @@ export class WorkflowAgentRunner {
|
|
|
253
294
|
this.providedModelRuntime = options.modelRuntime;
|
|
254
295
|
this.defaultModel = options.model;
|
|
255
296
|
this.defaultThinking = options.thinkingLevel;
|
|
256
|
-
|
|
297
|
+
this.usesPiSessionFactory = options.createSession === undefined;
|
|
257
298
|
this.createSession = options.createSession ?? (createAgentSession as unknown as AgentSessionFactory);
|
|
258
299
|
this.createModelRuntime = options.createModelRuntime
|
|
259
|
-
?? (options.modelRuntime !== undefined || !usesPiSessionFactory ? undefined : createPiModelRuntime);
|
|
300
|
+
?? (options.modelRuntime !== undefined || !this.usesPiSessionFactory ? undefined : createPiModelRuntime);
|
|
260
301
|
this.runtimeSupportsMaxThinking = options.supportsMaxThinking ?? piVersionSupportsMaxThinking(PI_VERSION);
|
|
261
302
|
}
|
|
262
303
|
|
|
@@ -304,22 +345,29 @@ export class WorkflowAgentRunner {
|
|
|
304
345
|
throw error;
|
|
305
346
|
}
|
|
306
347
|
|
|
307
|
-
const createSession = (level: ThinkingLevel | undefined) =>
|
|
308
|
-
cwd,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
348
|
+
const createSession = async (level: ThinkingLevel | undefined) => {
|
|
349
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
350
|
+
const resourceLoader = this.usesPiSessionFactory
|
|
351
|
+
? await createWorkflowChildResourceLoader({ cwd, agentDir, settingsManager })
|
|
352
|
+
: undefined;
|
|
353
|
+
return this.createSession({
|
|
354
|
+
cwd,
|
|
355
|
+
agentDir,
|
|
356
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
357
|
+
settingsManager,
|
|
358
|
+
...(resourceLoader ? { resourceLoader } : {}),
|
|
359
|
+
customTools,
|
|
360
|
+
excludeTools: [...CHILD_ORCHESTRATION_TOOLS],
|
|
361
|
+
...(model ? { model: model as any } : {}),
|
|
362
|
+
...(level ? { thinkingLevel: level as any } : {}),
|
|
363
|
+
...(toolAllowlist ? { tools: toolAllowlist } : {}),
|
|
364
|
+
...(modelRuntime
|
|
365
|
+
? { modelRuntime }
|
|
366
|
+
: this.modelRegistry
|
|
367
|
+
? { modelRegistry: this.modelRegistry as any }
|
|
368
|
+
: {}),
|
|
369
|
+
});
|
|
370
|
+
};
|
|
323
371
|
|
|
324
372
|
const sessionThinking = resolveSessionThinkingLevel(thinkingLevel, model);
|
|
325
373
|
safeEmitTelemetry(call.onTelemetry, {
|
|
@@ -619,9 +667,27 @@ export function resolveSessionThinkingLevel(
|
|
|
619
667
|
|
|
620
668
|
function matchExactModelIn(models: ModelLike[] | undefined, pattern: string): ModelLike | undefined {
|
|
621
669
|
if (!models || !pattern.trim()) return undefined;
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
670
|
+
const trimmed = pattern.trim();
|
|
671
|
+
const lower = trimmed.toLowerCase();
|
|
672
|
+
const canonicalMatches = models.filter((model) =>
|
|
673
|
+
`${model.provider}/${model.id}`.toLowerCase() === lower
|
|
674
|
+
);
|
|
675
|
+
if (canonicalMatches.length === 1) return canonicalMatches[0];
|
|
676
|
+
if (canonicalMatches.length > 1) throw ambiguousModelReferenceError(trimmed, canonicalMatches);
|
|
677
|
+
|
|
678
|
+
const idMatches = models.filter((model) => model.id.toLowerCase() === lower);
|
|
679
|
+
if (idMatches.length === 1) return idMatches[0];
|
|
680
|
+
if (idMatches.length > 1) throw ambiguousModelReferenceError(trimmed, idMatches);
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function ambiguousModelReferenceError(pattern: string, matches: ModelLike[]): Error {
|
|
685
|
+
const references = [...new Set(matches.map((model) => `${model.provider}/${model.id}`))]
|
|
686
|
+
.sort((left, right) => left.localeCompare(right))
|
|
687
|
+
.join(", ");
|
|
688
|
+
return new Error(
|
|
689
|
+
`Model "${safeDisplayText(pattern, 80)}" is ambiguous across providers: ${safeDisplayText(references, 400)}. Use provider/model.`,
|
|
690
|
+
);
|
|
625
691
|
}
|
|
626
692
|
|
|
627
693
|
/** Match a model pattern against a registry list: exact provider/id, then exact id, then substring. */
|
|
@@ -24,6 +24,12 @@ const MIN_SPLIT_WIDTH = 100;
|
|
|
24
24
|
const MAX_RENDER_FPS_INTERVAL_MS = 100;
|
|
25
25
|
let overlayOpen = false;
|
|
26
26
|
|
|
27
|
+
type OverlayNavigationKey = "pageUp" | "pageDown" | "end";
|
|
28
|
+
|
|
29
|
+
function matchesOverlayNavigationKey(data: string, key: OverlayNavigationKey): boolean {
|
|
30
|
+
return matchesKey(data, key) || matchesKey(data, `ctrl+${key}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
interface TaskViewport {
|
|
28
34
|
offset: number;
|
|
29
35
|
follow: boolean;
|
|
@@ -318,8 +324,8 @@ export class WorkflowOverlayComponent implements Component {
|
|
|
318
324
|
if (currentIndex >= 0) this.selectedTaskIndex = currentIndex;
|
|
319
325
|
if (matchesKey(data, "up")) this.selectedTaskIndex = Math.max(0, this.selectedTaskIndex - 1);
|
|
320
326
|
else if (matchesKey(data, "down")) this.selectedTaskIndex = Math.min(tasks.length - 1, this.selectedTaskIndex + 1);
|
|
321
|
-
else if (
|
|
322
|
-
else if (
|
|
327
|
+
else if (matchesOverlayNavigationKey(data, "pageUp")) this.selectedTaskIndex = Math.max(0, this.selectedTaskIndex - 10);
|
|
328
|
+
else if (matchesOverlayNavigationKey(data, "pageDown")) this.selectedTaskIndex = Math.min(tasks.length - 1, this.selectedTaskIndex + 10);
|
|
323
329
|
else if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
324
330
|
if (split) this.focus = "detail";
|
|
325
331
|
else {
|
|
@@ -341,13 +347,13 @@ export class WorkflowOverlayComponent implements Component {
|
|
|
341
347
|
} else if (matchesKey(data, "down")) {
|
|
342
348
|
viewport.offset++;
|
|
343
349
|
viewport.follow = false;
|
|
344
|
-
} else if (
|
|
350
|
+
} else if (matchesOverlayNavigationKey(data, "pageUp")) {
|
|
345
351
|
viewport.offset = Math.max(0, viewport.offset - page);
|
|
346
352
|
viewport.follow = false;
|
|
347
|
-
} else if (
|
|
353
|
+
} else if (matchesOverlayNavigationKey(data, "pageDown")) {
|
|
348
354
|
viewport.offset += page;
|
|
349
355
|
viewport.follow = false;
|
|
350
|
-
} else if (
|
|
356
|
+
} else if (matchesOverlayNavigationKey(data, "end")) {
|
|
351
357
|
viewport.follow = true;
|
|
352
358
|
viewport.newLines = 0;
|
|
353
359
|
}
|
|
@@ -456,9 +462,13 @@ export class WorkflowOverlayComponent implements Component {
|
|
|
456
462
|
viewport.offset = Math.max(0, Math.min(viewport.offset, maxOffset));
|
|
457
463
|
}
|
|
458
464
|
const visible = body.slice(viewport.offset, viewport.offset + bodyHeight);
|
|
465
|
+
// Fullscreen owns unmodified paging keys before focused overlays receive input.
|
|
466
|
+
// Ctrl variants remain routed to the overlay under Pi 0.84's default bindings.
|
|
467
|
+
const pageKeys = this.tui.mode === "fullscreen" ? "Ctrl+PgUp/Dn" : "PgUp/Dn";
|
|
468
|
+
const endKey = this.tui.mode === "fullscreen" ? "Ctrl+End" : "End";
|
|
459
469
|
const footer = viewport.follow
|
|
460
|
-
?
|
|
461
|
-
: `↓ ${viewport.newLines} new lines ·
|
|
470
|
+
? `↑↓/${pageKeys} scroll · ${endKey} follow · p prompt · Tab tasks · Esc back`
|
|
471
|
+
: `↓ ${viewport.newLines} new lines · ${endKey} to follow`;
|
|
462
472
|
return fitHeight([...fixed, "", ...visible, this.theme.fg("dim", footer)], height);
|
|
463
473
|
}
|
|
464
474
|
|