@mono-agent/agent-runtime 0.20.11 → 0.21.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.
Files changed (148) hide show
  1. package/ARCHITECTURE.md +50 -11
  2. package/MIGRATION.md +288 -26
  3. package/README.md +352 -477
  4. package/package.json +13 -44
  5. package/src/agent/tool-bloat.js +145 -9
  6. package/src/agent/tools/agent-tool.js +108 -9
  7. package/src/agent/tools/bash.js +11 -26
  8. package/src/agent/tools/codex-subscription-search.js +123 -29
  9. package/src/agent/tools/exec.js +10 -2
  10. package/src/agent/tools/index.js +7 -0
  11. package/src/agent/tools/monitor.js +149 -0
  12. package/src/agent/tools/pi-bridge.js +123 -19
  13. package/src/agent/tools/shared/bash-environment.js +31 -0
  14. package/src/agent/tools/shared/monitors.js +293 -0
  15. package/src/agent/tools/shared/path-resolver.js +25 -6
  16. package/src/agent/tools/shared/process-jobs.js +6 -1
  17. package/src/agent/tools/shared/process-runner.js +26 -6
  18. package/src/agent/tools/shared/tool-context.js +8 -0
  19. package/src/agent/tools/web-access-interstitial.js +70 -0
  20. package/src/agent/tools/web-browser-render.js +83 -58
  21. package/src/agent/tools/web-controller.js +112 -21
  22. package/src/agent/tools/web-document-extractor.js +379 -0
  23. package/src/agent/tools/web-fetch.js +271 -243
  24. package/src/agent/tools/web-request.js +65 -0
  25. package/src/agent/tools/web-search-output.js +165 -0
  26. package/src/agent/tools/web-search-state.js +75 -0
  27. package/src/agent/tools/web-search.js +532 -71
  28. package/src/ai/cost.js +13 -68
  29. package/src/ai/failure.js +3 -3
  30. package/src/ai/index.js +5 -17
  31. package/src/ai/observer.js +8 -0
  32. package/src/ai/pi-interop.js +221 -1
  33. package/src/ai/pi-oauth-compat.js +1 -1
  34. package/src/ai/provider-check.js +131 -0
  35. package/src/ai/providers/codex/app-server-client.js +592 -0
  36. package/src/ai/providers/pi-models.js +18 -10
  37. package/src/ai/providers/pi-native/compaction-driver.js +94 -42
  38. package/src/ai/providers/pi-native/compaction-summary.js +140 -0
  39. package/src/ai/providers/pi-native/harness-adapter.js +376 -0
  40. package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
  41. package/src/ai/providers/pi-native/provider-attribution.js +102 -0
  42. package/src/ai/providers/pi-native/result-builder.js +38 -14
  43. package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
  44. package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
  45. package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
  46. package/src/ai/providers/pi-native/turn-runner.js +279 -28
  47. package/src/ai/providers/pi-native.js +206 -61
  48. package/src/ai/runtime/capabilities.js +11 -56
  49. package/src/ai/runtime/live-input-events.js +250 -54
  50. package/src/ai/runtime/model-refs.js +118 -153
  51. package/src/ai/runtime/registry.js +22 -56
  52. package/src/ai/runtime/router.js +76 -417
  53. package/src/ai/runtime/session-liveness.js +3 -4
  54. package/src/ai/runtime/sessions.js +4 -5
  55. package/src/ai/runtime/tool-policy.js +0 -2
  56. package/src/ai/tool-lifecycle.js +32 -18
  57. package/src/ai/types.js +37 -112
  58. package/src/index.js +0 -6
  59. package/src/runtime.js +29 -16
  60. package/types/agent/tool-bloat.d.ts +1 -1
  61. package/types/agent/tools/agent-tool.d.ts +4 -2
  62. package/types/agent/tools/bash.d.ts +5 -3
  63. package/types/agent/tools/codex-subscription-search.d.ts +7 -3
  64. package/types/agent/tools/exec.d.ts +5 -3
  65. package/types/agent/tools/index.d.ts +1 -0
  66. package/types/agent/tools/monitor.d.ts +47 -0
  67. package/types/agent/tools/pi-bridge.d.ts +7 -4
  68. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  69. package/types/agent/tools/shared/monitors.d.ts +98 -0
  70. package/types/agent/tools/shared/process-jobs.d.ts +5 -1
  71. package/types/agent/tools/shared/process-runner.d.ts +14 -4
  72. package/types/agent/tools/shared/tool-context.d.ts +2 -0
  73. package/types/agent/tools/web-access-interstitial.d.ts +23 -0
  74. package/types/agent/tools/web-browser-render.d.ts +4 -1
  75. package/types/agent/tools/web-controller.d.ts +4 -2
  76. package/types/agent/tools/web-document-extractor.d.ts +27 -0
  77. package/types/agent/tools/web-fetch.d.ts +19 -24
  78. package/types/agent/tools/web-request.d.ts +20 -0
  79. package/types/agent/tools/web-search-output.d.ts +31 -0
  80. package/types/agent/tools/web-search-state.d.ts +21 -0
  81. package/types/agent/tools/web-search.d.ts +10 -45
  82. package/types/ai/cost.d.ts +1 -2
  83. package/types/ai/index.d.ts +2 -4
  84. package/types/ai/observer.d.ts +6 -0
  85. package/types/ai/pi-interop.d.ts +81 -0
  86. package/types/ai/provider-check.d.ts +53 -0
  87. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  88. package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
  89. package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
  90. package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
  91. package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
  92. package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
  93. package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
  94. package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
  95. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  96. package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
  97. package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
  98. package/types/ai/providers/pi-native.d.ts +21 -4
  99. package/types/ai/runtime/capabilities.d.ts +21 -70
  100. package/types/ai/runtime/live-input-events.d.ts +32 -8
  101. package/types/ai/runtime/model-refs.d.ts +0 -24
  102. package/types/ai/runtime/router.d.ts +3 -10
  103. package/types/ai/runtime/tool-policy.d.ts +0 -2
  104. package/types/ai/tool-lifecycle.d.ts +4 -3
  105. package/types/ai/types.d.ts +162 -256
  106. package/types/index.d.ts +0 -1
  107. package/src/ai/providers/acp-client.js +0 -1149
  108. package/src/ai/providers/acp-privacy.js +0 -124
  109. package/src/ai/providers/acp-public.js +0 -21
  110. package/src/ai/providers/acp-session-tokens.js +0 -282
  111. package/src/ai/providers/acp-transport.js +0 -356
  112. package/src/ai/providers/acp.js +0 -543
  113. package/src/ai/providers/claude-cli.js +0 -883
  114. package/src/ai/providers/claude-sandbox.js +0 -71
  115. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  116. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  117. package/src/ai/providers/claude-sdk.js +0 -1127
  118. package/src/ai/providers/claude-subagent-activity.js +0 -719
  119. package/src/ai/providers/claude-subagents.js +0 -88
  120. package/src/ai/providers/codex-app.js +0 -2946
  121. package/src/ai/providers/opencode-app.js +0 -1109
  122. package/src/ai/providers/opencode-discovery.js +0 -39
  123. package/src/ai/providers/opencode-server.js +0 -508
  124. package/src/ai/runtime/context-windows.js +0 -46
  125. package/src/ai/runtime/fast-mode.js +0 -8
  126. package/src/ai/streaming/codex-events.js +0 -146
  127. package/src/ai/streaming/opencode-events.js +0 -59
  128. package/types/ai/providers/acp-client.d.ts +0 -227
  129. package/types/ai/providers/acp-privacy.d.ts +0 -25
  130. package/types/ai/providers/acp-public.d.ts +0 -7
  131. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  132. package/types/ai/providers/acp-transport.d.ts +0 -45
  133. package/types/ai/providers/acp.d.ts +0 -93
  134. package/types/ai/providers/claude-cli.d.ts +0 -305
  135. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  136. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  137. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  138. package/types/ai/providers/claude-sdk.d.ts +0 -138
  139. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  140. package/types/ai/providers/claude-subagents.d.ts +0 -18
  141. package/types/ai/providers/codex-app.d.ts +0 -151
  142. package/types/ai/providers/opencode-app.d.ts +0 -96
  143. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  144. package/types/ai/providers/opencode-server.d.ts +0 -20
  145. package/types/ai/runtime/context-windows.d.ts +0 -9
  146. package/types/ai/runtime/fast-mode.d.ts +0 -2
  147. package/types/ai/streaming/codex-events.d.ts +0 -40
  148. package/types/ai/streaming/opencode-events.d.ts +0 -42
