zeitlich 0.2.25 → 0.2.27

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 (117) hide show
  1. package/dist/activities-DE3_q9yq.d.ts +140 -0
  2. package/dist/activities-p8PDlRIK.d.cts +140 -0
  3. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/virtual/index.d.cts +8 -7
  5. package/dist/adapters/sandbox/virtual/index.d.ts +8 -7
  6. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  7. package/dist/adapters/sandbox/virtual/workflow.d.cts +3 -2
  8. package/dist/adapters/sandbox/virtual/workflow.d.ts +3 -2
  9. package/dist/adapters/thread/anthropic/index.cjs +363 -0
  10. package/dist/adapters/thread/anthropic/index.cjs.map +1 -0
  11. package/dist/adapters/thread/anthropic/index.d.cts +151 -0
  12. package/dist/adapters/thread/anthropic/index.d.ts +151 -0
  13. package/dist/adapters/thread/anthropic/index.js +358 -0
  14. package/dist/adapters/thread/anthropic/index.js.map +1 -0
  15. package/dist/adapters/thread/anthropic/workflow.cjs +38 -0
  16. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -0
  17. package/dist/adapters/thread/anthropic/workflow.d.cts +37 -0
  18. package/dist/adapters/thread/anthropic/workflow.d.ts +37 -0
  19. package/dist/adapters/thread/anthropic/workflow.js +36 -0
  20. package/dist/adapters/thread/anthropic/workflow.js.map +1 -0
  21. package/dist/adapters/thread/google-genai/index.cjs +102 -99
  22. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  23. package/dist/adapters/thread/google-genai/index.d.cts +14 -113
  24. package/dist/adapters/thread/google-genai/index.d.ts +14 -113
  25. package/dist/adapters/thread/google-genai/index.js +103 -99
  26. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  27. package/dist/adapters/thread/google-genai/workflow.cjs +9 -4
  28. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  29. package/dist/adapters/thread/google-genai/workflow.d.cts +10 -5
  30. package/dist/adapters/thread/google-genai/workflow.d.ts +10 -5
  31. package/dist/adapters/thread/google-genai/workflow.js +9 -4
  32. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  33. package/dist/adapters/thread/langchain/index.cjs +73 -63
  34. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  35. package/dist/adapters/thread/langchain/index.d.cts +39 -40
  36. package/dist/adapters/thread/langchain/index.d.ts +39 -40
  37. package/dist/adapters/thread/langchain/index.js +73 -64
  38. package/dist/adapters/thread/langchain/index.js.map +1 -1
  39. package/dist/adapters/thread/langchain/workflow.cjs +9 -4
  40. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  41. package/dist/adapters/thread/langchain/workflow.d.cts +10 -5
  42. package/dist/adapters/thread/langchain/workflow.d.ts +10 -5
  43. package/dist/adapters/thread/langchain/workflow.js +9 -4
  44. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  45. package/dist/index.cjs +27 -10
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +13 -12
  48. package/dist/index.d.ts +13 -12
  49. package/dist/index.js +28 -11
  50. package/dist/index.js.map +1 -1
  51. package/dist/proxy-BK1ydQt0.d.ts +24 -0
  52. package/dist/proxy-BMAsMHdp.d.cts +24 -0
  53. package/dist/{queries-DwBe2CAA.d.ts → queries-BCgJ9Sr5.d.ts} +1 -1
  54. package/dist/{queries-BYGBImeC.d.cts → queries-DwnE2bu3.d.cts} +1 -1
  55. package/dist/thread-manager-Bh9x847n.d.ts +31 -0
  56. package/dist/thread-manager-BlHua5_v.d.cts +39 -0
  57. package/dist/thread-manager-Bz8txKKj.d.cts +31 -0
  58. package/dist/thread-manager-dzaJHQEA.d.ts +39 -0
  59. package/dist/types-BfIQABzu.d.cts +73 -0
  60. package/dist/types-CIkYBoF8.d.ts +73 -0
  61. package/dist/{types-hmferhc2.d.ts → types-CvJyXDYt.d.ts} +44 -123
  62. package/dist/{types-LVKmCNds.d.ts → types-DFUNSYbj.d.ts} +1 -1
  63. package/dist/{types-Bf8KV0Ci.d.cts → types-DRnz-OZp.d.cts} +1 -1
  64. package/dist/{types-7PeMi1bD.d.cts → types-DSOefLpY.d.cts} +44 -123
  65. package/dist/{types-D_igp10o.d.cts → types-mCVxKIZb.d.cts} +233 -137
  66. package/dist/{types-D_igp10o.d.ts → types-mCVxKIZb.d.ts} +233 -137
  67. package/dist/workflow.cjs +25 -9
  68. package/dist/workflow.cjs.map +1 -1
  69. package/dist/workflow.d.cts +11 -11
  70. package/dist/workflow.d.ts +11 -11
  71. package/dist/workflow.js +26 -10
  72. package/dist/workflow.js.map +1 -1
  73. package/package.json +26 -1
  74. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +8 -3
  75. package/src/adapters/thread/anthropic/activities.ts +226 -0
  76. package/src/adapters/thread/anthropic/index.ts +44 -0
  77. package/src/adapters/thread/anthropic/model-invoker.ts +129 -0
  78. package/src/adapters/thread/anthropic/proxy.ts +33 -0
  79. package/src/adapters/thread/anthropic/thread-manager.test.ts +137 -0
  80. package/src/adapters/thread/anthropic/thread-manager.ts +202 -0
  81. package/src/adapters/thread/google-genai/activities.ts +110 -33
  82. package/src/adapters/thread/google-genai/index.ts +3 -1
  83. package/src/adapters/thread/google-genai/model-invoker.ts +13 -42
  84. package/src/adapters/thread/google-genai/proxy.ts +6 -34
  85. package/src/adapters/thread/google-genai/thread-manager.test.ts +159 -0
  86. package/src/adapters/thread/google-genai/thread-manager.ts +96 -105
  87. package/src/adapters/thread/langchain/activities.ts +56 -21
  88. package/src/adapters/thread/langchain/hooks.ts +37 -0
  89. package/src/adapters/thread/langchain/index.ts +6 -1
  90. package/src/adapters/thread/langchain/model-invoker.ts +13 -12
  91. package/src/adapters/thread/langchain/proxy.ts +6 -34
  92. package/src/adapters/thread/langchain/thread-manager.test.ts +144 -0
  93. package/src/adapters/thread/langchain/thread-manager.ts +55 -98
  94. package/src/index.ts +5 -1
  95. package/src/lib/activity.ts +4 -3
  96. package/src/lib/hooks/types.ts +12 -12
  97. package/src/lib/model/types.ts +2 -0
  98. package/src/lib/session/session-edge-cases.integration.test.ts +24 -6
  99. package/src/lib/session/session.ts +18 -14
  100. package/src/lib/session/types.ts +31 -14
  101. package/src/lib/subagent/handler.ts +15 -8
  102. package/src/lib/subagent/types.ts +3 -2
  103. package/src/lib/thread/index.ts +3 -0
  104. package/src/lib/thread/manager.ts +4 -7
  105. package/src/lib/thread/proxy.ts +57 -0
  106. package/src/lib/thread/types.ts +44 -0
  107. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +54 -0
  108. package/src/lib/tool-router/auto-append.ts +5 -2
  109. package/src/lib/tool-router/router-edge-cases.integration.test.ts +9 -5
  110. package/src/lib/tool-router/router.ts +13 -7
  111. package/src/lib/tool-router/types.ts +20 -13
  112. package/src/lib/tool-router/with-sandbox.ts +4 -3
  113. package/src/lib/types.ts +7 -14
  114. package/src/workflow.ts +0 -4
  115. package/tsup.config.ts +5 -0
  116. package/dist/types-35POpVfa.d.cts +0 -40
  117. package/dist/types-35POpVfa.d.ts +0 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -67,6 +67,26 @@
