@mono-agent/agent-runtime 0.13.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/README.md CHANGED
@@ -1,15 +1,104 @@
1
1
  # @mono-agent/agent-runtime
2
2
 
3
+ Use this package when you need direct, capability-aware access to mono-agent's
4
+ five built-in model runtime bridges.
5
+
3
6
  ## Category
4
7
 
8
+ <!-- package-metadata:start -->
9
+ <!-- Generated by scripts/generate-package-docs.mjs. Do not edit by hand. -->
10
+
5
11
  Category: `runtime`
12
+ Tier: `core`
13
+ Catalog responsibility: Provides five runtime bridges (Claude SDK, Claude Code CLI, Codex app-server, OpenCode app-server, Pi SDK); direct OpenCode requires stable CLI >=1.15.0 on PATH.
14
+
15
+ <!-- package-metadata:end -->
6
16
 
7
17
  ## Responsibility
8
18
 
9
19
  Provides five runtime bridges (Claude SDK, Claude Code CLI, Codex app-server, OpenCode app-server, Pi SDK), with capabilities declared per bridge. This is the runtime layer that `@mono-agent/runtime-adapter` wraps behind runtime contracts. Pi enforces optional mono-agent sandbox policy for runtime-owned tools through an injectable `RuntimeSandbox` seam (a fail-closed passthrough by default; `@mono-agent/runtime-adapter` injects the real implementation). The router supports a compatibility-preserving uniform contract or explicit isolated per-route-native contracts; no provider route silently drops required capabilities.
10
20
 
