zeitlich 0.2.37 → 0.2.39
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 +18 -0
- package/dist/{activities-Bb-nAjwQ.d.ts → activities-Bmu7XnaG.d.ts} +4 -4
- package/dist/{activities-vkI4_3CC.d.cts → activities-ByBFLvm2.d.cts} +4 -4
- package/dist/adapter-id-BB-mmrts.d.cts +17 -0
- package/dist/adapter-id-BB-mmrts.d.ts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
- package/dist/adapters/sandbox/bedrock/index.cjs +3 -3
- package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
- package/dist/adapters/sandbox/bedrock/index.d.cts +6 -6
- package/dist/adapters/sandbox/bedrock/index.d.ts +6 -6
- package/dist/adapters/sandbox/bedrock/index.js +3 -3
- package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
- package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/daytona/index.cjs +3 -3
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +4 -4
- package/dist/adapters/sandbox/daytona/index.d.ts +4 -4
- package/dist/adapters/sandbox/daytona/index.js +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.cjs +26 -14
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +24 -4
- package/dist/adapters/sandbox/e2b/index.d.ts +24 -4
- package/dist/adapters/sandbox/e2b/index.js +26 -14
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs +3 -3
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +4 -4
- package/dist/adapters/sandbox/inmemory/index.d.ts +4 -4
- package/dist/adapters/sandbox/inmemory/index.js +3 -3
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +150 -13
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +9 -8
- package/dist/adapters/thread/anthropic/index.d.ts +9 -8
- package/dist/adapters/thread/anthropic/index.js +150 -14
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +9 -3
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +6 -5
- package/dist/adapters/thread/anthropic/workflow.d.ts +6 -5
- package/dist/adapters/thread/anthropic/workflow.js +9 -4
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +154 -13
- package/dist/adapters/thread/google-genai/index.cjs.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/index.js +154 -14
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +9 -3
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +6 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +6 -5
- package/dist/adapters/thread/google-genai/workflow.js +9 -4
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/index.cjs +16 -0
- package/dist/adapters/thread/index.cjs.map +1 -0
- package/dist/adapters/thread/index.d.cts +34 -0
- package/dist/adapters/thread/index.d.ts +34 -0
- package/dist/adapters/thread/index.js +12 -0
- package/dist/adapters/thread/index.js.map +1 -0
- package/dist/adapters/thread/langchain/index.cjs +149 -14
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +9 -8
- package/dist/adapters/thread/langchain/index.d.ts +9 -8
- package/dist/adapters/thread/langchain/index.js +149 -15
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +9 -3
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +6 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +6 -5
- package/dist/adapters/thread/langchain/workflow.js +9 -4
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/index.cjs +367 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +365 -61
- package/dist/index.js.map +1 -1
- package/dist/{proxy-DEtowJyd.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
- package/dist/{proxy-0smGKvx8.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
- package/dist/{thread-manager-C-C4pI2z.d.ts → thread-manager-CcRXasqs.d.ts} +2 -2
- package/dist/{thread-manager-D4vgzYrh.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -2
- package/dist/{thread-manager-3fszQih4.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -2
- package/dist/{thread-manager-CzYln2OC.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -2
- package/dist/{types-CPKDl-y_.d.ts → types-Bcbiq8iv.d.cts} +195 -22
- package/dist/{types-CNuWnvy9.d.ts → types-DAsQ21Rt.d.ts} +1 -1
- package/dist/{types-B37hKoWA.d.ts → types-DpHTX-iO.d.ts} +58 -1
- package/dist/{types-BO7Yju20.d.cts → types-Dt8-HBBT.d.ts} +195 -22
- package/dist/{types-D08CXPh8.d.cts → types-hFFi-Zd9.d.cts} +58 -1
- package/dist/{types-DWEUmYAJ.d.cts → types-lm8tMNJQ.d.cts} +1 -1
- package/dist/{types-tQL9njTu.d.cts → types-yx0LzPGn.d.cts} +21 -7
- package/dist/{types-tQL9njTu.d.ts → types-yx0LzPGn.d.ts} +21 -7
- package/dist/{workflow-CjXHbZZc.d.ts → workflow-Bmf9EtDW.d.ts} +83 -3
- package/dist/{workflow-Do_lzJpT.d.cts → workflow-Bx9utBwb.d.cts} +83 -3
- package/dist/workflow.cjs +266 -39
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +3 -3
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.js +264 -41
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -2
- package/src/adapters/sandbox/bedrock/index.ts +12 -3
- package/src/adapters/sandbox/daytona/index.ts +12 -3
- package/src/adapters/sandbox/e2b/index.ts +36 -14
- package/src/adapters/sandbox/e2b/types.ts +16 -0
- package/src/adapters/sandbox/inmemory/index.ts +12 -3
- package/src/adapters/thread/adapter-id.test.ts +42 -0
- package/src/adapters/thread/anthropic/activities.ts +40 -5
- package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
- package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
- package/src/adapters/thread/anthropic/index.ts +3 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +7 -1
- package/src/adapters/thread/anthropic/proxy.ts +3 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +27 -1
- package/src/adapters/thread/google-genai/activities.ts +44 -5
- package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
- package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
- package/src/adapters/thread/google-genai/index.ts +3 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +8 -2
- package/src/adapters/thread/google-genai/proxy.ts +3 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +27 -1
- package/src/adapters/thread/index.ts +39 -0
- package/src/adapters/thread/langchain/activities.ts +40 -5
- package/src/adapters/thread/langchain/adapter-id.ts +16 -0
- package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
- package/src/adapters/thread/langchain/index.ts +3 -0
- package/src/adapters/thread/langchain/model-invoker.ts +7 -1
- package/src/adapters/thread/langchain/proxy.ts +3 -2
- package/src/adapters/thread/langchain/thread-manager.ts +27 -1
- package/src/lib/lifecycle.ts +14 -5
- package/src/lib/model/types.ts +7 -0
- package/src/lib/sandbox/manager.ts +26 -18
- package/src/lib/sandbox/types.ts +27 -7
- package/src/lib/session/session-edge-cases.integration.test.ts +336 -4
- package/src/lib/session/session.integration.test.ts +192 -2
- package/src/lib/session/session.ts +102 -8
- package/src/lib/session/types.ts +66 -3
- package/src/lib/state/index.ts +1 -0
- package/src/lib/state/manager.integration.test.ts +109 -0
- package/src/lib/state/manager.ts +38 -8
- package/src/lib/state/types.ts +25 -0
- package/src/lib/subagent/handler.ts +124 -11
- package/src/lib/subagent/index.ts +5 -1
- package/src/lib/subagent/subagent.integration.test.ts +628 -104
- package/src/lib/subagent/types.ts +63 -14
- package/src/lib/subagent/workflow.ts +29 -2
- package/src/lib/thread/index.ts +5 -0
- package/src/lib/thread/keys.test.ts +101 -0
- package/src/lib/thread/keys.ts +94 -0
- package/src/lib/thread/manager.test.ts +139 -0
- package/src/lib/thread/manager.ts +105 -9
- package/src/lib/thread/proxy.ts +3 -0
- package/src/lib/thread/types.ts +64 -1
- package/src/lib/tool-router/index.ts +2 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +92 -0
- package/src/lib/tool-router/router.integration.test.ts +12 -0
- package/src/lib/tool-router/router.ts +89 -16
- package/src/lib/tool-router/types.ts +42 -1
- package/src/lib/types.ts +12 -0
- package/src/workflow.ts +14 -1
- package/tsup.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeitlich",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.39",
|
|
4
4
|
"description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
"default": "./dist/workflow.js"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
+
"./adapters/thread": {
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./dist/adapters/thread/index.d.ts",
|
|
33
|
+
"default": "./dist/adapters/thread/index.js"
|
|
34
|
+
},
|
|
35
|
+
"require": {
|
|
36
|
+
"types": "./dist/adapters/thread/index.d.ts",
|
|
37
|
+
"default": "./dist/adapters/thread/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
30
40
|
"./adapters/thread/langchain": {
|
|
31
41
|
"import": {
|
|
32
42
|
"types": "./dist/adapters/thread/langchain/index.d.ts",
|
|
@@ -210,7 +220,7 @@
|
|
|
210
220
|
"node": ">=18"
|
|
211
221
|
},
|
|
212
222
|
"devDependencies": {
|
|
213
|
-
"@anthropic-ai/sdk": "^0.
|
|
223
|
+
"@anthropic-ai/sdk": "^0.81.0",
|
|
214
224
|
"@aws-sdk/client-bedrock-agentcore": "^3.900.0",
|
|
215
225
|
"@daytonaio/sdk": "^0.158.1",
|
|
216
226
|
"@e2b/code-interpreter": "^2.3.3",
|
|
@@ -239,15 +239,24 @@ export class BedrockSandboxProvider implements SandboxProvider<
|
|
|
239
239
|
// Bedrock sandboxes don't support pause, so resume is a no-op
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
async snapshot(
|
|
242
|
+
async snapshot(
|
|
243
|
+
_sandboxId: string,
|
|
244
|
+
_options?: BedrockSandboxCreateOptions
|
|
245
|
+
): Promise<SandboxSnapshot> {
|
|
243
246
|
throw new SandboxNotSupportedError("snapshot");
|
|
244
247
|
}
|
|
245
248
|
|
|
246
|
-
async restore(
|
|
249
|
+
async restore(
|
|
250
|
+
_snapshot: SandboxSnapshot,
|
|
251
|
+
_options?: BedrockSandboxCreateOptions
|
|
252
|
+
): Promise<never> {
|
|
247
253
|
throw new SandboxNotSupportedError("restore");
|
|
248
254
|
}
|
|
249
255
|
|
|
250
|
-
async fork(
|
|
256
|
+
async fork(
|
|
257
|
+
_sandboxId: string,
|
|
258
|
+
_options?: BedrockSandboxCreateOptions
|
|
259
|
+
): Promise<Sandbox> {
|
|
251
260
|
throw new SandboxNotSupportedError("fork");
|
|
252
261
|
}
|
|
253
262
|
|
|
@@ -149,17 +149,26 @@ export class DaytonaSandboxProvider implements SandboxProvider<
|
|
|
149
149
|
// Daytona sandboxes don't support pause, so resume is a no-op
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
async fork(
|
|
152
|
+
async fork(
|
|
153
|
+
_sandboxId: string,
|
|
154
|
+
_options?: DaytonaSandboxCreateOptions
|
|
155
|
+
): Promise<Sandbox> {
|
|
153
156
|
throw new Error("Not implemented");
|
|
154
157
|
}
|
|
155
158
|
|
|
156
|
-
async snapshot(
|
|
159
|
+
async snapshot(
|
|
160
|
+
_sandboxId: string,
|
|
161
|
+
_options?: DaytonaSandboxCreateOptions
|
|
162
|
+
): Promise<SandboxSnapshot> {
|
|
157
163
|
throw new SandboxNotSupportedError(
|
|
158
164
|
"snapshot (use Daytona's native snapshot API directly)"
|
|
159
165
|
);
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
async restore(
|
|
168
|
+
async restore(
|
|
169
|
+
_snapshot: SandboxSnapshot,
|
|
170
|
+
_options?: DaytonaSandboxCreateOptions
|
|
171
|
+
): Promise<never> {
|
|
163
172
|
throw new SandboxNotSupportedError(
|
|
164
173
|
"restore (use Daytona's native snapshot API directly)"
|
|
165
174
|
);
|
|
@@ -76,11 +76,19 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
76
76
|
private readonly defaultTemplate?: string;
|
|
77
77
|
private readonly defaultWorkspaceBase: string;
|
|
78
78
|
private readonly defaultTimeoutMs?: number;
|
|
79
|
+
private readonly defaultAllowInternetAccess?: boolean;
|
|
80
|
+
private readonly defaultNetwork?: E2bSandboxConfig["network"];
|
|
81
|
+
private readonly defaultMetadata?: E2bSandboxConfig["metadata"];
|
|
82
|
+
private readonly defaultLifecycle?: E2bSandboxConfig["lifecycle"];
|
|
79
83
|
|
|
80
84
|
constructor(config?: E2bSandboxConfig) {
|
|
81
85
|
this.defaultTemplate = config?.template;
|
|
82
86
|
this.defaultWorkspaceBase = config?.workspaceBase ?? "/home/user";
|
|
83
87
|
this.defaultTimeoutMs = config?.timeoutMs;
|
|
88
|
+
this.defaultAllowInternetAccess = config?.allowInternetAccess;
|
|
89
|
+
this.defaultNetwork = config?.network;
|
|
90
|
+
this.defaultMetadata = config?.metadata;
|
|
91
|
+
this.defaultLifecycle = config?.lifecycle;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
async create(
|
|
@@ -142,7 +150,10 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
142
150
|
await E2bSdkSandbox.connect(sandboxId);
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
async snapshot(
|
|
153
|
+
async snapshot(
|
|
154
|
+
sandboxId: string,
|
|
155
|
+
_options?: E2bSandboxCreateOptions
|
|
156
|
+
): Promise<SandboxSnapshot> {
|
|
146
157
|
const { snapshotId } = await E2bSdkSandbox.createSnapshot(sandboxId);
|
|
147
158
|
return {
|
|
148
159
|
sandboxId,
|
|
@@ -152,14 +163,18 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
152
163
|
};
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
async restore(
|
|
166
|
+
async restore(
|
|
167
|
+
snapshot: SandboxSnapshot,
|
|
168
|
+
options?: E2bSandboxCreateOptions
|
|
169
|
+
): Promise<Sandbox> {
|
|
156
170
|
const data = snapshot.data as { snapshotId?: string } | null;
|
|
157
171
|
if (!data?.snapshotId) {
|
|
158
172
|
throw new SandboxNotSupportedError(
|
|
159
173
|
"restore: snapshot is missing snapshotId"
|
|
160
174
|
);
|
|
161
175
|
}
|
|
162
|
-
const
|
|
176
|
+
const sdkOpts = this.buildSdkCreateOpts(options);
|
|
177
|
+
const sdkSandbox = await E2bSdkSandbox.create(data.snapshotId, sdkOpts);
|
|
163
178
|
return new E2bSandboxImpl(
|
|
164
179
|
sdkSandbox.sandboxId,
|
|
165
180
|
sdkSandbox,
|
|
@@ -177,9 +192,13 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
194
|
|
|
180
|
-
async fork(
|
|
195
|
+
async fork(
|
|
196
|
+
sandboxId: string,
|
|
197
|
+
options?: E2bSandboxCreateOptions
|
|
198
|
+
): Promise<Sandbox> {
|
|
181
199
|
const { snapshotId } = await E2bSdkSandbox.createSnapshot(sandboxId);
|
|
182
|
-
const
|
|
200
|
+
const sdkOpts = this.buildSdkCreateOpts(options);
|
|
201
|
+
const sdkSandbox = await E2bSdkSandbox.create(snapshotId, sdkOpts);
|
|
183
202
|
return new E2bSandboxImpl(
|
|
184
203
|
sdkSandbox.sandboxId,
|
|
185
204
|
sdkSandbox,
|
|
@@ -188,22 +207,25 @@ export class E2bSandboxProvider implements SandboxProvider<
|
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
private buildSdkCreateOpts(options?: E2bSandboxCreateOptions) {
|
|
210
|
+
const network = options?.network ?? this.defaultNetwork;
|
|
211
|
+
const lifecycle = options?.lifecycle ?? this.defaultLifecycle;
|
|
191
212
|
return {
|
|
192
213
|
envs: options?.env,
|
|
193
214
|
timeoutMs: options?.timeoutMs ?? this.defaultTimeoutMs,
|
|
194
|
-
metadata: options?.metadata,
|
|
195
|
-
allowInternetAccess:
|
|
196
|
-
|
|
215
|
+
metadata: options?.metadata ?? this.defaultMetadata,
|
|
216
|
+
allowInternetAccess:
|
|
217
|
+
options?.allowInternetAccess ?? this.defaultAllowInternetAccess,
|
|
218
|
+
network: network
|
|
197
219
|
? {
|
|
198
|
-
allowOut:
|
|
199
|
-
denyOut:
|
|
200
|
-
allowPublicTraffic:
|
|
220
|
+
allowOut: network.allowOut,
|
|
221
|
+
denyOut: network.denyOut,
|
|
222
|
+
allowPublicTraffic: network.allowPublicTraffic,
|
|
201
223
|
}
|
|
202
224
|
: undefined,
|
|
203
|
-
lifecycle:
|
|
225
|
+
lifecycle: lifecycle
|
|
204
226
|
? {
|
|
205
|
-
onTimeout:
|
|
206
|
-
autoResume:
|
|
227
|
+
onTimeout: lifecycle.onTimeout,
|
|
228
|
+
autoResume: lifecycle.autoResume,
|
|
207
229
|
}
|
|
208
230
|
: undefined,
|
|
209
231
|
};
|
|
@@ -6,6 +6,14 @@ import type { E2bSandboxFileSystem } from "./filesystem";
|
|
|
6
6
|
*/
|
|
7
7
|
export type E2bSandbox = Sandbox & { fs: E2bSandboxFileSystem };
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Provider-level defaults for E2B sandboxes. Every lifecycle op
|
|
11
|
+
* (`create` / `restore` / `fork`) merges per-call options on top of these —
|
|
12
|
+
* per-call options win per-field. This is how E2B preserves sandbox-level
|
|
13
|
+
* config (network policy, metadata, lifecycle, timeout) across
|
|
14
|
+
* snapshot/restore and fork, since the E2B API treats those as pure
|
|
15
|
+
* create-time inputs that aren't carried in the snapshot blob.
|
|
16
|
+
*/
|
|
9
17
|
export interface E2bSandboxConfig {
|
|
10
18
|
/** Sandbox template name or ID */
|
|
11
19
|
template?: string;
|
|
@@ -13,6 +21,14 @@ export interface E2bSandboxConfig {
|
|
|
13
21
|
workspaceBase?: string;
|
|
14
22
|
/** Sandbox idle timeout in milliseconds */
|
|
15
23
|
timeoutMs?: number;
|
|
24
|
+
/** Default outbound internet access policy */
|
|
25
|
+
allowInternetAccess?: boolean;
|
|
26
|
+
/** Default outbound network allow/deny rules */
|
|
27
|
+
network?: SandboxCreateOptions["network"];
|
|
28
|
+
/** Default metadata surfaced via provider list/query APIs */
|
|
29
|
+
metadata?: SandboxCreateOptions["metadata"];
|
|
30
|
+
/** Default sandbox timeout behaviour */
|
|
31
|
+
lifecycle?: SandboxCreateOptions["lifecycle"];
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
export interface E2bSandboxCreateOptions extends SandboxCreateOptions {
|
|
@@ -183,7 +183,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
|
|
|
183
183
|
return { sandbox };
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
async snapshot(
|
|
186
|
+
async snapshot(
|
|
187
|
+
sandboxId: string,
|
|
188
|
+
_options?: SandboxCreateOptions
|
|
189
|
+
): Promise<SandboxSnapshot> {
|
|
187
190
|
const sandbox = this.sandboxes.get(sandboxId);
|
|
188
191
|
if (!sandbox) throw new SandboxNotFoundError(sandboxId);
|
|
189
192
|
|
|
@@ -210,7 +213,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
|
|
|
210
213
|
};
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
async fork(
|
|
216
|
+
async fork(
|
|
217
|
+
sandboxId: string,
|
|
218
|
+
_options?: SandboxCreateOptions
|
|
219
|
+
): Promise<Sandbox> {
|
|
214
220
|
const sandbox = await this.get(sandboxId);
|
|
215
221
|
|
|
216
222
|
const entries = await sandbox.fs.readdirWithFileTypes("/");
|
|
@@ -228,7 +234,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
|
|
|
228
234
|
return newSandbox.sandbox;
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
async restore(
|
|
237
|
+
async restore(
|
|
238
|
+
snapshot: SandboxSnapshot,
|
|
239
|
+
_options?: SandboxCreateOptions
|
|
240
|
+
): Promise<Sandbox> {
|
|
232
241
|
const { files } = snapshot.data as { files: Record<string, string> };
|
|
233
242
|
const initialFiles: InitialFiles = {};
|
|
234
243
|
for (const [path, content] of Object.entries(files)) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, expectTypeOf } from "vitest";
|
|
2
|
+
import { ADAPTER_ID as LANGCHAIN } from "./langchain/adapter-id";
|
|
3
|
+
import { ADAPTER_ID as GOOGLE_GENAI } from "./google-genai/adapter-id";
|
|
4
|
+
import { ADAPTER_ID as ANTHROPIC } from "./anthropic/adapter-id";
|
|
5
|
+
import {
|
|
6
|
+
LANGCHAIN_ADAPTER_ID,
|
|
7
|
+
GOOGLE_GENAI_ADAPTER_ID,
|
|
8
|
+
ANTHROPIC_ADAPTER_ID,
|
|
9
|
+
type ThreadAdapterId,
|
|
10
|
+
} from "./index";
|
|
11
|
+
|
|
12
|
+
describe("thread adapter identity", () => {
|
|
13
|
+
it("langchain ADAPTER_ID is the wire-format string", () => {
|
|
14
|
+
expect(LANGCHAIN).toBe("langChain");
|
|
15
|
+
expect(LANGCHAIN_ADAPTER_ID).toBe("langChain");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("google-genai ADAPTER_ID is the wire-format string", () => {
|
|
19
|
+
expect(GOOGLE_GENAI).toBe("googleGenAI");
|
|
20
|
+
expect(GOOGLE_GENAI_ADAPTER_ID).toBe("googleGenAI");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("anthropic ADAPTER_ID is the wire-format string", () => {
|
|
24
|
+
expect(ANTHROPIC).toBe("anthropic");
|
|
25
|
+
expect(ANTHROPIC_ADAPTER_ID).toBe("anthropic");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("ADAPTER_ID values narrow to string literals, not `string`", () => {
|
|
29
|
+
expectTypeOf(LANGCHAIN).toEqualTypeOf<"langChain">();
|
|
30
|
+
expectTypeOf(GOOGLE_GENAI).toEqualTypeOf<"googleGenAI">();
|
|
31
|
+
expectTypeOf(ANTHROPIC).toEqualTypeOf<"anthropic">();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("ThreadAdapterId is the discriminated union of every built-in id", () => {
|
|
35
|
+
const allow = (_id: ThreadAdapterId): void => undefined;
|
|
36
|
+
allow(LANGCHAIN);
|
|
37
|
+
allow(GOOGLE_GENAI);
|
|
38
|
+
allow(ANTHROPIC);
|
|
39
|
+
// @ts-expect-error — arbitrary strings aren't members of the union
|
|
40
|
+
allow("someOtherAdapter");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type Redis from "ioredis";
|
|
2
2
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
3
3
|
import type { ToolResultConfig } from "../../../lib/types";
|
|
4
|
+
import type { PersistedThreadState } from "../../../lib/state/types";
|
|
4
5
|
import type {
|
|
5
6
|
ActivityToolHandler,
|
|
6
7
|
RouterContext,
|
|
@@ -22,11 +23,10 @@ import {
|
|
|
22
23
|
createAnthropicModelInvoker,
|
|
23
24
|
type AnthropicModelInvokerConfig,
|
|
24
25
|
} from "./model-invoker";
|
|
25
|
-
|
|
26
|
-
const ADAPTER_PREFIX = "anthropic" as const;
|
|
26
|
+
import { ADAPTER_ID } from "./adapter-id";
|
|
27
27
|
|
|
28
28
|
export type AnthropicThreadOps<TScope extends string = ""> = PrefixedThreadOps<
|
|
29
|
-
ScopedPrefix<TScope, typeof
|
|
29
|
+
ScopedPrefix<TScope, typeof ADAPTER_ID>,
|
|
30
30
|
AnthropicContent
|
|
31
31
|
>;
|
|
32
32
|
|
|
@@ -209,17 +209,52 @@ export function createAnthropicAdapter(
|
|
|
209
209
|
redis,
|
|
210
210
|
threadId: sourceThreadId,
|
|
211
211
|
key: threadKey,
|
|
212
|
+
hooks: config.hooks,
|
|
212
213
|
});
|
|
213
214
|
await thread.fork(targetThreadId);
|
|
214
215
|
},
|
|
216
|
+
|
|
217
|
+
async truncateThread(
|
|
218
|
+
threadId: string,
|
|
219
|
+
messageId: string,
|
|
220
|
+
threadKey?: string,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
|
|
223
|
+
await thread.truncateFromId(messageId);
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async loadThreadState(
|
|
227
|
+
threadId: string,
|
|
228
|
+
threadKey?: string
|
|
229
|
+
): Promise<PersistedThreadState | null> {
|
|
230
|
+
const thread = createAnthropicThreadManager({
|
|
231
|
+
redis,
|
|
232
|
+
threadId,
|
|
233
|
+
key: threadKey,
|
|
234
|
+
});
|
|
235
|
+
return thread.loadState();
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
async saveThreadState(
|
|
239
|
+
threadId: string,
|
|
240
|
+
state: PersistedThreadState,
|
|
241
|
+
threadKey?: string
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
const thread = createAnthropicThreadManager({
|
|
244
|
+
redis,
|
|
245
|
+
threadId,
|
|
246
|
+
key: threadKey,
|
|
247
|
+
});
|
|
248
|
+
await thread.saveState(state);
|
|
249
|
+
},
|
|
215
250
|
};
|
|
216
251
|
|
|
217
252
|
function createActivities<S extends string = "">(
|
|
218
253
|
scope?: S
|
|
219
254
|
): AnthropicThreadOps<S> {
|
|
220
255
|
const prefix = scope
|
|
221
|
-
? `${
|
|
222
|
-
:
|
|
256
|
+
? `${ADAPTER_ID}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
|
|
257
|
+
: ADAPTER_ID;
|
|
223
258
|
const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
|
|
224
259
|
return Object.fromEntries(
|
|
225
260
|
Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public adapter identity for the Anthropic thread adapter.
|
|
3
|
+
*
|
|
4
|
+
* This value is wire format — it appears as the prefix for Temporal
|
|
5
|
+
* activity names (e.g. `anthropicCodingAgentInitializeThread`) and must
|
|
6
|
+
* never change, since renaming it would orphan existing persisted
|
|
7
|
+
* threads and break in-flight workflows.
|
|
8
|
+
*
|
|
9
|
+
* Re-exported from `zeitlich/adapters/thread/anthropic` so downstream
|
|
10
|
+
* consumers can use the exact same literal the adapter uses internally,
|
|
11
|
+
* typed as the narrow string literal `"anthropic"`.
|
|
12
|
+
*/
|
|
13
|
+
export const ADAPTER_ID = "anthropic" as const;
|
|
14
|
+
|
|
15
|
+
/** Narrow string-literal type for {@link ADAPTER_ID}. */
|
|
16
|
+
export type AdapterId = typeof ADAPTER_ID;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { StoredMessage } from "./thread-manager";
|
|
3
|
+
import { createAnthropicThreadManager } from "./thread-manager";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Stateful in-memory Redis mock sufficient for fork / replaceAll flows.
|
|
7
|
+
// Only the commands used by createThreadManager are implemented.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
function createStatefulRedis() {
|
|
11
|
+
const lists = new Map<string, string[]>();
|
|
12
|
+
const strings = new Map<string, string>();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
exists: vi.fn(async (...keys: string[]) =>
|
|
16
|
+
keys.reduce(
|
|
17
|
+
(acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
|
|
18
|
+
0
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
lrange: vi.fn(async (key: string, start: number, stop: number) => {
|
|
22
|
+
const list = lists.get(key) ?? [];
|
|
23
|
+
const end = stop === -1 ? list.length : stop + 1;
|
|
24
|
+
return list.slice(start, end);
|
|
25
|
+
}),
|
|
26
|
+
rpush: vi.fn(async (key: string, ...values: string[]) => {
|
|
27
|
+
const list = lists.get(key) ?? [];
|
|
28
|
+
list.push(...values);
|
|
29
|
+
lists.set(key, list);
|
|
30
|
+
return list.length;
|
|
31
|
+
}),
|
|
32
|
+
ltrim: vi.fn(async (key: string, start: number, stop: number) => {
|
|
33
|
+
const list = lists.get(key) ?? [];
|
|
34
|
+
const end = stop === -1 ? list.length : stop + 1;
|
|
35
|
+
lists.set(key, list.slice(start, end));
|
|
36
|
+
return "OK";
|
|
37
|
+
}),
|
|
38
|
+
del: vi.fn(async (...keys: string[]) => {
|
|
39
|
+
let removed = 0;
|
|
40
|
+
for (const k of keys) {
|
|
41
|
+
if (lists.delete(k)) removed++;
|
|
42
|
+
if (strings.delete(k)) removed++;
|
|
43
|
+
}
|
|
44
|
+
return removed;
|
|
45
|
+
}),
|
|
46
|
+
set: vi.fn(async (key: string, value: string) => {
|
|
47
|
+
strings.set(key, value);
|
|
48
|
+
return "OK";
|
|
49
|
+
}),
|
|
50
|
+
get: vi.fn(async (key: string) => strings.get(key) ?? null),
|
|
51
|
+
expire: vi.fn(async (_key: string, _ttl: number) => 1),
|
|
52
|
+
llen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
|
|
53
|
+
eval: vi.fn(
|
|
54
|
+
async (_script: string, _numKeys: number, ...args: string[]) => {
|
|
55
|
+
const [dedupKey, listKey, , ...serialised] = args;
|
|
56
|
+
if (!dedupKey || !listKey) return 0;
|
|
57
|
+
if (strings.has(dedupKey)) return 0;
|
|
58
|
+
const list = lists.get(listKey) ?? [];
|
|
59
|
+
list.push(...serialised);
|
|
60
|
+
lists.set(listKey, list);
|
|
61
|
+
strings.set(dedupKey, "1");
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
__peek: {
|
|
66
|
+
list: (key: string): string[] => [...(lists.get(key) ?? [])],
|
|
67
|
+
strings,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const userMsg: StoredMessage = {
|
|
73
|
+
id: "msg-1",
|
|
74
|
+
message: { role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const assistantMsg: StoredMessage = {
|
|
78
|
+
id: "msg-2",
|
|
79
|
+
message: {
|
|
80
|
+
role: "assistant",
|
|
81
|
+
content: [{ type: "text", text: "Hi there!" }],
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const userMsg2: StoredMessage = {
|
|
86
|
+
id: "msg-3",
|
|
87
|
+
message: { role: "user", content: [{ type: "text", text: "Again please" }] },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
async function seedSource(
|
|
91
|
+
redis: ReturnType<typeof createStatefulRedis>,
|
|
92
|
+
threadId: string,
|
|
93
|
+
messages: StoredMessage[]
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const tm = createAnthropicThreadManager({
|
|
96
|
+
redis: redis as never,
|
|
97
|
+
threadId,
|
|
98
|
+
});
|
|
99
|
+
await tm.initialize();
|
|
100
|
+
await tm.append(messages);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe("Anthropic fork + transform hooks", () => {
|
|
104
|
+
it("behaves like fork when neither onFork hook is configured", async () => {
|
|
105
|
+
const redis = createStatefulRedis();
|
|
106
|
+
await seedSource(redis, "src", [userMsg, assistantMsg]);
|
|
107
|
+
|
|
108
|
+
const tm = createAnthropicThreadManager({
|
|
109
|
+
redis: redis as never,
|
|
110
|
+
threadId: "src",
|
|
111
|
+
});
|
|
112
|
+
const forked = await tm.fork("dst");
|
|
113
|
+
const loaded = await forked.load();
|
|
114
|
+
|
|
115
|
+
expect(loaded).toEqual([userMsg, assistantMsg]);
|
|
116
|
+
|
|
117
|
+
// Source is untouched
|
|
118
|
+
const srcLoaded = await tm.load();
|
|
119
|
+
expect(srcLoaded).toEqual([userMsg, assistantMsg]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("applies onForkTransform alone as a per-message map", async () => {
|
|
123
|
+
const redis = createStatefulRedis();
|
|
124
|
+
await seedSource(redis, "src", [userMsg, assistantMsg, userMsg2]);
|
|
125
|
+
|
|
126
|
+
const calls: Array<{
|
|
127
|
+
idx: number;
|
|
128
|
+
id: string;
|
|
129
|
+
total: number;
|
|
130
|
+
}> = [];
|
|
131
|
+
const onForkTransform = vi.fn(
|
|
132
|
+
(msg: StoredMessage, index: number, messages: readonly StoredMessage[]) => {
|
|
133
|
+
calls.push({ idx: index, id: msg.id, total: messages.length });
|
|
134
|
+
const firstBlock = (msg.message.content as Array<{ text?: string }>)[0];
|
|
135
|
+
return {
|
|
136
|
+
...msg,
|
|
137
|
+
message: {
|
|
138
|
+
...msg.message,
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text" as const,
|
|
142
|
+
text: `[T${index}] ${firstBlock?.text ?? ""}`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const tm = createAnthropicThreadManager({
|
|
151
|
+
redis: redis as never,
|
|
152
|
+
threadId: "src",
|
|
153
|
+
hooks: { onForkTransform },
|
|
154
|
+
});
|
|
155
|
+
const forked = await tm.fork("dst");
|
|
156
|
+
const loaded = await forked.load();
|
|
157
|
+
|
|
158
|
+
expect(onForkTransform).toHaveBeenCalledTimes(3);
|
|
159
|
+
expect(calls).toEqual([
|
|
160
|
+
{ idx: 0, id: "msg-1", total: 3 },
|
|
161
|
+
{ idx: 1, id: "msg-2", total: 3 },
|
|
162
|
+
{ idx: 2, id: "msg-3", total: 3 },
|
|
163
|
+
]);
|
|
164
|
+
expect(loaded).toHaveLength(3);
|
|
165
|
+
expect(loaded[0]?.message.content).toEqual([
|
|
166
|
+
{ type: "text", text: "[T0] Hello" },
|
|
167
|
+
]);
|
|
168
|
+
expect(loaded[1]?.message.content).toEqual([
|
|
169
|
+
{ type: "text", text: "[T1] Hi there!" },
|
|
170
|
+
]);
|
|
171
|
+
expect(loaded[2]?.message.content).toEqual([
|
|
172
|
+
{ type: "text", text: "[T2] Again please" },
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// Source is unchanged.
|
|
176
|
+
const srcLoaded = await tm.load();
|
|
177
|
+
expect(srcLoaded.map((m) => m.id)).toEqual(["msg-1", "msg-2", "msg-3"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("applies onForkPrepareThread alone and may change list length", async () => {
|
|
181
|
+
const redis = createStatefulRedis();
|
|
182
|
+
await seedSource(redis, "src", [userMsg, assistantMsg, userMsg2]);
|
|
183
|
+
|
|
184
|
+
const onForkPrepareThread = vi.fn(
|
|
185
|
+
async (messages: readonly StoredMessage[]) =>
|
|
186
|
+
// Drop first message and prepend a summary.
|
|
187
|
+
[
|
|
188
|
+
{
|
|
189
|
+
id: "summary-1",
|
|
190
|
+
message: {
|
|
191
|
+
role: "user" as const,
|
|
192
|
+
content: [{ type: "text" as const, text: "[summary]" }],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
...messages.slice(1),
|
|
196
|
+
]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const tm = createAnthropicThreadManager({
|
|
200
|
+
redis: redis as never,
|
|
201
|
+
threadId: "src",
|
|
202
|
+
hooks: { onForkPrepareThread },
|
|
203
|
+
});
|
|
204
|
+
const forked = await tm.fork("dst");
|
|
205
|
+
const loaded = await forked.load();
|
|
206
|
+
|
|
207
|
+
expect(onForkPrepareThread).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(loaded.map((m) => m.id)).toEqual(["summary-1", "msg-2", "msg-3"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("runs onForkPrepareThread before onForkTransform and passes prepared list as messages", async () => {
|
|
212
|
+
const redis = createStatefulRedis();
|
|
213
|
+
await seedSource(redis, "src", [userMsg, assistantMsg, userMsg2]);
|
|
214
|
+
|
|
215
|
+
const order: string[] = [];
|
|
216
|
+
const indicesSeen: Array<{ idx: number; total: number; id: string }> = [];
|
|
217
|
+
|
|
218
|
+
const onForkPrepareThread = vi.fn(
|
|
219
|
+
async (messages: readonly StoredMessage[]) => {
|
|
220
|
+
order.push("prepare");
|
|
221
|
+
// Drop the last message (length changes).
|
|
222
|
+
return messages.slice(0, -1);
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const onForkTransform = vi.fn(
|
|
227
|
+
(
|
|
228
|
+
msg: StoredMessage,
|
|
229
|
+
index: number,
|
|
230
|
+
messages: readonly StoredMessage[]
|
|
231
|
+
) => {
|
|
232
|
+
order.push("transform");
|
|
233
|
+
indicesSeen.push({ idx: index, total: messages.length, id: msg.id });
|
|
234
|
+
return {
|
|
235
|
+
...msg,
|
|
236
|
+
message: {
|
|
237
|
+
...msg.message,
|
|
238
|
+
content: [{ type: "text" as const, text: `[x${index}]` }],
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const tm = createAnthropicThreadManager({
|
|
245
|
+
redis: redis as never,
|
|
246
|
+
threadId: "src",
|
|
247
|
+
hooks: { onForkPrepareThread, onForkTransform },
|
|
248
|
+
});
|
|
249
|
+
const forked = await tm.fork("dst");
|
|
250
|
+
const loaded = await forked.load();
|
|
251
|
+
|
|
252
|
+
// prepare runs once, transform once per survivor.
|
|
253
|
+
expect(order).toEqual(["prepare", "transform", "transform"]);
|
|
254
|
+
expect(indicesSeen).toEqual([
|
|
255
|
+
{ idx: 0, total: 2, id: "msg-1" },
|
|
256
|
+
{ idx: 1, total: 2, id: "msg-2" },
|
|
257
|
+
]);
|
|
258
|
+
expect(loaded).toHaveLength(2);
|
|
259
|
+
expect(loaded[0]?.message.content).toEqual([{ type: "text", text: "[x0]" }]);
|
|
260
|
+
expect(loaded[1]?.message.content).toEqual([{ type: "text", text: "[x1]" }]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("leaves dedup markers cleared so the transformed thread can accept replays", async () => {
|
|
264
|
+
const redis = createStatefulRedis();
|
|
265
|
+
await seedSource(redis, "src", [userMsg, assistantMsg]);
|
|
266
|
+
|
|
267
|
+
const onForkTransform = vi.fn(
|
|
268
|
+
(msg: StoredMessage) => ({
|
|
269
|
+
...msg,
|
|
270
|
+
message: {
|
|
271
|
+
...msg.message,
|
|
272
|
+
content: [{ type: "text" as const, text: "[replaced]" }],
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const tm = createAnthropicThreadManager({
|
|
278
|
+
redis: redis as never,
|
|
279
|
+
threadId: "src",
|
|
280
|
+
hooks: { onForkTransform },
|
|
281
|
+
});
|
|
282
|
+
await tm.fork("dst");
|
|
283
|
+
|
|
284
|
+
// After replaceAll, dedup markers from the pre-replacement writes must be
|
|
285
|
+
// gone — otherwise an append with the same id would be silently skipped.
|
|
286
|
+
const lingering = Array.from(redis.__peek.strings.keys()).filter((k) =>
|
|
287
|
+
k.startsWith("messages:thread:dst:dedup:")
|
|
288
|
+
);
|
|
289
|
+
expect(lingering).toEqual([]);
|
|
290
|
+
});
|
|
291
|
+
});
|