pi-ptc-subagents 2.0.1 → 2.0.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/CHANGELOG.md +20 -0
- package/README.md +102 -249
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.0.2] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **The README leads with what the package is instead of how it works.** The opening now
|
|
13
|
+
names the two capabilities up front — subagent fan-out and programmable tool calling —
|
|
14
|
+
and states the relationship with pi's own `codemode` in the first screen. The surface
|
|
15
|
+
detection deep-dive (three-question table, startup notices, `--print` / `--no-extensions`
|
|
16
|
+
edge cases) moves out of the README to
|
|
17
|
+
[docs/usage/surface.md](./docs/usage/surface.md), which now owns it; the README keeps
|
|
18
|
+
the short version and a pointer. Everything else is tightened, not deleted.
|
|
19
|
+
- **The built-in tool enumeration no longer claims a count pi moved past.** The README used
|
|
20
|
+
to say "all seven built-ins"; pi 1.1.0 ships eight (`powershell` joined), so the copy now
|
|
21
|
+
enumerates the tools this package binds (`read`, `bash`, `edit`, `write`, `grep`, `find`,
|
|
22
|
+
`ls`) without claiming that is pi's full set. Docs only; no code change — note the
|
|
23
|
+
binding list itself is unchanged, and `powershell` is currently not reachable from PTC
|
|
24
|
+
programs even when enabled (tracked separately).
|
|
25
|
+
|
|
26
|
+
[2.0.2]: https://github.com/a1121611810/pi-ptc-subagents/compare/v2.0.1...v2.0.2
|
|
27
|
+
|
|
8
28
|
## [2.0.1] - 2026-10-10
|
|
9
29
|
|
|
10
30
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
# pi-ptc-subagents
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Subagents and programmable tool calling for [pi](https://pi.dev).
|
|
4
|
+
|
|
5
|
+
One package, two capabilities:
|
|
6
|
+
|
|
7
|
+
- **Subagent fan-out** — dispatch a task to a fresh `pi` subprocess running a named agent
|
|
8
|
+
(from `~/.pi/agent/agents/<name>.md` or `.pi/agents/<name>.md`), in isolation. Foreground
|
|
9
|
+
await, or background with a lifecycle you can inspect and stop.
|
|
10
|
+
- **Programmable tool calling** — `ptc_run_code` / `ptc_workflow` run a JS/TS program in a
|
|
11
|
+
worker; the program composes the session's tools as `tools.<name>(args)`, and only the
|
|
12
|
+
program's return value plus its logs come back to the model.
|
|
13
|
+
|
|
14
|
+
pi ships its own programmable tool calling (`codemode` — a QuickJS sandbox that cannot spawn
|
|
15
|
+
processes). This package is the part that can spawn processes — subagents, background tasks,
|
|
16
|
+
a real Node runtime inside programs — and it composes with `codemode` instead of replacing
|
|
17
|
+
it. On a session where pi's `codemode` orchestrates, this package registers underneath it as
|
|
18
|
+
the execution layer ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)); the deep
|
|
19
|
+
dive lives in [docs/usage/surface.md](./docs/usage/surface.md).
|
|
6
20
|
|
|
7
21
|
**Source is open.** This repository is public and the source is here — `dist/` on npm is the
|
|
8
22
|
compiled form of what you read below. Contributions go through pull requests: see
|
|
@@ -10,57 +24,44 @@ compiled form of what you read below. Contributions go through pull requests: se
|
|
|
10
24
|
[SECURITY.md](./SECURITY.md) before reporting anything. Releases are cut from `main` by the
|
|
11
25
|
maintainer only; if you find something you think needs a release, open an issue and say so.
|
|
12
26
|
|
|
13
|
-
## Status
|
|
14
|
-
|
|
15
|
-
Functional and actively used: `ptc_run_code` and `ptc_workflow` are registered and run
|
|
16
|
-
programs through the same tested worker machinery (dispatcher, wire protocol,
|
|
17
|
-
budgets, built-in bindings). The implementation is written clean-room from
|
|
18
|
-
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) PTC
|
|
19
|
-
behaviour — see [ADR-0002](./docs/adr/0002-source-strategy.md) for how that boundary is
|
|
20
|
-
kept, and [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for the attribution that
|
|
21
|
-
follows from it.
|
|
22
|
-
|
|
23
27
|
## Install
|
|
24
28
|
|
|
25
29
|
```bash
|
|
26
30
|
pi install npm:pi-ptc-subagents
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
From a local checkout: `pnpm install && pnpm run build && pi install /abs/path/to/this/repo
|
|
33
|
+
From a local checkout: `pnpm install && pnpm run build && pi install /abs/path/to/this/repo`.
|
|
30
34
|
|
|
31
|
-
pi reads the `pi.extensions` manifest field
|
|
32
|
-
|
|
35
|
+
pi reads the `pi.extensions` manifest field — install it and the extension is on for the next
|
|
36
|
+
pi startup, no extra setup.
|
|
33
37
|
|
|
34
38
|
## Tools
|
|
35
39
|
|
|
36
|
-
- `
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
[
|
|
49
|
-
- `ptc_task_list` / `ptc_task_output` / `ptc_task_stop` — manage background
|
|
50
|
-
dispatches (see [Background dispatch](#background-dispatch)). They stay
|
|
51
|
-
available when PTC mode is off.
|
|
40
|
+
- `ptc_subagent` — the top-level subagent face: dispatch a fresh `pi` subprocess for a task
|
|
41
|
+
without writing a program. Registered only when the detected surface is `subagents`
|
|
42
|
+
([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)); on `full` the same
|
|
43
|
+
capability is the `pi.dispatch` binding inside a program.
|
|
44
|
+
- `ptc_run_code` — run a JS/TS program that composes tool calls; the program reaches the
|
|
45
|
+
session's enabled built-in tools as `tools.read(...)`, `tools.write(...)`, etc.
|
|
46
|
+
(`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`), plus `tools["pi.dispatch"]`;
|
|
47
|
+
its return value and `console.log` output are reported back.
|
|
48
|
+
- `ptc_workflow` — structured variant with `meta` + plain-JSON `args`, plus the workflow
|
|
49
|
+
helpers (`log`, `phase`, `parallel`, `pipeline`). There is no `agent()` helper on either
|
|
50
|
+
surface.
|
|
51
|
+
- `ptc_task_list` / `ptc_task_output` / `ptc_task_stop` — manage background dispatches
|
|
52
|
+
(see [Background dispatch](#background-dispatch)). They stay available when PTC mode is off.
|
|
52
53
|
|
|
53
54
|
Long output follows pi's own truncation contract ([ADR-0015](./docs/adr/0015-pi-truncation-contract.md)):
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
the next program can `tools.read`; the collapsed row then shows `truncated` in its meta.
|
|
55
|
+
the text block keeps the tail (50 KB / 2000 lines), the untruncated text is written to a temp
|
|
56
|
+
file the next program can `tools.read`, and the collapsed row shows `truncated` in its meta.
|
|
58
57
|
|
|
59
58
|
## Dispatch (fan-out to per-call pi subprocesses)
|
|
60
59
|
|
|
61
|
-
PTC programs can spawn a fresh `pi` subprocess per call via the **`pi.dispatch(...)`** binding
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
PTC programs can spawn a fresh `pi` subprocess per call via the **`pi.dispatch(...)`** binding
|
|
61
|
+
([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md)) — the child loads the named agent's
|
|
62
|
+
markdown, runs that agent's tool set and system prompt in isolation, and returns a structured
|
|
63
|
+
result. Bindings live in the one `tools` table — there is no `pi` global in the worker — so
|
|
64
|
+
the binding is called as `tools["pi.dispatch"]({ … })` with a single object argument.
|
|
64
65
|
|
|
65
66
|
```ts
|
|
66
67
|
// inside a ptc_run_code program
|
|
@@ -76,7 +77,9 @@ const result = await tools["pi.dispatch"]({
|
|
|
76
77
|
// result.exitCode, result.durationMs, result.stderr?, result.errorMessage?
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
**Fan out in parallel** with the rest of PTC's tools — dispatch is a binding, not a
|
|
80
|
+
**Fan out in parallel** with the rest of PTC's tools — dispatch is a binding, not a
|
|
81
|
+
model-visible lifecycle tool, so the dispatcher handles concurrency the same way it does for
|
|
82
|
+
any other tool call:
|
|
80
83
|
|
|
81
84
|
```ts
|
|
82
85
|
const [read, scoutA, scoutB] = await Promise.all([
|
|
@@ -86,62 +89,34 @@ const [read, scoutA, scoutB] = await Promise.all([
|
|
|
86
89
|
]);
|
|
87
90
|
```
|
|
88
91
|
|
|
89
|
-
**The child report.** A dispatched child returns more than prose
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
} else {
|
|
98
|
-
for (const f of r.report?.findings ?? []) console.log(f.what, "←", f.evidence);
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
`reportChannel` is **always present** — `"tool"`, `"prompt-json"` or `"none"` — because a degradation a caller cannot see is a silent failure, and "ran but did not comply" must not read as "returned nothing". `ptc_subagent` renders the same report into the text the model reads, bounded at 20 findings with the withheld count stated in-band.
|
|
103
|
-
|
|
104
|
-
The contract is **on by default**. An agent opts out with one line of frontmatter, `childReport: false`, and then its channel reads `"opted-out"` — nobody was asked, which is a different claim from having been asked and ignored.
|
|
92
|
+
**The child report.** A dispatched child returns more than prose: under the report contract
|
|
93
|
+
([ADR-0032](./docs/adr/0032-child-report.md)) it hands back a `summary`, `findings` each
|
|
94
|
+
carrying independent evidence, `files_touched`, and token usage the host measured. The
|
|
95
|
+
result names the channel that delivered it — `"tool"`, `"prompt-json"`, or `"none"` — and
|
|
96
|
+
`"none"` means the child ran but did not comply, which must not read as "returned nothing".
|
|
97
|
+
An agent opts out with one frontmatter line, `childReport: false`, and its channel reads
|
|
98
|
+
`"opted-out"`. `ptc_subagent` renders the same report into the text the model reads, bounded
|
|
99
|
+
at 20 findings with the withheld count stated in-band.
|
|
105
100
|
|
|
106
101
|
**Bounded.** Three knobs keep fan-out from running away:
|
|
107
102
|
|
|
108
|
-
- `PtcConfig.dispatchConcurrency` (default **8**) —
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
neither is a rounding difference: two programs running concurrently in one session now share 8
|
|
117
|
-
rather than 8 each, and a program sharing a session with eight live background children can be
|
|
118
|
-
refused **every** foreground slot. The live control is `createBackgroundTaskRuntime({ concurrency })`,
|
|
119
|
-
the call that builds that session counter, and the value it is given is `PtcConfig.dispatchConcurrency`.
|
|
120
|
-
It is not a background-only knob: changing it changes how many foreground children a whole session
|
|
121
|
-
can have in flight.
|
|
122
|
-
|
|
123
|
-
The `dispatchConcurrency` a caller passes to `runPtcProgram({ config })` sizes the
|
|
124
|
-
dispatcher's own per-run counter, and that counter is only reached when no session counter is
|
|
125
|
-
supplied (`dispatcher.ts` hands the binding `options.dispatchDeps?.slots ?? dispatchSlots`). In a
|
|
126
|
-
pi session a session counter always is, so the per-run one is not what enforces the cap you are
|
|
127
|
-
looking at.
|
|
128
|
-
|
|
129
|
-
**Opt out.** Pass an explicit binding subset to `createBuiltinBindings` to opt out — the parallel binding is mixed in only when the caller accepts the default set:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
// in a hypothetical runner that wants to keep reads-only:
|
|
133
|
-
createBuiltinBindings({ cwd: "/abs/path", names: ["read", "grep"] });
|
|
134
|
-
// `pi.dispatch` is NOT in the resulting `tools` table.
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
**Not a subagent.** The term _subagent_ is overloaded in this field (DSH's `subagent` is a different thing; pi's `examples/extensions/subagent/` extension is also a different thing). pi-ptc uses _parallel binding_ and _concurrent tool call_ throughout; see `CONTEXT.md` for the canonical terms.
|
|
103
|
+
- `PtcConfig.dispatchConcurrency` (default **8**) — one counter per pi session, spent by
|
|
104
|
+
foreground dispatches, the `ptc_subagent` front, and live background children. A call over
|
|
105
|
+
the cap resolves `{ status: "rejected", errorMessage: "dispatch concurrency limit reached" }`
|
|
106
|
+
instead of queueing.
|
|
107
|
+
- `PtcConfig.maxDispatchDepth` (default **3**) — recursion bound; the child sees a
|
|
108
|
+
`<pi-ptc-context depth="N" max-depth="M">` hint so it can budget its own recursion.
|
|
109
|
+
- `signal` — when the parent run is cancelled, every in-flight child gets `SIGTERM` then
|
|
110
|
+
`SIGKILL` after a 5-second grace window.
|
|
138
111
|
|
|
139
112
|
### Background dispatch
|
|
140
113
|
|
|
141
|
-
Foreground `pi.dispatch` blocks the program until the child exits. Pass `background: true` to
|
|
114
|
+
Foreground `pi.dispatch` blocks the program until the child exits. Pass `background: true` to
|
|
115
|
+
spawn the child and return immediately with a `DispatchHandle`
|
|
116
|
+
([ADR-0022](./docs/adr/0022-background-dispatch.md)); the child outlives both the program and
|
|
117
|
+
the turn:
|
|
142
118
|
|
|
143
119
|
```ts
|
|
144
|
-
// inside a ptc_run_code program — bindings are reached as tools["<name>"]
|
|
145
120
|
const handle = await tools["pi.dispatch"]({
|
|
146
121
|
agent: "scout",
|
|
147
122
|
task: "audit the auth code",
|
|
@@ -151,15 +126,19 @@ const handle = await tools["pi.dispatch"]({
|
|
|
151
126
|
// handle: { taskId: "01J…", label: "auth audit", status: "running" }
|
|
152
127
|
```
|
|
153
128
|
|
|
154
|
-
|
|
129
|
+
A detached pump drives the task's lifecycle (`running` -> `succeeded` / `failed` / `canceled`
|
|
130
|
+
/ `lost`). The model observes it with three always-on tools — they survive `/ptc off` and are
|
|
131
|
+
registered on every surface:
|
|
155
132
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
- `ptc_task_output({ taskId, sinceBytes? })` — read a task's captured output, tail-truncated to pi's 50 KB / 2000-line contract (ADR-0015).
|
|
133
|
+
- `ptc_task_list({ status?, limit? })` — this session's tasks, newest first (default limit 100).
|
|
134
|
+
- `ptc_task_output({ taskId, sinceBytes? })` — a task's captured output, tail-truncated to the
|
|
135
|
+
50 KB / 2000-line contract (ADR-0015).
|
|
160
136
|
- `ptc_task_stop({ taskId, reason? })` — ask a running task to stop.
|
|
161
137
|
|
|
162
|
-
Background tasks count against the same `dispatchConcurrency`
|
|
138
|
+
Background tasks count against the same `dispatchConcurrency` for their whole lifetime and
|
|
139
|
+
share the `maxDispatchDepth` bound. A background task is owned by the dispatching pi process
|
|
140
|
+
(ADR-0023): it survives programs, turns, and `/ptc off`, and ends when that session ends. Full
|
|
141
|
+
guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md).
|
|
163
142
|
|
|
164
143
|
## TUI rendering
|
|
165
144
|
|
|
@@ -176,167 +155,41 @@ PTC Find AssistantMessageComponent instantiations
|
|
|
176
155
|
└─ totalLines: 47
|
|
177
156
|
```
|
|
178
157
|
|
|
179
|
-
The
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`failed: <reason>` in red. The run's countable facts — output lines, workflow phases, attached
|
|
186
|
-
images, warnings, duration — stay pinned to the right edge of the area's first row. Nothing is ever
|
|
187
|
-
printed as escaped JSON. Expanding a row (ctrl+e) adds the code head, phase roll-up, `console.log`
|
|
188
|
-
output and plan-drift warnings, each block labelled and capped. `renderShell` stays at pi's default,
|
|
189
|
-
so these rows keep the same box and colors as the built-in tools.
|
|
190
|
-
|
|
191
|
-
The copy above is the human's. The text block the **model** reads is a separate contract with
|
|
192
|
-
separate bounds ([ADR-0012](./docs/adr/0012-model-facing-result-text.md)): a completion value whose
|
|
193
|
-
compact form fits in 100 characters stays on one line, and every line of the assembled block is
|
|
194
|
-
capped at 200 characters with a trailing `…`. Those are not the numbers above, and they are not
|
|
195
|
-
variants of them. **4 / 6 / 120** bound the on-screen tree — depth, children per container,
|
|
196
|
-
characters per row, aligned by visible width — because they serve the eye; **100 / 200** bound the
|
|
197
|
-
model's copy because they serve what the model has to read. Neither set derives from the other, so
|
|
198
|
-
moving 200 to 120 so they "match" is a behaviour change that needs its own ADR, not an edit to a
|
|
199
|
-
number on this page.
|
|
158
|
+
The completion value renders as a **value tree** — one row per property, nested containers
|
|
159
|
+
behind `├─` / `└─` / `│`, capped at 4 levels deep / 6 children per container / 120 characters
|
|
160
|
+
per row, with the withheld amount stated in-band. A scalar is one line (`→ 47`, `done`, or
|
|
161
|
+
`failed: <reason>` in red). The text block the model reads is a separate contract with its own
|
|
162
|
+
bounds ([ADR-0012](./docs/adr/0012-model-facing-result-text.md)): a compact value under 100
|
|
163
|
+
characters stays on one line, every line caps at 200. Nothing is ever printed as escaped JSON.
|
|
200
164
|
|
|
201
165
|
## Images
|
|
202
166
|
|
|
203
|
-
An image read _inside_ a program — `await tools.read({ path: "shot.png" })` — is attached to
|
|
204
|
-
tool result as a real image block, so the model sees the picture instead of a marker
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
call. The collapsed row's meta shows the count (`· 1 image`), so the volume is visible without being
|
|
209
|
-
policed. The program receives the image either way.
|
|
167
|
+
An image read _inside_ a program — `await tools.read({ path: "shot.png" })` — is attached to
|
|
168
|
+
the PTC tool result as a real image block, so the model sees the picture instead of a marker
|
|
169
|
+
string or a wall of base64 ([ADR-0014](./docs/adr/0014-image-hoisting.md)). Nothing is capped
|
|
170
|
+
or deduped: every image the program's tool calls produced is attached, in call order; the
|
|
171
|
+
collapsed row's meta shows the count (`· 1 image`).
|
|
210
172
|
|
|
211
173
|
## PTC default mode
|
|
212
174
|
|
|
213
|
-
On a TUI start
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{ "defaultMode": false }
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Choosing the surface.** `defaultMode` decides whether the session _enters_ PTC mode. Which
|
|
235
|
-
model-facing tools this package registers is **detected, not configured** — there is no setting for
|
|
236
|
-
it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)):
|
|
237
|
-
|
|
238
|
-
- `subagents` — `ptc_subagent` plus the three `ptc_task_*` tools, with pi's own `codemode`
|
|
239
|
-
doing the orchestration; warns at startup when `codemode` is not in the active tool set and
|
|
240
|
-
`ptc_run_code` is not declared to the model either (which on a pi below 0.99.0 it is, since
|
|
241
|
-
`exposure` does not exist there and the pair falls back to being model-visible).
|
|
242
|
-
`ptc_run_code` / `ptc_workflow` are registered here too, but at `codemode` reach: a `codemode`
|
|
243
|
-
script can call them, and the model is not shown them. That is what puts `pi.dispatch` and
|
|
244
|
-
background tasks underneath `codemode`, whose sandbox cannot spawn a process itself.
|
|
245
|
-
- `full` — the rest: `ptc_run_code` / `ptc_workflow` plus the three `ptc_task_*` tools. Also the
|
|
246
|
-
answer to every "the probe could not tell" case.
|
|
247
|
-
|
|
248
|
-
**To keep this package out of your sessions, use pi, not this package.** Run `pi config` and
|
|
249
|
-
disable this package's extensions there. Measured on pi 1.1.0 against this package's own
|
|
250
|
-
`dist/index.js`: a `packages` entry whose `extensions` is `[]` or `["!dist/index.js"]` is not
|
|
251
|
-
loaded at all, while `["+dist/index.js"]` and an omitted key are. pi does this **without loading
|
|
252
|
-
the extension**, which no value of a key this package reads could achieve — reading the key
|
|
253
|
-
requires having run the code that reads it.
|
|
254
|
-
|
|
255
|
-
> In the **project-level** `.pi/settings.json`, write `"extensions": ["!dist/index.js"]` rather than
|
|
256
|
-
> `"extensions": []`. The two settings files are resolved by two different functions in pi
|
|
257
|
-
> (`package-manager.js:1850`): the personal-level path reads `[]` as "load nothing", the
|
|
258
|
-
> project-level path reads it as an empty delta, which means "no change".
|
|
259
|
-
|
|
260
|
-
**If you had `surfaceMode` in `ptc.json`, it is no longer read** — including `"off"`, so upgrading
|
|
261
|
-
brings this package back. At session start you get one `warning` naming the file and pointing here.
|
|
262
|
-
Delete the key to silence it.
|
|
263
|
-
|
|
264
|
-
**The surface is detected, and it follows three questions, not one**
|
|
265
|
-
([ADR-0026](./docs/adr/0026-surface-default-is-detected.md),
|
|
266
|
-
[ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md),
|
|
267
|
-
[ADR-0029](./docs/adr/0029-surface-follows-codemode-activation.md)):
|
|
268
|
-
|
|
269
|
-
| does this pi ship `codemode`? | will pi load it? | can the model call it? | surface |
|
|
270
|
-
| ----------------------------- | ---------------------------------------------- | --------------------------------------------- | ----------- |
|
|
271
|
-
| yes | yes (default, or `+builtin:codemode`) | yes (`--tools …,codemode`, or `defaultTools`) | `subagents` |
|
|
272
|
-
| yes | yes (default, or `+builtin:codemode`) | no — **the default on a stock install** | `full` |
|
|
273
|
-
| yes | no (`-builtin:codemode`, or `--no-extensions`) | — | `full` |
|
|
274
|
-
| no | — | — | `full` |
|
|
275
|
-
|
|
276
|
-
The third column is the one that decides most sessions, and it is why the answer is `full` on a pi
|
|
277
|
-
that has never been configured. pi ships `codemode` and loads it by default, but registers it
|
|
278
|
-
**inactive** (`defaultActive: false`) — it joins the model's tool list only when a loadout names it.
|
|
279
|
-
Handing orchestration to a tool the model cannot call is the failure this avoids, so `subagents` is
|
|
280
|
-
chosen only on positive evidence.
|
|
281
|
-
|
|
282
|
-
A probe that cannot answer falls back to `full` — the safe direction, since `subagents` as a
|
|
283
|
-
failure mode would take away the orchestration tool the session was relying on. There is no key
|
|
284
|
-
that overrides this, by design ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)).
|
|
285
|
-
|
|
286
|
-
> **To use the `subagents` surface, put `codemode` in your tool list.** Without that you get
|
|
287
|
-
> `ptc_run_code` / `ptc_workflow` and no startup warning, which is the correct answer for a session
|
|
288
|
-
> that never asked for delegation:
|
|
289
|
-
>
|
|
290
|
-
> ```jsonc
|
|
291
|
-
> // ~/.pi/agent/settings.json
|
|
292
|
-
> { "defaultTools": ["read", "bash", "edit", "write", "+codemode"] }
|
|
293
|
-
> ```
|
|
294
|
-
>
|
|
295
|
-
> or per launch, `pi --tools read,bash,edit,write,codemode`. A list made only of modifiers starts
|
|
296
|
-
> from pi's four defaults, so `{ "defaultTools": ["+codemode"] }` means the same thing.
|
|
297
|
-
|
|
298
|
-
> Turning pi's `codemode` **off** — `"extensions": ["-builtin:codemode"]`, or launching with
|
|
299
|
-
> `--no-extensions` — brings the PTC surfaces back on its own. Before ADR-0027 it did not: the
|
|
300
|
-
> detection asked only whether the extension directory exists, so a pi told not to load it still
|
|
301
|
-
> counted as an orchestrator and you got `ptc_subagent` with nothing to compose with. The switch is
|
|
302
|
-
> read from the same three places pi reads it — the command line, `<cwd>/.pi/settings.json`, and
|
|
303
|
-
> `<agentDir>/settings.json` — in the same order. The activation probe does **not** read the
|
|
304
|
-
> settings at all: it asks pi for its own tool loadout at session start, by which time pi has
|
|
305
|
-
> already applied the command line _and_ its project-trust decision. That matters for a project
|
|
306
|
-
> you have not approved — its `defaultTools` is not evidence of anything, because pi is not
|
|
307
|
-
> reading it either ([ADR-0035](./docs/adr/0035-ask-pi-for-the-loadout.md)).
|
|
308
|
-
|
|
309
|
-
**A detection you cannot see is the failure this design has**, so the result is reported. The
|
|
310
|
-
outcome is issued through the TUI notification channel at session start — how the probe came out
|
|
311
|
-
and which surface therefore is — but only when the probe could not answer. A pi that ships
|
|
312
|
-
`codemode`, loads it, and has it in the tool list is the expected case and says nothing. A second
|
|
313
|
-
notice is issued when the probe and pi's own tool registry disagree, which is the case the probe
|
|
314
|
-
structurally cannot see: it walks the filesystem, so under `--exclude-tools codemode` it answers
|
|
315
|
-
`present` for a tool this session does not have (and the mirror: a restructured `dist` answers
|
|
316
|
-
`not-found` for one pi plainly registers). A third notice covers ADR-0027: a settings file that
|
|
317
|
-
could not be read. What is **not** established is that either line actually paints in a real pi
|
|
318
|
-
TUI: a pty capture at review time showed neither the notice nor a control marker, and a TUI quits
|
|
319
|
-
on stdin EOF before a toast renders, so that is an unmeasured end to end rather than a broken one.
|
|
320
|
-
No test in this repository observes a notice through a real TUI.
|
|
321
|
-
|
|
322
|
-
**On a `--print` session, none of it prints.** `ui.notify` is the TUI channel; measured across
|
|
323
|
-
three `--print` runs that each emit one of these notices, stdout and stderr received **0 bytes**
|
|
324
|
-
each. That makes this page the only channel on which a `--print` user learns why they got the
|
|
325
|
-
surface they got.
|
|
326
|
-
|
|
327
|
-
**Where it does not run.** Print / JSON / RPC sessions are left exactly as launched, and so is a
|
|
328
|
-
session started with an explicit tool restriction (`--tools`, `--exclude-tools`,
|
|
329
|
-
`--no-builtin-tools`, `--no-extensions`) — the extension does not override what you asked for.
|
|
330
|
-
`--no-extensions` does, however, change the **detected surface**: pi's own `codemode` is a built-in
|
|
331
|
-
extension, so turning extensions off means it will not load, and ADR-0027's table resolves to
|
|
332
|
-
`full` — you keep `ptc_run_code` / `ptc_workflow` rather than a `ptc_subagent` with
|
|
333
|
-
nothing to compose with. If another extension changes the tool set while the mode is on, the mode
|
|
334
|
-
yields and tells you.
|
|
335
|
-
|
|
336
|
-
**The important consequence:** in a TUI session, bindings come from the loadout recorded _before_
|
|
337
|
-
the mode narrowed it. That is what keeps `tools.read(…)` working — and it is why a `--tools`
|
|
338
|
-
restriction still holds: the snapshot is read from `pi.getActiveTools()`, so it can never contain
|
|
339
|
-
tools your session was not launched with.
|
|
175
|
+
On a TUI start the session narrows its tool loadout so the built-in tools are reachable only
|
|
176
|
+
_from inside a program_: the model calls `ptc_run_code` / `ptc_workflow` (or `ptc_subagent`)
|
|
177
|
+
and reaches `read` / `bash` / `edit` / `write` / `grep` / `find` / `ls` through
|
|
178
|
+
`tools.<name>(args)` inside it. Tools contributed by _other_ extensions (`web_search`, `todo`,
|
|
179
|
+
…) stay directly callable. Turn it off for one session with `/ptc off` (also `/ptc on`, `/ptc`
|
|
180
|
+
for status), permanently with `{ "defaultMode": false }` in `~/.pi/agent/ptc.json`.
|
|
181
|
+
|
|
182
|
+
Which model-facing tools this package registers is **detected, not configured** — there is no
|
|
183
|
+
setting for it ([ADR-0034](./docs/adr/0034-surface-is-detected-not-set.md)). The short version:
|
|
184
|
+
a pi whose `codemode` is loaded _and_ callable by the model gets the `subagents` surface
|
|
185
|
+
(`ptc_subagent` on top; the program pair underneath at `codemode` reach); every other pi gets
|
|
186
|
+
`full` (`ptc_run_code` / `ptc_workflow` model-visible). To use the `subagents` surface, put
|
|
187
|
+
`codemode` in your tool list (`pi --tools read,bash,edit,write,codemode`, or `"defaultTools":
|
|
188
|
+
["read", "bash", "edit", "write", "+codemode"]`). To keep this package out of your sessions
|
|
189
|
+
entirely, run `pi config` and disable its extensions there.
|
|
190
|
+
|
|
191
|
+
The full detection table, startup notices, and edge cases (`--print`, `--no-extensions`,
|
|
192
|
+
untrusted projects) live in [docs/usage/surface.md](./docs/usage/surface.md).
|
|
340
193
|
|
|
341
194
|
## Trust posture (read me)
|
|
342
195
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-ptc-subagents",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Programmable tool calling (PTC) and subagent fan-out for pi — the model writes JS/TS programs that call pi's tools, or fans out to a fresh pi subprocess per subagent task",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|