@namzu/sdk 56.2.0 → 57.1.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 (199) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -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 +3 -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 +9 -0
  25. package/dist/compaction/manual.d.ts.map +1 -1
  26. package/dist/compaction/manual.js +64 -16
  27. package/dist/compaction/manual.js.map +1 -1
  28. package/dist/compaction/model-summary.d.ts +101 -0
  29. package/dist/compaction/model-summary.d.ts.map +1 -0
  30. package/dist/compaction/model-summary.js +333 -0
  31. package/dist/compaction/model-summary.js.map +1 -0
  32. package/dist/compaction/restore.d.ts +51 -0
  33. package/dist/compaction/restore.d.ts.map +1 -0
  34. package/dist/compaction/restore.js +123 -0
  35. package/dist/compaction/restore.js.map +1 -0
  36. package/dist/compaction/types.d.ts +22 -0
  37. package/dist/compaction/types.d.ts.map +1 -1
  38. package/dist/compaction/verifier.d.ts +6 -1
  39. package/dist/compaction/verifier.d.ts.map +1 -1
  40. package/dist/compaction/verifier.js +3 -3
  41. package/dist/compaction/verifier.js.map +1 -1
  42. package/dist/compaction/wire.d.ts +3 -1
  43. package/dist/compaction/wire.d.ts.map +1 -1
  44. package/dist/compaction/wire.js +16 -0
  45. package/dist/compaction/wire.js.map +1 -1
  46. package/dist/config/runtime.d.ts +62 -0
  47. package/dist/config/runtime.d.ts.map +1 -1
  48. package/dist/config/runtime.js +16 -0
  49. package/dist/config/runtime.js.map +1 -1
  50. package/dist/index.d.ts +1 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/plugin/shell-hook.d.ts +19 -1
  53. package/dist/plugin/shell-hook.d.ts.map +1 -1
  54. package/dist/plugin/shell-hook.js +21 -5
  55. package/dist/plugin/shell-hook.js.map +1 -1
  56. package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
  57. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  58. package/dist/prompt/coding-agent-doctrine.js +5 -3
  59. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  60. package/dist/provider/errors.d.ts +3 -0
  61. package/dist/provider/errors.d.ts.map +1 -1
  62. package/dist/provider/errors.js +23 -3
  63. package/dist/provider/errors.js.map +1 -1
  64. package/dist/provider/fallback.d.ts.map +1 -1
  65. package/dist/provider/fallback.js +34 -3
  66. package/dist/provider/fallback.js.map +1 -1
  67. package/dist/provider/retry.d.ts +12 -5
  68. package/dist/provider/retry.d.ts.map +1 -1
  69. package/dist/provider/retry.js +22 -8
  70. package/dist/provider/retry.js.map +1 -1
  71. package/dist/public-runtime.d.ts +5 -2
  72. package/dist/public-runtime.d.ts.map +1 -1
  73. package/dist/public-runtime.js +4 -1
  74. package/dist/public-runtime.js.map +1 -1
  75. package/dist/public-types.d.ts +1 -1
  76. package/dist/public-types.d.ts.map +1 -1
  77. package/dist/runtime/jobs/registry.d.ts +42 -0
  78. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  79. package/dist/runtime/jobs/registry.js +77 -0
  80. package/dist/runtime/jobs/registry.js.map +1 -1
  81. package/dist/runtime/query/calls-that-never-started.d.ts +12 -0
  82. package/dist/runtime/query/calls-that-never-started.d.ts.map +1 -0
  83. package/dist/runtime/query/calls-that-never-started.js +41 -0
  84. package/dist/runtime/query/calls-that-never-started.js.map +1 -0
  85. package/dist/runtime/query/executor/tool-call-admission.d.ts +2 -2
  86. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  87. package/dist/runtime/query/executor/tool-call-admission.js +13 -9
  88. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  89. package/dist/runtime/query/executor.d.ts +11 -0
  90. package/dist/runtime/query/executor.d.ts.map +1 -1
  91. package/dist/runtime/query/executor.js +48 -0
  92. package/dist/runtime/query/executor.js.map +1 -1
  93. package/dist/runtime/query/file-evidence-context.js +1 -1
  94. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  95. package/dist/runtime/query/index.d.ts +12 -0
  96. package/dist/runtime/query/index.d.ts.map +1 -1
  97. package/dist/runtime/query/index.js +9 -0
  98. package/dist/runtime/query/index.js.map +1 -1
  99. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  100. package/dist/runtime/query/iteration/phases/compaction.js +66 -1
  101. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  102. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  103. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  104. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  105. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  106. package/dist/runtime/query/iteration/stream-turn.js +45 -25
  107. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  108. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  109. package/dist/runtime/query/prepare-turn.js +10 -1
  110. package/dist/runtime/query/prepare-turn.js.map +1 -1
  111. package/dist/scheduler/completion-inbox.js +3 -3
  112. package/dist/scheduler/completion-inbox.js.map +1 -1
  113. package/dist/scheduler/local.js +1 -1
  114. package/dist/scheduler/local.js.map +1 -1
  115. package/dist/tools/admit-facts.d.ts.map +1 -1
  116. package/dist/tools/admit-facts.js +1 -0
  117. package/dist/tools/admit-facts.js.map +1 -1
  118. package/dist/tools/builtins/bash.d.ts +42 -1
  119. package/dist/tools/builtins/bash.d.ts.map +1 -1
  120. package/dist/tools/builtins/bash.js +115 -48
  121. package/dist/tools/builtins/bash.js.map +1 -1
  122. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  123. package/dist/tools/builtins/read-file.js +55 -5
  124. package/dist/tools/builtins/read-file.js.map +1 -1
  125. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  126. package/dist/tools/file-read-tracker.js +28 -0
  127. package/dist/tools/file-read-tracker.js.map +1 -1
  128. package/dist/types/agent/base.d.ts +14 -0
  129. package/dist/types/agent/base.d.ts.map +1 -1
  130. package/dist/types/agent/scheduler.d.ts +8 -0
  131. package/dist/types/agent/scheduler.d.ts.map +1 -1
  132. package/dist/types/errors/catalog.d.ts +3 -0
  133. package/dist/types/errors/catalog.d.ts.map +1 -1
  134. package/dist/types/errors/catalog.js +24 -2
  135. package/dist/types/errors/catalog.js.map +1 -1
  136. package/dist/types/plugin/index.d.ts +2 -2
  137. package/dist/types/provider/error.d.ts +7 -0
  138. package/dist/types/provider/error.d.ts.map +1 -1
  139. package/dist/types/provider/errors.d.ts +24 -0
  140. package/dist/types/provider/errors.d.ts.map +1 -1
  141. package/dist/types/provider/errors.js +101 -1
  142. package/dist/types/provider/errors.js.map +1 -1
  143. package/dist/types/provider/stream.d.ts +6 -0
  144. package/dist/types/provider/stream.d.ts.map +1 -1
  145. package/dist/types/session/events.d.ts +8 -0
  146. package/dist/types/session/events.d.ts.map +1 -1
  147. package/dist/types/session/events.js.map +1 -1
  148. package/dist/types/tool/index.d.ts +54 -0
  149. package/dist/types/tool/index.d.ts.map +1 -1
  150. package/dist/types/tool/index.js.map +1 -1
  151. package/dist/types/tool/presentation.d.ts +4 -0
  152. package/dist/types/tool/presentation.d.ts.map +1 -1
  153. package/package.json +1 -1
  154. package/src/agents/QueryAgent.ts +2 -0
  155. package/src/agents/index.ts +1 -0
  156. package/src/agents/plan.ts +30 -0
  157. package/src/compaction/dangling.ts +35 -3
  158. package/src/compaction/index.ts +5 -0
  159. package/src/compaction/manager.ts +53 -0
  160. package/src/compaction/manual.ts +110 -40
  161. package/src/compaction/model-summary.ts +412 -0
  162. package/src/compaction/restore.ts +159 -0
  163. package/src/compaction/types.ts +25 -0
  164. package/src/compaction/verifier.ts +3 -3
  165. package/src/compaction/wire.ts +26 -1
  166. package/src/config/runtime.ts +16 -0
  167. package/src/index.ts +2 -0
  168. package/src/plugin/shell-hook.ts +33 -5
  169. package/src/prompt/coding-agent-doctrine.ts +5 -3
  170. package/src/provider/errors.ts +27 -3
  171. package/src/provider/fallback.ts +34 -2
  172. package/src/provider/retry.ts +30 -10
  173. package/src/public-runtime.ts +12 -0
  174. package/src/public-types.ts +3 -0
  175. package/src/runtime/jobs/registry.ts +105 -0
  176. package/src/runtime/query/calls-that-never-started.ts +37 -0
  177. package/src/runtime/query/executor/tool-call-admission.ts +12 -9
  178. package/src/runtime/query/executor.ts +51 -0
  179. package/src/runtime/query/file-evidence-context.ts +1 -1
  180. package/src/runtime/query/index.ts +23 -0
  181. package/src/runtime/query/iteration/phases/compaction.ts +65 -1
  182. package/src/runtime/query/iteration/phases/context.ts +7 -0
  183. package/src/runtime/query/iteration/stream-turn.ts +47 -25
  184. package/src/runtime/query/prepare-turn.ts +10 -1
  185. package/src/scheduler/completion-inbox.ts +3 -3
  186. package/src/scheduler/local.ts +1 -1
  187. package/src/tools/admit-facts.ts +1 -0
  188. package/src/tools/builtins/bash.ts +149 -54
  189. package/src/tools/builtins/read-file.ts +66 -4
  190. package/src/tools/file-read-tracker.ts +25 -1
  191. package/src/types/agent/base.ts +16 -0
  192. package/src/types/agent/scheduler.ts +9 -0
  193. package/src/types/errors/catalog.ts +31 -2
  194. package/src/types/provider/error.ts +7 -0
  195. package/src/types/provider/errors.ts +120 -1
  196. package/src/types/provider/stream.ts +6 -0
  197. package/src/types/session/events.ts +8 -0
  198. package/src/types/tool/index.ts +56 -0
  199. package/src/types/tool/presentation.ts +4 -0