67
67
  "default": "./dist/adapters/thread/google-genai/workflow.js"
68
68
  }
69
69
  },
70
+ "./adapters/thread/anthropic": {
71
+ "import": {
72
+ "types": "./dist/adapters/thread/anthropic/index.d.ts",
73
+ "default": "./dist/adapters/thread/anthropic/index.js"
74
+ },
75
+ "require": {
76
+ "types": "./dist/adapters/thread/anthropic/index.d.ts",
77
+ "default": "./dist/adapters/thread/anthropic/index.js"
78
+ }
79
+ },
80
+ "./adapters/thread/anthropic/workflow": {
81
+ "import": {
82
+ "types": "./dist/adapters/thread/anthropic/workflow.d.ts",
83
+ "default": "./dist/adapters/thread/anthropic/workflow.js"
84
+ },
85
+ "require": {
86
+ "types": "./dist/adapters/thread/anthropic/workflow.d.ts",
87
+ "default": "./dist/adapters/thread/anthropic/workflow.js"
88
+ }
89
+ },
70
90
  "./adapters/sandbox/inmemory": {
71
91
  "import": {
72
92
  "types": "./dist/adapters/sandbox/inmemory/index.d.ts",
@@ -190,6 +210,7 @@
190
210
  "node": ">=18"
191
211
  },
192
212
  "devDependencies": {
213
+ "@anthropic-ai/sdk": "^0.80.0",
193
214
  "@aws-sdk/client-bedrock-agentcore": "^3.900.0",
194
215
  "@daytonaio/sdk": "^0.149.0",
195
216
  "@e2b/code-interpreter": "^2.3.3",
@@ -210,6 +231,7 @@
210
231
  "vitest": "^4.0.18"
211
232
  },
212
233
  "peerDependencies": {
234
+ "@anthropic-ai/sdk": ">=0.50.0",
213
235
  "@aws-sdk/client-bedrock-agentcore": "^3.900.0",
214
236
  "@daytonaio/sdk": ">=0.153.0",
215
237
  "@e2b/code-interpreter": "^2.3.3",
@@ -225,6 +247,9 @@
225
247
  "@daytonaio/sdk": {
226
248
  "optional": true
227
249
  },
250
+ "@anthropic-ai/sdk": {
251
+ "optional": true
252
+ },
228
253
  "@google/genai": {
229
254
  "optional": true
230
255
  },
@@ -1,6 +1,7 @@
1
1
  import type { WorkflowClient } from "@temporalio/client";
2
2
  import { queryParentWorkflowState } from "../../../lib/activity";
3
- import type { ActivityToolHandler } from "../../../lib/tool-router/types";
3
+ import type { JsonValue } from "../../../lib/state/types";
4
+ import type { ActivityToolHandler, RouterContext } from "../../../lib/tool-router/types";
4
5
  import type {
5
6
  FileEntryMetadata,
6
7
  TreeMutation,
@@ -47,17 +48,21 @@ export function withVirtualSandbox<
47
48
  TResult,
48
49
  TCtx,
49
50
  TMeta = FileEntryMetadata,
51
+ TToolResponse = JsonValue,
50
52
  >(
51
53
  client: WorkflowClient,
52
54
  provider: VirtualSandboxProvider<TCtx, TMeta>,
53
55
  handler: ActivityToolHandler<
54
56
  TArgs,
55
57
  TResult,
56
- VirtualSandboxContext<TCtx, TMeta>
58
+ VirtualSandboxContext<TCtx, TMeta>,
59
+ TToolResponse
57
60
  >
58
61
  ): ActivityToolHandler<
59
62
  TArgs,
60
- (TResult & { treeMutations: TreeMutation<TMeta>[] }) | null
63
+ (TResult & { treeMutations: TreeMutation<TMeta>[] }) | null,
64
+ RouterContext,
65
+ TToolResponse | string
61
66
  > {
62
67
  return async (args, context) => {
63
68
  const state =
@@ -0,0 +1,226 @@
1
+ import type Redis from "ioredis";
2
+ import type Anthropic from "@anthropic-ai/sdk";
3
+ import type { ToolResultConfig } from "../../../lib/types";
4
+ import type {
5
+ ActivityToolHandler,
6
+ RouterContext,
7
+ ToolHandlerResponse,
8
+ } from "../../../lib/tool-router/types";
9
+ import type {
10
+ ThreadOps,
11
+ PrefixedThreadOps,
12
+ ScopedPrefix,
13
+ } from "../../../lib/session/types";
14
+ import type { ModelInvoker } from "../../../lib/model";
15
+ import {
16
+ createAnthropicThreadManager,
17
+ type AnthropicContent,
18
+ type AnthropicThreadManagerHooks,
19
+ } from "./thread-manager";
20
+ import {
21
+ createAnthropicModelInvoker,
22
+ type AnthropicModelInvokerConfig,
23
+ } from "./model-invoker";
24
+
25
+ const ADAPTER_PREFIX = "anthropic" as const;
26
+
27
+ export type AnthropicThreadOps<TScope extends string = ""> =
28
+ PrefixedThreadOps<ScopedPrefix<TScope, typeof ADAPTER_PREFIX>, AnthropicContent>;
29
+
30
+ export interface AnthropicAdapterConfig {
31
+ redis: Redis;
32
+ client: Anthropic;
33
+ /** Default model name (e.g. 'claude-sonnet-4-20250514'). If omitted, use `createModelInvoker()` */
34
+ model?: string;
35
+ /** Maximum tokens to generate. Defaults to 16384. */
36
+ maxTokens?: number;
37
+ hooks?: AnthropicThreadManagerHooks;
38
+ }
39
+
40
+ /**
41
+ * Tool response type accepted by the Anthropic adapter.
42
+ *
43
+ * Handlers can return:
44
+ * - **`string`** — plain text content for the tool result.
45
+ * - **`Anthropic.Messages.ToolResultBlockParam["content"]`** — array of content blocks
46
+ * (e.g. `{ type: "text", text: "..." }`, `{ type: "image", source: { ... } }`).
47
+ * Passed through as-is to the `tool_result` block.
48
+ */
49
+ export type AnthropicToolResponse = Anthropic.Messages.ToolResultBlockParam["content"];
50
+
51
+ export interface AnthropicAdapter {
52
+ /** Model invoker using the default model (only available when `model` was provided) */
53
+ invoker: ModelInvoker<Anthropic.Messages.Message>;
54
+ /** Create an invoker for a specific model name (for multi-model setups) */
55
+ createModelInvoker(
56
+ model: string,
57
+ maxTokens?: number,
58
+ ): ModelInvoker<Anthropic.Messages.Message>;
59
+ /**
60
+ * Create prefixed thread activities for registration on the worker.
61
+ *
62
+ * @param scope - Workflow name appended to the adapter prefix.
63
+ * Use different scopes for the main agent vs subagents to avoid collisions.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * adapter.createActivities("codingAgent")
68
+ * // → { anthropicCodingAgentInitializeThread, anthropicCodingAgentAppendHumanMessage, … }
69
+ *
70
+ * adapter.createActivities("researchAgent")
71
+ * // → { anthropicResearchAgentInitializeThread, … }
72
+ * ```
73
+ */
74
+ createActivities<S extends string = "">(
75
+ scope?: S,
76
+ ): AnthropicThreadOps<S>;
77
+
78
+ /**
79
+ * Identity wrapper that types a tool handler for this adapter.
80
+ * Constrains `toolResponse` to {@link AnthropicToolResponse}.
81
+ */
82
+ wrapHandler<TArgs, TResult, TContext extends RouterContext = RouterContext>(
83
+ handler: (
84
+ args: TArgs,
85
+ context: TContext,
86
+ ) => Promise<ToolHandlerResponse<TResult, AnthropicToolResponse>>,
87
+ ): ActivityToolHandler<TArgs, TResult, TContext, AnthropicToolResponse>;
88
+ }
89
+
90
+ /**
91
+ * Creates an Anthropic adapter that bundles thread operations and model
92
+ * invocation using the `@anthropic-ai/sdk`.
93
+ *
94
+ * Use `createActivities(scope)` to register scoped thread operations as
95
+ * Temporal activities on the worker. The `invoker` (or invokers created via
96
+ * `createModelInvoker`) should be wrapped with `createRunAgentActivity`.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * import { createAnthropicAdapter } from 'zeitlich/adapters/thread/anthropic';
101
+ * import { createRunAgentActivity } from 'zeitlich';
102
+ * import Anthropic from '@anthropic-ai/sdk';
103
+ *
104
+ * const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
105
+ * const adapter = createAnthropicAdapter({ redis, client, model: 'claude-sonnet-4-20250514' });
106
+ *
107
+ * export function createActivities(temporalClient: WorkflowClient) {
108
+ * return {
109
+ * ...adapter.createActivities("codingAgent"),
110
+ * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
111
+ * };
112
+ * }
113
+ * ```
114
+ *
115
+ * @example Multi-agent worker (main + subagent share the adapter)
116
+ * ```typescript
117
+ * export function createActivities(temporalClient: WorkflowClient) {
118
+ * return {
119
+ * ...adapter.createActivities("codingAgent"),
120
+ * ...adapter.createActivities("researchAgent"),
121
+ * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
122
+ * runResearchAgent: createRunAgentActivity(
123
+ * temporalClient,
124
+ * adapter.createModelInvoker('claude-sonnet-4-20250514'),
125
+ * ),
126
+ * };
127
+ * }
128
+ * ```
129
+ */
130
+ export function createAnthropicAdapter(
131
+ config: AnthropicAdapterConfig,
132
+ ): AnthropicAdapter {
133
+ const { redis, client } = config;
134
+
135
+ const threadOps: ThreadOps<AnthropicContent> = {
136
+ async initializeThread(threadId: string, threadKey?: string): Promise<void> {
137
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
138
+ await thread.initialize();
139
+ },
140
+
141
+ async appendHumanMessage(
142
+ threadId: string,
143
+ id: string,
144
+ content: AnthropicContent,
145
+ threadKey?: string,
146
+ ): Promise<void> {
147
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
148
+ await thread.appendUserMessage(id, content);
149
+ },
150
+
151
+ async appendSystemMessage(
152
+ threadId: string,
153
+ id: string,
154
+ content: string,
155
+ threadKey?: string,
156
+ ): Promise<void> {
157
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
158
+ await thread.appendSystemMessage(id, content);
159
+ },
160
+
161
+ async appendToolResult(id: string, cfg: ToolResultConfig): Promise<void> {
162
+ const { threadId, threadKey, toolCallId, toolName, content } = cfg;
163
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
164
+ await thread.appendToolResult(id, toolCallId, toolName, content);
165
+ },
166
+
167
+ async forkThread(
168
+ sourceThreadId: string,
169
+ targetThreadId: string,
170
+ threadKey?: string,
171
+ ): Promise<void> {
172
+ const thread = createAnthropicThreadManager({
173
+ redis,
174
+ threadId: sourceThreadId,
175
+ key: threadKey,
176
+ });
177
+ await thread.fork(targetThreadId);
178
+ },
179
+ };
180
+
181
+ function createActivities<S extends string = "">(
182
+ scope?: S,
183
+ ): AnthropicThreadOps<S> {
184
+ const prefix = scope
185
+ ? `${ADAPTER_PREFIX}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
186
+ : ADAPTER_PREFIX;
187
+ const cap = (s: string): string =>
188
+ s.charAt(0).toUpperCase() + s.slice(1);
189
+ return Object.fromEntries(
190
+ Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v]),
191
+ ) as AnthropicThreadOps<S>;
192
+ }
193
+
194
+ const makeInvoker = (
195
+ model: string,
196
+ maxTokens?: number,
197
+ ): ModelInvoker<Anthropic.Messages.Message> => {
198
+ const invokerConfig: AnthropicModelInvokerConfig = {
199
+ redis,
200
+ client,
201
+ model,
202
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
203
+ ...(config.maxTokens !== undefined && maxTokens === undefined
204
+ ? { maxTokens: config.maxTokens }
205
+ : {}),
206
+ hooks: config.hooks,
207
+ };
208
+ return createAnthropicModelInvoker(invokerConfig);
209
+ };
210
+
211
+ const invoker: ModelInvoker<Anthropic.Messages.Message> = config.model
212
+ ? makeInvoker(config.model)
213
+ : ((() => {
214
+ throw new Error(
215
+ "No default model provided to createAnthropicAdapter. " +
216
+ "Either pass `model` in the config or use `createModelInvoker(model)` instead.",
217
+ );
218
+ }) as unknown as ModelInvoker<Anthropic.Messages.Message>);
219
+
220
+ return {
221
+ createActivities,
222
+ invoker,
223
+ createModelInvoker: makeInvoker,
224
+ wrapHandler: (handler) => handler,
225
+ };
226
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Anthropic adapter for Zeitlich.
3
+ *
4
+ * Provides a unified adapter that bundles thread management and model
5
+ * invocation using the `@anthropic-ai/sdk`.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import {
10
+ * createAnthropicAdapter,
11
+ * createAnthropicThreadManager,
12
+ * } from 'zeitlich/adapters/thread/anthropic';
13
+ * import Anthropic from '@anthropic-ai/sdk';
14
+ *
15
+ * const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
16
+ * const adapter = createAnthropicAdapter({ redis, client, model: 'claude-sonnet-4-20250514' });
17
+ * ```
18
+ */
19
+
20
+ // Adapter (primary API)
21
+ export {
22
+ createAnthropicAdapter,
23
+ type AnthropicAdapter,
24
+ type AnthropicAdapterConfig,
25
+ type AnthropicThreadOps,
26
+ type AnthropicToolResponse,
27
+ } from "./activities";
28
+
29
+ // Thread manager
30
+ export {
31
+ createAnthropicThreadManager,
32
+ type AnthropicThreadManager,
33
+ type AnthropicThreadManagerConfig,
34
+ type AnthropicContent,
35
+ type AnthropicInvocationPayload,
36
+ type StoredMessage,
37
+ } from "./thread-manager";
38
+
39
+ // Model invoker (for advanced use — prefer adapter.createModelInvoker)
40
+ export {
41
+ createAnthropicModelInvoker,
42
+ invokeAnthropicModel,
43
+ type AnthropicModelInvokerConfig,
44
+ } from "./model-invoker";
@@ -0,0 +1,129 @@
1
+ import type Redis from "ioredis";
2
+ import type Anthropic from "@anthropic-ai/sdk";
3
+ import type { SerializableToolDefinition } from "../../../lib/types";
4
+ import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
5
+ import { createAnthropicThreadManager, type AnthropicThreadManagerHooks } from "./thread-manager";
6
+ import { v4 as uuidv4 } from "uuid";
7
+
8
+ export interface AnthropicModelInvokerConfig {
9
+ redis: Redis;
10
+ client: Anthropic;
11
+ model: string;
12
+ /** Maximum tokens to generate. Defaults to 16384. */
13
+ maxTokens?: number;
14
+ hooks?: AnthropicThreadManagerHooks;
15
+ }
16
+
17
+ function toAnthropicTools(
18
+ tools: SerializableToolDefinition[],
19
+ ): Anthropic.Messages.Tool[] {
20
+ return tools.map((t) => ({
21
+ name: t.name,
22
+ description: t.description,
23
+ input_schema: t.schema as Anthropic.Messages.Tool.InputSchema,
24
+ }));
25
+ }
26
+
27
+ /**
28
+ * Creates an Anthropic model invoker that satisfies the generic
29
+ * `ModelInvoker<Anthropic.Messages.Message>` contract.
30
+ *
31
+ * Loads the conversation thread from Redis, invokes the Claude model via
32
+ * `client.messages.create`, appends the AI response, and returns
33
+ * a normalised AgentResponse.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * import { createAnthropicModelInvoker } from 'zeitlich/adapters/thread/anthropic';
38
+ * import { createRunAgentActivity } from 'zeitlich';
39
+ * import Anthropic from '@anthropic-ai/sdk';
40
+ *
41
+ * const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
42
+ * const invoker = createAnthropicModelInvoker({
43
+ * redis,
44
+ * client,
45
+ * model: 'claude-sonnet-4-20250514',
46
+ * });
47
+ *
48
+ * return { runAgent: createRunAgentActivity(client, invoker) };
49
+ * ```
50
+ */
51
+ export function createAnthropicModelInvoker({
52
+ redis,
53
+ client,
54
+ model,
55
+ maxTokens = 16384,
56
+ hooks,
57
+ }: AnthropicModelInvokerConfig) {
58
+ return async function invokeAnthropicModel(
59
+ config: ModelInvokerConfig,
60
+ ): Promise<AgentResponse<Anthropic.Messages.Message>> {
61
+ const { threadId, threadKey, state } = config;
62
+
63
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey, hooks });
64
+ const { messages, system } = await thread.prepareForInvocation();
65
+
66
+ const anthropicTools = toAnthropicTools(state.tools);
67
+ const tools = anthropicTools.length > 0 ? anthropicTools : undefined;
68
+
69
+ const response = await client.messages.create({
70
+ model,
71
+ max_tokens: maxTokens,
72
+ messages,
73
+ ...(system ? { system } : {}),
74
+ ...(tools ? { tools } : {}),
75
+ });
76
+
77
+ await thread.appendAssistantMessage(uuidv4(), response.content);
78
+
79
+ const toolCalls = response.content.filter(
80
+ (block): block is Anthropic.Messages.ToolUseBlock =>
81
+ block.type === "tool_use",
82
+ );
83
+
84
+ return {
85
+ message: response,
86
+ rawToolCalls: toolCalls.map((tc) => ({
87
+ id: tc.id,
88
+ name: tc.name,
89
+ args: (tc.input as Record<string, unknown>) ?? {},
90
+ })),
91
+ usage: {
92
+ inputTokens: response.usage.input_tokens,
93
+ outputTokens: response.usage.output_tokens,
94
+ cachedWriteTokens: response.usage.cache_creation_input_tokens ?? undefined,
95
+ cachedReadTokens: response.usage.cache_read_input_tokens ?? undefined,
96
+ },
97
+ };
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Standalone function for one-shot Anthropic model invocation.
103
+ * Convenience wrapper around createAnthropicModelInvoker for cases
104
+ * where you don't need to reuse the invoker.
105
+ */
106
+ export async function invokeAnthropicModel({
107
+ redis,
108
+ client,
109
+ model,
110
+ maxTokens,
111
+ hooks,
112
+ config,
113
+ }: {
114
+ redis: Redis;
115
+ client: Anthropic;
116
+ model: string;
117
+ maxTokens?: number;
118
+ hooks?: AnthropicThreadManagerHooks;
119
+ config: ModelInvokerConfig;
120
+ }): Promise<AgentResponse<Anthropic.Messages.Message>> {
121
+ const invoker = createAnthropicModelInvoker({
122
+ redis,
123
+ client,
124
+ model,
125
+ maxTokens,
126
+ hooks,
127
+ });
128
+ return invoker(config);
129
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Workflow-safe proxy for Anthropic thread operations.
3
+ *
4
+ * Import this from `zeitlich/adapters/thread/anthropic/workflow`
5
+ * in your Temporal workflow files.
6
+ *
7
+ * By default the scope is derived from `workflowInfo().workflowType`,
8
+ * so activities are automatically namespaced per workflow.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { proxyAnthropicThreadOps } from 'zeitlich/adapters/thread/anthropic/workflow';
13
+ *
14
+ * // Auto-scoped to the current workflow name
15
+ * const threadOps = proxyAnthropicThreadOps();
16
+ *
17
+ * // Explicit scope override
18
+ * const threadOps = proxyAnthropicThreadOps("customScope");
19
+ * ```
20
+ */
21
+ import { type ActivityInterfaceFor } from "@temporalio/workflow";
22
+ import type { ThreadOps } from "../../../lib/session/types";
23
+ import type { AnthropicContent } from "./thread-manager";
24
+ import { createThreadOpsProxy } from "../../../lib/thread/proxy";
25
+
26
+ const ADAPTER_PREFIX = "anthropic";
27
+
28
+ export function proxyAnthropicThreadOps(
29
+ scope?: string,
30
+ options?: Parameters<typeof createThreadOpsProxy>[2],
31
+ ): ActivityInterfaceFor<ThreadOps<AnthropicContent>> {
32
+ return createThreadOpsProxy(ADAPTER_PREFIX, scope, options) as ActivityInterfaceFor<ThreadOps<AnthropicContent>>;
33
+ }
@@ -0,0 +1,137 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { StoredMessage } from "./thread-manager";
3
+ import { createAnthropicThreadManager } from "./thread-manager";
4
+
5
+ function createMockRedis(stored: StoredMessage[]) {
6
+ return {
7
+ exists: vi.fn().mockResolvedValue(1),
8
+ lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
9
+ del: vi.fn().mockResolvedValue(1),
10
+ set: vi.fn().mockResolvedValue("OK"),
11
+ rpush: vi.fn().mockResolvedValue(1),
12
+ expire: vi.fn().mockResolvedValue(1),
13
+ eval: vi.fn().mockResolvedValue(1),
14
+ };
15
+ }
16
+
17
+ const systemMsg: StoredMessage = {
18
+ id: "sys-1",
19
+ message: { role: "user", content: "You are helpful." },
20
+ isSystem: true,
21
+ };
22
+
23
+ const userMsg: StoredMessage = {
24
+ id: "msg-1",
25
+ message: { role: "user", content: [{ type: "text", text: "Hello" }] },
26
+ };
27
+
28
+ const assistantMsg: StoredMessage = {
29
+ id: "msg-2",
30
+ message: { role: "assistant", content: [{ type: "text", text: "Hi there!" }] },
31
+ };
32
+
33
+ describe("Anthropic thread manager hooks", () => {
34
+ describe("onPrepareMessage", () => {
35
+ it("transforms stored messages before system extraction and merge", async () => {
36
+ const hook = vi.fn((msg: StoredMessage) => {
37
+ if (msg.isSystem) return msg;
38
+ const firstBlock = (msg.message.content as Array<{ text: string }>)[0];
39
+ return {
40
+ ...msg,
41
+ message: {
42
+ ...msg.message,
43
+ content: [{ type: "text" as const, text: `[modified] ${firstBlock?.text}` }],
44
+ },
45
+ };
46
+ });
47
+
48
+ const redis = createMockRedis([systemMsg, userMsg, assistantMsg]);
49
+ const tm = createAnthropicThreadManager({
50
+ redis: redis as never,
51
+ threadId: "t1",
52
+ hooks: { onPrepareMessage: hook },
53
+ });
54
+
55
+ const { messages, system } = await tm.prepareForInvocation();
56
+
57
+ expect(hook).toHaveBeenCalledTimes(3);
58
+ expect(hook).toHaveBeenCalledWith(systemMsg, 0, [systemMsg, userMsg, assistantMsg]);
59
+ expect(system).toBe("You are helpful.");
60
+ expect(messages[0]?.content).toEqual([{ type: "text", text: "[modified] Hello" }]);
61
+ expect(messages[1]?.content).toEqual([{ type: "text", text: "[modified] Hi there!" }]);
62
+ });
63
+
64
+ it("is not called when not configured", async () => {
65
+ const redis = createMockRedis([userMsg]);
66
+ const tm = createAnthropicThreadManager({
67
+ redis: redis as never,
68
+ threadId: "t1",
69
+ });
70
+
71
+ const { messages } = await tm.prepareForInvocation();
72
+ expect(messages).toHaveLength(1);
73
+ });
74
+ });
75
+
76
+ describe("onPreparedMessage", () => {
77
+ it("transforms SDK-native messages after merge", async () => {
78
+ const hook = vi.fn((msg) => ({
79
+ ...msg,
80
+ content: [{ type: "text" as const, text: "[post] done" }],
81
+ }));
82
+
83
+ const redis = createMockRedis([userMsg, assistantMsg]);
84
+ const tm = createAnthropicThreadManager({
85
+ redis: redis as never,
86
+ threadId: "t1",
87
+ hooks: { onPreparedMessage: hook },
88
+ });
89
+
90
+ const { messages } = await tm.prepareForInvocation();
91
+
92
+ expect(hook).toHaveBeenCalledTimes(2);
93
+ expect(messages[0]?.content).toEqual([{ type: "text", text: "[post] done" }]);
94
+ });
95
+
96
+ it("receives the full prepared messages array", async () => {
97
+ const hook = vi.fn((msg) => msg);
98
+
99
+ const redis = createMockRedis([userMsg, assistantMsg]);
100
+ const tm = createAnthropicThreadManager({
101
+ redis: redis as never,
102
+ threadId: "t1",
103
+ hooks: { onPreparedMessage: hook },
104
+ });
105
+
106
+ await tm.prepareForInvocation();
107
+
108
+ const args = hook.mock.calls[0] as unknown as [unknown, number, unknown[]];
109
+ expect(args[2]).toHaveLength(2);
110
+ });
111
+ });
112
+
113
+ describe("both hooks combined", () => {
114
+ it("runs onPrepareMessage before onPreparedMessage", async () => {
115
+ const order: string[] = [];
116
+
117
+ const redis = createMockRedis([userMsg]);
118
+ const tm = createAnthropicThreadManager({
119
+ redis: redis as never,
120
+ threadId: "t1",
121
+ hooks: {
122
+ onPrepareMessage: (msg) => {
123
+ order.push("pre");
124
+ return msg;
125
+ },
126
+ onPreparedMessage: (msg) => {
127
+ order.push("post");
128
+ return msg;
129
+ },
130
+ },
131
+ });
132
+
133
+ await tm.prepareForInvocation();
134
+ expect(order).toEqual(["pre", "post"]);
135
+ });
136
+ });
137
+ });