zeitlich 0.2.46 → 0.2.48
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 +66 -6
- package/dist/{activities-CyeiqK_f.d.cts → activities-BlQR5gX4.d.cts} +3 -3
- package/dist/{activities-Bm4TLTid.d.ts → activities-DCaIPQBT.d.ts} +3 -3
- package/dist/adapters/thread/anthropic/index.cjs +105 -6
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +48 -9
- package/dist/adapters/thread/anthropic/index.d.ts +48 -9
- package/dist/adapters/thread/anthropic/index.js +104 -7
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +38 -22
- 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 -22
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +6 -5
- package/dist/adapters/thread/google-genai/index.d.ts +6 -5
- package/dist/adapters/thread/google-genai/workflow.cjs +38 -22
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +7 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +7 -5
- package/dist/adapters/thread/google-genai/workflow.js +38 -22
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +6 -5
- package/dist/adapters/thread/langchain/index.d.ts +6 -5
- package/dist/adapters/thread/langchain/workflow.cjs +38 -22
- 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 -22
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/{cold-store-BC5L5Z8A.d.cts → cold-store-UL13Sstw.d.cts} +8 -11
- package/dist/{cold-store-CFHwemBJ.d.ts → cold-store-aD4TSKlU.d.ts} +8 -11
- package/dist/index.cjs +311 -99
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -9
- package/dist/index.d.ts +21 -9
- package/dist/index.js +312 -102
- package/dist/index.js.map +1 -1
- package/dist/proxy-BAty3CWM.d.cts +40 -0
- package/dist/proxy-mbnwBhHw.d.ts +40 -0
- package/dist/{thread-manager-DduoSkvJ.d.ts → thread-manager-CICj68PI.d.ts} +2 -2
- package/dist/{thread-manager-D33SUmZa.d.cts → thread-manager-DsXvJ5cJ.d.cts} +2 -2
- package/dist/{thread-manager-B-zy3xrs.d.ts → thread-manager-DtEtbUkp.d.ts} +2 -2
- package/dist/{thread-manager-9tezUcLW.d.cts → thread-manager-R6c3lnJy.d.cts} +2 -2
- package/dist/{types-oxt8GN97.d.cts → types-DDLPnxBh.d.cts} +1 -1
- package/dist/{types-L5bvbF-n.d.ts → types-DF4wzWQG.d.ts} +1 -1
- package/dist/{types-CnuN9T6t.d.cts → types-DWeyCTYK.d.cts} +47 -0
- package/dist/{types-CwN6_tAL.d.ts → types-DwBYd0ij.d.ts} +47 -0
- package/dist/{workflow-DIaIV7L2.d.cts → workflow-DVNPR7eX.d.cts} +17 -2
- package/dist/{workflow-B1TOcHbt.d.ts → workflow-DdaU7_j4.d.ts} +17 -2
- package/dist/workflow.cjs +80 -12
- 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 +80 -13
- package/dist/workflow.js.map +1 -1
- package/package.json +14 -8
- package/src/adapters/thread/anthropic/activities.ts +18 -11
- 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/google-genai/proxy.ts +1 -0
- package/src/adapters/thread/langchain/proxy.ts +1 -0
- package/src/index.ts +1 -1
- package/src/lib/lifecycle.ts +13 -1
- package/src/lib/session/session-edge-cases.integration.test.ts +44 -0
- package/src/lib/session/session.ts +15 -0
- package/src/lib/subagent/define.ts +1 -0
- package/src/lib/subagent/handler.ts +41 -6
- package/src/lib/subagent/subagent.integration.test.ts +178 -0
- package/src/lib/subagent/types.ts +16 -0
- package/src/lib/thread/cold-store.test.ts +33 -5
- package/src/lib/thread/cold-store.ts +50 -31
- package/src/lib/thread/proxy.ts +79 -29
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +36 -0
- package/src/lib/tool-router/router.ts +21 -3
- package/src/lib/tool-router/types.ts +20 -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-BxFyd6cg.d.cts +0 -24
- package/dist/proxy-Cskmj4Yx.d.ts +0 -24
package/src/lib/thread/proxy.ts
CHANGED
|
@@ -8,55 +8,105 @@ import {
|
|
|
8
8
|
proxyActivities,
|
|
9
9
|
workflowInfo,
|
|
10
10
|
type ActivityInterfaceFor,
|
|
11
|
+
type ActivityOptions,
|
|
11
12
|
} from "@temporalio/workflow";
|
|
12
13
|
import type { ThreadOps } from "../session/types";
|
|
13
14
|
|
|
15
|
+
type OpName = keyof ThreadOps;
|
|
16
|
+
|
|
17
|
+
/** Tight `startToCloseTimeout` so a sick Redis surfaces quickly via retry. */
|
|
18
|
+
const DEFAULT_OPTIONS: ActivityOptions = {
|
|
19
|
+
startToCloseTimeout: "10s",
|
|
20
|
+
retry: {
|
|
21
|
+
maximumAttempts: 6,
|
|
22
|
+
initialInterval: "5s",
|
|
23
|
+
maximumInterval: "15m",
|
|
24
|
+
backoffCoefficient: 4,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
|
-
*
|
|
29
|
+
* `heartbeatTimeout` assumes the built-in S3 cold store's progress
|
|
30
|
+
* events (multipart `Upload` + chunked stream read). Stalls trip via
|
|
31
|
+
* heartbeat rather than `startToCloseTimeout`. Custom backends without
|
|
32
|
+
* progress events should override via `perOp`. Harmless on Redis-only
|
|
33
|
+
* deployments — the activities no-op.
|
|
34
|
+
*/
|
|
35
|
+
const BUILTIN_PER_OP: Partial<Record<OpName, ActivityOptions>> = {
|
|
36
|
+
hydrateThread: { startToCloseTimeout: "60s", heartbeatTimeout: "15s" },
|
|
37
|
+
flushThread: { startToCloseTimeout: "60s", heartbeatTimeout: "15s" },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `perOp[op]` layers shallow-rightmost over `defaults` and the
|
|
42
|
+
* built-in cold-tier overlay (`hydrateThread` / `flushThread`).
|
|
43
|
+
* A bare {@link ActivityOptions} is also accepted (treated as `{ defaults }`).
|
|
16
44
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* proxyAnthropicThreadOps(undefined, {
|
|
48
|
+
* defaults: { startToCloseTimeout: "5s" },
|
|
49
|
+
* perOp: {
|
|
50
|
+
* flushThread: { startToCloseTimeout: "180s" }, // heartbeatTimeout still inherited
|
|
51
|
+
* },
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export interface ThreadOpsProxyOptions {
|
|
56
|
+
defaults?: ActivityOptions;
|
|
57
|
+
perOp?: Partial<Record<OpName, ActivityOptions>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isProxyOptionsShape(o: object): o is ThreadOpsProxyOptions {
|
|
61
|
+
return "defaults" in o || "perOp" in o;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a workflow-safe Temporal activity proxy for {@link ThreadOps}.
|
|
20
66
|
*
|
|
21
67
|
* @param adapterPrefix - Adapter identifier (e.g. "anthropic", "googleGenAI", "langChain")
|
|
22
|
-
* @param scope -
|
|
23
|
-
* @param options -
|
|
68
|
+
* @param scope - Workflow scope. Defaults to `workflowInfo().workflowType`.
|
|
69
|
+
* @param options - {@link ThreadOpsProxyOptions} or a bare {@link ActivityOptions}.
|
|
24
70
|
*/
|
|
25
71
|
export function createThreadOpsProxy(
|
|
26
72
|
adapterPrefix: string,
|
|
27
73
|
scope?: string,
|
|
28
|
-
options?:
|
|
74
|
+
options?: ActivityOptions | ThreadOpsProxyOptions
|
|
29
75
|
): ActivityInterfaceFor<ThreadOps> {
|
|
30
76
|
const resolvedScope = scope ?? workflowInfo().workflowType;
|
|
31
77
|
|
|
78
|
+
const opts: ThreadOpsProxyOptions =
|
|
79
|
+
options && isProxyOptionsShape(options) ? options : { defaults: options };
|
|
80
|
+
|
|
81
|
+
const base = opts.defaults ?? DEFAULT_OPTIONS;
|
|
32
82
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
-
const
|
|
34
|
-
options ?? {
|
|
35
|
-
startToCloseTimeout: "10s",
|
|
36
|
-
retry: {
|
|
37
|
-
maximumAttempts: 6,
|
|
38
|
-
initialInterval: "5s",
|
|
39
|
-
maximumInterval: "15m",
|
|
40
|
-
backoffCoefficient: 4,
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
);
|
|
83
|
+
const baseActs = proxyActivities<Record<string, (...args: any[]) => any>>(base);
|
|
44
84
|
|
|
45
85
|
const prefix = `${adapterPrefix}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
|
|
46
86
|
const p = (key: string): string =>
|
|
47
87
|
`${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
48
88
|
|
|
89
|
+
const pick = (op: OpName): unknown => {
|
|
90
|
+
const overlay = { ...BUILTIN_PER_OP[op], ...opts.perOp?.[op] };
|
|
91
|
+
if (Object.keys(overlay).length === 0) return baseActs[p(op)];
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
|
+
return proxyActivities<Record<string, (...args: any[]) => any>>({
|
|
94
|
+
...base,
|
|
95
|
+
...overlay,
|
|
96
|
+
})[p(op)];
|
|
97
|
+
};
|
|
98
|
+
|
|
49
99
|
return {
|
|
50
|
-
initializeThread:
|
|
51
|
-
appendHumanMessage:
|
|
52
|
-
appendToolResult:
|
|
53
|
-
appendAgentMessage:
|
|
54
|
-
appendSystemMessage:
|
|
55
|
-
forkThread:
|
|
56
|
-
truncateThread:
|
|
57
|
-
loadThreadState:
|
|
58
|
-
saveThreadState:
|
|
59
|
-
hydrateThread:
|
|
60
|
-
flushThread:
|
|
100
|
+
initializeThread: pick("initializeThread"),
|
|
101
|
+
appendHumanMessage: pick("appendHumanMessage"),
|
|
102
|
+
appendToolResult: pick("appendToolResult"),
|
|
103
|
+
appendAgentMessage: pick("appendAgentMessage"),
|
|
104
|
+
appendSystemMessage: pick("appendSystemMessage"),
|
|
105
|
+
forkThread: pick("forkThread"),
|
|
106
|
+
truncateThread: pick("truncateThread"),
|
|
107
|
+
loadThreadState: pick("loadThreadState"),
|
|
108
|
+
saveThreadState: pick("saveThreadState"),
|
|
109
|
+
hydrateThread: pick("hydrateThread"),
|
|
110
|
+
flushThread: pick("flushThread"),
|
|
61
111
|
} as ActivityInterfaceFor<ThreadOps>;
|
|
62
112
|
}
|
|
@@ -134,6 +134,42 @@ describe("createToolRouter edge cases", () => {
|
|
|
134
134
|
expect(appendSpy.calls).toHaveLength(0);
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
// --- assistantMessageId propagation into RouterContext ---
|
|
138
|
+
|
|
139
|
+
it("forwards assistantMessageId from ProcessToolCallsContext into RouterContext", async () => {
|
|
140
|
+
let capturedAssistantMessageId: string | undefined;
|
|
141
|
+
const captureTool = defineTool({
|
|
142
|
+
name: "Capture" as const,
|
|
143
|
+
description: "captures router context",
|
|
144
|
+
schema: z.object({}),
|
|
145
|
+
handler: async (
|
|
146
|
+
_args: Record<string, never>,
|
|
147
|
+
ctx: { assistantMessageId?: string }
|
|
148
|
+
): Promise<ToolHandlerResponse<null>> => {
|
|
149
|
+
capturedAssistantMessageId = ctx.assistantMessageId;
|
|
150
|
+
return { toolResponse: "ok", data: null };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const router = createToolRouter({
|
|
155
|
+
tools: { Capture: captureTool } as const,
|
|
156
|
+
threadId: "t-1",
|
|
157
|
+
appendToolResult: appendSpy.fn,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const parsed = router.parseToolCall({
|
|
161
|
+
id: "tc-1",
|
|
162
|
+
name: "Capture",
|
|
163
|
+
args: {},
|
|
164
|
+
});
|
|
165
|
+
await router.processToolCalls([parsed], {
|
|
166
|
+
turn: 1,
|
|
167
|
+
assistantMessageId: "asst-msg-42",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(capturedAssistantMessageId).toBe("asst-msg-42");
|
|
171
|
+
});
|
|
172
|
+
|
|
137
173
|
// --- Both global and per-tool pre-hooks run in order ---
|
|
138
174
|
|
|
139
175
|
it("global pre-hook runs before per-tool pre-hook", async () => {
|
|
@@ -220,7 +220,8 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
220
220
|
toolCall: ParsedToolCallUnion<T>,
|
|
221
221
|
turn: number,
|
|
222
222
|
sandboxId?: string,
|
|
223
|
-
onRewindRequested?: (signal: RewindSignal) => void
|
|
223
|
+
onRewindRequested?: (signal: RewindSignal) => void,
|
|
224
|
+
assistantMessageId?: string
|
|
224
225
|
): Promise<ProcessedToolCall> {
|
|
225
226
|
const startTime = Date.now();
|
|
226
227
|
const tool = toolMap.get(toolCall.name);
|
|
@@ -263,6 +264,7 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
263
264
|
toolCallId: toolCall.id,
|
|
264
265
|
toolName: toolCall.name,
|
|
265
266
|
...(sandboxId !== undefined && { sandboxId }),
|
|
267
|
+
...(assistantMessageId !== undefined && { assistantMessageId }),
|
|
266
268
|
};
|
|
267
269
|
const response = await tool.handler(
|
|
268
270
|
effectiveArgs as Parameters<typeof tool.handler>[0],
|
|
@@ -419,6 +421,7 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
419
421
|
|
|
420
422
|
const turn = context?.turn ?? 0;
|
|
421
423
|
const sandboxId = context?.sandboxId;
|
|
424
|
+
const assistantMessageId = context?.assistantMessageId;
|
|
422
425
|
|
|
423
426
|
let rewindSignal: RewindSignal | undefined;
|
|
424
427
|
|
|
@@ -435,7 +438,13 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
435
438
|
const outcomes = await scope.run(async () =>
|
|
436
439
|
Promise.allSettled(
|
|
437
440
|
toolCalls.map((tc) =>
|
|
438
|
-
processToolCall(
|
|
441
|
+
processToolCall(
|
|
442
|
+
tc,
|
|
443
|
+
turn,
|
|
444
|
+
sandboxId,
|
|
445
|
+
onRewindRequested,
|
|
446
|
+
assistantMessageId
|
|
447
|
+
)
|
|
439
448
|
)
|
|
440
449
|
)
|
|
441
450
|
);
|
|
@@ -457,7 +466,13 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
457
466
|
|
|
458
467
|
const results: ToolCallResultUnion<TResults>[] = [];
|
|
459
468
|
for (const toolCall of toolCalls) {
|
|
460
|
-
const outcome = await processToolCall(
|
|
469
|
+
const outcome = await processToolCall(
|
|
470
|
+
toolCall,
|
|
471
|
+
turn,
|
|
472
|
+
sandboxId,
|
|
473
|
+
undefined,
|
|
474
|
+
assistantMessageId
|
|
475
|
+
);
|
|
461
476
|
if (outcome.kind === "rewind") {
|
|
462
477
|
rewindSignal = outcome.signal;
|
|
463
478
|
break;
|
|
@@ -492,6 +507,9 @@ export function createToolRouter<T extends ToolMap>(
|
|
|
492
507
|
...(context?.sandboxId !== undefined && {
|
|
493
508
|
sandboxId: context.sandboxId,
|
|
494
509
|
}),
|
|
510
|
+
...(context?.assistantMessageId !== undefined && {
|
|
511
|
+
assistantMessageId: context.assistantMessageId,
|
|
512
|
+
}),
|
|
495
513
|
};
|
|
496
514
|
const response = await handler(
|
|
497
515
|
toolCall.args as ToolArgs<T, TName>,
|
|
@@ -178,6 +178,18 @@ export interface RouterContext {
|
|
|
178
178
|
toolCallId: string;
|
|
179
179
|
toolName: string;
|
|
180
180
|
sandboxId?: string;
|
|
181
|
+
/**
|
|
182
|
+
* Id of the assistant message that issued this tool call (the message
|
|
183
|
+
* the session passed as `assistantMessageId` into `runAgent`). Present
|
|
184
|
+
* for any tool call processed through `processToolCalls` from a
|
|
185
|
+
* session; may be absent when the router is driven manually (e.g.
|
|
186
|
+
* tests, custom orchestrators).
|
|
187
|
+
*
|
|
188
|
+
* Subagent handlers that fork the parent's thread mid-call use this
|
|
189
|
+
* to truncate the orphan trailing assistant message from the forked
|
|
190
|
+
* thread so the child's first model call sees a well-formed history.
|
|
191
|
+
*/
|
|
192
|
+
assistantMessageId?: string;
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
/**
|
|
@@ -294,6 +306,14 @@ export interface ProcessToolCallsContext {
|
|
|
294
306
|
turn?: number;
|
|
295
307
|
/** Active sandbox ID (when a sandbox is configured for this session) */
|
|
296
308
|
sandboxId?: string;
|
|
309
|
+
/**
|
|
310
|
+
* Id of the assistant message that produced these tool calls. The
|
|
311
|
+
* router forwards it into every handler's {@link RouterContext} so
|
|
312
|
+
* handlers can reference the message they were issued from (e.g.
|
|
313
|
+
* subagent forks that need to truncate the orphan assistant message
|
|
314
|
+
* out of a parent-forked thread).
|
|
315
|
+
*/
|
|
316
|
+
assistantMessageId?: string;
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
/**
|
|
@@ -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
|
+
});
|