@sebastianandreasson/pi-autonomous-agents 0.5.2 → 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 -9
- package/SETUP.md +5 -0
- package/docs/PI_SUPERVISOR.md +14 -65
- package/package.json +6 -3
- package/pi.config.json +1 -2
- package/src/cli.mjs +1 -1
- package/src/index.mjs +2 -0
- package/src/pi-client.mjs +68 -119
- package/src/pi-config.mjs +3 -3
- package/src/pi-sdk-turn.mjs +654 -0
- package/src/pi-supervisor.mjs +58 -0
- 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 -668
package/src/pi-rpc-adapter.mjs
DELETED
|
@@ -1,668 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { createInterface } from 'node:readline'
|
|
4
|
-
import { spawn } from 'node:child_process'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import process from 'node:process'
|
|
7
|
-
import {
|
|
8
|
-
formatHeartbeatReason,
|
|
9
|
-
formatHeartbeatTimeoutMessage,
|
|
10
|
-
getHeartbeatDecision,
|
|
11
|
-
resolveHeartbeatConfig,
|
|
12
|
-
} from './pi-heartbeat.mjs'
|
|
13
|
-
import {
|
|
14
|
-
registerOwnedChildProcess,
|
|
15
|
-
signalOwnedChildProcesses,
|
|
16
|
-
signalProcessTree,
|
|
17
|
-
watchParentProcess,
|
|
18
|
-
} from './pi-repo.mjs'
|
|
19
|
-
|
|
20
|
-
function createJsonlReader(stream, onLine) {
|
|
21
|
-
const rl = createInterface({ input: stream })
|
|
22
|
-
rl.on('line', onLine)
|
|
23
|
-
return () => rl.close()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function readRequest() {
|
|
27
|
-
const chunks = []
|
|
28
|
-
for await (const chunk of process.stdin) {
|
|
29
|
-
chunks.push(chunk)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const raw = Buffer.concat(chunks).toString('utf8').trim()
|
|
33
|
-
if (raw === '') {
|
|
34
|
-
throw new Error('Expected JSON request on stdin.')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return JSON.parse(raw)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function formatValue(value) {
|
|
41
|
-
const text = JSON.stringify(value)
|
|
42
|
-
if (text === undefined) {
|
|
43
|
-
return ''
|
|
44
|
-
}
|
|
45
|
-
if (text.length <= 160) {
|
|
46
|
-
return text
|
|
47
|
-
}
|
|
48
|
-
return `${text.slice(0, 157)}...`
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function extractToolTarget(toolName, args) {
|
|
52
|
-
if (!args || typeof args !== 'object') {
|
|
53
|
-
return ''
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if ((toolName === 'read' || toolName === 'write' || toolName === 'edit') && typeof args.path === 'string') {
|
|
57
|
-
return args.path
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return ''
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function extractShellCommand(args) {
|
|
64
|
-
if (!args || typeof args !== 'object') {
|
|
65
|
-
return ''
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (typeof args.command === 'string') {
|
|
69
|
-
return args.command
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (typeof args.cmd === 'string') {
|
|
73
|
-
return args.cmd
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return ''
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function isLargeShellRead(command) {
|
|
80
|
-
const text = String(command ?? '').trim()
|
|
81
|
-
if (text === '') {
|
|
82
|
-
return false
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (/^\s*cat\s+\S+/.test(text)) {
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const sedMatch = text.match(/sed\s+-n\s+['"]?(\d+)\s*,\s*(\d+)p['"]?/)
|
|
90
|
-
if (sedMatch) {
|
|
91
|
-
const start = Number.parseInt(sedMatch[1], 10)
|
|
92
|
-
const end = Number.parseInt(sedMatch[2], 10)
|
|
93
|
-
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
94
|
-
return (end - start) >= 120
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return false
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function extractAssistantText(message) {
|
|
102
|
-
if (!message || message.role !== 'assistant' || !Array.isArray(message.content)) {
|
|
103
|
-
return ''
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return message.content
|
|
107
|
-
.filter((item) => item?.type === 'text')
|
|
108
|
-
.map((item) => item.text)
|
|
109
|
-
.join('')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getLastAssistantMessageFromEvents(events) {
|
|
113
|
-
const agentEnd = [...events].reverse().find((event) => event.type === 'agent_end')
|
|
114
|
-
if (!agentEnd || !Array.isArray(agentEnd.messages)) {
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const messages = [...agentEnd.messages].reverse()
|
|
119
|
-
return messages.find((message) => message?.role === 'assistant') ?? null
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function run() {
|
|
123
|
-
const request = await readRequest()
|
|
124
|
-
const runtimeDir = path.resolve(request.runtimeDir ?? path.join(request.cwd, '.pi-runtime'))
|
|
125
|
-
const sessionDir = path.join(runtimeDir, 'sessions')
|
|
126
|
-
const cli = request.piCli || 'pi'
|
|
127
|
-
const args = ['--mode', 'rpc', '--session-dir', sessionDir]
|
|
128
|
-
|
|
129
|
-
if (request.sessionFile) {
|
|
130
|
-
args.push('--session', request.sessionFile)
|
|
131
|
-
} else if (request.sessionId) {
|
|
132
|
-
args.push('--continue')
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (request.model) {
|
|
136
|
-
args.push('--model', request.model)
|
|
137
|
-
}
|
|
138
|
-
if (request.tools) {
|
|
139
|
-
args.push('--tools', request.tools)
|
|
140
|
-
}
|
|
141
|
-
if (request.thinking) {
|
|
142
|
-
args.push('--thinking', request.thinking)
|
|
143
|
-
}
|
|
144
|
-
if (request.noExtensions) {
|
|
145
|
-
args.push('--no-extensions')
|
|
146
|
-
}
|
|
147
|
-
if (request.noSkills) {
|
|
148
|
-
args.push('--no-skills')
|
|
149
|
-
}
|
|
150
|
-
if (request.noPromptTemplates) {
|
|
151
|
-
args.push('--no-prompt-templates')
|
|
152
|
-
}
|
|
153
|
-
if (request.noThemes ?? true) {
|
|
154
|
-
args.push('--no-themes')
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const child = spawn(cli, args, {
|
|
158
|
-
cwd: request.cwd,
|
|
159
|
-
env: process.env,
|
|
160
|
-
detached: process.platform !== 'win32',
|
|
161
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
162
|
-
})
|
|
163
|
-
registerOwnedChildProcess(child, {
|
|
164
|
-
useProcessGroup: process.platform !== 'win32',
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
let stderr = ''
|
|
168
|
-
const events = []
|
|
169
|
-
const pending = new Map()
|
|
170
|
-
let requestCounter = 0
|
|
171
|
-
const streamTerminal = request.streamTerminal === true
|
|
172
|
-
const requestedModel = typeof request.model === 'string' ? request.model : ''
|
|
173
|
-
const loopRepeatThreshold = Number.isFinite(Number(request.loopRepeatThreshold))
|
|
174
|
-
? Number(request.loopRepeatThreshold)
|
|
175
|
-
: 12
|
|
176
|
-
const samePathRepeatThreshold = Number.isFinite(Number(request.samePathRepeatThreshold))
|
|
177
|
-
? Number(request.samePathRepeatThreshold)
|
|
178
|
-
: 8
|
|
179
|
-
let assistantLineOpen = false
|
|
180
|
-
let streamedAssistantText = false
|
|
181
|
-
let lastToolSignature = ''
|
|
182
|
-
let repeatedToolCount = 0
|
|
183
|
-
let lastToolTarget = ''
|
|
184
|
-
let repeatedTargetCount = 0
|
|
185
|
-
let loopDetected = false
|
|
186
|
-
let loopSignature = ''
|
|
187
|
-
let abortRequested = false
|
|
188
|
-
const {
|
|
189
|
-
continueAfterSeconds,
|
|
190
|
-
noEventTimeoutSeconds,
|
|
191
|
-
toolContinueAfterSeconds,
|
|
192
|
-
toolNoEventTimeoutSeconds,
|
|
193
|
-
} = resolveHeartbeatConfig(request)
|
|
194
|
-
const continueMessage = typeof request.continueMessage === 'string' && request.continueMessage.trim() !== ''
|
|
195
|
-
? request.continueMessage.trim()
|
|
196
|
-
: 'continue'
|
|
197
|
-
let agentStarted = false
|
|
198
|
-
let agentEnded = false
|
|
199
|
-
let heartbeatTimedOut = false
|
|
200
|
-
let heartbeatReason = ''
|
|
201
|
-
let lastEventAt = Date.now()
|
|
202
|
-
let activeToolName = ''
|
|
203
|
-
let activeToolStartedAt = 0
|
|
204
|
-
let heartbeatInterval = null
|
|
205
|
-
let continueAttempted = false
|
|
206
|
-
let continueAccepted = false
|
|
207
|
-
let continueRejected = false
|
|
208
|
-
let shutdownRequested = false
|
|
209
|
-
let shutdownReason = ''
|
|
210
|
-
let shutdownEscalationTimer = null
|
|
211
|
-
|
|
212
|
-
const requestShutdown = (reason, signal = 'SIGTERM') => {
|
|
213
|
-
if (shutdownRequested) {
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
shutdownRequested = true
|
|
218
|
-
shutdownReason = String(reason ?? 'adapter_shutdown')
|
|
219
|
-
closeAssistantLine()
|
|
220
|
-
signalOwnedChildProcesses(signal)
|
|
221
|
-
shutdownEscalationTimer = setTimeout(() => {
|
|
222
|
-
signalOwnedChildProcesses('SIGKILL')
|
|
223
|
-
}, 1000)
|
|
224
|
-
if (typeof shutdownEscalationTimer.unref === 'function') {
|
|
225
|
-
shutdownEscalationTimer.unref()
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const stopWatchingParent = watchParentProcess(() => {
|
|
230
|
-
requestShutdown('parent_exit', 'SIGTERM')
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
234
|
-
process.on(signal, () => {
|
|
235
|
-
requestShutdown(signal, signal)
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const writeLive = (text) => {
|
|
240
|
-
if (!streamTerminal) {
|
|
241
|
-
return
|
|
242
|
-
}
|
|
243
|
-
process.stderr.write(text)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const ensureAssistantLine = () => {
|
|
247
|
-
if (!assistantLineOpen) {
|
|
248
|
-
writeLive('[PI assistant] ')
|
|
249
|
-
assistantLineOpen = true
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const closeAssistantLine = () => {
|
|
254
|
-
if (assistantLineOpen) {
|
|
255
|
-
writeLive('\n')
|
|
256
|
-
assistantLineOpen = false
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const requestAbortForLoop = () => {
|
|
261
|
-
if (abortRequested) {
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
abortRequested = true
|
|
266
|
-
closeAssistantLine()
|
|
267
|
-
writeLive(`[PI guard] repeated tool loop detected: ${loopSignature} x${repeatedToolCount}. Aborting current turn.\n`)
|
|
268
|
-
void send({ type: 'abort' }).catch(() => {})
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const requestAbortForHeartbeat = (decision) => {
|
|
272
|
-
if (abortRequested) {
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
abortRequested = true
|
|
277
|
-
heartbeatTimedOut = true
|
|
278
|
-
heartbeatReason = formatHeartbeatReason(decision)
|
|
279
|
-
closeAssistantLine()
|
|
280
|
-
writeLive(`[PI guard] ${formatHeartbeatTimeoutMessage(decision)} Aborting current turn (pid=${child.pid ?? 'unknown'}).\n`)
|
|
281
|
-
void send({ type: 'abort' }).catch(() => {})
|
|
282
|
-
signalProcessTree(child.pid, 'SIGTERM')
|
|
283
|
-
setTimeout(() => {
|
|
284
|
-
if (child.exitCode === null) {
|
|
285
|
-
signalProcessTree(child.pid, 'SIGKILL')
|
|
286
|
-
}
|
|
287
|
-
}, 1000)
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const requestSoftContinue = (decision) => {
|
|
291
|
-
if (abortRequested || continueAttempted || agentEnded) {
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
continueAttempted = true
|
|
296
|
-
closeAssistantLine()
|
|
297
|
-
const context = decision.activeToolName
|
|
298
|
-
? ` while tool "${decision.activeToolName}" is running`
|
|
299
|
-
: ''
|
|
300
|
-
writeLive(`[PI guard] no PI events for ${decision.continueAfterSeconds}s${context}. Sending soft continue prompt.\n`)
|
|
301
|
-
void send({ type: 'prompt', message: continueMessage })
|
|
302
|
-
.then((response) => {
|
|
303
|
-
if (response?.success) {
|
|
304
|
-
continueAccepted = true
|
|
305
|
-
writeLive('[PI guard] soft continue accepted by PI.\n')
|
|
306
|
-
} else {
|
|
307
|
-
continueRejected = true
|
|
308
|
-
writeLive(`[PI guard] soft continue rejected: ${String(response?.error ?? 'unknown error')}\n`)
|
|
309
|
-
}
|
|
310
|
-
})
|
|
311
|
-
.catch((error) => {
|
|
312
|
-
continueRejected = true
|
|
313
|
-
writeLive(`[PI guard] soft continue failed: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
314
|
-
})
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
child.stderr.on('data', (chunk) => {
|
|
318
|
-
stderr += chunk.toString()
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
const stopReading = createJsonlReader(child.stdout, (line) => {
|
|
322
|
-
if (line.trim() === '') {
|
|
323
|
-
return
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
let data
|
|
327
|
-
try {
|
|
328
|
-
data = JSON.parse(line)
|
|
329
|
-
} catch {
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
lastEventAt = Date.now()
|
|
334
|
-
|
|
335
|
-
if (data.type === 'response' && typeof data.id === 'string' && pending.has(data.id)) {
|
|
336
|
-
const current = pending.get(data.id)
|
|
337
|
-
pending.delete(data.id)
|
|
338
|
-
current.resolve(data)
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
events.push(data)
|
|
343
|
-
|
|
344
|
-
if (data.type === 'agent_start') {
|
|
345
|
-
agentStarted = true
|
|
346
|
-
writeLive('[PI] agent started\n')
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (data.type === 'message_update' && data.message?.role === 'assistant') {
|
|
350
|
-
const assistantEvent = data.assistantMessageEvent
|
|
351
|
-
if (assistantEvent?.type === 'text_delta') {
|
|
352
|
-
ensureAssistantLine()
|
|
353
|
-
writeLive(assistantEvent.delta)
|
|
354
|
-
streamedAssistantText = true
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (data.type === 'message_end' && data.message?.role === 'assistant') {
|
|
359
|
-
const text = extractAssistantText(data.message)
|
|
360
|
-
if (assistantLineOpen) {
|
|
361
|
-
closeAssistantLine()
|
|
362
|
-
} else if (!streamedAssistantText && text.trim() !== '') {
|
|
363
|
-
writeLive(`[PI assistant] ${text}\n`)
|
|
364
|
-
}
|
|
365
|
-
streamedAssistantText = false
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (data.type === 'tool_execution_start') {
|
|
369
|
-
closeAssistantLine()
|
|
370
|
-
const argsText = formatValue(data.args)
|
|
371
|
-
const suffix = argsText === '' ? '' : ` ${argsText}`
|
|
372
|
-
const signature = `${data.toolName}${suffix}`
|
|
373
|
-
activeToolName = String(data.toolName ?? '')
|
|
374
|
-
activeToolStartedAt = Date.now()
|
|
375
|
-
const target = extractToolTarget(data.toolName, data.args)
|
|
376
|
-
const shellCommand = data.toolName === 'bash' ? extractShellCommand(data.args) : ''
|
|
377
|
-
if (signature === lastToolSignature) {
|
|
378
|
-
repeatedToolCount += 1
|
|
379
|
-
} else {
|
|
380
|
-
lastToolSignature = signature
|
|
381
|
-
repeatedToolCount = 1
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (target !== '' && target === lastToolTarget) {
|
|
385
|
-
repeatedTargetCount += 1
|
|
386
|
-
} else if (target !== '') {
|
|
387
|
-
lastToolTarget = target
|
|
388
|
-
repeatedTargetCount = 1
|
|
389
|
-
} else {
|
|
390
|
-
lastToolTarget = ''
|
|
391
|
-
repeatedTargetCount = 0
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (!loopDetected && repeatedToolCount >= loopRepeatThreshold) {
|
|
395
|
-
loopDetected = true
|
|
396
|
-
loopSignature = signature
|
|
397
|
-
requestAbortForLoop()
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (!loopDetected && target !== '' && repeatedTargetCount >= samePathRepeatThreshold) {
|
|
401
|
-
loopDetected = true
|
|
402
|
-
loopSignature = `same_path:${target}`
|
|
403
|
-
requestAbortForLoop()
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
writeLive(`[PI tool:start] ${data.toolName}${suffix}\n`)
|
|
407
|
-
if (data.toolName === 'bash' && isLargeShellRead(shellCommand)) {
|
|
408
|
-
writeLive('[PI warning] large bash file read detected; prefer read or a smaller exact window to avoid truncated context.\n')
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (data.type === 'tool_execution_end') {
|
|
413
|
-
closeAssistantLine()
|
|
414
|
-
activeToolName = ''
|
|
415
|
-
activeToolStartedAt = 0
|
|
416
|
-
writeLive(`[PI tool:end] ${data.toolName} ${data.isError ? 'error' : 'ok'}\n`)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (data.type === 'agent_end') {
|
|
420
|
-
agentEnded = true
|
|
421
|
-
closeAssistantLine()
|
|
422
|
-
writeLive('[PI] agent finished\n')
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (data.type === 'extension_ui_request') {
|
|
426
|
-
closeAssistantLine()
|
|
427
|
-
writeLive(`[PI ui] ${data.method} requested and auto-cancelled in headless mode\n`)
|
|
428
|
-
child.stdin.write(`${JSON.stringify({
|
|
429
|
-
type: 'extension_ui_response',
|
|
430
|
-
id: data.id,
|
|
431
|
-
cancelled: true,
|
|
432
|
-
})}\n`)
|
|
433
|
-
}
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
const send = (command) => new Promise((resolve, reject) => {
|
|
437
|
-
requestCounter += 1
|
|
438
|
-
const id = `adapter_${requestCounter}`
|
|
439
|
-
pending.set(id, { resolve, reject })
|
|
440
|
-
child.stdin.write(`${JSON.stringify({ ...command, id })}\n`)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
const waitForAgentEnd = () => new Promise((resolve) => {
|
|
444
|
-
const interval = setInterval(() => {
|
|
445
|
-
if (events.some((event) => event.type === 'agent_end') || child.exitCode !== null || heartbeatTimedOut) {
|
|
446
|
-
clearInterval(interval)
|
|
447
|
-
resolve()
|
|
448
|
-
}
|
|
449
|
-
}, 50)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
heartbeatInterval = setInterval(() => {
|
|
453
|
-
const decision = getHeartbeatDecision({
|
|
454
|
-
now: Date.now(),
|
|
455
|
-
agentStarted,
|
|
456
|
-
agentEnded,
|
|
457
|
-
heartbeatTimedOut,
|
|
458
|
-
childExited: child.exitCode !== null,
|
|
459
|
-
lastEventAt,
|
|
460
|
-
continueAttempted,
|
|
461
|
-
activeToolName,
|
|
462
|
-
activeToolStartedAt,
|
|
463
|
-
continueAfterSeconds,
|
|
464
|
-
noEventTimeoutSeconds,
|
|
465
|
-
toolContinueAfterSeconds,
|
|
466
|
-
toolNoEventTimeoutSeconds,
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
if (decision.action === 'soft_continue') {
|
|
470
|
-
requestSoftContinue(decision)
|
|
471
|
-
return
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (decision.action === 'abort') {
|
|
475
|
-
requestAbortForHeartbeat(decision)
|
|
476
|
-
}
|
|
477
|
-
}, 1000)
|
|
478
|
-
|
|
479
|
-
try {
|
|
480
|
-
const initialState = await send({ type: 'get_state' })
|
|
481
|
-
if (!initialState.success) {
|
|
482
|
-
throw new Error(initialState.error)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (!request.model && !initialState.data.model) {
|
|
486
|
-
throw new Error('No PI model configured. Set PI_MODEL or configure a default model in PI.')
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
await send({ type: 'set_auto_retry', enabled: false })
|
|
490
|
-
|
|
491
|
-
const promptResponse = await send({
|
|
492
|
-
type: 'prompt',
|
|
493
|
-
message: request.prompt,
|
|
494
|
-
})
|
|
495
|
-
if (!promptResponse.success) {
|
|
496
|
-
throw new Error(promptResponse.error)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
await waitForAgentEnd()
|
|
500
|
-
|
|
501
|
-
if (shutdownRequested) {
|
|
502
|
-
console.log(JSON.stringify({
|
|
503
|
-
sessionId: request.sessionId ?? '',
|
|
504
|
-
sessionFile: request.sessionFile ?? '',
|
|
505
|
-
status: 'failed',
|
|
506
|
-
output: '',
|
|
507
|
-
notes: shutdownReason,
|
|
508
|
-
role: '',
|
|
509
|
-
model: requestedModel,
|
|
510
|
-
toolCalls: 0,
|
|
511
|
-
toolErrors: 0,
|
|
512
|
-
messageUpdates: 0,
|
|
513
|
-
stopReason: '',
|
|
514
|
-
loopDetected: false,
|
|
515
|
-
loopSignature: '',
|
|
516
|
-
terminalReason: 'adapter_shutdown',
|
|
517
|
-
}))
|
|
518
|
-
return
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (heartbeatTimedOut) {
|
|
522
|
-
const toolCalls = events.filter((event) => event.type === 'tool_execution_start').length
|
|
523
|
-
const toolErrors = events.filter((event) => event.type === 'tool_execution_end' && event.isError).length
|
|
524
|
-
const messageUpdates = events.filter((event) => event.type === 'message_update').length
|
|
525
|
-
console.log(JSON.stringify({
|
|
526
|
-
sessionId: request.sessionId ?? '',
|
|
527
|
-
sessionFile: request.sessionFile ?? '',
|
|
528
|
-
status: 'timed_out',
|
|
529
|
-
output: [
|
|
530
|
-
'[heartbeat_timeout]',
|
|
531
|
-
formatHeartbeatTimeoutMessage({
|
|
532
|
-
noEventTimeoutSeconds,
|
|
533
|
-
activeToolName,
|
|
534
|
-
toolRuntimeSeconds: activeToolStartedAt > 0
|
|
535
|
-
? Math.max(0, Math.floor((Date.now() - activeToolStartedAt) / 1000))
|
|
536
|
-
: 0,
|
|
537
|
-
}),
|
|
538
|
-
stderr.trim() !== '' ? `\n[stderr]\n${stderr.trim()}` : '',
|
|
539
|
-
].join('\n').trim(),
|
|
540
|
-
notes: [
|
|
541
|
-
`pi_pid=${child.pid ?? 'unknown'}`,
|
|
542
|
-
heartbeatReason,
|
|
543
|
-
continueAttempted ? `continue_attempted=${continueMessage}` : '',
|
|
544
|
-
continueAccepted ? 'continue_accepted=true' : '',
|
|
545
|
-
continueRejected ? 'continue_rejected=true' : '',
|
|
546
|
-
].join(' '),
|
|
547
|
-
role: '',
|
|
548
|
-
model: requestedModel,
|
|
549
|
-
toolCalls,
|
|
550
|
-
toolErrors,
|
|
551
|
-
messageUpdates,
|
|
552
|
-
stopReason: '',
|
|
553
|
-
loopDetected: false,
|
|
554
|
-
loopSignature: '',
|
|
555
|
-
terminalReason: 'heartbeat_timeout',
|
|
556
|
-
}))
|
|
557
|
-
return
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const state = await send({ type: 'get_state' })
|
|
561
|
-
if (!state.success) {
|
|
562
|
-
throw new Error(state.error)
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const lastAssistant = await send({ type: 'get_last_assistant_text' })
|
|
566
|
-
if (!lastAssistant.success) {
|
|
567
|
-
throw new Error(lastAssistant.error)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const toolCalls = events.filter((event) => event.type === 'tool_execution_start').length
|
|
571
|
-
const toolErrors = events.filter((event) => event.type === 'tool_execution_end' && event.isError).length
|
|
572
|
-
const messageUpdates = events.filter((event) => event.type === 'message_update').length
|
|
573
|
-
const lastAssistantMessage = getLastAssistantMessageFromEvents(events)
|
|
574
|
-
const assistantText = String(lastAssistant.data.text ?? '').trim()
|
|
575
|
-
const assistantError = String(lastAssistantMessage?.errorMessage ?? '').trim()
|
|
576
|
-
const assistantStopReason = String(lastAssistantMessage?.stopReason ?? '').trim()
|
|
577
|
-
const status = loopDetected
|
|
578
|
-
? 'stalled'
|
|
579
|
-
: assistantError !== '' || (assistantText === '' && toolCalls === 0 && messageUpdates === 0)
|
|
580
|
-
? 'failed'
|
|
581
|
-
: 'success'
|
|
582
|
-
const terminalReason = loopDetected
|
|
583
|
-
? 'loop_detected'
|
|
584
|
-
: assistantError !== ''
|
|
585
|
-
? 'assistant_error'
|
|
586
|
-
: assistantStopReason === 'length'
|
|
587
|
-
? 'assistant_stop_length'
|
|
588
|
-
: status === 'failed'
|
|
589
|
-
? 'empty_agent_turn'
|
|
590
|
-
: 'agent_completed'
|
|
591
|
-
const notes = [
|
|
592
|
-
`PI session ${state.data.sessionId}`,
|
|
593
|
-
`pi_pid=${child.pid ?? 'unknown'}`,
|
|
594
|
-
`tool_calls=${toolCalls}`,
|
|
595
|
-
`tool_errors=${toolErrors}`,
|
|
596
|
-
`message_updates=${messageUpdates}`,
|
|
597
|
-
activeToolName !== '' ? `active_tool=${activeToolName}` : '',
|
|
598
|
-
continueAttempted ? `continue_attempted=${continueMessage}` : '',
|
|
599
|
-
continueAccepted ? 'continue_accepted=true' : '',
|
|
600
|
-
continueRejected ? 'continue_rejected=true' : '',
|
|
601
|
-
loopDetected ? `loop_detected=${loopSignature}` : '',
|
|
602
|
-
loopDetected ? `loop_repeats=${repeatedToolCount}` : '',
|
|
603
|
-
assistantStopReason !== '' ? `stop_reason=${assistantStopReason}` : '',
|
|
604
|
-
assistantError !== '' ? `assistant_error=${assistantError}` : '',
|
|
605
|
-
status === 'failed' && assistantError === '' ? 'empty_agent_turn' : '',
|
|
606
|
-
].join(' ')
|
|
607
|
-
|
|
608
|
-
const output = [
|
|
609
|
-
assistantText,
|
|
610
|
-
loopDetected ? `\n[loop_guard]\nRepeated tool loop detected: ${loopSignature} x${repeatedToolCount}` : '',
|
|
611
|
-
assistantError !== '' ? `\n[assistant_error]\n${assistantError}` : '',
|
|
612
|
-
stderr.trim() !== '' ? `\n[stderr]\n${stderr.trim()}` : '',
|
|
613
|
-
].join('').trim()
|
|
614
|
-
|
|
615
|
-
console.log(JSON.stringify({
|
|
616
|
-
sessionId: state.data.sessionId,
|
|
617
|
-
sessionFile: state.data.sessionFile ?? '',
|
|
618
|
-
status,
|
|
619
|
-
output,
|
|
620
|
-
notes,
|
|
621
|
-
role: '',
|
|
622
|
-
model: requestedModel,
|
|
623
|
-
toolCalls,
|
|
624
|
-
toolErrors,
|
|
625
|
-
messageUpdates,
|
|
626
|
-
stopReason: assistantStopReason,
|
|
627
|
-
loopDetected,
|
|
628
|
-
loopSignature,
|
|
629
|
-
terminalReason,
|
|
630
|
-
}))
|
|
631
|
-
} finally {
|
|
632
|
-
stopWatchingParent()
|
|
633
|
-
if (heartbeatInterval) {
|
|
634
|
-
clearInterval(heartbeatInterval)
|
|
635
|
-
}
|
|
636
|
-
if (shutdownEscalationTimer) {
|
|
637
|
-
clearTimeout(shutdownEscalationTimer)
|
|
638
|
-
}
|
|
639
|
-
stopReading()
|
|
640
|
-
for (const current of pending.values()) {
|
|
641
|
-
current.reject(new Error('RPC adapter shutting down'))
|
|
642
|
-
}
|
|
643
|
-
pending.clear()
|
|
644
|
-
|
|
645
|
-
signalProcessTree(child.pid, 'SIGTERM')
|
|
646
|
-
await new Promise((resolve) => {
|
|
647
|
-
const timeout = setTimeout(() => {
|
|
648
|
-
signalProcessTree(child.pid, 'SIGKILL')
|
|
649
|
-
resolve()
|
|
650
|
-
}, 1000)
|
|
651
|
-
|
|
652
|
-
child.on('exit', () => {
|
|
653
|
-
clearTimeout(timeout)
|
|
654
|
-
resolve()
|
|
655
|
-
})
|
|
656
|
-
})
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
run().catch((error) => {
|
|
661
|
-
console.log(JSON.stringify({
|
|
662
|
-
sessionId: '',
|
|
663
|
-
sessionFile: '',
|
|
664
|
-
status: 'failed',
|
|
665
|
-
output: '',
|
|
666
|
-
notes: error instanceof Error ? error.message : String(error),
|
|
667
|
-
}))
|
|
668
|
-
})
|