@namzu/sdk 39.0.0 → 41.0.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 (289) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  3. package/dist/bridge/a2a/mapper.js +8 -0
  4. package/dist/bridge/a2a/mapper.js.map +1 -1
  5. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  6. package/dist/bridge/sse/mapper.js +11 -0
  7. package/dist/bridge/sse/mapper.js.map +1 -1
  8. package/dist/connector/index.d.ts +2 -2
  9. package/dist/connector/index.d.ts.map +1 -1
  10. package/dist/connector/index.js +1 -1
  11. package/dist/connector/index.js.map +1 -1
  12. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  13. package/dist/connector/mcp/adapter.js +112 -10
  14. package/dist/connector/mcp/adapter.js.map +1 -1
  15. package/dist/connector/mcp/audio-admission.d.ts +17 -0
  16. package/dist/connector/mcp/audio-admission.d.ts.map +1 -0
  17. package/dist/connector/mcp/audio-admission.js +171 -0
  18. package/dist/connector/mcp/audio-admission.js.map +1 -0
  19. package/dist/connector/mcp/client.d.ts +252 -1
  20. package/dist/connector/mcp/client.d.ts.map +1 -1
  21. package/dist/connector/mcp/client.js +611 -39
  22. package/dist/connector/mcp/client.js.map +1 -1
  23. package/dist/connector/mcp/envelope.d.ts +91 -0
  24. package/dist/connector/mcp/envelope.d.ts.map +1 -0
  25. package/dist/connector/mcp/envelope.js +173 -0
  26. package/dist/connector/mcp/envelope.js.map +1 -0
  27. package/dist/connector/mcp/era.d.ts +130 -0
  28. package/dist/connector/mcp/era.d.ts.map +1 -0
  29. package/dist/connector/mcp/era.js +304 -0
  30. package/dist/connector/mcp/era.js.map +1 -0
  31. package/dist/connector/mcp/errors.d.ts +106 -0
  32. package/dist/connector/mcp/errors.d.ts.map +1 -0
  33. package/dist/connector/mcp/errors.js +154 -0
  34. package/dist/connector/mcp/errors.js.map +1 -0
  35. package/dist/connector/mcp/http-sse.d.ts +11 -0
  36. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  37. package/dist/connector/mcp/http-sse.js +21 -6
  38. package/dist/connector/mcp/http-sse.js.map +1 -1
  39. package/dist/connector/mcp/index.d.ts +7 -0
  40. package/dist/connector/mcp/index.d.ts.map +1 -1
  41. package/dist/connector/mcp/index.js +10 -0
  42. package/dist/connector/mcp/index.js.map +1 -1
  43. package/dist/connector/mcp/streamable-http.d.ts +83 -0
  44. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  45. package/dist/connector/mcp/streamable-http.js +177 -11
  46. package/dist/connector/mcp/streamable-http.js.map +1 -1
  47. package/dist/connector/mcp/x-mcp-header.d.ts +56 -0
  48. package/dist/connector/mcp/x-mcp-header.d.ts.map +1 -0
  49. package/dist/connector/mcp/x-mcp-header.js +254 -0
  50. package/dist/connector/mcp/x-mcp-header.js.map +1 -0
  51. package/dist/constants/mcp/index.d.ts +123 -15
  52. package/dist/constants/mcp/index.d.ts.map +1 -1
  53. package/dist/constants/mcp/index.js +135 -16
  54. package/dist/constants/mcp/index.js.map +1 -1
  55. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  56. package/dist/manager/agent/lifecycle.js +23 -0
  57. package/dist/manager/agent/lifecycle.js.map +1 -1
  58. package/dist/manager/run/persistence.d.ts +8 -0
  59. package/dist/manager/run/persistence.d.ts.map +1 -1
  60. package/dist/manager/run/persistence.js +12 -0
  61. package/dist/manager/run/persistence.js.map +1 -1
  62. package/dist/prompt/coding-agent-doctrine.d.ts +20 -0
  63. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  64. package/dist/prompt/coding-agent-doctrine.js +19 -3
  65. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  66. package/dist/prompt/index.d.ts +1 -1
  67. package/dist/prompt/index.d.ts.map +1 -1
  68. package/dist/prompt/index.js +1 -1
  69. package/dist/prompt/index.js.map +1 -1
  70. package/dist/public-runtime.d.ts +6 -4
  71. package/dist/public-runtime.d.ts.map +1 -1
  72. package/dist/public-runtime.js +8 -4
  73. package/dist/public-runtime.js.map +1 -1
  74. package/dist/public-tools.d.ts +11 -0
  75. package/dist/public-tools.d.ts.map +1 -1
  76. package/dist/public-tools.js +14 -0
  77. package/dist/public-tools.js.map +1 -1
  78. package/dist/registry/tool/execute.d.ts.map +1 -1
  79. package/dist/registry/tool/execute.js +2 -3
  80. package/dist/registry/tool/execute.js.map +1 -1
  81. package/dist/registry/tool/portable.d.ts +65 -0
  82. package/dist/registry/tool/portable.d.ts.map +1 -0
  83. package/dist/registry/tool/portable.js +244 -0
  84. package/dist/registry/tool/portable.js.map +1 -0
  85. package/dist/registry/tool/schema.d.ts +32 -5
  86. package/dist/registry/tool/schema.d.ts.map +1 -1
  87. package/dist/registry/tool/schema.js +35 -9
  88. package/dist/registry/tool/schema.js.map +1 -1
  89. package/dist/registry/toolset/catalog.js +8 -8
  90. package/dist/registry/toolset/catalog.js.map +1 -1
  91. package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
  92. package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
  93. package/dist/runtime/jobs/awaited-jobs.js +259 -0
  94. package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
  95. package/dist/runtime/jobs/registry.d.ts +33 -2
  96. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  97. package/dist/runtime/jobs/registry.js +37 -0
  98. package/dist/runtime/jobs/registry.js.map +1 -1
  99. package/dist/runtime/query/executor.d.ts +28 -0
  100. package/dist/runtime/query/executor.d.ts.map +1 -1
  101. package/dist/runtime/query/executor.js +39 -1
  102. package/dist/runtime/query/executor.js.map +1 -1
  103. package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
  104. package/dist/runtime/query/file-evidence-context.js +159 -43
  105. package/dist/runtime/query/file-evidence-context.js.map +1 -1
  106. package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
  107. package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
  108. package/dist/runtime/query/file-evidence-replay.js +647 -0
  109. package/dist/runtime/query/file-evidence-replay.js.map +1 -0
  110. package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
  111. package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
  112. package/dist/runtime/query/file-evidence-seed.js +100 -0
  113. package/dist/runtime/query/file-evidence-seed.js.map +1 -0
  114. package/dist/runtime/query/index.d.ts.map +1 -1
  115. package/dist/runtime/query/index.js +94 -2
  116. package/dist/runtime/query/index.js.map +1 -1
  117. package/dist/runtime/query/iteration/index.d.ts +87 -9
  118. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  119. package/dist/runtime/query/iteration/index.js +193 -28
  120. package/dist/runtime/query/iteration/index.js.map +1 -1
  121. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  122. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  123. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  124. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  125. package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
  126. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  127. package/dist/runtime/query/plugin-hooks.d.ts +14 -0
  128. package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
  129. package/dist/runtime/query/plugin-hooks.js +18 -0
  130. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  131. package/dist/runtime/query/repeat-call.d.ts +17 -4
  132. package/dist/runtime/query/repeat-call.d.ts.map +1 -1
  133. package/dist/runtime/query/repeat-call.js +26 -19
  134. package/dist/runtime/query/repeat-call.js.map +1 -1
  135. package/dist/runtime/query/steering.d.ts +11 -1
  136. package/dist/runtime/query/steering.d.ts.map +1 -1
  137. package/dist/runtime/query/steering.js +12 -1
  138. package/dist/runtime/query/steering.js.map +1 -1
  139. package/dist/runtime/query/tooling.d.ts +2 -0
  140. package/dist/runtime/query/tooling.d.ts.map +1 -1
  141. package/dist/runtime/query/tooling.js +1 -0
  142. package/dist/runtime/query/tooling.js.map +1 -1
  143. package/dist/sandbox/provider/local.d.ts.map +1 -1
  144. package/dist/sandbox/provider/local.js +46 -3
  145. package/dist/sandbox/provider/local.js.map +1 -1
  146. package/dist/scheduler/completion-inbox.d.ts +48 -2
  147. package/dist/scheduler/completion-inbox.d.ts.map +1 -1
  148. package/dist/scheduler/completion-inbox.js +102 -10
  149. package/dist/scheduler/completion-inbox.js.map +1 -1
  150. package/dist/scheduler/local.d.ts.map +1 -1
  151. package/dist/scheduler/local.js +8 -0
  152. package/dist/scheduler/local.js.map +1 -1
  153. package/dist/store/run/disk.d.ts +35 -1
  154. package/dist/store/run/disk.d.ts.map +1 -1
  155. package/dist/store/run/disk.js +100 -0
  156. package/dist/store/run/disk.js.map +1 -1
  157. package/dist/tools/builtins/bash.d.ts.map +1 -1
  158. package/dist/tools/builtins/bash.js +4 -10
  159. package/dist/tools/builtins/bash.js.map +1 -1
  160. package/dist/tools/builtins/edit-apply.d.ts +126 -0
  161. package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
  162. package/dist/tools/builtins/edit-apply.js +360 -0
  163. package/dist/tools/builtins/edit-apply.js.map +1 -0
  164. package/dist/tools/builtins/edit.d.ts +143 -1
  165. package/dist/tools/builtins/edit.d.ts.map +1 -1
  166. package/dist/tools/builtins/edit.js +37 -219
  167. package/dist/tools/builtins/edit.js.map +1 -1
  168. package/dist/tools/builtins/index.d.ts +1 -0
  169. package/dist/tools/builtins/index.d.ts.map +1 -1
  170. package/dist/tools/builtins/index.js +9 -3
  171. package/dist/tools/builtins/index.js.map +1 -1
  172. package/dist/tools/builtins/job.js +1 -1
  173. package/dist/tools/builtins/job.js.map +1 -1
  174. package/dist/tools/builtins/read-file.d.ts +2 -2
  175. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  176. package/dist/tools/builtins/read-file.js +50 -65
  177. package/dist/tools/builtins/read-file.js.map +1 -1
  178. package/dist/tools/builtins/read-render.d.ts +56 -0
  179. package/dist/tools/builtins/read-render.d.ts.map +1 -0
  180. package/dist/tools/builtins/read-render.js +73 -0
  181. package/dist/tools/builtins/read-render.js.map +1 -0
  182. package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
  183. package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
  184. package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
  185. package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
  186. package/dist/tools/builtins/wait-for-job.d.ts +6 -0
  187. package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
  188. package/dist/tools/builtins/wait-for-job.js +162 -0
  189. package/dist/tools/builtins/wait-for-job.js.map +1 -0
  190. package/dist/tools/builtins/write-file.js +5 -0
  191. package/dist/tools/builtins/write-file.js.map +1 -1
  192. package/dist/tools/coordinator/index.d.ts.map +1 -1
  193. package/dist/tools/coordinator/index.js +1 -7
  194. package/dist/tools/coordinator/index.js.map +1 -1
  195. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  196. package/dist/tools/file-read-tracker.js +88 -10
  197. package/dist/tools/file-read-tracker.js.map +1 -1
  198. package/dist/types/agent/scheduler.d.ts +20 -0
  199. package/dist/types/agent/scheduler.d.ts.map +1 -1
  200. package/dist/types/agent/task.d.ts +20 -0
  201. package/dist/types/agent/task.d.ts.map +1 -1
  202. package/dist/types/connector/mcp.d.ts +205 -0
  203. package/dist/types/connector/mcp.d.ts.map +1 -1
  204. package/dist/types/message/index.d.ts +1 -1
  205. package/dist/types/message/index.d.ts.map +1 -1
  206. package/dist/types/message/index.js +2 -0
  207. package/dist/types/message/index.js.map +1 -1
  208. package/dist/types/run/entity.d.ts +13 -0
  209. package/dist/types/run/entity.d.ts.map +1 -1
  210. package/dist/types/run/events.d.ts +56 -0
  211. package/dist/types/run/events.d.ts.map +1 -1
  212. package/dist/types/run/events.js.map +1 -1
  213. package/dist/types/run/store.d.ts +41 -0
  214. package/dist/types/run/store.d.ts.map +1 -1
  215. package/dist/types/sandbox/index.d.ts +83 -15
  216. package/dist/types/sandbox/index.d.ts.map +1 -1
  217. package/dist/types/sandbox/index.js.map +1 -1
  218. package/dist/types/tool/index.d.ts +109 -0
  219. package/dist/types/tool/index.d.ts.map +1 -1
  220. package/dist/types/tool/index.js.map +1 -1
  221. package/dist/utils/env.d.ts +19 -0
  222. package/dist/utils/env.d.ts.map +1 -0
  223. package/dist/utils/env.js +25 -0
  224. package/dist/utils/env.js.map +1 -0
  225. package/package.json +1 -1
  226. package/src/bridge/a2a/mapper.ts +8 -0
  227. package/src/bridge/sse/mapper.ts +11 -0
  228. package/src/connector/index.ts +27 -0
  229. package/src/connector/mcp/adapter.ts +123 -10
  230. package/src/connector/mcp/audio-admission.ts +173 -0
  231. package/src/connector/mcp/client.ts +694 -45
  232. package/src/connector/mcp/envelope.ts +235 -0
  233. package/src/connector/mcp/era.ts +400 -0
  234. package/src/connector/mcp/errors.ts +171 -0
  235. package/src/connector/mcp/http-sse.ts +23 -6
  236. package/src/connector/mcp/index.ts +37 -0
  237. package/src/connector/mcp/streamable-http.ts +199 -11
  238. package/src/connector/mcp/x-mcp-header.ts +322 -0
  239. package/src/constants/mcp/index.ts +145 -16
  240. package/src/manager/agent/lifecycle.ts +29 -0
  241. package/src/manager/run/persistence.ts +12 -0
  242. package/src/prompt/coding-agent-doctrine.ts +31 -4
  243. package/src/prompt/index.ts +1 -0
  244. package/src/public-runtime.ts +37 -1
  245. package/src/public-tools.ts +18 -0
  246. package/src/registry/tool/execute.ts +2 -4
  247. package/src/registry/tool/portable.ts +264 -0
  248. package/src/registry/tool/schema.ts +38 -8
  249. package/src/registry/toolset/catalog.ts +8 -9
  250. package/src/runtime/jobs/awaited-jobs.ts +271 -0
  251. package/src/runtime/jobs/registry.ts +50 -0
  252. package/src/runtime/query/executor.ts +49 -1
  253. package/src/runtime/query/file-evidence-context.ts +190 -46
  254. package/src/runtime/query/file-evidence-replay.ts +776 -0
  255. package/src/runtime/query/file-evidence-seed.ts +126 -0
  256. package/src/runtime/query/index.ts +104 -2
  257. package/src/runtime/query/iteration/index.ts +202 -28
  258. package/src/runtime/query/iteration/phases/context.ts +10 -0
  259. package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
  260. package/src/runtime/query/plugin-hooks.ts +20 -0
  261. package/src/runtime/query/repeat-call.ts +28 -18
  262. package/src/runtime/query/steering.ts +11 -0
  263. package/src/runtime/query/tooling.ts +3 -0
  264. package/src/sandbox/provider/local.ts +45 -2
  265. package/src/scheduler/completion-inbox.ts +105 -9
  266. package/src/scheduler/local.ts +8 -0
  267. package/src/store/run/disk.ts +108 -0
  268. package/src/tools/builtins/bash.ts +4 -10
  269. package/src/tools/builtins/edit-apply.ts +456 -0
  270. package/src/tools/builtins/edit.ts +39 -270
  271. package/src/tools/builtins/index.ts +9 -3
  272. package/src/tools/builtins/job.ts +1 -1
  273. package/src/tools/builtins/read-file.ts +56 -77
  274. package/src/tools/builtins/read-render.ts +104 -0
  275. package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
  276. package/src/tools/builtins/wait-for-job.ts +184 -0
  277. package/src/tools/builtins/write-file.ts +5 -0
  278. package/src/tools/coordinator/index.ts +1 -7
  279. package/src/tools/file-read-tracker.ts +85 -7
  280. package/src/types/agent/scheduler.ts +21 -0
  281. package/src/types/agent/task.ts +21 -0
  282. package/src/types/connector/mcp.ts +205 -1
  283. package/src/types/message/index.ts +2 -0
  284. package/src/types/run/entity.ts +14 -0
  285. package/src/types/run/events.ts +56 -0
  286. package/src/types/run/store.ts +42 -0
  287. package/src/types/sandbox/index.ts +84 -15
  288. package/src/types/tool/index.ts +104 -0
  289. package/src/utils/env.ts +23 -0