21
+ ## Install / Usage
22
+
23
+ ```bash
24
+ pnpm add @mono-agent/agent-runtime
25
+ ```
26
+
27
+ Node.js 22.19 or newer is required. The Claude Code, Codex, and direct
28
+ OpenCode bridges also require their matching CLI on `PATH`; direct OpenCode
29
+ requires stable OpenCode 1.15.0 or newer. SDK-only Claude and Pi runs do not
30
+ spawn those CLIs.
31
+
32
+ Create one runtime for a host, parse a model reference, and run a turn:
33
+
34
+ ```js
35
+ import {
36
+ createRuntime,
37
+ parseRuntimeModelReference,
38
+ } from "@mono-agent/agent-runtime";
39
+
40
+ const runtime = createRuntime({ workspace: process.cwd() });
41
+ const result = await runtime.run("You are a concise repository assistant.", {
42
+ model: parseRuntimeModelReference("claude:claude-sonnet-4-6"),
43
+ executionMode: "sdk",
44
+ messages: [{ role: "user", content: "Summarize README.md." }],
45
+ cwd: process.cwd(),
46
+ allowedTools: ["Read"],
47
+ });
48
+
49
+ if (result.error) throw new Error(result.error);
50
+ console.log(result.text);
51
+ ```
52
+
53
+ `Glob` and `Grep` prefer the packaged `@vscode/ripgrep` binary on supported
54
+ platforms. An explicit `ripgrepPath` wins, with `PATH` as the final fallback.
55
+
56
+ ## Architecture
57
+
58
+ The package uses a fixed registry of bridge descriptors and loads provider code
59
+ only after a run selects a matching model reference and execution mode:
60
+
61
+ ### Data flow
62
+
63
+ 1. `createRuntime()` binds host callbacks and creates an isolated tool context.
64
+ 2. `resolveRuntimeBridge()` checks the five static bridge descriptors in order.
65
+ 3. The selected descriptor lazily imports its provider implementation.
66
+ 4. The bridge prepares the runtime inputs it supports, including managed or MCP
67
+ tools only where that bridge can represent them, streams normalized events,
68
+ and returns a provider-neutral `RuntimeResult`.
69
+ 5. The host validates any domain-specific result and owns persistence or UI
70
+ effects.
71
+
72
+ ### Package structure
73
+
74
+ | Source area | Responsibility |
75
+ | --- | --- |
76
+ | `src/runtime.js` | Host binding, per-instance tool context, bridge dispatch, and observer flushing |
77
+ | `src/ai/runtime/` | Model-reference parsing, the lazy bridge registry, capabilities, sessions, and fallback routing |
78
+ | `src/ai/providers/` | Claude SDK/CLI, Codex app-server, OpenCode app-server, and Pi SDK integrations |
79
+ | `src/agent/tools/` | Managed tools, MCP adaptation, output limits, and the injectable sandbox seam |
80
+ | `src/agent/` | Approvals, allowlists, transcript snapshots, and compaction policy helpers |
81
+
82
+ The detailed lifecycle, provider-session differences, and host boundary are in
83
+ the [architecture guide](https://github.com/robertsreberski/mono-agent/blob/main/packages/agent-runtime/ARCHITECTURE.md).
84
+
11
85
  ## Public API
12
86
 
87
+ ### Start here
88
+
89
+ | API | Use it for |
90
+ | --- | --- |
91
+ | `createRuntime()` | Run one model bridge with host-owned credentials, observers, tools, and lifecycle callbacks |
92
+ | `createRouterRuntime()` | Retry an ordered model chain while preserving explicit route-safety contracts |
93
+ | `parseRuntimeModelReference()` | Convert a canonical `claude:`, `codex:`, `opencode:`, or `pi:` string into the object required by `run()` |
94
+ | `listRuntimeBridges()` / `runtimeCapabilities()` | Inspect the five built-in bridge descriptors without loading provider implementations |
95
+ | `createPiOAuthApiKeyResolver()` | Bind a host-owned Pi auth file with refresh-safe writes |
96
+ | `createMetricsObserver()` | Aggregate normalized event, token, cache, cost, tool, error, turn, and approval metrics |
97
+
98
+ Most hosts should use `@mono-agent/runtime-adapter` instead of importing deep
99
+ runtime surfaces. The exhaustive inventory below is generated from the package
100
+ export map.
101
+
13
102
  <!-- public-api-inventory:start -->
14
103
  <!-- Generated by scripts/generate-public-api-docs.mjs. Do not edit by hand. -->
15
104
 
@@ -53,13 +142,11 @@ discoverClaudeSdkModels
53
142
  disposeAllProviderSessions
54
143
  disposeProviderSession
55
144
  executionModeIncompatibilityReason
56
- findProviderForModel
57
145
  generatePiNativeResponse
58
146
  inferAllowlistMode
59
147
  invalidateProviderSession
60
148
  isLikelyContextTermination
61
149
  isModelCompatibleWithExecutionMode
62
- listProviders
63
150
  listRuntimeBridges
64
151
  normalizeAllowlistMode
65
152
  normalizeClaudeSdkCatalog
@@ -222,11 +309,9 @@ discoverClaudeSdkModels
222
309
  disposeAllProviderSessions
223
310
  disposeProviderSession
224
311
  executionModeIncompatibilityReason
225
- findProviderForModel
226
312
  generatePiNativeResponse
227
313
  invalidateProviderSession
228
314
  isModelCompatibleWithExecutionMode
229
- listProviders
230
315
  listRuntimeBridges
231
316
  normalizeClaudeSdkCatalog
232
317
  normalizeClaudeSdkModelId
@@ -241,15 +326,6 @@ syncProviderSession
241
326
  toolCompactionAppliedFromWarnings
242
327
  ```
243
328
 
244
- **`@mono-agent/agent-runtime/ai/backend.js`**
245
-
246
- ```text
247
- BACKEND_CAPABILITIES
248
- backendCapabilities
249
- backendSupportsSessionResume
250
- backendUsesExecenvConfig
251
- ```
252
-
253
329
  **`@mono-agent/agent-runtime/ai/cost.js`**
254
330
 
255
331
  ```text
@@ -299,9 +375,7 @@ formatLiveInputGuidance
299
375
 
300
376
  ```text
301
377
  buildCliCommand
302
- claudeCodeBackend
303
378
  claudeCodeRuntimeBridge
304
- codexCliBackend
305
379
  createThinkingBuffer
306
380
  generateCliResponse
307
381
  normalizeCliEvent
@@ -325,7 +399,6 @@ normalizeClaudeSdkModelId
325
399
  ```text
326
400
  claudeEffortOptions
327
401
  claudeRuntimeBridge
328
- claudeSdkBackend
329
402
  claudeSdkModelForQuery
330
403
  generateClaudeResponse
331
404
  toolPayloadLimit
@@ -334,7 +407,6 @@ toolPayloadLimit
334
407
  **`@mono-agent/agent-runtime/ai/providers/codex-app.js`**
335
408
 
336
409
  ```text
337
- codexAppBackend
338
410
  codexAppRuntimeBridge
339
411
  createCodexAppServerClient
340
412
  generateCodexAppResponse
@@ -403,78 +475,7 @@ normalizeCodexItemType
403
475
 
404
476
  <!-- public-api-inventory:end -->
405
477
 
406
- ## Dependency Boundary
407
-
408
- Depends on external provider SDKs only (`@anthropic-ai/claude-agent-sdk`, `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, `@modelcontextprotocol/sdk`, `@opencode-ai/sdk`, `zod`) — **zero `@mono-agent/*` workspace-package dependencies**. Sandbox enforcement for runtime-owned command preparation and network/path policy checks is an injectable `RuntimeSandbox` seam; `@mono-agent/runtime-adapter` wires in the real sandbox implementation automatically for mono-agent hosts.
409
-
410
- ## What This Package Does Not Own
411
-
412
- - runtime contracts and backend descriptors (`@mono-agent/runtime-adapter`)
413
- - Conversation history, context building, or host-side session TTL policy (`@mono-agent/agent-harness`)
414
- - Host configuration (`@mono-agent/config`, `@mono-agent/agent-app`)
415
-
416
- ## Verification
417
-
418
- ```bash
419
- pnpm --filter @mono-agent/agent-runtime run test
420
- ```
421
-
422
- ## Overview
423
-
424
- Generic agent runtime that supports five bridges out of the box:
425
-
426
- - **Claude SDK** (`@anthropic-ai/claude-agent-sdk` 0.3.206)
427
- - **Claude Code CLI** (the `claude` binary)
428
- - **Pi SDK** (`@earendil-works/pi-agent-core`, used for OpenAI / Codex / Gemini / OpenRouter / Ollama / etc. via Pi providers)
429
- - **Codex CLI** (the `codex` app-server)
430
- - **OpenCode CLI** (an isolated `opencode` app-server driven through `@opencode-ai/sdk/v2`)
431
-
432
- Hosts wire in their own pricing, persistence, and credential callbacks (plus an `onCompactionRecorded` hook that fires on every automatic compaction — proactive or reactive — the pi bridge drives; see "Context compaction"). The runtime returns raw text + raw structured output; hosts that want a domain-specific contract parse it on their end.
433
-
434
- See [ARCHITECTURE.md](./ARCHITECTURE.md) for the package boundary, runtime
435
- selection flow, lifecycle diagrams, and host responsibilities. Upgrading from
436
- `0.3.x`? See [MIGRATION.md](./MIGRATION.md) for the Pi-native bridge, removed run
437
- options, durable-session resume semantics, and fallback-router changes.
438
-
439
- ## Install / Usage
440
-
441
- ```bash
442
- npm install @mono-agent/agent-runtime
443
- ```
444
-
445
- Peer requirements:
446
-
447
- - Node.js ≥ 22.19.0
448
- - `claude` CLI on PATH (only for `executionMode: "cli"` with `claude` SDK)
449
- - `codex` CLI on PATH (only for `executionMode: "cli"` with `codex` SDK; override via the `codexAppServerCommand` option)
450
- - stable `opencode` CLI >= 1.15.0 on PATH (only for direct `opencode:<provider>:<model>` refs)
451
- - `Glob` and `Grep` use the packaged `@vscode/ripgrep` binary on supported platforms. An explicit `ripgrepPath` is authoritative and PATH remains a fallback; provide one of those when optional dependencies are omitted or the platform is unsupported.
452
-
453
- ## Quick start
454
-
455
- ```js
456
- import { createRuntime } from "@mono-agent/agent-runtime";
457
-
458
- const runtime = createRuntime({
459
- // Host integration (all optional)
460
- workspace: "/path/to/repo",
461
- ripgrepPath: "/usr/bin/rg",
462
- });
463
-
464
- const result = await runtime.run("You are a helpful assistant.", {
465
- model: { sdk: "claude", model: "claude-sonnet-4-6" },
466
- executionMode: "sdk",
467
- messages: [{ role: "user", content: "Read README.md and summarize it." }],
468
- cwd: "/path/to/repo",
469
- allowedTools: ["Read", "Bash"],
470
- maxTurns: 10,
471
- onEvent: (event) => console.log(event.type),
472
- });
473
-
474
- console.log(result.text);
475
- ```
476
-
477
- ## When to reach for this vs. other JS agent runtimes
478
+ ### When to reach for this vs. other JS agent runtimes
478
479
 
