@mono-agent/agent-runtime 0.20.11 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/ARCHITECTURE.md +50 -11
  2. package/MIGRATION.md +288 -26
  3. package/README.md +352 -477
  4. package/package.json +13 -44
  5. package/src/agent/tool-bloat.js +145 -9
  6. package/src/agent/tools/agent-tool.js +108 -9
  7. package/src/agent/tools/bash.js +11 -26
  8. package/src/agent/tools/codex-subscription-search.js +123 -29
  9. package/src/agent/tools/exec.js +10 -2
  10. package/src/agent/tools/index.js +7 -0
  11. package/src/agent/tools/monitor.js +149 -0
  12. package/src/agent/tools/pi-bridge.js +123 -19
  13. package/src/agent/tools/shared/bash-environment.js +31 -0
  14. package/src/agent/tools/shared/monitors.js +293 -0
  15. package/src/agent/tools/shared/path-resolver.js +25 -6
  16. package/src/agent/tools/shared/process-jobs.js +6 -1
  17. package/src/agent/tools/shared/process-runner.js +26 -6
  18. package/src/agent/tools/shared/tool-context.js +8 -0
  19. package/src/agent/tools/web-access-interstitial.js +70 -0
  20. package/src/agent/tools/web-browser-render.js +83 -58
  21. package/src/agent/tools/web-controller.js +112 -21
  22. package/src/agent/tools/web-document-extractor.js +379 -0
  23. package/src/agent/tools/web-fetch.js +271 -243
  24. package/src/agent/tools/web-request.js +65 -0
  25. package/src/agent/tools/web-search-output.js +165 -0
  26. package/src/agent/tools/web-search-state.js +75 -0
  27. package/src/agent/tools/web-search.js +532 -71
  28. package/src/ai/cost.js +13 -68
  29. package/src/ai/failure.js +3 -3
  30. package/src/ai/index.js +5 -17
  31. package/src/ai/observer.js +8 -0
  32. package/src/ai/pi-interop.js +221 -1
  33. package/src/ai/pi-oauth-compat.js +1 -1
  34. package/src/ai/provider-check.js +131 -0
  35. package/src/ai/providers/codex/app-server-client.js +592 -0
  36. package/src/ai/providers/pi-models.js +18 -10
  37. package/src/ai/providers/pi-native/compaction-driver.js +94 -42
  38. package/src/ai/providers/pi-native/compaction-summary.js +140 -0
  39. package/src/ai/providers/pi-native/harness-adapter.js +376 -0
  40. package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
  41. package/src/ai/providers/pi-native/provider-attribution.js +102 -0
  42. package/src/ai/providers/pi-native/result-builder.js +38 -14
  43. package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
  44. package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
  45. package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
  46. package/src/ai/providers/pi-native/turn-runner.js +279 -28
  47. package/src/ai/providers/pi-native.js +206 -61
  48. package/src/ai/runtime/capabilities.js +11 -56
  49. package/src/ai/runtime/live-input-events.js +250 -54
  50. package/src/ai/runtime/model-refs.js +118 -153
  51. package/src/ai/runtime/registry.js +22 -56
  52. package/src/ai/runtime/router.js +76 -417
  53. package/src/ai/runtime/session-liveness.js +3 -4
  54. package/src/ai/runtime/sessions.js +4 -5
  55. package/src/ai/runtime/tool-policy.js +0 -2
  56. package/src/ai/tool-lifecycle.js +32 -18
  57. package/src/ai/types.js +37 -112
  58. package/src/index.js +0 -6
  59. package/src/runtime.js +29 -16
  60. package/types/agent/tool-bloat.d.ts +1 -1
  61. package/types/agent/tools/agent-tool.d.ts +4 -2
  62. package/types/agent/tools/bash.d.ts +5 -3
  63. package/types/agent/tools/codex-subscription-search.d.ts +7 -3
  64. package/types/agent/tools/exec.d.ts +5 -3
  65. package/types/agent/tools/index.d.ts +1 -0
  66. package/types/agent/tools/monitor.d.ts +47 -0
  67. package/types/agent/tools/pi-bridge.d.ts +7 -4
  68. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  69. package/types/agent/tools/shared/monitors.d.ts +98 -0
  70. package/types/agent/tools/shared/process-jobs.d.ts +5 -1
  71. package/types/agent/tools/shared/process-runner.d.ts +14 -4
  72. package/types/agent/tools/shared/tool-context.d.ts +2 -0
  73. package/types/agent/tools/web-access-interstitial.d.ts +23 -0
  74. package/types/agent/tools/web-browser-render.d.ts +4 -1
  75. package/types/agent/tools/web-controller.d.ts +4 -2
  76. package/types/agent/tools/web-document-extractor.d.ts +27 -0
  77. package/types/agent/tools/web-fetch.d.ts +19 -24
  78. package/types/agent/tools/web-request.d.ts +20 -0
  79. package/types/agent/tools/web-search-output.d.ts +31 -0
  80. package/types/agent/tools/web-search-state.d.ts +21 -0
  81. package/types/agent/tools/web-search.d.ts +10 -45
  82. package/types/ai/cost.d.ts +1 -2
  83. package/types/ai/index.d.ts +2 -4
  84. package/types/ai/observer.d.ts +6 -0
  85. package/types/ai/pi-interop.d.ts +81 -0
  86. package/types/ai/provider-check.d.ts +53 -0
  87. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  88. package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
  89. package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
  90. package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
  91. package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
  92. package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
  93. package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
  94. package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
  95. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  96. package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
  97. package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
  98. package/types/ai/providers/pi-native.d.ts +21 -4
  99. package/types/ai/runtime/capabilities.d.ts +21 -70
  100. package/types/ai/runtime/live-input-events.d.ts +32 -8
  101. package/types/ai/runtime/model-refs.d.ts +0 -24
  102. package/types/ai/runtime/router.d.ts +3 -10
  103. package/types/ai/runtime/tool-policy.d.ts +0 -2
  104. package/types/ai/tool-lifecycle.d.ts +4 -3
  105. package/types/ai/types.d.ts +162 -256
  106. package/types/index.d.ts +0 -1
  107. package/src/ai/providers/acp-client.js +0 -1149
  108. package/src/ai/providers/acp-privacy.js +0 -124
  109. package/src/ai/providers/acp-public.js +0 -21
  110. package/src/ai/providers/acp-session-tokens.js +0 -282
  111. package/src/ai/providers/acp-transport.js +0 -356
  112. package/src/ai/providers/acp.js +0 -543
  113. package/src/ai/providers/claude-cli.js +0 -883
  114. package/src/ai/providers/claude-sandbox.js +0 -71
  115. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  116. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  117. package/src/ai/providers/claude-sdk.js +0 -1127
  118. package/src/ai/providers/claude-subagent-activity.js +0 -719
  119. package/src/ai/providers/claude-subagents.js +0 -88
  120. package/src/ai/providers/codex-app.js +0 -2946
  121. package/src/ai/providers/opencode-app.js +0 -1109
  122. package/src/ai/providers/opencode-discovery.js +0 -39
  123. package/src/ai/providers/opencode-server.js +0 -508
  124. package/src/ai/runtime/context-windows.js +0 -46
  125. package/src/ai/runtime/fast-mode.js +0 -8
  126. package/src/ai/streaming/codex-events.js +0 -146
  127. package/src/ai/streaming/opencode-events.js +0 -59
  128. package/types/ai/providers/acp-client.d.ts +0 -227
  129. package/types/ai/providers/acp-privacy.d.ts +0 -25
  130. package/types/ai/providers/acp-public.d.ts +0 -7
  131. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  132. package/types/ai/providers/acp-transport.d.ts +0 -45
  133. package/types/ai/providers/acp.d.ts +0 -93
  134. package/types/ai/providers/claude-cli.d.ts +0 -305
  135. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  136. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  137. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  138. package/types/ai/providers/claude-sdk.d.ts +0 -138
  139. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  140. package/types/ai/providers/claude-subagents.d.ts +0 -18
  141. package/types/ai/providers/codex-app.d.ts +0 -151
  142. package/types/ai/providers/opencode-app.d.ts +0 -96
  143. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  144. package/types/ai/providers/opencode-server.d.ts +0 -20
  145. package/types/ai/runtime/context-windows.d.ts +0 -9
  146. package/types/ai/runtime/fast-mode.d.ts +0 -2
  147. package/types/ai/streaming/codex-events.d.ts +0 -40
  148. package/types/ai/streaming/opencode-events.d.ts +0 -42
