@namzu/sdk 62.6.1 → 62.8.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 (344) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +5 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/examples/SupervisorAgent.js +1 -0
  7. package/dist/agents/examples/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/explore.d.ts.map +1 -1
  9. package/dist/agents/explore.js +2 -1
  10. package/dist/agents/explore.js.map +1 -1
  11. package/dist/compaction/index.d.ts +1 -1
  12. package/dist/compaction/index.d.ts.map +1 -1
  13. package/dist/compaction/index.js +1 -1
  14. package/dist/compaction/index.js.map +1 -1
  15. package/dist/compaction/model-summary.js +2 -2
  16. package/dist/compaction/model-summary.js.map +1 -1
  17. package/dist/compaction/summary.d.ts +16 -1
  18. package/dist/compaction/summary.d.ts.map +1 -1
  19. package/dist/compaction/summary.js +25 -2
  20. package/dist/compaction/summary.js.map +1 -1
  21. package/dist/config/runtime.d.ts +8 -0
  22. package/dist/config/runtime.d.ts.map +1 -1
  23. package/dist/config/runtime.js +15 -0
  24. package/dist/config/runtime.js.map +1 -1
  25. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  26. package/dist/execution/workflow/runtime.js +3 -0
  27. package/dist/execution/workflow/runtime.js.map +1 -1
  28. package/dist/execution/workflow/types.d.ts +8 -0
  29. package/dist/execution/workflow/types.d.ts.map +1 -1
  30. package/dist/manager/agent/child-session.d.ts +12 -0
  31. package/dist/manager/agent/child-session.d.ts.map +1 -1
  32. package/dist/manager/agent/child-session.js +29 -0
  33. package/dist/manager/agent/child-session.js.map +1 -1
  34. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  35. package/dist/manager/agent/lifecycle.js +8 -0
  36. package/dist/manager/agent/lifecycle.js.map +1 -1
  37. package/dist/manager/session/turn-recorder.d.ts +13 -2
  38. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  39. package/dist/manager/session/turn-recorder.js +107 -29
  40. package/dist/manager/session/turn-recorder.js.map +1 -1
  41. package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
  42. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  43. package/dist/prompt/coding-agent-doctrine.js +5 -3
  44. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  45. package/dist/provider/fallback.d.ts.map +1 -1
  46. package/dist/provider/fallback.js +5 -1
  47. package/dist/provider/fallback.js.map +1 -1
  48. package/dist/provider/retry.d.ts +25 -0
  49. package/dist/provider/retry.d.ts.map +1 -1
  50. package/dist/provider/retry.js +28 -0
  51. package/dist/provider/retry.js.map +1 -1
  52. package/dist/public-runtime.d.ts +3 -3
  53. package/dist/public-runtime.d.ts.map +1 -1
  54. package/dist/public-runtime.js +3 -3
  55. package/dist/public-runtime.js.map +1 -1
  56. package/dist/public-tools.d.ts +4 -2
  57. package/dist/public-tools.d.ts.map +1 -1
  58. package/dist/public-tools.js +5 -2
  59. package/dist/public-tools.js.map +1 -1
  60. package/dist/runtime/query/auto-review/classifier.d.ts +7 -1
  61. package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -1
  62. package/dist/runtime/query/auto-review/classifier.js +8 -4
  63. package/dist/runtime/query/auto-review/classifier.js.map +1 -1
  64. package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -1
  65. package/dist/runtime/query/auto-review/prompt.js +2 -0
  66. package/dist/runtime/query/auto-review/prompt.js.map +1 -1
  67. package/dist/runtime/query/auto-review/script-evidence.d.ts +93 -0
  68. package/dist/runtime/query/auto-review/script-evidence.d.ts.map +1 -0
  69. package/dist/runtime/query/auto-review/script-evidence.js +566 -0
  70. package/dist/runtime/query/auto-review/script-evidence.js.map +1 -0
  71. package/dist/runtime/query/checkpoint.d.ts +13 -0
  72. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  73. package/dist/runtime/query/checkpoint.js +37 -0
  74. package/dist/runtime/query/checkpoint.js.map +1 -1
  75. package/dist/runtime/query/context.d.ts.map +1 -1
  76. package/dist/runtime/query/context.js +28 -2
  77. package/dist/runtime/query/context.js.map +1 -1
  78. package/dist/runtime/query/executor.d.ts +38 -1
  79. package/dist/runtime/query/executor.d.ts.map +1 -1
  80. package/dist/runtime/query/executor.js +154 -13
  81. package/dist/runtime/query/executor.js.map +1 -1
  82. package/dist/runtime/query/finalize-turn.d.ts +7 -1
  83. package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
  84. package/dist/runtime/query/finalize-turn.js +2 -0
  85. package/dist/runtime/query/finalize-turn.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +7 -0
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts +32 -1
  91. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/index.js +158 -10
  93. package/dist/runtime/query/iteration/index.js.map +1 -1
  94. package/dist/runtime/query/iteration/phases/compaction.d.ts +12 -0
  95. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  96. package/dist/runtime/query/iteration/phases/compaction.js +48 -1
  97. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  98. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  99. package/dist/runtime/query/iteration/phases/context.js +5 -0
  100. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  101. package/dist/runtime/query/iteration/stream-turn.js +3 -3
  102. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  103. package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
  104. package/dist/runtime/query/prelude-lease.js +22 -0
  105. package/dist/runtime/query/prelude-lease.js.map +1 -1
  106. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  107. package/dist/runtime/query/prepare-turn.js +30 -12
  108. package/dist/runtime/query/prepare-turn.js.map +1 -1
  109. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  110. package/dist/runtime/query/review-policy.js +29 -0
  111. package/dist/runtime/query/review-policy.js.map +1 -1
  112. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  113. package/dist/runtime/query/session-storage.js +44 -3
  114. package/dist/runtime/query/session-storage.js.map +1 -1
  115. package/dist/runtime/query/tooling.d.ts +2 -0
  116. package/dist/runtime/query/tooling.d.ts.map +1 -1
  117. package/dist/runtime/query/tooling.js +1 -0
  118. package/dist/runtime/query/tooling.js.map +1 -1
  119. package/dist/runtime/query/turn/completion-audit.d.ts +27 -1
  120. package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -1
  121. package/dist/runtime/query/turn/completion-audit.js +71 -6
  122. package/dist/runtime/query/turn/completion-audit.js.map +1 -1
  123. package/dist/runtime/query/turn/outcome-ledger.d.ts +113 -0
  124. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -0
  125. package/dist/runtime/query/turn/outcome-ledger.js +578 -0
  126. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -0
  127. package/dist/sandbox/file-walk.d.ts +7 -1
  128. package/dist/sandbox/file-walk.d.ts.map +1 -1
  129. package/dist/sandbox/file-walk.js +30 -4
  130. package/dist/sandbox/file-walk.js.map +1 -1
  131. package/dist/sandbox/glob-segment.d.ts +8 -1
  132. package/dist/sandbox/glob-segment.d.ts.map +1 -1
  133. package/dist/sandbox/glob-segment.js +28 -9
  134. package/dist/sandbox/glob-segment.js.map +1 -1
  135. package/dist/store/checkpoint/contract.d.ts +12 -0
  136. package/dist/store/checkpoint/contract.d.ts.map +1 -1
  137. package/dist/store/checkpoint/contract.js.map +1 -1
  138. package/dist/store/checkpoint/disk.d.ts.map +1 -1
  139. package/dist/store/checkpoint/disk.js +34 -2
  140. package/dist/store/checkpoint/disk.js.map +1 -1
  141. package/dist/store/checkpoint/prune.d.ts.map +1 -1
  142. package/dist/store/checkpoint/prune.js +6 -0
  143. package/dist/store/checkpoint/prune.js.map +1 -1
  144. package/dist/store/evidence/disk.js +40 -3
  145. package/dist/store/evidence/disk.js.map +1 -1
  146. package/dist/store/session-log/incremental.d.ts +23 -0
  147. package/dist/store/session-log/incremental.d.ts.map +1 -0
  148. package/dist/store/session-log/incremental.js +117 -0
  149. package/dist/store/session-log/incremental.js.map +1 -0
  150. package/dist/store/session-log/index.d.ts +1 -0
  151. package/dist/store/session-log/index.d.ts.map +1 -1
  152. package/dist/store/session-log/index.js +1 -0
  153. package/dist/store/session-log/index.js.map +1 -1
  154. package/dist/store/session-log/turn-outcome.d.ts +37 -0
  155. package/dist/store/session-log/turn-outcome.d.ts.map +1 -0
  156. package/dist/store/session-log/turn-outcome.js +77 -0
  157. package/dist/store/session-log/turn-outcome.js.map +1 -0
  158. package/dist/tools/admit-facts.d.ts.map +1 -1
  159. package/dist/tools/admit-facts.js +1 -0
  160. package/dist/tools/admit-facts.js.map +1 -1
  161. package/dist/tools/builtins/bash.d.ts.map +1 -1
  162. package/dist/tools/builtins/bash.js +11 -2
  163. package/dist/tools/builtins/bash.js.map +1 -1
  164. package/dist/tools/builtins/bounded-file-read.d.ts +19 -1
  165. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -1
  166. package/dist/tools/builtins/bounded-file-read.js +56 -3
  167. package/dist/tools/builtins/bounded-file-read.js.map +1 -1
  168. package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
  169. package/dist/tools/builtins/edit-apply.js +24 -3
  170. package/dist/tools/builtins/edit-apply.js.map +1 -1
  171. package/dist/tools/builtins/edit.d.ts +31 -17
  172. package/dist/tools/builtins/edit.d.ts.map +1 -1
  173. package/dist/tools/builtins/edit.js +92 -27
  174. package/dist/tools/builtins/edit.js.map +1 -1
  175. package/dist/tools/builtins/glob.js +1 -1
  176. package/dist/tools/builtins/glob.js.map +1 -1
  177. package/dist/tools/builtins/grep.d.ts +8 -0
  178. package/dist/tools/builtins/grep.d.ts.map +1 -1
  179. package/dist/tools/builtins/grep.js +148 -21
  180. package/dist/tools/builtins/grep.js.map +1 -1
  181. package/dist/tools/builtins/notebook-guard.d.ts +19 -0
  182. package/dist/tools/builtins/notebook-guard.d.ts.map +1 -0
  183. package/dist/tools/builtins/notebook-guard.js +75 -0
  184. package/dist/tools/builtins/notebook-guard.js.map +1 -0
  185. package/dist/tools/builtins/read-render.d.ts.map +1 -1
  186. package/dist/tools/builtins/read-render.js +8 -2
  187. package/dist/tools/builtins/read-render.js.map +1 -1
  188. package/dist/tools/builtins/search-support.d.ts +8 -0
  189. package/dist/tools/builtins/search-support.d.ts.map +1 -1
  190. package/dist/tools/builtins/search-support.js +66 -0
  191. package/dist/tools/builtins/search-support.js.map +1 -1
  192. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  193. package/dist/tools/builtins/write-file.js +14 -1
  194. package/dist/tools/builtins/write-file.js.map +1 -1
  195. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  196. package/dist/tools/coordinator/agent.js +25 -6
  197. package/dist/tools/coordinator/agent.js.map +1 -1
  198. package/dist/tools/coordinator/index.d.ts.map +1 -1
  199. package/dist/tools/coordinator/index.js +6 -1
  200. package/dist/tools/coordinator/index.js.map +1 -1
  201. package/dist/tools/coordinator/outcome-record.d.ts +22 -0
  202. package/dist/tools/coordinator/outcome-record.d.ts.map +1 -0
  203. package/dist/tools/coordinator/outcome-record.js +91 -0
  204. package/dist/tools/coordinator/outcome-record.js.map +1 -0
  205. package/dist/tools/defineTool.d.ts +2 -0
  206. package/dist/tools/defineTool.d.ts.map +1 -1
  207. package/dist/tools/defineTool.js +1 -0
  208. package/dist/tools/defineTool.js.map +1 -1
  209. package/dist/tools/file-read-tracker.d.ts +23 -2
  210. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  211. package/dist/tools/file-read-tracker.js +37 -2
  212. package/dist/tools/file-read-tracker.js.map +1 -1
  213. package/dist/tools/untrusted-envelope.d.ts +6 -0
  214. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  215. package/dist/tools/untrusted-envelope.js +22 -1
  216. package/dist/tools/untrusted-envelope.js.map +1 -1
  217. package/dist/toolsets/manager.d.ts.map +1 -1
  218. package/dist/toolsets/manager.js +3 -4
  219. package/dist/toolsets/manager.js.map +1 -1
  220. package/dist/toolsets/validation-issues.d.ts +27 -0
  221. package/dist/toolsets/validation-issues.d.ts.map +1 -0
  222. package/dist/toolsets/validation-issues.js +83 -0
  223. package/dist/toolsets/validation-issues.js.map +1 -0
  224. package/dist/turn/memory-recall.d.ts +13 -0
  225. package/dist/turn/memory-recall.d.ts.map +1 -1
  226. package/dist/turn/memory-recall.js +18 -0
  227. package/dist/turn/memory-recall.js.map +1 -1
  228. package/dist/types/agent/base.d.ts +19 -0
  229. package/dist/types/agent/base.d.ts.map +1 -1
  230. package/dist/types/agent/query.d.ts +2 -0
  231. package/dist/types/agent/query.d.ts.map +1 -1
  232. package/dist/types/common/index.d.ts +17 -0
  233. package/dist/types/common/index.d.ts.map +1 -1
  234. package/dist/types/common/index.js +8 -1
  235. package/dist/types/common/index.js.map +1 -1
  236. package/dist/types/hitl/index.d.ts +22 -0
  237. package/dist/types/hitl/index.d.ts.map +1 -1
  238. package/dist/types/hitl/index.js.map +1 -1
  239. package/dist/types/session/config.d.ts +20 -0
  240. package/dist/types/session/config.d.ts.map +1 -1
  241. package/dist/types/session/events.d.ts +3 -3
  242. package/dist/types/session/events.d.ts.map +1 -1
  243. package/dist/types/session/index.d.ts +1 -0
  244. package/dist/types/session/index.d.ts.map +1 -1
  245. package/dist/types/session/index.js +1 -0
  246. package/dist/types/session/index.js.map +1 -1
  247. package/dist/types/session/outcome.d.ts +95 -0
  248. package/dist/types/session/outcome.d.ts.map +1 -0
  249. package/dist/types/session/outcome.js +45 -0
  250. package/dist/types/session/outcome.js.map +1 -0
  251. package/dist/types/session/prepare-step.d.ts +2 -0
  252. package/dist/types/session/prepare-step.d.ts.map +1 -1
  253. package/dist/types/session/records.d.ts +167 -2
  254. package/dist/types/session/records.d.ts.map +1 -1
  255. package/dist/types/session/records.js +42 -0
  256. package/dist/types/session/records.js.map +1 -1
  257. package/dist/types/session/turn.d.ts +18 -0
  258. package/dist/types/session/turn.d.ts.map +1 -1
  259. package/dist/types/session/turn.js.map +1 -1
  260. package/dist/types/tool/index.d.ts +30 -0
  261. package/dist/types/tool/index.d.ts.map +1 -1
  262. package/dist/types/tool/index.js.map +1 -1
  263. package/dist/types/tool/presentation.d.ts +6 -0
  264. package/dist/types/tool/presentation.d.ts.map +1 -1
  265. package/package.json +1 -1
  266. package/src/agents/QueryAgent.ts +5 -0
  267. package/src/agents/examples/SupervisorAgent.ts +1 -0
  268. package/src/agents/explore.ts +2 -1
  269. package/src/compaction/index.ts +1 -0
  270. package/src/compaction/model-summary.ts +2 -2
  271. package/src/compaction/summary.ts +30 -2
  272. package/src/config/runtime.ts +15 -0
  273. package/src/execution/workflow/runtime.ts +4 -0
  274. package/src/execution/workflow/types.ts +9 -0
  275. package/src/manager/agent/child-session.ts +36 -0
  276. package/src/manager/agent/lifecycle.ts +9 -0
  277. package/src/manager/session/turn-recorder.ts +115 -30
  278. package/src/pricing/rates.source.json +1 -1
  279. package/src/prompt/coding-agent-doctrine.ts +5 -3
  280. package/src/provider/fallback.ts +6 -1
  281. package/src/provider/retry.ts +51 -0
  282. package/src/public-runtime.ts +10 -1
  283. package/src/public-tools.ts +6 -1
  284. package/src/runtime/query/auto-review/classifier.ts +21 -5
  285. package/src/runtime/query/auto-review/prompt.ts +2 -0
  286. package/src/runtime/query/auto-review/script-evidence.ts +618 -0
  287. package/src/runtime/query/checkpoint.ts +53 -1
  288. package/src/runtime/query/context.ts +33 -6
  289. package/src/runtime/query/executor.ts +183 -21
  290. package/src/runtime/query/finalize-turn.ts +8 -1
  291. package/src/runtime/query/index.ts +19 -0
  292. package/src/runtime/query/iteration/index.ts +167 -14
  293. package/src/runtime/query/iteration/phases/compaction.ts +53 -3
  294. package/src/runtime/query/iteration/phases/context.ts +6 -0
  295. package/src/runtime/query/iteration/stream-turn.ts +3 -3
  296. package/src/runtime/query/prelude-lease.ts +19 -0
  297. package/src/runtime/query/prepare-turn.ts +29 -12
  298. package/src/runtime/query/review-policy.ts +33 -0
  299. package/src/runtime/query/session-storage.ts +42 -3
  300. package/src/runtime/query/tooling.ts +3 -0
  301. package/src/runtime/query/turn/completion-audit.ts +76 -5
  302. package/src/runtime/query/turn/outcome-ledger.ts +633 -0
  303. package/src/sandbox/file-walk.ts +41 -4
  304. package/src/sandbox/glob-segment.ts +48 -24
  305. package/src/store/checkpoint/contract.ts +15 -0
  306. package/src/store/checkpoint/disk.ts +34 -2
  307. package/src/store/checkpoint/prune.ts +13 -0
  308. package/src/store/evidence/disk.ts +57 -2
  309. package/src/store/session-log/incremental.ts +147 -0
  310. package/src/store/session-log/index.ts +7 -0
  311. package/src/store/session-log/turn-outcome.ts +84 -0
  312. package/src/tools/admit-facts.ts +1 -0
  313. package/src/tools/builtins/bash.ts +12 -2
  314. package/src/tools/builtins/bounded-file-read.ts +59 -2
  315. package/src/tools/builtins/edit-apply.ts +26 -4
  316. package/src/tools/builtins/edit.ts +106 -41
  317. package/src/tools/builtins/glob.ts +1 -1
  318. package/src/tools/builtins/grep.ts +173 -27
  319. package/src/tools/builtins/notebook-guard.ts +76 -0
  320. package/src/tools/builtins/read-render.ts +12 -2
  321. package/src/tools/builtins/search-support.ts +62 -0
  322. package/src/tools/builtins/write-file.ts +12 -1
  323. package/src/tools/coordinator/agent.ts +31 -7
  324. package/src/tools/coordinator/index.ts +6 -1
  325. package/src/tools/coordinator/outcome-record.ts +93 -0
  326. package/src/tools/defineTool.ts +3 -0
  327. package/src/tools/file-read-tracker.ts +51 -2
  328. package/src/tools/untrusted-envelope.ts +23 -1
  329. package/src/toolsets/manager.ts +6 -10
  330. package/src/toolsets/validation-issues.ts +97 -0
  331. package/src/turn/memory-recall.ts +28 -0
  332. package/src/types/agent/base.ts +20 -0
  333. package/src/types/agent/query.ts +2 -0
  334. package/src/types/common/index.ts +25 -1
  335. package/src/types/hitl/index.ts +27 -0
  336. package/src/types/session/config.ts +20 -0
  337. package/src/types/session/events.ts +3 -3
  338. package/src/types/session/index.ts +1 -0
  339. package/src/types/session/outcome.ts +139 -0
  340. package/src/types/session/prepare-step.ts +2 -0
  341. package/src/types/session/records.ts +67 -1
  342. package/src/types/session/turn.ts +19 -0
  343. package/src/types/tool/index.ts +30 -0
  344. package/src/types/tool/presentation.ts +6 -0
