@namzu/sdk 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/compaction/__tests__/verifier-empty-reply.test.d.ts +12 -0
  3. package/dist/compaction/__tests__/verifier-empty-reply.test.d.ts.map +1 -0
  4. package/dist/compaction/__tests__/verifier-empty-reply.test.js +73 -0
  5. package/dist/compaction/__tests__/verifier-empty-reply.test.js.map +1 -0
  6. package/dist/compaction/verifier.d.ts.map +1 -1
  7. package/dist/compaction/verifier.js +10 -1
  8. package/dist/compaction/verifier.js.map +1 -1
  9. package/dist/config/__tests__/compaction-budget-schema.test.d.ts +18 -0
  10. package/dist/config/__tests__/compaction-budget-schema.test.d.ts.map +1 -0
  11. package/dist/config/__tests__/compaction-budget-schema.test.js +96 -0
  12. package/dist/config/__tests__/compaction-budget-schema.test.js.map +1 -0
  13. package/dist/config/runtime.d.ts.map +1 -1
  14. package/dist/config/runtime.js +20 -11
  15. package/dist/config/runtime.js.map +1 -1
  16. package/dist/manager/run/persistence.d.ts +2 -1
  17. package/dist/manager/run/persistence.d.ts.map +1 -1
  18. package/dist/manager/run/persistence.js +3 -1
  19. package/dist/manager/run/persistence.js.map +1 -1
  20. package/dist/provider/__tests__/errors.test.d.ts +2 -0
  21. package/dist/provider/__tests__/errors.test.d.ts.map +1 -0
  22. package/dist/provider/__tests__/errors.test.js +61 -0
  23. package/dist/provider/__tests__/errors.test.js.map +1 -0
  24. package/dist/provider/errors.d.ts +108 -0
  25. package/dist/provider/errors.d.ts.map +1 -0
  26. package/dist/provider/errors.js +311 -0
  27. package/dist/provider/errors.js.map +1 -0
  28. package/dist/provider/index.d.ts +2 -0
  29. package/dist/provider/index.d.ts.map +1 -1
  30. package/dist/provider/index.js +1 -0
  31. package/dist/provider/index.js.map +1 -1
  32. package/dist/public-runtime.d.ts +11 -11
  33. package/dist/public-runtime.d.ts.map +1 -1
  34. package/dist/public-runtime.js +11 -11
  35. package/dist/public-runtime.js.map +1 -1
  36. package/dist/registry/tool/execute.d.ts.map +1 -1
  37. package/dist/registry/tool/execute.js +45 -12
  38. package/dist/registry/tool/execute.js.map +1 -1
  39. package/dist/registry/tool/execute.test.js +104 -7
  40. package/dist/registry/tool/execute.test.js.map +1 -1
  41. package/dist/registry/toolset/catalog.d.ts.map +1 -1
  42. package/dist/registry/toolset/catalog.js +17 -4
  43. package/dist/registry/toolset/catalog.js.map +1 -1
  44. package/dist/registry/toolset/catalog.test.js +101 -0
  45. package/dist/registry/toolset/catalog.test.js.map +1 -1
  46. package/dist/runtime/query/__tests__/capability-negotiation.test.js +79 -0
  47. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  48. package/dist/runtime/query/__tests__/deferred-tools.test.js +100 -0
  49. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
  50. package/dist/runtime/query/__tests__/long-document-flow.test.js +12 -12
  51. package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -1
  52. package/dist/runtime/query/__tests__/stream-recovery.test.js +55 -2
  53. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -1
  54. package/dist/runtime/query/executor.js +2 -2
  55. package/dist/runtime/query/executor.js.map +1 -1
  56. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  57. package/dist/runtime/query/iteration/index.js +10 -0
  58. package/dist/runtime/query/iteration/index.js.map +1 -1
  59. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.d.ts +32 -0
  60. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.d.ts.map +1 -0
  61. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.js +152 -0
  62. package/dist/runtime/query/iteration/phases/compaction-safe-cut.test.js.map +1 -0
  63. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  64. package/dist/runtime/query/iteration/phases/compaction.js +52 -12
  65. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  66. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/stream-turn.js +11 -5
  68. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  69. package/dist/runtime/query/result.d.ts.map +1 -1
  70. package/dist/runtime/query/result.js +11 -1
  71. package/dist/runtime/query/result.js.map +1 -1
  72. package/dist/tools/builtins/__tests__/edit.test.js +216 -10
  73. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  74. package/dist/tools/builtins/__tests__/payload-budget.test.js +30 -16
  75. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
  76. package/dist/tools/builtins/__tests__/write-file.test.js +37 -13
  77. package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -1
  78. package/dist/tools/builtins/atomic-write-file.d.ts +13 -0
  79. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -0
  80. package/dist/tools/builtins/atomic-write-file.js +46 -0
  81. package/dist/tools/builtins/atomic-write-file.js.map +1 -0
  82. package/dist/tools/builtins/bash.js +2 -2
  83. package/dist/tools/builtins/bash.js.map +1 -1
  84. package/dist/tools/builtins/edit.d.ts +2 -5
  85. package/dist/tools/builtins/edit.d.ts.map +1 -1
  86. package/dist/tools/builtins/edit.js +105 -125
  87. package/dist/tools/builtins/edit.js.map +1 -1
  88. package/dist/tools/builtins/file-mutation-lock.d.ts +7 -0
  89. package/dist/tools/builtins/file-mutation-lock.d.ts.map +1 -0
  90. package/dist/tools/builtins/file-mutation-lock.js +25 -0
  91. package/dist/tools/builtins/file-mutation-lock.js.map +1 -0
  92. package/dist/tools/builtins/index.js +2 -2
  93. package/dist/tools/builtins/index.js.map +1 -1
  94. package/dist/tools/builtins/write-file.d.ts +1 -2
  95. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  96. package/dist/tools/builtins/write-file.js +66 -40
  97. package/dist/tools/builtins/write-file.js.map +1 -1
  98. package/dist/tools/coordinator/__tests__/task-list.test.js +14 -0
  99. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  100. package/dist/tools/coordinator/agent.d.ts +6 -8
  101. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  102. package/dist/tools/coordinator/agent.js.map +1 -1
  103. package/dist/tools/coordinator/index.d.ts.map +1 -1
  104. package/dist/tools/coordinator/index.js +11 -32
  105. package/dist/tools/coordinator/index.js.map +1 -1
  106. package/dist/tools/defineTool.d.ts +3 -0
  107. package/dist/tools/defineTool.d.ts.map +1 -1
  108. package/dist/tools/defineTool.js +3 -0
  109. package/dist/tools/defineTool.js.map +1 -1
  110. package/dist/types/provider/chat.d.ts +9 -0
  111. package/dist/types/provider/chat.d.ts.map +1 -1
  112. package/dist/types/provider/error.d.ts +22 -0
  113. package/dist/types/provider/error.d.ts.map +1 -0
  114. package/dist/types/provider/error.js +2 -0
  115. package/dist/types/provider/error.js.map +1 -0
  116. package/dist/types/provider/index.d.ts +1 -0
  117. package/dist/types/provider/index.d.ts.map +1 -1
  118. package/dist/types/run/entity.d.ts +2 -0
  119. package/dist/types/run/entity.d.ts.map +1 -1
  120. package/dist/types/run/events.d.ts +2 -0
  121. package/dist/types/run/events.d.ts.map +1 -1
  122. package/dist/types/run/events.js.map +1 -1
  123. package/dist/types/sandbox/index.d.ts +4 -0
  124. package/dist/types/sandbox/index.d.ts.map +1 -1
  125. package/dist/types/sandbox/index.js.map +1 -1
  126. package/dist/types/tool/index.d.ts +20 -0
  127. package/dist/types/tool/index.d.ts.map +1 -1
  128. package/package.json +2 -1
  129. package/src/compaction/__tests__/verifier-empty-reply.test.ts +96 -0
  130. package/src/compaction/verifier.ts +10 -1
  131. package/src/config/__tests__/compaction-budget-schema.test.ts +114 -0
  132. package/src/config/runtime.ts +20 -11
  133. package/src/manager/run/persistence.ts +3 -1
  134. package/src/provider/__tests__/errors.test.ts +85 -0
  135. package/src/provider/errors.ts +344 -0
  136. package/src/provider/index.ts +15 -0
  137. package/src/public-runtime.ts +53 -10
  138. package/src/registry/tool/execute.test.ts +121 -7
  139. package/src/registry/tool/execute.ts +50 -12
  140. package/src/registry/toolset/catalog.test.ts +118 -0
  141. package/src/registry/toolset/catalog.ts +26 -4
  142. package/src/runtime/query/__tests__/capability-negotiation.test.ts +88 -0
  143. package/src/runtime/query/__tests__/deferred-tools.test.ts +111 -0
  144. package/src/runtime/query/__tests__/long-document-flow.test.ts +12 -12
  145. package/src/runtime/query/__tests__/stream-recovery.test.ts +65 -2
  146. package/src/runtime/query/executor.ts +2 -2
  147. package/src/runtime/query/iteration/index.ts +15 -0
  148. package/src/runtime/query/iteration/phases/compaction-safe-cut.test.ts +186 -0
  149. package/src/runtime/query/iteration/phases/compaction.ts +54 -12
  150. package/src/runtime/query/iteration/stream-turn.ts +11 -6
  151. package/src/runtime/query/result.ts +11 -1
  152. package/src/tools/builtins/__tests__/edit.test.ts +270 -10
  153. package/src/tools/builtins/__tests__/payload-budget.test.ts +40 -20
  154. package/src/tools/builtins/__tests__/write-file.test.ts +52 -13
  155. package/src/tools/builtins/atomic-write-file.ts +58 -0
  156. package/src/tools/builtins/bash.ts +2 -2
  157. package/src/tools/builtins/edit.ts +116 -162
  158. package/src/tools/builtins/file-mutation-lock.ts +27 -0
  159. package/src/tools/builtins/index.ts +2 -2
  160. package/src/tools/builtins/write-file.ts +68 -44
  161. package/src/tools/coordinator/__tests__/task-list.test.ts +16 -0
  162. package/src/tools/coordinator/agent.ts +6 -8
  163. package/src/tools/coordinator/index.ts +11 -34
  164. package/src/tools/defineTool.ts +6 -0
  165. package/src/types/provider/chat.ts +14 -1
  166. package/src/types/provider/error.ts +29 -0
  167. package/src/types/provider/index.ts +5 -0
  168. package/src/types/run/entity.ts +2 -0
  169. package/src/types/run/events.ts +7 -1
  170. package/src/types/sandbox/index.ts +4 -0
  171. package/src/types/tool/index.ts +20 -0
