@namzu/sdk 59.0.0 → 60.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 (370) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  6. package/dist/bridge/a2a/mapper.js +16 -0
  7. package/dist/bridge/a2a/mapper.js.map +1 -1
  8. package/dist/bridge/acp/update.d.ts.map +1 -1
  9. package/dist/bridge/acp/update.js +1 -0
  10. package/dist/bridge/acp/update.js.map +1 -1
  11. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  12. package/dist/bridge/sse/mapper.js +12 -0
  13. package/dist/bridge/sse/mapper.js.map +1 -1
  14. package/dist/compaction/context-window.d.ts +53 -0
  15. package/dist/compaction/context-window.d.ts.map +1 -1
  16. package/dist/compaction/context-window.js +141 -2
  17. package/dist/compaction/context-window.js.map +1 -1
  18. package/dist/constants/continuation.d.ts +9 -0
  19. package/dist/constants/continuation.d.ts.map +1 -1
  20. package/dist/constants/continuation.js +9 -0
  21. package/dist/constants/continuation.js.map +1 -1
  22. package/dist/contracts/session/api.d.ts +7 -1
  23. package/dist/contracts/session/api.d.ts.map +1 -1
  24. package/dist/execution/workflow/index.d.ts +3 -1
  25. package/dist/execution/workflow/index.d.ts.map +1 -1
  26. package/dist/execution/workflow/index.js +2 -1
  27. package/dist/execution/workflow/index.js.map +1 -1
  28. package/dist/execution/workflow/journal.d.ts +24 -4
  29. package/dist/execution/workflow/journal.d.ts.map +1 -1
  30. package/dist/execution/workflow/journal.js +131 -9
  31. package/dist/execution/workflow/journal.js.map +1 -1
  32. package/dist/execution/workflow/prelude.d.ts +2 -0
  33. package/dist/execution/workflow/prelude.d.ts.map +1 -1
  34. package/dist/execution/workflow/prelude.js +49 -8
  35. package/dist/execution/workflow/prelude.js.map +1 -1
  36. package/dist/execution/workflow/resume.d.ts +59 -0
  37. package/dist/execution/workflow/resume.d.ts.map +1 -0
  38. package/dist/execution/workflow/resume.js +273 -0
  39. package/dist/execution/workflow/resume.js.map +1 -0
  40. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  41. package/dist/execution/workflow/runtime.js +112 -8
  42. package/dist/execution/workflow/runtime.js.map +1 -1
  43. package/dist/execution/workflow/types.d.ts +105 -1
  44. package/dist/execution/workflow/types.d.ts.map +1 -1
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  50. package/dist/manager/session/turn-recorder.js +1 -0
  51. package/dist/manager/session/turn-recorder.js.map +1 -1
  52. package/dist/plugin/compat.d.ts +24 -0
  53. package/dist/plugin/compat.d.ts.map +1 -0
  54. package/dist/plugin/compat.js +149 -0
  55. package/dist/plugin/compat.js.map +1 -0
  56. package/dist/plugin/loader.d.ts +2 -1
  57. package/dist/plugin/loader.d.ts.map +1 -1
  58. package/dist/plugin/loader.js +6 -0
  59. package/dist/plugin/loader.js.map +1 -1
  60. package/dist/pricing/index.d.ts.map +1 -1
  61. package/dist/pricing/index.js +2 -1
  62. package/dist/pricing/index.js.map +1 -1
  63. package/dist/process/kill-tree.d.ts +44 -0
  64. package/dist/process/kill-tree.d.ts.map +1 -1
  65. package/dist/process/kill-tree.js +102 -4
  66. package/dist/process/kill-tree.js.map +1 -1
  67. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  69. package/dist/prompt/coding-agent-doctrine.js +9 -1
  70. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  71. package/dist/provider/collect-chat-completion.d.ts.map +1 -1
  72. package/dist/provider/collect-chat-completion.js +4 -0
  73. package/dist/provider/collect-chat-completion.js.map +1 -1
  74. package/dist/provider/fallback.d.ts.map +1 -1
  75. package/dist/provider/fallback.js +11 -0
  76. package/dist/provider/fallback.js.map +1 -1
  77. package/dist/provider/idle-timeout.d.ts.map +1 -1
  78. package/dist/provider/idle-timeout.js +5 -0
  79. package/dist/provider/idle-timeout.js.map +1 -1
  80. package/dist/provider/mock.d.ts +2 -0
  81. package/dist/provider/mock.d.ts.map +1 -1
  82. package/dist/provider/mock.js +16 -8
  83. package/dist/provider/mock.js.map +1 -1
  84. package/dist/provider/model-window-suffix.d.ts +18 -0
  85. package/dist/provider/model-window-suffix.d.ts.map +1 -0
  86. package/dist/provider/model-window-suffix.js +78 -0
  87. package/dist/provider/model-window-suffix.js.map +1 -0
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +5 -3
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +5 -4
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/public-types.d.ts +1 -0
  96. package/dist/public-types.d.ts.map +1 -1
  97. package/dist/registry/command/kernel-commands.d.ts +1 -4
  98. package/dist/registry/command/kernel-commands.d.ts.map +1 -1
  99. package/dist/registry/command/kernel-commands.js +0 -28
  100. package/dist/registry/command/kernel-commands.js.map +1 -1
  101. package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
  102. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
  103. package/dist/runtime/jobs/awaited-jobs.js +11 -0
  104. package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
  105. package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
  106. package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
  107. package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
  108. package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
  109. package/dist/runtime/jobs/liveness.d.ts +67 -0
  110. package/dist/runtime/jobs/liveness.d.ts.map +1 -0
  111. package/dist/runtime/jobs/liveness.js +94 -0
  112. package/dist/runtime/jobs/liveness.js.map +1 -0
  113. package/dist/runtime/jobs/registry.d.ts +21 -0
  114. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  115. package/dist/runtime/jobs/registry.js +104 -2
  116. package/dist/runtime/jobs/registry.js.map +1 -1
  117. package/dist/runtime/jobs/wake-budget.d.ts +35 -0
  118. package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
  119. package/dist/runtime/jobs/wake-budget.js +48 -0
  120. package/dist/runtime/jobs/wake-budget.js.map +1 -0
  121. package/dist/runtime/query/detached-calls.d.ts +65 -0
  122. package/dist/runtime/query/detached-calls.d.ts.map +1 -0
  123. package/dist/runtime/query/detached-calls.js +116 -0
  124. package/dist/runtime/query/detached-calls.js.map +1 -0
  125. package/dist/runtime/query/executor.d.ts +4 -1
  126. package/dist/runtime/query/executor.d.ts.map +1 -1
  127. package/dist/runtime/query/executor.js +90 -6
  128. package/dist/runtime/query/executor.js.map +1 -1
  129. package/dist/runtime/query/index.d.ts +34 -0
  130. package/dist/runtime/query/index.d.ts.map +1 -1
  131. package/dist/runtime/query/index.js +32 -5
  132. package/dist/runtime/query/index.js.map +1 -1
  133. package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
  134. package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
  135. package/dist/runtime/query/iteration/effort-clamp.js +42 -0
  136. package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
  137. package/dist/runtime/query/iteration/index.d.ts +34 -0
  138. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  139. package/dist/runtime/query/iteration/index.js +199 -6
  140. package/dist/runtime/query/iteration/index.js.map +1 -1
  141. package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
  142. package/dist/runtime/query/iteration/outstanding-work.js +11 -3
  143. package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
  144. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  145. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  147. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  148. package/dist/runtime/query/iteration/phases/tool-review.js +63 -1
  149. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  150. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  151. package/dist/runtime/query/iteration/stream-turn.js +6 -0
  152. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  153. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  154. package/dist/runtime/query/session-storage.js +8 -3
  155. package/dist/runtime/query/session-storage.js.map +1 -1
  156. package/dist/runtime/query/steering.d.ts +29 -3
  157. package/dist/runtime/query/steering.d.ts.map +1 -1
  158. package/dist/runtime/query/steering.js +58 -3
  159. package/dist/runtime/query/steering.js.map +1 -1
  160. package/dist/runtime/query/tooling.d.ts +1 -0
  161. package/dist/runtime/query/tooling.d.ts.map +1 -1
  162. package/dist/runtime/query/tooling.js +1 -0
  163. package/dist/runtime/query/tooling.js.map +1 -1
  164. package/dist/runtime/system-events.d.ts +5 -0
  165. package/dist/runtime/system-events.d.ts.map +1 -1
  166. package/dist/runtime/system-events.js +7 -1
  167. package/dist/runtime/system-events.js.map +1 -1
  168. package/dist/sandbox/denial.d.ts +11 -0
  169. package/dist/sandbox/denial.d.ts.map +1 -0
  170. package/dist/sandbox/denial.js +61 -0
  171. package/dist/sandbox/denial.js.map +1 -0
  172. package/dist/sandbox/file-walk-program.d.ts.map +1 -1
  173. package/dist/sandbox/file-walk-program.js +31 -13
  174. package/dist/sandbox/file-walk-program.js.map +1 -1
  175. package/dist/sandbox/file-walk.d.ts +2 -0
  176. package/dist/sandbox/file-walk.d.ts.map +1 -1
  177. package/dist/sandbox/file-walk.js +64 -13
  178. package/dist/sandbox/file-walk.js.map +1 -1
  179. package/dist/sandbox/ignore-matcher.d.ts +30 -0
  180. package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
  181. package/dist/sandbox/ignore-matcher.js +11 -0
  182. package/dist/sandbox/ignore-matcher.js.map +1 -0
  183. package/dist/sandbox/ignore-source.d.ts +26 -0
  184. package/dist/sandbox/ignore-source.d.ts.map +1 -0
  185. package/dist/sandbox/ignore-source.js +221 -0
  186. package/dist/sandbox/ignore-source.js.map +1 -0
  187. package/dist/sandbox/provider/local.d.ts.map +1 -1
  188. package/dist/sandbox/provider/local.js +11 -1
  189. package/dist/sandbox/provider/local.js.map +1 -1
  190. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  191. package/dist/scheduler/completion-inbox.js +1 -0
  192. package/dist/scheduler/completion-inbox.js.map +1 -1
  193. package/dist/session/log-hash.d.ts.map +1 -1
  194. package/dist/session/log-hash.js +18 -1
  195. package/dist/session/log-hash.js.map +1 -1
  196. package/dist/store/memory/disk.d.ts.map +1 -1
  197. package/dist/store/memory/disk.js +1 -1
  198. package/dist/store/memory/disk.js.map +1 -1
  199. package/dist/store/memory/markdown.d.ts.map +1 -1
  200. package/dist/store/memory/markdown.js +1 -1
  201. package/dist/store/memory/markdown.js.map +1 -1
  202. package/dist/store/memory/operation-lock.d.ts +20 -4
  203. package/dist/store/memory/operation-lock.d.ts.map +1 -1
  204. package/dist/store/memory/operation-lock.js +235 -58
  205. package/dist/store/memory/operation-lock.js.map +1 -1
  206. package/dist/store/session-index/sqlite.d.ts.map +1 -1
  207. package/dist/store/session-index/sqlite.js +169 -16
  208. package/dist/store/session-index/sqlite.js.map +1 -1
  209. package/dist/tools/builtins/bash.d.ts.map +1 -1
  210. package/dist/tools/builtins/bash.js +10 -4
  211. package/dist/tools/builtins/bash.js.map +1 -1
  212. package/dist/tools/builtins/computer-use.d.ts +35 -0
  213. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  214. package/dist/tools/builtins/computer-use.js +512 -67
  215. package/dist/tools/builtins/computer-use.js.map +1 -1
  216. package/dist/tools/builtins/glob.d.ts +1 -0
  217. package/dist/tools/builtins/glob.d.ts.map +1 -1
  218. package/dist/tools/builtins/glob.js +65 -28
  219. package/dist/tools/builtins/glob.js.map +1 -1
  220. package/dist/tools/builtins/grep.d.ts +2 -0
  221. package/dist/tools/builtins/grep.d.ts.map +1 -1
  222. package/dist/tools/builtins/grep.js +94 -40
  223. package/dist/tools/builtins/grep.js.map +1 -1
  224. package/dist/tools/builtins/job.d.ts +1 -1
  225. package/dist/tools/builtins/job.d.ts.map +1 -1
  226. package/dist/tools/builtins/job.js +34 -7
  227. package/dist/tools/builtins/job.js.map +1 -1
  228. package/dist/tools/builtins/search-support.d.ts +44 -0
  229. package/dist/tools/builtins/search-support.d.ts.map +1 -0
  230. package/dist/tools/builtins/search-support.js +89 -0
  231. package/dist/tools/builtins/search-support.js.map +1 -0
  232. package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
  233. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
  234. package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
  235. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
  236. package/dist/tools/builtins/wait-for-job.d.ts +1 -1
  237. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
  238. package/dist/tools/builtins/wait-for-job.js +13 -2
  239. package/dist/tools/builtins/wait-for-job.js.map +1 -1
  240. package/dist/tools/command-shell.d.ts.map +1 -1
  241. package/dist/tools/command-shell.js +14 -5
  242. package/dist/tools/command-shell.js.map +1 -1
  243. package/dist/turn/reporter.d.ts.map +1 -1
  244. package/dist/turn/reporter.js +11 -0
  245. package/dist/turn/reporter.js.map +1 -1
  246. package/dist/types/agent/query.d.ts +2 -0
  247. package/dist/types/agent/query.d.ts.map +1 -1
  248. package/dist/types/computer-use/index.d.ts +129 -13
  249. package/dist/types/computer-use/index.d.ts.map +1 -1
  250. package/dist/types/computer-use/index.js.map +1 -1
  251. package/dist/types/message/index.d.ts +1 -1
  252. package/dist/types/message/index.d.ts.map +1 -1
  253. package/dist/types/message/index.js +1 -0
  254. package/dist/types/message/index.js.map +1 -1
  255. package/dist/types/plugin/index.d.ts +24 -0
  256. package/dist/types/plugin/index.d.ts.map +1 -1
  257. package/dist/types/plugin/index.js +4 -0
  258. package/dist/types/plugin/index.js.map +1 -1
  259. package/dist/types/provider/chat.d.ts +19 -0
  260. package/dist/types/provider/chat.d.ts.map +1 -1
  261. package/dist/types/provider/config.d.ts +7 -0
  262. package/dist/types/provider/config.d.ts.map +1 -1
  263. package/dist/types/provider/index.d.ts +1 -1
  264. package/dist/types/provider/index.d.ts.map +1 -1
  265. package/dist/types/provider/interface.d.ts +15 -0
  266. package/dist/types/provider/interface.d.ts.map +1 -1
  267. package/dist/types/provider/stream.d.ts +6 -0
  268. package/dist/types/provider/stream.d.ts.map +1 -1
  269. package/dist/types/sandbox/index.d.ts +38 -1
  270. package/dist/types/sandbox/index.d.ts.map +1 -1
  271. package/dist/types/sandbox/index.js.map +1 -1
  272. package/dist/types/session/events.d.ts +34 -0
  273. package/dist/types/session/events.d.ts.map +1 -1
  274. package/dist/types/session/events.js.map +1 -1
  275. package/dist/types/session/records.d.ts +298 -34
  276. package/dist/types/session/records.d.ts.map +1 -1
  277. package/dist/types/session/records.js +35 -1
  278. package/dist/types/session/records.js.map +1 -1
  279. package/dist/types/session/stop-reason.d.ts +11 -0
  280. package/dist/types/session/stop-reason.d.ts.map +1 -1
  281. package/dist/types/tool/index.d.ts +30 -0
  282. package/dist/types/tool/index.d.ts.map +1 -1
  283. package/dist/types/tool/index.js.map +1 -1
  284. package/dist/types/tool/presentation.d.ts +8 -0
  285. package/dist/types/tool/presentation.d.ts.map +1 -1
  286. package/package.json +1 -1
  287. package/src/agents/QueryAgent.ts +1 -0
  288. package/src/bridge/a2a/mapper.ts +19 -0
  289. package/src/bridge/acp/update.ts +6 -1
  290. package/src/bridge/sse/mapper.ts +13 -0
  291. package/src/compaction/context-window.ts +163 -2
  292. package/src/constants/continuation.ts +13 -0
  293. package/src/contracts/session/api.ts +6 -0
  294. package/src/execution/workflow/index.ts +14 -1
  295. package/src/execution/workflow/journal.ts +161 -11
  296. package/src/execution/workflow/prelude.ts +51 -8
  297. package/src/execution/workflow/resume.ts +313 -0
  298. package/src/execution/workflow/runtime.ts +127 -9
  299. package/src/execution/workflow/types.ts +109 -1
  300. package/src/index.ts +17 -0
  301. package/src/manager/session/turn-recorder.ts +1 -0
  302. package/src/plugin/compat.ts +173 -0
  303. package/src/plugin/loader.ts +7 -1
  304. package/src/pricing/index.ts +2 -1
  305. package/src/process/kill-tree.ts +106 -4
  306. package/src/prompt/coding-agent-doctrine.ts +9 -1
  307. package/src/provider/collect-chat-completion.ts +3 -0
  308. package/src/provider/fallback.ts +17 -0
  309. package/src/provider/idle-timeout.ts +9 -0
  310. package/src/provider/mock.ts +19 -9
  311. package/src/provider/model-window-suffix.ts +85 -0
  312. package/src/provider/retry.ts +9 -0
  313. package/src/public-runtime.ts +21 -2
  314. package/src/public-types.ts +1 -0
  315. package/src/registry/command/kernel-commands.ts +1 -32
  316. package/src/runtime/jobs/awaited-jobs.ts +11 -0
  317. package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
  318. package/src/runtime/jobs/liveness.ts +144 -0
  319. package/src/runtime/jobs/registry.ts +114 -2
  320. package/src/runtime/jobs/wake-budget.ts +55 -0
  321. package/src/runtime/query/detached-calls.ts +135 -0
  322. package/src/runtime/query/executor.ts +92 -10
  323. package/src/runtime/query/index.ts +65 -7
  324. package/src/runtime/query/iteration/effort-clamp.ts +46 -0
  325. package/src/runtime/query/iteration/index.ts +260 -6
  326. package/src/runtime/query/iteration/outstanding-work.ts +12 -3
  327. package/src/runtime/query/iteration/phases/context.ts +9 -0
  328. package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
  329. package/src/runtime/query/iteration/stream-turn.ts +5 -0
  330. package/src/runtime/query/session-storage.ts +8 -3
  331. package/src/runtime/query/steering.ts +77 -6
  332. package/src/runtime/query/tooling.ts +2 -0
  333. package/src/runtime/system-events.ts +12 -1
  334. package/src/sandbox/denial.ts +79 -0
  335. package/src/sandbox/file-walk-program.ts +32 -13
  336. package/src/sandbox/file-walk.ts +75 -12
  337. package/src/sandbox/ignore-matcher.ts +46 -0
  338. package/src/sandbox/ignore-source.ts +220 -0
  339. package/src/sandbox/provider/local.ts +11 -1
  340. package/src/scheduler/completion-inbox.ts +1 -0
  341. package/src/session/log-hash.ts +21 -0
  342. package/src/store/memory/disk.ts +1 -0
  343. package/src/store/memory/markdown.ts +1 -0
  344. package/src/store/memory/operation-lock.ts +288 -57
  345. package/src/store/session-index/sqlite.ts +171 -16
  346. package/src/tools/builtins/bash.ts +11 -4
  347. package/src/tools/builtins/computer-use.ts +593 -68
  348. package/src/tools/builtins/glob.ts +79 -32
  349. package/src/tools/builtins/grep.ts +107 -43
  350. package/src/tools/builtins/job.ts +42 -12
  351. package/src/tools/builtins/search-support.ts +107 -0
  352. package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
  353. package/src/tools/builtins/wait-for-job.ts +14 -2
  354. package/src/tools/command-shell.ts +13 -5
  355. package/src/turn/reporter.ts +12 -0
  356. package/src/types/agent/query.ts +2 -0
  357. package/src/types/computer-use/index.ts +157 -15
  358. package/src/types/message/index.ts +5 -3
  359. package/src/types/plugin/index.ts +8 -0
  360. package/src/types/provider/chat.ts +20 -0
  361. package/src/types/provider/config.ts +7 -0
  362. package/src/types/provider/index.ts +1 -0
  363. package/src/types/provider/interface.ts +20 -0
  364. package/src/types/provider/stream.ts +6 -0
  365. package/src/types/sandbox/index.ts +40 -1
  366. package/src/types/session/events.ts +65 -5
  367. package/src/types/session/records.ts +39 -2
  368. package/src/types/session/stop-reason.ts +11 -0
  369. package/src/types/tool/index.ts +30 -0
  370. package/src/types/tool/presentation.ts +8 -0
