@mono-agent/agent-runtime 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/ARCHITECTURE.md +75 -9
  2. package/MIGRATION.md +293 -0
  3. package/README.md +46 -20
  4. package/package.json +104 -10
  5. package/src/agent/allowlists.js +3 -0
  6. package/src/agent/approval.js +3 -0
  7. package/src/agent/compaction.js +231 -654
  8. package/src/agent/index.js +1 -1
  9. package/src/agent/prompt/skill-index.js +6 -0
  10. package/src/agent/sandbox-seam.js +227 -0
  11. package/src/agent/tool-bloat.js +8 -2
  12. package/src/agent/tools/bash.js +16 -8
  13. package/src/agent/tools/edit.js +7 -3
  14. package/src/agent/tools/glob.js +11 -5
  15. package/src/agent/tools/grep.js +11 -5
  16. package/src/agent/tools/pi-bridge.js +272 -54
  17. package/src/agent/tools/read.js +28 -3
  18. package/src/agent/tools/shared/output-truncation.js +8 -3
  19. package/src/agent/tools/shared/path-resolver.js +24 -18
  20. package/src/agent/tools/shared/ripgrep.js +33 -6
  21. package/src/agent/tools/shared/runtime-context.js +60 -50
  22. package/src/agent/tools/shared/tool-context.js +157 -0
  23. package/src/agent/tools/web-fetch.js +65 -18
  24. package/src/agent/tools/web-search.js +11 -4
  25. package/src/agent/tools/write.js +7 -3
  26. package/src/agent/transcript.js +16 -1
  27. package/src/ai/cost.js +128 -3
  28. package/src/ai/failure.js +96 -4
  29. package/src/ai/index.js +1 -0
  30. package/src/ai/live-input-prompt.js +11 -1
  31. package/src/ai/providers/claude-cli.js +6 -2
  32. package/src/ai/providers/claude-sdk.js +55 -12
  33. package/src/ai/providers/codex-app.js +36 -23
  34. package/src/ai/providers/opencode-app.js +2 -2
  35. package/src/ai/providers/opencode-discovery.js +2 -2
  36. package/src/ai/providers/pi-errors.js +65 -0
  37. package/src/ai/providers/pi-events.js +5 -0
  38. package/src/ai/providers/pi-models.js +21 -4
  39. package/src/ai/providers/pi-native/compaction-driver.js +394 -0
  40. package/src/ai/providers/pi-native/result-builder.js +312 -0
  41. package/src/ai/providers/pi-native/session-lifecycle.js +438 -0
  42. package/src/ai/providers/pi-native/stream-subscriber.js +148 -0
  43. package/src/ai/providers/pi-native/structured-output.js +130 -0
  44. package/src/ai/providers/pi-native/turn-runner.js +278 -0
  45. package/src/ai/providers/pi-native.js +754 -0
  46. package/src/ai/runtime/capabilities.js +3 -0
  47. package/src/ai/runtime/model-refs.js +30 -0
  48. package/src/ai/runtime/registry.js +33 -2
  49. package/src/ai/runtime/router.js +119 -19
  50. package/src/ai/runtime/session-liveness.js +110 -0
  51. package/src/ai/runtime/sessions.js +19 -2
  52. package/src/ai/types.js +252 -20
  53. package/src/index.js +1 -1
  54. package/src/pi-auth.js +147 -16
  55. package/src/runtime-brand.js +21 -1
  56. package/src/runtime.js +75 -10
  57. package/types/agent/allowlists.d.ts +25 -0
  58. package/types/agent/approval.d.ts +30 -0
  59. package/types/agent/compaction.d.ts +97 -0
  60. package/types/agent/index.d.ts +5 -0
  61. package/types/agent/prompt/skill-index.d.ts +19 -0
  62. package/types/agent/sandbox-seam.d.ts +148 -0
  63. package/types/agent/tool-bloat.d.ts +22 -0
  64. package/types/agent/tools/bash.d.ts +16 -0
  65. package/types/agent/tools/edit.d.ts +14 -0
  66. package/types/agent/tools/glob.d.ts +16 -0
  67. package/types/agent/tools/grep.d.ts +22 -0
  68. package/types/agent/tools/index.d.ts +10 -0
  69. package/types/agent/tools/pi-bridge.d.ts +144 -0
  70. package/types/agent/tools/read.d.ts +19 -0
  71. package/types/agent/tools/shared/constants.d.ts +13 -0
  72. package/types/agent/tools/shared/dedup.d.ts +8 -0
  73. package/types/agent/tools/shared/output-truncation.d.ts +22 -0
  74. package/types/agent/tools/shared/path-resolver.d.ts +6 -0
  75. package/types/agent/tools/shared/ripgrep.d.ts +38 -0
  76. package/types/agent/tools/shared/runtime-context.d.ts +27 -0
  77. package/types/agent/tools/shared/tool-context.d.ts +48 -0
  78. package/types/agent/tools/web-fetch.d.ts +13 -0
  79. package/types/agent/tools/web-search.d.ts +11 -0
  80. package/types/agent/tools/write.d.ts +12 -0
  81. package/types/agent/transcript.d.ts +45 -0
  82. package/types/ai/backend.d.ts +41 -0
  83. package/types/ai/cost.d.ts +96 -0
  84. package/types/ai/failure.d.ts +117 -0
  85. package/types/ai/file-change-stats.d.ts +75 -0
  86. package/types/ai/index.d.ts +7 -0
  87. package/types/ai/live-input-prompt.d.ts +6 -0
  88. package/types/ai/observer.d.ts +68 -0
  89. package/types/ai/providers/claude-cli.d.ts +211 -0
  90. package/types/ai/providers/claude-sdk.d.ts +66 -0
  91. package/types/ai/providers/claude-subagents.d.ts +2 -0
  92. package/types/ai/providers/codex-app.d.ts +151 -0
  93. package/types/ai/providers/opencode-app.d.ts +95 -0
  94. package/types/ai/providers/opencode-discovery.d.ts +4 -0
  95. package/types/ai/providers/pi-errors.d.ts +3 -0
  96. package/types/ai/providers/pi-events.d.ts +24 -0
  97. package/types/ai/providers/pi-messages.d.ts +5 -0
  98. package/types/ai/providers/pi-models.d.ts +57 -0
  99. package/types/ai/providers/pi-native/compaction-driver.d.ts +86 -0
  100. package/types/ai/providers/pi-native/result-builder.d.ts +168 -0
  101. package/types/ai/providers/pi-native/session-lifecycle.d.ts +62 -0
  102. package/types/ai/providers/pi-native/stream-subscriber.d.ts +50 -0
  103. package/types/ai/providers/pi-native/structured-output.d.ts +69 -0
  104. package/types/ai/providers/pi-native/turn-runner.d.ts +60 -0
  105. package/types/ai/providers/pi-native.d.ts +18 -0
  106. package/types/ai/registry.d.ts +1 -0
  107. package/types/ai/runtime/capabilities-used.d.ts +21 -0
  108. package/types/ai/runtime/capabilities.d.ts +33 -0
  109. package/types/ai/runtime/context-windows.d.ts +8 -0
  110. package/types/ai/runtime/fast-mode.d.ts +2 -0
  111. package/types/ai/runtime/model-refs.d.ts +35 -0
  112. package/types/ai/runtime/registry.d.ts +38 -0
  113. package/types/ai/runtime/router.d.ts +56 -0
  114. package/types/ai/runtime/session-liveness.d.ts +55 -0
  115. package/types/ai/runtime/sessions.d.ts +38 -0
  116. package/types/ai/streaming/codex-events.d.ts +30 -0
  117. package/types/ai/streaming/opencode-events.d.ts +41 -0
  118. package/types/ai/types.d.ts +702 -0
  119. package/types/index.d.ts +7 -0
  120. package/types/pi-auth.d.ts +28 -0
  121. package/types/runtime-brand.d.ts +30 -0
  122. package/types/runtime.d.ts +10 -0
  123. package/src/ai/providers/pi-sdk.js +0 -1310