@@ -0,0 +1,344 @@
1
+ /**
2
+ * One classified error for every provider failure.
3
+ *
4
+ * Before this, a failure from any of the seven drivers reached the caller as an
5
+ * opaque string, so nothing downstream could decide whether it was worth
6
+ * retrying, whether it was the caller's fault, or whether the context window had
7
+ * simply run out. Two drivers built that string by interpolating the response
8
+ * body; three more inherited a vendor SDK error whose message IS the response
9
+ * body. Either way, a credential the upstream echoed back landed in a message
10
+ * that gets logged.
11
+ *
12
+ * So the contract here is deliberately narrow:
13
+ *
14
+ * - the message is built from the STATUS LINE and the classified `kind`. The
15
+ * response body is read to classify and then dropped. It is never
16
+ * interpolated, never re-thrown, and never attached as `cause` — a `cause`
17
+ * survives every logger that serializes an error chain, which defeats the
18
+ * point.
19
+ * - `retryAfterMs` is DATA. Nothing in this module sleeps, backs off or
20
+ * retries. A retry loop inside a driver burns the run's wall clock and hides
21
+ * the failure from the layer that should decide.
22
+ */
23
+
24
+ import type { ProviderErrorKind, ProviderRequestErrorInit } from '../types/provider/error.js'
25
+ export type {
26
+ ProviderErrorInfo,
27
+ ProviderErrorKind,
28
+ ProviderRequestErrorInit,
29
+ } from '../types/provider/error.js'
30
+
31
+ const PROVIDER_ERROR_KINDS: readonly ProviderErrorKind[] = [
32
+ 'throttle',
33
+ 'network',
34
+ 'auth',
35
+ 'context_overflow',
36
+ 'bad_request',
37
+ 'server',
38
+ ]
39
+
40
+ /**
41
+ * A provider request that failed, classified.
42
+ *
43
+ * `name` is set explicitly rather than inherited, because the classifier is
44
+ * matched structurally across a package boundary (a driver in one package throws
45
+ * it; the runtime in another reads it) and `instanceof` is unreliable when two
46
+ * copies of the SDK end up in one process.
47
+ */
48
+ export class ProviderRequestError extends Error {
49
+ public readonly kind: ProviderErrorKind
50
+ public readonly providerId: string
51
+ public readonly status?: number
52
+ public readonly retryAfterMs?: number
53
+
54
+ constructor(init: ProviderRequestErrorInit) {
55
+ super(buildProviderErrorMessage(init))
56
+ this.name = 'ProviderRequestError'
57
+ this.kind = init.kind
58
+ this.providerId = init.providerId
59
+ if (init.status !== undefined) this.status = init.status
60
+ if (init.retryAfterMs !== undefined) this.retryAfterMs = init.retryAfterMs
61
+ }
62
+ }
63
+
64
+ /** Is this a classified provider failure, whichever SDK copy threw it? */
65
+ export function isProviderRequestError(err: unknown): err is ProviderRequestError {
66
+ return (
67
+ err instanceof Error &&
68
+ err.name === 'ProviderRequestError' &&
69
+ typeof (err as { providerId?: unknown }).providerId === 'string' &&
70
+ PROVIDER_ERROR_KINDS.includes((err as { kind?: ProviderErrorKind }).kind as ProviderErrorKind)
71
+ )
72
+ }
73
+
74
+ /**
75
+ * Did the caller's own AbortSignal terminate this request?
76
+ *
77
+ * Provider SDKs do not agree on the object they reject with: some preserve
78
+ * `signal.reason`, while others replace it with `AbortError` or
79
+ * `APIUserAbortError`. Reclassifying any of those as a network failure breaks
80
+ * the runtime's Stop/cancel path, which depends on the abort escaping the
81
+ * provider boundary.
82
+ *
83
+ * The signal must itself be aborted. That condition distinguishes a caller Stop
84
+ * from an SDK timeout which may also use an `AbortError`-shaped rejection.
85
+ */
86
+ export function isCallerAbortError(error: unknown, signal?: AbortSignal): boolean {
87
+ if (!signal?.aborted) return false
88
+ if (error === signal.reason) return true
89
+ if (!(error instanceof Error)) return false
90
+ return error.name === 'AbortError' || error.name === 'APIUserAbortError'
91
+ }
92
+
93
+ const KIND_SENTENCE: Record<ProviderErrorKind, string> = {
94
+ throttle: 'rate limited by the provider',
95
+ network: 'could not reach the provider',
96
+ auth: 'the provider rejected the request credentials',
97
+ context_overflow: 'the request exceeded the model context window',
98
+ bad_request: 'the provider rejected the request as invalid',
99
+ server: 'the provider failed to complete the request',
100
+ }
101
+
102
+ function buildProviderErrorMessage(init: ProviderRequestErrorInit): string {
103
+ const status = init.status !== undefined ? ` (HTTP ${init.status})` : ''
104
+ const detail = init.detail ? `: ${init.detail}` : ''
105
+ return `${init.providerId}${status} — ${KIND_SENTENCE[init.kind]}${detail}`
106
+ }
107
+
108
+ /**
109
+ * Body fragments that mean "the request did not fit", across the vendors we
110
+ * drive. Matched case-insensitively against the raw body, which is then
111
+ * discarded.
112
+ *
113
+ * A 400 is otherwise `bad_request`: an overflow is the one 400 a caller can act
114
+ * on automatically (compact and retry), and mistaking a genuine schema error for
115
+ * an overflow would send the run into a pointless compaction loop.
116
+ */
117
+ const OVERFLOW_BODY_PATTERNS: readonly RegExp[] = [
118
+ /prompt is too long/i,
119
+ /context[_ ]length[_ ]exceeded/i,
120
+ /maximum context length/i,
121
+ /too long for requested model/i,
122
+ /input is too long/i,
123
+ /(?:input|prompt|context).{0,80}exceeds the maximum/i,
124
+ /reduce the (?:input|prompt|context) length/i,
125
+ // Ollama phrases it the other way round — "input length exceeds context
126
+ // length" — so a pattern anchored on "exceeded" misses it entirely.
127
+ /exceeds?\s+(?:the\s+)?(?:model'?s?\s+)?context\s+(?:length|window|size)/i,
128
+ /context\s+(?:length|window|size)\s+exceed/i,
129
+ ]
130
+
131
+ /** Does this response body say the request did not fit the window? */
132
+ export function bodySaysContextOverflow(body: string | undefined | null): boolean {
133
+ if (!body) return false
134
+ return OVERFLOW_BODY_PATTERNS.some((re) => re.test(body))
135
+ }
136
+
137
+ /**
138
+ * `Retry-After` in milliseconds. The header is either delta-seconds or an
139
+ * HTTP-date; both are specified, and vendors use both.
140
+ *
141
+ * Returns undefined for anything unparseable or for a date already in the past —
142
+ * a negative delay is worse than none, because a caller would treat it as
143
+ * "retry immediately" against a provider that just asked it to wait.
144
+ */
145
+ export function parseRetryAfterMs(
146
+ headerValue: string | null | undefined,
147
+ now: number = Date.now(),
148
+ ): number | undefined {
149
+ if (!headerValue) return undefined
150
+ const trimmed = headerValue.trim()
151
+ if (trimmed === '') return undefined
152
+
153
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
154
+ const seconds = Number(trimmed)
155
+ if (!Number.isFinite(seconds) || seconds < 0) return undefined
156
+ return Math.round(seconds * 1000)
157
+ }
158
+
159
+ const at = Date.parse(trimmed)
160
+ if (Number.isNaN(at)) return undefined
161
+ const delta = at - now
162
+ return delta > 0 ? delta : undefined
163
+ }
164
+
165
+ /**
166
+ * Classify an HTTP failure. `body` is used ONLY to separate a context overflow
167
+ * from an ordinary bad request, and is not retained.
168
+ */
169
+ export function classifyProviderHttpStatus(
170
+ status: number,
171
+ body?: string | null,
172
+ ): ProviderErrorKind {
173
+ if (status === 401 || status === 403) return 'auth'
174
+ if (status === 429) return 'throttle'
175
+ if (status === 408 || status === 425) return 'network'
176
+ if (status >= 500) return 'server'
177
+ if (status === 413) {
178
+ return bodySaysContextOverflow(body) ? 'context_overflow' : 'bad_request'
179
+ }
180
+ if (status >= 400) {
181
+ return bodySaysContextOverflow(body) ? 'context_overflow' : 'bad_request'
182
+ }
183
+ // A non-failure status reaching here is a caller bug, not a provider one.
184
+ return 'server'
185
+ }
186
+
187
+ /**
188
+ * Vendor error TYPES, as the vendors name them in their own payloads. This is a
189
+ * defined vocabulary, not prose, which is what makes it safe to classify on:
190
+ * Anthropic sends `{"error":{"type":"overloaded_error"}}` and OpenAI sends
191
+ * `{"error":{"code":"context_length_exceeded"}}`.
192
+ *
193
+ * It matters for MID-STREAM failures above all. Those arrive after a 200, so
194
+ * there is no status to classify from, and without this an upstream overload
195
+ * would be filed as `network` — "we could not reach the provider" — when the
196
+ * provider answered and then gave up. The distinction is the whole point of the
197
+ * taxonomy: one is worth retrying elsewhere, the other is worth retrying here.
198
+ */
199
+ const VENDOR_TYPE_KINDS: ReadonlyArray<readonly [RegExp, ProviderErrorKind]> = [
200
+ [/overloaded_error|api_error|service_unavailable/i, 'server'],
201
+ [/rate_limit_error|rate_limit_exceeded/i, 'throttle'],
202
+ [/authentication_error|permission_error|invalid_api_key|unauthorized/i, 'auth'],
203
+ [/timeout_error|connection_error/i, 'network'],
204
+ ]
205
+
206
+ /** The kind a vendor's own error-type vocabulary implies, if it says one. */
207
+ function vendorTypeKind(message: string): ProviderErrorKind | undefined {
208
+ for (const [re, kind] of VENDOR_TYPE_KINDS) {
209
+ if (re.test(message)) return kind
210
+ }
211
+ return undefined
212
+ }
213
+
214
+ /**
215
+ * Status code off a vendor SDK's own error object, whatever it calls the field.
216
+ * Anthropic and OpenAI use `status`; the ollama client uses `status_code`; AWS
217
+ * puts it under `$metadata.httpStatusCode`.
218
+ */
219
+ function vendorErrorStatus(err: unknown): number | undefined {
220
+ if (typeof err !== 'object' || err === null) return undefined
221
+ const e = err as {
222
+ status?: unknown
223
+ statusCode?: unknown
224
+ status_code?: unknown
225
+ $metadata?: { httpStatusCode?: unknown }
226
+ }
227
+ for (const candidate of [e.status, e.statusCode, e.status_code, e.$metadata?.httpStatusCode]) {
228
+ if (typeof candidate === 'number' && Number.isFinite(candidate)) return candidate
229
+ }
230
+ return undefined
231
+ }
232
+
233
+ /**
234
+ * `retry-after` off a vendor SDK error's captured response headers. The
235
+ * Anthropic and OpenAI clients both attach them (as a `Headers` instance or a
236
+ * plain object depending on version); the ollama client throws them away, which
237
+ * is why `retryAfterMs` is genuinely unavailable on that driver.
238
+ */
239
+ function vendorRetryAfter(err: unknown): string | undefined {
240
+ if (typeof err !== 'object' || err === null) return undefined
241
+ // AWS keeps the response one level down, on `$response`; the Anthropic and
242
+ // OpenAI clients attach `headers` directly.
243
+ const headers =
244
+ (err as { headers?: unknown }).headers ??
245
+ (err as { $response?: { headers?: unknown } }).$response?.headers
246
+ if (!headers) return undefined
247
+ if (typeof (headers as Headers).get === 'function') {
248
+ return (headers as Headers).get('retry-after') ?? undefined
249
+ }
250
+ const record = headers as Record<string, unknown>
251
+ for (const key of ['retry-after', 'Retry-After', 'retryAfter']) {
252
+ const value = record[key]
253
+ if (typeof value === 'string') return value
254
+ }
255
+ return undefined
256
+ }
257
+
258
+ /**
259
+ * Classify an error thrown by a vendor SDK and replace it.
260
+ *
261
+ * This exists because wrapping our OWN `!response.ok` throws is not enough. The
262
+ * Anthropic, OpenAI and ollama clients each build their error message FROM the
263
+ * response body, so a credential the upstream echoed back is already inside
264
+ * `err.message` before our code sees it. Proven with a planted fake token on all
265
+ * three.
266
+ *
267
+ * So the vendor error is read for its status and scanned for an overflow
268
+ * signature, and then **dropped entirely** — not re-thrown, not wrapped, and not
269
+ * attached as `cause`. A `cause` is exactly what a structured logger walks, so
270
+ * keeping one for debuggability would reintroduce the leak it is meant to close.
271
+ *
272
+ * `name` is checked too, because AWS models its failures as distinct classes
273
+ * (`ThrottlingException`, `ValidationException`, `AccessDeniedException`) rather
274
+ * than as status codes.
275
+ */
276
+ export function providerVendorError(input: {
277
+ readonly providerId: string
278
+ readonly error: unknown
279
+ readonly retryAfter?: string | null
280
+ readonly now?: number
281
+ }): ProviderRequestError {
282
+ const { error } = input
283
+ const status = vendorErrorStatus(error)
284
+ const name = error instanceof Error ? error.name : ''
285
+ const message = error instanceof Error ? error.message : ''
286
+
287
+ let kind: ProviderErrorKind
288
+ if (/ThrottlingException|TooManyRequests|ServiceQuotaExceeded/i.test(name)) {
289
+ kind = 'throttle'
290
+ } else if (/AccessDenied|Unauthorized|Forbidden|Authentication/i.test(name)) {
291
+ kind = 'auth'
292
+ } else if (/ValidationException/i.test(name)) {
293
+ kind = bodySaysContextOverflow(message) ? 'context_overflow' : 'bad_request'
294
+ } else if (
295
+ /ServiceUnavailable|InternalServer|ModelTimeout|ModelStreamError|ModelError|ModelNotReady/i.test(
296
+ name,
297
+ )
298
+ ) {
299
+ kind = 'server'
300
+ } else if (status !== undefined) {
301
+ kind = classifyProviderHttpStatus(status, message)
302
+ } else if (bodySaysContextOverflow(message)) {
303
+ kind = 'context_overflow'
304
+ } else {
305
+ // No status: a mid-stream failure, or a transport error. The vendor's own
306
+ // error-type vocabulary is the only signal left; absent that, the request
307
+ // genuinely did not get an answer.
308
+ kind = vendorTypeKind(message) ?? 'network'
309
+ }
310
+
311
+ const retryAfterMs = parseRetryAfterMs(
312
+ input.retryAfter ?? vendorRetryAfter(error),
313
+ input.now ?? Date.now(),
314
+ )
315
+ return new ProviderRequestError({
316
+ kind,
317
+ providerId: input.providerId,
318
+ ...(status !== undefined ? { status } : {}),
319
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
320
+ })
321
+ }
322
+
323
+ /**
324
+ * Build a classified error from a failed HTTP response.
325
+ *
326
+ * Callers pass the body they already read for classification; this function
327
+ * does not return it, store it, or put it in the message.
328
+ */
329
+ export function providerHttpError(input: {
330
+ readonly providerId: string
331
+ readonly status: number
332
+ readonly body?: string | null
333
+ readonly retryAfter?: string | null
334
+ readonly now?: number
335
+ }): ProviderRequestError {
336
+ const kind = classifyProviderHttpStatus(input.status, input.body)
337
+ const retryAfterMs = parseRetryAfterMs(input.retryAfter, input.now ?? Date.now())
338
+ return new ProviderRequestError({
339
+ kind,
340
+ providerId: input.providerId,
341
+ status: input.status,
342
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
343
+ })
344
+ }
@@ -1,3 +1,18 @@
1
+ export {
2
+ ProviderRequestError,
3
+ isCallerAbortError,
4
+ isProviderRequestError,
5
+ classifyProviderHttpStatus,
6
+ bodySaysContextOverflow,
7
+ parseRetryAfterMs,
8
+ providerHttpError,
9
+ providerVendorError,
10
+ } from './errors.js'
11
+ export type {
12
+ ProviderErrorInfo,
13
+ ProviderErrorKind,
14
+ ProviderRequestErrorInit,
15
+ } from './errors.js'
1
16
  export {
2
17
  ProviderRegistry,
3
18
  UnknownProviderError,
@@ -47,11 +47,19 @@ export * from './utils/id.js'
47
47
 
48
48
  // ─── utility helpers ─────────────────────────────────────────────────────
49
49
 
50
- export { accumulateCost, calculateCost, formatCost, ZERO_COST } from './utils/cost.js'
50
+ export {
51
+ accumulateCost,
52
+ calculateCost,
53
+ formatCost,
54
+ ZERO_COST,
55
+ } from './utils/cost.js'
51
56
  export { toErrorMessage } from './utils/error.js'
52
57
  export { configureLogger, getRootLogger, Logger } from './utils/logger.js'
53
58
  export { buildToolResultHashes, hashToolResult } from './utils/hash.js'
54
- export { compressShellOutput, compressShellOutputFull } from './utils/shell-compress.js'
59
+ export {
60
+ compressShellOutput,
61
+ compressShellOutputFull,
62
+ } from './utils/shell-compress.js'
55
63
  export { createChildAbortController } from './utils/abort.js'
56
64
  export { memoizeAsync } from './utils/memoize.js'
57
65
  export { extractFinalResponse } from './utils/conversation.js'
@@ -61,7 +69,10 @@ export { extractFinalResponse } from './utils/conversation.js'
61
69
  export { resolveTaskModel } from './router/task-router.js'
62
70
  export { drainQuery, query } from './runtime/query/index.js'
63
71
  export { ContextCache } from './runtime/query/context-cache.js'
64
- export { CheckpointManager, projectEmergencyToCheckpoint } from './runtime/query/checkpoint.js'
72
+ export {
73
+ CheckpointManager,
74
+ projectEmergencyToCheckpoint,
75
+ } from './runtime/query/checkpoint.js'
65
76
  export { prepareReplayState } from './runtime/query/replay/prepare.js'
66
77
  export { listCheckpoints } from './runtime/query/replay/list.js'
67
78
  export { DecisionParser, FallbackResolver } from './runtime/decision/index.js'
@@ -73,8 +84,17 @@ export {
73
84
 
74
85
  // ─── personas, skills, advisory ──────────────────────────────────────────
75
86
 
76
- export { assembleSystemPrompt, mergePersonas, withSessionContext } from './persona/index.js'
77
- export { discoverSkills, loadSkill, resolveSkillChain, SkillRegistry } from './skills/index.js'
87
+ export {
88
+ assembleSystemPrompt,
89
+ mergePersonas,
90
+ withSessionContext,
91
+ } from './persona/index.js'
92
+ export {
93
+ discoverSkills,
94
+ loadSkill,
95
+ resolveSkillChain,
96
+ SkillRegistry,
97
+ } from './skills/index.js'
78
98
  export {
79
99
  AdvisorRegistry,
80
100
  AdvisoryContext,
@@ -144,19 +164,30 @@ export { LocalTaskGateway } from './gateway/local.js'
144
164
  // ─── providers, sandbox, vault ───────────────────────────────────────────
145
165
 
146
166
  export {
167
+ bodySaysContextOverflow,
168
+ classifyProviderHttpStatus,
147
169
  DuplicateProviderError,
170
+ isCallerAbortError,
171
+ isProviderRequestError,
148
172
  LazyProviderLoadError,
149
173
  LazyProviderSyncCreateError,
150
174
  MOCK_CAPABILITIES,
151
175
  MockLLMProvider,
176
+ parseRetryAfterMs,
152
177
  PERMISSIVE_PROVIDER_CAPABILITIES,
178
+ providerHttpError,
179
+ providerVendorError,
153
180
  ProviderRegistry,
181
+ ProviderRequestError,
154
182
  registerMock,
155
183
  resolveProviderCapabilities,
156
184
  UnknownProviderError,
157
185
  } from './provider/index.js'
158
186
 
159
- export { LocalSandboxProvider, SandboxProviderFactory } from './sandbox/index.js'
187
+ export {
188
+ LocalSandboxProvider,
189
+ SandboxProviderFactory,
190
+ } from './sandbox/index.js'
160
191
 
161
192
  export { InMemoryCredentialVault } from './vault/index.js'
162
193
 
@@ -222,7 +253,10 @@ export {
222
253
  runToA2ATask,
223
254
  } from './bridge/a2a/index.js'
224
255
 
225
- export { mapRunToStreamEvent, mapSessionToStreamEvent } from './bridge/sse/index.js'
256
+ export {
257
+ mapRunToStreamEvent,
258
+ mapSessionToStreamEvent,
259
+ } from './bridge/sse/index.js'
226
260
 
227
261
  // ─── bus, verification ───────────────────────────────────────────────────
228
262
 
@@ -355,10 +389,19 @@ export {
355
389
  // ─── runtime helpers colocated with shapes under `types/` (§1.5) ─────────
356
390
 
357
391
  export { A2AProtocolError } from './types/a2a/index.js'
358
- export { isTerminalActivityStatus, resolveActivityTracking } from './types/activity/index.js'
392
+ export {
393
+ isTerminalActivityStatus,
394
+ resolveActivityTracking,
395
+ } from './types/activity/index.js'
359
396
  export { isTerminalAgentTaskState } from './types/agent/task.js'
360
- export { accumulateTokenUsage, isTerminalStatus } from './types/common/index.js'
361
- export { assertComputerUseActionType, assertDisplayServer } from './types/computer-use/index.js'
397
+ export {
398
+ accumulateTokenUsage,
399
+ isTerminalStatus,
400
+ } from './types/common/index.js'
401
+ export {
402
+ assertComputerUseActionType,
403
+ assertDisplayServer,
404
+ } from './types/computer-use/index.js'
362
405
  export { isConnectorActive } from './types/connector/core.js'
363
406
  export { CONNECTOR_SCOPE_ORDER } from './types/connector/scope.js'
364
407
  export { RoutingResponseSchema } from './types/decision/index.js'
@@ -98,6 +98,17 @@ describe('ToolRegistry — register + availability', () => {
98
98
  expect(() => r.register('oops', 'not-a-tool' as never)).toThrow(/requires a ToolDefinition/)
99
99
  })
100
100
 
101
+ it('rejects enforced model input without an explicit model schema', () => {
102
+ const r = new ToolRegistry()
103
+ expect(() =>
104
+ r.register(
105
+ makeTool('unsafe_strict', {
106
+ enforceModelInput: true,
107
+ }),
108
+ ),
109
+ ).toThrow(/enforceModelInput.*modelInputSchema/)
110
+ })
111
+
101
112
  it('getAvailability returns active for unknown names (current default)', () => {
102
113
  const r = new ToolRegistry()
103
114
  expect(r.getAvailability('never-registered')).toBe('active')
@@ -228,7 +239,10 @@ describe('ToolRegistry — searchDeferred', () => {
228
239
  r.register(
229
240
  [
230
241
  makeTool('send_invoice', {
231
- inputSchema: z.object({ customerEmail: z.string(), amount: z.number() }),
242
+ inputSchema: z.object({
243
+ customerEmail: z.string(),
244
+ amount: z.number(),
245
+ }),
232
246
  }),
233
247
  ],
234
248
  'deferred',
@@ -237,10 +251,50 @@ describe('ToolRegistry — searchDeferred', () => {
237
251
  expect(r.searchDeferred('customerEmail').map((t) => t.name)).toEqual(['send_invoice'])
238
252
  })
239
253
 
254
+ it('indexes canonical model-schema branches without re-advertising runtime aliases', () => {
255
+ const r = new ToolRegistry()
256
+ r.register(
257
+ [
258
+ makeTool('edit_document', {
259
+ inputSchema: z.object({ oldStr: z.string(), newStr: z.string() }),
260
+ modelInputSchema: {
261
+ type: 'object',
262
+ properties: {
263
+ path: { type: 'string' },
264
+ old_string: { type: 'string' },
265
+ new_string: { type: 'string' },
266
+ },
267
+ required: ['path', 'new_string'],
268
+ additionalProperties: false,
269
+ anyOf: [
270
+ {
271
+ type: 'object',
272
+ properties: {
273
+ path: { type: 'string' },
274
+ old_string: { type: 'string' },
275
+ new_string: { type: 'string' },
276
+ },
277
+ required: ['path', 'old_string', 'new_string'],
278
+ additionalProperties: false,
279
+ },
280
+ ],
281
+ },
282
+ }),
283
+ ],
284
+ 'deferred',
285
+ )
286
+ expect(r.searchDeferred('old_string').map((t) => t.name)).toEqual(['edit_document'])
287
+ expect(r.searchDeferred('oldStr')).toEqual([])
288
+ })
289
+
240
290
  it('stops generic CRUD verbs so they cannot activate catalog slices', () => {
241
291
  const r = new ToolRegistry()
242
292
  r.register(
243
- [makeTool('list_deals', { description: 'List the deals in an account.' })],
293
+ [
294
+ makeTool('list_deals', {
295
+ description: 'List the deals in an account.',
296
+ }),
297
+ ],
244
298
  'deferred',
245
299
  )
246
300
  r.register([makeTool('list_workflows', { description: 'List workflows.' })], 'deferred')
@@ -370,6 +424,34 @@ describe('ToolRegistry — toPromptSection + toLLMTools', () => {
370
424
  expect(names).toEqual(['a', 'c'])
371
425
  })
372
426
 
427
+ it('toLLMTools: prefers a canonical model schema over runtime compatibility aliases', () => {
428
+ const r = new ToolRegistry()
429
+ const modelInputSchema = {
430
+ type: 'object',
431
+ properties: { new_string: { type: 'string' } },
432
+ required: ['new_string'],
433
+ additionalProperties: false,
434
+ }
435
+ r.register(
436
+ makeTool('edit', {
437
+ inputSchema: z.object({
438
+ new_string: z.string().optional(),
439
+ newStr: z.string().optional(),
440
+ }),
441
+ modelInputSchema,
442
+ }),
443
+ )
444
+
445
+ const firstSchema = r.toLLMTools()[0]?.function.parameters
446
+ expect(firstSchema).toEqual(modelInputSchema)
447
+ expect(firstSchema).not.toBe(modelInputSchema)
448
+ ;(firstSchema?.properties as Record<string, unknown>).newStr = { type: 'string' }
449
+
450
+ const nextSchema = r.toLLMTools()[0]?.function.parameters
451
+ expect(nextSchema).toEqual(modelInputSchema)
452
+ expect(JSON.stringify(nextSchema)).not.toContain('newStr')
453
+ })
454
+
373
455
  it('toLLMTools: prefixes description with tier label when labelInDescription is true', () => {
374
456
  const tierConfig: ToolTierConfig = {
375
457
  tiers: [{ id: 'safe', label: 'Safe', priority: 1 }],
@@ -398,7 +480,11 @@ describe('ToolRegistry — execute', () => {
398
480
  'write',
399
481
  {},
400
482
  makeContext({
401
- permissionContext: { mode: 'plan', runId: 'run_1', workingDirectory: '/tmp' },
483
+ permissionContext: {
484
+ mode: 'plan',
485
+ runId: 'run_1',
486
+ workingDirectory: '/tmp',
487
+ },
402
488
  }),
403
489
  )
404
490
  expect(result.success).toBe(false)
@@ -413,7 +499,11 @@ describe('ToolRegistry — execute', () => {
413
499
  'read',
414
500
  {},
415
501
  makeContext({
416
- permissionContext: { mode: 'plan', runId: 'run_1', workingDirectory: '/tmp' },
502
+ permissionContext: {
503
+ mode: 'plan',
504
+ runId: 'run_1',
505
+ workingDirectory: '/tmp',
506
+ },
417
507
  }),
418
508
  )
419
509
  expect(result.success).toBe(true)
@@ -433,11 +523,28 @@ describe('ToolRegistry — execute', () => {
433
523
  expect(result.error).toContain('Expected string, received number')
434
524
  })
435
525
 
526
+ it('appends a tool-specific recovery hint to validation failures', async () => {
527
+ const r = new ToolRegistry()
528
+ r.register(
529
+ makeTool('strict', {
530
+ inputSchema: z.object({ required: z.string() }),
531
+ validationErrorHint: 'Retry with {"required":"value"}.',
532
+ }),
533
+ )
534
+ const result = await r.execute('strict', { required: 123 }, makeContext())
535
+ expect(result.success).toBe(false)
536
+ expect(result.error).toContain('Required: required: string.')
537
+ expect(result.error).toContain('Retry with {"required":"value"}.')
538
+ })
539
+
436
540
  it('empty-args validation lists required params with descriptions', async () => {
437
541
  const r = new ToolRegistry()
438
542
  r.register(
439
543
  makeTool('needs', {
440
- inputSchema: z.object({ q: z.string().describe('the query'), n: z.number() }),
544
+ inputSchema: z.object({
545
+ q: z.string().describe('the query'),
546
+ n: z.number(),
547
+ }),
441
548
  }),
442
549
  )
443
550
  const result = await r.execute('needs', {}, makeContext())
@@ -458,7 +565,10 @@ describe('ToolRegistry — execute', () => {
458
565
  it('validation hint tolerates a schema it cannot introspect', async () => {
459
566
  const r = new ToolRegistry()
460
567
  const bogusSchema = {
461
- safeParse: () => ({ success: false, error: { issues: [{ path: [], message: 'nope' }] } }),
568
+ safeParse: () => ({
569
+ success: false,
570
+ error: { issues: [{ path: [], message: 'nope' }] },
571
+ }),
462
572
  }
463
573
  r.register(makeTool('weird', { inputSchema: bogusSchema as never }))
464
574
  const result = await r.execute('weird', { a: 1 }, makeContext())
@@ -516,7 +626,11 @@ describe('ToolRegistry — execute', () => {
516
626
  'mutate',
517
627
  {},
518
628
  makeContext({
519
- permissionContext: { mode: 'plan', runId: 'run_1', workingDirectory: '/tmp' },
629
+ permissionContext: {
630
+ mode: 'plan',
631
+ runId: 'run_1',
632
+ workingDirectory: '/tmp',
633
+ },
520
634
  }),
521
635
  )
522
636
  expect(result.success).toBe(false)