pi-ptc-subagents 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +215 -0
- package/README.md +91 -4
- package/dist/index.d.ts +181 -11
- package/dist/index.js +41 -37
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,221 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.3.0] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A failing `bash` binding no longer resolves as a success on pi 1.0.0.** pi 1.0.0 added a
|
|
15
|
+
non-throwing failure channel — `AgentToolResult.isError`: "Report a failure without throwing. The
|
|
16
|
+
model sees `content` as an error result, like a thrown error" — and `bash` moved its non-zero exit
|
|
17
|
+
onto it, so `tools.bash({ command: "exit 3" })` inside a PTC program **resolved** where on 0.86.1
|
|
18
|
+
it threw. The binding wrapper forwarded only `{ content, details }`, so `isError` was dropped at
|
|
19
|
+
the boundary and the failure crossed into the program as a successful resolution whose only tell
|
|
20
|
+
was a sentence at the end of its stdout. The binding layer now translates `isError` back into the
|
|
21
|
+
rejection ADR-0024's contract already promises, which is what keeps a failing call from being
|
|
22
|
+
silent.
|
|
23
|
+
- **Disabling pi's `codemode` now brings the PTC surfaces back.** The detected surface asked one
|
|
24
|
+
question — does this pi ship a `codemode` extension directory — and since pi 0.99.0 that is no
|
|
25
|
+
longer the same as the one that matters. A user who put `"extensions": ["-builtin:codemode"]` in
|
|
26
|
+
their settings, or launched with `--no-extensions`, turned the orchestrator off and this package
|
|
27
|
+
kept handing the orchestration to it: `ptc_run_code` and `ptc_workflow` stayed unregistered and
|
|
28
|
+
the session was left with `ptc_subagent` and nothing to compose with. Nothing errored; two tools
|
|
29
|
+
were simply missing.
|
|
30
|
+
|
|
31
|
+
A second probe now reads the **switch** — whether pi will actually load the extension — from the
|
|
32
|
+
same three places pi reads it and in the same order (command line, `<cwd>/.pi/settings.json`,
|
|
33
|
+
`<agentDir>/settings.json`), and the detected default follows both questions:
|
|
34
|
+
|
|
35
|
+
| ships `codemode`? | will load it? | surface |
|
|
36
|
+
| ----------------- | ------------------------------------------- | ----------- |
|
|
37
|
+
| yes | yes (default, or `+builtin:codemode`) | `subagents` |
|
|
38
|
+
| yes | no (`-builtin:codemode`, `--no-extensions`) | `full` |
|
|
39
|
+
| no | — | `full` |
|
|
40
|
+
|
|
41
|
+
An explicit `surfaceMode` still wins; a settings file that cannot be read as a JSON object still
|
|
42
|
+
falls back to the next source rather than half-applying. Two notices are added at session start:
|
|
43
|
+
one naming an unreadable settings file, one reporting that a pinned `surfaceMode` disagrees with
|
|
44
|
+
the table. [ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md)
|
|
45
|
+
|
|
46
|
+
Verified on a real 1.0.0 install for the three reachable cells, asserting on the registered tool
|
|
47
|
+
set rather than on the resolver: `-builtin:codemode` → `ptc_run_code` + `ptc_workflow`; no switch
|
|
48
|
+
entry → `ptc_subagent`; `+builtin:codemode` → `ptc_subagent`. The fourth cell (no `codemode` on
|
|
49
|
+
disk at all) is not reachable on a machine with 1.0 installed and is covered by unit tests only.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **The four model-facing tools declare a structured result, so pi's `codemode` can consume them.**
|
|
54
|
+
A script calling a tool used to get back prose: `ptc_subagent` returned
|
|
55
|
+
`"Started background task 01J…"` with the id embedded, and the `ptc_task_*` tools returned
|
|
56
|
+
newline-joined lines to re-parse. `ptc_subagent` is the acute case — it is the only way a
|
|
57
|
+
`subagents`-surface session can start a subagent at all, because pi's QuickJS sandbox has no file
|
|
58
|
+
system, no network and no `child_process`.
|
|
59
|
+
|
|
60
|
+
All four now declare an `outputSchema` and return a matching `structuredContent`, which codemode
|
|
61
|
+
scripts receive instead of the text. The shape is a lean projection rather than a mirror of
|
|
62
|
+
`details`: `ptc_task_list` mirroring `TaskRecord[]` would push ~200 KB of per-record
|
|
63
|
+
`outputPreview` into a sandbox whose purpose is to keep intermediate data away from the model.
|
|
64
|
+
`content` and `details` are byte-identical to before on every path, and the model still sees
|
|
65
|
+
exactly the same text — `structuredContent` is documented as "not sent to the model".
|
|
66
|
+
[ADR-0028](./docs/adr/0028-structured-results-for-codemode.md); usage in
|
|
67
|
+
[Structured results for codemode](./docs/usage/structured-results.md).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- **The codemode switch now mirrors pi's own matchers instead of approximating them.** Reading the
|
|
72
|
+
`!` bucket by comparing a pattern's literal prefix was the wrong SHAPE of fix: the bucket is a
|
|
73
|
+
continuum (globs, character classes, extglobs, nested negations, backslash escapes, multi-segment
|
|
74
|
+
paths), so every review round found one more member — four costful defects in four rounds, and a
|
|
75
|
+
412-case sweep of the prefix version still had 24 divergences. A case list cannot be finished.
|
|
76
|
+
`resolveCodemodeSwitch` now calls the same `minimatch` pi does, through the same
|
|
77
|
+
`matchesAnyPattern` / `normalizeExactPattern` helpers and with pi's own `baseDir` per scope, which
|
|
78
|
+
adds one small runtime dependency (`minimatch ^10.2.6`, the version pi itself pins) and **shortens**
|
|
79
|
+
the code. A 192-case sweep of the mirror has zero divergences in either direction, and all eight
|
|
80
|
+
reachable cells agree with a real pi 1.0.0 end to end.
|
|
81
|
+
- **The dev toolchain now compiles against pi 1.0.0** rather than 0.86.1 / 0.87.0, so the type check
|
|
82
|
+
and the whole suite run against the pi this package is actually used with. `peerDependencies`
|
|
83
|
+
already declared `>=0.86.0`; that claim had no evidence behind it until now. Two consequences of
|
|
84
|
+
the bump are fixed in the same change: pi narrowed a tool's fifth `execute` parameter from
|
|
85
|
+
`ExtensionContext` to `ExtensionToolContext` (which adds `tools` and `executeTool`), and the
|
|
86
|
+
`isError` channel above.
|
|
87
|
+
|
|
88
|
+
## [1.2.1] - 2026-09-30
|
|
89
|
+
|
|
90
|
+
### Fixed
|
|
91
|
+
|
|
92
|
+
- **The `ptc_subagent` tests no longer depend on a pi agent being registered.**
|
|
93
|
+
Five tests reached for pi's `__smoke_echo` agent, which does not resolve on every pi build,
|
|
94
|
+
so the suite was green on one platform and red on the release runner. The fixture is now
|
|
95
|
+
written by the test into `<cwd>/.pi/agents`, the path the agent resolver actually reads at
|
|
96
|
+
project scope, and every dispatch asks for project scope explicitly rather than inheriting
|
|
97
|
+
the `user` default, which points at the real `~/.pi`. Verified with an empty `HOME`: the whole
|
|
98
|
+
suite is green with no ambient agent state at all.
|
|
99
|
+
|
|
100
|
+
No product code changed in this release.
|
|
101
|
+
|
|
102
|
+
## [1.2.0] - 2026-09-30
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- **The surface default is detected from the pi that loaded us (ADR-0026).** With no
|
|
107
|
+
`surfaceMode` set, a pi that ships its own `codemode` resolves to `subagents` and a pi that
|
|
108
|
+
does not resolves to `full`. Setting the key always wins, and a probe that cannot answer
|
|
109
|
+
falls back to `full` rather than guessing.
|
|
110
|
+
|
|
111
|
+
This is the one behaviour change that is not invisible on upgrade: on pi 0.99.1 or newer, a
|
|
112
|
+
user who has never set `surfaceMode` moves from `full` to `subagents`. `codemode` ships
|
|
113
|
+
inactive (`defaultActive: false`), so such a session has a subagent front and no active
|
|
114
|
+
orchestrator until codemode is added to the tool list -- the startup warning says so. Set
|
|
115
|
+
`{ "surfaceMode": "full" }` to keep today's tools.
|
|
116
|
+
|
|
117
|
+
Detection is a filesystem probe over `process.argv[1]`, because pi's own tool listing is
|
|
118
|
+
unavailable at factory time: `getAllTools()` is a `notInitialized` stub until `bindCore`
|
|
119
|
+
runs, and a throwing factory makes the extension fail to load rather than return an empty
|
|
120
|
+
list.
|
|
121
|
+
|
|
122
|
+
A detection the user cannot see is the one failure this design has, so the outcome is
|
|
123
|
+
reported: at session start, through the TUI notification channel, a session with no
|
|
124
|
+
`surfaceMode` set is told how the probe came out whenever it could not answer, and which
|
|
125
|
+
surface the default therefore is. The expected case — a pi that ships `codemode`, detected as
|
|
126
|
+
`subagents` — stays silent. A second notice covers the case the probe structurally cannot
|
|
127
|
+
see: it walks the filesystem, so under `--no-extensions` or `--exclude-tools codemode` it
|
|
128
|
+
answers `present` for a tool the session does not have. `session_start` cross-checks the
|
|
129
|
+
probe against pi's own `getAllTools()` and reports a disagreement in either direction.
|
|
130
|
+
|
|
131
|
+
Both notices are `ui.notify`, which is TUI-only — and that gap is **new here**, not inherited:
|
|
132
|
+
measured, three `--print` runs that each emit one produced 0 bytes on stdout and 0 on
|
|
133
|
+
stderr. The README is the only channel on which a `--print` user learns why they got the
|
|
134
|
+
surface they got. What is established is that the outcome is issued through the documented
|
|
135
|
+
TUI channel; no test observes either notice end to end through a real pi TUI.
|
|
136
|
+
|
|
137
|
+
- **The model-facing surface is now a setting, and a subagent can be started
|
|
138
|
+
without writing a program (ADR-0025).** A new `surfaceMode` key in
|
|
139
|
+
`~/.pi/agent/ptc.json` decides what this extension registers: `full`
|
|
140
|
+
(today's behaviour, and the detected default on a pi without `codemode`)
|
|
141
|
+
registers `ptc_run_code`, `ptc_workflow` and the `ptc_task_*` trio;
|
|
142
|
+
`subagents` registers a new top-level `ptc_subagent`
|
|
143
|
+
plus the `ptc_task_*` trio and leaves orchestration to pi's own `codemode`;
|
|
144
|
+
`off` registers nothing at all, so the session is stock pi. The new tool takes
|
|
145
|
+
the same arguments as the `pi.dispatch` binding and calls the same dispatcher,
|
|
146
|
+
so depth, concurrency and the background task lifecycle behave identically and
|
|
147
|
+
a task spawned through it is listable with `ptc_task_list`.
|
|
148
|
+
|
|
149
|
+
Motivation, measured rather than assumed: pi 0.99.1 ships its own programmable
|
|
150
|
+
tool calling as the built-in `codemode`, which is stronger than `ptc_run_code` at
|
|
151
|
+
isolation and tool discovery but cannot spawn a process at all. With both
|
|
152
|
+
installed the model is taught two programming models per request
|
|
153
|
+
(`docs/research/codemode-vs-ptc-capability-20260930.md`).
|
|
154
|
+
|
|
155
|
+
- **The PTC tool descriptions now declare what a binding call resolves to
|
|
156
|
+
(ADR-0024).** `ptc_run_code` and `ptc_workflow` carry one shared binding
|
|
157
|
+
contract: a binding result is `{ content, details }`, `content` is an array of
|
|
158
|
+
content blocks (the text of a text-file result is `result.content[0].text`),
|
|
159
|
+
`details` is an object or `null`, and there is no `files` / `output` /
|
|
160
|
+
`matches` / `entries` field to read — `bash`, `grep`, `find` and `ls` return one text block
|
|
161
|
+
of newline-separated rows that the program splits itself. A `builtin binding`
|
|
162
|
+
that fails rejects with `ToolCallError`; `pi.dispatch` resolves to a
|
|
163
|
+
`DispatchResult` with `text` and `status` and no `content`.
|
|
164
|
+
|
|
165
|
+
Model-facing only: the wire, the worker and every existing program are
|
|
166
|
+
unchanged. Field report before: a pty-driven run of the real TUI with PTC mode
|
|
167
|
+
on produced 31 program crashes across 16 runs, the largest error class being the
|
|
168
|
+
model treating a binding result as a string or as an object with a `files`
|
|
169
|
+
field — neither is ever true, and nothing said so
|
|
170
|
+
(`docs/research/ptc-binding-contract-measurement-20260930.md`).
|
|
171
|
+
|
|
172
|
+
Re-measured after the change, same harness and tasks: **0 of those three crash
|
|
173
|
+
classes in 16 runs**, the correct `result.content[0].text` access in 16 of 16,
|
|
174
|
+
and the context median for a PTC run down from 36,669 to 8,360 with median
|
|
175
|
+
turns from 8 to 2. Exact-match rate is not claimed to have improved — it moved
|
|
176
|
+
10/16 to 11/16 while the control arm moved 15/16 to 13/16, which is this
|
|
177
|
+
harness's noise floor
|
|
178
|
+
(`docs/research/ptc-binding-contract-re-measurement-20260930.md`).
|
|
179
|
+
|
|
180
|
+
### Changed
|
|
181
|
+
|
|
182
|
+
- **The dispatch concurrency cap is one counter per session, not one per run.** The acquire
|
|
183
|
+
moved out of the dispatcher and into `dispatch()`, so a single `DispatchSlotCounter` now
|
|
184
|
+
serves every front: concurrent programs in the same session, the `ptc_subagent` tool, and
|
|
185
|
+
background children. The value is still `PtcConfig.dispatchConcurrency` (default 8) and the
|
|
186
|
+
refusal shape is unchanged; what changed is whose calls the cap counts.
|
|
187
|
+
|
|
188
|
+
This reduces effective concurrency in two measurable ways, and neither is a rounding
|
|
189
|
+
difference — 24 concurrent foreground calls against a real fake-`pi` spawn:
|
|
190
|
+
|
|
191
|
+
- two concurrent programs that could each have 8 in flight now share 8 (16 spawned -> 8);
|
|
192
|
+
- a program sharing a session with 8 live background children can now be refused **every**
|
|
193
|
+
foreground slot (8 foreground spawned -> 0). The refusal is a hard reject with no queue, so
|
|
194
|
+
an over-cap call is not parked behind a long-running child.
|
|
195
|
+
|
|
196
|
+
A single program on its own is unchanged (8 of 24, before and after), which is why the
|
|
197
|
+
change passed three review rounds. ADR-0016 §2 and ADR-0022 §9 are amended; the depth cap is
|
|
198
|
+
not.
|
|
199
|
+
|
|
200
|
+
The knob is now live rather than dead: the live control is
|
|
201
|
+
`createBackgroundTaskRuntime({ concurrency })`, which sizes the session counter from
|
|
202
|
+
`PtcConfig.dispatchConcurrency`. Passing `dispatchConcurrency` through
|
|
203
|
+
`runPtcProgram({ config })` no longer sizes the cap a pi session uses (measured: 2
|
|
204
|
+
configured, 8 dispatched).
|
|
205
|
+
|
|
206
|
+
### Fixed
|
|
207
|
+
|
|
208
|
+
- **A background task that produced no answer is no longer reported as a
|
|
209
|
+
success (#70, field report).** A child that hits a rate limit or a model
|
|
210
|
+
error still exits 0: pi writes the reason onto the assistant `message_end`
|
|
211
|
+
as `stopReason: "error"`, retries, and closes clean. The background pump
|
|
212
|
+
read the exit code alone, so such a task was recorded `succeeded` and the
|
|
213
|
+
model was told it had worked — while `ptc_task_output` answered "(no output
|
|
214
|
+
yet; task X is succeeded)". `succeeded` now requires the child to exit 0
|
|
215
|
+
**and** to have produced assistant text, which is the rule
|
|
216
|
+
`decideCloseOutcome` already applied to the foreground path; one failure now
|
|
217
|
+
has one verdict and one sentence in both paths. A failing `resolve-exit`
|
|
218
|
+
writes `errorMessage`, and the child's own `stopReason: "error"` text is
|
|
219
|
+
carried onto the record so the model is told _why_, not only _that_
|
|
220
|
+
(ADR-0022 §2, amended). A non-zero exit is still left unlabelled and a model
|
|
221
|
+
stop still resolves `canceled` — neither is relabelled by this change.
|
|
222
|
+
|
|
8
223
|
## [1.1.1] - 2026-09-30
|
|
9
224
|
|
|
10
225
|
### Fixed
|
package/README.md
CHANGED
|
@@ -74,10 +74,27 @@ const [read, scoutA, scoutB] = await Promise.all([
|
|
|
74
74
|
|
|
75
75
|
**Bounded.** Three knobs keep fan-out from running away:
|
|
76
76
|
|
|
77
|
-
- `PtcConfig.dispatchConcurrency` (default **8**) — hard cap on concurrently in-flight `pi.dispatch`
|
|
77
|
+
- `PtcConfig.dispatchConcurrency` (default **8**) — hard cap on concurrently in-flight dispatch **in one pi session**. It is one counter, not one per run: foreground `pi.dispatch`, the top-level `ptc_subagent` front, and live background children all spend it, and a background child holds its slot for its whole lifetime. The N+1th concurrent call resolves immediately with `{ status: "rejected", errorMessage: "dispatch concurrency limit reached" }` instead of queuing or spawning — so a call over the cap is not made to wait for a slot to come back.
|
|
78
78
|
- `PtcConfig.maxDispatchDepth` (default **3**) — recursion bound. The child subprocess loads pi-ptc too, so it can write its own PTC programs and call `pi.dispatch` itself; the `childDepth = parentDepth + 1` is rejected when it would exceed `maxDispatchDepth`. The child sees a `<pi-ptc-context depth="N" max-depth="M">…</pi-ptc-context>` hint appended to its system prompt so it can budget its recursion.
|
|
79
79
|
- `signal` — when the parent run is cancelled (deadline, abort, user Esc), every in-flight child receives `SIGTERM` followed by `SIGKILL` after a 5-second grace window, the same shape as pi's `examples/extensions/subagent/index.ts` reference.
|
|
80
80
|
|
|
81
|
+
**What the concurrency cap now governs, and which knob is live.** The cap is **one counter per pi
|
|
82
|
+
session** ([ADR-0016](./docs/adr/0016-ptc-dispatch-binding.md) §2 as amended,
|
|
83
|
+
[ADR-0022](./docs/adr/0022-background-dispatch.md) §9), acquired inside `dispatch()` so a single
|
|
84
|
+
owner gates every front. Two consequences are worth stating plainly, because both were measured and
|
|
85
|
+
neither is a rounding difference: two programs running concurrently in one session now share 8
|
|
86
|
+
rather than 8 each, and a program sharing a session with eight live background children can be
|
|
87
|
+
refused **every** foreground slot. The live control is `createBackgroundTaskRuntime({ concurrency })`,
|
|
88
|
+
the call that builds that session counter, and the value it is given is `PtcConfig.dispatchConcurrency`.
|
|
89
|
+
It is not a background-only knob: changing it changes how many foreground children a whole session
|
|
90
|
+
can have in flight.
|
|
91
|
+
|
|
92
|
+
The `dispatchConcurrency` a caller passes to `runPtcProgram({ config })` sizes the
|
|
93
|
+
dispatcher's own per-run counter, and that counter is only reached when no session counter is
|
|
94
|
+
supplied (`dispatcher.ts` hands the binding `options.dispatchDeps?.slots ?? dispatchSlots`). In a
|
|
95
|
+
pi session a session counter always is, so the per-run one is not what enforces the cap you are
|
|
96
|
+
looking at.
|
|
97
|
+
|
|
81
98
|
**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:
|
|
82
99
|
|
|
83
100
|
```ts
|
|
@@ -111,7 +128,7 @@ A detached pump drives the task's lifecycle (`running` -> `succeeded` / `failed`
|
|
|
111
128
|
- `ptc_task_output({ taskId, sinceBytes? })` — read a task's captured output, tail-truncated to pi's 50 KB / 2000-line contract (ADR-0015).
|
|
112
129
|
- `ptc_task_stop({ taskId, reason? })` — ask a running task to stop.
|
|
113
130
|
|
|
114
|
-
Background tasks count against the same `dispatchConcurrency` (default 8) for their whole lifetime and share the `maxDispatchDepth` (default 3) recursion bound. A pre-spawn refusal (depth or concurrency cap, unknown agent) still comes back as the familiar `DispatchResult` with `status: "rejected"`. Full guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md).
|
|
131
|
+
Background tasks count against the same `dispatchConcurrency` (default 8) for their whole lifetime and share the `maxDispatchDepth` (default 3) recursion bound — and since the gate moved into `dispatch()` that cap is the **one session counter** the foreground path uses too, not a second one held beside it. A session running eight long background children therefore has no foreground dispatch headroom left, and a foreground call over the cap is refused outright rather than queued behind them. A pre-spawn refusal (depth or concurrency cap, unknown agent) still comes back as the familiar `DispatchResult` with `status: "rejected"`. Full guide: [`docs/usage/bgdispatch.md`](./docs/usage/bgdispatch.md).
|
|
115
132
|
|
|
116
133
|
## TUI rendering
|
|
117
134
|
|
|
@@ -183,10 +200,80 @@ the session. The mode's rationale and rejected alternatives are in [ADR-0010](./
|
|
|
183
200
|
{ "defaultMode": false }
|
|
184
201
|
```
|
|
185
202
|
|
|
203
|
+
**Choosing the surface.** `defaultMode` decides whether the session _enters_ PTC mode; `surfaceMode`
|
|
204
|
+
decides which model-facing tools this package registers at all. It is read once, at startup, so a
|
|
205
|
+
surface change needs a new session ([ADR-0025](./docs/adr/0025-extension-surface-is-a-setting.md)):
|
|
206
|
+
|
|
207
|
+
```jsonc
|
|
208
|
+
// ~/.pi/agent/ptc.json
|
|
209
|
+
{ "surfaceMode": "subagents" }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `off` — a stock pi session: no tool, no `/ptc` command, no briefing.
|
|
213
|
+
- `subagents` — `ptc_subagent` plus the three `ptc_task_*` tools, with pi's own `codemode`
|
|
214
|
+
doing the orchestration; warns at startup when `codemode` is not in the active tool set.
|
|
215
|
+
- `full` — today's set: `ptc_run_code` / `ptc_workflow` plus the three `ptc_task_*` tools.
|
|
216
|
+
|
|
217
|
+
**The default is detected, and it is not `full` everywhere.** With no `surfaceMode` key, the surface
|
|
218
|
+
follows two questions, not one ([ADR-0026](./docs/adr/0026-surface-default-is-detected.md),
|
|
219
|
+
[ADR-0027](./docs/adr/0027-codemode-switch-decides-surface.md)):
|
|
220
|
+
|
|
221
|
+
| does this pi ship `codemode`? | will pi load it? | surface |
|
|
222
|
+
| ----------------------------- | ---------------------------------------------- | ----------- |
|
|
223
|
+
| yes | yes (default, or `+builtin:codemode`) | `subagents` |
|
|
224
|
+
| yes | no (`-builtin:codemode`, or `--no-extensions`) | `full` |
|
|
225
|
+
| no | — | `full` |
|
|
226
|
+
|
|
227
|
+
Setting the key always wins. A probe that cannot answer falls back to `full` — the safe direction,
|
|
228
|
+
since `subagents` as a failure mode would take away the orchestration tool the session was relying
|
|
229
|
+
on.
|
|
230
|
+
|
|
231
|
+
> Upgrading onto pi 0.99.1 or newer without setting the key moves you to `subagents`. `codemode`
|
|
232
|
+
> ships **inactive** (`defaultActive: false`), so until you add it to your tool list you get a
|
|
233
|
+
> subagent front with no orchestrator, and the startup warning says so. If you would rather keep
|
|
234
|
+
> today's tools, set `{ "surfaceMode": "full" }`.
|
|
235
|
+
|
|
236
|
+
> Turning pi's `codemode` **off** — `"extensions": ["-builtin:codemode"]`, or launching with
|
|
237
|
+
> `--no-extensions` — brings the PTC surfaces back on its own. Before ADR-0027 it did not: the
|
|
238
|
+
> detection asked only whether the extension directory exists, so a pi told not to load it still
|
|
239
|
+
> counted as an orchestrator and you got `ptc_subagent` with nothing to compose with. The switch is
|
|
240
|
+
> read from the same three places pi reads it — the command line, `<cwd>/.pi/settings.json`, and
|
|
241
|
+
> `<agentDir>/settings.json` — in the same order.
|
|
242
|
+
|
|
243
|
+
No file, or a value outside that set, falls back to the detected default and says so at startup
|
|
244
|
+
rather than half-applying: which tools exist is not something to change on a guess.
|
|
245
|
+
|
|
246
|
+
**A detection you cannot see is the failure this design has**, so the result is reported. With no
|
|
247
|
+
`surfaceMode` key, the outcome is issued through the TUI notification channel at session start —
|
|
248
|
+
how the probe came out and which surface the default therefore is — but only when the probe could
|
|
249
|
+
not answer. A pi that ships `codemode` and is detected as `subagents` is the expected case and says
|
|
250
|
+
nothing. A second notice is issued when the probe and pi's own tool registry disagree, which is the
|
|
251
|
+
case the probe structurally cannot see: it walks the filesystem, so under `--exclude-tools codemode`
|
|
252
|
+
it answers `present` for a tool this session does not have (and the mirror: a restructured `dist`
|
|
253
|
+
answers `not-found` for one pi plainly registers). A third notice covers ADR-0027: a settings file
|
|
254
|
+
that could not be read, and an explicit `surfaceMode` that disagrees with the table — the pinned
|
|
255
|
+
value still wins, and the notice only says so. What is **not**
|
|
256
|
+
established is that either line actually paints in a real pi TUI: a pty capture at review time
|
|
257
|
+
showed neither the notice nor a control marker, and a TUI quits on stdin EOF before a toast
|
|
258
|
+
renders, so that is an unmeasured end to end rather than a broken one. No test in this repository
|
|
259
|
+
observes a notice through a real TUI. If you are relying on the notice rather than on your own
|
|
260
|
+
`surfaceMode` key, verify it once.
|
|
261
|
+
|
|
262
|
+
**On a `--print` session, none of it prints.** `ui.notify` is the TUI channel; measured across
|
|
263
|
+
three `--print` runs that each emit one of these notices, stdout and stderr received **0 bytes**
|
|
264
|
+
each. That makes this page the only channel on which a `--print` user learns why they got the
|
|
265
|
+
surface they got — ADR-0025's decision-4 warning shares the gap, and there the answer is the same
|
|
266
|
+
one line of JSON: set `surfaceMode` yourself and the detection no longer matters.
|
|
267
|
+
|
|
186
268
|
**Where it does not run.** Print / JSON / RPC sessions are left exactly as launched, and so is a
|
|
187
269
|
session started with an explicit tool restriction (`--tools`, `--exclude-tools`,
|
|
188
|
-
`--no-builtin-tools`) — the extension does not override what you asked for.
|
|
189
|
-
|
|
270
|
+
`--no-builtin-tools`, `--no-extensions`) — the extension does not override what you asked for.
|
|
271
|
+
`--no-extensions` does, however, change the **detected surface**: pi's own `codemode` is a built-in
|
|
272
|
+
extension, so turning extensions off means it will not load, and ADR-0027's table resolves the
|
|
273
|
+
default to `full` — you keep `ptc_run_code` / `ptc_workflow` rather than a `ptc_subagent` with
|
|
274
|
+
nothing to compose with. If you would rather pin the surface regardless, set `"surfaceMode"` in
|
|
275
|
+
`ptc.json`. If another extension changes the tool set while the mode is on, the mode yields and
|
|
276
|
+
tells you.
|
|
190
277
|
|
|
191
278
|
**The important consequence:** in a TUI session, bindings come from the loadout recorded _before_
|
|
192
279
|
the mode narrowed it. That is what keeps `tools.read(…)` working — and it is why a `--tools`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExtensionAPI, Skill, truncateTail } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import "typebox";
|
|
2
3
|
import "@earendil-works/pi-ai";
|
|
3
4
|
import { MessagePort, Worker } from "node:worker_threads";
|
|
4
5
|
//#region src/runtime/ulid.d.ts
|
|
@@ -271,6 +272,7 @@ type TaskCommand = {
|
|
|
271
272
|
outputRef?: string;
|
|
272
273
|
outputBytes?: number;
|
|
273
274
|
outputPreview?: string;
|
|
275
|
+
childError?: string;
|
|
274
276
|
};
|
|
275
277
|
/** Outcome of one successful command: the persisted record, emitted events, and cursor. */
|
|
276
278
|
interface TransitionResult {
|
|
@@ -362,10 +364,11 @@ declare class DispatchSlotCounter {
|
|
|
362
364
|
release(holder?: string): void;
|
|
363
365
|
}
|
|
364
366
|
/**
|
|
365
|
-
* Injected dependencies
|
|
366
|
-
*
|
|
367
|
-
* existing zero-argument call sites keep working
|
|
368
|
-
*
|
|
367
|
+
* Injected dependencies. The background branch uses this object for everything; the foreground
|
|
368
|
+
* branch uses `slots` (the one cap both fronts share since ADR-0026 round 3) and
|
|
369
|
+
* `lifecycle`. Every field is optional so existing zero-argument call sites keep working, and
|
|
370
|
+
* both branches fall back to the module-level {@link DISPATCH_LIFECYCLE}. Tests pass an
|
|
371
|
+
* in-memory registry and a mock lifecycle so no real `pi` process is ever spawned.
|
|
369
372
|
*/
|
|
370
373
|
interface DispatchDeps {
|
|
371
374
|
/** Session-level TaskRegistry (ADR-0022 §3). Defaults to a lazy in-memory registry. */
|
|
@@ -544,6 +547,139 @@ interface DefaultModeConfig {
|
|
|
544
547
|
* on** — the caller should surface the problem rather than silently changing behavior.
|
|
545
548
|
*/
|
|
546
549
|
export declare function readDefaultModeConfig(agentDir: string): DefaultModeConfig;
|
|
550
|
+
/**
|
|
551
|
+
* Which model-facing tools this package registers, independent of PTC mode (which decides
|
|
552
|
+
* which registered tools are *active*). Read from the same agent-dir file as
|
|
553
|
+
* {@link readDefaultModeConfig}, beside the `defaultMode` key. ADR-0025.
|
|
554
|
+
*
|
|
555
|
+
* The order below is the order of increasing responsibility: `off` hands the whole
|
|
556
|
+
* orchestration question back to pi, `subagents` keeps only the subagent face and lets pi's
|
|
557
|
+
* `codemode` orchestrate, `full` keeps today's set.
|
|
558
|
+
*/
|
|
559
|
+
declare const SURFACE_MODES: readonly ["off", "subagents", "full"];
|
|
560
|
+
type SurfaceMode = (typeof SURFACE_MODES)[number];
|
|
561
|
+
/**
|
|
562
|
+
* Whether the pi that launched us will actually LOAD its `codemode` extension.
|
|
563
|
+
*
|
|
564
|
+
* - `"absent"` — nothing in any settings file names it, so pi's own default applies, which is to
|
|
565
|
+
* load it (`settings.md`: "They load by default").
|
|
566
|
+
* - `"enabled"` — an explicit `+builtin:codemode`, or `-e builtin:codemode` on the command line.
|
|
567
|
+
* - `"disabled"` — `-builtin:codemode` or `!builtin:codemode` in a settings file, or `--no-extensions`.
|
|
568
|
+
*
|
|
569
|
+
* This is a different question from {@link CodemodePresence}, which asks whether the directory is
|
|
570
|
+
* on disk. Both are needed, and conflating them is what ADR-0027 fixes: pi 0.99.0 added
|
|
571
|
+
* `-builtin:<name>`, so "the directory exists" and "codemode will run" stopped being the same
|
|
572
|
+
* answer, and a probe that only asks the first one hands orchestration to a tool that is not there.
|
|
573
|
+
*/
|
|
574
|
+
type CodemodeSwitch = "absent" | "enabled" | "disabled";
|
|
575
|
+
/** How the switch was decided, so a test can tell a measured answer from a failed read. */
|
|
576
|
+
type CodemodeSwitchSource = "cli" | "project" | "user" | "default" | "invalid";
|
|
577
|
+
/** The switch plus enough provenance to explain it in a notice. */
|
|
578
|
+
interface CodemodeSwitchResolution {
|
|
579
|
+
switch: CodemodeSwitch;
|
|
580
|
+
source: CodemodeSwitchSource;
|
|
581
|
+
/** Set when a settings file could not be read as a JSON object; the switch still resolves. */
|
|
582
|
+
error?: string;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* The two directories pi resolves the `!` bucket's globs against.
|
|
586
|
+
*
|
|
587
|
+
* `matchesAnyPattern` also tests `relative(baseDir, path)` and the basename, so a glob whose
|
|
588
|
+
* answer depends on where pi's directories sit is only correct for the directories pi actually
|
|
589
|
+
* used. These are the two it uses: `join(cwd, CONFIG_DIR_NAME)` for project scope -- pi 1.0.0's
|
|
590
|
+
* `CONFIG_DIR_NAME` is `".pi"`, `package-manager.js:720` -- and `agentDir` for user scope (`:719`).
|
|
591
|
+
*/
|
|
592
|
+
interface CodemodeBaseDirs {
|
|
593
|
+
project: string;
|
|
594
|
+
user: string;
|
|
595
|
+
}
|
|
596
|
+
export declare function resolveCodemodeSwitch(argv: readonly string[], projectSettings: unknown, userSettings: unknown, baseDirs?: CodemodeBaseDirs): CodemodeSwitchResolution;
|
|
597
|
+
/**
|
|
598
|
+
* Read pi's own settings files and resolve the switch.
|
|
599
|
+
*
|
|
600
|
+
* Project settings live at `<cwd>/.pi/settings.json` and user settings at
|
|
601
|
+
* `<agentDir>/settings.json` — the two `DefaultPackageManager` reads, via
|
|
602
|
+
* `join(this.cwd, CONFIG_DIR_NAME)` and `this.agentDir`.
|
|
603
|
+
*/
|
|
604
|
+
export declare function readCodemodeSwitch(agentDir: string, cwd: string, argv?: readonly string[]): CodemodeSwitchResolution;
|
|
605
|
+
/**
|
|
606
|
+
* Whether an explicit `surfaceMode` disagrees with what the table above decided.
|
|
607
|
+
*
|
|
608
|
+
* The explicit key always WINS — that is what an override is for — so this exists only to be
|
|
609
|
+
* reported. `off` is exempt: it means "I do not want this package's surface at all", which is a
|
|
610
|
+
* statement about the package rather than a claim about who orchestrates, and warning about it on
|
|
611
|
+
* every session would be crying wolf.
|
|
612
|
+
*/
|
|
613
|
+
export declare function surfaceModeConflict(explicit: SurfaceMode | undefined, detected: SurfaceMode): boolean;
|
|
614
|
+
/**
|
|
615
|
+
* Whether the pi that launched us ships its own `codemode` orchestration tool.
|
|
616
|
+
*
|
|
617
|
+
* pi's own tool listing is NOT usable here, and that is why this probe exists at all.
|
|
618
|
+
* `getAllTools()` and `getActiveTools()` are `notInitialized` stubs until `bindCore` runs
|
|
619
|
+
* (`loader.js:106-108`), which happens after every factory body has returned. Calling one from
|
|
620
|
+
* a factory throws, and `initializeExtension` catches that throw while `loadExtension` answers
|
|
621
|
+
* `{ extension: null, error }` (`loader.js:493-520`) -- a throwing factory makes the extension
|
|
622
|
+
* fail to load entirely, not degrade to an empty tool list. Registration has to happen in the
|
|
623
|
+
* factory, so the default has to be knowable there.
|
|
624
|
+
*
|
|
625
|
+
* Those line numbers are pi **0.99.1**'s `dist/core/extensions/loader.js`, read from a real
|
|
626
|
+
* install, and that is the version this reasoning is about rather than the one this repo compiles
|
|
627
|
+
* against: the 0.86.1 in `devDependencies` has the same two regions at 106-108 and 445-473.
|
|
628
|
+
*
|
|
629
|
+
* So this walks the filesystem from the entry script instead. `process.argv[1]` is whatever the
|
|
630
|
+
* user typed, which for a package-manager install is a shim, so it is resolved first. The answer
|
|
631
|
+
* is "does this pi ship codemode", NOT "can this session call it": codemode registers with
|
|
632
|
+
* `defaultActive: false`, so it is absent from `getActiveTools()` even when fully present.
|
|
633
|
+
* `session_start` is where the second question gets asked, and where a session handed to an
|
|
634
|
+
* orchestrator that is not there gets told.
|
|
635
|
+
*/
|
|
636
|
+
export declare function probeCodemodePresence(argv?: readonly string[]): CodemodePresence;
|
|
637
|
+
interface CodemodePresence {
|
|
638
|
+
/** Whether the directory was found. */
|
|
639
|
+
present: boolean;
|
|
640
|
+
/** How the answer was reached, so a test can tell a measured yes from a failed probe. */
|
|
641
|
+
how: "found" | "not-found" | "no-entry" | "unresolvable-entry";
|
|
642
|
+
}
|
|
643
|
+
/** Result of reading the surface mode, with enough detail to warn about a broken file. */
|
|
644
|
+
interface SurfaceModeConfig {
|
|
645
|
+
surfaceMode: SurfaceMode;
|
|
646
|
+
source: "file" | "default" | "invalid";
|
|
647
|
+
error?: string;
|
|
648
|
+
/**
|
|
649
|
+
* What the probe found, on the paths where the surface was NOT decided by the file. Absent
|
|
650
|
+
* when the user set the key: that call never consults the probe (see
|
|
651
|
+
* {@link readSurfaceModeConfig}), so there is nothing to report.
|
|
652
|
+
*/
|
|
653
|
+
codemode?: CodemodePresence;
|
|
654
|
+
/**
|
|
655
|
+
* Whether pi will actually load its own codemode (ADR-0027). Absent for the same reason as
|
|
656
|
+
* {@link codemode}: an explicit key short-circuits the probe, so there is nothing to report.
|
|
657
|
+
*/
|
|
658
|
+
codemodeSwitch?: CodemodeSwitchResolution;
|
|
659
|
+
/**
|
|
660
|
+
* What the four-case table decided, carried even when an explicit key overrode it — that
|
|
661
|
+
* difference is exactly what {@link surfaceModeConflict} reports on.
|
|
662
|
+
*/
|
|
663
|
+
detected?: SurfaceMode;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Read `surfaceMode` from the agent-dir config file.
|
|
667
|
+
*
|
|
668
|
+
* A pure function over the filesystem, shaped like {@link readDefaultModeConfig} on purpose:
|
|
669
|
+
* an absent file, an absent key, unparseable JSON, a non-object, a wrong-typed value and an
|
|
670
|
+
* out-of-set value all resolve to the detected default rather than a hardcoded one, and every
|
|
671
|
+
* malformed shape additionally reports `invalid` with a reason. A malformed setting must never
|
|
672
|
+
* half-apply - which tools exist is not something to change on a guess.
|
|
673
|
+
*
|
|
674
|
+
* The `presence` and `codemodeSwitch` arguments are parameters rather than hidden calls, so a test
|
|
675
|
+
* can state the pi it is reasoning about instead of depending on the machine it runs on. Omit
|
|
676
|
+
* them and the real probes answer, but only on a path that actually needs the answer: they are
|
|
677
|
+
* resolved inside the fallback branches rather than in a default parameter, because a default
|
|
678
|
+
* parameter is evaluated on EVERY call -- including the ones an explicit `surfaceMode` key
|
|
679
|
+
* short-circuits, where the user paid a `realpathSync` plus up to three `statSync` and two
|
|
680
|
+
* settings reads to set one line of JSON and get a constant.
|
|
681
|
+
*/
|
|
682
|
+
export declare function readSurfaceModeConfig(agentDir: string, presence?: CodemodePresence, codemodeSwitch?: CodemodeSwitchResolution, cwd?: string): SurfaceModeConfig;
|
|
547
683
|
/** Why the mode declined to turn on. Surfaced in the entry notification / debug logs. */
|
|
548
684
|
type ModeBlockReason = "not-tui" | "config-off" | "tools-unavailable" | "restricted-session";
|
|
549
685
|
/** Either the loadout to apply, or the reason the mode stayed off. */
|
|
@@ -689,13 +825,26 @@ interface PtcConfig {
|
|
|
689
825
|
*/
|
|
690
826
|
maxParallelSubCalls: number;
|
|
691
827
|
/**
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
828
|
+
* Hard cap on concurrently in-flight dispatch in ONE pi session, not
|
|
829
|
+
* per run. Enforced inside `dispatch()` by a single
|
|
830
|
+
* `DispatchSlotCounter`, so every front spends it: concurrent programs,
|
|
831
|
+
* the `ptc_subagent` tool, and background children (which hold their
|
|
832
|
+
* slot for the task's whole lifetime). The next call over the cap
|
|
833
|
+
* resolves immediately with `{ status: "rejected", errorMessage:
|
|
834
|
+
* "dispatch concurrency limit reached" }` — never queued, never
|
|
835
|
+
* spawned, and never parked behind a long-running child.
|
|
696
836
|
* Default 8, matches pi's `subagent` extension `MAX_PARALLEL_TASKS`.
|
|
697
|
-
* ADR-0016 section 2
|
|
698
|
-
*
|
|
837
|
+
* ADR-0016 section 2 (as amended 2026-09-30), ADR-0022 section 9.
|
|
838
|
+
*
|
|
839
|
+
* Where the number is READ from is the counter's construction, not
|
|
840
|
+
* this field alone: a pi session's counter is built by
|
|
841
|
+
* `createBackgroundTaskRuntime({ concurrency })` with this value, and
|
|
842
|
+
* the dispatcher hands the binding `options.dispatchDeps?.slots ??
|
|
843
|
+
* dispatchSlots` — so a `runPtcProgram({ config })` override sizes only
|
|
844
|
+
* the per-run counter, which a pi session never reaches.
|
|
845
|
+
*
|
|
846
|
+
* Independent of `maxParallelSubCalls`: builtin calls never consume a
|
|
847
|
+
* dispatch slot and vice versa.
|
|
699
848
|
*/
|
|
700
849
|
dispatchConcurrency: number;
|
|
701
850
|
/** Maximum recursion depth for `pi.dispatch`. The child PTC run spawned by
|
|
@@ -1170,8 +1319,29 @@ export declare class TurnPools {
|
|
|
1170
1319
|
export interface PtcSubagentsOptions {
|
|
1171
1320
|
/** Use this session-scoped background runtime instead of constructing one. */
|
|
1172
1321
|
backgroundRuntime?: BackgroundTaskRuntime;
|
|
1322
|
+
/**
|
|
1323
|
+
* Test seam for ADR-0025's surface mode. When set it wins over the agent-dir `ptc.json`,
|
|
1324
|
+
* so a test never reads the developer's real settings -- and the four test files that all
|
|
1325
|
+
* build this factory through one stub would otherwise inherit whatever the machine happens
|
|
1326
|
+
* to have. Undefined in production, where the file is the only source.
|
|
1327
|
+
*/
|
|
1328
|
+
surfaceMode?: SurfaceMode;
|
|
1329
|
+
/**
|
|
1330
|
+
* ADR-0026 test seam: what the codemode probe found. Undefined in production, where the
|
|
1331
|
+
* probe really runs. A test that exercises the DETECTED default states the pi it
|
|
1332
|
+
* assumes rather than inheriting whatever process.argv the test runner happens to have,
|
|
1333
|
+
* which is how the previous version of that test passed for the wrong reason.
|
|
1334
|
+
*/
|
|
1335
|
+
codemode?: CodemodePresence;
|
|
1336
|
+
/**
|
|
1337
|
+
* ADR-0027 test seam: whether pi will actually LOAD its own codemode. Undefined in
|
|
1338
|
+
* production, where `readCodemodeSwitch` reads pi's real settings files. Separate from
|
|
1339
|
+
* `codemode` on purpose — a pi can ship the directory and still be told not to load it,
|
|
1340
|
+
* and that is the case this seam exists to state.
|
|
1341
|
+
*/
|
|
1342
|
+
codemodeSwitch?: CodemodeSwitchResolution;
|
|
1173
1343
|
}
|
|
1174
1344
|
export default function ptcSubagents(pi: ExtensionAPI, options?: PtcSubagentsOptions): void;
|
|
1175
1345
|
//#endregion
|
|
1176
|
-
export type { Binding, BindingContext, BindingTable, CreateBuiltinBindingsOptions, DefaultModeConfig, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcImage, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions, TurnPoolsOptions, WorkerPoolOptions, WorkerPoolStats, WorkerPoolWorkerOptions };
|
|
1346
|
+
export type { Binding, BindingContext, BindingTable, CodemodePresence, CodemodeSwitch, CodemodeSwitchResolution, CodemodeSwitchSource, CreateBuiltinBindingsOptions, DefaultModeConfig, ModeBlockReason, ModeEntryDecision, ModeEntryInput, ModeHideStrategy, PersistedModeState, PtcConfig, PtcErrorKind, PtcErrorShape, PtcImage, PtcJsonValue, PtcModeState, PtcRunOutcome, PtcSurface, RunPtcProgramOptions, SurfaceModeConfig, TurnPoolsOptions, WorkerPoolOptions, WorkerPoolStats, WorkerPoolWorkerOptions };
|
|
1177
1347
|
//# sourceMappingURL=index.d.ts.map
|