@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
package/src/ai/cost.js CHANGED
@@ -9,8 +9,7 @@ import { getBuiltinModel as getPiModel } from "@earendil-works/pi-ai/providers/a
9
9
 
10
10
  /**
11
11
  * @typedef {Object} ParsedModelReference
12
- * @property {string|null} sdk
13
- * @property {string} [provider]
12
+ * @property {string} provider
14
13
  * @property {string} model
15
14
  */
16
15
 
@@ -40,25 +39,6 @@ import { getBuiltinModel as getPiModel } from "@earendil-works/pi-ai/providers/a
40
39
  * @property {number|string} [output_per_million]
41
40
  */
42
41
 
43
- // STATIC FALLBACK, consulted only AFTER pi's live catalog (piCatalogPricing via
44
- // getBuiltinModel("anthropic", ...)). pi's anthropic catalog already carries the
45
- // same per-million rates for the currently-shipping models, so this table only
46
- // wins for Claude ids pi's catalog does not (yet) know — newer/renamed models
47
- // added here before they land in a pinned pi-ai release. STALENESS: these are
48
- // hand-maintained USD/1M-token rates and can drift from Anthropic's published
49
- // pricing; treat them as a best-effort backstop for cost DIAGNOSTICS only (never
50
- // control flow), and refresh when bumping pi-ai or when Anthropic reprices.
51
- const CLAUDE_PRICING = {
52
- "claude-haiku-4-5-20251001": { input: 1.0, cacheRead: 0.1, cacheWrite: 1.25, output: 5.0 },
53
- "claude-haiku-4-5": { input: 1.0, cacheRead: 0.1, cacheWrite: 1.25, output: 5.0 },
54
- "claude-sonnet-4-6": { input: 3.0, cacheRead: 0.3, cacheWrite: 3.75, output: 15.0 },
55
- "claude-sonnet-4-5": { input: 3.0, cacheRead: 0.3, cacheWrite: 3.75, output: 15.0 },
56
- "claude-sonnet-4": { input: 3.0, cacheRead: 0.3, cacheWrite: 3.75, output: 15.0 },
57
- "claude-opus-4-7": { input: 5.0, cacheRead: 0.5, cacheWrite: 6.25, output: 25.0 },
58
- "claude-opus-4-6": { input: 5.0, cacheRead: 0.5, cacheWrite: 6.25, output: 25.0 },
59
- "claude-opus-4-5": { input: 5.0, cacheRead: 0.5, cacheWrite: 6.25, output: 25.0 },
60
- };
61
-
62
42
  /**
63
43
  * @param {*} value
64
44
  * @returns {number|null}
@@ -126,26 +106,13 @@ function unknownPricing() {
126
106
  * @returns {ParsedModelReference|null}
127
107
  */
128
108
  function parseReference(reference) {
129
- if (typeof reference !== "string" || !reference.trim()) return null;
130
- if (reference.startsWith("vercel:")) {
131
- const rest = reference.slice("vercel:".length);
132
- const i = rest.indexOf(":");
133
- return i > 0 ? { sdk: "pi", provider: rest.slice(0, i), model: rest.slice(i + 1) } : null;
134
- }
135
- if (reference.startsWith("codex:")) {
136
- return { sdk: "pi", provider: "openai-codex", model: reference.slice("codex:".length) };
137
- }
138
- if (reference.startsWith("openai:")) {
139
- return { sdk: "pi", provider: "openai", model: reference.slice("openai:".length) };
140
- }
141
- if (reference.startsWith("pi:")) {
142
- const rest = reference.slice("pi:".length);
143
- const i = rest.indexOf(":");
144
- return i > 0 ? { sdk: "pi", provider: rest.slice(0, i), model: rest.slice(i + 1) } : null;
145
- }
146
- const i = reference.indexOf(":");
147
- if (i <= 0) return { sdk: null, model: reference };
148
- return { sdk: reference.slice(0, i), model: reference.slice(i + 1) };
109
+ if (typeof reference !== "string" || reference.length === 0 || reference.trim() !== reference) return null;
110
+ const separator = reference.indexOf(":");
111
+ if (separator <= 0 || separator === reference.length - 1) return null;
112
+ const provider = reference.slice(0, separator);
113
+ const model = reference.slice(separator + 1);
114
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(provider) || model.trim() !== model) return null;
115
+ return { provider, model };
149
116
  }
