@mono-agent/agent-runtime 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/MIGRATION.md +147 -69
  2. package/README.md +83 -20
  3. package/package.json +3 -6
  4. package/src/agent/compaction.js +0 -11
  5. package/src/agent/prompt/skill-index.js +5 -1
  6. package/src/ai/index.js +7 -1
  7. package/src/ai/observer.js +48 -13
  8. package/src/ai/pi-interop.js +156 -0
  9. package/src/ai/providers/claude-cli.js +2 -13
  10. package/src/ai/providers/claude-sdk.js +14 -8
  11. package/src/ai/providers/codex-app.js +235 -56
  12. package/src/ai/providers/opencode-app.js +168 -3
  13. package/src/ai/providers/pi-messages.js +0 -8
  14. package/src/ai/providers/pi-native/compaction-driver.js +106 -10
  15. package/src/ai/providers/pi-native/result-builder.js +2 -14
  16. package/src/ai/providers/pi-native/stream-subscriber.js +20 -2
  17. package/src/ai/providers/pi-native/turn-runner.js +31 -8
  18. package/src/ai/providers/pi-native.js +2 -5
  19. package/src/ai/runtime/live-input-events.js +94 -0
  20. package/src/ai/runtime/registry.js +8 -1
  21. package/src/ai/runtime/router.js +21 -0
  22. package/src/ai/types.js +2 -1
  23. package/src/runtime.js +3 -0
  24. package/types/agent/compaction.d.ts +0 -2
  25. package/types/agent/prompt/skill-index.d.ts +3 -0
  26. package/types/ai/index.d.ts +1 -1
  27. package/types/ai/observer.d.ts +4 -2
  28. package/types/ai/pi-interop.d.ts +113 -0
  29. package/types/ai/providers/claude-cli.d.ts +6 -30
  30. package/types/ai/providers/claude-sdk.d.ts +2 -9
  31. package/types/ai/providers/codex-app.d.ts +4 -10
  32. package/types/ai/providers/pi-messages.d.ts +0 -1
  33. package/types/ai/providers/pi-native/result-builder.d.ts +3 -11
  34. package/types/ai/providers/pi-native/stream-subscriber.d.ts +4 -2
  35. package/types/ai/providers/pi-native/turn-runner.d.ts +1 -1
  36. package/types/ai/runtime/live-input-events.d.ts +22 -0
  37. package/types/ai/types.d.ts +10 -2
  38. package/src/ai/backend.js +0 -17
  39. package/src/ai/registry.js +0 -5
  40. package/types/ai/backend.d.ts +0 -57
  41. package/types/ai/registry.d.ts +0 -1
@@ -33,6 +33,7 @@ import {
33
33
  prepareCompaction,
34
34
  shouldCompact,
35
35
  } from "@earendil-works/pi-agent-core";