@@ -4,6 +4,7 @@ import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
4
4
  import { DANGEROUS_PATTERNS } from '../../constants/tools/index.js'
5
5
  import { killTree } from '../../process/kill-tree.js'
6
6
  import { genericLabel } from '../../registry/tool/presentation.js'
7
+ import { cancelCauseOf } from '../../types/session/cancel-cause.js'
7
8
  import type { ToolFacts } from '../../types/tool/presentation.js'
8
9
  import { subscribeToAbort } from '../../utils/abort.js'
9
10
  import { readPositiveIntEnv } from '../../utils/env.js'
@@ -93,6 +94,28 @@ export function movedToBackgroundMessage(jobId: string, timeoutMs: number): stri
93
94
  return `Command did not complete within its ${seconds}s timeout and was moved to the background as job ${jobId}; it was not stopped. You will be told when it ends. Its output so far is above.`
94
95
  }
95
96
 
97
+ /**
98
+ * How long a foreground command runs before it is offered for hand-over to
99
+ * the background. A command that ends sooner is never interrupted by a stray
100
+ * key press, and a host that shows a hint shows it from this moment.
101
+ */
102
+ export const FOREGROUND_HANDOFF_AFTER_MS = 2_000
103
+
104
+ /**
105
+ * What the model is told when the person moved a running foreground command to
106
+ * the background instead of stopping it. Exported so the test states the same
107
+ * words.
108
+ */
109
+ export function handedOffMessage(jobId: string, reason: 'user' | 'message' | 'interrupt'): string {
110
+ const why =
111
+ reason === 'user'
112
+ ? "at the person's request"
113
+ : reason === 'message'
114
+ ? 'so a message from the person could reach you'
115
+ : 'because the person interrupted the turn'
116
+ return `Command was moved to the background as job ${jobId} ${why}; it was not stopped. You will be told when it ends. Its output so far is above.`
117
+ }
118
+
96
119
  const inputSchema = z.object({
97
120
  command: z
98
121
  .string()
@@ -240,10 +263,34 @@ export function execHostShell(
240
263
  child: ReturnType<typeof spawnHostShell>
241
264
  initialOutput: readonly { stream: 'stdout' | 'stderr'; text: string }[]
242
265
  }) => string | undefined
266
+ /**
267
+ * Takes the live process over for a hand-over the person asked for
268
+ * (a key press, a message that must reach the model, an interrupt),
269
+ * with the same contract as `onTimeout`. Absent, those hand-overs are
270
+ * not offered and an interrupt kills the command as it always did.
271
+ */
272
+ adopt?: (running: {
273
+ child: ReturnType<typeof spawnHostShell>
274
+ initialOutput: readonly { stream: 'stdout' | 'stderr'; text: string }[]
275
+ }) => string | undefined
276
+ /**
277
+ * An interrupt the person caused (Esc, Ctrl+C) hands the command to
278
+ * `adopt` instead of killing it. Any other abort, a parent stopping a
279
+ * child or a budget stop, still kills it.
280
+ */
281
+ adoptOnInterrupt?: boolean
282
+ /**
283
+ * Called once, right after the process is spawned, with the means to
284
+ * hand it over later. `background` returns whether the process was
285
+ * taken over; it settles the call with `backgroundedJobId` set.
286
+ */
287
+ onRunning?: (control: { background(reason: 'user' | 'message'): boolean }) => void
243
288
  },
