@sema-agent/core 1.451.0 → 2.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 (191) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/observer.js +3 -0
  5. package/dist/agents/repair-loop.d.ts +1 -1
  6. package/dist/agents/repair-loop.js +1 -1
  7. package/dist/agents/retain-ledger.d.ts +60 -0
  8. package/dist/agents/retain-ledger.js +225 -0
  9. package/dist/agents/roster-store.d.ts +1 -0
  10. package/dist/agents/roster-store.js +9 -0
  11. package/dist/agents/send-message-tool.d.ts +41 -0
  12. package/dist/agents/send-message-tool.js +469 -0
  13. package/dist/agents/subagent.d.ts +33 -108
  14. package/dist/agents/subagent.js +40 -813
  15. package/dist/agents/teacher.d.ts +0 -1
  16. package/dist/agents/teacher.js +0 -9
  17. package/dist/bin/sema-tb.js +3 -3
  18. package/dist/brain/anthropic.js +15 -40
  19. package/dist/brain/openai.js +15 -40
  20. package/dist/brain/reasoning.d.ts +1 -1
  21. package/dist/brain/stream-engine.js +2 -2
  22. package/dist/brain/stream-shared.d.ts +17 -0
  23. package/dist/brain/stream-shared.js +50 -0
  24. package/dist/brain/timeout.d.ts +1 -1
  25. package/dist/brain/timeout.js +1 -1
  26. package/dist/config/catalog.js +1 -3
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.js +6 -0
  29. package/dist/core/ask-question.d.ts +1 -1
  30. package/dist/core/ask-question.js +5 -5
  31. package/dist/core/background-agent-store.d.ts +2 -0
  32. package/dist/core/background-agent-store.js +5 -1
  33. package/dist/core/background-shell.d.ts +1 -1
  34. package/dist/core/checkpoint-store.d.ts +0 -1
  35. package/dist/core/exec-gate.d.ts +1 -1
  36. package/dist/core/exec-output-tail.d.ts +19 -1
  37. package/dist/core/exec-output-tail.js +44 -5
  38. package/dist/core/file-snapshot-store.d.ts +1 -1
  39. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  40. package/dist/core/git-worktree-env.d.ts +1 -1
  41. package/dist/core/hooks.d.ts +1 -1
  42. package/dist/core/image-downsample.d.ts +44 -0
  43. package/dist/core/image-downsample.js +97 -0
  44. package/dist/core/lsp-session.d.ts +1 -0
  45. package/dist/core/lsp-session.js +24 -6
  46. package/dist/core/lsp.d.ts +11 -1
  47. package/dist/core/lsp.js +63 -6
  48. package/dist/core/mailbox-store.d.ts +3 -2
  49. package/dist/core/mailbox-store.js +19 -4
  50. package/dist/core/mcp.d.ts +3 -45
  51. package/dist/core/mcp.js +2 -97
  52. package/dist/core/media-byte-cap.d.ts +1 -1
  53. package/dist/core/memory-recall.d.ts +0 -12
  54. package/dist/core/memory-recall.js +0 -222
  55. package/dist/core/memory.d.ts +0 -6
  56. package/dist/core/memory.js +0 -88
  57. package/dist/core/message-utils.d.ts +1 -1
  58. package/dist/core/oracle-isolation.d.ts +1 -1
  59. package/dist/core/remote-env.d.ts +1 -1
  60. package/dist/core/roles.d.ts +1 -1
  61. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  62. package/dist/core/runner/assemble-result.d.ts +0 -2
  63. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  64. package/dist/core/runner/prepare-memory.d.ts +16 -0
  65. package/dist/core/runner/prepare-memory.js +191 -0
  66. package/dist/core/runner/prepare-task.d.ts +4 -28
  67. package/dist/core/runner/prepare-task.js +29 -208
  68. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  69. package/dist/core/runner/runtask.d.ts +2 -3
  70. package/dist/core/runner/runtask.js +337 -529
  71. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  73. package/dist/core/runner/synthetic-tools.js +4 -4
  74. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  75. package/dist/core/runner/tool-disclosure.js +2 -2
  76. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  77. package/dist/core/runner/tool-output-projection.js +81 -0
  78. package/dist/core/runtime.d.ts +1 -1
  79. package/dist/core/scheduler.d.ts +1 -1
  80. package/dist/core/sensitive-path-policy.d.ts +1 -1
  81. package/dist/core/session-reconcile.d.ts +1 -1
  82. package/dist/core/session-reconcile.js +5 -2
  83. package/dist/core/session-store.d.ts +2 -1
  84. package/dist/core/session-store.js +2 -1
  85. package/dist/core/session.d.ts +0 -1
  86. package/dist/core/task-notification.d.ts +1 -0
  87. package/dist/core/task-registry-agent.d.ts +115 -0
  88. package/dist/core/task-registry-agent.js +1163 -0
  89. package/dist/core/task-registry-monitor.d.ts +13 -0
  90. package/dist/core/task-registry-monitor.js +347 -0
  91. package/dist/core/task-registry-shared.d.ts +294 -0
  92. package/dist/core/task-registry-shared.js +271 -0
  93. package/dist/core/task-registry-workflow.d.ts +5 -0
  94. package/dist/core/task-registry-workflow.js +143 -0
  95. package/dist/core/task-registry.d.ts +54 -213
  96. package/dist/core/task-registry.js +190 -1766
  97. package/dist/core/task-tool-shape.d.ts +32 -0
  98. package/dist/core/task-tool-shape.js +83 -0
  99. package/dist/core/tool-policy.d.ts +1 -2
  100. package/dist/core/tool-result-budget.d.ts +1 -1
  101. package/dist/core/tool-result-store.d.ts +2 -2
  102. package/dist/core/tool-result-store.js +3 -3
  103. package/dist/core/tools.d.ts +7 -3
  104. package/dist/core/tools.js +3 -8
  105. package/dist/core/trace.d.ts +2 -2
  106. package/dist/core/types.d.ts +1 -29
  107. package/dist/core/workflow-journal-store.d.ts +2 -0
  108. package/dist/core/workflow-journal-store.js +14 -0
  109. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  110. package/dist/core/workflow-run-store-contract.js +19 -19
  111. package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
  112. package/dist/engine/execution-env/node-execution-env.js +184 -44
  113. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  114. package/dist/engine/lsp/frame-decoder.js +1 -1
  115. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
  116. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  117. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  118. package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
  119. package/dist/index.d.ts +18 -16
  120. package/dist/index.js +15 -13
  121. package/dist/internal/harness-types.d.ts +7 -0
  122. package/dist/internal/harness-types.js +1 -0
  123. package/dist/internal/harness.d.ts +1 -7
  124. package/dist/internal/harness.js +1 -0
  125. package/dist/orchestration/goal.js +2 -2
  126. package/dist/orchestration/run-workflow-tool.d.ts +2 -1
  127. package/dist/orchestration/run-workflow-tool.js +29 -6
  128. package/dist/orchestration/workflow-types.d.ts +193 -0
  129. package/dist/orchestration/workflow-types.js +54 -0
  130. package/dist/orchestration/workflow.d.ts +3 -184
  131. package/dist/orchestration/workflow.js +87 -64
  132. package/dist/prompt-assembly/assemble.d.ts +3 -3
  133. package/dist/prompt-assembly/assemble.js +0 -16
  134. package/dist/prompt-assembly/explain.d.ts +1 -1
  135. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  136. package/dist/prompts/default.d.ts +1 -6
  137. package/dist/prompts/default.js +3 -11
  138. package/dist/scenarios/env.d.ts +1 -1
  139. package/dist/scenarios/full-body.d.ts +0 -3
  140. package/dist/scenarios/full-body.js +0 -2
  141. package/dist/stores/cc/mailbox-store.js +6 -1
  142. package/dist/stores/file/background-agent-store.d.ts +2 -15
  143. package/dist/stores/file/background-agent-store.js +33 -102
  144. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  145. package/dist/stores/file/checkpoint-store.js +47 -119
  146. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  147. package/dist/stores/file/mailbox-store.d.ts +1 -1
  148. package/dist/stores/file/mailbox-store.js +9 -7
  149. package/dist/stores/file/shared-ledger.d.ts +35 -0
  150. package/dist/stores/file/shared-ledger.js +94 -0
  151. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  152. package/dist/stores/file/workflow-run-store.js +24 -93
  153. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  154. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  155. package/dist/tools/fs/encoding.d.ts +5 -0
  156. package/dist/tools/fs/encoding.js +6 -0
  157. package/dist/tools/fs/fs-bash.d.ts +31 -0
  158. package/dist/tools/fs/fs-bash.js +672 -0
  159. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  160. package/dist/tools/fs/fs-pdf.js +236 -0
  161. package/dist/tools/fs/fs-read.d.ts +8 -0
  162. package/dist/tools/fs/fs-read.js +286 -0
  163. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  164. package/dist/tools/fs/fs-search-tools.js +173 -0
  165. package/dist/tools/fs/fs-shared.d.ts +55 -0
  166. package/dist/tools/fs/fs-shared.js +144 -0
  167. package/dist/tools/fs/fs-write.d.ts +8 -0
  168. package/dist/tools/fs/fs-write.js +423 -0
  169. package/dist/tools/fs/index.d.ts +12 -65
  170. package/dist/tools/fs/index.js +26 -2026
  171. package/dist/tools/fs/notebook.d.ts +43 -0
  172. package/dist/tools/fs/notebook.js +141 -0
  173. package/dist/tools/fs/pdf.d.ts +1 -1
  174. package/dist/tools/fs/repo-map.d.ts +3 -3
  175. package/dist/tools/fs/repo-map.js +3 -3
  176. package/dist/tools/fs/safety.d.ts +1 -1
  177. package/dist/tools/fs/search.d.ts +1 -1
  178. package/dist/tools/fs/search.js +141 -12
  179. package/dist/tools/gitea-issue.js +4 -2
  180. package/dist/tools/monitor.d.ts +1 -1
  181. package/dist/tools/monitor.js +12 -8
  182. package/dist/tools/scheduler-tools.d.ts +1 -1
  183. package/dist/tools/scheduler-tools.js +16 -16
  184. package/dist/tools/task-list.js +34 -12
  185. package/dist/tools/web.d.ts +4 -2
  186. package/dist/tools/web.js +106 -20
  187. package/dist/tools/worktree.d.ts +1 -1
  188. package/dist/tools/worktree.js +14 -14
  189. package/package.json +1 -1
  190. package/dist/core/dynamic-recall.d.ts +0 -32
  191. package/dist/core/dynamic-recall.js +0 -76
