@tt-a1i/openpi 0.1.0 → 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/LICENSE +21 -0
- package/README.md +295 -389
- package/SETUP.md +24 -22
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/assets/readme-hero-mobile.svg +2 -2
- package/assets/readme-hero.svg +10 -10
- 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 +241 -45
- package/extensions/setup/intercom-fs-helper.cjs +130 -0
- package/extensions/setup/intercom.ts +603 -0
- package/extensions/shared/child-session.ts +42 -5
- package/extensions/shared/setup-config.ts +27 -1
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +16 -1
- 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/artifacts.ts +6 -1
- package/extensions/workflows/dashboard.ts +138 -27
- package/extensions/workflows/graph-projection.ts +240 -0
- package/extensions/workflows/handoff.ts +194 -0
- package/extensions/workflows/index.ts +258 -56
- package/extensions/workflows/invocation-ledger.ts +368 -0
- package/extensions/workflows/model.ts +57 -1
- package/extensions/workflows/operator.ts +131 -0
- package/extensions/workflows/prompt.ts +10 -38
- package/extensions/workflows/replay-safety.ts +9 -8
- package/extensions/workflows/runner.ts +10 -2
- package/extensions/workflows/sandbox.ts +5 -0
- package/package.json +15 -15
- 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/SETUP.md
CHANGED
|
@@ -9,14 +9,14 @@ pi install npm:@tt-a1i/openpi
|
|
|
9
9
|
To inspect the current source before loading it, install directly from GitHub instead:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pi install git:github.com/tt-a1i/
|
|
12
|
+
pi install git:github.com/tt-a1i/openpi
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Pi installs the package dependencies automatically. Restart Pi or run `/reload` after installation.
|
|
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
|
|
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,31 +28,33 @@ 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 package-owned command. With no arguments, the current model explains the configurable areas and
|
|
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
|
-
/
|
|
39
|
-
/
|
|
40
|
-
/
|
|
41
|
-
/
|
|
42
|
-
/
|
|
43
|
-
/
|
|
44
|
-
/
|
|
45
|
-
/
|
|
46
|
-
/
|
|
47
|
-
/
|
|
48
|
-
/
|
|
49
|
-
/
|
|
50
|
-
/
|
|
51
|
-
/
|
|
52
|
-
/
|
|
38
|
+
/openpi-setup
|
|
39
|
+
/openpi-setup 让模型在合适时自主发现并采用 OpenPI 能力
|
|
40
|
+
/openpi-setup 只在我明确要求时加载 OpenPI 能力
|
|
41
|
+
/openpi-setup 开启下一步预测,使用 seal/deepseek-v4-flash,关闭推理
|
|
42
|
+
/openpi-setup 关闭下一步预测
|
|
43
|
+
/openpi-setup workflow 同时跑 16 个 agent,总任务最多 256 个
|
|
44
|
+
/openpi-setup 显示大标题
|
|
45
|
+
/openpi-setup 切换 Footer 为 powerline
|
|
46
|
+
/openpi-setup 用 mono powerline Footer
|
|
47
|
+
/openpi-setup Footer 用 compact
|
|
48
|
+
/openpi-setup Footer 两行:cwd flex model / context cost flex git
|
|
49
|
+
/openpi-setup Footer 只显示 model、thinking、context、cache 和 git
|
|
50
|
+
/openpi-setup 关闭自定义状态栏
|
|
51
|
+
/openpi-setup 编辑后自动跑 npm run format
|
|
52
|
+
/openpi-setup 关闭 post-edit 命令
|
|
53
|
+
/openpi-setup 给 explorer 指定当前 Registry 中可用的模型
|
|
54
|
+
/openpi-setup 清除 explorer 的模型,让它继承父模型
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
Next-action suggestions default to off. Run `/
|
|
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 `/
|
|
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.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -10,7 +10,6 @@ The upstream project is distributed under the MIT License. Its notice states
|
|
|
10
10
|
`Copyright (c) 2026`; the complete license text is included at
|
|
11
11
|
[`extensions/sessions/LICENSE.upstream`](extensions/sessions/LICENSE.upstream).
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
redistribute code beyond rights supplied by an applicable upstream license.
|
|
13
|
+
OpenPI is distributed under the MIT License; see [`LICENSE`](LICENSE). Portions
|
|
14
|
+
identified in this notice retain their original copyright notices and license
|
|
15
|
+
terms. The project-wide MIT license does not replace or remove those notices.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="900" viewBox="0 0 720 900" role="img" aria-labelledby="title desc">
|
|
2
2
|
<title id="title">OpenPI</title>
|
|
3
|
-
<desc id="desc">
|
|
3
|
+
<desc id="desc">OpenPI — small harness, deep extensions, clean context.</desc>
|
|
4
4
|
<defs>
|
|
5
5
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
6
6
|
<stop stop-color="#0d1117"/>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<text x="42" y="28" fill="#a5d6ff" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="18" font-weight="700" letter-spacing="1">PI NATIVE</text>
|
|
22
22
|
|
|
23
23
|
<text x="0" y="122" fill="#f0f6fc" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="66" font-weight="760" letter-spacing="-2">OpenPI</text>
|
|
24
|
-
<text x="0" y="168" fill="#8b949e" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="24">Small harness.
|
|
24
|
+
<text x="0" y="168" fill="#8b949e" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="24">Small harness. Deep extensions. Clean context.</text>
|
|
25
25
|
</g>
|
|
26
26
|
|
|
27
27
|
<g transform="translate(48 250)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="23" font-weight="600">
|
package/assets/readme-hero.svg
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500" role="img" aria-labelledby="title desc">
|
|
2
2
|
<title id="title">OpenPI</title>
|
|
3
|
-
<desc id="desc">
|
|
3
|
+
<desc id="desc">OpenPI — small harness, deep extensions, clean context.</desc>
|
|
4
4
|
|
|
5
5
|
<defs>
|
|
6
6
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
<g>
|
|
35
35
|
<rect width="112" height="34" rx="17" fill="#12233d" stroke="#2f81f7"/>
|
|
36
36
|
<circle cx="18" cy="17" r="5" fill="#3fb950"/>
|
|
37
|
-
<text x="33" y="22" fill="#a5d6ff" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="
|
|
37
|
+
<text x="33" y="22" fill="#a5d6ff" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="14" font-weight="700" letter-spacing="0.7">PI NATIVE</text>
|
|
38
38
|
</g>
|
|
39
39
|
|
|
40
40
|
<text x="0" y="112" fill="#f0f6fc" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="64" font-weight="760" letter-spacing="-2">OpenPI</text>
|
|
41
|
-
<text x="0" y="155" fill="#8b949e" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="
|
|
41
|
+
<text x="0" y="155" fill="#8b949e" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="20">Small harness. Deep extensions. Clean context.</text>
|
|
42
42
|
|
|
43
43
|
<g transform="translate(0 198)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="16">
|
|
44
44
|
<path d="M5 9h14M12 2l7 7-7 7" fill="none" stroke="#58a6ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
<circle cx="22" cy="22" r="5" fill="#f85149"/>
|
|
63
63
|
<circle cx="40" cy="22" r="5" fill="#d29922"/>
|
|
64
64
|
<circle cx="58" cy="22" r="5" fill="#3fb950"/>
|
|
65
|
-
<text x="229" y="27" text-anchor="middle" fill="#7d8590" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="
|
|
65
|
+
<text x="229" y="27" text-anchor="middle" fill="#7d8590" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="15">pi · main</text>
|
|
66
66
|
|
|
67
|
-
<g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="
|
|
67
|
+
<g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="16">
|
|
68
68
|
<text x="24" y="78" fill="#7d8590">$ pi</text>
|
|
69
69
|
<rect x="24" y="92" width="410" height="44" rx="9" fill="#0d1117" stroke="#21262d"/>
|
|
70
70
|
<circle cx="44" cy="114" r="5" fill="#58a6ff"/>
|
|
71
71
|
<text x="59" y="119" fill="#f0f6fc">main agent</text>
|
|
72
|
-
<text x="320" y="119" fill="#7d8590">orchestrating</text>
|
|
72
|
+
<text x="320" y="119" fill="#7d8590" font-size="14">orchestrating</text>
|
|
73
73
|
|
|
74
74
|
<path d="M47 136V155M47 155H379" fill="none" stroke="#30363d" stroke-width="2"/>
|
|
75
75
|
<path d="M90 155V169M229 155V169M379 155V169" stroke="#30363d" stroke-width="2"/>
|
|
@@ -91,18 +91,18 @@
|
|
|
91
91
|
|
|
92
92
|
<rect x="24" y="256" width="410" height="34" rx="8" fill="#0d1117" stroke="#21262d"/>
|
|
93
93
|
<text x="38" y="278" fill="#7d8590">context</text>
|
|
94
|
-
<rect x="
|
|
95
|
-
<rect x="
|
|
94
|
+
<rect x="115" y="268" width="155" height="10" rx="5" fill="#21262d"/>
|
|
95
|
+
<rect x="115" y="268" width="57" height="10" rx="5" fill="#2f81f7"/>
|
|
96
96
|
<text x="285" y="278" fill="#c9d1d9">37%</text>
|
|
97
97
|
<text x="338" y="278" fill="#3fb950">settled</text>
|
|
98
98
|
|
|
99
99
|
<path d="M24 308H434" stroke="#21262d"/>
|
|
100
100
|
<rect x="24" y="318" width="410" height="18" rx="4" fill="#0b1720"/>
|
|
101
|
-
<text x="34" y="332" fill="#39c5cf" font-size="
|
|
101
|
+
<text x="34" y="332" fill="#39c5cf" font-size="15">run tests and open the PR</text>
|
|
102
102
|
</g>
|
|
103
103
|
</g>
|
|
104
104
|
|
|
105
|
-
<g transform="translate(70 444)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="
|
|
105
|
+
<g transform="translate(70 444)" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="14" font-weight="600" letter-spacing="0.25">
|
|
106
106
|
<text fill="#58a6ff">BACKGROUND</text>
|
|
107
107
|
<circle cx="110" cy="-4" r="2" fill="#30363d"/>
|
|
108
108
|
<text x="126" fill="#3fb950">SUBAGENTS</text>
|
|
@@ -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) => {
|