@mono-agent/agent-runtime 0.14.0 → 0.15.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/MIGRATION.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # `@mono-agent/agent-runtime` — Migration Guide
2
2
 
3
3
  Breaking and behavioral changes for consumers upgrading from `0.3.x` to the
4
- current `0.13.x` contract. `createRuntime()` remains the package entry point;
4
+ current pre-1.0 contract. `createRuntime()` remains the package entry point;
5
5
  `createMonoRuntime()` remains the typed facade in
6
6
  `@mono-agent/runtime-adapter`. Provider-session input/output uses
7
7
  `providerSessionId`, with `disposeSession()` and `disposeAllSessions()` retained.
@@ -13,6 +13,17 @@ primary route.
13
13
 
14
14
  ---
15
15
 
16
+ ## Pre-1.0 public-surface cleanup
17
+
18
+ The compatibility entrypoints `./ai/backend.js` and `./ai/registry.js` were
19
+ removed after repository-wide reachability checks found no supported caller.
20
+ The old `findProviderForModel` / `listProviders` aliases and provider/backend
21
+ constant objects were removed at the same time. Import `resolveRuntimeBridge`
22
+ or `listRuntimeBridges` from `@mono-agent/agent-runtime` (or its `./ai` barrel)
23
+ instead. Runtime behavior and the canonical bridge descriptors are unchanged.
24
+
25
+ ---
26
+
16
27
  ## 1. Pi is now native-only (`pi-sdk.js` → `pi-native.js`)
17
28
 
18
29
  The hand-rolled Pi bridge that drove the low-level `Agent` was replaced by a
@@ -209,7 +220,7 @@ now a loud failure (guarded by `scripts/verify-deep-imports.mjs`).
209
220
  <!-- public-api-js-subpaths:start -->
210
221
  <!-- Generated by scripts/generate-public-api-docs.mjs. Do not edit by hand. -->
211
222
 
212
- The package exposes **22 named deep `.js` subpaths**:
223
+ The package exposes **21 named deep `.js` subpaths**:
213
224
 
214
225
  ```text
215
226
  @mono-agent/agent-runtime/agent/allowlists.js
@@ -219,7 +230,6 @@ The package exposes **22 named deep `.js` subpaths**:
219
230
  @mono-agent/agent-runtime/agent/tools/shared/ripgrep.js
220
231
  @mono-agent/agent-runtime/agent/tools/shared/runtime-context.js
221
232
  @mono-agent/agent-runtime/agent/transcript.js
222
- @mono-agent/agent-runtime/ai/backend.js
223
233
  @mono-agent/agent-runtime/ai/cost.js
224
234
  @mono-agent/agent-runtime/ai/failure.js
225
235
  @mono-agent/agent-runtime/ai/file-change-stats.js
package/README.md CHANGED
@@ -142,13 +142,11 @@ discoverClaudeSdkModels
142
142
  disposeAllProviderSessions
143
143
  disposeProviderSession
144
144
  executionModeIncompatibilityReason
145
- findProviderForModel
146
145
  generatePiNativeResponse
147
146
  inferAllowlistMode
148
147
  invalidateProviderSession
149
148
  isLikelyContextTermination
150
149
  isModelCompatibleWithExecutionMode
151
- listProviders
152
150
  listRuntimeBridges
153
151
  normalizeAllowlistMode
154
152
  normalizeClaudeSdkCatalog
@@ -311,11 +309,9 @@ discoverClaudeSdkModels
311
309
  disposeAllProviderSessions
312
310
  disposeProviderSession
313
311
  executionModeIncompatibilityReason
314
- findProviderForModel
315
312
  generatePiNativeResponse
316
313
  invalidateProviderSession
317
314
  isModelCompatibleWithExecutionMode
318
- listProviders
319
315
  listRuntimeBridges
320
316
  normalizeClaudeSdkCatalog
321
317
  normalizeClaudeSdkModelId
@@ -330,15 +326,6 @@ syncProviderSession
330
326
  toolCompactionAppliedFromWarnings
331
327
  ```
332
328
 
