@mono-agent/agent-runtime 0.1.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.
Files changed (60) hide show
  1. package/ARCHITECTURE.md +219 -0
  2. package/LICENSE +674 -0
  3. package/README.md +430 -0
  4. package/package.json +46 -0
  5. package/src/agent/allowlists.js +49 -0
  6. package/src/agent/approval.js +211 -0
  7. package/src/agent/compaction.js +752 -0
  8. package/src/agent/index.js +40 -0
  9. package/src/agent/prompt/skill-index.js +66 -0
  10. package/src/agent/tool-bloat.js +164 -0
  11. package/src/agent/tools/bash.js +156 -0
  12. package/src/agent/tools/edit.js +15 -0
  13. package/src/agent/tools/glob.js +71 -0
  14. package/src/agent/tools/grep.js +84 -0
  15. package/src/agent/tools/index.js +17 -0
  16. package/src/agent/tools/pi-bridge.js +638 -0
  17. package/src/agent/tools/read.js +39 -0
  18. package/src/agent/tools/shared/constants.js +21 -0
  19. package/src/agent/tools/shared/dedup.js +31 -0
  20. package/src/agent/tools/shared/output-truncation.js +54 -0
  21. package/src/agent/tools/shared/path-resolver.js +156 -0
  22. package/src/agent/tools/shared/ripgrep.js +130 -0
  23. package/src/agent/tools/shared/runtime-context.js +69 -0
  24. package/src/agent/tools/web-fetch.js +59 -0
  25. package/src/agent/tools/web-search.js +21 -0
  26. package/src/agent/tools/write.js +14 -0
  27. package/src/agent/transcript.js +227 -0
  28. package/src/ai/backend.js +17 -0
  29. package/src/ai/cost.js +164 -0
  30. package/src/ai/failure.js +165 -0
  31. package/src/ai/file-change-stats.js +234 -0
  32. package/src/ai/index.js +16 -0
  33. package/src/ai/live-input-prompt.js +15 -0
  34. package/src/ai/observer.js +233 -0
  35. package/src/ai/providers/claude-cli.js +694 -0
  36. package/src/ai/providers/claude-sdk.js +864 -0
  37. package/src/ai/providers/claude-subagents.js +67 -0
  38. package/src/ai/providers/codex-app.js +1045 -0
  39. package/src/ai/providers/opencode-app.js +356 -0
  40. package/src/ai/providers/opencode-discovery.js +39 -0
  41. package/src/ai/providers/pi-events.js +62 -0
  42. package/src/ai/providers/pi-messages.js +68 -0
  43. package/src/ai/providers/pi-models.js +111 -0
  44. package/src/ai/providers/pi-sdk.js +1310 -0
  45. package/src/ai/registry.js +5 -0
  46. package/src/ai/runtime/capabilities-used.js +56 -0
  47. package/src/ai/runtime/capabilities.js +44 -0
  48. package/src/ai/runtime/context-windows.js +38 -0
  49. package/src/ai/runtime/fast-mode.js +8 -0
  50. package/src/ai/runtime/model-refs.js +144 -0
  51. package/src/ai/runtime/registry.js +57 -0
  52. package/src/ai/runtime/router.js +214 -0
  53. package/src/ai/runtime/sessions.js +126 -0
  54. package/src/ai/streaming/codex-events.js +139 -0
  55. package/src/ai/streaming/opencode-events.js +54 -0
  56. package/src/ai/types.js +70 -0
  57. package/src/index.js +23 -0
  58. package/src/pi-auth.js +80 -0
  59. package/src/runtime-brand.js +32 -0
  60. package/src/runtime.js +104 -0
