zeitlich 0.2.46 → 0.2.47

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 (83) hide show
  1. package/README.md +64 -6
  2. package/dist/{activities-CyeiqK_f.d.cts → activities-CPwKoUlD.d.cts} +3 -3
  3. package/dist/{activities-Bm4TLTid.d.ts → activities-DlaBxNID.d.ts} +3 -3
  4. package/dist/adapters/thread/anthropic/index.cjs +105 -6
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +48 -9
  7. package/dist/adapters/thread/anthropic/index.d.ts +48 -9
  8. package/dist/adapters/thread/anthropic/index.js +104 -7
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +38 -22
  11. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
  14. package/dist/adapters/thread/anthropic/workflow.js +38 -22
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.d.cts +6 -5
  17. package/dist/adapters/thread/google-genai/index.d.ts +6 -5
  18. package/dist/adapters/thread/google-genai/workflow.cjs +38 -22
  19. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  20. package/dist/adapters/thread/google-genai/workflow.d.cts +7 -5
  21. package/dist/adapters/thread/google-genai/workflow.d.ts +7 -5
  22. package/dist/adapters/thread/google-genai/workflow.js +38 -22
  23. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  24. package/dist/adapters/thread/langchain/index.d.cts +6 -5
  25. package/dist/adapters/thread/langchain/index.d.ts +6 -5
  26. package/dist/adapters/thread/langchain/workflow.cjs +38 -22
  27. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  28. package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
  29. package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
  30. package/dist/adapters/thread/langchain/workflow.js +38 -22
  31. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  32. package/dist/{cold-store-CFHwemBJ.d.ts → cold-store-BDgJpwLI.d.ts} +8 -11
  33. package/dist/{cold-store-BC5L5Z8A.d.cts → cold-store-Z2wvK2cV.d.cts} +8 -11
  34. package/dist/index.cjs +264 -90
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +21 -9
  37. package/dist/index.d.ts +21 -9
  38. package/dist/index.js +265 -93
  39. package/dist/index.js.map +1 -1
  40. package/dist/proxy-CDh3Rsa7.d.cts +40 -0
  41. package/dist/proxy-Du8ggERu.d.ts +40 -0
  42. package/dist/{thread-manager-D33SUmZa.d.cts → thread-manager-BjoYYXgd.d.cts} +2 -2
  43. package/dist/{thread-manager-9tezUcLW.d.cts → thread-manager-D8zKNFZ9.d.cts} +2 -2
  44. package/dist/{thread-manager-B-zy3xrs.d.ts → thread-manager-DtHYws2F.d.ts} +2 -2
  45. package/dist/{thread-manager-DduoSkvJ.d.ts → thread-manager-Dw96FKH1.d.ts} +2 -2
  46. package/dist/{types-oxt8GN97.d.cts → types-BMJrsHo0.d.cts} +1 -1
  47. package/dist/{types-L5bvbF-n.d.ts → types-CtdOquo3.d.ts} +1 -1
  48. package/dist/{types-CnuN9T6t.d.cts → types-DNEl5uxQ.d.cts} +16 -0
  49. package/dist/{types-CwN6_tAL.d.ts → types-qQVZfhoT.d.ts} +16 -0
  50. package/dist/{workflow-DIaIV7L2.d.cts → workflow-BH9ImDGq.d.cts} +17 -2
  51. package/dist/{workflow-B1TOcHbt.d.ts → workflow-Cdw3-RNB.d.ts} +17 -2
  52. package/dist/workflow.cjs +33 -3
  53. package/dist/workflow.cjs.map +1 -1
  54. package/dist/workflow.d.cts +2 -2
  55. package/dist/workflow.d.ts +2 -2
  56. package/dist/workflow.js +33 -4
  57. package/dist/workflow.js.map +1 -1
  58. package/package.json +9 -3
  59. package/src/adapters/thread/anthropic/activities.ts +18 -11
  60. package/src/adapters/thread/anthropic/index.ts +8 -0
  61. package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
  62. package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
  63. package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
  64. package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
  65. package/src/adapters/thread/anthropic/proxy.ts +1 -0
  66. package/src/adapters/thread/google-genai/proxy.ts +1 -0
  67. package/src/adapters/thread/langchain/proxy.ts +1 -0
  68. package/src/index.ts +1 -1
  69. package/src/lib/subagent/define.ts +1 -0
  70. package/src/lib/subagent/handler.ts +11 -2
  71. package/src/lib/subagent/subagent.integration.test.ts +139 -0
  72. package/src/lib/subagent/types.ts +16 -0
  73. package/src/lib/thread/cold-store.test.ts +33 -5
  74. package/src/lib/thread/cold-store.ts +50 -31
  75. package/src/lib/thread/proxy.ts +79 -29
  76. package/src/tools/edit/handler.test.ts +177 -0
  77. package/src/tools/edit/handler.ts +249 -47
  78. package/src/tools/edit/tool.ts +40 -0
  79. package/src/tools/task-create/handler.ts +1 -1
  80. package/src/tools/task-update/handler.ts +1 -1
  81. package/src/workflow.ts +2 -2
  82. package/dist/proxy-BxFyd6cg.d.cts +0 -24
  83. package/dist/proxy-Cskmj4Yx.d.ts +0 -24
