@namzu/sdk 56.2.0 → 57.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 (177) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/index.d.ts +1 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/index.js +1 -0
  8. package/dist/agents/index.js.map +1 -1
  9. package/dist/agents/plan.d.ts +20 -0
  10. package/dist/agents/plan.d.ts.map +1 -0
  11. package/dist/agents/plan.js +27 -0
  12. package/dist/agents/plan.js.map +1 -0
  13. package/dist/compaction/dangling.d.ts +12 -1
  14. package/dist/compaction/dangling.d.ts.map +1 -1
  15. package/dist/compaction/dangling.js +17 -3
  16. package/dist/compaction/dangling.js.map +1 -1
  17. package/dist/compaction/index.d.ts +2 -2
  18. package/dist/compaction/index.d.ts.map +1 -1
  19. package/dist/compaction/index.js.map +1 -1
  20. package/dist/compaction/manager.d.ts +16 -0
  21. package/dist/compaction/manager.d.ts.map +1 -1
  22. package/dist/compaction/manager.js +55 -0
  23. package/dist/compaction/manager.js.map +1 -1
  24. package/dist/compaction/manual.d.ts +2 -0
  25. package/dist/compaction/manual.d.ts.map +1 -1
  26. package/dist/compaction/manual.js +33 -4
  27. package/dist/compaction/manual.js.map +1 -1
  28. package/dist/compaction/restore.d.ts +51 -0
  29. package/dist/compaction/restore.d.ts.map +1 -0
  30. package/dist/compaction/restore.js +123 -0
  31. package/dist/compaction/restore.js.map +1 -0
  32. package/dist/compaction/types.d.ts +22 -0
  33. package/dist/compaction/types.d.ts.map +1 -1
  34. package/dist/compaction/wire.d.ts +3 -1
  35. package/dist/compaction/wire.d.ts.map +1 -1
  36. package/dist/compaction/wire.js +16 -0
  37. package/dist/compaction/wire.js.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/plugin/shell-hook.d.ts +19 -1
  41. package/dist/plugin/shell-hook.d.ts.map +1 -1
  42. package/dist/plugin/shell-hook.js +21 -5
  43. package/dist/plugin/shell-hook.js.map +1 -1
  44. package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
  45. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  46. package/dist/prompt/coding-agent-doctrine.js +3 -2
  47. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  48. package/dist/provider/errors.d.ts +3 -0
  49. package/dist/provider/errors.d.ts.map +1 -1
  50. package/dist/provider/errors.js +23 -3
  51. package/dist/provider/errors.js.map +1 -1
  52. package/dist/provider/fallback.d.ts.map +1 -1
  53. package/dist/provider/fallback.js +34 -3
  54. package/dist/provider/fallback.js.map +1 -1
  55. package/dist/provider/retry.d.ts +12 -5
  56. package/dist/provider/retry.d.ts.map +1 -1
  57. package/dist/provider/retry.js +22 -8
  58. package/dist/provider/retry.js.map +1 -1
  59. package/dist/public-runtime.d.ts +4 -2
  60. package/dist/public-runtime.d.ts.map +1 -1
  61. package/dist/public-runtime.js +4 -1
  62. package/dist/public-runtime.js.map +1 -1
  63. package/dist/public-types.d.ts +1 -1
  64. package/dist/public-types.d.ts.map +1 -1
  65. package/dist/runtime/jobs/registry.d.ts +42 -0
  66. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  67. package/dist/runtime/jobs/registry.js +77 -0
  68. package/dist/runtime/jobs/registry.js.map +1 -1
  69. package/dist/runtime/query/calls-that-never-started.d.ts +12 -0
  70. package/dist/runtime/query/calls-that-never-started.d.ts.map +1 -0
  71. package/dist/runtime/query/calls-that-never-started.js +41 -0
  72. package/dist/runtime/query/calls-that-never-started.js.map +1 -0
  73. package/dist/runtime/query/executor/tool-call-admission.d.ts +2 -2
  74. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  75. package/dist/runtime/query/executor/tool-call-admission.js +13 -9
  76. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  77. package/dist/runtime/query/executor.d.ts +8 -0
  78. package/dist/runtime/query/executor.d.ts.map +1 -1
  79. package/dist/runtime/query/executor.js +35 -0
  80. package/dist/runtime/query/executor.js.map +1 -1
  81. package/dist/runtime/query/file-evidence-context.js +1 -1
  82. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  83. package/dist/runtime/query/index.d.ts +6 -0
  84. package/dist/runtime/query/index.d.ts.map +1 -1
  85. package/dist/runtime/query/index.js +5 -0
  86. package/dist/runtime/query/index.js.map +1 -1
  87. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  88. package/dist/runtime/query/iteration/phases/compaction.js +20 -0
  89. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  90. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  91. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  93. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  94. package/dist/runtime/query/iteration/stream-turn.js +45 -25
  95. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  96. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  97. package/dist/runtime/query/prepare-turn.js +10 -1
  98. package/dist/runtime/query/prepare-turn.js.map +1 -1
  99. package/dist/scheduler/completion-inbox.js +3 -3
  100. package/dist/scheduler/completion-inbox.js.map +1 -1
  101. package/dist/tools/admit-facts.d.ts.map +1 -1
  102. package/dist/tools/admit-facts.js +1 -0
  103. package/dist/tools/admit-facts.js.map +1 -1
  104. package/dist/tools/builtins/bash.d.ts +42 -1
  105. package/dist/tools/builtins/bash.d.ts.map +1 -1
  106. package/dist/tools/builtins/bash.js +115 -48
  107. package/dist/tools/builtins/bash.js.map +1 -1
  108. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  109. package/dist/tools/builtins/read-file.js +55 -5
  110. package/dist/tools/builtins/read-file.js.map +1 -1
  111. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  112. package/dist/tools/file-read-tracker.js +28 -0
  113. package/dist/tools/file-read-tracker.js.map +1 -1
  114. package/dist/types/agent/base.d.ts +6 -0
  115. package/dist/types/agent/base.d.ts.map +1 -1
  116. package/dist/types/errors/catalog.d.ts +3 -0
  117. package/dist/types/errors/catalog.d.ts.map +1 -1
  118. package/dist/types/errors/catalog.js +24 -2
  119. package/dist/types/errors/catalog.js.map +1 -1
  120. package/dist/types/provider/error.d.ts +7 -0
  121. package/dist/types/provider/error.d.ts.map +1 -1
  122. package/dist/types/provider/errors.d.ts +24 -0
  123. package/dist/types/provider/errors.d.ts.map +1 -1
  124. package/dist/types/provider/errors.js +101 -1
  125. package/dist/types/provider/errors.js.map +1 -1
  126. package/dist/types/provider/stream.d.ts +6 -0
  127. package/dist/types/provider/stream.d.ts.map +1 -1
  128. package/dist/types/session/events.d.ts +8 -0
  129. package/dist/types/session/events.d.ts.map +1 -1
  130. package/dist/types/session/events.js.map +1 -1
  131. package/dist/types/tool/index.d.ts +39 -0
  132. package/dist/types/tool/index.d.ts.map +1 -1
  133. package/dist/types/tool/index.js.map +1 -1
  134. package/dist/types/tool/presentation.d.ts +4 -0
  135. package/dist/types/tool/presentation.d.ts.map +1 -1
  136. package/package.json +1 -1
  137. package/src/agents/QueryAgent.ts +1 -0
  138. package/src/agents/index.ts +1 -0
  139. package/src/agents/plan.ts +30 -0
  140. package/src/compaction/dangling.ts +35 -3
  141. package/src/compaction/index.ts +4 -0
  142. package/src/compaction/manager.ts +53 -0
  143. package/src/compaction/manual.ts +42 -10
  144. package/src/compaction/restore.ts +159 -0
  145. package/src/compaction/types.ts +25 -0
  146. package/src/compaction/wire.ts +26 -1
  147. package/src/index.ts +2 -0
  148. package/src/plugin/shell-hook.ts +33 -5
  149. package/src/prompt/coding-agent-doctrine.ts +3 -2
  150. package/src/provider/errors.ts +27 -3
  151. package/src/provider/fallback.ts +34 -2
  152. package/src/provider/retry.ts +30 -10
  153. package/src/public-runtime.ts +8 -0
  154. package/src/public-types.ts +3 -0
  155. package/src/runtime/jobs/registry.ts +105 -0
  156. package/src/runtime/query/calls-that-never-started.ts +37 -0
  157. package/src/runtime/query/executor/tool-call-admission.ts +12 -9
  158. package/src/runtime/query/executor.ts +37 -0
  159. package/src/runtime/query/file-evidence-context.ts +1 -1
  160. package/src/runtime/query/index.ts +12 -0
  161. package/src/runtime/query/iteration/phases/compaction.ts +19 -0
  162. package/src/runtime/query/iteration/phases/context.ts +3 -0
  163. package/src/runtime/query/iteration/stream-turn.ts +47 -25
  164. package/src/runtime/query/prepare-turn.ts +10 -1
  165. package/src/scheduler/completion-inbox.ts +3 -3
  166. package/src/tools/admit-facts.ts +1 -0
  167. package/src/tools/builtins/bash.ts +149 -54
  168. package/src/tools/builtins/read-file.ts +66 -4
  169. package/src/tools/file-read-tracker.ts +25 -1
  170. package/src/types/agent/base.ts +7 -0
  171. package/src/types/errors/catalog.ts +31 -2
  172. package/src/types/provider/error.ts +7 -0
  173. package/src/types/provider/errors.ts +120 -1
  174. package/src/types/provider/stream.ts +6 -0
  175. package/src/types/session/events.ts +8 -0
  176. package/src/types/tool/index.ts +40 -0
  177. package/src/types/tool/presentation.ts +4 -0