@@ -1,68 +1,102 @@
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.
1
+ // Metadata-only live-input lifecycle instrumentation and logical-run replay fencing.
2
+ // Stable ids identify one logical delivery across provider retries. The first
3
+ // occurrence owns the body and identity; later same-id occurrences are invalid
4
+ // duplicates unless an exact opaque logical-owner token proves they carry a new
5
+ // callback lease for that first owner. Anonymous input remains legal but is
6
+ // never replayed.
8
7
 
9
8
  // @ts-check
10
9
 
11
- const LIVE_INPUT_APPLIED_INSTRUMENTED = Symbol("mono-agent.live-input-applied-instrumented");
10
+ const LIVE_INPUT_INSTRUMENTED = Symbol("mono-agent.live-input-instrumented");
11
+ const MAX_DIAGNOSTICS_PER_KIND = 100;
12
12
 
13
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
14
+ * @typedef {{providerEntryId?: string, providerRunId?: string}} RuntimeLiveInputEvidence
15
+ * @typedef {{reason: "delivery_uncertain", providerEntryId?: string, providerRunId?: string}} RuntimeLiveInputUncertainty
16
+ * @typedef {{body: string, id?: string, receivedAt?: string, logicalOwner?: object, accepted?: (evidence?: RuntimeLiveInputEvidence) => unknown, acknowledge?: (evidence?: RuntimeLiveInputEvidence) => unknown, uncertain?: (details: RuntimeLiveInputUncertainty) => unknown, reject?: (reason?: unknown) => unknown}} RuntimeLiveInputMessage
17
+ * @typedef {{message: RuntimeLiveInputMessage, callbackMessage: RuntimeLiveInputMessage, logicalOwner?: object, inputId: string, receivedAt?: string, phase: "available"|"leased"|"native_accepted"|"consumed"|"uncertain", attempt: number, generation: number}} LiveInputOwner
18
+ * @typedef {{type: string, [key: string]: unknown}} LiveInputEvent
16
19
  */