333
- **`@mono-agent/agent-runtime/ai/backend.js`**
334
-
335
- ```text
336
- BACKEND_CAPABILITIES
337
- backendCapabilities
338
- backendSupportsSessionResume
339
- backendUsesExecenvConfig
340
- ```
341
-
342
329
  **`@mono-agent/agent-runtime/ai/cost.js`**
343
330
 
344
331
  ```text
@@ -388,9 +375,7 @@ formatLiveInputGuidance
388
375
 
389
376
  ```text
390
377
  buildCliCommand
391
- claudeCodeBackend
392
378
  claudeCodeRuntimeBridge
393
- codexCliBackend
394
379
  createThinkingBuffer
395
380
  generateCliResponse
396
381
  normalizeCliEvent
@@ -414,7 +399,6 @@ normalizeClaudeSdkModelId
414
399
  ```text
415
400
  claudeEffortOptions
416
401
  claudeRuntimeBridge
417
- claudeSdkBackend
418
402
  claudeSdkModelForQuery
419
403
  generateClaudeResponse
420
404
  toolPayloadLimit
@@ -423,7 +407,6 @@ toolPayloadLimit
423
407
  **`@mono-agent/agent-runtime/ai/providers/codex-app.js`**
424
408
 
425
409
  ```text
426
- codexAppBackend
427
410
  codexAppRuntimeBridge
428
411
  createCodexAppServerClient
429
412
  generateCodexAppResponse
@@ -645,7 +628,7 @@ Per-call options (a non-exhaustive selection):
645
628
  | `maxTurns` | `number` | Hard cap on agent turns. |
646
629
  | `outputSchema` | `JSONSchema` | Requests structured JSON on capable bridges; see “Structured output” below for bridge-specific return behavior. |
647
630
  | `abortSignal` | `AbortSignal` | Cancel the run. |
648
- | `liveInput` | `AsyncIterable<{ body: string; id?: string }>` | Stream of in-flight user messages for steering on capable bridges. |
631
+ | `liveInput` | `AsyncIterable<{ body: string; id?: string; receivedAt?: string; acknowledge?: () => void; reject?: (error?: unknown) => void }>` | Stream of in-flight user messages for steering on capable bridges. A bridge acknowledges only after its native steering boundary accepts the message; per-attempt rejection permits router replay. |
649
632
  | `onEvent` | `(event) => void` | Fired for every event the provider emits (assistant text, tool calls/results, runtime warnings, structured output). |
650
633
  | `runId` | `string` | Tag this run for downstream callbacks (e.g. `onCompactionRecorded`). |
651
634
  | `providerSessionId` | `string` | Resume a prior provider session. |
@@ -653,6 +636,10 @@ Per-call options (a non-exhaustive selection):
653
636
  | `codexAppServerCommand` | `string` | Override the Codex CLI binary. |
654
637
  | `codexAppServerArgs` | `string[]` | Override the Codex CLI arguments. |
655
638
 
639
+ Live input is native on the Claude SDK, Codex app-server, and Pi bridges. The
640
+ one-shot Claude CLI and direct OpenCode bridges advertise it as unsupported so
641
+ routers skip them when a direct runtime call requires steering.
642
+
656
643
  Returns:
657
644
 