@@ -84,6 +84,17 @@ function summarizeTurn(turn, { maxChars = DEFAULT_TURN_SUMMARY_CHARS } = {}) {
84
84
  // is nothing usable to resume from. Older turns beyond `verbatimTurns` are
85
85
  // summarized into a one-paragraph snippet; only the trailing `verbatimTurns`
86
86
  // keep their full assistant text + tool detail.
87
+ /**
88
+ * @param {Array<*>} events
89
+ * @param {Object} [options]
90
+ * @param {number} [options.maxTurns]
91
+ * @param {number} [options.verbatimTurns]
92
+ * @param {number} [options.maxChars]
93
+ * @param {number} [options.toolResultChars]
94
+ * @param {number} [options.assistantTextChars]
95
+ * @param {number} [options.turnSummaryChars]
96
+ * @param {import('../runtime-brand.js').RuntimeBrand} [options.runtimeBrand]
97
+ */
87
98
  export function buildTranscriptTailSnapshot(events, {
88
99
  maxTurns = DEFAULT_MAX_TURNS,
89
100
  verbatimTurns = DEFAULT_VERBATIM_TURNS,
@@ -91,6 +102,10 @@ export function buildTranscriptTailSnapshot(events, {
91
102
  toolResultChars = DEFAULT_TOOL_RESULT_CHARS,
92
103
  assistantTextChars = DEFAULT_ASSISTANT_TEXT_CHARS,
93
104
  turnSummaryChars = DEFAULT_TURN_SUMMARY_CHARS,
105
+ // The resolved RuntimeBrand for the schema id. The router (the only caller)
106
+ // threads the instance brand; absent (deep/default callers) it falls back to
107
+ // the module-default context brand.
108
+ runtimeBrand,
94
109
  } = {}) {
95
110
  if (!Array.isArray(events) || events.length === 0) return null;
96
111
  const turns = [];
@@ -154,7 +169,7 @@ export function buildTranscriptTailSnapshot(events, {
154
169
  turn_index: turns.length - tailWindow.length + idx + 1,
155
170
  summary: summarizeTurn(turn, { maxChars: turnSummaryChars }),
156
171
  }));
157
- const brand = readRuntimeBrand();
172
+ const brand = runtimeBrand ?? readRuntimeBrand();
158
173
  const snapshot = {
159
174
  schema: `${brand.schemaPrefix}.transcript-tail.v1`,
160
175
  captured_at: Date.now(),
package/src/ai/cost.js CHANGED
@@ -1,5 +1,52 @@
1
- import { getModel as getPiModel } from "@earendil-works/pi-ai";
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?.sdk !== "pi" || !parsed.provider || !parsed.model) return null;
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
- const model = getPiModel(parsed.provider, parsed.model);
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 PROVIDER_UNAVAILABLE_RE = /(econn|enotfound|etimedout|service unavailable|503|502|gateway|fetch failed|network|websocket)/i;
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
- 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)/i;
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
- if (/stream disconnected|fetch failed|econnreset|econnrefused|eai_again|enotfound|network/i.test(text)) return "network";
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;
package/src/ai/index.js CHANGED
@@ -9,6 +9,7 @@ export {
9
9
  disposeProviderSession,
10
10
  } from "./runtime/sessions.js";
11
11
  export { createMetricsObserver, createObserverHub } from "./observer.js";
12
+ export { generatePiNativeResponse, piNativeRuntimeBridge } from "./providers/pi-native.js";
12
13
  export {
13
14
  buildCapabilitiesUsed,
14
15
  toolCompactionAppliedFromWarnings,
@@ -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
- export function formatLiveInputGuidance(text) {
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,14 +443,18 @@ 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 || {};
450
450
  const mcpConfigPath = hasEntries(mcpServers) && resolved.sdk === "claude-code"
451
451
  ? join(dir, "mcp.json")
452
452
  : null;
453
- if (mcpConfigPath) writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2));
453
+ // Owner-only mode: this temp config carries full mcpServers env (including a
454
+ // resolved embedding apiKey) and lives under shared /tmp. The 0700 parent dir
455
+ // (mkdtempSync) already blocks cross-user traversal; 0o600 is defense-in-depth
456
+ // matching the idiom in packages/agent-contracts/src/json-source.ts and pi-auth.js.
457
+ if (mcpConfigPath) writeFileSync(mcpConfigPath, JSON.stringify({ mcpServers }, null, 2), { mode: 0o600 });
454
458
  const reusableSessionId = (typeof options.sessionId === "string" && options.sessionId.trim())
455
459
  || (typeof options.providerSessionId === "string" && options.providerSessionId.trim())
456
460
  || null;
@@ -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
- function toolPayloadLimit(options) {
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
- else if (event.type === "error") {
676
- const message = event.error?.message || event.error || "sdk stream error";
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({