36
+ import { randomUUID } from "node:crypto";
36
37
  import {
37
38
  estimateFixedOverheadTokens,
38
39
  isLikelyContextTermination,
@@ -204,6 +205,52 @@ function previewCompactedContext(branchEntries, result) {
204
205
  return estimateBuiltContextTokens([...branchEntries, previewEntry]);
205
206
  }
206
207
 
208
+ function canonicalCompactionTrigger(trigger) {
209
+ return trigger === "reactive_overflow" ? "overflow" : trigger;
210
+ }
211
+
212
+ function finiteTokenCount(value) {
213
+ if (value === null || value === undefined) return undefined;
214
+ const count = Number(value);
215
+ return Number.isFinite(count) && count >= 0 ? count : undefined;
216
+ }
217
+
218
+ /**
219
+ * @param {((event: any) => void)|undefined} onEvent
220
+ * @param {{operationId: string, status: string, trigger: string, model?: string, tokensBefore?: number|null, tokensAfter?: number|null, reason?: string, message?: string}} event
221
+ */
222
+ function emitCompactionEvent(onEvent, {
223
+ operationId,
224
+ status,
225
+ trigger,
226
+ model,
227
+ tokensBefore,
228
+ tokensAfter,
229
+ reason,
230
+ message,
231
+ }) {
232
+ const before = finiteTokenCount(tokensBefore);
233
+ const after = finiteTokenCount(tokensAfter);
234
+ try {
235
+ onEvent?.({
236
+ type: "context_compaction",
237
+ operationId,
238
+ status,
239
+ sdk: "pi",
240
+ trigger: canonicalCompactionTrigger(trigger),
241
+ timestamp: Date.now(),
242
+ ...(model ? { model } : {}),
243
+ ...(before === undefined ? {} : { tokensBefore: before }),
244
+ ...(after === undefined ? {} : { tokensAfter: after }),
245
+ ...(before === undefined && after === undefined ? {} : { tokenCountsExact: false }),
246
+ ...(reason ? { reason } : {}),
247
+ ...(message ? { message } : {}),
248
+ });
249
+ } catch {
250
+ // Observability must never change whether compaction itself succeeds.
251
+ }
252
+ }
253
+
207
254
  // Run a single guarded compaction. Requires the harness idle (callers
208
255
  // waitForIdle first). Never throws — classifies AgentHarnessError into a warning
209
256
  // and reports back whether anything was compacted. Fires onCompactionRecorded on
@@ -218,14 +265,22 @@ export async function tryCompact(harness, {
218
265
  session,
219
266
  policy,
220
267
  }) {
221
- const adaptivePolicy = resolveAgentCompactionPolicy({}, {
222
- contextWindow: typeof harness?.getModel === "function" ? harness.getModel()?.contextWindow : undefined,
268
+ const operationId = randomUUID();
269
+ emitCompactionEvent(onEvent, {
270
+ operationId,
271
+ status: "running",
272
+ trigger,
273
+ model,
223
274
  });
224
- const effectivePolicy = { ...adaptivePolicy, ...(policy || {}) };
275
+ let effectivePolicy = policy || {};
225
276
  /** @type {null | {kind: string, tokensBefore?: number|null, tokensAfter?: number|null, savings?: number|null, error?: any}} */
226
277
  let hookDecision = null;
227
278
  let removeHook = null;
228
279
  try {
280
+ const adaptivePolicy = resolveAgentCompactionPolicy({}, {
281
+ contextWindow: typeof harness?.getModel === "function" ? harness.getModel()?.contextWindow : undefined,
282
+ });
283
+ effectivePolicy = { ...adaptivePolicy, ...(policy || {}) };
229
284
  if (typeof harness?.on !== "function") {
230
285
  throw new Error("Pi AgentHarness does not expose session_before_compact hooks");
231
286
  }
@@ -298,14 +353,13 @@ export async function tryCompact(harness, {
298
353
  const reduced = measuredTokensBefore === null || tokensAfter === null
299
354
  ? null
300
355
  : tokensAfter < measuredTokensBefore;
301
- onEvent?.({
302
- type: "runtime_warning",
303
- warning_kind: "context_compaction_applied",
304
- source: "pi",
356
+ emitCompactionEvent(onEvent, {
357
+ operationId,
358
+ status: "succeeded",
305
359
  trigger,
306
- tokens_before: tokensBefore,
307
- tokens_after: tokensAfter,
308
- reduced,
360
+ model,
361
+ tokensBefore,
362
+ tokensAfter,
309
363
  });
310
364
  if (reduced === false) {
311
365
  runtimeWarnings?.push({
@@ -354,6 +408,15 @@ export async function tryCompact(harness, {
354
408
  ? { minimum_savings_tokens: effectivePolicy.compactionMinSavingsTokens }
355
409
  : {}),
356
410
  });
411
+ emitCompactionEvent(onEvent, {
412
+ operationId,
413
+ status: "skipped",
414
+ trigger,
415
+ model,
416
+ tokensBefore: hookDecision.tokensBefore,
417
+ tokensAfter: hookDecision.tokensAfter,
418
+ reason: hookDecision.kind,
419
+ });
357
420
  return {
358
421
  applied: false,
359
422
  tokensBefore: hookDecision.tokensBefore ?? null,
@@ -369,6 +432,13 @@ export async function tryCompact(harness, {
369
432
  trigger,
370
433
  message: "Nothing to compact",
371
434
  });
435
+ emitCompactionEvent(onEvent, {
436
+ operationId,
437
+ status: "skipped",
438
+ trigger,
439
+ model,
440
+ reason: "nothing_to_compact",
441
+ });
372
442
  return { applied: false, tokensBefore: null, tokensAfter: null, reduced: null, nothingToCompact: true };
373
443
  }
374
444
  const effectiveError = hookDecision?.kind === "failed" && hookDecision.error
@@ -385,6 +455,32 @@ export async function tryCompact(harness, {
385
455
  ? "context_compaction_busy"
386
456
  : "context_compaction_failed";
387
457
  runtimeWarnings?.push({ warning_kind: warningKind, source: "pi", trigger, message });
458
+ emitCompactionEvent(onEvent, {
459
+ operationId,
460
+ status: nothingToCompact ? "skipped" : "failed",
461
+ trigger,
462
+ model,
463
+ reason: nothingToCompact
464
+ ? "nothing_to_compact"
465
+ : code === "auth"
466
+ ? "authentication"
467
+ : code === "busy"
468
+ ? "busy"
469
+ : code === "aborted"
470
+ ? "cancelled"
471
+ : "provider_error",
472
+ ...(nothingToCompact
473
+ ? {}
474
+ : {
475
+ message: code === "auth"
476
+ ? "Compaction authentication failed."
477
+ : code === "busy"
478
+ ? "Context was busy and could not be compacted."
479
+ : code === "aborted"
480
+ ? "Compaction was cancelled."
481
+ : "Compaction failed.",
482
+ }),
483
+ });
388
484
  return { applied: false, tokensBefore: null, tokensAfter: null, reduced: null, nothingToCompact };
389
485
  } finally {
390
486
  removeHook?.();
@@ -33,7 +33,7 @@ export function usageFromMessages(messages = []) {
33
33
  }
34
34
 
35
35
  /**
36
- * Normalize the final provider request's usage into an exact context snapshot.
36
+ * Normalize one provider request's usage into an exact context snapshot.
37
37
  * Unlike usageFromMessages(), this deliberately does not aggregate earlier
38
38
  * requests in the run: the last assistant usage is the same provider-counted
39
39
  * value Pi's compaction logic trusts, so it can decrease after compaction.
@@ -74,15 +74,13 @@ export function failureKindForPiError(message, diagnostics, { maxTurnsHit = fals
74
74
 
75
75
  /**
76
76
  * Emit the per-run cache / cost / provider-completed events.
77
- * @param {{onEvent: (event: any) => void, resolved: any, reference: string, usage: {input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}, contextUsage?: {input: number, output: number, cacheRead: number, cacheCreation: number, total: number}|null, contextWindow?: number, estimatedCost: number, start: number, externalAbort: boolean}} params
77
+ * @param {{onEvent: (event: any) => void, resolved: any, reference: string, usage: {input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}, estimatedCost: number, start: number, externalAbort: boolean}} params
78
78
  */
79
79
  export function emitUsageCostEvents({
80
80
  onEvent,
81
81
  resolved,
82
82
  reference,
83
83
  usage,
84
- contextUsage,
85
- contextWindow,
86
84
  estimatedCost,
87
85
  start,
88
86
  externalAbort,
@@ -105,16 +103,6 @@ export function emitUsageCostEvents({
105
103
  cacheCreationTokens: Number(usage.cacheWrite) || 0,
106
104
  },
107
105
  });
108
- if (contextUsage) {
109
- const effectiveContextWindow = Number(contextWindow) || 0;
110
- onEvent({
111
- type: "context_usage",
112
- sdk: resolved.sdk,
113
- model: reference,
114
- ...(effectiveContextWindow > 0 ? { contextWindow: effectiveContextWindow } : {}),
115
- tokens: contextUsage,
116
- });
117
- }
118
106
  onEvent({
119
107
  type: "provider_request_completed",
120
108
  sdk: resolved.sdk,
@@ -13,6 +13,7 @@ import {
13
13
  jsonSerializable,
14
14
  streamContentKey,
15
15
  } from "../pi-events.js";
16
+ import { contextUsageFromAssistantMessage } from "./result-builder.js";
16
17
 
17
18
  function toolResultFileChange(result) {
18
19
  const fileChange = result?.details?.file_change;
@@ -41,10 +42,10 @@ function toolResultFileChange(result) {
41
42
  * abort; it is already constructed when this is wired (subscribe follows the
42
43
  * AgentHarness constructor).
43
44
  * @param {StreamSubscriberState} runState
44
- * @param {{onEvent: (event: any) => void, options: any, toolLimits: any, harness: any}} deps
45
+ * @param {{onEvent: (event: any) => void, options: any, toolLimits: any, harness: any, sdk: string, model: string}} deps
45
46
  * @returns {(event: any) => void}
46
47
  */
47
- export function createStreamSubscriber(runState, { onEvent, options, toolLimits, harness }) {
48
+ export function createStreamSubscriber(runState, { onEvent, options, toolLimits, harness, sdk, model }) {
48
49
  return (event) => {
49
50
  if (event.type === "message_update") {
50
51
  const streamEvent = event.assistantMessageEvent;
@@ -69,6 +70,23 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
69
70
  onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.content }] } });
70
71
  }
71
72
  }
73
+ } else if (event.type === "message_end") {
74
+ const contextUsage = contextUsageFromAssistantMessage(event.message);
75
+ if (contextUsage) {
76
+ const contextWindow = Number(harness?.getModel?.()?.contextWindow) || 0;
77
+ const measurementId = typeof event.message?.id === "string" && event.message.id.trim().length > 0
78
+ ? event.message.id
79
+ : undefined;
80
+ onEvent({
81
+ type: "context_usage",
82
+ sdk,
83
+ model,
84
+ timestamp: Date.now(),
85
+ ...(measurementId === undefined ? {} : { measurementId }),
86
+ ...(contextWindow > 0 ? { contextWindow } : {}),
87
+ tokens: contextUsage,
88
+ });
89
+ }
72
90
  } else if (event.type === "tool_execution_start") {
73
91
  if (event.toolName) runState.lastToolName = event.toolName;
74
92
  if (event.toolCallId) runState.toolStartTimes.set(event.toolCallId, Date.now());
@@ -205,6 +205,8 @@ export function buildTurnHarness(runState, {
205
205
  onEvent,
206
206
  options,
207
207
  toolLimits,
208
+ sdk,
209
+ reference,
208
210
  }) {
209
211
  const harness = new AgentHarness({
210
212
  env: new NodeExecutionEnv({ cwd: cwd || process.cwd() }),
@@ -228,7 +230,14 @@ export function buildTurnHarness(runState, {
228
230
  : undefined);
229
231
  runState.harness = harness;
230
232
 
231
- harness.subscribe(createStreamSubscriber(runState, { onEvent, options, toolLimits, harness }));
233
+ harness.subscribe(createStreamSubscriber(runState, {
234
+ onEvent,
235
+ options,
236
+ toolLimits,
237
+ harness,
238
+ sdk,
239
+ model: reference,
240
+ }));
232
241
 
233
242
  const abortHandler = () => {
234
243
  runState.externalAbort = true;
@@ -255,12 +264,24 @@ export function startLiveInput({ harness, options, onEvent }) {
255
264
  ? options.liveInput[Symbol.asyncIterator]()
256
265
  : options.liveInput;
257
266
  let runComplete = false;
267
+ /** @type {() => void} */
268
+ let signalStop = () => {};
269
+ const stopped = new Promise((resolve) => { signalStop = () => resolve(); });
258
270
  const task = (async () => {
259
271
  try {
260
272
  while (!runComplete && !options.abortSignal?.aborted) {
261
- const next = await iterator.next();
273
+ const next = await Promise.race([
274
+ iterator.next(),
275
+ stopped.then(() => ({ done: true, value: undefined })),
276
+ ]);
262
277
  if (next.done || runComplete || options.abortSignal?.aborted) break;
263
- await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
278
+ try {
279
+ await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
280
+ next.value.acknowledge?.();
281
+ } catch (err) {
282
+ next.value.reject?.(err);
283
+ throw err;
284
+ }
264
285
  }
265
286
  } catch (err) {
266
287
  onEvent({
@@ -273,15 +294,17 @@ export function startLiveInput({ harness, options, onEvent }) {
273
294
  return {
274
295
  // The run is done: stop the live-steering consumer so it cannot steer a
275
296
  // finished harness or swallow a follow-up meant for the next turn. We signal
276
- // completion, then best-effort return() the iterator to unblock a pending
277
- // next(). We do NOT await the task (it could block on next() if the source
278
- // has no return()), but the runComplete guard prevents any further steering.
297
+ // completion, then best-effort return() the iterator. The explicit stop
298
+ // race releases the task even when a third-party iterator's return() does
299
+ // not unblock its pending next(); awaiting the task still closes any steer
300
+ // acknowledgement already in progress.
279
301
  stop: async () => {
280
302
  runComplete = true;
303
+ signalStop();
281
304
  if (iterator && typeof iterator.return === "function") {
282
- try { await iterator.return(); } catch { /* best-effort */ }
305
+ try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
283
306
  }
284
- void task;
307
+ await task;
285
308
  },
286
309
  };
287
310
  }
@@ -52,7 +52,6 @@ import {
52
52
  buildErrorDetails,
53
53
  buildErrorResult,
54
54
  buildSuccessResult,
55
- contextUsageFromAssistantMessage,
56
55
  emitCapabilitiesResolved,
57
56
  emitUsageCostEvents,
58
57
  usageFromMessages,
@@ -453,6 +452,8 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
453
452
  onEvent,
454
453
  options,
455
454
  toolLimits,
455
+ sdk: resolved.sdk,
456
+ reference,
456
457
  });
457
458
 
458
459
  // Seed prior transcript (everything before the trailing user turn) into the
@@ -614,10 +615,6 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
614
615
  resolved,
615
616
  reference,
616
617
  usage,
617
- contextUsage: runState.externalAbort || runState.maxTurnsHit || runError
618
- ? null
619
- : contextUsageFromAssistantMessage(lastAssistant),
620
- contextWindow: runState.compaction.policy?.contextWindow,
621
618
  estimatedCost,
622
619
  start,
623
620
  externalAbort: runState.externalAbort,
@@ -0,0 +1,94 @@
1
+ // Metadata-only live-input acknowledgement instrumentation.
2
+ //
3
+ // Provider bridges already call message.acknowledge() only after their native
4
+ // steering boundary accepts guidance. Wrapping that callback here creates one
5
+ // adapter-neutral `live_input_applied` event without copying the guidance body
6
+ // into runtime telemetry. A wrapper owns one logical-run dedupe set and is
7
+ // intentionally reused by the fallback router across provider attempts.
8
+
9
+ // @ts-check
10
+
11
+ const LIVE_INPUT_APPLIED_INSTRUMENTED = Symbol("mono-agent.live-input-applied-instrumented");
12
+
13
+ /**
14
+ * @typedef {{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (reason?: unknown) => void}} RuntimeLiveInputMessage
15
+ * @typedef {{type: "live_input_applied", inputId: string, receivedAt?: string}} LiveInputAppliedEvent
16
+ */
17
+
18
+ /**
19
+ * @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
20
+ * @param {(event: LiveInputAppliedEvent) => void} onApplied
21
+ * @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
22
+ */
23
+ export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
24
+ if (liveInput === undefined || isInstrumented(liveInput)) return liveInput;
25
+
26
+ const appliedInputIds = new Set();
27
+ const instrumented = {
28
+ [LIVE_INPUT_APPLIED_INSTRUMENTED]: true,
29
+ [Symbol.asyncIterator]() {
30
+ const iterator = liveInput[Symbol.asyncIterator]();
31
+ let ordinal = 0;
32
+ return {
33
+ async next() {
34
+ const next = await iterator.next();
35
+ if (next.done === true) return next;
36
+ ordinal += 1;
37
+ const message = next.value;
38
+ const inputId = stableInputId(message?.id, ordinal);
39
+ const receivedAt = typeof message?.receivedAt === "string" && message.receivedAt.length > 0
40
+ ? message.receivedAt
41
+ : undefined;
42
+ const acknowledge = typeof message?.acknowledge === "function"
43
+ ? message.acknowledge.bind(message)
44
+ : undefined;
45
+ return {
46
+ done: false,
47
+ value: {
48
+ ...message,
49
+ acknowledge: () => {
50
+ acknowledge?.();
51
+ if (appliedInputIds.has(inputId)) return;
52
+ appliedInputIds.add(inputId);
53
+ try {
54
+ onApplied({
55
+ type: "live_input_applied",
56
+ inputId,
57
+ ...(receivedAt === undefined ? {} : { receivedAt }),
58
+ });
59
+ } catch {
60
+ // Telemetry must never turn accepted guidance into a provider
61
+ // failure after the native steering call already succeeded.
62
+ }
63
+ },
64
+ },
65
+ };
66
+ },
67
+ async return(value) {
68
+ return typeof iterator.return === "function"
69
+ ? iterator.return(value)
70
+ : { done: true, value };
71
+ },
72
+ async throw(error) {
73
+ if (typeof iterator.throw === "function") return iterator.throw(error);
74
+ throw error;
75
+ },
76
+ };
77
+ },
78
+ };
79
+ return /** @type {AsyncIterable<RuntimeLiveInputMessage>} */ (instrumented);
80
+ }
81
+
82
+ /** @param {unknown} value */
83
+ function isInstrumented(value) {
84
+ return typeof value === "object"
85
+ && value !== null
86
+ && value[LIVE_INPUT_APPLIED_INSTRUMENTED] === true;
87
+ }
88
+
89
+ /** @param {unknown} value @param {number} ordinal */
90
+ function stableInputId(value, ordinal) {
91
+ return typeof value === "string" && value.trim().length > 0
92
+ ? value
93
+ : `anonymous:${ordinal}`;
94
+ }
@@ -29,7 +29,14 @@ const builtinBridgeSpecs = {
29
29
  id: "claude-code",
30
30
  supports: (ref, options) => ref?.sdk === "claude" && options?.executionMode === "cli",
31
31
  // The claude CLI resumes prior sessions via `--resume <sessionId>`.
32
- capabilities: () => ({ kind: "claude-code", runtime: "cli", ...COMMON_CAPABILITIES, supports_session_resume: true }),
32
+ capabilities: () => ({
33
+ kind: "claude-code",
34
+ runtime: "cli",
35
+ ...COMMON_CAPABILITIES,
36
+ supports_session_resume: true,
37
+ // The one-shot CLI bridge has no bidirectional stdin steering channel.
38
+ supports_live_input: false,
39
+ }),
33
40
  load: async () => (await import("../providers/claude-cli.js")).claudeCodeRuntimeBridge,
34
41
  },
35
42
  "codex-app": {
@@ -41,6 +41,8 @@ import { runtimeCapabilities } from "./capabilities.js";
41
41
  import { buildTranscriptTailSnapshot, renderResumeSnapshot } from "../../agent/transcript.js";
42
42
  import { passthroughSandbox } from "../../agent/sandbox-seam.js";
43
43
  import { resolveRuntimeBrand } from "../../runtime-brand.js";
44
+ import { createObserverHub } from "../observer.js";
45
+ import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
44
46
 
45
47
  /**
46
48
  * @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef
@@ -127,6 +129,22 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
127
129
  * @returns {Promise<RuntimeResult>}
128
130
  */
129
131
  async run(systemPrompt, options = {}) {
132
+ const liveInputHub = options.liveInput === undefined
133
+ ? undefined
134
+ : createObserverHub({
135
+ observers: [
136
+ ...(Array.isArray(host.observers) ? host.observers : []),
137
+ ...(Array.isArray(options.observers) ? options.observers : []),
138
+ ],
139
+ onEvent: options.onEvent,
140
+ });
141
+ if (options.liveInput !== undefined && liveInputHub !== undefined) {
142
+ options = {
143
+ ...options,
144
+ liveInput: instrumentLiveInputAppliedEvents(options.liveInput, liveInputHub.emit),
145
+ };
146
+ }
147
+ try {
130
148
  /** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null), routeSafety?: import('../types.js').RuntimeRouteSafetyMode, safetyContract?: import('../types.js').RuntimeRouteSafetyContract}>} */
131
149
  const failoverHistory = [];
132
150
  /** @type {RuntimeResult|null} */
@@ -374,6 +392,9 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
374
392
  failoverHistory,
375
393
  routeSafetyHistory,
376
394
  };
395
+ } finally {
396
+ await liveInputHub?.flush();
397
+ }
377
398
  },
378
399
  chain: () => entries.slice(),
379
400
  configureTools(next = {}) {
package/src/ai/types.js CHANGED
@@ -131,9 +131,10 @@
131
131
  * @property {string} [executionMode] "sdk" (default) or "cli"; selects which bridge variant handles the model.
132
132
  * @property {string} [sessionId] Host conversation/session key for resumable bridges.
133
133
  * @property {string} [providerSessionId] Provider-owned resume id for resumable bridges.
134
+ * @property {typeof import("@anthropic-ai/claude-agent-sdk").query} [claudeAgentQuery] Advanced programmatic/test seam for the Claude SDK route; omitted runs use the runtime's pinned SDK query implementation.
134
135
  * @property {boolean} [sessionKeepAlive] Keep resumable provider state alive after the turn.
135
136
  * @property {number} [sessionIdleTimeoutMs] Idle TTL for resumable provider state.
136
- * @property {AsyncIterable<{body: string, id?: string}>} [liveInput] Stream of in-flight user messages for steering an active run.
137
+ * @property {AsyncIterable<{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (error?: unknown) => void}>} [liveInput] Stream of in-flight user messages for steering an active run. Providers acknowledge only after accepting a message into the active turn.
137
138
  * @property {ReadonlyArray<*>} [observers] Per-call observers (see RuntimeObserver) merged with host-level (createRuntime) observers.
138
139
  * @property {(event: RuntimeEvent) => void} [onEvent]
139
140
  * @property {ReadonlyArray<Object>} [messages]
package/src/runtime.js CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  import { createToolContext, updateToolContext } from "./agent/tools/shared/tool-context.js";
41
41
  import { resolveRuntimeBrand } from "./runtime-brand.js";
42
42
  import { retireDurableNativeSession } from "./ai/providers/pi-native/session-lifecycle.js";
43
+ import { instrumentLiveInputAppliedEvents } from "./ai/runtime/live-input-events.js";
43
44
 
44
45
  /**
45
46
  * @typedef {import('./ai/types.js').AgentRuntimeHostOptions} AgentRuntimeHostOptions
@@ -159,6 +160,7 @@ export function createRuntime(host = {}) {
159
160
  observers: [...hostObservers, ...callObservers],
160
161
  onEvent: options.onEvent,
161
162
  });
163
+ const liveInput = instrumentLiveInputAppliedEvents(options.liveInput, hub.emit);
162
164
  const prompts = resolvePrompts(host.prompts, options.prompts);
163
165
  const result = await bridge.execute(systemPrompt, {
164
166
  ...hostDefaults,
@@ -172,6 +174,7 @@ export function createRuntime(host = {}) {
172
174
  toolContext,
173
175
  observerHub: hub,
174
176
  onEvent: hub.emit,
177
+ ...(liveInput === undefined ? {} : { liveInput }),
175
178
  // Merged AFTER the spreads so the per-field run>host>default precedence
176
179
  // wins over either bag's whole-object `prompts`.
177
180
  ...(prompts === undefined ? {} : { prompts }),
@@ -84,8 +84,6 @@ export type AgentCompactionPolicy = {
84
84
  summaryMaxTokens: number;
85
85
  fixedOverheadEnabled: boolean;
86
86
  compactionMinSavingsTokens: number;
87
- toolPayloadCompactionTriggerChars: number;
88
- toolPruneTriggerTokens: number;
89
87
  toolTextLimitChars: number;
90
88
  bashOutputLimitChars: number;
91
89
  mcpTextLimitChars: number;
@@ -8,6 +8,9 @@ export function buildSkillPathNote({ assetsPath, skillsRoot }?: {
8
8
  skillsRoot?: any;
9
9
  }): string;
10
10
  /**
11
+ * Render a complete skill body plus its path note. Omitting `maxChars` returns
12
+ * the full text; pass a positive `maxChars` only when explicit truncation is
13
+ * required by the caller.
11
14
  * @param {{body?: string, assetsPath?: string, skillsRoot?: any, maxChars?: number}} [options]
12
15
  */
13
16
  export function formatSkillBodyWithPathNote({ body, assetsPath, skillsRoot, maxChars }?: {
@@ -1,8 +1,8 @@
1
- export * from "./registry.js";
2
1
  export * from "./runtime/model-refs.js";
3
2
  export * from "./runtime/registry.js";
4
3
  export { createSessionRegistry, disposeAllProviderSessions, disposeProviderSession, invalidateProviderSession, refreshProviderSession, syncProviderSession } from "./runtime/sessions.js";
5
4
  export { createMetricsObserver, createObserverHub } from "./observer.js";
6
5
  export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
6
+ export { getPiBuiltinModel, listPiBuiltinModels, loginPiOAuth, reasoningLevelsForPiModel, resolvePiOAuthApiKey } from "./pi-interop.js";
7
7
  export { CLAUDE_SDK_CATALOG_VERSION, createClaudeSdkDiscoveryIsolation, curatedClaudeSdkModels, discoverClaudeSdkModels, normalizeClaudeSdkCatalog, normalizeClaudeSdkModelId } from "./providers/claude-sdk-discovery.js";
8
8
  export { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings, UNKNOWN_CAPABILITY } from "./runtime/capabilities-used.js";
@@ -14,8 +14,9 @@ export function createObserverHub({ observers, onEvent }?: {
14
14
  flush: () => Promise<void>;
15
15
  observers: () => any[];
16
16
  };
17
- export function createMetricsObserver({ name }?: {
17
+ export function createMetricsObserver({ name, maxLatencySamples }?: {
18
18
  name?: string;
19
+ maxLatencySamples?: number;
19
20
  }): {
20
21
  name: string;
21
22
  recordEvent: (event: any) => void;
@@ -49,7 +50,8 @@ export function createMetricsObserver({ name }?: {
49
50
  byKind: any;
50
51
  };
51
52
  turns: {
52
- count: number;
53
+ count: any;
54
+ sampleCount: number;
53
55
  latencyMsP50: any;
54
56
  latencyMsP95: any;
55
57
  };