658
645
  ```ts
@@ -693,6 +680,23 @@ Returns:
693
680
 
694
681
  `capabilitiesUsed` is the per-call complement to `runtimeCapabilities()`. Tristate fields use `null` to mean "this provider can't tell" — distinct from `false` ("definitely off"). It's also emitted as a `capabilities_resolved` event near the end of the run, so observers can capture it without inspecting the result object.
695
682
 
683
+ Successful provider requests may also emit exact context telemetry through
684
+ `onEvent` and `result.events`:
685
+
686
+ - `context_usage` is one provider-counted request snapshot, never the run's
687
+ aggregate processed-token total. Pi emits it at each successful assistant
688
+ `message_end`; Codex uses `thread/tokenUsage/updated.tokenUsage.last`; direct
689
+ OpenCode requires a completed assistant message with native `tokens.total`.
690
+ Each event identifies the measured model and includes `contextWindow` only
691
+ when the provider's own model metadata supplied it. The Claude bridges do not
692
+ currently emit this event.
693
+ - `context_compaction` is a lifecycle event with a stable `operationId`,
694
+ `status` (`running`, `succeeded`, `skipped`, or `failed`), `sdk`, `trigger`,
695
+ `timestamp`, and optional safe reason/model/count fields. Pi drives and emits
696
+ its own lifecycle; Codex and OpenCode normalize their native notifications and
697
+ suppress deprecated duplicate notifications. Pi's before/after counts are
698
+ estimates and explicitly set `tokenCountsExact: false`.
699
+
696
700
  ### Built-in tools
697
701
 
698
702
  The agent kernel's managed tools are `Read`, `Write`, `Edit`, `Glob`, `Grep`, `Bash`, `NodeRepl`, `WebFetch`, and `WebSearch`. `NodeRepl({ code })` is backed by one lazily started Node.js REPL child per run. You select them via `allowedTools`. Tool implementations honor:
@@ -784,7 +788,7 @@ console.log(metrics.snapshot());
784
788
  // cache: { hits, misses, hitRatio, readTokensFromEvents },
785
789
  // tools: { callsByName: { Bash: 3, Read: 2 }, errorsByName: { ... } },
786
790
  // errors: { total, byKind: { provider_unavailable: 1 } },
787
- // turns: { count, latencyMsP50, latencyMsP95 },
791
+ // turns: { count, sampleCount, latencyMsP50, latencyMsP95 },
788
792
  // approvals: { pending, granted, denied },
789
793
  // }
790
794
  ```
@@ -868,6 +872,10 @@ correction, while learned evidence may still lower it.
868
872
  Runs report `context_compaction_applied: true` (fired), `false` (enabled but not needed),
869
873
  or `null` (disabled), plus request-estimate, fixed-overhead, reactive-attempted,
870
874
  tokens-after, and reduced diagnostics.
875
+ Every attempt also emits one `context_compaction` start and exactly one terminal
876
+ lifecycle event. A successful retry then emits a new exact `context_usage`
877
+ snapshot, allowing consumers to discard the pre-compaction value rather than
878
+ guessing the resulting occupancy from the compaction estimate.
871
879
  Persistent overflow is classified as `context_limit`, allowing the fallback router to
872
880
  try the next configured model. The other backends manage their windows per their own behavior.
873
881
  (`docs/reference/feature-registry.md` is the source of truth for this row.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, and Pi SDK bridges out of the box",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -39,10 +39,6 @@
39
39
  "types": "./types/ai/cost.d.ts",
40
40
  "default": "./src/ai/cost.js"
41
41
  },
42
- "./ai/backend.js": {
43
- "types": "./types/ai/backend.d.ts",
44
- "default": "./src/ai/backend.js"
45
- },
46
42
  "./ai/runtime/model-refs.js": {
47
43
  "types": "./types/ai/runtime/model-refs.d.ts",
48
44
  "default": "./src/ai/runtime/model-refs.js"
@@ -147,6 +143,7 @@
147
143
  },
148
144
  "scripts": {
149
145
  "build": "tsc -p tsconfig.types.json",
150
- "test": "vitest run --passWithNoTests"
146
+ "typecheck": "tsc -p tsconfig.types.json --noEmit",
147
+ "test": "vitest run"
151
148
  }
152
149
  }
package/src/ai/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // Public surface of the provider layer.
2
2
 
3
- export * from "./registry.js";
4
3
  export * from "./runtime/model-refs.js";
5
4
  export * from "./runtime/registry.js";
