@mono-agent/agent-runtime 0.3.0 → 0.4.1
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 +75 -9
- package/MIGRATION.md +293 -0
- package/README.md +46 -20
- package/package.json +104 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +231 -654
- package/src/agent/index.js +1 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +272 -54
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +65 -18
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/index.js +1 -0
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +6 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-errors.js +65 -0
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +754 -0
- package/src/ai/runtime/capabilities.js +3 -0
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +33 -2
- package/src/ai/runtime/router.js +119 -19
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +19 -2
- package/src/ai/types.js +252 -20
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +75 -10
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- package/src/ai/providers/pi-sdk.js +0 -1310
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
// Pi-NATIVE runtime bridge.
|
|
2
|
+
//
|
|
3
|
+
// This is the SOLE pi runtime path: the hand-rolled pi bridge (formerly
|
|
4
|
+
// pi-sdk.js, driving the low-level `Agent` with manual MCP init, transcript
|
|
5
|
+
// handling, compaction, a hand-rolled stream-retry loop, and session
|
|
6
|
+
// bookkeeping) was removed once this bridge reached parity. This bridge builds
|
|
7
|
+
// on pi-agent-core's high-level AgentHarness plus native primitives:
|
|
8
|
+
//
|
|
9
|
+
// * AgentHarness OWNS a session and performs durable writes itself, so resume
|
|
10
|
+
// is "open the session from a repo and hand it to a new harness". There is
|
|
11
|
+
// no separate live-session registry here.
|
|
12
|
+
// * The provider transport (pi-ai streamSimple) is invoked by the harness;
|
|
13
|
+
// retry/backoff is delegated to pi-ai via streamOptions.maxRetries instead
|
|
14
|
+
// of the legacy manual loop.
|
|
15
|
+
// * Tool sandboxing, approval gates, allowlist/bloat filtering, and the MCP
|
|
16
|
+
// tool bridge are reused shared pieces — they are wired into the harness
|
|
17
|
+
// via its `tools` option, never reimplemented.
|
|
18
|
+
//
|
|
19
|
+
// The result/event contract is the package's unified runtime-result shape, so
|
|
20
|
+
// callers and the test suite see the same artifact the retired bridge produced.
|
|
21
|
+
|
|
22
|
+
import { createModels, createProvider, envApiKeyAuth } from "@earendil-works/pi-ai";
|
|
23
|
+
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
24
|
+
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { estimateCost } from "../cost.js";
|
|
27
|
+
import { retryableProviderFailureInfo } from "../failure.js";
|
|
28
|
+
import { runtimeCapabilities } from "../runtime/capabilities.js";
|
|
29
|
+
import {
|
|
30
|
+
deprecatedSettingsWarning,
|
|
31
|
+
resolveAgentCompactionPolicy,
|
|
32
|
+
resolveRuntimePolicyInputs,
|
|
33
|
+
} from "../../agent/compaction.js";
|
|
34
|
+
import { closePiMcpClients } from "../../agent/tools/pi-bridge.js";
|
|
35
|
+
import { createApprovalManager } from "../../agent/approval.js";
|
|
36
|
+
import { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings } from "../runtime/capabilities-used.js";
|
|
37
|
+
import { resolvePiRuntimeModel } from "./pi-models.js";
|
|
38
|
+
import {
|
|
39
|
+
textFromContent,
|
|
40
|
+
thinkingFromContent,
|
|
41
|
+
toAgentMessages,
|
|
42
|
+
} from "./pi-messages.js";
|
|
43
|
+
import { emitCaptured } from "./pi-events.js";
|
|
44
|
+
import { normalizePiErrorMessage } from "./pi-errors.js";
|
|
45
|
+
import {
|
|
46
|
+
runStructuredOutputFinalizationRetry,
|
|
47
|
+
shouldRetryStructuredOutputFinalization,
|
|
48
|
+
} from "./pi-native/structured-output.js";
|
|
49
|
+
import {
|
|
50
|
+
abortedResult,
|
|
51
|
+
buildDiagnostics,
|
|
52
|
+
buildErrorDetails,
|
|
53
|
+
buildErrorResult,
|
|
54
|
+
buildSuccessResult,
|
|
55
|
+
emitCapabilitiesResolved,
|
|
56
|
+
emitUsageCostEvents,
|
|
57
|
+
usageFromMessages,
|
|
58
|
+
} from "./pi-native/result-builder.js";
|
|
59
|
+
import {
|
|
60
|
+
cleanupSessionOnThrow,
|
|
61
|
+
commitSession,
|
|
62
|
+
discardUncommittedSession,
|
|
63
|
+
resolveDurableNativeSessionRepo,
|
|
64
|
+
resolveSession,
|
|
65
|
+
rollbackAbortedTurn,
|
|
66
|
+
} from "./pi-native/session-lifecycle.js";
|
|
67
|
+
import {
|
|
68
|
+
resolveLiveCompactionPolicy,
|
|
69
|
+
runProactiveCompaction,
|
|
70
|
+
runReactiveCompaction,
|
|
71
|
+
} from "./pi-native/compaction-driver.js";
|
|
72
|
+
import {
|
|
73
|
+
buildTurnHarness,
|
|
74
|
+
buildTurnTools,
|
|
75
|
+
runHarnessPrompt,
|
|
76
|
+
startLiveInput,
|
|
77
|
+
thinkingLevelForEffort,
|
|
78
|
+
} from "./pi-native/turn-runner.js";
|
|
79
|
+
|
|
80
|
+
async function resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnings }) {
|
|
81
|
+
if (apiKeys?.has(provider)) return apiKeys.get(provider);
|
|
82
|
+
if (typeof resolvePiApiKey !== "function") return undefined;
|
|
83
|
+
try {
|
|
84
|
+
return await resolvePiApiKey(provider);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
runtimeWarnings.push({
|
|
87
|
+
warning_kind: "pi_auth_failed",
|
|
88
|
+
provider,
|
|
89
|
+
message: err?.message || String(err),
|
|
90
|
+
});
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readCredential(provider, { apiKeys, resolvePiApiKey, runtimeWarnings }) {
|
|
96
|
+
if (apiKeys?.has(provider)) {
|
|
97
|
+
const key = apiKeys.get(provider);
|
|
98
|
+
return typeof key === "string" && key.length > 0 ? { type: "api_key", key } : undefined;
|
|
99
|
+
}
|
|
100
|
+
if (typeof resolvePiApiKey?.readCredential === "function") {
|
|
101
|
+
const credential = await resolvePiApiKey.readCredential(provider);
|
|
102
|
+
return isCredential(credential) ? credential : undefined;
|
|
103
|
+
}
|
|
104
|
+
const key = await resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnings });
|
|
105
|
+
return typeof key === "string" && key.length > 0 ? { type: "api_key", key } : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isCredential(credential) {
|
|
109
|
+
return credential
|
|
110
|
+
&& typeof credential === "object"
|
|
111
|
+
&& (credential.type === "api_key" || credential.type === "oauth");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// pi 0.80 removed the harness `getApiKeyAndHeaders` hook: request auth now
|
|
115
|
+
// resolves through a `Models` collection's `CredentialStore`. This store keeps
|
|
116
|
+
// the bridge's per-run key-resolution contract intact — an `apiKeys` map entry
|
|
117
|
+
// wins, else an enhanced host resolver's credential-store methods are used, else
|
|
118
|
+
// the legacy `resolvePiApiKey(provider)` callback is consulted. Legacy callback
|
|
119
|
+
// failures remain soft (`pi_auth_failed` warning + keyless env fallback);
|
|
120
|
+
// credential-store read/modify failures reject so Pi can surface them as auth
|
|
121
|
+
// storage failures instead of silently treating a corrupt store as no credential.
|
|
122
|
+
export function createDynamicCredentialStore(apiKeys, resolvePiApiKey, runtimeWarnings) {
|
|
123
|
+
const read = async (providerId) => readCredential(providerId, { apiKeys, resolvePiApiKey, runtimeWarnings });
|
|
124
|
+
return /** @type {any} */ ({
|
|
125
|
+
read,
|
|
126
|
+
async modify(providerId, fn) {
|
|
127
|
+
if (!apiKeys?.has(providerId) && typeof resolvePiApiKey?.modifyCredential === "function") {
|
|
128
|
+
return resolvePiApiKey.modifyCredential(providerId, fn);
|
|
129
|
+
}
|
|
130
|
+
const current = await read(providerId);
|
|
131
|
+
const next = await fn(current);
|
|
132
|
+
if (apiKeys?.has(providerId) && next?.type === "api_key" && typeof next.key === "string") {
|
|
133
|
+
apiKeys.set(providerId, next.key);
|
|
134
|
+
}
|
|
135
|
+
return next ?? current;
|
|
136
|
+
},
|
|
137
|
+
async delete(providerId) {
|
|
138
|
+
if (apiKeys?.has(providerId)) {
|
|
139
|
+
apiKeys.delete(providerId);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (typeof resolvePiApiKey?.deleteCredential === "function") {
|
|
143
|
+
await resolvePiApiKey.deleteCredential(providerId);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Assemble the pi 0.80 `Models` collection serving this run. Builtin models
|
|
150
|
+
// reuse pi's own provider factories (correct per-provider baseUrl/headers and
|
|
151
|
+
// env-var fallback); a custom OpenAI-completions provider is registered from the
|
|
152
|
+
// resolved model. `piResolvedModels` is an advanced/test seam mirroring
|
|
153
|
+
// `piResolvedModel`: when supplied it is used verbatim (the model dispatched via
|
|
154
|
+
// `piResolvedModel` may live outside pi's builtin catalog, e.g. a faux model).
|
|
155
|
+
function buildRunModels(runtime, options, runtimeWarnings) {
|
|
156
|
+
if (options.piResolvedModels) return options.piResolvedModels;
|
|
157
|
+
const credentials = createDynamicCredentialStore(runtime.apiKeys, options.resolvePiApiKey, runtimeWarnings);
|
|
158
|
+
if (options.customProvider) {
|
|
159
|
+
const model = runtime.model;
|
|
160
|
+
const models = createModels({ credentials });
|
|
161
|
+
models.setProvider(createProvider({
|
|
162
|
+
id: model.provider,
|
|
163
|
+
name: model.name || model.provider,
|
|
164
|
+
baseUrl: model.baseUrl,
|
|
165
|
+
auth: { apiKey: envApiKeyAuth(model.name || model.provider, []) },
|
|
166
|
+
models: [model],
|
|
167
|
+
api: openAICompletionsApi(),
|
|
168
|
+
}));
|
|
169
|
+
return models;
|
|
170
|
+
}
|
|
171
|
+
return builtinModels({ credentials });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Normalize the incoming runtime messages into AgentMessages the harness can
|
|
175
|
+
// seed/prompt. Returns the prior messages (appended to the session before the
|
|
176
|
+
// run) and the final user text used to drive `harness.prompt`.
|
|
177
|
+
export function splitPromptMessages(messages, model) {
|
|
178
|
+
const source = Array.isArray(messages) && messages.length
|
|
179
|
+
? messages
|
|
180
|
+
: [{ role: "user", content: "" }];
|
|
181
|
+
// The harness `prompt` takes the trailing user turn; everything before it is
|
|
182
|
+
// seeded as transcript context.
|
|
183
|
+
let lastUserIndex = -1;
|
|
184
|
+
for (let i = source.length - 1; i >= 0; i -= 1) {
|
|
185
|
+
if (source[i]?.role === "user") { lastUserIndex = i; break; }
|
|
186
|
+
}
|
|
187
|
+
if (lastUserIndex === -1) {
|
|
188
|
+
// No user turn: seed everything (structure preserved), nothing to prompt.
|
|
189
|
+
return { priorMessages: toAgentMessages(source, model), promptText: "", promptImages: [] };
|
|
190
|
+
}
|
|
191
|
+
// Prior turns: preserve structure (incl. image blocks) via toAgentMessages
|
|
192
|
+
// instead of stringifying — this is the format the harness seeds from.
|
|
193
|
+
const priorMessages = lastUserIndex > 0 ? toAgentMessages(source.slice(0, lastUserIndex), model) : [];
|
|
194
|
+
// Final user turn: split into plain text + structured images so harness.prompt
|
|
195
|
+
// can receive them as ImageContent[] rather than a JSON-stringified blob.
|
|
196
|
+
const { text, images } = splitUserContent(source[lastUserIndex].content);
|
|
197
|
+
return { priorMessages, promptText: text, promptImages: images };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Split a user message's content into joined text and ImageContent[] image
|
|
201
|
+
// parts ({ type, data, mimeType }), preserving multimodal input for the runtime.
|
|
202
|
+
function splitUserContent(content) {
|
|
203
|
+
if (typeof content === "string") return { text: content, images: [] };
|
|
204
|
+
if (!Array.isArray(content)) return { text: String(content ?? ""), images: [] };
|
|
205
|
+
const texts = [];
|
|
206
|
+
const images = [];
|
|
207
|
+
for (const part of content) {
|
|
208
|
+
if (typeof part === "string") { texts.push(part); continue; }
|
|
209
|
+
if (part?.type === "text" && typeof part.text === "string") { texts.push(part.text); continue; }
|
|
210
|
+
if (part?.type === "image" && part.data) {
|
|
211
|
+
images.push({ type: "image", data: part.data, mimeType: part.mimeType || part.mime_type || "image/png" });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
texts.push(JSON.stringify(part ?? ""));
|
|
215
|
+
}
|
|
216
|
+
return { text: texts.join("\n"), images };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
220
|
+
const resolved = options.model;
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
const events = [];
|
|
223
|
+
const runtimeWarnings = [];
|
|
224
|
+
let mcpClients = [];
|
|
225
|
+
let harness = null;
|
|
226
|
+
// The ONE explicit runState the extracted modules (stream subscriber, session
|
|
227
|
+
// lifecycle, compaction driver, turn runner, result builder) read/write.
|
|
228
|
+
// Reassignable scalars/refs live here so a module can rebind them (an
|
|
229
|
+
// orchestrator local cannot be reassigned from a module). `events` /
|
|
230
|
+
// `runtimeWarnings` stay as consts shared by reference. `toolStartTimes` maps
|
|
231
|
+
// toolCallId -> start timestamp so per-tool execution latency can be emitted.
|
|
232
|
+
const runState = {
|
|
233
|
+
assistantTexts: [],
|
|
234
|
+
assistantThinking: [],
|
|
235
|
+
textDeltaIndexes: new Set(),
|
|
236
|
+
thinkingDeltaIndexes: new Set(),
|
|
237
|
+
toolStartTimes: new Map(),
|
|
238
|
+
turnCount: 0,
|
|
239
|
+
toolResultsSeen: 0,
|
|
240
|
+
lastToolName: null,
|
|
241
|
+
maxTurnsHit: false,
|
|
242
|
+
// Populated by the StructuredOutput tool callback (built in the turn runner);
|
|
243
|
+
// read by the finalization retry predicate and the result assembly.
|
|
244
|
+
structuredResult: null,
|
|
245
|
+
// Set by the turn-runner abort handler; the OUTER catch and lifecycle
|
|
246
|
+
// decisions re-check it (`externalAbort ||= aborted`). removeAbortHandler is
|
|
247
|
+
// installed by the turn runner and cleared in finally; harness is set there
|
|
248
|
+
// too.
|
|
249
|
+
externalAbort: false,
|
|
250
|
+
removeAbortHandler: null,
|
|
251
|
+
harness: null,
|
|
252
|
+
// Auto-compaction sub-state. The policy is (re)computed at the decision
|
|
253
|
+
// point against the model actually serving the request; these flags track
|
|
254
|
+
// whether a compaction fired so the run reports context_compaction_applied
|
|
255
|
+
// honestly and never double-compacts.
|
|
256
|
+
compaction: {
|
|
257
|
+
applied: false,
|
|
258
|
+
reactiveAttempted: false,
|
|
259
|
+
compactedThisRun: false,
|
|
260
|
+
policy: null,
|
|
261
|
+
diagnostics: {},
|
|
262
|
+
},
|
|
263
|
+
session: null,
|
|
264
|
+
sessionEntry: null,
|
|
265
|
+
// True when a requestedSessionId had no live entry AND no durable transcript,
|
|
266
|
+
// so a fresh durable session was created under that id (cross-restart resume,
|
|
267
|
+
// first turn for the conversation). Distinct from a true resume (sessionEntry
|
|
268
|
+
// set): the on-disk transcript is empty, so prior messages must still be
|
|
269
|
+
// seeded — unlike a resume, where the session already holds them.
|
|
270
|
+
createdOnMiss: false,
|
|
271
|
+
// The create-on-miss BUSY reservation handle (R8 concurrent-first-turn
|
|
272
|
+
// reservation) when the create path inserted its placeholder; null
|
|
273
|
+
// otherwise. Its release()/commit() clean the placeholder on the
|
|
274
|
+
// drop/error/abort/keep-alive paths, since createdOnMiss leaves sessionEntry
|
|
275
|
+
// null so the finally's busy-clear does not cover it.
|
|
276
|
+
reservation: null,
|
|
277
|
+
sessionBaselineCount: 0,
|
|
278
|
+
// Leaf captured before a resumed turn runs, so a failed resume can be rolled
|
|
279
|
+
// back to the last good transcript via moveTo. On runState so the OUTER catch
|
|
280
|
+
// (host/runtime-side throws after the session mutated) can roll back too, not
|
|
281
|
+
// just the success path.
|
|
282
|
+
baselineLeafId: null,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const providerSessionId = options.sessionId
|
|
286
|
+
|| options.providerSessionId
|
|
287
|
+
|| options.runId
|
|
288
|
+
|| randomUUID();
|
|
289
|
+
// Prefer the explicit sessionId, but fall back to providerSessionId so a caller
|
|
290
|
+
// that only supplies providerSessionId still resumes the prior session instead
|
|
291
|
+
// of being treated as a fresh run (which would drop prior context).
|
|
292
|
+
const requestedSessionId = typeof options.sessionId === "string" && options.sessionId.trim()
|
|
293
|
+
? options.sessionId
|
|
294
|
+
: (typeof options.providerSessionId === "string" && options.providerSessionId.trim()
|
|
295
|
+
? options.providerSessionId
|
|
296
|
+
: null);
|
|
297
|
+
// Bridge TTL is a backstop behind the host's session policy; the grace
|
|
298
|
+
// keeps host-side lazy expiry firing first.
|
|
299
|
+
const sessionTtlMs = Number.isFinite(Number(options.sessionIdleTimeoutMs))
|
|
300
|
+
? Number(options.sessionIdleTimeoutMs) + 60_000
|
|
301
|
+
: undefined;
|
|
302
|
+
let structuredOutputFinalizationRetryAttempts = 0;
|
|
303
|
+
let structuredOutputFinalizationRetryReason = null;
|
|
304
|
+
let structuredOutputFinalizationRetryFailed = false;
|
|
305
|
+
|
|
306
|
+
const onEvent = (event) => emitCaptured(events, options.onEvent, event);
|
|
307
|
+
const approvalManager = options.onToolApprovalRequest
|
|
308
|
+
? createApprovalManager({
|
|
309
|
+
onToolApprovalRequest: options.onToolApprovalRequest,
|
|
310
|
+
defaultRiskTier: options.approvalDefaultRiskTier,
|
|
311
|
+
timeoutMs: options.approvalTimeoutMs,
|
|
312
|
+
onEvent,
|
|
313
|
+
riskTiersByTool: options.toolRiskTiers,
|
|
314
|
+
alwaysAllowTools: options.approvalAlwaysAllowTools,
|
|
315
|
+
})
|
|
316
|
+
: null;
|
|
317
|
+
|
|
318
|
+
if (options.abortSignal?.aborted) {
|
|
319
|
+
return abortedResult({ resolved, options, events, runtimeWarnings, start, providerSessionId });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const durableRepo = resolveDurableNativeSessionRepo(options.piSessionsRoot);
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// Resume the session (warm registry hit, durable cold reopen, or
|
|
326
|
+
// create-on-miss for a durable cross-restart first turn) or create a fresh
|
|
327
|
+
// one. resolveSession mutates runState.session / sessionEntry / createdOnMiss
|
|
328
|
+
// / reservation, and returns an early fast-fail result (session_not_found /
|
|
329
|
+
// session_busy) to return verbatim. Its liveness claims (I1 busy-claim, R8
|
|
330
|
+
// reservation, F4 cold-reopen re-read) are await-free by construction. A
|
|
331
|
+
// session miss stays cheap: no tool/MCP/harness init runs before this
|
|
332
|
+
// fast-fail.
|
|
333
|
+
const resolvedSession = await resolveSession(runState, {
|
|
334
|
+
requestedSessionId,
|
|
335
|
+
providerSessionId,
|
|
336
|
+
durableRepo,
|
|
337
|
+
sessionTtlMs,
|
|
338
|
+
cwd: options.cwd,
|
|
339
|
+
resolved,
|
|
340
|
+
options,
|
|
341
|
+
events,
|
|
342
|
+
runtimeWarnings,
|
|
343
|
+
start,
|
|
344
|
+
});
|
|
345
|
+
if (resolvedSession.done) return resolvedSession.result;
|
|
346
|
+
|
|
347
|
+
// `piResolvedModel` is an advanced/test seam: when supplied it provides a
|
|
348
|
+
// ready pi-ai Model (e.g. a registered faux provider model) plus optional
|
|
349
|
+
// capabilities, bypassing the static model-registry lookup. Production
|
|
350
|
+
// callers leave it undefined and resolve through pi-ai's registry.
|
|
351
|
+
const runtime = options.piResolvedModel
|
|
352
|
+
? {
|
|
353
|
+
model: options.piResolvedModel,
|
|
354
|
+
capabilities: options.piResolvedCapabilities || {
|
|
355
|
+
tool_use: true,
|
|
356
|
+
reasoning: !!options.piResolvedModel.reasoning,
|
|
357
|
+
reasoning_mode: options.piResolvedModel.reasoning ? "effort" : "none",
|
|
358
|
+
json_mode: true,
|
|
359
|
+
},
|
|
360
|
+
apiKeys: new Map(),
|
|
361
|
+
}
|
|
362
|
+
: resolvePiRuntimeModel(resolved, options);
|
|
363
|
+
const capabilities = runtime.capabilities || {};
|
|
364
|
+
const effectiveThinkingLevel = thinkingLevelForEffort(options.effort || "medium", capabilities);
|
|
365
|
+
const reference = resolved.reference
|
|
366
|
+
|| (resolved.sdk === "pi" ? `pi:${resolved.provider}:${resolved.model}` : `${resolved.sdk}:${resolved.model}`);
|
|
367
|
+
|
|
368
|
+
// Resolve the typed `toolLimits` / `compaction` policy objects against the
|
|
369
|
+
// deprecated `settings` fallback (per-group precedence: a present typed
|
|
370
|
+
// object wins and its group's legacy keys are ignored; an absent object
|
|
371
|
+
// falls back to `settings`). Consuming any legacy key emits exactly one
|
|
372
|
+
// deprecation warning per run. mono-agent never passes `settings`, so this
|
|
373
|
+
// is a no-op there; the shim exists for worklab's day-one port.
|
|
374
|
+
const { settingsLike, consumedSettingsKeys } = resolveRuntimePolicyInputs({
|
|
375
|
+
toolLimits: options.toolLimits,
|
|
376
|
+
compaction: options.compaction,
|
|
377
|
+
settings: options.settings,
|
|
378
|
+
});
|
|
379
|
+
if (consumedSettingsKeys.length > 0) {
|
|
380
|
+
const warning = deprecatedSettingsWarning(consumedSettingsKeys);
|
|
381
|
+
runtimeWarnings.push(warning);
|
|
382
|
+
onEvent({ type: "runtime_warning", ...warning });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Tool-output limits (clamps for tool/MCP payloads). The legacy pi-sdk bridge
|
|
386
|
+
// wired these via the compaction manager's `.policy`; resolveAgentCompactionPolicy
|
|
387
|
+
// is pure (no manager/Agent), so we compute the same policy directly from the
|
|
388
|
+
// resolved settings-like inputs and pass it into the tool builders + display
|
|
389
|
+
// normalization. Restores configurable clamping (toolTextLimitChars,
|
|
390
|
+
// searchResultLimit, ...) on top of the 256KB hard ceiling.
|
|
391
|
+
const toolLimits = resolveAgentCompactionPolicy(settingsLike, runtime.model);
|
|
392
|
+
|
|
393
|
+
// Build the turn's tools (builtins + MCP bridge + StructuredOutput). The
|
|
394
|
+
// StructuredOutput callback writes runState.structuredResult; the MCP clients
|
|
395
|
+
// are closed in the finally.
|
|
396
|
+
const { tools, structuredTool, mcpClients: builtMcpClients } = await buildTurnTools(runState, {
|
|
397
|
+
options,
|
|
398
|
+
capabilities,
|
|
399
|
+
toolLimits,
|
|
400
|
+
approvalManager,
|
|
401
|
+
runtime,
|
|
402
|
+
resolved,
|
|
403
|
+
onEvent,
|
|
404
|
+
runtimeWarnings,
|
|
405
|
+
});
|
|
406
|
+
mcpClients = builtMcpClients;
|
|
407
|
+
|
|
408
|
+
// Provider retry/backoff is delegated to pi-ai via streamOptions, replacing
|
|
409
|
+
// the legacy hand-rolled stream-retry loop.
|
|
410
|
+
const maxRetries = Number.isFinite(Number(options.piMaxRetries))
|
|
411
|
+
? Math.max(0, Math.min(8, Number(options.piMaxRetries)))
|
|
412
|
+
: 2;
|
|
413
|
+
const maxRetryDelayMs = Number.isFinite(Number(options.maxRetryDelayMs))
|
|
414
|
+
? Number(options.maxRetryDelayMs)
|
|
415
|
+
: 60_000;
|
|
416
|
+
// Tool steering: default "one-at-a-time" (safe, deterministic ordering).
|
|
417
|
+
// Opt-in "all" lets pi-agent-core run a model step's tool calls concurrently
|
|
418
|
+
// (QueueMode). Only enable when tools in a step are independent.
|
|
419
|
+
const toolSteeringMode = options.piToolParallelismMode === "all" ? "all" : "one-at-a-time";
|
|
420
|
+
|
|
421
|
+
const piModels = buildRunModels(runtime, options, runtimeWarnings);
|
|
422
|
+
|
|
423
|
+
// Construct the harness, subscribe the stream normalizer, and wire the
|
|
424
|
+
// external abort handler (which sets runState.externalAbort and aborts the
|
|
425
|
+
// harness). Sets runState.harness + runState.removeAbortHandler.
|
|
426
|
+
harness = buildTurnHarness(runState, {
|
|
427
|
+
cwd: options.cwd,
|
|
428
|
+
session: runState.session,
|
|
429
|
+
piModels,
|
|
430
|
+
model: runtime.model,
|
|
431
|
+
thinkingLevel: effectiveThinkingLevel,
|
|
432
|
+
systemPrompt,
|
|
433
|
+
outputSchema: options.outputSchema,
|
|
434
|
+
tools,
|
|
435
|
+
maxRetries,
|
|
436
|
+
maxRetryDelayMs,
|
|
437
|
+
steeringMode: toolSteeringMode,
|
|
438
|
+
onEvent,
|
|
439
|
+
options,
|
|
440
|
+
toolLimits,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Seed prior transcript (everything before the trailing user turn) into the
|
|
444
|
+
// harness-owned session. On a true resume the session already holds the
|
|
445
|
+
// transcript, so prior messages are skipped; a fresh run AND a create-on-miss
|
|
446
|
+
// (requestedSessionId set but the durable session was just created empty)
|
|
447
|
+
// both seed, since their on-disk transcript is empty.
|
|
448
|
+
const { priorMessages, promptText, promptImages } = splitPromptMessages(options.messages, runtime.model);
|
|
449
|
+
runState.sessionBaselineCount = (await runState.session.buildContext()).messages.length;
|
|
450
|
+
if (!requestedSessionId || runState.createdOnMiss) {
|
|
451
|
+
for (const message of priorMessages) {
|
|
452
|
+
await harness.appendMessage(message);
|
|
453
|
+
runState.sessionBaselineCount += 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// The harness persists each turn INLINE into the live session. To preserve
|
|
457
|
+
// the legacy "a failed resumed turn does not corrupt the session" contract,
|
|
458
|
+
// remember the leaf before the run so a failed resume can be rolled back to
|
|
459
|
+
// the last good transcript via the session tree's moveTo primitive. Only a
|
|
460
|
+
// TRUE resume needs this: a create-on-miss session is fresh, so a failure
|
|
461
|
+
// drops it entirely via the fresh-run path (no leaf to roll back to).
|
|
462
|
+
if (requestedSessionId && !runState.createdOnMiss) {
|
|
463
|
+
try { runState.baselineLeafId = await runState.session.getLeafId(); } catch { /* best-effort */ }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Live steering: consume follow-up messages and steer the harness mid-run.
|
|
467
|
+
// The consumer is tied to run completion so it stops steering once the run
|
|
468
|
+
// finishes and does not swallow messages meant for a later turn.
|
|
469
|
+
const liveInput = startLiveInput({ harness, options, onEvent });
|
|
470
|
+
|
|
471
|
+
// Re-check abort right before issuing the provider request. The abort
|
|
472
|
+
// handler is only installed at ~:639, AFTER a long stretch of awaited setup
|
|
473
|
+
// (reopen, create, MCP init, buildContext, appendMessage, getLeafId). If
|
|
474
|
+
// abort fired DURING any of those awaits the listener was not yet attached,
|
|
475
|
+
// so the event was dropped and no run is active for harness.abort() to
|
|
476
|
+
// target. Without this re-check a full provider/LLM request would be issued
|
|
477
|
+
// for a run the caller already aborted (mirrors the entry pre-check at ~:356).
|
|
478
|
+
if (options.abortSignal?.aborted) {
|
|
479
|
+
// Drop a freshly-created non-keep-alive session (and any create-on-miss
|
|
480
|
+
// reservation) so an aborted-before-run turn does not leave an orphan
|
|
481
|
+
// jsonl / leaked busy placeholder. discardUncommittedSession guards
|
|
482
|
+
// `session && !sessionEntry` so a resumed (user-owned) session is NEVER
|
|
483
|
+
// deleted; the finally clears sessionEntry.busy, removes the abort handler,
|
|
484
|
+
// and closes MCP clients. For a resume no transcript was appended yet
|
|
485
|
+
// (prompt never ran), so the live session needs no rollback.
|
|
486
|
+
await discardUncommittedSession(runState, { durableRepo });
|
|
487
|
+
return abortedResult({ resolved, options, events, runtimeWarnings, start, providerSessionId });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Compaction policy against the LIVE model's context window, then proactive
|
|
491
|
+
// compaction: if the session is already near the window, compact BEFORE
|
|
492
|
+
// issuing the request so a long-lived session never overflows.
|
|
493
|
+
runState.compaction.policy = resolveLiveCompactionPolicy({
|
|
494
|
+
harness,
|
|
495
|
+
runtime,
|
|
496
|
+
resolved,
|
|
497
|
+
settings: settingsLike,
|
|
498
|
+
contextWindowOverride: options.compaction?.contextWindowOverride,
|
|
499
|
+
});
|
|
500
|
+
await runProactiveCompaction(runState, {
|
|
501
|
+
harness,
|
|
502
|
+
systemPrompt,
|
|
503
|
+
options,
|
|
504
|
+
tools,
|
|
505
|
+
promptText,
|
|
506
|
+
promptImages,
|
|
507
|
+
reference,
|
|
508
|
+
onEvent,
|
|
509
|
+
runtimeWarnings,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
onEvent({
|
|
513
|
+
type: "provider_request_started",
|
|
514
|
+
sdk: resolved.sdk,
|
|
515
|
+
model: reference,
|
|
516
|
+
runtime: "pi",
|
|
517
|
+
timestamp: Date.now(),
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
let { runError } = await runHarnessPrompt(harness, promptText, promptImages);
|
|
521
|
+
|
|
522
|
+
// The run is done: stop the live-steering consumer so it cannot steer a
|
|
523
|
+
// finished harness or swallow a follow-up meant for the next turn.
|
|
524
|
+
await liveInput.stop();
|
|
525
|
+
|
|
526
|
+
runState.externalAbort ||= !!options.abortSignal?.aborted;
|
|
527
|
+
|
|
528
|
+
const captureState = async () => {
|
|
529
|
+
const context = await runState.session.buildContext();
|
|
530
|
+
const transcript = context.messages || [];
|
|
531
|
+
const runTranscript = transcript.slice(runState.sessionBaselineCount);
|
|
532
|
+
const assistantMessages = runTranscript.filter((message) => message?.role === "assistant");
|
|
533
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1] || null;
|
|
534
|
+
return {
|
|
535
|
+
transcript,
|
|
536
|
+
runTranscript,
|
|
537
|
+
assistantMessages,
|
|
538
|
+
lastAssistant,
|
|
539
|
+
stopReason: lastAssistant?.stopReason || null,
|
|
540
|
+
finalText: textFromContent(lastAssistant?.content) || runState.assistantTexts.join(""),
|
|
541
|
+
finalThinking: thinkingFromContent(lastAssistant?.content) || runState.assistantThinking.join(""),
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
let state = await captureState();
|
|
546
|
+
|
|
547
|
+
// Structured-output finalization retry: if the turn ended with neither
|
|
548
|
+
// text nor a StructuredOutput call, re-prompt ONCE in the same session with
|
|
549
|
+
// only StructuredOutput active so the model can submit the required result.
|
|
550
|
+
// This replicates the legacy bridge's single re-prompt via the harness's
|
|
551
|
+
// followUp + setActiveTools instead of the low-level agent.continue() loop.
|
|
552
|
+
if (!runError && shouldRetryStructuredOutputFinalization({
|
|
553
|
+
outputSchema: options.outputSchema,
|
|
554
|
+
structuredResult: runState.structuredResult,
|
|
555
|
+
finalText: state.finalText,
|
|
556
|
+
stopReason: state.stopReason,
|
|
557
|
+
externalAbort: runState.externalAbort,
|
|
558
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
559
|
+
})) {
|
|
560
|
+
const retry = await runStructuredOutputFinalizationRetry({ harness, structuredTool, runtimeWarnings, prompts: options.prompts });
|
|
561
|
+
structuredOutputFinalizationRetryAttempts = retry.attempts;
|
|
562
|
+
structuredOutputFinalizationRetryReason = retry.reason;
|
|
563
|
+
structuredOutputFinalizationRetryFailed = runState.structuredResult === null || runState.structuredResult === undefined;
|
|
564
|
+
state = await captureState();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Reactive recovery: if the turn ended in a context overflow and we have not
|
|
568
|
+
// already compacted-and-retried this run, compact once and re-prompt once
|
|
569
|
+
// (and re-capture state). Learns the real window ceiling from the error.
|
|
570
|
+
({ state, runError } = await runReactiveCompaction(runState, {
|
|
571
|
+
harness,
|
|
572
|
+
runtime,
|
|
573
|
+
resolved,
|
|
574
|
+
options,
|
|
575
|
+
promptText,
|
|
576
|
+
promptImages,
|
|
577
|
+
reference,
|
|
578
|
+
onEvent,
|
|
579
|
+
runtimeWarnings,
|
|
580
|
+
state,
|
|
581
|
+
runError,
|
|
582
|
+
captureState,
|
|
583
|
+
}));
|
|
584
|
+
|
|
585
|
+
const { runTranscript, lastAssistant, stopReason, finalText, finalThinking } = state;
|
|
586
|
+
const runAssistantCount = state.assistantMessages.length;
|
|
587
|
+
|
|
588
|
+
const usage = usageFromMessages(runTranscript);
|
|
589
|
+
const estimatedCost = estimateCost({
|
|
590
|
+
resolveCustomPricing: options.resolveCustomPricing,
|
|
591
|
+
model: reference,
|
|
592
|
+
inputTokens: usage.input,
|
|
593
|
+
outputTokens: usage.output,
|
|
594
|
+
cachedTokens: usage.cacheRead,
|
|
595
|
+
cacheWriteTokens: usage.cacheWrite,
|
|
596
|
+
});
|
|
597
|
+
emitUsageCostEvents({ onEvent, resolved, reference, usage, estimatedCost, start, externalAbort: runState.externalAbort });
|
|
598
|
+
|
|
599
|
+
const rawErrorMessage = runState.externalAbort
|
|
600
|
+
? null
|
|
601
|
+
: runState.maxTurnsHit
|
|
602
|
+
? "Pi agent stopped before final output: max turns reached"
|
|
603
|
+
: (stopReason === "error" || stopReason === "aborted"
|
|
604
|
+
? lastAssistant?.errorMessage || runError?.message || "Pi agent aborted before final output"
|
|
605
|
+
: (runError ? runError.message || String(runError) : null));
|
|
606
|
+
const errorMessage = normalizePiErrorMessage(rawErrorMessage);
|
|
607
|
+
|
|
608
|
+
const structuredRetry = {
|
|
609
|
+
attempts: structuredOutputFinalizationRetryAttempts,
|
|
610
|
+
reason: structuredOutputFinalizationRetryReason,
|
|
611
|
+
failed: structuredOutputFinalizationRetryFailed,
|
|
612
|
+
};
|
|
613
|
+
const diagnostics = buildDiagnostics({
|
|
614
|
+
providerSessionId,
|
|
615
|
+
stopReason,
|
|
616
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
617
|
+
maxTurns: options.maxTurns,
|
|
618
|
+
turnCount: runState.turnCount,
|
|
619
|
+
runAssistantCount,
|
|
620
|
+
externalAbort: runState.externalAbort,
|
|
621
|
+
maxRetries,
|
|
622
|
+
lastToolName: runState.lastToolName,
|
|
623
|
+
structuredRetry,
|
|
624
|
+
contextCompactionDiagnostics: runState.compaction.diagnostics,
|
|
625
|
+
});
|
|
626
|
+
const errorDetails = buildErrorDetails({
|
|
627
|
+
errorMessage,
|
|
628
|
+
stopReason,
|
|
629
|
+
lastToolName: runState.lastToolName,
|
|
630
|
+
toolResultsSeen: runState.toolResultsSeen,
|
|
631
|
+
turnCount: runState.turnCount,
|
|
632
|
+
runAssistantCount,
|
|
633
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
634
|
+
providerSessionId,
|
|
635
|
+
structuredRetry,
|
|
636
|
+
contextCompactionDiagnostics: runState.compaction.diagnostics,
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
const capabilitiesUsed = buildCapabilitiesUsed({
|
|
640
|
+
promptCacheActive: usage.cacheRead > 0 || usage.cacheWrite > 0,
|
|
641
|
+
thinkingEnabled: effectiveThinkingLevel !== "off" && effectiveThinkingLevel !== "low",
|
|
642
|
+
structuredOutputEnforced: !!options.outputSchema,
|
|
643
|
+
subagentInvoked: false,
|
|
644
|
+
mcpServersUsed: mcpClients.map((entry) => entry?.name).filter(Boolean),
|
|
645
|
+
nativeSubagentsUsed: [],
|
|
646
|
+
toolCompactionApplied: toolCompactionAppliedFromWarnings(runtimeWarnings),
|
|
647
|
+
// Tristate: true = a compaction fired this run (proactive or reactive),
|
|
648
|
+
// false = the path is enabled but did not need to fire, null = disabled via
|
|
649
|
+
// agent_compaction_enabled. See docs/reference/feature-registry.md runtime.context-compaction.
|
|
650
|
+
contextCompactionApplied: runState.compaction.policy?.enabled ? runState.compaction.applied : null,
|
|
651
|
+
});
|
|
652
|
+
emitCapabilitiesResolved(onEvent, { sdk: resolved.sdk, model: reference, capabilitiesUsed });
|
|
653
|
+
|
|
654
|
+
// Re-check the abort signal: a cancel can land during the post-run work above
|
|
655
|
+
// (live-input teardown, structured-output finalization retry) after the line
|
|
656
|
+
// ~780 check. Pick it up here so the lifecycle decision below rolls back the
|
|
657
|
+
// cancelled turn instead of committing it into a durable transcript a later
|
|
658
|
+
// resume would replay.
|
|
659
|
+
runState.externalAbort ||= !!options.abortSignal?.aborted;
|
|
660
|
+
|
|
661
|
+
// Session lifecycle parity with the legacy bridge. The harness already
|
|
662
|
+
// durably persisted the transcript into its session object (in-memory for
|
|
663
|
+
// the default repo, jsonl on disk when piSessionsRoot is set); commitSession
|
|
664
|
+
// tracks LIVENESS so disposeProviderSession / idle-TTL eviction can reach
|
|
665
|
+
// native sessions, keep-alive registers the session, and a failed/aborted
|
|
666
|
+
// resumed turn rolls back to its pre-turn leaf.
|
|
667
|
+
await commitSession(runState, {
|
|
668
|
+
options,
|
|
669
|
+
requestedSessionId,
|
|
670
|
+
providerSessionId,
|
|
671
|
+
durableRepo,
|
|
672
|
+
sessionTtlMs,
|
|
673
|
+
externalAbort: runState.externalAbort,
|
|
674
|
+
errorMessage,
|
|
675
|
+
onEvent,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Final abort guard (durable cancel TOCTOU): if a cancel raced the lifecycle
|
|
679
|
+
// commit above — landing AFTER the keep-alive/!externalAbort decision but
|
|
680
|
+
// before this return — the cancelled turn is still in the durable transcript
|
|
681
|
+
// and (for keep-alive) the live registry. Roll it back so the next resume sees
|
|
682
|
+
// the pre-turn state (rollbackAbortedTurn: a resumed session moves to its
|
|
683
|
+
// baseline leaf and drops its live entry; a fresh durable session deletes its
|
|
684
|
+
// jsonl). The abort re-check + return stay inline with NO await between the
|
|
685
|
+
// false-branch check and the return, so an external cancel cannot newly fire
|
|
686
|
+
// past it (I10).
|
|
687
|
+
if (!runState.externalAbort && options.abortSignal?.aborted) {
|
|
688
|
+
runState.externalAbort = true;
|
|
689
|
+
await rollbackAbortedTurn(runState, { requestedSessionId, providerSessionId, durableRepo });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return buildSuccessResult({
|
|
693
|
+
finalText,
|
|
694
|
+
finalThinking,
|
|
695
|
+
events,
|
|
696
|
+
usage,
|
|
697
|
+
estimatedCost,
|
|
698
|
+
start,
|
|
699
|
+
turnCount: runState.turnCount,
|
|
700
|
+
runAssistantCount,
|
|
701
|
+
resolved,
|
|
702
|
+
options,
|
|
703
|
+
externalAbort: runState.externalAbort,
|
|
704
|
+
errorMessage,
|
|
705
|
+
errorDetails,
|
|
706
|
+
diagnostics,
|
|
707
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
708
|
+
providerSessionId,
|
|
709
|
+
runtimeWarnings,
|
|
710
|
+
capabilitiesUsed,
|
|
711
|
+
structuredResult: runState.structuredResult,
|
|
712
|
+
});
|
|
713
|
+
} catch (err) {
|
|
714
|
+
runState.externalAbort ||= !!options.abortSignal?.aborted;
|
|
715
|
+
// Drop a just-created fresh durable session, release a create-on-miss
|
|
716
|
+
// reservation placeholder, and roll a resumed session back to its pre-turn
|
|
717
|
+
// leaf for host/runtime-side throws that landed after the harness already
|
|
718
|
+
// mutated the live session (guards preserved in cleanupSessionOnThrow).
|
|
719
|
+
await cleanupSessionOnThrow(runState, { durableRepo });
|
|
720
|
+
const errorMessage = normalizePiErrorMessage(err?.message || String(err));
|
|
721
|
+
const isRetryable = retryableProviderFailureInfo({
|
|
722
|
+
errorText: errorMessage,
|
|
723
|
+
failureKind: "provider_unavailable",
|
|
724
|
+
}).retryable;
|
|
725
|
+
return buildErrorResult({
|
|
726
|
+
assistantTexts: runState.assistantTexts,
|
|
727
|
+
events,
|
|
728
|
+
start,
|
|
729
|
+
turnCount: runState.turnCount,
|
|
730
|
+
resolved,
|
|
731
|
+
options,
|
|
732
|
+
externalAbort: runState.externalAbort,
|
|
733
|
+
errorMessage,
|
|
734
|
+
lastToolName: runState.lastToolName,
|
|
735
|
+
toolResultsSeen: runState.toolResultsSeen,
|
|
736
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
737
|
+
providerSessionId,
|
|
738
|
+
runtimeWarnings,
|
|
739
|
+
isRetryable,
|
|
740
|
+
});
|
|
741
|
+
} finally {
|
|
742
|
+
if (runState.sessionEntry) runState.sessionEntry.busy = false;
|
|
743
|
+
runState.removeAbortHandler?.();
|
|
744
|
+
await closePiMcpClients(mcpClients);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export const piNativeRuntimeBridge = {
|
|
749
|
+
id: "pi",
|
|
750
|
+
kind: "pi",
|
|
751
|
+
capabilities: runtimeCapabilities("pi"),
|
|
752
|
+
supports: (ref) => ref?.sdk === "pi",
|
|
753
|
+
execute: generatePiNativeResponse,
|
|
754
|
+
};
|