@nathapp/nax-agent 0.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 (398) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
  5. package/dist/coding-tools/coding-tool-bash.js +44 -0
  6. package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
  7. package/dist/coding-tools/coding-tool-extras.js +56 -0
  8. package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
  9. package/dist/coding-tools/coding-tool-sandbox.js +215 -0
  10. package/dist/coding-tools/coding-tool-support.d.ts +132 -0
  11. package/dist/coding-tools/coding-tool-support.js +159 -0
  12. package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
  13. package/dist/coding-tools/universal-coding-tools.js +5 -0
  14. package/dist/command-interceptor/index.d.ts +55 -0
  15. package/dist/command-interceptor/index.js +60 -0
  16. package/dist/command-interceptor/shell.d.ts +36 -0
  17. package/dist/command-interceptor/shell.js +137 -0
  18. package/dist/command-safety/build.d.ts +19 -0
  19. package/dist/command-safety/build.js +47 -0
  20. package/dist/command-safety/guard.d.ts +32 -0
  21. package/dist/command-safety/guard.js +179 -0
  22. package/dist/command-safety/identifiers.d.ts +24 -0
  23. package/dist/command-safety/identifiers.js +20 -0
  24. package/dist/command-safety/index.d.ts +17 -0
  25. package/dist/command-safety/index.js +11 -0
  26. package/dist/command-safety/questions.d.ts +19 -0
  27. package/dist/command-safety/questions.js +60 -0
  28. package/dist/command-safety/row.d.ts +2 -0
  29. package/dist/command-safety/row.js +14 -0
  30. package/dist/command-safety/rule-scorer.d.ts +9 -0
  31. package/dist/command-safety/rule-scorer.js +391 -0
  32. package/dist/command-safety/shadow.d.ts +33 -0
  33. package/dist/command-safety/shadow.js +188 -0
  34. package/dist/command-safety/systemone-client.d.ts +16 -0
  35. package/dist/command-safety/systemone-client.js +92 -0
  36. package/dist/command-safety/tap.d.ts +37 -0
  37. package/dist/command-safety/tap.js +77 -0
  38. package/dist/command-safety/temp-only.d.ts +6 -0
  39. package/dist/command-safety/temp-only.js +215 -0
  40. package/dist/command-safety/tmp-write.d.ts +2 -0
  41. package/dist/command-safety/tmp-write.js +112 -0
  42. package/dist/command-safety/types.d.ts +185 -0
  43. package/dist/command-safety/types.js +21 -0
  44. package/dist/config/bash-approval.d.ts +33 -0
  45. package/dist/config/bash-approval.js +30 -0
  46. package/dist/config/catalog-overrides.d.ts +94 -0
  47. package/dist/config/catalog-overrides.js +7 -0
  48. package/dist/config/native-agent/index.d.ts +8 -0
  49. package/dist/config/native-agent/index.js +8 -0
  50. package/dist/config/schemas-sandbox.d.ts +31 -0
  51. package/dist/config/schemas-sandbox.js +45 -0
  52. package/dist/cost/core/index.d.ts +8 -0
  53. package/dist/cost/core/index.js +7 -0
  54. package/dist/cost/estimate.d.ts +34 -0
  55. package/dist/cost/estimate.js +65 -0
  56. package/dist/cost/model-spec.d.ts +28 -0
  57. package/dist/cost/model-spec.js +29 -0
  58. package/dist/cost/standard-types.d.ts +9 -0
  59. package/dist/cost/standard-types.js +1 -0
  60. package/dist/cost/usage-math.d.ts +18 -0
  61. package/dist/cost/usage-math.js +46 -0
  62. package/dist/index.d.ts +26 -0
  63. package/dist/index.js +21 -0
  64. package/dist/infra/agent-logger.d.ts +22 -0
  65. package/dist/infra/agent-logger.js +19 -0
  66. package/dist/infra/credentials-config.d.ts +20 -0
  67. package/dist/infra/credentials-config.js +15 -0
  68. package/dist/infra/errors.d.ts +9 -0
  69. package/dist/infra/errors.js +11 -0
  70. package/dist/infra/index.d.ts +7 -0
  71. package/dist/infra/index.js +7 -0
  72. package/dist/infra/nax-error.d.ts +10 -0
  73. package/dist/infra/nax-error.js +16 -0
  74. package/dist/infra/spin-breaker/hash.d.ts +2 -0
  75. package/dist/infra/spin-breaker/hash.js +5 -0
  76. package/dist/infra/spin-breaker/index.d.ts +120 -0
  77. package/dist/infra/spin-breaker/index.js +340 -0
  78. package/dist/internal/agent-output-env.d.ts +44 -0
  79. package/dist/internal/agent-output-env.js +57 -0
  80. package/dist/internal/argv-exec.d.ts +58 -0
  81. package/dist/internal/argv-exec.js +224 -0
  82. package/dist/internal/bounded-io.d.ts +24 -0
  83. package/dist/internal/bounded-io.js +51 -0
  84. package/dist/internal/command-spec/index.d.ts +29 -0
  85. package/dist/internal/command-spec/index.js +36 -0
  86. package/dist/internal/describe-value-type.d.ts +8 -0
  87. package/dist/internal/describe-value-type.js +14 -0
  88. package/dist/internal/exec-framing.d.ts +18 -0
  89. package/dist/internal/exec-framing.js +22 -0
  90. package/dist/internal/file-lock.d.ts +91 -0
  91. package/dist/internal/file-lock.js +212 -0
  92. package/dist/internal/file-size.d.ts +2 -0
  93. package/dist/internal/file-size.js +12 -0
  94. package/dist/internal/git-add.d.ts +43 -0
  95. package/dist/internal/git-add.js +59 -0
  96. package/dist/internal/git-env.d.ts +59 -0
  97. package/dist/internal/git-env.js +93 -0
  98. package/dist/internal/git-exec.d.ts +58 -0
  99. package/dist/internal/git-exec.js +115 -0
  100. package/dist/internal/path-file-lock.d.ts +23 -0
  101. package/dist/internal/path-file-lock.js +25 -0
  102. package/dist/internal/process-alive.d.ts +42 -0
  103. package/dist/internal/process-alive.js +56 -0
  104. package/dist/internal/process-kill.d.ts +27 -0
  105. package/dist/internal/process-kill.js +54 -0
  106. package/dist/internal/realpath.d.ts +29 -0
  107. package/dist/internal/realpath.js +52 -0
  108. package/dist/internal/redact.d.ts +40 -0
  109. package/dist/internal/redact.js +161 -0
  110. package/dist/internal/shell-quote.d.ts +2 -0
  111. package/dist/internal/shell-quote.js +4 -0
  112. package/dist/internal/sort.d.ts +20 -0
  113. package/dist/internal/sort.js +20 -0
  114. package/dist/internal/strip-control-chars.d.ts +19 -0
  115. package/dist/internal/strip-control-chars.js +38 -0
  116. package/dist/internal/thenable.d.ts +10 -0
  117. package/dist/internal/thenable.js +15 -0
  118. package/dist/internal.d.ts +91 -0
  119. package/dist/internal.js +91 -0
  120. package/dist/native/adapter-deps.d.ts +46 -0
  121. package/dist/native/adapter-deps.js +43 -0
  122. package/dist/native/auth-types.d.ts +70 -0
  123. package/dist/native/auth-types.js +13 -0
  124. package/dist/native/auth.d.ts +133 -0
  125. package/dist/native/auth.js +362 -0
  126. package/dist/native/client.d.ts +50 -0
  127. package/dist/native/client.js +174 -0
  128. package/dist/native/complete.d.ts +33 -0
  129. package/dist/native/complete.js +88 -0
  130. package/dist/native/credentials/chained-store.d.ts +30 -0
  131. package/dist/native/credentials/chained-store.js +68 -0
  132. package/dist/native/credentials/change-guard.d.ts +51 -0
  133. package/dist/native/credentials/change-guard.js +136 -0
  134. package/dist/native/credentials/exec-source.d.ts +50 -0
  135. package/dist/native/credentials/exec-source.js +222 -0
  136. package/dist/native/credentials/fingerprint.d.ts +26 -0
  137. package/dist/native/credentials/fingerprint.js +174 -0
  138. package/dist/native/credentials/helper-process.d.ts +79 -0
  139. package/dist/native/credentials/helper-process.js +259 -0
  140. package/dist/native/credentials/helper-protocol.d.ts +36 -0
  141. package/dist/native/credentials/helper-protocol.js +79 -0
  142. package/dist/native/credentials/index.d.ts +60 -0
  143. package/dist/native/credentials/index.js +184 -0
  144. package/dist/native/errors.d.ts +34 -0
  145. package/dist/native/errors.js +109 -0
  146. package/dist/native/index.d.ts +25 -0
  147. package/dist/native/index.js +23 -0
  148. package/dist/native/model-resolver.d.ts +57 -0
  149. package/dist/native/model-resolver.js +73 -0
  150. package/dist/native/models.d.ts +116 -0
  151. package/dist/native/models.js +183 -0
  152. package/dist/native/session/ask-human.d.ts +15 -0
  153. package/dist/native/session/ask-human.js +22 -0
  154. package/dist/native/session/compaction.d.ts +94 -0
  155. package/dist/native/session/compaction.js +170 -0
  156. package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
  157. package/dist/native/session/handle-invalid-tool-call.js +159 -0
  158. package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
  159. package/dist/native/session/loop-events/cache-boundary.js +29 -0
  160. package/dist/native/session/loop-events/external-handler.d.ts +38 -0
  161. package/dist/native/session/loop-events/external-handler.js +193 -0
  162. package/dist/native/session/loop-events/index.d.ts +16 -0
  163. package/dist/native/session/loop-events/index.js +15 -0
  164. package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
  165. package/dist/native/session/loop-events/payload-guard.js +31 -0
  166. package/dist/native/session/loop-events/registry.d.ts +84 -0
  167. package/dist/native/session/loop-events/registry.js +305 -0
  168. package/dist/native/session/loop-events/types.d.ts +251 -0
  169. package/dist/native/session/loop-events/types.js +14 -0
  170. package/dist/native/session/loop-handlers.d.ts +73 -0
  171. package/dist/native/session/loop-handlers.js +137 -0
  172. package/dist/native/session/nudge.d.ts +29 -0
  173. package/dist/native/session/nudge.js +35 -0
  174. package/dist/native/session/rate-provenance.d.ts +17 -0
  175. package/dist/native/session/rate-provenance.js +39 -0
  176. package/dist/native/session/session.d.ts +139 -0
  177. package/dist/native/session/session.js +208 -0
  178. package/dist/native/session/tool-input-exemplar.d.ts +2 -0
  179. package/dist/native/session/tool-input-exemplar.js +66 -0
  180. package/dist/native/session/tool-input-validation.d.ts +15 -0
  181. package/dist/native/session/tool-input-validation.js +146 -0
  182. package/dist/native/session/tool-mapping.d.ts +18 -0
  183. package/dist/native/session/tool-mapping.js +27 -0
  184. package/dist/native/session/tool-result.d.ts +37 -0
  185. package/dist/native/session/tool-result.js +25 -0
  186. package/dist/native/session/transcript-store.d.ts +77 -0
  187. package/dist/native/session/transcript-store.js +200 -0
  188. package/dist/native/session/truncation-handler.d.ts +47 -0
  189. package/dist/native/session/truncation-handler.js +83 -0
  190. package/dist/native/session/turn-accumulator.d.ts +45 -0
  191. package/dist/native/session/turn-accumulator.js +67 -0
  192. package/dist/native/session/turn-ask-human.d.ts +23 -0
  193. package/dist/native/session/turn-ask-human.js +41 -0
  194. package/dist/native/session/turn-compaction-step.d.ts +74 -0
  195. package/dist/native/session/turn-compaction-step.js +128 -0
  196. package/dist/native/session/turn-complete-step.d.ts +77 -0
  197. package/dist/native/session/turn-complete-step.js +159 -0
  198. package/dist/native/session/turn-end-event.d.ts +3 -0
  199. package/dist/native/session/turn-end-event.js +17 -0
  200. package/dist/native/session/turn-events.d.ts +63 -0
  201. package/dist/native/session/turn-events.js +48 -0
  202. package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
  203. package/dist/native/session/turn-loop-round-trip.js +318 -0
  204. package/dist/native/session/turn-loop.d.ts +23 -0
  205. package/dist/native/session/turn-loop.js +250 -0
  206. package/dist/native/session/turn-result.d.ts +39 -0
  207. package/dist/native/session/turn-result.js +64 -0
  208. package/dist/native/session/turn-retry.d.ts +120 -0
  209. package/dist/native/session/turn-retry.js +182 -0
  210. package/dist/native/session/turn-tool-batch.d.ts +61 -0
  211. package/dist/native/session/turn-tool-batch.js +259 -0
  212. package/dist/native/session/turn-types.d.ts +164 -0
  213. package/dist/native/session/turn-types.js +36 -0
  214. package/dist/native/session-adapter.d.ts +56 -0
  215. package/dist/native/session-adapter.js +369 -0
  216. package/dist/native/session-affinity.d.ts +36 -0
  217. package/dist/native/session-affinity.js +43 -0
  218. package/dist/native/tier-providers.d.ts +18 -0
  219. package/dist/native/tier-providers.js +20 -0
  220. package/dist/permissions/approval-audit.d.ts +10 -0
  221. package/dist/permissions/approval-audit.js +22 -0
  222. package/dist/permissions/approvals-link.d.ts +20 -0
  223. package/dist/permissions/approvals-link.js +62 -0
  224. package/dist/permissions/approvals-store.d.ts +168 -0
  225. package/dist/permissions/approvals-store.js +259 -0
  226. package/dist/permissions/approvals-taint.d.ts +33 -0
  227. package/dist/permissions/approvals-taint.js +138 -0
  228. package/dist/permissions/ask-chain.d.ts +69 -0
  229. package/dist/permissions/ask-chain.js +32 -0
  230. package/dist/permissions/ask.d.ts +23 -0
  231. package/dist/permissions/ask.js +25 -0
  232. package/dist/permissions/bash-lex.d.ts +59 -0
  233. package/dist/permissions/bash-lex.js +279 -0
  234. package/dist/permissions/grammar.d.ts +12 -0
  235. package/dist/permissions/grammar.js +24 -0
  236. package/dist/permissions/index.d.ts +11 -0
  237. package/dist/permissions/index.js +9 -0
  238. package/dist/permissions/secret-spans.d.ts +19 -0
  239. package/dist/permissions/secret-spans.js +88 -0
  240. package/dist/permissions/types.d.ts +85 -0
  241. package/dist/permissions/types.js +1 -0
  242. package/dist/runtime/index.d.ts +3 -0
  243. package/dist/runtime/index.js +2 -0
  244. package/dist/runtime/node-glob.d.ts +3 -0
  245. package/dist/runtime/node-glob.js +83 -0
  246. package/dist/runtime/node-runtime.d.ts +3 -0
  247. package/dist/runtime/node-runtime.js +126 -0
  248. package/dist/runtime/slot.d.ts +11 -0
  249. package/dist/runtime/slot.js +17 -0
  250. package/dist/runtime/types.d.ts +51 -0
  251. package/dist/runtime/types.js +1 -0
  252. package/dist/runtime/which.d.ts +8 -0
  253. package/dist/runtime/which.js +32 -0
  254. package/dist/sandbox/argv-quote.d.ts +11 -0
  255. package/dist/sandbox/argv-quote.js +13 -0
  256. package/dist/sandbox/defaults.d.ts +27 -0
  257. package/dist/sandbox/defaults.js +47 -0
  258. package/dist/sandbox/git-guards.d.ts +64 -0
  259. package/dist/sandbox/git-guards.js +161 -0
  260. package/dist/sandbox/index.d.ts +11 -0
  261. package/dist/sandbox/index.js +10 -0
  262. package/dist/sandbox/launcher.d.ts +37 -0
  263. package/dist/sandbox/launcher.js +160 -0
  264. package/dist/sandbox/messages.d.ts +17 -0
  265. package/dist/sandbox/messages.js +46 -0
  266. package/dist/sandbox/policy-builder.d.ts +44 -0
  267. package/dist/sandbox/policy-builder.js +149 -0
  268. package/dist/sandbox/policy-inputs.d.ts +53 -0
  269. package/dist/sandbox/policy-inputs.js +76 -0
  270. package/dist/sandbox/probe.d.ts +8 -0
  271. package/dist/sandbox/probe.js +66 -0
  272. package/dist/sandbox/registry.d.ts +17 -0
  273. package/dist/sandbox/registry.js +57 -0
  274. package/dist/sandbox/session-tmp.d.ts +20 -0
  275. package/dist/sandbox/session-tmp.js +95 -0
  276. package/dist/sandbox/srt-backend.d.ts +21 -0
  277. package/dist/sandbox/srt-backend.js +99 -0
  278. package/dist/sandbox/types.d.ts +102 -0
  279. package/dist/sandbox/types.js +1 -0
  280. package/dist/session/adapter-failure.d.ts +60 -0
  281. package/dist/session/adapter-failure.js +11 -0
  282. package/dist/session/agent-stream-events.d.ts +103 -0
  283. package/dist/session/agent-stream-events.js +6 -0
  284. package/dist/session/interaction-handler.d.ts +53 -0
  285. package/dist/session/interaction-handler.js +2 -0
  286. package/dist/session/no-op-interaction-handler.d.ts +9 -0
  287. package/dist/session/no-op-interaction-handler.js +11 -0
  288. package/dist/session/protocol-types.d.ts +12 -0
  289. package/dist/session/protocol-types.js +1 -0
  290. package/dist/session/session-types.d.ts +456 -0
  291. package/dist/session/session-types.js +64 -0
  292. package/dist/session/tool-descriptor.d.ts +23 -0
  293. package/dist/session/tool-descriptor.js +5 -0
  294. package/dist/session/turn-deadline.d.ts +17 -0
  295. package/dist/session/turn-deadline.js +23 -0
  296. package/dist/tools/ask-request.d.ts +35 -0
  297. package/dist/tools/ask-request.js +57 -0
  298. package/dist/tools/bash-cwd.d.ts +82 -0
  299. package/dist/tools/bash-cwd.js +55 -0
  300. package/dist/tools/bash.d.ts +69 -0
  301. package/dist/tools/bash.js +283 -0
  302. package/dist/tools/delete.d.ts +57 -0
  303. package/dist/tools/delete.js +154 -0
  304. package/dist/tools/denial-redirect.d.ts +42 -0
  305. package/dist/tools/denial-redirect.js +266 -0
  306. package/dist/tools/deny-paths.d.ts +35 -0
  307. package/dist/tools/deny-paths.js +76 -0
  308. package/dist/tools/edit-region.d.ts +50 -0
  309. package/dist/tools/edit-region.js +90 -0
  310. package/dist/tools/edit.d.ts +25 -0
  311. package/dist/tools/edit.js +114 -0
  312. package/dist/tools/exec-allowlist-text.d.ts +18 -0
  313. package/dist/tools/exec-allowlist-text.js +24 -0
  314. package/dist/tools/exec-guard.d.ts +69 -0
  315. package/dist/tools/exec-guard.js +151 -0
  316. package/dist/tools/git-commit.d.ts +59 -0
  317. package/dist/tools/git-commit.js +220 -0
  318. package/dist/tools/git-flags/index.d.ts +10 -0
  319. package/dist/tools/git-flags/index.js +10 -0
  320. package/dist/tools/git.d.ts +88 -0
  321. package/dist/tools/git.js +397 -0
  322. package/dist/tools/glob.d.ts +29 -0
  323. package/dist/tools/glob.js +153 -0
  324. package/dist/tools/grep.d.ts +21 -0
  325. package/dist/tools/grep.js +162 -0
  326. package/dist/tools/index.d.ts +36 -0
  327. package/dist/tools/index.js +28 -0
  328. package/dist/tools/narrow-grants.d.ts +19 -0
  329. package/dist/tools/narrow-grants.js +54 -0
  330. package/dist/tools/nax-owned-writes.d.ts +135 -0
  331. package/dist/tools/nax-owned-writes.js +259 -0
  332. package/dist/tools/package-managers-table.d.ts +146 -0
  333. package/dist/tools/package-managers-table.js +325 -0
  334. package/dist/tools/package-managers-types.d.ts +36 -0
  335. package/dist/tools/package-managers-types.js +6 -0
  336. package/dist/tools/package-managers.d.ts +85 -0
  337. package/dist/tools/package-managers.js +391 -0
  338. package/dist/tools/path-list.d.ts +38 -0
  339. package/dist/tools/path-list.js +62 -0
  340. package/dist/tools/policy-bash-raw.d.ts +19 -0
  341. package/dist/tools/policy-bash-raw.js +210 -0
  342. package/dist/tools/policy-bash.d.ts +64 -0
  343. package/dist/tools/policy-bash.js +232 -0
  344. package/dist/tools/policy-command-branch.d.ts +32 -0
  345. package/dist/tools/policy-command-branch.js +63 -0
  346. package/dist/tools/policy-input.d.ts +2 -0
  347. package/dist/tools/policy-input.js +10 -0
  348. package/dist/tools/policy-match.d.ts +75 -0
  349. package/dist/tools/policy-match.js +117 -0
  350. package/dist/tools/policy-paths-branch.d.ts +97 -0
  351. package/dist/tools/policy-paths-branch.js +314 -0
  352. package/dist/tools/policy.d.ts +65 -0
  353. package/dist/tools/policy.js +232 -0
  354. package/dist/tools/protected-paths.d.ts +26 -0
  355. package/dist/tools/protected-paths.js +8 -0
  356. package/dist/tools/provider-adapt.d.ts +4 -0
  357. package/dist/tools/provider-adapt.js +32 -0
  358. package/dist/tools/provider-advertise.d.ts +39 -0
  359. package/dist/tools/provider-advertise.js +50 -0
  360. package/dist/tools/provider-grants.d.ts +36 -0
  361. package/dist/tools/provider-grants.js +72 -0
  362. package/dist/tools/provider-sanitize.d.ts +19 -0
  363. package/dist/tools/provider-sanitize.js +53 -0
  364. package/dist/tools/provider-types.d.ts +41 -0
  365. package/dist/tools/provider-types.js +26 -0
  366. package/dist/tools/read-continuation.d.ts +147 -0
  367. package/dist/tools/read-continuation.js +176 -0
  368. package/dist/tools/read-file.d.ts +32 -0
  369. package/dist/tools/read-file.js +85 -0
  370. package/dist/tools/read.d.ts +25 -0
  371. package/dist/tools/read.js +182 -0
  372. package/dist/tools/registry.d.ts +125 -0
  373. package/dist/tools/registry.js +67 -0
  374. package/dist/tools/request-capability.d.ts +13 -0
  375. package/dist/tools/request-capability.js +23 -0
  376. package/dist/tools/run-command-exec.d.ts +14 -0
  377. package/dist/tools/run-command-exec.js +161 -0
  378. package/dist/tools/run-command.d.ts +107 -0
  379. package/dist/tools/run-command.js +398 -0
  380. package/dist/tools/runtime-calltool.d.ts +146 -0
  381. package/dist/tools/runtime-calltool.js +223 -0
  382. package/dist/tools/runtime.d.ts +149 -0
  383. package/dist/tools/runtime.js +345 -0
  384. package/dist/tools/scratchpad.d.ts +20 -0
  385. package/dist/tools/scratchpad.js +197 -0
  386. package/dist/tools/spill.d.ts +81 -0
  387. package/dist/tools/spill.js +251 -0
  388. package/dist/tools/tool-audit.d.ts +171 -0
  389. package/dist/tools/tool-audit.js +183 -0
  390. package/dist/tools/truncate.d.ts +98 -0
  391. package/dist/tools/truncate.js +297 -0
  392. package/dist/tools/types.d.ts +145 -0
  393. package/dist/tools/types.js +24 -0
  394. package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
  395. package/dist/tools/verb-denial-argv-hint.js +40 -0
  396. package/dist/tools/write.d.ts +9 -0
  397. package/dist/tools/write.js +45 -0
  398. package/package.json +54 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Ranged-read limit-stop footer (US-001) and cap footer / cap cut (US-002).