@@ -0,0 +1,1310 @@
1
+ import { Agent, InMemorySessionRepo, JsonlSessionRepo } from "@earendil-works/pi-agent-core";
2
+ import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
3
+ import { stream as piStream, streamSimple as piStreamSimple } from "@earendil-works/pi-ai";
4
+ import * as openAiCodexResponses from "@earendil-works/pi-ai/openai-codex-responses";
5
+ import { randomUUID } from "node:crypto";
6
+ import { estimateCost } from "../cost.js";
7
+ import { PROVIDER_ABORT_RE } from "../failure.js";
8
+ import { runtimeCapabilities } from "../runtime/capabilities.js";
9
+ import { createSessionRegistry } from "../runtime/sessions.js";
10
+ import { formatLiveInputGuidance } from "../live-input-prompt.js";
11
+ import {
12
+ createAgentCompactionManager,
13
+ isLikelyContextTermination,
14
+ } from "../../agent/compaction.js";
15
+ import {
16
+ closePiMcpClients,
17
+ createStructuredOutputTool,
18
+ getPiBuiltinTools,
19
+ initPiMcpTools,
20
+ } from "../../agent/tools/pi-bridge.js";
21
+ import { createApprovalManager } from "../../agent/approval.js";
22
+ import { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings } from "../runtime/capabilities-used.js";
23
+ import { resolvePiRuntimeModel } from "./pi-models.js";
24
+ import {
25
+ promptTextFromMessages,
26
+ textFromContent,
27
+ thinkingFromContent,
28
+ toAgentMessages,
29
+ toolResultContent,
30
+ } from "./pi-messages.js";
31
+ import {
32
+ compactToolRawResult,
33
+ emitCaptured,
34
+ eventToolArgs,
35
+ jsonSerializable,
36
+ streamContentKey,
37
+ } from "./pi-events.js";
38
+
39
+ function usageFromMessages(messages = []) {
40
+ const usage = {
41
+ input: 0,
42
+ output: 0,
43
+ cacheRead: 0,
44
+ cacheWrite: 0,
45
+ cost: 0,
46
+ };
47
+ for (const message of messages) {
48
+ if (message?.role !== "assistant") continue;
49
+ const next = message.usage || {};
50
+ usage.input += Number(next.input) || 0;
51
+ usage.output += Number(next.output) || 0;
52
+ usage.cacheRead += Number(next.cacheRead) || 0;
53
+ usage.cacheWrite += Number(next.cacheWrite) || 0;
54
+ usage.cost += Number(next.cost?.total) || 0;
55
+ }
56
+ return usage;
57
+ }
58
+
59
+ function objectSchema(properties, required = []) {
60
+ return { type: "object", properties, required, additionalProperties: false };
61
+ }
62
+
63
+ function textToolResult(text, details = {}) {
64
+ return {
65
+ content: [{ type: "text", text: String(text ?? "") }],
66
+ details,
67
+ };
68
+ }
69
+
70
+ function usageFromRuntimeResult(result) {
71
+ const usage = result?.usage || {};
72
+ return {
73
+ input: Number(usage.input_tokens ?? usage.input) || 0,
74
+ output: Number(usage.output_tokens ?? usage.output) || 0,
75
+ cacheRead: Number(usage.cache_read_tokens ?? usage.cacheRead) || 0,
76
+ cacheWrite: Number(usage.cache_creation_tokens ?? usage.cache_write_tokens ?? usage.cacheWrite) || 0,
77
+ cost: Number(usage.cost_usd ?? usage.cost?.total ?? usage.cost) || 0,
78
+ };
79
+ }
80
+
81
+ function addUsage(target, addition) {
82
+ target.input += Number(addition.input) || 0;
83
+ target.output += Number(addition.output) || 0;
84
+ target.cacheRead += Number(addition.cacheRead) || 0;
85
+ target.cacheWrite += Number(addition.cacheWrite) || 0;
86
+ target.cost += Number(addition.cost) || 0;
87
+ }
88
+
89
+ function thinkingLevelForEffort(effort, capabilities) {
90
+ if (!capabilities?.reasoning || capabilities.reasoning_mode === "none") return "off";
91
+ if (effort === "none") return "off";
92
+ if (effort === "max") return "xhigh";
93
+ if (effort === "xhigh") return "xhigh";
94
+ if (effort === "high") return "high";
95
+ if (effort === "medium") return "medium";
96
+ return "low";
97
+ }
98
+
99
+ const OPENAI_REASONING_APIS = new Set(["openai-responses", "openai-codex-responses"]);
100
+
101
+ function openAiReasoningOptions(model, streamOptions = {}, runtimeOptions = {}) {
102
+ const hasConfiguredSummary = Object.prototype.hasOwnProperty.call(runtimeOptions, "piReasoningSummary")
103
+ && runtimeOptions.piReasoningSummary !== undefined;
104
+ if (!hasConfiguredSummary) return streamOptions;
105
+ if (!OPENAI_REASONING_APIS.has(model?.api)) return streamOptions;
106
+ if (!streamOptions.reasoning || streamOptions.reasoning === "off") return streamOptions;
107
+ const reasoningSummary = Object.prototype.hasOwnProperty.call(streamOptions, "reasoningSummary")
108
+ ? streamOptions.reasoningSummary
109
+ : runtimeOptions.piReasoningSummary;
110
+ return {
111
+ ...streamOptions,
112
+ reasoningEffort: streamOptions.reasoningEffort ?? streamOptions.reasoning,
113
+ reasoningSummary,
114
+ };
115
+ }
116
+
117
+ function createPiRuntimeStreamFn(streamFn, runtimeOptions = {}) {
118
+ return (model, context, options = {}) => {
119
+ const enrichedOptions = openAiReasoningOptions(model, options, runtimeOptions);
120
+ if (streamFn) return streamFn(model, context, enrichedOptions);
121
+ const useFullOpenAiStream = enrichedOptions !== options && OPENAI_REASONING_APIS.has(model?.api);
122
+ return (useFullOpenAiStream ? piStream : piStreamSimple)(model, context, enrichedOptions);
123
+ };
124
+ }
125
+
126
+ function appendStructuredOutputInstruction(systemPrompt, outputSchema) {
127
+ if (!outputSchema) return systemPrompt;
128
+ return [
129
+ systemPrompt,
130
+ "",
131
+ "Structured output is available through the `StructuredOutput` tool.",
132
+ "When the final result is ready, call `StructuredOutput` with the complete JSON object matching the requested schema.",
133
+ "Do not also print the same JSON as prose unless tool calling is unavailable.",
134
+ ].join("\n");
135
+ }
136
+
137
+ function structuredOutputFinalizationPrompt() {
138
+ return [
139
+ "The previous assistant turn ended without submitting the required structured result.",
140
+ "Do not run tools, inspect files, or redo work.",
141
+ "Call only `StructuredOutput` once with the final object matching the requested schema, based on the completed transcript above.",
142
+ "Do not print prose before or after the tool call.",
143
+ ].join("\n");
144
+ }
145
+
146
+ function shouldRetryStructuredOutputFinalization({
147
+ outputSchema,
148
+ structuredResult,
149
+ finalText,
150
+ stopReason,
151
+ externalAbort,
152
+ maxTurnsHit,
153
+ }) {
154
+ if (!outputSchema) return false;
155
+ if (structuredResult !== null && structuredResult !== undefined) return false;
156
+ if (String(finalText || "").trim()) return false;
157
+ if (externalAbort || maxTurnsHit) return false;
158
+ return stopReason !== "error" && stopReason !== "aborted";
159
+ }
160
+
161
+ function structuredOutputRetryDiagnostics(attempts, reason, failed) {
162
+ if (!attempts) return {};
163
+ return {
164
+ structured_output_finalization_retry_attempts: attempts,
165
+ structured_output_finalization_retry_reason: reason,
166
+ structured_output_finalization_retry_failed: !!failed,
167
+ };
168
+ }
169
+
170
+ async function resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnings }) {
171
+ if (apiKeys?.has(provider)) return apiKeys.get(provider);
172
+ if (typeof resolvePiApiKey !== "function") return undefined;
173
+ try {
174
+ return await resolvePiApiKey(provider);
175
+ } catch (err) {
176
+ runtimeWarnings.push({
177
+ warning_kind: "pi_auth_failed",
178
+ provider,
179
+ message: err?.message || String(err),
180
+ });
181
+ return undefined;
182
+ }
183
+ }
184
+
185
+ function tryParseJson(text) {
186
+ try { return JSON.parse(text); } catch { return null; }
187
+ }
188
+
189
+ export function normalizePiErrorMessage(message) {
190
+ const text = String(message || "").trim();
191
+ if (!text) return null;
192
+ const codexMatch = /^Codex error:\s*(\{[\s\S]*\})$/i.exec(text);
193
+ const parsed = tryParseJson(codexMatch ? codexMatch[1] : text);
194
+ const nested = parsed?.error || parsed;
195
+ if (typeof nested?.message === "string" && nested.message.trim()) return nested.message.trim();
196
+ if (typeof nested?.error?.message === "string" && nested.error.message.trim()) return nested.error.message.trim();
197
+ return text;
198
+ }
199
+
200
+ export function isContextLimitError(message) {
201
+ const text = String(message || "");
202
+ if (/rate limit|too many requests/i.test(text)) return false;
203
+ return /context[_ ]length[_ ]exceeded|exceeds the context window|too many tokens|maximum context length|token limit exceeded|prompt is too long/i.test(text);
204
+ }
205
+
206
+ function failureKindForPiError(message, diagnostics, { maxTurnsHit = false } = {}) {
207
+ if (!message) return null;
208
+ if (maxTurnsHit || isContextLimitError(message) || isLikelyContextTermination(message, diagnostics)) return "usage_limit";
209
+ return "provider_unavailable";
210
+ }
211
+
212
+ function pickFirstString(...values) {
213
+ for (const value of values) {
214
+ if (typeof value === "string" && value.trim()) return value.trim();
215
+ }
216
+ return null;
217
+ }
218
+
219
+ function capturePiErrorPayload(message) {
220
+ if (!message) return null;
221
+ const errorMessage = pickFirstString(
222
+ message.errorMessage,
223
+ message.error?.errorMessage,
224
+ message.error?.message,
225
+ );
226
+ const code = pickFirstString(
227
+ message.code,
228
+ message.error?.code,
229
+ message.cause?.code,
230
+ message.errorCode,
231
+ message.error?.error?.code,
232
+ );
233
+ const requestId = pickFirstString(
234
+ message.requestId,
235
+ message.request_id,
236
+ message.error?.requestId,
237
+ message.error?.request_id,
238
+ );
239
+ const stopReason = pickFirstString(message.stopReason, message.stop_reason);
240
+ if (!errorMessage && !code && !requestId && !stopReason) return null;
241
+ return {
242
+ stop_reason: stopReason,
243
+ error_message: errorMessage,
244
+ code,
245
+ request_id: requestId,
246
+ };
247
+ }
248
+
249
+ function pickPiErrorCodeFromException(err) {
250
+ if (!err) return null;
251
+ return pickFirstString(
252
+ err.code,
253
+ err.cause?.code,
254
+ err.errno && String(err.errno),
255
+ );
256
+ }
257
+
258
+ function inferPiErrorCode(message) {
259
+ if (/websocket/i.test(String(message || ""))) return "websocket_error";
260
+ return null;
261
+ }
262
+
263
+ function lastTextSnippet(...sources) {
264
+ for (let i = sources.length - 1; i >= 0; i -= 1) {
265
+ const arr = sources[i];
266
+ if (!Array.isArray(arr)) continue;
267
+ for (let j = arr.length - 1; j >= 0; j -= 1) {
268
+ const text = arr[j];
269
+ if (typeof text === "string" && text.trim()) {
270
+ const trimmed = text.trim();
271
+ return trimmed.length > 200 ? trimmed.slice(-200) : trimmed;
272
+ }
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
278
+ function readRuntimeSettings(explicitSettings) {
279
+ // Settings are now resolved by the caller (core/ai.js#generateResponse)
280
+ // and passed in via options.settings. The provider no longer reaches
281
+ // back into core/settings.js.
282
+ return explicitSettings && typeof explicitSettings === "object" ? explicitSettings : {};
283
+ }
284
+
285
+ const PI_CODEX_TRANSPORTS = new Set(["sse", "auto", "websocket", "websocket-cached"]);
286
+ const PI_CODEX_WEBSOCKET_TRANSPORTS = new Set(["auto", "websocket", "websocket-cached"]);
287
+
288
+ function resolvePiTransport(model, runtimeWarnings, requested) {
289
+ if (model?.api !== "openai-codex-responses") return "auto";
290
+ const raw = typeof requested === "string" ? requested.trim() : "";
291
+ if (!raw) return "sse";
292
+ const transport = raw.toLowerCase();
293
+ if (PI_CODEX_TRANSPORTS.has(transport)) return transport;
294
+ runtimeWarnings.push({
295
+ warning_kind: "invalid_pi_codex_transport",
296
+ message: "Ignoring invalid piCodexTransport; expected sse, auto, websocket, or websocket-cached.",
297
+ value: raw,
298
+ });
299
+ return "sse";
300
+ }
301
+
302
+ function isPlainObject(value) {
303
+ return !!value && typeof value === "object" && !Array.isArray(value);
304
+ }
305
+
306
+ function sanitizeDiagnosticsObject(value) {
307
+ if (!isPlainObject(value)) return null;
308
+ try {
309
+ return JSON.parse(JSON.stringify(value));
310
+ } catch {
311
+ const out = {};
312
+ for (const [key, item] of Object.entries(value)) {
313
+ if (
314
+ item == null
315
+ || typeof item === "string"
316
+ || typeof item === "number"
317
+ || typeof item === "boolean"
318
+ ) {
319
+ out[key] = item;
320
+ }
321
+ }
322
+ return Object.keys(out).length ? out : null;
323
+ }
324
+ }
325
+
326
+ function piWebSocketDebugStats(providerSessionId, transport) {
327
+ if (!providerSessionId || !PI_CODEX_WEBSOCKET_TRANSPORTS.has(transport)) return null;
328
+ try {
329
+ const stats = openAiCodexResponses.getOpenAICodexWebSocketDebugStats?.(providerSessionId);
330
+ return sanitizeDiagnosticsObject(stats);
331
+ } catch {
332
+ return null;
333
+ }
334
+ }
335
+
336
+ function normalizeTransportFailureDiagnostic(diagnostic) {
337
+ if (!isPlainObject(diagnostic) || diagnostic.type !== "provider_transport_failure") return null;
338
+ const details = isPlainObject(diagnostic.details) ? diagnostic.details : {};
339
+ const error = isPlainObject(diagnostic.error) ? diagnostic.error : {};
340
+ return {
341
+ type: "provider_transport_failure",
342
+ error_message: typeof error.message === "string" ? error.message : null,
343
+ error_name: typeof error.name === "string" ? error.name : null,
344
+ configured_transport: typeof details.configuredTransport === "string" ? details.configuredTransport : null,
345
+ fallback_transport: typeof details.fallbackTransport === "string" ? details.fallbackTransport : null,
346
+ phase: typeof details.phase === "string" ? details.phase : null,
347
+ events_emitted: typeof details.eventsEmitted === "boolean" ? details.eventsEmitted : null,
348
+ request_bytes: Number.isFinite(Number(details.requestBytes)) ? Number(details.requestBytes) : null,
349
+ };
350
+ }
351
+
352
+ function latestTransportFailureDiagnostic(messages = []) {
353
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
354
+ const diagnostics = Array.isArray(messages[i]?.diagnostics) ? messages[i].diagnostics : [];
355
+ for (let j = diagnostics.length - 1; j >= 0; j -= 1) {
356
+ const normalized = normalizeTransportFailureDiagnostic(diagnostics[j]);
357
+ if (normalized) return normalized;
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+
363
+ function piNativeTeammates(nativeSubagents) {
364
+ if (nativeSubagents?.provider !== "pi" || !Array.isArray(nativeSubagents.teammates)) return [];
365
+ return nativeSubagents.teammates.filter((agent) => agent?.name && agent?.helperSystemPrompt);
366
+ }
367
+
368
+ function createPiSubagentTool(nativeSubagents, parentOptions, recordResult) {
369
+ const teammates = piNativeTeammates(nativeSubagents);
370
+ if (!teammates.length) return null;
371
+ const byName = new Map(teammates.map((agent) => [agent.name, agent]));
372
+ const maxTasks = Math.max(1, Number(nativeSubagents.maxChildrenPerRound) || 1);
373
+ const maxParallel = Math.max(1, Number(nativeSubagents.maxParallelChildren) || 1);
374
+
375
+ async function runTask(task, signal, onUpdate) {
376
+ const agentName = String(task?.agent || "").trim();
377
+ const prompt = String(task?.prompt || "").trim();
378
+ if (!agentName) throw new Error("AskAgent requires an agent name.");
379
+ if (!prompt) throw new Error("AskAgent requires a prompt.");
380
+ const target = byName.get(agentName);
381
+ if (!target) throw new Error(`AskAgent target ${agentName} is not an available native teammate.`);
382
+ onUpdate?.(textToolResult(`Asking ${agentName}...`, { agent: agentName, status: "running" }));
383
+ const child = await generatePiResponse(target.helperSystemPrompt, {
384
+ ...parentOptions,
385
+ model: target.model || parentOptions.model,
386
+ effort: target.effort || parentOptions.effort,
387
+ messages: [{ role: "user", content: prompt }],
388
+ skills: Array.isArray(target.skills) ? target.skills : [],
389
+ skillDirs: Array.isArray(target.skillDirs) ? target.skillDirs : [],
390
+ mcpServers: target.mcpServers || {},
391
+ allowedTools: Array.isArray(target.allowedTools) ? target.allowedTools : [],
392
+ disallowedTools: Array.isArray(target.disallowedTools) ? target.disallowedTools : [],
393
+ toolPolicy: target.toolPolicy || {},
394
+ nativeSubagents: null,
395
+ outputSchema: null,
396
+ liveInput: null,
397
+ onEvent: null,
398
+ // sessionId now means "resume"; subagents only need a stable id for
399
+ // provider cache reuse and must never inherit the parent's session.
400
+ providerSessionId: `${parentOptions.runId || "pi"}:subagent:${agentName}:${randomUUID()}`,
401
+ sessionId: null,
402
+ sessionKeepAlive: false,
403
+ abortSignal: signal || parentOptions.abortSignal,
404
+ });
405
+ const summary = {
406
+ agent: agentName,
407
+ prompt,
408
+ text: child.text || "",
409
+ error: child.error || null,
410
+ usage: child.usage || {},
411
+ durationMs: child.durationMs || 0,
412
+ numTurns: child.numTurns || 0,
413
+ };
414
+ recordResult(summary);
415
+ if (child.cancelled) throw new Error(`AskAgent target ${agentName} was cancelled.`);
416
+ if (child.error) throw new Error(`AskAgent target ${agentName} failed: ${child.error}`);
417
+ return summary;
418
+ }
419
+
420
+ return {
421
+ name: "AskAgent",
422
+ label: "Ask Agent",
423
+ description: `Ask one of these native teammate agents for bounded help: ${teammates.map((agent) => agent.name).join(", ")}.`,
424
+ executionMode: nativeSubagents.mode === "workspace" ? "parallel" : "sequential",
425
+ parameters: objectSchema({
426
+ agent: { type: "string", enum: teammates.map((agent) => agent.name) },
427
+ prompt: { type: "string", description: "A bounded request for the teammate agent." },
428
+ tasks: {
429
+ type: "array",
430
+ maxItems: maxTasks,
431
+ items: objectSchema({
432
+ agent: { type: "string", enum: teammates.map((agent) => agent.name) },
433
+ prompt: { type: "string" },
434
+ }, ["agent", "prompt"]),
435
+ },
436
+ }),
437
+ async execute(toolCallId, params, signal, onUpdate) {
438
+ const requestedTasks = Array.isArray(params?.tasks) && params.tasks.length
439
+ ? params.tasks
440
+ : [{ agent: params?.agent, prompt: params?.prompt }];
441
+ if (requestedTasks.length > maxTasks) {
442
+ throw new Error(`AskAgent received ${requestedTasks.length} tasks, above the configured limit of ${maxTasks}.`);
443
+ }
444
+ const results = [];
445
+ for (let i = 0; i < requestedTasks.length; i += maxParallel) {
446
+ const batch = requestedTasks.slice(i, i + maxParallel);
447
+ results.push(...await Promise.all(batch.map((task) => runTask(task, signal, onUpdate))));
448
+ }
449
+ const body = results.map((result) => [
450
+ `### ${result.agent}`,
451
+ result.text || "(no text returned)",
452
+ ].join("\n\n")).join("\n\n");
453
+ return textToolResult(body, {
454
+ mode: nativeSubagents.mode || "advisory",
455
+ tasks: results,
456
+ });
457
+ },
458
+ };
459
+ }
460
+
461
+ // Live pi sessions, keyed by provider session id. Entries are
462
+ // { session, metadata, repo, durable, busy }. In-memory transcripts are freed
463
+ // when the registry evicts them; durable (jsonl) transcripts must survive
464
+ // eviction so a later resume can reopen them from disk.
465
+ const piSessionRepo = new InMemorySessionRepo();
466
+ const piSessions = createSessionRegistry({
467
+ isBusy: (entry) => entry.busy === true,
468
+ onEvict: async (entry) => {
469
+ if (entry.durable) return;
470
+ try {
471
+ await entry.repo.delete(entry.metadata);
472
+ } catch { /* best-effort */ }
473
+ },
474
+ });
475
+
476
+ const durablePiSessionRepos = new Map();
477
+
478
+ function resolveDurablePiSessionRepo(piSessionsRoot) {
479
+ if (typeof piSessionsRoot !== "string" || !piSessionsRoot.trim()) return null;
480
+ const root = piSessionsRoot.trim();
481
+ let repo = durablePiSessionRepos.get(root);
482
+ if (!repo) {
483
+ repo = new JsonlSessionRepo({
484
+ fs: new NodeExecutionEnv({ cwd: process.cwd() }),
485
+ sessionsRoot: root,
486
+ });
487
+ durablePiSessionRepos.set(root, repo);
488
+ }
489
+ return repo;
490
+ }
491
+
492
+ async function reopenDurablePiSession(repo, sessionId) {
493
+ try {
494
+ const metadata = (await repo.list()).find((entry) => entry?.id === sessionId);
495
+ if (!metadata) return null;
496
+ const session = await repo.open(metadata);
497
+ return { session, metadata, repo, durable: true, busy: false };
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+
503
+ function sessionUnavailableResult({
504
+ resolved,
505
+ options,
506
+ events,
507
+ runtimeWarnings,
508
+ start,
509
+ sessionId,
510
+ errorMessage,
511
+ failureKind,
512
+ piErrorCode,
513
+ }) {
514
+ return {
515
+ text: null,
516
+ events,
517
+ usage: {},
518
+ durationMs: Date.now() - start,
519
+ numTurns: 0,
520
+ model: resolved?.reference || resolved?.model || null,
521
+ effort: options.effort || null,
522
+ sdk: resolved?.sdk || "pi",
523
+ cancelled: false,
524
+ error: errorMessage,
525
+ failureKind,
526
+ providerSessionId: sessionId,
527
+ runtimeWarnings,
528
+ diagnostics: {
529
+ provider_session_id: sessionId,
530
+ pi_error_code: piErrorCode,
531
+ },
532
+ };
533
+ }
534
+
535
+ export async function generatePiResponse(systemPrompt, options = {}) {
536
+ const resolved = options.model;
537
+ const start = Date.now();
538
+ const events = [];
539
+ const runtimeWarnings = [];
540
+ const assistantTexts = [];
541
+ const assistantThinking = [];
542
+ const textDeltaIndexes = new Set();
543
+ const thinkingDeltaIndexes = new Set();
544
+ let structuredResult = null;
545
+ let mcpClients = [];
546
+ let finalMessages = [];
547
+ let externalAbort = false;
548
+ let maxTurnsHit = false;
549
+ let turnCount = 0;
550
+ let compaction = null;
551
+ let piErrorPayload = null;
552
+ let toolResultsSeen = 0;
553
+ let lastToolName = null;
554
+ let piTransport = "auto";
555
+ let agent = null;
556
+ let removeAbortHandler = null;
557
+ let structuredOutputFinalizationRetryAttempts = 0;
558
+ let structuredOutputFinalizationRetryReason = null;
559
+ let structuredOutputFinalizationRetryFailed = false;
560
+ const subagentResults = [];
561
+ const providerSessionId = options.sessionId
562
+ || options.providerSessionId
563
+ || options.runId
564
+ || randomUUID();
565
+ const requestedSessionId = typeof options.sessionId === "string" && options.sessionId.trim()
566
+ ? options.sessionId
567
+ : null;
568
+ // Bridge TTL is a backstop behind the host's session policy; the grace
569
+ // keeps host-side lazy expiry firing first.
570
+ const sessionTtlMs = Number.isFinite(Number(options.sessionIdleTimeoutMs))
571
+ ? Number(options.sessionIdleTimeoutMs) + 60_000
572
+ : undefined;
573
+ let sessionEntry = null;
574
+ let sessionBaselineCount = 0;
575
+
576
+ const onEvent = (event) => emitCaptured(events, options.onEvent, event);
577
+ const approvalManager = options.onToolApprovalRequest
578
+ ? createApprovalManager({
579
+ onToolApprovalRequest: options.onToolApprovalRequest,
580
+ defaultRiskTier: options.approvalDefaultRiskTier,
581
+ timeoutMs: options.approvalTimeoutMs,
582
+ onEvent,
583
+ riskTiersByTool: options.toolRiskTiers,
584
+ alwaysAllowTools: options.approvalAlwaysAllowTools,
585
+ })
586
+ : null;
587
+
588
+ try {
589
+ // Resume check first: a session miss must stay cheap (no tool/MCP init).
590
+ const durableRepo = resolveDurablePiSessionRepo(options.piSessionsRoot);
591
+ let resumeMessages = null;
592
+ if (requestedSessionId) {
593
+ let entry = piSessions.get(requestedSessionId);
594
+ if (!entry && durableRepo) {
595
+ entry = await reopenDurablePiSession(durableRepo, requestedSessionId);
596
+ if (entry) piSessions.set(requestedSessionId, entry, { idleTimeoutMs: sessionTtlMs });
597
+ }
598
+ if (!entry) {
599
+ return sessionUnavailableResult({
600
+ resolved,
601
+ options,
602
+ events,
603
+ runtimeWarnings,
604
+ start,
605
+ sessionId: requestedSessionId,
606
+ errorMessage: `Pi session ${requestedSessionId} is not live`,
607
+ failureKind: "session_not_found",
608
+ piErrorCode: "pi_session_not_found",
609
+ });
610
+ }
611
+ if (entry.busy) {
612
+ return sessionUnavailableResult({
613
+ resolved,
614
+ options,
615
+ events,
616
+ runtimeWarnings,
617
+ start,
618
+ sessionId: requestedSessionId,
619
+ errorMessage: `Pi session ${requestedSessionId} is busy with another turn`,
620
+ failureKind: "session_busy",
621
+ piErrorCode: "pi_session_busy",
622
+ });
623
+ }
624
+ entry.busy = true;
625
+ sessionEntry = entry;
626
+ const sessionContext = await sessionEntry.session.buildContext();
627
+ resumeMessages = sessionContext.messages;
628
+ sessionBaselineCount = resumeMessages.length;
629
+ }
630
+
631
+ const runtime = resolvePiRuntimeModel(resolved, options);
632
+ piTransport = resolvePiTransport(runtime.model, runtimeWarnings, options.piCodexTransport);
633
+ const capabilities = runtime.capabilities || {};
634
+ const effectiveThinkingLevel = thinkingLevelForEffort(options.effort || "medium", capabilities);
635
+ const settings = readRuntimeSettings(options.settings);
636
+ const reference = resolved.reference
637
+ || (resolved.sdk === "pi" ? `pi:${resolved.provider}:${resolved.model}` : `${resolved.sdk}:${resolved.model}`);
638
+ compaction = createAgentCompactionManager({
639
+ runId: options.runId || null,
640
+ providerKind: resolved.sdk,
641
+ modelReference: reference,
642
+ model: runtime.model,
643
+ settings,
644
+ onEvent,
645
+ onCompactionRecorded: options.onCompactionRecorded,
646
+ });
647
+ const onTruncate = (info) => {
648
+ try {
649
+ onEvent({
650
+ type: "runtime_warning",
651
+ warning_kind: "tool_payload_truncated",
652
+ source: "tool_bloat_guard",
653
+ ...info,
654
+ });
655
+ } catch { /* best-effort */ }
656
+ };
657
+ const persistArtifact = options.persistArtifact || null;
658
+ const qaOutputDir = options.qaOutputDir || options.runArtifactDir || null;
659
+ const toolPayloadMaxBytes = compaction.policy?.toolPayloadMaxBytes;
660
+ const builtIns = capabilities.tool_use === false
661
+ ? []
662
+ : getPiBuiltinTools(options.allowedTools, {
663
+ skillNames: (options.skills || []).map((skill) => skill.name),
664
+ dataDir: options.dataDir,
665
+ cwd: options.cwd,
666
+ onEvent,
667
+ toolLimits: compaction.policy,
668
+ persistArtifact,
669
+ toolPayloadMaxBytes,
670
+ onTruncate,
671
+ toolPolicy: options.toolPolicy,
672
+ sandboxPolicy: options.sandboxPolicy,
673
+ sandboxEngine: options.sandboxEngine,
674
+ approvalManager,
675
+ approvalModel: runtime.model?.id || runtime.model?.name || resolved.model,
676
+ });
677
+
678
+ const structuredTool = createStructuredOutputTool(options.outputSchema, (value) => {
679
+ structuredResult = value;
680
+ });
681
+ const subagentTool = capabilities.tool_use === false
682
+ ? null
683
+ : createPiSubagentTool(options.nativeSubagents, options, (result) => subagentResults.push(result));
684
+ const reservedNames = new Set(builtIns.map((toolDef) => toolDef.name));
685
+ if (subagentTool) reservedNames.add(subagentTool.name);
686
+ if (structuredTool) reservedNames.add(structuredTool.name);
687
+ const mcpInit = capabilities.tool_use === false
688
+ ? { clients: [], tools: [], warnings: [] }
689
+ : await initPiMcpTools(options.mcpServers || {}, reservedNames, {
690
+ limits: compaction.policy,
691
+ cwd: options.cwd,
692
+ persistArtifact,
693
+ qaOutputDir,
694
+ toolPayloadMaxBytes,
695
+ onTruncate,
696
+ sandboxPolicy: options.sandboxPolicy,
697
+ sandboxEngine: options.sandboxEngine,
698
+ });
699
+ mcpClients = mcpInit.clients;
700
+ for (const warning of mcpInit.warnings || []) onEvent(warning);
701
+
702
+ const tools = [
703
+ ...builtIns,
704
+ ...(subagentTool ? [subagentTool] : []),
705
+ ...mcpInit.tools,
706
+ ...(structuredTool ? [structuredTool] : []),
707
+ ];
708
+
709
+ agent = new Agent({
710
+ initialState: {
711
+ systemPrompt: appendStructuredOutputInstruction(systemPrompt, options.outputSchema),
712
+ model: runtime.model,
713
+ thinkingLevel: effectiveThinkingLevel,
714
+ tools,
715
+ ...(resumeMessages ? { messages: resumeMessages } : {}),
716
+ },
717
+ streamFn: createPiRuntimeStreamFn(options.streamFn, {
718
+ piReasoningSummary: options.piReasoningSummary,
719
+ }),
720
+ transformContext: compaction.transformContext,
721
+ afterToolCall: compaction.afterToolCall,
722
+ sessionId: providerSessionId,
723
+ transport: piTransport,
724
+ steeringMode: "one-at-a-time",
725
+ followUpMode: "one-at-a-time",
726
+ toolExecution: "sequential",
727
+ getApiKey: (provider) => resolveApiKey(provider, {
728
+ apiKeys: runtime.apiKeys,
729
+ resolvePiApiKey: options.resolvePiApiKey,
730
+ runtimeWarnings,
731
+ }),
732
+ maxRetryDelayMs: options.maxRetryDelayMs || 60_000,
733
+ });
734
+
735
+ agent.subscribe((event) => {
736
+ if (event.type === "message_update") {
737
+ const streamEvent = event.assistantMessageEvent;
738
+ if (streamEvent?.type === "text_delta" && streamEvent.delta) {
739
+ textDeltaIndexes.add(streamContentKey(streamEvent, "text"));
740
+ assistantTexts.push(streamEvent.delta);
741
+ onEvent({ type: "assistant", message: { content: [{ type: "text", text: streamEvent.delta }] } });
742
+ } else if (streamEvent?.type === "text_end" && streamEvent.content) {
743
+ const key = streamContentKey(streamEvent, "text");
744
+ if (!textDeltaIndexes.has(key)) {
745
+ assistantTexts.push(streamEvent.content);
746
+ onEvent({ type: "assistant", message: { content: [{ type: "text", text: streamEvent.content }] } });
747
+ }
748
+ } else if (streamEvent?.type === "thinking_delta" && streamEvent.delta) {
749
+ thinkingDeltaIndexes.add(streamContentKey(streamEvent, "thinking"));
750
+ assistantThinking.push(streamEvent.delta);
751
+ onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.delta }] } });
752
+ } else if (streamEvent?.type === "thinking_end" && streamEvent.content) {
753
+ const key = streamContentKey(streamEvent, "thinking");
754
+ if (!thinkingDeltaIndexes.has(key)) {
755
+ assistantThinking.push(streamEvent.content);
756
+ onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.content }] } });
757
+ }
758
+ }
759
+ } else if (event.type === "tool_execution_start") {
760
+ if (event.toolName) lastToolName = event.toolName;
761
+ const input = eventToolArgs(event.toolName, event.args, {
762
+ cwd: options.cwd,
763
+ toolLimits: compaction?.policy,
764
+ });
765
+ onEvent({
766
+ type: "assistant",
767
+ message: { content: [{ type: "tool_use", id: event.toolCallId, name: event.toolName, input }] },
768
+ });
769
+ } else if (event.type === "tool_execution_update") {
770
+ const input = eventToolArgs(event.toolName, event.args, {
771
+ cwd: options.cwd,
772
+ toolLimits: compaction?.policy,
773
+ });
774
+ onEvent({
775
+ type: "tool_update",
776
+ tool_use_id: event.toolCallId,
777
+ name: event.toolName,
778
+ input,
779
+ partial_result: jsonSerializable(event.partialResult, String(event.partialResult ?? "")),
780
+ });
781
+ } else if (event.type === "tool_execution_end") {
782
+ const resultContent = toolResultContent(event.result);
783
+ if (!event.isError) toolResultsSeen += 1;
784
+ onEvent({
785
+ type: "user",
786
+ message: {
787
+ content: [{
788
+ type: "tool_result",
789
+ tool_use_id: event.toolCallId,
790
+ content: resultContent,
791
+ raw_result: compactToolRawResult(jsonSerializable(event.result, resultContent), resultContent),
792
+ is_error: !!event.isError,
793
+ }],
794
+ },
795
+ });
796
+ } else if (event.type === "turn_end") {
797
+ turnCount += 1;
798
+ if (Number.isFinite(Number(options.maxTurns))
799
+ && Number(options.maxTurns) > 0
800
+ && turnCount >= Number(options.maxTurns)
801
+ && event.message?.stopReason === "toolUse") {
802
+ maxTurnsHit = true;
803
+ agent.abort();
804
+ }
805
+ } else if (event.type === "agent_end") {
806
+ finalMessages = event.messages || [];
807
+ }
808
+ });
809
+
810
+ const abortHandler = () => {
811
+ externalAbort = true;
812
+ agent.abort();
813
+ };
814
+ if (options.abortSignal) {
815
+ if (options.abortSignal.aborted) {
816
+ externalAbort = true;
817
+ return {
818
+ text: null,
819
+ thinking: "",
820
+ events,
821
+ usage: {},
822
+ durationMs: Date.now() - start,
823
+ numTurns: turnCount,
824
+ model: resolved?.reference || resolved?.model || null,
825
+ effort: options.effort || null,
826
+ sdk: resolved?.sdk || "pi",
827
+ cancelled: true,
828
+ error: null,
829
+ failureKind: null,
830
+ providerSessionId,
831
+ runtimeWarnings,
832
+ diagnostics: {
833
+ provider_session_id: providerSessionId,
834
+ pi_stop_reason: "aborted",
835
+ max_turns_hit: false,
836
+ max_turns: Number.isFinite(Number(options.maxTurns)) ? Number(options.maxTurns) : null,
837
+ pi_transport: piTransport,
838
+ turn_count: turnCount,
839
+ external_abort: true,
840
+ ...(compaction?.diagnostics?.() || {}),
841
+ },
842
+ };
843
+ }
844
+ else {
845
+ options.abortSignal.addEventListener("abort", abortHandler, { once: true });
846
+ removeAbortHandler = () => options.abortSignal.removeEventListener?.("abort", abortHandler);
847
+ }
848
+ }
849
+
850
+ if (options.liveInput) {
851
+ (async () => {
852
+ try {
853
+ for await (const message of options.liveInput) {
854
+ if (options.abortSignal?.aborted) break;
855
+ agent.steer({
856
+ role: "user",
857
+ content: formatLiveInputGuidance(message.body),
858
+ timestamp: message.createdAt || Date.now(),
859
+ });
860
+ }
861
+ } catch (err) {
862
+ onEvent({
863
+ type: "runtime_warning",
864
+ warning_kind: "live_input_failed",
865
+ message: err?.message || String(err),
866
+ });
867
+ }
868
+ })();
869
+ }
870
+
871
+ onEvent({
872
+ type: "provider_request_started",
873
+ sdk: resolved.sdk,
874
+ model: reference,
875
+ runtime: "pi",
876
+ timestamp: Date.now(),
877
+ });
878
+
879
+ await agent.prompt(toAgentMessages(options.messages, runtime.model));
880
+
881
+ const streamRetryMax = Number.isFinite(Number(options.piStreamRetryMax))
882
+ ? Math.max(0, Math.min(5, Number(options.piStreamRetryMax)))
883
+ : 2;
884
+ const streamRetryBaseMs = Number.isFinite(Number(options.piStreamRetryBaseMs))
885
+ ? Math.max(0, Number(options.piStreamRetryBaseMs))
886
+ : 1000;
887
+ let streamRetryAttempts = 0;
888
+ const streamRetryEvents = [];
889
+ while (streamRetryAttempts < streamRetryMax) {
890
+ if (externalAbort || options.abortSignal?.aborted) break;
891
+ const msgs = agent.state?.messages || [];
892
+ let lastAssistant = null;
893
+ for (let i = msgs.length - 1; i >= 0; i -= 1) {
894
+ if (msgs[i]?.role === "assistant") { lastAssistant = msgs[i]; break; }
895
+ }
896
+ const lastStopReason = lastAssistant?.stopReason || null;
897
+ const lastErrorMessage = String(lastAssistant?.errorMessage || "");
898
+ if (lastStopReason !== "error") break;
899
+ if (!PROVIDER_ABORT_RE.test(lastErrorMessage)) break;
900
+ streamRetryAttempts += 1;
901
+ const attempt = streamRetryAttempts;
902
+ streamRetryEvents.push({ attempt, reason: lastErrorMessage });
903
+ runtimeWarnings.push({
904
+ warning_kind: "pi_stream_retry",
905
+ source: "pi",
906
+ reason: lastErrorMessage,
907
+ attempt,
908
+ message: `Retrying pi stream after ${lastErrorMessage} (attempt ${attempt}/${streamRetryMax}).`,
909
+ });
910
+ onEvent({
911
+ type: "runtime_warning",
912
+ warning_kind: "pi_stream_retry",
913
+ reason: lastErrorMessage,
914
+ attempt,
915
+ });
916
+ while (
917
+ agent.state.messages.length > 0
918
+ && agent.state.messages[agent.state.messages.length - 1]?.role === "assistant"
919
+ ) {
920
+ agent.state.messages.pop();
921
+ }
922
+ if (agent.state.messages.length === 0) break;
923
+ if (streamRetryBaseMs > 0) {
924
+ await new Promise((resolve) => setTimeout(resolve, streamRetryBaseMs * (2 ** (attempt - 1))));
925
+ }
926
+ if (externalAbort || options.abortSignal?.aborted) break;
927
+ try {
928
+ await agent.continue();
929
+ } catch (err) {
930
+ runtimeWarnings.push({
931
+ warning_kind: "pi_stream_retry_failed",
932
+ source: "pi",
933
+ attempt,
934
+ message: err?.message || String(err),
935
+ });
936
+ break;
937
+ }
938
+ }
939
+
940
+ const captureState = () => {
941
+ const transcript = agent.state.messages || [];
942
+ const assistantMessages = transcript.filter((message) => message?.role === "assistant");
943
+ const lastAssistant = assistantMessages[assistantMessages.length - 1] || null;
944
+ return {
945
+ transcript,
946
+ assistantMessages,
947
+ lastAssistant,
948
+ piTransportFailure: latestTransportFailureDiagnostic(assistantMessages),
949
+ piWebSocketDebug: piWebSocketDebugStats(providerSessionId, piTransport),
950
+ finalText: textFromContent(lastAssistant?.content) || assistantTexts.join(""),
951
+ finalThinking: thinkingFromContent(lastAssistant?.content) || assistantThinking.join(""),
952
+ stopReason: lastAssistant?.stopReason || null,
953
+ };
954
+ };
955
+
956
+ let state = captureState();
957
+ externalAbort ||= !!options.abortSignal?.aborted;
958
+ if (shouldRetryStructuredOutputFinalization({
959
+ outputSchema: options.outputSchema,
960
+ structuredResult,
961
+ finalText: state.finalText,
962
+ stopReason: state.stopReason,
963
+ externalAbort,
964
+ maxTurnsHit,
965
+ })) {
966
+ structuredOutputFinalizationRetryAttempts = 1;
967
+ structuredOutputFinalizationRetryReason = "empty_final_output";
968
+ runtimeWarnings.push({
969
+ warning_kind: "structured_output_finalization_retry",
970
+ source: "pi",
971
+ reason: structuredOutputFinalizationRetryReason,
972
+ message: "Pi stopped without text or structured output; retrying once in the same session with only StructuredOutput enabled.",
973
+ });
974
+ const previousTools = agent.state.tools;
975
+ try {
976
+ agent.state.tools = structuredTool ? [structuredTool] : [];
977
+ agent.followUp({
978
+ role: "user",
979
+ content: structuredOutputFinalizationPrompt(),
980
+ timestamp: Date.now(),
981
+ });
982
+ await agent.continue();
983
+ } finally {
984
+ agent.state.tools = previousTools;
985
+ }
986
+ structuredOutputFinalizationRetryFailed = structuredResult === null || structuredResult === undefined;
987
+ state = captureState();
988
+ }
989
+
990
+ const {
991
+ transcript,
992
+ assistantMessages,
993
+ lastAssistant,
994
+ piTransportFailure,
995
+ piWebSocketDebug,
996
+ finalText,
997
+ finalThinking,
998
+ stopReason,
999
+ } = state;
1000
+ const text = finalText;
1001
+ // Resumed runs restore prior turns into the transcript; usage, cost, and
1002
+ // turn counts must only cover messages produced by THIS run.
1003
+ const runTranscript = transcript.slice(sessionBaselineCount);
1004
+ const runAssistantCount = runTranscript.filter((message) => message?.role === "assistant").length;
1005
+ const usage = usageFromMessages(runTranscript);
1006
+ for (const child of subagentResults) addUsage(usage, usageFromRuntimeResult(child));
1007
+ const estimatedCost = estimateCost({
1008
+ resolveCustomPricing: options.resolveCustomPricing,
1009
+ model: reference,
1010
+ inputTokens: usage.input,
1011
+ outputTokens: usage.output,
1012
+ cachedTokens: usage.cacheRead,
1013
+ cacheWriteTokens: usage.cacheWrite,
1014
+ });
1015
+ if (usage.cacheRead > 0) {
1016
+ onEvent({ type: "cache_hit", sdk: resolved.sdk, model: reference, tokens: usage.cacheRead, source: "prompt_cache" });
1017
+ }
1018
+ if (usage.cacheWrite > 0) {
1019
+ onEvent({ type: "cache_miss", sdk: resolved.sdk, model: reference, tokens: usage.cacheWrite, source: "prompt_cache" });
1020
+ }
1021
+ onEvent({
1022
+ type: "cost_accumulated",
1023
+ sdk: resolved.sdk,
1024
+ model: reference,
1025
+ cumulativeUsd: Number(usage.cost) || Number(estimatedCost) || 0,
1026
+ tokens: {
1027
+ input: Number(usage.input) || 0,
1028
+ output: Number(usage.output) || 0,
1029
+ cacheReadTokens: Number(usage.cacheRead) || 0,
1030
+ cacheCreationTokens: Number(usage.cacheWrite) || 0,
1031
+ },
1032
+ });
1033
+ onEvent({
1034
+ type: "provider_request_completed",
1035
+ sdk: resolved.sdk,
1036
+ model: reference,
1037
+ runtime: "pi",
1038
+ timestamp: Date.now(),
1039
+ durationMs: Date.now() - start,
1040
+ cancelled: externalAbort,
1041
+ });
1042
+ if (!piErrorPayload && (stopReason === "error" || stopReason === "aborted")) {
1043
+ piErrorPayload = capturePiErrorPayload(lastAssistant);
1044
+ }
1045
+ const rawErrorMessage = externalAbort
1046
+ ? null
1047
+ : maxTurnsHit
1048
+ ? "Pi agent stopped before final output: max turns reached"
1049
+ : (stopReason === "error" || stopReason === "aborted"
1050
+ ? piErrorPayload?.error_message
1051
+ || lastAssistant?.errorMessage
1052
+ || agent.state.errorMessage
1053
+ || "Pi agent aborted before final output"
1054
+ : null);
1055
+ const errorMessage = normalizePiErrorMessage(rawErrorMessage);
1056
+ const piErrorCode = piErrorPayload?.code || inferPiErrorCode(errorMessage);
1057
+ const hadPartialProgress = !externalAbort
1058
+ && (stopReason === "error" || stopReason === "aborted")
1059
+ && (toolResultsSeen > 0 || assistantTexts.length > 0 || assistantThinking.length > 0);
1060
+ const errorDetails = errorMessage ? {
1061
+ pi_stop_reason: stopReason,
1062
+ pi_error_code: piErrorCode || null,
1063
+ pi_request_id: piErrorPayload?.request_id || null,
1064
+ last_text_excerpt: lastTextSnippet(assistantTexts, assistantThinking),
1065
+ last_tool_name: lastToolName,
1066
+ had_partial_progress: hadPartialProgress,
1067
+ tool_results_seen: toolResultsSeen,
1068
+ turn_count: turnCount || runAssistantCount || finalMessages.length,
1069
+ max_turns_hit: maxTurnsHit,
1070
+ provider_session_id: providerSessionId,
1071
+ pi_transport: piTransport,
1072
+ ...structuredOutputRetryDiagnostics(
1073
+ structuredOutputFinalizationRetryAttempts,
1074
+ structuredOutputFinalizationRetryReason,
1075
+ structuredOutputFinalizationRetryFailed,
1076
+ ),
1077
+ ...(piTransportFailure ? { pi_transport_failure: piTransportFailure } : {}),
1078
+ ...(piWebSocketDebug ? { pi_websocket_debug: piWebSocketDebug } : {}),
1079
+ } : null;
1080
+ const diagnostics = {
1081
+ provider_session_id: providerSessionId,
1082
+ pi_stop_reason: stopReason,
1083
+ pi_transport: piTransport,
1084
+ max_turns_hit: maxTurnsHit,
1085
+ max_turns: Number.isFinite(Number(options.maxTurns)) ? Number(options.maxTurns) : null,
1086
+ turn_count: turnCount || runAssistantCount || finalMessages.length,
1087
+ external_abort: externalAbort,
1088
+ ...(piErrorCode ? { pi_error_code: piErrorCode } : {}),
1089
+ ...(piErrorPayload?.request_id ? { pi_request_id: piErrorPayload.request_id } : {}),
1090
+ ...(piErrorPayload ? { pi_error_payload: piErrorPayload } : {}),
1091
+ ...(piTransportFailure ? { pi_transport_failure: piTransportFailure } : {}),
1092
+ ...(piWebSocketDebug ? { pi_websocket_debug: piWebSocketDebug } : {}),
1093
+ ...(lastToolName ? { last_tool_name: lastToolName } : {}),
1094
+ ...structuredOutputRetryDiagnostics(
1095
+ structuredOutputFinalizationRetryAttempts,
1096
+ structuredOutputFinalizationRetryReason,
1097
+ structuredOutputFinalizationRetryFailed,
1098
+ ),
1099
+ ...(hadPartialProgress ? { had_partial_progress: true, tool_results_seen: toolResultsSeen } : {}),
1100
+ ...(streamRetryAttempts > 0
1101
+ ? { pi_stream_retries: streamRetryAttempts, pi_stream_retry_events: streamRetryEvents }
1102
+ : {}),
1103
+ ...(subagentResults.length ? {
1104
+ pi_subagents: {
1105
+ count: subagentResults.length,
1106
+ errors: subagentResults.filter((child) => child.error).length,
1107
+ agents: subagentResults.map((child) => child.agent),
1108
+ },
1109
+ } : {}),
1110
+ ...(compaction?.diagnostics?.() || {}),
1111
+ };
1112
+
1113
+ const compactionDiag = compaction?.diagnostics?.() || {};
1114
+ const capabilitiesUsed = buildCapabilitiesUsed({
1115
+ promptCacheActive: usage.cacheRead > 0 || usage.cacheWrite > 0,
1116
+ thinkingEnabled: effectiveThinkingLevel !== "off" && effectiveThinkingLevel !== "low",
1117
+ structuredOutputEnforced: !!options.outputSchema,
1118
+ subagentInvoked: subagentResults.length > 0,
1119
+ mcpServersUsed: mcpClients.map((entry) => entry?.name).filter(Boolean),
1120
+ nativeSubagentsUsed: piNativeTeammates(options.nativeSubagents).map((entry) => entry.name),
1121
+ toolCompactionApplied: toolCompactionAppliedFromWarnings(runtimeWarnings),
1122
+ contextCompactionApplied: Number(compactionDiag?.context_compactions || 0) > 0
1123
+ ? true
1124
+ : compaction
1125
+ ? false
1126
+ : null,
1127
+ });
1128
+ onEvent({ type: "capabilities_resolved", sdk: resolved.sdk, model: reference, capabilitiesUsed });
1129
+
1130
+ if (options.sessionKeepAlive === true && !externalAbort && !errorMessage) {
1131
+ let createdEntry = null;
1132
+ try {
1133
+ const newMessages = transcript.slice(sessionBaselineCount);
1134
+ if (sessionEntry) {
1135
+ for (const message of newMessages) await sessionEntry.session.appendMessage(message);
1136
+ piSessions.touch(requestedSessionId, { idleTimeoutMs: sessionTtlMs });
1137
+ } else {
1138
+ const repo = durableRepo || piSessionRepo;
1139
+ const session = await repo.create({ id: providerSessionId, cwd: options.cwd || process.cwd() });
1140
+ createdEntry = {
1141
+ session,
1142
+ metadata: await session.getMetadata(),
1143
+ repo,
1144
+ durable: !!durableRepo,
1145
+ busy: false,
1146
+ };
1147
+ for (const message of newMessages) await createdEntry.session.appendMessage(message);
1148
+ piSessions.set(providerSessionId, createdEntry, { idleTimeoutMs: sessionTtlMs });
1149
+ }
1150
+ } catch (err) {
1151
+ // Session persistence must never fail the run; drop the (now
1152
+ // inconsistent) session instead of resuming from a broken transcript.
1153
+ onEvent({
1154
+ type: "runtime_warning",
1155
+ warning_kind: "pi_session_persist_failed",
1156
+ message: err?.message || String(err),
1157
+ });
1158
+ piSessions.delete(providerSessionId);
1159
+ const broken = sessionEntry || createdEntry;
1160
+ if (broken) {
1161
+ try {
1162
+ await broken.repo.delete(broken.metadata);
1163
+ } catch { /* best-effort */ }
1164
+ }
1165
+ }
1166
+ }
1167
+
1168
+ return {
1169
+ text,
1170
+ thinking: finalThinking,
1171
+ events,
1172
+ usage: {
1173
+ input_tokens: usage.input || null,
1174
+ output_tokens: usage.output || null,
1175
+ cache_read_tokens: usage.cacheRead || null,
1176
+ cache_creation_tokens: usage.cacheWrite || null,
1177
+ cache_write_tokens: usage.cacheWrite || null,
1178
+ cost_usd: usage.cost || estimatedCost,
1179
+ },
1180
+ durationMs: Date.now() - start,
1181
+ numTurns: turnCount || runAssistantCount || finalMessages.length,
1182
+ model: resolved.reference || `pi:${resolved.provider}:${resolved.model}`,
1183
+ effort: options.effort || null,
1184
+ sdk: resolved.sdk,
1185
+ cancelled: externalAbort,
1186
+ error: errorMessage,
1187
+ errorDetails,
1188
+ failureKind: failureKindForPiError(errorMessage, diagnostics, { maxTurnsHit }),
1189
+ providerSessionId,
1190
+ runtimeWarnings,
1191
+ diagnostics,
1192
+ capabilitiesUsed,
1193
+ ...(structuredResult !== null && structuredResult !== undefined
1194
+ ? { structuredResult, structuredResultSource: "StructuredOutput" }
1195
+ : { structuredResult: undefined, structuredResultSource: null }),
1196
+ };
1197
+ } catch (err) {
1198
+ externalAbort ||= !!options.abortSignal?.aborted;
1199
+ const assistantMessages = Array.isArray(agent?.state?.messages)
1200
+ ? agent.state.messages.filter((message) => message?.role === "assistant")
1201
+ : [];
1202
+ const piTransportFailure = latestTransportFailureDiagnostic(assistantMessages);
1203
+ const piWebSocketDebug = piWebSocketDebugStats(providerSessionId, piTransport);
1204
+ const exceptionCode = pickPiErrorCodeFromException(err);
1205
+ const errorMessage = normalizePiErrorMessage(
1206
+ piErrorPayload?.error_message || err?.message || String(err),
1207
+ );
1208
+ const piErrorCode = piErrorPayload?.code || exceptionCode || inferPiErrorCode(errorMessage);
1209
+ const hadPartialProgress = !externalAbort
1210
+ && (toolResultsSeen > 0 || assistantTexts.length > 0 || assistantThinking.length > 0);
1211
+ const errorDetails = (!externalAbort && errorMessage) ? {
1212
+ pi_stop_reason: "error",
1213
+ pi_error_code: piErrorCode || null,
1214
+ pi_request_id: piErrorPayload?.request_id || null,
1215
+ last_text_excerpt: lastTextSnippet(assistantTexts, assistantThinking),
1216
+ last_tool_name: lastToolName,
1217
+ had_partial_progress: hadPartialProgress,
1218
+ tool_results_seen: toolResultsSeen,
1219
+ turn_count: turnCount,
1220
+ max_turns_hit: maxTurnsHit,
1221
+ provider_session_id: providerSessionId,
1222
+ pi_transport: piTransport,
1223
+ ...(piTransportFailure ? { pi_transport_failure: piTransportFailure } : {}),
1224
+ ...(piWebSocketDebug ? { pi_websocket_debug: piWebSocketDebug } : {}),
1225
+ ...structuredOutputRetryDiagnostics(
1226
+ structuredOutputFinalizationRetryAttempts,
1227
+ structuredOutputFinalizationRetryReason,
1228
+ true,
1229
+ ),
1230
+ } : null;
1231
+ return {
1232
+ text: assistantTexts.join("") || null,
1233
+ events,
1234
+ usage: {},
1235
+ durationMs: Date.now() - start,
1236
+ numTurns: turnCount,
1237
+ model: resolved?.reference || resolved?.model || null,
1238
+ effort: options.effort || null,
1239
+ sdk: resolved?.sdk || "pi",
1240
+ cancelled: externalAbort,
1241
+ error: externalAbort ? null : errorMessage,
1242
+ errorDetails,
1243
+ failureKind: externalAbort ? null : failureKindForPiError(errorMessage, {
1244
+ ...(compaction?.diagnostics?.() || {}),
1245
+ }, { maxTurnsHit }),
1246
+ providerSessionId,
1247
+ runtimeWarnings,
1248
+ diagnostics: {
1249
+ provider_session_id: providerSessionId,
1250
+ pi_stop_reason: externalAbort ? "aborted" : "error",
1251
+ pi_transport: piTransport,
1252
+ max_turns_hit: maxTurnsHit,
1253
+ max_turns: Number.isFinite(Number(options.maxTurns)) ? Number(options.maxTurns) : null,
1254
+ turn_count: turnCount,
1255
+ external_abort: externalAbort,
1256
+ ...(piErrorCode
1257
+ ? { pi_error_code: piErrorCode }
1258
+ : {}),
1259
+ ...(piErrorPayload?.request_id ? { pi_request_id: piErrorPayload.request_id } : {}),
1260
+ ...(piErrorPayload ? { pi_error_payload: piErrorPayload } : {}),
1261
+ ...(piTransportFailure ? { pi_transport_failure: piTransportFailure } : {}),
1262
+ ...(piWebSocketDebug ? { pi_websocket_debug: piWebSocketDebug } : {}),
1263
+ ...(lastToolName ? { last_tool_name: lastToolName } : {}),
1264
+ ...structuredOutputRetryDiagnostics(
1265
+ structuredOutputFinalizationRetryAttempts,
1266
+ structuredOutputFinalizationRetryReason,
1267
+ true,
1268
+ ),
1269
+ ...(hadPartialProgress ? { had_partial_progress: true, tool_results_seen: toolResultsSeen } : {}),
1270
+ ...(compaction?.diagnostics?.() || {}),
1271
+ },
1272
+ };
1273
+ } finally {
1274
+ if (sessionEntry) sessionEntry.busy = false;
1275
+ removeAbortHandler?.();
1276
+ await closePiMcpClients(mcpClients);
1277
+ }
1278
+ }
1279
+
1280
+ export const piOpenAiBackend = {
1281
+ kind: "pi",
1282
+ capabilities: runtimeCapabilities("pi"),
1283
+ execute: generatePiResponse,
1284
+ };
1285
+
1286
+ export const piCodexBackend = {
1287
+ kind: "pi",
1288
+ capabilities: runtimeCapabilities("pi"),
1289
+ execute: generatePiResponse,
1290
+ };
1291
+
1292
+ export const piVercelBackend = {
1293
+ kind: "pi",
1294
+ capabilities: runtimeCapabilities("pi"),
1295
+ execute: generatePiResponse,
1296
+ };
1297
+
1298
+ export const piGenericBackend = {
1299
+ kind: "pi",
1300
+ capabilities: runtimeCapabilities("pi"),
1301
+ execute: generatePiResponse,
1302
+ };
1303
+
1304
+ export const piRuntimeBridge = {
1305
+ id: "pi",
1306
+ kind: "pi",
1307
+ capabilities: runtimeCapabilities("pi"),
1308
+ supports: (ref) => ref?.sdk === "pi",
1309
+ execute: generatePiResponse,
1310
+ };