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.
- package/README.md +64 -6
- package/dist/{activities-CyeiqK_f.d.cts → activities-CPwKoUlD.d.cts} +3 -3
- package/dist/{activities-Bm4TLTid.d.ts → activities-DlaBxNID.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-CFHwemBJ.d.ts → cold-store-BDgJpwLI.d.ts} +8 -11
- package/dist/{cold-store-BC5L5Z8A.d.cts → cold-store-Z2wvK2cV.d.cts} +8 -11
- package/dist/index.cjs +264 -90
- 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 +265 -93
- 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-D33SUmZa.d.cts → thread-manager-BjoYYXgd.d.cts} +2 -2
- package/dist/{thread-manager-9tezUcLW.d.cts → thread-manager-D8zKNFZ9.d.cts} +2 -2
- package/dist/{thread-manager-B-zy3xrs.d.ts → thread-manager-DtHYws2F.d.ts} +2 -2
- package/dist/{thread-manager-DduoSkvJ.d.ts → thread-manager-Dw96FKH1.d.ts} +2 -2
- package/dist/{types-oxt8GN97.d.cts → types-BMJrsHo0.d.cts} +1 -1
- package/dist/{types-L5bvbF-n.d.ts → types-CtdOquo3.d.ts} +1 -1
- package/dist/{types-CnuN9T6t.d.cts → types-DNEl5uxQ.d.cts} +16 -0
- package/dist/{types-CwN6_tAL.d.ts → types-qQVZfhoT.d.ts} +16 -0
- package/dist/{workflow-DIaIV7L2.d.cts → workflow-BH9ImDGq.d.cts} +17 -2
- package/dist/{workflow-B1TOcHbt.d.ts → workflow-Cdw3-RNB.d.ts} +17 -2
- package/dist/workflow.cjs +33 -3
- 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 +33 -4
- package/dist/workflow.js.map +1 -1
- package/package.json +9 -3
- 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/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 +33 -5
- package/src/lib/thread/cold-store.ts +50 -31
- package/src/lib/thread/proxy.ts +79 -29
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeitlich",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.47",
|
|
4
4
|
"description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -181,7 +181,8 @@
|
|
|
181
181
|
"release:pr:dry": "release-please release-pr --repo-url=bead-ai/zeitlich --token=$GITHUB_TOKEN --dry-run",
|
|
182
182
|
"release:github": "release-please github-release --repo-url=bead-ai/zeitlich --token=$GITHUB_TOKEN",
|
|
183
183
|
"release:npm": "npm publish --access public",
|
|
184
|
-
"release:publish": "npm run release:github && npm run release:npm"
|
|
184
|
+
"release:publish": "npm run release:github && npm run release:npm",
|
|
185
|
+
"eval:edit": "node scripts/run-edit-tool-evals.mjs"
|
|
185
186
|
},
|
|
186
187
|
"keywords": [
|
|
187
188
|
"ai",
|
|
@@ -200,8 +201,9 @@
|
|
|
200
201
|
"node": ">=18"
|
|
201
202
|
},
|
|
202
203
|
"devDependencies": {
|
|
203
|
-
"@anthropic-ai/sdk": "^0.
|
|
204
|
+
"@anthropic-ai/sdk": "^0.98.0",
|
|
204
205
|
"@aws-sdk/client-s3": "^3.1000.0",
|
|
206
|
+
"@aws-sdk/lib-storage": "^3.1000.0",
|
|
205
207
|
"@daytonaio/sdk": "^0.171.0",
|
|
206
208
|
"@e2b/code-interpreter": "^2.3.3",
|
|
207
209
|
"@eslint/js": "^10.0.1",
|
|
@@ -225,6 +227,7 @@
|
|
|
225
227
|
"peerDependencies": {
|
|
226
228
|
"@anthropic-ai/sdk": ">=0.50.0",
|
|
227
229
|
"@aws-sdk/client-s3": ">=3.700.0",
|
|
230
|
+
"@aws-sdk/lib-storage": ">=3.700.0",
|
|
228
231
|
"@daytonaio/sdk": ">=0.153.0",
|
|
229
232
|
"@e2b/code-interpreter": "^2.3.3",
|
|
230
233
|
"@google/genai": "^1.43.0",
|
|
@@ -246,6 +249,9 @@
|
|
|
246
249
|
"@aws-sdk/client-s3": {
|
|
247
250
|
"optional": true
|
|
248
251
|
},
|
|
252
|
+
"@aws-sdk/lib-storage": {
|
|
253
|
+
"optional": true
|
|
254
|
+
},
|
|
249
255
|
"@google/genai": {
|
|
250
256
|
"optional": true
|
|
251
257
|
},
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
createAnthropicModelInvoker,
|
|
28
28
|
type AnthropicModelInvokerConfig,
|
|
29
29
|
} from "./model-invoker";
|
|
30
|
+
import type { AnthropicPromptCacheConfig } from "./prompt-cache";
|
|
30
31
|
import { ADAPTER_ID } from "./adapter-id";
|
|
31
32
|
|
|
32
33
|
export type AnthropicThreadOps<TScope extends string = ""> = PrefixedThreadOps<
|
|
@@ -41,6 +42,11 @@ export interface AnthropicAdapterConfig {
|
|
|
41
42
|
model?: string;
|
|
42
43
|
/** Maximum tokens to generate. Defaults to 16384. */
|
|
43
44
|
maxTokens?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Controls Anthropic/Bedrock-compatible prompt caching. Defaults to enabled
|
|
47
|
+
* with an explicit 5 minute TTL. Set to `false` to disable.
|
|
48
|
+
*/
|
|
49
|
+
promptCache?: AnthropicPromptCacheConfig;
|
|
44
50
|
hooks?: AnthropicThreadManagerHooks;
|
|
45
51
|
/**
|
|
46
52
|
* Optional durable cold tier (e.g. S3, R2, GCS). When provided,
|
|
@@ -76,7 +82,8 @@ export interface AnthropicAdapter {
|
|
|
76
82
|
/** Create an invoker for a specific model name (for multi-model setups) */
|
|
77
83
|
createModelInvoker(
|
|
78
84
|
model: string,
|
|
79
|
-
maxTokens?: number
|
|
85
|
+
maxTokens?: number,
|
|
86
|
+
promptCache?: AnthropicPromptCacheConfig
|
|
80
87
|
): ModelInvoker<Anthropic.Messages.Message>;
|
|
81
88
|
/**
|
|
82
89
|
* Create prefixed thread activities for registration on the worker.
|
|
@@ -245,7 +252,7 @@ export function createAnthropicAdapter(
|
|
|
245
252
|
async truncateThread(
|
|
246
253
|
threadId: string,
|
|
247
254
|
messageId: string,
|
|
248
|
-
threadKey?: string
|
|
255
|
+
threadKey?: string
|
|
249
256
|
): Promise<void> {
|
|
250
257
|
const thread = makeProviderThread(threadId, threadKey);
|
|
251
258
|
await thread.truncateFromId(messageId);
|
|
@@ -268,18 +275,12 @@ export function createAnthropicAdapter(
|
|
|
268
275
|
await thread.saveState(state);
|
|
269
276
|
},
|
|
270
277
|
|
|
271
|
-
async hydrateThread(
|
|
272
|
-
threadId: string,
|
|
273
|
-
threadKey?: string
|
|
274
|
-
): Promise<void> {
|
|
278
|
+
async hydrateThread(threadId: string, threadKey?: string): Promise<void> {
|
|
275
279
|
if (!config.coldStore) return;
|
|
276
280
|
await makeTieredBase(threadId, threadKey).hydrate();
|
|
277
281
|
},
|
|
278
282
|
|
|
279
|
-
async flushThread(
|
|
280
|
-
threadId: string,
|
|
281
|
-
threadKey?: string
|
|
282
|
-
): Promise<void> {
|
|
283
|
+
async flushThread(threadId: string, threadKey?: string): Promise<void> {
|
|
283
284
|
if (!config.coldStore) return;
|
|
284
285
|
await makeTieredBase(threadId, threadKey).flush();
|
|
285
286
|
},
|
|
@@ -299,7 +300,8 @@ export function createAnthropicAdapter(
|
|
|
299
300
|
|
|
300
301
|
const makeInvoker = (
|
|
301
302
|
model: string,
|
|
302
|
-
maxTokens?: number
|
|
303
|
+
maxTokens?: number,
|
|
304
|
+
promptCache?: AnthropicPromptCacheConfig
|
|
303
305
|
): ModelInvoker<Anthropic.Messages.Message> => {
|
|
304
306
|
const invokerConfig: AnthropicModelInvokerConfig = {
|
|
305
307
|
redis,
|
|
@@ -309,6 +311,11 @@ export function createAnthropicAdapter(
|
|
|
309
311
|
...(config.maxTokens !== undefined && maxTokens === undefined
|
|
310
312
|
? { maxTokens: config.maxTokens }
|
|
311
313
|
: {}),
|
|
314
|
+
...(promptCache !== undefined
|
|
315
|
+
? { promptCache }
|
|
316
|
+
: config.promptCache !== undefined
|
|
317
|
+
? { promptCache: config.promptCache }
|
|
318
|
+
: {}),
|
|
312
319
|
hooks: config.hooks,
|
|
313
320
|
};
|
|
314
321
|
return createAnthropicModelInvoker(invokerConfig);
|
|
@@ -45,3 +45,11 @@ export {
|
|
|
45
45
|
invokeAnthropicModel,
|
|
46
46
|
type AnthropicModelInvokerConfig,
|
|
47
47
|
} from "./model-invoker";
|
|
48
|
+
|
|
49
|
+
// Prompt caching helpers
|
|
50
|
+
export {
|
|
51
|
+
addPromptCacheControl,
|
|
52
|
+
resolvePromptCacheOptions,
|
|
53
|
+
type AnthropicPromptCacheConfig,
|
|
54
|
+
type AnthropicPromptCacheOptions,
|
|
55
|
+
} from "./prompt-cache";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import { createAnthropicModelInvoker } from "./model-invoker";
|
|
4
|
+
import type { StoredMessage } from "./thread-manager";
|
|
5
|
+
|
|
6
|
+
function createMockRedis(stored: StoredMessage[]) {
|
|
7
|
+
return {
|
|
8
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
+
lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
|
+
ltrim: vi.fn().mockResolvedValue("OK"),
|
|
11
|
+
del: vi.fn().mockResolvedValue(1),
|
|
12
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
13
|
+
rpush: vi.fn().mockResolvedValue(1),
|
|
14
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
15
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createMockClient() {
|
|
20
|
+
const finalMessage: Anthropic.Messages.Message = {
|
|
21
|
+
id: "msg-response",
|
|
22
|
+
type: "message",
|
|
23
|
+
role: "assistant",
|
|
24
|
+
container: null,
|
|
25
|
+
model: "claude-test",
|
|
26
|
+
content: [{ type: "text", text: "ok", citations: null }],
|
|
27
|
+
stop_details: null,
|
|
28
|
+
stop_reason: "end_turn",
|
|
29
|
+
stop_sequence: null,
|
|
30
|
+
usage: {
|
|
31
|
+
cache_creation: null,
|
|
32
|
+
cache_creation_input_tokens: null,
|
|
33
|
+
cache_read_input_tokens: null,
|
|
34
|
+
inference_geo: null,
|
|
35
|
+
input_tokens: 1,
|
|
36
|
+
output_tokens: 1,
|
|
37
|
+
server_tool_use: null,
|
|
38
|
+
service_tier: null,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const stream = {
|
|
42
|
+
async *[Symbol.asyncIterator]() {},
|
|
43
|
+
finalMessage: vi.fn().mockResolvedValue(finalMessage),
|
|
44
|
+
};
|
|
45
|
+
const client = {
|
|
46
|
+
messages: {
|
|
47
|
+
stream: vi.fn().mockReturnValue(stream),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return { client, stream };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("createAnthropicModelInvoker prompt caching", () => {
|
|
54
|
+
it("sends explicit block-level cache_control by default", async () => {
|
|
55
|
+
const redis = createMockRedis([
|
|
56
|
+
{ id: "msg-1", message: { role: "user", content: "hello" } },
|
|
57
|
+
]);
|
|
58
|
+
const { client } = createMockClient();
|
|
59
|
+
const invoker = createAnthropicModelInvoker({
|
|
60
|
+
redis: redis as never,
|
|
61
|
+
client: client as never,
|
|
62
|
+
model: "claude-test",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await invoker({
|
|
66
|
+
threadId: "thread-1",
|
|
67
|
+
assistantMessageId: "assistant-1",
|
|
68
|
+
state: { tools: [] } as never,
|
|
69
|
+
agentName: "Agent",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const params = client.messages.stream.mock.calls[0]?.[0] as
|
|
73
|
+
| Anthropic.MessageCreateParams
|
|
74
|
+
| undefined;
|
|
75
|
+
expect(params).toBeDefined();
|
|
76
|
+
expect(params).not.toHaveProperty("cache_control");
|
|
77
|
+
expect(params?.messages[0]?.content).toEqual([
|
|
78
|
+
{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: "hello",
|
|
81
|
+
cache_control: { type: "ephemeral", ttl: "5m" },
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("can disable prompt caching", async () => {
|
|
87
|
+
const redis = createMockRedis([
|
|
88
|
+
{ id: "msg-1", message: { role: "user", content: "hello" } },
|
|
89
|
+
]);
|
|
90
|
+
const { client } = createMockClient();
|
|
91
|
+
const invoker = createAnthropicModelInvoker({
|
|
92
|
+
redis: redis as never,
|
|
93
|
+
client: client as never,
|
|
94
|
+
model: "claude-test",
|
|
95
|
+
promptCache: false,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await invoker({
|
|
99
|
+
threadId: "thread-1",
|
|
100
|
+
assistantMessageId: "assistant-1",
|
|
101
|
+
state: { tools: [] } as never,
|
|
102
|
+
agentName: "Agent",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const params = client.messages.stream.mock.calls[0]?.[0] as
|
|
106
|
+
| Anthropic.MessageCreateParams
|
|
107
|
+
| undefined;
|
|
108
|
+
expect(params?.messages[0]?.content).toBe("hello");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
createAnthropicThreadManager,
|
|
7
7
|
type AnthropicThreadManagerHooks,
|
|
8
8
|
} from "./thread-manager";
|
|
9
|
+
import {
|
|
10
|
+
addPromptCacheControl,
|
|
11
|
+
resolvePromptCacheOptions,
|
|
12
|
+
type AnthropicPromptCacheConfig,
|
|
13
|
+
} from "./prompt-cache";
|
|
9
14
|
import { getActivityContext } from "../../../lib/activity";
|
|
10
15
|
|
|
11
16
|
export interface AnthropicModelInvokerConfig {
|
|
@@ -14,6 +19,11 @@ export interface AnthropicModelInvokerConfig {
|
|
|
14
19
|
model: string;
|
|
15
20
|
/** Maximum tokens to generate. Defaults to 16384. */
|
|
16
21
|
maxTokens?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Controls Anthropic/Bedrock-compatible prompt caching. Defaults to enabled
|
|
24
|
+
* with an explicit 5 minute TTL. Set to `false` to disable.
|
|
25
|
+
*/
|
|
26
|
+
promptCache?: AnthropicPromptCacheConfig;
|
|
17
27
|
hooks?: AnthropicThreadManagerHooks;
|
|
18
28
|
}
|
|
19
29
|
|
|
@@ -56,6 +66,7 @@ export function createAnthropicModelInvoker({
|
|
|
56
66
|
client,
|
|
57
67
|
model,
|
|
58
68
|
maxTokens = 16384,
|
|
69
|
+
promptCache,
|
|
59
70
|
hooks,
|
|
60
71
|
}: AnthropicModelInvokerConfig) {
|
|
61
72
|
return async function invokeAnthropicModel(
|
|
@@ -76,17 +87,24 @@ export function createAnthropicModelInvoker({
|
|
|
76
87
|
// attempt's assistant + tool results so the LLM sees the same
|
|
77
88
|
// pre-call state that it saw originally.
|
|
78
89
|
await thread.truncateFromId(assistantMessageId);
|
|
79
|
-
const
|
|
90
|
+
const prepared = await thread.prepareForInvocation();
|
|
80
91
|
|
|
81
92
|
const anthropicTools = toAnthropicTools(state.tools);
|
|
82
|
-
const
|
|
93
|
+
const preparedPayload = {
|
|
94
|
+
...prepared,
|
|
95
|
+
...(anthropicTools.length > 0 ? { tools: anthropicTools } : {}),
|
|
96
|
+
};
|
|
97
|
+
const cacheOptions = resolvePromptCacheOptions(promptCache);
|
|
98
|
+
const payload = cacheOptions
|
|
99
|
+
? addPromptCacheControl(preparedPayload, cacheOptions)
|
|
100
|
+
: preparedPayload;
|
|
83
101
|
|
|
84
102
|
const params: Anthropic.MessageCreateParams = {
|
|
85
103
|
model,
|
|
86
104
|
max_tokens: maxTokens,
|
|
87
|
-
messages,
|
|
88
|
-
...(system ? { system } : {}),
|
|
89
|
-
...(tools ? { tools } : {}),
|
|
105
|
+
messages: payload.messages,
|
|
106
|
+
...(payload.system ? { system: payload.system } : {}),
|
|
107
|
+
...(payload.tools ? { tools: payload.tools } : {}),
|
|
90
108
|
};
|
|
91
109
|
|
|
92
110
|
const stream = client.messages.stream(params, { signal });
|
|
@@ -130,6 +148,7 @@ export async function invokeAnthropicModel({
|
|
|
130
148
|
client,
|
|
131
149
|
model,
|
|
132
150
|
maxTokens,
|
|
151
|
+
promptCache,
|
|
133
152
|
hooks,
|
|
134
153
|
config,
|
|
135
154
|
}: {
|
|
@@ -137,6 +156,7 @@ export async function invokeAnthropicModel({
|
|
|
137
156
|
client: Anthropic;
|
|
138
157
|
model: string;
|
|
139
158
|
maxTokens?: number;
|
|
159
|
+
promptCache?: AnthropicPromptCacheConfig;
|
|
140
160
|
hooks?: AnthropicThreadManagerHooks;
|
|
141
161
|
config: ModelInvokerConfig;
|
|
142
162
|
}): Promise<AgentResponse<Anthropic.Messages.Message>> {
|
|
@@ -145,6 +165,7 @@ export async function invokeAnthropicModel({
|
|
|
145
165
|
client,
|
|
146
166
|
model,
|
|
147
167
|
maxTokens,
|
|
168
|
+
promptCache,
|
|
148
169
|
hooks,
|
|
149
170
|
});
|
|
150
171
|
return invoker(config);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import {
|
|
4
|
+
addPromptCacheControl,
|
|
5
|
+
resolvePromptCacheOptions,
|
|
6
|
+
} from "./prompt-cache";
|
|
7
|
+
|
|
8
|
+
function firstContentBlock(
|
|
9
|
+
message: Anthropic.Messages.MessageParam
|
|
10
|
+
): Record<string, unknown> {
|
|
11
|
+
if (!Array.isArray(message.content)) {
|
|
12
|
+
throw new Error("Expected array content");
|
|
13
|
+
}
|
|
14
|
+
const block = message.content[0];
|
|
15
|
+
if (!block || typeof block !== "object") {
|
|
16
|
+
throw new Error("Expected content block");
|
|
17
|
+
}
|
|
18
|
+
return block as unknown as Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function messageAt(
|
|
22
|
+
messages: Anthropic.Messages.MessageParam[],
|
|
23
|
+
index: number
|
|
24
|
+
): Anthropic.Messages.MessageParam {
|
|
25
|
+
const message = messages[index];
|
|
26
|
+
if (!message) throw new Error(`Expected message at index ${String(index)}`);
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Anthropic prompt cache helpers", () => {
|
|
31
|
+
it("enables prompt caching by default", () => {
|
|
32
|
+
expect(resolvePromptCacheOptions()).toEqual({});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("can be disabled", () => {
|
|
36
|
+
expect(resolvePromptCacheOptions(false)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("adds Bedrock-compatible block-level cache_control to the last message", () => {
|
|
40
|
+
const payload = {
|
|
41
|
+
messages: [{ role: "user" as const, content: "hello" }],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const result = addPromptCacheControl(payload);
|
|
45
|
+
const block = firstContentBlock(messageAt(result.messages, 0));
|
|
46
|
+
|
|
47
|
+
expect(block).toEqual({
|
|
48
|
+
type: "text",
|
|
49
|
+
text: "hello",
|
|
50
|
+
cache_control: { type: "ephemeral", ttl: "5m" },
|
|
51
|
+
});
|
|
52
|
+
expect("cache_control" in result).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("supports a 1h TTL", () => {
|
|
56
|
+
const result = addPromptCacheControl(
|
|
57
|
+
{
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
role: "user" as const,
|
|
61
|
+
content: [{ type: "text" as const, text: "hello" }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{ ttl: "1h" }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
firstContentBlock(messageAt(result.messages, 0)).cache_control
|
|
70
|
+
).toEqual({
|
|
71
|
+
type: "ephemeral",
|
|
72
|
+
ttl: "1h",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not add a fifth cache breakpoint", () => {
|
|
77
|
+
const cacheControl = { type: "ephemeral" as const };
|
|
78
|
+
const result = addPromptCacheControl({
|
|
79
|
+
system: [
|
|
80
|
+
{ type: "text" as const, text: "system", cache_control: cacheControl },
|
|
81
|
+
],
|
|
82
|
+
tools: [
|
|
83
|
+
{
|
|
84
|
+
name: "tool",
|
|
85
|
+
description: "A test tool",
|
|
86
|
+
input_schema: { type: "object", properties: {} },
|
|
87
|
+
cache_control: cacheControl,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
messages: [
|
|
91
|
+
{
|
|
92
|
+
role: "user" as const,
|
|
93
|
+
content: [
|
|
94
|
+
{ type: "text" as const, text: "1", cache_control: cacheControl },
|
|
95
|
+
{ type: "text" as const, text: "2", cache_control: cacheControl },
|
|
96
|
+
{ type: "text" as const, text: "latest" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const latest = (
|
|
103
|
+
messageAt(result.messages, 0).content as unknown as Array<
|
|
104
|
+
Record<string, unknown>
|
|
105
|
+
>
|
|
106
|
+
)[2];
|
|
107
|
+
expect(latest?.cache_control).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("preserves an existing cache marker on the last cacheable block", () => {
|
|
111
|
+
const cacheControl = { type: "ephemeral" as const, ttl: "1h" as const };
|
|
112
|
+
const payload = {
|
|
113
|
+
messages: [
|
|
114
|
+
{
|
|
115
|
+
role: "user" as const,
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text" as const,
|
|
119
|
+
text: "hello",
|
|
120
|
+
cache_control: cacheControl,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const result = addPromptCacheControl(payload, { ttl: "5m" });
|
|
128
|
+
|
|
129
|
+
expect(result).toBe(payload);
|
|
130
|
+
expect(
|
|
131
|
+
firstContentBlock(messageAt(result.messages, 0)).cache_control
|
|
132
|
+
).toEqual(cacheControl);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
|
|
3
|
+
export interface AnthropicPromptCacheOptions {
|
|
4
|
+
/** TTL for the cache checkpoint. Defaults to 5m. */
|
|
5
|
+
ttl?: Anthropic.Messages.CacheControlEphemeral["ttl"];
|
|
6
|
+
/** Claude models support at most 4 cache breakpoints per request. */
|
|
7
|
+
maxBreakpoints?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type AnthropicPromptCacheConfig = boolean | AnthropicPromptCacheOptions;
|
|
11
|
+
|
|
12
|
+
interface PromptCachePayload {
|
|
13
|
+
messages: Anthropic.Messages.MessageParam[];
|
|
14
|
+
system?: string | Anthropic.Messages.TextBlockParam[];
|
|
15
|
+
tools?: Anthropic.Messages.Tool[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type CacheControl = Anthropic.Messages.CacheControlEphemeral;
|
|
19
|
+
type CacheableRecord = Record<string, unknown> & {
|
|
20
|
+
cache_control?: CacheControl | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MAX_CACHE_BREAKPOINTS = 4;
|
|
24
|
+
const UNCACHEABLE_BLOCK_TYPES = new Set(["thinking", "redacted_thinking"]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve model-invoker prompt-cache config. Undefined means the default:
|
|
28
|
+
* enabled with an explicit 5 minute TTL.
|
|
29
|
+
*/
|
|
30
|
+
export function resolvePromptCacheOptions(
|
|
31
|
+
promptCache?: AnthropicPromptCacheConfig
|
|
32
|
+
): AnthropicPromptCacheOptions | undefined {
|
|
33
|
+
if (promptCache === false) return undefined;
|
|
34
|
+
if (promptCache === true || promptCache === undefined) return {};
|
|
35
|
+
return promptCache;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add an explicit `cache_control` marker to the final cacheable message block.
|
|
40
|
+
*
|
|
41
|
+
* This intentionally uses block-level cache control rather than Anthropic's
|
|
42
|
+
* top-level automatic `cache_control` field because Amazon Bedrock does not
|
|
43
|
+
* support the top-level form. The block-level shape is accepted by both the
|
|
44
|
+
* Anthropic Messages API and Bedrock InvokeModel for Anthropic Claude models.
|
|
45
|
+
*/
|
|
46
|
+
export function addPromptCacheControl<TPayload extends PromptCachePayload>(
|
|
47
|
+
payload: TPayload,
|
|
48
|
+
options: AnthropicPromptCacheOptions = {}
|
|
49
|
+
): TPayload {
|
|
50
|
+
const maxBreakpoints =
|
|
51
|
+
options.maxBreakpoints ?? DEFAULT_MAX_CACHE_BREAKPOINTS;
|
|
52
|
+
if (maxBreakpoints <= 0) return payload;
|
|
53
|
+
|
|
54
|
+
if (countCacheControls(payload) >= maxBreakpoints) return payload;
|
|
55
|
+
|
|
56
|
+
const cacheControl: CacheControl = {
|
|
57
|
+
type: "ephemeral",
|
|
58
|
+
ttl: options.ttl ?? "5m",
|
|
59
|
+
};
|
|
60
|
+
const messages = addCacheControlToLastMessageBlock(
|
|
61
|
+
payload.messages,
|
|
62
|
+
cacheControl
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (messages === payload.messages) return payload;
|
|
66
|
+
return { ...payload, messages };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function addCacheControlToLastMessageBlock(
|
|
70
|
+
messages: Anthropic.Messages.MessageParam[],
|
|
71
|
+
cacheControl: CacheControl
|
|
72
|
+
): Anthropic.Messages.MessageParam[] {
|
|
73
|
+
for (
|
|
74
|
+
let messageIndex = messages.length - 1;
|
|
75
|
+
messageIndex >= 0;
|
|
76
|
+
messageIndex--
|
|
77
|
+
) {
|
|
78
|
+
const message = messages[messageIndex];
|
|
79
|
+
if (!message) continue;
|
|
80
|
+
|
|
81
|
+
if (typeof message.content === "string") {
|
|
82
|
+
if (message.content.length === 0) continue;
|
|
83
|
+
return replaceMessage(messages, messageIndex, {
|
|
84
|
+
...message,
|
|
85
|
+
content: [
|
|
86
|
+
{ type: "text", text: message.content, cache_control: cacheControl },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!Array.isArray(message.content)) continue;
|
|
92
|
+
|
|
93
|
+
for (
|
|
94
|
+
let blockIndex = message.content.length - 1;
|
|
95
|
+
blockIndex >= 0;
|
|
96
|
+
blockIndex--
|
|
97
|
+
) {
|
|
98
|
+
const block = message.content[blockIndex];
|
|
99
|
+
if (!isCacheableContentBlock(block)) continue;
|
|
100
|
+
if (hasCacheControl(block)) return messages;
|
|
101
|
+
|
|
102
|
+
const content = [...message.content];
|
|
103
|
+
content[blockIndex] = {
|
|
104
|
+
...(block as Record<string, unknown>),
|
|
105
|
+
cache_control: cacheControl,
|
|
106
|
+
} as Anthropic.Messages.ContentBlockParam;
|
|
107
|
+
return replaceMessage(messages, messageIndex, { ...message, content });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return messages;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function replaceMessage(
|
|
115
|
+
messages: Anthropic.Messages.MessageParam[],
|
|
116
|
+
index: number,
|
|
117
|
+
message: Anthropic.Messages.MessageParam
|
|
118
|
+
): Anthropic.Messages.MessageParam[] {
|
|
119
|
+
const next = [...messages];
|
|
120
|
+
next[index] = message;
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isCacheableContentBlock(
|
|
125
|
+
block: Anthropic.Messages.ContentBlockParam | undefined
|
|
126
|
+
): block is Anthropic.Messages.ContentBlockParam & CacheableRecord {
|
|
127
|
+
if (!isRecord(block)) return false;
|
|
128
|
+
const type = typeof block.type === "string" ? block.type : undefined;
|
|
129
|
+
if (type && UNCACHEABLE_BLOCK_TYPES.has(type)) return false;
|
|
130
|
+
if (type === "text" && block.text === "") return false;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function countCacheControls(payload: PromptCachePayload): number {
|
|
135
|
+
let count = 0;
|
|
136
|
+
|
|
137
|
+
for (const tool of payload.tools ?? []) {
|
|
138
|
+
if (hasCacheControl(tool)) count++;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(payload.system)) {
|
|
142
|
+
for (const block of payload.system) {
|
|
143
|
+
if (hasCacheControl(block)) count++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const message of payload.messages) {
|
|
148
|
+
if (!Array.isArray(message.content)) continue;
|
|
149
|
+
for (const block of message.content) {
|
|
150
|
+
if (hasCacheControl(block)) count++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return count;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hasCacheControl(value: unknown): value is CacheableRecord {
|
|
158
|
+
return isRecord(value) && value.cache_control != null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
162
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
163
|
+
}
|
|
@@ -25,6 +25,7 @@ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
|
|
|
25
25
|
import { ADAPTER_ID } from "./adapter-id";
|
|
26
26
|
|
|
27
27
|
export { ADAPTER_ID, type AdapterId } from "./adapter-id";
|
|
28
|
+
export type { ThreadOpsProxyOptions } from "../../../lib/thread/proxy";
|
|
28
29
|
|
|
29
30
|
export function proxyAnthropicThreadOps(
|
|
30
31
|
scope?: string,
|
|
@@ -25,6 +25,7 @@ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
|
|
|
25
25
|
import { ADAPTER_ID } from "./adapter-id";
|
|
26
26
|
|
|
27
27
|
export { ADAPTER_ID, type AdapterId } from "./adapter-id";
|
|
28
|
+
export type { ThreadOpsProxyOptions } from "../../../lib/thread/proxy";
|
|
28
29
|
|
|
29
30
|
export function proxyGoogleGenAIThreadOps(
|
|
30
31
|
scope?: string,
|
|
@@ -25,6 +25,7 @@ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
|
|
|
25
25
|
import { ADAPTER_ID } from "./adapter-id";
|
|
26
26
|
|
|
27
27
|
export { ADAPTER_ID, type AdapterId } from "./adapter-id";
|
|
28
|
+
export type { ThreadOpsProxyOptions } from "../../../lib/thread/proxy";
|
|
28
29
|
|
|
29
30
|
export function proxyLangChainThreadOps(
|
|
30
31
|
scope?: string,
|
package/src/index.ts
CHANGED
|
@@ -93,7 +93,7 @@ export type { VirtualFsContext } from "./lib/virtual-fs/types";
|
|
|
93
93
|
// Tool handlers (activity implementations)
|
|
94
94
|
// Wrap sandbox handlers with withSandbox(manager, handler) at registration time
|
|
95
95
|
export { bashHandler } from "./tools/bash/handler";
|
|
96
|
-
export { editHandler } from "./tools/edit/handler";
|
|
96
|
+
export { editHandler, multiEditHandler } from "./tools/edit/handler";
|
|
97
97
|
export { globHandler } from "./tools/glob/handler";
|
|
98
98
|
export { readFileHandler } from "./tools/read-file/handler";
|
|
99
99
|
export { writeFileHandler } from "./tools/write-file/handler";
|