@sebastianandreasson/pi-autonomous-agents 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -8
- package/SETUP.md +5 -0
- package/docs/PI_SUPERVISOR.md +14 -64
- package/package.json +6 -3
- package/pi.config.json +1 -2
- package/src/cli.mjs +50 -2
- package/src/index.mjs +2 -0
- package/src/pi-client.mjs +68 -119
- package/src/pi-config.mjs +3 -3
- package/src/pi-repo.mjs +92 -11
- package/src/pi-sdk-turn.mjs +654 -0
- package/src/pi-supervisor.mjs +85 -4
- package/src/pi-telemetry.mjs +4 -0
- package/src/pi-visualizer-shared.mjs +219 -0
- package/src/pi-visualizer.mjs +476 -0
- package/templates/pi.config.example.json +1 -2
- package/src/pi-rpc-adapter.mjs +0 -606
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ The package ships a top-level [SETUP.md](./SETUP.md) specifically for that workf
|
|
|
29
29
|
## What This Package Owns
|
|
30
30
|
|
|
31
31
|
- unattended loop orchestration
|
|
32
|
-
- PI
|
|
32
|
+
- PI Node SDK integration
|
|
33
33
|
- config loading
|
|
34
34
|
- prompt assembly
|
|
35
35
|
- verification/tester/visual-review handoff
|
|
@@ -60,6 +60,10 @@ pi/
|
|
|
60
60
|
|
|
61
61
|
Typical scripts:
|
|
62
62
|
|
|
63
|
+
- `pi:once` / `pi:run` use default `sdk` transport
|
|
64
|
+
- `pi:mock` skips real agent execution
|
|
65
|
+
|
|
66
|
+
|
|
63
67
|
```json
|
|
64
68
|
{
|
|
65
69
|
"scripts": {
|
|
@@ -67,7 +71,8 @@ Typical scripts:
|
|
|
67
71
|
"pi:once": "PI_CONFIG_FILE=pi.config.json pi-harness once",
|
|
68
72
|
"pi:run": "PI_CONFIG_FILE=pi.config.json pi-harness run",
|
|
69
73
|
"pi:report": "PI_CONFIG_FILE=pi.config.json pi-harness report",
|
|
70
|
-
"pi:visual:once": "PI_CONFIG_FILE=pi.config.json pi-harness visual-once"
|
|
74
|
+
"pi:visual:once": "PI_CONFIG_FILE=pi.config.json pi-harness visual-once",
|
|
75
|
+
"pi:visualize": "PI_CONFIG_FILE=pi.config.json pi-harness visualize"
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
```
|
|
@@ -82,7 +87,7 @@ pi-harness run
|
|
|
82
87
|
pi-harness report
|
|
83
88
|
pi-harness clear-history
|
|
84
89
|
pi-harness visual-once
|
|
85
|
-
pi-harness
|
|
90
|
+
pi-harness visualize
|
|
86
91
|
pi-harness visual-review-worker
|
|
87
92
|
```
|
|
88
93
|
|
|
@@ -115,6 +120,7 @@ The package supports:
|
|
|
115
120
|
- one default visual-review model via `visualReviewModel`
|
|
116
121
|
- optional per-role overrides via `roleModels`
|
|
117
122
|
- per-model endpoint config in `models`
|
|
123
|
+
- default transport via `transport` (`sdk` or `mock`)
|
|
118
124
|
|
|
119
125
|
Typical pattern:
|
|
120
126
|
|
|
@@ -173,8 +179,7 @@ Common fields in `pi.config.json`:
|
|
|
173
179
|
- `taskFile`
|
|
174
180
|
- `developerInstructionsFile`
|
|
175
181
|
- `testerInstructionsFile`
|
|
176
|
-
- `transport`
|
|
177
|
-
- `adapterCommand`
|
|
182
|
+
- `transport` (`sdk` or `mock`)
|
|
178
183
|
- `piModel`
|
|
179
184
|
- `models`
|
|
180
185
|
- `roleModels`
|
|
@@ -192,7 +197,7 @@ Common fields in `pi.config.json`:
|
|
|
192
197
|
|
|
193
198
|
Key defaults:
|
|
194
199
|
|
|
195
|
-
- `transport`: `
|
|
200
|
+
- `transport`: `sdk`
|
|
196
201
|
- `commitMode`: `agent`
|
|
197
202
|
- `promptMode`: `compact`
|
|
198
203
|
- `piTools`: `read,edit,write,find,ls,bash`
|
|
@@ -209,7 +214,7 @@ The package is optimized for local models by default:
|
|
|
209
214
|
- changed-file lists and feedback excerpts are capped
|
|
210
215
|
- prompts prefer `read` for source inspection
|
|
211
216
|
- shell is intended for `git`, tests, and narrow diagnostics
|
|
212
|
-
-
|
|
217
|
+
- SDK transport carries forward oversized shell-read warnings and loop/timeout guards
|
|
213
218
|
- the supervisor emits large-file/spec warnings when touched files are getting risky
|
|
214
219
|
|
|
215
220
|
This is deliberate. Large monolith files, huge e2e specs, and broad TODO items are one of the main causes of local-model drift and retry loops.
|
|
@@ -232,6 +237,7 @@ Recent versions of the package isolate each run more aggressively:
|
|
|
232
237
|
- in-progress iteration state persisted before agent work starts
|
|
233
238
|
- stale run locks recovered when the owning PID is gone
|
|
234
239
|
- timeout cleanup kills the full spawned process group, not only the direct child
|
|
240
|
+
- parent-death watchers shut down orphaned supervisor layers instead of letting them continue under `PPID 1`
|
|
235
241
|
|
|
236
242
|
That is meant to prevent orphaned timed-out agents or concurrent supervisors from corrupting shared state.
|
|
237
243
|
|
|
@@ -258,6 +264,15 @@ Useful files during a run:
|
|
|
258
264
|
|
|
259
265
|
`pi-harness report` summarizes recent telemetry and surfaces things like terminal reasons and large-file warnings.
|
|
260
266
|
|
|
267
|
+
`pi-harness visualize` starts lightweight local web UI for orchestration flow. By default it listens on `127.0.0.1:4317`. Override with `PI_VISUALIZER_HOST` and `PI_VISUALIZER_PORT`.
|
|
268
|
+
|
|
269
|
+
Visualizer now includes:
|
|
270
|
+
- run history selector from `.pi-runtime/runs/`
|
|
271
|
+
- orchestration flow strip
|
|
272
|
+
- per-iteration stage graph with retries/rechecks
|
|
273
|
+
- clickable graph nodes and timeline rows that show full event JSON
|
|
274
|
+
- historical run summaries and per-run last output snapshots
|
|
275
|
+
|
|
261
276
|
## Visual Review Contract
|
|
262
277
|
|
|
263
278
|
Visual review is optional and generic. The harness does not know how to navigate your app.
|
|
@@ -281,7 +296,7 @@ That clears configured harness runtime/history artifacts and verifies they are g
|
|
|
281
296
|
- [SETUP.md](./SETUP.md)
|
|
282
297
|
Agent-facing setup instructions for consuming repos.
|
|
283
298
|
- [docs/PI_SUPERVISOR.md](./docs/PI_SUPERVISOR.md)
|
|
284
|
-
More detailed flow,
|
|
299
|
+
More detailed flow, transport, telemetry, and runtime documentation.
|
|
285
300
|
- [templates/PROJECT_SETUP.md](./templates/PROJECT_SETUP.md)
|
|
286
301
|
Minimal consuming-repo layout summary.
|
|
287
302
|
|
package/SETUP.md
CHANGED
|
@@ -82,6 +82,9 @@ Minimal example:
|
|
|
82
82
|
|
|
83
83
|
5. Add package scripts.
|
|
84
84
|
|
|
85
|
+
- `pi:once` and `pi:run` should use default `sdk` transport unless the repo has a very specific reason not to.
|
|
86
|
+
- `pi:mock` is for setup validation when real PI execution is not ready yet.
|
|
87
|
+
|
|
85
88
|
Add these scripts to the consuming repo `package.json`, adapting only if necessary:
|
|
86
89
|
|
|
87
90
|
```json
|
|
@@ -174,6 +177,8 @@ If the repo is not ready for a real run yet, at minimum run:
|
|
|
174
177
|
PI_CONFIG_FILE=pi.config.json PI_TRANSPORT=mock PI_TEST_CMD= pi-harness once
|
|
175
178
|
```
|
|
176
179
|
|
|
180
|
+
Default transport is `sdk`. Only set `PI_TRANSPORT` when you explicitly want `mock`.
|
|
181
|
+
|
|
177
182
|
If setup validation fails, fix the config rather than leaving a half-configured repo.
|
|
178
183
|
|
|
179
184
|
The harness should fail fast if:
|
package/docs/PI_SUPERVISOR.md
CHANGED
|
@@ -28,13 +28,15 @@ Main package files:
|
|
|
28
28
|
|
|
29
29
|
- `src/pi-supervisor.mjs`: controller
|
|
30
30
|
- `src/pi-client.mjs`: transport layer
|
|
31
|
-
- `src/pi-
|
|
31
|
+
- `src/pi-sdk-turn.mjs`: in-process PI SDK turn runner for `transport: "sdk"`
|
|
32
32
|
- `src/pi-config.mjs`: config loader
|
|
33
|
-
- `src/pi-repo.mjs`: repo helpers, verification runner, and
|
|
33
|
+
- `src/pi-repo.mjs`: repo helpers, verification runner, and git finalize helpers
|
|
34
34
|
- `src/pi-telemetry.mjs`: telemetry writer/reader
|
|
35
35
|
- `src/pi-prompts.mjs`: default prompt builders
|
|
36
36
|
- `src/pi-visual-review.mjs`: multimodal visual-review worker
|
|
37
37
|
- `src/pi-visual-once.mjs`: one-shot manual visual review runner
|
|
38
|
+
- `src/pi-visualizer.mjs`: local web UI for orchestration flow and active stage
|
|
39
|
+
- `src/pi-visualizer-shared.mjs`: flow-state helpers for visualizer
|
|
38
40
|
- `src/pi-report.mjs`: telemetry summary report
|
|
39
41
|
- `templates/DEVELOPER.md`: default developer-role instructions template
|
|
40
42
|
- `templates/TESTER.md`: default tester-role instructions template
|
|
@@ -46,10 +48,15 @@ pi-harness once
|
|
|
46
48
|
pi-harness run
|
|
47
49
|
pi-harness report
|
|
48
50
|
pi-harness visual-once
|
|
51
|
+
pi-harness visualize
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
The package reads `PI_CONFIG_FILE` if provided. Otherwise it falls back to the bundled generic `pi.config.json`.
|
|
52
55
|
|
|
56
|
+
`pi-harness visualize` serves polling web UI over local HTTP. Defaults: `PI_VISUALIZER_HOST=127.0.0.1`, `PI_VISUALIZER_PORT=4317`.
|
|
57
|
+
|
|
58
|
+
It reads active-run lock, per-run state, per-run iteration summary, per-run last output snapshot, and telemetry to show current stage plus historical runs.
|
|
59
|
+
|
|
53
60
|
## Config Contract
|
|
54
61
|
|
|
55
62
|
Projects typically provide their own `pi.config.json` with fields such as:
|
|
@@ -98,67 +105,9 @@ For unattended inner-loop work, `testCommand` should be a bounded smoke gate rat
|
|
|
98
105
|
The supervisor supports:
|
|
99
106
|
|
|
100
107
|
- `PI_TRANSPORT=mock`
|
|
101
|
-
- `PI_TRANSPORT=
|
|
102
|
-
|
|
103
|
-
The built-in adapter command is typically:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
pi-harness adapter
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
When using `adapter`, set `PI_ADAPTER_COMMAND` to a command that:
|
|
110
|
-
|
|
111
|
-
1. Reads one JSON request from `stdin`
|
|
112
|
-
2. Talks to PI RPC or your own PI wrapper
|
|
113
|
-
3. Writes one JSON response to `stdout`
|
|
114
|
-
4. Exits with code `0` on success
|
|
115
|
-
|
|
116
|
-
Request shape:
|
|
117
|
-
|
|
118
|
-
```json
|
|
119
|
-
{
|
|
120
|
-
"sessionId": "existing-or-empty",
|
|
121
|
-
"sessionFile": "/absolute/path/to/session.jsonl",
|
|
122
|
-
"prompt": "controller prompt",
|
|
123
|
-
"cwd": "/absolute/repo/path",
|
|
124
|
-
"taskFile": "/absolute/repo/path/TODOS.md",
|
|
125
|
-
"instructionsFile": "/absolute/repo/path/pi/DEVELOPER.md",
|
|
126
|
-
"runtimeDir": "/absolute/repo/path/.pi-runtime",
|
|
127
|
-
"piCli": "pi",
|
|
128
|
-
"model": "local/model-name",
|
|
129
|
-
"tools": "read,edit,write,find,ls,bash",
|
|
130
|
-
"thinking": "",
|
|
131
|
-
"noExtensions": false,
|
|
132
|
-
"noSkills": false,
|
|
133
|
-
"noPromptTemplates": false,
|
|
134
|
-
"noThemes": true,
|
|
135
|
-
"metadata": {
|
|
136
|
-
"iteration": 1,
|
|
137
|
-
"retryCount": 0,
|
|
138
|
-
"reason": "main_workflow"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Response shape:
|
|
144
|
-
|
|
145
|
-
```json
|
|
146
|
-
{
|
|
147
|
-
"sessionId": "stable-session-id",
|
|
148
|
-
"sessionFile": "/absolute/path/to/session.jsonl",
|
|
149
|
-
"status": "success",
|
|
150
|
-
"output": "agent output text",
|
|
151
|
-
"notes": "short controller note"
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Allowed response `status` values:
|
|
108
|
+
- `PI_TRANSPORT=sdk` (default)
|
|
156
109
|
|
|
157
|
-
- `
|
|
158
|
-
- `stalled`
|
|
159
|
-
- `timed_out`
|
|
160
|
-
- `failed`
|
|
161
|
-
- `canceled`
|
|
110
|
+
`sdk` runs PI in-process via Node SDK. `mock` preserves harness flow but skips real agent work.
|
|
162
111
|
|
|
163
112
|
## Git Finalization
|
|
164
113
|
|
|
@@ -215,15 +164,16 @@ The capture command must write a JSON manifest at `PI_VISUAL_MANIFEST_FILE` with
|
|
|
215
164
|
|
|
216
165
|
## Loop Mitigation
|
|
217
166
|
|
|
218
|
-
|
|
167
|
+
SDK transport mitigates obvious local loops by watching agent and tool events:
|
|
219
168
|
|
|
220
169
|
- repeated identical tool calls are aborted
|
|
221
170
|
- repeated same-path churn is aborted
|
|
222
171
|
- a soft `continue` can be sent after inactivity
|
|
223
172
|
- a separate tool-aware watchdog can tolerate long-running `bash` or browser work without treating the turn as dead
|
|
224
173
|
- a hard no-event timeout aborts a wedged turn instead of hanging indefinitely
|
|
174
|
+
- parent-loss shutdown tears down owned supervisor child work instead of allowing orphaned background runs
|
|
225
175
|
|
|
226
|
-
Important: terminal streaming does not reset the heartbeat by itself. The watchdog keys off
|
|
176
|
+
Important: terminal streaming does not reset the heartbeat by itself. The watchdog keys off SDK agent events and active tool state, not raw shell output.
|
|
227
177
|
|
|
228
178
|
## Telemetry
|
|
229
179
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebastianandreasson/pi-autonomous-agents",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Portable unattended PI harness for developer/tester/visual-review loops.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"pi-harness": "./src/cli.mjs"
|
|
17
17
|
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@mariozechner/pi-coding-agent": "^0.66.1"
|
|
20
|
+
},
|
|
18
21
|
"scripts": {
|
|
19
|
-
"check": "node --check src/cli.mjs && node --check src/pi-clear-history.mjs && node --check src/pi-client.mjs && node --check src/pi-config.mjs && node --check src/pi-flow.mjs && node --check src/pi-heartbeat.mjs && node --check src/pi-history.mjs && node --check src/pi-preflight.mjs && node --check src/pi-prompts.mjs && node --check src/pi-repo.mjs && node --check src/pi-report.mjs && node --check src/pi-
|
|
20
|
-
"test": "node --test test/pi-heartbeat.test.mjs test/pi-role-models.test.mjs test/pi-flow.test.mjs test/pi-history.test.mjs test/pi-prompts.test.mjs test/pi-preflight.test.mjs test/pi-repo.test.mjs test/pi-telemetry.test.mjs"
|
|
22
|
+
"check": "node --check src/cli.mjs && node --check src/pi-clear-history.mjs && node --check src/pi-client.mjs && node --check src/pi-config.mjs && node --check src/pi-flow.mjs && node --check src/pi-heartbeat.mjs && node --check src/pi-history.mjs && node --check src/pi-preflight.mjs && node --check src/pi-prompts.mjs && node --check src/pi-repo.mjs && node --check src/pi-report.mjs && node --check src/pi-sdk-turn.mjs && node --check src/pi-supervisor.mjs && node --check src/pi-telemetry.mjs && node --check src/pi-visual-once.mjs && node --check src/pi-visual-review.mjs && node --check src/pi-visualizer.mjs && node --check src/pi-visualizer-shared.mjs && node --check src/index.mjs && node --check test/pi-heartbeat.test.mjs && node --check test/pi-lifecycle.test.mjs && node --check test/pi-role-models.test.mjs && node --check test/pi-flow.test.mjs && node --check test/pi-history.test.mjs && node --check test/pi-prompts.test.mjs && node --check test/pi-preflight.test.mjs && node --check test/pi-repo.test.mjs && node --check test/pi-sdk-supervisor.test.mjs && node --check test/pi-sdk-turn.test.mjs && node --check test/pi-telemetry.test.mjs && node --check test/pi-visualizer-shared.test.mjs && node --check test/fixtures/fake-pi.mjs && node --check test/fixtures/fake-pi-sdk.mjs",
|
|
23
|
+
"test": "node --test test/pi-heartbeat.test.mjs test/pi-lifecycle.test.mjs test/pi-role-models.test.mjs test/pi-flow.test.mjs test/pi-history.test.mjs test/pi-prompts.test.mjs test/pi-preflight.test.mjs test/pi-repo.test.mjs test/pi-sdk-supervisor.test.mjs test/pi-sdk-turn.test.mjs test/pi-telemetry.test.mjs test/pi-visualizer-shared.test.mjs"
|
|
21
24
|
},
|
|
22
25
|
"files": [
|
|
23
26
|
"src",
|
package/pi.config.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -4,6 +4,11 @@ import path from 'node:path'
|
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import process from 'node:process'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import {
|
|
8
|
+
registerOwnedChildProcess,
|
|
9
|
+
signalChildProcess,
|
|
10
|
+
watchParentProcess,
|
|
11
|
+
} from './pi-repo.mjs'
|
|
7
12
|
|
|
8
13
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
9
14
|
|
|
@@ -13,7 +18,7 @@ const COMMANDS = new Map([
|
|
|
13
18
|
['report', 'pi-report.mjs'],
|
|
14
19
|
['clear-history', 'pi-clear-history.mjs'],
|
|
15
20
|
['visual-once', 'pi-visual-once.mjs'],
|
|
16
|
-
['
|
|
21
|
+
['visualize', 'pi-visualizer.mjs'],
|
|
17
22
|
['visual-review-worker', 'pi-visual-review.mjs'],
|
|
18
23
|
])
|
|
19
24
|
|
|
@@ -36,10 +41,53 @@ function main() {
|
|
|
36
41
|
env: process.env,
|
|
37
42
|
stdio: 'inherit',
|
|
38
43
|
})
|
|
44
|
+
registerOwnedChildProcess(child)
|
|
45
|
+
|
|
46
|
+
let shuttingDown = false
|
|
47
|
+
let forceKillTimer = null
|
|
48
|
+
const stopWatchingParent = watchParentProcess(() => {
|
|
49
|
+
shutdown({
|
|
50
|
+
signal: 'SIGTERM',
|
|
51
|
+
exitCode: 1,
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function shutdown({
|
|
56
|
+
signal,
|
|
57
|
+
exitCode,
|
|
58
|
+
}) {
|
|
59
|
+
if (shuttingDown) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
shuttingDown = true
|
|
64
|
+
stopWatchingParent()
|
|
65
|
+
signalChildProcess(child.pid, signal)
|
|
66
|
+
forceKillTimer = setTimeout(() => {
|
|
67
|
+
signalChildProcess(child.pid, 'SIGKILL')
|
|
68
|
+
}, 1000)
|
|
69
|
+
if (typeof forceKillTimer.unref === 'function') {
|
|
70
|
+
forceKillTimer.unref()
|
|
71
|
+
}
|
|
72
|
+
process.exitCode = exitCode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
76
|
+
process.on(signal, () => {
|
|
77
|
+
shutdown({
|
|
78
|
+
signal,
|
|
79
|
+
exitCode: 128,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
}
|
|
39
83
|
|
|
40
84
|
child.on('exit', (code, signal) => {
|
|
85
|
+
stopWatchingParent()
|
|
86
|
+
if (forceKillTimer) {
|
|
87
|
+
clearTimeout(forceKillTimer)
|
|
88
|
+
}
|
|
41
89
|
if (signal) {
|
|
42
|
-
process.
|
|
90
|
+
process.exitCode = 128
|
|
43
91
|
return
|
|
44
92
|
}
|
|
45
93
|
process.exitCode = code ?? 1
|
package/src/index.mjs
CHANGED
|
@@ -12,3 +12,5 @@ export {
|
|
|
12
12
|
export { clearHarnessHistory, collectHistoryTargets } from './pi-history.mjs'
|
|
13
13
|
export { collectLargeFileWarnings } from './pi-repo.mjs'
|
|
14
14
|
export { runAgentTurn } from './pi-client.mjs'
|
|
15
|
+
export { createSdkSession, createTools, normalizeToolNames, resolveModel, runSdkTurn, runSdkTurnWithPi, splitModelSpec } from './pi-sdk-turn.mjs'
|
|
16
|
+
export { deriveCurrentIteration, deriveFlowSnapshot, deriveStageGraph, formatActiveLabel, getFlowSteps, getLabelForKind, getStepKeyForActiveRun, getStepKeyForKind } from './pi-visualizer-shared.mjs'
|
package/src/pi-client.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import {
|
|
3
3
|
appendLog,
|
|
4
|
-
runShellCommand,
|
|
5
4
|
writeTextFile,
|
|
6
5
|
} from './pi-repo.mjs'
|
|
6
|
+
import { runSdkTurn } from './pi-sdk-turn.mjs'
|
|
7
7
|
|
|
8
8
|
function truncateForNotes(text) {
|
|
9
9
|
const trimmed = text.trim()
|
|
@@ -30,6 +30,13 @@ function formatLastAgentOutput(response) {
|
|
|
30
30
|
return `${sections.join('\n')}\n`
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
async function writeAgentOutputSnapshot(config, content) {
|
|
34
|
+
await writeTextFile(config.lastAgentOutputFile, content)
|
|
35
|
+
if (config.runLastAgentOutputFile && config.runLastAgentOutputFile !== config.lastAgentOutputFile) {
|
|
36
|
+
await writeTextFile(config.runLastAgentOutputFile, content)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
async function runMockTurn({ config, sessionId, sessionFile, prompt, reason }) {
|
|
34
41
|
const nextSessionId = sessionId || `mock-${randomUUID()}`
|
|
35
42
|
const nextSessionFile = sessionFile || `${config.piRuntimeDir}/mock-${nextSessionId}.jsonl`
|
|
@@ -40,10 +47,10 @@ async function runMockTurn({ config, sessionId, sessionFile, prompt, reason }) {
|
|
|
40
47
|
'Prompt preview:',
|
|
41
48
|
prompt,
|
|
42
49
|
'',
|
|
43
|
-
'Mock mode does not edit files.
|
|
50
|
+
'Mock mode does not edit files. Use default sdk transport for real unattended work.',
|
|
44
51
|
].join('\n')
|
|
45
52
|
|
|
46
|
-
await
|
|
53
|
+
await writeAgentOutputSnapshot(config, `${output}\n`)
|
|
47
54
|
await appendLog(config.logFile, `Mock agent turn completed for session ${nextSessionId}`)
|
|
48
55
|
if (config.streamTerminal) {
|
|
49
56
|
process.stderr.write(`[PI mock] ${reason}\n`)
|
|
@@ -71,111 +78,60 @@ async function runMockTurn({ config, sessionId, sessionFile, prompt, reason }) {
|
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
function
|
|
75
|
-
const trimmed = stdout.trim()
|
|
76
|
-
if (trimmed === '') {
|
|
77
|
-
throw new Error('Adapter returned no JSON on stdout.')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
return JSON.parse(trimmed)
|
|
82
|
-
} catch {
|
|
83
|
-
const lines = trimmed.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
84
|
-
const lastLine = lines.at(-1)
|
|
85
|
-
if (!lastLine) {
|
|
86
|
-
throw new Error('Adapter returned no parseable JSON on stdout.')
|
|
87
|
-
}
|
|
88
|
-
return JSON.parse(lastLine)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function runAdapterTurn({ config, model, sessionId, sessionFile, prompt, iteration, retryCount, reason }) {
|
|
93
|
-
if (config.adapterCommand.trim() === '') {
|
|
94
|
-
throw new Error('PI_TRANSPORT=adapter requires PI_ADAPTER_COMMAND to be set.')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const request = {
|
|
98
|
-
sessionId,
|
|
99
|
-
sessionFile,
|
|
100
|
-
prompt,
|
|
101
|
-
cwd: config.cwd,
|
|
102
|
-
taskFile: config.taskFile,
|
|
103
|
-
instructionsFile: config.instructionsFile,
|
|
104
|
-
developerInstructionsFile: config.developerInstructionsFile,
|
|
105
|
-
testerInstructionsFile: config.testerInstructionsFile,
|
|
106
|
-
runtimeDir: config.runRuntimeDir || config.piRuntimeDir,
|
|
107
|
-
piCli: config.piCli,
|
|
108
|
-
model: model ?? config.piModel,
|
|
109
|
-
tools: config.piTools,
|
|
110
|
-
thinking: config.piThinking,
|
|
111
|
-
noExtensions: config.piNoExtensions,
|
|
112
|
-
noSkills: config.piNoSkills,
|
|
113
|
-
noPromptTemplates: config.piNoPromptTemplates,
|
|
114
|
-
noThemes: config.piNoThemes,
|
|
115
|
-
streamTerminal: config.streamTerminal,
|
|
116
|
-
loopRepeatThreshold: config.loopRepeatThreshold,
|
|
117
|
-
samePathRepeatThreshold: config.samePathRepeatThreshold,
|
|
118
|
-
continueAfterSeconds: config.continueAfterSeconds,
|
|
119
|
-
toolContinueAfterSeconds: config.toolContinueAfterSeconds,
|
|
120
|
-
continueMessage: config.continueMessage,
|
|
121
|
-
noEventTimeoutSeconds: config.noEventTimeoutSeconds,
|
|
122
|
-
toolNoEventTimeoutSeconds: config.toolNoEventTimeoutSeconds,
|
|
123
|
-
metadata: {
|
|
124
|
-
iteration,
|
|
125
|
-
retryCount,
|
|
126
|
-
reason,
|
|
127
|
-
},
|
|
128
|
-
}
|
|
129
|
-
|
|
81
|
+
async function runSdkTransportTurn({ config, model, sessionId, sessionFile, prompt, iteration, retryCount, reason }) {
|
|
130
82
|
await appendLog(
|
|
131
83
|
config.logFile,
|
|
132
|
-
`Starting
|
|
84
|
+
`Starting SDK turn iteration=${iteration} retry=${retryCount} reason=${reason}`
|
|
133
85
|
)
|
|
134
|
-
const result = await runShellCommand({
|
|
135
|
-
cwd: config.cwd,
|
|
136
|
-
command: config.adapterCommand,
|
|
137
|
-
timeoutSeconds: config.agentTimeoutSeconds,
|
|
138
|
-
stdinText: `${JSON.stringify(request)}\n`,
|
|
139
|
-
streamStderrToParent: config.streamTerminal,
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
await writeTextFile(config.lastAgentOutputFile, result.combinedOutput)
|
|
143
86
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
87
|
+
const startedAt = Date.now()
|
|
88
|
+
let response
|
|
89
|
+
try {
|
|
90
|
+
response = await runSdkTurn({
|
|
91
|
+
sessionId,
|
|
92
|
+
sessionFile,
|
|
93
|
+
prompt,
|
|
94
|
+
cwd: config.cwd,
|
|
95
|
+
taskFile: config.taskFile,
|
|
96
|
+
instructionsFile: config.instructionsFile,
|
|
97
|
+
developerInstructionsFile: config.developerInstructionsFile,
|
|
98
|
+
testerInstructionsFile: config.testerInstructionsFile,
|
|
99
|
+
runtimeDir: config.runRuntimeDir || config.piRuntimeDir,
|
|
100
|
+
piCli: config.piCli,
|
|
156
101
|
model: model ?? config.piModel,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
102
|
+
tools: config.piTools,
|
|
103
|
+
thinking: config.piThinking,
|
|
104
|
+
noExtensions: config.piNoExtensions,
|
|
105
|
+
noSkills: config.piNoSkills,
|
|
106
|
+
noPromptTemplates: config.piNoPromptTemplates,
|
|
107
|
+
noThemes: config.piNoThemes,
|
|
108
|
+
streamTerminal: config.streamTerminal,
|
|
109
|
+
loopRepeatThreshold: config.loopRepeatThreshold,
|
|
110
|
+
samePathRepeatThreshold: config.samePathRepeatThreshold,
|
|
111
|
+
continueAfterSeconds: config.continueAfterSeconds,
|
|
112
|
+
toolContinueAfterSeconds: config.toolContinueAfterSeconds,
|
|
113
|
+
continueMessage: config.continueMessage,
|
|
114
|
+
noEventTimeoutSeconds: config.noEventTimeoutSeconds,
|
|
115
|
+
toolNoEventTimeoutSeconds: config.toolNoEventTimeoutSeconds,
|
|
116
|
+
metadata: {
|
|
117
|
+
iteration,
|
|
118
|
+
retryCount,
|
|
119
|
+
reason,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const notes = error instanceof Error ? error.message : String(error)
|
|
124
|
+
await writeAgentOutputSnapshot(config, `${notes}\n`)
|
|
125
|
+
await appendLog(config.logFile, `SDK turn failed: ${notes}`)
|
|
170
126
|
return {
|
|
171
127
|
sessionId: sessionId || '',
|
|
172
128
|
sessionFile: sessionFile || '',
|
|
173
129
|
status: 'failed',
|
|
174
|
-
exitCode:
|
|
130
|
+
exitCode: 1,
|
|
175
131
|
timedOut: false,
|
|
176
|
-
durationSeconds:
|
|
177
|
-
output:
|
|
178
|
-
notes
|
|
132
|
+
durationSeconds: Math.max(0, Math.round((Date.now() - startedAt) / 1000)),
|
|
133
|
+
output: '',
|
|
134
|
+
notes,
|
|
179
135
|
role: '',
|
|
180
136
|
model: model ?? config.piModel,
|
|
181
137
|
toolCalls: 0,
|
|
@@ -184,29 +140,22 @@ async function runAdapterTurn({ config, model, sessionId, sessionFile, prompt, i
|
|
|
184
140
|
stopReason: '',
|
|
185
141
|
loopDetected: false,
|
|
186
142
|
loopSignature: '',
|
|
187
|
-
terminalReason: '
|
|
143
|
+
terminalReason: 'sdk_failed',
|
|
188
144
|
}
|
|
189
145
|
}
|
|
190
146
|
|
|
191
|
-
|
|
192
|
-
await
|
|
193
|
-
const nextSessionId = String(response.sessionId ?? sessionId ?? '')
|
|
194
|
-
const nextSessionFile = String(response.sessionFile ?? sessionFile ?? '')
|
|
195
|
-
const status = String(response.status ?? 'success')
|
|
196
|
-
const output = String(response.output ?? result.combinedOutput)
|
|
197
|
-
const notes = String(response.notes ?? truncateForNotes(output))
|
|
198
|
-
|
|
199
|
-
await appendLog(config.logFile, `Adapter turn completed with status ${status}`)
|
|
147
|
+
await writeAgentOutputSnapshot(config, formatLastAgentOutput(response))
|
|
148
|
+
await appendLog(config.logFile, `SDK turn completed with status ${String(response.status ?? 'success')}`)
|
|
200
149
|
|
|
201
150
|
return {
|
|
202
|
-
sessionId:
|
|
203
|
-
sessionFile:
|
|
204
|
-
status,
|
|
205
|
-
exitCode:
|
|
206
|
-
timedOut:
|
|
207
|
-
durationSeconds:
|
|
208
|
-
output,
|
|
209
|
-
notes,
|
|
151
|
+
sessionId: String(response.sessionId ?? sessionId ?? ''),
|
|
152
|
+
sessionFile: String(response.sessionFile ?? sessionFile ?? ''),
|
|
153
|
+
status: String(response.status ?? 'success'),
|
|
154
|
+
exitCode: 0,
|
|
155
|
+
timedOut: String(response.status ?? '') === 'timed_out',
|
|
156
|
+
durationSeconds: Math.max(0, Math.round((Date.now() - startedAt) / 1000)),
|
|
157
|
+
output: String(response.output ?? ''),
|
|
158
|
+
notes: String(response.notes ?? ''),
|
|
210
159
|
role: String(response.role ?? ''),
|
|
211
160
|
model: String(response.model ?? model ?? config.piModel ?? ''),
|
|
212
161
|
toolCalls: Number.isFinite(Number(response.toolCalls)) ? Number(response.toolCalls) : 0,
|
|
@@ -224,9 +173,9 @@ export async function runAgentTurn(args) {
|
|
|
224
173
|
return await runMockTurn(args)
|
|
225
174
|
}
|
|
226
175
|
|
|
227
|
-
if (args.config.transport === '
|
|
228
|
-
return await
|
|
176
|
+
if (args.config.transport === 'sdk') {
|
|
177
|
+
return await runSdkTransportTurn(args)
|
|
229
178
|
}
|
|
230
179
|
|
|
231
|
-
throw new Error(`Unsupported PI transport "${args.config.transport}". Expected "mock" or "
|
|
180
|
+
throw new Error(`Unsupported PI transport "${args.config.transport}". Expected "mock" or "sdk".`)
|
|
232
181
|
}
|
package/src/pi-config.mjs
CHANGED
|
@@ -196,7 +196,6 @@ export function loadConfig(mode = 'once') {
|
|
|
196
196
|
const cwd = process.cwd()
|
|
197
197
|
const repoConfig = readRepoConfig(cwd)
|
|
198
198
|
const file = repoConfig.values
|
|
199
|
-
const bundledAdapterCommand = 'pi-harness adapter'
|
|
200
199
|
const bundledDeveloperInstructionsFile = path.join(packageRoot, 'templates', 'DEVELOPER.md')
|
|
201
200
|
const bundledTesterInstructionsFile = path.join(packageRoot, 'templates', 'TESTER.md')
|
|
202
201
|
const modelProfiles = readObject('models', file.models, {})
|
|
@@ -222,13 +221,14 @@ export function loadConfig(mode = 'once') {
|
|
|
222
221
|
: bundledTesterInstructionsFile
|
|
223
222
|
)
|
|
224
223
|
|
|
224
|
+
const transport = readString('PI_TRANSPORT', file.transport, 'sdk')
|
|
225
|
+
|
|
225
226
|
return {
|
|
226
227
|
cwd,
|
|
227
228
|
configFile: repoConfig.configFile,
|
|
228
229
|
mode: mode === 'run' ? 'run' : 'once',
|
|
229
|
-
transport
|
|
230
|
+
transport,
|
|
230
231
|
agentName: readString('PI_AGENT_NAME', file.agentName, 'PI'),
|
|
231
|
-
adapterCommand: readString('PI_ADAPTER_COMMAND', file.adapterCommand, bundledAdapterCommand),
|
|
232
232
|
taskFile: resolveFromCwd(cwd, 'PI_TASK_FILE', file.taskFile, 'TODOS.md'),
|
|
233
233
|
instructionsFile: resolveInstructionsFile(cwd, 'PI_INSTRUCTIONS_FILE', file.instructionsFile, bundledDeveloperInstructionsFile),
|
|
234
234
|
developerInstructionsFile,
|