@@ -0,0 +1,177 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import { InMemorySandboxProvider } from "../../adapters/sandbox/inmemory/index";
3
+ import type { Sandbox, SandboxCreateOptions } from "../../lib/sandbox";
4
+ import { SandboxManager } from "../../lib/sandbox/manager";
5
+ import type { RouterContext } from "../../lib/tool-router/types";
6
+ import { withSandbox } from "../../lib/tool-router/with-sandbox";
7
+ import { applyEditPlan, editHandler, multiEditHandler } from "./handler";
8
+
9
+ describe("edit handlers", () => {
10
+ let manager: SandboxManager<SandboxCreateOptions, Sandbox, "inMemory">;
11
+ let sandboxId: string;
12
+
13
+ const ctx = (id: string): RouterContext => ({
14
+ sandboxId: id,
15
+ threadId: "test-thread",
16
+ toolCallId: "test-call",
17
+ toolName: "FileEdit",
18
+ });
19
+
20
+ beforeEach(async () => {
21
+ manager = new SandboxManager(new InMemorySandboxProvider());
22
+ const result = await manager.create({
23
+ initialFiles: {
24
+ "/src/app.ts": [
25
+ "export function greet(name: string) {",
26
+ ' return "hello " + name;',
27
+ "}",
28
+ "",
29
+ "export const status = 'draft';",
30
+ "export const repeated = 'draft';",
31
+ "",
32
+ ].join("\n"),
33
+ },
34
+ });
35
+ expect(result).not.toBeNull();
36
+ sandboxId = (result as NonNullable<typeof result>).sandboxId;
37
+ });
38
+
39
+ it("applies one unique exact replacement", async () => {
40
+ const handler = withSandbox(manager, editHandler);
41
+
42
+ const response = await handler(
43
+ {
44
+ file_path: "/src/app.ts",
45
+ old_string: ' return "hello " + name;',
46
+ new_string: " return `hello ${name}`;",
47
+ },
48
+ ctx(sandboxId)
49
+ );
50
+
51
+ const sandbox = await manager.getSandbox(sandboxId);
52
+ await expect(sandbox.fs.readFile("/src/app.ts")).resolves.toContain(
53
+ "return `hello ${name}`;"
54
+ );
55
+ expect(response.data?.success).toBe(true);
56
+ expect(response.data?.replacements).toBe(1);
57
+ expect(response.data?.hunks?.[0]).toMatchObject({
58
+ oldStartLine: 2,
59
+ newStartLine: 2,
60
+ oldLines: [' return "hello " + name;'],
61
+ newLines: [" return `hello ${name}`;"],
62
+ });
63
+ });
64
+
65
+ it("refuses ambiguous single edits without replace_all", async () => {
66
+ const handler = withSandbox(manager, editHandler);
67
+
68
+ const response = await handler(
69
+ {
70
+ file_path: "/src/app.ts",
71
+ old_string: "draft",
72
+ new_string: "ready",
73
+ },
74
+ ctx(sandboxId)
75
+ );
76
+
77
+ const sandbox = await manager.getSandbox(sandboxId);
78
+ await expect(sandbox.fs.readFile("/src/app.ts")).resolves.toContain(
79
+ "status = 'draft'"
80
+ );
81
+ expect(response.data?.success).toBe(false);
82
+ expect(response.toolResponse).toContain("appears 2 times");
83
+ });
84
+
85
+ it("supports replace_all for one edit", async () => {
86
+ const handler = withSandbox(manager, editHandler);
87
+
88
+ const response = await handler(
89
+ {
90
+ file_path: "/src/app.ts",
91
+ old_string: "draft",
92
+ new_string: "ready",
93
+ replace_all: true,
94
+ },
95
+ ctx(sandboxId)
96
+ );
97
+
98
+ const sandbox = await manager.getSandbox(sandboxId);
99
+ const content = await sandbox.fs.readFile("/src/app.ts");
100
+ expect(content).toContain("status = 'ready'");
101
+ expect(content).toContain("repeated = 'ready'");
102
+ expect(response.data?.success).toBe(true);
103
+ expect(response.data?.replacements).toBe(2);
104
+ });
105
+
106
+ it("applies multiple edits sequentially and atomically", async () => {
107
+ const handler = withSandbox(manager, multiEditHandler);
108
+
109
+ const response = await handler(
110
+ {
111
+ file_path: "/src/app.ts",
112
+ edits: [
113
+ {
114
+ old_string: ' return "hello " + name;',
115
+ new_string: " return `hello ${name}`;",
116
+ },
117
+ { old_string: "draft", new_string: "ready", replace_all: true },
118
+ ],
119
+ },
120
+ { ...ctx(sandboxId), toolName: "FileMultiEdit" }
121
+ );
122
+
123
+ const sandbox = await manager.getSandbox(sandboxId);
124
+ const content = await sandbox.fs.readFile("/src/app.ts");
125
+ expect(content).toContain("return `hello ${name}`;");
126
+ expect(content).toContain("status = 'ready'");
127
+ expect(content).toContain("repeated = 'ready'");
128
+ expect(response.data?.success).toBe(true);
129
+ expect(response.data?.replacements).toBe(3);
130
+ expect(response.data?.hunks).toHaveLength(3);
131
+ });
132
+
133
+ it("leaves the file unchanged when a later multi-edit fails", async () => {
134
+ const handler = withSandbox(manager, multiEditHandler);
135
+ const sandbox = await manager.getSandbox(sandboxId);
136
+ const before = await sandbox.fs.readFile("/src/app.ts");
137
+
138
+ const response = await handler(
139
+ {
140
+ file_path: "/src/app.ts",
141
+ edits: [
142
+ {
143
+ old_string: ' return "hello " + name;',
144
+ new_string: " return `hello ${name}`;",
145
+ },
146
+ { old_string: "missing text", new_string: "replacement" },
147
+ ],
148
+ },
149
+ { ...ctx(sandboxId), toolName: "FileMultiEdit" }
150
+ );
151
+
152
+ await expect(sandbox.fs.readFile("/src/app.ts")).resolves.toBe(before);
153
+ expect(response.data?.success).toBe(false);
154
+ expect(response.toolResponse).toContain("edit 1");
155
+ });
156
+ });
157
+
158
+ describe("applyEditPlan", () => {
159
+ it("rejects empty old_string before mutating content", () => {
160
+ const result = applyEditPlan("abc", [{ old_string: "", new_string: "x" }]);
161
+
162
+ expect(result).toMatchObject({ ok: false, editIndex: 0 });
163
+ });
164
+
165
+ it("treats replacement text literally", () => {
166
+ const result = applyEditPlan("a.$^ b.$^", [
167
+ { old_string: ".$^", new_string: "literal" },
168
+ ]);
169
+
170
+ expect(result).toMatchObject({ ok: false });
171
+
172
+ const unique = applyEditPlan("a.$^", [
173
+ { old_string: ".$^", new_string: "literal" },
174
+ ]);
175
+ expect(unique).toMatchObject({ ok: true, content: "aliteral" });
176
+ });
177
+ });
@@ -1,15 +1,200 @@
1
1
  import type { ActivityToolHandler } from "../../lib/tool-router";
