@sebastianandreasson/pi-autonomous-agents 0.9.1 → 0.10.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 -0
- package/docs/PI_SUPERVISOR.md +6 -1
- package/docs/VISUALIZER_UI_PLAN.md +117 -0
- package/package.json +7 -3
- package/src/cli.mjs +1 -0
- package/src/pi-client.mjs +62 -1
- package/src/pi-debug-live.mjs +117 -0
- package/src/pi-telemetry.mjs +70 -5
- package/src/pi-visualizer-server.mjs +98 -549
- package/visualizer-ui/dist/assets/index-C398cGuP.js +12 -0
- package/visualizer-ui/dist/assets/index-DuJxYqkl.css +1 -0
- package/visualizer-ui/dist/index.html +13 -0
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ pi-harness report
|
|
|
89
89
|
pi-harness clear-history
|
|
90
90
|
pi-harness visual-once
|
|
91
91
|
pi-harness visualize
|
|
92
|
+
pi-harness debug-live
|
|
92
93
|
pi-harness visual-review-worker
|
|
93
94
|
```
|
|
94
95
|
|
|
@@ -319,4 +320,26 @@ npm run check
|
|
|
319
320
|
npm test
|
|
320
321
|
```
|
|
321
322
|
|
|
323
|
+
For local visualizer iteration against fake live SDK agent:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
npm run debug:live-ui
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
For React/Vite visualizer UI dev loop:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npm run dev:visualizer:ui
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
For production visualizer UI build:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
npm run build:visualizer:ui
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
This seeds `.pi-debug/live-ui/`, runs harness there with streaming fake SDK fixture, hosts visualizer, and gives stable local repro loop for UI work. React app lives in `visualizer-ui/`. Visualizer server now serves built assets from `visualizer-ui/dist/` and falls back to build-instructions page if build artifacts are missing.
|
|
342
|
+
|
|
343
|
+
See `docs/VISUALIZER_UI_PLAN.md` for migration plan.
|
|
344
|
+
|
|
322
345
|
The package requires Node `>=20`.
|
package/docs/PI_SUPERVISOR.md
CHANGED
|
@@ -35,8 +35,10 @@ Main package files:
|
|
|
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
|
|
38
|
+
- `src/pi-visualizer.mjs`: local web UI entrypoint
|
|
39
|
+
- `src/pi-visualizer-server.mjs`: shared visualizer server/runtime
|
|
39
40
|
- `src/pi-visualizer-shared.mjs`: flow-state helpers for visualizer
|
|
41
|
+
- `src/pi-debug-live.mjs`: local fake-live sandbox runner for visualizer debugging
|
|
40
42
|
- `src/pi-report.mjs`: telemetry summary report
|
|
41
43
|
- `templates/DEVELOPER.md`: default developer-role instructions template
|
|
42
44
|
- `templates/TESTER.md`: default tester-role instructions template
|
|
@@ -49,6 +51,7 @@ pi-harness run
|
|
|
49
51
|
pi-harness report
|
|
50
52
|
pi-harness visual-once
|
|
51
53
|
pi-harness visualize
|
|
54
|
+
pi-harness debug-live
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
The package reads `PI_CONFIG_FILE` if provided. Otherwise it falls back to the bundled generic `pi.config.json`.
|
|
@@ -59,6 +62,8 @@ The package reads `PI_CONFIG_FILE` if provided. Otherwise it falls back to the b
|
|
|
59
62
|
|
|
60
63
|
Visualizer reads active-run lock, TODO file, per-run state, per-run iteration summary, per-run last output snapshot, live feed JSONL, and telemetry to show current stage plus historical runs.
|
|
61
64
|
|
|
65
|
+
For local UI iteration in this package repo, use `pi-harness debug-live` to run against seeded fake live SDK sandbox.
|
|
66
|
+
|
|
62
67
|
## Config Contract
|
|
63
68
|
|
|
64
69
|
Projects typically provide their own `pi.config.json` with fields such as:
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Visualizer UI migration plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Replace inline browser JS/HTML in `src/pi-visualizer-server.mjs` with maintainable React frontend, while keeping Node visualizer server as API + SSE + static asset host.
|
|
6
|
+
|
|
7
|
+
## Chosen stack
|
|
8
|
+
|
|
9
|
+
- React
|
|
10
|
+
- Vite
|
|
11
|
+
- TypeScript
|
|
12
|
+
- Zustand
|
|
13
|
+
- Plain CSS
|
|
14
|
+
|
|
15
|
+
## Repo layout
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
src/
|
|
19
|
+
pi-visualizer-server.mjs # API, SSE, static asset host
|
|
20
|
+
visualizer-ui/
|
|
21
|
+
package.json
|
|
22
|
+
tsconfig.json
|
|
23
|
+
vite.config.ts
|
|
24
|
+
index.html
|
|
25
|
+
src/
|
|
26
|
+
main.tsx
|
|
27
|
+
App.tsx
|
|
28
|
+
api.ts
|
|
29
|
+
store.ts
|
|
30
|
+
types.ts
|
|
31
|
+
styles.css
|
|
32
|
+
components/
|
|
33
|
+
TodoList.tsx
|
|
34
|
+
FlowStrip.tsx
|
|
35
|
+
LiveFeed.tsx
|
|
36
|
+
CurrentEdits.tsx
|
|
37
|
+
DiagnosticsPanel.tsx
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## State model
|
|
41
|
+
|
|
42
|
+
Zustand store owns:
|
|
43
|
+
|
|
44
|
+
- latest snapshot
|
|
45
|
+
- selected run
|
|
46
|
+
- selected todo
|
|
47
|
+
- selected event
|
|
48
|
+
- feed toggles
|
|
49
|
+
- SSE lifecycle
|
|
50
|
+
- initial load status / error
|
|
51
|
+
|
|
52
|
+
## API contract
|
|
53
|
+
|
|
54
|
+
Current server routes:
|
|
55
|
+
|
|
56
|
+
- `GET /api/state` → full snapshot
|
|
57
|
+
- `GET /api/stream` → SSE full snapshots
|
|
58
|
+
|
|
59
|
+
Short term: React app consumes current full-snapshot SSE.
|
|
60
|
+
|
|
61
|
+
Next improvement:
|
|
62
|
+
|
|
63
|
+
- add monotonic `seq` to live feed entries
|
|
64
|
+
- optionally move SSE from full snapshots to patch events
|
|
65
|
+
- add snapshot version to ignore stale payloads
|
|
66
|
+
|
|
67
|
+
## Migration phases
|
|
68
|
+
|
|
69
|
+
### Phase 1
|
|
70
|
+
- scaffold `visualizer-ui/`
|
|
71
|
+
- keep current inline HTML as fallback
|
|
72
|
+
- add built asset serving from `visualizer-ui/dist`
|
|
73
|
+
- add `/api/state` alias
|
|
74
|
+
|
|
75
|
+
### Phase 2
|
|
76
|
+
- port current layout into React components
|
|
77
|
+
- use Zustand store + initial snapshot fetch
|
|
78
|
+
- use SSE reconnect from frontend
|
|
79
|
+
|
|
80
|
+
### Phase 3
|
|
81
|
+
- move feed/timeline ordering to stable `seq`
|
|
82
|
+
- reduce full rerenders
|
|
83
|
+
- preserve scroll behavior inside components
|
|
84
|
+
|
|
85
|
+
### Phase 4
|
|
86
|
+
- remove inline browser app from server once built UI covers current features
|
|
87
|
+
- keep server only as API/static host
|
|
88
|
+
|
|
89
|
+
## Dev workflow
|
|
90
|
+
|
|
91
|
+
Backend + fake live harness:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm run debug:live-ui
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Frontend dev server:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm run dev:visualizer:ui
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Frontend build:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run build:visualizer:ui
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
Current state:
|
|
112
|
+
|
|
113
|
+
- React/Vite/Zustand UI scaffold added
|
|
114
|
+
- built assets generated under `visualizer-ui/dist/`
|
|
115
|
+
- server serves built UI directly
|
|
116
|
+
- legacy inline browser app removed
|
|
117
|
+
- fallback page only shows build instructions when dist missing
|
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.10.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Portable unattended PI harness for developer/tester/visual-review loops.",
|
|
7
7
|
"license": "MIT",
|
|
@@ -19,13 +19,17 @@
|
|
|
19
19
|
"@mariozechner/pi-coding-agent": "^0.66.1"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
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-server.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"
|
|
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-debug-live.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-server.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 && node --check test/fixtures/fake-live-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",
|
|
24
|
+
"debug:live-ui": "node src/cli.mjs debug-live --reset",
|
|
25
|
+
"dev:visualizer:ui": "npm --prefix visualizer-ui run dev",
|
|
26
|
+
"build:visualizer:ui": "npm --prefix visualizer-ui run build"
|
|
24
27
|
},
|
|
25
28
|
"files": [
|
|
26
29
|
"src",
|
|
27
30
|
"templates",
|
|
28
31
|
"docs",
|
|
32
|
+
"visualizer-ui/dist",
|
|
29
33
|
"pi.config.json",
|
|
30
34
|
"SETUP.md",
|
|
31
35
|
"README.md"
|
package/src/cli.mjs
CHANGED
package/src/pi-client.mjs
CHANGED
|
@@ -3,6 +3,9 @@ import path from 'node:path'
|
|
|
3
3
|
import { randomUUID } from 'node:crypto'
|
|
4
4
|
|
|
5
5
|
const liveFeedWriteQueues = new Map()
|
|
6
|
+
const liveFeedSequences = new Map()
|
|
7
|
+
const MAX_LIVE_FEED_TEXT = 2000
|
|
8
|
+
const MAX_LIVE_FEED_SUMMARY = 600
|
|
6
9
|
import {
|
|
7
10
|
appendLog,
|
|
8
11
|
writeTextFile,
|
|
@@ -41,6 +44,63 @@ async function writeAgentOutputSnapshot(config, content) {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
function truncateText(value, maxChars) {
|
|
48
|
+
const text = String(value ?? '')
|
|
49
|
+
if (text.length <= maxChars) {
|
|
50
|
+
return text
|
|
51
|
+
}
|
|
52
|
+
return `${text.slice(0, maxChars - 16)}\n... [truncated]`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function summarizeValue(value, maxChars = MAX_LIVE_FEED_SUMMARY) {
|
|
56
|
+
if (value === null || value === undefined) {
|
|
57
|
+
return ''
|
|
58
|
+
}
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
return truncateText(value, maxChars)
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return truncateText(JSON.stringify(value), maxChars)
|
|
64
|
+
} catch {
|
|
65
|
+
return truncateText(String(value), maxChars)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sanitizeLiveFeedEvent(filePath, event) {
|
|
70
|
+
const nextSeq = (liveFeedSequences.get(filePath) ?? 0) + 1
|
|
71
|
+
liveFeedSequences.set(filePath, nextSeq)
|
|
72
|
+
|
|
73
|
+
const normalized = {
|
|
74
|
+
seq: nextSeq,
|
|
75
|
+
timestamp: String(event?.timestamp ?? new Date().toISOString()),
|
|
76
|
+
iteration: Number(event?.iteration ?? 0),
|
|
77
|
+
retryCount: Number(event?.retryCount ?? 0),
|
|
78
|
+
reason: String(event?.reason ?? ''),
|
|
79
|
+
phase: String(event?.phase ?? ''),
|
|
80
|
+
role: String(event?.role ?? ''),
|
|
81
|
+
kind: String(event?.kind ?? ''),
|
|
82
|
+
type: String(event?.type ?? 'event'),
|
|
83
|
+
toolName: String(event?.toolName ?? ''),
|
|
84
|
+
isError: event?.isError === true,
|
|
85
|
+
text: truncateText(event?.text ?? '', MAX_LIVE_FEED_TEXT),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const argsSummary = summarizeValue(event?.args)
|
|
89
|
+
const partialSummary = summarizeValue(event?.partialResult)
|
|
90
|
+
const resultSummary = summarizeValue(event?.result)
|
|
91
|
+
if (argsSummary !== '') {
|
|
92
|
+
normalized.argsSummary = argsSummary
|
|
93
|
+
}
|
|
94
|
+
if (partialSummary !== '') {
|
|
95
|
+
normalized.partialSummary = partialSummary
|
|
96
|
+
}
|
|
97
|
+
if (resultSummary !== '') {
|
|
98
|
+
normalized.resultSummary = resultSummary
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return normalized
|
|
102
|
+
}
|
|
103
|
+
|
|
44
104
|
async function appendLiveFeedEvent(config, event) {
|
|
45
105
|
if (!config.runLiveFeedFile) {
|
|
46
106
|
return
|
|
@@ -51,8 +111,9 @@ async function appendLiveFeedEvent(config, event) {
|
|
|
51
111
|
const next = previous
|
|
52
112
|
.catch(() => {})
|
|
53
113
|
.then(async () => {
|
|
114
|
+
const sanitized = sanitizeLiveFeedEvent(filePath, event)
|
|
54
115
|
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
55
|
-
await fs.appendFile(filePath, `${JSON.stringify(
|
|
116
|
+
await fs.appendFile(filePath, `${JSON.stringify(sanitized)}\n`, 'utf8')
|
|
56
117
|
})
|
|
57
118
|
|
|
58
119
|
liveFeedWriteQueues.set(filePath, next)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const packageRoot = path.resolve(scriptDir, '..')
|
|
11
|
+
const cliFile = path.join(scriptDir, 'cli.mjs')
|
|
12
|
+
const fakePiFile = path.join(packageRoot, 'test', 'fixtures', 'fake-pi.mjs')
|
|
13
|
+
const fakeLiveSdkFile = path.join(packageRoot, 'test', 'fixtures', 'fake-live-pi-sdk.mjs')
|
|
14
|
+
const sandboxDir = path.join(packageRoot, '.pi-debug', 'live-ui')
|
|
15
|
+
|
|
16
|
+
function shellQuote(value) {
|
|
17
|
+
return JSON.stringify(String(value))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function ensureRepo(cwd) {
|
|
21
|
+
try {
|
|
22
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, stdio: 'ignore' })
|
|
23
|
+
} catch {
|
|
24
|
+
execFileSync('git', ['init'], { cwd, stdio: 'ignore' })
|
|
25
|
+
execFileSync('git', ['config', 'user.name', 'PI Harness Debug'], { cwd, stdio: 'ignore' })
|
|
26
|
+
execFileSync('git', ['config', 'user.email', 'pi-harness-debug@example.com'], { cwd, stdio: 'ignore' })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function seedFiles(cwd) {
|
|
31
|
+
await fs.mkdir(path.join(cwd, 'pi'), { recursive: true })
|
|
32
|
+
await fs.writeFile(path.join(cwd, 'TODOS.md'), [
|
|
33
|
+
'## Phase 1',
|
|
34
|
+
'',
|
|
35
|
+
'- [ ] Fake live task one',
|
|
36
|
+
'- [ ] Fake live task two',
|
|
37
|
+
'- [ ] Fake live task three',
|
|
38
|
+
'',
|
|
39
|
+
'## Phase 2',
|
|
40
|
+
'',
|
|
41
|
+
'- [ ] Fake live task four',
|
|
42
|
+
].join('\n') + '\n', 'utf8')
|
|
43
|
+
await fs.writeFile(path.join(cwd, 'DEVELOPER.md'), 'Developer instructions for local visualizer debugging.\n', 'utf8')
|
|
44
|
+
await fs.writeFile(path.join(cwd, 'TESTER.md'), 'Tester instructions for local visualizer debugging.\n', 'utf8')
|
|
45
|
+
await fs.writeFile(path.join(cwd, 'pi.config.json'), `${JSON.stringify({
|
|
46
|
+
transport: 'sdk',
|
|
47
|
+
taskFile: 'TODOS.md',
|
|
48
|
+
developerInstructionsFile: 'DEVELOPER.md',
|
|
49
|
+
testerInstructionsFile: 'TESTER.md',
|
|
50
|
+
piCli: fakePiFile,
|
|
51
|
+
piModel: 'fake-model',
|
|
52
|
+
roleModels: {
|
|
53
|
+
developer: 'fake-model',
|
|
54
|
+
developerRetry: 'fake-model',
|
|
55
|
+
developerFix: 'fake-model',
|
|
56
|
+
tester: 'fake-model',
|
|
57
|
+
testerCommit: 'fake-model',
|
|
58
|
+
},
|
|
59
|
+
testCommand: `${shellQuote(process.execPath)} -e ${shellQuote('setTimeout(()=>process.exit(0), 250)')}`,
|
|
60
|
+
streamTerminal: true,
|
|
61
|
+
continueAfterSeconds: 3600,
|
|
62
|
+
noEventTimeoutSeconds: 3600,
|
|
63
|
+
toolContinueAfterSeconds: 3600,
|
|
64
|
+
toolNoEventTimeoutSeconds: 3600,
|
|
65
|
+
sleepBetweenSeconds: 1,
|
|
66
|
+
maxIterations: 20,
|
|
67
|
+
}, null, 2)}\n`, 'utf8')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function ensureInitialCommit(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'ignore' })
|
|
73
|
+
} catch {
|
|
74
|
+
execFileSync('git', ['add', '.'], { cwd, stdio: 'ignore' })
|
|
75
|
+
execFileSync('git', ['commit', '-m', 'chore(debug): seed fake live sandbox'], { cwd, stdio: 'ignore' })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function main() {
|
|
80
|
+
const reset = process.argv.includes('--reset')
|
|
81
|
+
if (reset) {
|
|
82
|
+
await fs.rm(sandboxDir, { recursive: true, force: true })
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await fs.mkdir(sandboxDir, { recursive: true })
|
|
86
|
+
await ensureRepo(sandboxDir)
|
|
87
|
+
await seedFiles(sandboxDir)
|
|
88
|
+
await ensureInitialCommit(sandboxDir)
|
|
89
|
+
|
|
90
|
+
process.stdout.write(`PI debug sandbox: ${sandboxDir}\n`)
|
|
91
|
+
process.stdout.write(`Using fake live SDK fixture: ${fakeLiveSdkFile}\n`)
|
|
92
|
+
|
|
93
|
+
const child = spawn(process.execPath, [cliFile, 'run'], {
|
|
94
|
+
cwd: sandboxDir,
|
|
95
|
+
env: {
|
|
96
|
+
...process.env,
|
|
97
|
+
PI_CONFIG_FILE: 'pi.config.json',
|
|
98
|
+
PI_SDK_MODULE: fakeLiveSdkFile,
|
|
99
|
+
PI_VISUALIZER_HOST: process.env.PI_VISUALIZER_HOST || '127.0.0.1',
|
|
100
|
+
PI_VISUALIZER_PORT: process.env.PI_VISUALIZER_PORT || '4317',
|
|
101
|
+
},
|
|
102
|
+
stdio: 'inherit',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
child.on('exit', (code, signal) => {
|
|
106
|
+
if (signal) {
|
|
107
|
+
process.exitCode = 128
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
process.exitCode = code ?? 1
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main().catch((error) => {
|
|
115
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error))
|
|
116
|
+
process.exitCode = 1
|
|
117
|
+
})
|
package/src/pi-telemetry.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
4
|
const CSV_HEADER = 'timestamp,run_id,iteration,phase,kind,status,transport,session_id,timed_out,exit_code,duration_seconds,commit_before,commit_after,repo_changed,changed_files_count,verification_status,retry_count,role,model,tool_calls,tool_errors,message_updates,stop_reason,loop_detected,loop_signature,tester_verdict,commit_plan_found,terminal_reason,risk_warnings,notes\n'
|
|
5
|
+
const DEFAULT_JSONL_TAIL_BYTES = 512 * 1024
|
|
6
|
+
const JSONL_TAIL_CHUNK_BYTES = 64 * 1024
|
|
5
7
|
|
|
6
8
|
function csvEscape(value) {
|
|
7
9
|
const text = String(value ?? '')
|
|
@@ -95,15 +97,78 @@ export async function appendTelemetry(config, event) {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
function parseJsonlLines(raw, { dropFirstLine = false, maxItems = Infinity } = {}) {
|
|
101
|
+
const lines = raw.split('\n')
|
|
102
|
+
if (dropFirstLine && lines.length > 0) {
|
|
103
|
+
lines.shift()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const items = []
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const trimmed = line.trim()
|
|
109
|
+
if (trimmed === '') {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
items.push(JSON.parse(trimmed))
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore partial/truncated JSONL records while file is actively being appended.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Number.isFinite(maxItems) ? items.slice(-maxItems) : items
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function readJsonlTail(filePath, options = {}) {
|
|
123
|
+
const maxItems = Number.isFinite(Number(options.maxItems)) ? Number(options.maxItems) : 200
|
|
124
|
+
const maxBytes = Number.isFinite(Number(options.maxBytes)) ? Number(options.maxBytes) : DEFAULT_JSONL_TAIL_BYTES
|
|
125
|
+
|
|
126
|
+
let handle
|
|
127
|
+
try {
|
|
128
|
+
handle = await fs.open(filePath, 'r')
|
|
129
|
+
const stat = await handle.stat()
|
|
130
|
+
if (!Number.isFinite(stat.size) || stat.size <= 0) {
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let position = stat.size
|
|
135
|
+
let text = ''
|
|
136
|
+
let newlineCount = 0
|
|
137
|
+
|
|
138
|
+
while (position > 0 && Buffer.byteLength(text, 'utf8') < maxBytes && newlineCount <= (maxItems + 1)) {
|
|
139
|
+
const remainingBudget = maxBytes - Buffer.byteLength(text, 'utf8')
|
|
140
|
+
const chunkSize = Math.min(JSONL_TAIL_CHUNK_BYTES, position, remainingBudget)
|
|
141
|
+
if (chunkSize <= 0) {
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
position -= chunkSize
|
|
146
|
+
const buffer = Buffer.alloc(chunkSize)
|
|
147
|
+
const { bytesRead } = await handle.read(buffer, 0, chunkSize, position)
|
|
148
|
+
text = buffer.subarray(0, bytesRead).toString('utf8') + text
|
|
149
|
+
newlineCount = (text.match(/\n/g) ?? []).length
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return parseJsonlLines(text, {
|
|
153
|
+
dropFirstLine: position > 0,
|
|
154
|
+
maxItems,
|
|
155
|
+
})
|
|
156
|
+
} catch {
|
|
157
|
+
return []
|
|
158
|
+
} finally {
|
|
159
|
+
await handle?.close().catch(() => {})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
98
163
|
export async function readTelemetry(config) {
|
|
99
164
|
try {
|
|
100
165
|
const raw = await fs.readFile(config.telemetryJsonl, 'utf8')
|
|
101
|
-
return raw
|
|
102
|
-
.split('\n')
|
|
103
|
-
.map((line) => line.trim())
|
|
104
|
-
.filter(Boolean)
|
|
105
|
-
.map((line) => JSON.parse(line))
|
|
166
|
+
return parseJsonlLines(raw)
|
|
106
167
|
} catch {
|
|
107
168
|
return []
|
|
108
169
|
}
|
|
109
170
|
}
|
|
171
|
+
|
|
172
|
+
export async function readTelemetryTail(config, maxItems = 200, maxBytes = DEFAULT_JSONL_TAIL_BYTES) {
|
|
173
|
+
return await readJsonlTail(config.telemetryJsonl, { maxItems, maxBytes })
|
|
174
|
+
}
|