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.
- package/dist/activities-DE3_q9yq.d.ts +140 -0
- package/dist/activities-p8PDlRIK.d.cts +140 -0
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
- package/dist/adapters/sandbox/virtual/index.d.cts +8 -7
- package/dist/adapters/sandbox/virtual/index.d.ts +8 -7
- package/dist/adapters/sandbox/virtual/index.js.map +1 -1
- package/dist/adapters/sandbox/virtual/workflow.d.cts +3 -2
- package/dist/adapters/sandbox/virtual/workflow.d.ts +3 -2
- package/dist/adapters/thread/anthropic/index.cjs +363 -0
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -0
- package/dist/adapters/thread/anthropic/index.d.cts +151 -0
- package/dist/adapters/thread/anthropic/index.d.ts +151 -0
- package/dist/adapters/thread/anthropic/index.js +358 -0
- package/dist/adapters/thread/anthropic/index.js.map +1 -0
- package/dist/adapters/thread/anthropic/workflow.cjs +38 -0
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -0
- package/dist/adapters/thread/anthropic/workflow.d.cts +37 -0
- package/dist/adapters/thread/anthropic/workflow.d.ts +37 -0
- package/dist/adapters/thread/anthropic/workflow.js +36 -0
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -0
- package/dist/adapters/thread/google-genai/index.cjs +102 -99
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +14 -113
- package/dist/adapters/thread/google-genai/index.d.ts +14 -113
- package/dist/adapters/thread/google-genai/index.js +103 -99
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +9 -4
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +10 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +10 -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/langchain/index.cjs +73 -63
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +39 -40
- package/dist/adapters/thread/langchain/index.d.ts +39 -40
- package/dist/adapters/thread/langchain/index.js +73 -64
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +9 -4
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +10 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +10 -5
- package/dist/adapters/thread/langchain/workflow.js +9 -4
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/index.cjs +27 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -12
- package/dist/index.d.ts +13 -12
- package/dist/index.js +28 -11
- package/dist/index.js.map +1 -1
- package/dist/proxy-BK1ydQt0.d.ts +24 -0
- package/dist/proxy-BMAsMHdp.d.cts +24 -0
- package/dist/{queries-DwBe2CAA.d.ts → queries-BCgJ9Sr5.d.ts} +1 -1
- package/dist/{queries-BYGBImeC.d.cts → queries-DwnE2bu3.d.cts} +1 -1
- package/dist/thread-manager-Bh9x847n.d.ts +31 -0
- package/dist/thread-manager-BlHua5_v.d.cts +39 -0
- package/dist/thread-manager-Bz8txKKj.d.cts +31 -0
- package/dist/thread-manager-dzaJHQEA.d.ts +39 -0
- package/dist/types-BfIQABzu.d.cts +73 -0
- package/dist/types-CIkYBoF8.d.ts +73 -0
- package/dist/{types-hmferhc2.d.ts → types-CvJyXDYt.d.ts} +44 -123
- package/dist/{types-LVKmCNds.d.ts → types-DFUNSYbj.d.ts} +1 -1
- package/dist/{types-Bf8KV0Ci.d.cts → types-DRnz-OZp.d.cts} +1 -1
- package/dist/{types-7PeMi1bD.d.cts → types-DSOefLpY.d.cts} +44 -123
- package/dist/{types-D_igp10o.d.cts → types-mCVxKIZb.d.cts} +233 -137
- package/dist/{types-D_igp10o.d.ts → types-mCVxKIZb.d.ts} +233 -137
- package/dist/workflow.cjs +25 -9
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +11 -11
- package/dist/workflow.d.ts +11 -11
- package/dist/workflow.js +26 -10
- package/dist/workflow.js.map +1 -1
- package/package.json +26 -1
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +8 -3
- package/src/adapters/thread/anthropic/activities.ts +226 -0
- package/src/adapters/thread/anthropic/index.ts +44 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +129 -0
- package/src/adapters/thread/anthropic/proxy.ts +33 -0
- package/src/adapters/thread/anthropic/thread-manager.test.ts +137 -0
- package/src/adapters/thread/anthropic/thread-manager.ts +202 -0
- package/src/adapters/thread/google-genai/activities.ts +110 -33
- package/src/adapters/thread/google-genai/index.ts +3 -1
- package/src/adapters/thread/google-genai/model-invoker.ts +13 -42
- package/src/adapters/thread/google-genai/proxy.ts +6 -34
- package/src/adapters/thread/google-genai/thread-manager.test.ts +159 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +96 -105
- package/src/adapters/thread/langchain/activities.ts +56 -21
- package/src/adapters/thread/langchain/hooks.ts +37 -0
- package/src/adapters/thread/langchain/index.ts +6 -1
- package/src/adapters/thread/langchain/model-invoker.ts +13 -12
- package/src/adapters/thread/langchain/proxy.ts +6 -34
- package/src/adapters/thread/langchain/thread-manager.test.ts +144 -0
- package/src/adapters/thread/langchain/thread-manager.ts +55 -98
- package/src/index.ts +5 -1
- package/src/lib/activity.ts +4 -3
- package/src/lib/hooks/types.ts +12 -12
- package/src/lib/model/types.ts +2 -0
- package/src/lib/session/session-edge-cases.integration.test.ts +24 -6
- package/src/lib/session/session.ts +18 -14
- package/src/lib/session/types.ts +31 -14
- package/src/lib/subagent/handler.ts +15 -8
- package/src/lib/subagent/types.ts +3 -2
- package/src/lib/thread/index.ts +3 -0
- package/src/lib/thread/manager.ts +4 -7
- package/src/lib/thread/proxy.ts +57 -0
- package/src/lib/thread/types.ts +44 -0
- package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +54 -0
- package/src/lib/tool-router/auto-append.ts +5 -2
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +9 -5
- package/src/lib/tool-router/router.ts +13 -7
- package/src/lib/tool-router/types.ts +20 -13
- package/src/lib/tool-router/with-sandbox.ts +4 -3
- package/src/lib/types.ts +7 -14
- package/src/workflow.ts +0 -4
- package/tsup.config.ts +5 -0
- package/dist/types-35POpVfa.d.cts +0 -40
- 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.
|
|
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 {
|
|
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
|
+
});
|