@@ -102,7 +102,9 @@ export const ReadFileTool = defineTool({
102
102
  const capped = renderCappedRead(content, buffer.length, input, capOptions(input, context))
103
103
  if (!capped.ok) return refusedRead(input.path, buffer.length, capped.message, true)
104
104
  const rendered = capped.rendered
105
- recordObservedRead(context, input.path, content, rendered)
105
+ const unchanged = unchangedRead(context, input.path, input, content, buffer.length)
106
+ if (unchanged) return unchanged
107
+ recordObservedRead(context, input.path, content, rendered, input)
106
108
 
107
109
  return {
108
110
  success: true,
@@ -149,7 +151,12 @@ export const ReadFileTool = defineTool({
149
151
  const capped = renderCappedRead(content, buffer.length, input, capOptions(input, context))
150
152
  if (!capped.ok) return refusedRead(filePath, buffer.length, capped.message, false)
151
153
  const rendered = capped.rendered
152
- recordObservedRead(context, filePath, content, rendered)
154
+ const unchanged =
155
+ retained === undefined
156
+ ? unchangedRead(context, filePath, input, content, buffer.length)
157
+ : undefined
158
+ if (unchanged) return unchanged
159
+ recordObservedRead(context, filePath, content, rendered, input)
153
160
 
154
161
  return {
155
162
  success: true,
@@ -243,14 +250,69 @@ function recordObservedRead(
243
250
  key: string,
244
251
  content: string,
245
252
  rendered: RenderedRead,
253
+ input: ReadWindowInput,
246
254
  ): void {
247
255
  const tracker = context.fileReadTracker
248
256
  if (!tracker) return
249
257
  if (!rendered.partial && tracker.recordFullRead && context.toolUseId) {
250
258
  tracker.recordFullRead(key, content, context.toolUseId, fingerprintContent(rendered.output))
251
- return
259
+ } else {
260
+ tracker.recordRead(key, content)
261
+ }
262
+ if (tracker.recordReadWindow && context.toolUseId) {
263
+ tracker.recordReadWindow(key, windowKey(input), {
264
+ callId: context.toolUseId,
265
+ renderedFingerprint: fingerprintContent(rendered.output),
266
+ contentFingerprint: fingerprintContent(content),
267
+ })
268
+ }
269
+ }
270
+
271
+ type ReadWindowInput = {
272
+ readonly readRange?: readonly number[] | undefined
273
+ readonly offset?: number | undefined
274
+ readonly limit?: number | undefined
275
+ readonly allow_large?: boolean | undefined
276
+ }
277
+
278
+ /** The lines a read asked for, as one comparable string. */
279
+ function windowKey(input: ReadWindowInput): string {
280
+ const range = input.readRange
281
+ const lines = range
282
+ ? `r${range[0]}-${range[1]}`
283
+ : `o${input.offset ?? 0}l${input.limit === undefined ? 'all' : input.limit}`
284
+ return input.allow_large === true ? `${lines}+large` : lines
285
+ }
286
+
287
+ /**
288
+ * The answer to a repeat read of the same lines of a file that has not changed.
289
+ *
290
+ * The earlier result is still in front of the model, so returning the lines a
291
+ * second time only spends the window. It is given ONLY while that is true:
292
+ * the body must fingerprint the same as at the earlier read, and the earlier
293
+ * receipt must still be in the conversation exactly as the tool returned it
294
+ * (the host says so through `receiptVisible`). A receipt that was cleared,
295
+ * summarised away or annotated is no answer, and the lines are returned again.
296
+ */
297
+ function unchangedRead(
298
+ context: ToolContext,
299
+ key: string,
300
+ input: ReadWindowInput,
301
+ content: string,
302
+ bytes: number,
303
+ ) {
304
+ const tracker = context.fileReadTracker
305
+ if (!tracker?.readWindow || !context.receiptVisible) return undefined
306
+ const earlier = tracker.readWindow(key, windowKey(input))
307
+ if (!earlier || earlier.contentFingerprint !== fingerprintContent(content)) return undefined
308
+ if (!context.receiptVisible(earlier.callId, earlier.renderedFingerprint)) return undefined
309
+ return {
310
+ success: true as const,
311
+ output:
312
+ 'Unchanged since your earlier read of the same lines. That result is still current; use it instead of reading again.',
313
+ data: { path: key, unchanged: true },
314
+ facts: { kind: 'read' as const, path: key, bytes, unchanged: true },
252
315
  }
253
- tracker.recordRead(key, content)
254
316
  }
255
317
 
256
318
  function describeStructuredBinaryRead(path: string, buffer: Buffer): string | null {
@@ -1,4 +1,4 @@
1
- import type { FileReadTracker } from '../types/tool/index.js'
1
+ import type { FileReadTracker, ReadWindowEntry } from '../types/tool/index.js'
2
2
  import { fingerprintContent } from './builtins/content-fingerprint.js'
3
3
 
4
4
  /** Host-owned observation ledger. Share only across turns of the same conversation and filesystem. */
@@ -48,7 +48,31 @@ export function createFileReadTracker(): FileReadTracker {
48
48
  chains.delete(key)
49
49
  }
50
50
  }
51
+ // The last few windows read of each file, newest last. Bounded because a
52
+ // long session reads many files and a window key is cheap to repeat.
53
+ const windows = new Map<string, Map<string, ReadWindowEntry>>()
54
+ const MAX_WINDOWS_PER_FILE = 6
55
+ const MAX_WINDOWED_FILES = 512
51
56
  return {
57
+ recordReadWindow(key, window, entry) {
58
+ let forFile = windows.get(key)
59
+ if (forFile) windows.delete(key)
60
+ else forFile = new Map()
61
+ forFile.delete(window)
62
+ forFile.set(window, entry)
63
+ while (forFile.size > MAX_WINDOWS_PER_FILE) {
64
+ const oldest = forFile.keys().next().value
65
+ if (oldest === undefined) break
66
+ forFile.delete(oldest)
67
+ }
68
+ windows.set(key, forFile)
69
+ while (windows.size > MAX_WINDOWED_FILES) {
70
+ const oldest = windows.keys().next().value
71
+ if (oldest === undefined) break
72
+ windows.delete(oldest)
73
+ }
74
+ },
75
+ readWindow: (key, window) => windows.get(key)?.get(window),
52
76
  recordRead: observe,
53
77
  recordFullRead(key, content, callId, renderedFingerprint) {
54
78
  observe(key, content)
@@ -118,6 +118,13 @@ export interface BaseAgentConfig {
118
118
  /** Live project policy, flushed independently of human continuation text. */
119
119
  projectInstructionContext?: ProjectInstructionContext
120
120
 
121
+ /**
122
+ * Text the host wants carried across a compaction, such as where its plan
123
+ * file is. Asked once per pass, after the summary is written; the answer is
124
+ * appended to the restored context. Returning nothing adds nothing.
125
+ */
126
+ postCompactionContext?: () => string | undefined | Promise<string | undefined>
127
+
121
128
  allowedTools?: readonly string[]
122
129
 
123
130
  /**
@@ -1,4 +1,9 @@
1
- import { ProviderError, classifyProviderError } from '../provider/errors.js'
1
+ import {
2
+ ProviderError,
3
+ type TransportCause,
4
+ classifyProviderError,
5
+ readTransportCause,
6
+ } from '../provider/errors.js'
2
7
  import { isNamzuError } from './index.js'
3
8
 
4
9
  /**
@@ -33,6 +38,8 @@ export interface ErrorFacts {
33
38
  readonly retryAfterMs?: number
34
39
  /** A hint the throw site attached directly. See {@link readHint}. */
35
40
  readonly hint?: string
41
+ /** When the failure is the network path rather than the provider: which part of it. */
42
+ readonly transport?: TransportCause
36
43
  }
37
44
 
38
45
  export interface ErrorCatalogRule {
@@ -91,10 +98,12 @@ export function factsOf(err: unknown): ErrorFacts {
91
98
  ? classifyProviderError(err)
92
99
  : undefined
93
100
  if (providerFailure) {
101
+ const transport = readTransportCause(providerFailure)
94
102
  return {
95
103
  code: providerFailure.code,
96
104
  message,
97
105
  name: providerFailure.name,
106
+ ...(transport !== undefined ? { transport } : {}),
98
107
  ...(providerFailure.status !== undefined ? { status: providerFailure.status } : {}),
99
108
  ...(providerFailure.retryAfterMs !== undefined
100
109
  ? { retryAfterMs: providerFailure.retryAfterMs }
@@ -159,7 +168,7 @@ export const DEFAULT_ERROR_RULES: readonly ErrorCatalogRule[] = [
159
168
  id: 'provider.rate_limit',
160
169
  when: (facts) => facts.code === 'rate_limit' || facts.status === 429,
161
170
  message: 'The provider is rate limiting this turn.',
162
- hint: 'Respect the provider retry delay when reported. Lower concurrency, or wait for the quota window to reset before resuming.',
171
+ hint: 'Wait for the provider retry delay when reported, or for the quota window to reset, before resuming. When the wait is long, continue on another model or provider.',
163
172
  },
164
173
  {
165
174
  id: 'provider.context_overflow',
@@ -176,6 +185,26 @@ export const DEFAULT_ERROR_RULES: readonly ErrorCatalogRule[] = [
176
185
  message: 'The provider is failing on its own side.',
177
186
  hint: 'Nothing in the turn is wrong. Resume from the last checkpoint once the provider recovers.',
178
187
  },
188
+ {
189
+ // Before the general network rule: a proxy that wants a sign-in and a
190
+ // certificate nobody trusts each have a fix the general advice does not name.
191
+ id: 'provider.proxy_auth',
192
+ when: (facts) => facts.transport === 'proxy_auth_required',
193
+ message: 'The network proxy asked for a sign-in before it would pass the request on.',
194
+ hint: 'Put the proxy user name and password in the proxy address (for example HTTPS_PROXY=http://user:password@host:port), or ask whoever runs the proxy for access.',
195
+ },
196
+ {
197
+ id: 'provider.proxy_refused',
198
+ when: (facts) => facts.transport === 'proxy_refused',
199
+ message: 'The network proxy refused to pass the request on.',
200
+ hint: 'Check the proxy address and the NO_PROXY list, and ask whoever runs the proxy whether this host is allowed to reach the provider.',
201
+ },
202
+ {
203
+ id: 'provider.untrusted_certificate',
204
+ when: (facts) => facts.transport === 'untrusted_certificate',
205
+ message: 'The connection was refused because its certificate is not trusted.',
206
+ hint: 'If a proxy or firewall inspects encrypted traffic, set NODE_EXTRA_CA_CERTS to the file holding its certificate authority and start again.',
207
+ },
179
208
  {
180
209
  id: 'provider.network',
181
210
  when: (facts) => facts.code === 'network' || facts.code === 'timeout',
@@ -51,4 +51,11 @@ export interface ProviderRequestErrorInit extends ProviderErrorInfo {
51
51
  * is believed over the status code, so the retry loop leaves it alone.
52
52
  */
53
53
  readonly shouldRetry?: boolean
54
+ /**
55
+ * Which part of the network path the request tripped on (a proxy that wants a
56
+ * sign-in, a certificate nobody trusts), when the vendor error's causes say.
57
+ * Data and not a `cause`: the raw error is never attached, but the diagnosis
58
+ * has to survive the wrapping or no one downstream can act on it.
59
+ */
60
+ readonly transport?: import('./errors.js').TransportCause
54
61
  }
@@ -278,6 +278,59 @@ function isUndiciTermination(err: unknown): boolean {
278
278
  return false
279
279
  }
280
280
 
281
+ /**
282
+ * Why a request never reached the model when the cause is the network path
283
+ * between this host and the provider, not the provider.
284
+ *
285
+ * - `proxy_auth_required`: a proxy answered the tunnel request asking for credentials.
286
+ * - `proxy_refused`: a proxy answered the tunnel request with any other refusal.
287
+ * - `untrusted_certificate`: the certificate on the connection did not chain to an
288
+ * authority this process trusts (typically a firewall that inspects encrypted traffic).
289
+ */
290
+ export type TransportCause = 'proxy_auth_required' | 'proxy_refused' | 'untrusted_certificate'
291
+
292
+ const TRANSPORT_CAUSES: ReadonlySet<string> = new Set([
293
+ 'proxy_auth_required',
294
+ 'proxy_refused',
295
+ 'untrusted_certificate',
296
+ ])
297
+
298
+ const UNTRUSTED_CERTIFICATE_CODES: ReadonlySet<string> = new Set([
299
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
300
+ 'UNABLE_TO_GET_ISSUER_CERT',
301
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
302
+ 'SELF_SIGNED_CERT_IN_CHAIN',
303
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
304
+ 'CERT_UNTRUSTED',
305
+ 'CERT_CHAIN_TOO_LONG',
306
+ ])
307
+
308
+ /**
309
+ * Read a proxy refusal or an untrusted certificate off a failure's cause chain.
310
+ *
311
+ * Structural where it can be (an error code) and, for the proxy, the one
312
+ * sentence undici writes when a tunnel request is answered with a status other
313
+ * than 200, because that failure carries no code or status of its own.
314
+ */
315
+ export function readTransportCause(err: unknown): TransportCause | undefined {
316
+ for (const link of causeChain(err)) {
317
+ // A driver's own error carries the diagnosis as data (see
318
+ // `ProviderRequestError.transport`) because it never keeps the cause.
319
+ if (typeof link.transport === 'string' && TRANSPORT_CAUSES.has(link.transport)) {
320
+ return link.transport as TransportCause
321
+ }
322
+ if (typeof link.code === 'string' && UNTRUSTED_CERTIFICATE_CODES.has(link.code)) {
323
+ return 'untrusted_certificate'
324
+ }
325
+ const tunnel =
326
+ typeof link.message === 'string'
327
+ ? /Proxy response \((\d{3})\) !== 200 when HTTP Tunneling/.exec(link.message)
328
+ : null
329
+ if (tunnel) return tunnel[1] === '407' ? 'proxy_auth_required' : 'proxy_refused'
330
+ }
331
+ return undefined
332
+ }
333
+
281
334
  /** Node/undici transport failures that mean "never reached the model". */
282
335
  const NETWORK_ERRNOS: ReadonlySet<string> = new Set([
283
336
  'ECONNRESET',
@@ -288,12 +341,54 @@ const NETWORK_ERRNOS: ReadonlySet<string> = new Set([
288
341
  'ENETUNREACH',
289
342
  'ENOTFOUND',
290
343
  'EAI_AGAIN',
344
+ 'ENETDOWN',
345
+ 'EHOSTDOWN',
346
+ 'ERR_PROXY_TUNNEL',
291
347
  'EPROTO',
292
348
  'UND_ERR_SOCKET',
349
+ 'UND_ERR_CLOSED',
293
350
  'UND_ERR_CONNECT_TIMEOUT',
294
351
  'UND_ERR_RESPONSE_STATUS_CODE',
295
352
  ])
296
353
 
354
+ /**
355
+ * A certificate that fails verification. The peer was reached and answered,
356
+ * and resending the same request meets the same refusal, so it is a network
357
+ * failure that is not retried.
358
+ */
359
+ const CERTIFICATE_ERRNOS: ReadonlySet<string> = new Set([
360
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
361
+ 'UNABLE_TO_GET_ISSUER_CERT',
362
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
363
+ 'CERT_HAS_EXPIRED',
364
+ 'CERT_NOT_YET_VALID',
365
+ 'CERT_UNTRUSTED',
366
+ 'CERT_REVOKED',
367
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
368
+ 'SELF_SIGNED_CERT_IN_CHAIN',
369
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
370
+ 'HOSTNAME_MISMATCH',
371
+ ])
372
+
373
+ /**
374
+ * The code of a certificate-verification failure anywhere on the chain, or
375
+ * `undefined`. Other TLS failures (a handshake cut short, a protocol error)
376
+ * are transient and are not this.
377
+ */
378
+ export function readCertificateFailure(err: unknown): string | undefined {
379
+ for (const link of causeChain(err)) {
380
+ if (typeof link.code === 'string' && CERTIFICATE_ERRNOS.has(link.code)) return link.code
381
+ }
382
+ return undefined
383
+ }
384
+
385
+ /** Other TLS and socket-layer codes that mean the connection broke, not that it was refused. */
386
+ function isTransientTransportCode(code: string): boolean {
387
+ return (
388
+ code.startsWith('ERR_SSL_') || (code.startsWith('ERR_TLS_') && !CERTIFICATE_ERRNOS.has(code))
389
+ )
390
+ }
391
+
297
392
  const TIMEOUT_ERRNOS: ReadonlySet<string> = new Set([
298
393
  'ETIMEDOUT',
299
394
  'ESOCKETTIMEDOUT',
@@ -544,6 +639,20 @@ export function classifyProviderError(
544
639
  })
545
640
  }
546
641
 
642
+ // The network path itself: a proxy that wants credentials or refuses the
643
+ // tunnel, or a certificate nobody here trusts. Filed as `network` so it falls
644
+ // over like any unreachable provider, but never retried — the same request
645
+ // meets the same proxy or the same certificate again.
646
+ if (readTransportCause(err) !== undefined) {
647
+ return new ProviderError({
648
+ code: 'network',
649
+ message,
650
+ providerId,
651
+ retryable: false,
652
+ cause: err,
653
+ })
654
+ }
655
+
547
656
  // A 400 that is really a window overflow must not be filed as a plain
548
657
  // invalid_request: the caller can recover from one and not the other.
549
658
  if (messages.some((m) => codeFromMessage(m, status) === 'context_length_exceeded')) {
@@ -570,6 +679,16 @@ export function classifyProviderError(
570
679
  }
571
680
 
572
681
  if (errno !== undefined) {
682
+ const certificate = readCertificateFailure(err)
683
+ if (certificate !== undefined) {
684
+ return new ProviderError({
685
+ code: 'network',
686
+ message,
687
+ providerId,
688
+ retryable: false,
689
+ cause: err,
690
+ })
691
+ }
573
692
  if (TIMEOUT_ERRNOS.has(errno)) {
574
693
  return new ProviderError({
575
694
  code: 'timeout',
@@ -579,7 +698,7 @@ export function classifyProviderError(
579
698
  cause: err,
580
699
  })
581
700
  }
582
- if (NETWORK_ERRNOS.has(errno)) {
701
+ if (NETWORK_ERRNOS.has(errno) || isTransientTransportCode(errno)) {
583
702
  return new ProviderError({
584
703
  code: 'network',
585
704
  message,
@@ -204,4 +204,10 @@ export interface ProviderFallbackNotice {
204
204
  readonly status?: number
205
205
  /** The classified failure's own sentence, already redacted at its source. */
206
206
  readonly reason: string
207
+ /**
208
+ * The member that failed had streamed only pieces that can be taken back
209
+ * (reasoning, or a tool call still being written). The consumer drops them
210
+ * before the next chunk, as it does for a restarting retry.
211
+ */
212
+ readonly restart?: boolean
207
213
  }
@@ -447,6 +447,12 @@ type CoreSessionEvent =
447
447
  status?: number
448
448
  /** The classified failure's own sentence. */
449
449
  reason: string
450
+ /**
451
+ * The member that failed had streamed only pieces that can be taken
452
+ * back (reasoning, or a tool call still being written): drop what this
453
+ * message showed so far, as for a restarting retry.
454
+ */
455
+ restart?: boolean
450
456
  }
451
457
  | {
452
458
  type: 'tool_completed'
@@ -800,6 +806,8 @@ type CoreSessionEvent =
800
806
  duplicateToolResultsRemoved: number
801
807
  orphanedToolResultsRemoved: number
802
808
  syntheticToolResultsInserted: number
809
+ /** Of those, how many say the call never started. Absent when none. */
810
+ neverRanToolResultsInserted?: number
803
811
  /** Exact number of durable image occurrences withheld from later requests. */
804
812
  providerRejectedImagesSuppressed?: number
805
813
  }
@@ -180,6 +180,18 @@ export interface BackgroundJobRegistryRef {
180
180
  workingDirectory?: string
181
181
  initialOutput?: readonly { readonly stream: 'stdout' | 'stderr'; readonly text: string }[]
182
182
  }): { id: string; status: string }
183
+ /**
184
+ * Offer a foreground command this call is waiting on for hand-over to the
185
+ * background, until the returned function is called. `background` is asked
186
+ * to take the command over (`user`: the person asked; `message`: a message
187
+ * from the person must reach the model) and says whether it did. Optional:
188
+ * a host without it never moves a running command, and the call keeps the
189
+ * kill.
190
+ */
191
+ registerForeground?(entry: {
192
+ command: string
193
+ background(reason: 'user' | 'message'): boolean
194
+ }): () => void
183
195
  /**
184
196
  * Resolves once the process exists or failed to start (`error` says why).
185
197
  * Optional: a host that cannot tell may omit it, and `bash` then replies
@@ -341,6 +353,19 @@ export interface FileReadTracker {
341
353
  * referencing a body it has been told is stale, without a check of its own.
342
354
  */
343
355
  recordDriftObserved?(key: string): void
356
+ /**
357
+ * Optional note of the last read of one window of a file: the call whose
358
+ * receipt shows it, the fingerprint of that receipt, and the fingerprint of
359
+ * the whole body at the time.
360
+ *
361
+ * Lets a repeat read of the same window answer "unchanged" instead of
362
+ * returning the lines again. Only the read tool writes it, and it is only
363
+ * trusted together with {@link ToolContext.receiptVisible}: a receipt that
364
+ * has since been cleared, summarised away or edited is no answer.
365
+ */
366
+ recordReadWindow?(key: string, window: string, entry: ReadWindowEntry): void
367
+ /** What {@link recordReadWindow} last noted for this window of this file. */
368
+ readWindow?(key: string, window: string): ReadWindowEntry | undefined
344
369
  /** Whether a refused mutation has reported this path stale since the last observation. */
345
370
  driftObserved?(key: string): boolean
346
371
  /**
@@ -356,6 +381,15 @@ export interface FileReadTracker {
356
381
  fingerprint?(key: string): string | undefined
357
382
  }
358
383
 
384
+ /** The last read of one window of a file; see {@link FileReadTracker.recordReadWindow}. */
385
+ export interface ReadWindowEntry {
386
+ readonly callId: string
387
+ /** Fingerprint of the exact text the read returned. */
388
+ readonly renderedFingerprint: string
389
+ /** Fingerprint of the whole file body at that read. */
390
+ readonly contentFingerprint: string
391
+ }
392
+
359
393
  export interface ToolPauseOption {
360
394
  readonly id: string
361
395
  readonly label: string
@@ -533,6 +567,12 @@ export interface ToolContext {
533
567
  */
534
568
  codeAnalysis?: CodeAnalysisProvider
535
569
  fileReadTracker?: FileReadTracker
570
+ /**
571
+ * Whether the receipt of an earlier call is still, byte for byte, in the
572
+ * conversation the model is shown. Absent when the host cannot say; a tool
573
+ * must then treat the answer as no.
574
+ */
575
+ receiptVisible?: (callId: string, renderedFingerprint: string) => boolean
536
576
 
537
577
  /**
538
578
  * Where work that outlives this call is held.
@@ -151,6 +151,8 @@ export type ToolFacts =
151
151
  readonly binary?: { readonly bytes: number }
152
152
  /** The command was handed to a background job instead of being waited on. */
153
153
  readonly background?: true
154
+ /** The command was already running when it was handed over (a key, a message, an interrupt or its timeout). */
155
+ readonly continued?: true
154
156
  }
155
157
  | {
156
158
  readonly kind: 'read'
@@ -162,6 +164,8 @@ export type ToolFacts =
162
164
  readonly returnedLines?: number
163
165
  /** Only a window of the file was returned. */
164
166
  readonly partial?: boolean
167
+ /** The lines were not repeated: an earlier read of the same lines still holds. */
168
+ readonly unchanged?: boolean
165
169
  }
166
170
  | {
167
171
  readonly kind: 'search'