pi-worker-graph 0.1.0-dev.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 +455 -0
- package/SECURITY.md +54 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +181 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +81 -0
- package/dist/context.js.map +1 -0
- package/dist/coordination.d.ts +19 -0
- package/dist/coordination.d.ts.map +1 -0
- package/dist/coordination.js +267 -0
- package/dist/coordination.js.map +1 -0
- package/dist/execution-failure.d.ts +42 -0
- package/dist/execution-failure.d.ts.map +1 -0
- package/dist/execution-failure.js +90 -0
- package/dist/execution-failure.js.map +1 -0
- package/dist/extension.d.ts +8 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +641 -0
- package/dist/extension.js.map +1 -0
- package/dist/graph.d.ts +44 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +292 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +9 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +66 -0
- package/dist/json.js.map +1 -0
- package/dist/orchestrator.d.ts +15 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +473 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/output.d.ts +61 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +248 -0
- package/dist/output.js.map +1 -0
- package/dist/pi-subprocess.d.ts +92 -0
- package/dist/pi-subprocess.d.ts.map +1 -0
- package/dist/pi-subprocess.js +897 -0
- package/dist/pi-subprocess.js.map +1 -0
- package/dist/run.d.ts +89 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +562 -0
- package/dist/run.js.map +1 -0
- package/dist/store.d.ts +331 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +1993 -0
- package/dist/store.js.map +1 -0
- package/dist/usage.d.ts +35 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +88 -0
- package/dist/usage.js.map +1 -0
- package/docs/DECISIONS.md +221 -0
- package/docs/DESIGN.md +392 -0
- package/docs/NEXT.md +229 -0
- package/docs/PLAN.md +203 -0
- package/docs/worker-graph.example.json +12 -0
- package/extensions/index.ts +1 -0
- package/extensions/tsconfig.json +11 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pi-worker-graph contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# pi-worker-graph
|
|
2
|
+
|
|
3
|
+
A small DAG-first worker orchestration runtime for the
|
|
4
|
+
[Pi coding agent](https://github.com/earendil-works/pi).
|
|
5
|
+
|
|
6
|
+
`pi-worker-graph` is designed for one orchestrator coordinating multiple writable
|
|
7
|
+
workers in a shared checkout. Dependency edges control scheduling and carry
|
|
8
|
+
bounded structured context; an optional run-scoped journal carries facts
|
|
9
|
+
discovered while work is in progress.
|
|
10
|
+
|
|
11
|
+
## Status
|
|
12
|
+
|
|
13
|
+
Early implementation. The package currently provides tested graph primitives,
|
|
14
|
+
a versioned structured worker-report contract, canonical byte-bounded
|
|
15
|
+
prerequisite context, an explicit-root filesystem store, a bounded DAG runner,
|
|
16
|
+
a one-shot Pi subprocess adapter, immutable run-scoped coordination events and
|
|
17
|
+
inboxes, and an explicitly activated parent orchestration tool. The parent tool
|
|
18
|
+
is inactive by default; worker children receive only the final-report and
|
|
19
|
+
coordination tools.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
The extension has no provider or model defaults, so it does not run until a
|
|
24
|
+
configuration exists. Copy the example and edit its profiles:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
mkdir -p ~/.pi/agent
|
|
28
|
+
cp docs/worker-graph.example.json ~/.pi/agent/worker-graph.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The npm artifact ships `docs/` too, so the same file is present in an installed
|
|
32
|
+
copy of the package.
|
|
33
|
+
|
|
34
|
+
Every profile must name its `provider`, `model`, `thinkingLevel`, and `tools`
|
|
35
|
+
explicitly; unknown fields are rejected. Valid thinking levels are `off`,
|
|
36
|
+
`minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Valid worker tools are
|
|
37
|
+
`read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. A graph
|
|
38
|
+
task selects a profile by name, so the name in the example is illustrative
|
|
39
|
+
rather than required.
|
|
40
|
+
|
|
41
|
+
Profiles configure workers. The capable model belongs on the parent, which
|
|
42
|
+
decomposes the work, reads the integrated checkout, and decides acceptance; a
|
|
43
|
+
worker executes one narrow assignment the parent already scoped, so the example
|
|
44
|
+
configures a cheaper model here. Add further profiles when tasks genuinely need
|
|
45
|
+
different capability or a narrower tool set.
|
|
46
|
+
|
|
47
|
+
The parent session can be configured in the same file, through an optional
|
|
48
|
+
`orchestrator` block:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"orchestrator": {
|
|
53
|
+
"provider": "anthropic",
|
|
54
|
+
"model": "claude-opus-4-5",
|
|
55
|
+
"thinkingLevel": "high"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Enabling the mode then moves the session onto that model and restores the
|
|
61
|
+
previous one on `/swarm off` and on leaving the branch. Shutdown attempts the
|
|
62
|
+
same restore, but it is a best effort: the model is put back through an
|
|
63
|
+
asynchronous Pi call, and a session ending need not wait for it. A restore that
|
|
64
|
+
cannot be performed at all — the earlier model has left Pi's catalogue, or its
|
|
65
|
+
provider lost authentication — is reported rather than passed over in silence.
|
|
66
|
+
The block has no `tools` field: the parent's tools stay governed by the mode's
|
|
67
|
+
own snapshot.
|
|
68
|
+
`thinkingLevel` accepts every level a worker profile accepts except `off`,
|
|
69
|
+
which Pi's session thinking level cannot express.
|
|
70
|
+
|
|
71
|
+
The block is deliberately absent from the example, because a model your Pi
|
|
72
|
+
install cannot find would refuse to enable the mode. Leave it out and the
|
|
73
|
+
parent stays exactly as you started it, with its model coming from Pi's own
|
|
74
|
+
settings or preset.
|
|
75
|
+
|
|
76
|
+
A valid configuration is required to enable the mode at all, and by `/swarm
|
|
77
|
+
runs` and `/swarm delete`, which resolve the run-store state root through it.
|
|
78
|
+
See [Orchestrator tool](#orchestrator-tool) for `stateRoot`,
|
|
79
|
+
`maxRetainedRuns`, and the activation commands.
|
|
80
|
+
|
|
81
|
+
## Graph semantics
|
|
82
|
+
|
|
83
|
+
- The complete graph is validated before execution.
|
|
84
|
+
- Task IDs are normalized, non-empty, and unique.
|
|
85
|
+
- Dependencies must exist, cannot refer to the same task, and must be acyclic.
|
|
86
|
+
- A task becomes ready only after all direct prerequisites succeed.
|
|
87
|
+
- Failed, aborted, or blocked tasks block their descendants.
|
|
88
|
+
- Unrelated branches remain eligible to run.
|
|
89
|
+
- Ready task ordering is deterministic.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import {
|
|
93
|
+
createInitialState,
|
|
94
|
+
normalizeGraph,
|
|
95
|
+
readyFrontier,
|
|
96
|
+
setNodeStatus,
|
|
97
|
+
} from "pi-worker-graph";
|
|
98
|
+
|
|
99
|
+
const graph = normalizeGraph({
|
|
100
|
+
tasks: [
|
|
101
|
+
{ id: "api", payload: { task: "Implement the API" } },
|
|
102
|
+
{ id: "ui", payload: { task: "Implement the UI" } },
|
|
103
|
+
{
|
|
104
|
+
id: "integration",
|
|
105
|
+
needs: ["api", "ui"],
|
|
106
|
+
payload: { task: "Integrate and validate" },
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
concurrency: 2,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let state = createInitialState(graph);
|
|
113
|
+
console.log(readyFrontier(graph, state)); // ["api", "ui"]
|
|
114
|
+
|
|
115
|
+
state = setNodeStatus(graph, state, "api", "running");
|
|
116
|
+
state = setNodeStatus(graph, state, "api", "succeeded");
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The graph module is generic over `payload` and has no provider, model, process,
|
|
120
|
+
filesystem, or Pi runtime dependency. `readyFrontier()` returns every eligible
|
|
121
|
+
task; `runGraph()` applies concurrency and fixed resource limits when selecting
|
|
122
|
+
and executing work through an injected adapter.
|
|
123
|
+
|
|
124
|
+
## Worker reports
|
|
125
|
+
|
|
126
|
+
A completed task executor must return a complete schema-versioned `NodeOutput`
|
|
127
|
+
containing its summary, changed files, interfaces, decisions, validation, and
|
|
128
|
+
blockers. Changed-file paths are normalized repository-relative paths. Reports
|
|
129
|
+
reject unknown fields, empty or oversized text, excessive item counts, hostile
|
|
130
|
+
values, and oversized serialized JSON. `parseNodeOutput()` validates untrusted
|
|
131
|
+
values and returns an immutable snapshot suitable for publication or
|
|
132
|
+
dependency-edge propagation. A report with blockers fails its node, retains the
|
|
133
|
+
report for review, and blocks dependents.
|
|
134
|
+
|
|
135
|
+
Direct-prerequisite reports are serialized in deterministic task-ID order into
|
|
136
|
+
named JSON blocks. The complete UTF-8 context, including labels and an explicit
|
|
137
|
+
untrusted-worker-data warning, is measured against a hard byte limit. Oversized
|
|
138
|
+
context fails the downstream node and is never silently truncated. Only the
|
|
139
|
+
validated reports of declared direct prerequisites are included.
|
|
140
|
+
|
|
141
|
+
A worker may also retain one bounded text artifact beside its report, through
|
|
142
|
+
the optional `artifact` field on `worker_graph_report`: supplemental long-form
|
|
143
|
+
material such as a log, investigation notes, or detailed review findings. It is
|
|
144
|
+
a sibling of the report rather than a field of it, so the report envelope stays
|
|
145
|
+
at schema version 1 and the artifact is bounded separately. The runtime never
|
|
146
|
+
parses it and never places it on a dependency edge, so a report that leans on
|
|
147
|
+
its artifact is an incomplete report. It is stored under the run, the output
|
|
148
|
+
envelope records the byte length that vouches for it, and it is removed when
|
|
149
|
+
the run is deleted. `readNodeArtifact()` reads it back, and the orchestrator
|
|
150
|
+
tool result names its byte length so a retained artifact is discoverable. An
|
|
151
|
+
aborted task produced nothing to retain and may not publish one.
|
|
152
|
+
|
|
153
|
+
Neither overflow becomes truncation. An oversized report is rejected so the
|
|
154
|
+
worker can correct and resubmit it, and oversized prerequisite context fails
|
|
155
|
+
the downstream node rather than handing it a partial prerequisite contract.
|
|
156
|
+
|
|
157
|
+
## Pi worker adapter
|
|
158
|
+
|
|
159
|
+
`createPiSubprocessExecutor()` selects an explicitly named worker profile for
|
|
160
|
+
each task. It starts one ephemeral Pi JSON-mode child in the target checkout,
|
|
161
|
+
disables discovered extensions, skills, prompts, and session persistence, and
|
|
162
|
+
applies a strict built-in tool allowlist plus the child-only report tool. Task
|
|
163
|
+
assignments and edge context are written to stdin and never added to child-process
|
|
164
|
+
arguments.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { createPiSubprocessExecutor, runGraph } from "pi-worker-graph";
|
|
168
|
+
|
|
169
|
+
const executor = createPiSubprocessExecutor({
|
|
170
|
+
profiles: {
|
|
171
|
+
worker: {
|
|
172
|
+
provider: "anthropic",
|
|
173
|
+
model: "claude-haiku-4-5",
|
|
174
|
+
thinkingLevel: "medium",
|
|
175
|
+
tools: ["read", "bash", "edit", "write"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await runGraph({
|
|
181
|
+
stateRoot: "/path/outside/the/checkout",
|
|
182
|
+
workingDirectory: process.cwd(),
|
|
183
|
+
executor,
|
|
184
|
+
graph: {
|
|
185
|
+
tasks: [
|
|
186
|
+
{
|
|
187
|
+
id: "implementation",
|
|
188
|
+
payload: {
|
|
189
|
+
profile: "worker",
|
|
190
|
+
assignment: "Implement the requested change",
|
|
191
|
+
acceptanceCriteria: ["Tests pass"],
|
|
192
|
+
expectedPaths: ["src/"],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The adapter requires a valid terminating `worker_graph_report` call, maps
|
|
201
|
+
process/provider/report failures to fixed safe diagnostics, and terminates the
|
|
202
|
+
child process group on cancellation. Automated tests use fake subprocesses and
|
|
203
|
+
make no provider calls.
|
|
204
|
+
|
|
205
|
+
Event-stream handling is deliberately tolerant of normal worker behaviour. Pi's
|
|
206
|
+
JSON mode reports every session event, so single lines carry whole tool results
|
|
207
|
+
and whole assistant messages; lines too large to parse are skipped rather than
|
|
208
|
+
failing the task, and the framing bound is derived from the report envelope so a
|
|
209
|
+
valid report can never be skipped. A rejected report is recoverable: the worker
|
|
210
|
+
may correct and resubmit it, and only a worker that never produces a valid
|
|
211
|
+
report fails on that signal. Once a report is captured, a provider error or a
|
|
212
|
+
nonzero exit afterwards does not discard it — the structured report is the task
|
|
213
|
+
contract. Cancellation still outranks a captured report.
|
|
214
|
+
|
|
215
|
+
The report must also be the worker's *last* action. Pi executes the tool calls
|
|
216
|
+
of one assistant message as a batch, and a terminating result only ends the
|
|
217
|
+
session when every result in that batch terminates, so a report called alongside
|
|
218
|
+
`write` or `bash` leaves the worker running. The adapter tracks the batch the
|
|
219
|
+
report belonged to and any tool that runs afterwards, and fails such a task
|
|
220
|
+
instead of accepting a report that work outlived.
|
|
221
|
+
|
|
222
|
+
With no explicit `command`, the adapter locates Pi's CLI entry point through
|
|
223
|
+
this package's dependency on Pi and runs it under the current JavaScript
|
|
224
|
+
runtime. Nothing is inferred from `PI_CODING_AGENT`, which Pi exports to every
|
|
225
|
+
process it launches, and no command interpreter is involved on any platform.
|
|
226
|
+
Supply `command` when Pi cannot be resolved that way.
|
|
227
|
+
|
|
228
|
+
An executor rejects with `TaskExecutionFailure`, whose diagnostics come from a
|
|
229
|
+
fixed allowlist, so no provider text or repository content reaches persisted run
|
|
230
|
+
state. `runGraph` reports a graph the executor refuses through
|
|
231
|
+
`RunGraphValidationError` with an `adapter_validation` issue, the same error type
|
|
232
|
+
as every other pre-run rejection.
|
|
233
|
+
|
|
234
|
+
The adapter projects bounded progress snapshots containing only task identity,
|
|
235
|
+
phase, allowlisted tool name, and numeric usage. Worker text, tool arguments,
|
|
236
|
+
tool results, and stderr are never included. Progress callbacks are capped and
|
|
237
|
+
cannot alter worker execution if an observer throws.
|
|
238
|
+
|
|
239
|
+
## Worker coordination
|
|
240
|
+
|
|
241
|
+
Workers receive four child-only tools for facts discovered after scheduling:
|
|
242
|
+
`worker_graph_event` publishes one bounded coordination fact, optionally
|
|
243
|
+
addressed to named tasks and tagged with paths or symbols;
|
|
244
|
+
`worker_graph_message` sends one directed handoff; `worker_graph_events` and
|
|
245
|
+
`worker_graph_inbox` read them. Nothing is injected automatically — a worker
|
|
246
|
+
reads only what it asks for — and returned records are labeled as untrusted
|
|
247
|
+
worker-authored data.
|
|
248
|
+
|
|
249
|
+
Every record in a run shares one monotonic sequence, so an identifier is also a
|
|
250
|
+
position: a cursor cannot skip a record published between two reads. Claiming a
|
|
251
|
+
sequence number and storing the record it names are one exclusive create in one
|
|
252
|
+
journal, so no identifier is ever reserved for a record that lands after a
|
|
253
|
+
reader has been handed a cursor past it. A read is bounded by a requested record
|
|
254
|
+
count and by the serialized JSON array size, including its brackets and
|
|
255
|
+
separators. This holds strictly because every record is bounded at 32 KiB when
|
|
256
|
+
published, half of one 64 KiB page; a read returns a cursor for the remainder.
|
|
257
|
+
Both kinds share the journal, so a read also passes over records it is never
|
|
258
|
+
given — the other kind, another task's mail — and its cursor runs past them, so
|
|
259
|
+
polling an inbox costs only the records published since the last call rather
|
|
260
|
+
than the whole journal each time. A cursor therefore belongs to the query that
|
|
261
|
+
produced it, and a page can come back empty with one; a reader stops when no
|
|
262
|
+
cursor is returned.
|
|
263
|
+
Publication synchronizes the active-owner check with ownership release. The
|
|
264
|
+
parent and every worker contend for one run mutation lock, which is claimed by
|
|
265
|
+
linking a record that already names its holder, so contention with a live
|
|
266
|
+
worker is never mistaken for a lock a killed worker left behind: a parent
|
|
267
|
+
mutation waits contention out and recovers a lock only when it names a task of
|
|
268
|
+
its own graph that has already finished. Publication requires the run to have
|
|
269
|
+
an active owner but not the orchestrator's ownership capability, which never
|
|
270
|
+
leaves the parent,
|
|
271
|
+
so workers cannot advance node state or publish another task's output. Records
|
|
272
|
+
are attributed to the publishing task rather than authenticated: workers of one
|
|
273
|
+
run share the state root as they already share the checkout.
|
|
274
|
+
|
|
275
|
+
A run retains at most 256 coordination records. Publishing past that, or a
|
|
276
|
+
record over its size bound, fails explicitly and the worker continues without
|
|
277
|
+
it; as with retained runs, nothing is deleted automatically, so reclaiming the
|
|
278
|
+
capacity means deleting the run directory and its slot together.
|
|
279
|
+
|
|
280
|
+
## Orchestrator tool
|
|
281
|
+
|
|
282
|
+
The extension reads `worker-graph.json` from Pi's agent directory (normally
|
|
283
|
+
`~/.pi/agent`); [`docs/worker-graph.example.json`](docs/worker-graph.example.json) is
|
|
284
|
+
a copyable starting point. The configuration is byte-bounded, rejects unknown
|
|
285
|
+
fields, and requires every worker profile to select its provider, model,
|
|
286
|
+
thinking level, and tool allowlist explicitly:
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"schemaVersion": 1,
|
|
291
|
+
"maxRetainedRuns": 64,
|
|
292
|
+
"profiles": {
|
|
293
|
+
"worker": {
|
|
294
|
+
"provider": "anthropic",
|
|
295
|
+
"model": "claude-haiku-4-5",
|
|
296
|
+
"thinkingLevel": "medium",
|
|
297
|
+
"tools": ["read", "bash", "edit", "write"]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Run state defaults to the `worker-graph` subdirectory of Pi's agent directory.
|
|
304
|
+
An optional `stateRoot` may be absolute or relative to the agent directory, but
|
|
305
|
+
the extension rejects the filesystem root and any path that is inside the target
|
|
306
|
+
checkout, including through an existing symlink.
|
|
307
|
+
`maxRetainedRuns` defaults to 64 and may be set from 1 through 256. Capacity is
|
|
308
|
+
a fixed set of slot files under `runs/slots`, so the limit is structural: at
|
|
309
|
+
most that many slots can exist, so at most that many runs can publish, and
|
|
310
|
+
concurrent creators are arbitrated by the filesystem rather than by counting —
|
|
311
|
+
an available slot is always claimed by exactly one of them. A slot is claimed by
|
|
312
|
+
hard-linking a record that is already complete on disk, so an interrupted
|
|
313
|
+
creation can never leave a slot that holds capacity without naming its owner.
|
|
314
|
+
Reaching the limit rejects the new graph; run state is never deleted
|
|
315
|
+
automatically.
|
|
316
|
+
|
|
317
|
+
A creation interrupted between claiming its slot and publishing its run leaves
|
|
318
|
+
the slot claimed. `/swarm runs` names everything holding capacity — each run
|
|
319
|
+
with its slot and creation time, a slot whose run was never published, and a
|
|
320
|
+
run whose slot is missing — and `/swarm delete <run-id>` removes a named run's
|
|
321
|
+
directory and its slot together. The same operations are exported as
|
|
322
|
+
`listRetainedRuns()` and `deleteRun()`. They are a command and not a parent
|
|
323
|
+
tool: deleting a run destroys the diagnostic state it was kept for, so it is an
|
|
324
|
+
operator's act and the model has no way to reach it. Cleanup is by name:
|
|
325
|
+
nothing decides on the operator's behalf which diagnostic state is worth
|
|
326
|
+
losing, so there is no deletion by age or by count. A run an orchestrator holds
|
|
327
|
+
is refused. The directory goes first and the slot second, so an interruption
|
|
328
|
+
strands a slot the store reports as reclaimable rather than leaving a published
|
|
329
|
+
run whose slot is missing: that disagreement stops the store admitting any new
|
|
330
|
+
work at all — rather than letting every waiting creator claim the same
|
|
331
|
+
apparently free capacity — until the two agree again.
|
|
332
|
+
|
|
333
|
+
Load the package and activate orchestration explicitly:
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
/swarm on
|
|
337
|
+
/swarm status
|
|
338
|
+
/swarm off
|
|
339
|
+
/swarm runs
|
|
340
|
+
/swarm usage <run-id>
|
|
341
|
+
/swarm delete <run-id>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The `--swarm` extension flag enables the mode at startup. While the mode is
|
|
345
|
+
off, the `worker_graph` tool is excluded from the active tool set. Enabling the
|
|
346
|
+
mode snapshots the active tools, disables the built-in `bash`, `edit`, and
|
|
347
|
+
`write` tools in the parent, and persists the mode state in the Pi session.
|
|
348
|
+
Turning it off restores the exact snapshot. When the configuration names an
|
|
349
|
+
`orchestrator` block, the same snapshot covers the parent's model and thinking
|
|
350
|
+
level, and enabling the mode is refused outright unless the mode first knows
|
|
351
|
+
the identifiers that put the session back, the configured model is one Pi can
|
|
352
|
+
find, and its provider has configured authentication; a refusal applies
|
|
353
|
+
nothing. Navigating the session tree
|
|
354
|
+
restores whatever the target branch recorded, so `/swarm off` is never undone
|
|
355
|
+
by the flag that started the session. A tool set the extension could not read
|
|
356
|
+
back — more than 256 tools, or a tool name longer than 256 bytes — refuses to
|
|
357
|
+
enable the mode rather than suppressing parent tools it could not restore after
|
|
358
|
+
a reload. While a graph runs, the tool streams bounded status and returns
|
|
359
|
+
deterministic node statuses, aggregate usage, and a compact bounded projection
|
|
360
|
+
of worker reports. Worker transcripts never enter the parent model context;
|
|
361
|
+
projected report fields are marked as untrusted data inside a labeled block
|
|
362
|
+
that worker text cannot close.
|
|
363
|
+
|
|
364
|
+
Only one graph may run in a parent session at a time. Include validation as a
|
|
365
|
+
dependent worker task. After reviewing the shared checkout with the remaining
|
|
366
|
+
read-only tools, invoke another narrow graph for any repairs.
|
|
367
|
+
|
|
368
|
+
## Cost and token accounting
|
|
369
|
+
|
|
370
|
+
Each worker's spend is recorded with the attempt that incurred it, in the node
|
|
371
|
+
output envelope: turns, input, output, cache-read and cache-write tokens, and
|
|
372
|
+
Pi's price for each. It is recorded for every terminal status,
|
|
373
|
+
including a task the run aborted, because an attempt that was stopped had still
|
|
374
|
+
spent what it spent by then. A failed worker reports its spend on
|
|
375
|
+
`TaskExecutionFailure`, and a timeout or an abort — where the runner discards
|
|
376
|
+
the executor's own outcome — still keeps the usage the executor reported.
|
|
377
|
+
|
|
378
|
+
Because it is persisted rather than only reported to the session, spend
|
|
379
|
+
survives the run. `/swarm usage <run-id>` sums a retained run and breaks it
|
|
380
|
+
down by task, and `readRunUsage()` returns the same thing to a library caller.
|
|
381
|
+
The total is derived from the outputs the run published, so it is correct for
|
|
382
|
+
an interrupted run; a task with no readable output is named as unaccounted
|
|
383
|
+
rather than counted as work that was free.
|
|
384
|
+
|
|
385
|
+
The `worker_graph` tool result carries the same numbers per node, so an
|
|
386
|
+
orchestrator can see which worker was expensive rather than only what the graph
|
|
387
|
+
cost in total. Its aggregate comes from live progress instead of the store, so
|
|
388
|
+
it still accounts for a task whose output could not be persisted.
|
|
389
|
+
|
|
390
|
+
Token counts come from the provider's telemetry; cost is Pi's pricing of those
|
|
391
|
+
tokens, so it is only as good as Pi's pricing table. Treat cost as an estimate
|
|
392
|
+
and tokens as the sturdier number.
|
|
393
|
+
|
|
394
|
+
Zero and unknown are kept apart. A worker whose provider reported no usage, or
|
|
395
|
+
reported a field that was not a usable number, records no usage at all rather
|
|
396
|
+
than a complete-looking set of zeros, and `/swarm usage` names it as
|
|
397
|
+
unaccounted. A task that never ran is reported separately again: it is not a
|
|
398
|
+
gap in the accounting. The live aggregate on the tool result is best-effort by
|
|
399
|
+
contrast — it projects the running figures whether or not they turned out to be
|
|
400
|
+
usable, which is what makes it a superset of what was persisted.
|
|
401
|
+
|
|
402
|
+
The runtime bounds tasks, concurrency, payload, output, context, and runtime,
|
|
403
|
+
but does not yet enforce a token or cost ceiling.
|
|
404
|
+
|
|
405
|
+
## Planned runtime
|
|
406
|
+
|
|
407
|
+
The remaining runtime will add:
|
|
408
|
+
|
|
409
|
+
- a configured token or cost budget, enforced against recorded usage;
|
|
410
|
+
- persisted worker attempts and interrupted-run recovery behavior.
|
|
411
|
+
|
|
412
|
+
Writable workers will intentionally share one checkout. The runtime will not
|
|
413
|
+
create worktrees or perform automatic branches, commits, merges, stashes, resets,
|
|
414
|
+
restores, cleans, or pushes.
|
|
415
|
+
|
|
416
|
+
## Development
|
|
417
|
+
|
|
418
|
+
Requires Node.js 22.19 or newer. Pi integration is currently tested against
|
|
419
|
+
`@earendil-works/pi-coding-agent` 0.85.1; the peer dependency follows Pi package
|
|
420
|
+
conventions and compatibility outside the tested version is not yet guaranteed.
|
|
421
|
+
|
|
422
|
+
For a local source checkout, install dependencies and build before loading the
|
|
423
|
+
package:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
npm install
|
|
427
|
+
npm run check
|
|
428
|
+
npm run build
|
|
429
|
+
pi -e /absolute/path/to/pi-worker-graph
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
After version `0.1.0` is published, install that exact packaged build with:
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
pi install npm:pi-worker-graph@0.1.0
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
The npm artifact contains the compiled `dist/` tree. Do not install directly
|
|
439
|
+
from Git until the repository has a production-safe build lifecycle for Pi's
|
|
440
|
+
`--omit=dev` package installation path.
|
|
441
|
+
|
|
442
|
+
See [`docs/NEXT.md`](docs/NEXT.md) for current development status,
|
|
443
|
+
[`docs/PLAN.md`](docs/PLAN.md) for the implementation sequence, and
|
|
444
|
+
[`docs/DESIGN.md`](docs/DESIGN.md) for the proposed runtime contract.
|
|
445
|
+
|
|
446
|
+
## Security
|
|
447
|
+
|
|
448
|
+
Pi packages execute with the permissions of the Pi process. Concurrent writable
|
|
449
|
+
workers are not sandboxed and can conflict even when tasks appear independent.
|
|
450
|
+
Review the source and use the runtime only in checkouts where this operating model
|
|
451
|
+
is acceptable. See [`SECURITY.md`](SECURITY.md).
|
|
452
|
+
|
|
453
|
+
## License
|
|
454
|
+
|
|
455
|
+
[MIT](LICENSE)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report suspected vulnerabilities privately through the repository's
|
|
6
|
+
GitHub Security Advisory page. Do not include credentials, private source code,
|
|
7
|
+
or sensitive run artifacts in a public issue.
|
|
8
|
+
|
|
9
|
+
## Security model
|
|
10
|
+
|
|
11
|
+
Pi packages execute with the permissions of the Pi process. This project does
|
|
12
|
+
not provide a sandbox for workers. Worker subprocesses inherit the parent
|
|
13
|
+
environment so Pi can resolve configured provider credentials, and writable tools
|
|
14
|
+
can access everything permitted to the parent operating-system user. Task content
|
|
15
|
+
is sent over stdin rather than process arguments, but provider requests and tool
|
|
16
|
+
activity still handle repository data. Repository `AGENTS.md`/`CLAUDE.md` context
|
|
17
|
+
files remain enabled intentionally and must be treated as trusted worker
|
|
18
|
+
instructions.
|
|
19
|
+
|
|
20
|
+
Worker profiles are loaded from the global Pi agent directory, not from the
|
|
21
|
+
target checkout. The configuration must not contain provider credentials or
|
|
22
|
+
other secrets. Progress projection retains only bounded task status, allowlisted
|
|
23
|
+
tool names, and numeric usage; worker messages, tool arguments, tool results, and
|
|
24
|
+
stderr are not forwarded into the parent model context. The final result carries
|
|
25
|
+
a bounded projection of worker reports inside a labeled block; every `<` in that
|
|
26
|
+
serialization is escaped, so worker-authored text cannot close the block and
|
|
27
|
+
address the parent as something other than untrusted data.
|
|
28
|
+
|
|
29
|
+
Run state defaults beneath the global Pi agent directory. Explicit state roots
|
|
30
|
+
cannot be the filesystem root or resolve into the target checkout through an
|
|
31
|
+
existing symlink; operators should still choose a private, access-controlled
|
|
32
|
+
directory outside repositories. Retained runs have a configurable hard count
|
|
33
|
+
limit enforced by atomically claimed capacity slots, and reaching it rejects
|
|
34
|
+
new work rather than deleting prior state. No elapsed-time heuristic can release
|
|
35
|
+
a claimed slot, so a slow creator is never displaced by a second one, and a
|
|
36
|
+
published run whose slot is missing stops the store from admitting new work
|
|
37
|
+
instead of handing the same free capacity to several creators at once.
|
|
38
|
+
|
|
39
|
+
Swarm mode removes Pi's built-in `bash`, `edit`, and `write` tools from the
|
|
40
|
+
parent tool set and restores the exact pre-mode snapshot on exit. Pi does not
|
|
41
|
+
label arbitrary extension tools as read-only or writable, so separately
|
|
42
|
+
installed third-party tools remain the operator's responsibility.
|
|
43
|
+
|
|
44
|
+
Workers are started without a command interpreter: the executable is Pi's own
|
|
45
|
+
CLI entry point, resolved through this package's dependency on Pi, and every
|
|
46
|
+
argument is passed through `spawn` with `shell: false`. Configured provider and
|
|
47
|
+
model values are restricted to a plain-identifier allowlist, and Pi's own
|
|
48
|
+
`PI_CODING_AGENT` variable is never treated as proof of what the current process
|
|
49
|
+
is, because Pi exports it to everything it launches.
|
|
50
|
+
|
|
51
|
+
Review the source before installation, use least-privilege credentials, and run
|
|
52
|
+
concurrent writable workers only in a checkout where overlapping edits are
|
|
53
|
+
acceptable. Process isolation, tool allowlists, and disabled child session
|
|
54
|
+
persistence reduce accidental coupling; they are not security boundaries.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PiOrchestratorProfile, PiWorkerProfile } from "./pi-subprocess.js";
|
|
2
|
+
export declare const WORKER_GRAPH_CONFIG_FILENAME = "worker-graph.json";
|
|
3
|
+
export declare const WORKER_GRAPH_DEFAULT_STATE_DIRECTORY = "worker-graph";
|
|
4
|
+
export declare const WORKER_GRAPH_CONFIG_MAX_BYTES: number;
|
|
5
|
+
export type WorkerGraphConfigurationErrorCode = "missing" | "too_large" | "malformed" | "invalid" | "unsafe_state_root";
|
|
6
|
+
export declare class WorkerGraphConfigurationError extends Error {
|
|
7
|
+
readonly code: WorkerGraphConfigurationErrorCode;
|
|
8
|
+
constructor(code: WorkerGraphConfigurationErrorCode);
|
|
9
|
+
}
|
|
10
|
+
export interface WorkerGraphConfiguration {
|
|
11
|
+
readonly stateRoot: string;
|
|
12
|
+
readonly maxRetainedRuns: number;
|
|
13
|
+
/**
|
|
14
|
+
* The parent session's own model while the mode is active. Absent leaves the
|
|
15
|
+
* session exactly as the operator started it.
|
|
16
|
+
*/
|
|
17
|
+
readonly orchestrator?: PiOrchestratorProfile;
|
|
18
|
+
readonly profiles: Readonly<Record<string, PiWorkerProfile>>;
|
|
19
|
+
}
|
|
20
|
+
export interface LoadWorkerGraphConfigurationOptions {
|
|
21
|
+
readonly agentDirectory: string;
|
|
22
|
+
readonly workingDirectory: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function loadWorkerGraphConfiguration(options: LoadWorkerGraphConfigurationOptions): Promise<WorkerGraphConfiguration>;
|
|
25
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,qBAAqB,EACrB,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAO5B,eAAO,MAAM,4BAA4B,sBAAsB,CAAC;AAChE,eAAO,MAAM,oCAAoC,iBAAiB,CAAC;AACnE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAWvD,MAAM,MAAM,iCAAiC,GACzC,SAAS,GACT,WAAW,GACX,WAAW,GACX,SAAS,GACT,mBAAmB,CAAC;AAYxB,qBAAa,6BAA8B,SAAQ,KAAK;IACtD,QAAQ,CAAC,IAAI,EAAE,iCAAiC,CAAC;gBAErC,IAAI,EAAE,iCAAiC;CAKpD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAyJD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,mCAAmC,GAC3C,OAAO,CAAC,wBAAwB,CAAC,CA2BnC"}
|