244
289
  ): Promise<{
245
- /** Set when `onTimeout` took the process over; the command was not stopped. */
290
+ /** Set when the process was taken over; the command was not stopped. */
246
291
  backgroundedJobId?: string
292
+ /** Why it was taken over. */
293
+ backgroundedBy?: 'timeout' | 'user' | 'message' | 'interrupt'
247
294
  stdout: string
248
295
  stderr: string
249
296
  /** Bytes captured on each stream, before decoding; absent from a runner that does not count them. */
@@ -401,44 +448,64 @@ export function execHostShell(
401
448
  )
402
449
  }
403
450
  child.once('close', onClose)
404
- const deadline = setTimeout(() => {
405
- if (options.onTimeout && !closed && cause === undefined) {
406
- let jobId: string | undefined
407
- try {
408
- jobId = options.onTimeout({
409
- child,
410
- initialOutput: (['stdout', 'stderr'] as const)
411
- .map((stream) => ({ stream, text: decodeCapture(stream) }))
412
- .filter((part) => part.text !== ''),
413
- })
414
- } catch {
415
- // A host that cannot take the process over leaves the kill in place.
416
- }
417
- if (jobId !== undefined) {
418
- // The registry owns the process from here: stop listening to it,
419
- // so a later Stop or its exit is not this call's business.
420
- closed = true
421
- disposeAbort?.()
422
- child.stdout?.off('data', onStdout)
423
- child.stderr?.off('data', onStderr)
424
- child.off('close', onClose)
425
- child.off('error', onError)
426
- child.off('spawn', onSpawn)
427
- resolve({
428
- backgroundedJobId: jobId,
429
- stdout: decodeCapture('stdout'),
430
- stderr: decodeCapture('stderr'),
431
- })
432
- return
433
- }
451
+ // Hands the live process to the host's job registry, once. `onTimeout`
452
+ // answers for the deadline; every other reason goes to `adopt`.
453
+ const takeOver = (reason: 'timeout' | 'user' | 'message' | 'interrupt'): boolean => {
454
+ const adopter =
455
+ reason === 'timeout' ? options.onTimeout : (options.adopt ?? options.onTimeout)
456
+ if (!adopter || closed || cause !== undefined) return false
457
+ let jobId: string | undefined
458
+ try {
459
+ jobId = adopter({
460
+ child,
461
+ initialOutput: (['stdout', 'stderr'] as const)
462
+ .map((stream) => ({ stream, text: decodeCapture(stream) }))
463
+ .filter((part) => part.text !== ''),
464
+ })
465
+ } catch {
466
+ // A host that cannot take the process over leaves the process where it is.
434
467
  }
468
+ if (jobId === undefined) return false
469
+ // The registry owns the process from here: stop listening to it,
470
+ // so a later Stop or its exit is not this call's business.
471
+ closed = true
472
+ disposeAbort?.()
473
+ clearTimeout(deadline)
474
+ child.stdout?.off('data', onStdout)
475
+ child.stderr?.off('data', onStderr)
476
+ child.off('close', onClose)
477
+ child.off('error', onError)
478
+ child.off('spawn', onSpawn)
479
+ resolve({
480
+ backgroundedJobId: jobId,
481
+ backgroundedBy: reason,
482
+ stdout: decodeCapture('stdout'),
483
+ stderr: decodeCapture('stderr'),
484
+ })
485
+ return true
486
+ }
487
+ const deadline = setTimeout(() => {
488
+ if (takeOver('timeout')) return
435
489
  cancel('timeout')
436
490
  }, options.timeout)
437
491
  deadline.unref?.()
438
492
  if (options.signal) {
439
- disposeAbort = subscribeToAbort(options.signal, () => cancel('caller'))
440
- if (options.signal.aborted) cancel('caller')
493
+ const signal = options.signal
494
+ const onAbort = () => {
495
+ // Esc or Ctrl+C keeps what the command was doing; any other stop
496
+ // (a parent cancelling a child, a budget) still ends it.
497
+ if (
498
+ options.adoptOnInterrupt === true &&
499
+ cancelCauseOf(signal.reason) === 'user' &&
500
+ takeOver('interrupt')
501
+ )
502
+ return
503
+ cancel('caller')
504
+ }
505
+ disposeAbort = subscribeToAbort(signal, onAbort)
506
+ if (signal.aborted) onAbort()
441
507
  }
508
+ options.onRunning?.({ background: (reason) => takeOver(reason) })
442
509
  })
443
510
  }