479
480
  `@mono-agent/agent-runtime` is purpose-built for **autonomous, long-running agent work** with provider portability and operational resilience as first-class concerns. It is *not* a streaming-chat UI kit. Where each peer fits:
480
481
 
@@ -510,7 +511,7 @@ console.log(result.text);
510
511
 
511
512
  Honest summary: if the agent runs **without a human watching the screen** for minutes-to-hours and **must survive provider blips**, this is the right tool. If a human is watching a streaming chat, Vercel's SDK is the right tool. Both can coexist in the same app.
512
513
 
513
- ## Picking a backend
514
+ ### Picking a backend
514
515
 
515
516
  The runtime picks a backend from `options.model` + `options.executionMode`:
516
517
 
@@ -518,13 +519,15 @@ The runtime picks a backend from `options.model` + `options.executionMode`:
518
519
  |---|---|---|
519
520
  | `"claude"` | `"sdk"` (or omitted) | Claude SDK |
520
521
  | `"claude"` | `"cli"` | `claude` CLI |
521
- | `"pi"` | any | Pi SDK |
522
+ | `"pi"` | `"sdk"` (or omitted) | Pi SDK |
522
523
  | `"codex"` | `"cli"` | Codex app-server CLI |
523
524
  | `"opencode"` | `"cli"` | Isolated OpenCode app-server CLI |