@@ -55,12 +55,15 @@ import {
55
55
  buildErrorDetails,
56
56
  buildErrorResult,
57
57
  buildSuccessResult,
58
+ failureKindForPiError,
58
59
  emitCapabilitiesResolved,
59
60
  emitUsageCostEvents,
60
61
  usageFromMessages,
62
+ hasMeasuredUsage,
61
63
  withSubagentUsage,
62
64
  } from "./pi-native/result-builder.js";
63
65
  import {
66
+ captureSessionRecovery,
64
67
  cleanupSessionOnThrow,
65
68
  commitSession,
66
69
  discardUncommittedSession,
@@ -74,18 +77,22 @@ import {
74
77
  runReactiveCompaction,
75
78
  } from "./pi-native/compaction-driver.js";
76
79
  import {
80
+ activateTurnHarness,
77
81
  buildTurnHarness,
78
82
  buildTurnTools,
83
+ createLiveInputPromptEpoch,
79
84
  runHarnessPrompt,
80
85
  startLiveInput,
81
86
  thinkingLevelForEffort,
82
87
  } from "./pi-native/turn-runner.js";
83
88
  import { resolvePiTransport } from "./pi-native/transport.js";
89
+ import { withOpenCodeSessionHeaders } from "./pi-native/provider-attribution.js";
84
90
 
85
91
  /**
86
- * Pi 0.80.6 exposes per-tool execution markers but not AgentHarness's global
87
- * toolExecution option. Resolve mono-agent's programmatic mode once per run;
88
- * individual tool builders then mark stateful/mutating tools sequential.
92
+ * Resolve mono-agent's programmatic mode once per run. Tool builders mark
93
+ * stateful/mutating tools sequential; the Pi 0.85 harness adapter promotes any
94
+ * such marker to its global toolExecution setting because mixed scheduling is
95
+ * no longer available upstream.
89
96
  */
90
97
  export function resolvePiToolExecutionMode(options = {}) {
91
98
  const warnings = [];
@@ -188,23 +195,82 @@ export function createDynamicCredentialStore(apiKeys, resolvePiApiKey, runtimeWa
188
195
  // resolved model. `piResolvedModels` is an advanced/test seam mirroring
189
196
  // `piResolvedModel`: when supplied it is used verbatim (the model dispatched via
190
197
  // `piResolvedModel` may live outside pi's builtin catalog, e.g. a faux model).
191
- function buildRunModels(runtime, options, runtimeWarnings) {
192
- if (options.piResolvedModels) return options.piResolvedModels;
193
- const credentials = createDynamicCredentialStore(runtime.apiKeys, options.resolvePiApiKey, runtimeWarnings);
194
- if (options.customProvider) {
195
- const model = runtime.model;
196
- const models = createModels({ credentials });
197
- models.setProvider(createProvider({
198
- id: model.provider,
199
- name: model.name || model.provider,
200
- baseUrl: model.baseUrl,
201
- auth: { apiKey: envApiKeyAuth(model.name || model.provider, []) },
202
- models: [model],
203
- api: openAICompletionsApi(),
204
- }));
205
- return models;
198
+ function buildRunModels(runtime, options, runtimeWarnings, providerAttributionSessionId) {
199
+ let models;
200
+ if (options.piResolvedModels) {
201
+ models = options.piResolvedModels;
202
+ } else {
203
+ const credentials = createDynamicCredentialStore(runtime.apiKeys, options.resolvePiApiKey, runtimeWarnings);
204
+ if (options.customProvider) {
205
+ const model = runtime.model;
206
+ models = createModels({ credentials });
207
+ models.setProvider(createProvider({
208
+ id: model.provider,
209
+ name: model.name || model.provider,
210
+ baseUrl: model.baseUrl,
211
+ auth: { apiKey: envApiKeyAuth(model.name || model.provider, []) },
212
+ models: [model],
213
+ api: openAICompletionsApi(),
214
+ }));
215
+ } else {
216
+ models = builtinModels({
217
+ credentials,
218
+ ...(options.providerCheckAuthContext === undefined
219
+ ? {}
220
+ : { authContext: options.providerCheckAuthContext }),
221
+ });
222
+ }
206
223
  }
207
- return builtinModels({ credentials });
224
+ return withProviderCheckOutputCap(
225
+ withOpenCodeSessionHeaders(models, providerAttributionSessionId),
226
+ options.providerCheckMaxTokens,
227
+ );
228
+ }
229
+
230
+ const PROVIDER_CHECK_REQUEST_METHODS = new Set([
231
+ "stream",
232
+ "complete",
233
+ "streamSimple",
234
+ "completeSimple",
235
+ "streamDeferred",
236
+ "fetchDeferred",
237
+ "cancelDeferred",
238
+ ]);
239
+
240
+ /**
241
+ * Pi's Agent resolves the transport model through `Models` again, so capping
242
+ * only the model handed to the harness does not constrain the actual provider
243
+ * request. Bind the same cap at the dispatcher boundary used by every request
244
+ * path. This wrapper is activated only for explicit provider checks.
245
+ *
246
+ * @param {import("@earendil-works/pi-ai").Models} models
247
+ * @param {unknown} requestedCap
248
+ * @returns {import("@earendil-works/pi-ai").Models}
249
+ */
250
+ function withProviderCheckOutputCap(models, requestedCap) {
251
+ const cap = Number(requestedCap);
252
+ if (!Number.isSafeInteger(cap) || cap <= 0) return models;
253
+ const wrappers = new Map();
254
+ return /** @type {import("@earendil-works/pi-ai").Models} */ (new Proxy(models, {
255
+ get(target, property) {
256
+ const value = Reflect.get(target, property, target);
257
+ if (typeof property !== "string" || typeof value !== "function") return value;
258
+ if (!PROVIDER_CHECK_REQUEST_METHODS.has(property)) return value.bind(target);
259
+ let wrapper = wrappers.get(property);
260
+ if (wrapper === undefined) {
261
+ wrapper = (model, ...args) => {
262
+ const current = Number(model?.maxTokens);
263
+ const cappedModel = {
264
+ ...model,
265
+ maxTokens: Number.isFinite(current) && current > 0 ? Math.min(current, cap) : cap,
266
+ };
267
+ return value.call(target, cappedModel, ...args);
268
+ };
269
+ wrappers.set(property, wrapper);
270
+ }
271
+ return wrapper;
272
+ },
273
+ }));
208
274
  }
209
275
 
210
276
  // Normalize the incoming runtime messages into AgentMessages the harness can
@@ -258,6 +324,14 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
258
324
  // an intermediate layer flattens the Error to its message.
259
325
  installTransportErrorProbe();
260
326
  const resolved = options.model;
327
+ if (
328
+ typeof resolved?.provider !== "string"
329
+ || resolved.provider.length === 0
330
+ || typeof resolved.model !== "string"
331
+ || resolved.model.length === 0
332
+ ) {
333
+ throw new Error("invalid pi model reference: provider and model are required");
334
+ }
261
335
  const start = Date.now();
262
336
  const events = [];
263
337
  const runtimeWarnings = [];
@@ -303,7 +377,14 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
303
377
  diagnostics: {},
304
378
  },
305
379
  session: null,
380
+ // Fresh stateless calls own a private repo, even when attribution matches a
381
+ // live primary session. Cleanup must never delete that primary's record.
382
+ ephemeralSessionRepo: null,
306
383
  sessionEntry: null,
384
+ // Fresh keep-alive sessions are registered busy until the harness closes;
385
+ // this prevents another turn from reopening the same repo record while its
386
+ // first handle is still live.
387
+ registeredSessionEntry: null,
307
388
  // True when a requestedSessionId had no live entry AND no durable transcript,
308
389
  // so a fresh durable session was created under that id (cross-restart resume,
309
390
  // first turn for the conversation). Distinct from a true resume (sessionEntry
@@ -322,12 +403,17 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
322
403
  // (host/runtime-side throws after the session mutated) can roll back too, not
323
404
  // just the success path.
324
405
  baselineLeafId: null,
406
+ recoveryOperationId: undefined,
407
+ recoveryInputIds: null,
408
+ retainRecoveryTail: false,
325
409
  };
326
410
 
327
411
  const providerSessionId = options.sessionId
328
412
  || options.providerSessionId
413
+ || options.providerAttributionSessionId
329
414
  || options.runId
330
415
  || randomUUID();
416
+ const providerAttributionSessionId = options.providerAttributionSessionId || providerSessionId;
331
417
  // Prefer the explicit sessionId, but fall back to providerSessionId so a caller
332
418
  // that only supplies providerSessionId still resumes the prior session instead
333
419
  // of being treated as a fresh run (which would drop prior context).
@@ -352,13 +438,17 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
352
438
  }
353
439
  emitCaptured(events, options.onEvent, event);
354
440
  };
441
+ // Exec and Monitor both run a command a host configured Bash's risk tier for.
442
+ // Leaving either unmapped would let the same shell command take a lower
443
+ // approval tier simply by being started through a different tool.
444
+ const bashRiskTier = options.toolRiskTiers?.Bash;
355
445
  const approvalRiskTiers = {
356
446
  ...(options.toolRiskTiers || {}),
357
- ...(
358
- options.toolRiskTiers?.Exec === undefined && options.toolRiskTiers?.Bash !== undefined
359
- ? { Exec: options.toolRiskTiers.Bash }
360
- : {}
361
- ),
447
+ ...(bashRiskTier !== undefined
448
+ ? Object.fromEntries(["Exec", "Monitor"]
449
+ .filter((name) => options.toolRiskTiers?.[name] === undefined)
450
+ .map((name) => [name, bashRiskTier]))
451
+ : {}),
362
452
  };
363
453
  const approvalManager = options.onToolApprovalRequest
364
454
  ? createApprovalManager({
@@ -405,7 +495,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
405
495
  // ready pi-ai Model (e.g. a registered faux provider model) plus optional
406
496
  // capabilities, bypassing the static model-registry lookup. Production
407
497
  // callers leave it undefined and resolve through pi-ai's registry.
408
- const runtime = options.piResolvedModel
498
+ let runtime = options.piResolvedModel
409
499
  ? {
410
500
  model: options.piResolvedModel,
411
501
  capabilities: options.piResolvedCapabilities || {
@@ -420,10 +510,18 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
420
510
  apiKeys: new Map(),
421
511
  }
422
512
  : resolvePiRuntimeModel(resolved, options);
513
+ if (Number.isSafeInteger(options.providerCheckMaxTokens) && options.providerCheckMaxTokens > 0) {
514
+ runtime = {
515
+ ...runtime,
516
+ model: {
517
+ ...runtime.model,
518
+ maxTokens: Math.min(runtime.model.maxTokens, options.providerCheckMaxTokens),
519
+ },
520
+ };
521
+ }
423
522
  const capabilities = runtime.capabilities || {};
424
523
  const effectiveThinkingLevel = thinkingLevelForEffort(options.effort || "medium", capabilities);
425
- const reference = resolved.reference
426
- || (resolved.sdk === "pi" ? `pi:${resolved.provider}:${resolved.model}` : `${resolved.sdk}:${resolved.model}`);
524
+ const reference = resolved.reference || `${resolved.provider}:${resolved.model}`;
427
525
 
428
526
  // Resolve the typed `toolLimits` / `compaction` policy objects against the
429
527
  // deprecated `settings` fallback (per-group precedence: a present typed
@@ -486,15 +584,17 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
486
584
  ? Number(options.maxRetryDelayMs)
487
585
  : 60_000;
488
586
  // Steering controls user follow-up delivery, not tool scheduling. Keep it
489
- // independent from piToolExecutionMode; tools carry their own executionMode.
587
+ // independent from piToolExecutionMode; the adapter derives Pi 0.85's
588
+ // global scheduling mode from the tools' executionMode markers.
490
589
  const toolSteeringMode = "one-at-a-time";
491
590
 
492
- const piModels = buildRunModels(runtime, options, runtimeWarnings);
591
+ const piModels = buildRunModels(runtime, options, runtimeWarnings, providerAttributionSessionId);
493
592
 
494
- // Construct the harness, subscribe the stream normalizer, and wire the
495
- // external abort handler (which sets runState.externalAbort and aborts the
496
- // harness). Sets runState.harness + runState.removeAbortHandler.
497
- harness = buildTurnHarness(runState, {
593
+ // Construct the harness and recover any interrupted durable operation.
594
+ // Stream subscription and the external abort handler are activated only
595
+ // after prior transcript seeding, so manual append lifecycle events cannot
596
+ // surface as output from this run.
597
+ harness = await buildTurnHarness(runState, {
498
598
  session: runState.session,
499
599
  piModels,
500
600
  model: runtime.model,
@@ -506,11 +606,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
506
606
  maxRetries,
507
607
  maxRetryDelayMs,
508
608
  steeringMode: toolSteeringMode,
509
- onEvent,
510
609
  options,
511
- toolLimits,
512
- sdk: resolved.sdk,
513
- reference,
514
610
  });
515
611
 
516
612
  // Seed prior transcript (everything before the trailing user turn) into the
@@ -536,10 +632,14 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
536
632
  try { runState.baselineLeafId = await runState.session.getLeafId(); } catch { /* best-effort */ }
537
633
  }
538
634
 
539
- // Live steering: consume follow-up messages and steer the harness mid-run.
540
- // The consumer is tied to run completion so it stops steering once the run
541
- // finishes and does not swallow messages meant for a later turn.
542
- const liveInput = startLiveInput({ harness, options, onEvent });
635
+ activateTurnHarness(runState, {
636
+ harness,
637
+ onEvent,
638
+ options,
639
+ toolLimits,
640
+ sdk: "pi",
641
+ reference,
642
+ });
543
643
 
544
644
  // Re-check abort right before issuing the provider request. The abort
545
645
  // handler is only installed at ~:639, AFTER a long stretch of awaited setup
@@ -582,19 +682,53 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
582
682
  runtimeWarnings,
583
683
  });
584
684
 
685
+ // Arm the main-prompt epoch after proactive compaction so compaction and
686
+ // transcript seeding cannot be mistaken for live-input consumption.
687
+ runState.recoveryBaselineTipId = await runState.session.getLeafId();
688
+ const liveInputEpoch = createLiveInputPromptEpoch({ harness, onEvent });
689
+ const liveInput = startLiveInput({ harness, options, onEvent, promptEpoch: liveInputEpoch });
690
+
691
+ // Report the live harness value, not a downstream recreation of Pi's
692
+ // thinking-level normalization. This also captures any future adapter-side
693
+ // adjustment between construction and the provider request.
694
+ const providerEffectiveEffort = harness.getThinkingLevel();
695
+ onEvent({
696
+ type: "provider_execution_config",
697
+ sdk: "pi",
698
+ model: reference,
699
+ ...(options.effort ? { effort: options.effort } : {}),
700
+ effectiveEffort: providerEffectiveEffort,
701
+ timestamp: Date.now(),
702
+ });
703
+
585
704
  onEvent({
586
705
  type: "provider_request_started",
587
- sdk: resolved.sdk,
706
+ sdk: "pi",
588
707
  model: reference,
589
708
  runtime: "pi",
590
709
  timestamp: Date.now(),
591
710
  });
592
711
 
593
- let { runError } = await runHarnessPrompt(harness, promptText, promptImages);
594
-
595
- // The run is done: stop the live-steering consumer so it cannot steer a
596
- // finished harness or swallow a follow-up meant for the next turn.
597
- await liveInput.stop();
712
+ let runError;
713
+ try {
714
+ // Own the operation at admission so steers consumed mid-run settle as
715
+ // applied immediately; finish() re-checks the settled id at the end.
716
+ const promptResult = await runHarnessPrompt(harness, promptText, promptImages, {
717
+ onOperationAdmitted: (operationId) => {
718
+ runState.recoveryOperationId = operationId;
719
+ liveInputEpoch.confirm(operationId);
720
+ },
721
+ });
722
+ runError = promptResult.runError;
723
+ liveInputEpoch.finish(promptResult.operationId);
724
+ } finally {
725
+ // Stop joins unresolved native enqueue and reconciles every returned
726
+ // entry before the exact-operation subscription is removed.
727
+ try { await liveInput.stop(); } finally {
728
+ runState.recoveryInputIds = liveInputEpoch.consumedInputIds();
729
+ liveInputEpoch.close();
730
+ }
731
+ }
598
732
 
599
733
  runState.externalAbort ||= !!options.abortSignal?.aborted;
600
734
 
@@ -765,7 +899,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
765
899
  // runtime.compaction.enabled. See docs/reference/feature-registry.md runtime.context-compaction.
766
900
  contextCompactionApplied: runState.compaction.policy?.enabled ? runState.compaction.applied : null,
767
901
  });
768
- emitCapabilitiesResolved(onEvent, { sdk: resolved.sdk, model: reference, capabilitiesUsed });
902
+ emitCapabilitiesResolved(onEvent, { sdk: "pi", model: reference, capabilitiesUsed });
769
903
 
770
904
  // Re-check the abort signal: a cancel can land during the post-run work above
771
905
  // (live-input teardown, structured-output finalization retry) after the line
@@ -780,6 +914,12 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
780
914
  // tracks LIVENESS so disposeProviderSession / idle-TTL eviction can reach
781
915
  // native sessions, keep-alive registers the session, and a failed/aborted
782
916
  // resumed turn rolls back to its pre-turn leaf.
917
+ runState.retainRecoveryTail = !!durableRepo && options.sessionKeepAlive === true
918
+ && !runState.maxTurnsHit
919
+ && (!errorMessage || failureKindForPiError(errorMessage, diagnostics) === "provider_unavailable")
920
+ && typeof options.sessionRecovery?.runId === "string" && options.sessionRecovery.runId.length > 0
921
+ && Number.isSafeInteger(options.sessionRecovery?.revision) && options.sessionRecovery.revision >= 0
922
+ && typeof runState.recoveryOperationId === "string";
783
923
  await commitSession(runState, {
784
924
  options,
785
925
  requestedSessionId,
@@ -791,21 +931,21 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
791
931
  onEvent,
792
932
  });
793
933
 
794
- // Final abort guard (durable cancel TOCTOU): if a cancel raced the lifecycle
795
- // commit above landing AFTER the keep-alive/!externalAbort decision but
796
- // before this return the cancelled turn is still in the durable transcript
797
- // and (for keep-alive) the live registry. Roll it back so the next resume sees
798
- // the pre-turn state (rollbackAbortedTurn: a resumed session moves to its
799
- // baseline leaf and drops its live entry; a fresh durable session deletes its
800
- // jsonl). The abort re-check + return stay inline with NO await between the
801
- // false-branch check and the return, so an external cancel cannot newly fire
802
- // past it (I10).
934
+ // Uncoordinated calls keep their final rollback guard. Coordinated calls
935
+ // retain the tail and close it before granting a recovery receipt; recheck
936
+ // cancellation after that await as well.
803
937
  if (!runState.externalAbort && options.abortSignal?.aborted) {
804
938
  runState.externalAbort = true;
805
- await rollbackAbortedTurn(runState, { requestedSessionId, providerSessionId, durableRepo });
939
+ if (!runState.retainRecoveryTail) await rollbackAbortedTurn(runState, { requestedSessionId, providerSessionId, durableRepo });
806
940
  }
807
941
 
808
- return buildSuccessResult({
942
+ // A receipt is proof of a successfully closed native operation, never a
943
+ // promise that the finally block will eventually close it.
944
+ const providerSessionRecovery = runState.retainRecoveryTail
945
+ ? await captureSessionRecovery(runState, { options, providerSessionId, modelKey: `${resolved.provider}:${resolved.model}`, model: runtime.model, pending: !!errorMessage || runState.externalAbort })
946
+ : undefined;
947
+ if (runState.retainRecoveryTail) runState.externalAbort ||= !!options.abortSignal?.aborted;
948
+ return { ...buildSuccessResult({
809
949
  finalText,
810
950
  finalThinking,
811
951
  events,
@@ -824,8 +964,10 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
824
964
  providerSessionId,
825
965
  runtimeWarnings,
826
966
  capabilitiesUsed,
967
+ usageMeasured: hasMeasuredUsage(runTranscript),
827
968
  structuredResult: runState.structuredResult,
828
- });
969
+ effectiveEffort: providerEffectiveEffort,
970
+ }), ...(providerSessionRecovery ? { providerSessionRecovery } : {}) };
829
971
  } catch (err) {
830
972
  runState.externalAbort ||= !!options.abortSignal?.aborted;
831
973
  // Drop a just-created fresh durable session, release a create-on-miss
@@ -859,9 +1001,12 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
859
1001
  runtimeWarnings,
860
1002
  isRetryable,
861
1003
  piTransport,
1004
+ effectiveEffort: harness?.getThinkingLevel?.(),
862
1005
  });