150
117
 
151
118
  /**
@@ -182,30 +149,19 @@ function pricingHasRates(pricing = {}) {
182
149
  }
183
150
 
184
151
  /**
185
- * Live pricing from pi-ai's builtin catalog (getBuiltinModel). Handles two
186
- * shapes:
187
- * - sdk "pi": `parsed.provider` is the pi provider id (openai, openai-codex,
188
- * github-copilot, custom, ...). Codex/openai references route here via
189
- * parseReference (codex:* -> openai-codex, openai:* -> openai), so they get
190
- * the SAME catalog treatment — priced when pi's catalog has that model
191
- * (openai gpt-* do), unpriced (-> falls through to unknown) when it does not
192
- * (e.g. openai-codex `gpt-5-codex` is not in the pinned catalog).
193
- * - sdk "claude": looked up under pi's "anthropic" provider (its models carry
194
- * `cost`), so pi's live rates win over the static CLAUDE_PRICING fallback.
152
+ * Live pricing from pi-ai's builtin catalog (getBuiltinModel). The parsed
153
+ * provider is already the Pi catalog provider id; the model remains opaque and
154
+ * may contain additional colons or slashes.
195
155
  * @param {ParsedModelReference|null|undefined} parsed
196
156
  * @returns {import("@earendil-works/pi-ai").Model<any>|null}
197
157
  */