524
525
 
525
- A `model` reference can be the parsed shape `{ sdk, model, provider? }` or a string (`"pi:openai:gpt-5.5"`, `"claude:claude-sonnet-4-6"`, etc.) that you parse with the package's `parseRuntimeModelReference` helper.
526
+ A `model` is a parsed `{ sdk, model, provider? }` object. Convert canonical
527
+ strings such as `"pi:openai:gpt-5.5"` with
528
+ `parseRuntimeModelReference()` before calling `run()`.
526
529
 
527
- ## `createRuntime(host)`
530
+ ### `createRuntime(host)`
528
531
 
529
532
  Pass host-level integration once at boot. All keys are optional.
530
533
 
@@ -608,13 +611,13 @@ Returns:
608
611
  - `retireDurableSession(id, sessionsRoot)` — delete and verify every exact-id durable Pi transcript, including cold duplicates.
609
612
  - `disposeSession(id)` / `invalidateSession(id)` / `disposeAllSessions()` — ordinary best-effort eviction, destructive live invalidation, and shutdown cleanup.
610
613
 
611
- ### `runtime.run(systemPrompt, options)`
614
+ #### `runtime.run(systemPrompt, options)`
612
615
 
613
616
  Per-call options (a non-exhaustive selection):
614
617
 
615
618
  | Option | Type | Notes |
616
619
  |---|---|---|
617
- | `model` | `object \| string` | **Required.** See "Picking a backend". |
620
+ | `model` | `RuntimeModelRef` | **Required.** Pass the object returned by `parseRuntimeModelReference()`; `run()` does not parse strings. |
618
621
  | `executionMode` | `"sdk" \| "cli"` | Default `"sdk"`. |
619
622
  | `messages` | `Message[]` | Conversation history. |
620
623
  | `cwd` | `string` | Working directory for the agent's tools. |
