@mono-agent/agent-runtime 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MIGRATION.md CHANGED
@@ -13,7 +13,88 @@ primary route.
13
13
 
14
14
  ---
15
15
 
16
- ## Pre-1.0 public-surface cleanup
16
+ ## 0.15.1
17
+
18
+ - **Runtime-owned Pi interoperability:** consumers that directly import
19
+ `@earendil-works/pi-ai` only for catalog, reasoning, or OAuth behavior should
20
+ switch to `listPiBuiltinModels`, `getPiBuiltinModel`,
21
+ `reasoningLevelsForPiModel`, `resolvePiOAuthApiKey`, and `loginPiOAuth` from
22
+ `@mono-agent/agent-runtime/ai`. The runtime keeps Pi AI and Pi Agent Core
23
+ exact-pinned at `0.80.6`; the façade returns cloned model and credential
24
+ snapshots rather than exposing mutable upstream registries.
25
+ - **Claude test seam:** downstream tests should pass
26
+ `RuntimeRunOptions.claudeAgentQuery` instead of mocking
27
+ `@anthropic-ai/claude-agent-sdk` by package name. Normal runs omit this option
28
+ and use the runtime-owned SDK. Pi AI's `@anthropic-ai/sdk@0.91.1` pin and the
29
+ Claude Agent SDK's `@anthropic-ai/sdk>=0.93.0` requirement intentionally
30
+ remain as two isolated SDK versions.
31
+ - **Compaction policy cleanup:** the inert
32
+ `toolPayloadCompactionTriggerChars` and `toolPruneTriggerTokens` properties
33
+ were removed from `AgentCompactionPolicy`, policy resolution, defaults, and
34
+ declarations. They had no supported typed/config path and did not activate
35
+ runtime pruning, so no replacement is required.
36
+ - **Codex live-input teardown:** a Codex app-server transport death now also
37
+ terminates a pending live-input read. Runs settle with the existing
38
+ `provider_unavailable` / `codex_app_server_closed` classification instead of
39
+ waiting forever for the input iterator. If a host `acknowledge` or `reject`
40
+ callback throws, the already-decided native steering result remains
41
+ authoritative and the runtime emits a bounded
42
+ `live_input_callback_failed` warning.
43
+
44
+ ## 0.15.x baseline
45
+
46
+ This is the current published baseline for the detailed pre-1.0 reference
47
+ below. It includes the explicit exports map, the five-bridge lazy registry,
48
+ typed runtime policies, runtime-owned provider dependencies, and the
49
+ public-surface cleanup described in this guide.
50
+
51
+ ## 0.12.x
52
+
53
+ - Persistent provider context overflow is classified as `context_limit`, which
54
+ lets a fallback router try its next model without conflating context capacity
55
+ with quota, output, or max-turn `usage_limit` failures.
56
+ - Omitted Pi compaction values resolve from effective context window `W`:
57
+ trigger ratio `0.70`, retained context `10%`, summary output `4%`, and minimum
58
+ proactive savings `10%`, subject to the documented scalar clamps. Numeric
59
+ provider limits and generic overflow evidence may lower a learned
60
+ process-local ceiling; `contextWindowOverride` remains the persistent
61
+ correction.
62
+
63
+ ## 0.10.x
64
+
65
+ - `ReadSkill` returns complete skill instructions by default, including content
66
+ beyond the former 12,000-character boundary. Programmatic callers of
67
+ `formatSkillBodyWithPathNote()` opt into truncation by passing a positive
68
+ `maxChars`; omitting it means no helper-level cap. The standard 256 KiB
69
+ tool-payload guard remains in effect.
70
+
71
+ ## 0.7.x
72
+
73
+ - Omitting Claude SDK effort now preserves the provider default instead of
74
+ deriving a `thinking` option. Supported effort values are forwarded exactly;
75
+ explicit `none` is unsupported (`skipped_capability_mismatch` through the
76
+ bridge, while direct `claudeEffortOptions("none")` calls throw).
77
+ - Cancellation uses a private abort controller and closes the active Claude SDK
78
+ `Query` with `Query.close()`. Test doubles must honor both boundaries rather
79
+ than implementing only iterator `return()`.
80
+ - `Glob` and `Grep` prefer an explicit `ripgrepPath`, then the packaged
81
+ `@vscode/ripgrep` binary on supported platforms, and finally `PATH`.
82
+
83
+ ## 0.6.2
84
+
85
+ - Codex file edits use a flat top-level `file_change` event instead of synthetic
86
+ assistant/user `file_edit` tool-use/tool-result pairs.
87
+ - The synthetic `createClaudeFileEditHooks`,
88
+ `createFileEditToolUseEvent`, and `createFileEditToolResultEvent` exports were
89
+ removed. Consumers should observe normalized runtime events or use the
90
+ remaining file-change statistics helpers rather than recreating provider hook
91
+ behavior.
92
+
93
+ ---
94
+
95
+ ## Detailed pre-1.0 reference
96
+
97
+ ### Pre-1.0 public-surface cleanup
17
98
 
18
99
  The compatibility entrypoints `./ai/backend.js` and `./ai/registry.js` were
19
100
  removed after repository-wide reachability checks found no supported caller.
@@ -24,7 +105,7 @@ instead. Runtime behavior and the canonical bridge descriptors are unchanged.
24
105
 
25
106
  ---
26
107
 
27
- ## 1. Pi is now native-only (`pi-sdk.js` → `pi-native.js`)
108
+ ### 1. Pi is now native-only (`pi-sdk.js` → `pi-native.js`)
28
109
 
29
110
  The hand-rolled Pi bridge that drove the low-level `Agent` was replaced by a
30
111
  bridge built on `@earendil-works/pi-agent-core`'s high-level `AgentHarness`. The
@@ -43,7 +124,7 @@ registry resolves `pi` → the native bridge unconditionally; there is no
43
124
  `@mono-agent/agent-runtime/ai/failure.js`. The `pi*Backend` aliases are gone —
44
125
  all Pi routes through the native bridge.
45
126
 
46
- ## 2. Removed run options: `piReasoningSummary`, `piCodexTransport`
127
+ ### 2. Removed run options: `piReasoningSummary`, `piCodexTransport`
47
128
 
48
129
  These were Pi-bridge knobs the native path does not consume.
49
130
 
@@ -54,7 +135,7 @@ These were Pi-bridge knobs the native path does not consume.
54
135
  `runtime.reasoningSummary` config field has also been removed.
55
136
  - `piCodexTransport` was doc-only and is removed. No replacement is needed.
56
137
 
57
- ## 3. Pi context compaction: bridge-driven via AgentHarness.compact()
138
+ ### 3. Pi context compaction: bridge-driven via AgentHarness.compact()
58
139
 
59
140
  `AgentHarness` has no automatic compaction, so the pi bridge drives it directly
