@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
@@ -150,6 +150,22 @@ export const CompactionConfigSchema = z.object({
150
150
  keepFirstEntries: z.number().min(0).default(3),
151
151
  llmVerification: z.boolean().default(true),
152
152
  llmVerificationMaxTokens: z.number().int().positive().default(2048),
153
+ /**
154
+ * Let the model the host names for it write the compaction summary.
155
+ * Default `true`, and it takes effect only where the host supplies a
156
+ * model; without one, or when its call fails, the summary is the extracted
157
+ * working state. Left optional so an existing config literal still
158
+ * type-checks.
159
+ */
160
+ modelSummary: z.boolean().optional(),
161
+ /** Longest model-written summary, in tokens. Default 4,096. */
162
+ modelSummaryMaxTokens: z.number().int().positive().optional(),
163
+ /** Characters of transcript the summary model reads; the newest and the first request are kept when it is longer. Default 240,000. */
164
+ modelSummaryInputChars: z.number().int().positive().optional(),
165
+ /** Characters of one tool result the summary model reads. Default 1,500. */
166
+ modelSummaryToolResultChars: z.number().int().positive().optional(),
167
+ /** Longest the whole summary call may take, in milliseconds, however steadily it streams. Past it the extracted summary is used. Default 120,000. */
168
+ modelSummaryTimeoutMs: z.number().int().positive().optional(),
153
169
  richStateThreshold: z.number().int().positive().default(15),
154
170
  convoTextBudget: z.number().int().positive().default(12_000),
155
171
  maxSentencesPerTurn: z.number().int().positive().default(5),
package/src/index.ts CHANGED
@@ -47,6 +47,8 @@ export {
47
47
  } from './runtime/jobs/job-id.js'
48
48
  export type {
49
49
  BackgroundJob,
50
+ ForegroundCommand,
51
+ ForegroundHandoffReason,
50
52
  BackgroundJobOutput,
51
53
  BackgroundJobRegistryConfig,
52
54
  BackgroundJobStatus,
@@ -14,7 +14,13 @@
14
14
  * The command runs with `sh -c` in `cwd`. It receives one JSON object on
15
15
  * stdin — `event`, `cwd`, `session_id`, `turn_id` inside a turn, and for
16
16
  * tool events `tool_name`, `tool_input` and (after the call) `tool_result`;
17
- * `subagent_stop` adds `parent_session_id` and `parent_turn_id` — and the
17
+ * `subagent_stop` adds `parent_session_id` and `parent_turn_id`. Each fact a
18
+ * script written for another host would look for under a different name is
19
+ * also sent under that name: `hook_event_name` (the PascalCase spelling,
20
+ * `PreToolUse`, `Stop`, ...), `tool_response` (the same object as
21
+ * `tool_result`), and, when the host knows them, `transcript_path` and
22
+ * `permission_mode`. The Namzu names stay, so no existing script changes.
23
+ * The same facts go out as
18
24
  * same facts as `NAMZU_HOOK_EVENT`, `NAMZU_SESSION_ID`, `NAMZU_TURN_ID`,
19
25
  * `NAMZU_TOOL_NAME` and, when the input names a file, `NAMZU_TOOL_PATH`.
20
26
  *
@@ -305,6 +311,10 @@ export interface ShellHookInput {
305
311
  readonly notification?: PluginNotificationInfo
306
312
  /** On `turn_end` and `subagent_stop`: an earlier answer already kept this turn going. */
307
313
  readonly stopHookActive?: boolean
314
+ /** The session's transcript file, when the host has one. Sent as `transcript_path`. */
315
+ readonly transcriptPath?: string
316
+ /** How tool calls are being decided right now. Sent as `permission_mode`. */
317
+ readonly permissionMode?: string
308
318
  }
309
319
 
310
320
  /** Run one hook command to completion, bounded by its deadline and capture. */
@@ -319,8 +329,11 @@ export function runShellHook(
319
329
  )
320
330
  const payload = JSON.stringify({
321
331
  event: input.event,
332
+ hook_event_name: PASCAL_EVENT_NAMES[input.event] ?? input.event,
322
333
  cwd: input.cwd,
323
334
  session_id: input.sessionId,
335
+ ...(input.transcriptPath !== undefined ? { transcript_path: input.transcriptPath } : {}),
336
+ ...(input.permissionMode !== undefined ? { permission_mode: input.permissionMode } : {}),
324
337
  ...(input.turnId !== undefined ? { turn_id: input.turnId } : {}),
325
338
  ...(input.parentSessionId !== undefined ? { parent_session_id: input.parentSessionId } : {}),
326
339
  ...(input.parentTurnId !== undefined ? { parent_turn_id: input.parentTurnId } : {}),
@@ -328,7 +341,9 @@ export function runShellHook(
328
341
  ...(input.compaction !== undefined ? { compaction: input.compaction } : {}),
329
342
  ...(input.toolName !== undefined ? { tool_name: input.toolName } : {}),
330
343
  ...(input.toolInput !== undefined ? { tool_input: input.toolInput } : {}),
331
- ...(input.toolResult !== undefined ? { tool_result: input.toolResult } : {}),
344
+ ...(input.toolResult !== undefined
345
+ ? { tool_result: input.toolResult, tool_response: input.toolResult }
346
+ : {}),
332
347
  ...(input.event === 'post_tool_use_failure' && input.toolResult?.error !== undefined
333
348
  ? { error: input.toolResult.error }
334
349
  : {}),
@@ -589,11 +604,12 @@ function readShellHookJson(event: ShellHookEvent, stdout: string): ShellHookRead
589
604
  reading.permission = { decision, ...(why ? { reason: why } : {}) }
590
605
  } else if (decision !== 'defer') {
591
606
  return {
592
- invalid: '`permissionDecision` must be "allow", "deny" or "ask"',
607
+ invalid: '`permissionDecision` must be "allow", "deny", "ask" or "defer"',
593
608
  }
594
609
  }
595
- // "defer" hands the decision to a separate permission service the kernel
596
- // does not have: nothing is decided, and the ordinary rules apply.
610
+ // "defer" is accepted and decides nothing: a script that hands a call
611
+ // back to a separate permission service gets the ordinary rules, the
612
+ // same as a hook that said nothing.
597
613
  }
598
614
  if (specific.updatedInput !== undefined) {
599
615
  if (!isRecord(specific.updatedInput)) return { invalid: '`updatedInput` must be an object' }
@@ -802,6 +818,14 @@ export interface ShellHookOptions {
802
818
  /** Where the command runs and what `cwd` says on stdin. */
803
819
  readonly cwd: string
804
820
  readonly log?: Logger
821
+ /**
822
+ * The transcript file of a session, sent as `transcript_path`. Called at
823
+ * each run, because a host's session can change under one manager. Absent
824
+ * or returning `undefined`, the field is left out.
825
+ */
826
+ readonly transcriptPath?: (sessionId: string) => string | undefined
827
+ /** The permission mode in force now, sent as `permission_mode`. Called at each run. */
828
+ readonly permissionMode?: () => string | undefined
805
829
  }
806
830
 
807
831
  /** One entry as a hook definition the lifecycle manager accepts. */
@@ -828,6 +852,8 @@ export function createShellHook(
828
852
  return { action: 'continue' }
829
853
  }
830
854
  if (entry.once === true) spent = true
855
+ const transcriptPath = options.transcriptPath?.(String(context.sessionId))
856
+ const permissionMode = options.permissionMode?.()
831
857
  const outcome = await runShellHook(
832
858
  entry,
833
859
  {
@@ -858,6 +884,8 @@ export function createShellHook(
858
884
  : {}),
859
885
  ...(context.notification !== undefined ? { notification: context.notification } : {}),
860
886
  ...(context.stopHookActive === true ? { stopHookActive: true } : {}),
887
+ ...(transcriptPath !== undefined ? { transcriptPath } : {}),
888
+ ...(permissionMode !== undefined ? { permissionMode } : {}),
861
889
  },
862
890
  context.signal,
863
891
  )
@@ -70,7 +70,7 @@ export const CODING_AGENT_WORKING_DOCTRINE = `## How you work
70
70
  - Never change git configuration or other persistent settings without asking: not \`user.name\` or \`user.email\`, hooks, remotes, credential helpers, or any \`git config\` (local or global), nor a shell profile or a tool's own config file. If a commit fails because no identity is set, stop and tell the user the command to set one; do not invent one.
71
71
 
72
72
  ### Keeping the user informed
73
- - Before a batch of tool calls, say in one short line what you are about to do and why. When you have been working for a while without saying anything, say in a few words where you are, then continue. One line, not a paragraph; the tool rows on screen already show the details.
73
+ - Before your first tool call, say in one short line what you are about to do and why. While you work, add a one-line update when you find something, change direction or hit a blocker, and after a long stretch without one, say in a few words where you are, then continue. Brief is good and silence is not; the tool rows on screen already show the details.
74
74
  - Keep the final reply short and specific: what changed, what was verified, what is left.`
75
75
 
76
76
  /**
@@ -78,8 +78,10 @@ export const CODING_AGENT_WORKING_DOCTRINE = `## How you work
78
78
  * delegated sub-agent does not have. See the module comment for the split.
79
79
  */
80
80
  export const CODING_AGENT_DELEGATION_DOCTRINE = `### Planning and delegating
81
- - For work that is multi-step — several files, several distinct stages, anything you would write a checklist for — open a task list with \`task_create\` and keep it current with \`task_update\`, marking each step done when it is done rather than at the end. Skip it for a single inspect-edit-test cycle: the list is for the user to follow, and a list of one item tells them nothing.
82
- - When the \`Agent\` tool is available, delegate independent work through it and give each delegation a short, specific description; the user watches those descriptions, not the prompts. Brief each sub-agent with four things: the objective; what is out of bounds; which tools and sources to use; and the shape of the reply you want, such as a short summary with a file path for anything long. It starts with none of your context and returns what you ask for.
81
+ - For work of three or more steps — several files, several distinct stages, anything you would write a checklist for — open a task list with \`task_create\` and keep it current with \`task_update\`, marking each step done when it is done rather than at the end. Skip it for a single inspect-edit-test cycle or a two-step job: the list is for the user to follow, and a list of one or two items tells them nothing.
82
+ - When the \`Agent\` tool is available, delegate independent work through it and give each delegation a short, specific description; the user watches those descriptions, not the prompts. Brief each sub-agent with four things: the objective; what is out of bounds; which tools and sources to use; and the shape of the reply you want, such as a short summary with a file path for anything long. A fresh sub-agent starts with none of your context and returns what you ask for.
83
+ - When the \`Agent\` tool lists a \`fork\` type, use \`subagent_type: "fork"\` when a side task would fill your context with output you will not need again, such as an open-ended survey. A fork starts from this conversation under your own instructions, so its prompt is only the directive: what to do and where its scope ends, without the background. Never write or guess what a fork will find before its report arrives. A fork cannot start forks of its own.
84
+ - When delegated agents finish, tell the user what each one found in your next message. Their results reach you and not the user, so a result you do not pass on stays unseen.
83
85
  - Delegate lookups — where something is defined, which files reference it, how a module works — with \`subagent_type: "explore"\`: it has reading and searching tools only and never interrupts the user for permission. Reserve the default sub-agent for work that changes files or runs commands.`
84
86
 
85
87
  /**
@@ -31,6 +31,11 @@
31
31
 
32
32
  import { LOG_SECRET_PATTERNS } from '../constants/secret-patterns.js'
33
33
  import type { ProviderErrorKind, ProviderRequestErrorInit } from '../types/provider/error.js'
34
+ import {
35
+ type TransportCause,
36
+ readCertificateFailure,
37
+ readTransportCause,
38
+ } from '../types/provider/errors.js'
34
39
  export type {
35
40
  ProviderErrorInfo,
36
41
  ProviderErrorKind,
@@ -78,6 +83,8 @@ export class ProviderRequestError extends Error {
78
83
  public readonly detail?: string
79
84
  /** The provider's `x-should-retry` verdict, when it sent one; see {@link ProviderRequestErrorInit.shouldRetry}. */
80
85
  public readonly shouldRetry?: boolean
86
+ /** The part of the network path the request tripped on, when known; see {@link ProviderRequestErrorInit.transport}. */
87
+ public readonly transport?: TransportCause
81
88
 
82
89
  constructor(init: ProviderRequestErrorInit) {
83
90
  super(buildProviderErrorMessage(init))
@@ -90,6 +97,7 @@ export class ProviderRequestError extends Error {
90
97
  if (init.retryAfterMs !== undefined) this.retryAfterMs = init.retryAfterMs
91
98
  if (init.detail !== undefined) this.detail = init.detail
92
99
  if (init.shouldRetry !== undefined) this.shouldRetry = init.shouldRetry
100
+ if (init.transport !== undefined) this.transport = init.transport
93
101
  }
94
102
  }
95
103
 
@@ -520,8 +528,14 @@ export function providerVendorError(input: {
520
528
  const name = error instanceof Error ? error.name : ''
521
529
  const message = error instanceof Error ? error.message : ''
522
530
 
531
+ // A proxy that refuses the tunnel or a certificate nobody trusts has no status
532
+ // to read, and the same request meets the same obstacle again.
533
+ const transport = readTransportCause(error)
534
+
523
535
  let kind: ProviderErrorKind
524
- if (/ThrottlingException|TooManyRequests|ServiceQuotaExceeded/i.test(name)) {
536
+ if (transport !== undefined) {
537
+ kind = 'network'
538
+ } else if (/ThrottlingException|TooManyRequests|ServiceQuotaExceeded/i.test(name)) {
525
539
  kind = 'throttle'
526
540
  } else if (/AccessDenied|Unauthorized|Forbidden|Authentication/i.test(name)) {
527
541
  kind = 'auth'
@@ -550,7 +564,12 @@ export function providerVendorError(input: {
550
564
  input.retryAfter ?? vendorRetryAfter(error),
551
565
  input.now ?? Date.now(),
552
566
  )
553
- const shouldRetry = parseShouldRetry(vendorHeader(error, ['x-should-retry']))
567
+ // A certificate that fails verification is refused the same way every time.
568
+ const certificate = status === undefined ? readCertificateFailure(error) : undefined
569
+ const shouldRetry =
570
+ transport !== undefined || certificate !== undefined
571
+ ? false
572
+ : parseShouldRetry(vendorHeader(error, ['x-should-retry']))
554
573
  const fields = error as { type?: unknown; param?: unknown; requestID?: unknown } | null
555
574
  // A message that already carries its body (Anthropic's) has the fields; one that is only the
556
575
  // parsed sentence (OpenAI's) gets them from the SDK error's own properties.
@@ -566,11 +585,16 @@ export function providerVendorError(input: {
566
585
  return new ProviderRequestError({
567
586
  kind,
568
587
  providerId: input.providerId,
569
- ...(exactProviderCode !== undefined ? { providerCode: exactProviderCode } : {}),
588
+ ...(certificate !== undefined
589
+ ? { providerCode: certificate }
590
+ : exactProviderCode !== undefined
591
+ ? { providerCode: exactProviderCode }
592
+ : {}),
570
593
  ...(status !== undefined ? { status } : {}),
571
594
  ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
572
595
  ...(detail !== undefined ? { detail } : {}),
573
596
  ...(shouldRetry !== undefined ? { shouldRetry } : {}),
597
+ ...(transport !== undefined ? { transport } : {}),
574
598
  })
575
599
  }
576
600
 
@@ -225,6 +225,24 @@ function isOutputChunk(chunk: StreamChunk): boolean {
225
225
  )
226
226
  }
227
227
 
228
+ /**
229
+ * Output the consumer can take back: reasoning, or the pieces of a tool call
230
+ * that is still being written (nothing runs before the response settles).
231
+ * A finished call, text, a citation, a hosted tool or a finish reason is real.
232
+ */
233
+ function isVoidable(chunk: StreamChunk): boolean {
234
+ const { content, toolCallEnd, citation, hostedTool, textPart } = chunk.delta
235
+ return !(
236
+ content ||
237
+ toolCallEnd ||
238
+ citation !== undefined ||
239
+ hostedTool !== undefined ||
240
+ textPart !== undefined ||
241
+ chunk.textParts !== undefined ||
242
+ chunk.finishReason !== undefined
243
+ )
244
+ }
245
+
228
246
  /** Read one member's canonical menu, adapting the one-release legacy member. */
229
247
  function memberReasoningEffortLevels(
230
248
  member: ProviderChainMember,
@@ -405,9 +423,20 @@ export function withProviderFallback(
405
423
  assertNativeStructuredOutputSupported(member.provider, request)
406
424
  assertHostedWebSearchSupported(member.provider, request)
407
425
  let produced = false
426
+ let fragments = false
408
427
  try {
409
428
  for await (const chunk of member.provider.chatStream(request)) {
410
- if (isOutputChunk(chunk)) produced = true
429
+ // The retry notice that restarts a request voids what the failed
430
+ // attempt wrote, and the consumer drops it: nothing real was produced.
431
+ if (chunk.retry?.restart) {
432
+ produced = false
433
+ fragments = false
434
+ } else if (isOutputChunk(chunk)) {
435
+ // Pieces the consumer can drop on a notice (a call still being
436
+ // written, reasoning) are not output a swap would duplicate.
437
+ if (isVoidable(chunk)) fragments = true
438
+ else produced = true
439
+ }
411
440
  yield chunk
412
441
  }
413
442
  return
@@ -457,13 +486,16 @@ export function withProviderFallback(
457
486
  fallback: {
458
487
  fromIndex: cursor - 1,
459
488
  fromProviderId: member.provider.id,
460
- ...(member.model !== undefined ? { fromModel: member.model } : {}),
489
+ // The model this member was asked for, which for the head of the chain
490
+ // (declared without one) is the request's own.
491
+ ...(targetModel ? { fromModel: targetModel } : {}),
461
492
  toIndex: cursor,
462
493
  toProviderId: to.provider.id,
463
494
  ...(to.model !== undefined ? { toModel: to.model } : {}),
464
495
  code: classified.code,
465
496
  ...(classified.status !== undefined ? { status: classified.status } : {}),
466
497
  reason: classified.message,
498
+ ...(fragments ? { restart: true } : {}),
467
499
  },
468
500
  }
469
501
  }
@@ -60,8 +60,13 @@ export interface ProviderRetryConfig extends BackoffPolicy {
60
60
  * Off by default, because the decorator cannot un-emit: the consumer has
61
61
  * already seen those deltas. When on, the retry notice carries
62
62
  * `restart: true` and the consumer must discard what the failed attempt
63
- * produced; the runtime does this itself. A stream that has begun a tool
64
- * call is never restarted, because the call may already be underway.
63
+ * produced; the runtime does this itself.
64
+ *
65
+ * A stream that has so far produced only tool-call fragments (no text) is
66
+ * restarted whatever this says: nothing runs before the response settles,
67
+ * and the half-written call is discarded on the same notice. A stream in
68
+ * which a call finished, or the provider ran a hosted tool, is never
69
+ * restarted, because that work is real.
65
70
  */
66
71
  readonly retryMidStream?: boolean
67
72
  }
@@ -113,9 +118,11 @@ function isReasoningOnly(chunk: StreamChunk): boolean {
113
118
  * blocks by index, and clears them on a restart). Once a text or tool-call delta has been
114
119
  * yielded the consumer has already emitted `text_delta` events and
115
120
  * appended to its buffer, so restarting the request would duplicate
116
- * output. A mid-stream failure is therefore surfaced, not retried — the
117
- * existing stream-error path in `streamProviderTurn` handles it (and can
118
- * still salvage a truncated tool call). This is the whole reason the
121
+ * output. A mid-stream failure after text is therefore surfaced, not retried,
122
+ * unless `retryMidStream` — the existing stream-error path in
123
+ * `streamProviderTurn` handles it (and can still salvage a finished call).
124
+ * A stream that has only begun tool calls has shown no text, so it is retried
125
+ * too, the half-written call dropped. This is the whole reason the
119
126
  * retry lives in a decorator rather than inside the loop: the loop cannot
120
127
  * un-emit.
121
128
  *
@@ -158,8 +165,13 @@ export function withProviderRetry(
158
165
  // this is retryable without `retryMidStream`; the notice says
159
166
  // `restart` so a host that showed the thinking drops it.
160
167
  let reasoned = false
161
- // A tool call or hosted tool began: past this point a restart could
162
- // repeat work that is already underway, so it is never offered.
168
+ // Text, a citation or a part reached the consumer: a pipe cannot take
169
+ // that back, so a restart needs `retryMidStream`.
170
+ let printed = false
171
+ // A call finished (its arguments are whole) or the provider ran a hosted
172
+ // tool: that work is real, so a restart is never offered. A call still
173
+ // being written is not work: nothing runs before the response settles,
174
+ // and the consumer drops the half-written call on `restart`.
163
175
  let acted = false
164
176
  try {
165
177
  for await (const chunk of provider.chatStream(params)) {
@@ -173,9 +185,16 @@ export function withProviderRetry(
173
185
  if (isReasoningOnly(chunk)) reasoned = true
174
186
  else produced = true
175
187
  }
176
- if (chunk.delta.toolCalls?.length || chunk.delta.toolCallEnd || chunk.delta.hostedTool) {
177
- acted = true
188
+ if (
189
+ chunk.delta.content ||
190
+ chunk.delta.textPart !== undefined ||
191
+ chunk.textParts !== undefined ||
192
+ chunk.delta.citation !== undefined
193
+ ) {
194
+ printed = true
178
195
  }
196
+ // A reported finish means the response was whole.
197
+ if (chunk.delta.toolCallEnd || chunk.delta.hostedTool || chunk.finishReason) acted = true
179
198
 
180
199
  if (isErrorOnly && !produced && attempt < config.maxRetries) {
181
200
  throw new Error(chunk.error)
@@ -203,7 +222,8 @@ export function withProviderRetry(
203
222
  const exhausted = attempt >= config.maxRetries
204
223
 
205
224
  const restartable =
206
- (produced && config.retryMidStream === true && !acted) || (!produced && reasoned)
225
+ (produced && !acted && (config.retryMidStream === true || !printed)) ||
226
+ (!produced && reasoned)
207
227
  if ((produced && !restartable) || !classified.retryable || exhausted) {
208
228
  log?.warn('Provider call failed', {
209
229
  [GENAI.SYSTEM]: provider.id,
@@ -543,6 +543,9 @@ export {
543
543
  EXPLORE_AGENT_ID,
544
544
  EXPLORE_AGENT_PROMPT,
545
545
  MAX_AGENT_FILE_CHARS,
546
+ PLAN_AGENT_DESCRIPTION,
547
+ PLAN_AGENT_ID,
548
+ PLAN_AGENT_PROMPT,
546
549
  parseAgentFile,
547
550
  parseAgentMarkdown,
548
551
  } from './agents/index.js'
@@ -782,6 +785,10 @@ export type {
782
785
  WithProviderFallbackOptions,
783
786
  WithProviderRetryOptions,
784
787
  } from './provider/index.js'
788
+ // Which part of the network path a failed request tripped on (a proxy that
789
+ // wants a sign-in, an untrusted certificate), read off the failure's causes.
790
+ export { readTransportCause } from './types/provider/errors.js'
791
+ export type { TransportCause } from './types/provider/errors.js'
785
792
  // The curve `ProviderRetryConfig` extends and `query({ toolRetryBackoff })`
786
793
  // takes a partial of. Both public surfaces name it, so a consumer has to be
787
794
  // able to name it too — a type reachable only through an inline `import(...)`
@@ -1316,6 +1323,10 @@ export { isWorkingMemoryMessage } from './runtime/query/iteration/phases/working
1316
1323
  // `any` — and the first host to try (`@namzu/cli`'s `/compact`) did exactly
1317
1324
  // that before this line existed.
1318
1325
  export type { CompactNowInput, CompactionResult } from './compaction/manual.js'
1326
+ export type {
1327
+ CompactionSummaryRoute,
1328
+ CompactionSummaryRouter,
1329
+ } from './compaction/model-summary.js'
1319
1330
 
1320
1331
  export {
1321
1332
  buildVerifiedSummary,
@@ -1340,6 +1351,7 @@ export {
1340
1351
  WorkingStateManager,
1341
1352
  } from './compaction/index.js'
1342
1353
  export type {
1354
+ ToolHistoryRepairOptions,
1343
1355
  ToolHistoryRepairReport,
1344
1356
  ToolHistoryRepairResult,
1345
1357
  } from './compaction/index.js'
@@ -353,6 +353,9 @@ export type {
353
353
  ToolResultSlot,
354
354
  WorkingState,
355
355
  PinSlot,
356
+ RestorableFile,
357
+ RestorableSkill,
358
+ RestorableState,
356
359
  WorkingStatePin,
357
360
  } from './compaction/index.js'
358
361
  export type * from './eval/index.js'
@@ -358,12 +358,111 @@ function groupAlive(pid: number | undefined): boolean {
358
358
 
359
359
  const GROUP_POLL_MS = 250
360
360
 
361
+ /**
362
+ * Why a command that was running in the foreground moved to the background:
363
+ * the person asked (`user`), or the person sent a message that has to reach
364
+ * the model while the command is still running (`message`).
365
+ */
366
+ export type ForegroundHandoffReason = 'user' | 'message'
367
+
368
+ /** A foreground command that can be moved to the background right now. */
369
+ export interface ForegroundCommand {
370
+ readonly id: string
371
+ readonly owner: string
372
+ readonly command: string
373
+ readonly startedAt: number
374
+ }
375
+
376
+ interface ForegroundEntry extends ForegroundCommand {
377
+ readonly background: (reason: ForegroundHandoffReason) => boolean
378
+ }
379
+
361
380
  export class BackgroundJobRegistry {
362
381
  private readonly jobs = new Map<string, JobEntry>()
363
382
  private readonly exitListeners = new Set<(job: BackgroundJob) => void>()
364
383
  private readonly monitorListeners = new Set<(event: MonitorEvent) => void>()
365
384
  private outputWaiters = 0
366
385
  private readonly outputWaitersByOwner = new Map<string, number>()
386
+ private readonly foreground = new Map<string, ForegroundEntry>()
387
+ private readonly foregroundListeners = new Set<(owner: string) => void>()
388
+ private foregroundSeq = 0
389
+
390
+ /**
391
+ * A foreground command a tool is waiting on, offered for hand-over: while it
392
+ * is registered, {@link backgroundForeground} can move it to a job. The tool
393
+ * registers once the command has run long enough that moving it is worth
394
+ * offering, and unregisters (the returned function) when the call settles.
395
+ * `background` returns whether the command was taken over.
396
+ */
397
+ registerForeground(
398
+ owner: string,
399
+ entry: {
400
+ readonly command: string
401
+ readonly background: (reason: ForegroundHandoffReason) => boolean
402
+ },
403
+ ): () => void {
404
+ const id = `fg-${++this.foregroundSeq}`
405
+ this.foreground.set(id, {
406
+ id,
407
+ owner,
408
+ command: entry.command,
409
+ startedAt: Date.now(),
410
+ background: entry.background,
411
+ })
412
+ this.announceForeground(owner)
413
+ return () => {
414
+ if (this.foreground.delete(id)) this.announceForeground(owner)
415
+ }
416
+ }
417
+
418
+ /** Foreground commands of one owner that can be moved to the background, oldest first. */
419
+ foregroundCommands(owner: string): readonly ForegroundCommand[] {
420
+ return [...this.foreground.values()]
421
+ .filter((entry) => entry.owner === owner)
422
+ .map(({ id, command, startedAt }) => ({ id, owner, command, startedAt }))
423
+ }
424
+
425
+ /**
426
+ * Move every foreground command of `owner` to the background. Returns how
427
+ * many were taken over; a command that ended or could not be taken over
428
+ * keeps its own outcome and is not counted.
429
+ */
430
+ backgroundForeground(owner: string, reason: ForegroundHandoffReason): number {
431
+ let moved = 0
432
+ for (const entry of [...this.foreground.values()]) {
433
+ if (entry.owner !== owner) continue
434
+ let taken = false
435
+ try {
436
+ taken = entry.background(reason)
437
+ } catch {
438
+ // A command that cannot be handed over keeps running where it is.
439
+ }
440
+ if (taken) {
441
+ moved += 1
442
+ this.foreground.delete(entry.id)
443
+ }
444
+ }
445
+ if (moved > 0) this.announceForeground(owner)
446
+ return moved
447
+ }
448
+
449
+ /** Be told when the set of hand-over-able foreground commands of any owner changes. */
450
+ onForegroundChange(listener: (owner: string) => void): () => void {
451
+ this.foregroundListeners.add(listener)
452
+ return () => {
453
+ this.foregroundListeners.delete(listener)
454
+ }
455
+ }
456
+
457
+ private announceForeground(owner: string): void {
458
+ for (const listener of this.foregroundListeners) {
459
+ try {
460
+ listener(owner)
461
+ } catch {
462
+ // A listener that throws is its owner's problem.
463
+ }
464
+ }
465
+ }
367
466
 
368
467
  /**
369
468
  * Be told when a job ends, whoever owns it. A job outlives the call that
@@ -1257,6 +1356,12 @@ export function bindOwner(
1257
1356
  hostOutput: true,
1258
1357
  ...(params.initialOutput ? { initialOutput: params.initialOutput } : {}),
1259
1358
  }),
1359
+ // The owner is bound here, so a tool can offer its own waiting command and
1360
+ // no other turn's.
1361
+ registerForeground: (entry: {
1362
+ command: string
1363
+ background: (reason: ForegroundHandoffReason) => boolean
1364
+ }) => registry.registerForeground(owner, entry),
1260
1365
  get: (id: string) => mine(id),
1261
1366
  awaitStarted: (id: string) => {
1262
1367
  mine(id)
@@ -0,0 +1,37 @@
1
+ import type { SessionLog } from '../../store/session-log/index.js'
2
+ import type { SessionRecord } from '../../types/session/records.js'
3
+
4
+ /**
5
+ * Ids of the tool calls the log proves never started.
6
+ *
7
+ * A call that started has a `tool_executing` record, so one with no result is
8
+ * of unknown outcome. A call of a turn that journals its calls (it has a
9
+ * `tool_executing` or a `tool_review_requested` record) and has none of its
10
+ * own ran nothing: it was queued or waiting for approval when the process
11
+ * ended. A turn with no such record proves nothing, so its calls are left out.
12
+ */
13
+ export async function readCallsThatNeverStarted(log: SessionLog): Promise<ReadonlySet<string>> {
14
+ const started = new Set<string>()
15
+ const journaled = new Set<string>()
16
+ const callsOfTurn = new Map<string, string[]>()
17
+ for await (const entry of log.read()) {
18
+ const record = entry.record as SessionRecord
19
+ if (record.type === 'tool_review_requested') {
20
+ journaled.add(record.turnId)
21
+ } else if (record.type === 'tool_executing') {
22
+ journaled.add(record.turnId)
23
+ started.add(record.toolUseId)
24
+ } else if (record.type === 'message') {
25
+ const content = record.content as { role?: string; toolCalls?: readonly { id: string }[] }
26
+ if (content?.role !== 'assistant' || !content.toolCalls) continue
27
+ const ids = callsOfTurn.get(record.turnId) ?? []
28
+ for (const call of content.toolCalls) ids.push(call.id)
29
+ callsOfTurn.set(record.turnId, ids)
30
+ }
31
+ }
32
+ const never = new Set<string>()
33
+ for (const turnId of journaled) {
34
+ for (const id of callsOfTurn.get(turnId) ?? []) if (!started.has(id)) never.add(id)
35
+ }
36
+ return never
37
+ }
@@ -717,8 +717,8 @@ function ceilingBelow(length: number): number | undefined {
717
717
 
718
718
  /**
719
719
  * How much one call should carry, in the model's words, or `undefined` when
720
- * there is no number to give: a stream that ended, to a tool that declares no
721
- * large arguments.
720
+ * there is no number to give: after an output limit that left no figure worth
721
+ * stating, to a tool that declares no large arguments.
722
722
  *
723
723
  * A tool that declares large arguments is given a budget for each of them.
724
724
  * After an output limit, any tool is told to keep its arguments under half of
@@ -741,9 +741,7 @@ function sizeAdvice(
741
741
  // would tell a call cut after three characters to keep `content` under
742
742
  // 12000 of them: more than a budget, an invitation back into the same
743
743
  // cutoff, and a broken invariant (a call is always told less than what
744
- // arrived). Only a stream that ended for a reason other than the output
745
- // limit — where no ceiling applies at all — still gets the plain
746
- // declared budgets below.
744
+ // arrived).
747
745
  if (lengthCutoff && ceiling === undefined) return undefined
748
746
  const declared = Object.entries(largeStringArguments ?? {}).filter(
749
747
  ([, budget]) => Number.isFinite(budget) && budget > 0,
@@ -831,8 +829,8 @@ function unseenOutput(error: ToolInputError): string | undefined {
831
829
  * call is to carry less, {@link sizeAdvice} and the tool's
832
830
  * `truncatedInputHint`, with nothing about reasoning or what came before
833
831
  * it: the window is the conversation's length, not this response's.
834
- * - Cut off by the stream ending: the declared budgets, if any, or just to
835
- * send the call again, and the tool's `truncatedInputHint`.
832
+ * - Cut off by the connection dropping: no size and no hint. The call did not
833
+ * come near any limit, so it is to be sent again unchanged.
836
834
  * - Stopped by a content filter: no advice. Sending less does not get past a
837
835
  * filter.
838
836
  *
@@ -874,7 +872,7 @@ export function unreadableToolInputMessage(
874
872
  ? 'the response reached its output token limit'
875
873
  : error.finishReason === 'content_filter'
876
874
  ? "the provider's content filter stopped the response"
877
- : 'the response stream ended'
875
+ : 'the connection dropped'
878
876
  parts.push(
879
877
  `Error: The call to "${toolName}" was cut off: ${cause} after ${error.length} characters of its arguments, before they were complete. The tool was NOT executed.`,
880
878
  )
@@ -897,9 +895,14 @@ export function unreadableToolInputMessage(
897
895
  parts.push(
898
896
  `${error.precedingLength} of the ${error.precedingLength + error.length} characters the response streamed came before this call, so send the call again with less before it in the same response.`,
899
897
  )
900
- } else if (error.finishReason !== 'content_filter') {
898
+ } else if (error.finishReason === 'length') {
901
899
  parts.push(sizeAdvice(tool?.largeStringArguments, error) ?? 'Send the call again.')
902
900
  hint(tool?.truncatedInputHint)
901
+ } else if (error.finishReason !== 'content_filter') {
902
+ // The transport ended the response, not a limit: the call may have been
903
+ // well inside any budget, so no size is named and the call is not to
904
+ // change. Sending the same call again is the whole remedy.
905
+ parts.push('Nothing was wrong with the call. Send the same call again, unchanged.')
903
906
  }
904
907
  }
905
908
  return parts.join(' ')