@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
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import {
|
|
5
|
+
formatHeartbeatReason,
|
|
6
|
+
formatHeartbeatTimeoutMessage,
|
|
7
|
+
getHeartbeatDecision,
|
|
8
|
+
resolveHeartbeatConfig,
|
|
9
|
+
} from './pi-heartbeat.mjs'
|
|
10
|
+
|
|
11
|
+
const THINKING_LEVELS = new Set(['off', 'minimal', 'low', 'medium', 'high', 'xhigh'])
|
|
12
|
+
|
|
13
|
+
function formatValue(value) {
|
|
14
|
+
const text = JSON.stringify(value)
|
|
15
|
+
if (text === undefined) {
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
if (text.length <= 160) {
|
|
19
|
+
return text
|
|
20
|
+
}
|
|
21
|
+
return `${text.slice(0, 157)}...`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractToolTarget(toolName, args) {
|
|
25
|
+
if (!args || typeof args !== 'object') {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ((toolName === 'read' || toolName === 'write' || toolName === 'edit') && typeof args.path === 'string') {
|
|
30
|
+
return args.path
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractShellCommand(args) {
|
|
37
|
+
if (!args || typeof args !== 'object') {
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof args.command === 'string') {
|
|
42
|
+
return args.command
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof args.cmd === 'string') {
|
|
46
|
+
return args.cmd
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isLargeShellRead(command) {
|
|
53
|
+
const text = String(command ?? '').trim()
|
|
54
|
+
if (text === '') {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (/^\s*cat\s+\S+/.test(text)) {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sedMatch = text.match(/sed\s+-n\s+['"]?(\d+)\s*,\s*(\d+)p['"]?/)
|
|
63
|
+
if (sedMatch) {
|
|
64
|
+
const start = Number.parseInt(sedMatch[1], 10)
|
|
65
|
+
const end = Number.parseInt(sedMatch[2], 10)
|
|
66
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
67
|
+
return (end - start) >= 120
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractAssistantText(message) {
|
|
75
|
+
if (!message || message.role !== 'assistant') {
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof message.content === 'string') {
|
|
80
|
+
return message.content
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(message.content)) {
|
|
84
|
+
return ''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return message.content
|
|
88
|
+
.filter((item) => item?.type === 'text')
|
|
89
|
+
.map((item) => item.text)
|
|
90
|
+
.join('')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getLastAssistantMessage(messages) {
|
|
94
|
+
if (!Array.isArray(messages)) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const reversed = [...messages].reverse()
|
|
99
|
+
return reversed.find((message) => message?.role === 'assistant') ?? null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function splitModelSpec(modelSpec) {
|
|
103
|
+
const raw = String(modelSpec ?? '').trim()
|
|
104
|
+
if (raw === '') {
|
|
105
|
+
return {
|
|
106
|
+
modelName: '',
|
|
107
|
+
thinkingLevel: '',
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lastColonIndex = raw.lastIndexOf(':')
|
|
112
|
+
if (lastColonIndex === -1) {
|
|
113
|
+
return {
|
|
114
|
+
modelName: raw,
|
|
115
|
+
thinkingLevel: '',
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const maybeThinking = raw.slice(lastColonIndex + 1).trim().toLowerCase()
|
|
120
|
+
if (!THINKING_LEVELS.has(maybeThinking)) {
|
|
121
|
+
return {
|
|
122
|
+
modelName: raw,
|
|
123
|
+
thinkingLevel: '',
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
modelName: raw.slice(0, lastColonIndex).trim(),
|
|
129
|
+
thinkingLevel: maybeThinking,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function normalizeToolNames(tools) {
|
|
134
|
+
if (typeof tools !== 'string') {
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [...new Set(
|
|
139
|
+
tools
|
|
140
|
+
.split(',')
|
|
141
|
+
.map((value) => value.trim())
|
|
142
|
+
.filter(Boolean)
|
|
143
|
+
)]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function loadPiSdk() {
|
|
147
|
+
const overrideModule = String(process.env.PI_SDK_MODULE ?? '').trim()
|
|
148
|
+
if (overrideModule !== '') {
|
|
149
|
+
try {
|
|
150
|
+
const specifier = path.isAbsolute(overrideModule)
|
|
151
|
+
? pathToFileURL(overrideModule).href
|
|
152
|
+
: overrideModule
|
|
153
|
+
return await import(specifier)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Failed to load PI SDK override module "${overrideModule}". `
|
|
157
|
+
+ `Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return await import('@mariozechner/pi-coding-agent')
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'SDK transport requires @mariozechner/pi-coding-agent to be installed in this package. '
|
|
167
|
+
+ `Original error: ${error instanceof Error ? error.message : String(error)}`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveAgentDir(pi) {
|
|
173
|
+
if (process.env.PI_CODING_AGENT_DIR) {
|
|
174
|
+
return path.resolve(process.env.PI_CODING_AGENT_DIR)
|
|
175
|
+
}
|
|
176
|
+
return pi.getAgentDir()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function createSessionManager(pi, request, sessionDir) {
|
|
180
|
+
if (request.sessionFile) {
|
|
181
|
+
return pi.SessionManager.open(request.sessionFile, sessionDir)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (request.sessionId) {
|
|
185
|
+
return pi.SessionManager.continueRecent(request.cwd, sessionDir)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return pi.SessionManager.create(request.cwd, sessionDir)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createTools(pi, cwd, tools) {
|
|
192
|
+
const names = normalizeToolNames(tools)
|
|
193
|
+
if (names.length === 0) {
|
|
194
|
+
return undefined
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const factories = {
|
|
198
|
+
read: pi.createReadTool,
|
|
199
|
+
bash: pi.createBashTool,
|
|
200
|
+
edit: pi.createEditTool,
|
|
201
|
+
write: pi.createWriteTool,
|
|
202
|
+
grep: pi.createGrepTool,
|
|
203
|
+
find: pi.createFindTool,
|
|
204
|
+
ls: pi.createLsTool,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return names.map((name) => {
|
|
208
|
+
const factory = factories[name]
|
|
209
|
+
if (!factory) {
|
|
210
|
+
throw new Error(`Unsupported PI tool "${name}" in SDK transport.`)
|
|
211
|
+
}
|
|
212
|
+
return factory(cwd)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function resolveModel(modelRegistry, requestedModel) {
|
|
217
|
+
const raw = String(requestedModel ?? '').trim()
|
|
218
|
+
if (raw === '') {
|
|
219
|
+
return undefined
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const { modelName } = splitModelSpec(raw)
|
|
223
|
+
if (modelName === '') {
|
|
224
|
+
return undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const slashIndex = modelName.indexOf('/')
|
|
228
|
+
if (slashIndex !== -1) {
|
|
229
|
+
const provider = modelName.slice(0, slashIndex).trim()
|
|
230
|
+
const id = modelName.slice(slashIndex + 1).trim()
|
|
231
|
+
const resolved = modelRegistry.find(provider, id)
|
|
232
|
+
if (!resolved) {
|
|
233
|
+
throw new Error(`Configured PI model "${raw}" could not be resolved via SDK model registry.`)
|
|
234
|
+
}
|
|
235
|
+
return resolved
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const matches = modelRegistry.getAll().filter((model) => {
|
|
239
|
+
if (!model || typeof model !== 'object') {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return model.id === modelName
|
|
244
|
+
|| model.name === modelName
|
|
245
|
+
|| `${model.provider}/${model.id}` === modelName
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
if (matches.length === 1) {
|
|
249
|
+
return matches[0]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (matches.length > 1) {
|
|
253
|
+
const candidates = matches.map((model) => `${model.provider}/${model.id}`).join(', ')
|
|
254
|
+
throw new Error(`Configured PI model "${raw}" is ambiguous in SDK model registry. Candidates: ${candidates}`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
throw new Error(`Configured PI model "${raw}" could not be resolved via SDK model registry.`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function createSdkSession(pi, request) {
|
|
261
|
+
const sessionDir = path.join(path.resolve(request.runtimeDir ?? path.join(request.cwd, '.pi-runtime')), 'sessions')
|
|
262
|
+
const agentDir = resolveAgentDir(pi)
|
|
263
|
+
const authStorage = pi.AuthStorage.create(path.join(agentDir, 'auth.json'))
|
|
264
|
+
const modelRegistry = pi.ModelRegistry.create(authStorage, path.join(agentDir, 'models.json'))
|
|
265
|
+
const settingsManager = pi.SettingsManager.create(request.cwd, agentDir)
|
|
266
|
+
settingsManager.applyOverrides({
|
|
267
|
+
retry: {
|
|
268
|
+
enabled: false,
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const { thinkingLevel: modelSpecThinking } = splitModelSpec(request.model)
|
|
273
|
+
const thinkingLevel = String(request.thinking || modelSpecThinking || '').trim()
|
|
274
|
+
const resourceLoader = new pi.DefaultResourceLoader({
|
|
275
|
+
cwd: request.cwd,
|
|
276
|
+
agentDir,
|
|
277
|
+
settingsManager,
|
|
278
|
+
noExtensions: request.noExtensions === true,
|
|
279
|
+
noSkills: request.noSkills === true,
|
|
280
|
+
noPromptTemplates: request.noPromptTemplates === true,
|
|
281
|
+
noThemes: request.noThemes !== false,
|
|
282
|
+
})
|
|
283
|
+
await resourceLoader.reload()
|
|
284
|
+
|
|
285
|
+
const model = await resolveModel(modelRegistry, request.model)
|
|
286
|
+
const tools = createTools(pi, request.cwd, request.tools)
|
|
287
|
+
const sessionManager = createSessionManager(pi, request, sessionDir)
|
|
288
|
+
|
|
289
|
+
return pi.createAgentSession({
|
|
290
|
+
cwd: request.cwd,
|
|
291
|
+
agentDir,
|
|
292
|
+
authStorage,
|
|
293
|
+
modelRegistry,
|
|
294
|
+
resourceLoader,
|
|
295
|
+
sessionManager,
|
|
296
|
+
settingsManager,
|
|
297
|
+
...(model ? { model } : {}),
|
|
298
|
+
...(thinkingLevel !== '' ? { thinkingLevel } : {}),
|
|
299
|
+
...(tools ? { tools } : {}),
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function safeAbort(session) {
|
|
304
|
+
try {
|
|
305
|
+
await session.abort()
|
|
306
|
+
} catch {}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function runSdkTurnWithPi(pi, request) {
|
|
310
|
+
const streamTerminal = request.streamTerminal === true
|
|
311
|
+
const requestedModel = typeof request.model === 'string' ? request.model : ''
|
|
312
|
+
const loopRepeatThreshold = Number.isFinite(Number(request.loopRepeatThreshold))
|
|
313
|
+
? Number(request.loopRepeatThreshold)
|
|
314
|
+
: 12
|
|
315
|
+
const samePathRepeatThreshold = Number.isFinite(Number(request.samePathRepeatThreshold))
|
|
316
|
+
? Number(request.samePathRepeatThreshold)
|
|
317
|
+
: 8
|
|
318
|
+
const {
|
|
319
|
+
continueAfterSeconds,
|
|
320
|
+
noEventTimeoutSeconds,
|
|
321
|
+
toolContinueAfterSeconds,
|
|
322
|
+
toolNoEventTimeoutSeconds,
|
|
323
|
+
} = resolveHeartbeatConfig(request)
|
|
324
|
+
const continueMessage = typeof request.continueMessage === 'string' && request.continueMessage.trim() !== ''
|
|
325
|
+
? request.continueMessage.trim()
|
|
326
|
+
: 'continue'
|
|
327
|
+
|
|
328
|
+
let unsubscribe = () => {}
|
|
329
|
+
let assistantLineOpen = false
|
|
330
|
+
let streamedAssistantText = false
|
|
331
|
+
let lastToolSignature = ''
|
|
332
|
+
let repeatedToolCount = 0
|
|
333
|
+
let lastToolTarget = ''
|
|
334
|
+
let repeatedTargetCount = 0
|
|
335
|
+
let loopDetected = false
|
|
336
|
+
let loopSignature = ''
|
|
337
|
+
let abortRequested = false
|
|
338
|
+
let heartbeatTimedOut = false
|
|
339
|
+
let heartbeatReason = ''
|
|
340
|
+
let heartbeatInterval = null
|
|
341
|
+
let continueAttempted = false
|
|
342
|
+
let continueAccepted = false
|
|
343
|
+
let continueRejected = false
|
|
344
|
+
let agentStarted = false
|
|
345
|
+
let agentEnded = false
|
|
346
|
+
let activeToolName = ''
|
|
347
|
+
let activeToolStartedAt = 0
|
|
348
|
+
let lastEventAt = Date.now()
|
|
349
|
+
const events = []
|
|
350
|
+
|
|
351
|
+
const writeLive = (text) => {
|
|
352
|
+
if (!streamTerminal) {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
process.stderr.write(text)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const ensureAssistantLine = () => {
|
|
359
|
+
if (!assistantLineOpen) {
|
|
360
|
+
writeLive('[PI assistant] ')
|
|
361
|
+
assistantLineOpen = true
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const closeAssistantLine = () => {
|
|
366
|
+
if (assistantLineOpen) {
|
|
367
|
+
writeLive('\n')
|
|
368
|
+
assistantLineOpen = false
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let session
|
|
373
|
+
try {
|
|
374
|
+
const created = await createSdkSession(pi, request)
|
|
375
|
+
session = created.session
|
|
376
|
+
|
|
377
|
+
if (!request.model && !session.model) {
|
|
378
|
+
throw new Error('No PI model configured. Set PI_MODEL or configure a default model in PI.')
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const requestAbortForLoop = () => {
|
|
382
|
+
if (abortRequested) {
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
abortRequested = true
|
|
387
|
+
closeAssistantLine()
|
|
388
|
+
writeLive(`[PI guard] repeated tool loop detected: ${loopSignature} x${repeatedToolCount}. Aborting current turn.\n`)
|
|
389
|
+
void safeAbort(session)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const requestAbortForHeartbeat = (decision) => {
|
|
393
|
+
if (abortRequested) {
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
abortRequested = true
|
|
398
|
+
heartbeatTimedOut = true
|
|
399
|
+
heartbeatReason = formatHeartbeatReason(decision)
|
|
400
|
+
closeAssistantLine()
|
|
401
|
+
writeLive(`[PI guard] ${formatHeartbeatTimeoutMessage(decision)} Aborting current turn.\n`)
|
|
402
|
+
void safeAbort(session)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const requestSoftContinue = (decision) => {
|
|
406
|
+
if (abortRequested || continueAttempted || agentEnded) {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
continueAttempted = true
|
|
411
|
+
closeAssistantLine()
|
|
412
|
+
const context = decision.activeToolName
|
|
413
|
+
? ` while tool "${decision.activeToolName}" is running`
|
|
414
|
+
: ''
|
|
415
|
+
writeLive(`[PI guard] no PI events for ${decision.continueAfterSeconds}s${context}. Sending soft continue prompt.\n`)
|
|
416
|
+
void session.steer(continueMessage)
|
|
417
|
+
.then(() => {
|
|
418
|
+
continueAccepted = true
|
|
419
|
+
writeLive('[PI guard] soft continue accepted by PI.\n')
|
|
420
|
+
})
|
|
421
|
+
.catch((error) => {
|
|
422
|
+
continueRejected = true
|
|
423
|
+
writeLive(`[PI guard] soft continue failed: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
unsubscribe = session.subscribe((event) => {
|
|
428
|
+
events.push(event)
|
|
429
|
+
lastEventAt = Date.now()
|
|
430
|
+
|
|
431
|
+
if (event.type === 'agent_start') {
|
|
432
|
+
agentStarted = true
|
|
433
|
+
writeLive('[PI] agent started\n')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
|
|
437
|
+
ensureAssistantLine()
|
|
438
|
+
writeLive(event.assistantMessageEvent.delta)
|
|
439
|
+
streamedAssistantText = true
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (event.type === 'message_end' && event.message?.role === 'assistant') {
|
|
443
|
+
const text = extractAssistantText(event.message)
|
|
444
|
+
if (assistantLineOpen) {
|
|
445
|
+
closeAssistantLine()
|
|
446
|
+
} else if (!streamedAssistantText && text.trim() !== '') {
|
|
447
|
+
writeLive(`[PI assistant] ${text}\n`)
|
|
448
|
+
}
|
|
449
|
+
streamedAssistantText = false
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (event.type === 'tool_execution_start') {
|
|
453
|
+
closeAssistantLine()
|
|
454
|
+
const argsText = formatValue(event.args)
|
|
455
|
+
const suffix = argsText === '' ? '' : ` ${argsText}`
|
|
456
|
+
const signature = `${event.toolName}${suffix}`
|
|
457
|
+
activeToolName = String(event.toolName ?? '')
|
|
458
|
+
activeToolStartedAt = Date.now()
|
|
459
|
+
const target = extractToolTarget(event.toolName, event.args)
|
|
460
|
+
const shellCommand = event.toolName === 'bash' ? extractShellCommand(event.args) : ''
|
|
461
|
+
|
|
462
|
+
if (signature === lastToolSignature) {
|
|
463
|
+
repeatedToolCount += 1
|
|
464
|
+
} else {
|
|
465
|
+
lastToolSignature = signature
|
|
466
|
+
repeatedToolCount = 1
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (target !== '' && target === lastToolTarget) {
|
|
470
|
+
repeatedTargetCount += 1
|
|
471
|
+
} else if (target !== '') {
|
|
472
|
+
lastToolTarget = target
|
|
473
|
+
repeatedTargetCount = 1
|
|
474
|
+
} else {
|
|
475
|
+
lastToolTarget = ''
|
|
476
|
+
repeatedTargetCount = 0
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!loopDetected && repeatedToolCount >= loopRepeatThreshold) {
|
|
480
|
+
loopDetected = true
|
|
481
|
+
loopSignature = signature
|
|
482
|
+
requestAbortForLoop()
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!loopDetected && target !== '' && repeatedTargetCount >= samePathRepeatThreshold) {
|
|
486
|
+
loopDetected = true
|
|
487
|
+
loopSignature = `same_path:${target}`
|
|
488
|
+
requestAbortForLoop()
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
writeLive(`[PI tool:start] ${event.toolName}${suffix}\n`)
|
|
492
|
+
if (event.toolName === 'bash' && isLargeShellRead(shellCommand)) {
|
|
493
|
+
writeLive('[PI warning] large bash file read detected; prefer read or a smaller exact window to avoid truncated context.\n')
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (event.type === 'tool_execution_end') {
|
|
498
|
+
closeAssistantLine()
|
|
499
|
+
activeToolName = ''
|
|
500
|
+
activeToolStartedAt = 0
|
|
501
|
+
writeLive(`[PI tool:end] ${event.toolName} ${event.isError ? 'error' : 'ok'}\n`)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (event.type === 'agent_end') {
|
|
505
|
+
agentEnded = true
|
|
506
|
+
closeAssistantLine()
|
|
507
|
+
writeLive('[PI] agent finished\n')
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
heartbeatInterval = setInterval(() => {
|
|
512
|
+
const decision = getHeartbeatDecision({
|
|
513
|
+
now: Date.now(),
|
|
514
|
+
agentStarted,
|
|
515
|
+
agentEnded,
|
|
516
|
+
heartbeatTimedOut,
|
|
517
|
+
childExited: false,
|
|
518
|
+
lastEventAt,
|
|
519
|
+
continueAttempted,
|
|
520
|
+
activeToolName,
|
|
521
|
+
activeToolStartedAt,
|
|
522
|
+
continueAfterSeconds,
|
|
523
|
+
noEventTimeoutSeconds,
|
|
524
|
+
toolContinueAfterSeconds,
|
|
525
|
+
toolNoEventTimeoutSeconds,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
if (decision.action === 'soft_continue') {
|
|
529
|
+
requestSoftContinue(decision)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (decision.action === 'abort') {
|
|
534
|
+
requestAbortForHeartbeat(decision)
|
|
535
|
+
}
|
|
536
|
+
}, 1000)
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
await session.prompt(request.prompt)
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (!heartbeatTimedOut && !loopDetected) {
|
|
542
|
+
throw error
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (heartbeatTimedOut) {
|
|
547
|
+
const toolCalls = events.filter((event) => event.type === 'tool_execution_start').length
|
|
548
|
+
const toolErrors = events.filter((event) => event.type === 'tool_execution_end' && event.isError).length
|
|
549
|
+
const messageUpdates = events.filter((event) => event.type === 'message_update').length
|
|
550
|
+
return {
|
|
551
|
+
sessionId: session.sessionId ?? request.sessionId ?? '',
|
|
552
|
+
sessionFile: session.sessionFile ?? request.sessionFile ?? '',
|
|
553
|
+
status: 'timed_out',
|
|
554
|
+
output: [
|
|
555
|
+
'[heartbeat_timeout]',
|
|
556
|
+
formatHeartbeatTimeoutMessage({
|
|
557
|
+
noEventTimeoutSeconds,
|
|
558
|
+
activeToolName,
|
|
559
|
+
toolRuntimeSeconds: activeToolStartedAt > 0
|
|
560
|
+
? Math.max(0, Math.floor((Date.now() - activeToolStartedAt) / 1000))
|
|
561
|
+
: 0,
|
|
562
|
+
}),
|
|
563
|
+
].join('\n').trim(),
|
|
564
|
+
notes: [
|
|
565
|
+
heartbeatReason,
|
|
566
|
+
continueAttempted ? `continue_attempted=${continueMessage}` : '',
|
|
567
|
+
continueAccepted ? 'continue_accepted=true' : '',
|
|
568
|
+
continueRejected ? 'continue_rejected=true' : '',
|
|
569
|
+
].join(' '),
|
|
570
|
+
role: '',
|
|
571
|
+
model: requestedModel,
|
|
572
|
+
toolCalls,
|
|
573
|
+
toolErrors,
|
|
574
|
+
messageUpdates,
|
|
575
|
+
stopReason: '',
|
|
576
|
+
loopDetected: false,
|
|
577
|
+
loopSignature: '',
|
|
578
|
+
terminalReason: 'heartbeat_timeout',
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const toolCalls = events.filter((event) => event.type === 'tool_execution_start').length
|
|
583
|
+
const toolErrors = events.filter((event) => event.type === 'tool_execution_end' && event.isError).length
|
|
584
|
+
const messageUpdates = events.filter((event) => event.type === 'message_update').length
|
|
585
|
+
const lastAssistantMessage = getLastAssistantMessage(session.messages)
|
|
586
|
+
const assistantText = extractAssistantText(lastAssistantMessage).trim()
|
|
587
|
+
const assistantError = String(lastAssistantMessage?.errorMessage ?? '').trim()
|
|
588
|
+
const assistantStopReason = String(lastAssistantMessage?.stopReason ?? '').trim()
|
|
589
|
+
const status = loopDetected
|
|
590
|
+
? 'stalled'
|
|
591
|
+
: assistantError !== '' || (assistantText === '' && toolCalls === 0 && messageUpdates === 0)
|
|
592
|
+
? 'failed'
|
|
593
|
+
: 'success'
|
|
594
|
+
const terminalReason = loopDetected
|
|
595
|
+
? 'loop_detected'
|
|
596
|
+
: assistantError !== ''
|
|
597
|
+
? 'assistant_error'
|
|
598
|
+
: assistantStopReason === 'length'
|
|
599
|
+
? 'assistant_stop_length'
|
|
600
|
+
: status === 'failed'
|
|
601
|
+
? 'empty_agent_turn'
|
|
602
|
+
: 'agent_completed'
|
|
603
|
+
const notes = [
|
|
604
|
+
`PI session ${session.sessionId}`,
|
|
605
|
+
`tool_calls=${toolCalls}`,
|
|
606
|
+
`tool_errors=${toolErrors}`,
|
|
607
|
+
`message_updates=${messageUpdates}`,
|
|
608
|
+
activeToolName !== '' ? `active_tool=${activeToolName}` : '',
|
|
609
|
+
continueAttempted ? `continue_attempted=${continueMessage}` : '',
|
|
610
|
+
continueAccepted ? 'continue_accepted=true' : '',
|
|
611
|
+
continueRejected ? 'continue_rejected=true' : '',
|
|
612
|
+
loopDetected ? `loop_detected=${loopSignature}` : '',
|
|
613
|
+
loopDetected ? `loop_repeats=${repeatedToolCount}` : '',
|
|
614
|
+
assistantStopReason !== '' ? `stop_reason=${assistantStopReason}` : '',
|
|
615
|
+
assistantError !== '' ? `assistant_error=${assistantError}` : '',
|
|
616
|
+
status === 'failed' && assistantError === '' ? 'empty_agent_turn' : '',
|
|
617
|
+
].join(' ')
|
|
618
|
+
const output = [
|
|
619
|
+
assistantText,
|
|
620
|
+
loopDetected ? `\n[loop_guard]\nRepeated tool loop detected: ${loopSignature} x${repeatedToolCount}` : '',
|
|
621
|
+
assistantError !== '' ? `\n[assistant_error]\n${assistantError}` : '',
|
|
622
|
+
].join('').trim()
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
sessionId: session.sessionId,
|
|
626
|
+
sessionFile: session.sessionFile ?? '',
|
|
627
|
+
status,
|
|
628
|
+
output,
|
|
629
|
+
notes,
|
|
630
|
+
role: '',
|
|
631
|
+
model: requestedModel,
|
|
632
|
+
toolCalls,
|
|
633
|
+
toolErrors,
|
|
634
|
+
messageUpdates,
|
|
635
|
+
stopReason: assistantStopReason,
|
|
636
|
+
loopDetected,
|
|
637
|
+
loopSignature,
|
|
638
|
+
terminalReason,
|
|
639
|
+
}
|
|
640
|
+
} finally {
|
|
641
|
+
unsubscribe()
|
|
642
|
+
if (heartbeatInterval) {
|
|
643
|
+
clearInterval(heartbeatInterval)
|
|
644
|
+
}
|
|
645
|
+
if (session) {
|
|
646
|
+
session.dispose()
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export async function runSdkTurn(request) {
|
|
652
|
+
const pi = await loadPiSdk()
|
|
653
|
+
return await runSdkTurnWithPi(pi, request)
|
|
654
|
+
}
|