@mono-agent/agent-runtime 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/MIGRATION.md +147 -69
  2. package/README.md +83 -20
  3. package/package.json +3 -6
  4. package/src/agent/compaction.js +0 -11
  5. package/src/agent/prompt/skill-index.js +5 -1
  6. package/src/ai/index.js +7 -1
  7. package/src/ai/observer.js +48 -13
  8. package/src/ai/pi-interop.js +156 -0
  9. package/src/ai/providers/claude-cli.js +2 -13
  10. package/src/ai/providers/claude-sdk.js +14 -8
  11. package/src/ai/providers/codex-app.js +235 -56
  12. package/src/ai/providers/opencode-app.js +168 -3
  13. package/src/ai/providers/pi-messages.js +0 -8
  14. package/src/ai/providers/pi-native/compaction-driver.js +106 -10
  15. package/src/ai/providers/pi-native/result-builder.js +2 -14
  16. package/src/ai/providers/pi-native/stream-subscriber.js +20 -2
  17. package/src/ai/providers/pi-native/turn-runner.js +31 -8
  18. package/src/ai/providers/pi-native.js +2 -5
  19. package/src/ai/runtime/live-input-events.js +94 -0
  20. package/src/ai/runtime/registry.js +8 -1
  21. package/src/ai/runtime/router.js +21 -0
  22. package/src/ai/types.js +2 -1
  23. package/src/runtime.js +3 -0
  24. package/types/agent/compaction.d.ts +0 -2
  25. package/types/agent/prompt/skill-index.d.ts +3 -0
  26. package/types/ai/index.d.ts +1 -1
  27. package/types/ai/observer.d.ts +4 -2
  28. package/types/ai/pi-interop.d.ts +113 -0
  29. package/types/ai/providers/claude-cli.d.ts +6 -30
  30. package/types/ai/providers/claude-sdk.d.ts +2 -9
  31. package/types/ai/providers/codex-app.d.ts +4 -10
  32. package/types/ai/providers/pi-messages.d.ts +0 -1
  33. package/types/ai/providers/pi-native/result-builder.d.ts +3 -11
  34. package/types/ai/providers/pi-native/stream-subscriber.d.ts +4 -2
  35. package/types/ai/providers/pi-native/turn-runner.d.ts +1 -1
  36. package/types/ai/runtime/live-input-events.d.ts +22 -0
  37. package/types/ai/types.d.ts +10 -2
  38. package/src/ai/backend.js +0 -17
  39. package/src/ai/registry.js +0 -5
  40. package/types/ai/backend.d.ts +0 -57
  41. package/types/ai/registry.d.ts +0 -1
package/src/ai/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // Public surface of the provider layer.
2
2
 
3
- export * from "./registry.js";
4
3
  export * from "./runtime/model-refs.js";
5
4
  export * from "./runtime/registry.js";
