@mono-agent/agent-runtime 0.4.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 +37 -16
- package/MIGRATION.md +185 -7
- package/README.md +24 -14
- package/package.json +103 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +244 -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 +227 -45
- 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 +21 -10
- 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/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +2 -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-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 +415 -1106
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +29 -0
- package/src/ai/runtime/router.js +96 -12
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +16 -0
- package/src/ai/types.js +252 -19
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +72 -8
- 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 -35
|
@@ -19,28 +19,19 @@
|
|
|
19
19
|
// The result/event contract is the package's unified runtime-result shape, so
|
|
20
20
|
// callers and the test suite see the same artifact the retired bridge produced.
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
JsonlSessionRepo,
|
|
26
|
-
calculateContextTokens,
|
|
27
|
-
estimateTokens,
|
|
28
|
-
getLastAssistantUsage,
|
|
29
|
-
} from "@earendil-works/pi-agent-core";
|
|
30
|
-
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
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";
|
|
31
25
|
import { randomUUID } from "node:crypto";
|
|
32
26
|
import { estimateCost } from "../cost.js";
|
|
33
27
|
import { retryableProviderFailureInfo } from "../failure.js";
|
|
34
28
|
import { runtimeCapabilities } from "../runtime/capabilities.js";
|
|
35
|
-
import { createSessionRegistry } from "../runtime/sessions.js";
|
|
36
|
-
import { formatLiveInputGuidance } from "../live-input-prompt.js";
|
|
37
|
-
import { isLikelyContextTermination, resolveAgentCompactionPolicy } from "../../agent/compaction.js";
|
|
38
29
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from "../../agent/tools/pi-bridge.js";
|
|
30
|
+
deprecatedSettingsWarning,
|
|
31
|
+
resolveAgentCompactionPolicy,
|
|
32
|
+
resolveRuntimePolicyInputs,
|
|
33
|
+
} from "../../agent/compaction.js";
|
|
34
|
+
import { closePiMcpClients } from "../../agent/tools/pi-bridge.js";
|
|
44
35
|
import { createApprovalManager } from "../../agent/approval.js";
|
|
45
36
|
import { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings } from "../runtime/capabilities-used.js";
|
|
46
37
|
import { resolvePiRuntimeModel } from "./pi-models.js";
|
|
@@ -48,224 +39,43 @@ import {
|
|
|
48
39
|
textFromContent,
|
|
49
40
|
thinkingFromContent,
|
|
50
41
|
toAgentMessages,
|
|
51
|
-
toolResultContent,
|
|
52
42
|
} from "./pi-messages.js";
|
|
43
|
+
import { emitCaptured } from "./pi-events.js";
|
|
44
|
+
import { normalizePiErrorMessage } from "./pi-errors.js";
|
|
53
45
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
jsonSerializable,
|
|
58
|
-
streamContentKey,
|
|
59
|
-
} from "./pi-events.js";
|
|
46
|
+
runStructuredOutputFinalizationRetry,
|
|
47
|
+
shouldRetryStructuredOutputFinalization,
|
|
48
|
+
} from "./pi-native/structured-output.js";
|
|
60
49
|
import {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
function appendStructuredOutputInstruction(systemPrompt, outputSchema) {
|
|
91
|
-
if (!outputSchema) return systemPrompt;
|
|
92
|
-
return [
|
|
93
|
-
systemPrompt,
|
|
94
|
-
"",
|
|
95
|
-
"Structured output is available through the `StructuredOutput` tool.",
|
|
96
|
-
"When the final result is ready, call `StructuredOutput` with the complete JSON object matching the requested schema.",
|
|
97
|
-
"Do not also print the same JSON as prose unless tool calling is unavailable.",
|
|
98
|
-
].join("\n");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function toolStartProgressText(toolName) {
|
|
102
|
-
if (typeof toolName !== "string" || toolName.trim().length === 0) return null;
|
|
103
|
-
return `Running ${toolName}...`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function structuredOutputFinalizationPrompt() {
|
|
107
|
-
return [
|
|
108
|
-
"The previous assistant turn ended without submitting the required structured result.",
|
|
109
|
-
"Do not run tools, inspect files, or redo work.",
|
|
110
|
-
"Call only `StructuredOutput` once with the final object matching the requested schema, based on the completed transcript above.",
|
|
111
|
-
"Do not print prose before or after the tool call.",
|
|
112
|
-
].join("\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function shouldRetryStructuredOutputFinalization({
|
|
116
|
-
outputSchema,
|
|
117
|
-
structuredResult,
|
|
118
|
-
finalText,
|
|
119
|
-
stopReason,
|
|
120
|
-
externalAbort,
|
|
121
|
-
maxTurnsHit,
|
|
122
|
-
}) {
|
|
123
|
-
if (!outputSchema) return false;
|
|
124
|
-
if (structuredResult !== null && structuredResult !== undefined) return false;
|
|
125
|
-
if (String(finalText || "").trim()) return false;
|
|
126
|
-
if (externalAbort || maxTurnsHit) return false;
|
|
127
|
-
return stopReason !== "error" && stopReason !== "aborted";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function structuredOutputRetryDiagnostics(attempts, reason, failed) {
|
|
131
|
-
if (!attempts) return {};
|
|
132
|
-
return {
|
|
133
|
-
structured_output_finalization_retry_attempts: attempts,
|
|
134
|
-
structured_output_finalization_retry_reason: reason,
|
|
135
|
-
structured_output_finalization_retry_failed: !!failed,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function failureKindForPiError(message, diagnostics, { maxTurnsHit = false } = {}) {
|
|
140
|
-
if (!message) return null;
|
|
141
|
-
if (maxTurnsHit || isContextLimitError(message) || isLikelyContextTermination(message, diagnostics)) {
|
|
142
|
-
return "usage_limit";
|
|
143
|
-
}
|
|
144
|
-
return "provider_unavailable";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// AUTO-COMPACTION. pi-agent-core performs NO automatic in-loop compaction
|
|
148
|
-
// (shouldCompact/compact are exported helpers its loop never calls), so this
|
|
149
|
-
// bridge drives it: proactively before a turn when the running model's context
|
|
150
|
-
// is near the window, and reactively (compact + single re-prompt) if a turn
|
|
151
|
-
// still overflows. The window auto-tracks the model actually serving the request
|
|
152
|
-
// and self-corrects from any real ceiling stated in an overflow error.
|
|
153
|
-
|
|
154
|
-
// Per-process cache of real context-window ceilings discovered from overflow
|
|
155
|
-
// errors, keyed by model reference/id. The long-running host re-learns quickly
|
|
156
|
-
// after a restart; this just spares repeated first-overflow round-trips.
|
|
157
|
-
const discoveredContextWindows = new Map();
|
|
158
|
-
|
|
159
|
-
function modelWindowKey(harness, runtime, resolved) {
|
|
160
|
-
const live = typeof harness?.getModel === "function" ? harness.getModel() : null;
|
|
161
|
-
return resolved?.reference || runtime?.model?.id || live?.id || "unknown";
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// The window of the model that ACTUALLY serves this request: prefer the harness's
|
|
165
|
-
// live model (authoritative for native pi models), fall back to the resolved
|
|
166
|
-
// runtime model. Returns 0 when unknown so callers can skip the proactive trigger.
|
|
167
|
-
function liveModelContextWindow(harness, runtime) {
|
|
168
|
-
const live = typeof harness?.getModel === "function" ? harness.getModel() : null;
|
|
169
|
-
const win = Number(live?.contextWindow) || Number(runtime?.model?.contextWindow) || 0;
|
|
170
|
-
return win > 0 ? win : 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function effectiveContextWindow(harness, runtime, resolved) {
|
|
174
|
-
const declared = liveModelContextWindow(harness, runtime);
|
|
175
|
-
const discovered = discoveredContextWindows.get(modelWindowKey(harness, runtime, resolved));
|
|
176
|
-
if (Number.isFinite(discovered) && discovered > 0) {
|
|
177
|
-
return declared > 0 ? Math.min(declared, discovered) : discovered;
|
|
178
|
-
}
|
|
179
|
-
return declared;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function recordDiscoveredContextWindow(harness, runtime, resolved, limit) {
|
|
183
|
-
const n = Number(limit);
|
|
184
|
-
if (!Number.isFinite(n) || n <= 0) return;
|
|
185
|
-
const key = modelWindowKey(harness, runtime, resolved);
|
|
186
|
-
const existing = discoveredContextWindows.get(key);
|
|
187
|
-
discoveredContextWindows.set(key, Number.isFinite(existing) && existing > 0 ? Math.min(existing, n) : n);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Best-effort estimate of the current session's context size. The last assistant
|
|
191
|
-
// usage is authoritative (it reflects what the provider actually counted,
|
|
192
|
-
// including cache reads), but it can be stale/zero (e.g. seeded history), so we
|
|
193
|
-
// take the MAX of the usage-based count and a raw per-message estimate. Either
|
|
194
|
-
// being large is a reason to compact; overcounting only compacts slightly early.
|
|
195
|
-
async function estimateCurrentContextTokens(session) {
|
|
196
|
-
let usageTokens = 0;
|
|
197
|
-
let rawTokens = 0;
|
|
198
|
-
try {
|
|
199
|
-
const usage = getLastAssistantUsage(await session.getEntries());
|
|
200
|
-
if (usage) usageTokens = Number(calculateContextTokens(usage)) || 0;
|
|
201
|
-
} catch { /* ignore — fall back to the raw estimate */ }
|
|
202
|
-
try {
|
|
203
|
-
const context = await session.buildContext();
|
|
204
|
-
for (const message of context?.messages || []) rawTokens += Number(estimateTokens(message)) || 0;
|
|
205
|
-
} catch { /* ignore — usage-based estimate stands */ }
|
|
206
|
-
if (usageTokens === 0 && rawTokens === 0) return { tokens: 0, source: "unavailable" };
|
|
207
|
-
return usageTokens >= rawTokens
|
|
208
|
-
? { tokens: usageTokens, source: "usage" }
|
|
209
|
-
: { tokens: rawTokens, source: "estimate" };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Run a single guarded compaction. Requires the harness idle (callers
|
|
213
|
-
// waitForIdle first). Never throws — classifies AgentHarnessError into a warning
|
|
214
|
-
// and reports back whether anything was compacted. Fires onCompactionRecorded on
|
|
215
|
-
// success so a host can persist the compaction row.
|
|
216
|
-
async function tryCompact(harness, { trigger, onEvent, runtimeWarnings, onCompactionRecorded, runId, model }) {
|
|
217
|
-
try {
|
|
218
|
-
const result = await harness.compact();
|
|
219
|
-
const tokensBefore = Number(result?.tokensBefore) || null;
|
|
220
|
-
onEvent?.({
|
|
221
|
-
type: "runtime_warning",
|
|
222
|
-
warning_kind: "context_compaction_applied",
|
|
223
|
-
source: "pi",
|
|
224
|
-
trigger,
|
|
225
|
-
tokens_before: tokensBefore,
|
|
226
|
-
});
|
|
227
|
-
if (typeof onCompactionRecorded === "function") {
|
|
228
|
-
try {
|
|
229
|
-
onCompactionRecorded({
|
|
230
|
-
task_run_id: runId || null,
|
|
231
|
-
trigger,
|
|
232
|
-
provider_kind: "pi",
|
|
233
|
-
model: model || null,
|
|
234
|
-
tokens_before: tokensBefore,
|
|
235
|
-
summary: result?.summary || "",
|
|
236
|
-
first_kept_entry_id: result?.firstKeptEntryId || null,
|
|
237
|
-
status: "succeeded",
|
|
238
|
-
created_at: Date.now(),
|
|
239
|
-
});
|
|
240
|
-
} catch (err) {
|
|
241
|
-
runtimeWarnings.push({
|
|
242
|
-
warning_kind: "context_compaction_record_failed",
|
|
243
|
-
source: "pi",
|
|
244
|
-
message: err?.message || String(err),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return { applied: true, tokensBefore, nothingToCompact: false };
|
|
249
|
-
} catch (err) {
|
|
250
|
-
const message = err?.message || String(err);
|
|
251
|
-
const code = err?.code;
|
|
252
|
-
const nothingToCompact = code === "compaction" && /nothing to compact/i.test(message);
|
|
253
|
-
const warningKind = nothingToCompact
|
|
254
|
-
? "context_compaction_nothing_to_compact"
|
|
255
|
-
: code === "auth"
|
|
256
|
-
? "context_compaction_auth_failed"
|
|
257
|
-
: code === "busy"
|
|
258
|
-
? "context_compaction_busy"
|
|
259
|
-
: "context_compaction_failed";
|
|
260
|
-
runtimeWarnings.push({ warning_kind: warningKind, source: "pi", trigger, message });
|
|
261
|
-
return { applied: false, tokensBefore: null, nothingToCompact };
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function isReactiveCompactionCandidate(errorMessage, diagnostics) {
|
|
266
|
-
if (!errorMessage) return false;
|
|
267
|
-
return isContextLimitError(errorMessage) || isLikelyContextTermination(errorMessage, diagnostics);
|
|
268
|
-
}
|
|
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";
|
|
269
79
|
|
|
270
80
|
async function resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnings }) {
|
|
271
81
|
if (apiKeys?.has(provider)) return apiKeys.get(provider);
|
|
@@ -282,6 +92,85 @@ async function resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnin
|
|
|
282
92
|
}
|
|
283
93
|
}
|
|
284
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
|
+
|
|
285
174
|
// Normalize the incoming runtime messages into AgentMessages the harness can
|
|
286
175
|
// seed/prompt. Returns the prior messages (appended to the session before the
|
|
287
176
|
// run) and the final user text used to drive `harness.prompt`.
|
|
@@ -327,149 +216,71 @@ function splitUserContent(content) {
|
|
|
327
216
|
return { text: texts.join("\n"), images };
|
|
328
217
|
}
|
|
329
218
|
|
|
330
|
-
// Live pi-native sessions, keyed by provider session id. Entries are
|
|
331
|
-
// { session, metadata, repo, durable, busy } — identical shape and lifecycle
|
|
332
|
-
// policy to the (now-retired) pi-sdk bridge: in-memory transcripts are freed
|
|
333
|
-
// when the registry evicts them; durable (jsonl) transcripts survive eviction
|
|
334
|
-
// so a later resume can reopen them from disk. Registering here gives
|
|
335
|
-
// runtime.disposeSession / disposeProviderSession + idle-TTL eviction the same
|
|
336
|
-
// reach over native pi sessions that the legacy bridge had.
|
|
337
|
-
const nativeSessionRepo = new InMemorySessionRepo();
|
|
338
|
-
const nativeSessions = createSessionRegistry({
|
|
339
|
-
isBusy: (entry) => entry.busy === true,
|
|
340
|
-
onEvict: async (entry) => {
|
|
341
|
-
if (entry.durable) return;
|
|
342
|
-
try {
|
|
343
|
-
await entry.repo.delete(entry.metadata);
|
|
344
|
-
} catch { /* best-effort */ }
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const durableNativeSessionRepos = new Map();
|
|
349
|
-
|
|
350
|
-
function resolveDurableNativeSessionRepo(piSessionsRoot) {
|
|
351
|
-
if (typeof piSessionsRoot !== "string" || !piSessionsRoot.trim()) return null;
|
|
352
|
-
const root = piSessionsRoot.trim();
|
|
353
|
-
let repo = durableNativeSessionRepos.get(root);
|
|
354
|
-
if (!repo) {
|
|
355
|
-
repo = new JsonlSessionRepo({
|
|
356
|
-
fs: new NodeExecutionEnv({ cwd: process.cwd() }),
|
|
357
|
-
sessionsRoot: root,
|
|
358
|
-
});
|
|
359
|
-
durableNativeSessionRepos.set(root, repo);
|
|
360
|
-
}
|
|
361
|
-
return repo;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Defense in depth (R4): create-on-miss passes the caller-controlled session id
|
|
365
|
-
// straight to durableRepo.create({ id }), and JsonlSessionRepo writes
|
|
366
|
-
// `${createdAt}_${id}.jsonl` — so an id like "../../../../tmp/pwn" would escape
|
|
367
|
-
// piSessionsRoot and name a file anywhere on disk. The harness-derived id is a
|
|
368
|
-
// sha256 hex (always safe), but the public runtime API is caller-controlled.
|
|
369
|
-
// Only an id that is a single safe filename component may CREATE a session;
|
|
370
|
-
// anything else falls through to the existing session_not_found fast-fail, so a
|
|
371
|
-
// malicious id can never name a file. (A genuinely on-disk session reopened by
|
|
372
|
-
// reopenDurableNativeSession is matched by `.id`, never used to build a path, so
|
|
373
|
-
// this gate is confined to the create path.)
|
|
374
|
-
const SAFE_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
375
|
-
|
|
376
|
-
function isSafeSessionId(id) {
|
|
377
|
-
return typeof id === "string"
|
|
378
|
-
&& SAFE_SESSION_ID.test(id)
|
|
379
|
-
&& !id.includes("..")
|
|
380
|
-
&& !id.includes("/")
|
|
381
|
-
&& !id.includes("\\");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
async function reopenDurableNativeSession(repo, sessionId) {
|
|
385
|
-
try {
|
|
386
|
-
const metadata = (await repo.list()).find((entry) => entry?.id === sessionId);
|
|
387
|
-
if (!metadata) return null;
|
|
388
|
-
const session = await repo.open(metadata);
|
|
389
|
-
return { session, metadata, repo, durable: true, busy: false };
|
|
390
|
-
} catch {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function sessionUnavailableResult({
|
|
396
|
-
resolved,
|
|
397
|
-
options,
|
|
398
|
-
events,
|
|
399
|
-
runtimeWarnings,
|
|
400
|
-
start,
|
|
401
|
-
sessionId,
|
|
402
|
-
errorMessage,
|
|
403
|
-
failureKind,
|
|
404
|
-
piErrorCode,
|
|
405
|
-
}) {
|
|
406
|
-
return {
|
|
407
|
-
text: null,
|
|
408
|
-
events,
|
|
409
|
-
usage: {},
|
|
410
|
-
durationMs: Date.now() - start,
|
|
411
|
-
numTurns: 0,
|
|
412
|
-
model: resolved?.reference || resolved?.model || null,
|
|
413
|
-
effort: options.effort || null,
|
|
414
|
-
sdk: resolved?.sdk || "pi",
|
|
415
|
-
cancelled: false,
|
|
416
|
-
error: errorMessage,
|
|
417
|
-
failureKind,
|
|
418
|
-
providerSessionId: sessionId,
|
|
419
|
-
runtimeWarnings,
|
|
420
|
-
diagnostics: {
|
|
421
|
-
provider_session_id: sessionId,
|
|
422
|
-
pi_error_code: piErrorCode,
|
|
423
|
-
pi_engine: "native",
|
|
424
|
-
},
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function abortedResult({ resolved, options, events, runtimeWarnings, start, providerSessionId }) {
|
|
429
|
-
return {
|
|
430
|
-
text: null,
|
|
431
|
-
thinking: "",
|
|
432
|
-
events,
|
|
433
|
-
usage: {},
|
|
434
|
-
durationMs: Date.now() - start,
|
|
435
|
-
numTurns: 0,
|
|
436
|
-
model: resolved?.reference || resolved?.model || null,
|
|
437
|
-
effort: options.effort || null,
|
|
438
|
-
sdk: resolved?.sdk || "pi",
|
|
439
|
-
cancelled: true,
|
|
440
|
-
error: null,
|
|
441
|
-
failureKind: null,
|
|
442
|
-
providerSessionId,
|
|
443
|
-
runtimeWarnings,
|
|
444
|
-
diagnostics: {
|
|
445
|
-
provider_session_id: providerSessionId,
|
|
446
|
-
pi_stop_reason: "aborted",
|
|
447
|
-
pi_engine: "native",
|
|
448
|
-
external_abort: true,
|
|
449
|
-
},
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
219
|
export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
454
220
|
const resolved = options.model;
|
|
455
221
|
const start = Date.now();
|
|
456
222
|
const events = [];
|
|
457
223
|
const runtimeWarnings = [];
|
|
458
|
-
const assistantTexts = [];
|
|
459
|
-
const assistantThinking = [];
|
|
460
|
-
const textDeltaIndexes = new Set();
|
|
461
|
-
const thinkingDeltaIndexes = new Set();
|
|
462
|
-
let structuredResult = null;
|
|
463
224
|
let mcpClients = [];
|
|
464
|
-
let externalAbort = false;
|
|
465
|
-
let maxTurnsHit = false;
|
|
466
|
-
let turnCount = 0;
|
|
467
|
-
let toolResultsSeen = 0;
|
|
468
|
-
let lastToolName = null;
|
|
469
|
-
// toolCallId -> start timestamp, so we can emit per-tool execution latency.
|
|
470
|
-
const toolStartTimes = new Map();
|
|
471
225
|
let harness = null;
|
|
472
|
-
|
|
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
|
+
};
|
|
473
284
|
|
|
474
285
|
const providerSessionId = options.sessionId
|
|
475
286
|
|| options.providerSessionId
|
|
@@ -509,142 +320,29 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
509
320
|
}
|
|
510
321
|
|
|
511
322
|
const durableRepo = resolveDurableNativeSessionRepo(options.piSessionsRoot);
|
|
512
|
-
let session = null;
|
|
513
|
-
let sessionEntry = null;
|
|
514
|
-
// True when a requestedSessionId had no live entry AND no durable transcript,
|
|
515
|
-
// so a fresh durable session was created under that id (cross-restart resume,
|
|
516
|
-
// first turn for the conversation). Distinct from a true resume (sessionEntry
|
|
517
|
-
// set): the on-disk transcript is empty, so prior messages must still be
|
|
518
|
-
// seeded — unlike a resume, where the session already holds them.
|
|
519
|
-
let createdOnMiss = false;
|
|
520
|
-
// True once the create-on-miss path inserted its BUSY placeholder into the
|
|
521
|
-
// registry (R8 concurrent-first-turn reservation). Used to clean the
|
|
522
|
-
// placeholder up on the drop/error/abort paths, since createdOnMiss leaves
|
|
523
|
-
// sessionEntry null so the finally's busy-clear does not cover it.
|
|
524
|
-
let createdOnMissPlaceholder = false;
|
|
525
|
-
let sessionBaselineCount = 0;
|
|
526
|
-
// Leaf captured before a resumed turn runs, so a failed resume can be rolled
|
|
527
|
-
// back to the last good transcript via moveTo. Hoisted to function scope so
|
|
528
|
-
// the OUTER catch (host/runtime-side throws after the session mutated) can
|
|
529
|
-
// roll back too, not just the success path.
|
|
530
|
-
let baselineLeafId = null;
|
|
531
323
|
|
|
532
324
|
try {
|
|
533
|
-
// Resume
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
if (!entry) {
|
|
556
|
-
if (durableRepo && isSafeSessionId(providerSessionId)) {
|
|
557
|
-
// Create-on-miss (durable resume only): the requested id has no live
|
|
558
|
-
// registry entry AND no JSONL on disk under piSessionsRoot. This is
|
|
559
|
-
// the cross-restart resume case — the harness derives a stable id from
|
|
560
|
-
// the conversationId and passes it before any session exists on a
|
|
561
|
-
// fresh process. Rather than fail with session_not_found (which would
|
|
562
|
-
// make the harness re-send full history into yet another fresh,
|
|
563
|
-
// randomly-named session and orphan future resumes), create a durable
|
|
564
|
-
// session UNDER the requested id so this and every later turn for the
|
|
565
|
-
// conversation resolve to the same on-disk transcript. sessionEntry
|
|
566
|
-
// stays null so this proceeds exactly like a fresh run (prior messages
|
|
567
|
-
// are seeded, the keep-alive success path registers + persists it).
|
|
568
|
-
// The IN-MEMORY resume miss (no durableRepo) — and a create-on-miss
|
|
569
|
-
// with an UNSAFE id (R4) — keep fast-failing below, preserving the
|
|
570
|
-
// existing per-process session_not_found contract.
|
|
571
|
-
//
|
|
572
|
-
// Concurrent-first-turn race (R8): two concurrent first turns for the
|
|
573
|
-
// same durable id would BOTH miss here and BOTH create, producing two
|
|
574
|
-
// transcripts for one logical id (JsonlSessionRepo names files by
|
|
575
|
-
// `${createdAt}_${id}`, so there is no fs-level dedup). Mirror the
|
|
576
|
-
// cold-reopen-race defense: synchronously (NO await) re-check the
|
|
577
|
-
// registry, then reserve the id with a BUSY placeholder before the
|
|
578
|
-
// create await. The get→check→set span MUST stay await-free, so the
|
|
579
|
-
// loser observes the busy placeholder and returns session_busy via the
|
|
580
|
-
// same busy-claim path below — exactly one create per durable id.
|
|
581
|
-
const concurrent = nativeSessions.get(requestedSessionId);
|
|
582
|
-
if (concurrent) {
|
|
583
|
-
// A concurrent caller already reserved/created this id in the window
|
|
584
|
-
// since the miss above. Adopt its entry and fall into the busy-claim
|
|
585
|
-
// logic (session_busy if its turn is in flight, else resume).
|
|
586
|
-
entry = concurrent;
|
|
587
|
-
} else {
|
|
588
|
-
// Reserve the id with a busy placeholder BEFORE the create await so a
|
|
589
|
-
// second concurrent first turn observes busy and returns session_busy.
|
|
590
|
-
// The keep-alive success path (set(providerSessionId, ...) with
|
|
591
|
-
// busy:false) overwrites this placeholder on success; the drop/abort/
|
|
592
|
-
// catch paths delete it by requestedSessionId/providerSessionId (they
|
|
593
|
-
// are equal here). Keyed by requestedSessionId === providerSessionId.
|
|
594
|
-
nativeSessions.set(requestedSessionId, {
|
|
595
|
-
session: null,
|
|
596
|
-
metadata: null,
|
|
597
|
-
repo: durableRepo,
|
|
598
|
-
durable: true,
|
|
599
|
-
busy: true,
|
|
600
|
-
}, { idleTimeoutMs: sessionTtlMs });
|
|
601
|
-
createdOnMissPlaceholder = true;
|
|
602
|
-
session = await durableRepo.create({ id: providerSessionId, cwd: options.cwd || process.cwd() });
|
|
603
|
-
createdOnMiss = true;
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
return sessionUnavailableResult({
|
|
607
|
-
resolved,
|
|
608
|
-
options,
|
|
609
|
-
events,
|
|
610
|
-
runtimeWarnings,
|
|
611
|
-
start,
|
|
612
|
-
sessionId: requestedSessionId,
|
|
613
|
-
errorMessage: `Pi session ${requestedSessionId} is not live`,
|
|
614
|
-
failureKind: "session_not_found",
|
|
615
|
-
piErrorCode: "pi_session_not_found",
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
if (entry && !createdOnMiss) {
|
|
620
|
-
// The busy claim MUST stay await-free between the registry adoption
|
|
621
|
-
// above and `entry.busy = true` below: get/set + this check/claim are
|
|
622
|
-
// all synchronous, which is what makes the cold-resume race (F4) safe.
|
|
623
|
-
// Do not introduce any await in this span or the TOCTOU window reopens.
|
|
624
|
-
if (entry.busy) {
|
|
625
|
-
return sessionUnavailableResult({
|
|
626
|
-
resolved,
|
|
627
|
-
options,
|
|
628
|
-
events,
|
|
629
|
-
runtimeWarnings,
|
|
630
|
-
start,
|
|
631
|
-
sessionId: requestedSessionId,
|
|
632
|
-
errorMessage: `Pi session ${requestedSessionId} is busy with another turn`,
|
|
633
|
-
failureKind: "session_busy",
|
|
634
|
-
piErrorCode: "pi_session_busy",
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
|
-
entry.busy = true;
|
|
638
|
-
sessionEntry = entry;
|
|
639
|
-
session = entry.session;
|
|
640
|
-
}
|
|
641
|
-
} else {
|
|
642
|
-
// Fresh runs persist into the durable jsonl repo when piSessionsRoot is
|
|
643
|
-
// set, so a kept-alive session can be reopened from disk after the live
|
|
644
|
-
// entry is evicted; otherwise the in-memory repo is used.
|
|
645
|
-
session = await (durableRepo || nativeSessionRepo)
|
|
646
|
-
.create({ id: providerSessionId, cwd: options.cwd || process.cwd() });
|
|
647
|
-
}
|
|
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;
|
|
648
346
|
|
|
649
347
|
// `piResolvedModel` is an advanced/test seam: when supplied it provides a
|
|
650
348
|
// ready pi-ai Model (e.g. a registered faux provider model) plus optional
|
|
@@ -667,84 +365,45 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
667
365
|
const reference = resolved.reference
|
|
668
366
|
|| (resolved.sdk === "pi" ? `pi:${resolved.provider}:${resolved.model}` : `${resolved.sdk}:${resolved.model}`);
|
|
669
367
|
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// it is declared there; these flags track whether a compaction fired so the
|
|
681
|
-
// run reports context_compaction_applied honestly and never double-compacts.
|
|
682
|
-
let contextCompactionApplied = false;
|
|
683
|
-
let contextCompactionReactiveAttempted = false;
|
|
684
|
-
let contextCompactedThisRun = false;
|
|
685
|
-
let compactionPolicy = null;
|
|
686
|
-
const contextCompactionDiagnostics = {};
|
|
687
|
-
|
|
688
|
-
const onTruncate = (info) => {
|
|
689
|
-
try {
|
|
690
|
-
onEvent({
|
|
691
|
-
type: "runtime_warning",
|
|
692
|
-
warning_kind: "tool_payload_truncated",
|
|
693
|
-
source: "tool_bloat_guard",
|
|
694
|
-
...info,
|
|
695
|
-
});
|
|
696
|
-
} catch { /* best-effort */ }
|
|
697
|
-
};
|
|
698
|
-
const persistArtifact = options.persistArtifact || null;
|
|
699
|
-
const qaOutputDir = options.qaOutputDir || options.runArtifactDir || null;
|
|
700
|
-
|
|
701
|
-
// REUSED custom pieces: built-in tool sandboxing + allowlist/bloat filter +
|
|
702
|
-
// approval gates. These are identical to the legacy bridge.
|
|
703
|
-
const builtIns = capabilities.tool_use === false
|
|
704
|
-
? []
|
|
705
|
-
: getPiBuiltinTools(options.allowedTools, {
|
|
706
|
-
skillNames: (options.skills || []).map((skill) => skill.name),
|
|
707
|
-
dataDir: options.dataDir,
|
|
708
|
-
cwd: options.cwd,
|
|
709
|
-
onEvent,
|
|
710
|
-
persistArtifact,
|
|
711
|
-
onTruncate,
|
|
712
|
-
toolLimits,
|
|
713
|
-
toolPayloadMaxBytes: toolLimits.toolPayloadMaxBytes,
|
|
714
|
-
toolPolicy: options.toolPolicy,
|
|
715
|
-
sandboxPolicy: options.sandboxPolicy,
|
|
716
|
-
sandboxEngine: options.sandboxEngine,
|
|
717
|
-
approvalManager,
|
|
718
|
-
approvalModel: runtime.model?.id || runtime.model?.name || resolved.model,
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
const structuredTool = createStructuredOutputTool(options.outputSchema, (value) => {
|
|
722
|
-
structuredResult = value;
|
|
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,
|
|
723
378
|
});
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
? { clients: [], tools: [], warnings: [] }
|
|
730
|
-
: await initPiMcpTools(options.mcpServers || {}, reservedNames, {
|
|
731
|
-
cwd: options.cwd,
|
|
732
|
-
persistArtifact,
|
|
733
|
-
qaOutputDir,
|
|
734
|
-
onTruncate,
|
|
735
|
-
limits: toolLimits,
|
|
736
|
-
toolPayloadMaxBytes: toolLimits.toolPayloadMaxBytes,
|
|
737
|
-
sandboxPolicy: options.sandboxPolicy,
|
|
738
|
-
sandboxEngine: options.sandboxEngine,
|
|
739
|
-
});
|
|
740
|
-
mcpClients = mcpInit.clients;
|
|
741
|
-
for (const warning of mcpInit.warnings || []) onEvent(warning);
|
|
379
|
+
if (consumedSettingsKeys.length > 0) {
|
|
380
|
+
const warning = deprecatedSettingsWarning(consumedSettingsKeys);
|
|
381
|
+
runtimeWarnings.push(warning);
|
|
382
|
+
onEvent({ type: "runtime_warning", ...warning });
|
|
383
|
+
}
|
|
742
384
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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;
|
|
748
407
|
|
|
749
408
|
// Provider retry/backoff is delegated to pi-ai via streamOptions, replacing
|
|
750
409
|
// the legacy hand-rolled stream-retry loop.
|
|
@@ -759,129 +418,39 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
759
418
|
// (QueueMode). Only enable when tools in a step are independent.
|
|
760
419
|
const toolSteeringMode = options.piToolParallelismMode === "all" ? "all" : "one-at-a-time";
|
|
761
420
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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,
|
|
765
430
|
model: runtime.model,
|
|
766
431
|
thinkingLevel: effectiveThinkingLevel,
|
|
767
|
-
systemPrompt
|
|
432
|
+
systemPrompt,
|
|
433
|
+
outputSchema: options.outputSchema,
|
|
768
434
|
tools,
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const apiKey = await resolveApiKey(model.provider, {
|
|
772
|
-
apiKeys: runtime.apiKeys,
|
|
773
|
-
resolvePiApiKey: options.resolvePiApiKey,
|
|
774
|
-
runtimeWarnings,
|
|
775
|
-
});
|
|
776
|
-
return apiKey ? { apiKey } : undefined;
|
|
777
|
-
},
|
|
435
|
+
maxRetries,
|
|
436
|
+
maxRetryDelayMs,
|
|
778
437
|
steeringMode: toolSteeringMode,
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
harness.subscribe((event) => {
|
|
783
|
-
if (event.type === "message_update") {
|
|
784
|
-
const streamEvent = event.assistantMessageEvent;
|
|
785
|
-
if (streamEvent?.type === "text_delta" && streamEvent.delta) {
|
|
786
|
-
textDeltaIndexes.add(streamContentKey(streamEvent, "text"));
|
|
787
|
-
assistantTexts.push(streamEvent.delta);
|
|
788
|
-
onEvent({ type: "assistant", message: { content: [{ type: "text", text: streamEvent.delta }] } });
|
|
789
|
-
} else if (streamEvent?.type === "text_end" && streamEvent.content) {
|
|
790
|
-
const key = streamContentKey(streamEvent, "text");
|
|
791
|
-
if (!textDeltaIndexes.has(key)) {
|
|
792
|
-
assistantTexts.push(streamEvent.content);
|
|
793
|
-
onEvent({ type: "assistant", message: { content: [{ type: "text", text: streamEvent.content }] } });
|
|
794
|
-
}
|
|
795
|
-
} else if (streamEvent?.type === "thinking_delta" && streamEvent.delta) {
|
|
796
|
-
thinkingDeltaIndexes.add(streamContentKey(streamEvent, "thinking"));
|
|
797
|
-
assistantThinking.push(streamEvent.delta);
|
|
798
|
-
onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.delta }] } });
|
|
799
|
-
} else if (streamEvent?.type === "thinking_end" && streamEvent.content) {
|
|
800
|
-
const key = streamContentKey(streamEvent, "thinking");
|
|
801
|
-
if (!thinkingDeltaIndexes.has(key)) {
|
|
802
|
-
assistantThinking.push(streamEvent.content);
|
|
803
|
-
onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.content }] } });
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
} else if (event.type === "tool_execution_start") {
|
|
807
|
-
if (event.toolName) lastToolName = event.toolName;
|
|
808
|
-
if (event.toolCallId) toolStartTimes.set(event.toolCallId, Date.now());
|
|
809
|
-
const input = eventToolArgs(event.toolName, event.args, { cwd: options.cwd, toolLimits });
|
|
810
|
-
const progressText = toolStartProgressText(event.toolName);
|
|
811
|
-
if (progressText) {
|
|
812
|
-
onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: progressText }] } });
|
|
813
|
-
}
|
|
814
|
-
onEvent({
|
|
815
|
-
type: "assistant",
|
|
816
|
-
message: { content: [{ type: "tool_use", id: event.toolCallId, name: event.toolName, input }] },
|
|
817
|
-
});
|
|
818
|
-
} else if (event.type === "tool_execution_update") {
|
|
819
|
-
const input = eventToolArgs(event.toolName, event.args, { cwd: options.cwd, toolLimits });
|
|
820
|
-
onEvent({
|
|
821
|
-
type: "tool_update",
|
|
822
|
-
tool_use_id: event.toolCallId,
|
|
823
|
-
name: event.toolName,
|
|
824
|
-
input,
|
|
825
|
-
partial_result: jsonSerializable(event.partialResult, String(event.partialResult ?? "")),
|
|
826
|
-
});
|
|
827
|
-
} else if (event.type === "tool_execution_end") {
|
|
828
|
-
const resultContent = toolResultContent(event.result);
|
|
829
|
-
if (!event.isError) toolResultsSeen += 1;
|
|
830
|
-
const startedAt = toolStartTimes.get(event.toolCallId);
|
|
831
|
-
if (startedAt !== undefined) {
|
|
832
|
-
toolStartTimes.delete(event.toolCallId);
|
|
833
|
-
onEvent({
|
|
834
|
-
type: "tool_timing",
|
|
835
|
-
tool_use_id: event.toolCallId,
|
|
836
|
-
name: event.toolName,
|
|
837
|
-
execution_ms: Date.now() - startedAt,
|
|
838
|
-
is_error: !!event.isError,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
onEvent({
|
|
842
|
-
type: "user",
|
|
843
|
-
message: {
|
|
844
|
-
content: [{
|
|
845
|
-
type: "tool_result",
|
|
846
|
-
tool_use_id: event.toolCallId,
|
|
847
|
-
content: resultContent,
|
|
848
|
-
raw_result: compactToolRawResult(jsonSerializable(event.result, resultContent), resultContent),
|
|
849
|
-
is_error: !!event.isError,
|
|
850
|
-
}],
|
|
851
|
-
},
|
|
852
|
-
});
|
|
853
|
-
} else if (event.type === "turn_end") {
|
|
854
|
-
turnCount += 1;
|
|
855
|
-
if (Number.isFinite(Number(options.maxTurns))
|
|
856
|
-
&& Number(options.maxTurns) > 0
|
|
857
|
-
&& turnCount >= Number(options.maxTurns)
|
|
858
|
-
&& event.message?.stopReason === "toolUse") {
|
|
859
|
-
maxTurnsHit = true;
|
|
860
|
-
harness.abort();
|
|
861
|
-
}
|
|
862
|
-
}
|
|
438
|
+
onEvent,
|
|
439
|
+
options,
|
|
440
|
+
toolLimits,
|
|
863
441
|
});
|
|
864
442
|
|
|
865
|
-
const abortHandler = () => {
|
|
866
|
-
externalAbort = true;
|
|
867
|
-
harness.abort();
|
|
868
|
-
};
|
|
869
|
-
if (options.abortSignal) {
|
|
870
|
-
options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
871
|
-
removeAbortHandler = () => options.abortSignal.removeEventListener?.("abort", abortHandler);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
443
|
// Seed prior transcript (everything before the trailing user turn) into the
|
|
875
444
|
// harness-owned session. On a true resume the session already holds the
|
|
876
445
|
// transcript, so prior messages are skipped; a fresh run AND a create-on-miss
|
|
877
446
|
// (requestedSessionId set but the durable session was just created empty)
|
|
878
447
|
// both seed, since their on-disk transcript is empty.
|
|
879
448
|
const { priorMessages, promptText, promptImages } = splitPromptMessages(options.messages, runtime.model);
|
|
880
|
-
sessionBaselineCount = (await session.buildContext()).messages.length;
|
|
881
|
-
if (!requestedSessionId || createdOnMiss) {
|
|
449
|
+
runState.sessionBaselineCount = (await runState.session.buildContext()).messages.length;
|
|
450
|
+
if (!requestedSessionId || runState.createdOnMiss) {
|
|
882
451
|
for (const message of priorMessages) {
|
|
883
452
|
await harness.appendMessage(message);
|
|
884
|
-
sessionBaselineCount += 1;
|
|
453
|
+
runState.sessionBaselineCount += 1;
|
|
885
454
|
}
|
|
886
455
|
}
|
|
887
456
|
// The harness persists each turn INLINE into the live session. To preserve
|
|
@@ -890,36 +459,14 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
890
459
|
// the last good transcript via the session tree's moveTo primitive. Only a
|
|
891
460
|
// TRUE resume needs this: a create-on-miss session is fresh, so a failure
|
|
892
461
|
// drops it entirely via the fresh-run path (no leaf to roll back to).
|
|
893
|
-
if (requestedSessionId && !createdOnMiss) {
|
|
894
|
-
try { baselineLeafId = await session.getLeafId(); } catch { /* best-effort */ }
|
|
462
|
+
if (requestedSessionId && !runState.createdOnMiss) {
|
|
463
|
+
try { runState.baselineLeafId = await runState.session.getLeafId(); } catch { /* best-effort */ }
|
|
895
464
|
}
|
|
896
465
|
|
|
897
466
|
// Live steering: consume follow-up messages and steer the harness mid-run.
|
|
898
|
-
// The consumer is tied to run completion
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
let liveInputTask = null;
|
|
902
|
-
let runComplete = false;
|
|
903
|
-
if (options.liveInput) {
|
|
904
|
-
liveInputIterator = typeof options.liveInput[Symbol.asyncIterator] === "function"
|
|
905
|
-
? options.liveInput[Symbol.asyncIterator]()
|
|
906
|
-
: options.liveInput;
|
|
907
|
-
liveInputTask = (async () => {
|
|
908
|
-
try {
|
|
909
|
-
while (!runComplete && !options.abortSignal?.aborted) {
|
|
910
|
-
const next = await liveInputIterator.next();
|
|
911
|
-
if (next.done || runComplete || options.abortSignal?.aborted) break;
|
|
912
|
-
await harness.steer(formatLiveInputGuidance(next.value.body));
|
|
913
|
-
}
|
|
914
|
-
} catch (err) {
|
|
915
|
-
onEvent({
|
|
916
|
-
type: "runtime_warning",
|
|
917
|
-
warning_kind: "live_input_failed",
|
|
918
|
-
message: err?.message || String(err),
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
})();
|
|
922
|
-
}
|
|
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 });
|
|
923
470
|
|
|
924
471
|
// Re-check abort right before issuing the provider request. The abort
|
|
925
472
|
// handler is only installed at ~:639, AFTER a long stretch of awaited setup
|
|
@@ -929,65 +476,38 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
929
476
|
// target. Without this re-check a full provider/LLM request would be issued
|
|
930
477
|
// for a run the caller already aborted (mirrors the entry pre-check at ~:356).
|
|
931
478
|
if (options.abortSignal?.aborted) {
|
|
932
|
-
// Drop a freshly-created non-keep-alive session
|
|
933
|
-
// turn does not leave an orphan
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
// sessionEntry.busy, removes the abort handler,
|
|
937
|
-
// For a resume no transcript was appended yet
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
try { await (durableRepo || nativeSessionRepo).delete(await session.getMetadata()); } catch { /* best-effort */ }
|
|
941
|
-
}
|
|
942
|
-
// Drop the create-on-miss BUSY reservation too. The finally only clears
|
|
943
|
-
// sessionEntry.busy (null here), so without this the busy placeholder leaks
|
|
944
|
-
// and every future resume of this conversation's stable id returns
|
|
945
|
-
// session_busy forever (busy entries are never idle-evicted). Mirrors the
|
|
946
|
-
// lifecycle drop branch + outer catch cleanup.
|
|
947
|
-
if (createdOnMissPlaceholder) nativeSessions.delete(providerSessionId);
|
|
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 });
|
|
948
487
|
return abortedResult({ resolved, options, events, runtimeWarnings, start, providerSessionId });
|
|
949
488
|
}
|
|
950
489
|
|
|
951
|
-
// Compaction policy against the LIVE model's context window
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
model: reference,
|
|
973
|
-
});
|
|
974
|
-
if (res.applied) {
|
|
975
|
-
contextCompactionApplied = true;
|
|
976
|
-
contextCompactedThisRun = true;
|
|
977
|
-
Object.assign(contextCompactionDiagnostics, {
|
|
978
|
-
context_compaction_proactive: true,
|
|
979
|
-
context_compaction_tokens_before: res.tokensBefore,
|
|
980
|
-
context_compaction_estimate_source: est.source,
|
|
981
|
-
context_window: compactionPolicy.contextWindow,
|
|
982
|
-
});
|
|
983
|
-
// Compaction collapses the transcript prefix, so the pre-run baseline
|
|
984
|
-
// no longer aligns. Re-anchor it to the compacted length so the run's
|
|
985
|
-
// own turns (issued next) slice out correctly in captureState.
|
|
986
|
-
sessionBaselineCount = (await session.buildContext()).messages.length;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
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
|
+
});
|
|
991
511
|
|
|
992
512
|
onEvent({
|
|
993
513
|
type: "provider_request_started",
|
|
@@ -997,39 +517,18 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
997
517
|
timestamp: Date.now(),
|
|
998
518
|
});
|
|
999
519
|
|
|
1000
|
-
let runError =
|
|
1001
|
-
try {
|
|
1002
|
-
// Pass structured images (when present) so multimodal input reaches the
|
|
1003
|
-
// model as image blocks rather than stringified text. AgentHarness.prompt
|
|
1004
|
-
// takes them under an options object (`{ images }`); a bare array would be
|
|
1005
|
-
// read as `options` and silently dropped (options?.images === undefined).
|
|
1006
|
-
if (Array.isArray(promptImages) && promptImages.length > 0) {
|
|
1007
|
-
await harness.prompt(promptText, { images: promptImages });
|
|
1008
|
-
} else {
|
|
1009
|
-
await harness.prompt(promptText);
|
|
1010
|
-
}
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
runError = err;
|
|
1013
|
-
}
|
|
1014
|
-
await harness.waitForIdle();
|
|
520
|
+
let { runError } = await runHarnessPrompt(harness, promptText, promptImages);
|
|
1015
521
|
|
|
1016
522
|
// The run is done: stop the live-steering consumer so it cannot steer a
|
|
1017
|
-
// finished harness or swallow a follow-up meant for the next turn.
|
|
1018
|
-
|
|
1019
|
-
// next(). We do NOT await the task (it could block on next() if the source
|
|
1020
|
-
// has no return()), but the runComplete guard prevents any further steering.
|
|
1021
|
-
runComplete = true;
|
|
1022
|
-
if (liveInputIterator && typeof liveInputIterator.return === "function") {
|
|
1023
|
-
try { await liveInputIterator.return(); } catch { /* best-effort */ }
|
|
1024
|
-
}
|
|
1025
|
-
void liveInputTask;
|
|
523
|
+
// finished harness or swallow a follow-up meant for the next turn.
|
|
524
|
+
await liveInput.stop();
|
|
1026
525
|
|
|
1027
|
-
externalAbort ||= !!options.abortSignal?.aborted;
|
|
526
|
+
runState.externalAbort ||= !!options.abortSignal?.aborted;
|
|
1028
527
|
|
|
1029
528
|
const captureState = async () => {
|
|
1030
|
-
const context = await session.buildContext();
|
|
529
|
+
const context = await runState.session.buildContext();
|
|
1031
530
|
const transcript = context.messages || [];
|
|
1032
|
-
const runTranscript = transcript.slice(sessionBaselineCount);
|
|
531
|
+
const runTranscript = transcript.slice(runState.sessionBaselineCount);
|
|
1033
532
|
const assistantMessages = runTranscript.filter((message) => message?.role === "assistant");
|
|
1034
533
|
const lastAssistant = assistantMessages[assistantMessages.length - 1] || null;
|
|
1035
534
|
return {
|
|
@@ -1038,8 +537,8 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
1038
537
|
assistantMessages,
|
|
1039
538
|
lastAssistant,
|
|
1040
539
|
stopReason: lastAssistant?.stopReason || null,
|
|
1041
|
-
finalText: textFromContent(lastAssistant?.content) || assistantTexts.join(""),
|
|
1042
|
-
finalThinking: thinkingFromContent(lastAssistant?.content) || assistantThinking.join(""),
|
|
540
|
+
finalText: textFromContent(lastAssistant?.content) || runState.assistantTexts.join(""),
|
|
541
|
+
finalThinking: thinkingFromContent(lastAssistant?.content) || runState.assistantThinking.join(""),
|
|
1043
542
|
};
|
|
1044
543
|
};
|
|
1045
544
|
|
|
@@ -1052,100 +551,36 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
1052
551
|
// followUp + setActiveTools instead of the low-level agent.continue() loop.
|
|
1053
552
|
if (!runError && shouldRetryStructuredOutputFinalization({
|
|
1054
553
|
outputSchema: options.outputSchema,
|
|
1055
|
-
structuredResult,
|
|
554
|
+
structuredResult: runState.structuredResult,
|
|
1056
555
|
finalText: state.finalText,
|
|
1057
556
|
stopReason: state.stopReason,
|
|
1058
|
-
externalAbort,
|
|
1059
|
-
maxTurnsHit,
|
|
557
|
+
externalAbort: runState.externalAbort,
|
|
558
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
1060
559
|
})) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
source: "pi",
|
|
1066
|
-
reason: structuredOutputFinalizationRetryReason,
|
|
1067
|
-
message: "Pi stopped without text or structured output; retrying once in the same session with only StructuredOutput enabled.",
|
|
1068
|
-
});
|
|
1069
|
-
const previousActive = harness.getActiveTools().map((toolDef) => toolDef.name);
|
|
1070
|
-
try {
|
|
1071
|
-
// The harness is idle after waitForIdle(), so re-prompt (not followUp,
|
|
1072
|
-
// which only queues onto an active run) with only StructuredOutput
|
|
1073
|
-
// active. This re-prompts ONCE in the same session, matching the legacy
|
|
1074
|
-
// single agent.continue() finalization re-prompt.
|
|
1075
|
-
await harness.setActiveTools(structuredTool ? [structuredTool.name] : []);
|
|
1076
|
-
await harness.prompt(structuredOutputFinalizationPrompt());
|
|
1077
|
-
await harness.waitForIdle();
|
|
1078
|
-
} catch (err) {
|
|
1079
|
-
runtimeWarnings.push({
|
|
1080
|
-
warning_kind: "structured_output_finalization_retry_failed",
|
|
1081
|
-
source: "pi",
|
|
1082
|
-
message: err?.message || String(err),
|
|
1083
|
-
});
|
|
1084
|
-
} finally {
|
|
1085
|
-
try { await harness.setActiveTools(previousActive); } catch { /* best-effort */ }
|
|
1086
|
-
}
|
|
1087
|
-
structuredOutputFinalizationRetryFailed = structuredResult === null || structuredResult === undefined;
|
|
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;
|
|
1088
564
|
state = await captureState();
|
|
1089
565
|
}
|
|
1090
566
|
|
|
1091
567
|
// Reactive recovery: if the turn ended in a context overflow and we have not
|
|
1092
|
-
// already compacted-and-retried this run, compact once and re-prompt once
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
recordDiscoveredContextWindow(harness, runtime, resolved, parseContextLimitFromError(provisionalError));
|
|
1109
|
-
// A second compaction immediately after a fresh proactive one is almost
|
|
1110
|
-
// always "nothing to compact"; skip it and surface the original error.
|
|
1111
|
-
if (!contextCompactedThisRun) {
|
|
1112
|
-
await harness.waitForIdle();
|
|
1113
|
-
const res = await tryCompact(harness, {
|
|
1114
|
-
trigger: "reactive_overflow",
|
|
1115
|
-
onEvent,
|
|
1116
|
-
runtimeWarnings,
|
|
1117
|
-
onCompactionRecorded: options.onCompactionRecorded,
|
|
1118
|
-
runId: options.runId,
|
|
1119
|
-
model: reference,
|
|
1120
|
-
});
|
|
1121
|
-
if (res.applied) {
|
|
1122
|
-
contextCompactionApplied = true;
|
|
1123
|
-
contextCompactedThisRun = true;
|
|
1124
|
-
Object.assign(contextCompactionDiagnostics, {
|
|
1125
|
-
context_compaction_reactive: true,
|
|
1126
|
-
context_compaction_tokens_before: res.tokensBefore,
|
|
1127
|
-
});
|
|
1128
|
-
// Re-anchor the transcript baseline to the compacted length so the
|
|
1129
|
-
// re-prompt's turn (and its stopReason/usage) slices out correctly.
|
|
1130
|
-
sessionBaselineCount = (await session.buildContext()).messages.length;
|
|
1131
|
-
// Re-prompt ONCE in the now-compacted session. The trailing user turn
|
|
1132
|
-
// is already persisted, so a fresh prompt continues against it.
|
|
1133
|
-
runError = null;
|
|
1134
|
-
try {
|
|
1135
|
-
if (Array.isArray(promptImages) && promptImages.length > 0) {
|
|
1136
|
-
await harness.prompt(promptText, { images: promptImages });
|
|
1137
|
-
} else {
|
|
1138
|
-
await harness.prompt(promptText);
|
|
1139
|
-
}
|
|
1140
|
-
} catch (err) {
|
|
1141
|
-
runError = err;
|
|
1142
|
-
}
|
|
1143
|
-
await harness.waitForIdle();
|
|
1144
|
-
state = await captureState();
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
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
|
+
}));
|
|
1149
584
|
|
|
1150
585
|
const { runTranscript, lastAssistant, stopReason, finalText, finalThinking } = state;
|
|
1151
586
|
const runAssistantCount = state.assistantMessages.length;
|
|
@@ -1159,75 +594,47 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
1159
594
|
cachedTokens: usage.cacheRead,
|
|
1160
595
|
cacheWriteTokens: usage.cacheWrite,
|
|
1161
596
|
});
|
|
1162
|
-
|
|
1163
|
-
onEvent({ type: "cache_hit", sdk: resolved.sdk, model: reference, tokens: usage.cacheRead, source: "prompt_cache" });
|
|
1164
|
-
}
|
|
1165
|
-
if (usage.cacheWrite > 0) {
|
|
1166
|
-
onEvent({ type: "cache_miss", sdk: resolved.sdk, model: reference, tokens: usage.cacheWrite, source: "prompt_cache" });
|
|
1167
|
-
}
|
|
1168
|
-
onEvent({
|
|
1169
|
-
type: "cost_accumulated",
|
|
1170
|
-
sdk: resolved.sdk,
|
|
1171
|
-
model: reference,
|
|
1172
|
-
cumulativeUsd: Number(usage.cost) || Number(estimatedCost) || 0,
|
|
1173
|
-
tokens: {
|
|
1174
|
-
input: Number(usage.input) || 0,
|
|
1175
|
-
output: Number(usage.output) || 0,
|
|
1176
|
-
cacheReadTokens: Number(usage.cacheRead) || 0,
|
|
1177
|
-
cacheCreationTokens: Number(usage.cacheWrite) || 0,
|
|
1178
|
-
},
|
|
1179
|
-
});
|
|
1180
|
-
onEvent({
|
|
1181
|
-
type: "provider_request_completed",
|
|
1182
|
-
sdk: resolved.sdk,
|
|
1183
|
-
model: reference,
|
|
1184
|
-
runtime: "pi",
|
|
1185
|
-
timestamp: Date.now(),
|
|
1186
|
-
durationMs: Date.now() - start,
|
|
1187
|
-
cancelled: externalAbort,
|
|
1188
|
-
});
|
|
597
|
+
emitUsageCostEvents({ onEvent, resolved, reference, usage, estimatedCost, start, externalAbort: runState.externalAbort });
|
|
1189
598
|
|
|
1190
|
-
const rawErrorMessage = externalAbort
|
|
599
|
+
const rawErrorMessage = runState.externalAbort
|
|
1191
600
|
? null
|
|
1192
|
-
: maxTurnsHit
|
|
601
|
+
: runState.maxTurnsHit
|
|
1193
602
|
? "Pi agent stopped before final output: max turns reached"
|
|
1194
603
|
: (stopReason === "error" || stopReason === "aborted"
|
|
1195
604
|
? lastAssistant?.errorMessage || runError?.message || "Pi agent aborted before final output"
|
|
1196
605
|
: (runError ? runError.message || String(runError) : null));
|
|
1197
606
|
const errorMessage = normalizePiErrorMessage(rawErrorMessage);
|
|
1198
607
|
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
max_turns_hit: maxTurnsHit,
|
|
1204
|
-
max_turns: Number.isFinite(Number(options.maxTurns)) ? Number(options.maxTurns) : null,
|
|
1205
|
-
turn_count: turnCount || runAssistantCount,
|
|
1206
|
-
external_abort: externalAbort,
|
|
1207
|
-
pi_max_retries: maxRetries,
|
|
1208
|
-
...(lastToolName ? { last_tool_name: lastToolName } : {}),
|
|
1209
|
-
...structuredOutputRetryDiagnostics(
|
|
1210
|
-
structuredOutputFinalizationRetryAttempts,
|
|
1211
|
-
structuredOutputFinalizationRetryReason,
|
|
1212
|
-
structuredOutputFinalizationRetryFailed,
|
|
1213
|
-
),
|
|
1214
|
-
...contextCompactionDiagnostics,
|
|
608
|
+
const structuredRetry = {
|
|
609
|
+
attempts: structuredOutputFinalizationRetryAttempts,
|
|
610
|
+
reason: structuredOutputFinalizationRetryReason,
|
|
611
|
+
failed: structuredOutputFinalizationRetryFailed,
|
|
1215
612
|
};
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
+
});
|
|
1231
638
|
|
|
1232
639
|
const capabilitiesUsed = buildCapabilitiesUsed({
|
|
1233
640
|
promptCacheActive: usage.cacheRead > 0 || usage.cacheWrite > 0,
|
|
@@ -1239,199 +646,101 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
1239
646
|
toolCompactionApplied: toolCompactionAppliedFromWarnings(runtimeWarnings),
|
|
1240
647
|
// Tristate: true = a compaction fired this run (proactive or reactive),
|
|
1241
648
|
// false = the path is enabled but did not need to fire, null = disabled via
|
|
1242
|
-
// agent_compaction_enabled. See docs/feature-registry.md runtime.context-compaction.
|
|
1243
|
-
contextCompactionApplied:
|
|
649
|
+
// agent_compaction_enabled. See docs/reference/feature-registry.md runtime.context-compaction.
|
|
650
|
+
contextCompactionApplied: runState.compaction.policy?.enabled ? runState.compaction.applied : null,
|
|
1244
651
|
});
|
|
1245
|
-
onEvent
|
|
652
|
+
emitCapabilitiesResolved(onEvent, { sdk: resolved.sdk, model: reference, capabilitiesUsed });
|
|
1246
653
|
|
|
1247
654
|
// Re-check the abort signal: a cancel can land during the post-run work above
|
|
1248
655
|
// (live-input teardown, structured-output finalization retry) after the line
|
|
1249
656
|
// ~780 check. Pick it up here so the lifecycle decision below rolls back the
|
|
1250
657
|
// cancelled turn instead of committing it into a durable transcript a later
|
|
1251
658
|
// resume would replay.
|
|
1252
|
-
externalAbort ||= !!options.abortSignal?.aborted;
|
|
659
|
+
runState.externalAbort ||= !!options.abortSignal?.aborted;
|
|
1253
660
|
|
|
1254
661
|
// Session lifecycle parity with the legacy bridge. The harness already
|
|
1255
662
|
// durably persisted the transcript into its session object (in-memory for
|
|
1256
|
-
// the default repo, jsonl on disk when piSessionsRoot is set);
|
|
663
|
+
// the default repo, jsonl on disk when piSessionsRoot is set); commitSession
|
|
1257
664
|
// tracks LIVENESS so disposeProviderSession / idle-TTL eviction can reach
|
|
1258
|
-
// native sessions,
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
nativeSessions.set(providerSessionId, {
|
|
1271
|
-
session,
|
|
1272
|
-
metadata,
|
|
1273
|
-
repo: durableRepo || nativeSessionRepo,
|
|
1274
|
-
durable: !!durableRepo,
|
|
1275
|
-
busy: false,
|
|
1276
|
-
}, { idleTimeoutMs: sessionTtlMs });
|
|
1277
|
-
}
|
|
1278
|
-
} catch (err) {
|
|
1279
|
-
// Session persistence must never fail the run; drop the (now
|
|
1280
|
-
// inconsistent) session instead of resuming from a broken transcript.
|
|
1281
|
-
onEvent({
|
|
1282
|
-
type: "runtime_warning",
|
|
1283
|
-
warning_kind: "pi_session_persist_failed",
|
|
1284
|
-
message: err?.message || String(err),
|
|
1285
|
-
});
|
|
1286
|
-
nativeSessions.delete(providerSessionId);
|
|
1287
|
-
if (requestedSessionId) nativeSessions.delete(requestedSessionId);
|
|
1288
|
-
const broken = sessionEntry;
|
|
1289
|
-
if (broken) {
|
|
1290
|
-
try { await broken.repo.delete(broken.metadata); } catch { /* best-effort */ }
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
} else if (sessionEntry) {
|
|
1294
|
-
// Resumed run that errored (or was aborted): roll the live session back to
|
|
1295
|
-
// the leaf captured before this turn so the failed turn never leaks into a
|
|
1296
|
-
// later resume. The next resume then sees the last good transcript. The
|
|
1297
|
-
// entry stays live (busy is cleared in finally) and its idle TTL re-arms.
|
|
1298
|
-
if (baselineLeafId && (errorMessage || externalAbort)) {
|
|
1299
|
-
try { await session.moveTo(baselineLeafId); } catch { /* best-effort */ }
|
|
1300
|
-
}
|
|
1301
|
-
nativeSessions.touch(requestedSessionId, { idleTimeoutMs: sessionTtlMs });
|
|
1302
|
-
} else {
|
|
1303
|
-
// Fresh, non-keep-alive (or failed first) run: never leave a live session
|
|
1304
|
-
// behind. A durable jsonl transcript on disk is dropped too, matching the
|
|
1305
|
-
// legacy default contract that a non-keep-alive run is not resumable.
|
|
1306
|
-
// A create-on-miss BUSY placeholder (R8) is keyed under
|
|
1307
|
-
// requestedSessionId === providerSessionId; drop it here too so a
|
|
1308
|
-
// non-keep-alive / errored / aborted first turn never leaks a busy entry
|
|
1309
|
-
// (the success keep-alive path overwrites it with the finalized entry, so
|
|
1310
|
-
// it is only this drop branch that must clean it up).
|
|
1311
|
-
if (createdOnMissPlaceholder) nativeSessions.delete(providerSessionId);
|
|
1312
|
-
try {
|
|
1313
|
-
await (durableRepo || nativeSessionRepo).delete(await session.getMetadata());
|
|
1314
|
-
} catch { /* best-effort */ }
|
|
1315
|
-
}
|
|
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
|
+
});
|
|
1316
677
|
|
|
1317
678
|
// Final abort guard (durable cancel TOCTOU): if a cancel raced the lifecycle
|
|
1318
679
|
// commit above — landing AFTER the keep-alive/!externalAbort decision but
|
|
1319
680
|
// before this return — the cancelled turn is still in the durable transcript
|
|
1320
681
|
// and (for keep-alive) the live registry. Roll it back so the next resume sees
|
|
1321
|
-
// the pre-turn state: a resumed session moves to its
|
|
1322
|
-
// its live entry; a fresh durable session deletes its
|
|
1323
|
-
//
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1330
|
-
nativeSessions.delete(requestedSessionId);
|
|
1331
|
-
} else {
|
|
1332
|
-
nativeSessions.delete(providerSessionId);
|
|
1333
|
-
try { await (durableRepo || nativeSessionRepo).delete(await session.getMetadata()); } catch { /* best-effort */ }
|
|
1334
|
-
}
|
|
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 });
|
|
1335
690
|
}
|
|
1336
691
|
|
|
1337
|
-
return {
|
|
1338
|
-
|
|
1339
|
-
|
|
692
|
+
return buildSuccessResult({
|
|
693
|
+
finalText,
|
|
694
|
+
finalThinking,
|
|
1340
695
|
events,
|
|
1341
|
-
usage
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
numTurns: turnCount || runAssistantCount,
|
|
1351
|
-
model: resolved.reference || `pi:${resolved.provider}:${resolved.model}`,
|
|
1352
|
-
effort: options.effort || null,
|
|
1353
|
-
sdk: resolved.sdk,
|
|
1354
|
-
cancelled: externalAbort,
|
|
1355
|
-
error: errorMessage,
|
|
696
|
+
usage,
|
|
697
|
+
estimatedCost,
|
|
698
|
+
start,
|
|
699
|
+
turnCount: runState.turnCount,
|
|
700
|
+
runAssistantCount,
|
|
701
|
+
resolved,
|
|
702
|
+
options,
|
|
703
|
+
externalAbort: runState.externalAbort,
|
|
704
|
+
errorMessage,
|
|
1356
705
|
errorDetails,
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
: null,
|
|
706
|
+
diagnostics,
|
|
707
|
+
maxTurnsHit: runState.maxTurnsHit,
|
|
1360
708
|
providerSessionId,
|
|
1361
709
|
runtimeWarnings,
|
|
1362
|
-
diagnostics,
|
|
1363
710
|
capabilitiesUsed,
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
: { structuredResult: undefined, structuredResultSource: null }),
|
|
1367
|
-
};
|
|
711
|
+
structuredResult: runState.structuredResult,
|
|
712
|
+
});
|
|
1368
713
|
} catch (err) {
|
|
1369
|
-
externalAbort ||= !!options.abortSignal?.aborted;
|
|
1370
|
-
// Drop a just-created
|
|
1371
|
-
//
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
|
|
1375
|
-
// here would be data loss) and never when the throw preceded session create.
|
|
1376
|
-
if (session && !sessionEntry) {
|
|
1377
|
-
try { await (durableRepo || nativeSessionRepo).delete(await session.getMetadata()); } catch { /* best-effort */ }
|
|
1378
|
-
}
|
|
1379
|
-
// Drop a create-on-miss BUSY placeholder (R8) left in the registry by a throw
|
|
1380
|
-
// during/after the reservation — including a throw inside the create await
|
|
1381
|
-
// itself, where `session` is still null so the jsonl-delete above is skipped.
|
|
1382
|
-
// Keyed under requestedSessionId === providerSessionId; never set on a resume
|
|
1383
|
-
// (sessionEntry would be non-null), so this never deletes a live user session.
|
|
1384
|
-
if (createdOnMissPlaceholder && !sessionEntry) nativeSessions.delete(providerSessionId);
|
|
1385
|
-
// Resumed-session rollback for host/runtime-side throws (e.g. a throwing
|
|
1386
|
-
// custom pricing resolver / bridge event callback) that land here AFTER the
|
|
1387
|
-
// harness already mutated the live session. Mirrors the success-path
|
|
1388
|
-
// rollback at ~:925-932: move the live session back to the pre-turn leaf so
|
|
1389
|
-
// the failed turn never leaks into a later resume. Gated on `sessionEntry &&
|
|
1390
|
-
// baselineLeafId` so it only fires for resumes that captured a baseline.
|
|
1391
|
-
if (sessionEntry && baselineLeafId) {
|
|
1392
|
-
try { await session.moveTo(baselineLeafId); } catch { /* best-effort */ }
|
|
1393
|
-
}
|
|
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 });
|
|
1394
720
|
const errorMessage = normalizePiErrorMessage(err?.message || String(err));
|
|
1395
721
|
const isRetryable = retryableProviderFailureInfo({
|
|
1396
722
|
errorText: errorMessage,
|
|
1397
723
|
failureKind: "provider_unavailable",
|
|
1398
724
|
}).retryable;
|
|
1399
|
-
return {
|
|
1400
|
-
|
|
725
|
+
return buildErrorResult({
|
|
726
|
+
assistantTexts: runState.assistantTexts,
|
|
1401
727
|
events,
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
pi_stop_reason: "error",
|
|
1412
|
-
last_tool_name: lastToolName,
|
|
1413
|
-
tool_results_seen: toolResultsSeen,
|
|
1414
|
-
turn_count: turnCount,
|
|
1415
|
-
max_turns_hit: maxTurnsHit,
|
|
1416
|
-
provider_session_id: providerSessionId,
|
|
1417
|
-
pi_engine: "native",
|
|
1418
|
-
pi_error_retryable: isRetryable,
|
|
1419
|
-
},
|
|
1420
|
-
failureKind: externalAbort ? null : failureKindForPiError(errorMessage, {}, { maxTurnsHit }),
|
|
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,
|
|
1421
737
|
providerSessionId,
|
|
1422
738
|
runtimeWarnings,
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
pi_stop_reason: externalAbort ? "aborted" : "error",
|
|
1426
|
-
pi_engine: "native",
|
|
1427
|
-
max_turns_hit: maxTurnsHit,
|
|
1428
|
-
turn_count: turnCount,
|
|
1429
|
-
external_abort: externalAbort,
|
|
1430
|
-
},
|
|
1431
|
-
};
|
|
739
|
+
isRetryable,
|
|
740
|
+
});
|
|
1432
741
|
} finally {
|
|
1433
|
-
if (sessionEntry) sessionEntry.busy = false;
|
|
1434
|
-
removeAbortHandler?.();
|
|
742
|
+
if (runState.sessionEntry) runState.sessionEntry.busy = false;
|
|
743
|
+
runState.removeAbortHandler?.();
|
|
1435
744
|
await closePiMcpClients(mcpClients);
|
|
1436
745
|
}
|
|
1437
746
|
}
|