3
+ *
4
+ * A ranged `Read` whose `limit` ended before the file's known line count
5
+ * appends a single trailer line that names both the count of unseen lines
6
+ * and the offset to call back with. Without it, a model that asked for the
7
+ * first five lines of a fifty-line file would have no in-band signal that
8
+ * the file continues or how to ask for the next slice — every continuation
9
+ * had to be inferred by trial and error.
10
+ *
11
+ * The trailer is composed deterministically by `limitStopFooter` from three
12
+ * inputs the tool already has on hand: the slice's last line number, the
13
+ * floor or true total from the ranged read, and whether that total is a
14
+ * floor (the read stopped at its I/O bound — `maxFileBytes` on this path).
15
+ *
16
+ * The exact predicate that gates the footer lives here as well: a limit
17
+ * was supplied AND the slice ends before the last line the tool can see.
18
+ * That second clause is what suppresses the footer in the two ACs where
19
+ * it would be wrong — `endLine == totalLines`, and the offset-past-last-read
20
+ * case where the floor header already proves nothing more is reachable
21
+ * within the byte budget.
22
+ *
23
+ * US-002 — the cap footer replaces the limit-stop footer when a Read
24
+ * result overflows either model-facing budget. `applyCapCut` is the
25
+ * fit-check → overlong-line → cap-cut → no-k-fits state machine; the cut
26
+ * keeps the largest `k` such that the header, the first `k` body lines,
27
+ * and the cap footer fit both budgets.
28
+ *
29
+ * The overlong-line step (US-001 of the spill-marker story) is what keeps a
30
+ * single enormous line from hiding the file's tail: a whole-line cut can
31
+ * neither shorten such a line nor skip it, so stopping just before it drops
32
+ * every line after it — including the last output line of a spilled command,
33
+ * which is often the only signal in the body. See its comment in
34
+ * `applyCapCut`.
35
+ *
36
+ * File-local on purpose. These helpers are properties of the `Read`
37
+ * composition, nothing else composes them, and `src/tools` does not
38
+ * export them from its barrel — `read.ts` imports them by relative path.
39
+ */
40
+ /** Inputs the ranged branch already has on hand. */
41
+ export interface LimitStopFooterInput {
42
+ /** The offset to continue from: `endLine + 1`. */
43
+ readonly nextOffset: number;
44
+ /** True iff the read stopped at the I/O bound (`maxFileBytes` here). */
45
+ readonly totalIsFloor: boolean;
46
+ /** The slice's last line number — call it `b` in the spec. */
47
+ readonly endLine: number;
48
+ /** The total the ranged branch reported (the `T` of `R = T - b`). */
49
+ readonly totalLines: number;
50
+ }
51
+ /**
52
+ * Compose the single trailer line that names the continuation offset.
53
+ *
54
+ * `R = totalLines - endLine`, with a trailing `+` exactly when the total
55
+ * is a floor — the read stopped at its I/O bound and the model cannot
56
+ * know how many more lines the file really has. The footer is the result's
57
+ * LAST line: the ranged `Read` joins the body and the footer with `\n`
58
+ * and writes nothing after the footer.
59
+ *
60
+ * The "should the footer be appended at all" check is
61
+ * `shouldAppendLimitStopFooter` — callers decide first, then ask this helper
62
+ * to compose the trailer. Splitting the gate from the composition keeps the
63
+ * no-footer case a pure body with no trailing delimiter to manage.
64
+ */
65
+ export declare function limitStopFooter(input: LimitStopFooterInput): string;
66
+ /**
67
+ * True iff the ranged branch should append the limit-stop footer. Two
68
+ * conditions, both required:
69
+ *
70
+ * 1. `limit` was supplied. Without a limit the slice already runs to
71
+ * the end of what the tool can read, so there is nothing to name.
72
+ * 2. `endLine < totalLines`. When the slice's last line IS the last line
73
+ * the tool can see, the model cannot ask for anything more — the
74
+ * floor header already names that ceiling. Appending a footer that
75
+ * points back inside the slice would mislead the model into reading
76
+ * the same lines twice.
77
+ *
78
+ * Hides the predicate behind a function so the `read.ts` call site reads
79
+ * as the spec, not as the arithmetic.
80
+ */
81
+ export declare function shouldAppendLimitStopFooter(hasLimit: boolean, endLine: number, totalLines: number): boolean;
82
+ /** Inputs to compose a single cap footer line. */
83
+ export interface CapFooterInput {
84
+ /** First selected line number: 1 on the whole-file path, `offset` on the ranged path. */
85
+ readonly firstLine: number;
86
+ /** Last selected line number in the cut. The cap footer names `firstLine..lastLine`. */
87
+ readonly lastLine: number;
88
+ /**
89
+ * Pre-formatted total label: `1500` when the read saw the whole file,
90
+ * `6+` when the read stopped at its I/O bound (the `readCeiling` floor on
91
+ * the whole-file path, `maxFileBytes` floor on the ranged path). The
92
+ * trailing `+` is the caller's responsibility; the helper does not
93
+ * re-derive the floor because it does not know which I/O bound was used.
94
+ */
95
+ readonly totalLabel: string;
96
+ }
97
+ /**
98
+ * The cap footer: `[Showing lines a-b of T. Use offset=X to continue.]`,
99
+ * where `X = b + 1`. Same text the limit-stop footer uses for `X`; the
100
+ * difference is the rest of the message — the cap footer names the
101
+ * delivered range, the limit-stop footer names the unseen count.
102
+ *
103
+ * The footer is the result's last line. `read.ts` joins it to the body
104
+ * with `\n` and writes nothing after it.
105
+ */
106
+ export declare function capFooter(input: CapFooterInput): string;
107
+ /** Inputs to `applyCapCut` — the tool's two paths pass them in their own shape. */
108
+ export interface ApplyCapCutInput {
109
+ /** Header line, already composed: `[1500 lines]`, `[6+ lines]`, `[lines 100-1299 of 1500]`. */
110
+ readonly header: string;
111
+ /** Selected lines, already split by `\n`. Whole-file: every line of the prefix; ranged: `offset..endLine`. */
112
+ readonly lines: readonly string[];
113
+ /** First selected line number (1 on whole-file, `offset` on ranged). */
114
+ readonly firstLine: number;
115
+ /** Pre-formatted total label with the floor `+` if applicable. */
116
+ readonly totalLabel: string;
117
+ /** Limit-stop footer text if rule 1 applied, otherwise `""`. Replaced by the cap footer on a cut. */
118
+ readonly limitStopFooter: string;
119
+ /** Today's unshaped body — returned verbatim when no `k >= 1` fits. */
120
+ readonly unshapedBody: string;
121
+ /** Model-facing byte ceiling (`ctx.maxBytes`). */
122
+ readonly maxBytes: number;
123
+ /** Model-facing line ceiling (`MODEL_MAX_LINES`). */
124
+ readonly maxLines: number;
125
+ }
126
+ /** Output of `applyCapCut`. */
127
+ export interface ApplyCapCutResult {
128
+ /** The composed result content. */
129
+ readonly content: string;
130
+ }
131
+ /**
132
+ * Compose the final result: fit the candidate if it fits, else cut at the
133
+ * largest `k >= 1` such that `header + first k lines + cap footer` fits
134
+ * both budgets, else return the unshaped body.
135
+ *
136
+ * The byte budget is measured over the WHOLE result (header and footer
137
+ * included); the line budget counts the whole result's lines (the same
138
+ * `MODEL_MAX_LINES` the after_tool policy uses). The cap footer is the
139
+ * last line and replaces the limit-stop footer when it fires.
140
+ *
141
+ * On no-`k`-fits, the limit-stop footer (if rule 1 produced one) is
142
+ * appended to today's unshaped body — the spec's "today's result, plus
143
+ * the limit-stop footer from rule 1 if it applied" — so an over-the-cap
144
+ * read that the tool cannot even trim by one line still tells the model
145
+ * how to continue. The after_tool policy then shapes the bytes.
146
+ */
147
+ export declare function applyCapCut(input: ApplyCapCutInput): ApplyCapCutResult;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Ranged-read limit-stop footer (US-001) and cap footer / cap cut (US-002).
3
+ *
4
+ * A ranged `Read` whose `limit` ended before the file's known line count
5
+ * appends a single trailer line that names both the count of unseen lines
6
+ * and the offset to call back with. Without it, a model that asked for the
7
+ * first five lines of a fifty-line file would have no in-band signal that
8
+ * the file continues or how to ask for the next slice — every continuation
9
+ * had to be inferred by trial and error.
10
+ *
11
+ * The trailer is composed deterministically by `limitStopFooter` from three
12
+ * inputs the tool already has on hand: the slice's last line number, the
13
+ * floor or true total from the ranged read, and whether that total is a
14
+ * floor (the read stopped at its I/O bound — `maxFileBytes` on this path).
15
+ *
16
+ * The exact predicate that gates the footer lives here as well: a limit
17
+ * was supplied AND the slice ends before the last line the tool can see.
18
+ * That second clause is what suppresses the footer in the two ACs where
19
+ * it would be wrong — `endLine == totalLines`, and the offset-past-last-read
20
+ * case where the floor header already proves nothing more is reachable
21
+ * within the byte budget.
22
+ *
23
+ * US-002 — the cap footer replaces the limit-stop footer when a Read
24
+ * result overflows either model-facing budget. `applyCapCut` is the
25
+ * fit-check → overlong-line → cap-cut → no-k-fits state machine; the cut
26
+ * keeps the largest `k` such that the header, the first `k` body lines,
27
+ * and the cap footer fit both budgets.
28
+ *
29
+ * The overlong-line step (US-001 of the spill-marker story) is what keeps a
30
+ * single enormous line from hiding the file's tail: a whole-line cut can
31
+ * neither shorten such a line nor skip it, so stopping just before it drops
32
+ * every line after it — including the last output line of a spilled command,
33
+ * which is often the only signal in the body. See its comment in
34
+ * `applyCapCut`.
35
+ *
36
+ * File-local on purpose. These helpers are properties of the `Read`
37
+ * composition, nothing else composes them, and `src/tools` does not
38
+ * export them from its barrel — `read.ts` imports them by relative path.
39
+ */
40
+ import { splitModelLines } from "./truncate.js";
41
+ /**
42
+ * Compose the single trailer line that names the continuation offset.
43
+ *
44
+ * `R = totalLines - endLine`, with a trailing `+` exactly when the total
45
+ * is a floor — the read stopped at its I/O bound and the model cannot
46
+ * know how many more lines the file really has. The footer is the result's
47
+ * LAST line: the ranged `Read` joins the body and the footer with `\n`
48
+ * and writes nothing after the footer.
49
+ *
50
+ * The "should the footer be appended at all" check is
51
+ * `shouldAppendLimitStopFooter` — callers decide first, then ask this helper
52
+ * to compose the trailer. Splitting the gate from the composition keeps the
53
+ * no-footer case a pure body with no trailing delimiter to manage.
54
+ */
55
+ export function limitStopFooter(input) {
56
+ const { nextOffset, totalIsFloor, endLine, totalLines } = input;
57
+ const remaining = Math.max(0, totalLines - endLine);
58
+ const countLabel = totalIsFloor ? `${remaining}+` : `${remaining}`;
59
+ return `[${countLabel} more lines in file. Use offset=${nextOffset} to continue.]`;
60
+ }
61
+ /**
62
+ * True iff the ranged branch should append the limit-stop footer. Two
63
+ * conditions, both required:
64
+ *
65
+ * 1. `limit` was supplied. Without a limit the slice already runs to
66
+ * the end of what the tool can read, so there is nothing to name.
67
+ * 2. `endLine < totalLines`. When the slice's last line IS the last line
68
+ * the tool can see, the model cannot ask for anything more — the
69
+ * floor header already names that ceiling. Appending a footer that
70
+ * points back inside the slice would mislead the model into reading
71
+ * the same lines twice.
72
+ *
73
+ * Hides the predicate behind a function so the `read.ts` call site reads
74
+ * as the spec, not as the arithmetic.
75
+ */
76
+ export function shouldAppendLimitStopFooter(hasLimit, endLine, totalLines) {
77
+ return hasLimit && endLine < totalLines;
78
+ }
79
+ /**
80
+ * The cap footer: `[Showing lines a-b of T. Use offset=X to continue.]`,
81
+ * where `X = b + 1`. Same text the limit-stop footer uses for `X`; the
82
+ * difference is the rest of the message — the cap footer names the
83
+ * delivered range, the limit-stop footer names the unseen count.
84
+ *
85
+ * The footer is the result's last line. `read.ts` joins it to the body
86
+ * with `\n` and writes nothing after it.
87
+ */
88
+ export function capFooter(input) {
89
+ const { firstLine, lastLine, totalLabel } = input;
90
+ return `[Showing lines ${firstLine}-${lastLine} of ${totalLabel}. Use offset=${lastLine + 1} to continue.]`;
91
+ }
92
+ /**
93
+ * Compose the final result: fit the candidate if it fits, else cut at the
94
+ * largest `k >= 1` such that `header + first k lines + cap footer` fits
95
+ * both budgets, else return the unshaped body.
96
+ *
97
+ * The byte budget is measured over the WHOLE result (header and footer
98
+ * included); the line budget counts the whole result's lines (the same
99
+ * `MODEL_MAX_LINES` the after_tool policy uses). The cap footer is the
100
+ * last line and replaces the limit-stop footer when it fires.
101
+ *
102
+ * On no-`k`-fits, the limit-stop footer (if rule 1 produced one) is
103
+ * appended to today's unshaped body — the spec's "today's result, plus
104
+ * the limit-stop footer from rule 1 if it applied" — so an over-the-cap
105
+ * read that the tool cannot even trim by one line still tells the model
106
+ * how to continue. The after_tool policy then shapes the bytes.
107
+ */
108
+ export function applyCapCut(input) {
109
+ const { header, lines, firstLine, totalLabel, limitStopFooter, unshapedBody, maxBytes, maxLines } = input;
110
+ // Rule 1's candidate: today's body, plus the limit-stop footer if it
111
+ // applied. The fit check decides whether this form is what the model
112
+ // sees (returning it unchanged when it fits) or whether the cap cut
113
+ // has to fire.
114
+ const candidate = limitStopFooter === "" ? unshapedBody : `${unshapedBody}\n${limitStopFooter}`;
115
+ // Step 1 — fit check. If today's candidate fits both the line cap and
116
+ // the byte cap, return it unchanged. The after_tool policy then sees a
117
+ // within-cap result and is a no-op on it.
118
+ //
119
+ // The line count uses `splitModelLines`, the same helper the cut path
120
+ // and the after_tool policy use, so the cap matches across all three
121
+ // seams. A raw `split("\n")` would count the trailing newline as a
122
+ // phantom empty line and reject a candidate that is exactly at the
123
+ // cap (e.g. header + 999 body lines + "\n" reports as 1001 lines via
124
+ // raw split but 1000 via `splitModelLines`).
125
+ if (splitModelLines(candidate).length <= maxLines && Buffer.byteLength(candidate, "utf8") <= maxBytes) {
126
+ return { content: candidate };
127
+ }
128
+ // Step 2 — overlong line: hand the body to the after_tool policy.
129
+ //
130
+ // A line whose own bytes exceed the WHOLE model-facing byte budget can
131
+ // never be delivered by this cut, whatever precedes it: the cut is
132
+ // whole-line, so it can neither shorten such a line nor skip it. Cutting
133
+ // just before it would therefore spend the result on the short lines in
134
+ // front of it and drop every line behind it — including the LAST line of a
135
+ // spilled command's output, which is exactly the line a caller reaches for.
136
+ // (`[3 lines] / exit 0 / <160 KB blob> / LAST LINE` cut to the 68 bytes
137
+ // before the blob is the shape this rules out.)
138
+ //
139
+ // So the candidate passes through unshaped, exactly as the no-k-fits case
140
+ // below does, and the after_tool policy shapes it: that policy caps an
141
+ // overlong line at the per-line ceiling instead of evicting the lines
142
+ // around it, and spills what it cuts, so the tail stays reachable.
143
+ if (lines.some((line) => Buffer.byteLength(line, "utf8") > maxBytes)) {
144
+ return { content: candidate };
145
+ }
146
+ // Step 3 — cap cut. Try k from largest to smallest; the first one that
147
+ // fits both budgets is the cut. Walking largest-first is what guarantees
148
+ // the result holds the most whole lines that can fit — picking a smaller
149
+ // k would leave whole lines on the floor that could have been delivered.
150
+ //
151
+ // The upper bound is min(lines.length, maxLines - 2): the line cap
152
+ // already bars any `k` greater than `maxLines - 2`, so iterating higher
153
+ // would only hit the `continue` guard. For a 2 MB ceiling of 1-byte lines
154
+ // (`lines.length` ≈ 1 000 000) the unconstrained start wastes ~998 000
155
+ // pointless iterations before reaching the meaningful range.
156
+ for (let k = Math.min(lines.length, maxLines - 2); k >= 1; k -= 1) {
157
+ // Line budget: header + k body lines + cap footer line.
158
+ if (1 + k + 1 > maxLines)
159
+ continue;
160
+ const lastLine = firstLine + k - 1;
161
+ const footer = capFooter({ firstLine, lastLine, totalLabel });
162
+ // Compose: header\n + (k lines joined by \n) + \n + footer.
163
+ // No trailing newline — the cap footer is the last line, with
164
+ // nothing after it. A cut result never ends with a newline.
165
+ const bodyJoined = lines.slice(0, k).join("\n");
166
+ const cut = `${header}\n${bodyJoined}\n${footer}`;
167
+ if (Buffer.byteLength(cut, "utf8") <= maxBytes) {
168
+ return { content: cut };
169
+ }
170
+ }
171
+ // Step 4 — no k fits: today's candidate passes through unchanged. The
172
+ // cap footer is absent here — the rule is "no line fits with the header
173
+ // and cap footer", so neither can be appended. The after_tool policy
174
+ // is the backstop for these cases.
175
+ return { content: candidate };
176
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared ranged file-read core — US-001.
3
+ *
4
+ * A single implementation that backs both `readTool` and (under US-003)
5
+ * `ScratchpadRead`. The `readCeiling` parameter is the tool-layer I/O bound,
6
+ * distinct from `maxFileBytes` which remains the whole-file Edit/Write cap.
7
+ */
8
+ export interface ReadFileSliceOptions {
9
+ /** Tool-layer I/O bound. Omit to default to `READ_CEILING`. */
10
+ readonly readCeiling?: number;
11
+ /** 1-based line number to start reading from. Omit to start at the first line. */
12
+ readonly offset?: number;
13
+ /** Maximum number of lines to return. Omit to read until the end of the file. */
14
+ readonly limit?: number;
15
+ }
16
+ export interface ReadFileSliceResult {
17
+ /** The slice body (empty string when the offset is past the end). */
18
+ readonly content: string;
19
+ /** True when the file is larger than the supplied `readCeiling`. */
20
+ readonly bounded: boolean;
21
+ /** Total number of lines the file holds (a floor when `bounded` is true). */
22
+ readonly totalLines: number;
23
+ }
24
+ /**
25
+ * Read a ranged slice of a UTF-8 file.
26
+ *
27
+ * Honours the trailing-newline convention: a body ending in "\n" has that
28
+ * newline TERMINATE the last line rather than open an empty one, so the
29
+ * returned `totalLines` agrees with `readTool`'s `[N lines]` header and the
30
+ * model's mental model of a file. An empty body has zero lines.
31
+ */
32
+ export declare function readFileSlice(target: string, opts?: ReadFileSliceOptions): Promise<ReadFileSliceResult>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared ranged file-read core — US-001.
3
+ *
4
+ * A single implementation that backs both `readTool` and (under US-003)
5
+ * `ScratchpadRead`. The `readCeiling` parameter is the tool-layer I/O bound,
6
+ * distinct from `maxFileBytes` which remains the whole-file Edit/Write cap.
7
+ */
8
+ import { open } from "node:fs/promises";
9
+ import { NaxError } from "#src/infra/index";
10
+ import { fileSizeOrZero } from "#src/internal/file-size";
11
+ import { cutBufferToByteCap, READ_CEILING } from "./truncate.js";
12
+ /** Reject offset/limit values that would silently drift to the file's last line. */
13
+ function validateRange(opts) {
14
+ if (opts.offset !== undefined && (!Number.isInteger(opts.offset) || opts.offset <= 0)) {
15
+ throw new NaxError(`offset must be a positive integer (1-based); got ${JSON.stringify(opts.offset)}`, "READ_RANGE_INVALID", { stage: "tools", offset: opts.offset });
16
+ }
17
+ if (opts.limit !== undefined && (!Number.isInteger(opts.limit) || opts.limit <= 0)) {
18
+ throw new NaxError(`limit must be a positive integer; got ${JSON.stringify(opts.limit)}`, "READ_RANGE_INVALID", {
19
+ stage: "tools",
20
+ limit: opts.limit,
21
+ });
22
+ }
23
+ }
24
+ /**
25
+ * Read a ranged slice of a UTF-8 file.
26
+ *
27
+ * Honours the trailing-newline convention: a body ending in "\n" has that
28
+ * newline TERMINATE the last line rather than open an empty one, so the
29
+ * returned `totalLines` agrees with `readTool`'s `[N lines]` header and the
30
+ * model's mental model of a file. An empty body has zero lines.
31
+ */
32
+ export async function readFileSlice(target, opts = {}) {
33
+ validateRange(opts);
34
+ const ceiling = opts.readCeiling ?? READ_CEILING;
35
+ // Probe the file size with `fileSizeOrZero(target)` so we know whether the
36
+ // file exceeds the ceiling without reading past it. That stat is also why
37
+ // this reads exactly `ceiling` bytes rather than `readPrefix`'s
38
+ // ceiling-plus-one: the overshoot exists to infer "there was more" from the
39
+ // read itself, and here the stat already answers that — so the extra byte
40
+ // would only carry the read past the bound it is meant to enforce.
41
+ const fileSize = fileSizeOrZero(target);
42
+ const readBudget = Math.min(ceiling, fileSize);
43
+ const handle = await open(target, "r");
44
+ let body = "";
45
+ try {
46
+ const buffer = Buffer.alloc(readBudget);
47
+ const { bytesRead } = await handle.read(buffer, 0, readBudget, 0);
48
+ // The bound can land inside a codepoint, and a partial tail decodes to
49
+ // U+FFFD (3 bytes) — which would put the returned body back over the
50
+ // ceiling it was just capped at. Trim to a clean boundary so the byte
51
+ // budget holds for the caller.
52
+ body = cutBufferToByteCap(buffer.subarray(0, bytesRead), ceiling).toString("utf8");
53
+ }
54
+ finally {
55
+ await handle.close();
56
+ }
57
+ const bounded = fileSize > ceiling;
58
+ // Trailing-newline convention: strip the trailing "\n" before splitting,
59
+ // so "a\nb\n" yields two lines, not three.
60
+ const trailingNewline = body.endsWith("\n");
61
+ const lines = trailingNewline ? body.slice(0, -1).split("\n") : body === "" ? [] : body.split("\n");
62
+ // When bounded, we read exactly `ceiling` bytes — there are more lines past
63
+ // what we observed. The "lines.length + 1" floor captures that: a bounded
64
+ // file's last visible line either is truncated (a partial line continuing
65
+ // past the ceiling) or ended with the trailing "\n" we already stripped, so
66
+ // at least one more line lives beyond what we read.
67
+ const totalLines = bounded ? lines.length + 1 : lines.length;
68
+ // Offset/limit slicing.
69
+ if (opts.offset !== undefined && opts.offset > totalLines) {
70
+ return { content: "", bounded, totalLines };
71
+ }
72
+ if (opts.offset === undefined && opts.limit === undefined) {
73
+ // Whole-file path: return the read body verbatim so callers see the
74
+ // same trailing newline the file holds.
75
+ return { content: body, bounded, totalLines };
76
+ }
77
+ const startIndex = (opts.offset ?? 1) - 1;
78
+ const endLine = opts.limit === undefined ? lines.length : Math.min(startIndex + opts.limit, lines.length);
79
+ // Joined with no synthesised terminator, mirroring `readTool`'s offset/limit
80
+ // path (src/tools/read.ts): a caller asking for lines 3-4 gets lines 3 and 4,
81
+ // not a newline it did not ask for. The whole-file path above stays verbatim,
82
+ // trailing newline included, because there the caller asked for the file.
83
+ const content = lines.slice(startIndex, endLine).join("\n");
84
+ return { content, bounded, totalLines };
85
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Read one file, already resolved and approved by the policy.
3
+ *
4
+ * The tool never resolves a path itself: it uses ctx.resolvedPaths, which the
5
+ * policy produced. That is what keeps containment in one seam.
6
+ *
7
+ * offset/limit (#1923): models constantly ask for a line range under six
8
+ * different invented spellings, and every one of them was silently discarded
9
+ * because `run`'s input parameter was unused. offset/limit is the spelling
10
+ * they reach for most (144 of 187 observed range arguments), so that is the
11
+ * only shape honoured -- every other spelling is rejected by name rather than
12
+ * silently dropped, the same principle policy.ts uses for a denied argv: name
13
+ * the supported forms instead of a bare refusal.
14
+ *
15
+ * Self-cap at whole-line model caps (US-002): when today's body overflows the
16
+ * line or byte ceiling, the tool cuts at the largest whole-line boundary that
17
+ * still fits and appends a single trailer naming the delivered range. The
18
+ * after_tool policy (`applyModelTruncationPolicy`) is the unconditional
19
+ * backstop — a result that already fits every cap passes through it untouched
20
+ * (its within-cap contract), so no spill file is written for it. The cap
21
+ * footer replaces the limit-stop footer when it fires; a result never carries
22
+ * both.
23
+ */
24
+ import type { CodingTool } from "./registry.ts";
25
+ export declare const readTool: CodingTool;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Read one file, already resolved and approved by the policy.
3
+ *
4
+ * The tool never resolves a path itself: it uses ctx.resolvedPaths, which the
5
+ * policy produced. That is what keeps containment in one seam.
6
+ *
7
+ * offset/limit (#1923): models constantly ask for a line range under six
8
+ * different invented spellings, and every one of them was silently discarded
9
+ * because `run`'s input parameter was unused. offset/limit is the spelling
10
+ * they reach for most (144 of 187 observed range arguments), so that is the
11
+ * only shape honoured -- every other spelling is rejected by name rather than
12
+ * silently dropped, the same principle policy.ts uses for a denied argv: name
13
+ * the supported forms instead of a bare refusal.
14
+ *
15
+ * Self-cap at whole-line model caps (US-002): when today's body overflows the
16
+ * line or byte ceiling, the tool cuts at the largest whole-line boundary that
17
+ * still fits and appends a single trailer naming the delivered range. The
18
+ * after_tool policy (`applyModelTruncationPolicy`) is the unconditional
19
+ * backstop — a result that already fits every cap passes through it untouched
20
+ * (its within-cap contract), so no spill file is written for it. The cap
21
+ * footer replaces the limit-stop footer when it fires; a result never carries
22
+ * both.
23
+ */
24
+ import { readPrefix } from "#src/internal/bounded-io";
25
+ import { applyCapCut, limitStopFooter, shouldAppendLimitStopFooter } from "./read-continuation.js";
26
+ import { MODEL_MAX_BYTES, MODEL_MAX_LINES, READ_CEILING, splitModelLines } from "./truncate.js";
27
+ /** Range arguments models invent instead of offset/limit -- rejected by name, never silently dropped. */
28
+ const UNSUPPORTED_RANGE_ALIASES = ["start_line", "end_line", "start", "end", "line", "lineEnd", "size"];
29
+ /** A positive integer, or an error string naming which constraint failed. */
30
+ function parsePositiveInt(value, field) {
31
+ if (typeof value !== "number" || !Number.isInteger(value))
32
+ return `${field} must be an integer`;
33
+ if (value < 1)
34
+ return `${field} must be >= 1`;
35
+ return value;
36
+ }
37
+ /** Count lines in a UTF-8 string. Empty string returns 0. Trailing newline is not a line. */
38
+ function countLines(prefix) {
39
+ if (prefix === "")
40
+ return 0;
41
+ const trimmed = prefix.endsWith("\n") ? prefix.slice(0, -1) : prefix;
42
+ return trimmed.split("\n").length;
43
+ }
44
+ export const readTool = {
45
+ name: "Read",
46
+ description: "Read a UTF-8 text file from the repository. Paths are relative to the repository root. " +
47
+ "Optionally pass offset (1-based line number to start from) and/or limit (maximum number of " +
48
+ "lines to return) to read a slice instead of the whole file. " +
49
+ "Use Read to examine files instead of cat, sed, head, tail or awk in Bash. " +
50
+ "A read that stops before the end of the file ends with a line naming the offset to continue from. " +
51
+ "For a large file, read the part you need with offset/limit; when you need the whole file, continue with offset until complete. " +
52
+ `Output is capped at ${MODEL_MAX_LINES} lines or ${MODEL_MAX_BYTES} bytes.`,
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ path: { type: "string", description: "Path relative to the repository root" },
57
+ offset: { type: "integer", minimum: 1, description: "1-based line number to start reading from" },
58
+ limit: { type: "integer", minimum: 1, description: "Maximum number of lines to return" },
59
+ },
60
+ required: ["path"],
61
+ },
62
+ scope: { pathFields: ["path"] },
63
+ async run(input, ctx) {
64
+ const [target] = ctx.resolvedPaths;
65
+ if (target === undefined)
66
+ return { content: "no path supplied", isError: true };
67
+ const usedAlias = UNSUPPORTED_RANGE_ALIASES.find((alias) => input[alias] !== undefined);
68
+ if (usedAlias !== undefined) {
69
+ return {
70
+ content: `"${usedAlias}" is not a supported Read argument -- use offset (1-based line number) and/or limit (line count) instead`,
71
+ isError: true,
72
+ };
73
+ }
74
+ const hasOffset = input.offset !== undefined;
75
+ const hasLimit = input.limit !== undefined;
76
+ try {
77
+ if (!hasOffset && !hasLimit) {
78
+ // Leading [N lines] header so an agent has a way to count the lines it
79
+ // is reading. The count comes from the maxBytes prefix, not a second,
80
+ // larger read: readPrefix asks for maxBytes + 1 and the overshoot byte
81
+ // is what tells us the prefix hit the ceiling -- in that case the count
82
+ // is a floor and we mark it with '+'. The ranged branch reads with
83
+ // maxFileBytes and compares against maxFileBytes for the same reason.
84
+ const readCeiling = ctx.readCeiling ?? READ_CEILING;
85
+ const prefix = await readPrefix(target, readCeiling);
86
+ const bounded = Buffer.byteLength(prefix, "utf8") > readCeiling;
87
+ const lineCount = countLines(prefix);
88
+ const totalLabel = bounded ? `${lineCount}+` : `${lineCount}`;
89
+ const header = `[${totalLabel} lines]`;
90
+ // US-002: the whole-file prefix is shaped at the model-facing caps
91
+ // before being handed to the runtime. Today's unshaped body is
92
+ // `header + (prefix when non-empty)`. The cap cut tries the largest
93
+ // k such that `header + first k lines + cap footer` fits both budgets;
94
+ // when no k fits, today's body passes through unchanged and the
95
+ // after_tool policy shapes it.
96
+ const unshaped = prefix === "" ? header : `${header}\n${prefix}`;
97
+ const result = applyCapCut({
98
+ header,
99
+ lines: splitModelLines(prefix),
100
+ firstLine: 1,
101
+ totalLabel,
102
+ limitStopFooter: "",
103
+ unshapedBody: unshaped,
104
+ maxBytes: ctx.maxBytes,
105
+ maxLines: MODEL_MAX_LINES,
106
+ });
107
+ return { content: result.content };
108
+ }
109
+ let offset = 1;
110
+ if (hasOffset) {
111
+ const parsed = parsePositiveInt(input.offset, "offset");
112
+ if (typeof parsed === "string")
113
+ return { content: parsed, isError: true };
114
+ offset = parsed;
115
+ }
116
+ let limit;
117
+ if (hasLimit) {
118
+ const parsed = parsePositiveInt(input.limit, "limit");
119
+ if (typeof parsed === "string")
120
+ return { content: parsed, isError: true };
121
+ limit = parsed;
122
+ }
123
+ // Read up to maxFileBytes, not maxBytes: the requested range may start
124
+ // past the display ceiling, and we still need to reach it to slice.
125
+ const body = await readPrefix(target, ctx.maxFileBytes);
126
+ // readPrefix asks for maxFileBytes + 1, so overshooting the ceiling is how
127
+ // we know the file continues past what we read. The line count is then a
128
+ // floor, not a total, and must not be reported as one -- claiming a false
129
+ // total to the model is the same class of defect #1923 is fixing.
130
+ const bounded = Buffer.byteLength(body, "utf8") > ctx.maxFileBytes;
131
+ const totalLines = countLines(body);
132
+ const totalLabel = bounded ? `${totalLines}+` : `${totalLines}`;
133
+ if (offset > totalLines) {
134
+ return {
135
+ content: bounded
136
+ ? `offset ${offset} is past the first ${totalLines} lines, which is all that could be read within the ${ctx.maxFileBytes}-byte ceiling`
137
+ : `offset ${offset} is past the end of the file -- it has ${totalLines} lines`,
138
+ };
139
+ }
140
+ const lines = splitModelLines(body);
141
+ const startIndex = offset - 1;
142
+ const endLine = limit === undefined ? totalLines : Math.min(startIndex + limit, totalLines);
143
+ const selected = lines.slice(startIndex, endLine).join("\n");
144
+ const headerLine = `[lines ${offset}-${endLine} of ${totalLabel}]`;
145
+ // US-001: when a `limit` cut the slice short of the file's known line
146
+ // count, append a single trailer that names the continuation offset.
147
+ // The helper hides the predicate (limit given AND endLine < totalLines)
148
+ // and the `+`-on-floor rule, both of which are easy to drift apart from
149
+ // the header if inlined.
150
+ const limitStop = shouldAppendLimitStopFooter(limit !== undefined, endLine, totalLines)
151
+ ? limitStopFooter({
152
+ nextOffset: endLine + 1,
153
+ totalIsFloor: bounded,
154
+ endLine,
155
+ totalLines,
156
+ })
157
+ : "";
158
+ // US-002: shape today's body at the model-facing caps. The cap cut
159
+ // replaces the limit-stop footer when it fires; when no whole line
160
+ // fits with the header and cap footer, today's body is returned
161
+ // unchanged (plus the limit-stop footer from rule 1 if it applied),
162
+ // and the after_tool policy then shapes it.
163
+ const unshapedBody = `${headerLine}\n${selected}`;
164
+ const result = applyCapCut({
165
+ header: headerLine,
166
+ lines: lines.slice(startIndex, endLine),
167
+ firstLine: offset,
168
+ totalLabel,
169
+ limitStopFooter: limitStop,
170
+ unshapedBody,
171
+ maxBytes: ctx.maxBytes,
172
+ maxLines: MODEL_MAX_LINES,
173
+ });
174
+ return { content: result.content };
175
+ }
176
+ catch (err) {
177
+ // An unreadable file is a tool ERROR the model can react to, never a
178
+ // denial: the policy already said yes.
179
+ return { content: err instanceof Error ? err.message : String(err), isError: true };
180
+ }
181
+ },
182
+ };