444
511
 
@@ -714,51 +781,76 @@ const BashToolBase = defineTool({
714
781
  // registry, a sandboxed turn, a one-off sandbox escape, a plain wait)
715
782
  // is there anything to hand the process to, so those keep the kill.
716
783
  const adopt = context.backgroundJobs?.adopt?.bind(context.backgroundJobs)
717
- const canMoveToBackground =
718
- adopt !== undefined &&
719
- context.sandbox === undefined &&
720
- !backgroundTasksDisabled() &&
721
- !startsWithSleep(input.command)
784
+ const canHandOff =
785
+ adopt !== undefined && context.sandbox === undefined && !backgroundTasksDisabled()
786
+ const canMoveOnTimeout = canHandOff && !startsWithSleep(input.command)
787
+ const adoptRunning = (running: {
788
+ child: Parameters<NonNullable<typeof adopt>>[0]['child']
789
+ initialOutput: Parameters<NonNullable<typeof adopt>>[0]['initialOutput']
790
+ }) =>
791
+ adopt?.({
792
+ command: input.command,
793
+ child: running.child,
794
+ workingDirectory: context.workingDirectory,
795
+ initialOutput: running.initialOutput,
796
+ }).id
797
+ // Offered for hand-over once it has run long enough to be worth moving.
798
+ let offerTimer: ReturnType<typeof setTimeout> | undefined
799
+ let withdrawOffer: (() => void) | undefined
800
+ const offer = context.backgroundJobs?.registerForeground?.bind(context.backgroundJobs)
722
801
 
723
802
  try {
724
- const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId } = await execHostShell(
725
- input.command,
726
- {
803
+ const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId, backgroundedBy } =
804
+ await execHostShell(input.command, {
727
805
  cwd: context.workingDirectory,
728
806
  timeout: input.timeout,
729
807
  env: { ...inherited.env, ...context.env },
730
808
  maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
731
809
  signal: context.abortSignal,
732
810
  onOutput,
733
- ...(canMoveToBackground
811
+ ...(canMoveOnTimeout ? { onTimeout: adoptRunning } : {}),
812
+ ...(canHandOff
734
813
  ? {
735
- onTimeout: ({ child, initialOutput }) =>
736
- adopt({
737
- command: input.command,
738
- child,
739
- workingDirectory: context.workingDirectory,
740
- initialOutput,
741
- }).id,
814
+ adopt: adoptRunning,
815
+ adoptOnInterrupt: true,
816
+ ...(offer
817
+ ? {
818
+ onRunning: (control: {
819
+ background(reason: 'user' | 'message'): boolean
820
+ }) => {
821
+ offerTimer = setTimeout(() => {
822
+ withdrawOffer = offer({
823
+ command: input.command,
824
+ background: (reason) => control.background(reason),
825
+ })
826
+ }, FOREGROUND_HANDOFF_AFTER_MS)
827
+ offerTimer.unref?.()
828
+ },
829
+ }
830
+ : {}),
742
831
  }
743
832
  : {}),
744
- },
745
- )
833
+ })
746
834
  if (backgroundedJobId !== undefined) {
835
+ const timedOut = backgroundedBy === 'timeout'
747
836
  return {
748
837
  success: true,
749
838
  output: [
750
839
  formatShellOutput(stdout, stderr),
751
- movedToBackgroundMessage(backgroundedJobId, input.timeout),
840
+ timedOut || backgroundedBy === undefined
841
+ ? movedToBackgroundMessage(backgroundedJobId, input.timeout)
842
+ : handedOffMessage(backgroundedJobId, backgroundedBy),
752
843
  ]
753
844
  .filter(Boolean)
754
845
  .join('\n\n'),
755
846
  data: {
756
847
  jobId: backgroundedJobId,
757
848
  background: true,
758
- timedOut: true,
849
+ ...(timedOut || backgroundedBy === undefined ? { timedOut: true } : {}),
759
850
  movedToBackground: true,
851
+ ...(backgroundedBy ? { backgroundedBy } : {}),
760
852
  },
761
- facts: { kind: 'command', command: input.command, background: true },
853
+ facts: { kind: 'command', command: input.command, background: true, continued: true },
762
854
  }
763
855
  }
764
856
  return {
@@ -856,6 +948,9 @@ const BashToolBase = defineTool({
856
948
  ...(failure.stderrBytes !== undefined ? { stderrBytes: failure.stderrBytes } : {}),
857
949
  }),
858
950
  }
951
+ } finally {
952
+ if (offerTimer !== undefined) clearTimeout(offerTimer)
953
+ withdrawOffer?.()
859
954
  }
860
955
  },