@@ -1,13 +1,23 @@
1
1
  import { isAbsolute, join, relative, sep } from 'node:path'
2
2
  import { z } from 'zod'
3
3
  import { walkFilesLocally } from '../../sandbox/file-walk.js'
4
- import type { SandboxWalkFilesOptions } from '../../types/sandbox/index.js'
4
+ import type { SandboxWalkFilesOptions, SandboxWalkSummary } from '../../types/sandbox/index.js'
5
5
  import { defineTool } from '../defineTool.js'
6
6
  import { resolveWithin, resolveWithinAny, resolveWithinAnyReal, toolRoots } from '../paths.js'
7
7
  import { relativePosix, resolveWithinPosix } from '../posix-path.js'
8
+ import {
9
+ SEARCH_BUDGET_MS,
10
+ hostDisplayPath,
11
+ ignoredHint,
12
+ isEntryLimit,
13
+ isReadDenied,
14
+ openSearchBudget,
15
+ plural,
16
+ quoteControlPath,
17
+ stoppedNotice,
18
+ } from './search-support.js'
8
19
 
9
20
  const MAX_GLOB_RESULTS = 500
10
- const MAX_VISITED_ENTRIES = 20_000
11
21
 
12
22
  const inputSchema = z.object({
13
23
  pattern: z
@@ -27,7 +37,13 @@ const inputSchema = z.object({
27
37
  .boolean()
28
38
  .optional()
29
39
  .describe(
30
- 'Include hidden files and directories in wildcard matches. Default: false. Explicit dotfile patterns still match.',
40
+ 'Include hidden files and directories in wildcard matches. Default: true. Version-control directories are always skipped.',
41
+ ),
42
+ include_ignored: z
43
+ .boolean()
44
+ .optional()
45
+ .describe(
46
+ 'Also return files that .gitignore and .ignore rules exclude, such as build output and dependencies. Default: false.',
31
47
  ),
32
48
  })
33
49
 
@@ -49,16 +65,10 @@ function relativePattern(pattern: string, root: string, sandboxed: boolean): str
49
65
  return result || '.'
50
66
  }
51
67
 
52
- /** Keep unusual filenames unambiguous without changing the returned path data. */
53
- function displayPath(path: string): string {
54
- // biome-ignore lint/suspicious/noControlCharactersInRegex: quote filenames containing control bytes instead of creating fake result lines.
55
- return /[\x00-\x1f\x7f]/.test(path) ? JSON.stringify(path) : path
56
- }
57
-
58
68
  export const GlobTool = defineTool({
59
69
  name: 'glob',
60
70
  description:
61
- 'Finds regular files by glob pattern in a bounded search. "*" searches one directory; use "**" explicitly for recursive discovery. Returns at most 500 paths and identifies incomplete searches. 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.',
62
72
  inputSchema,
63
73
  category: 'filesystem',
64
74
  // Declared so a path outside the turn's roots can be reviewed before the
@@ -102,57 +112,94 @@ export const GlobTool = defineTool({
102
112
  'This sandbox does not support bounded file discovery (Sandbox.walkFiles). Update its adapter or use an available directory-listing command inside the sandbox.',
103
113
  }
104
114
  }
115
+ const budget = openSearchBudget(context.abortSignal)
116
+ let summary: SandboxWalkSummary | undefined
105
117
  const options: SandboxWalkFilesOptions = {
106
118
  pattern,
107
- signal: context.abortSignal,
119
+ signal: budget.signal,
108
120
  maxEntries: MAX_GLOB_RESULTS + 1,
109
- maxVisitedEntries: MAX_VISITED_ENTRIES,
110
- includeHidden: input.include_hidden ?? false,
121
+ includeHidden: input.include_hidden ?? true,
122
+ respectIgnore: input.include_ignored !== true,
123
+ onSummary: (value) => {
124
+ summary = value
125
+ },
111
126
  }
112
127
  const entries = walkSandbox ? walkSandbox(root, options) : walkFilesLocally(root, options)
113
128
  const files: string[] = []
114
- let truncated = false
129
+ let capped = false
130
+ let timedOut = false
131
+ let legacyEntryLimit = false
115
132
  let failure: string | undefined
116
133
  try {
117
134
  for await (const entry of entries) {
118
- context.abortSignal?.throwIfAborted()
135
+ budget.signal.throwIfAborted()
119
136
  const absolute = sandbox
120
137
  ? resolveWithinPosix(root, entry.path)
121
138
  : resolveWithin(root, entry.path)
139
+ if (isReadDenied(context, absolute, sandbox?.rootDir)) continue
122
140
  if (files.length === MAX_GLOB_RESULTS) {
123
- truncated = true
141
+ capped = true
124
142
  break
125
143
  }
126
144
  // Readers resolve relative input against the original working path,
127
145
  // which may be a symlink. Project back through the selected alias
128
146
  // before relativizing so added directories remain reachable.
129
- const file = sandbox
130
- ? relativePosix(sandbox.rootDir, absolute)
131
- : relative(context.workingDirectory, join(lexicalRoot, relative(root, absolute)))
132
- files.push(`./${sandbox ? file : file.split(sep).join('/')}`)
147
+ files.push(
148
+ sandbox
149
+ ? relativePosix(sandbox.rootDir, absolute)
150
+ : hostDisplayPath(
151
+ context.workingDirectory,
152
+ join(lexicalRoot, relative(root, absolute)),
153
+ ),
154
+ )
133
155
  }
134
156
  } catch (error) {
135
157
  if (context.abortSignal?.aborted) throw error
136
- truncated = true
137
- failure = error instanceof Error ? error.message : String(error)
158
+ if (budget.expired()) timedOut = true
159
+ else if (isEntryLimit(error)) legacyEntryLimit = true
160
+ else failure = error instanceof Error ? error.message : String(error)
161
+ } finally {
162
+ budget.close()
138
163
  }
139
- const notice = failure
140
- ? `[Search incomplete: ${failure}]`
141
- : truncated
142
- ? `[Showing the first ${MAX_GLOB_RESULTS} matching files. Narrow the directory or pattern for the remaining results.]`
143
- : undefined
164
+ const endState = summary as SandboxWalkSummary | undefined
165
+ const entryLimit = endState?.stopped === 'entry-limit' || legacyEntryLimit
166
+ const incomplete = timedOut || entryLimit
167
+ if (timedOut && files.length === 0) {
168
+ return {
169
+ success: false,
170
+ output: '',
171
+ error: `Search timed out after ${SEARCH_BUDGET_MS / 1000} seconds with no matches. Try a more specific path or pattern.`,
172
+ }
173
+ }
174
+ const notices = [
175
+ ...(capped
176
+ ? [
177
+ `(Showing the first ${plural(MAX_GLOB_RESULTS, 'matching file')}; more match. Narrow the pattern or path to see the rest.)`,
178
+ ]
179
+ : []),
180
+ ...(incomplete ? [stoppedNotice(timedOut ? 'time' : 'entries', endState?.visited)] : []),
181
+ ...(failure ? [`[Search incomplete: ${failure}]`] : []),
182
+ ]
183
+ const ignored = endState?.ignoredDirectories ?? 0
184
+ const hint = ignored > 0 ? `\n${ignoredHint(ignored)}` : ''
185
+ const empty = `No files found matching pattern "${input.pattern}" in ${root}${hint}`
144
186
  return {
145
187
  success: failure === undefined,
146
- output:
147
- [...files.map(displayPath), ...(notice ? [notice] : [])].join('\n') ||
148
- `No files found matching pattern "${input.pattern}" in ${root}`,
149
- data: { count: files.length, files, truncated, ...(sandbox ? { sandboxed: true } : {}) },
188
+ output: [...files.map(quoteControlPath), ...notices].join('\n') || empty,
189
+ data: {
190
+ count: files.length,
191
+ files,
192
+ truncated: capped || incomplete,
193
+ ...(sandbox ? { sandboxed: true } : {}),
194
+ },
150
195
  facts: {
151
196
  kind: 'list',
152
197
  pattern: input.pattern,
153
198
  ...(input.path !== undefined ? { path: input.path } : {}),
154
199
  count: files.length,
155
- truncated,
200
+ truncated: capped || incomplete,
201
+ ...(capped ? { limited: true } : {}),
202
+ ...(incomplete ? { incomplete: true } : {}),
156
203
  },
157
204
  ...(failure ? { error: failure } : {}),
158
205
  }
@@ -2,7 +2,7 @@ import { stat } from 'node:fs/promises'
2
2
  import { isAbsolute, join, relative, sep } from 'node:path'
3
3
  import { z } from 'zod'
4
4
  import { walkFilesLocally } from '../../sandbox/file-walk.js'
5
- import type { SandboxWalkFilesOptions } from '../../types/sandbox/index.js'
5
+ import type { SandboxWalkFilesOptions, SandboxWalkSummary } from '../../types/sandbox/index.js'
6
6
  import type { ToolContext } from '../../types/tool/index.js'
7
7
  import { subscribeToAbort } from '../../utils/abort.js'
8
8
  import { defineTool } from '../defineTool.js'
@@ -15,6 +15,17 @@ import {
15
15
  } from '../paths.js'
16
16
  import { relativePosix, resolveWithinPosix } from '../posix-path.js'
17
17
  import { readBoundedChunks, readBoundedFile } from './bounded-file-read.js'
18
+ import {
19
+ MAX_FILES_SEARCHED,
20
+ SEARCH_BUDGET_MS,
21
+ hostDisplayPath,
22
+ ignoredHint,
23
+ isEntryLimit,
24
+ isReadDenied,
25
+ openSearchBudget,
26
+ plural,
27
+ stoppedNotice,
28
+ } from './search-support.js'
18
29
 
19
30
  /** A remote read has no signal parameter; stop consuming it when the turn ends. */
20
31
  async function readWithSignal(read: () => Promise<Buffer>, signal?: AbortSignal): Promise<Buffer> {
@@ -166,6 +177,18 @@ const inputSchema = z.object({
166
177
  .describe(
167
178
  'Only search files of this kind, e.g. "js", "ts", "py", "rust", "go", "java", "md". Narrows what include selects.',
168
179
  ),
180
+ include_hidden: z
181
+ .boolean()
182
+ .optional()
183
+ .describe(
184
+ 'Search hidden files and directories (names starting with a dot). Default: true. Version-control directories are always skipped.',
185
+ ),
186
+ include_ignored: z
187
+ .boolean()
188
+ .optional()
189
+ .describe(
190
+ 'Also search files that .gitignore and .ignore rules exclude, such as build output and dependencies. Default: false.',
191
+ ),
169
192
  max_results: z.coerce
170
193
  .number()
171
194
  .int()
@@ -176,7 +199,6 @@ const inputSchema = z.object({
176
199
 
177
200
  const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB — skip binaries/large files
178
201
  const BINARY_CHECK_BYTES = 512
179
- const MAX_VISITED_ENTRIES = 20_000
180
202
 
181
203
  function isBinaryContent(buffer: Buffer): boolean {
182
204
  const check = buffer.subarray(0, BINARY_CHECK_BYTES)
@@ -189,7 +211,7 @@ function isBinaryContent(buffer: Buffer): boolean {
189
211
  export const GrepTool = defineTool({
190
212
  name: 'grep',
191
213
  description:
192
- 'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches with a bounded traversal and identifies incomplete searches. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
214
+ 'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches hidden files unless include_hidden is false. Skips files that .gitignore and .ignore rules exclude unless include_ignored is set, and files the read rules of the session deny. Says when a result is limited or a search stopped early. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
193
215
  inputSchema,
194
216
  category: 'analysis',
195
217
  // Declared so a path outside the turn's roots can be reviewed before the
@@ -256,14 +278,18 @@ export const GrepTool = defineTool({
256
278
  'This sandbox does not support bounded file discovery (Sandbox.walkFiles). Update its adapter before searching file contents.',
257
279
  }
258
280
  }
281
+ const budget = openSearchBudget(context.abortSignal)
282
+ const signal = budget.signal
283
+ let walkEnd: SandboxWalkSummary | undefined
259
284
  const options: SandboxWalkFilesOptions = {
260
285
  pattern: filePattern,
261
- signal: context.abortSignal,
262
- maxEntries: MAX_VISITED_ENTRIES + 1,
263
- maxVisitedEntries: MAX_VISITED_ENTRIES,
264
- // Preserve existing source behavior: sandbox grep included dotfiles;
265
- // node's host glob excluded wildcard dotfiles.
266
- includeHidden: sandbox !== undefined,
286
+ signal,
287
+ maxEntries: MAX_FILES_SEARCHED + 1,
288
+ includeHidden: input.include_hidden ?? true,
289
+ respectIgnore: input.include_ignored !== true,
290
+ onSummary: (value) => {
291
+ walkEnd = value
292
+ },
267
293
  }
268
294
  const entries = walkSandbox ? walkSandbox(root, options) : walkFilesLocally(root, options)
269
295
 
@@ -282,24 +308,33 @@ export const GrepTool = defineTool({
282
308
  let filesSearched = 0
283
309
  let filesMatched = 0
284
310
 
285
- let truncated = false
286
311
  let pageHasMore = false
287
312
  let failure: string | undefined
288
313
  let unreadableFiles = 0
314
+ let timedOut = false
315
+ let legacyEntryLimit = false
316
+ let fileLimit = false
317
+ // A file list is ordered newest first, so the walk cannot stop at the
318
+ // first page: it reads a generous multiple of it, then sorts and pages.
319
+ // The count includes the offset, so every page the model is told to ask
320
+ // for can be served.
321
+ const filesToCollect =
322
+ headLimit > 0 ? offset + Math.max(headLimit * 4, 1000) : Number.POSITIVE_INFINITY
323
+ let enough = false
289
324
  try {
290
325
  for await (const entry of entries) {
291
- context.abortSignal?.throwIfAborted()
326
+ signal?.throwIfAborted()
292
327
  const filePath = sandbox
293
328
  ? resolveWithinPosix(root, entry.path)
294
329
  : resolveWithin(root, entry.path)
295
- if (filesSearched >= MAX_VISITED_ENTRIES) {
296
- throw new Error(
297
- `File search stopped after examining ${MAX_VISITED_ENTRIES} files; narrow its root or include pattern.`,
298
- )
330
+ if (filesSearched >= MAX_FILES_SEARCHED) {
331
+ fileLimit = true
332
+ break
299
333
  }
300
334
  filesSearched++
301
335
  if (fileType && !matchesType(entry.path, fileType)) continue
302
336
  if (entry.size > MAX_FILE_SIZE) continue
337
+ if (isReadDenied(context, filePath, sandbox?.rootDir)) continue
303
338
 
304
339
  let content: string
305
340
  let contained: string | undefined
@@ -309,21 +344,21 @@ export const GrepTool = defineTool({
309
344
  sandbox
310
345
  ? readBoundedChunks(
311
346
  (offset, length) =>
312
- sandbox.readFile(filePath, { offset, length, signal: context.abortSignal }),
347
+ sandbox.readFile(filePath, { offset, length, signal: signal }),
313
348
  MAX_FILE_SIZE,
314
- context.abortSignal,
349
+ signal,
315
350
  )
316
351
  : resolveWithinReal(root, filePath).then((path) => {
317
352
  contained = path
318
- return readBoundedFile(path, MAX_FILE_SIZE, context.abortSignal)
353
+ return readBoundedFile(path, MAX_FILE_SIZE, signal)
319
354
  }),
320
- context.abortSignal,
355
+ signal,
321
356
  )
322
- context.abortSignal?.throwIfAborted()
357
+ signal?.throwIfAborted()
323
358
  if (buffer.length > MAX_FILE_SIZE || isBinaryContent(buffer)) continue
324
359
  content = buffer.toString('utf-8')
325
360
  } catch (error) {
326
- if (context.abortSignal?.aborted) throw error
361
+ if (signal?.aborted) throw error
327
362
  unreadableFiles++
328
363
  continue
329
364
  }
@@ -331,14 +366,12 @@ export const GrepTool = defineTool({
331
366
  const lines = content.split('\n')
332
367
  let fileMatches = 0
333
368
  const relPath = sandbox
334
- ? `./${relativePosix(sandbox.rootDir, filePath)}`
335
- : `./${relative(context.workingDirectory, join(lexicalRoot, relative(root, filePath)))
336
- .split(sep)
337
- .join('/')}`
369
+ ? relativePosix(sandbox.rootDir, filePath)
370
+ : hostDisplayPath(context.workingDirectory, join(lexicalRoot, relative(root, filePath)))
338
371
 
339
372
  if (mode === 'content' && input.only_matching) {
340
373
  for (const fragment of matchingFragments(content, regex, multiline)) {
341
- context.abortSignal?.throwIfAborted()
374
+ signal?.throwIfAborted()
342
375
  if (fileMatches++ === 0) filesMatched++
343
376
  totalMatches++
344
377
  results.push(`${relPath}:${fragment.line}:${clipLine(fragment.text, 'matching')}`)
@@ -356,7 +389,7 @@ export const GrepTool = defineTool({
356
389
  // Count mode reports matching lines, so a line covered twice counts once.
357
390
  const countedLines = new Set<number>()
358
391
  for (const [first, last] of hits) {
359
- context.abortSignal?.throwIfAborted()
392
+ signal?.throwIfAborted()
360
393
  if (fileMatches === 0) filesMatched++
361
394
  fileMatches++
362
395
  totalMatches++
@@ -397,16 +430,32 @@ export const GrepTool = defineTool({
397
430
  })
398
431
  }
399
432
  if (pageHasMore) break
433
+ if (mode === 'files_with_matches' && matchedFiles.length >= filesToCollect) {
434
+ enough = true
435
+ break
436
+ }
400
437
  }
401
438
  } catch (error) {
402
- truncated = true
403
- failure = (error instanceof Error ? error.message : String(error)) || 'File search failed.'
439
+ // A caller's cancellation is reported with the matches already found:
440
+ // grep-search-scope.test.ts pins that, so a cancelled search keeps what it read.
441
+ if (budget.expired()) timedOut = true
442
+ else if (isEntryLimit(error)) legacyEntryLimit = true
443
+ else
444
+ failure = (error instanceof Error ? error.message : String(error)) || 'File search failed.'
445
+ } finally {
446
+ budget.close()
404
447
  }
405
- if (unreadableFiles > 0) {
406
- truncated = true
407
- failure ??= `${unreadableFiles} file(s) could not be read.`
448
+ const endState = walkEnd as SandboxWalkSummary | undefined
449
+ const entryLimit = endState?.stopped === 'entry-limit' || legacyEntryLimit
450
+ const stoppedEarly = timedOut || entryLimit || fileLimit
451
+ if (timedOut && totalMatches === 0 && failure === undefined) {
452
+ return {
453
+ success: false,
454
+ output: '',
455
+ error: `Search timed out after ${SEARCH_BUDGET_MS / 1000} seconds with no matches. Try a more specific path or pattern.`,
456
+ }
408
457
  }
409
- if (pageHasMore) truncated = true
458
+ const truncated = stoppedEarly || pageHasMore || enough || failure !== undefined
410
459
 
411
460
  // Entries as the model will page through them.
412
461
  let entriesOut: string[]
@@ -422,7 +471,7 @@ export const GrepTool = defineTool({
422
471
  headLimit === 0 ? entriesOut.slice(offset) : entriesOut.slice(offset, offset + headLimit)
423
472
  // Reported only when it did something, like a `| head`: a limit that cut
424
473
  // nothing off is not news.
425
- const moreRemain = headLimit > 0 && (pageHasMore || total - offset > headLimit)
474
+ const moreRemain = headLimit > 0 && (pageHasMore || enough || total - offset > headLimit)
426
475
  const paging = [
427
476
  ...(moreRemain ? [`limit: ${headLimit}`] : []),
428
477
  ...(offset > 0 ? [`offset: ${offset}`] : []),
@@ -435,9 +484,12 @@ export const GrepTool = defineTool({
435
484
  summary =
436
485
  offset > 0 && total > 0
437
486
  ? 'No entries at this offset.'
438
- : truncated
487
+ : stoppedEarly || failure !== undefined || unreadableFiles > 0
439
488
  ? `No matches found in the files searched for pattern "${input.pattern}"`
440
489
  : `No matches found for pattern "${input.pattern}"`
490
+ if ((endState?.ignoredDirectories ?? 0) > 0 && offset === 0) {
491
+ summary = `${summary}\n${ignoredHint(endState?.ignoredDirectories ?? 0)}`
492
+ }
441
493
  } else if (mode === 'files_with_matches') {
442
494
  body = `Found ${page.length} ${page.length === 1 ? 'file' : 'files'}${paging ? ` (${paging})` : ''}\n${page.join('\n')}`
443
495
  } else if (mode === 'count') {
@@ -447,21 +499,31 @@ export const GrepTool = defineTool({
447
499
  body = page.join('\n')
448
500
  summary = `Found ${totalMatches} match(es) in ${filesMatched} file(s) (${filesSearched} files searched)${offset > 0 ? ` from offset ${offset}` : ''}`
449
501
  }
450
- const notice = failure
451
- ? `[Search incomplete: ${failure}]`
452
- : pageHasMore
453
- ? `[Search incomplete: the page of ${headLimit} is full, so more may follow. Pass offset to continue, or narrow the directory or include pattern.]`
454
- : truncated
455
- ? '[Search incomplete: the search did not reach every file. Narrow the directory or include pattern for a complete search.]'
456
- : undefined
502
+ const notices = [
503
+ ...(moreRemain
504
+ ? [
505
+ `[More results are available: pass offset: ${offset + headLimit} to continue, or narrow the path or pattern.${enough ? ` Files are ordered newest first among the first ${filesToCollect.toLocaleString('en-US')} that matched.` : ''}]`,
506
+ ]
507
+ : []),
508
+ ...(stoppedEarly
509
+ ? [stoppedNotice(timedOut ? 'time' : entryLimit ? 'entries' : 'files', endState?.visited)]
510
+ : []),
511
+ ...(unreadableFiles > 0
512
+ ? [
513
+ `(${plural(unreadableFiles, 'file')} could not be read and ${unreadableFiles === 1 ? 'was' : 'were'} skipped.)`,
514
+ ]
515
+ : []),
516
+ ...(failure ? [`[Search incomplete: ${failure}]`] : []),
517
+ ]
457
518
  return {
458
519
  success: failure === undefined,
459
- output: [body, summary, notice].filter(Boolean).join('\n\n'),
520
+ output: [body, summary, ...notices].filter(Boolean).join('\n\n'),
460
521
  data: {
461
522
  totalMatches,
462
523
  filesSearched,
463
524
  filesMatched,
464
525
  truncated,
526
+ ...(stoppedEarly ? { incomplete: true } : {}),
465
527
  mode,
466
528
  ...(moreRemain ? { appliedLimit: headLimit } : {}),
467
529
  ...(offset > 0 ? { appliedOffset: offset } : {}),
@@ -474,6 +536,8 @@ export const GrepTool = defineTool({
474
536
  filesMatched,
475
537
  filesSearched,
476
538
  truncated,
539
+ ...(moreRemain ? { limited: true } : {}),
540
+ ...(stoppedEarly ? { incomplete: true } : {}),
477
541
  },
478
542
  ...(failure ? { error: failure } : {}),
479
543
  }
@@ -1,6 +1,11 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import { invalidBackgroundJobIdInput } from '../../runtime/jobs/job-id.js'
4
+ import {
5
+ formatLivenessAge,
6
+ formatLivenessLine,
7
+ judgeLiveness,
8
+ } from '../../runtime/jobs/liveness.js'
4
9
  import { defineTool } from '../defineTool.js'
5
10
 
6
11
  /**
@@ -21,7 +26,7 @@ const inputSchema = z.object({
21
26
  action: z
22
27
  .enum(['read', 'list', 'kill'])
23
28
  .describe(
24
- 'read: output since `from_offset`. list: every background job this turn started, with status. kill: stop a job and everything it forked.',
29
+ 'read: output since `from_offset`. list: every background job this turn started, each running one with whether it is progressing, waiting or stalled, how long since its last output, and its last output line. kill: stop a job and everything it forked.',
25
30
  ),
26
31
  id: z
27
32
  .string()
@@ -76,18 +81,33 @@ export const JobTool = defineTool({
76
81
  if (input.action === 'list') {
77
82
  const all = jobs.list()
78
83
  if (all.length === 0) return { success: true, output: 'No background jobs.' }
84
+ const now = Date.now()
85
+ const lines = all.map((job) => {
86
+ const label = job.kind === 'monitor' ? (job.description ?? job.command) : job.command
87
+ if (job.startedAt === undefined) {
88
+ return `${job.id} ${job.status.padEnd(8)} ${job.kind === 'monitor' ? `monitor · ${label}` : label}`
89
+ }
90
+ const liveness = judgeLiveness(
91
+ {
92
+ kind: job.kind === 'monitor' ? 'monitor' : 'job',
93
+ id: job.id,
94
+ label,
95
+ ...(job.lastOutputLine !== undefined ? { where: job.lastOutputLine } : {}),
96
+ startedAt: job.startedAt,
97
+ ...(job.lastOutputAt !== undefined ? { lastProgressAt: job.lastOutputAt } : {}),
98
+ alive: job.status === 'running' && (jobs.isAlive ? jobs.isAlive(job.id) : true),
99
+ finished: job.status !== 'running',
100
+ },
101
+ now,
102
+ )
103
+ // An ended job keeps its status word, which carries the exit.
104
+ return job.status === 'running'
105
+ ? formatLivenessLine(liveness)
106
+ : `${job.id} ${job.status.padEnd(8)} ${label}`
107
+ })
79
108
  return {
80
109
  success: true,
81
- // A monitor is named by what it watches: its command is the means,
82
- // and the description is what the model wrote to tell its watches apart.
83
- output: all
84
- .map(
85
- (job) =>
86
- `${job.id} ${job.status.padEnd(8)} ${
87
- job.kind === 'monitor' ? `monitor · ${job.description ?? job.command}` : job.command
88
- }`,
89
- )
90
- .join('\n'),
110
+ output: lines.join('\n'),
91
111
  data: { jobs: all },
92
112
  }
93
113
  }
@@ -116,6 +136,11 @@ export const JobTool = defineTool({
116
136
  const read = jobs.read(input.id, {
117
137
  ...(input.from_offset === undefined ? {} : { fromOffset: input.from_offset }),
118
138
  })
139
+ // A stopped job read to its end is collected: its exit notice would only
140
+ // repeat what this result says. A read always reaches the end of the
141
+ // output; what it can miss is bytes the retention cap dropped, and then
142
+ // the notice stays.
143
+ if (read.status !== 'running' && read.droppedBytes === 0) jobs.markCollected?.(input.id)
119
144
  // The dropped count is stated, never absorbed. A job whose middle
120
145
  // vanished quietly reads as a complete result that happens to be
121
146
  // short, and the model concludes the build passed.
@@ -123,9 +148,14 @@ export const JobTool = defineTool({
123
148
  read.droppedBytes > 0
124
149
  ? `[${read.droppedBytes} bytes were dropped before this point — the job produced output faster than the retention cap holds]\n`
125
150
  : ''
151
+ const lastOutputAt = read.status === 'running' ? jobs.get(input.id)?.lastOutputAt : undefined
126
152
  const status =
127
153
  read.status === 'running'
128
- ? 'still running'
154
+ ? `still running${
155
+ lastOutputAt === undefined
156
+ ? '; no output yet'
157
+ : `; last output ${formatLivenessAge((Date.now() - lastOutputAt) / 1000)} ago`
158
+ }`
129
159
  : read.error !== undefined
130
160
  ? `failed to start: ${read.error}`
131
161
  : `${read.status}${read.exitCode === undefined ? '' : ` with code ${read.exitCode}`}`
@@ -0,0 +1,107 @@
1
+ import { isAbsolute, join as joinPath, posix, relative, sep } from 'node:path'
2
+
3
+ import { subscribeToAbort } from '../../utils/abort.js'
4
+
5
+ /**
6
+ * How long grep and glob search before they return what they have. The tool
7
+ * deadline (15 s) cancels a call outright and discards its output; a search
8
+ * that ends itself a little earlier can still hand back its matches.
9
+ */
10
+ export const SEARCH_BUDGET_MS = 12_000
11
+
12
+ /** Matching files a search reads at most; past it the search ends with what it has. */
13
+ export const MAX_FILES_SEARCHED = 50_000
14
+
15
+ export interface SearchBudget {
16
+ /** Fires when the caller cancels or the budget runs out. */
17
+ readonly signal: AbortSignal
18
+ /** True only when the budget ran out, not when the caller cancelled. */
19
+ expired(): boolean
20
+ close(): void
21
+ }
22
+
23
+ /** A signal that follows `parent` and also fires when the search budget is spent. */
24
+ export function openSearchBudget(parent?: AbortSignal, budgetMs = SEARCH_BUDGET_MS): SearchBudget {
25
+ const controller = new AbortController()
26
+ let spent = false
27
+ const stopWatching = parent
28
+ ? subscribeToAbort(parent, () => controller.abort(parent.reason))
29
+ : undefined
30
+ const timer = setTimeout(() => {
31
+ spent = true
32
+ controller.abort(new Error('search budget spent'))
33
+ }, budgetMs)
34
+ return {
35
+ signal: controller.signal,
36
+ expired: () => spent && !parent?.aborted,
37
+ close() {
38
+ clearTimeout(timer)
39
+ stopWatching?.()
40
+ },
41
+ }
42
+ }
43
+
44
+ /**
45
+ * A file as the model sees it: relative to the working directory when it lies
46
+ * under it, otherwise absolute. Never prefixed with `./`.
47
+ */
48
+ export function hostDisplayPath(workingDirectory: string, absolute: string): string {
49
+ const rel = relative(workingDirectory, absolute)
50
+ if (rel === '') return '.'
51
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return absolute
52
+ return sep === '/' ? rel : rel.split(sep).join('/')
53
+ }
54
+
55
+ /** Keep unusual filenames unambiguous without changing the returned path data. */
56
+ export function quoteControlPath(path: string): string {
57
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: quote filenames containing control bytes instead of creating fake result lines.
58
+ return /[\x00-\x1f\x7f]/.test(path) ? JSON.stringify(path) : path
59
+ }
60
+
61
+ export function plural(count: number, one: string, many = `${one}s`): string {
62
+ return `${count.toLocaleString('en-US')} ${count === 1 ? one : many}`
63
+ }
64
+
65
+ /** The line that tells the model why a search stopped before it finished. */
66
+ export function stoppedNotice(reason: 'time' | 'entries' | 'files', visited?: number): string {
67
+ const how =
68
+ reason === 'time'
69
+ ? `after ${SEARCH_BUDGET_MS / 1000} seconds`
70
+ : reason === 'entries'
71
+ ? `after examining ${(visited ?? 0).toLocaleString('en-US')} entries`
72
+ : `after reading ${MAX_FILES_SEARCHED.toLocaleString('en-US')} files`
73
+ return `[Search stopped early ${how}. Results may be incomplete; narrow the path or pattern, or add an ignore rule.]`
74
+ }
75
+
76
+ /** Tells the model that an empty result may hide files an ignore rule excluded. */
77
+ export function ignoredHint(ignoredDirectories: number): string {
78
+ return `(${plural(ignoredDirectories, 'ignored directory', 'ignored directories')} skipped by ignore rules; pass include_ignored: true to search ${ignoredDirectories === 1 ? 'it' : 'them'}.)`
79
+ }
80
+
81
+ /** A walker written before the entry bound became a normal stop still throws this. */
82
+ export function isEntryLimit(error: unknown): boolean {
83
+ return (error as { code?: unknown } | null)?.code === 'ERR_FILE_WALK_LIMIT'
84
+ }
85
+
86
+ /**
87
+ * Whether the session's read rules deny this file, so a search neither lists
88
+ * nor opens it. `path` is absolute: a host path, or, when `sandboxRoot` is
89
+ * given, a path inside the sandbox, which is mapped to the same place under
90
+ * the working directory because that is the folder the sandbox binds; a path
91
+ * outside that folder is denied. A host
92
+ * that supplies no rules (`ToolContext.readDenied` absent) denies nothing.
93
+ */
94
+ export function isReadDenied(
95
+ context: { workingDirectory: string; readDenied?: (path: string) => boolean },
96
+ path: string,
97
+ sandboxRoot?: string,
98
+ ): boolean {
99
+ if (!context.readDenied) return false
100
+ if (sandboxRoot === undefined) return context.readDenied(path)
101
+ const relative = posix.relative(sandboxRoot, path)
102
+ // A guest path outside the bound folder has no host counterpart the rules
103
+ // could be checked against, so a session that has read rules does not list
104
+ // or open it.
105
+ if (relative === '..' || relative.startsWith('../') || posix.isAbsolute(relative)) return true
106
+ return context.readDenied(joinPath(context.workingDirectory, relative))
107
+ }