@@ -0,0 +1,423 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Type } from "typebox";
3
+ import { defineTool, errorResult } from "../../core/tools.js";
4
+ import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, } from "./safety.js";
5
+ import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText, splitLeadingBom } from "./encoding.js";
6
+ import { MAX_EDIT_BYTES, formatByteSize, decodeEditBytes, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
7
+ async function gateToolWrite(hook, tool, path, key, content) {
8
+ if (hook === undefined)
9
+ return undefined;
10
+ let res;
11
+ try {
12
+ res = await hook({ tool, path, key, content });
13
+ }
14
+ catch (err) {
15
+ return `Error (${tool}): the write gate failed (${err instanceof Error ? err.message : String(err)}) — nothing was written.`;
16
+ }
17
+ if (res !== undefined && res.ok === false)
18
+ return `Error (${tool}): write rejected by the write gate (${res.code}): ${res.reason}`;
19
+ return undefined;
20
+ }
21
+ export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite) {
22
+ return defineTool({
23
+ name: "Edit",
24
+ contract: { contractId: "core.edit@1", implementationRevision: "1" },
25
+ description: "Performs exact string replacement in a file.\n" +
26
+ "\n" +
27
+ "- You must Read the file in this conversation before editing, or the call will fail.\n" +
28
+ "- `old_string` must match the file exactly, including indentation, and be unique — the edit fails otherwise. Strip the Read line prefix (line number + tab) before matching.\n" +
29
+ "- `replace_all: true` replaces every occurrence instead.\n" +
30
+ "- The edit also fails if the file changed on disk since you read it — Read it again first.",
31
+ aliases: ["MultiEdit"],
32
+ parameters: Type.Object({
33
+ ...FILE_PATH_PARAMS,
34
+ old_string: Type.Optional(Type.String({ description: "The text to replace" })),
35
+ new_string: Type.Optional(Type.String({ description: "The text to replace it with (must be different from old_string)" })),
36
+ replace_all: Type.Optional(Type.Boolean({ description: "Replace all occurrences of old_string (default false)" })),
37
+ }),
38
+ effect: "write",
39
+ execute: async (args, ctx) => {
40
+ const a = args;
41
+ const batch = Array.isArray(a.edits) && a.edits.length > 0;
42
+ const edits = batch
43
+ ? a.edits
44
+ : [{ old_string: a.old_string ?? "", new_string: a.new_string ?? "", ...(a.replace_all !== undefined ? { replace_all: a.replace_all } : {}) }];
45
+ if (!batch && (typeof a.old_string !== "string" || typeof a.new_string !== "string")) {
46
+ return errorResult(`Error (Edit): old_string and new_string are required.`);
47
+ }
48
+ for (let i = 0; i < edits.length; i++) {
49
+ const e = edits[i];
50
+ if (!e || typeof e.old_string !== "string" || typeof e.new_string !== "string") {
51
+ return errorResult(`Error (Edit): edit ${i + 1} of ${edits.length}: old_string and new_string must both be strings (no changes written — the batch is atomic).`);
52
+ }
53
+ }
54
+ const path = fileArgPath(args);
55
+ if (path === undefined)
56
+ return errorResult(`Error (Edit): file_path is required.`);
57
+ const ipynb = ipynbRedirect("Edit", path);
58
+ if (ipynb)
59
+ return errorResult(ipynb);
60
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
61
+ if (!r.ok)
62
+ return errorResult(violationText("Edit", r.violation));
63
+ const singleOld = batch ? undefined : a.old_string;
64
+ const exists = await env.exists(r.key, ctx.signal);
65
+ if (!exists.ok)
66
+ return errorResult(`Error (Edit): cannot stat "${path}": ${exists.error.message}`);
67
+ if (!exists.value) {
68
+ if (singleOld === "") {
69
+ const created = a.new_string ?? "";
70
+ if (created === "")
71
+ return errorResult(violationText("Edit", { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." }));
72
+ const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, created);
73
+ if (gated !== undefined)
74
+ return errorResult(gated);
75
+ const write = env.writeFileExclusive !== undefined
76
+ ? await env.writeFileExclusive(r.key, created, ctx.signal)
77
+ : await env.writeFile(r.key, created, ctx.signal);
78
+ if (!write.ok) {
79
+ if (write.error.code === "already_exists") {
80
+ return errorResult(`Error (Edit): cannot create "${path}": file already exists (created concurrently since the existence check). Read the file first, then edit it normally (or Write after the Read to overwrite it).`);
81
+ }
82
+ return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
83
+ }
84
+ const createdNorm = normalizeFileText(created);
85
+ state.set(r.key, { hash: sha256(createdNorm), totalLines: countLines(createdNorm), truncated: false, lastReadAt: Date.now() });
86
+ return {
87
+ content: `Created ${path} (${countLines(createdNorm)} lines).`,
88
+ details: { type: "create", filePath: path, content: created },
89
+ };
90
+ }
91
+ return errorResult(`Error (Edit): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal)}`);
92
+ }
93
+ const editInfo = await env.fileInfo(r.key, ctx.signal);
94
+ if (editInfo.ok && editInfo.value.size > MAX_EDIT_BYTES) {
95
+ return errorResult(`Error (Edit): "${path}" is too large to edit (${formatByteSize(editInfo.value.size)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.`);
96
+ }
97
+ if (singleOld === "") {
98
+ const preBin = await env.readBinaryFile(r.key, ctx.signal);
99
+ if (!preBin.ok)
100
+ return errorResult(`Error (Edit): cannot read "${path}": ${preBin.error.message}`);
101
+ if (preBin.value.byteLength > MAX_EDIT_BYTES) {
102
+ return errorResult(`Error (Edit): "${path}" is too large to edit (${formatByteSize(preBin.value.byteLength)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.`);
103
+ }
104
+ const preDecResult = decodeEditBytes(preBin.value, path);
105
+ if (!preDecResult.ok)
106
+ return errorResult(preDecResult.message);
107
+ const preDec = preDecResult.value;
108
+ if (preDec.malformed)
109
+ return errorResult(`Error (Edit): "${path}" has a truncated UTF-16 body; repair/convert it with bash first.`);
110
+ if (preDec.text.trim() !== "")
111
+ return errorResult(`Error (Edit): Cannot create new file - file already exists.`);
112
+ const priorRead = state.get(r.key);
113
+ if (priorRead !== undefined) {
114
+ const staleCreate = checkStale(priorRead, sha256(preDec.text));
115
+ if (staleCreate)
116
+ return errorResult(violationText("Edit", staleCreate));
117
+ }
118
+ const newContent = normalizeEditText(a.new_string ?? "");
119
+ if (newContent === preDec.text)
120
+ return errorResult(violationText("Edit", { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." }));
121
+ const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, newContent);
122
+ if (gated !== undefined)
123
+ return errorResult(gated);
124
+ const encodedOverwrite = encodeTextForFile(newContent, preDec.encoding, preDec.endings);
125
+ const write = await env.writeFile(r.key, encodedOverwrite, ctx.signal);
126
+ if (!write.ok)
127
+ return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
128
+ const persistedOverwrite = persistedTextOf(encodedOverwrite);
129
+ state.set(r.key, { hash: sha256(persistedOverwrite), totalLines: countLines(persistedOverwrite), truncated: false, lastReadAt: Date.now() });
130
+ return {
131
+ content: `The file ${path} has been updated successfully.${FILE_STATE_TRAILER}`,
132
+ details: { type: "edit", filePath: path, originalFile: preDec.text, oldString: "", newString: newContent, replaceAll: false, edits: [{ oldString: "", newString: newContent, replaceAll: false }] },
133
+ };
134
+ }
135
+ const notRead = requireRead(state, r.key);
136
+ if (notRead)
137
+ return errorResult(await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal));
138
+ const readBin = await env.readBinaryFile(r.key, ctx.signal);
139
+ if (!readBin.ok)
140
+ return errorResult(`Error (Edit): cannot read "${path}": ${readBin.error.message}`);
141
+ if (readBin.value.byteLength > MAX_EDIT_BYTES) {
142
+ return errorResult(`Error (Edit): "${path}" is too large to edit (${formatByteSize(readBin.value.byteLength)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.`);
143
+ }
144
+ const decodedResult = decodeEditBytes(readBin.value, path);
145
+ if (!decodedResult.ok)
146
+ return errorResult(decodedResult.message);
147
+ const decoded = decodedResult.value;
148
+ if (decoded.malformed)
149
+ return errorResult(`Error (Edit): "${path}" has a truncated UTF-16 body; repair/convert it with bash first.`);
150
+ const original = decoded.text;
151
+ const entry = state.get(r.key);
152
+ const stale = checkStale(entry, sha256(original));
153
+ if (stale)
154
+ return errorResult(violationText("Edit", stale));
155
+ let working = original;
156
+ let replacements = 0;
157
+ const appliedEdits = [];
158
+ for (let i = 0; i < edits.length; i++) {
159
+ const e = edits[i];
160
+ const where = batch ? `edit ${i + 1} of ${edits.length}: ` : "";
161
+ let oldS = normalizeEditText(e.old_string);
162
+ let newS = normalizeEditText(e.new_string);
163
+ const noChange = checkNoChange(oldS, newS);
164
+ if (noChange)
165
+ return errorResult(batch ? `Error (Edit): ${where}${noChange.message}` : violationText("Edit", noChange));
166
+ if (oldS !== "" && !working.includes(oldS)) {
167
+ const resolved = resolveQuoteMatch(working, oldS);
168
+ if (resolved !== undefined && resolved !== oldS) {
169
+ newS = adaptNewStringQuotes(resolved, newS);
170
+ oldS = resolved;
171
+ }
172
+ }
173
+ const match = checkEditMatch(working, oldS, e.replace_all === true, entry.truncated);
174
+ if (match)
175
+ return errorResult(batch ? `Error (Edit): ${where}${match.message} (no changes written — the batch is atomic).` : violationText("Edit", match));
176
+ replacements += e.replace_all === true ? countOccurrences(working, oldS) : 1;
177
+ const effOld = e.replace_all === true ? oldS : deletionOldString(working, oldS, newS);
178
+ working = e.replace_all === true ? working.split(effOld).join(newS) : working.replace(effOld, () => newS);
179
+ appliedEdits.push({ oldString: effOld, newString: newS, replaceAll: e.replace_all === true });
180
+ }
181
+ const gated = await gateToolWrite(beforeWrite, "Edit", path, r.key, working);
182
+ if (gated !== undefined)
183
+ return errorResult(gated);
184
+ const encodedEdit = encodeTextForFile(working, decoded.encoding, decoded.endings);
185
+ const write = await env.writeFile(r.key, encodedEdit, ctx.signal);
186
+ if (!write.ok)
187
+ return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
188
+ const persistedEdit = persistedTextOf(encodedEdit);
189
+ state.set(r.key, { hash: sha256(persistedEdit), totalLines: countLines(persistedEdit), truncated: false, lastReadAt: Date.now() });
190
+ const a0 = appliedEdits[0];
191
+ return {
192
+ content: batch
193
+ ? `Edited ${path} (${edits.length} edit${edits.length === 1 ? "" : "s"}, ${replacements} replacement${replacements === 1 ? "" : "s"}).`
194
+ : a0.replaceAll
195
+ ? `The file ${path} has been updated. All occurrences were successfully replaced.${FILE_STATE_TRAILER}`
196
+ : `The file ${path} has been updated successfully.${FILE_STATE_TRAILER}`,
197
+ details: {
198
+ type: "edit",
199
+ filePath: path,
200
+ originalFile: original,
201
+ edits: appliedEdits,
202
+ ...(batch ? {} : { oldString: a0.oldString, newString: a0.newString, replaceAll: a0.replaceAll }),
203
+ },
204
+ };
205
+ },
206
+ });
207
+ }
208
+ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite) {
209
+ return defineTool({
210
+ name: "Write",
211
+ contract: { contractId: "core.write@1", implementationRevision: "1" },
212
+ description: "Writes a file to the local filesystem, overwriting if one exists.\n" +
213
+ "\n" +
214
+ "When to use: creating a new file, or fully replacing one you've already Read. Overwriting an existing " +
215
+ "file you haven't Read will fail. For partial changes, use Edit instead. The overwrite also fails if the " +
216
+ "file changed on disk since you read it — Read it again first.",
217
+ parameters: Type.Object({
218
+ ...FILE_PATH_PARAMS,
219
+ content: Type.String({ description: "The content to write to the file" }),
220
+ }),
221
+ effect: "idempotent",
222
+ execute: async (args, ctx) => {
223
+ const { content } = args;
224
+ const path = fileArgPath(args);
225
+ if (path === undefined)
226
+ return errorResult(`Error (Write): file_path is required.`);
227
+ const ipynb = ipynbRedirect("Write", path);
228
+ if (ipynb)
229
+ return errorResult(ipynb);
230
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
231
+ if (!r.ok)
232
+ return errorResult(violationText("Write", r.violation));
233
+ const exists = await env.exists(r.key, ctx.signal);
234
+ if (!exists.ok)
235
+ return errorResult(`Error (Write): cannot stat "${path}": ${exists.error.message}`);
236
+ let originalFile;
237
+ if (exists.value) {
238
+ const notRead = requireRead(state, r.key);
239
+ if (notRead) {
240
+ return errorResult(`${violationText("Write", notRead)} ` +
241
+ `(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead — e.g. \`rm\` + rewrite, or \`iconv\`.)`);
242
+ }
243
+ const readBin = await env.readBinaryFile(r.key, ctx.signal);
244
+ if (!readBin.ok)
245
+ return errorResult(`Error (Write): cannot re-read "${path}" to verify it is unchanged: ${readBin.error.message}`);
246
+ const decodedPrev = decodeTextBytes(readBin.value);
247
+ if (decodedPrev.malformed)
248
+ return errorResult(`Error (Write): "${path}" has a truncated UTF-16 body; repair/convert it with bash first.`);
249
+ const stale = checkStale(state.get(r.key), sha256(decodedPrev.text));
250
+ if (stale)
251
+ return errorResult(violationText("Write", stale));
252
+ originalFile = decodedPrev.text;
253
+ const gated = await gateToolWrite(beforeWrite, "Write", path, r.key, content);
254
+ if (gated !== undefined)
255
+ return errorResult(gated);
256
+ const outgoing = splitLeadingBom(content);
257
+ const writeEncoding = outgoing.hadBom && !decodedPrev.encoding.hadBom ? { ...decodedPrev.encoding, hadBom: true } : decodedPrev.encoding;
258
+ const write = await env.writeFile(r.key, encodeTextForFile(outgoing.text, writeEncoding, "preserve"), ctx.signal);
259
+ if (!write.ok)
260
+ return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
261
+ const totalLines = countLines(outgoing.text);
262
+ state.set(r.key, { hash: sha256(normalizeFileText(content)), totalLines, truncated: false, lastReadAt: Date.now() });
263
+ return {
264
+ content: `The file ${path} has been updated successfully.${FILE_STATE_TRAILER}`,
265
+ details: { type: "update", filePath: path, content: normalizeFileText(content), originalFile },
266
+ };
267
+ }
268
+ const gatedCreate = await gateToolWrite(beforeWrite, "Write", path, r.key, content);
269
+ if (gatedCreate !== undefined)
270
+ return errorResult(gatedCreate);
271
+ const write = await env.writeFile(r.key, content, ctx.signal);
272
+ if (!write.ok)
273
+ return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
274
+ const totalLines = countLines(content);
275
+ state.set(r.key, { hash: sha256(normalizeFileText(content)), totalLines, truncated: false, lastReadAt: Date.now() });
276
+ void originalFile;
277
+ return {
278
+ content: `File created successfully at: ${path}${FILE_STATE_TRAILER}`,
279
+ details: { type: "create", filePath: path, content },
280
+ };
281
+ },
282
+ });
283
+ }
284
+ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite) {
285
+ return defineTool({
286
+ name: "NotebookEdit",
287
+ contract: { contractId: "core.notebook_edit@1", implementationRevision: "1" },
288
+ description: "Replaces, inserts, or deletes a single cell in a Jupyter notebook (.ipynb file).\n" +
289
+ "\n" +
290
+ "Usage:\n" +
291
+ "- You must use the Read tool on the notebook in this conversation before editing — this tool will fail otherwise.\n" +
292
+ "- `cell_id` is the `id` attribute shown in the Read tool's `<cell id=\"...\">` output. It is required for `replace` and `delete`.\n" +
293
+ "- `edit_mode` defaults to `replace`. Use `insert` to add a new cell after the cell with the given `cell_id` " +
294
+ "(or at the beginning of the notebook if `cell_id` is omitted) — `cell_type` is required when inserting. " +
295
+ "Use `delete` to remove the cell.",
296
+ parameters: Type.Object({
297
+ notebook_path: Type.String({ description: "The path to the Jupyter notebook file to edit (within the configured root)." }),
298
+ cell_id: Type.Optional(Type.String({
299
+ description: "The ID of the cell to edit. When inserting a new cell, the new cell will be inserted after the cell with this ID, or at the beginning if not specified.",
300
+ })),
301
+ new_source: Type.String({ description: "The new source for the cell" }),
302
+ cell_type: Type.Optional(Type.Union([Type.Literal("code"), Type.Literal("markdown")], {
303
+ description: "The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.",
304
+ })),
305
+ edit_mode: Type.Optional(Type.Union([Type.Literal("replace"), Type.Literal("insert"), Type.Literal("delete")], {
306
+ description: "The type of edit to make (replace, insert, delete). Defaults to replace.",
307
+ })),
308
+ }),
309
+ effect: "write",
310
+ execute: async (args, ctx) => {
311
+ const a = args;
312
+ const { notebook_path, cell_id, new_source } = a;
313
+ let cellType = a.cell_type;
314
+ const mode = a.edit_mode ?? "replace";
315
+ if (!notebook_path.toLowerCase().endsWith(".ipynb")) {
316
+ return errorResult(`Error (NotebookEdit): "${notebook_path}" is not a .ipynb file; use Edit for other file types.`);
317
+ }
318
+ if (mode === "insert" && !cellType)
319
+ return errorResult(`Error (NotebookEdit): Cell type is required when using edit_mode=insert.`);
320
+ if (mode !== "insert" && !cell_id)
321
+ return errorResult(`Error (NotebookEdit): cell_id is required for replace/delete.`);
322
+ const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
323
+ if (!r.ok)
324
+ return errorResult(violationText("NotebookEdit", r.violation));
325
+ const notRead = requireRead(state, r.key);
326
+ if (notRead)
327
+ return errorResult(await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal));
328
+ const readBin = await env.readBinaryFile(r.key, ctx.signal);
329
+ if (!readBin.ok)
330
+ return errorResult(`Error (NotebookEdit): cannot read "${notebook_path}": ${readBin.error.message}`);
331
+ const decodedNb = decodeTextBytes(readBin.value);
332
+ if (decodedNb.malformed)
333
+ return errorResult(`Error (NotebookEdit): "${notebook_path}" has a truncated UTF-16 body; repair/convert it with bash first.`);
334
+ const nbText = decodedNb.text;
335
+ const stale = checkStale(state.get(r.key), sha256(nbText));
336
+ if (stale)
337
+ return errorResult(violationText("NotebookEdit", stale));
338
+ let nb;
339
+ try {
340
+ nb = JSON.parse(nbText);
341
+ }
342
+ catch {
343
+ return errorResult(`Error (NotebookEdit): "${notebook_path}" is not valid JSON.`);
344
+ }
345
+ if (!nb || typeof nb !== "object" || !Array.isArray(nb.cells)) {
346
+ return errorResult(`Error (NotebookEdit): "${notebook_path}" is not a valid notebook (no cells array).`);
347
+ }
348
+ const cells = nb.cells;
349
+ let h;
350
+ if (!cell_id) {
351
+ h = 0;
352
+ }
353
+ else {
354
+ h = cells.findIndex((c) => c && c.id === cell_id);
355
+ if (h === -1) {
356
+ const m = /^cell-(\d+)$/.exec(cell_id);
357
+ if (m) {
358
+ const idx = parseInt(m[1], 10);
359
+ if (idx >= 0 && idx < cells.length)
360
+ h = idx;
361
+ }
362
+ }
363
+ if (h === -1)
364
+ return errorResult(`Error (NotebookEdit): Cell with ID "${cell_id}" not found in notebook.`);
365
+ if (mode === "insert")
366
+ h += 1;
367
+ }
368
+ let g = mode;
369
+ if (g === "replace" && h === cells.length) {
370
+ g = "insert";
371
+ cellType = cellType ?? "code";
372
+ }
373
+ const supportsId = (nb.nbformat ?? 0) > 4 || ((nb.nbformat ?? 0) === 4 && (nb.nbformat_minor ?? 0) >= 5);
374
+ const newId = supportsId && g === "insert" ? randomUUID().slice(0, 8) : undefined;
375
+ let oldSource;
376
+ if (g === "delete") {
377
+ const prev = cells[h];
378
+ oldSource = Array.isArray(prev.source) ? prev.source.join("") : typeof prev.source === "string" ? prev.source : undefined;
379
+ cells.splice(h, 1);
380
+ }
381
+ else if (g === "insert") {
382
+ const cell = cellType === "markdown"
383
+ ? { cell_type: "markdown", id: newId, source: new_source, metadata: {} }
384
+ : { cell_type: "code", id: newId, source: new_source, metadata: {}, execution_count: null, outputs: [] };
385
+ cells.splice(h, 0, cell);
386
+ }
387
+ else {
388
+ const cell = cells[h];
389
+ oldSource = Array.isArray(cell.source) ? cell.source.join("") : typeof cell.source === "string" ? cell.source : undefined;
390
+ cell.source = new_source;
391
+ if (cell.cell_type === "code") {
392
+ cell.execution_count = null;
393
+ cell.outputs = [];
394
+ }
395
+ if (cellType && cellType !== cell.cell_type)
396
+ cell.cell_type = cellType;
397
+ }
398
+ const updated = JSON.stringify(nb, null, 1);
399
+ const gated = await gateToolWrite(beforeWrite, "NotebookEdit", notebook_path, r.key, updated);
400
+ if (gated !== undefined)
401
+ return errorResult(gated);
402
+ const w = await env.writeFile(r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal);
403
+ if (!w.ok)
404
+ return errorResult(`Error (NotebookEdit): cannot write "${notebook_path}": ${w.error.message}`);
405
+ state.set(r.key, { hash: sha256(updated), totalLines: countLines(updated), truncated: false, lastReadAt: Date.now() });
406
+ const nbDetails = {
407
+ type: "notebook-edit",
408
+ notebookPath: notebook_path,
409
+ ...(cell_id ? { cellId: cell_id } : {}),
410
+ editMode: g,
411
+ ...(cellType ? { cellType } : {}),
412
+ newSource: new_source,
413
+ ...(oldSource !== undefined ? { oldSource } : {}),
414
+ originalFile: nbText,
415
+ };
416
+ if (g === "delete")
417
+ return { content: `Deleted cell ${cell_id}`, details: nbDetails };
418
+ if (g === "insert")
419
+ return { content: `Inserted cell${newId ? ` ${newId}` : ""} with ${new_source}`, details: { ...nbDetails, ...(newId ? { newCellId: newId } : {}) } };
420
+ return { content: `Updated cell ${cell_id} with ${new_source}`, details: nbDetails };
421
+ },
422
+ });
423
+ }
@@ -1,70 +1,17 @@
1
- import type { AgentTool, ExecutionEnv } from "../../internal/harness.js";
2
- import type { BeforeWriteHook, ToolEffect } from "../../core/types.js";
1
+ import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
+ import type { BeforeWriteHook } from "../../core/types.js";
3
3
  import { type TaskRegistry } from "../../core/task-registry.js";
