zeitlich 0.2.46 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +64 -6
  2. package/dist/{activities-CyeiqK_f.d.cts → activities-CPwKoUlD.d.cts} +3 -3
  3. package/dist/{activities-Bm4TLTid.d.ts → activities-DlaBxNID.d.ts} +3 -3
  4. package/dist/adapters/thread/anthropic/index.cjs +105 -6
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +48 -9
  7. package/dist/adapters/thread/anthropic/index.d.ts +48 -9
  8. package/dist/adapters/thread/anthropic/index.js +104 -7
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +38 -22
  11. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
  14. package/dist/adapters/thread/anthropic/workflow.js +38 -22
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.d.cts +6 -5
  17. package/dist/adapters/thread/google-genai/index.d.ts +6 -5
  18. package/dist/adapters/thread/google-genai/workflow.cjs +38 -22
  19. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  20. package/dist/adapters/thread/google-genai/workflow.d.cts +7 -5
  21. package/dist/adapters/thread/google-genai/workflow.d.ts +7 -5
  22. package/dist/adapters/thread/google-genai/workflow.js +38 -22
  23. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  24. package/dist/adapters/thread/langchain/index.d.cts +6 -5
  25. package/dist/adapters/thread/langchain/index.d.ts +6 -5
  26. package/dist/adapters/thread/langchain/workflow.cjs +38 -22
  27. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  28. package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
  29. package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
  30. package/dist/adapters/thread/langchain/workflow.js +38 -22
  31. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  32. package/dist/{cold-store-CFHwemBJ.d.ts → cold-store-BDgJpwLI.d.ts} +8 -11
  33. package/dist/{cold-store-BC5L5Z8A.d.cts → cold-store-Z2wvK2cV.d.cts} +8 -11
  34. package/dist/index.cjs +264 -90
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +21 -9
  37. package/dist/index.d.ts +21 -9
  38. package/dist/index.js +265 -93
  39. package/dist/index.js.map +1 -1
  40. package/dist/proxy-CDh3Rsa7.d.cts +40 -0
  41. package/dist/proxy-Du8ggERu.d.ts +40 -0
  42. package/dist/{thread-manager-D33SUmZa.d.cts → thread-manager-BjoYYXgd.d.cts} +2 -2
  43. package/dist/{thread-manager-9tezUcLW.d.cts → thread-manager-D8zKNFZ9.d.cts} +2 -2
  44. package/dist/{thread-manager-B-zy3xrs.d.ts → thread-manager-DtHYws2F.d.ts} +2 -2
  45. package/dist/{thread-manager-DduoSkvJ.d.ts → thread-manager-Dw96FKH1.d.ts} +2 -2
  46. package/dist/{types-oxt8GN97.d.cts → types-BMJrsHo0.d.cts} +1 -1
  47. package/dist/{types-L5bvbF-n.d.ts → types-CtdOquo3.d.ts} +1 -1
  48. package/dist/{types-CnuN9T6t.d.cts → types-DNEl5uxQ.d.cts} +16 -0
  49. package/dist/{types-CwN6_tAL.d.ts → types-qQVZfhoT.d.ts} +16 -0
  50. package/dist/{workflow-DIaIV7L2.d.cts → workflow-BH9ImDGq.d.cts} +17 -2
  51. package/dist/{workflow-B1TOcHbt.d.ts → workflow-Cdw3-RNB.d.ts} +17 -2
  52. package/dist/workflow.cjs +33 -3
  53. package/dist/workflow.cjs.map +1 -1
  54. package/dist/workflow.d.cts +2 -2
  55. package/dist/workflow.d.ts +2 -2
  56. package/dist/workflow.js +33 -4
  57. package/dist/workflow.js.map +1 -1
  58. package/package.json +9 -3
  59. package/src/adapters/thread/anthropic/activities.ts +18 -11
  60. package/src/adapters/thread/anthropic/index.ts +8 -0
  61. package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
  62. package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
  63. package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
  64. package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
  65. package/src/adapters/thread/anthropic/proxy.ts +1 -0
  66. package/src/adapters/thread/google-genai/proxy.ts +1 -0
  67. package/src/adapters/thread/langchain/proxy.ts +1 -0
  68. package/src/index.ts +1 -1
  69. package/src/lib/subagent/define.ts +1 -0
  70. package/src/lib/subagent/handler.ts +11 -2
  71. package/src/lib/subagent/subagent.integration.test.ts +139 -0
  72. package/src/lib/subagent/types.ts +16 -0
  73. package/src/lib/thread/cold-store.test.ts +33 -5
  74. package/src/lib/thread/cold-store.ts +50 -31
  75. package/src/lib/thread/proxy.ts +79 -29
  76. package/src/tools/edit/handler.test.ts +177 -0
  77. package/src/tools/edit/handler.ts +249 -47
  78. package/src/tools/edit/tool.ts +40 -0
  79. package/src/tools/task-create/handler.ts +1 -1
  80. package/src/tools/task-update/handler.ts +1 -1
  81. package/src/workflow.ts +2 -2
  82. package/dist/proxy-BxFyd6cg.d.cts +0 -24
  83. package/dist/proxy-Cskmj4Yx.d.ts +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.46",
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.93.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 { messages, system } = await thread.prepareForInvocation();
90
+ const prepared = await thread.prepareForInvocation();
80
91
 
81
92
  const anthropicTools = toAnthropicTools(state.tools);
82
- const tools = anthropicTools.length > 0 ? anthropicTools : undefined;
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";
@@ -44,6 +44,7 @@ export function defineSubagent<
44
44
  enabled?: boolean | (() => boolean);
45
45
  taskQueue?: string;
46
46
  thread?: "new" | "fork" | "continue";
47
+ newThreadSource?: "new" | "from-parent";
47
48
  sandbox?: SubagentSandboxConfig;
48
49
  }
49
50
  ): SubagentConfig<TResult> {