861
956
  })
@@ -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)
@@ -1,3 +1,4 @@
1
+ import type { CompactionSummaryRouter } from '../../compaction/model-summary.js'
1
2
  import type { ProjectInstructionContext } from '../../runtime/query/project-instructions.js'
2
3
  import type { SessionPaths } from '../../session/paths.js'
3
4
  import type { SessionTokenBudget } from '../../store/budget/index.js'
@@ -118,6 +119,21 @@ export interface BaseAgentConfig {
118
119
  /** Live project policy, flushed independently of human continuation text. */
119
120
  projectInstructionContext?: ProjectInstructionContext
120
121
 
122
+ /**
123
+ * Text the host wants carried across a compaction, such as where its plan
124
+ * file is. Asked once per pass, after the summary is written; the answer is
125
+ * appended to the restored context. Returning nothing adds nothing.
126
+ */
127
+ postCompactionContext?: () => string | undefined | Promise<string | undefined>
128
+
129
+ /**
130
+ * Names the model that writes a compaction summary, asked once per pass.
131
+ * Absent, or resolving nothing, the summary is the extracted working state;
132
+ * a call that fails falls back to it too. The call's cost is charged to the
133
+ * turn.
134
+ */
135
+ compactionSummaryRoute?: CompactionSummaryRouter
136
+
121
137
  allowedTools?: readonly string[]
122
138
 
123
139
  /**
@@ -132,6 +132,15 @@ export interface CreateTaskOptions {
132
132
 
133
133
  prompt: string
134
134
 
135
+ /**
136
+ * Messages the child starts from, placed before {@link prompt}. A host that
137
+ * delegates work which continues the caller's conversation passes that
138
+ * conversation here; the prompt then arrives as the last user message.
139
+ * Absent, the child sees only the prompt. The caller is responsible for the
140
+ * list being a valid conversation (every tool call answered).
141
+ */
142
+ readonly inheritedMessages?: readonly import('../message/index.js').Message[]
143
+
135
144
  workingDirectory: string
136
145
 
137
146
  runtimeContext?: AgentRuntimeContext
@@ -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
  }