17
20
 
18
21
  /**
19
22
  * @param {AsyncIterable<RuntimeLiveInputMessage>|undefined} liveInput
20
- * @param {(event: LiveInputAppliedEvent) => void} onApplied
23
+ * @param {(event: LiveInputEvent) => void} onEvent
21
24
  * @returns {AsyncIterable<RuntimeLiveInputMessage>|undefined}
22
25
  */
23
- export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
26
+ export function instrumentLiveInputAppliedEvents(liveInput, onEvent) {
24
27
  if (liveInput === undefined || isInstrumented(liveInput)) return liveInput;
25
28
 
26
- const appliedInputIds = new Set();
29
+ /** @type {Map<string, LiveInputOwner>} */
30
+ const owners = new Map();
31
+ const diagnosticCounts = new Map();
32
+ let generation = 0;
33
+
27
34
  const instrumented = {
28
- [LIVE_INPUT_APPLIED_INSTRUMENTED]: true,
35
+ [LIVE_INPUT_INSTRUMENTED]: true,
29
36
  [Symbol.asyncIterator]() {
37
+ generation += 1;
38
+ const iteratorGeneration = generation;
30
39
  const iterator = liveInput[Symbol.asyncIterator]();
40
+ const yieldedIds = new Set();
31
41
  let ordinal = 0;
42
+
32
43
  return {
33
44
  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
- };
45
+ while (true) {
46
+ const next = await iterator.next();
47
+ if (next.done === true) return next;
48
+ ordinal += 1;
49
+ const message = next.value;
50
+ const stableId = normalizedStableId(message?.id);
51
+
52
+ if (stableId === undefined) {
53
+ if (iteratorGeneration > 1) {
54
+ emitBounded("anonymous_identity", {
55
+ type: "live_input_replay_suppressed",
56
+ reason: "anonymous_identity",
57
+ generation: iteratorGeneration,
58
+ ordinal,
59
+ });
60
+ continue;
61
+ }
62
+ const owner = createOwner(message, `anonymous:${iteratorGeneration}:${ordinal}`, iteratorGeneration);
63
+ return { done: false, value: lease(owner) };
64
+ }
65
+
66
+ const existing = owners.get(stableId);
67
+ if (existing === undefined) {
68
+ const owner = createOwner(message, stableId, iteratorGeneration);
69
+ owners.set(stableId, owner);
70
+ yieldedIds.add(stableId);
71
+ return { done: false, value: lease(owner) };
72
+ }
73
+
74
+ emitBounded("duplicate_id", {
75
+ type: "live_input_duplicate_suppressed",
76
+ inputId: stableId,
77
+ generation: iteratorGeneration,
78
+ });
79
+ if (
80
+ iteratorGeneration > existing.generation
81
+ && existing.phase === "available"
82
+ && !yieldedIds.has(stableId)
83
+ ) {
84
+ // Suppress the later occurrence itself. It merely reveals that the
85
+ // first owner is present in this replay generation; replay the
86
+ // first owner's immutable body and identity.
87
+ yieldedIds.add(stableId);
88
+ existing.generation = iteratorGeneration;
89
+ if (
90
+ existing.logicalOwner !== undefined
91
+ && existing.logicalOwner === validLogicalOwner(message?.logicalOwner)
92
+ ) {
93
+ // The logical owner remains the first occurrence. This is only
94
+ // its fresh, independently fenced host callback lease.
95
+ existing.callbackMessage = message;
96
+ }
97
+ return { done: false, value: lease(existing) };
98
+ }
99
+ }
66
100
  },
67
101
  async return(value) {
68
102
  return typeof iterator.return === "function"
@@ -76,19 +110,181 @@ export function instrumentLiveInputAppliedEvents(liveInput, onApplied) {
76
110
  };
77
111
  },
78
112
  };
113
+
79
114
  return /** @type {AsyncIterable<RuntimeLiveInputMessage>} */ (instrumented);
115
+
116
+ /** @param {RuntimeLiveInputMessage} message @param {string} inputId @param {number} ownerGeneration */
117
+ function createOwner(message, inputId, ownerGeneration) {
118
+ const receivedAt = validString(message?.receivedAt);
119
+ const logicalOwner = validLogicalOwner(message?.logicalOwner);
120
+ return {
121
+ message,
122
+ callbackMessage: message,
123
+ ...(logicalOwner === undefined ? {} : { logicalOwner }),
124
+ inputId,
125
+ ...(receivedAt === undefined ? {} : { receivedAt }),
126
+ phase: /** @type {const} */ ("available"),
127
+ attempt: 0,
128
+ generation: ownerGeneration,
129
+ };
130
+ }
131
+
132
+ /** @param {LiveInputOwner} owner */
133
+ function lease(owner) {
134
+ owner.phase = "leased";
135
+ owner.attempt += 1;
136
+ const attempt = owner.attempt;
137
+ const host = owner.callbackMessage;
138
+ const base = eventBase(owner);
139
+
140
+ /** @type {RuntimeLiveInputMessage} */
141
+ const wrapped = {
142
+ ...owner.message,
143
+ accepted(evidence) {
144
+ if (!isCurrent(owner, attempt, "leased")) return "ignored";
145
+ owner.phase = "native_accepted";
146
+ const disposition = callHost(host.accepted, host, evidence);
147
+ emit({
148
+ type: "live_input_native_accepted",
149
+ ...base,
150
+ ...safeEvidence(evidence),
151
+ settlementDisposition: disposition,
152
+ });
153
+ return publicDisposition(disposition);
154
+ },
155
+ acknowledge(evidence) {
156
+ if (!isCurrent(owner, attempt, "leased", "native_accepted")) {
157
+ emit({ type: "live_input_consumed", ...base, ...safeEvidence(evidence), late: true });
158
+ return "ignored";
159
+ }
160
+ owner.phase = "consumed";
161
+ emit({ type: "live_input_consumed", ...base, ...safeEvidence(evidence) });
162
+ const disposition = callHost(host.acknowledge, host, evidence);
163
+ if (disposition === "recorded") {
164
+ emit({ type: "live_input_applied", ...base, ...safeEvidence(evidence) });
165
+ } else {
166
+ emit({
167
+ type: "live_input_settlement_unconfirmed",
168
+ ...base,
169
+ phase: "consumed",
170
+ settlementDisposition: disposition,
171
+ });
172
+ }
173
+ return publicDisposition(disposition);
174
+ },
175
+ uncertain(details) {
176
+ if (!isCurrent(owner, attempt, "leased", "native_accepted")) return "ignored";
177
+ owner.phase = "uncertain";
178
+ const normalized = {
179
+ reason: /** @type {const} */ ("delivery_uncertain"),
180
+ ...safeEvidence(details),
181
+ };
182
+ const disposition = callHost(host.uncertain, host, normalized);
183
+ emit({
184
+ type: "live_input_uncertain",
185
+ ...base,
186
+ ...normalized,
187
+ settlementDisposition: disposition,
188
+ });
189
+ return publicDisposition(disposition);
190
+ },
191
+ reject(reason) {
192
+ if (!isCurrent(owner, attempt, "leased", "native_accepted")) return "ignored";
193
+ if (owner.phase === "native_accepted" && !isNativeQueueRemoved(reason)) {
194
+ return wrapped.uncertain?.({ reason: "delivery_uncertain" });
195
+ }
196
+ const disposition = callHost(host.reject, host, reason);
197
+ // "ignored" is affirmative host evidence that this attempt no longer
198
+ // owns settlement (normally because the mailbox already sealed it).
199
+ // Legacy void and thrown callbacks remain replay-compatible because
200
+ // native removal still proves the provider did not consume the input.
201
+ owner.phase = disposition === "ignored" ? "uncertain" : "available";
202
+ return publicDisposition(disposition);
203
+ },
204
+ };
205
+ return wrapped;
206
+ }
207
+
208
+ /** @param {string} kind @param {LiveInputEvent} event */
209
+ function emitBounded(kind, event) {
210
+ const count = diagnosticCounts.get(kind) ?? 0;
211
+ if (count >= MAX_DIAGNOSTICS_PER_KIND) return;
212
+ diagnosticCounts.set(kind, count + 1);
213
+ emit(event);
214
+ }
215
+
216
+ /** @param {LiveInputEvent} event */
217
+ function emit(event) {
218
+ try { onEvent(event); } catch { /* telemetry never changes delivery */ }
219
+ }
220
+ }
221
+
222
+ /** @param {unknown} callback @param {RuntimeLiveInputMessage} receiver @param {unknown} argument */
223
+ function callHost(callback, receiver, argument) {
224
+ if (typeof callback !== "function") return "unconfirmed";
225
+ try {
226
+ const result = callback.call(receiver, argument);
227
+ return result === "recorded" || result === "ignored" ? result : "unconfirmed";
228
+ } catch {
229
+ return "threw";
230
+ }
231
+ }
232
+
233
+ /** @param {string} disposition */
234
+ function publicDisposition(disposition) {
235
+ return disposition === "recorded" || disposition === "ignored" ? disposition : undefined;
236
+ }
237
+
238
+ /** @param {LiveInputOwner} owner */
239
+ function eventBase(owner) {
240
+ return {
241
+ inputId: owner.inputId,
242
+ ...(owner.receivedAt === undefined ? {} : { receivedAt: owner.receivedAt }),
243
+ };
244
+ }
245
+
246
+ /** @param {LiveInputOwner} owner @param {number} attempt @param {...string} phases */
247
+ function isCurrent(owner, attempt, ...phases) {
248
+ return owner.attempt === attempt && phases.includes(owner.phase);
80
249
  }
