@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.
- package/ARCHITECTURE.md +219 -0
- package/LICENSE +674 -0
- package/README.md +430 -0
- package/package.json +46 -0
- package/src/agent/allowlists.js +49 -0
- package/src/agent/approval.js +211 -0
- package/src/agent/compaction.js +752 -0
- package/src/agent/index.js +40 -0
- package/src/agent/prompt/skill-index.js +66 -0
- package/src/agent/tool-bloat.js +164 -0
- package/src/agent/tools/bash.js +156 -0
- package/src/agent/tools/edit.js +15 -0
- package/src/agent/tools/glob.js +71 -0
- package/src/agent/tools/grep.js +84 -0
- package/src/agent/tools/index.js +17 -0
- package/src/agent/tools/pi-bridge.js +638 -0
- package/src/agent/tools/read.js +39 -0
- package/src/agent/tools/shared/constants.js +21 -0
- package/src/agent/tools/shared/dedup.js +31 -0
- package/src/agent/tools/shared/output-truncation.js +54 -0
- package/src/agent/tools/shared/path-resolver.js +156 -0
- package/src/agent/tools/shared/ripgrep.js +130 -0
- package/src/agent/tools/shared/runtime-context.js +69 -0
- package/src/agent/tools/web-fetch.js +59 -0
- package/src/agent/tools/web-search.js +21 -0
- package/src/agent/tools/write.js +14 -0
- package/src/agent/transcript.js +227 -0
- package/src/ai/backend.js +17 -0
- package/src/ai/cost.js +164 -0
- package/src/ai/failure.js +165 -0
- package/src/ai/file-change-stats.js +234 -0
- package/src/ai/index.js +16 -0
- package/src/ai/live-input-prompt.js +15 -0
- package/src/ai/observer.js +233 -0
- package/src/ai/providers/claude-cli.js +694 -0
- package/src/ai/providers/claude-sdk.js +864 -0
- package/src/ai/providers/claude-subagents.js +67 -0
- package/src/ai/providers/codex-app.js +1045 -0
- package/src/ai/providers/opencode-app.js +356 -0
- package/src/ai/providers/opencode-discovery.js +39 -0
- package/src/ai/providers/pi-events.js +62 -0
- package/src/ai/providers/pi-messages.js +68 -0
- package/src/ai/providers/pi-models.js +111 -0
- package/src/ai/providers/pi-sdk.js +1310 -0
- package/src/ai/registry.js +5 -0
- package/src/ai/runtime/capabilities-used.js +56 -0
- package/src/ai/runtime/capabilities.js +44 -0
- package/src/ai/runtime/context-windows.js +38 -0
- package/src/ai/runtime/fast-mode.js +8 -0
- package/src/ai/runtime/model-refs.js +144 -0
- package/src/ai/runtime/registry.js +57 -0
- package/src/ai/runtime/router.js +214 -0
- package/src/ai/runtime/sessions.js +126 -0
- package/src/ai/streaming/codex-events.js +139 -0
- package/src/ai/streaming/opencode-events.js +54 -0
- package/src/ai/types.js +70 -0
- package/src/index.js +23 -0
- package/src/pi-auth.js +80 -0
- package/src/runtime-brand.js +32 -0
- 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
|
+
};
|