@mono-agent/agent-runtime 0.20.14 → 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 (82) hide show
  1. package/ARCHITECTURE.md +50 -11
  2. package/MIGRATION.md +30 -7
  3. package/README.md +219 -35
  4. package/package.json +9 -4
  5. package/src/agent/tool-bloat.js +145 -9
  6. package/src/agent/tools/agent-tool.js +104 -5
  7. package/src/agent/tools/bash.js +10 -2
  8. package/src/agent/tools/codex-subscription-search.js +122 -28
  9. package/src/agent/tools/exec.js +10 -2
  10. package/src/agent/tools/monitor.js +11 -2
  11. package/src/agent/tools/pi-bridge.js +33 -14
  12. package/src/agent/tools/shared/monitors.js +22 -3
  13. package/src/agent/tools/shared/path-resolver.js +25 -6
  14. package/src/agent/tools/shared/process-jobs.js +6 -1
  15. package/src/agent/tools/shared/process-runner.js +3 -1
  16. package/src/agent/tools/shared/tool-context.js +8 -0
  17. package/src/agent/tools/web-access-interstitial.js +70 -0
  18. package/src/agent/tools/web-browser-render.js +83 -58
  19. package/src/agent/tools/web-controller.js +112 -21
  20. package/src/agent/tools/web-document-extractor.js +379 -0
  21. package/src/agent/tools/web-fetch.js +271 -243
  22. package/src/agent/tools/web-request.js +65 -0
  23. package/src/agent/tools/web-search-output.js +165 -0
  24. package/src/agent/tools/web-search-state.js +75 -0
  25. package/src/agent/tools/web-search.js +532 -71
  26. package/src/ai/failure.js +3 -3
  27. package/src/ai/index.js +1 -0
  28. package/src/ai/observer.js +8 -0
  29. package/src/ai/pi-interop.js +156 -0
  30. package/src/ai/provider-check.js +131 -0
  31. package/src/ai/providers/pi-native/compaction-driver.js +45 -21
  32. package/src/ai/providers/pi-native/compaction-summary.js +140 -0
  33. package/src/ai/providers/pi-native/harness-adapter.js +40 -2
  34. package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
  35. package/src/ai/providers/pi-native/provider-attribution.js +102 -0
  36. package/src/ai/providers/pi-native/result-builder.js +28 -4
  37. package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
  38. package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
  39. package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
  40. package/src/ai/providers/pi-native/turn-runner.js +245 -13
  41. package/src/ai/providers/pi-native.js +159 -40
  42. package/src/ai/runtime/live-input-events.js +250 -54
  43. package/src/ai/runtime/router.js +30 -11
  44. package/src/ai/tool-lifecycle.js +32 -18
  45. package/src/ai/types.js +26 -5
  46. package/src/runtime.js +24 -5
  47. package/types/agent/tool-bloat.d.ts +1 -1
  48. package/types/agent/tools/agent-tool.d.ts +4 -1
  49. package/types/agent/tools/bash.d.ts +5 -3
  50. package/types/agent/tools/codex-subscription-search.d.ts +6 -2
  51. package/types/agent/tools/exec.d.ts +5 -3
  52. package/types/agent/tools/monitor.d.ts +5 -2
  53. package/types/agent/tools/pi-bridge.d.ts +6 -4
  54. package/types/agent/tools/shared/monitors.d.ts +17 -2
  55. package/types/agent/tools/shared/process-jobs.d.ts +5 -1
  56. package/types/agent/tools/shared/process-runner.d.ts +3 -2
  57. package/types/agent/tools/shared/tool-context.d.ts +2 -0
  58. package/types/agent/tools/web-access-interstitial.d.ts +23 -0
  59. package/types/agent/tools/web-browser-render.d.ts +4 -1
  60. package/types/agent/tools/web-controller.d.ts +4 -2
  61. package/types/agent/tools/web-document-extractor.d.ts +27 -0
  62. package/types/agent/tools/web-fetch.d.ts +19 -24
  63. package/types/agent/tools/web-request.d.ts +20 -0
  64. package/types/agent/tools/web-search-output.d.ts +31 -0
  65. package/types/agent/tools/web-search-state.d.ts +21 -0
  66. package/types/agent/tools/web-search.d.ts +10 -45
  67. package/types/ai/index.d.ts +1 -0
  68. package/types/ai/observer.d.ts +6 -0
  69. package/types/ai/pi-interop.d.ts +61 -0
  70. package/types/ai/provider-check.d.ts +53 -0
  71. package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
  72. package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
  73. package/types/ai/providers/pi-native/harness-adapter.d.ts +3 -1
  74. package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
  75. package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
  76. package/types/ai/providers/pi-native/result-builder.d.ts +11 -1
  77. package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
  78. package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
  79. package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
  80. package/types/ai/runtime/live-input-events.d.ts +32 -8
  81. package/types/ai/tool-lifecycle.d.ts +4 -3
  82. package/types/ai/types.d.ts +140 -12