2
2
  import type { SandboxContext } from "../../lib/tool-router/with-sandbox";
3
- import type { FileEditArgs } from "./tool";
3
+ import type { FileEditArgs, FileMultiEditArgs } from "./tool";
4
4
 
5
5
  interface EditResult {
6
6
  path: string;
7
7
  success: boolean;
8
8
  replacements: number;
9
+ hunks?: EditHunk[];
9
10
  }
10
11
 
11
- function escapeRegExp(str: string): string {
12
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12
+ export interface EditHunk {
13
+ editIndex: number;
14
+ oldStartLine: number;
15
+ oldEndLine: number;
16
+ newStartLine: number;
17
+ newEndLine: number;
18
+ oldLines: string[];
19
+ newLines: string[];
20
+ }
21
+
22
+ type TextEdit = FileMultiEditArgs["edits"][number];
23
+
24
+ interface EditPlanSuccess {
25
+ ok: true;
26
+ content: string;
27
+ replacements: number;
28
+ hunks: EditHunk[];
29
+ }
30
+
31
+ interface EditPlanFailure {
32
+ ok: false;
33
+ message: string;
34
+ editIndex?: number;
35
+ }
36
+
37
+ type EditPlanResult = EditPlanSuccess | EditPlanFailure;
38
+
39
+ function splitLines(text: string): string[] {
40
+ if (text.length === 0) return [];
41
+ return text.replace(/\r\n/g, "\n").split("\n");
42
+ }
43
+
44
+ function lineNumberAt(content: string, index: number): number {
45
+ let line = 1;
46
+ for (let i = 0; i < index; i++) {
47
+ if (content.charCodeAt(i) === 10) line++;
48
+ }
49
+ return line;
50
+ }
51
+
52
+ function lineEnd(startLine: number, lines: string[]): number {
53
+ return lines.length === 0 ? startLine : startLine + lines.length - 1;
54
+ }
55
+
56
+ function indicesOf(content: string, needle: string): number[] {
57
+ const indices: number[] = [];
58
+ let cursor = 0;
59
+ while (cursor <= content.length) {
60
+ const index = content.indexOf(needle, cursor);
61
+ if (index === -1) break;
62
+ indices.push(index);
63
+ cursor = index + needle.length;
64
+ }
65
+ return indices;
66
+ }
67
+
68
+ function makeHunk(
69
+ editIndex: number,
70
+ beforeContent: string,
71
+ replacementIndex: number,
72
+ oldString: string,
73
+ newString: string
74
+ ): EditHunk {
75
+ const oldStartLine = lineNumberAt(beforeContent, replacementIndex);
76
+ const oldLines = splitLines(oldString);
77
+ const newLines = splitLines(newString);
78
+ return {
79
+ editIndex,
80
+ oldStartLine,
81
+ oldEndLine: lineEnd(oldStartLine, oldLines),
82
+ newStartLine: oldStartLine,
83
+ newEndLine: lineEnd(oldStartLine, newLines),
84
+ oldLines,
85
+ newLines,
86
+ };
87
+ }
88
+
89
+ function applyOneEdit(
90
+ content: string,
91
+ edit: TextEdit,
92
+ editIndex: number
93
+ ): EditPlanResult {
94
+ const { old_string, new_string, replace_all = false } = edit;
95
+
96
+ if (old_string.length === 0) {
97
+ return {
98
+ ok: false,
99
+ editIndex,
100
+ message: `Error: old_string for edit ${editIndex} must not be empty.`,
101
+ };
102
+ }
103
+
104
+ if (old_string === new_string) {
105
+ return {
106
+ ok: false,
107
+ editIndex,
108
+ message: `Error: old_string and new_string must be different for edit ${editIndex}.`,
109
+ };
110
+ }
111
+
112
+ const matches = indicesOf(content, old_string);
113
+
114
+ if (matches.length === 0) {
115
+ return {
116
+ ok: false,
117
+ editIndex,
118
+ message: `Error: Could not find old_string for edit ${editIndex}. Make sure it matches exactly (whitespace-sensitive).`,
119
+ };
120
+ }
121
+
122
+ if (!replace_all && matches.length > 1) {
123
+ return {
124
+ ok: false,
125
+ editIndex,
126
+ message: `Error: old_string for edit ${editIndex} appears ${matches.length} times. Provide more context to make it unique, or use replace_all: true for that edit.`,
127
+ };
128
+ }
129
+
130
+ if (replace_all) {
131
+ const hunks = matches.map((index) =>
132
+ makeHunk(editIndex, content, index, old_string, new_string)
133
+ );
134
+ return {
135
+ ok: true,
136
+ content: content.split(old_string).join(new_string),
137
+ replacements: matches.length,
138
+ hunks,
139
+ };
140
+ }
141
+
142
+ const index = matches[0];
143
+ if (index === undefined) {
144
+ return {
145
+ ok: false,
146
+ editIndex,
147
+ message: `Error: Could not find old_string for edit ${editIndex}.`,
148
+ };
149
+ }
150
+ return {
151
+ ok: true,
152
+ content:
153
+ content.slice(0, index) +
154
+ new_string +
155
+ content.slice(index + old_string.length),
156
+ replacements: 1,
157
+ hunks: [makeHunk(editIndex, content, index, old_string, new_string)],
158
+ };
159
+ }
160
+
161
+ export function applyEditPlan(
162
+ content: string,
163
+ edits: readonly TextEdit[]
164
+ ): EditPlanResult {
165
+ if (edits.length === 0) {
166
+ return {
167
+ ok: false,
168
+ message: "Error: edits must contain at least one edit.",
169
+ };
170
+ }
171
+
172
+ let current = content;
173
+ let replacements = 0;
174
+ const hunks: EditHunk[] = [];
175
+
176
+ for (const [index, edit] of edits.entries()) {
177
+ const result = applyOneEdit(current, edit, index);
178
+ if (!result.ok) return result;
179
+ current = result.content;
180
+ replacements += result.replacements;
181
+ hunks.push(...result.hunks);
182
+ }
183
+
184
+ return { ok: true, content: current, replacements, hunks };
185
+ }
186
+
187
+ function editFailureResult(
188
+ filePath: string,
189
+ message: string
190
+ ): {
191
+ toolResponse: string;
192
+ data: EditResult;
193
+ } {
194
+ return {
195
+ toolResponse: message,
196
+ data: { path: filePath, success: false, replacements: 0 },
197
+ };
13
198
  }
14
199
 
15
200
  /**
@@ -26,66 +211,83 @@ export const editHandler: ActivityToolHandler<
26
211
  const { fs } = sandbox;
27
212
  const { file_path, old_string, new_string, replace_all = false } = args;
28
213
 
29
- if (old_string === new_string) {
30
- return {
31
- toolResponse: `Error: old_string and new_string must be different.`,
32
- data: { path: file_path, success: false, replacements: 0 },
33
- };
34
- }
35
-
36
214
  try {
37
215
  const exists = await fs.exists(file_path);
38
216
  if (!exists) {
39
- return {
40
- toolResponse: `Error: File "${file_path}" does not exist.`,
41
- data: { path: file_path, success: false, replacements: 0 },
42
- };
217
+ return editFailureResult(
218
+ file_path,
219
+ `Error: File "${file_path}" does not exist.`
220
+ );
43
221
  }
44
222
 
45
223
  const content = await fs.readFile(file_path);
224
+ const result = applyEditPlan(content, [
225
+ { old_string, new_string, replace_all },
226
+ ]);
46
227
 
47
- if (!content.includes(old_string)) {
48
- return {
49
- toolResponse: `Error: Could not find the specified text in "${file_path}". Make sure old_string matches exactly (whitespace-sensitive).`,
50
- data: { path: file_path, success: false, replacements: 0 },
51
- };
228
+ if (!result.ok) {
229
+ return editFailureResult(file_path, result.message);
52
230
  }
53
231
 
54
- const escapedOldString = escapeRegExp(old_string);
55
- const globalRegex = new RegExp(escapedOldString, "g");
56
- const occurrences = (content.match(globalRegex) || []).length;
232
+ await fs.writeFile(file_path, result.content);
57
233
 
58
- if (!replace_all && occurrences > 1) {
59
- return {
60
- toolResponse: `Error: old_string appears ${occurrences} times in "${file_path}". Either provide more context to make it unique, or use replace_all: true.`,
61
- data: { path: file_path, success: false, replacements: 0 },
62
- };
63
- }
234
+ const summary = replace_all
235
+ ? `Replaced ${result.replacements} occurrence(s)`
236
+ : `Replaced 1 occurrence`;
237
+
238
+ return {
239
+ toolResponse: `${summary} in ${file_path}`,
240
+ data: {
241
+ path: file_path,
242
+ success: true,
243
+ replacements: result.replacements,
244
+ hunks: result.hunks,
245
+ },
246
+ };
247
+ } catch (error) {
248
+ const message = error instanceof Error ? error.message : "Unknown error";
249
+ return {
250
+ toolResponse: `Error editing file "${file_path}": ${message}`,
251
+ data: { path: file_path, success: false, replacements: 0 },
252
+ };
253
+ }
254
+ };
64
255
 
65
- let newContent: string;
66
- let replacements: number;
67
-
68
- if (replace_all) {
69
- newContent = content.split(old_string).join(new_string);
70
- replacements = occurrences;
71
- } else {
72
- const index = content.indexOf(old_string);
73
- newContent =
74
- content.slice(0, index) +
75
- new_string +
76
- content.slice(index + old_string.length);
77
- replacements = 1;
256
+ export const multiEditHandler: ActivityToolHandler<
257
+ FileMultiEditArgs,
258
+ EditResult,
259
+ SandboxContext
260
+ > = async (args, { sandbox }) => {
261
+ const { fs } = sandbox;
262
+ const { file_path, edits } = args;
263
+
264
+ try {
265
+ const exists = await fs.exists(file_path);
266
+ if (!exists) {
267
+ return editFailureResult(
268
+ file_path,
269
+ `Error: File "${file_path}" does not exist.`
270
+ );
78
271
  }
79
272
 
80
- await fs.writeFile(file_path, newContent);
273
+ const content = await fs.readFile(file_path);
274
+ const result = applyEditPlan(content, edits);
81
275
 
82
- const summary = replace_all
83
- ? `Replaced ${replacements} occurrence(s)`
84
- : `Replaced 1 occurrence`;
276
+ if (!result.ok) {
277
+ const suffix = result.editIndex === undefined ? "" : ` in ${file_path}`;
278
+ return editFailureResult(file_path, `${result.message}${suffix}`);
279
+ }
280
+
281
+ await fs.writeFile(file_path, result.content);
85
282
 
86
283
  return {
87
- toolResponse: `${summary} in ${file_path}`,
88
- data: { path: file_path, success: true, replacements },
284
+ toolResponse: `Applied ${edits.length} edit(s), ${result.replacements} replacement(s) in ${file_path}`,
285
+ data: {
286
+ path: file_path,
287
+ success: true,
288
+ replacements: result.replacements,
289
+ hunks: result.hunks,
290
+ },
89
291
  };
90
292
  } catch (error) {
91
293
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -1,6 +1,17 @@
1
1
  import { z } from "zod";
2
2
  import type { ToolDefinition } from "../../lib/tool-router";
3
3
 
4
+ const textEditSchema = z.object({
5
+ old_string: z.string().describe("The exact text to replace"),
6
+ new_string: z.string().describe("The text to replace it with"),
7
+ replace_all: z
8
+ .boolean()
9
+ .optional()
10
+ .describe(
11
+ "If true, replace all occurrences of old_string for this edit (default: false)"
12
+ ),
13
+ });
14
+
4
15
  export const editTool = {
5
16
  name: "FileEdit" as const,
6
17
  description: `Edit specific sections of a file by replacing text.
@@ -38,3 +49,32 @@ IMPORTANT:
38
49
  } satisfies ToolDefinition;
39
50
 
40
51
  export type FileEditArgs = z.infer<typeof editTool.schema>;
52
+
53
+ export const multiEditTool = {
54
+ name: "FileMultiEdit" as const,
55
+ description: `Apply multiple exact text replacements to one file in order.
56
+
57
+ Usage:
58
+ - Use this when a task needs several related edits in the same file
59
+ - Each edit is applied to the file content produced by the prior edit
60
+ - The operation is atomic: if any edit fails, the file is left unchanged
61
+
62
+ IMPORTANT:
63
+ - You must read the file first (in this session) before editing it
64
+ - Each old_string must match exactly (whitespace-sensitive)
65
+ - Each old_string must be unique unless that edit uses replace_all: true
66
+ - old_string and new_string must be different for every edit
67
+ `,
68
+ schema: z.object({
69
+ file_path: z
70
+ .string()
71
+ .describe("The absolute virtual path to the file to modify"),
72
+ edits: z
73
+ .array(textEditSchema)
74
+ .min(1)
75
+ .describe("Exact replacements to apply sequentially to the file"),
76
+ }),
77
+ strict: true,
78
+ } satisfies ToolDefinition;
79
+
80
+ export type FileMultiEditArgs = z.infer<typeof multiEditTool.schema>;
@@ -30,7 +30,7 @@ export function createTaskCreateHandler<
30
30
  stateManager.setTask(task);
31
31
 
32
32
  return {
33
- toolResponse: JSON.stringify(task, null, 2),
33
+ toolResponse: `Task ${task.id} created`,
34
34
  data: task,
35
35
  };
36
36
  };
@@ -64,7 +64,7 @@ export function createTaskUpdateHandler<
64
64
  stateManager.setTask(task);
65
65
 
66
66
  return {
67
- toolResponse: JSON.stringify(task, null, 2),
67
+ toolResponse: `Task ${task.id} updated`,
68
68
  data: task,
69
69
  };
70
70
  };
package/src/workflow.ts CHANGED
@@ -237,8 +237,8 @@ export { readFileTool } from "./tools/read-file/tool";
237
237
  export type { FileReadArgs } from "./tools/read-file/tool";
238
238
  export { writeFileTool } from "./tools/write-file/tool";
239
239
  export type { FileWriteArgs } from "./tools/write-file/tool";
240
- export { editTool } from "./tools/edit/tool";
241
- export type { FileEditArgs } from "./tools/edit/tool";
240
+ export { editTool, multiEditTool } from "./tools/edit/tool";
241
+ export type { FileEditArgs, FileMultiEditArgs } from "./tools/edit/tool";
242
242
 
243
243
  // Workflow task tools (state-only, no activities needed)
244
244
  export { taskCreateTool } from "./tools/task-create/tool";
@@ -1,24 +0,0 @@
1
- import { proxyActivities, ActivityInterfaceFor } from '@temporalio/workflow';
2
- import { T as ThreadOps } from './types-CnuN9T6t.cjs';
3
-
4
- /**
5
- * Shared proxy helper for thread operations.
6
- *
7
- * Each adapter re-exports a thin wrapper that supplies its prefix and
8
- * casts the return type to carry the adapter's native content type.
9
- */
10
-
11
- /**
12
- * Creates a workflow-safe Temporal activity proxy for {@link ThreadOps}.
13
- *
14
- * The proxy resolves activity names by combining the adapter prefix with
15
- * the workflow scope, so each adapter + workflow combination gets its own
16
- * namespace.
17
- *
18
- * @param adapterPrefix - Adapter identifier (e.g. "anthropic", "googleGenAI", "langChain")
19
- * @param scope - Optional workflow scope override. Defaults to `workflowInfo().workflowType`.
20
- * @param options - Optional Temporal `proxyActivities` options.
21
- */
22
- declare function createThreadOpsProxy(adapterPrefix: string, scope?: string, options?: Parameters<typeof proxyActivities>[0]): ActivityInterfaceFor<ThreadOps>;
23
-
24
- export { createThreadOpsProxy as c };
@@ -1,24 +0,0 @@
1
- import { proxyActivities, ActivityInterfaceFor } from '@temporalio/workflow';
2
- import { T as ThreadOps } from './types-CwN6_tAL.js';
3
-
4
- /**
5
- * Shared proxy helper for thread operations.
6
- *
7
- * Each adapter re-exports a thin wrapper that supplies its prefix and
8
- * casts the return type to carry the adapter's native content type.
9
- */
10
-
11
- /**
12
- * Creates a workflow-safe Temporal activity proxy for {@link ThreadOps}.
13
- *
14
- * The proxy resolves activity names by combining the adapter prefix with
15
- * the workflow scope, so each adapter + workflow combination gets its own
16
- * namespace.
17
- *
18
- * @param adapterPrefix - Adapter identifier (e.g. "anthropic", "googleGenAI", "langChain")
19
- * @param scope - Optional workflow scope override. Defaults to `workflowInfo().workflowType`.
20
- * @param options - Optional Temporal `proxyActivities` options.
21
- */
22
- declare function createThreadOpsProxy(adapterPrefix: string, scope?: string, options?: Parameters<typeof proxyActivities>[0]): ActivityInterfaceFor<ThreadOps>;
23
-
24
- export { createThreadOpsProxy as c };