zeitlich 0.2.45 → 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.
- package/README.md +137 -11
- package/dist/{activities-Coafq5zr.d.cts → activities-CPwKoUlD.d.cts} +22 -2
- package/dist/{activities-CrN-ghLo.d.ts → activities-DlaBxNID.d.ts} +22 -2
- package/dist/adapters/thread/anthropic/index.cjs +276 -71
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +62 -8
- package/dist/adapters/thread/anthropic/index.d.ts +62 -8
- package/dist/adapters/thread/anthropic/index.js +275 -72
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +38 -20
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
- package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
- package/dist/adapters/thread/anthropic/workflow.js +38 -20
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +171 -69
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +6 -4
- package/dist/adapters/thread/google-genai/index.d.ts +6 -4
- package/dist/adapters/thread/google-genai/index.js +171 -69
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +38 -20
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +7 -4
- package/dist/adapters/thread/google-genai/workflow.d.ts +7 -4
- package/dist/adapters/thread/google-genai/workflow.js +38 -20
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/langchain/index.cjs +170 -66
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +19 -4
- package/dist/adapters/thread/langchain/index.d.ts +19 -4
- package/dist/adapters/thread/langchain/index.js +170 -66
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +38 -20
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
- package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
- package/dist/adapters/thread/langchain/workflow.js +38 -20
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/cold-store-BDgJpwLI.d.ts +114 -0
- package/dist/cold-store-Z2wvK2cV.d.cts +114 -0
- package/dist/index.cjs +440 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -8
- package/dist/index.d.ts +150 -8
- package/dist/index.js +432 -68
- package/dist/index.js.map +1 -1
- package/dist/proxy-CDh3Rsa7.d.cts +40 -0
- package/dist/proxy-Du8ggERu.d.ts +40 -0
- package/dist/{thread-manager-wRVVBFgj.d.cts → thread-manager-BjoYYXgd.d.cts} +8 -2
- package/dist/{thread-manager-BsLO3Fgc.d.cts → thread-manager-D8zKNFZ9.d.cts} +8 -2
- package/dist/{thread-manager-Bi1XlbpJ.d.ts → thread-manager-DtHYws2F.d.ts} +8 -2
- package/dist/{thread-manager-BhkOyQ1I.d.ts → thread-manager-Dw96FKH1.d.ts} +8 -2
- package/dist/{types-C66-BVBr.d.cts → types-BMJrsHo0.d.cts} +17 -1
- package/dist/{types-BkX4HLzi.d.ts → types-CtdOquo3.d.ts} +17 -1
- package/dist/{types-CdALEF3z.d.cts → types-DNEl5uxQ.d.cts} +38 -0
- package/dist/{types-ChAy_jSP.d.ts → types-qQVZfhoT.d.ts} +38 -0
- package/dist/{workflow-DMmiaw6w.d.cts → workflow-BH9ImDGq.d.cts} +48 -2
- package/dist/{workflow-BwT5EybR.d.ts → workflow-Cdw3-RNB.d.ts} +48 -2
- package/dist/workflow.cjs +47 -4
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +2 -2
- package/dist/workflow.d.ts +2 -2
- package/dist/workflow.js +47 -5
- package/dist/workflow.js.map +1 -1
- package/package.json +14 -3
- package/src/adapters/thread/anthropic/activities.ts +82 -39
- package/src/adapters/thread/anthropic/index.ts +8 -0
- package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
- package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
- package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
- package/src/adapters/thread/anthropic/proxy.ts +1 -0
- package/src/adapters/thread/anthropic/thread-manager.ts +9 -1
- package/src/adapters/thread/google-genai/activities.ts +64 -40
- package/src/adapters/thread/google-genai/proxy.ts +1 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +9 -1
- package/src/adapters/thread/langchain/activities.ts +63 -36
- package/src/adapters/thread/langchain/proxy.ts +1 -0
- package/src/adapters/thread/langchain/thread-manager.ts +9 -1
- package/src/index.ts +21 -2
- package/src/lib/session/session-edge-cases.integration.test.ts +12 -0
- package/src/lib/session/session.integration.test.ts +138 -0
- package/src/lib/session/session.ts +29 -0
- package/src/lib/session/types.ts +22 -0
- package/src/lib/subagent/define.ts +1 -0
- package/src/lib/subagent/handler.ts +11 -2
- package/src/lib/subagent/subagent.integration.test.ts +139 -0
- package/src/lib/subagent/types.ts +16 -0
- package/src/lib/thread/cold-store.test.ts +221 -0
- package/src/lib/thread/cold-store.ts +269 -0
- package/src/lib/thread/index.ts +32 -0
- package/src/lib/thread/keys.ts +20 -0
- package/src/lib/thread/manager.ts +16 -27
- package/src/lib/thread/proxy.ts +79 -27
- package/src/lib/thread/snapshot.test.ts +443 -0
- package/src/lib/thread/snapshot.ts +163 -0
- package/src/lib/thread/test-utils.ts +228 -0
- package/src/lib/thread/tiered.test.ts +281 -0
- package/src/lib/thread/tiered.ts +135 -0
- package/src/lib/thread/types.ts +16 -0
- package/src/tools/edit/handler.test.ts +177 -0
- package/src/tools/edit/handler.ts +249 -47
- package/src/tools/edit/tool.ts +40 -0
- package/src/tools/task-create/handler.ts +1 -1
- package/src/tools/task-update/handler.ts +1 -1
- package/src/workflow.ts +2 -2
- package/dist/proxy-Bf7uI-Hw.d.cts +0 -24
- package/dist/proxy-COqA95FW.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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
273
|
+
const content = await fs.readFile(file_path);
|
|
274
|
+
const result = applyEditPlan(content, edits);
|
|
81
275
|
|
|
82
|
-
|
|
83
|
-
? `
|
|
84
|
-
|
|
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:
|
|
88
|
-
data: {
|
|
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";
|
package/src/tools/edit/tool.ts
CHANGED
|
@@ -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>;
|
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-CdALEF3z.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 };
|
package/dist/proxy-COqA95FW.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { proxyActivities, ActivityInterfaceFor } from '@temporalio/workflow';
|
|
2
|
-
import { T as ThreadOps } from './types-ChAy_jSP.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 };
|