4
4
  import { type ReadFileState } from "./safety.js";
5
- import { type ImageDownsampler } from "../../core/mcp.js";
6
- import { type PdfModelCapabilities } from "./pdf.js";
7
- export { pdfModelCapabilitiesOf, type PdfModelCapabilities } from "./pdf.js";
8
- export declare const SLICED_READ_MAX_BYTES: number;
9
- export declare const MAX_EDIT_BYTES: number;
10
- export type ReadImageDownsamplerOption = ImageDownsampler | false | undefined;
11
- export declare function msTimeoutToSec(timeoutMs: number | undefined): number;
12
- export declare function seededFileUnchangedReminder(filePath: string): string;
13
- export declare function isReadDedupStubResult(resultText: string): boolean;
14
- export declare function seedReadFileStateFromContext(state: ReadFileState, key: string, content: string): void;
15
- export declare function applyCompactionToReadFileState(state: ReadFileState, attachedComplete: ReadonlyArray<{
16
- path: string;
17
- content: string;
18
- }>, preserveKeys?: ReadonlyArray<string>): void;
19
- export interface CwdRef {
20
- current: string;
21
- }
22
- export declare function makeReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
23
- taskId?: string;
24
- principal?: string;
25
- }) => boolean): AgentTool;
26
- export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook } from "../../core/types.js";
27
- export declare function makeEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
28
- export declare function makeWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
29
- export declare function makeNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook): AgentTool;
30
- export declare function makeGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
31
- export declare function makeGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
32
- export declare const HAND_TOOL_EFFECTS: Readonly<Record<string, ToolEffect>>;
33
- export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
34
- export declare function parseLeadingCommandName(command: string): {
35
- name: string;
36
- } | {
37
- reject: string;
38
- };
39
- export declare function classifyCompoundReadonly(command: string, allow: ReadonlySet<string>): string | undefined;
40
- export declare function bashReversibilityProbe(allow?: readonly string[]): (args: unknown) => {
41
- reversible: boolean;
42
- };
43
- export interface ExecClampOption {
44
- deadlineMs: () => number | undefined;
45
- onClamp?: (requestedSec: number, clampedSec: number) => void;
46
- registerKillHandle?: (toolCallId: string, handle: {
47
- pid: number;
48
- kill: () => void;
49
- }) => () => void;
50
- }
51
- export declare function bashExitOneInterpretation(command: string): string | undefined;
52
- export declare function canAutoBackground(command: string): boolean;
53
- export declare function makeBashTool(env: ExecutionEnv, rootCanonical: string, coAuthor?: string | false, cwdRef?: CwdRef, taskOpts?: {
54
- taskRegistry?: TaskRegistry;
55
- taskOwner?: string;
56
- taskScope?: string;
57
- sessionId?: string;
58
- onTaskNotification?: (n: import("../../core/task-notification.js").TaskNotificationPayload, opts?: {
59
- priority?: "now" | "next" | "later";
60
- }) => void;
61
- detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
62
- execClamp?: ExecClampOption;
63
- autoBackgroundOnTimeout?: boolean;
64
- }): AgentTool;
65
- export declare function makeBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption): AgentTool;
66
- export declare function makeBashOutputTool(env: ExecutionEnv): AgentTool;
67
- export declare function makeKillShellTool(env: ExecutionEnv, registry?: TaskRegistry): AgentTool;
5
+ import type { PdfModelCapabilities } from "./pdf.js";
6
+ export * from "./fs-shared.js";
7
+ export * from "./fs-pdf.js";
8
+ export * from "./fs-read.js";
9
+ export * from "./fs-write.js";
10
+ export * from "./fs-search-tools.js";
11
+ export * from "./bash-readonly-classifier.js";
12
+ export * from "./fs-bash.js";
13
+ import { type CwdRef, type ReadImageDownsamplerOption } from "./fs-shared.js";
14
+ import { type ExecClampOption } from "./fs-bash.js";
68
15
  export interface HandsToolkitOptions {
69
16
  additionalRoots?: readonly string[];
70
17
  includeShell?: boolean;