@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/ARCHITECTURE.md +62 -20
- package/MIGRATION.md +44 -36
- package/README.md +181 -114
- package/package.json +3 -6
- package/src/agent/approval.js +4 -2
- package/src/ai/index.js +0 -1
- package/src/ai/observer.js +48 -13
- package/src/ai/providers/claude-cli.js +2 -13
- package/src/ai/providers/claude-sdk.js +12 -7
- package/src/ai/providers/codex-app.js +205 -55
- package/src/ai/providers/opencode-app.js +168 -3
- package/src/ai/providers/pi-messages.js +0 -8
- package/src/ai/providers/pi-native/compaction-driver.js +106 -10
- package/src/ai/providers/pi-native/result-builder.js +2 -14
- package/src/ai/providers/pi-native/stream-subscriber.js +20 -2
- package/src/ai/providers/pi-native/turn-runner.js +31 -8
- package/src/ai/providers/pi-native.js +2 -5
- package/src/ai/runtime/model-refs.js +1 -1
- package/src/ai/runtime/registry.js +8 -1
- package/src/ai/types.js +1 -1
- package/src/runtime.js +4 -4
- package/types/ai/index.d.ts +0 -1
- package/types/ai/observer.d.ts +4 -2
- package/types/ai/providers/claude-cli.d.ts +6 -30
- package/types/ai/providers/claude-sdk.d.ts +2 -9
- package/types/ai/providers/codex-app.d.ts +4 -10
- package/types/ai/providers/pi-messages.d.ts +0 -1
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -11
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +4 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +1 -1
- package/types/ai/types.d.ts +9 -3
- package/src/ai/backend.js +0 -17
- package/src/ai/registry.js +0 -5
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
|
-
|
|
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
|
-
|
|
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"` |
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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` | `
|
|
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` |
|
|
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` | `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
712
|
+
### Structured output
|
|
689
713
|
|
|
690
|
-
Pass `options.outputSchema` (a JSON Schema).
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
925
|
+
## What This Package Does Not Own
|
|
883
926
|
|
|
884
|
-
|
|
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.
|
|
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
|
-
"
|
|
146
|
+
"typecheck": "tsc -p tsconfig.types.json --noEmit",
|
|
147
|
+
"test": "vitest run"
|
|
151
148
|
}
|
|
152
149
|
}
|
package/src/agent/approval.js
CHANGED
|
@@ -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
|
-
//
|
|
6
|
-
//
|
|
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
package/src/ai/observer.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
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);
|