6
5
  export {
@@ -82,10 +82,11 @@ function addObserver(list, observer) {
82
82
  // cache: { hits, misses, hitRatio }, // hitRatio in [0,1]; null if no signal
83
83
  // tools: { callsByName: { ... }, errorsByName: { ... } },
84
84
  // errors: { total, byKind: { ... } },
85
- // turns: { count, latencyMsP50, latencyMsP95 },
85
+ // turns: { count, sampleCount, latencyMsP50, latencyMsP95 },
86
86
  // approvals: { pending, granted, denied },
87
87
  // }
88
- export function createMetricsObserver({ name = "metrics" } = {}) {
88
+ export function createMetricsObserver({ name = "metrics", maxLatencySamples = 2_048 } = {}) {
89
+ const latencySampleLimit = normalizePositiveInteger(maxLatencySamples, 2_048);
89
90
  const state = {
90
91
  eventsTotal: 0,
91
92
  eventsByType: new Map(),
@@ -99,7 +100,8 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
99
100
  errorTotal: 0,
100
101
  errorsByKind: new Map(),
101
102
  turnLatencies: [],
102
- turnStartByModel: new Map(),
103
+ turnLatencyCount: 0,
104
+ pendingTurnStarts: [],
103
105
  approvalPending: 0,
104
106
  approvalGranted: 0,
105
107
  approvalDenied: 0,
@@ -167,17 +169,24 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
167
169
  }
168
170
 
169
171
  if (type === "provider_request_started" && event.model) {
170
- state.turnStartByModel.set(event.model, (Number.isFinite(event.timestamp) ? event.timestamp : Date.now()));
172
+ state.pendingTurnStarts.push({
173
+ key: latencyEventKey(event),
174
+ model: String(event.model),
175
+ timestamp: Number.isFinite(event.timestamp) ? event.timestamp : Date.now(),
176
+ });
177
+ if (state.pendingTurnStarts.length > latencySampleLimit) state.pendingTurnStarts.shift();
171
178
  }
172
179
  if (type === "provider_request_completed" && event.model) {
173
- const started = state.turnStartByModel.get(event.model);
174
- if (started !== undefined) {
175
- state.turnLatencies.push(Math.max(0, ((Number.isFinite(event.timestamp) ? event.timestamp : Date.now())) - started));
176
- state.turnStartByModel.delete(event.model);
180
+ const startIndex = matchingStartIndex(state.pendingTurnStarts, event);
181
+ const started = startIndex === -1 ? undefined : state.pendingTurnStarts.splice(startIndex, 1)[0];
182
+ const explicitDuration = Number(event.durationMs);
183
+ if (Number.isFinite(explicitDuration)) recordTurnLatency(Math.max(0, explicitDuration));
184
+ else if (started !== undefined) {
185
+ recordTurnLatency(Math.max(0, ((Number.isFinite(event.timestamp) ? event.timestamp : Date.now())) - started.timestamp));
177
186
  }
178
187
  }
179
188
  if (type === "turn_latency" && Number.isFinite(Number(event.durationMs))) {
180
- state.turnLatencies.push(Number(event.durationMs));
189
+ recordTurnLatency(Math.max(0, Number(event.durationMs)));
181
190
  }
182
191
 
183
192
  if (type === "tool_approval_pending") state.approvalPending += 1;
@@ -187,6 +196,12 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
187
196
 
188
197
  function recordMetric() { /* future hook */ }
189
198
 
199
+ function recordTurnLatency(durationMs) {
200
+ state.turnLatencyCount += 1;
201
+ state.turnLatencies.push(durationMs);
202
+ if (state.turnLatencies.length > latencySampleLimit) state.turnLatencies.shift();
203
+ }
204
+
190
205
  function snapshot() {
191
206
  const cacheTotal = state.cacheHits + state.cacheMisses;
192
207
  const hitRatio = cacheTotal > 0 ? state.cacheHits / cacheTotal : null;
@@ -205,7 +220,7 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
205
220
  errorsByName: Object.fromEntries(state.toolErrorsByName),
206
221
  },
207
222
  errors: { total: state.errorTotal, byKind: Object.fromEntries(state.errorsByKind) },
208
- turns: percentilesFor(state.turnLatencies),
223
+ turns: percentilesFor(state.turnLatencies, state.turnLatencyCount),
209
224
  approvals: { pending: state.approvalPending, granted: state.approvalGranted, denied: state.approvalDenied },
210
225
  };
211
226
  }
@@ -213,16 +228,36 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
213
228
  return { name, recordEvent, recordMetric, snapshot };
214
229
  }
215
230
 
216
- function percentilesFor(samples) {
231
+ function percentilesFor(samples, count = samples.length) {
217
232
  const arr = Array.isArray(samples) ? samples.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b) : [];
218
- if (!arr.length) return { count: 0, latencyMsP50: null, latencyMsP95: null };
233
+ if (!arr.length) return { count, sampleCount: 0, latencyMsP50: null, latencyMsP95: null };
219
234
  return {
220
- count: arr.length,
235
+ count,
236
+ sampleCount: arr.length,
221
237
  latencyMsP50: percentile(arr, 0.5),
222
238
  latencyMsP95: percentile(arr, 0.95),
223
239
  };
224
240
  }
225
241
 
242
+ function latencyEventKey(event) {
243
+ const requestId = event.requestId ?? event.turnId;
244
+ return requestId === undefined || requestId === null ? undefined : String(requestId);
245
+ }
246
+
247
+ function matchingStartIndex(starts, event) {
248
+ const key = latencyEventKey(event);
249
+ if (key !== undefined) {
250
+ const keyedIndex = starts.findIndex((start) => start.key === key);
251
+ if (keyedIndex !== -1) return keyedIndex;
252
+ }
253
+ return starts.findIndex((start) => start.model === String(event.model));
254
+ }
255
+
256
+ function normalizePositiveInteger(value, fallback) {
257
+ const number = Number(value);
258
+ return Number.isSafeInteger(number) && number > 0 ? number : fallback;
259
+ }
260
+
226
261
  function percentile(sortedArr, q) {
227
262
  if (!sortedArr.length) return null;
228
263
  const rank = q * (sortedArr.length - 1);
@@ -83,7 +83,8 @@ const DORMANT_CLI_CAPABILITIES = {
83
83
  supports_mcp: true,
84
84
  supports_skills: true,
85
85
  supports_builtin_tools: true,
86
- supports_live_input: true,
86
+ // The one-shot CLI bridge cannot add stdin messages after process launch.
87
+ supports_live_input: false,
87
88
  supports_native_subagents: true,
88
89
  };
89
90
 
@@ -776,18 +777,6 @@ export async function generateCliResponse(systemPrompt, options = {}) {
776
777
  }
777
778
  }
778
779
 
779
- export const claudeCodeBackend = {
780
- kind: "claude-code",
781
- capabilities: { kind: "claude-code", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
782
- execute: generateCliResponse,
783
- };
784
-
785
- export const codexCliBackend = {
786
- kind: "codex-cli",
787
- capabilities: { kind: "codex-cli", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
788
- execute: generateCliResponse,
789
- };
790
-
791
780
  // CLI bridge for sdk='claude' agents that opt into execution_mode='cli'.
792
781
  // generateCliResponse internally branches on resolved.sdk; the SDK shape
793
782
  // from parseModelReference uses 'claude', the CLI builder expects
@@ -563,7 +563,18 @@ function createClaudeCanUseTool(approvalManager, modelName) {
563
563
  async function* livePromptMessages({ initialPrompt, liveInput, sessionId, prompts }) {
564
564
  yield makeSdkUserMessage(initialPrompt, sessionId);
565
565
  for await (const message of liveInput) {
566
- yield makeSdkUserMessage(formatLiveInputGuidance(message.body, prompts), sessionId, message.id || randomUUID());
566
+ try {
567
+ const sdkMessage = makeSdkUserMessage(
568
+ formatLiveInputGuidance(message.body, prompts),
569
+ sessionId,
570
+ message.id || randomUUID(),
571
+ );
572
+ message.acknowledge?.();
573
+ yield sdkMessage;
574
+ } catch (err) {
575
+ message.reject?.(err);
576
+ throw err;
577
+ }
567
578
  }
568
579
  }
569
580
 
@@ -1051,12 +1062,6 @@ export async function generateClaudeResponse(systemPrompt, options) {
1051
1062
  };
1052
1063
  }
1053
1064
 
1054
- export const claudeSdkBackend = {
1055
- kind: "claude",
1056
- capabilities: runtimeCapabilities("claude"),
1057
- execute: generateClaudeResponse,
1058
- };
1059
-
1060
1065
  export const claudeRuntimeBridge = {
1061
1066
  id: "claude",
1062
1067
  kind: "claude",