60
141
  (the legacy low-level `transformContext` / `afterToolCall` hooks and
@@ -76,7 +157,7 @@ These were Pi-bridge knobs the native path does not consume.
76
157
  correction. Deprecated programmatic `agent_compaction_*` settings and
77
158
  `resolveAgentCompactionPolicy` remain compatibility surfaces.
78
159
 
79
- ## 4. Durable Pi session resume: create-on-miss semantics
160
+ ### 4. Durable Pi session resume: create-on-miss semantics
80
161
 
81
162
  When a run supplies a `providerSessionId` (or the legacy `sessionId` alias) **and**
82
163
  durable storage is configured (`piSessionsRoot`), Pi-native now **creates the
@@ -91,7 +172,7 @@ passed an arbitrary `providerSessionId` to a durable run expecting a hard
91
172
  The in-memory (non-durable) resume path still fast-fails `session_not_found` on a
92
173
  miss.
93
174
 
94
- ## 5. Fallback router enforces requested native-subagent capability
175
+ ### 5. Fallback router enforces requested native-subagent capability
95
176
 
96
177
  Pi advertises `supports_native_subagents: false`. The fallback router now infers
97
178
  a `supports_native_subagents` requirement when a run passes
@@ -103,7 +184,7 @@ a `supports_native_subagents` requirement when a run passes
103
184
  native-subagent runs, ensure at least one entry supports native subagents, or the
104
185
  run reports exhausted instead of degrading silently.
105
186
 
106
- ## 6. Diagnostics & internal behavior changes (no API change)
187
+ ### 6. Diagnostics & internal behavior changes (no API change)
107
188
 
108
189
  - **Pi multimodal**: image inputs are delivered to the model as image content
109
190
  blocks (internal fix; affects behavior, not the call shape).
@@ -119,7 +200,7 @@ run reports exhausted instead of degrading silently.
119
200
  resumed sessions roll back to their pre-turn leaf on host-side (outer-catch)
120
201
  failures. These are correctness fixes with no API surface change.
121
202
 
122
- ## 7. Sandbox enforcement is now an injectable seam (agent-runtime has zero workspace-package dependencies)
203
+ ### 7. Sandbox enforcement is now an injectable seam (agent-runtime has zero workspace-package dependencies)
123
204
 
124
205
  `@mono-agent/agent-runtime` does not depend on `@mono-agent/runtime-adapter`. Sandbox
125
206
  enforcement (command sandboxing, network-policy checks, and monotonic policy
@@ -147,7 +228,7 @@ mono-agent hosts — no action needed if you build your runtime through
147
228
  `@mono-agent/runtime-adapter`, also pass a `sandbox` implementation, or drop
148
229
  the policy.
149
230
 
150
- ## 8. Typed run options replace the `settings` bag (`toolLimits` / `compaction` / `prompts`)
231
+ ### 8. Typed run options replace the `settings` bag (`toolLimits` / `compaction` / `prompts`)
151
232
 
152
233
  The flat `options.settings` bag is **deprecated** as the way to configure
153
234
  tool-output clamps and context compaction. The supported replacements are typed,
@@ -177,7 +258,7 @@ mapper's fallback window.
177
258
  **Action:** migrate `settings` → `toolLimits` / `compaction`; until then the shim
178
259
  keeps working with one deprecation warning per run.
179
260
 
180
- ## 9. New per-run overrides: `sandbox`, `sandboxPolicy`, `prompts`
261
+ ### 9. New per-run overrides: `sandbox`, `sandboxPolicy`, `prompts`
181
262
 
182
263
  Beyond `toolLimits` / `compaction`, `RuntimeRunOptions` gained:
183
264
 
@@ -193,7 +274,7 @@ Beyond `toolLimits` / `compaction`, `RuntimeRunOptions` gained:
193
274
  (byte-identical default). These are also accepted on `AgentRuntimeHostOptions`
194
275
  as the host-level default.
195
276
 
196
- ## 10. Pi 0.80 auth: `Models` credential store (`resolvePiApiKey` semantics preserved)
277
+ ### 10. Pi 0.80 auth: `Models` credential store (`resolvePiApiKey` semantics preserved)
197
278
 
198
279
  Pi 0.80 removed the harness `getApiKeyAndHeaders` hook; request auth now resolves
199
280
  through a `Models` collection's `CredentialStore`. The bridge's **per-run
@@ -208,7 +289,7 @@ now `0.80.6`** (the initial Pi 0.80 migration landed at `0.80.5`, from
208
289
  `^0.79.1`). Compaction is driven natively (section 3). The `0.80.6` refresh also
209
290
  preserves model-native `max` reasoning and Pi's request-wide pricing tiers.
210
291
 
211
- ## 11. Exports map: wildcards removed (explicit deep-path map)
292
+ ### 11. Exports map: wildcards removed (explicit deep-path map)
212
293
 
213
294
  The package's `./ai/*` and `./agent/*` **wildcard exports were replaced by an
214
295
  explicit `exports` map**: 3 barrels (`.`, `./ai`, `./agent`) plus the generated
@@ -257,64 +338,51 @@ a compatibility subpath.
257
338
 
258
339
  ## Version
259
340
 
260
- This guide describes the published `0.13.x` package contract. Keep
341
+ This guide describes the published `0.15.x` package contract. Keep
261
342
  `@mono-agent/agent-runtime`, `@mono-agent/runtime-adapter`, and other
262
343
  `@mono-agent/*` packages on the same lockstep version when upgrading. The paired
263
344
  runtime adapter no longer exposes `piReasoningSummary` in its run-options type.
264
345
 
265
346
  ---
266
347
 
267
- ## Appendix — Porting this kernel to a new scope/host (worklab port-readiness)
268
-
269
- This kernel is designed to be vendored into a differently-scoped host (the
270
- concrete target is **worklab**, `@worklab-ai/agent-runtime`, GPL-3.0-only, npm
271
- workspaces, pure-JS no-build, consuming this package's raw `src/`). The port
272
- itself is a follow-up; this is the executable checklist, with the port-readiness
273
- dry-run results recorded inline (verified against the worklab tree read-only).
274
-
275
- Run these before/at the port:
276
-
277
- 1. **Scope rename `@mono-agent/` `@worklab-ai/`.** Touches `package.json`
278
- (`name` + the package-name prefix inside each `exports` key's consumer
279
- specifier) only the kernel's own source uses **relative** imports, so no
280
- source import references the scope. *(Verified: zero `@mono-agent/*` specifiers
281
- in `src/`.)*
282
- 2. **Dependencies.** Post-decoupling the kernel has **zero workspace-package
283
- deps**; only the third-party pins need aligning: `@earendil-works/pi-ai` +
284
- `@earendil-works/pi-agent-core` (`0.80.6`), `@modelcontextprotocol/sdk`,
285
- `@opencode-ai/sdk`, `@anthropic-ai/claude-agent-sdk`, `zod`.
286
- 3. **Pi bump `^0.74.0` `0.80.6` in lockstep.** worklab tests that use old pi
287
- APIs are rewritten at the port. Do not restore the old `pi-sdk.js` deep
288
- import; use `generatePiNativeResponse` from `@mono-agent/agent-runtime/ai`.
289
- 4. **Sandbox.** worklab passes **no** `sandbox` implementation → `passthroughSandbox`,
290
- and **never sets `sandboxPolicy`** *(verified: zero `sandboxPolicy` /
291
- `sandbox:` in worklab `src/`)*, so with no policy every tool runs unsandboxed
292
- exactly as today — behavior is byte-identical. (If worklab later adds a policy,
293
- it must also inject a `RuntimeSandbox` impl section 7's fail-closed rule.)
294
- 5. **License / packaging.** GPL-3.0-only stays; `files` includes `types/`
295
- (additive worklab consumes raw `src/`, `.d.ts` generation is optional).
296
- 6. **Deep imports resolve.** `node scripts/verify-deep-imports.mjs` (default +
297
- types conditions) is green. Every worklab **non-test** deep import resolves in
298
- the explicit exports map *(verified no gap)*, and the Worklab-test provider
299
- bridge imports for `claude-sdk.js`, `claude-cli.js`, and `codex-app.js` are
300
- supported as exported subpaths. The only worklab deep import NOT in the map is
301
- the removed **test-only** `pi-sdk.js`; those tests are rewritten at the port
302
- (step 3), so no export entry is added for it.
303
- 7. **Contract supersets.** `HOST_KEYS` worklab's host bag *(verified:
304
- worklab passes `resolveCustomPricing`, `onCompactionRecorded`, `persistArtifact`,
305
- `resolvePiApiKey`, `observers` all covered)*; the deep-import
306
- `configureToolRuntime` accepts worklab's keys *(verified: `workspace`,
307
- `repoRoot`, `runId`, `toolArtifactDir`, `ripgrepPath`, `qaOutputDir` ⊂
308
- `TOOL_CONTEXT_KEYS`)*; and every `RuntimeResult` field worklab's
309
- `worker/agent-turn.js` reads exists on the result *(verified: `cancelled`,
310
- `providerSessionId`, `error`, `failureKind`, `errorDetails`, `diagnostics`,
311
- `runtimeWarnings`, plus `text`/`usage`/`model`/`effort`/`numTurns`/
312
- `structuredResult`/`capabilitiesUsed`/`durationMs`/`failoverHistory`;
313
- `observerSnapshot` is worklab-side, folded from its own metrics observer)*.
314
- 8. **`options.settings` day one.** Works via the deprecated shim (section 8) with
315
- one `deprecated_settings_option` warning per run; worklab later maps
316
- `settings` → the typed policy objects in its `core/ai.js`.
317
- 9. **Test layout + no-build consumption.** `src/__tests__` + vitest already match;
318
- the package is fully consumable from raw `src/` with **no build**
319
- *(verified: a smoke import of `createRuntime` / `createRouterRuntime` from
320
- `src/index.js` constructs a runtime with no model call)*.
348
+ ## Appendix — Worklab shared-kernel adoption
349
+
350
+ Worklab should consume the published `@mono-agent/agent-runtime` package rather
351
+ than vendor or rename its source. The products remain separate, but provider
352
+ execution has one owner. Apply this downstream checklist when removing
353
+ Worklab's runtime fork:
354
+
355
+ 1. **Install the lockstep runtime package.** Keep the `@mono-agent/*` packages a
356
+ Worklab release uses on the same published version. Preserve the shared
357
+ kernel's `GPL-3.0-only` distribution boundary.
358
+ 2. **Remove direct provider ownership.** Delete Worklab production imports from
359
+ `@earendil-works/pi-ai`, its separate Pi version constraint, and local copies
360
+ of provider bridge code. Move tests off Pi's faux-provider helpers too; until
361
+ that is complete, isolate the fixture or pin its development-only Pi
362
+ dependency to exact `0.80.6` rather than a floating range. Do not restore the
363
+ removed `pi-sdk.js` subpath.
364
+ 3. **Use the public Pi surfaces.** Run models through
365
+ `generatePiNativeResponse` or the runtime registry. Use
366
+ `listPiBuiltinModels`, `getPiBuiltinModel`,
367
+ `reasoningLevelsForPiModel`, `resolvePiOAuthApiKey`, and `loginPiOAuth` for
368
+ catalog and OAuth integration. Those façades keep Pi mutable state and the
369
+ exact `0.80.6` compatibility pin inside the runtime. OAuth login adapters
370
+ must supply `onAuth`, `onDeviceCode`, `onPrompt`, and `onSelect`; the façade
371
+ rejects an incomplete callback contract before starting provider login.
372
+ 4. **Inject Claude tests.** Replace package-level mocks of
373
+ `@anthropic-ai/claude-agent-sdk` with
374
+ `RuntimeRunOptions.claudeAgentQuery`. Production calls omit the seam. Expect
375
+ the runtime installation to retain Pi's Anthropic SDK `0.91.1` beside the
376
+ newer Anthropic SDK required by Claude; do not force-deduplicate them.
377
+ 5. **Preserve the sandbox boundary.** A direct runtime consumer that supplies
378
+ `sandboxPolicy` must also inject a `RuntimeSandbox`; otherwise the runtime
379
+ intentionally fails closed. With neither a policy nor an implementation,
380
+ passthrough behavior remains unchanged.
381
+ 6. **Use supported contracts.** Keep host callbacks within
382
+ `AgentRuntimeHostOptions`, tool state within the exported tool-runtime
383
+ context, and reads within `RuntimeResult`. Replace deprecated
384
+ `options.settings` with typed `toolLimits` and `compaction` objects.
385
+ 7. **Verify the installed package.** Run Worklab's provider and worker tests
386
+ against the packed or published package, assert that no production import
387
+ resolves Pi directly, and prove the injected Claude query performs no
388
+ network call.
package/README.md CHANGED
@@ -93,6 +93,8 @@ the [architecture guide](https://github.com/robertsreberski/mono-agent/blob/main
93
93
  | `parseRuntimeModelReference()` | Convert a canonical `claude:`, `codex:`, `opencode:`, or `pi:` string into the object required by `run()` |
94
94
  | `listRuntimeBridges()` / `runtimeCapabilities()` | Inspect the five built-in bridge descriptors without loading provider implementations |
95
95
  | `createPiOAuthApiKeyResolver()` | Bind a host-owned Pi auth file with refresh-safe writes |
96
+ | `listPiBuiltinModels()` / `getPiBuiltinModel()` | Read cloned snapshots from the runtime-owned, exact-pinned Pi model catalog without importing Pi directly |
97
+ | `resolvePiOAuthApiKey()` / `loginPiOAuth()` | Use the runtime-owned Pi OAuth implementation without importing Pi's mutable provider registry |
96
98
  | `createMetricsObserver()` | Aggregate normalized event, token, cache, cost, tool, error, turn, and approval metrics |
97
99
 
98
100
  Most hosts should use `@mono-agent/runtime-adapter` instead of importing deep
@@ -143,11 +145,14 @@ disposeAllProviderSessions
143
145
  disposeProviderSession
144
146
  executionModeIncompatibilityReason
145
147
  generatePiNativeResponse
148
+ getPiBuiltinModel
146
149
  inferAllowlistMode
147
150
  invalidateProviderSession
148
151
  isLikelyContextTermination
149
152
  isModelCompatibleWithExecutionMode
153
+ listPiBuiltinModels
150
154
  listRuntimeBridges
155
+ loginPiOAuth
151
156
  normalizeAllowlistMode
152
157
  normalizeClaudeSdkCatalog
153
158
  normalizeClaudeSdkModelId
@@ -158,12 +163,14 @@ parseStoredAllowlist
158
163
  piNativeRuntimeBridge
159
164
  readRuntimeBrand
160
165
  readToolRuntime
166
+ reasoningLevelsForPiModel
161
167
  refreshProviderSession
162
168
  renderResumeSnapshot
163
169
  resetToolRuntime
164
170
  resolveAgentCompactionPolicy
165
171
  resolveAllowlist
166
172
  resolveAllowlistMap
173
+ resolvePiOAuthApiKey
167
174
  resolveRuntimeBrand
168
175
  resolveRuntimeBridge
169
176
  runtimeCapabilities
@@ -310,15 +317,20 @@ disposeAllProviderSessions
310
317
  disposeProviderSession
311
318
  executionModeIncompatibilityReason
312
319
  generatePiNativeResponse
320
+ getPiBuiltinModel
313
321
  invalidateProviderSession
314
322
  isModelCompatibleWithExecutionMode
323
+ listPiBuiltinModels
315
324
  listRuntimeBridges
325
+ loginPiOAuth
316
326
  normalizeClaudeSdkCatalog
317
327
  normalizeClaudeSdkModelId
318
328
  normalizeRuntimeModelReference
319
329
  parseRuntimeModelReference
320
330
  piNativeRuntimeBridge
331
+ reasoningLevelsForPiModel
321
332
  refreshProviderSession
333
+ resolvePiOAuthApiKey
322
334
  resolveRuntimeBridge
323
335
  runtimeCapabilities
324
336
  sdkFromModelReference
@@ -602,6 +614,21 @@ The resolver reads provider credentials from the configured file, delegates toke
602
614
  refresh to `@earendil-works/pi-ai/oauth`, and writes refreshed credentials back
603
615
  with `0600` permissions.
604
616
 
617
+ Consumers that need lower-level Pi interoperability should still import only
618
+ `@mono-agent/agent-runtime/ai`. `listPiBuiltinModels(providerId)` returns fresh,
619
+ defensively cloned model snapshots; `getPiBuiltinModel(providerId, modelId)`
620
+ returns one cloned snapshot or `undefined`; and
621
+ `reasoningLevelsForPiModel(model)` translates a Pi model into mono-agent's
622
+ reasoning vocabulary, including `none` rather than Pi's `off`.
623
+ `resolvePiOAuthApiKey(providerId, credentials)` refreshes a caller-owned
624
+ credential snapshot and returns `{ apiKey, newCredentials }` or `null`, while
625
+ `loginPiOAuth(providerId, callbacks)` runs the selected supported login flow.
626
+ Login callers must implement Pi's four required interaction callbacks:
627
+ `onAuth`, `onDeviceCode`, `onPrompt`, and `onSelect`; optional progress,
628
+ manual-code, and abort callbacks pass through unchanged.
629
+ These functions deliberately do not expose Pi's mutable model collections or
630
+ OAuth-provider registry.
631
+
605
632
  Returns:
606
633
 
607
634
  - `run(systemPrompt, options)` — async, runs one agent turn against the chosen backend.
@@ -628,8 +655,9 @@ Per-call options (a non-exhaustive selection):
628
655
  | `maxTurns` | `number` | Hard cap on agent turns. |
629
656
  | `outputSchema` | `JSONSchema` | Requests structured JSON on capable bridges; see “Structured output” below for bridge-specific return behavior. |
630
657
  | `abortSignal` | `AbortSignal` | Cancel the run. |
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. |
632
- | `onEvent` | `(event) => void` | Fired for every event the provider emits (assistant text, tool calls/results, runtime warnings, structured output). |
658
+ | `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. Acknowledgement emits metadata-only `live_input_applied` telemetry. |
659
+ | `claudeAgentQuery` | `typeof query` | Advanced programmatic/test seam for the Claude SDK bridge. When omitted, the bridge uses the runtime's pinned Claude Agent SDK. This is not a config field or telemetry value. |
660
+ | `onEvent` | `(event) => void` | Fired for every runtime event (assistant text, tool calls/results, applied live input, runtime warnings, structured output). |
633
661
  | `runId` | `string` | Tag this run for downstream callbacks (e.g. `onCompactionRecorded`). |
634
662
  | `providerSessionId` | `string` | Resume a prior provider session. |
635
663
  | `runArtifactDir` | `string` | Used by some providers as the Playwright MCP filename target. |
@@ -639,6 +667,13 @@ Per-call options (a non-exhaustive selection):
639
667
  Live input is native on the Claude SDK, Codex app-server, and Pi bridges. The
640
668
  one-shot Claude CLI and direct OpenCode bridges advertise it as unsupported so
641
669
  routers skip them when a direct runtime call requires steering.
670
+ After a native bridge invokes `acknowledge()`, the runtime emits exactly one
671
+ `{ type: "live_input_applied", inputId, receivedAt? }` event for that logical
672
+ run. It deliberately omits the guidance body. A fallback router reuses the same
673
+ instrumented input stream, so replay or duplicate acknowledgement cannot emit a
674
+ second applied event. A throwing host `acknowledge` or `reject` callback cannot
675
+ change the native steering outcome; the Codex bridge reports it as a bounded
676
+ `live_input_callback_failed` runtime warning.
642
677
 
643
678
  Returns:
644
679
 
@@ -707,6 +742,13 @@ The agent kernel's managed tools are `Read`, `Write`, `Edit`, `Glob`, `Grep`, `B
707
742
 
708
743
  `NodeRepl` uses Node's default `node:repl` evaluator, so variables, `_`, `_error`, and loaded modules persist across calls in the same run. It supports multiline input and top-level `await`, resolves workspace-installed packages, and is closed with the run. Its child is prepared through the same sandbox seam as `Bash`; abort, the fixed 120-second timeout, child exit, or hard output overflow resets the session. It deliberately has no session ids, persistent history, terminal commands, or package-install surface.
709
744
 
745
+ Pi runs with selected skills also expose `ReadSkill`. It returns the complete
746
+ skill instructions by default, including content beyond the former
747
+ 12,000-character boundary. Programmatic callers of
748
+ `formatSkillBodyWithPathNote()` may pass a positive `maxChars` only when
749
+ truncation is explicitly desired; omitting it is not a separate hidden limit.
750
+ The standard 256 KiB tool-payload guard still applies to oversized tool results.
751
+
710
752
  Override or extend the tool surface by passing `mcpServers` for MCP-backed tools.
711
753
 
712
754
  ### Structured output
@@ -917,6 +959,19 @@ dependencies are `@anthropic-ai/claude-agent-sdk`, `@anthropic-ai/sdk`,
917
959
  `@modelcontextprotocol/sdk`, `@opencode-ai/sdk`, `@vscode/ripgrep`,
918
960
  `cross-spawn`, and `zod`.
919
961
 
962
+ The runtime owns and exact-pins the compatible Pi pair at `0.80.6`; consumers
963
+ use the runtime's Pi façade rather than coordinating a second direct
964
+ `@earendil-works/pi-ai` dependency. Do not attempt to flatten the resulting
965
+ Anthropic dependency tree: Pi AI pins `@anthropic-ai/sdk@0.91.1`, while the
966
+ Claude Agent SDK requires `@anthropic-ai/sdk>=0.93.0` and the runtime supplies
967
+ its compatible newer SDK. Two isolated Anthropic SDK versions are therefore
968
+ expected. `RuntimeRunOptions.claudeAgentQuery` provides deterministic Claude
969
+ tests without mocking package resolution or sending real SDK traffic.
970
+ If a downstream test suite still needs Pi's faux-provider helpers, isolate that
971
+ fixture or keep its development-only Pi dependency on the runtime's exact
972
+ `0.80.6` version until the fixture is removed; a broad host range can otherwise
973
+ float Pi Agent Core's own upstream dependency independently of this façade.
974
+
920
975
  Sandbox enforcement is an injectable `RuntimeSandbox` seam.
921
976
  `@mono-agent/runtime-adapter` supplies the mono-agent implementation; a direct
922
977
  consumer that configures a sandbox policy must inject an implementation or the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
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",
@@ -22,8 +22,6 @@
22
22
  * @property {number} summaryMaxTokens
23
23
  * @property {boolean} fixedOverheadEnabled
24
24
  * @property {number} compactionMinSavingsTokens
25
- * @property {number} toolPayloadCompactionTriggerChars
26
- * @property {number} toolPruneTriggerTokens
27
25
  * @property {number} toolTextLimitChars
28
26
  * @property {number} bashOutputLimitChars
29
27
  * @property {number} mcpTextLimitChars
@@ -36,8 +34,6 @@
36
34
 
37
35
  const DEFAULT_CONTEXT_WINDOW = 128000;
38
36
  const DEFAULT_TRIGGER_RATIO = 0.70;
39
- const DEFAULT_TOOL_PAYLOAD_COMPACTION_TRIGGER_CHARS = 0;
40
- const DEFAULT_TOOL_PRUNE_TRIGGER_TOKENS = 40000;
41
37
  // intelligence-ramp Phase 3: lifted from 16K/20K/12K. Mid-task tool reads
42
38
  // (large file edits, long bash output, deep MCP results) were being silently
43
39
  // clipped before the agent could reason about them. The 256KB hard ceiling
@@ -134,13 +130,6 @@ export function resolveAgentCompactionPolicy(settings = {}, model = {}) {
134
130
  0,
135
131
  500000,
136
132
  ),
137
- toolPayloadCompactionTriggerChars: clampInteger(
138
- settings.agent_tool_payload_compaction_trigger_chars,
139
- DEFAULT_TOOL_PAYLOAD_COMPACTION_TRIGGER_CHARS,
140
- 0,
141
- 10 * 1024 * 1024,
142
- ),
143
- toolPruneTriggerTokens: clampInteger(settings.agent_tool_prune_trigger_tokens, DEFAULT_TOOL_PRUNE_TRIGGER_TOKENS, 0, 500000),
144
133
  toolTextLimitChars: clampInteger(settings.agent_tool_text_limit_chars, DEFAULT_TOOL_TEXT_LIMIT_CHARS, 1000, 200000),
145
134
  bashOutputLimitChars: clampInteger(settings.agent_bash_output_limit_chars, DEFAULT_BASH_OUTPUT_LIMIT_CHARS, 1000, 200000),
146
135
  mcpTextLimitChars: clampInteger(settings.agent_mcp_text_limit_chars, DEFAULT_MCP_TEXT_LIMIT_CHARS, 1000, 200000),
@@ -35,6 +35,9 @@ export function buildSkillPathNote({ assetsPath, skillsRoot } = {}) {
35
35
  }
36
36
 
37
37
  /**
38
+ * Render a complete skill body plus its path note. Omitting `maxChars` returns
39
+ * the full text; pass a positive `maxChars` only when explicit truncation is
40
+ * required by the caller.
38
41
  * @param {{body?: string, assetsPath?: string, skillsRoot?: any, maxChars?: number}} [options]
39
42
  */
40
43
  export function formatSkillBodyWithPathNote({ body, assetsPath, skillsRoot, maxChars } = {}) {
@@ -42,7 +45,8 @@ export function formatSkillBodyWithPathNote({ body, assetsPath, skillsRoot, maxC
42
45
  buildSkillPathNote({ assetsPath, skillsRoot }),
43
46
  String(body || "").trim(),
44
47
  ].filter(Boolean).join("\n\n");
45
- return maxChars ? text.slice(0, maxChars) : text;
48
+ const limit = Number(maxChars);
49
+ return Number.isFinite(limit) && limit > 0 ? text.slice(0, Math.floor(limit)) : text;
46
50
  }
47
51
 
48
52
  export function buildSkillIndex(skills) {
package/src/ai/index.js CHANGED
@@ -12,6 +12,13 @@ export {
12
12
  } from "./runtime/sessions.js";
13
13
  export { createMetricsObserver, createObserverHub } from "./observer.js";
14
14
  export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
15
+ export {
16
+ getPiBuiltinModel,
17
+ listPiBuiltinModels,
18
+ loginPiOAuth,
19
+ reasoningLevelsForPiModel,
20
+ resolvePiOAuthApiKey,
21
+ } from "./pi-interop.js";
15
22
  export {
16
23
  CLAUDE_SDK_CATALOG_VERSION,
17
24
  createClaudeSdkDiscoveryIsolation,
@@ -0,0 +1,156 @@
1
+ // Runtime-owned interoperability facade for Pi's built-in model and OAuth
2
+ // surfaces. Consumers should use these functions instead of importing pi-ai
3
+ // directly so the runtime's known-good Pi version remains authoritative.
4
+
5
+ import { getBuiltinModel, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
6
+ import { getOAuthApiKey, getOAuthProvider } from "@earendil-works/pi-ai/oauth";
7
+ import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers/pi-models.js";
8
+
9
+ /**
10
+ * @typedef {{
11
+ * id: string,
12
+ * name: string,
13
+ * api: string,
14
+ * provider: string,
15
+ * baseUrl: string,
16
+ * reasoning: boolean,
17
+ * input: Array<"text"|"image">,
18
+ * cost: {
19
+ * input: number,
20
+ * output: number,
21
+ * cacheRead: number,
22
+ * cacheWrite: number,
23
+ * tiers?: Array<{
24
+ * inputTokensAbove: number,
25
+ * input: number,
26
+ * output: number,
27
+ * cacheRead: number,
28
+ * cacheWrite: number
29
+ * }>
30
+ * },
31
+ * contextWindow: number,
32
+ * maxTokens: number,
33
+ * thinkingLevelMap?: Object<string, string|null>,
34
+ * compat?: Object<string, *>,
35
+ * headers?: Object<string, string>,
36
+ * [key: string]: *
37
+ * }} PiBuiltinModelSnapshot
38
+ */
39
+
40
+ /**
41
+ * @typedef {"none"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"} PiReasoningLevel
42
+ */
43
+
44
+ /**
45
+ * @typedef {{
46
+ * refresh: string,
47
+ * access: string,
48
+ * expires: number,
49
+ * [key: string]: *
50
+ * }} PiOAuthCredentialsSnapshot
51
+ */
52
+
53
+ /**
54
+ * @typedef {Object} PiOAuthLoginCallbacks
55
+ * @property {(info: {url: string, instructions?: string}) => void} onAuth
56
+ * @property {(info: {userCode: string, verificationUri: string, intervalSeconds?: number, expiresInSeconds?: number}) => void} onDeviceCode
57
+ * @property {(prompt: {message: string, placeholder?: string, allowEmpty?: boolean}) => Promise<string>} onPrompt
58
+ * @property {(message: string) => void} [onProgress]
59
+ * @property {() => Promise<string>} [onManualCodeInput]
60
+ * @property {(prompt: {message: string, options: Array<{id: string, label: string}>}) => Promise<string|undefined>} onSelect
61
+ * @property {AbortSignal} [signal]
62
+ */
63
+
64
+ /**
65
+ * Clone provider-owned data before it crosses the public runtime boundary.
66
+ * Pi's built-in models and OAuth credentials are structured data on the
67
+ * supported version, and the package requires a Node release with
68
+ * `structuredClone`.
69
+ *
70
+ * @template T
71
+ * @param {T} value
72
+ * @returns {T}
73
+ */
74
+ function cloneInteropValue(value) {
75
+ return structuredClone(value);
76
+ }
77
+
78
+ /**
79
+ * List defensive snapshots of Pi's built-in models for one provider.
80
+ *
81
+ * @param {string} providerId
82
+ * @returns {PiBuiltinModelSnapshot[]}
83
+ */
84
+ export function listPiBuiltinModels(providerId) {
85
+ const models = getBuiltinModels(/** @type {any} */ (providerId));
86
+ return /** @type {PiBuiltinModelSnapshot[]} */ (cloneInteropValue(models));
87
+ }
88
+
89
+ /**
90
+ * Read a defensive snapshot of one Pi built-in model.
91
+ *
92
+ * @param {string} providerId
93
+ * @param {string} modelId
94
+ * @returns {PiBuiltinModelSnapshot|undefined}
95
+ */
96
+ export function getPiBuiltinModel(providerId, modelId) {
97
+ const model = getBuiltinModel(
98
+ /** @type {any} */ (providerId),
99
+ /** @type {any} */ (modelId),
100
+ );
101
+ return model === undefined
102
+ ? undefined
103
+ : /** @type {PiBuiltinModelSnapshot} */ (cloneInteropValue(model));
104
+ }
105
+
106
+ /**
107
+ * Translate Pi's model-native thinking levels to mono-agent effort spelling.
108
+ *
109
+ * @param {PiBuiltinModelSnapshot} model
110
+ * @returns {PiReasoningLevel[]}
111
+ */
112
+ export function reasoningLevelsForPiModel(model) {
113
+ return /** @type {PiReasoningLevel[]} */ (resolveReasoningLevels(model));
114
+ }
115
+
116
+ /**
117
+ * Resolve an OAuth-backed API key without allowing Pi to mutate the caller's
118
+ * credential record or returning Pi-owned credential objects.
119
+ *
120
+ * @param {string} providerId
121
+ * @param {Object<string, PiOAuthCredentialsSnapshot>} credentials
122
+ * @returns {Promise<{apiKey: string, newCredentials: PiOAuthCredentialsSnapshot}|null>}
123
+ */
124
+ export async function resolvePiOAuthApiKey(providerId, credentials) {
125
+ const result = await getOAuthApiKey(
126
+ providerId,
127
+ /** @type {any} */ (cloneInteropValue(credentials)),
128
+ );
129
+ if (!result) return null;
130
+ return {
131
+ apiKey: result.apiKey,
132
+ newCredentials: cloneInteropValue(result.newCredentials),
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Run a supported Pi OAuth login flow without exposing Pi's mutable provider
138
+ * registry or provider instances.
139
+ *
140
+ * @param {string} providerId
141
+ * @param {PiOAuthLoginCallbacks} callbacks
142
+ * @returns {Promise<PiOAuthCredentialsSnapshot>}
143
+ */
144
+ export async function loginPiOAuth(providerId, callbacks) {
145
+ const provider = getOAuthProvider(providerId);
146
+ if (!provider || typeof provider.login !== "function") {
147
+ throw new Error(`Pi OAuth provider is unavailable: ${providerId}`);
148
+ }
149
+ for (const callbackName of ["onAuth", "onDeviceCode", "onPrompt", "onSelect"]) {
150
+ if (typeof callbacks?.[callbackName] !== "function") {
151
+ throw new TypeError(`loginPiOAuth requires callbacks.${callbackName}()`);
152
+ }
153
+ }
154
+ const credentials = await provider.login(/** @type {any} */ ({ ...callbacks }));
155
+ return cloneInteropValue(credentials);
156
+ }
@@ -748,6 +748,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
748
748
  const prompt = options.liveInput
749
749
  ? livePromptMessages({ initialPrompt: promptString, liveInput: options.liveInput, sessionId: reusableProviderSessionId || randomUUID(), prompts: options.prompts })
750
750
  : promptString;
751
+ const claudeAgentQuery = options.claudeAgentQuery ?? query;
751
752
  const providerRequestStartedAt = Date.now();
752
753
  emitEvent({
753
754
  type: "provider_request_started",
@@ -805,7 +806,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
805
806
  }
806
807
 
807
808
  try {
808
- stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
809
+ stream = claudeAgentQuery({ prompt: /** @type {any} */ (prompt), options: queryOptions });
809
810
  for await (const event of stream) {
810
811
  const nextSessionId = sessionIdFromEvent(event);
811
812
  if (nextSessionId) providerSessionId = nextSessionId;
@@ -1143,7 +1143,9 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1143
1143
  let serverRequestViolation = null;
1144
1144
  let resolveTurn;
1145
1145
  let resolveTurnReady;
1146
+ let resolveLiveInputStop;
1146
1147
  let turnReadyResolved = false;
1148
+ let liveInputStopped = false;
1147
1149
  const fileChangeSnapshots = new Map();
1148
1150
  const codexItemContext = {
1149
1151
  fileChangePayload: (raw) => createFileChangePayload(raw, {
@@ -1153,6 +1155,13 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1153
1155
  };
1154
1156
  const turnDone = new Promise((resolve) => { resolveTurn = resolve; });
1155
1157
  const turnReady = new Promise((resolve) => { resolveTurnReady = resolve; });
1158
+ const liveInputStop = new Promise((resolve) => { resolveLiveInputStop = resolve; });
1159
+
1160
+ function stopLiveInput() {
1161
+ if (liveInputStopped) return;
1162
+ liveInputStopped = true;
1163
+ resolveLiveInputStop();
1164
+ }
1156
1165
 
1157
1166
  function setActiveTurnId(turnId, { steerReady = false } = {}) {
1158
1167
  activeTurnId = turnId || activeTurnId;
@@ -1175,6 +1184,21 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1175
1184
  options.onEvent?.(safeEvent);
1176
1185
  }
1177
1186
 
1187
+ function invokeLiveInputCallback(message, callbackName, ...args) {
1188
+ const callback = message?.[callbackName];
1189
+ if (typeof callback !== "function") return;
1190
+ try {
1191
+ callback.apply(message, args);
1192
+ } catch (err) {
1193
+ const detail = safeDiagnostic(err, 512);
1194
+ emitEvent({
1195
+ type: "runtime_warning",
1196
+ warning_kind: "live_input_callback_failed",
1197
+ message: safeDiagnostic(`Live-input ${callbackName} callback failed: ${detail}`, 1_024),
1198
+ });
1199
+ }
1200
+ }
1201
+
1178
1202
  const compactionTurnKey = (params = {}) => `${params.threadId || threadId || "thread"}:${params.turnId || activeTurnId || "turn"}`;
1179
1203
 
1180
1204
  /**
@@ -1269,6 +1293,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1269
1293
  client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
1270
1294
  }
1271
1295
  turnCompleted = true;
1296
+ stopLiveInput();
1272
1297
  resolveTurn({ id: activeTurnId, status: "interrupted" });
1273
1298
  }
1274
1299
 
@@ -1293,6 +1318,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1293
1318
  message: errorMessage,
1294
1319
  });
1295
1320
  turnCompleted = true;
1321
+ stopLiveInput();
1296
1322
  resolveTurn({ id: activeTurnId, status: "interrupted" });
1297
1323
  }
1298
1324
 
@@ -1323,6 +1349,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1323
1349
  if (method === "turn/completed") {
1324
1350
  setActiveTurnId(params.turn?.id);
1325
1351
  turnCompleted = true;
1352
+ stopLiveInput();
1326
1353
  if (params.turn?.status === "failed") {
1327
1354
  errorMessage = safeDiagnostic(params.turn?.error?.message || params.turn?.error || "Codex turn failed");
1328
1355
  failureKind = "provider_unavailable";
@@ -1482,6 +1509,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1482
1509
 
1483
1510
  const abortHandler = () => {
1484
1511
  abortRequested = true;
1512
+ stopLiveInput();
1485
1513
  if (threadId && activeTurnId && !interruptSent) {
1486
1514
  interruptSent = true;
1487
1515
  client?.request("turn/interrupt", { threadId, turnId: activeTurnId }).catch(() => {});
@@ -1495,20 +1523,19 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1495
1523
  if (!options.liveInput) return;
1496
1524
  const iterator = options.liveInput[Symbol.asyncIterator]();
1497
1525
  try {
1498
- while (!turnCompleted) {
1526
+ while (!turnCompleted && !liveInputStopped) {
1499
1527
  const next = await Promise.race([
1500
1528
  iterator.next(),
1501
- turnDone.then(() => ({ done: true, value: undefined })),
1529
+ liveInputStop.then(() => ({ done: true, value: undefined })),
1502
1530
  ]);
1503
- if (next.done || turnCompleted) break;
1531
+ if (next.done || turnCompleted || liveInputStopped) break;
1504
1532
  const message = next.value;
1505
1533
  if (!threadId || !activeTurnId || !turnReadyResolved) {
1506
1534
  await Promise.race([
1507
1535
  turnReady,
1508
- turnDone,
1509
- client.closed.then((err) => { throw err; }),
1536
+ liveInputStop,
1510
1537
  ]);
1511
- if (turnCompleted || !turnReadyResolved) break;
1538
+ if (turnCompleted || liveInputStopped || !turnReadyResolved) break;
1512
1539
  }
1513
1540
  const input = userTextInput(formatLiveInputGuidance(message.body, options.prompts));
1514
1541
  try {
@@ -1518,16 +1545,15 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1518
1545
  input,
1519
1546
  });
1520
1547
  activeTurnId = response?.turnId || activeTurnId;
1521
- message.acknowledge?.();
1548
+ invokeLiveInputCallback(message, "acknowledge");
1522
1549
  } catch (err) {
1523
1550
  const providerError = err?.responseError;
1524
1551
  if (isNoActiveTurnToSteer(providerError || err)) {
1525
1552
  await Promise.race([
1526
1553
  turnReady,
1527
- turnDone,
1528
- client.closed.then((closedErr) => { throw closedErr; }),
1554
+ liveInputStop,
1529
1555
  ]);
1530
- if (turnCompleted) break;
1556
+ if (turnCompleted || liveInputStopped) break;
1531
1557
  try {
1532
1558
  const response = await client.request("turn/steer", {
1533
1559
  threadId,
@@ -1535,10 +1561,10 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1535
1561
  input,
1536
1562
  });
1537
1563
  activeTurnId = response?.turnId || activeTurnId;
1538
- message.acknowledge?.();
1564
+ invokeLiveInputCallback(message, "acknowledge");
1539
1565
  continue;
1540
1566
  } catch (retryErr) {
1541
- message.reject?.(retryErr);
1567
+ invokeLiveInputCallback(message, "reject", retryErr);
1542
1568
  const retryProviderError = retryErr?.responseError
1543
1569
  ? safeResponseError(retryErr.responseError)
1544
1570
  : null;
@@ -1552,7 +1578,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1552
1578
  break;
1553
1579
  }
1554
1580
  }
1555
- message.reject?.(err);
1581
+ invokeLiveInputCallback(message, "reject", err);
1556
1582
  emitEvent({
1557
1583
  type: "runtime_warning",
1558
1584
  warning_kind: isActiveTurnNotSteerable(providerError) ? "active_turn_not_steerable" : "live_input_rejected",
@@ -1789,6 +1815,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1789
1815
  closedSignal.then((err) => {
1790
1816
  if (!turnCompleted) {
1791
1817
  prematureClose = true;
1818
+ stopLiveInput();
1792
1819
  throw err || new Error("codex app-server closed");
1793
1820
  }
1794
1821
  return null;
@@ -1803,6 +1830,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1803
1830
  }
1804
1831
  } finally {
1805
1832
  abortRaceCleanup();
1833
+ stopLiveInput();
1806
1834
  }
1807
1835
  turnCompleted = true;
1808
1836
  await steerTask;
@@ -1923,6 +1951,7 @@ export async function generateCodexAppResponse(systemPrompt, options = {}) {
1923
1951
  }),
1924
1952
  };
1925
1953
  } finally {
1954
+ stopLiveInput();
1926
1955
  if (activeCompactions.size > 0) {
1927
1956
  const cancelled = !!options.abortSignal?.aborted;
1928
1957
  finalizeOpenCompactions(
@@ -0,0 +1,94 @@
1
+ // Metadata-only live-input acknowledgement instrumentation.
2
+ //
3
+ // Provider bridges already call message.acknowledge() only after their native
4
+ // steering boundary accepts guidance. Wrapping that callback here creates one
5
+ // adapter-neutral `live_input_applied` event without copying the guidance body
6
+ // into runtime telemetry. A wrapper owns one logical-run dedupe set and is
7
+ // intentionally reused by the fallback router across provider attempts.
8
+
9
+ // @ts-check
10
+
11
+ const LIVE_INPUT_APPLIED_INSTRUMENTED = Symbol("mono-agent.live-input-applied-instrumented");
12
+
13
+ /**
14
+ * @typedef {{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (reason?: unknown) => void}} RuntimeLiveInputMessage
15
+ * @typedef {{type: "live_input_applied", inputId: string, receivedAt?: string}} LiveInputAppliedEvent
16
+ */
17
+
18
+ /**
19
+ * @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
20
+ * @param {(event: LiveInputAppliedEvent) => void} onApplied
21
+ * @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
22
+ */
23
+ export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
24
+ if (liveInput === undefined || isInstrumented(liveInput)) return liveInput;
25
+
26
+ const appliedInputIds = new Set();
27
+ const instrumented = {
28
+ [LIVE_INPUT_APPLIED_INSTRUMENTED]: true,
29
+ [Symbol.asyncIterator]() {
30
+ const iterator = liveInput[Symbol.asyncIterator]();
31
+ let ordinal = 0;
32
+ return {
33
+ async next() {
34
+ const next = await iterator.next();
35
+ if (next.done === true) return next;
36
+ ordinal += 1;
37
+ const message = next.value;
38
+ const inputId = stableInputId(message?.id, ordinal);
39
+ const receivedAt = typeof message?.receivedAt === "string" && message.receivedAt.length > 0
40
+ ? message.receivedAt
41
+ : undefined;
42
+ const acknowledge = typeof message?.acknowledge === "function"
43
+ ? message.acknowledge.bind(message)
44
+ : undefined;
45
+ return {
46
+ done: false,
47
+ value: {
48
+ ...message,
49
+ acknowledge: () => {
50
+ acknowledge?.();
51
+ if (appliedInputIds.has(inputId)) return;
52
+ appliedInputIds.add(inputId);
53
+ try {
54
+ onApplied({
55
+ type: "live_input_applied",
56
+ inputId,
57
+ ...(receivedAt === undefined ? {} : { receivedAt }),
58
+ });
59
+ } catch {
60
+ // Telemetry must never turn accepted guidance into a provider
61
+ // failure after the native steering call already succeeded.
62
+ }
63
+ },
64
+ },
65
+ };
66
+ },
67
+ async return(value) {
68
+ return typeof iterator.return === "function"
69
+ ? iterator.return(value)
70
+ : { done: true, value };
71
+ },
72
+ async throw(error) {
73
+ if (typeof iterator.throw === "function") return iterator.throw(error);
74
+ throw error;
75
+ },
76
+ };
77
+ },
78
+ };
79
+ return /** @type {AsyncIterable<RuntimeLiveInputMessage>} */ (instrumented);
80
+ }
81
+
82
+ /** @param {unknown} value */
83
+ function isInstrumented(value) {
84
+ return typeof value === "object"
85
+ && value !== null
86
+ && value[LIVE_INPUT_APPLIED_INSTRUMENTED] === true;
87
+ }
88
+
89
+ /** @param {unknown} value @param {number} ordinal */
90
+ function stableInputId(value, ordinal) {
91
+ return typeof value === "string" && value.trim().length > 0
92
+ ? value
93
+ : `anonymous:${ordinal}`;
94
+ }
@@ -41,6 +41,8 @@ import { runtimeCapabilities } from "./capabilities.js";
41
41
  import { buildTranscriptTailSnapshot, renderResumeSnapshot } from "../../agent/transcript.js";
42
42
  import { passthroughSandbox } from "../../agent/sandbox-seam.js";
43
43
  import { resolveRuntimeBrand } from "../../runtime-brand.js";
44
+ import { createObserverHub } from "../observer.js";
45
+ import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
44
46
 
45
47
  /**
46
48
  * @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef
@@ -127,6 +129,22 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
127
129
  * @returns {Promise<RuntimeResult>}
128
130
  */
129
131
  async run(systemPrompt, options = {}) {
132
+ const liveInputHub = options.liveInput === undefined
133
+ ? undefined
134
+ : createObserverHub({
135
+ observers: [
136
+ ...(Array.isArray(host.observers) ? host.observers : []),
137
+ ...(Array.isArray(options.observers) ? options.observers : []),
138
+ ],
139
+ onEvent: options.onEvent,
140
+ });
141
+ if (options.liveInput !== undefined && liveInputHub !== undefined) {
142
+ options = {
143
+ ...options,
144
+ liveInput: instrumentLiveInputAppliedEvents(options.liveInput, liveInputHub.emit),
145
+ };
146
+ }
147
+ try {
130
148
  /** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null), routeSafety?: import('../types.js').RuntimeRouteSafetyMode, safetyContract?: import('../types.js').RuntimeRouteSafetyContract}>} */
131
149
  const failoverHistory = [];
132
150
  /** @type {RuntimeResult|null} */
@@ -374,6 +392,9 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
374
392
  failoverHistory,
375
393
  routeSafetyHistory,
376
394
  };
395
+ } finally {
396
+ await liveInputHub?.flush();
397
+ }
377
398
  },
378
399
  chain: () => entries.slice(),
379
400
  configureTools(next = {}) {
package/src/ai/types.js CHANGED
@@ -131,6 +131,7 @@
131
131
  * @property {string} [executionMode] "sdk" (default) or "cli"; selects which bridge variant handles the model.
132
132
  * @property {string} [sessionId] Host conversation/session key for resumable bridges.
133
133
  * @property {string} [providerSessionId] Provider-owned resume id for resumable bridges.
134
+ * @property {typeof import("@anthropic-ai/claude-agent-sdk").query} [claudeAgentQuery] Advanced programmatic/test seam for the Claude SDK route; omitted runs use the runtime's pinned SDK query implementation.
134
135
  * @property {boolean} [sessionKeepAlive] Keep resumable provider state alive after the turn.
135
136
  * @property {number} [sessionIdleTimeoutMs] Idle TTL for resumable provider state.
136
137
  * @property {AsyncIterable<{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (error?: unknown) => void}>} [liveInput] Stream of in-flight user messages for steering an active run. Providers acknowledge only after accepting a message into the active turn.
package/src/runtime.js CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  import { createToolContext, updateToolContext } from "./agent/tools/shared/tool-context.js";
41
41
  import { resolveRuntimeBrand } from "./runtime-brand.js";
42
42
  import { retireDurableNativeSession } from "./ai/providers/pi-native/session-lifecycle.js";
43
+ import { instrumentLiveInputAppliedEvents } from "./ai/runtime/live-input-events.js";
43
44
 
44
45
  /**
45
46
  * @typedef {import('./ai/types.js').AgentRuntimeHostOptions} AgentRuntimeHostOptions
@@ -159,6 +160,7 @@ export function createRuntime(host = {}) {
159
160
  observers: [...hostObservers, ...callObservers],
160
161
  onEvent: options.onEvent,
161
162
  });
163
+ const liveInput = instrumentLiveInputAppliedEvents(options.liveInput, hub.emit);
162
164
  const prompts = resolvePrompts(host.prompts, options.prompts);
163
165
  const result = await bridge.execute(systemPrompt, {
164
166
  ...hostDefaults,
@@ -172,6 +174,7 @@ export function createRuntime(host = {}) {
172
174
  toolContext,
173
175
  observerHub: hub,
174
176
  onEvent: hub.emit,
177
+ ...(liveInput === undefined ? {} : { liveInput }),
175
178
  // Merged AFTER the spreads so the per-field run>host>default precedence
176
179
  // wins over either bag's whole-object `prompts`.
177
180
  ...(prompts === undefined ? {} : { prompts }),
@@ -84,8 +84,6 @@ export type AgentCompactionPolicy = {
84
84
  summaryMaxTokens: number;
85
85
  fixedOverheadEnabled: boolean;
86
86
  compactionMinSavingsTokens: number;
87
- toolPayloadCompactionTriggerChars: number;
88
- toolPruneTriggerTokens: number;
89
87
  toolTextLimitChars: number;
90
88
  bashOutputLimitChars: number;
91
89
  mcpTextLimitChars: number;
@@ -8,6 +8,9 @@ export function buildSkillPathNote({ assetsPath, skillsRoot }?: {
8
8
  skillsRoot?: any;
9
9
  }): string;
10
10
  /**
11
+ * Render a complete skill body plus its path note. Omitting `maxChars` returns
12
+ * the full text; pass a positive `maxChars` only when explicit truncation is
13
+ * required by the caller.
11
14
  * @param {{body?: string, assetsPath?: string, skillsRoot?: any, maxChars?: number}} [options]
12
15
  */
13
16
  export function formatSkillBodyWithPathNote({ body, assetsPath, skillsRoot, maxChars }?: {
@@ -3,5 +3,6 @@ export * from "./runtime/registry.js";
3
3
  export { createSessionRegistry, disposeAllProviderSessions, disposeProviderSession, invalidateProviderSession, refreshProviderSession, syncProviderSession } from "./runtime/sessions.js";
4
4
  export { createMetricsObserver, createObserverHub } from "./observer.js";
5
5
  export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
6
+ export { getPiBuiltinModel, listPiBuiltinModels, loginPiOAuth, reasoningLevelsForPiModel, resolvePiOAuthApiKey } from "./pi-interop.js";
6
7
  export { CLAUDE_SDK_CATALOG_VERSION, createClaudeSdkDiscoveryIsolation, curatedClaudeSdkModels, discoverClaudeSdkModels, normalizeClaudeSdkCatalog, normalizeClaudeSdkModelId } from "./providers/claude-sdk-discovery.js";
7
8
  export { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings, UNKNOWN_CAPABILITY } from "./runtime/capabilities-used.js";
@@ -0,0 +1,113 @@
1
+ /**
2
+ * List defensive snapshots of Pi's built-in models for one provider.
3
+ *
4
+ * @param {string} providerId
5
+ * @returns {PiBuiltinModelSnapshot[]}
6
+ */
7
+ export function listPiBuiltinModels(providerId: string): PiBuiltinModelSnapshot[];
8
+ /**
9
+ * Read a defensive snapshot of one Pi built-in model.
10
+ *
11
+ * @param {string} providerId
12
+ * @param {string} modelId
13
+ * @returns {PiBuiltinModelSnapshot|undefined}
14
+ */
15
+ export function getPiBuiltinModel(providerId: string, modelId: string): PiBuiltinModelSnapshot | undefined;
16
+ /**
17
+ * Translate Pi's model-native thinking levels to mono-agent effort spelling.
18
+ *
19
+ * @param {PiBuiltinModelSnapshot} model
20
+ * @returns {PiReasoningLevel[]}
21
+ */
22
+ export function reasoningLevelsForPiModel(model: PiBuiltinModelSnapshot): PiReasoningLevel[];
23
+ /**
24
+ * Resolve an OAuth-backed API key without allowing Pi to mutate the caller's
25
+ * credential record or returning Pi-owned credential objects.
26
+ *
27
+ * @param {string} providerId
28
+ * @param {Object<string, PiOAuthCredentialsSnapshot>} credentials
29
+ * @returns {Promise<{apiKey: string, newCredentials: PiOAuthCredentialsSnapshot}|null>}
30
+ */
31
+ export function resolvePiOAuthApiKey(providerId: string, credentials: {
32
+ [x: string]: PiOAuthCredentialsSnapshot;
33
+ }): Promise<{
34
+ apiKey: string;
35
+ newCredentials: PiOAuthCredentialsSnapshot;
36
+ } | null>;
37
+ /**
38
+ * Run a supported Pi OAuth login flow without exposing Pi's mutable provider
39
+ * registry or provider instances.
40
+ *
41
+ * @param {string} providerId
42
+ * @param {PiOAuthLoginCallbacks} callbacks
43
+ * @returns {Promise<PiOAuthCredentialsSnapshot>}
44
+ */
45
+ export function loginPiOAuth(providerId: string, callbacks: PiOAuthLoginCallbacks): Promise<PiOAuthCredentialsSnapshot>;
46
+ export type PiBuiltinModelSnapshot = {
47
+ id: string;
48
+ name: string;
49
+ api: string;
50
+ provider: string;
51
+ baseUrl: string;
52
+ reasoning: boolean;
53
+ input: Array<"text" | "image">;
54
+ cost: {
55
+ input: number;
56
+ output: number;
57
+ cacheRead: number;
58
+ cacheWrite: number;
59
+ tiers?: Array<{
60
+ inputTokensAbove: number;
61
+ input: number;
62
+ output: number;
63
+ cacheRead: number;
64
+ cacheWrite: number;
65
+ }>;
66
+ };
67
+ contextWindow: number;
68
+ maxTokens: number;
69
+ thinkingLevelMap?: {
70
+ [x: string]: string | null;
71
+ };
72
+ compat?: {
73
+ [x: string]: any;
74
+ };
75
+ headers?: {
76
+ [x: string]: string;
77
+ };
78
+ [key: string]: any;
79
+ };
80
+ export type PiReasoningLevel = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
81
+ export type PiOAuthCredentialsSnapshot = {
82
+ refresh: string;
83
+ access: string;
84
+ expires: number;
85
+ [key: string]: any;
86
+ };
87
+ export type PiOAuthLoginCallbacks = {
88
+ onAuth: (info: {
89
+ url: string;
90
+ instructions?: string;
91
+ }) => void;
92
+ onDeviceCode: (info: {
93
+ userCode: string;
94
+ verificationUri: string;
95
+ intervalSeconds?: number;
96
+ expiresInSeconds?: number;
97
+ }) => void;
98
+ onPrompt: (prompt: {
99
+ message: string;
100
+ placeholder?: string;
101
+ allowEmpty?: boolean;
102
+ }) => Promise<string>;
103
+ onProgress?: (message: string) => void;
104
+ onManualCodeInput?: () => Promise<string>;
105
+ onSelect: (prompt: {
106
+ message: string;
107
+ options: Array<{
108
+ id: string;
109
+ label: string;
110
+ }>;
111
+ }) => Promise<string | undefined>;
112
+ signal?: AbortSignal;
113
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @typedef {{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (reason?: unknown) => void}} RuntimeLiveInputMessage
3
+ * @typedef {{type: "live_input_applied", inputId: string, receivedAt?: string}} LiveInputAppliedEvent
4
+ */
5
+ /**
6
+ * @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
7
+ * @param {(event: LiveInputAppliedEvent) => void} onApplied
8
+ * @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
9
+ */
10
+ export function instrumentLiveInputAppliedEvents(liveInput: AsyncIterable<RuntimeLiveInputMessage> | undefined, onApplied: (event: LiveInputAppliedEvent) => void): AsyncIterable<RuntimeLiveInputMessage> | undefined;
11
+ export type RuntimeLiveInputMessage = {
12
+ body: string;
13
+ id?: string;
14
+ receivedAt?: string;
15
+ acknowledge?: () => void;
16
+ reject?: (reason?: unknown) => void;
17
+ };
18
+ export type LiveInputAppliedEvent = {
19
+ type: "live_input_applied";
20
+ inputId: string;
21
+ receivedAt?: string;
22
+ };
@@ -102,6 +102,7 @@
102
102
  * @property {string} [executionMode] "sdk" (default) or "cli"; selects which bridge variant handles the model.
103
103
  * @property {string} [sessionId] Host conversation/session key for resumable bridges.
104
104
  * @property {string} [providerSessionId] Provider-owned resume id for resumable bridges.
105
+ * @property {typeof import("@anthropic-ai/claude-agent-sdk").query} [claudeAgentQuery] Advanced programmatic/test seam for the Claude SDK route; omitted runs use the runtime's pinned SDK query implementation.
105
106
  * @property {boolean} [sessionKeepAlive] Keep resumable provider state alive after the turn.
106
107
  * @property {number} [sessionIdleTimeoutMs] Idle TTL for resumable provider state.
107
108
  * @property {AsyncIterable<{body: string, id?: string, receivedAt?: string, acknowledge?: () => void, reject?: (error?: unknown) => void}>} [liveInput] Stream of in-flight user messages for steering an active run. Providers acknowledge only after accepting a message into the active turn.
@@ -481,6 +482,10 @@ export type RuntimeRunOptions = {
481
482
  * Provider-owned resume id for resumable bridges.
482
483
  */
483
484
  providerSessionId?: string;
485
+ /**
486
+ * Advanced programmatic/test seam for the Claude SDK route; omitted runs use the runtime's pinned SDK query implementation.
487
+ */
488
+ claudeAgentQuery?: typeof import("@anthropic-ai/claude-agent-sdk").query;
484
489
  /**
485
490
  * Keep resumable provider state alive after the turn.
486
491
  */
@@ -1,57 +0,0 @@
1
- export function backendCapabilities(sdkOrModel: any): any;
2
- export function backendUsesExecenvConfig(sdk: any): boolean;
3
- export function backendSupportsSessionResume(sdk: any): boolean;
4
- export const BACKEND_CAPABILITIES: {
5
- claude: {
6
- supports_session_resume: boolean;
7
- streaming: boolean;
8
- structured_output: boolean;
9
- native_runtime_config: any;
10
- supports_mcp: boolean;
11
- supports_skills: boolean;
12
- supports_builtin_tools: boolean;
13
- supports_live_input: boolean;
14
- supports_native_subagents: boolean;
15
- supports_fast_mode: boolean;
16
- runtime: string;
17
- };
18
- pi: {
19
- supports_session_resume: boolean;
20
- supports_native_subagents: boolean;
21
- streaming: boolean;
22
- structured_output: boolean;
23
- native_runtime_config: any;
24
- supports_mcp: boolean;
25
- supports_skills: boolean;
26
- supports_builtin_tools: boolean;
27
- supports_live_input: boolean;
28
- supports_fast_mode: boolean;
29
- runtime: string;
30
- };
31
- codex: {
32
- supports_session_resume: boolean;
33
- supports_fast_mode: boolean;
34
- streaming: boolean;
35
- structured_output: boolean;
36
- native_runtime_config: any;
37
- supports_mcp: boolean;
38
- supports_skills: boolean;
39
- supports_builtin_tools: boolean;
40
- supports_live_input: boolean;
41
- supports_native_subagents: boolean;
42
- runtime: string;
43
- };
44
- opencode: {
45
- structured_output: boolean;
46
- supports_session_resume: boolean;
47
- supports_mcp: boolean;
48
- supports_skills: boolean;
49
- supports_live_input: boolean;
50
- supports_native_subagents: boolean;
51
- streaming: boolean;
52
- native_runtime_config: any;
53
- supports_builtin_tools: boolean;
54
- supports_fast_mode: boolean;
55
- runtime: string;
56
- };
57
- };
@@ -1 +0,0 @@
1
- export { listRuntimeBridges as listProviders, resolveRuntimeBridge as findProviderForModel, runtimeCapabilities } from "./runtime/registry.js";