863
1006
  } finally {
1007
+ try { await harness?.close?.(); } catch { /* best-effort */ }
864
1008
  if (runState.sessionEntry) runState.sessionEntry.busy = false;
1009
+ if (runState.registeredSessionEntry) runState.registeredSessionEntry.busy = false;
865
1010
  runState.removeAbortHandler?.();
866
1011
  try {
867
1012
  await closeRunTools();
@@ -875,6 +1020,6 @@ export const piNativeRuntimeBridge = {
875
1020
  id: "pi",
876
1021
  kind: "pi",
877
1022
  capabilities: runtimeCapabilities("pi"),
878
- supports: (ref) => ref?.sdk === "pi",
1023
+ supports: (ref) => Boolean(ref?.provider && ref?.model),
879
1024
  execute: generatePiNativeResponse,
880
1025
  };
@@ -1,7 +1,4 @@
1
- import {
2
- TOOL_POLICY_ALLOW_ALL_ONLY,
3
- TOOL_POLICY_PROJECTED,
4
- } from "./tool-policy.js";
1
+ import { TOOL_POLICY_PROJECTED } from "./tool-policy.js";
5
2
 
6
3
  export const COMMON_CAPABILITIES = {
7
4
  streaming: true,
@@ -15,18 +12,9 @@ export const COMMON_CAPABILITIES = {
15
12
  supports_live_input: true,
16
13
  supports_native_subagents: true,
17
14
  supports_request_tool_environment: false,
18
- supports_fast_mode: false,
19
- tool_policy: TOOL_POLICY_PROJECTED,
20
15
  };
21
16
 
22
17
  export const RUNTIME_CAPABILITIES = {
23
- claude: {
24
- runtime: "sdk",
25
- ...COMMON_CAPABILITIES,
26
- // The Claude Agent SDK persists sessions on disk; the bridge resumes
27
- // them via queryOptions.resume when options.sessionId is supplied.
28
- supports_session_resume: true,
29
- },
30
18
  pi: {
31
19
  runtime: "pi-agent",
32
20
  ...COMMON_CAPABILITIES,
@@ -38,50 +26,17 @@ export const RUNTIME_CAPABILITIES = {
38
26
  supports_native_subagents: false,
39
27
  supports_request_tool_environment: true,
40
28
  supports_mcp_apps: true,
41
- },
42
- codex: {
43
- runtime: "cli",
44
- ...COMMON_CAPABILITIES,
45
- // The codex-app bridge keeps the app-server subprocess + thread alive
46
- // when options.sessionKeepAlive is set; resumed turns reuse the thread.
47
- supports_session_resume: true,
48
- supports_fast_mode: true,
49
- tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
50
- },
51
- opencode: {
52
- runtime: "cli",
53
- ...COMMON_CAPABILITIES,
54
- structured_output: false,
55
- supports_session_resume: false,
56
- supports_mcp: false,
57
- supports_skills: false,
58
- supports_live_input: false,
59
- supports_native_subagents: false,
60
- tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
61
- },
62
- acp: {
63
- runtime: "acp-stdio",
64
- ...COMMON_CAPABILITIES,
65
- structured_output: false,
66
- supports_session_resume: true,
67
- // Runtime request-scoped MCP servers are not projected into ACP sessions.
68
- // Profiles may own static ACP MCP configuration, but that is not the
69
- // supports_mcp contract advertised to the route capability gate.
70
- supports_mcp: false,
71
- supports_skills: false,
72
- supports_builtin_tools: false,
73
- supports_live_input: false,
74
- supports_native_subagents: false,
75
- supports_request_tool_environment: false,
76
- tool_policy: TOOL_POLICY_ALLOW_ALL_ONLY,
29
+ tool_policy: TOOL_POLICY_PROJECTED,
77
30
  },
78
31
  };
79
32
 
80
- export function runtimeCapabilities(sdkOrModel) {
81
- if (!sdkOrModel) throw new Error("runtimeCapabilities requires a model reference or sdk kind");
82
- const sdk = typeof sdkOrModel === "string" ? sdkOrModel : sdkOrModel?.sdk;
83
- if (!sdk) throw new Error("runtimeCapabilities: unrecognized argument");
84
- const caps = RUNTIME_CAPABILITIES[sdk];
85
- if (!caps) throw new Error(`unknown provider sdk: ${sdk}`);
86
- return { kind: sdk, ...caps };
33
+ /**
34
+ * @param {*} [_legacySelection]
35
+ * @returns {{kind: "pi"} & typeof RUNTIME_CAPABILITIES.pi}
36
+ */
37
+ export function runtimeCapabilities(_legacySelection) {
38
+ // Capability lookup no longer selects a backend. The ignored optional value
39
+ // keeps the still-separate router migration type-safe without allowing its
40
+ // legacy route ids to influence the sole surviving runtime contract.
41
+ return { kind: "pi", ...RUNTIME_CAPABILITIES.pi };
87
42
  }