81
250
 
82
251
  /** @param {unknown} value */
83
- function isInstrumented(value) {
84
- return typeof value === "object"
85
- && value !== null
86
- && value[LIVE_INPUT_APPLIED_INSTRUMENTED] === true;
252
+ function normalizedStableId(value) {
253
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
87
254
  }
88
255
 
89
- /** @param {unknown} value @param {number} ordinal */
90
- function stableInputId(value, ordinal) {
91
- return typeof value === "string" && value.trim().length > 0
256
+ /** @param {unknown} value */
257
+ function validLogicalOwner(value) {
258
+ return value !== null && (typeof value === "object" || typeof value === "function")
92
259
  ? value
93
- : `anonymous:${ordinal}`;
260
+ : undefined;
261
+ }
262
+
263
+ /** @param {unknown} value */
264
+ function validString(value) {
265
+ return typeof value === "string" && value.length > 0 ? value : undefined;
266
+ }
267
+
268
+ /** @param {unknown} value */
269
+ function safeEvidence(value) {
270
+ if (typeof value !== "object" || value === null) return {};
271
+ const evidence = /** @type {{providerEntryId?: unknown, providerRunId?: unknown}} */ (value);
272
+ return {
273
+ ...(validString(evidence.providerEntryId) === undefined ? {} : { providerEntryId: evidence.providerEntryId }),
274
+ ...(validString(evidence.providerRunId) === undefined ? {} : { providerRunId: evidence.providerRunId }),
275
+ };
276
+ }
277
+
278
+ /** @param {unknown} reason */
279
+ function isNativeQueueRemoved(reason) {
280
+ return typeof reason === "object"
281
+ && reason !== null
282
+ && /** @type {{code?: unknown}} */ (reason).code === "native_queue_removed";
283
+ }
284
+
285
+ /** @param {unknown} value */
286
+ function isInstrumented(value) {
287
+ return typeof value === "object"
288
+ && value !== null
289
+ && value[LIVE_INPUT_INSTRUMENTED] === true;
94
290
  }
@@ -2,70 +2,139 @@
2
2
 
3
3
  /** @typedef {import('../types.js').RuntimeModelRef} RuntimeModelRef */
4
4
 
5
- const RESERVED_RUNTIME_IDS = new Set(["openai", "vercel", "claude-code", "codex-cli"]);
6
- const ACTIVE_RUNTIME_IDS = new Set(["claude", "pi", "codex", "opencode", "acp"]);
7
- const ACP_PROFILE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
+ const PROVIDER_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8
6
 
9
- function requirePart(value, message) {
10
- if (!value || typeof value !== "string" || value.trim() !== value) {
11
- throw new Error(message);
7
+ /**
8
+ * Control (`Cc`), format (`Cf`), line- and paragraph-separator code points. Every one of them
9
+ * either moves the cursor or is invisible, which is what lets a model id restyle or extend
10
+ * the diagnostic that quotes it -- and an *accepted* reference is quoted verbatim, without
11
+ * re-validation, by `mono-agent validate`, `doctor`, the TUI, the web console, the daemon log
12
+ * and launchd's captured stdout, all of them line-oriented and durable.
13
+ *
14
+ * Deliberately the same set that `DIAGNOSTIC_UNSAFE_CHARACTERS` in @mono-agent/runtime-adapter
15
+ * escapes when it quotes a *rejected* value: that layer makes an unparseable value safe to
16
+ * print, this one makes a parsed value safe to print. runtime-adapter's model-reference bound
17
+ * suite asserts the two agree code point by code point rather than by comment.
18
+ */
19
+ const UNQUOTABLE_REFERENCE_CHARACTERS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
20
+
21
+ /**
22
+ * @param {string} provider
23
+ * @param {string} model
24
+ */
25
+ function rejectRemovedRuntimeReference(provider, model) {
26
+ if (provider === "codex") {
27
+ throw new Error(`codex is no longer a runtime backend; use openai-codex:${model}`);
28
+ }
29
+ if (provider === "claude" || provider === "claude-code") {
30
+ throw new Error(`${provider} is no longer a runtime backend; use anthropic:${model}`);
31
+ }
32
+ if (provider === "codex-cli") {
33
+ throw new Error(`codex-cli is no longer a runtime backend; use openai-codex:${model}`);
34
+ }
35
+ if (provider === "acp") {
36
+ throw new Error(
37
+ "ACP is no longer a runtime backend; use <provider>:<model> for Pi models, or mono-agent bridge acp to serve mono-agent over ACP",
38
+ );
39
+ }
40
+ if (provider === "vercel") {
41
+ const replacement = model.includes(":") ? model : "<provider>:<model>";
42
+ throw new Error(`vercel:<provider>:<model> is no longer supported; use ${replacement} directly`);
43
+ }
44
+ if (provider === "opencode" && model.includes(":")) {
45
+ throw new Error(`the opencode:<provider>:<model> runtime form is no longer supported; use ${model}`);
46
+ }
47
+ }
48
+
49
+ /** @param {string} provider */
50
+ function requireProvider(provider) {
51
+ if (!PROVIDER_ID_RE.test(provider)) {
52
+ throw new Error("invalid provider id; expected [A-Za-z0-9][A-Za-z0-9._-]*");
12
53
  }
13
- return value;
14
54
  }
15
55
 
16
- function rejectTierAlias(model) {
56
+ /** @param {string} model */
57
+ function requireModel(model) {
58
+ if (!model || model.trim() !== model) {
59
+ throw new Error("model id must be a non-empty trimmed string");
60
+ }
17
61
  if (["haiku", "sonnet", "opus"].includes(model)) {
18
- throw new Error("tier aliases are not valid model references; use an exact model id");
62
+ throw new Error("tier aliases are not valid model ids; use an exact model id");
19
63
  }
20
64
  }
21
65
 
22
66
  /**
23
- * @param {string} value
24
- * @returns {string}
67
+ * The one thing a parsed reference is guaranteed to CONTAIN nothing of.
68
+ *
69
+ * A reference this parser returns is handed to every operator surface that names a model and
70
+ * quoted verbatim by each of them, without re-validation: `mono-agent validate`, `doctor`, the
71
+ * TUI, the web console, the daemon log, launchd's captured stdout. All of them are
72
+ * line-oriented and most are durable, so a control or formatting code point in the value is
73
+ * enough to restyle the diagnostic quoting it or forge a second line inside it. No legitimate
74
+ * model id contains one, nothing downstream can repair one after the fact, and escaping it at
75
+ * six renderers is six chances to miss. So it is refused here, at the source, absolutely.
76
+ *
77
+ * There is deliberately NO length rule to go with it. A grammar layer does not get to decide
78
+ * what a provider may call a model, and three rounds of trying produced three wrong answers:
79
+ * 96 bytes refused a Hugging Face GGUF repo Ollama serves today, 160 bytes refused an
80
+ * `ollama:<model>:<tag>` reference whose two halves Ollama itself validates at 80 bytes each,
81
+ * and no maximum is published in common across Ollama, LM Studio, OpenRouter and custom
82
+ * `openai_compat` endpoints from which a third guess would be any better. mono-agent's own
83
+ * `discoverLocalProviderModels` was returning ids this parser then refused.
84
+ *
85
+ * The requirements that ceiling was carrying are all met by the layers that render or transmit
86
+ * a reference, where the answer to "too long" is a shorter STRING rather than a lost route:
87
+ * - a diagnostic echo is clamped by truncation, marking the cut -- `sanitizeModelReferenceText`
88
+ * on `MODEL_REFERENCE_ECHO_MAX_BYTES`, @mono-agent/runtime-adapter;
89
+ * - an adapter error body is clamped by `sendJsonError`;
90
+ * - the `/v1/info` payload is bounded by per-contributor measured budgets and a total
91
+ * serializer fence that sheds whole fields (`channel-drivers/tui.ts`, `sendBoundedInfo`).
92
+ * Each of those is asserted at its own layer; none of them is asserted here, because none of
93
+ * them is this function's job.
94
+ *
95
+ * Runs last, after `rejectRemovedRuntimeReference`, so a retired backend still gets its
96
+ * concrete repair named (`codex:x` -> `openai-codex:x`) instead of a generic shape complaint.
97
+ * Rejection messages are operator-supplied text too, and are bounded where they are rendered.
98
+ *
99
+ * @param {string} reference
25
100
  */
26
- export function canonicalizeLegacyModelReference(value) {
27
- if (!value || typeof value !== "string") throw new Error("model reference required");
28
-
29
- if (value.startsWith("openai:")) {
30
- const model = requirePart(value.slice("openai:".length), "model id required");
31
- return `pi:openai:${model}`;
32
- }
33
- if (value.startsWith("codex:")) {
34
- const model = requirePart(value.slice("codex:".length), "model id required");
35
- return `codex:${model}`;
36
- }
37
- if (value.startsWith("vercel:")) {
38
- const rest = value.slice("vercel:".length);
39
- const i = rest.indexOf(":");
40
- if (i <= 0 || i === rest.length - 1) {
41
- throw new Error("invalid vercel model reference; expected vercel:<providerId>:<modelName>");
42
- }
43
- const provider = requirePart(rest.slice(0, i), "provider id required");
44
- const model = requirePart(rest.slice(i + 1), "model name required");
45
- return `pi:${provider}:${model}`;
46
- }
47
- if (value.startsWith("claude-code:")) {
48
- const model = requirePart(value.slice("claude-code:".length), "model id required");
49
- return `claude:${model}`;
101
+ function requireQuotableReference(reference) {
102
+ if (UNQUOTABLE_REFERENCE_CHARACTERS.test(reference)) {
103
+ throw new Error("model reference must not contain control or formatting characters");
50
104
  }
51
- return value;
52
105
  }
53
106
 
54
107
  /**
108
+ * Parse the provider/model pair after the optional legacy `pi:` wrapper has
109
+ * been removed. Pi model ids are opaque and commonly contain further colons,
110
+ * so only the first colon is structural.
111
+ *
55
112
  * @param {string} value
56
113
  * @returns {RuntimeModelRef}
57
114
  */
58
- export function normalizeRuntimeModelReference(value) {
59
- return parseRuntimeModelReference(canonicalizeLegacyModelReference(value));
115
+ function parseCanonicalReference(value) {
116
+ const separator = value.indexOf(":");
117
+ if (separator <= 0 || separator === value.length - 1) {
118
+ throw new Error("invalid model reference; expected <provider>:<model>");
119
+ }
120
+
121
+ const provider = value.slice(0, separator);
122
+ const model = value.slice(separator + 1);
123
+ const reference = `${provider}:${model}`;
124
+ requireProvider(provider);
125
+ requireModel(model);
126
+ rejectRemovedRuntimeReference(provider, model);
127
+ requireQuotableReference(reference);
128
+
129
+ return { provider, model, reference };
60
130
  }
61
131
 
62
132
  /**
63
133
  * @param {string} value
64
- * @returns {RuntimeModelRef["sdk"]}
134
+ * @returns {RuntimeModelRef}
65
135
  */
66
- export function sdkFromModelReference(value) {
67
- const parsed = parseRuntimeModelReference(value);
68
- return parsed.sdk;
136
+ export function normalizeRuntimeModelReference(value) {
137
+ return parseRuntimeModelReference(value);
69
138
  }
70
139
 
71
140
  /**
@@ -73,121 +142,17 @@ export function sdkFromModelReference(value) {
73
142
  * @returns {RuntimeModelRef}
74
143
  */
75
144
  export function parseRuntimeModelReference(value) {
76
- if (!value || typeof value !== "string") throw new Error("model reference required");
77
-
78
- if (value.startsWith("acp:")) {
79
- const profileId = requirePart(value.slice("acp:".length), "ACP profile id required");
80
- if (!ACP_PROFILE_ID_RE.test(profileId)) {
81
- throw new Error("invalid acp model reference; expected acp:<profile-id>");
82
- }
83
- return { sdk: "acp", model: profileId, reference: value };
145
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
146
+ throw new Error("model reference must be a non-empty trimmed string");
84
147
  }
85
148
 
86
149
  if (value.startsWith("pi:")) {
87
- const rest = value.slice("pi:".length);
88
- const i = rest.indexOf(":");
89
- if (i <= 0 || i === rest.length - 1) {
90
- throw new Error("invalid pi model reference; expected pi:<providerId>:<modelName>");
91
- }
92
- const provider = requirePart(rest.slice(0, i), "provider id required");
93
- const model = requirePart(rest.slice(i + 1), "model id required");
94
- return { sdk: "pi", provider, model, reference: value };
95
- }
96
-
97
- if (value.startsWith("opencode:")) {
98
- // opencode:<providerID>:<modelID> — providerID/modelID come from OpenCode's own
99
- // provider registry (auth.json). Only the first colon separates them; modelID may
100
- // contain slashes (e.g. openrouter's `anthropic/claude-3.5-sonnet`).
101
- const rest = value.slice("opencode:".length);
102
- const i = rest.indexOf(":");
103
- if (i <= 0 || i === rest.length - 1) {
104
- throw new Error("invalid opencode model reference; expected opencode:<providerId>:<modelId>");
105
- }
106
- const provider = requirePart(rest.slice(0, i), "provider id required");
107
- const model = requirePart(rest.slice(i + 1), "model id required");
108
- return { sdk: "opencode", provider, model, reference: value };
109
- }
110
-
111
- const i = value.indexOf(":");
112
- if (i <= 0 || i === value.length - 1) {
113
- throw new Error("invalid model reference; expected <sdk>:<modelId>");
114
- }
115
- const sdk = value.slice(0, i);
116
- const model = requirePart(value.slice(i + 1), "model id required");
117
-
118
- if (RESERVED_RUNTIME_IDS.has(sdk)) {
119
- throw new Error(`reserved runtime id: ${sdk}; use a canonical pi:*, claude:*, or codex:* model reference`);
120
- }
121
- if (!ACTIVE_RUNTIME_IDS.has(sdk)) {
122
- throw new Error(`unknown sdk: ${sdk}`);
123
- }
124
- rejectTierAlias(model);
125
- return { sdk, model, reference: value };
126
- }
127
-
128
- export const ACTIVE_RUNTIME_KINDS = [...ACTIVE_RUNTIME_IDS];
129
- export const RESERVED_RUNTIME_KINDS = [...RESERVED_RUNTIME_IDS];
130
-
131
- // intelligence-ramp: which model refs can run under which execution_mode.
132
- // sdk='acp' → ACP only (dedicated stdio client mode)
133
- // sdk='claude' → CLI (claude binary) or SDK (Anthropic)
134
- // sdk='codex' → CLI only (codex app-server)
135
- // sdk='opencode' → CLI only (opencode server via @opencode-ai/sdk)
136
- // sdk='pi' → SDK only (the pi-native bridge handles openai-codex and other providers)
137
-
138
- // Returns null when the combo is fine; otherwise a short reason string the
139
- // UI / API can show.
140
- /**
141
- * @param {string|RuntimeModelRef} modelRefOrParsed
142
- * @param {string} executionMode
143
- * @returns {string|null}
144
- */
145
- export function executionModeIncompatibilityReason(modelRefOrParsed, executionMode) {
146
- let parsed;
147
- try {
148
- parsed = typeof modelRefOrParsed === "string"
149
- ? parseRuntimeModelReference(modelRefOrParsed)
150
- : modelRefOrParsed;
151
- } catch {
152
- return null;
153
- }
154
- if (!parsed) return null;
155
- if (!executionMode) return null;
156
- if (executionMode === "acp") {
157
- return parsed.sdk === "acp"
158
- ? null
159
- : `sdk \`${parsed.sdk}\` is not supported under ACP execution mode.`;
160
- }
161
- if (executionMode === "sdk") {
162
- if (parsed.sdk === "codex") {
163
- return "Codex CLI requires CLI execution mode.";
150
+ const wrapped = value.slice("pi:".length);
151
+ if (wrapped.indexOf(":") <= 0 || wrapped.endsWith(":")) {
152
+ throw new Error("invalid pi model reference; use pi:<provider>:<model>");
164
153
  }
165
- if (parsed.sdk === "opencode") {
166
- return "OpenCode CLI requires CLI execution mode.";
167
- }
168
- if (parsed.sdk === "acp") {
169
- return "ACP profiles require ACP execution mode.";
170
- }
171
- return null;
172
- }
173
- if (executionMode !== "cli") return null;
174
- if (parsed.sdk === "claude") return null;
175
- if (parsed.sdk === "codex") return null;
176
- if (parsed.sdk === "opencode") return null;
177
- if (parsed.sdk === "acp") return "ACP profiles require ACP execution mode.";
178
- if (parsed.sdk === "pi") {
179
- const provider = parsed.provider || "unknown";
180
- const suffix = provider === "openai-codex" ? "; use codex:<model> for Codex CLI" : "";
181
- return `Provider \`${provider}\` only runs under SDK execution mode${suffix}.`;
154
+ return parseCanonicalReference(wrapped);
182
155
  }
183
- return `sdk \`${parsed.sdk}\` is not supported under CLI execution mode.`;
184
- }
185
156
 
186
- /**
187
- * @param {string|RuntimeModelRef} modelRefOrParsed
188
- * @param {string} executionMode
189
- * @returns {boolean}
190
- */
191
- export function isModelCompatibleWithExecutionMode(modelRefOrParsed, executionMode) {
192
- return executionModeIncompatibilityReason(modelRefOrParsed, executionMode) === null;
157
+ return parseCanonicalReference(value);
193
158
  }