@tt-a1i/openpi 0.1.1 → 0.2.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 +37 -22
- package/SETUP.md +8 -6
- package/extensions/ask-user/handoff.ts +5 -1
- package/extensions/ask-user/index.ts +44 -0
- package/extensions/background-terminals/index.ts +118 -29
- package/extensions/background-terminals/src/domain.ts +5 -1
- package/extensions/background-terminals/src/manager.ts +2 -1
- package/extensions/background-terminals/src/prompt.ts +35 -0
- package/extensions/background-terminals/src/result-delivery.ts +76 -3
- package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
- package/extensions/capabilities/index.ts +198 -0
- package/extensions/context-pivot/index.ts +21 -0
- package/extensions/cron/index.ts +42 -15
- package/extensions/execution-convergence/active-evidence.ts +129 -0
- package/extensions/execution-convergence/index.ts +442 -0
- package/extensions/execution-convergence/workspace-provenance.ts +338 -0
- package/extensions/file-search/index.ts +8 -1
- package/extensions/file-search/src/binaries.ts +2 -1
- package/extensions/git-info/src/runtime.ts +1 -1
- package/extensions/goal/controller.ts +2 -1
- package/extensions/goal/index.ts +20 -1
- package/extensions/plan-mode/index.ts +12 -0
- package/extensions/setup/index.ts +93 -7
- package/extensions/shared/child-session.ts +40 -4
- package/extensions/shared/setup-config.ts +22 -0
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +15 -0
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +1 -1
- package/extensions/tasks/index.ts +39 -12
- package/extensions/ui-customization/footer.ts +6 -1
- package/extensions/workflows/graph-projection.ts +6 -4
- package/extensions/workflows/index.ts +16 -1
- package/extensions/workflows/invocation-ledger.ts +8 -2
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +10 -40
- package/extensions/workflows/replay-safety.ts +9 -8
- package/package.json +10 -10
- package/skills/subagents/SKILL.md +6 -0
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ pi install npm:@tt-a1i/openpi
|
|
|
54
54
|
OpenPI 会把长期进程放到后台,把独立任务交给隔离 Context 的 Pi Subagent,把多阶段依赖组织成 Workflow。状态会持续显示;完整运行可从 `/ps`、`/subagents` 和 `/workflows` 检查或终止。
|
|
55
55
|
|
|
56
56
|
> [!IMPORTANT]
|
|
57
|
-
> 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。
|
|
57
|
+
> 默认安装是安静的:不改主题、不绑定 Provider 或模型、不开启下一步预测,也不执行 post-edit 命令。Capability discovery 默认 `explicit`;只有用户通过 `/openpi-setup` 选择 `adaptive` 后,模型才会常驻看到一个小型发现网关并可自主加载额外能力。
|
|
58
58
|
|
|
59
59
|
```text
|
|
60
60
|
/openpi-setup
|
|
@@ -144,6 +144,8 @@ bg_start({
|
|
|
144
144
|
|
|
145
145
|
后台进程没有 stdin。需要交互输入的命令应由用户直接运行,而不是放进后台。
|
|
146
146
|
|
|
147
|
+
前台执行沿用 Pi 的 Bash 合同:`timeout` 可选且没有统一默认值,是否设置以及设置多长由模型或用户按命令语义决定。OpenPI 不再通过正则改写测试命令 timeout;确实需要有界执行时应显式传入 timeout,长期运行的 build、test、migration 或 server 可使用 Background Terminal 的生命周期能力。
|
|
148
|
+
|
|
147
149
|
### Pi-native Subagent:隔离 Context,不另起系统
|
|
148
150
|
|
|
149
151
|
```text
|
|
@@ -284,7 +286,7 @@ Workflow 在清理隔离 checkout 前原子保存有界 Handoff Manifest:track
|
|
|
284
286
|
|
|
285
287
|
| 能力 | 使用方式 | 它负责什么 |
|
|
286
288
|
| ------------- | --------------------------------------- | ------------------------------------------------------------------- |
|
|
287
|
-
| Tasks | `tasks_add` / `tasks_update` / `/tasks` |
|
|
289
|
+
| Tasks | `tasks_add` / `tasks_update` / `/tasks` | 逐项同步当前批次工作意图并刷新完整快照;不推断完成、不执行工作 |
|
|
288
290
|
| Goal | `/goal <目标>` | 驱动一个持续到终态的自主目标;完成前要求证据审计 |
|
|
289
291
|
| Plan Mode | `/plan [目标]` | 只读调研;`plan_ready` 后才准备可编辑的实施 Prompt,不自动执行 |
|
|
290
292
|
| Context Pivot | `/context-pivot <下一阶段>` | Context 超过约 30K Tokens 且任务换阶段时,用自包含 Brief 替换旧噪音 |
|
|
@@ -333,7 +335,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
|
|
|
333
335
|
| 终端输出 | 控制字符、方向格式符与超长内容在 ingress / render 边界清洗、限长 |
|
|
334
336
|
| Shutdown | Terminal、Subagent、Workflow 都有有界取消、清理与唯一终态 |
|
|
335
337
|
| 用户配置 | 单一受限 typed tool 写入;不散落扩展私有配置入口 |
|
|
336
|
-
| 模型消费 | Suggestion 默认关闭;
|
|
338
|
+
| 模型消费 | Suggestion 默认关闭;adaptive 仅在显式开启后允许模型自主加载能力 |
|
|
337
339
|
|
|
338
340
|
可选的 [pi-intercom](https://github.com/nicobailon/pi-intercom) 只在顶层 Pi Session 加载。它使用进程级身份,而 OpenPI Child 是同一进程内的并发 Session;Child Resource Loader 会移除 pi-intercom 扩展与 Skill,避免身份串线。Replay 也不会复用其调用。
|
|
339
341
|
|
|
@@ -352,6 +354,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
|
|
|
352
354
|
|
|
353
355
|
```text
|
|
354
356
|
/openpi-setup 开启下一步预测,选择 Registry 里的轻量模型,minimal 推理
|
|
357
|
+
/openpi-setup 让模型在合适时自主发现并采用 OpenPI 能力
|
|
355
358
|
/openpi-setup workflow 同时跑 16 个 agent,总调用最多 256
|
|
356
359
|
/openpi-setup Footer 两行:cwd flex model / context cost flex git
|
|
357
360
|
/openpi-setup Bash 展开,Write/Edit 保持紧凑
|
|
@@ -361,11 +364,14 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
|
|
|
361
364
|
|
|
362
365
|
配置保存在 `~/.pi/agent/my-pi-setup.json`,与包代码分离,升级不会覆盖。
|
|
363
366
|
|
|
367
|
+
一次 `/openpi-setup` episode 最多成功写入一次;成功后配置工具立即隐藏。若随后还要修改另一项,请重新执行 `/openpi-setup <自然语言请求>`,不要让模型重调已隐藏工具,也不要绕过入口直接编辑配置文件。
|
|
368
|
+
|
|
364
369
|
<details>
|
|
365
370
|
<summary><strong>默认值</strong></summary>
|
|
366
371
|
|
|
367
372
|
| 配置 | 默认值 |
|
|
368
373
|
| ---------------------------- | ---------------------------------------------- |
|
|
374
|
+
| Capability discovery | `explicit`;`adaptive` 必须显式开启 |
|
|
369
375
|
| Next-action Suggestion | 关闭;启用时显式选择 Registry 模型与 reasoning |
|
|
370
376
|
| Workflow 并发 / 总调用 | 8 / 128;硬上限 64 / 1024 |
|
|
371
377
|
| 大型 Header | 关闭 |
|
|
@@ -390,7 +396,7 @@ macOS/Linux arm64 与 x64 缺少二进制时,OpenPI 会从官方 Release 下
|
|
|
390
396
|
```bash
|
|
391
397
|
git clone https://github.com/tt-a1i/openpi.git ~/work/openpi
|
|
392
398
|
cd ~/work/openpi
|
|
393
|
-
|
|
399
|
+
bun install --frozen-lockfile
|
|
394
400
|
pi install ~/work/openpi
|
|
395
401
|
```
|
|
396
402
|
|
|
@@ -425,18 +431,23 @@ pi install npm:pi-intercom
|
|
|
425
431
|
<details>
|
|
426
432
|
<summary><strong>模型工具速查</strong></summary>
|
|
427
433
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
|
433
|
-
|
|
|
434
|
-
| `
|
|
435
|
-
| `
|
|
436
|
-
| `
|
|
437
|
-
| `
|
|
438
|
-
| `
|
|
439
|
-
| `
|
|
434
|
+
Capability discovery 默认是 `explicit`:普通父 Session 不常驻任何 OpenPI 模型工具,首轮保持 Pi 原生 `read`、`bash`、`edit`、`write`。用户明确要求结构化搜索、Subagent、Workflow、后台进程或 Session Goal/Tasks 时,OpenPI 在 `before_agent_start` 直接加载对应能力组;明确询问 OpenPI capabilities/tools/features 时显示 `openpi_load_tools`。可通过 `/openpi-setup` 显式选择 `adaptive`:此时只让小型 `openpi_load_tools` 网关常驻,模型可在判断任务确实受益时自主加载一个能力组。该选择也授权模型启动该组内的昂贵工作,因此不作为默认值。条件句(例如 “If you delegate…”)不会被当成显式委派意图。能力组在当前 Session 内单调保持,避免反复增删工具破坏缓存。组内管理工具仍只在资源成功创建或状态确实存在后出现。Mode / Setup / Context 工具独立跟随实时状态显示和隐藏。Background、Subagent 与 Workflow 的 Skill 文件仍随包发布,但只在对应能力触发后提示读取,不常驻普通系统 Prompt。
|
|
435
|
+
|
|
436
|
+
普通产品默认采用 Pi-native execution:保留 Pi 原生完整历史、工具输出上限、Session compaction、显式 Bash timeout 与 provider loop,不再额外做固定事务投影、成功 Bash 二次裁剪、测试 timeout 改写、重复失败硬拦或恢复/轨迹提示。OpenPI 只保留独立的工作区安全边界:阻止未授权删除 pre-existing 路径,并从实际文件状态识别本轮通过原生写入、文字重定向或 literal `mkdir -p` 创建的 scratch,避免误拦其清理。旧执行策略仅保留为受 benchmark root 门控的实验 profile,不会进入普通 Session。
|
|
437
|
+
|
|
438
|
+
| 工具 | 用途 | 可见时机 |
|
|
439
|
+
| -------------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------- |
|
|
440
|
+
| `openpi_load_tools` | 列出或加载可选工具组 | 明确询问;或启用 `adaptive` |
|
|
441
|
+
| `bg_start`, `bg_status`, `bg_list`, `bg_watch`, `bg_kill` | 后台进程生命周期 | 明确意图或 adaptive;启动后展开 |
|
|
442
|
+
| `subagent_spawn`, `subagent_check`, `subagent_list`, `subagent_wait`, `subagent_send`, `subagent_cancel` | 独立子 Agent | 明确意图或 adaptive;创建后展开 |
|
|
443
|
+
| `workflow`, `workflow_status`, `workflow_stop` | 动态多阶段编排与运行管理 | 明确意图或 adaptive;运行后展开 |
|
|
444
|
+
| `tasks_add`, `tasks_update`, `tasks_list` | Session 工作项 | 明确意图或 adaptive;存在后展开 |
|
|
445
|
+
| `get_goal`, `create_goal`, `update_goal` | Session Goal | 明确意图或 adaptive;存在后展开 |
|
|
446
|
+
| `context_pivot` | Context 阶段切换 | Context 达到阈值时 |
|
|
447
|
+
| `ask_user`, `human_handoff` | 经复核的用户决策与用户专属操作 | Plan 或 Setup 进行中 |
|
|
448
|
+
| `plan_ready` | 显式完成计划,不自动开始实施 | Plan 调研阶段 |
|
|
449
|
+
| `fd`, `rg` | 文件发现与内容搜索 | 明确意图或 adaptive 加载 search |
|
|
450
|
+
| `configure_my_pi_setup` | 受限配置写入 | `/openpi-setup` 进行中 |
|
|
440
451
|
|
|
441
452
|
</details>
|
|
442
453
|
|
|
@@ -451,7 +462,7 @@ pi install npm:pi-intercom
|
|
|
451
462
|
<details>
|
|
452
463
|
<summary><strong>安装后会自动调用额外模型吗?</strong></summary>
|
|
453
464
|
|
|
454
|
-
|
|
465
|
+
默认不会。Suggestion 默认关闭;Capability discovery 默认 `explicit`。如果用户显式开启 `adaptive`,网关本身不发模型请求,但主模型可以自主加载 Subagent 或 Workflow 并启动额外模型调用;并发和 Workflow 总调用上限仍然生效。
|
|
455
466
|
|
|
456
467
|
</details>
|
|
457
468
|
|
|
@@ -497,6 +508,7 @@ Plan Mode 不猜“任意 Shell 是否只读”,只放行由已知安全零件
|
|
|
497
508
|
```text
|
|
498
509
|
extensions/
|
|
499
510
|
├── setup/ # /openpi-setup 与受限配置工具
|
|
511
|
+
├── capabilities/ # 最小能力发现入口与 Session 工具面加载
|
|
500
512
|
├── background-terminals/ # 长进程、日志、/ps
|
|
501
513
|
├── subagents/ # Pi-native Backend、角色、/subagents
|
|
502
514
|
├── workflows/ # DSL、Ledger、Graph、Replay、Artifacts
|
|
@@ -510,17 +522,20 @@ extensions/
|
|
|
510
522
|
├── ui-customization/ # Header、Footer、Terminal title
|
|
511
523
|
└── shared/ # Child policy、配置、Worktree、终端清洗
|
|
512
524
|
|
|
513
|
-
skills/ # Background terminal 与
|
|
525
|
+
skills/ # Background terminal、Subagent 与 Workflow 指南
|
|
514
526
|
themes/ # github-dark-default
|
|
515
527
|
```
|
|
516
528
|
|
|
529
|
+
开发工具链使用 Bun `1.3.14` 管理依赖和脚本,Biome 负责 TypeScript / JavaScript / JSON 格式与基础 lint;产品运行时仍是 Node,测试仍由 `node:test` 与 Vitest 执行:
|
|
530
|
+
|
|
517
531
|
```bash
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
npm test
|
|
532
|
+
bun install --frozen-lockfile
|
|
533
|
+
bun run check
|
|
534
|
+
bun run test
|
|
522
535
|
```
|
|
523
536
|
|
|
537
|
+
npm 仍用于发布包的 `pack` / clean-install 验证,因为用户通过 npm Registry 安装 OpenPI。
|
|
538
|
+
|
|
524
539
|
测试覆盖进程树终止与竞态、Subagent 生命周期与工具边界、Workflow Sandbox / Ledger / Graph / Replay / Acceptance、Worktree 数据保全、Session 状态恢复、配置迁移和 TUI 渲染。设计记录见 [`docs/design/`](docs/design/),问题请提交到 [GitHub Issues](https://github.com/tt-a1i/openpi/issues)。
|
|
525
540
|
|
|
526
541
|
---
|
package/SETUP.md
CHANGED
|
@@ -16,7 +16,7 @@ Pi installs the package dependencies automatically. Restart Pi or run `/reload`
|
|
|
16
16
|
|
|
17
17
|
## fd and rg tools
|
|
18
18
|
|
|
19
|
-
The `file-search` extension registers `fd` and `rg` as model tools. No setup is normally needed: at startup it silently uses a system-installed `fd` (or `fdfind` on Debian/Ubuntu) and `rg` when available, or an existing fallback in this package's private `bin/` directory. Only when neither exists does it download an official release binary (macOS/Linux, arm64/x64, over HTTPS) into that package-local directory and show a one-time notification. If your platform is unsupported, install `fd` and `rg` with your package manager and restart Pi.
|
|
19
|
+
The `file-search` extension registers `fd` and `rg` as model tools. They stay outside an ordinary parent turn until the user explicitly asks to use `fd`/`rg` or structured file search, or the model loads the `search` group through `openpi_load_tools`. The gateway is shown after an explicit OpenPI-capability request, or remains visible when the user opts into adaptive discovery; child sessions may still receive `fd` and `rg` through the reviewed child-safe allowlist. No setup is normally needed: at startup it silently uses a system-installed `fd` (or `fdfind` on Debian/Ubuntu) and `rg` when available, or an existing fallback in this package's private `bin/` directory. Only when neither exists does it download an official release binary (macOS/Linux, arm64/x64, over HTTPS) into that package-local directory and show a one-time notification. If your platform is unsupported, install `fd` and `rg` with your package manager and restart Pi.
|
|
20
20
|
|
|
21
21
|
## Theme
|
|
22
22
|
|
|
@@ -28,14 +28,16 @@ Add the included theme to `~/.pi/agent/settings.json` while keeping your existin
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Pi will load the extensions,
|
|
31
|
+
Pi will load the extensions and theme the next time it starts. OpenPI's Background, Subagent, and Workflow Skill files remain in the package, but ordinary turns do not advertise them in the system prompt; the matching path is disclosed only after explicit capability intent or after the model loads that group through the opt-in adaptive gateway.
|
|
32
32
|
|
|
33
33
|
## Configure this package
|
|
34
34
|
|
|
35
|
-
Use the single canonical package-owned command. `/my-pi-setup` remains a compatibility alias. With no arguments, when optional pi-intercom is absent, the interactive TUI first offers a reviewed global installation; declining changes nothing. Acceptance installs the fixed `npm:pi-intercom` source through Pi's package manager. A new private config receives `confirmSend: true` and `inboundTrigger: "replies"`; an existing preference file is never rewritten and must already define both fields. Package download failure writes no config, while uncertain activation retains the safe new config. Setup asks for `/reload` instead of loading a new broker into the running Session. The current model then explains the remaining configurable areas and uses `ask_user`: first run initializes them; later runs explain the saved state and ask whether to keep it, change one area, or review everything. With arguments, it treats the rest as a targeted natural-language request:
|
|
35
|
+
Use the single canonical package-owned command. `/my-pi-setup` remains a compatibility alias. With no arguments, when optional pi-intercom is absent, the interactive TUI first offers a reviewed global installation; declining changes nothing. Acceptance installs the fixed `npm:pi-intercom` source through Pi's package manager. A new private config receives `confirmSend: true` and `inboundTrigger: "replies"`; an existing preference file is never rewritten and must already define both fields. Package download failure writes no config, while uncertain activation retains the safe new config. Setup asks for `/reload` instead of loading a new broker into the running Session. The current model then explains the remaining configurable areas and uses `ask_user`: first run initializes them; later runs explain the saved state and ask whether to keep it, change one area, or review everything. With arguments, it treats the rest as a targeted natural-language request. Persist still goes through the typed `configure_my_pi_setup` tool, which is exposed only while that `/openpi-setup` episode is in flight and is hidden again afterward. One successful apply completes the episode; a later configuration change starts a new one with `/openpi-setup <request>` rather than reusing the hidden tool:
|
|
36
36
|
|
|
37
37
|
```text
|
|
38
38
|
/openpi-setup
|
|
39
|
+
/openpi-setup 让模型在合适时自主发现并采用 OpenPI 能力
|
|
40
|
+
/openpi-setup 只在我明确要求时加载 OpenPI 能力
|
|
39
41
|
/openpi-setup 开启下一步预测,使用 seal/deepseek-v4-flash,关闭推理
|
|
40
42
|
/openpi-setup 关闭下一步预测
|
|
41
43
|
/openpi-setup workflow 同时跑 16 个 agent,总任务最多 256 个
|
|
@@ -52,7 +54,7 @@ Use the single canonical package-owned command. `/my-pi-setup` remains a compati
|
|
|
52
54
|
/openpi-setup 清除 explorer 的模型,让它继承父模型
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. `Right` accepts it into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; users who do not usually inspect implementation detail can select compact previews. Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand` (`Ctrl+O` by default). An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
|
|
57
|
+
Capability discovery defaults to `explicit`, preserving the zero-resident OpenPI tool surface until the user asks for a capability. `adaptive` is an explicit opt-in that keeps only `openpi_load_tools` visible and allows the model to load a useful group on its own; because this can start Subagents, Workflows, or background processes, normal permission and configured concurrency/call limits still apply. Changing the setting updates the current Session immediately, while already loaded groups remain stable for that Session. Next-action suggestions default to off. Run `/openpi-setup` to explicitly choose an available model and reasoning level. After a fully settled main-agent run, one suggestion may appear as dim inline text on the first row of an empty editor; reserved cells at the row end keep CJK IME preedit from overwriting it. `Right` accepts it into the editor without submitting, while any other editor input dismisses it. Suggestions are ephemeral and never enter session history or model context. Workflows default to 8 concurrent agents and 128 total agent calls per run; configurable hard maxima are 64 and 1024. The large decorative header defaults off and the custom dashboard footer defaults on with a one-line Powerline layout (`cwd model thinking context cache cost throughput |flex| git pr`). Footer presets are `powerline`, `powerline-mono`, and `compact`; style can also be set independently to `plain`, `powerline`, or `powerline-mono`. Custom layouts use a 2D `footerLines` array with at most one `flex` per row for left/right alignment. Nerd Font only affects powerline separator glyphs (``); metric text stays readable without it. Footer changes apply immediately in the active TUI session. Subagent results default to the existing full display; users who do not usually inspect implementation detail can select compact previews. Bash defaults to a folded one-line command with bounded output and a hidden-line count. Write/Edit defaults to an extra-short folded preview capped at three rendered lines including the operation header; its hidden-line hint remains inside the operation's status background. Select full independently for any category to keep it expanded. Compact views temporarily expand with `app.tools.expand` (`Ctrl+O` by default). An optional post-edit command is off by default: set one (for example `npm run format`, maximum 500 characters) and it runs once in the background after each interactive-TUI turn with successful Write/Edit operations, with failures reported as a notification. It deliberately does not guess whether arbitrary Bash commands changed files. Built-in Agent roles `explorer`, `implementer`, `reviewer`, and `advisor` are shared by `subagent_spawn.agent_type` and Workflow `agent(..., { agent_type })`; all inherit the parent model by default. `/openpi-setup` may assign a currently available Registry model to any subset; clearing one returns it to inheritance and omitted roles stay unchanged. Model precedence is explicit call > selected role-file model > setup assignment > parent inheritance; effort is explicit call > selected role > parent. A trusted project `.pi/agents/<role>.md` overrides global `~/.pi/agent/agents/<role>.md`, which overrides the complete built-in role definition; overrides are diagnosed. Role-model changes apply to the next spawn or Workflow agent call without reload. Configuration is stored privately at `~/.pi/agent/my-pi-setup.json`.
|
|
56
58
|
|
|
57
59
|
## Session Goal and Tasks
|
|
58
60
|
|
|
@@ -62,7 +64,7 @@ Model callers use `get_goal`, `create_goal`, and `update_goal`. `create_goal` is
|
|
|
62
64
|
|
|
63
65
|
There are no normal user-facing Turn, no-progress, or wall-clock caps; a hidden 1000-continuation circuit breaker exists only to stop runaway automation. An optional `token_budget` must only be positive. Goal non-cached Assistant input-plus-output Token and elapsed-time usage are persisted; crossing the budget marks `budget_limited` and queues one wrap-up Turn. Active goals continue after reload/resume. Fork and tree navigation defer inherited active continuation until the first explicit user input; paused, blocked, and usage-limited goals remain stopped and can prompt for Resume. A v1 active/waiting goal migrates once to paused. Assistant aborts pause an active goal and Assistant errors block it. Print/json automation is inert. Footer text mirrors Codex (`Pursuing goal (…)`, resume hints, `Goal unmet`, `Goal achieved`) without showing the objective or legacy Turn counts. An achieved Footer remains visible until the next explicit interactive/RPC input, then a branch-persisted acknowledgement hides only the Footer while `/goal` retains the completed record.
|
|
64
66
|
|
|
65
|
-
Session Tasks remain advisory multi-item work intent and do not determine Goal completion. They are scoped to the current request batch: once every item is done or dropped, the batch closes and the next `tasks_add` starts again at T1. Active items persist in a polished Claude Code-style panel above the editor; `Ctrl+Shift+T` or `/tasks hide|show|toggle` controls visibility, while `/tasks` opens the full list. No `/openpi-setup` setting or secondary judge model is required.
|
|
67
|
+
Session Tasks remain advisory multi-item work intent and do not determine Goal completion. They are scoped to the current request batch: once every item is done or dropped, the batch closes and the next `tasks_add` starts again at T1. The model marks a tracked item `in_progress` before starting it, records `done`, `blocked`, or `dropped` immediately after that item reaches a real outcome, and reconciles touched items before its final answer. Every add/update result returns the complete bounded current snapshot so the next item is explicit and the panel refreshes on each persisted transition. Commit, test, and authorization signals are only task-scoped evidence candidates; OpenPI never infers completion or mutates a task from those signals. Active items persist in a polished Claude Code-style panel above the editor; `Ctrl+Shift+T` or `/tasks hide|show|toggle` controls visibility, while `/tasks` opens the full list. No `/openpi-setup` setting or secondary judge model is required.
|
|
66
68
|
|
|
67
69
|
## Other commands added by this fork
|
|
68
70
|
|
|
@@ -70,5 +72,5 @@ Session Tasks remain advisory multi-item work intent and do not determine Goal c
|
|
|
70
72
|
- `/tasks` inspects branch-scoped advisory work items.
|
|
71
73
|
- `/goal ...` controls the persistent autonomous session objective.
|
|
72
74
|
- `/context-pivot <next phase>` deliberately compacts a long current session into a next-phase brief. It requires at least 30,000 context tokens and is rejected below that; use the separate `/handoff` skill when work should move to a genuinely new session.
|
|
73
|
-
- `/cron every <5m> <prompt>`, `/cron in <30s> <prompt>`, `/cron list`, and `/cron remove <id>` schedule a prompt for this session. Jobs are in-memory and session-scoped (cleared on shutdown), fire only while the session is idle, and use a duration grammar (`30s`/`5m`/`2h`, minimum 30s) rather than crontab fields, because the scheduler polls about every 30 seconds.
|
|
75
|
+
- `/cron every <5m> <prompt>`, `/cron in <30s> <prompt>`, `/cron list`, and `/cron remove <id>` schedule a prompt for this session. Jobs are in-memory and session-scoped (cleared on shutdown), fire only while the session is idle, and use a duration grammar (`30s`/`5m`/`2h`, minimum 30s) rather than crontab fields, because the scheduler polls about every 30 seconds. Jobs due in the same poll are delivered as one triggered turn while retaining each job's id and recurrence metadata; if that atomic delivery fails, every due job remains pending for retry.
|
|
74
76
|
- `/plan [objective]` explores read-only before changing anything: while armed it blocks `edit`, `write`, mutating Bash, `subagent_send`, `workflow`, and `bg_start`, while read/grep/find/ls/fd/rg and verified read-only Git/GitHub commands stay available. It permits `subagent_spawn`, but the harness narrows every newly spawned planning child to investigation-only tools; agent types can narrow that list further, never widen it. The model must submit the complete plan through parent-only `plan_ready`; the write gate stays closed until `/plan` prepares an editable implementation prompt for the current or a fresh Session. `/plan off` cancels.
|
|
@@ -33,7 +33,11 @@ const HumanHandoffParams = Type.Object({
|
|
|
33
33
|
|
|
34
34
|
export type HumanHandoffInput = Static<typeof HumanHandoffParams>;
|
|
35
35
|
export type HumanHandoffStatus =
|
|
36
|
-
|
|
36
|
+
| "completed"
|
|
37
|
+
| "unable"
|
|
38
|
+
| "dismissed"
|
|
39
|
+
| "cancelled"
|
|
40
|
+
| "unavailable";
|
|
37
41
|
|
|
38
42
|
export interface HumanHandoffDetails {
|
|
39
43
|
status: HumanHandoffStatus;
|
|
@@ -25,6 +25,18 @@ import {
|
|
|
25
25
|
import { Cause, Effect, Exit } from "effect";
|
|
26
26
|
import { Type, type Static } from "typebox";
|
|
27
27
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
28
|
+
import {
|
|
29
|
+
PLAN_MODE_CHANNEL,
|
|
30
|
+
type PlanModeState,
|
|
31
|
+
} from "../shared/plan-mode-state.ts";
|
|
32
|
+
import {
|
|
33
|
+
OPENPI_SETUP_EPISODE_CHANNEL,
|
|
34
|
+
type OpenPiSetupEpisodeState,
|
|
35
|
+
} from "../shared/setup-episode-state.ts";
|
|
36
|
+
import {
|
|
37
|
+
OPENPI_TOOL_SURFACE,
|
|
38
|
+
patchOwnedTools,
|
|
39
|
+
} from "../shared/tool-surface.ts";
|
|
28
40
|
import { createHumanHandoffToolDefinition } from "./handoff.ts";
|
|
29
41
|
import {
|
|
30
42
|
BRACKETED_PASTE_END,
|
|
@@ -355,6 +367,38 @@ export async function showQuestionsWithDialogs(
|
|
|
355
367
|
}
|
|
356
368
|
|
|
357
369
|
export default function askUser(pi: ExtensionAPI) {
|
|
370
|
+
let planning = false;
|
|
371
|
+
let setupActive = false;
|
|
372
|
+
const syncInteractionTools = () =>
|
|
373
|
+
patchOwnedTools(pi, "interaction", {
|
|
374
|
+
...(planning || setupActive
|
|
375
|
+
? { enable: OPENPI_TOOL_SURFACE.interaction.deferred }
|
|
376
|
+
: { disable: OPENPI_TOOL_SURFACE.interaction.deferred }),
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
pi.events.on(PLAN_MODE_CHANNEL, (state: unknown) => {
|
|
380
|
+
planning =
|
|
381
|
+
typeof state === "object" &&
|
|
382
|
+
state !== null &&
|
|
383
|
+
(state as PlanModeState).planning === true;
|
|
384
|
+
syncInteractionTools();
|
|
385
|
+
});
|
|
386
|
+
pi.events.on(OPENPI_SETUP_EPISODE_CHANNEL, (state: unknown) => {
|
|
387
|
+
setupActive =
|
|
388
|
+
typeof state === "object" &&
|
|
389
|
+
state !== null &&
|
|
390
|
+
(state as OpenPiSetupEpisodeState).active === true;
|
|
391
|
+
syncInteractionTools();
|
|
392
|
+
});
|
|
393
|
+
pi.on("session_shutdown", () => {
|
|
394
|
+
planning = false;
|
|
395
|
+
setupActive = false;
|
|
396
|
+
syncInteractionTools();
|
|
397
|
+
});
|
|
398
|
+
pi.on("session_start", () => {
|
|
399
|
+
syncInteractionTools();
|
|
400
|
+
});
|
|
401
|
+
|
|
358
402
|
pi.registerTool({
|
|
359
403
|
name: "ask_user",
|
|
360
404
|
label: "Ask User",
|
|
@@ -28,8 +28,16 @@ import type {
|
|
|
28
28
|
import { Type } from "typebox";
|
|
29
29
|
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
30
30
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
31
|
+
import {
|
|
32
|
+
OPENPI_TOOL_SURFACE,
|
|
33
|
+
patchOwnedTools,
|
|
34
|
+
} from "../shared/tool-surface.ts";
|
|
31
35
|
import type { TerminalSnapshot } from "./src/domain.ts";
|
|
32
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
MAX_RUNNING,
|
|
38
|
+
TerminalManager,
|
|
39
|
+
type TerminalManagerShape,
|
|
40
|
+
} from "./src/manager.ts";
|
|
33
41
|
import {
|
|
34
42
|
BG_KILL_PARAMETER_DESCRIPTIONS,
|
|
35
43
|
BG_KILL_TOOL_DESCRIPTION,
|
|
@@ -45,6 +53,7 @@ import {
|
|
|
45
53
|
buildKillReport,
|
|
46
54
|
buildStartResult,
|
|
47
55
|
buildStatusResult,
|
|
56
|
+
buildTerminalBatchResultMessage,
|
|
48
57
|
buildTerminalResultMessage,
|
|
49
58
|
buildWatchArmedResult,
|
|
50
59
|
buildWatchMatchMessage,
|
|
@@ -52,23 +61,29 @@ import {
|
|
|
52
61
|
} from "./src/prompt.ts";
|
|
53
62
|
import {
|
|
54
63
|
createDeferredResultDelivery,
|
|
64
|
+
createIdleResultBatcher,
|
|
65
|
+
hasTerminalCapacity,
|
|
55
66
|
resultDeliveryOptions,
|
|
56
67
|
} from "./src/result-delivery.ts";
|
|
57
|
-
import {
|
|
58
|
-
assertWatchableOutput,
|
|
59
|
-
compileWatchPattern,
|
|
60
|
-
createChunkMatcher,
|
|
61
|
-
matchCapturedOutput,
|
|
62
|
-
} from "./src/watch.ts";
|
|
63
68
|
import {
|
|
64
69
|
createTerminalRuntime,
|
|
65
70
|
runTool,
|
|
66
71
|
type TerminalRuntime,
|
|
67
72
|
} from "./src/runtime.ts";
|
|
68
73
|
import { openTerminalPicker } from "./src/ui/ps.ts";
|
|
69
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
renderTerminalBatchResult,
|
|
76
|
+
renderTerminalResult,
|
|
77
|
+
} from "./src/ui/tool-result.ts";
|
|
78
|
+
import {
|
|
79
|
+
assertWatchableOutput,
|
|
80
|
+
compileWatchPattern,
|
|
81
|
+
createChunkMatcher,
|
|
82
|
+
matchCapturedOutput,
|
|
83
|
+
} from "./src/watch.ts";
|
|
70
84
|
|
|
71
85
|
const WIDGET_KEY = "background-terminals";
|
|
86
|
+
const IDLE_RESULT_BATCH_MS = 200;
|
|
72
87
|
|
|
73
88
|
interface WatchToolDetails {
|
|
74
89
|
id: string;
|
|
@@ -83,7 +98,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
98
|
let sessionContext: ExtensionContext | undefined;
|
|
84
99
|
let ui: ExtensionUIContext | undefined;
|
|
85
100
|
let unsubStatus: (() => void) | undefined;
|
|
101
|
+
let startReservations = 0;
|
|
86
102
|
const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>();
|
|
103
|
+
const hideLifecycleTools = () =>
|
|
104
|
+
patchOwnedTools(pi, "background", {
|
|
105
|
+
disable: OPENPI_TOOL_SURFACE.background.deferred,
|
|
106
|
+
});
|
|
107
|
+
const showLifecycleTools = () =>
|
|
108
|
+
patchOwnedTools(pi, "background", {
|
|
109
|
+
enable: OPENPI_TOOL_SURFACE.background.deferred,
|
|
110
|
+
});
|
|
87
111
|
/** Active bg_watch disarm callbacks, keyed by terminal id (one per id). */
|
|
88
112
|
const watchers = new Map<string, () => void>();
|
|
89
113
|
|
|
@@ -160,7 +184,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
160
184
|
customType: "background-terminal-result",
|
|
161
185
|
// One message per flush, not per terminal: five processes exiting
|
|
162
186
|
// together are one event to react to, not five.
|
|
163
|
-
content:
|
|
187
|
+
content: buildTerminalBatchResultMessage(
|
|
188
|
+
snaps.map(buildTerminalResultMessage),
|
|
189
|
+
),
|
|
164
190
|
display: true,
|
|
165
191
|
details:
|
|
166
192
|
snaps.length === 1
|
|
@@ -198,12 +224,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
198
224
|
};
|
|
199
225
|
|
|
200
226
|
const flushResults = (wake: boolean) => {
|
|
201
|
-
const snaps = resultDelivery.drain();
|
|
202
|
-
if (!deliverResults(snaps, wake))
|
|
203
|
-
for (const snap of snaps) resultDelivery.defer(snap);
|
|
204
|
-
}
|
|
227
|
+
const snaps = resultDelivery.drain(MAX_RUNNING);
|
|
228
|
+
if (!deliverResults(snaps, wake)) resultDelivery.restore(snaps);
|
|
205
229
|
};
|
|
206
230
|
|
|
231
|
+
const idleResultBatcher = createIdleResultBatcher({
|
|
232
|
+
delayMs: IDLE_RESULT_BATCH_MS,
|
|
233
|
+
isIdle: () => sessionContext?.isIdle() === true,
|
|
234
|
+
flush: flushResults,
|
|
235
|
+
startTimer: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
236
|
+
clearTimer: (timer) => clearTimeout(timer),
|
|
237
|
+
});
|
|
238
|
+
|
|
207
239
|
const onSettled = (snap: TerminalSnapshot, consumed: boolean) => {
|
|
208
240
|
// A settled terminal has delivered its final result and will emit no more
|
|
209
241
|
// output, so any watch armed on it can never match — disarm it now instead
|
|
@@ -216,17 +248,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
216
248
|
}
|
|
217
249
|
// Defer a deep-enough copy: the live snapshot's output views keep
|
|
218
250
|
// mutating (late flushes) after settle.
|
|
219
|
-
resultDelivery.defer({
|
|
251
|
+
const pending = resultDelivery.defer({
|
|
220
252
|
...snap,
|
|
221
253
|
stdout: { ...snap.stdout },
|
|
222
254
|
stderr: { ...snap.stderr },
|
|
223
255
|
});
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
256
|
+
// Pending settlements remain retractable while busy, so bg_status/bg_kill
|
|
257
|
+
// can consume them before they are committed to context. bg_start applies
|
|
258
|
+
// backpressure across running + pending + reserved work, keeping this map
|
|
259
|
+
// bounded without dropping or prematurely queueing any result.
|
|
260
|
+
if (pending >= MAX_RUNNING && sessionContext?.isIdle()) {
|
|
261
|
+
idleResultBatcher.flushNow();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// Give near-simultaneous idle settlements one fixed, bounded window to
|
|
265
|
+
// join this result. This costs one model turn for a batch instead of one
|
|
266
|
+
// turn per process, while preserving the immediate path after 200 ms.
|
|
267
|
+
if (sessionContext?.isIdle()) idleResultBatcher.schedule();
|
|
227
268
|
};
|
|
228
269
|
|
|
229
270
|
pi.on("session_start", (_event, ctx) => {
|
|
271
|
+
hideLifecycleTools();
|
|
230
272
|
sessionContext = ctx;
|
|
231
273
|
if (ctx.hasUI) ui = ctx.ui;
|
|
232
274
|
});
|
|
@@ -236,7 +278,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
236
278
|
// double delivery is structurally impossible — whoever drains first wins.
|
|
237
279
|
// These finished while the model was working on something else, so they go
|
|
238
280
|
// into context without forcing a turn per stale process.
|
|
239
|
-
pi.on("agent_settled", () =>
|
|
281
|
+
pi.on("agent_settled", () => {
|
|
282
|
+
idleResultBatcher.flushWithoutWake();
|
|
283
|
+
// A failed send is restored exactly. Drain it in bounded chunks on later
|
|
284
|
+
// settled turns rather than growing a single unbounded message.
|
|
285
|
+
if (sessionContext?.isIdle() && resultDelivery.size() > 0) {
|
|
286
|
+
idleResultBatcher.schedule();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
240
289
|
|
|
241
290
|
// /new, /resume, /fork, /reload, and quit all emit session_shutdown for
|
|
242
291
|
// the old extension instance. Processes never survive a session
|
|
@@ -245,6 +294,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
245
294
|
// bounded so a wedged process cannot hang shutdown.
|
|
246
295
|
pi.on("session_shutdown", async () => {
|
|
247
296
|
sessionContext = undefined;
|
|
297
|
+
idleResultBatcher.clear();
|
|
248
298
|
resultDelivery.clear();
|
|
249
299
|
for (const disarm of [...watchers.values()]) disarm();
|
|
250
300
|
watchers.clear();
|
|
@@ -256,6 +306,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
256
306
|
// UI may already be gone.
|
|
257
307
|
}
|
|
258
308
|
widgetRunning = 0;
|
|
309
|
+
startReservations = 0;
|
|
259
310
|
ui = undefined;
|
|
260
311
|
const closing = runtime;
|
|
261
312
|
runtime = undefined;
|
|
@@ -309,15 +360,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
309
360
|
.replace(/\s+/g, " ")
|
|
310
361
|
.trim()
|
|
311
362
|
.slice(0, 80) || "terminal";
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
363
|
+
const running = manager.view
|
|
364
|
+
.list()
|
|
365
|
+
.filter((entry) => entry.status === "running").length;
|
|
366
|
+
if (
|
|
367
|
+
!hasTerminalCapacity({
|
|
368
|
+
running,
|
|
369
|
+
pending: resultDelivery.size(),
|
|
370
|
+
reserved: startReservations,
|
|
371
|
+
maximum: MAX_RUNNING,
|
|
372
|
+
})
|
|
373
|
+
) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Max ${MAX_RUNNING} background terminals may be running or awaiting delivery. Let the current turn settle, or inspect a finished terminal with bg_status before starting another.`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
startReservations++;
|
|
379
|
+
let snap: TerminalSnapshot;
|
|
380
|
+
try {
|
|
381
|
+
snap = await runTool(
|
|
382
|
+
getRuntime(),
|
|
383
|
+
manager.start({
|
|
384
|
+
command,
|
|
385
|
+
title,
|
|
386
|
+
cwd,
|
|
387
|
+
timeoutSeconds: params.timeout_seconds,
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
} finally {
|
|
391
|
+
startReservations--;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
showLifecycleTools();
|
|
321
395
|
|
|
322
396
|
return {
|
|
323
397
|
content: [{ type: "text", text: buildStartResult(snap) }],
|
|
@@ -591,7 +665,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
591
665
|
status?: string;
|
|
592
666
|
exitCode?: number;
|
|
593
667
|
signal?: string;
|
|
668
|
+
results?: Array<{
|
|
669
|
+
id: string;
|
|
670
|
+
title: string;
|
|
671
|
+
status: string;
|
|
672
|
+
exitCode?: number;
|
|
673
|
+
signal?: string;
|
|
674
|
+
}>;
|
|
594
675
|
};
|
|
676
|
+
const content =
|
|
677
|
+
typeof message.content === "string" ? message.content : "";
|
|
678
|
+
if (details.results && details.results.length > 1) {
|
|
679
|
+
return renderTerminalBatchResult(
|
|
680
|
+
content,
|
|
681
|
+
expanded || loadSetupConfig().ui.bashToolDisplay === "full",
|
|
682
|
+
theme,
|
|
683
|
+
details.results,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
595
686
|
const failed = details.status === "failed";
|
|
596
687
|
const killed = details.status === "killed";
|
|
597
688
|
const timedOut = details.status === "timed_out";
|
|
@@ -611,8 +702,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
611
702
|
theme.fg("accent", theme.bold(`terminal ${details.id ?? "?"}`)) +
|
|
612
703
|
theme.fg("muted", ` · ${details.title ?? ""} · ${how}`);
|
|
613
704
|
|
|
614
|
-
const content =
|
|
615
|
-
typeof message.content === "string" ? message.content : "";
|
|
616
705
|
return renderTerminalResult(
|
|
617
706
|
content,
|
|
618
707
|
expanded || loadSetupConfig().ui.bashToolDisplay === "full",
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
import { Data } from "effect";
|
|
10
10
|
|
|
11
11
|
export type TerminalStatus =
|
|
12
|
-
|
|
12
|
+
| "running"
|
|
13
|
+
| "done"
|
|
14
|
+
| "failed"
|
|
15
|
+
| "killed"
|
|
16
|
+
| "timed_out";
|
|
13
17
|
// "done" = exited with code 0
|
|
14
18
|
// "failed" = exited non-zero, or a spawn-level runtime error after start
|
|
15
19
|
// "killed" = terminated by bg_kill, the /ps UI, or session teardown
|
|
@@ -313,7 +313,8 @@ const makeManager = Effect.gen(function* () {
|
|
|
313
313
|
let disposed = false;
|
|
314
314
|
let spillDir: string | undefined | null;
|
|
315
315
|
let onSettled:
|
|
316
|
-
((snap: TerminalSnapshot, consumed: boolean) => void)
|
|
316
|
+
| ((snap: TerminalSnapshot, consumed: boolean) => void)
|
|
317
|
+
| undefined;
|
|
317
318
|
|
|
318
319
|
const notify = (id?: string) => {
|
|
319
320
|
for (const listener of [...listeners]) {
|
|
@@ -22,6 +22,8 @@ export const STATUS_STDERR_MAX = 8 * 1024;
|
|
|
22
22
|
export const RESULT_STDOUT_MAX = 8 * 1024;
|
|
23
23
|
/** Completion follow-up stderr tail. Keep this concise; /ps has the detailed view. */
|
|
24
24
|
export const RESULT_STDERR_MAX = 4 * 1024;
|
|
25
|
+
/** Global model-facing cap for one batched completion message. */
|
|
26
|
+
export const RESULT_BATCH_MAX = 48 * 1024;
|
|
25
27
|
const STATUS_STDOUT_MAX_LINES = 400;
|
|
26
28
|
const STATUS_STDERR_MAX_LINES = 200;
|
|
27
29
|
const RESULT_STDOUT_MAX_LINES = 40;
|
|
@@ -179,6 +181,39 @@ export function buildTerminalResultMessage(snap: TerminalSnapshot) {
|
|
|
179
181
|
return text;
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
/** Preserve every retained terminal identity while globally bounding batch logs. */
|
|
185
|
+
export function buildTerminalBatchResultMessage(
|
|
186
|
+
messages: readonly string[],
|
|
187
|
+
omitted = 0,
|
|
188
|
+
) {
|
|
189
|
+
if (messages.length === 1 && omitted === 0) return messages[0]!;
|
|
190
|
+
const summaries = messages.map(
|
|
191
|
+
(message) => message.split("\n", 1)[0] || "Background terminal result",
|
|
192
|
+
);
|
|
193
|
+
const header = [
|
|
194
|
+
`${messages.length} background terminal result${messages.length === 1 ? "" : "s"}:`,
|
|
195
|
+
...summaries.map((summary) => `- ${summary}`),
|
|
196
|
+
omitted > 0
|
|
197
|
+
? `- ${omitted} older result${omitted === 1 ? "" : "s"} omitted from this bounded batch; use bg_list/bg_status for retained details.`
|
|
198
|
+
: "",
|
|
199
|
+
]
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.join("\n");
|
|
202
|
+
const logsHeader = "\n\nLogs (newest tail):\n";
|
|
203
|
+
const truncationMarker =
|
|
204
|
+
"\n[batch logs truncated at the 48 KiB model-context limit; use bg_status or /ps for retained details.]";
|
|
205
|
+
const fixedBytes = Buffer.byteLength(
|
|
206
|
+
`${header}${logsHeader}${truncationMarker}`,
|
|
207
|
+
"utf8",
|
|
208
|
+
);
|
|
209
|
+
const logs = truncateTail(messages.join("\n\n"), {
|
|
210
|
+
maxBytes: Math.max(1, RESULT_BATCH_MAX - fixedBytes),
|
|
211
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
212
|
+
});
|
|
213
|
+
const text = `${header}${logsHeader}${logs.content}${logs.truncated ? truncationMarker : ""}`;
|
|
214
|
+
return text;
|
|
215
|
+
}
|
|
216
|
+
|
|
182
217
|
export function buildKillReport(results: ReadonlyArray<KillResult>) {
|
|
183
218
|
return results
|
|
184
219
|
.map((entry) => {
|