@@ -623,9 +626,9 @@ Per-call options (a non-exhaustive selection):
623
626
  | `mcpServers` | `Record<string, McpServerConfig>` | Configured MCP servers (stdio / sse / http). |
624
627
  | `sandboxPolicy` | `SandboxPolicy` | Optional fail-closed sandbox policy for built-in tools and stdio MCP process startup. |
625
628
  | `maxTurns` | `number` | Hard cap on agent turns. |
626
- | `outputSchema` | `JSONSchema` | If set, the agent is asked to produce structured JSON matching this schema. The result lands in `result.structuredResult`. |
629
+ | `outputSchema` | `JSONSchema` | Requests structured JSON on capable bridges; see “Structured output” below for bridge-specific return behavior. |
627
630
  | `abortSignal` | `AbortSignal` | Cancel the run. |
628
- | `liveInput` | `LiveInputQueue` | Stream of in-flight user messages (for human-in-the-loop steering). |
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. |
629
632
  | `onEvent` | `(event) => void` | Fired for every event the provider emits (assistant text, tool calls/results, runtime warnings, structured output). |
630
633
  | `runId` | `string` | Tag this run for downstream callbacks (e.g. `onCompactionRecorded`). |
631
634
  | `providerSessionId` | `string` | Resume a prior provider session. |
@@ -633,12 +636,16 @@ Per-call options (a non-exhaustive selection):
633
636
  | `codexAppServerCommand` | `string` | Override the Codex CLI binary. |
634
637
  | `codexAppServerArgs` | `string[]` | Override the Codex CLI arguments. |
635
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
+
636
643
  Returns:
637
644
 
638
645
  ```ts
639
646
  {
640
647
  text: string, // raw assistant text
641
- structuredResult?: any, // JSON returned via outputSchema (if any)
648
+ structuredResult?: any, // captured JSON on supported bridges
642
649
  structuredResultSource?: string, // where structuredResult came from
643
650
  events: RuntimeEvent[], // full event stream (for host-side parsing)
644
651
  usage: {
@@ -650,7 +657,7 @@ Returns:
650
657
  numTurns: number,
651
658
  model: string,
652
659
  effort: string,
653
- sdk: "claude" | "pi" | "codex",
660
+ sdk: "claude" | "pi" | "codex" | "opencode",
654
661
  cancelled: boolean,
655
662
  error: string | null,
656
663
  errorDetails: object | null,
@@ -673,7 +680,24 @@ Returns:
673
680
 
674
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.
675
682
 
676
- ## Built-in tools
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
+
700
+ ### Built-in tools
677
701
 
678
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:
679
703
 
@@ -685,13 +709,19 @@ The agent kernel's managed tools are `Read`, `Write`, `Edit`, `Glob`, `Grep`, `B
685
709
 
686
710
  Override or extend the tool surface by passing `mcpServers` for MCP-backed tools.
687
711
 
688
- ## Structured output
712
+ ### Structured output
689
713
 
690
- Pass `options.outputSchema` (a JSON Schema). On Claude SDK / Codex app-server / Pi SDK, the runtime wires the schema into the provider's structured-output API. The matched JSON lands in `result.structuredResult`.
714
+ Pass `options.outputSchema` (a JSON Schema). Claude SDK, Claude CLI, and Pi SDK
715
+ surface captured JSON as `result.structuredResult`. Codex app-server receives
716
+ the schema and reports that structured output was enforced, but its bridge
717
+ returns provider text rather than parsing `structuredResult`; hosts must parse
718
+ and validate `result.text`. Direct OpenCode rejects `outputSchema` with a typed
719
+ capability mismatch.
691
720
 
692
- The package does **not** validate `structuredResult` against your schema — it only forwards what the provider produced. Hosts run their own validation (Zod, AJV, etc.).
721
+ The package does **not** validate captured output against your schema. Hosts run
722
+ their own validation (Zod, AJV, and similar) before applying domain effects.
693
723
 
694
- ## Provider fallback router
724
+ ### Provider fallback router
695
725
 
