@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,318 @@
1
+ /**
2
+ * The round-trip loop and turn-end phase of the native turn loop.
3
+ *
4
+ * Split out of `./turn-loop.ts` during the complexity drain
5
+ * (docs/plans/STATUS-complexity-drain.md A2): `runNativeTurn` scored 97 as one
6
+ * function covering setup, the round-trip loop (deliberately unbounded —
7
+ * bounded by the deadline, the idle watchdog, and the spin breaker, not a
8
+ * call count), and the `before_turn_end` continuation decision. Both loop
9
+ * bodies move here unchanged; only the plumbing that threads state across
10
+ * calls changed shape. A pure extraction: no loop rule changed.
11
+ *
12
+ * `runRoundTripLoop` mutates its `state` argument in place rather than
13
+ * returning a new one, and does so on every field the moment each value is
14
+ * known — not only at a clean exit. The original was one function where
15
+ * `messages` (and friends) were plain `let`s: a mid-loop throw (the
16
+ * `batch.cancelled` path below) still left the catch block reading the
17
+ * latest values, because it was the same variable. Returning a fresh object
18
+ * only at the end of the function would lose every update since the last
19
+ * return the moment a throw skips it — exactly the synthetic tool-result
20
+ * messages `runToolBatch` just appended before signalling cancellation. The
21
+ * caller passes one `TurnLoopState` and keeps reading through that same
22
+ * reference after an `await` that might throw.
23
+ *
24
+ * `spinStopped` / `spinWarned` were two `let`s in `runNativeTurn`, one of them
25
+ * (`spinWarned`) mutated by an `onSpinStop` closure registered ONCE in setup,
26
+ * before either extracted function is ever called, and read/written across
27
+ * every outer-loop iteration after that. Splitting the loop body out cannot
28
+ * split that closure's target too, so both flags now live in one mutable
29
+ * `SpinFlags` object created in setup and threaded by reference into every
30
+ * call here — the same shared-mutable-state semantics as the original two
31
+ * `let`s, just addressed through an object instead of two closed-over
32
+ * bindings.
33
+ */
34
+ import { inputClassTokens } from "#src/cost/core/index";
35
+ import { getSafeLogger } from "#src/infra/index";
36
+ import { estimateContextTokens, shouldCompact, } from "./compaction.js";
37
+ import { nativeSessionLastUsage } from "./session.js";
38
+ import { usageBeat } from "./turn-accumulator.js";
39
+ import { runProactiveCompaction } from "./turn-compaction-step.js";
40
+ import { completeWithRecovery } from "./turn-complete-step.js";
41
+ import { runToolBatch } from "./turn-tool-batch.js";
42
+ /**
43
+ * The per-turn bound on `before_turn_end`'s followUp channel (spec 6.4): the
44
+ * only event that can spend money on its own, so injections are capped per
45
+ * turn regardless of what handlers return. At the cap the channel stops and
46
+ * the turn ends.
47
+ */
48
+ export const MAX_FOLLOW_UPS_PER_TURN = 3;
49
+ /**
50
+ * Compaction runs at most once per round trip. That bound is what stops a
51
+ * compact-still-over-compact loop when the pinned prompt alone is too large.
52
+ * The returned `summarizeFailed` is read by the overflow-retry backstop
53
+ * (`canRetry = ... && !summarizeFailed && ...`) inside `completeWithRecovery`,
54
+ * which suppresses a doomed retry after the summarizer has already failed
55
+ * this round trip.
56
+ */
57
+ async function maybeCompact(state, params) {
58
+ const { handle, opts, deps, loopEvents } = params;
59
+ // Guard clauses, not one big `&&`: each keeps TS's narrowing of deps.summarize
60
+ // / contextWindow / compaction live for the `deps: {...}` literal below,
61
+ // which a hoisted boolean would have thrown away.
62
+ if (deps.summarize === undefined || deps.contextWindow === undefined || deps.compaction === undefined) {
63
+ return { summarizeFailed: false };
64
+ }
65
+ if (!shouldCompact(estimateContextTokens(state.messages, state.lastUsage, state.anchorIndex), deps.contextWindow, deps.compaction)) {
66
+ return { summarizeFailed: false };
67
+ }
68
+ const step = await runProactiveCompaction({
69
+ messages: state.messages,
70
+ usage: params.usage,
71
+ sessionName: handle.id,
72
+ lastUsage: state.lastUsage,
73
+ anchorIndex: state.anchorIndex,
74
+ // The loop's local, not deps.loopEvents — the same registry
75
+ // completeWithRecovery and runToolBatch receive below, so every
76
+ // dispatch seam observes the same handlers.
77
+ loopEvents,
78
+ // Copied, not the `deps` object itself: the guards above narrow the
79
+ // three properties to defined, and a fresh object is what carries that
80
+ // narrowing into the step's `CompactionStepDeps` parameter.
81
+ deps: {
82
+ summarize: deps.summarize,
83
+ contextWindow: deps.contextWindow,
84
+ compaction: deps.compaction,
85
+ onActivity: deps.onActivity,
86
+ deadline: deps.deadline,
87
+ },
88
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
89
+ });
90
+ state.messages = [...step.messages];
91
+ if (step.compacted) {
92
+ // The anchor described the pre-compaction array; it is meaningless now.
93
+ state.lastUsage = undefined;
94
+ state.anchorIndex = undefined;
95
+ }
96
+ return { summarizeFailed: step.summarizeFailed };
97
+ }
98
+ /**
99
+ * One model call and its bookkeeping: `completeWithRecovery`, the usage/anchor
100
+ * updates, the activity beats, `after_response`, and pushing the settled
101
+ * assistant message. Returns the tool calls the model asked for, if any —
102
+ * `undefined` means the turn completed normally (mutates
103
+ * `state.completedNormally`).
104
+ */
105
+ async function runModelRoundTrip(state, params, summarizeFailed) {
106
+ const { handle, opts, deps, tools, loopEvents } = params;
107
+ const step = await completeWithRecovery({
108
+ messages: state.messages,
109
+ tools,
110
+ usage: params.usage,
111
+ summarizeFailed,
112
+ sessionName: handle.id,
113
+ lastUsage: state.lastUsage,
114
+ anchorIndex: state.anchorIndex,
115
+ // The loop's local, not deps.loopEvents — the same registry runToolBatch
116
+ // below receives, so both dispatch seams observe the same handlers.
117
+ loopEvents,
118
+ // 1-based, matching the usage beat below: the request being issued is
119
+ // round trip roundTrips + 1 — before_request(0-based N) and a later
120
+ // after_response(1-based N) would otherwise disagree on the same trip.
121
+ roundTrip: state.roundTrips + 1,
122
+ ...(handle.modelDef?.model !== undefined ? { model: handle.modelDef.model } : {}),
123
+ deps,
124
+ ...(opts.signal !== undefined ? { signal: opts.signal } : {}),
125
+ });
126
+ const res = step.res;
127
+ state.messages = [...step.messages];
128
+ // The anchor described the pre-compaction array (compacted), or a
129
+ // prefix the provider was never sent (a boundary-exempt transform_context
130
+ // rewrite — spec 3.6: the wire changed even though the saved array did
131
+ // not); it is meaningless now. Compaction invalidates it unconditionally;
132
+ // a transform_context honour clears only when it rode the boundary
133
+ // exemption — a prefix-stable honour leaves the anchor valid (spec 6.6:
134
+ // wire and persisted prefix are reference-identical, nothing to invalidate).
135
+ if (step.compacted || (step.honoured && step.boundary)) {
136
+ state.lastUsage = undefined;
137
+ state.anchorIndex = undefined;
138
+ }
139
+ state.roundTrips += 1;
140
+ params.usage.add(res.usage, res.costUsd, res.rates);
141
+ state.output = res.text;
142
+ // nax#1852: the anchor is the whole prompt the provider charged for, not
143
+ // just its uncached portion. Under prompt caching (which the round trip
144
+ // above always requests) the cached prefix arrives in the cache fields,
145
+ // and counting inputTokens alone reads a 71k-token context as ~16.
146
+ const promptTokens = inputClassTokens(res.usage);
147
+ state.lastUsage = { promptTokens };
148
+ state.anchorIndex = state.messages.length - 1;
149
+ nativeSessionLastUsage.set(handle.id, {
150
+ promptTokens,
151
+ anchorIndex: state.anchorIndex,
152
+ ...(params.transcriptModel !== undefined ? { model: params.transcriptModel } : {}),
153
+ });
154
+ // 1-based; `roundTrips` is incremented above, before this beat fires.
155
+ deps.onActivity?.(usageBeat(res.usage, res.costUsd, state.roundTrips));
156
+ if (res.text.length > 0)
157
+ deps.onActivity?.({ kind: "message", bytes: res.text.length });
158
+ if (res.thinking !== undefined && res.thinking.length > 0) {
159
+ deps.onActivity?.({
160
+ kind: "thinking",
161
+ bytes: res.thinking.reduce((n, t) => n + t.text.length, 0),
162
+ });
163
+ }
164
+ // P3 `after_response` (spec 6.1): fires once per round trip on the
165
+ // settled assistant message, BEFORE it enters the array — a patch is
166
+ // safe by construction because it shapes the message, never the array,
167
+ // so the anchorIndex recorded above (`messages.length - 1`, which runs
168
+ // before this push) keeps describing the prefix the provider charged
169
+ // for. `usage`/`costUsd` are surfaced readonly: the registry reads only
170
+ // the patchable fields off a return, so billing truth cannot surface
171
+ // even from a handler that bypasses the type.
172
+ const afterResponse = await loopEvents.dispatch("after_response", {
173
+ text: res.text,
174
+ ...(res.toolCalls !== undefined ? { toolCalls: res.toolCalls } : {}),
175
+ ...(res.thinking !== undefined ? { thinking: res.thinking } : {}),
176
+ usage: res.usage,
177
+ costUsd: res.costUsd,
178
+ roundTrip: state.roundTrips,
179
+ });
180
+ // The pushed message carries the patched shape, and the loop acts on
181
+ // what it records: answering the ORIGINAL calls while recording patched
182
+ // ones would desync the transcript from what actually executed.
183
+ const assistantText = afterResponse.text ?? res.text;
184
+ const assistantToolCalls = afterResponse.toolCalls ?? res.toolCalls;
185
+ const assistantThinking = afterResponse.thinking ?? res.thinking;
186
+ // Thinking blocks are appended, not merely representable: Anthropic needs
187
+ // the exact block back to continue a thinking conversation (ADR-028 s8).
188
+ state.messages.push({
189
+ role: "assistant",
190
+ content: assistantText,
191
+ ...(assistantToolCalls !== undefined ? { toolCalls: assistantToolCalls } : {}),
192
+ ...(assistantThinking !== undefined ? { thinking: assistantThinking } : {}),
193
+ });
194
+ if (assistantToolCalls === undefined || assistantToolCalls.length === 0) {
195
+ state.completedNormally = true;
196
+ return undefined;
197
+ }
198
+ return assistantToolCalls;
199
+ }
200
+ /**
201
+ * Answers the round trip's tool calls and decides whether the round-trip loop
202
+ * should keep going. `state` is already up to date (mutated above, not
203
+ * returned — see the file header) the moment `batch.cancelled` throws, so the
204
+ * caller's catch block still sees this batch's messages.
205
+ */
206
+ async function dispatchToolBatch(state, params, toolCalls) {
207
+ const { opts, deps, tools, codingToolNames, loopEvents, invalidCallBudget, spinBreaker, spinFlags, maxInteractions } = params;
208
+ const batch = await runToolBatch({
209
+ messages: state.messages,
210
+ toolCalls,
211
+ tools,
212
+ codingToolNames,
213
+ roundTrips: state.roundTrips,
214
+ opts,
215
+ deps,
216
+ loopEvents,
217
+ invalidCallBudget,
218
+ spinBreaker,
219
+ maxInteractions,
220
+ spinWarned: spinFlags.warned,
221
+ interactionsSoFar: params.interactions.length,
222
+ });
223
+ state.messages = [...batch.messages];
224
+ params.interactions.push(...batch.interactions);
225
+ params.codingToolsCalled.push(...batch.codingToolsCalled);
226
+ if (batch.spinStopped)
227
+ spinFlags.stopped = true;
228
+ if (batch.spinStopped || batch.budgetExceeded)
229
+ return { shouldBreak: true };
230
+ // US-002: a cancelled turn signal during the batch answers every
231
+ // outstanding call synthetically and stops the loop. The throw routes
232
+ // through the existing catch block — its best-effort saveTranscript
233
+ // persists the synthetic results before the throw propagates, keeping one
234
+ // result per assistant id in the saved transcript (AC2 / AC3). AC4 carries
235
+ // the abort reason verbatim; AC6 — a no-reason abort — produces a
236
+ // DOMException named AbortError, the contract `build-hop-callback` and the
237
+ // idle-watchdog key on.
238
+ if (batch.cancelled) {
239
+ throw deps.signal?.reason ?? new DOMException("signal is aborted without reason", "AbortError");
240
+ }
241
+ return { shouldBreak: false };
242
+ }
243
+ /**
244
+ * Deliberately unbounded by COUNT of varied calls. A coding agent working a
245
+ * story is bounded by wall clock (deps.deadline), by the idle watchdog, and
246
+ * — since nax#2013 — by the spin breaker, which ends a turn that keeps
247
+ * REPEATING a call it already made. `agent.maxInteractionTurns` is NOT this
248
+ * budget — it bounds human Q&A exchanges, which are counted separately.
249
+ */
250
+ export async function runRoundTripLoop(state, params) {
251
+ while (true) {
252
+ // Checked before starting a round-trip rather than after finishing one:
253
+ // starting a call we know cannot finish inside the budget spends money for
254
+ // an answer we will discard.
255
+ if (params.deps.deadline?.expired() === true) {
256
+ state.timedOut = true;
257
+ break;
258
+ }
259
+ const { summarizeFailed } = await maybeCompact(state, params);
260
+ const toolCalls = await runModelRoundTrip(state, params, summarizeFailed);
261
+ if (toolCalls === undefined)
262
+ break; // completedNormally already set
263
+ const { shouldBreak } = await dispatchToolBatch(state, params, toolCalls);
264
+ if (shouldBreak)
265
+ break;
266
+ }
267
+ }
268
+ /**
269
+ * P3 `before_turn_end` (spec 6.4): fires at every turn ENDING, before the
270
+ * final saveTranscript. `stopped` is dispatcher-computed: the three endings
271
+ * that are stops, not completions — the spin breaker (nax#2120), the
272
+ * invalid-call budget (nax#2047), and the deadline — close the followUp
273
+ * channel entirely, because resurrecting a turn a breaker just killed would
274
+ * re-open the loops those breakers exist to close. The payload's `stopped`
275
+ * flag is all a handler sees: WHICH breaker fired is not leaked, and any
276
+ * followUp returned against a stop is ignored (with a warn — a handler
277
+ * trying to resurrect is worth a trace). The channel is also capped per turn
278
+ * at MAX_FOLLOW_UPS_PER_TURN.
279
+ */
280
+ export async function runTurnEndPhase(state, params) {
281
+ const stopped = params.spinFlags.stopped || params.invalidCallBudget.exceeded || state.timedOut;
282
+ const turnEnd = await params.loopEvents.dispatch("before_turn_end", {
283
+ messages: state.messages,
284
+ roundTrips: state.roundTrips,
285
+ ended: "completed",
286
+ stopped,
287
+ followUpsSoFar: state.followUpsSoFar,
288
+ });
289
+ if (stopped || state.followUpsSoFar >= MAX_FOLLOW_UPS_PER_TURN) {
290
+ if (turnEnd.followUp !== undefined) {
291
+ getSafeLogger()?.warn("native-loop-events", "before_turn_end followUp ignored", {
292
+ sessionName: params.handle.id,
293
+ ...(stopped ? { reason: "stopped" } : { reason: "cap", cap: MAX_FOLLOW_UPS_PER_TURN }),
294
+ });
295
+ }
296
+ return { action: "break" };
297
+ }
298
+ if (turnEnd.followUp === undefined)
299
+ return { action: "break" };
300
+ // Honoured: the user message enters the array and the outer loop
301
+ // re-enters the round-trip loop — each injection counts as its own
302
+ // round trip through the normal loop, and the turn's result is NOT
303
+ // built here.
304
+ const messages = [...state.messages, { role: "user", content: turnEnd.followUp }];
305
+ // The continuation must earn its own clean exit, and a reprieved stop's
306
+ // terminal-round-trip snapshot does not carry into it — the breaker's
307
+ // own cumulative, session-lifetime counters (nax#2047) still enforce.
308
+ params.spinFlags.warned = false;
309
+ return {
310
+ action: "continue",
311
+ state: {
312
+ ...state,
313
+ messages,
314
+ followUpsSoFar: state.followUpsSoFar + 1,
315
+ completedNormally: false,
316
+ },
317
+ };
318
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The native turn loop.
3
+ *
4
+ * nax owns the conversation, so a turn is: append the prompt, call the model,
5
+ * and while it asks for tools, execute them and call again. Tools are executed
6
+ * through the InteractionHandler the ACP adapter already uses — this file never
7
+ * touches the context engine.
8
+ *
9
+ * The public types this loop consumes (`TurnDeps`, `NativeTurnResponse`,
10
+ * `NativeSummaryResponse`) and the failure-usage ledger
11
+ * (`readNativeTurnFailureUsage` / `recordNativeTurnFailureUsage`) live in
12
+ * `./turn-types.ts`. Splitting them out keeps this file focused on the
13
+ * algorithm and below the 600-line hard limit (project-conventions.md).
14
+ *
15
+ * The round-trip loop and the `before_turn_end` continuation decision live in
16
+ * `./turn-loop-round-trip.ts` (complexity drain A2,
17
+ * docs/plans/STATUS-complexity-drain.md) — this file does setup (transcript
18
+ * load, event registry, `before_turn`), sequences the outer while(true) over
19
+ * those two phases, and handles the error/completion tail.
20
+ */
21
+ import type { SendTurnOpts, SessionHandle, TurnResult } from "#src/session/session-types";
22
+ import { type TurnDeps } from "./turn-types.ts";
23
+ export declare function runNativeTurn(handle: SessionHandle, prompt: string, opts: SendTurnOpts, deps: TurnDeps): Promise<TurnResult>;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * The native turn loop.
3
+ *
4
+ * nax owns the conversation, so a turn is: append the prompt, call the model,
5
+ * and while it asks for tools, execute them and call again. Tools are executed
6
+ * through the InteractionHandler the ACP adapter already uses — this file never
7
+ * touches the context engine.
8
+ *
9
+ * The public types this loop consumes (`TurnDeps`, `NativeTurnResponse`,
10
+ * `NativeSummaryResponse`) and the failure-usage ledger
11
+ * (`readNativeTurnFailureUsage` / `recordNativeTurnFailureUsage`) live in
12
+ * `./turn-types.ts`. Splitting them out keeps this file focused on the
13
+ * algorithm and below the 600-line hard limit (project-conventions.md).
14
+ *
15
+ * The round-trip loop and the `before_turn_end` continuation decision live in
16
+ * `./turn-loop-round-trip.ts` (complexity drain A2,
17
+ * docs/plans/STATUS-complexity-drain.md) — this file does setup (transcript
18
+ * load, event registry, `before_turn`), sequences the outer while(true) over
19
+ * those two phases, and handles the error/completion tail.
20
+ */
21
+ import { getSafeLogger, NaxError } from "#src/infra/index";
22
+ import { askHumanToolDefinition } from "./ask-human.js";
23
+ import { createInvalidCallBudget } from "./handle-invalid-tool-call.js";
24
+ import { applyHistoryPatch } from "./loop-events/cache-boundary.js";
25
+ import { createLoopEventRegistry } from "./loop-events/index.js";
26
+ import { registerBuiltinLoopHandlers } from "./loop-handlers.js";
27
+ import { nativeSessionTranscriptOwners, nativeTranscriptDirs, sessionAnchorFor } from "./session.js";
28
+ import { codingToolsToDefinitions, toToolDefinitions } from "./tool-mapping.js";
29
+ import { loadTranscript, saveTranscript, transcriptModelIdentity, } from "./transcript-store.js";
30
+ import { createTurnAccumulator } from "./turn-accumulator.js";
31
+ import { dispatchTurnEndOnError } from "./turn-end-event.js";
32
+ import { runRoundTripLoop, runTurnEndPhase } from "./turn-loop-round-trip.js";
33
+ import { buildTurnResult, logTurnTailWarnings } from "./turn-result.js";
34
+ import { recordNativeTurnFailureUsage } from "./turn-types.js";
35
+ /**
36
+ * The messages `before_turn` appends as the turn's seed: the handler's patch
37
+ * when it is a non-empty user-role array (spec 6.1's stop rule), otherwise the
38
+ * prompt the caller handed the turn. A bad patch is rejected the way a bad
39
+ * history patch is — warn, original kept (spec 3.7) — because a seed that is
40
+ * empty or speaks with another role hands the model a conversation it cannot
41
+ * answer as itself.
42
+ */
43
+ function beforeTurnSeed(patch, prompt) {
44
+ if (patch === undefined)
45
+ return [{ role: "user", content: prompt }];
46
+ if (patch.length > 0 && patch.every((m) => m.role === "user"))
47
+ return [...patch];
48
+ getSafeLogger()?.warn("native-loop-events", "before_turn seed patch rejected: seed must be a non-empty user-role array", { seedLength: patch.length });
49
+ return [{ role: "user", content: prompt }];
50
+ }
51
+ export async function runNativeTurn(handle, prompt, opts, deps) {
52
+ const dir = nativeTranscriptDirs.get(handle.id);
53
+ if (dir === undefined) {
54
+ throw new NaxError(`no transcript directory for session "${handle.id}"`, "NATIVE_TRANSCRIPT_DIR_MISSING", {
55
+ stage: "native-session",
56
+ });
57
+ }
58
+ // nax#1877: an owner mismatch reads as a new conversation, so an abandoned
59
+ // invocation's history cannot ride along on the first request of this one.
60
+ // nax#2150 (P3 spec 8.3): so does a recorded different model — the store
61
+ // owns that guarantee, whatever the session layer above decided.
62
+ const transcriptIdentity = {
63
+ owner: nativeSessionTranscriptOwners.get(handle.id),
64
+ model: transcriptModelIdentity(handle.modelDef?.model),
65
+ };
66
+ let messages = [...(await loadTranscript(dir, handle.id, transcriptIdentity))];
67
+ // nax#2151: the invalid-call repair and the spin breaker are `before_tool`
68
+ // registrations rather than inline branches. Absent a caller-supplied
69
+ // registry the loop owns one, so both built-ins run exactly as they did
70
+ // before the seam. The block sits above `before_turn` now: the dispatch
71
+ // needs the registry, and the built-ins must be installed before ANY event
72
+ // fires, exactly as they were installed before any round trip ran.
73
+ const spinFlags = { stopped: false, warned: false };
74
+ const invalidCallBudget = createInvalidCallBudget();
75
+ const loopEvents = deps.loopEvents ?? createLoopEventRegistry();
76
+ registerBuiltinLoopHandlers(loopEvents, {
77
+ sessionName: handle.id,
78
+ budget: invalidCallBudget,
79
+ ...(deps.spinBreaker !== undefined ? { spinBreaker: deps.spinBreaker } : {}),
80
+ ...(deps.loopHandlers !== undefined ? { loopHandlers: deps.loopHandlers } : {}),
81
+ ...(deps.loopHandlerContext !== undefined ? { loopHandlerContext: deps.loopHandlerContext } : {}),
82
+ ...(deps.signal !== undefined ? { signal: deps.signal } : {}),
83
+ onSpinStop: () => {
84
+ spinFlags.warned = true;
85
+ },
86
+ });
87
+ const anchor = sessionAnchorFor(handle.id, transcriptIdentity.model);
88
+ const lastUsage = anchor?.promptTokens !== undefined ? { promptTokens: anchor.promptTokens } : undefined;
89
+ const anchorIndex = anchor?.anchorIndex;
90
+ // P3 `before_turn` (spec 6.1): fires ONCE, after the transcript loads and
91
+ // before the seed push. `boundary` is dispatcher-computed (spec 3.4) and
92
+ // always false: the model-change boundary never arises, because the
93
+ // transcript store refuses another model's history (spec 8.3), so the
94
+ // history channel is honoured only at an undefined anchor (spec 3.5) — an
95
+ // off-boundary patch is rejected + warned by applyHistoryPatch and the turn
96
+ // proceeds on the loaded history.
97
+ const turnStart = await loopEvents.dispatch("before_turn", {
98
+ prompt,
99
+ history: messages,
100
+ sessionName: handle.id,
101
+ boundary: false,
102
+ });
103
+ // An honoured history patch (reachable only at an undefined anchor today)
104
+ // rewrites the IN-MEMORY array, and the saveTranscript at the turn's end
105
+ // persists it — before_turn is the conversation-rewriting event, unlike
106
+ // transform_context's wire-copy-only ruling (spec 6.6).
107
+ messages = [
108
+ ...applyHistoryPatch({
109
+ before: messages,
110
+ patched: turnStart.history,
111
+ anchorIndex,
112
+ boundary: false,
113
+ event: "before_turn",
114
+ }).messages,
115
+ ];
116
+ messages.push(...beforeTurnSeed(turnStart.seed, prompt));
117
+ const codingTools = opts.codingTools ?? [];
118
+ const codingToolNames = new Set(codingTools.map((t) => t.name));
119
+ // Native spends the budget ONLY on ask_human exchanges. Unlike ACP it is
120
+ // not this loop's bound — the loop is `while (true)`, bounded by the
121
+ // whole-turn deadline and the idle watchdog (issue #1820).
122
+ //
123
+ // Advertised only while the Q&A budget can still be spent; a tool the model
124
+ // cannot successfully call is worse than no tool.
125
+ const maxInteractions = opts.maxInteractions ?? 0;
126
+ const tools = [
127
+ ...toToolDefinitions(opts.contextPullTools ?? []),
128
+ ...codingToolsToDefinitions(codingTools),
129
+ ...(maxInteractions > 0 ? [askHumanToolDefinition] : []),
130
+ ];
131
+ const usage = createTurnAccumulator();
132
+ // Reported on the result so the review guards can corroborate a reviewer's
133
+ // self-declared inspection trail against calls it actually made.
134
+ const codingToolsCalled = [];
135
+ const interactions = [];
136
+ const params = {
137
+ handle,
138
+ opts,
139
+ deps,
140
+ transcriptModel: transcriptIdentity.model,
141
+ tools,
142
+ codingToolNames,
143
+ loopEvents,
144
+ invalidCallBudget,
145
+ spinBreaker: deps.spinBreaker,
146
+ spinFlags,
147
+ maxInteractions,
148
+ interactions,
149
+ codingToolsCalled,
150
+ usage,
151
+ };
152
+ let state = {
153
+ messages,
154
+ lastUsage,
155
+ anchorIndex,
156
+ roundTrips: 0,
157
+ output: "",
158
+ // Set ONLY on the clean exit — the model returned no further tool calls.
159
+ // Every other way out of the loop (the deadline, or an abort) leaves work
160
+ // the model asked for unexecuted.
161
+ completedNormally: false,
162
+ timedOut: false,
163
+ // `before_turn_end`'s followUp injections so far this turn (spec 6.4) —
164
+ // reset every turn, capped at MAX_FOLLOW_UPS_PER_TURN.
165
+ followUpsSoFar: 0,
166
+ };
167
+ // nax#1838: the save below the loop is the clean exit's alone. A turn that
168
+ // throws must persist too — the retry reopens the same deterministic session
169
+ // name, so an unsaved conversation is one the model silently resumes without.
170
+ try {
171
+ // The OUTER loop is `before_turn_end`'s followUp continuation (spec 6.4):
172
+ // an honoured followUp re-enters the round-trip loop via runRoundTripLoop
173
+ // instead of building the turn's result — the result is built when the
174
+ // turn ENDS.
175
+ while (true) {
176
+ // Mutates `state` in place, including up to the point of a throw — see
177
+ // runRoundTripLoop's own header for why this is not `state = await ...`.
178
+ await runRoundTripLoop(state, params);
179
+ const turnEnd = await runTurnEndPhase(state, params);
180
+ if (turnEnd.action === "break")
181
+ break;
182
+ state = turnEnd.state;
183
+ }
184
+ }
185
+ catch (err) {
186
+ // Review #20: the event fires on EVERY ending, so a throwing turn
187
+ // dispatches it before the transcript save — a handler sees the failure
188
+ // ending with `ended: "aborted"` (signal fired) or `"errored"`, and its
189
+ // result is ignored: there is no turn left to continue. A rejecting
190
+ // dispatch must not replace the original error or skip the save below —
191
+ // it is logged and skipped, the same skip-not-fail contract the registry
192
+ // applies to a single throwing handler.
193
+ await dispatchTurnEndOnError(loopEvents, {
194
+ messages: state.messages,
195
+ roundTrips: state.roundTrips,
196
+ stopped: spinFlags.stopped || invalidCallBudget.exceeded || state.timedOut,
197
+ followUpsSoFar: state.followUpsSoFar,
198
+ }, deps.signal).catch((dispatchErr) => {
199
+ getSafeLogger()?.warn("native-loop-events", "before_turn_end dispatch failed on the error path; skipping it", {
200
+ sessionName: handle.id,
201
+ error: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
202
+ });
203
+ });
204
+ // Best-effort, and deliberately unlike the clean-exit save: there a write
205
+ // failure fails the turn, because continuing on unstored history is silent
206
+ // degradation. Here a failure is already in flight, and masking it with a
207
+ // write error would lose the cause.
208
+ await saveTranscript(dir, handle.id, state.messages, transcriptIdentity).catch((saveErr) => {
209
+ getSafeLogger()?.warn("native-adapter", "could not persist the transcript of a failed turn", {
210
+ sessionName: handle.id,
211
+ error: saveErr instanceof Error ? saveErr.message : String(saveErr),
212
+ });
213
+ });
214
+ // nax#1840: attach what was already spent, keyed on the error's own
215
+ // identity so the error itself is rethrown byte-for-byte unmodified.
216
+ if (typeof err === "object" && err !== null) {
217
+ recordNativeTurnFailureUsage(err, {
218
+ tokenUsage: usage.tokens(),
219
+ costUsd: usage.costUsd(),
220
+ });
221
+ }
222
+ throw err;
223
+ }
224
+ logTurnTailWarnings({
225
+ sessionName: handle.id,
226
+ completedNormally: state.completedNormally,
227
+ spinStopped: spinFlags.stopped,
228
+ roundTrips: state.roundTrips,
229
+ timedOut: state.timedOut,
230
+ spinBreaker: deps.spinBreaker,
231
+ invalidCallHalt: invalidCallBudget.halt,
232
+ });
233
+ // Persisted before returning, and a write failure fails the turn: continuing
234
+ // on a history that could not be stored is the silent degradation #1794
235
+ // removed from the pipeline (ADR-028 s4).
236
+ await saveTranscript(dir, handle.id, state.messages, transcriptIdentity);
237
+ return buildTurnResult({
238
+ output: state.output,
239
+ usage,
240
+ roundTrips: state.roundTrips,
241
+ codingTools,
242
+ codingToolsCalled,
243
+ completedNormally: state.completedNormally,
244
+ timedOut: state.timedOut,
245
+ spinStopped: spinFlags.stopped,
246
+ invalidCallHalt: invalidCallBudget.halt,
247
+ interactions,
248
+ pricingSource: deps.pricingSource,
249
+ });
250
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The turn's tail: the warnings emitted once the loop has exited, and the
3
+ * `TurnResult` assembled from what the loop accumulated.
4
+ *
5
+ * Extracted from `turn-loop.ts`, which keeps the `saveTranscript` call that
6
+ * sits between the two: a write failure there fails the turn, unlike the
7
+ * best-effort save in the catch, and that distinction belongs with the control
8
+ * flow rather than in a result builder.
9
+ */
10
+ import type { InteractionExchange, InvalidToolCallDetail, TurnResult } from "#src/session/session-types";
11
+ import type { CodingTool } from "#src/tools/index";
12
+ import type { TurnAccumulator } from "./turn-accumulator.ts";
13
+ import type { TurnDeps } from "./turn-types.ts";
14
+ export interface LogTurnTailWarningsArgs {
15
+ readonly sessionName: string;
16
+ readonly completedNormally: boolean;
17
+ readonly spinStopped: boolean;
18
+ readonly roundTrips: number;
19
+ readonly timedOut: boolean;
20
+ readonly spinBreaker: TurnDeps["spinBreaker"];
21
+ /** The call that tripped the invalid-call budget; absent when it did not trip. */
22
+ readonly invalidCallHalt?: InvalidToolCallDetail;
23
+ }
24
+ export declare function logTurnTailWarnings(args: LogTurnTailWarningsArgs): void;
25
+ export interface BuildTurnResultArgs {
26
+ readonly output: string;
27
+ readonly usage: TurnAccumulator;
28
+ readonly roundTrips: number;
29
+ readonly codingTools: readonly CodingTool[];
30
+ readonly codingToolsCalled: readonly string[];
31
+ readonly completedNormally: boolean;
32
+ readonly timedOut: boolean;
33
+ readonly spinStopped: boolean;
34
+ /** The call that tripped the invalid-call budget; absent when it did not trip. */
35
+ readonly invalidCallHalt: InvalidToolCallDetail | undefined;
36
+ readonly interactions: readonly InteractionExchange[];
37
+ readonly pricingSource: TurnDeps["pricingSource"];
38
+ }
39
+ export declare function buildTurnResult(args: BuildTurnResultArgs): TurnResult;