@@ -520,6 +520,11 @@ export function execHostShell(
520
520
 
521
521
  export { dangerousCommandReason }
522
522
 
523
+ /** A reason as a sentence a person reads under a row: first letter up. */
524
+ function sentence(reason: string): string {
525
+ return reason ? `${reason.charAt(0).toUpperCase()}${reason.slice(1)}` : reason
526
+ }
527
+
523
528
  /** The dangerous-pattern check `bash` applies, shared so the `monitor` tool refuses the same lines. */
524
529
  export function isDangerousCommand(command: string): boolean {
525
530
  return DANGEROUS_PATTERNS.some((pattern) => pattern.test(command))
@@ -537,7 +542,7 @@ export const SANDBOX_ESCAPE_NOT_APPROVED =
537
542
  'dangerously_disable_sandbox was not approved for this call, so the command did not run. Leaving the sandbox needs the user to confirm it each time, and this turn either cannot ask or does not allow it. Run the command inside the sandbox, or tell the user what it needs and let them run it.'
538
543
 
539
544
  const BASH_DESCRIPTION =
540
- 'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. The working directory persists between calls, but shell state does not. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
545
+ 'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. The working directory persists between calls, but shell state does not. Command timeout is configurable. Read and search with `read`, `grep` and `glob`, not shell loops: a file read through the shell earns no read credit, so its next edit is refused once. Run a check on its own, not piped: its output is already trimmed, and a pipe hides the exit status.'
541
546
 
542
547
  const BashToolBase = defineTool({
543
548
  name: 'bash',
@@ -596,7 +601,12 @@ const BashToolBase = defineTool({
596
601
  success: false,
597
602
  output: '',
598
603
  error: `Dangerous command blocked: "${input.command}". Why: ${dangerousCommandReason(input.command)}. Nothing ran. Use a narrower command that avoids it, or ask the user to run this one.`,
599
- facts: { kind: 'failure', reason: 'blocked', subject: 'bash' },
604
+ facts: {
605
+ kind: 'failure',
606
+ reason: 'blocked',
607
+ subject: 'bash',
608
+ note: sentence(dangerousCommandReason(input.command)),
609
+ },
600
610
  }
601
611
  }
602
612
 
@@ -1,4 +1,4 @@
1
- import { constants } from 'node:fs'
1
+ import { constants, closeSync, fstatSync, openSync, readSync } from 'node:fs'
2
2
  import { open } from 'node:fs/promises'
3
3
 
4
4
  const CHUNK_BYTES = 64 * 1024
@@ -40,10 +40,13 @@ export async function readBoundedFile(
40
40
  maximum: number,
41
41
  signal?: AbortSignal,
42
42
  selectLimit?: (prefix: Buffer) => number,
43
+ extraFlags = 0,
43
44
  ): Promise<Buffer> {
44
45
  // O_NONBLOCK prevents a substituted FIFO from blocking open before fstat.
46
+ // `extraFlags` adds to it, for a caller that also needs the last component
47
+ // refused when it is a link.
45
48
  signal?.throwIfAborted()
46
- const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK)
49
+ const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | extraFlags)
47
50
  try {
48
51
  const info = await file.stat()
49
52
  if (!info.isFile()) throw new Error('Read requires a regular file.')
@@ -78,3 +81,57 @@ function inputTooLarge(limit: number): Error {
78
81
  `File exceeds the bounded read limit (${limit} bytes). Extract a smaller file with a shell command.`,
79
82
  )
80
83
  }
84
+
85
+ /** True where the platform can refuse a final-component link at open time. */
86
+ export const CAN_REFUSE_LINKS = typeof constants.O_NOFOLLOW === 'number' && constants.O_NOFOLLOW > 0
87
+
88
+ /** The flag a caller passes to {@link readBoundedFile} to refuse a final-component link. */
89
+ export const REFUSE_LINK_FLAG: number = CAN_REFUSE_LINKS ? constants.O_NOFOLLOW : 0
90
+
91
+ /**
92
+ * The same bounded read as {@link readBoundedFile} through one synchronous
93
+ * handle: open, fstat, read, close. A link in the last path component is
94
+ * refused at open (`O_NOFOLLOW`), so a link swapped in after a walk listed the
95
+ * file cannot be followed. Callers yield between files; one file is at most
96
+ * `maximum` bytes; a file whose size is over it answers `'too-large'`. With a
97
+ * `scratch` buffer, a file that fits one chunk comes back as a view of it, valid
98
+ * until the next call. Requires {@link CAN_REFUSE_LINKS}.
99
+ *
100
+ * With `syncMaximum`, a file whose size is over it (and within `maximum`) is not
101
+ * read here: the answer is `'large'`, so the caller reads it through
102
+ * {@link readBoundedFile}, which does not hold the event loop for the whole file.
103
+ */
104
+ export function readBoundedFileSync(
105
+ path: string,
106
+ maximum: number,
107
+ scratch?: Buffer,
108
+ syncMaximum = maximum,
109
+ ): Buffer | 'too-large' | 'large' {
110
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW)
111
+ try {
112
+ const info = fstatSync(fd)
113
+ if (!info.isFile()) throw new Error('Read requires a regular file.')
114
+ if (info.size > maximum) return 'too-large'
115
+ if (info.size > syncMaximum) return 'large'
116
+ const chunks: Buffer[] = []
117
+ let offset = 0
118
+ for (;;) {
119
+ const length = Math.min(CHUNK_BYTES, maximum + 1 - offset)
120
+ const buffer =
121
+ offset === 0 && scratch && scratch.length >= length ? scratch : Buffer.allocUnsafe(length)
122
+ let used = 0
123
+ while (used < length) {
124
+ const bytesRead = readSync(fd, buffer, used, length - used, offset + used)
125
+ if (bytesRead === 0) break
126
+ used += bytesRead
127
+ }
128
+ offset += used
129
+ if (offset > maximum) throw inputTooLarge(maximum)
130
+ chunks.push(buffer.subarray(0, used))
131
+ if (used < length)
132
+ return chunks.length === 1 ? (chunks[0] as Buffer) : Buffer.concat(chunks, offset)
133
+ }
134
+ } finally {
135
+ closeSync(fd)
136
+ }
137
+ }
@@ -40,7 +40,7 @@ export type NormalizedEditInput =
40
40
  export function normalizeEditInput(
41
41
  input: EditInput,
42
42
  ): { success: true; operations: NormalizedEditInput[] } | { success: false; error: string } {
43
- if (input.edits !== undefined) {
43
+ if (input.edits !== undefined && input.edits.length > 0) {
44
44
  return {
45
45
  success: true,
46
46
  operations: input.edits.map((edit) => ({
@@ -197,11 +197,15 @@ function applyOne(
197
197
  if (!content.includes(oldString)) {
198
198
  const resolved = resolveOldString(content, oldString)
199
199
  if (resolved === null) {
200
+ const near = nearLine(content, oldString)
200
201
  return {
201
202
  success: false,
202
203
  reason: 'text_not_found',
203
- error:
204
- 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
204
+ error: `old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation. ${
205
+ near === undefined
206
+ ? 'Read the lines you mean to change again and copy them as they are.'
207
+ : `A line that matches its first line apart from spacing is at line ${near}; read around it again and copy it as it is.`
208
+ }`,
205
209
  }
206
210
  }
207
211
  newString = reconcileNewString(oldString, resolved, newString)
@@ -235,7 +239,7 @@ function applyOne(
235
239
  return {
236
240
  success: false,
237
241
  reason: 'not_unique',
238
- error: `old_string/oldStr is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true.`,
242
+ error: `old_string/oldStr is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true. Read around those lines again to choose context that is only in one of them.`,
239
243
  }
240
244
  }
241
245
 
@@ -247,6 +251,24 @@ function applyOne(
247
251
  }
248
252
  }
249
253
 
254
+ const NEAR_LINE_MIN_CHARS = 8
255
+
256
+ /** The line of `content` that matches the first non-blank line of `needle` apart from spacing, 1-based. */
257
+ function nearLine(content: string, needle: string): number | undefined {
258
+ const squash = (text: string) => text.replace(/\s+/g, ' ').trim()
259
+ const first = needle
260
+ .split('\n')
261
+ .map(squash)
262
+ .find((line) => line !== '')
263
+ // A bare brace or paren is on many lines; naming the first one would point at the wrong place.
264
+ if (first === undefined || first.length < NEAR_LINE_MIN_CHARS) return undefined
265
+ const lines = content.split('\n')
266
+ for (let i = 0; i < lines.length; i++) {
267
+ if (squash(lines[i] ?? '') === first) return i + 1
268
+ }
269
+ return undefined
270
+ }
271
+
250
272
  function applyLineInsert(
251
273
  content: string,
252
274
  input: Extract<NormalizedEditInput, { operation: 'insert' }>,
@@ -22,8 +22,91 @@ import {
22
22
  import { withFileMutationLock } from './file-mutation-lock.js'
23
23
  import { jsonStringEscapes } from './json-string-hint.js'
24
24
  import { lineDiff } from './line-diff.js'
25
+ import { notebookRefusal } from './notebook-guard.js'
25
26
  import { staleBeforeReview } from './stale-before-review.js'
26
27
 
28
+ type RawEditInput = {
29
+ path: string
30
+ old_string?: string
31
+ oldStr?: string
32
+ new_string?: string
33
+ newStr?: string
34
+ insertLine?: number | string
35
+ replace_all?: boolean
36
+ edits?: { old_string: string; new_string: string; replace_all?: boolean }[]
37
+ }
38
+
39
+ const ONE_SHAPE_EACH =
40
+ 'Single edit: {"path":"file.md","old_string":"exact unique text","new_string":"replacement text"}\nSeveral edits: {"path":"file.md","edits":[{"old_string":"a","new_string":"b"},{"old_string":"c","new_string":"d"}]}'
41
+
42
+ const hasTopLevelEdit = (value: RawEditInput): boolean =>
43
+ value.old_string !== undefined ||
44
+ value.oldStr !== undefined ||
45
+ value.new_string !== undefined ||
46
+ value.newStr !== undefined ||
47
+ value.insertLine !== undefined
48
+
49
+ /**
50
+ * A top-level edit that is exactly the one entry of a one-entry batch: the same
51
+ * call said twice, not two intentions. Anything else (a different text, a
52
+ * longer list, an insertion) is a real conflict and stays refused.
53
+ */
54
+ function restatesTheBatch(value: RawEditInput): boolean {
55
+ const entries = value.edits
56
+ if (entries === undefined || entries.length !== 1 || value.insertLine !== undefined) return false
57
+ const [entry] = entries
58
+ return (
59
+ entry !== undefined &&
60
+ (value.old_string ?? value.oldStr) === entry.old_string &&
61
+ (value.new_string ?? value.newStr) === entry.new_string &&
62
+ (value.replace_all ?? false) === (entry.replace_all ?? false)
63
+ )
64
+ }
65
+
66
+ /**
67
+ * Which of the two shapes this call is, and what to say when it is neither.
68
+ *
69
+ * An empty `edits` beside a top-level edit, and a one-entry `edits` that restates
70
+ * the top-level edit, are the single edit: the intent is not in doubt and nothing
71
+ * is dropped. A batch beside a DIFFERENT top-level edit stays refused, because
72
+ * any precedence would silently drop an edit somebody believes was made.
73
+ */
74
+ function checkEditShape(value: RawEditInput): string | undefined {
75
+ const batch = value.edits !== undefined && value.edits.length > 0
76
+ const top = hasTopLevelEdit(value)
77
+ if (value.edits !== undefined && !batch && !top) {
78
+ return `\`edits\` is an empty list, so there is nothing to change. Send exactly one of these, and leave out the field you are not using:\n${ONE_SHAPE_EACH}`
79
+ }
80
+ if (batch && top && !restatesTheBatch(value)) {
81
+ return `This call has both an \`edits\` list and a different top-level edit, and only one can be applied. Send exactly one of these, and leave out the field you are not using:\n${ONE_SHAPE_EACH}`
82
+ }
83
+ if (batch) return undefined
84
+ if (typeof value.new_string !== 'string' && typeof value.newStr !== 'string') {
85
+ return 'Either new_string or newStr is required.'
86
+ }
87
+ if (
88
+ value.insertLine === undefined &&
89
+ typeof value.old_string !== 'string' &&
90
+ typeof value.oldStr !== 'string'
91
+ ) {
92
+ return 'Either old_string/oldStr or insertLine is required.'
93
+ }
94
+ return undefined
95
+ }
96
+
97
+ /**
98
+ * The input every later stage sees: one shape, with the redundant half gone. The
99
+ * review card, the classifier, the dry run and the apply step all read this, so
100
+ * none of them meets `edits: []` or a restated edit.
101
+ */
102
+ function canonicalEditInput(value: RawEditInput): RawEditInput {
103
+ if (value.edits === undefined) return value
104
+ const { edits, ...rest } = value
105
+ if (edits.length === 0) return rest
106
+ const { old_string, oldStr, new_string, newStr, insertLine, ...kept } = rest
107
+ return { ...kept, edits }
108
+ }
109
+
27
110
  /**
28
111
  * Two schemas, on purpose.
29
112
  *
@@ -106,48 +189,23 @@ const inputSchema = z
106
189
  })
107
190
  .strict(),
108
191
  )
109
- .min(1)
192
+ // No `.min(1)`: a model that fills every optional field of a flat object
193
+ // sends `edits: []` next to a single top-level edit, and that is the
194
+ // single edit, not an error. An empty list with nothing beside it is
195
+ // refused by `checkEditShape` below, in a sentence that says what to send.
110
196
  .optional()
111
197
  .describe(
112
- 'Several replacements in one file, applied in order and committed together. Nothing is written unless every one applies.',
198
+ 'Several replacements in one file, applied in order and committed together. Nothing is written unless every one applies. Leave out when sending a single old_string/new_string.',
113
199
  ),
114
200
  })
115
201
  .strict()
116
- .refine(
117
- (value) =>
118
- value.edits !== undefined ||
119
- typeof value.new_string === 'string' ||
120
- typeof value.newStr === 'string',
121
- { message: 'Either new_string or newStr is required.' },
122
- )
123
- .refine(
124
- (value) =>
125
- value.edits !== undefined ||
126
- value.insertLine !== undefined ||
127
- typeof value.old_string === 'string' ||
128
- typeof value.oldStr === 'string',
129
- { message: 'Either old_string/oldStr or insertLine is required.' },
130
- )
131
- // Refused rather than resolved. A call carrying both an `edits` list and a
132
- // top-level replacement is two different intentions in one object, and any
133
- // precedence this code picked would be a guess about which the model meant
134
- // — silently dropping the other. There is no reading of that which is safe:
135
- // the dropped half is an edit somebody believes was made.
136
- .refine(
137
- (value) =>
138
- value.edits === undefined ||
139
- (value.old_string === undefined &&
140
- value.oldStr === undefined &&
141
- value.new_string === undefined &&
142
- value.newStr === undefined &&
143
- value.insertLine === undefined),
144
- {
145
- message:
146
- 'Use either `edits` or a single top-level edit, not both. Move the top-level old_string/new_string into the edits list.',
147
- },
148
- )
202
+ .superRefine((value, ctx) => {
203
+ const problem = checkEditShape(value)
204
+ if (problem) ctx.addIssue({ code: 'custom', message: problem })
205
+ })
206
+ .transform(canonicalEditInput)
149
207
 
150
- export type EditInput = z.infer<typeof inputSchema>
208
+ export type EditInput = z.output<typeof inputSchema>
151
209
 
152
210
  /**
153
211
  * What an `edit` call WOULD produce for a file body, without touching any file.
@@ -184,7 +242,10 @@ export function dryRunEdit(
184
242
  }
185
243
  }
186
244
  const result = applyEdit(content, normalized.operations)
187
- return result.success ? { success: true, content: result.content } : result
245
+ if (!result.success) return result
246
+ const notebook = notebookRefusal(parsed.data.path, result.content, content)
247
+ if (notebook) return { success: false, error: notebook }
248
+ return { success: true, content: result.content }
188
249
  }
189
250
 
190
251
  /**
@@ -205,7 +266,7 @@ const modelInputSchema: Record<string, unknown> = {
205
266
  old_string: {
206
267
  type: 'string',
207
268
  description:
208
- 'Exact unique text from the file, without read-tool line-number prefixes. Must not be empty.',
269
+ 'For a single change: exact unique text from the file, without read-tool line-number prefixes. Must not be empty. Do not send together with edits.',
209
270
  },
210
271
  new_string: {
211
272
  type: 'string',
@@ -282,7 +343,7 @@ const modelInputSchema: Record<string, unknown> = {
282
343
  additionalProperties: false,
283
344
  },
284
345
  description:
285
- 'Several replacements in one file, applied in order and committed together. Nothing is written unless every one applies. Use this instead of several edit calls when the changes only make sense together.',
346
+ 'For SEVERAL changes to one file: a non-empty list, applied in order and committed together. Nothing is written unless every one applies. Leave this field out entirely when you send old_string/new_string; never send an empty list.',
286
347
  },
287
348
  },
288
349
  // `old_string` is deliberately NOT required, and this is the fix.
@@ -437,7 +498,7 @@ function refusedEdit(result: EditApplyFailure, path: string) {
437
498
  export const EditTool = defineTool({
438
499
  name: 'edit',
439
500
  description:
440
- 'Makes targeted edits to a file using exact string find-and-replace or line insertion. To change an existing file, use `edit`: `write` overwrites the whole body and discards earlier work on partial failure, while `edit` keeps the rest of the file byte-for-byte intact and is recoverable: if a single edit fails (old_string/oldStr ambiguous, broader restructuring needed), follow up with another `edit` instead of re-emitting the entire file via `write`. The old_string/oldStr must be unique in the file unless replace_all is true. For insertions, pass insertLine plus new_string/newStr; use insertLine: "end" to extend a file at the end. Keep new_string/newStr under 12000 characters, because a longer payload can be cut off mid-call and fail; use repeated bounded edits for long sections. Preserves file formatting and indentation. To make several changes to one file that only make sense together, send them as `edits`: a list of {old_string, new_string} applied in order and committed as one write — if any of them does not apply, nothing is written and the reply names which one, so the file is never left half-changed.',
501
+ 'Makes targeted edits to a file using exact string find-and-replace or line insertion. To change an existing file, use `edit`: `write` overwrites the whole body and discards earlier work on partial failure, while `edit` keeps the rest of the file byte-for-byte intact and is recoverable: if a single edit fails (old_string/oldStr ambiguous, broader restructuring needed), follow up with another `edit` instead of re-emitting the entire file via `write`. The old_string/oldStr must be unique in the file unless replace_all is true. For insertions, pass insertLine plus new_string/newStr; use insertLine: "end" to extend a file at the end. Keep new_string/newStr under 12000 characters, because a longer payload can be cut off mid-call and fail; use repeated bounded edits for long sections. Preserves file formatting and indentation. To make several changes to one file that only make sense together, send them as `edits` (and then no top-level old_string/new_string; for a single change leave `edits` out): a list of {old_string, new_string} applied in order and committed as one write — if any of them does not apply, nothing is written and the reply names which one, so the file is never left half-changed. An edit that would leave a `.ipynb` file as invalid notebook JSON is refused and the file is unchanged.',
441
502
  inputSchema,
442
503
  modelInputSchema,
443
504
  enforceModelInput: true,
@@ -458,7 +519,7 @@ export const EditTool = defineTool({
458
519
  refuseBeforeReview: (input: EditInput, context) => staleBeforeReview(context, input.path, 'edit'),
459
520
  // The classifier judges which file changes and in what way, in a line.
460
521
  classifierInput: (input: EditInput) => {
461
- if (input.edits !== undefined) {
522
+ if (input.edits !== undefined && input.edits.length > 0) {
462
523
  const shown = input.edits.slice(0, 5).map((e) => describeChange(e.old_string, e.new_string))
463
524
  const more =
464
525
  input.edits.length > shown.length ? `; ${input.edits.length - shown.length} more` : ''
@@ -570,6 +631,8 @@ export const EditTool = defineTool({
570
631
  if (!admitted.success) return admitted
571
632
  const result = admitted.result
572
633
  if (!result.success) return refusedEdit(result, parsed.data.path)
634
+ const notebook = notebookRefusal(parsed.data.path, result.content, content)
635
+ if (notebook) return { success: false as const, output: '', error: notebook }
573
636
 
574
637
  await context.sandbox.writeFile(parsed.data.path, result.content)
575
638
  recordEditedContent(context, parsed.data.path, result.content)
@@ -593,6 +656,8 @@ export const EditTool = defineTool({
593
656
  if (!admitted.success) return admitted
594
657
  const result = admitted.result
595
658
  if (!result.success) return refusedEdit(result, hostPath)
659
+ const notebook = notebookRefusal(hostPath, result.content, content)
660
+ if (notebook) return { success: false as const, output: '', error: notebook }
596
661
 
597
662
  // Temp file, fsync, rename — a reader sees the old body or the new
598
663
  // one, never a half-written one. A plain `writeFile` that fails
@@ -68,7 +68,7 @@ function relativePattern(pattern: string, root: string, sandboxed: boolean): str
68
68
  export const GlobTool = defineTool({
69
69
  name: 'glob',
70
70
  description:
71
- 'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Returns at most 500 paths and says when a result is limited or a search stopped early. Does not follow symlinks.',
71
+ 'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Returns at most 500 paths and says when a result is limited or a search stopped early. Does not follow symlinks. To search inside files, use grep.',
72
72
  inputSchema,
73
73
  category: 'filesystem',
74
74
  // Declared so a path outside the turn's roots can be reviewed before the