@mono-agent/agent-runtime 0.4.0 → 0.5.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 +37 -16
- package/MIGRATION.md +185 -7
- package/README.md +24 -14
- package/package.json +103 -10
- package/src/agent/allowlists.js +3 -0
- package/src/agent/approval.js +3 -0
- package/src/agent/compaction.js +244 -1
- package/src/agent/prompt/skill-index.js +6 -0
- package/src/agent/sandbox-seam.js +227 -0
- package/src/agent/tool-bloat.js +8 -2
- package/src/agent/tools/bash.js +16 -8
- package/src/agent/tools/edit.js +7 -3
- package/src/agent/tools/glob.js +11 -5
- package/src/agent/tools/grep.js +11 -5
- package/src/agent/tools/pi-bridge.js +227 -45
- package/src/agent/tools/read.js +28 -3
- package/src/agent/tools/shared/output-truncation.js +8 -3
- package/src/agent/tools/shared/path-resolver.js +24 -18
- package/src/agent/tools/shared/ripgrep.js +33 -6
- package/src/agent/tools/shared/runtime-context.js +60 -50
- package/src/agent/tools/shared/tool-context.js +157 -0
- package/src/agent/tools/web-fetch.js +21 -10
- package/src/agent/tools/web-search.js +11 -4
- package/src/agent/tools/write.js +7 -3
- package/src/agent/transcript.js +16 -1
- package/src/ai/cost.js +128 -3
- package/src/ai/failure.js +96 -4
- package/src/ai/live-input-prompt.js +11 -1
- package/src/ai/providers/claude-cli.js +2 -2
- package/src/ai/providers/claude-sdk.js +55 -12
- package/src/ai/providers/codex-app.js +36 -23
- package/src/ai/providers/opencode-app.js +2 -2
- package/src/ai/providers/opencode-discovery.js +2 -2
- package/src/ai/providers/pi-events.js +5 -0
- package/src/ai/providers/pi-models.js +21 -4
- package/src/ai/providers/pi-native/compaction-driver.js +394 -0
- package/src/ai/providers/pi-native/result-builder.js +312 -0
- package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
- package/src/ai/providers/pi-native/structured-output.js +130 -0
- package/src/ai/providers/pi-native/turn-runner.js +278 -0
- package/src/ai/providers/pi-native.js +415 -1106
- package/src/ai/runtime/model-refs.js +30 -0
- package/src/ai/runtime/registry.js +29 -0
- package/src/ai/runtime/router.js +96 -12
- package/src/ai/runtime/session-liveness.js +110 -0
- package/src/ai/runtime/sessions.js +16 -0
- package/src/ai/types.js +252 -19
- package/src/index.js +1 -1
- package/src/pi-auth.js +147 -16
- package/src/runtime-brand.js +21 -1
- package/src/runtime.js +72 -8
- package/types/agent/allowlists.d.ts +25 -0
- package/types/agent/approval.d.ts +30 -0
- package/types/agent/compaction.d.ts +97 -0
- package/types/agent/index.d.ts +5 -0
- package/types/agent/prompt/skill-index.d.ts +19 -0
- package/types/agent/sandbox-seam.d.ts +148 -0
- package/types/agent/tool-bloat.d.ts +22 -0
- package/types/agent/tools/bash.d.ts +16 -0
- package/types/agent/tools/edit.d.ts +14 -0
- package/types/agent/tools/glob.d.ts +16 -0
- package/types/agent/tools/grep.d.ts +22 -0
- package/types/agent/tools/index.d.ts +10 -0
- package/types/agent/tools/pi-bridge.d.ts +144 -0
- package/types/agent/tools/read.d.ts +19 -0
- package/types/agent/tools/shared/constants.d.ts +13 -0
- package/types/agent/tools/shared/dedup.d.ts +8 -0
- package/types/agent/tools/shared/output-truncation.d.ts +22 -0
- package/types/agent/tools/shared/path-resolver.d.ts +6 -0
- package/types/agent/tools/shared/ripgrep.d.ts +38 -0
- package/types/agent/tools/shared/runtime-context.d.ts +27 -0
- package/types/agent/tools/shared/tool-context.d.ts +48 -0
- package/types/agent/tools/web-fetch.d.ts +13 -0
- package/types/agent/tools/web-search.d.ts +11 -0
- package/types/agent/tools/write.d.ts +12 -0
- package/types/agent/transcript.d.ts +45 -0
- package/types/ai/backend.d.ts +41 -0
- package/types/ai/cost.d.ts +96 -0
- package/types/ai/failure.d.ts +117 -0
- package/types/ai/file-change-stats.d.ts +75 -0
- package/types/ai/index.d.ts +7 -0
- package/types/ai/live-input-prompt.d.ts +6 -0
- package/types/ai/observer.d.ts +68 -0
- package/types/ai/providers/claude-cli.d.ts +211 -0
- package/types/ai/providers/claude-sdk.d.ts +66 -0
- package/types/ai/providers/claude-subagents.d.ts +2 -0
- package/types/ai/providers/codex-app.d.ts +151 -0
- package/types/ai/providers/opencode-app.d.ts +95 -0
- package/types/ai/providers/opencode-discovery.d.ts +4 -0
- package/types/ai/providers/pi-errors.d.ts +3 -0
- package/types/ai/providers/pi-events.d.ts +24 -0
- package/types/ai/providers/pi-messages.d.ts +5 -0
- package/types/ai/providers/pi-models.d.ts +57 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
- package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
- package/types/ai/providers/pi-native.d.ts +18 -0
- package/types/ai/registry.d.ts +1 -0
- package/types/ai/runtime/capabilities-used.d.ts +21 -0
- package/types/ai/runtime/capabilities.d.ts +33 -0
- package/types/ai/runtime/context-windows.d.ts +8 -0
- package/types/ai/runtime/fast-mode.d.ts +2 -0
- package/types/ai/runtime/model-refs.d.ts +35 -0
- package/types/ai/runtime/registry.d.ts +38 -0
- package/types/ai/runtime/router.d.ts +56 -0
- package/types/ai/runtime/session-liveness.d.ts +55 -0
- package/types/ai/runtime/sessions.d.ts +38 -0
- package/types/ai/streaming/codex-events.d.ts +30 -0
- package/types/ai/streaming/opencode-events.d.ts +41 -0
- package/types/ai/types.d.ts +702 -0
- package/types/index.d.ts +7 -0
- package/types/pi-auth.d.ts +28 -0
- package/types/runtime-brand.d.ts +30 -0
- package/types/runtime.d.ts +10 -0
- package/src/ai/providers/pi-sdk.js +0 -35
package/src/ai/cost.js
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-check
|
|
2
2
|
|
|
3
|
+
// pi 0.80 moved the static catalog reads off the pi-ai root (`getModel` is now
|
|
4
|
+
// deprecated/compat-only). `getBuiltinModel(provider, id)` from `providers/all`
|
|
5
|
+
// is the non-deprecated replacement with the same signature and the same
|
|
6
|
+
// undefined-on-miss behavior the pricing lookup below relies on.
|
|
7
|
+
import { getBuiltinModel as getPiModel } from "@earendil-works/pi-ai/providers/all";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} ParsedModelReference
|
|
11
|
+
* @property {string|null} sdk
|
|
12
|
+
* @property {string} [provider]
|
|
13
|
+
* @property {string} model
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} NormalizedPricing
|
|
18
|
+
* @property {number|null} input
|
|
19
|
+
* @property {number|null} cacheRead
|
|
20
|
+
* @property {number|null} cacheWrite
|
|
21
|
+
* @property {number|null} output
|
|
22
|
+
* @property {string} source
|
|
23
|
+
* @property {boolean} priced
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} PricingInputRow
|
|
28
|
+
* Duck-typed pricing row a host (or the pi catalog) supplies: either
|
|
29
|
+
* camelCase or the provider's `*_per_million` snake_case spelling.
|
|
30
|
+
* @property {number|string} [input]
|
|
31
|
+
* @property {number|string} [input_per_million]
|
|
32
|
+
* @property {number|string} [cacheRead]
|
|
33
|
+
* @property {number|string} [cachedInput]
|
|
34
|
+
* @property {number|string} [cached_input_per_million]
|
|
35
|
+
* @property {number|string} [cacheWrite]
|
|
36
|
+
* @property {number|string} [cache_write_per_million]
|
|
37
|
+
* @property {number|string} [cache_creation_per_million]
|
|
38
|
+
* @property {number|string} [output]
|
|
39
|
+
* @property {number|string} [output_per_million]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// STATIC FALLBACK, consulted only AFTER pi's live catalog (piCatalogPricing via
|
|
43
|
+
// getBuiltinModel("anthropic", ...)). pi's anthropic catalog already carries the
|
|
44
|
+
// same per-million rates for the currently-shipping models, so this table only
|
|
45
|
+
// wins for Claude ids pi's catalog does not (yet) know — newer/renamed models
|
|
46
|
+
// added here before they land in a pinned pi-ai release. STALENESS: these are
|
|
47
|
+
// hand-maintained USD/1M-token rates and can drift from Anthropic's published
|
|
48
|
+
// pricing; treat them as a best-effort backstop for cost DIAGNOSTICS only (never
|
|
49
|
+
// control flow), and refresh when bumping pi-ai or when Anthropic reprices.
|
|
3
50
|
const CLAUDE_PRICING = {
|
|
4
51
|
"claude-haiku-4-5-20251001": { input: 1.0, cacheRead: 0.1, cacheWrite: 1.25, output: 5.0 },
|
|
5
52
|
"claude-haiku-4-5": { input: 1.0, cacheRead: 0.1, cacheWrite: 1.25, output: 5.0 },
|
|
@@ -11,17 +58,34 @@ const CLAUDE_PRICING = {
|
|
|
11
58
|
"claude-opus-4-5": { input: 5.0, cacheRead: 0.5, cacheWrite: 6.25, output: 25.0 },
|
|
12
59
|
};
|
|
13
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {*} value
|
|
63
|
+
* @returns {number|null}
|
|
64
|
+
*/
|
|
14
65
|
function finiteOrNull(value) {
|
|
15
66
|
if (value == null || value === "") return null;
|
|
16
67
|
const n = Number(value);
|
|
17
68
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
18
69
|
}
|
|
19
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @param {*} value
|
|
73
|
+
* @param {number} [fallback]
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
20
76
|
function rate(value, fallback = 0) {
|
|
21
77
|
const n = finiteOrNull(value);
|
|
22
78
|
return n == null ? fallback : n;
|
|
23
79
|
}
|
|
24
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @param {PricingInputRow|null|undefined} pricing
|
|
83
|
+
* @param {Object} [options]
|
|
84
|
+
* @param {string} [options.source]
|
|
85
|
+
* @param {boolean} [options.priced]
|
|
86
|
+
* @param {number} [options.missing]
|
|
87
|
+
* @returns {NormalizedPricing|null}
|
|
88
|
+
*/
|
|
25
89
|
function normalizePricing(pricing, { source, priced = true, missing = 0 } = {}) {
|
|
26
90
|
if (!pricing || typeof pricing !== "object") return null;
|
|
27
91
|
const input = rate(pricing.input ?? pricing.input_per_million, missing);
|
|
@@ -41,14 +105,25 @@ function normalizePricing(pricing, { source, priced = true, missing = 0 } = {})
|
|
|
41
105
|
return { input, cacheRead, cacheWrite, output, source, priced };
|
|
42
106
|
}
|
|
43
107
|
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} source
|
|
110
|
+
* @returns {NormalizedPricing}
|
|
111
|
+
*/
|
|
44
112
|
function zeroPricing(source) {
|
|
45
113
|
return { input: 0, cacheRead: 0, cacheWrite: 0, output: 0, source, priced: true };
|
|
46
114
|
}
|
|
47
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @returns {NormalizedPricing}
|
|
118
|
+
*/
|
|
48
119
|
function unknownPricing() {
|
|
49
120
|
return { input: null, cacheRead: null, cacheWrite: null, output: null, source: "unknown", priced: false };
|
|
50
121
|
}
|
|
51
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @param {string} reference
|
|
125
|
+
* @returns {ParsedModelReference|null}
|
|
126
|
+
*/
|
|
52
127
|
function parseReference(reference) {
|
|
53
128
|
if (typeof reference !== "string" || !reference.trim()) return null;
|
|
54
129
|
if (reference.startsWith("vercel:")) {
|
|
@@ -72,6 +147,10 @@ function parseReference(reference) {
|
|
|
72
147
|
return { sdk: reference.slice(0, i), model: reference.slice(i + 1) };
|
|
73
148
|
}
|
|
74
149
|
|
|
150
|
+
/**
|
|
151
|
+
* @param {string} baseUrl
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
75
154
|
function isPrivateHost(baseUrl) {
|
|
76
155
|
try {
|
|
77
156
|
const host = new URL(baseUrl).hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -88,6 +167,10 @@ function isPrivateHost(baseUrl) {
|
|
|
88
167
|
}
|
|
89
168
|
}
|
|
90
169
|
|
|
170
|
+
/**
|
|
171
|
+
* @param {PricingInputRow} [pricing]
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
91
174
|
function pricingHasRates(pricing = {}) {
|
|
92
175
|
return [
|
|
93
176
|
pricing.input_per_million,
|
|
@@ -97,16 +180,41 @@ function pricingHasRates(pricing = {}) {
|
|
|
97
180
|
].some((value) => finiteOrNull(value) != null);
|
|
98
181
|
}
|
|
99
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Live pricing from pi-ai's builtin catalog (getBuiltinModel). Handles two
|
|
185
|
+
* shapes:
|
|
186
|
+
* - sdk "pi": `parsed.provider` is the pi provider id (openai, openai-codex,
|
|
187
|
+
* github-copilot, custom, ...). Codex/openai references route here via
|
|
188
|
+
* parseReference (codex:* -> openai-codex, openai:* -> openai), so they get
|
|
189
|
+
* the SAME catalog treatment — priced when pi's catalog has that model
|
|
190
|
+
* (openai gpt-* do), unpriced (-> falls through to unknown) when it does not
|
|
191
|
+
* (e.g. openai-codex `gpt-5-codex` is not in the pinned catalog).
|
|
192
|
+
* - sdk "claude": looked up under pi's "anthropic" provider (its models carry
|
|
193
|
+
* `cost`), so pi's live rates win over the static CLAUDE_PRICING fallback.
|
|
194
|
+
* @param {ParsedModelReference|null|undefined} parsed
|
|
195
|
+
* @returns {NormalizedPricing|null}
|
|
196
|
+
*/
|
|
100
197
|
function piCatalogPricing(parsed) {
|
|
101
|
-
if (parsed?.
|
|
198
|
+
if (!parsed?.model) return null;
|
|
199
|
+
let provider;
|
|
200
|
+
if (parsed.sdk === "pi" && parsed.provider) provider = parsed.provider;
|
|
201
|
+
else if (parsed.sdk === "claude") provider = "anthropic";
|
|
202
|
+
else return null;
|
|
102
203
|
try {
|
|
103
|
-
|
|
204
|
+
// `provider` may be a caller-supplied id (custom providers included), wider
|
|
205
|
+
// than pi-ai's built-in KnownProvider catalog union; the catalog lookup
|
|
206
|
+
// itself is the runtime check, guarded by the catch below.
|
|
207
|
+
const model = getPiModel(/** @type {*} */ (provider), parsed.model);
|
|
104
208
|
return model?.cost ? normalizePricing(model.cost, { source: "pi-catalog" }) : null;
|
|
105
209
|
} catch {
|
|
106
210
|
return null;
|
|
107
211
|
}
|
|
108
212
|
}
|
|
109
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @param {ParsedModelReference|null|undefined} parsed
|
|
216
|
+
* @returns {NormalizedPricing|null}
|
|
217
|
+
*/
|
|
110
218
|
function claudePricing(parsed) {
|
|
111
219
|
if (parsed?.sdk !== "claude") return null;
|
|
112
220
|
return normalizePricing(CLAUDE_PRICING[parsed.model], { source: "claude-table" });
|
|
@@ -118,6 +226,12 @@ function claudePricing(parsed) {
|
|
|
118
226
|
// The pricing helpers below (`normalizePricing`, `zeroPricing`, `unknownPricing`,
|
|
119
227
|
// `pricingHasRates`, `isPrivateHost`, `parseReference`) are exported so hosts
|
|
120
228
|
// can build their own resolvers without re-implementing the row-shape conversion.
|
|
229
|
+
/**
|
|
230
|
+
* @param {Object} [options]
|
|
231
|
+
* @param {(parsed: ParsedModelReference) => (NormalizedPricing|null)} [options.resolveCustomPricing]
|
|
232
|
+
* @param {string} [options.model]
|
|
233
|
+
* @returns {NormalizedPricing}
|
|
234
|
+
*/
|
|
121
235
|
export function resolvePricing({ resolveCustomPricing, model } = {}) {
|
|
122
236
|
const parsed = parseReference(model);
|
|
123
237
|
if (!parsed) return unknownPricing();
|
|
@@ -132,6 +246,17 @@ export function resolvePricing({ resolveCustomPricing, model } = {}) {
|
|
|
132
246
|
|
|
133
247
|
export { normalizePricing, zeroPricing, unknownPricing, pricingHasRates, isPrivateHost, parseReference };
|
|
134
248
|
|
|
249
|
+
/**
|
|
250
|
+
* @param {Object} [options]
|
|
251
|
+
* @param {(parsed: ParsedModelReference) => (NormalizedPricing|null)} [options.resolveCustomPricing]
|
|
252
|
+
* @param {string} [options.model]
|
|
253
|
+
* @param {number} [options.inputTokens]
|
|
254
|
+
* @param {number} [options.outputTokens]
|
|
255
|
+
* @param {number} [options.cachedTokens]
|
|
256
|
+
* @param {number} [options.cacheWriteTokens]
|
|
257
|
+
* @param {number} [options.cacheCreationTokens]
|
|
258
|
+
* @returns {number|null}
|
|
259
|
+
*/
|
|
135
260
|
export function estimateCost({
|
|
136
261
|
resolveCustomPricing,
|
|
137
262
|
model,
|
package/src/ai/failure.js
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} RetryableProviderFailureInfo
|
|
5
|
+
* @property {boolean} retryable
|
|
6
|
+
* @property {string|null} subkind
|
|
7
|
+
* @property {string|null} requestId
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {"spawn" | "timeout" | "stall" | "usage_limit" | "invalid_result"
|
|
12
|
+
* | "invalid_delegation" | "tool_failure" | "provider_unavailable"
|
|
13
|
+
* | "provider_unavailable_exhausted" | "provider_auth"
|
|
14
|
+
* | "skipped_capability_mismatch" | "cancelled" | "cancelled_user"
|
|
15
|
+
* | "cancelled_shutdown" | "cancelled_signal" | "abandoned"
|
|
16
|
+
* | "session_not_found" | "session_busy" | (string & {})} FailureKind
|
|
17
|
+
* OPEN string union. The literals above are the CORE taxonomy the kernel itself
|
|
18
|
+
* derives from provider/runtime signals in `classifyFailure` /
|
|
19
|
+
* `retryableProviderFailureInfo` (and `provider_unavailable_exhausted`, produced
|
|
20
|
+
* by the router on chain exhaustion). The union is intentionally open
|
|
21
|
+
* (`string & {}`) because `FAILURE_KINDS` also transports HOST-TAXONOMY kinds
|
|
22
|
+
* the kernel never originates on its own:
|
|
23
|
+
* - `child_failed`, `budget_exceeded` — `classifyFailure` only returns these
|
|
24
|
+
* when the HOST passes the matching `childFailed` / `budgetExceeded` (or a
|
|
25
|
+
* `cancelInitiator: "budget"`) input; the kernel never infers them from a
|
|
26
|
+
* provider signal.
|
|
27
|
+
* - `delegation_agent_not_in_team`, `delegation_team_roster_empty`,
|
|
28
|
+
* `invalid_delegation`, `cancelled_stale` (`stale_reconcile`) — set by a
|
|
29
|
+
* host coordinator (planner/roster/reconcile logic) and merely carried
|
|
30
|
+
* through `FAILURE_KINDS` / the `hint` passthrough; the kernel transports
|
|
31
|
+
* them in the result contract but never emits them from its own paths.
|
|
32
|
+
* Hosts (e.g. worklab's coordinator) validate against `FAILURE_KINDS` and may
|
|
33
|
+
* define additional kinds — accepting them at the type level is deliberate.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// FAILURE_KINDS is the runtime vocabulary: `classifyFailure`'s `hint`
|
|
37
|
+
// passthrough accepts any member, and hosts validate `task_runs.failure_kind`
|
|
38
|
+
// against it. See the `FailureKind` typedef above for which members the kernel
|
|
39
|
+
// derives itself vs. which are host-taxonomy kinds it only transports.
|
|
1
40
|
export const FAILURE_KINDS = [
|
|
2
41
|
"spawn",
|
|
3
42
|
"timeout",
|
|
@@ -8,6 +47,8 @@ export const FAILURE_KINDS = [
|
|
|
8
47
|
"tool_failure",
|
|
9
48
|
"provider_unavailable",
|
|
10
49
|
"provider_unavailable_exhausted",
|
|
50
|
+
"provider_auth",
|
|
51
|
+
"skipped_capability_mismatch",
|
|
11
52
|
"child_failed",
|
|
12
53
|
"budget_exceeded",
|
|
13
54
|
"cancelled",
|
|
@@ -29,12 +70,32 @@ export const FAILURE_KINDS = [
|
|
|
29
70
|
];
|
|
30
71
|
|
|
31
72
|
const USAGE_LIMIT_RE = /(rate limit|usage limit|max tokens|max turns|context length|too many tokens)/i;
|
|
32
|
-
const
|
|
73
|
+
const PROVIDER_AUTH_RE = /(no api key|missing api key|api key required|invalid api key|incorrect api key|authentication|authorization|not authorized|forbidden|oauth (?:refresh|auth|authentication|token).*failed|credential store (?:read|modify) failed|401|403)/i;
|
|
74
|
+
// Mirrors the conservative connection-error/refused/failed alternation added to
|
|
75
|
+
// RETRYABLE_PROVIDER_RE / retryableProviderSubkind below for pi 0.80's terse
|
|
76
|
+
// "Connection error." — without it, classifyFailure (used directly by hosts
|
|
77
|
+
// like worklab's coordinator, independent of retryableProviderFailureInfo) maps
|
|
78
|
+
// that same terse text to the generic "spawn" kind instead of
|
|
79
|
+
// "provider_unavailable".
|
|
80
|
+
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)/i;
|
|
33
81
|
const TOOL_FAILURE_RE = /(tool .* failed|mcp tool|permission denied|EACCES|read-only file system)/i;
|
|
34
|
-
const NON_RETRYABLE_PROVIDER_RE = /(invalid[_ ]request|unknown parameter|invalid api key|incorrect api key|authentication|authorization|not authorized|forbidden|billing|insufficient[_ ]quota|quota exceeded|model[_ ]not[_ ]found|unsupported model|permission denied|bad request|401|403|404)/i;
|
|
35
|
-
|
|
82
|
+
const NON_RETRYABLE_PROVIDER_RE = /(invalid[_ ]request|unknown parameter|no api key|missing api key|api key required|invalid api key|incorrect api key|authentication|authorization|not authorized|forbidden|billing|insufficient[_ ]quota|quota exceeded|model[_ ]not[_ ]found|unsupported model|permission denied|bad request|401|403|404)/i;
|
|
83
|
+
// pi 0.80's openai-client-style bridge collapses a connection-refused/unreachable
|
|
84
|
+
// provider down to a terse "Connection error." with no cause text (no ECONNREFUSED,
|
|
85
|
+
// no fetch failed) — the `\bconnection (?:error|refused|failed)\b|\bcould not connect\b`
|
|
86
|
+
// alternation below is the motivating fix so that case still fails over instead of
|
|
87
|
+
// being classified as non-retryable.
|
|
88
|
+
const RETRYABLE_PROVIDER_RE = /(currently overloaded|server(?:s)? (?:is |are )?overloaded|try again later|retry your request|request id|service unavailable|temporar(?:y|ily)|timed? ?out|stream disconnected|fetch failed|econnreset|econnrefused|eai_again|enotfound|etimedout|network|429|too many requests|500|502|503|504|gateway|internal server error|\bconnection (?:error|refused|failed)\b|\bcould not connect\b)/i;
|
|
36
89
|
export const PROVIDER_ABORT_RE = /\b(?:terminated|aborted before final output|aborted before final|stream aborted|stream was aborted|stream disconnected|websocket (?:error|disconnected|closed)|socket hang up|und_err_socket|econnreset|premature close)\b/i;
|
|
37
90
|
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} text
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
export function isProviderAuthFailureText(text = "") {
|
|
96
|
+
return PROVIDER_AUTH_RE.test(text || "");
|
|
97
|
+
}
|
|
98
|
+
|
|
38
99
|
function requestIdFromText(text) {
|
|
39
100
|
const match = /\b(?:request[_ -]?id|req[_ -]?id)\s*[:#]?\s*([A-Za-z0-9._:-]{8,})/i.exec(text || "");
|
|
40
101
|
return match?.[1]?.replace(/[.,;:]+$/, "") || null;
|
|
@@ -44,12 +105,21 @@ function retryableProviderSubkind(text) {
|
|
|
44
105
|
if (/overloaded/i.test(text)) return "overloaded";
|
|
45
106
|
if (/429|too many requests|rate limit/i.test(text)) return "rate_limited";
|
|
46
107
|
if (/timed? ?out|etimedout/i.test(text)) return "timeout";
|
|
47
|
-
|
|
108
|
+
// pi 0.80's terse "Connection error." (no ECONNREFUSED/fetch-failed detail) still
|
|
109
|
+
// needs to land in the "network" subkind so a down provider fails over.
|
|
110
|
+
if (/stream disconnected|fetch failed|econnreset|econnrefused|eai_again|enotfound|network|\bconnection (?:error|refused|failed)\b|\bcould not connect\b/i.test(text)) return "network";
|
|
48
111
|
if (/500|502|503|504|service unavailable|gateway|internal server error/i.test(text)) return "server_error";
|
|
49
112
|
if (/retry your request|try again later|request id|processing your request/i.test(text)) return "retryable_request";
|
|
50
113
|
return null;
|
|
51
114
|
}
|
|
52
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @param {Object} [options]
|
|
118
|
+
* @param {string} [options.errorText]
|
|
119
|
+
* @param {string} [options.stderrTail]
|
|
120
|
+
* @param {string|null} [options.failureKind]
|
|
121
|
+
* @returns {RetryableProviderFailureInfo}
|
|
122
|
+
*/
|
|
53
123
|
export function retryableProviderFailureInfo({
|
|
54
124
|
errorText = "",
|
|
55
125
|
stderrTail = "",
|
|
@@ -80,6 +150,22 @@ export function retryableProviderFailureInfo({
|
|
|
80
150
|
// errors) into one of FAILURE_KINDS. Every adapter / spawn-worker / watcher
|
|
81
151
|
// path should funnel through this so the values in `task_runs.failure_kind`
|
|
82
152
|
// stay coherent.
|
|
153
|
+
/**
|
|
154
|
+
* @param {Object} [options]
|
|
155
|
+
* @param {number|null} [options.exitCode]
|
|
156
|
+
* @param {string|null} [options.signal]
|
|
157
|
+
* @param {string} [options.errorText]
|
|
158
|
+
* @param {string} [options.stderrTail]
|
|
159
|
+
* @param {boolean} [options.timedOut]
|
|
160
|
+
* @param {boolean} [options.cancelRequested]
|
|
161
|
+
* @param {string|null} [options.cancelInitiator]
|
|
162
|
+
* @param {boolean} [options.resultParseError]
|
|
163
|
+
* @param {boolean} [options.mcpInitFailed]
|
|
164
|
+
* @param {boolean} [options.budgetExceeded]
|
|
165
|
+
* @param {boolean} [options.childFailed]
|
|
166
|
+
* @param {string|null} [options.hint]
|
|
167
|
+
* @returns {FailureKind|null} One of FAILURE_KINDS, or null for a clean exit.
|
|
168
|
+
*/
|
|
83
169
|
export function classifyFailure({
|
|
84
170
|
exitCode = null,
|
|
85
171
|
signal = null,
|
|
@@ -122,6 +208,7 @@ export function classifyFailure({
|
|
|
122
208
|
const haystack = `${errorText || ""}\n${stderrTail || ""}`;
|
|
123
209
|
if (USAGE_LIMIT_RE.test(haystack)) return "usage_limit";
|
|
124
210
|
if (TOOL_FAILURE_RE.test(haystack)) return "tool_failure";
|
|
211
|
+
if (PROVIDER_AUTH_RE.test(haystack)) return "provider_auth";
|
|
125
212
|
if (PROVIDER_UNAVAILABLE_RE.test(haystack)) return "provider_unavailable";
|
|
126
213
|
if (mcpInitFailed && haystack.toLowerCase().includes("mcp")) return "tool_failure";
|
|
127
214
|
|
|
@@ -133,6 +220,11 @@ export function classifyFailure({
|
|
|
133
220
|
// of stderr; we only want the last few KB for diagnostics. Returns a string
|
|
134
221
|
// guaranteed to be ≤ `limit` bytes, with a `[truncated …]` marker if anything
|
|
135
222
|
// was dropped.
|
|
223
|
+
/**
|
|
224
|
+
* @param {Object} [options]
|
|
225
|
+
* @param {number} [options.limit]
|
|
226
|
+
* @returns {{push: (chunk: (string|Buffer|null|undefined)) => void, toString: () => string, readonly bytesDropped: number}}
|
|
227
|
+
*/
|
|
136
228
|
export function createStderrTail({ limit = 8 * 1024 } = {}) {
|
|
137
229
|
let buffer = "";
|
|
138
230
|
let dropped = 0;
|
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
// consumers; the queue/normalize/supports helpers stay in core/live-input.js
|
|
4
4
|
// for the API + coordinator + worker callers that don't need this string.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// A host or run `prompts.liveInputGuidance(body)` override, when supplied,
|
|
7
|
+
// replaces the default guidance wrapper below; otherwise the default is used.
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} text
|
|
10
|
+
* @param {import('./types.js').RuntimePromptOverrides} [prompts]
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function formatLiveInputGuidance(text, prompts) {
|
|
14
|
+
if (typeof prompts?.liveInputGuidance === "function") {
|
|
15
|
+
return prompts.liveInputGuidance(String(text || ""));
|
|
16
|
+
}
|
|
7
17
|
return [
|
|
8
18
|
"Live guidance from the user:",
|
|
9
19
|
String(text || ""),
|
|
@@ -443,7 +443,7 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
443
443
|
const start = Date.now();
|
|
444
444
|
const resolved = options.model;
|
|
445
445
|
const prompt = promptFromMessages(options.messages);
|
|
446
|
-
const dir = mkdtempSync(join(tmpdir(), readRuntimeBrand().tempdirPrefix));
|
|
446
|
+
const dir = mkdtempSync(join(tmpdir(), (options.toolContext?.runtimeBrand ?? readRuntimeBrand()).tempdirPrefix));
|
|
447
447
|
const schemaPath = options.outputSchema ? join(dir, "output-schema.json") : null;
|
|
448
448
|
if (schemaPath) writeFileSync(schemaPath, JSON.stringify(options.outputSchema));
|
|
449
449
|
const mcpServers = options.mcpServers || {};
|
|
@@ -453,7 +453,7 @@ export async function generateCliResponse(systemPrompt, options = {}) {
|
|
|
453
453
|
// Owner-only mode: this temp config carries full mcpServers env (including a
|
|
454
454
|
// resolved embedding apiKey) and lives under shared /tmp. The 0700 parent dir
|
|
455
455
|
// (mkdtempSync) already blocks cross-user traversal; 0o600 is defense-in-depth
|
|
456
|
-
// matching the idiom in packages/
|
|
456
|
+
// matching the idiom in packages/agent-contracts/src/json-source.ts and pi-auth.js.
|
|
457
457
|
if (mcpConfigPath) writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
|
|
458
458
|
const reusableSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
|
|
459
459
|
|| (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
|
|
@@ -15,6 +15,7 @@ import { runtimeCapabilities } from "../runtime/capabilities.js";
|
|
|
15
15
|
import { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings } from "../runtime/capabilities-used.js";
|
|
16
16
|
import { MAX_TOOL_RESULT_BYTES, summarisePayload } from "../../agent/tool-bloat.js";
|
|
17
17
|
import { normalizeMcpToolParams } from "../../agent/tools/pi-bridge.js";
|
|
18
|
+
import { deprecatedSettingsWarning } from "../../agent/compaction.js";
|
|
18
19
|
import { readRuntimeBrand } from "../../agent/tools/shared/runtime-context.js";
|
|
19
20
|
import { createApprovalManager } from "../../agent/approval.js";
|
|
20
21
|
import {
|
|
@@ -266,12 +267,28 @@ function claudeToolResponseBlocks(toolResponse) {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
// Resolve the tool_result byte cap. Precedence is PER-GROUP (MIGRATION.md §8):
|
|
271
|
+
// explicit options.toolPayloadMaxBytes
|
|
272
|
+
// -> typed options.toolLimits — when this group is PRESENT it wins wholesale:
|
|
273
|
+
// its toolPayloadMaxBytes is used if valid, otherwise the default; the
|
|
274
|
+
// DEPRECATED settings fallback below is never consulted for this group,
|
|
275
|
+
// even if toolLimits.toolPayloadMaxBytes itself is absent/invalid.
|
|
276
|
+
// -> DEPRECATED options.settings.agent_tool_payload_max_bytes (usedSettings),
|
|
277
|
+
// only consulted when options.toolLimits is entirely absent.
|
|
278
|
+
// -> MAX_TOOL_RESULT_BYTES default.
|
|
279
|
+
// `usedSettings` lets the caller emit one deprecation warning per run when the
|
|
280
|
+
// legacy settings fallback was actually consumed.
|
|
281
|
+
export function toolPayloadLimit(options) {
|
|
270
282
|
const explicit = Number(options.toolPayloadMaxBytes);
|
|
271
|
-
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
|
|
283
|
+
if (Number.isFinite(explicit) && explicit > 0) return { bytes: Math.floor(explicit), usedSettings: false };
|
|
284
|
+
if (options.toolLimits) {
|
|
285
|
+
const typed = Number(options.toolLimits.toolPayloadMaxBytes);
|
|
286
|
+
if (Number.isFinite(typed) && typed > 0) return { bytes: Math.floor(typed), usedSettings: false };
|
|
287
|
+
return { bytes: MAX_TOOL_RESULT_BYTES, usedSettings: false };
|
|
288
|
+
}
|
|
272
289
|
const configured = Number(options.settings?.agent_tool_payload_max_bytes);
|
|
273
|
-
if (Number.isFinite(configured) && configured > 0) return Math.floor(configured);
|
|
274
|
-
return MAX_TOOL_RESULT_BYTES;
|
|
290
|
+
if (Number.isFinite(configured) && configured > 0) return { bytes: Math.floor(configured), usedSettings: true };
|
|
291
|
+
return { bytes: MAX_TOOL_RESULT_BYTES, usedSettings: false };
|
|
275
292
|
}
|
|
276
293
|
|
|
277
294
|
function claudeEditPath(toolName, toolInput) {
|
|
@@ -289,6 +306,10 @@ function fileEditStateKey(input, toolUseID, path) {
|
|
|
289
306
|
return toolUseID || input?.tool_use_id || input?.toolUseID || `${input?.tool_name || "file_edit"}:${path}`;
|
|
290
307
|
}
|
|
291
308
|
|
|
309
|
+
/**
|
|
310
|
+
* @param {any} change
|
|
311
|
+
* @param {{status?: any, before?: any, after?: any, error?: any}} [options]
|
|
312
|
+
*/
|
|
292
313
|
function fileEditPayload(change, { status, before, after, error } = {}) {
|
|
293
314
|
const lineStats = statsForCompletedChange(change, before, after);
|
|
294
315
|
const completedChange = lineStats ? { ...change, line_stats: lineStats } : change;
|
|
@@ -331,6 +352,11 @@ function createClaudeFileEditHooks({ cwd, emitEvent }) {
|
|
|
331
352
|
state.started = true;
|
|
332
353
|
}
|
|
333
354
|
|
|
355
|
+
/**
|
|
356
|
+
* @param {any} input
|
|
357
|
+
* @param {any} toolUseID
|
|
358
|
+
* @param {{status?: any, error?: any}} [options]
|
|
359
|
+
*/
|
|
334
360
|
function complete(input, toolUseID, { status, error } = {}) {
|
|
335
361
|
const directKey = toolUseID || input?.tool_use_id || input?.toolUseID;
|
|
336
362
|
const fallback = createState(input, toolUseID, { readBefore: false });
|
|
@@ -483,10 +509,10 @@ function createClaudeCanUseTool(approvalManager, modelName) {
|
|
|
483
509
|
};
|
|
484
510
|
}
|
|
485
511
|
|
|
486
|
-
async function* livePromptMessages({ initialPrompt, liveInput, sessionId }) {
|
|
512
|
+
async function* livePromptMessages({ initialPrompt, liveInput, sessionId, prompts }) {
|
|
487
513
|
yield makeSdkUserMessage(initialPrompt, sessionId);
|
|
488
514
|
for await (const message of liveInput) {
|
|
489
|
-
yield makeSdkUserMessage(formatLiveInputGuidance(message.body), sessionId, message.id || randomUUID());
|
|
515
|
+
yield makeSdkUserMessage(formatLiveInputGuidance(message.body, prompts), sessionId, message.id || randomUUID());
|
|
490
516
|
}
|
|
491
517
|
}
|
|
492
518
|
|
|
@@ -515,7 +541,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
515
541
|
const reusableProviderSessionId = pickSessionId(options.sessionId, options.providerSessionId);
|
|
516
542
|
const persistArtifact = options.persistArtifact || null;
|
|
517
543
|
const qaOutputDir = options.qaOutputDir || options.runArtifactDir || null;
|
|
518
|
-
const toolPayloadMaxBytes = toolPayloadLimit(options);
|
|
544
|
+
const { bytes: toolPayloadMaxBytes, usedSettings: toolPayloadFromSettings } = toolPayloadLimit(options);
|
|
519
545
|
let providerSessionId = reusableProviderSessionId;
|
|
520
546
|
let lastToolName = null;
|
|
521
547
|
let toolResultsSeen = 0;
|
|
@@ -526,6 +552,16 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
526
552
|
onEvent(event);
|
|
527
553
|
}
|
|
528
554
|
|
|
555
|
+
// Deprecated `settings` fallback for the tool_result byte cap was consumed;
|
|
556
|
+
// surface the one-per-run deprecation warning (the typed `toolLimits` object
|
|
557
|
+
// is the supported path). mono-agent never passes `settings`, so this never
|
|
558
|
+
// fires there.
|
|
559
|
+
if (toolPayloadFromSettings) {
|
|
560
|
+
const warning = deprecatedSettingsWarning(["agent_tool_payload_max_bytes"]);
|
|
561
|
+
runtimeWarnings.push(warning);
|
|
562
|
+
emitEvent({ type: "runtime_warning", ...warning });
|
|
563
|
+
}
|
|
564
|
+
|
|
529
565
|
function noteToolUse(toolName) {
|
|
530
566
|
if (toolName) lastToolName = toolName;
|
|
531
567
|
}
|
|
@@ -552,6 +588,9 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
552
588
|
? "default"
|
|
553
589
|
: permissionMode;
|
|
554
590
|
const nativeAgents = claudeNativeAgentDefinitions(options.nativeSubagents);
|
|
591
|
+
// Assembled incrementally, then handed across the SDK `query` boundary
|
|
592
|
+
// (outputFormat/resume/maxTurns are attached conditionally below).
|
|
593
|
+
/** @type {any} */
|
|
555
594
|
const queryOptions = {
|
|
556
595
|
systemPrompt,
|
|
557
596
|
model: modelWithContextWindow(model.model, options.contextWindow),
|
|
@@ -588,7 +627,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
588
627
|
}
|
|
589
628
|
|
|
590
629
|
const prompt = options.liveInput
|
|
591
|
-
? livePromptMessages({ initialPrompt: promptString, liveInput: options.liveInput, sessionId: reusableProviderSessionId || randomUUID() })
|
|
630
|
+
? livePromptMessages({ initialPrompt: promptString, liveInput: options.liveInput, sessionId: reusableProviderSessionId || randomUUID(), prompts: options.prompts })
|
|
592
631
|
: promptString;
|
|
593
632
|
const providerRequestStartedAt = Date.now();
|
|
594
633
|
emitEvent({
|
|
@@ -598,7 +637,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
598
637
|
runtime: "sdk",
|
|
599
638
|
timestamp: providerRequestStartedAt,
|
|
600
639
|
});
|
|
601
|
-
const stream = query({ prompt, options: queryOptions });
|
|
640
|
+
const stream = query({ prompt: /** @type {any} */ (prompt), options: queryOptions });
|
|
602
641
|
|
|
603
642
|
let text = "";
|
|
604
643
|
let usage = {};
|
|
@@ -672,8 +711,12 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
672
711
|
text += delta;
|
|
673
712
|
for (const toolName of assistantToolNames(event)) noteToolUse(toolName);
|
|
674
713
|
}
|
|
675
|
-
|
|
676
|
-
|
|
714
|
+
// The SDK's message union does not declare a runtime `error` event, but
|
|
715
|
+
// the runtime can emit one; keep this defensive branch and cast past the
|
|
716
|
+
// narrowed union.
|
|
717
|
+
else if (/** @type {any} */ (event).type === "error") {
|
|
718
|
+
const errorEvent = /** @type {any} */ (event);
|
|
719
|
+
const message = errorEvent.error?.message || errorEvent.error || "sdk stream error";
|
|
677
720
|
if (hasPreservableFinalOutput()) {
|
|
678
721
|
preservePostSuccessError(`Claude SDK emitted an error after final output; preserved final result. ${message}`);
|
|
679
722
|
} else {
|
|
@@ -711,7 +754,7 @@ export async function generateClaudeResponse(systemPrompt, options) {
|
|
|
711
754
|
if (failureKind === "invalid_result") {
|
|
712
755
|
runtimeWarnings.push(makeRuntimeWarning(
|
|
713
756
|
resultError.message,
|
|
714
|
-
`${readRuntimeBrand().schemaPrefix}_result_validation`,
|
|
757
|
+
`${(options.toolContext?.runtimeBrand ?? readRuntimeBrand()).schemaPrefix}_result_validation`,
|
|
715
758
|
));
|
|
716
759
|
}
|
|
717
760
|
errorDetails = buildClaudeErrorDetails({
|