@@ -78,6 +78,8 @@ export async function buildTurnTools(runState, {
78
78
  ? null
79
79
  : createWebToolController({
80
80
  searchConfig: options.webSearchConfig,
81
+ searchState: options.webSearchState,
82
+ coordinator: options.webRequestCoordinator,
81
83
  fetchConfig: options.webFetchConfig,
82
84
  sandboxPolicy: options.sandboxPolicy,
83
85
  sandboxEngine,
@@ -121,6 +123,7 @@ export async function buildTurnTools(runState, {
121
123
  nodeReplController,
122
124
  webController,
123
125
  processJobsController: options.processJobs,
126
+ processJobsAvailability: options.processJobsAvailability,
124
127
  monitorsController: options.monitors,
125
128
  toolExecutionMode,
126
129
  subagents: options.subagents,
@@ -143,6 +146,9 @@ export async function buildTurnTools(runState, {
143
146
  skills: options.skills,
144
147
  skillsRoot: options.skillsRoot,
145
148
  toolEnvironment: options.toolEnvironment,
149
+ webSearchConfig: options.webSearchConfig,
150
+ webRequestCoordinator: options.webRequestCoordinator,
151
+ webFetchConfig: options.webFetchConfig,
146
152
  },
147
153
  ctx: runCtx,
148
154
  }));
@@ -163,6 +169,7 @@ export async function buildTurnTools(runState, {
163
169
  qaOutputDir,
164
170
  onTruncate,
165
171
  limits: toolLimits,
172
+ mcpCallNoTotalTimeoutTools: options.mcpCallNoTotalTimeoutTools,
166
173
  toolPayloadMaxBytes: toolLimits.toolPayloadMaxBytes,
167
174
  sandboxPolicy: options.sandboxPolicy,
168
175
  sandboxEngine,
@@ -275,6 +282,8 @@ export async function buildTurnHarness(runState, {
275
282
  streamOptions: { transport, maxRetries, maxRetryDelayMs },
276
283
  steeringMode,
277
284
  followUpMode: steeringMode,
285
+ promptCacheDiagnostics: options.promptCacheDiagnostics,
286
+ onEvent: options.onEvent,
278
287
  });
279
288
  // MCP `CallToolResult.isError` is a successful protocol response, so pi's
280
289
  // execute() promise resolves. The bridge records that bit in result details;
@@ -343,10 +352,10 @@ export function activateTurnHarness(runState, {
343
352
  * the harness mid-run; the consumer is tied to run completion (an internal
344
353
  * runComplete flag) so it stops steering once the run finishes and does not
345
354
  * swallow a follow-up meant for a later turn. Returns a `stop()` teardown.
346
- * @param {{harness: any, options: any, onEvent: (event: any) => void}} deps
355
+ * @param {{harness: any, options: any, onEvent: (event: any) => void, promptEpoch?: any}} deps
347
356
  * @returns {{stop: () => Promise<void>}}
348
357
  */
349
- export function startLiveInput({ harness, options, onEvent }) {
358
+ export function startLiveInput({ harness, options, onEvent, promptEpoch }) {
350
359
  if (!options.liveInput) return { stop: async () => {} };
351
360
  const iterator = typeof options.liveInput[Symbol.asyncIterator] === "function"
352
361
  ? options.liveInput[Symbol.asyncIterator]()
@@ -355,6 +364,8 @@ export function startLiveInput({ harness, options, onEvent }) {
355
364
  /** @type {() => void} */
356
365
  let signalStop = () => {};
357
366
  const stopped = new Promise((resolve) => { signalStop = () => resolve(); });
367
+ /** @type {Array<{entryId: string, message: any}>} */
368
+ const acceptedEntries = [];
358
369
  const task = (async () => {
359
370
  try {
360
371
  while (!runComplete && !options.abortSignal?.aborted) {
@@ -364,8 +375,19 @@ export function startLiveInput({ harness, options, onEvent }) {
364
375
  ]);
365
376
  if (next.done || runComplete || options.abortSignal?.aborted) break;
366
377
  try {
367
- await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
368
- next.value.acknowledge?.();
378
+ const entryId = await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
379
+ if (typeof entryId !== "string" || entryId.length === 0) {
380
+ next.value.accepted?.();
381
+ next.value.uncertain?.({ reason: "delivery_uncertain" });
382
+ continue;
383
+ }
384
+ const evidence = {
385
+ providerEntryId: entryId,
386
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
387
+ };
388
+ acceptedEntries.push({ entryId, message: next.value });
389
+ next.value.accepted?.(evidence);
390
+ promptEpoch?.register(entryId, next.value);
369
391
  } catch (err) {
370
392
  next.value.reject?.(err);
371
393
  throw err;
@@ -379,6 +401,8 @@ export function startLiveInput({ harness, options, onEvent }) {
379
401
  });
380
402
  }
381
403
  })();
404
+ /** @type {Promise<void>|undefined} */
405
+ let stopPromise;
382
406
  return {
383
407
  // The run is done: stop the live-steering consumer so it cannot steer a
384
408
  // finished harness or swallow a follow-up meant for the next turn. We signal
@@ -386,15 +410,214 @@ export function startLiveInput({ harness, options, onEvent }) {
386
410
  // race releases the task even when a third-party iterator's return() does
387
411
  // not unblock its pending next(); awaiting the task still closes any steer
388
412
  // acknowledgement already in progress.
389
- stop: async () => {
413
+ stop: () => {
414
+ stopPromise ??= performStop();
415
+ return stopPromise;
416
+ },
417
+ };
418
+
419
+ async function performStop() {
390
420
  runComplete = true;
391
421
  signalStop();
392
422
  if (iterator && typeof iterator.return === "function") {
393
423
  try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
394
424
  }
395
425
  await task;
426
+ for (const accepted of acceptedEntries) {
427
+ if (promptEpoch?.isConsumed(accepted.entryId)) continue;
428
+ try {
429
+ if (typeof harness.cancelQueued !== "function") {
430
+ accepted.message.uncertain?.({
431
+ reason: "delivery_uncertain",
432
+ providerEntryId: accepted.entryId,
433
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
434
+ });
435
+ continue;
436
+ }
437
+ const cancellation = await harness.cancelQueued(accepted.entryId);
438
+ if (cancellation?.kind === "cancelled") {
439
+ accepted.message.reject?.({ code: "native_queue_removed" });
440
+ } else if (!promptEpoch?.isConsumed(accepted.entryId)) {
441
+ accepted.message.uncertain?.({
442
+ reason: "delivery_uncertain",
443
+ providerEntryId: accepted.entryId,
444
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
445
+ });
446
+ }
447
+ } catch {
448
+ accepted.message.uncertain?.({
449
+ reason: "delivery_uncertain",
450
+ providerEntryId: accepted.entryId,
451
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
452
+ });
453
+ onEvent({
454
+ type: "runtime_warning",
455
+ warning_kind: "live_input_cancellation_failed",
456
+ message: "Unable to prove whether queued live input was removed.",
457
+ });
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Own exact Pi run/entry correlation for the one main prompt in this Mono run.
465
+ * @param {{harness: any, onEvent: (event: any) => void}} deps
466
+ */
467
+ export function createLiveInputPromptEpoch({ harness, onEvent }) {
468
+ const BUFFER_LIMIT = 101;
469
+ /** @type {string|undefined} */
470
+ let runId;
471
+ let eventWindowClosed = false;
472
+ let invalid = false;
473
+ /** @type {Array<{entryId: string, runId: string}>} */
474
+ const buffered = [];
475
+ /** @type {Map<string, {message: any, observed: boolean, consumed: boolean}>} */
476
+ const entries = new Map();
477
+ // The operation id Pi admitted for the main prompt (via confirm) or that the
478
+ // settled prompt reported (via finish). Consumption is only acknowledged once
479
+ // the observed main-lane run_start carries this exact id.
480
+ /** @type {string|undefined} */
481
+ let admittedOperationId;
482
+ let operationConfirmed = false;
483
+
484
+ const remove = harness.subscribe((event) => {
485
+ if (!event || event.lane !== "main") return;
486
+ if (event.type === "run_start" && typeof event.runId === "string" && event.runId.length > 0) {
487
+ if (runId === undefined) {
488
+ runId = event.runId;
489
+ if (admittedOperationId !== undefined && admittedOperationId !== runId) {
490
+ invalidate("operation_mismatch");
491
+ return;
492
+ }
493
+ if (admittedOperationId !== undefined) operationConfirmed = true;
494
+ consumeBuffered();
495
+ } else if (runId !== event.runId) {
496
+ invalidate("multiple_run_start");
497
+ }
498
+ return;
499
+ }
500
+ if (event.type === "run_end" && event.runId === runId) {
501
+ eventWindowClosed = true;
502
+ return;
503
+ }
504
+ if (
505
+ event.type !== "message_end"
506
+ || eventWindowClosed
507
+ || event.message?.role !== "user"
508
+ || typeof event.entryId !== "string"
509
+ || event.entryId.length === 0
510
+ || typeof event.runId !== "string"
511
+ || event.runId.length === 0
512
+ ) return;
513
+ if (buffered.some((item) => item.entryId === event.entryId && item.runId === event.runId)) {
514
+ consumeBuffered();
515
+ return;
516
+ }
517
+ if (buffered.length >= BUFFER_LIMIT) {
518
+ invalidate("event_buffer_overflow");
519
+ return;
520
+ }
521
+ buffered.push({ entryId: event.entryId, runId: event.runId });
522
+ consumeBuffered();
523
+ });
524
+
525
+ return {
526
+ ownedRunId: () => runId,
527
+ consumedInputIds: () => invalid ? null : [...entries.values()].filter((entry) => entry.consumed).map((entry) => entry.message.id),
528
+ register(entryId, message) {
529
+ if (!entries.has(entryId)) entries.set(entryId, { message, observed: false, consumed: false });
530
+ if (invalid) {
531
+ settleUncertain(entries.get(entryId), entryId);
532
+ return;
533
+ }
534
+ consumeBuffered();
535
+ },
536
+ isConsumed: (entryId) => entries.get(entryId)?.consumed === true,
537
+ /**
538
+ * Own the admitted operation as soon as Pi reports its id, before the run
539
+ * settles, so entries consumed mid-run are acknowledged when their
540
+ * message_end arrives rather than in one batch at the end of the run.
541
+ * Safe in either order with run_start; a conflicting id invalidates.
542
+ */
543
+ confirm(operationId) {
544
+ if (invalid) return;
545
+ if (
546
+ typeof operationId !== "string"
547
+ || operationId.length === 0
548
+ || (admittedOperationId !== undefined && admittedOperationId !== operationId)
549
+ || (runId !== undefined && runId !== operationId)
550
+ ) {
551
+ invalidate("operation_mismatch");
552
+ return;
553
+ }
554
+ admittedOperationId = operationId;
555
+ if (runId === undefined) return;
556
+ operationConfirmed = true;
557
+ confirmObserved();
558
+ },
559
+ finish(operationId) {
560
+ if (
561
+ typeof operationId !== "string"
562
+ || operationId.length === 0
563
+ || runId === undefined
564
+ || operationId !== runId
565
+ || (admittedOperationId !== undefined && admittedOperationId !== operationId)
566
+ ) {
567
+ invalidate("operation_mismatch");
568
+ return;
569
+ }
570
+ admittedOperationId = operationId;
571
+ operationConfirmed = true;
572
+ confirmObserved();
573
+ },
574
+ close() {
575
+ remove?.();
396
576
  },
397
577
  };
578
+
579
+ function consumeBuffered() {
580
+ if (invalid || runId === undefined) return;
581
+ for (const evidence of buffered) {
582
+ if (evidence.runId !== runId) continue;
583
+ const entry = entries.get(evidence.entryId);
584
+ if (entry === undefined || entry.consumed) continue;
585
+ entry.observed = true;
586
+ }
587
+ if (operationConfirmed) confirmObserved();
588
+ }
589
+
590
+ function confirmObserved() {
591
+ if (!operationConfirmed || runId === undefined || invalid) return;
592
+ for (const [entryId, entry] of entries) {
593
+ if (!entry.observed || entry.consumed) continue;
594
+ entry.consumed = true;
595
+ entry.message.acknowledge?.({ providerEntryId: entryId, providerRunId: runId });
596
+ }
597
+ }
598
+
599
+ function invalidate(reason) {
600
+ if (invalid) return;
601
+ invalid = true;
602
+ try {
603
+ onEvent({
604
+ type: "runtime_warning",
605
+ warning_kind: "live_input_correlation_invalid",
606
+ message: "Live-input consumption could not be correlated to exactly one provider operation.",
607
+ reason,
608
+ });
609
+ } catch { /* diagnostics do not alter settlement */ }
610
+ for (const [entryId, entry] of entries) settleUncertain(entry, entryId);
611
+ }
612
+
613
+ function settleUncertain(entry, entryId) {
614
+ if (entry === undefined || entry.consumed) return;
615
+ entry.message.uncertain?.({
616
+ reason: "delivery_uncertain",
617
+ providerEntryId: entryId,
618
+ ...(runId === undefined ? {} : { providerRunId: runId }),
619
+ });
620
+ }
398
621
  }
399
622
 
400
623
  /**
@@ -404,23 +627,32 @@ export function startLiveInput({ harness, options, onEvent }) {
404
627
  * @param {any} harness
405
628
  * @param {string} promptText
406
629
  * @param {Array<any>} promptImages
407
- * @returns {Promise<{runError: any}>}
630
+ * @param {{onOperationAdmitted?: (operationId: string) => void}} [hooks]
631
+ * `onOperationAdmitted` fires as soon as Pi admits the run, before any
632
+ * provider request, so the live-input epoch can own the operation up front.
633
+ * @returns {Promise<{runError: any, operationId?: string}>}
408
634
  */
409
- export async function runHarnessPrompt(harness, promptText, promptImages) {
635
+ export async function runHarnessPrompt(harness, promptText, promptImages, hooks) {
410
636
  let runError = null;
637
+ let operationId;
411
638
  try {
412
639
  // Pass structured images (when present) so multimodal input reaches the
413
640
  // model as image blocks rather than stringified text. AgentHarness.prompt
414
641
  // takes them under an options object (`{ images }`); a bare array would be
415
642
  // read as `options` and silently dropped (options?.images === undefined).
416
- if (Array.isArray(promptImages) && promptImages.length > 0) {
417
- await harness.prompt(promptText, { images: promptImages });
418
- } else {
419
- await harness.prompt(promptText);
420
- }
643
+ const promptOptions = {
644
+ ...(Array.isArray(promptImages) && promptImages.length > 0 ? { images: promptImages } : {}),
645
+ ...(typeof hooks?.onOperationAdmitted === "function"
646
+ ? { onOperationAdmitted: hooks.onOperationAdmitted }
647
+ : {}),
648
+ };
649
+ const result = Object.keys(promptOptions).length > 0
650
+ ? await harness.prompt(promptText, promptOptions)
651
+ : await harness.prompt(promptText);
652
+ operationId = result?.operationId;
421
653
  } catch (err) {
422
654
  runError = err;
423
655
  }
424
656
  await harness.waitForIdle();
425
- return { runError };
657
+ return { runError, ...(operationId === undefined ? {} : { operationId }) };
426
658
  }
@@ -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,
@@ -77,11 +80,13 @@ import {
77
80
  activateTurnHarness,
78
81
  buildTurnHarness,
79
82
  buildTurnTools,
83
+ createLiveInputPromptEpoch,
80
84
  runHarnessPrompt,
81
85
  startLiveInput,
82
86
  thinkingLevelForEffort,
83
87
  } from "./pi-native/turn-runner.js";
84
88
  import { resolvePiTransport } from "./pi-native/transport.js";
89
+ import { withOpenCodeSessionHeaders } from "./pi-native/provider-attribution.js";
85
90
 
86
91
  /**
87
92
  * Resolve mono-agent's programmatic mode once per run. Tool builders mark
@@ -190,23 +195,82 @@ export function createDynamicCredentialStore(apiKeys, resolvePiApiKey, runtimeWa
190
195
  // resolved model. `piResolvedModels` is an advanced/test seam mirroring
191
196
  // `piResolvedModel`: when supplied it is used verbatim (the model dispatched via
192
197
  // `piResolvedModel` may live outside pi's builtin catalog, e.g. a faux model).
193
- function buildRunModels(runtime, options, runtimeWarnings) {
194
- if (options.piResolvedModels) return options.piResolvedModels;
195
- const credentials = createDynamicCredentialStore(runtime.apiKeys, options.resolvePiApiKey, runtimeWarnings);
196
- if (options.customProvider) {
197
- const model = runtime.model;
198
- const models = createModels({ credentials });
199
- models.setProvider(createProvider({
200
- id: model.provider,
201
- name: model.name || model.provider,
202
- baseUrl: model.baseUrl,
203
- auth: { apiKey: envApiKeyAuth(model.name || model.provider, []) },
204
- models: [model],
205
- api: openAICompletionsApi(),
206
- }));
207
- 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
+ }
208
223
  }
209
- 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
+ }));
210
274
  }
211
275
 
212
276
  // Normalize the incoming runtime messages into AgentMessages the harness can
@@ -313,6 +377,9 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
313
377
  diagnostics: {},
314
378
  },
315
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,
316
383
  sessionEntry: null,
317
384
  // Fresh keep-alive sessions are registered busy until the harness closes;
318
385
  // this prevents another turn from reopening the same repo record while its
@@ -336,12 +403,17 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
336
403
  // (host/runtime-side throws after the session mutated) can roll back too, not
337
404
  // just the success path.
338
405
  baselineLeafId: null,
406
+ recoveryOperationId: undefined,
407
+ recoveryInputIds: null,
408
+ retainRecoveryTail: false,
339
409
  };
340
410
 
341
411
  const providerSessionId = options.sessionId
342
412
  || options.providerSessionId
413
+ || options.providerAttributionSessionId
343
414
  || options.runId
344
415
  || randomUUID();
416
+ const providerAttributionSessionId = options.providerAttributionSessionId || providerSessionId;
345
417
  // Prefer the explicit sessionId, but fall back to providerSessionId so a caller
346
418
  // that only supplies providerSessionId still resumes the prior session instead
347
419
  // of being treated as a fresh run (which would drop prior context).
@@ -423,7 +495,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
423
495
  // ready pi-ai Model (e.g. a registered faux provider model) plus optional
424
496
  // capabilities, bypassing the static model-registry lookup. Production
425
497
  // callers leave it undefined and resolve through pi-ai's registry.
426
- const runtime = options.piResolvedModel
498
+ let runtime = options.piResolvedModel
427
499
  ? {
428
500
  model: options.piResolvedModel,
429
501
  capabilities: options.piResolvedCapabilities || {
@@ -438,6 +510,15 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
438
510
  apiKeys: new Map(),
439
511
  }
440
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
+ }
441
522
  const capabilities = runtime.capabilities || {};
442
523
  const effectiveThinkingLevel = thinkingLevelForEffort(options.effort || "medium", capabilities);
443
524
  const reference = resolved.reference || `${resolved.provider}:${resolved.model}`;
@@ -507,7 +588,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
507
588
  // global scheduling mode from the tools' executionMode markers.
508
589
  const toolSteeringMode = "one-at-a-time";
509
590
 
510
- const piModels = buildRunModels(runtime, options, runtimeWarnings);
591
+ const piModels = buildRunModels(runtime, options, runtimeWarnings, providerAttributionSessionId);
511
592
 
512
593
  // Construct the harness and recover any interrupted durable operation.
513
594
  // Stream subscription and the external abort handler are activated only
@@ -560,11 +641,6 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
560
641
  reference,
561
642
  });
562
643
 
563
- // Live steering: consume follow-up messages and steer the harness mid-run.
564
- // The consumer is tied to run completion so it stops steering once the run
565
- // finishes and does not swallow messages meant for a later turn.
566
- const liveInput = startLiveInput({ harness, options, onEvent });
567
-
568
644
  // Re-check abort right before issuing the provider request. The abort
569
645
  // handler is only installed at ~:639, AFTER a long stretch of awaited setup
570
646
  // (reopen, create, MCP init, buildContext, appendMessage, getLeafId). If
@@ -606,6 +682,25 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
606
682
  runtimeWarnings,
607
683
  });
608
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
+
609
704
  onEvent({
610
705
  type: "provider_request_started",
611
706
  sdk: "pi",
@@ -614,11 +709,26 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
614
709
  timestamp: Date.now(),
615
710
  });
616
711
 
617
- let { runError } = await runHarnessPrompt(harness, promptText, promptImages);
618
-
619
- // The run is done: stop the live-steering consumer so it cannot steer a
620
- // finished harness or swallow a follow-up meant for the next turn.
621
- 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
+ }
622
732
 
623
733
  runState.externalAbort ||= !!options.abortSignal?.aborted;
624
734
 
@@ -804,6 +914,12 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
804
914
  // tracks LIVENESS so disposeProviderSession / idle-TTL eviction can reach
805
915
  // native sessions, keep-alive registers the session, and a failed/aborted
806
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";
807
923
  await commitSession(runState, {
808
924
  options,
809
925
  requestedSessionId,
@@ -815,21 +931,21 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
815
931
  onEvent,
816
932
  });
817
933
 
818
- // Final abort guard (durable cancel TOCTOU): if a cancel raced the lifecycle
819
- // commit above landing AFTER the keep-alive/!externalAbort decision but
820
- // before this return the cancelled turn is still in the durable transcript
821
- // and (for keep-alive) the live registry. Roll it back so the next resume sees
822
- // the pre-turn state (rollbackAbortedTurn: a resumed session moves to its
823
- // baseline leaf and drops its live entry; a fresh durable session deletes its
824
- // jsonl). The abort re-check + return stay inline with NO await between the
825
- // false-branch check and the return, so an external cancel cannot newly fire
826
- // 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.
827
937
  if (!runState.externalAbort && options.abortSignal?.aborted) {
828
938
  runState.externalAbort = true;
829
- await rollbackAbortedTurn(runState, { requestedSessionId, providerSessionId, durableRepo });
939
+ if (!runState.retainRecoveryTail) await rollbackAbortedTurn(runState, { requestedSessionId, providerSessionId, durableRepo });
830
940
  }
831
941
 
832
- 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({
833
949
  finalText,
834
950
  finalThinking,
835
951
  events,
@@ -848,8 +964,10 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
848
964
  providerSessionId,
849
965
  runtimeWarnings,
850
966
  capabilitiesUsed,
967
+ usageMeasured: hasMeasuredUsage(runTranscript),
851
968
  structuredResult: runState.structuredResult,
852
- });
969
+ effectiveEffort: providerEffectiveEffort,
970
+ }), ...(providerSessionRecovery ? { providerSessionRecovery } : {}) };
853
971
  } catch (err) {
854
972
  runState.externalAbort ||= !!options.abortSignal?.aborted;
855
973
  // Drop a just-created fresh durable session, release a create-on-miss
@@ -883,6 +1001,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
883
1001
  runtimeWarnings,
884
1002
  isRetryable,
885
1003
  piTransport,
1004
+ effectiveEffort: harness?.getThinkingLevel?.(),
886
1005
  });
887
1006
  } finally {
888
1007
  try { await harness?.close?.(); } catch { /* best-effort */ }