696
726
  `createRouterRuntime({ host, chain, routeSafety, resolveAttempt })` wraps the standard runtime with an ordered chain of model references. On a retryable provider/auth failure it retries the logical run against the next entry with one bounded transcript-tail snapshot. A chain is stateless across provider sessions. Entry `effort` is tri-state: a string fixes that route, `null` asks for provider default, and omission inherits the legacy per-run effort.
697
727
 
@@ -736,7 +766,7 @@ Behaviour:
736
766
 
737
767
  Chain entries can require backend capabilities via `requires: { structured_output: true, supports_mcp: true, ... }`; entries that don't satisfy the requirements are skipped (logged in `failoverHistory` as `failureKind: "skipped_capability_mismatch"`).
738
768
 
739
- ## Observers & metrics
769
+ ### Observers & metrics
740
770
 
741
771
  The runtime emits structured events for everything that happens during a run — assistant messages, tool calls, runtime warnings, cache hits/misses, cost updates, provider request start/end, approval lifecycle. Hosts can subscribe via `host.observers[]` (any number) or the simpler `options.onEvent` callback (one subscriber). Both work simultaneously.
742
772
 
@@ -758,7 +788,7 @@ console.log(metrics.snapshot());
758
788
  // cache: { hits, misses, hitRatio, readTokensFromEvents },
759
789
  // tools: { callsByName: { Bash: 3, Read: 2 }, errorsByName: { ... } },
760
790
  // errors: { total, byKind: { provider_unavailable: 1 } },
761
- // turns: { count, latencyMsP50, latencyMsP95 },
791
+ // turns: { count, sampleCount, latencyMsP50, latencyMsP95 },
762
792
  // approvals: { pending, granted, denied },
763
793
  // }
