@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.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +288 -26
- package/README.md +352 -477
- package/package.json +13 -44
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +108 -9
- package/src/agent/tools/bash.js +11 -26
- package/src/agent/tools/codex-subscription-search.js +123 -29
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +149 -0
- package/src/agent/tools/pi-bridge.js +123 -19
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +293 -0
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +26 -6
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/cost.js +13 -68
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +5 -17
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +221 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +94 -42
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +376 -0
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +38 -14
- package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
- package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +279 -28
- package/src/ai/providers/pi-native.js +206 -61
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +76 -417
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +37 -112
- package/src/index.js +0 -6
- package/src/runtime.js +29 -16
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -2
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +7 -3
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +47 -0
- package/types/agent/tools/pi-bridge.d.ts +7 -4
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +98 -0
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +14 -4
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +2 -4
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +81 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +162 -256
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- 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
|
|
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" ||
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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).
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
package/src/ai/observer.js
CHANGED
|
@@ -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
|
};
|
package/src/ai/pi-interop.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 });
|