6
5
  export {
@@ -13,6 +12,13 @@ export {
13
12
  } from "./runtime/sessions.js";
14
13
  export { createMetricsObserver, createObserverHub } from "./observer.js";
15
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";
16
22
  export {
17
23
  CLAUDE_SDK_CATALOG_VERSION,
18
24
  createClaudeSdkDiscoveryIsolation,
@@ -82,10 +82,11 @@ function addObserver(list, observer) {
82
82
  // cache: { hits, misses, hitRatio }, // hitRatio in [0,1]; null if no signal
83
83
  // tools: { callsByName: { ... }, errorsByName: { ... } },
84
84
  // errors: { total, byKind: { ... } },
85
- // turns: { count, latencyMsP50, latencyMsP95 },
85
+ // turns: { count, sampleCount, latencyMsP50, latencyMsP95 },
86
86
  // approvals: { pending, granted, denied },
87
87
  // }
88
- export function createMetricsObserver({ name = "metrics" } = {}) {
88
+ export function createMetricsObserver({ name = "metrics", maxLatencySamples = 2_048 } = {}) {
89
+ const latencySampleLimit = normalizePositiveInteger(maxLatencySamples, 2_048);
89
90
  const state = {
90
91
  eventsTotal: 0,
91
92
  eventsByType: new Map(),
@@ -99,7 +100,8 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
99
100
  errorTotal: 0,
100
101
  errorsByKind: new Map(),
101
102
  turnLatencies: [],
102
- turnStartByModel: new Map(),
103
+ turnLatencyCount: 0,
104
+ pendingTurnStarts: [],
103
105
  approvalPending: 0,
104
106
  approvalGranted: 0,
105
107
  approvalDenied: 0,
@@ -167,17 +169,24 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
167
169
  }
168
170
 
169
171
  if (type === "provider_request_started" && event.model) {
170
- state.turnStartByModel.set(event.model, (Number.isFinite(event.timestamp) ? event.timestamp : Date.now()));
172
+ state.pendingTurnStarts.push({
173
+ key: latencyEventKey(event),
174
+ model: String(event.model),
175
+ timestamp: Number.isFinite(event.timestamp) ? event.timestamp : Date.now(),
176
+ });
177
+ if (state.pendingTurnStarts.length > latencySampleLimit) state.pendingTurnStarts.shift();
171
178
  }
172
179
  if (type === "provider_request_completed" && event.model) {
173
- const started = state.turnStartByModel.get(event.model);
174
- if (started !== undefined) {
175
- state.turnLatencies.push(Math.max(0, ((Number.isFinite(event.timestamp) ? event.timestamp : Date.now())) - started));
176
- state.turnStartByModel.delete(event.model);
180
+ const startIndex = matchingStartIndex(state.pendingTurnStarts, event);
181
+ const started = startIndex === -1 ? undefined : state.pendingTurnStarts.splice(startIndex, 1)[0];
182
+ const explicitDuration = Number(event.durationMs);
183
+ if (Number.isFinite(explicitDuration)) recordTurnLatency(Math.max(0, explicitDuration));
184
+ else if (started !== undefined) {
185
+ recordTurnLatency(Math.max(0, ((Number.isFinite(event.timestamp) ? event.timestamp : Date.now())) - started.timestamp));
177
186
  }
178
187
  }
179
188
  if (type === "turn_latency" && Number.isFinite(Number(event.durationMs))) {
180
- state.turnLatencies.push(Number(event.durationMs));
189
+ recordTurnLatency(Math.max(0, Number(event.durationMs)));
181
190
  }
182
191
 
183
192
  if (type === "tool_approval_pending") state.approvalPending += 1;
@@ -187,6 +196,12 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
187
196
 
188
197
  function recordMetric() { /* future hook */ }
189
198
 
199
+ function recordTurnLatency(durationMs) {
200
+ state.turnLatencyCount += 1;
201
+ state.turnLatencies.push(durationMs);
202
+ if (state.turnLatencies.length > latencySampleLimit) state.turnLatencies.shift();
203
+ }
204
+
190
205
  function snapshot() {
191
206
  const cacheTotal = state.cacheHits + state.cacheMisses;
192
207
  const hitRatio = cacheTotal > 0 ? state.cacheHits / cacheTotal : null;
@@ -205,7 +220,7 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
205
220
  errorsByName: Object.fromEntries(state.toolErrorsByName),
206
221
  },
207
222
  errors: { total: state.errorTotal, byKind: Object.fromEntries(state.errorsByKind) },
208
- turns: percentilesFor(state.turnLatencies),
223
+ turns: percentilesFor(state.turnLatencies, state.turnLatencyCount),
209
224
  approvals: { pending: state.approvalPending, granted: state.approvalGranted, denied: state.approvalDenied },
210
225
  };
211
226
  }
@@ -213,16 +228,36 @@ export function createMetricsObserver({ name = "metrics" } = {}) {
213
228
  return { name, recordEvent, recordMetric, snapshot };
214
229
  }
215
230
 
216
- function percentilesFor(samples) {
231
+ function percentilesFor(samples, count = samples.length) {
217
232
  const arr = Array.isArray(samples) ? samples.filter((n) => Number.isFinite(n)).slice().sort((a, b) => a - b) : [];
218
- if (!arr.length) return { count: 0, latencyMsP50: null, latencyMsP95: null };
233
+ if (!arr.length) return { count, sampleCount: 0, latencyMsP50: null, latencyMsP95: null };
219
234
  return {
220
- count: arr.length,
235
+ count,
236
+ sampleCount: arr.length,
221
237
  latencyMsP50: percentile(arr, 0.5),
222
238
  latencyMsP95: percentile(arr, 0.95),
223
239
  };
224
240
  }
225
241
 
242
+ function latencyEventKey(event) {
243
+ const requestId = event.requestId ?? event.turnId;
244
+ return requestId === undefined || requestId === null ? undefined : String(requestId);
245
+ }
246
+
247
+ function matchingStartIndex(starts, event) {
248
+ const key = latencyEventKey(event);
249
+ if (key !== undefined) {
250
+ const keyedIndex = starts.findIndex((start) => start.key === key);
251
+ if (keyedIndex !== -1) return keyedIndex;
252
+ }
253
+ return starts.findIndex((start) => start.model === String(event.model));
254
+ }
255
+
256
+ function normalizePositiveInteger(value, fallback) {
257
+ const number = Number(value);
258
+ return Number.isSafeInteger(number) && number > 0 ? number : fallback;
259
+ }
260
+
226
261
  function percentile(sortedArr, q) {
227
262
  if (!sortedArr.length) return null;
228
263
  const rank = q * (sortedArr.length - 1);
@@ -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
+ }
@@ -83,7 +83,8 @@ const DORMANT_CLI_CAPABILITIES = {
83
83
  supports_mcp: true,
84
84
  supports_skills: true,
85
85
  supports_builtin_tools: true,
86
- supports_live_input: true,
86
+ // The one-shot CLI bridge cannot add stdin messages after process launch.
87
+ supports_live_input: false,
87
88
  supports_native_subagents: true,
88
89
  };
89
90
 
@@ -776,18 +777,6 @@ export async function generateCliResponse(systemPrompt, options = {}) {
776
777
  }
777
778
  }
778
779
 
779
- export const claudeCodeBackend = {
780
- kind: "claude-code",
781
- capabilities: { kind: "claude-code", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
782
- execute: generateCliResponse,
783
- };
784
-
785
- export const codexCliBackend = {
786
- kind: "codex-cli",
787
- capabilities: { kind: "codex-cli", runtime: "cli", ...DORMANT_CLI_CAPABILITIES },
788
- execute: generateCliResponse,
789
- };
790
-
791
780
  // CLI bridge for sdk='claude' agents that opt into execution_mode='cli'.
792
781
  // generateCliResponse internally branches on resolved.sdk; the SDK shape
793
782
  // from parseModelReference uses 'claude', the CLI builder expects
@@ -563,7 +563,18 @@ function createClaudeCanUseTool(approvalManager, modelName) {
563
563
  async function* livePromptMessages({ initialPrompt, liveInput, sessionId, prompts }) {
564
564
  yield makeSdkUserMessage(initialPrompt, sessionId);
565
565
  for await (const message of liveInput) {
566
- yield makeSdkUserMessage(formatLiveInputGuidance(message.body, prompts), sessionId, message.id || randomUUID());
566
+ try {
567
+ const sdkMessage = makeSdkUserMessage(
568
+ formatLiveInputGuidance(message.body, prompts),
569
+ sessionId,
570
+ message.id || randomUUID(),
571
+ );
572
+ message.acknowledge?.();
573
+ yield sdkMessage;
574
+ } catch (err) {
575
+ message.reject?.(err);
576
+ throw err;
577
+ }
567
578
  }
568
579
  }
569
580
 
@@ -737,6 +748,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
737
748
  const prompt = options.liveInput
738
749
  ? livePromptMessages({ initialPrompt: promptString, liveInput: options.liveInput, sessionId: reusableProviderSessionId || randomUUID(), prompts: options.prompts })
739
750
  : promptString;
751
+ const claudeAgentQuery = options.claudeAgentQuery ?? query;
740
752
  const providerRequestStartedAt = Date.now();
741
753
  emitEvent({
742
754
  type: "provider_request_started",
@@ -794,7 +806,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
794
806
  }
795
807
 
796
808
  try {
797
- stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
809
+ stream = claudeAgentQuery({ prompt: /** @type {any} */ (prompt), options: queryOptions });
798
810
  for await (const event of stream) {
799
811
  const nextSessionId = sessionIdFromEvent(event);
800
812
  if (nextSessionId) providerSessionId = nextSessionId;
@@ -1051,12 +1063,6 @@ export async function generateClaudeResponse(systemPrompt, options) {
1051
1063
  };
1052
1064
  }
1053
1065
 
1054
- export const claudeSdkBackend = {
1055
- kind: "claude",
1056
- capabilities: runtimeCapabilities("claude"),
1057
- execute: generateClaudeResponse,
1058
- };
1059
-
1060
1066
  export const claudeRuntimeBridge = {
1061
1067
  id: "claude",
1062
1068
  kind: "claude",