764
794
  ```
@@ -771,7 +801,7 @@ Notable new events emitted by the bridges:
771
801
  - `cache_hit` / `cache_miss` — when the provider reports cached / cache-creation input tokens.
772
802
  - `cost_accumulated` — running cost in USD with cumulative token breakdown.
773
803
 
774
- ## Approval gates (human-in-the-loop)
804
+ ### Approval gates (human-in-the-loop)
775
805
 
776
806
  Pass `onToolApprovalRequest` to gate tool calls behind a runtime approval. The runtime calls your callback once per tool invocation whose risk tier requires it, and pauses the agent until you respond.
777
807
 
@@ -812,7 +842,7 @@ Approval lifecycle is observable via `onEvent`:
812
842
  - `tool_approval_granted` — host approved.
813
843
  - `tool_approval_denied` — host denied, timed out, threw, or no callback for a high-risk tool.
814
844
 
815
- ## Tool-result bloat handling
845
+ ### Tool-result bloat handling
816
846
 
817
847
  The kernel's tool-bloat guard (`agent/tool-bloat.js`, internal) enforces a 256 KB default cap per `tool_result`. When a payload exceeds the cap, the kernel:
818
848
 
@@ -822,7 +852,7 @@ The kernel's tool-bloat guard (`agent/tool-bloat.js`, internal) enforces a 256 K
822
852
 
823
853
  Hosts that don't supply `persistArtifact` get the truncation summary but no on-disk capture.
824
854
 
825
- ## Context compaction
855
+ ### Context compaction
826
856
 
827
857
  The sole pi bridge runs on pi-agent-core's native `AgentHarness`. pi performs **no**
828
858
  automatic in-loop compaction, so the bridge drives it: before each turn it estimates the
@@ -842,6 +872,10 @@ correction, while learned evidence may still lower it.
842
872
  Runs report `context_compaction_applied: true` (fired), `false` (enabled but not needed),
843
873
  or `null` (disabled), plus request-estimate, fixed-overhead, reactive-attempted,
844
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.
845
879
  Persistent overflow is classified as `context_limit`, allowing the fallback router to
846
880
  try the next configured model. The other backends manage their windows per their own behavior.
847
881
  (`docs/reference/feature-registry.md` is the source of truth for this row.)
@@ -856,7 +890,7 @@ summary output `clamp(floor(W × 0.04), 2000, 12000)`. Explicit values retain th
856
890
  scalar validation bounds. `onCompactionRecorded(record)` fires only for accepted,
857
891
  persisted automatic compactions.
858
892
 
859
- ## Advanced exports
893
+ ### Advanced exports
860
894
 
861
895
  The package exposes a fixed set of inner pieces via subpath imports. The
862
896
  `exports` map is explicit (no `./ai/*` / `./agent/*` wildcards): only the mapped
@@ -875,10 +909,43 @@ import { configureToolRuntime, readToolRuntime } from "@mono-agent/agent-runtime
875
909
 
876
910
  These are stable but treated as advanced API. Most consumers should reach for `createRuntime` first.
877
911
 
878
- ## Example consumer
912
+ ## Dependency Boundary
913
+
914
+ This package has zero `@mono-agent/*` workspace dependencies. Its runtime
915
+ dependencies are `@anthropic-ai/claude-agent-sdk`, `@anthropic-ai/sdk`,
916
+ `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`,
917
+ `@modelcontextprotocol/sdk`, `@opencode-ai/sdk`, `@vscode/ripgrep`,
918
+ `cross-spawn`, and `zod`.
879
919
 
880
- See [`examples/echo-agent/`](../../examples/echo-agent/) for a runnable consumer that imports `@mono-agent/agent-runtime`, runs a single Claude SDK turn with the Bash tool, and prints the result.
920
+ Sandbox enforcement is an injectable `RuntimeSandbox` seam.
921
+ `@mono-agent/runtime-adapter` supplies the mono-agent implementation; a direct
922
+ consumer that configures a sandbox policy must inject an implementation or the
923
+ runtime fails closed.
881
924
 
882
- ## License
925
+ ## What This Package Does Not Own
883
926
 
884
- GPL-3.0-only.
927
+ - Typed host-facing runtime contracts and SRT process wrapping, owned by
928
+ `@mono-agent/runtime-adapter`.
929
+ - Conversation history, context assembly, memory coordination, or host-side
930
+ session policy, owned by `@mono-agent/agent-harness`.
931
+ - Configuration loading, communication channels, domain result validation, UI,
932
+ and host persistence.
933
+
934
+ ## Related Documentation
935
+
936
+ - [Runtime and providers](https://mono-agent-docs.vercel.app/runtime/) explains the
937
+ config-first model and backend choices.
938
+ - [Backends and model references](https://mono-agent-docs.vercel.app/runtime/backends/)
939
+ documents all five bridges and their execution modes.
940
+ - [Programmatic approvals and structured output](https://mono-agent-docs.vercel.app/programmatic/approval-and-structured-output/)
941
+ shows the code-only host hooks.
942
+ - [Architecture](https://github.com/robertsreberski/mono-agent/blob/main/packages/agent-runtime/ARCHITECTURE.md)
943
+ and [migration guide](https://github.com/robertsreberski/mono-agent/blob/main/packages/agent-runtime/MIGRATION.md)
944
+ cover internal flow and upgrades from `0.3.x`.
945
+
946
+ ## Verification
947
+
948
+ ```bash
949
+ pnpm --filter @mono-agent/agent-runtime run build
950
+ pnpm --filter @mono-agent/agent-runtime run test
951
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.13.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
  }
@@ -2,8 +2,10 @@
2
2
  // callback that can approve, deny, or "always approve" (session-scoped
3
3
  // allowlist). Adapted from zeroclaw's ApprovalManager pattern.
4
4
  //
5
- // Hosts opt in by passing `onToolApprovalRequest` to createRuntime. When the
6
- // callback is not supplied, the gate falls back to per-tier defaults:
5
+ // Runtime bridges opt in by passing `onToolApprovalRequest`; the Claude SDK and
6
+ // Pi bridges do not install this manager when that callback is absent. A host
7
+ // that calls `createApprovalManager` directly without a callback gets these
8
+ // low-level per-tier defaults:
7
9
  // low risk → auto-approve
8
10
  // medium risk → auto-approve (no host means "don't pause")
9
11
  // high risk → deny (fail closed)
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);