@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.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +30 -7
- package/README.md +219 -35
- package/package.json +9 -4
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +104 -5
- package/src/agent/tools/bash.js +10 -2
- package/src/agent/tools/codex-subscription-search.js +122 -28
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/monitor.js +11 -2
- package/src/agent/tools/pi-bridge.js +33 -14
- package/src/agent/tools/shared/monitors.js +22 -3
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +3 -1
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +1 -0
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/pi-native/compaction-driver.js +45 -21
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +40 -2
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +28 -4
- package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
- package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +245 -13
- package/src/ai/providers/pi-native.js +159 -40
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/router.js +30 -11
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +26 -5
- package/src/runtime.js +24 -5
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -1
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +6 -2
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/monitor.d.ts +5 -2
- package/types/agent/tools/pi-bridge.d.ts +6 -4
- package/types/agent/tools/shared/monitors.d.ts +17 -2
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +3 -2
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/index.d.ts +1 -0
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +61 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +3 -1
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +11 -1
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/tool-lifecycle.d.ts +4 -3
- 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
|
-
|
|
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:
|
|
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
|
-
* @
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
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
|
-
|
|
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 */ }
|