198
158
  function piCatalogModel(parsed) {
199
- if (!parsed?.model) return null;
200
- let provider;
201
- if (parsed.sdk === "pi" && parsed.provider) provider = parsed.provider;
202
- else if (parsed.sdk === "claude") provider = "anthropic";
203
- else return null;
159
+ if (!parsed?.provider || !parsed.model) return null;
204
160
  try {
205
161
  // `provider` may be a caller-supplied id (custom providers included), wider
206
162
  // than pi-ai's built-in KnownProvider catalog union; the catalog lookup
207
163
  // itself is the runtime check, guarded by the catch below.
208
- return getPiModel(/** @type {*} */ (provider), parsed.model) || null;
164
+ return getPiModel(/** @type {*} */ (parsed.provider), parsed.model) || null;
209
165
  } catch {
210
166
  return null;
211
167
  }
@@ -220,15 +176,6 @@ function piCatalogPricing(parsed) {
220
176
  return model?.cost ? normalizePricing(model.cost, { source: "pi-catalog" }) : null;
221
177
  }
222
178
 
223
- /**
224
- * @param {ParsedModelReference|null|undefined} parsed
225
- * @returns {NormalizedPricing|null}
226
- */
227
- function claudePricing(parsed) {
228
- if (parsed?.sdk !== "claude") return null;
229
- return normalizePricing(CLAUDE_PRICING[parsed.model], { source: "claude-table" });
230
- }
231
-
232
179
  // `resolveCustomPricing(parsed) -> NormalizedPricing | null` lets a host plug
233
180
  // in user-defined pricing tables. Hosts query custom model/provider stores
234
181
  // in src/core/custom-pricing.js and passes the closure in via `generateResponse`.
@@ -249,7 +196,6 @@ export function resolvePricing({ resolveCustomPricing, model } = {}) {
249
196
  : null;
250
197
  return custom
251
198
  || piCatalogPricing(parsed)
252
- || claudePricing(parsed)
253
199
  || unknownPricing();
254
200
  }
255
201
 
@@ -301,7 +247,6 @@ export function estimateCost({
301
247
  return estimatePiCatalogCost(piModel, { input, output, cacheRead, cacheWrite });
302
248
  }
303
249
  const pricing = customPricing
304
- || claudePricing(parsed)
305
250
  || unknownPricing();
306
251
  if (!pricing?.priced) return null;
307
252
  const parts = [
package/src/ai/failure.js CHANGED
@@ -72,19 +72,19 @@ export const FAILURE_KINDS = [
72
72
  ];
73
73
 
74
74
  const CONTEXT_LIMIT_RE = /(?:context[_ ](?:length|window|budget)|token[_ ]limit|(?:input|prompt)(?:[_ ]tokens?)?[_ ](?:is[_ ])?too[_ ]long|(?:input|prompt|request)(?:[_ ]tokens?)?[_ ]exceeds?[_ ](?:the[_ ])?(?:context|maximum|max|limit|allowed[_ ]size)|too[_ ]many[_ ](?:input[_ ])?tokens?|tokens?[_ ]exceed(?:s|ed)?[_ ](?:the[_ ])?(?:context|maximum|max|(?:model[_ ])?limit))/i;
75
- const USAGE_LIMIT_RE = /(rate limit|usage limit|max(?:imum)?(?:[_ ]output)?[_ ]tokens?|max turns)/i;
75
+ const USAGE_LIMIT_RE = /(rate limit|usage limit|insufficient[_ ]quota|quota exceeded|billing limit|too many requests|429|max(?:imum)?(?:[_ ]output)?[_ ]tokens?|max turns)/i;
76
76
  // Pi's Models layer emits the exact `Provider is not configured: <id>` message
77
77
  // only after it has found the provider but cannot resolve that provider's auth.
78
78
  // Treating it as availability left credential fallbacks pinned to the dead
79
79
  // route because the router only advances on provider_auth or retryable outages.
80
- const PROVIDER_AUTH_RE = /(no api key|missing api key|api key required|invalid api key|incorrect api key|provider is not configured:|authentication|authorization|not authorized|forbidden|oauth (?:refresh|auth|authentication|token).*failed|credential store (?:read|modify) failed|401|403)/i;
80
+ const PROVIDER_AUTH_RE = /(no api key|missing api key|api key required|invalid api key|incorrect api key|provider is not configured:|authentication failed|authorization failed|unauthorized|oauth (?:refresh|auth|authentication|token).*failed|credential store (?:read|modify) failed|invalid[_ ]grant|token[_ ]revoked|revoked (?:oauth )?token|invalidated (?:oauth )?token|encountered invalidated oauth token|\b401\b)/i;
81
81
  // Mirrors the conservative connection-error/refused/failed alternation added to
82
82
  // RETRYABLE_PROVIDER_RE / retryableProviderSubkind below for pi 0.80's terse
83
83
  // "Connection error." — without it, classifyFailure (used directly by hosts
84
84
  // like worklab's coordinator, independent of retryableProviderFailureInfo) maps
85
85
  // that same terse text to the generic "spawn" kind instead of
86
86
  // "provider_unavailable".
87
- const PROVIDER_UNAVAILABLE_RE = /(econn|enotfound|etimedout|timed? ?out|service unavailable|503|502|gateway|fetch failed|network|websocket|\bconnection (?:error|refused|failed)\b|\bcould not connect\b|\bstream ended without finish_reason\b)/i;
87
+ const PROVIDER_UNAVAILABLE_RE = /(econn|enotfound|etimedout|timed? ?out|service unavailable|503|502|gateway|fetch failed|network|websocket|forbidden|\b403\b|model[_ ]not[_ ]found|unsupported model|no access to (?:the )?model|\b404\b|\bconnection (?:error|refused|failed)\b|\bcould not connect\b|\bstream ended without finish_reason\b)/i;
88
88
  const TOOL_FAILURE_RE = /(tool .* failed|mcp tool|permission denied|EACCES|read-only file system)/i;
89
89
  const NON_RETRYABLE_PROVIDER_RE = /(invalid[_ ]request|unknown parameter|no api key|missing api key|api key required|invalid api key|incorrect api key|provider is not configured:|authentication|authorization|not authorized|forbidden|billing|insufficient[_ ]quota|quota exceeded|model[_ ]not[_ ]found|unsupported model|permission denied|bad request|401|403|404)/i;
90
90
  // pi 0.80's openai-client-style bridge collapses a connection-refused/unreachable
package/src/ai/index.js CHANGED
@@ -12,23 +12,11 @@ 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 { generateAcpResponse, acpRuntimeBridge } from "./providers/acp.js";
16
- export * from "./providers/acp-public.js";
17
- export {
18
- getPiBuiltinModel,
19
- listPiBuiltinModels,
20
- loginPiOAuth,
21
- reasoningLevelsForPiModel,
22
- resolvePiOAuthApiKey,
23
- } from "./pi-interop.js";
24
- export {
25
- CLAUDE_SDK_CATALOG_VERSION,
26
- createClaudeSdkDiscoveryIsolation,
27
- curatedClaudeSdkModels,
28
- discoverClaudeSdkModels,
29
- normalizeClaudeSdkCatalog,
30
- normalizeClaudeSdkModelId,
31
- } from "./providers/claude-sdk-discovery.js";
15
+ // Re-exported with `export *` so the JSDoc-declared snapshot types
16
+ // (PiBuiltinModelSnapshot, PiBuiltinProviderSnapshot, …) travel with the
17
+ // functions; runtime-adapter re-exports these for host-side catalog builders.
18
+ export * from "./pi-interop.js";
19
+ export * from "./provider-check.js";
32
20
  export {
33
21
  buildCapabilitiesUsed,
34
22
  toolCompactionAppliedFromWarnings,
@@ -21,6 +21,7 @@
21
21
  * @typedef Observer
22
22
  * @property {string=} name
23
23
  * @property {(event: object) => void} recordEvent
24
+ * @property {(event: object) => void=} recordToolLifecycle Synchronous native lifecycle admission before queued persistence.
24
25
  * @property {(metric: object) => void=} recordMetric
25
26
  * @property {() => (void | Promise<void>)=} flush
26
27
  */
@@ -42,6 +43,12 @@ export function createObserverHub({ observers = [], onEvent = null } = {}) {
42
43
  }
43
44
  }
44
45
 
46
+ function recordToolLifecycle(event) {
47
+ for (const obs of list) {
48
+ try { obs.recordToolLifecycle?.(event); } catch { /* observers remain best-effort */ }
49
+ }
50
+ }
51
+
45
52
  function recordMetric(metric) {
46
53
  if (!metric) return;
47
54
  for (const obs of list) {
@@ -62,6 +69,7 @@ export function createObserverHub({ observers = [], onEvent = null } = {}) {
62
69
  return {
63
70
  emit,
64
71
  recordMetric,
72
+ recordToolLifecycle,
65
73
  flush,
66
74
  observers: () => list.slice(),
67
75
  };
@@ -2,7 +2,13 @@
2
2
  // surfaces. Consumers should use these functions instead of importing pi-ai
3
3
  // directly so the runtime's known-good Pi version remains authoritative.
4
4
 
5
- import { getBuiltinModel, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
5
+ import {
6
+ builtinModels,
7
+ builtinProviders,
8
+ getBuiltinModel,
9
+ getBuiltinModels,
10
+ getBuiltinProviders,
11
+ } from "@earendil-works/pi-ai/providers/all";
6
12
  import { getPiOAuthAuth, resolveOAuthApiKey, toAuthInteraction } from "./pi-oauth-compat.js";
7
13
  import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers/pi-models.js";
8
14
 
@@ -41,6 +47,13 @@ import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers
41
47
  * @typedef {"none"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"} PiReasoningLevel
42
48
  */
43
49
 
50
+ /**
51
+ * @typedef {{
52
+ * id: string,
53
+ * label: string
54
+ * }} PiBuiltinProviderSnapshot
55
+ */
56
+
44
57
  /**
45
58
  * @typedef {{
46
59
  * refresh: string,
@@ -61,6 +74,37 @@ import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers
61
74
  * @property {AbortSignal} [signal]
62
75
  */
63
76
 
77
+ /**
78
+ * @typedef {{
79
+ * providerId: string,
80
+ * label: string,
81
+ * methods: Array<{type: "oauth"|"api_key", label: string, interactive: boolean}>
82
+ * }} PiProviderAuthDescription
83
+ */
84
+
85
+ /**
86
+ * @typedef {{source: "stored"|"environment"|"ambient", type: "oauth"|"api_key"}} PiProviderAuthCheck
87
+ */
88
+
89
+ /**
90
+ * @typedef {{
91
+ * type: "text"|"secret"|"select"|"manual_code",
92
+ * message: string,
93
+ * placeholder?: string,
94
+ * allowEmpty?: boolean,
95
+ * options?: ReadonlyArray<{id: string, label: string, description?: string}>,
96
+ * signal?: AbortSignal
97
+ * }} PiProviderAuthPrompt
98
+ */
99
+
100
+ /**
101
+ * @typedef {{
102
+ * signal?: AbortSignal,
103
+ * prompt: (prompt: PiProviderAuthPrompt) => Promise<string>,
104
+ * notify: (event: *) => void
105
+ * }} PiProviderAuthInteraction
106
+ */
107
+
64
108
  /**
65
109
  * Clone provider-owned data before it crosses the public runtime boundary.
66
110
  * Pi's built-in models and OAuth credentials are structured data on the
@@ -103,6 +147,182 @@ export function getPiBuiltinModel(providerId, modelId) {
103
147
  : /** @type {PiBuiltinModelSnapshot} */ (cloneInteropValue(model));
104
148
  }
105
149
 
150
+ let builtinProviderLabels;
151
+ function builtinProviderLabelMap() {
152
+ // `getBuiltinProviders()` is the authoritative static catalog set (39 ids),
153
+ // but it returns bare ids — the human display label lives on the constructed
154
+ // `Provider.name`, which only `builtinProviders()` exposes. Build the name
155
+ // lookup once from the constructed providers and gate what we ADVERTISE on
156
+ // the static id set below, so the dynamic "radius" gateway (present in
157
+ // `builtinProviders()` but absent from `getBuiltinProviders()`) never enters
158
+ // the advertised catalog. A throwing construction degrades to id-as-label.
159
+ builtinProviderLabels ??= (() => {
160
+ try {
161
+ return new Map(builtinProviders().map((provider) => [provider.id, provider.name]));
162
+ } catch {
163
+ return new Map();
164
+ }
165
+ })();
166
+ return builtinProviderLabels;
167
+ }
168
+
169
+ /**
170
+ * List defensive snapshots of Pi's static built-in providers (id + display
171
+ * label). The dynamic "radius" gateway is deliberately excluded: it has no
172
+ * static catalog and must not be advertised as a browsable provider.
173
+ *
174
+ * @returns {PiBuiltinProviderSnapshot[]}
175
+ */
176
+ export function listPiBuiltinProviders() {
177
+ const labels = builtinProviderLabelMap();
178
+ return getBuiltinProviders().map((id) => ({
179
+ id,
180
+ label: labels.get(id) ?? id,
181
+ }));
182
+ }
183
+
184
+ /**
185
+ * Describe one static Pi built-in provider by id, or `undefined` for unknown
186
+ * ids (including the dynamic "radius" gateway).
187
+ *
188
+ * @param {string} providerId
189
+ * @returns {PiBuiltinProviderSnapshot|undefined}
190
+ */
191
+ export function describePiBuiltinProvider(providerId) {
192
+ const id = String(providerId);
193
+ if (!getBuiltinProviders().includes(/** @type {any} */ (id))) {
194
+ return undefined;
195
+ }
196
+ return {
197
+ id,
198
+ label: builtinProviderLabelMap().get(id) ?? id,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Describe one provider's supported authentication methods without exposing
204
+ * Pi provider objects across the runtime boundary.
205
+ *
206
+ * @param {string} providerId
207
+ * @returns {PiProviderAuthDescription|undefined}
208
+ */
209
+ export function describePiProviderAuth(providerId) {
210
+ let provider;
211
+ try {
212
+ provider = builtinProviders().find((candidate) => candidate.id === providerId);
213
+ } catch {
214
+ return undefined;
215
+ }
216
+ if (provider === undefined) return undefined;
217
+ const methods = [];
218
+ if (provider.auth.oauth !== undefined) {
219
+ methods.push({
220
+ type: /** @type {const} */ ("oauth"),
221
+ label: provider.auth.oauth.loginLabel ?? provider.auth.oauth.name,
222
+ interactive: true,
223
+ });
224
+ }
225
+ if (provider.auth.apiKey !== undefined) {
226
+ methods.push({
227
+ type: /** @type {const} */ ("api_key"),
228
+ label: provider.auth.apiKey.name,
229
+ interactive: typeof provider.auth.apiKey.login === "function",
230
+ });
231
+ }
232
+ return cloneInteropValue({ providerId: provider.id, label: provider.name, methods });
233
+ }
234
+
235
+ /**
236
+ * Run Pi's side-effect-free `Models.checkAuth()` against a caller-provided
237
+ * credential/environment snapshot. OAuth refresh and live provider requests do
238
+ * not occur. Only the non-secret source/type result crosses this facade.
239
+ *
240
+ * @param {string} providerId
241
+ * @param {*} credential
242
+ * @param {Readonly<Record<string, string|undefined>>} [environment]
243
+ * @param {AbortSignal} [signal]
244
+ * @returns {Promise<PiProviderAuthCheck|undefined>}
245
+ */
246
+ export async function checkPiProviderAuth(providerId, credential, environment = {}, signal) {
247
+ const credentials = memoryCredentialStore(credential === undefined ? {} : { [providerId]: credential });
248
+ const models = builtinModels({
249
+ credentials,
250
+ authContext: {
251
+ async env(name) { return environment[name]; },
252
+ async fileExists(path) {
253
+ try {
254
+ const fs = await import("node:fs/promises");
255
+ let resolved = path;
256
+ if (resolved.startsWith("~")) {
257
+ const os = await import("node:os");
258
+ resolved = os.homedir() + resolved.slice(1);
259
+ }
260
+ await fs.access(resolved);
261
+ return true;
262
+ } catch {
263
+ return false;
264
+ }
265
+ },
266
+ },
267
+ });
268
+ const result = await models.checkAuth(providerId, signal === undefined ? undefined : { signal });
269
+ if (result === undefined) return undefined;
270
+ const source = result.type === "oauth" || result.source === "stored credential"
271
+ ? "stored"
272
+ : typeof result.source === "string"
273
+ && typeof environment[result.source] === "string"
274
+ && environment[result.source].trim().length > 0
275
+ ? "environment"
276
+ : "ambient";
277
+ return cloneInteropValue({ source, type: result.type });
278
+ }
279
+
280
+ /**
281
+ * Run a provider-owned Pi login into a process-local store. The returned
282
+ * credential is a defensive snapshot; the caller remains responsible for its
283
+ * hardened durable transaction.
284
+ *
285
+ * @param {string} providerId
286
+ * @param {"oauth"|"api_key"} type
287
+ * @param {PiProviderAuthInteraction} interaction
288
+ * @returns {Promise<*>}
289
+ */
290
+ export async function loginPiProviderAuth(providerId, type, interaction) {
291
+ if (type !== "oauth" && type !== "api_key") {
292
+ throw new TypeError("Pi provider auth type must be oauth or api_key");
293
+ }
294
+ if (typeof interaction?.prompt !== "function" || typeof interaction?.notify !== "function") {
295
+ throw new TypeError("Pi provider auth interaction requires prompt() and notify()");
296
+ }
297
+ const models = builtinModels({ credentials: memoryCredentialStore({}) });
298
+ const credential = await models.login(providerId, type, {
299
+ signal: interaction.signal,
300
+ prompt: async (prompt) => await interaction.prompt(prompt),
301
+ notify: (event) => interaction.notify(cloneInteropValue(event)),
302
+ });
303
+ return cloneInteropValue(credential);
304
+ }
305
+
306
+ /** @param {Record<string, *>} initial */
307
+ function memoryCredentialStore(initial) {
308
+ const held = new Map(Object.entries(initial));
309
+ return {
310
+ async read(providerId) { return held.get(providerId); },
311
+ async list() {
312
+ return [...held.entries()].flatMap(([providerId, credential]) =>
313
+ credential?.type === "oauth" || credential?.type === "api_key"
314
+ ? [{ providerId, type: credential.type }]
315
+ : []);
316
+ },
317
+ async modify(providerId, fn) {
318
+ const next = await fn(held.get(providerId));
319
+ if (next !== undefined) held.set(providerId, next);
320
+ return held.get(providerId);
321
+ },
322
+ async delete(providerId) { held.delete(providerId); },
323
+ };
324
+ }
325
+
106
326
  /**
107
327
  * Translate Pi's model-native thinking levels to mono-agent effort spelling.
108
328
  *
@@ -10,7 +10,7 @@
10
10
  // they cannot be imported. The supported surface is `provider.auth.oauth`,
11
11
  // reached through the provider factories.
12
12
  //
13
- // mono-agent resolves providers dynamically from `pi:<provider>:<model>`, so it
13
+ // mono-agent resolves providers dynamically from `<provider>:<model>`, so it
14
14
  // needs a lookup by id — this module rebuilds that over `builtinProviders()` and
15
15
  // preserves the old call contracts exactly, keeping the migration confined here.
16
16
  //
@@ -0,0 +1,131 @@
1
+ // @ts-check
2
+
3
+ import { access } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+
6
+ import { generatePiNativeResponse } from "./providers/pi-native.js";
7
+
8
+ const SYSTEM_PROMPT = "Provider connectivity check. Reply OK.";
9
+ const USER_PROMPT = "OK";
10
+
11
+ /** @typedef {"passed"|"auth_failed"|"network_failed"|"quota_limited"|"model_not_entitled"|"inconclusive"} ProviderCheckOutcome */
12
+ /** @typedef {"passed"|"credential_rejected"|"provider_unavailable"|"quota_limited"|"model_not_entitled"|"forbidden"|"inconclusive"|"cancelled"} ProviderCheckCode */
13
+
14
+ /**
15
+ * Execute one target-only Pi request. This intentionally bypasses the router:
16
+ * a different provider or model must never prove the requested target healthy.
17
+ * Provider output and raw errors are consumed here and never returned.
18
+ *
19
+ * @param {{
20
+ * model: {provider: string, model: string, reference?: string},
21
+ * resolvePiApiKey?: Function,
22
+ * runtimeOptions?: Record<string, unknown>,
23
+ * environment?: Readonly<Record<string, string|undefined>>,
24
+ * abortSignal?: AbortSignal,
25
+ * execute?: typeof generatePiNativeResponse,
26
+ * }} input
27
+ * @returns {Promise<{state: ProviderCheckOutcome, code: ProviderCheckCode, message: string}>}
28
+ */
29
+ export async function runPiProviderCheck(input) {
30
+ const execute = input.execute ?? generatePiNativeResponse;
31
+ const runtimeOptions = providerConstructionOptions(input.runtimeOptions);
32
+ let result;
33
+ try {
34
+ result = await execute(SYSTEM_PROMPT, {
35
+ ...runtimeOptions,
36
+ model: {
37
+ provider: input.model.provider,
38
+ model: input.model.model,
39
+ reference: input.model.reference ?? `${input.model.provider}:${input.model.model}`,
40
+ },
41
+ messages: [{ role: "user", content: USER_PROMPT }],
42
+ effort: "none",
43
+ allowedTools: [],
44
+ disallowedTools: [],
45
+ mcpServers: {},
46
+ maxTurns: 1,
47
+ piMaxRetries: 0,
48
+ providerCheckMaxTokens: 4,
49
+ providerCheckAuthContext: {
50
+ async env(name) { return input.environment?.[name]; },
51
+ async fileExists(path) {
52
+ try {
53
+ await access(path.startsWith("~") ? homedir() + path.slice(1) : path);
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ },
59
+ },
60
+ ...(input.resolvePiApiKey === undefined ? {} : { resolvePiApiKey: input.resolvePiApiKey }),
61
+ ...(input.abortSignal === undefined ? {} : { abortSignal: input.abortSignal }),
62
+ });
63
+ } catch (error) {
64
+ return classifyProviderCheckFailure(error instanceof Error ? error.message : "", undefined);
65
+ }
66
+ if (result?.cancelled === true || input.abortSignal?.aborted === true) {
67
+ return { state: "inconclusive", code: "cancelled", message: "The provider check did not complete." };
68
+ }
69
+ if (!result?.error && result?.failureKind == null) {
70
+ return { state: "passed", code: "passed", message: "Provider request succeeded." };
71
+ }
72
+ return classifyProviderCheckFailure(
73
+ typeof result?.error === "string" ? result.error : "",
74
+ typeof result?.failureKind === "string" ? result.failureKind : undefined,
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Classify raw provider text inside the runtime boundary. The returned strings
80
+ * are closed, fixed projections and contain no provider-controlled content.
81
+ * @param {string} text
82
+ * @param {string|undefined} failureKind
83
+ * @returns {{state: ProviderCheckOutcome, code: ProviderCheckCode, message: string}}
84
+ */
85
+ export function classifyProviderCheckFailure(text, failureKind) {
86
+ const value = String(text || "");
87
+ if (failureKind === "provider_auth"
88
+ || /(invalid api key|incorrect api key|no api key|missing api key|authentication failed|authorization failed|unauthorized|invalid[_ ]grant|token[_ ]revoked|revoked (?:oauth )?token|invalidated (?:oauth )?token|\b401\b)/i.test(value)) {
89
+ return { state: "auth_failed", code: "credential_rejected", message: "Provider rejected the configured credential." };
90
+ }
91
+ if (failureKind === "usage_limit"
92
+ || /(rate limit|too many requests|insufficient[_ ]quota|quota exceeded|billing limit|\b429\b)/i.test(value)) {
93
+ return { state: "quota_limited", code: "quota_limited", message: "Provider quota or rate limit prevented the check." };
94
+ }
95
+ if (/(model[_ -]?not[_ -]?found|unsupported model|no access to (?:the )?model|model entitlement|model[^\n]{0,120}(?:does not exist|not found|unavailable))/i.test(value)) {
96
+ return { state: "model_not_entitled", code: "model_not_entitled", message: "The credential could not use the selected model." };
97
+ }
98
+ if (/forbidden|\b403\b/i.test(value)) {
99
+ return { state: "inconclusive", code: "forbidden", message: "The provider refused the check for an unspecified reason." };
100
+ }
101
+ if (failureKind === "provider_unavailable"
102
+ || /(econn|enotfound|etimedout|timed? ?out|service unavailable|gateway|fetch failed|network|websocket|\b5\d\d\b|\bconnection (?:error|refused|failed)\b)/i.test(value)) {
103
+ return { state: "network_failed", code: "provider_unavailable", message: "The provider could not be reached." };
104
+ }
105
+ return { state: "inconclusive", code: "inconclusive", message: "The provider check failed without a safe diagnosis." };
106
+ }
107
+
108
+ /**
109
+ * Keep the public check facade isolated from ordinary run state. These are the
110
+ * only provider/model construction seams required by configured local
111
+ * providers and deterministic faux-provider tests.
112
+ * @param {Record<string, unknown>|undefined} options
113
+ * @returns {Record<string, unknown>}
114
+ */
115
+ function providerConstructionOptions(options) {
116
+ if (options === undefined) return {};
117
+ const allowed = [
118
+ "customProvider",
119
+ "customModel",
120
+ "modelCapabilities",
121
+ "isPrivateProvider",
122
+ "piResolvedModel",
123
+ "piResolvedModels",
124
+ "piResolvedCapabilities",
125
+ ];
126
+ return Object.fromEntries(allowed
127
+ .filter((key) => Object.hasOwn(options, key))
128
+ .map((key) => [key, options[key]]));
129
+ }
130
+
131
+ export const PROVIDER_CHECK_PROMPT = Object.freeze({ system: SYSTEM_PROMPT, user: USER_PROMPT, maxOutputTokens: 4 });