@@ -0,0 +1,456 @@
1
+ import type { EditInput } from './edit.js'
2
+
3
+ /**
4
+ * The apply core `edit`'s tool definition calls at mutation time, pulled out
5
+ * on its own so a second caller — the step-context projection that replays a
6
+ * visible edit call to verify a claimed post-edit body — runs this exact
7
+ * code instead of a parallel implementation that could drift from it (most
8
+ * easily on the CRLF reconciliation below, which depends on the real file's
9
+ * line-ending mix).
10
+ *
11
+ * Pure by construction: no filesystem access, no `ToolContext`. Everything
12
+ * here is a function from strings to strings.
13
+ */
14
+
15
+ export type NormalizedEditInput =
16
+ | {
17
+ operation: 'replace'
18
+ oldString: string
19
+ newString: string
20
+ replace_all: boolean
21
+ }
22
+ | {
23
+ operation: 'insert'
24
+ insertLine: number | 'end'
25
+ newString: string
26
+ replace_all: boolean
27
+ }
28
+
29
+ /**
30
+ * Turn one call into the ordered list of operations it stands for.
31
+ *
32
+ * A list rather than a single operation because the batch shape is not a
33
+ * different kind of edit, only a longer one. Keeping ONE representation is
34
+ * what stops the two shapes diverging: everything below this function — the
35
+ * uniqueness check, the CRLF reconciliation, the identical-text refusal, the
36
+ * atomic write — sees a list of length one for a single edit and never learns
37
+ * which shape the caller used.
38
+ */
39
+ export function normalizeEditInput(
40
+ input: EditInput,
41
+ ): { success: true; operations: NormalizedEditInput[] } | { success: false; error: string } {
42
+ if (input.edits !== undefined) {
43
+ return {
44
+ success: true,
45
+ operations: input.edits.map((edit) => ({
46
+ operation: 'replace' as const,
47
+ oldString: edit.old_string,
48
+ newString: edit.new_string,
49
+ replace_all: edit.replace_all ?? false,
50
+ })),
51
+ }
52
+ }
53
+
54
+ const newString = input.new_string ?? input.newStr
55
+ if (typeof newString !== 'string') {
56
+ return {
57
+ success: false,
58
+ error: 'Either new_string or newStr is required.',
59
+ }
60
+ }
61
+
62
+ if (input.insertLine !== undefined) {
63
+ const insertLine = normalizeInsertLine(input.insertLine)
64
+ if (!insertLine.success) return insertLine
65
+ return {
66
+ success: true,
67
+ operations: [
68
+ {
69
+ operation: 'insert',
70
+ insertLine: insertLine.value,
71
+ newString,
72
+ replace_all: input.replace_all ?? false,
73
+ },
74
+ ],
75
+ }
76
+ }
77
+
78
+ const oldString = input.old_string ?? input.oldStr
79
+ if (typeof oldString !== 'string') {
80
+ return {
81
+ success: false,
82
+ error: 'Either old_string/oldStr or insertLine is required.',
83
+ }
84
+ }
85
+ return {
86
+ success: true,
87
+ operations: [
88
+ {
89
+ operation: 'replace',
90
+ oldString,
91
+ newString,
92
+ replace_all: input.replace_all ?? false,
93
+ },
94
+ ],
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Spellings of "the end of the file" a model reaches for.
100
+ *
101
+ * Liberal here and strict in the schema, which is the right way round: the
102
+ * schema makes `"end"` the only emittable string for a provider that
103
+ * constrains, and this catches the rest for one that does not. None of these
104
+ * is ambiguous — accepting them is not guessing at intent, it is declining to
105
+ * spend a round trip on a synonym.
106
+ */
107
+ const END_ALIASES = new Set(['end', 'eof', 'append', 'last', 'end_of_file', 'end-of-file'])
108
+
109
+ function normalizeInsertLine(
110
+ value: string | number,
111
+ ): { success: true; value: number | 'end' } | { success: false; error: string } {
112
+ if (typeof value === 'string') {
113
+ const normalized = value.trim().toLowerCase()
114
+ // `"end"` is the only spelling the model-facing schema admits, so a
115
+ // constrained decoder cannot produce anything else. These aliases are
116
+ // for the providers that do not constrain: a model reading "appends to
117
+ // the file" reaches for the word it knows, and every one of these says
118
+ // the same unambiguous thing. Refusing them bought strictness and cost
119
+ // a full model round trip per occurrence — measured by a consuming host
120
+ // as the single largest source of tool-call waste in its runs.
121
+ if (END_ALIASES.has(normalized)) return { success: true, value: 'end' }
122
+ const parsed = Number(value)
123
+ if (Number.isInteger(parsed) && parsed >= 0) return { success: true, value: parsed }
124
+ return {
125
+ success: false,
126
+ error: `insertLine must be a non-negative line number or "end" (also accepted: ${[...END_ALIASES].filter((a) => a !== 'end').join(', ')}). Received ${JSON.stringify(value)}.`,
127
+ }
128
+ }
129
+ return { success: true, value }
130
+ }
131
+
132
+ /**
133
+ * Apply every operation in order, or none of them.
134
+ *
135
+ * "Or none" is the whole reason this takes a list. Four related changes sent
136
+ * as four calls are four chances to stop halfway, and the file left behind
137
+ * after the third succeeded and the fourth did not is in a state no one wrote
138
+ * and no one is looking at. Here the fold runs entirely in memory and the
139
+ * caller writes once, so a failure anywhere leaves the file exactly as it was.
140
+ *
141
+ * Each operation matches against the content as the ones before it left it,
142
+ * not against the original. That is what lets a later edit target text an
143
+ * earlier one produced — and it is also why a failure names the INDEX: by the
144
+ * time hunk 3 fails, the string it was looking for may have been consumed by
145
+ * hunk 1, and "old_string not found" without a position sends the model to
146
+ * re-check the wrong hunk.
147
+ */
148
+ export function applyEdit(
149
+ content: string,
150
+ operations: readonly NormalizedEditInput[],
151
+ ): { success: true; content: string; replacements: number } | { success: false; error: string } {
152
+ let current = content
153
+ let replacements = 0
154
+
155
+ for (const [index, operation] of operations.entries()) {
156
+ const result = applyOne(current, operation)
157
+ if (!result.success) {
158
+ return {
159
+ success: false,
160
+ error: framedError(result.error, index, operations.length),
161
+ }
162
+ }
163
+ current = result.content
164
+ replacements += result.replacements
165
+ }
166
+
167
+ return { success: true, content: current, replacements }
168
+ }
169
+
170
+ function applyOne(
171
+ content: string,
172
+ input: NormalizedEditInput,
173
+ ): { success: true; content: string; replacements: number } | { success: false; error: string } {
174
+ if (input.operation === 'insert') {
175
+ return applyLineInsert(content, input)
176
+ }
177
+
178
+ const replacement = normalizeLineEndings(content, input)
179
+
180
+ if (!content.includes(replacement.oldString)) {
181
+ return {
182
+ success: false,
183
+ error:
184
+ 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
185
+ }
186
+ }
187
+
188
+ if (replacement.replace_all) {
189
+ const parts = content.split(replacement.oldString)
190
+ const replacements = parts.length - 1
191
+ return {
192
+ success: true,
193
+ content: parts.join(replacement.newString),
194
+ replacements,
195
+ }
196
+ }
197
+
198
+ // Uniqueness check: old_string/oldStr must appear exactly once
199
+ const firstIndex = content.indexOf(replacement.oldString)
200
+ const secondIndex = content.indexOf(replacement.oldString, firstIndex + 1)
201
+
202
+ if (secondIndex !== -1) {
203
+ const lineNumber = content.slice(0, firstIndex).split('\n').length
204
+ const secondLine = content.slice(0, secondIndex).split('\n').length
205
+ return {
206
+ success: false,
207
+ error: `old_string/oldStr is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true.`,
208
+ }
209
+ }
210
+
211
+ return {
212
+ success: true,
213
+ content:
214
+ content.slice(0, firstIndex) +
215
+ replacement.newString +
216
+ content.slice(firstIndex + replacement.oldString.length),
217
+ replacements: 1,
218
+ }
219
+ }
220
+
221
+ function applyLineInsert(
222
+ content: string,
223
+ input: Extract<NormalizedEditInput, { operation: 'insert' }>,
224
+ ): { success: true; content: string; replacements: number } {
225
+ const hasTrailingNewline = content.endsWith('\n')
226
+ const lines = content.split('\n')
227
+ if (hasTrailingNewline) lines.pop()
228
+
229
+ const line =
230
+ input.insertLine === 'end'
231
+ ? lines.length
232
+ : Math.min(Math.max(input.insertLine, 0), lines.length)
233
+ const inserted = input.newString.endsWith('\n')
234
+ ? input.newString.slice(0, -1).split('\n')
235
+ : input.newString.split('\n')
236
+ lines.splice(line, 0, ...inserted)
237
+ return {
238
+ success: true,
239
+ content: `${lines.join('\n')}${hasTrailingNewline ? '\n' : ''}`,
240
+ replacements: 1,
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Reconcile the caller's line endings with the file's.
246
+ *
247
+ * A model reading a CRLF file and writing back LF (or the reverse) produces
248
+ * an `old_string` that is correct in every visible way and matches nothing.
249
+ * The failure reads as "your text is wrong" when the text is right and only
250
+ * the invisible half of each line break differs.
251
+ *
252
+ * Only applied when the file is CONSISTENT. A mixed-ending file has no
253
+ * single right answer, and rewriting boundaries there would corrupt the
254
+ * half that was already correct.
255
+ */
256
+ function normalizeLineEndings(
257
+ content: string,
258
+ input: Extract<NormalizedEditInput, { operation: 'replace' }>,
259
+ ): Extract<NormalizedEditInput, { operation: 'replace' }> {
260
+ const withoutCrlf = content.replaceAll('\r\n', '')
261
+ const usesOnlyCrlf = content.includes('\r\n') && !withoutCrlf.includes('\n')
262
+ if (usesOnlyCrlf) {
263
+ return {
264
+ ...input,
265
+ oldString: content.includes(input.oldString)
266
+ ? input.oldString
267
+ : input.oldString.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n'),
268
+ newString: input.newString.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n'),
269
+ }
270
+ }
271
+
272
+ const usesOnlyLf = content.includes('\n') && !content.includes('\r\n')
273
+ if (usesOnlyLf) {
274
+ return {
275
+ ...input,
276
+ oldString: content.includes(input.oldString)
277
+ ? input.oldString
278
+ : input.oldString.replaceAll('\r\n', '\n'),
279
+ newString: input.newString.replaceAll('\r\n', '\n'),
280
+ }
281
+ }
282
+ return input
283
+ }
284
+
285
+ /** The batch framing an operation's own error is reported under. */
286
+ function framedError(error: string, index: number, total: number): string {
287
+ if (total === 1) return error
288
+ return `edits[${index}] of ${total}: ${error} Nothing was written — the whole batch is refused, so the file is exactly as it was.`
289
+ }
290
+
291
+ /**
292
+ * What a bounded replay did, and what it cost.
293
+ *
294
+ * A union rather than a throw because all three outcomes are ordinary answers
295
+ * to a caller replaying somebody else's call: it applied, it would have built
296
+ * more than the caller has room for, or it no longer applies to the content it
297
+ * was handed. Only the first carries a body; the other two carry the charge so
298
+ * the caller can settle the room the attempt actually used.
299
+ */
300
+ export type BoundedReplay =
301
+ | {
302
+ readonly outcome: 'replayed'
303
+ readonly content: string
304
+ readonly charged: number
305
+ readonly replacements: number
306
+ }
307
+ | { readonly outcome: 'refused'; readonly charged: number }
308
+ | {
309
+ readonly outcome: 'failed'
310
+ readonly charged: number
311
+ readonly error: string
312
+ }
313
+
314
+ /**
315
+ * The single door a caller outside this module uses.
316
+ *
317
+ * Runs a visible call's arguments through the same normalize-then-apply path
318
+ * `EditTool.execute` runs at mutation time, against a content string the
319
+ * caller already has in hand — never the filesystem — and under an
320
+ * `allowance`: the largest string the caller is willing to have built on its
321
+ * behalf.
322
+ *
323
+ * The allowance is honoured one OPERATION at a time. Each operation's
324
+ * post-image length is worked out exactly from the content it is about to be
325
+ * applied to, compared against the allowance, and only then applied — so
326
+ * nothing over the ceiling is ever materialised, and nothing under it is
327
+ * refused for a bound that guessed high. An earlier shape predicted the whole
328
+ * call up front, which meant folding operations after the first against a
329
+ * string it had never seen: a rename hunk at index 1 was charged one match per
330
+ * anchor-length window of the file, and batches that would have fitted were
331
+ * turned away for a number nothing had built.
332
+ *
333
+ * `charged` is the longest string this call actually materialised, which is
334
+ * the one the caller paid for holding. It is the post-image length exactly for
335
+ * the single-operation shape almost every call has; for a batch it is the
336
+ * largest intermediate the fold built rather than the body it ends on, because
337
+ * a batch that grows a file to twenty megabytes and then deletes every
338
+ * character has still built the twenty megabytes. An operation that is refused
339
+ * or fails is charged nothing — it built nothing — while the ones before it in
340
+ * the same batch are charged, having run.
341
+ */
342
+ export function replayEditCallWithin(
343
+ content: string,
344
+ rawArguments: unknown,
345
+ allowance: number,
346
+ ): BoundedReplay {
347
+ const normalized = normalizeEditInput(rawArguments as EditInput)
348
+ // A shape this cannot normalize is one the tool itself would have refused.
349
+ // Reported as a failure rather than a throw, and charged nothing: no
350
+ // operation ran, so nothing was built.
351
+ if (!normalized.success) return { outcome: 'failed', charged: 0, error: normalized.error }
352
+ return replayOperationsWithin(content, normalized.operations, allowance)
353
+ }
354
+
355
+ /**
356
+ * The same walk, entered with the operations already normalized.
357
+ *
358
+ * Separate from the entry point above so the equivalence with {@link applyEdit}
359
+ * can be exercised operation by operation — `replayOperationsWithin(c, [op], ∞)`
360
+ * is `applyOne(c, op)` plus its exact predicted length — rather than only in
361
+ * the aggregate, where a prediction that is wrong in two places by the same
362
+ * amount would pass.
363
+ */
364
+ export function replayOperationsWithin(
365
+ content: string,
366
+ operations: readonly NormalizedEditInput[],
367
+ allowance: number,
368
+ ): BoundedReplay {
369
+ let current = content
370
+ let replacements = 0
371
+ // The largest body this call has built so far, which is what it has cost
372
+ // the caller. Zero until an operation applies: a call refused at its first
373
+ // operation built nothing and owes nothing.
374
+ let charged = 0
375
+
376
+ for (const [index, operation] of operations.entries()) {
377
+ const predicted = predictOne(current, operation, allowance)
378
+ if (predicted > allowance) return { outcome: 'refused', charged }
379
+ const applied = applyOne(current, operation)
380
+ if (!applied.success) {
381
+ return {
382
+ outcome: 'failed',
383
+ charged,
384
+ error: framedError(applied.error, index, operations.length),
385
+ }
386
+ }
387
+ current = applied.content
388
+ replacements += applied.replacements
389
+ charged = Math.max(charged, current.length)
390
+ }
391
+
392
+ return { outcome: 'replayed', content: current, charged, replacements }
393
+ }
394
+
395
+ /**
396
+ * The exact length `applyOne` would produce, without producing it.
397
+ *
398
+ * Exact and not a bound, because the content it is measured against is the
399
+ * real one the operation is about to be applied to. The one place it stops
400
+ * short is the occurrence scan for `replace_all`: counting out every match of
401
+ * a one-character anchor in a large file is itself the work the allowance
402
+ * exists to avoid, so the scan stops as soon as one more match would carry the
403
+ * result past `allowance`. The number returned from a stopped scan is a lower
404
+ * bound on the real length and above the allowance, which is all a refusal
405
+ * needs.
406
+ *
407
+ * Here rather than in the caller because the prediction has to see the same
408
+ * line-ending reconciliation `applyOne` sees. An `old_string` written with LF
409
+ * against a CRLF file matches after normalization and not before, and a
410
+ * predictor blind to that would count zero occurrences for a replacement that
411
+ * was going to succeed.
412
+ *
413
+ * An operation that is not going to apply at all — an anchor that is missing,
414
+ * or matches twice where one match was required — gets a number that means
415
+ * nothing, and it is never used for anything but the comparison above: the
416
+ * apply immediately after this reports the real refusal.
417
+ */
418
+ function predictOne(content: string, operation: NormalizedEditInput, allowance: number): number {
419
+ if (operation.operation === 'insert') {
420
+ // `applyLineInsert` splits the inserted text into lines and joins it back
421
+ // with the rest, which costs the text itself plus the one separator that
422
+ // joins it to its neighbour — and a trailing newline in the text is
423
+ // consumed as that separator rather than added to it.
424
+ return (
425
+ content.length + operation.newString.length + (operation.newString.endsWith('\n') ? 0 : 1)
426
+ )
427
+ }
428
+ const { oldString, newString, replace_all } = normalizeLineEndings(content, operation)
429
+ const growth = newString.length - oldString.length
430
+ // A single replacement, because `applyOne` refuses a second match. The
431
+ // clamp is for an anchor longer than the whole content: that operation is
432
+ // about to fail, and a negative length would be a charge handing the caller
433
+ // back room it never had.
434
+ if (!replace_all) return Math.max(content.length + growth, 0)
435
+ const room = Math.max(allowance - content.length, 0)
436
+ // One past what fits is already a refusal, so the scan never needs to see
437
+ // the match after that. A replacement no longer than its anchor cannot grow
438
+ // the string however often it matches, so there is nothing to stop for.
439
+ const cap = growth > 0 ? Math.floor(room / growth) + 1 : content.length
440
+ return Math.max(content.length + countOccurrences(content, oldString, cap) * growth, 0)
441
+ }
442
+
443
+ /** Non-overlapping matches, up to `cap`, counted the way `split` counts them. */
444
+ function countOccurrences(content: string, needle: string, cap: number): number {
445
+ // `split('')` yields one part per character; scanning for it would never
446
+ // advance. Neither shape reaches here through the tool's schema, which
447
+ // requires a non-empty `old_string`.
448
+ if (needle.length === 0) return Math.min(Math.max(content.length - 1, 0), cap)
449
+ let count = 0
450
+ let index = content.indexOf(needle)
451
+ while (index !== -1 && count < cap) {
452
+ count += 1
453
+ index = content.indexOf(needle, index + needle.length)
454
+ }
455
+ return count
456
+ }