zeitlich 0.2.37 → 0.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/{activities-Bb-nAjwQ.d.ts → activities-Bmu7XnaG.d.ts} +4 -4
- package/dist/{activities-vkI4_3CC.d.cts → activities-ByBFLvm2.d.cts} +4 -4
- package/dist/adapter-id-BB-mmrts.d.cts +17 -0
- package/dist/adapter-id-BB-mmrts.d.ts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
- package/dist/adapters/sandbox/bedrock/index.cjs +3 -3
- package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
- package/dist/adapters/sandbox/bedrock/index.d.cts +6 -6
- package/dist/adapters/sandbox/bedrock/index.d.ts +6 -6
- package/dist/adapters/sandbox/bedrock/index.js +3 -3
- package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
- package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/daytona/index.cjs +3 -3
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +4 -4
- package/dist/adapters/sandbox/daytona/index.d.ts +4 -4
- package/dist/adapters/sandbox/daytona/index.js +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/e2b/index.cjs +26 -14
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +24 -4
- package/dist/adapters/sandbox/e2b/index.d.ts +24 -4
- package/dist/adapters/sandbox/e2b/index.js +26 -14
- package/dist/adapters/sandbox/e2b/index.js.map +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs +3 -3
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +4 -4
- package/dist/adapters/sandbox/inmemory/index.d.ts +4 -4
- package/dist/adapters/sandbox/inmemory/index.js +3 -3
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +150 -13
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +9 -8
- package/dist/adapters/thread/anthropic/index.d.ts +9 -8
- package/dist/adapters/thread/anthropic/index.js +150 -14
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +9 -3
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +6 -5
- package/dist/adapters/thread/anthropic/workflow.d.ts +6 -5
- package/dist/adapters/thread/anthropic/workflow.js +9 -4
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +154 -13
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +6 -5
- package/dist/adapters/thread/google-genai/index.d.ts +6 -5
- package/dist/adapters/thread/google-genai/index.js +154 -14
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +9 -3
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +6 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +6 -5
- package/dist/adapters/thread/google-genai/workflow.js +9 -4
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/index.cjs +16 -0
- package/dist/adapters/thread/index.cjs.map +1 -0
- package/dist/adapters/thread/index.d.cts +34 -0
- package/dist/adapters/thread/index.d.ts +34 -0
- package/dist/adapters/thread/index.js +12 -0
- package/dist/adapters/thread/index.js.map +1 -0
- package/dist/adapters/thread/langchain/index.cjs +149 -14
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +9 -8
- package/dist/adapters/thread/langchain/index.d.ts +9 -8
- package/dist/adapters/thread/langchain/index.js +149 -15
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +9 -3
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +6 -5
- package/dist/adapters/thread/langchain/workflow.d.ts +6 -5
- package/dist/adapters/thread/langchain/workflow.js +9 -4
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/index.cjs +367 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +365 -61
- package/dist/index.js.map +1 -1
- package/dist/{proxy-DEtowJyd.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
- package/dist/{proxy-0smGKvx8.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
- package/dist/{thread-manager-C-C4pI2z.d.ts → thread-manager-CcRXasqs.d.ts} +2 -2
- package/dist/{thread-manager-D4vgzYrh.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -2
- package/dist/{thread-manager-3fszQih4.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -2
- package/dist/{thread-manager-CzYln2OC.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -2
- package/dist/{types-CPKDl-y_.d.ts → types-Bcbiq8iv.d.cts} +195 -22
- package/dist/{types-CNuWnvy9.d.ts → types-DAsQ21Rt.d.ts} +1 -1
- package/dist/{types-B37hKoWA.d.ts → types-DpHTX-iO.d.ts} +58 -1
- package/dist/{types-BO7Yju20.d.cts → types-Dt8-HBBT.d.ts} +195 -22
- package/dist/{types-D08CXPh8.d.cts → types-hFFi-Zd9.d.cts} +58 -1
- package/dist/{types-DWEUmYAJ.d.cts → types-lm8tMNJQ.d.cts} +1 -1
- package/dist/{types-tQL9njTu.d.cts → types-yx0LzPGn.d.cts} +21 -7
- package/dist/{types-tQL9njTu.d.ts → types-yx0LzPGn.d.ts} +21 -7
- package/dist/{workflow-CjXHbZZc.d.ts → workflow-Bmf9EtDW.d.ts} +83 -3
- package/dist/{workflow-Do_lzJpT.d.cts → workflow-Bx9utBwb.d.cts} +83 -3
- package/dist/workflow.cjs +266 -39
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +3 -3
- package/dist/workflow.d.ts +3 -3
- package/dist/workflow.js +264 -41
- package/dist/workflow.js.map +1 -1
- package/package.json +12 -2
- package/src/adapters/sandbox/bedrock/index.ts +12 -3
- package/src/adapters/sandbox/daytona/index.ts +12 -3
- package/src/adapters/sandbox/e2b/index.ts +36 -14
- package/src/adapters/sandbox/e2b/types.ts +16 -0
- package/src/adapters/sandbox/inmemory/index.ts +12 -3
- package/src/adapters/thread/adapter-id.test.ts +42 -0
- package/src/adapters/thread/anthropic/activities.ts +40 -5
- package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
- package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
- package/src/adapters/thread/anthropic/index.ts +3 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +7 -1
- package/src/adapters/thread/anthropic/proxy.ts +3 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +27 -1
- package/src/adapters/thread/google-genai/activities.ts +44 -5
- package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
- package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
- package/src/adapters/thread/google-genai/index.ts +3 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +8 -2
- package/src/adapters/thread/google-genai/proxy.ts +3 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +27 -1
- package/src/adapters/thread/index.ts +39 -0
- package/src/adapters/thread/langchain/activities.ts +40 -5
- package/src/adapters/thread/langchain/adapter-id.ts +16 -0
- package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
- package/src/adapters/thread/langchain/index.ts +3 -0
- package/src/adapters/thread/langchain/model-invoker.ts +7 -1
- package/src/adapters/thread/langchain/proxy.ts +3 -2
- package/src/adapters/thread/langchain/thread-manager.ts +27 -1
- package/src/lib/lifecycle.ts +14 -5
- package/src/lib/model/types.ts +7 -0
- package/src/lib/sandbox/manager.ts +26 -18
- package/src/lib/sandbox/types.ts +27 -7
- package/src/lib/session/session-edge-cases.integration.test.ts +336 -4
- package/src/lib/session/session.integration.test.ts +192 -2
- package/src/lib/session/session.ts +102 -8
- package/src/lib/session/types.ts +66 -3
- package/src/lib/state/index.ts +1 -0
- package/src/lib/state/manager.integration.test.ts +109 -0
- package/src/lib/state/manager.ts +38 -8
- package/src/lib/state/types.ts +25 -0
- package/src/lib/subagent/handler.ts +124 -11
- package/src/lib/subagent/index.ts +5 -1
- package/src/lib/subagent/subagent.integration.test.ts +628 -104
- package/src/lib/subagent/types.ts +63 -14
- package/src/lib/subagent/workflow.ts +29 -2
- package/src/lib/thread/index.ts +5 -0
- package/src/lib/thread/keys.test.ts +101 -0
- package/src/lib/thread/keys.ts +94 -0
- package/src/lib/thread/manager.test.ts +139 -0
- package/src/lib/thread/manager.ts +105 -9
- package/src/lib/thread/proxy.ts +3 -0
- package/src/lib/thread/types.ts +64 -1
- package/src/lib/tool-router/index.ts +2 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +92 -0
- package/src/lib/tool-router/router.integration.test.ts +12 -0
- package/src/lib/tool-router/router.ts +89 -16
- package/src/lib/tool-router/types.ts +42 -1
- package/src/lib/types.ts +12 -0
- package/src/workflow.ts +14 -1
- package/tsup.config.ts +1 -0
|
@@ -61,7 +61,7 @@ export function createAnthropicModelInvoker({
|
|
|
61
61
|
return async function invokeAnthropicModel(
|
|
62
62
|
config: ModelInvokerConfig
|
|
63
63
|
): Promise<AgentResponse<Anthropic.Messages.Message>> {
|
|
64
|
-
const { threadId, threadKey, state } = config;
|
|
64
|
+
const { threadId, threadKey, state, assistantMessageId } = config;
|
|
65
65
|
const { heartbeat, signal } = getActivityContext();
|
|
66
66
|
|
|
67
67
|
const thread = createAnthropicThreadManager({
|
|
@@ -70,6 +70,12 @@ export function createAnthropicModelInvoker({
|
|
|
70
70
|
key: threadKey,
|
|
71
71
|
hooks,
|
|
72
72
|
});
|
|
73
|
+
// Truncate the thread starting at the id the assistant message
|
|
74
|
+
// will be stored under. On the happy path this is a no-op; on a
|
|
75
|
+
// rewind retry or a Temporal workflow reset it wipes the prior
|
|
76
|
+
// attempt's assistant + tool results so the LLM sees the same
|
|
77
|
+
// pre-call state that it saw originally.
|
|
78
|
+
await thread.truncateFromId(assistantMessageId);
|
|
73
79
|
const { messages, system } = await thread.prepareForInvocation();
|
|
74
80
|
|
|
75
81
|
const anthropicTools = toAnthropicTools(state.tools);
|
|
@@ -22,15 +22,16 @@ import { type ActivityInterfaceFor } from "@temporalio/workflow";
|
|
|
22
22
|
import type { ThreadOps } from "../../../lib/session/types";
|
|
23
23
|
import type { AnthropicContent } from "./thread-manager";
|
|
24
24
|
import { createThreadOpsProxy } from "../../../lib/thread/proxy";
|
|
25
|
+
import { ADAPTER_ID } from "./adapter-id";
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
export { ADAPTER_ID, type AdapterId } from "./adapter-id";
|
|
27
28
|
|
|
28
29
|
export function proxyAnthropicThreadOps(
|
|
29
30
|
scope?: string,
|
|
30
31
|
options?: Parameters<typeof createThreadOpsProxy>[2]
|
|
31
32
|
): ActivityInterfaceFor<ThreadOps<AnthropicContent>> {
|
|
32
33
|
return createThreadOpsProxy(
|
|
33
|
-
|
|
34
|
+
ADAPTER_ID,
|
|
34
35
|
scope,
|
|
35
36
|
options
|
|
36
37
|
) as ActivityInterfaceFor<ThreadOps<AnthropicContent>>;
|
|
@@ -224,5 +224,31 @@ export function createAnthropicThreadManager(
|
|
|
224
224
|
},
|
|
225
225
|
};
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
const manager = Object.assign(base, helpers);
|
|
228
|
+
|
|
229
|
+
const originalFork = manager.fork.bind(manager);
|
|
230
|
+
manager.fork = async (
|
|
231
|
+
newThreadId: string
|
|
232
|
+
): Promise<AnthropicThreadManager> => {
|
|
233
|
+
await originalFork(newThreadId);
|
|
234
|
+
const forked = createAnthropicThreadManager({
|
|
235
|
+
...config,
|
|
236
|
+
threadId: newThreadId,
|
|
237
|
+
});
|
|
238
|
+
const { onForkPrepareThread, onForkTransform } = config.hooks ?? {};
|
|
239
|
+
if (!onForkPrepareThread && !onForkTransform) {
|
|
240
|
+
return forked;
|
|
241
|
+
}
|
|
242
|
+
let next = await forked.load();
|
|
243
|
+
if (onForkPrepareThread) {
|
|
244
|
+
next = await onForkPrepareThread(next);
|
|
245
|
+
}
|
|
246
|
+
if (onForkTransform) {
|
|
247
|
+
next = next.map((msg, i) => onForkTransform(msg, i, next));
|
|
248
|
+
}
|
|
249
|
+
await forked.replaceAll(next);
|
|
250
|
+
return forked;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return manager;
|
|
228
254
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type Redis from "ioredis";
|
|
2
2
|
import type { GoogleGenAI, Content, Part } from "@google/genai";
|
|
3
3
|
import type { ToolResultConfig } from "../../../lib/types";
|
|
4
|
+
import type { PersistedThreadState } from "../../../lib/state/types";
|
|
4
5
|
import type {
|
|
5
6
|
ActivityToolHandler,
|
|
6
7
|
RouterContext,
|
|
@@ -19,12 +20,11 @@ import {
|
|
|
19
20
|
type GoogleGenAIThreadManagerHooks,
|
|
20
21
|
} from "./thread-manager";
|
|
21
22
|
import { createGoogleGenAIModelInvoker } from "./model-invoker";
|
|
22
|
-
|
|
23
|
-
const ADAPTER_PREFIX = "googleGenAI" as const;
|
|
23
|
+
import { ADAPTER_ID } from "./adapter-id";
|
|
24
24
|
|
|
25
25
|
export type GoogleGenAIThreadOps<TScope extends string = ""> =
|
|
26
26
|
PrefixedThreadOps<
|
|
27
|
-
ScopedPrefix<TScope, typeof
|
|
27
|
+
ScopedPrefix<TScope, typeof ADAPTER_ID>,
|
|
28
28
|
GoogleGenAIContent
|
|
29
29
|
>;
|
|
30
30
|
|
|
@@ -219,17 +219,56 @@ export function createGoogleGenAIAdapter(
|
|
|
219
219
|
redis,
|
|
220
220
|
threadId: sourceThreadId,
|
|
221
221
|
key: threadKey,
|
|
222
|
+
hooks: config.hooks,
|
|
222
223
|
});
|
|
223
224
|
await thread.fork(targetThreadId);
|
|
224
225
|
},
|
|
226
|
+
|
|
227
|
+
async truncateThread(
|
|
228
|
+
threadId: string,
|
|
229
|
+
messageId: string,
|
|
230
|
+
threadKey?: string,
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
const thread = createGoogleGenAIThreadManager({
|
|
233
|
+
redis,
|
|
234
|
+
threadId,
|
|
235
|
+
key: threadKey,
|
|
236
|
+
});
|
|
237
|
+
await thread.truncateFromId(messageId);
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async loadThreadState(
|
|
241
|
+
threadId: string,
|
|
242
|
+
threadKey?: string
|
|
243
|
+
): Promise<PersistedThreadState | null> {
|
|
244
|
+
const thread = createGoogleGenAIThreadManager({
|
|
245
|
+
redis,
|
|
246
|
+
threadId,
|
|
247
|
+
key: threadKey,
|
|
248
|
+
});
|
|
249
|
+
return thread.loadState();
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
async saveThreadState(
|
|
253
|
+
threadId: string,
|
|
254
|
+
state: PersistedThreadState,
|
|
255
|
+
threadKey?: string
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
const thread = createGoogleGenAIThreadManager({
|
|
258
|
+
redis,
|
|
259
|
+
threadId,
|
|
260
|
+
key: threadKey,
|
|
261
|
+
});
|
|
262
|
+
await thread.saveState(state);
|
|
263
|
+
},
|
|
225
264
|
};
|
|
226
265
|
|
|
227
266
|
function createActivities<S extends string = "">(
|
|
228
267
|
scope?: S
|
|
229
268
|
): GoogleGenAIThreadOps<S> {
|
|
230
269
|
const prefix = scope
|
|
231
|
-
? `${
|
|
232
|
-
:
|
|
270
|
+
? `${ADAPTER_ID}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
|
|
271
|
+
: ADAPTER_ID;
|
|
233
272
|
const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
|
|
234
273
|
return Object.fromEntries(
|
|
235
274
|
Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public adapter identity for the Google GenAI thread adapter.
|
|
3
|
+
*
|
|
4
|
+
* This value is wire format — it appears as the prefix for Temporal
|
|
5
|
+
* activity names (e.g. `googleGenAICodingAgentInitializeThread`) and
|
|
6
|
+
* must never change, since renaming it would orphan existing persisted
|
|
7
|
+
* threads and break in-flight workflows.
|
|
8
|
+
*
|
|
9
|
+
* Re-exported from `zeitlich/adapters/thread/google-genai` so downstream
|
|
10
|
+
* consumers can use the exact same literal the adapter uses internally,
|
|
11
|
+
* typed as the narrow string literal `"googleGenAI"`.
|
|
12
|
+
*/
|
|
13
|
+
export const ADAPTER_ID = "googleGenAI" as const;
|
|
14
|
+
|
|
15
|
+
/** Narrow string-literal type for {@link ADAPTER_ID}. */
|
|
16
|
+
export type AdapterId = typeof ADAPTER_ID;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { StoredContent } from "./thread-manager";
|
|
3
|
+
import { createGoogleGenAIThreadManager } from "./thread-manager";
|
|
4
|
+
|
|
5
|
+
function createStatefulRedis() {
|
|
6
|
+
const lists = new Map<string, string[]>();
|
|
7
|
+
const strings = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
exists: vi.fn(async (...keys: string[]) =>
|
|
11
|
+
keys.reduce(
|
|
12
|
+
(acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
|
|
13
|
+
0
|
|
14
|
+
)
|
|
15
|
+
),
|
|
16
|
+
lrange: vi.fn(async (key: string, start: number, stop: number) => {
|
|
17
|
+
const list = lists.get(key) ?? [];
|
|
18
|
+
const end = stop === -1 ? list.length : stop + 1;
|
|
19
|
+
return list.slice(start, end);
|
|
20
|
+
}),
|
|
21
|
+
rpush: vi.fn(async (key: string, ...values: string[]) => {
|
|
22
|
+
const list = lists.get(key) ?? [];
|
|
23
|
+
list.push(...values);
|
|
24
|
+
lists.set(key, list);
|
|
25
|
+
return list.length;
|
|
26
|
+
}),
|
|
27
|
+
ltrim: vi.fn(async () => "OK"),
|
|
28
|
+
del: vi.fn(async (...keys: string[]) => {
|
|
29
|
+
let removed = 0;
|
|
30
|
+
for (const k of keys) {
|
|
31
|
+
if (lists.delete(k)) removed++;
|
|
32
|
+
if (strings.delete(k)) removed++;
|
|
33
|
+
}
|
|
34
|
+
return removed;
|
|
35
|
+
}),
|
|
36
|
+
set: vi.fn(async (key: string, value: string) => {
|
|
37
|
+
strings.set(key, value);
|
|
38
|
+
return "OK";
|
|
39
|
+
}),
|
|
40
|
+
get: vi.fn(async (key: string) => strings.get(key) ?? null),
|
|
41
|
+
expire: vi.fn(async () => 1),
|
|
42
|
+
llen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
|
|
43
|
+
eval: vi.fn(
|
|
44
|
+
async (_script: string, _numKeys: number, ...args: string[]) => {
|
|
45
|
+
const [dedupKey, listKey, , ...serialised] = args;
|
|
46
|
+
if (!dedupKey || !listKey) return 0;
|
|
47
|
+
if (strings.has(dedupKey)) return 0;
|
|
48
|
+
const list = lists.get(listKey) ?? [];
|
|
49
|
+
list.push(...serialised);
|
|
50
|
+
lists.set(listKey, list);
|
|
51
|
+
strings.set(dedupKey, "1");
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const userContent: StoredContent = {
|
|
59
|
+
id: "msg-1",
|
|
60
|
+
content: { role: "user", parts: [{ text: "Hello" }] },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const modelContent: StoredContent = {
|
|
64
|
+
id: "msg-2",
|
|
65
|
+
content: { role: "model", parts: [{ text: "Hi there!" }] },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const userContent2: StoredContent = {
|
|
69
|
+
id: "msg-3",
|
|
70
|
+
content: { role: "user", parts: [{ text: "Again please" }] },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async function seed(
|
|
74
|
+
redis: ReturnType<typeof createStatefulRedis>,
|
|
75
|
+
threadId: string,
|
|
76
|
+
messages: StoredContent[]
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const tm = createGoogleGenAIThreadManager({
|
|
79
|
+
redis: redis as never,
|
|
80
|
+
threadId,
|
|
81
|
+
});
|
|
82
|
+
await tm.initialize();
|
|
83
|
+
await tm.append(messages);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("Google GenAI fork + transform hooks", () => {
|
|
87
|
+
it("falls back to plain fork when no hooks are set", async () => {
|
|
88
|
+
const redis = createStatefulRedis();
|
|
89
|
+
await seed(redis, "src", [userContent, modelContent]);
|
|
90
|
+
|
|
91
|
+
const tm = createGoogleGenAIThreadManager({
|
|
92
|
+
redis: redis as never,
|
|
93
|
+
threadId: "src",
|
|
94
|
+
});
|
|
95
|
+
const forked = await tm.fork("dst");
|
|
96
|
+
expect(await forked.load()).toEqual([userContent, modelContent]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("applies onForkPrepareThread then onForkTransform in order", async () => {
|
|
100
|
+
const redis = createStatefulRedis();
|
|
101
|
+
await seed(redis, "src", [userContent, modelContent, userContent2]);
|
|
102
|
+
|
|
103
|
+
const order: string[] = [];
|
|
104
|
+
const tm = createGoogleGenAIThreadManager({
|
|
105
|
+
redis: redis as never,
|
|
106
|
+
threadId: "src",
|
|
107
|
+
hooks: {
|
|
108
|
+
onForkPrepareThread: async (messages) => {
|
|
109
|
+
order.push("prepare");
|
|
110
|
+
return messages.slice(0, -1);
|
|
111
|
+
},
|
|
112
|
+
onForkTransform: (msg, i) => {
|
|
113
|
+
order.push("transform");
|
|
114
|
+
return {
|
|
115
|
+
...msg,
|
|
116
|
+
content: { ...msg.content, parts: [{ text: `[x${i}]` }] },
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const forked = await tm.fork("dst");
|
|
123
|
+
const loaded = await forked.load();
|
|
124
|
+
|
|
125
|
+
expect(order).toEqual(["prepare", "transform", "transform"]);
|
|
126
|
+
expect(loaded).toHaveLength(2);
|
|
127
|
+
expect(loaded[0]?.content.parts).toEqual([{ text: "[x0]" }]);
|
|
128
|
+
expect(loaded[1]?.content.parts).toEqual([{ text: "[x1]" }]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("keeps the source thread untouched", async () => {
|
|
132
|
+
const redis = createStatefulRedis();
|
|
133
|
+
await seed(redis, "src", [userContent, modelContent]);
|
|
134
|
+
|
|
135
|
+
const tm = createGoogleGenAIThreadManager({
|
|
136
|
+
redis: redis as never,
|
|
137
|
+
threadId: "src",
|
|
138
|
+
hooks: {
|
|
139
|
+
onForkTransform: (msg) => ({
|
|
140
|
+
...msg,
|
|
141
|
+
content: { ...msg.content, parts: [{ text: "mutated" }] },
|
|
142
|
+
}),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await tm.fork("dst");
|
|
147
|
+
expect(await tm.load()).toEqual([userContent, modelContent]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -64,7 +64,7 @@ export function createGoogleGenAIModelInvoker({
|
|
|
64
64
|
return async function invokeGoogleGenAIModel(
|
|
65
65
|
config: ModelInvokerConfig
|
|
66
66
|
): Promise<AgentResponse<Content>> {
|
|
67
|
-
const { threadId, threadKey, state } = config;
|
|
67
|
+
const { threadId, threadKey, state, assistantMessageId } = config;
|
|
68
68
|
const { heartbeat, signal } = getActivityContext();
|
|
69
69
|
|
|
70
70
|
const thread = createGoogleGenAIThreadManager({
|
|
@@ -73,7 +73,13 @@ export function createGoogleGenAIModelInvoker({
|
|
|
73
73
|
key: threadKey,
|
|
74
74
|
hooks,
|
|
75
75
|
});
|
|
76
|
-
|
|
76
|
+
// Truncate the thread starting at the id the assistant message
|
|
77
|
+
// will be stored under. No-op on the first attempt; on rewind
|
|
78
|
+
// retry / Temporal reset it wipes the prior attempt's assistant
|
|
79
|
+
// + tool results so the LLM sees the original pre-call state.
|
|
80
|
+
await thread.truncateFromId(assistantMessageId);
|
|
81
|
+
const { contents, systemInstruction } =
|
|
82
|
+
await thread.prepareForInvocation();
|
|
77
83
|
|
|
78
84
|
const functionDeclarations = toFunctionDeclarations(state.tools);
|
|
79
85
|
const tools =
|
|
@@ -22,15 +22,16 @@ import { type ActivityInterfaceFor } from "@temporalio/workflow";
|
|
|
22
22
|
import type { ThreadOps } from "../../../lib/session/types";
|
|
23
23
|
import type { GoogleGenAIContent } from "./thread-manager";
|
|
24
24
|
import { createThreadOpsProxy } from "../../../lib/thread/proxy";
|
|
25
|
+
import { ADAPTER_ID } from "./adapter-id";
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
export { ADAPTER_ID, type AdapterId } from "./adapter-id";
|
|
27
28
|
|
|
28
29
|
export function proxyGoogleGenAIThreadOps(
|
|
29
30
|
scope?: string,
|
|
30
31
|
options?: Parameters<typeof createThreadOpsProxy>[2]
|
|
31
32
|
): ActivityInterfaceFor<ThreadOps<GoogleGenAIContent>> {
|
|
32
33
|
return createThreadOpsProxy(
|
|
33
|
-
|
|
34
|
+
ADAPTER_ID,
|
|
34
35
|
scope,
|
|
35
36
|
options
|
|
36
37
|
) as ActivityInterfaceFor<ThreadOps<GoogleGenAIContent>>;
|
|
@@ -200,5 +200,31 @@ export function createGoogleGenAIThreadManager(
|
|
|
200
200
|
},
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
const manager = Object.assign(base, helpers);
|
|
204
|
+
|
|
205
|
+
const originalFork = manager.fork.bind(manager);
|
|
206
|
+
manager.fork = async (
|
|
207
|
+
newThreadId: string
|
|
208
|
+
): Promise<GoogleGenAIThreadManager> => {
|
|
209
|
+
await originalFork(newThreadId);
|
|
210
|
+
const forked = createGoogleGenAIThreadManager({
|
|
211
|
+
...config,
|
|
212
|
+
threadId: newThreadId,
|
|
213
|
+
});
|
|
214
|
+
const { onForkPrepareThread, onForkTransform } = config.hooks ?? {};
|
|
215
|
+
if (!onForkPrepareThread && !onForkTransform) {
|
|
216
|
+
return forked;
|
|
217
|
+
}
|
|
218
|
+
let next = await forked.load();
|
|
219
|
+
if (onForkPrepareThread) {
|
|
220
|
+
next = await onForkPrepareThread(next);
|
|
221
|
+
}
|
|
222
|
+
if (onForkTransform) {
|
|
223
|
+
next = next.map((msg, i) => onForkTransform(msg, i, next));
|
|
224
|
+
}
|
|
225
|
+
await forked.replaceAll(next);
|
|
226
|
+
return forked;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return manager;
|
|
204
230
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel re-exports for every built-in thread adapter's public identity.
|
|
3
|
+
*
|
|
4
|
+
* Downstream consumers reading persisted threads can import a narrow
|
|
5
|
+
* discriminated union of adapter identifiers without pulling the full
|
|
6
|
+
* adapter implementation (Redis, provider SDKs, etc.) as a dependency —
|
|
7
|
+
* each individual re-export resolves to an `adapter-id.ts` module with
|
|
8
|
+
* no runtime dependencies.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import {
|
|
13
|
+
* LANGCHAIN_ADAPTER_ID,
|
|
14
|
+
* GOOGLE_GENAI_ADAPTER_ID,
|
|
15
|
+
* ANTHROPIC_ADAPTER_ID,
|
|
16
|
+
* type ThreadAdapterId,
|
|
17
|
+
* } from 'zeitlich/adapters/thread';
|
|
18
|
+
*
|
|
19
|
+
* interface ThreadIdentity {
|
|
20
|
+
* adapter: ThreadAdapterId;
|
|
21
|
+
* threadKey: string;
|
|
22
|
+
* threadId: string;
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export { ADAPTER_ID as LANGCHAIN_ADAPTER_ID } from "./langchain/adapter-id";
|
|
28
|
+
export { ADAPTER_ID as GOOGLE_GENAI_ADAPTER_ID } from "./google-genai/adapter-id";
|
|
29
|
+
export { ADAPTER_ID as ANTHROPIC_ADAPTER_ID } from "./anthropic/adapter-id";
|
|
30
|
+
|
|
31
|
+
import type { ADAPTER_ID as LANGCHAIN } from "./langchain/adapter-id";
|
|
32
|
+
import type { ADAPTER_ID as GOOGLE_GENAI } from "./google-genai/adapter-id";
|
|
33
|
+
import type { ADAPTER_ID as ANTHROPIC } from "./anthropic/adapter-id";
|
|
34
|
+
|
|
35
|
+
/** Narrow discriminated union of every built-in thread adapter id. */
|
|
36
|
+
export type ThreadAdapterId =
|
|
37
|
+
| typeof LANGCHAIN
|
|
38
|
+
| typeof GOOGLE_GENAI
|
|
39
|
+
| typeof ANTHROPIC;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type Redis from "ioredis";
|
|
2
2
|
import type { ToolResultConfig } from "../../../lib/types";
|
|
3
|
+
import type { PersistedThreadState } from "../../../lib/state/types";
|
|
3
4
|
import type { MessageContent } from "@langchain/core/messages";
|
|
4
5
|
import type {
|
|
5
6
|
ActivityToolHandler,
|
|
@@ -21,11 +22,10 @@ import {
|
|
|
21
22
|
type LangChainThreadManagerHooks,
|
|
22
23
|
} from "./thread-manager";
|
|
23
24
|
import { createLangChainModelInvoker } from "./model-invoker";
|
|
24
|
-
|
|
25
|
-
const ADAPTER_PREFIX = "langChain" as const;
|
|
25
|
+
import { ADAPTER_ID } from "./adapter-id";
|
|
26
26
|
|
|
27
27
|
export type LangChainThreadOps<TScope extends string = ""> = PrefixedThreadOps<
|
|
28
|
-
ScopedPrefix<TScope, typeof
|
|
28
|
+
ScopedPrefix<TScope, typeof ADAPTER_ID>,
|
|
29
29
|
LangChainContent
|
|
30
30
|
>;
|
|
31
31
|
|
|
@@ -192,17 +192,52 @@ export function createLangChainAdapter(
|
|
|
192
192
|
redis,
|
|
193
193
|
threadId: sourceThreadId,
|
|
194
194
|
key: threadKey,
|
|
195
|
+
hooks: config.hooks,
|
|
195
196
|
});
|
|
196
197
|
await thread.fork(targetThreadId);
|
|
197
198
|
},
|
|
199
|
+
|
|
200
|
+
async truncateThread(
|
|
201
|
+
threadId: string,
|
|
202
|
+
messageId: string,
|
|
203
|
+
threadKey?: string,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
|
|
206
|
+
await thread.truncateFromId(messageId);
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
async loadThreadState(
|
|
210
|
+
threadId: string,
|
|
211
|
+
threadKey?: string
|
|
212
|
+
): Promise<PersistedThreadState | null> {
|
|
213
|
+
const thread = createLangChainThreadManager({
|
|
214
|
+
redis,
|
|
215
|
+
threadId,
|
|
216
|
+
key: threadKey,
|
|
217
|
+
});
|
|
218
|
+
return thread.loadState();
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async saveThreadState(
|
|
222
|
+
threadId: string,
|
|
223
|
+
state: PersistedThreadState,
|
|
224
|
+
threadKey?: string
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
const thread = createLangChainThreadManager({
|
|
227
|
+
redis,
|
|
228
|
+
threadId,
|
|
229
|
+
key: threadKey,
|
|
230
|
+
});
|
|
231
|
+
await thread.saveState(state);
|
|
232
|
+
},
|
|
198
233
|
};
|
|
199
234
|
|
|
200
235
|
function createActivities<S extends string = "">(
|
|
201
236
|
scope?: S
|
|
202
237
|
): LangChainThreadOps<S> {
|
|
203
238
|
const prefix = scope
|
|
204
|
-
? `${
|
|
205
|
-
:
|
|
239
|
+
? `${ADAPTER_ID}${scope.charAt(0).toUpperCase()}${scope.slice(1)}`
|
|
240
|
+
: ADAPTER_ID;
|
|
206
241
|
const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
|
|
207
242
|
return Object.fromEntries(
|
|
208
243
|
Object.entries(threadOps).map(([k, v]) => [`${prefix}${cap(k)}`, v])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public adapter identity for the LangChain thread adapter.
|
|
3
|
+
*
|
|
4
|
+
* This value is wire format — it appears as the prefix for Temporal
|
|
5
|
+
* activity names (e.g. `langChainCodingAgentInitializeThread`) and must
|
|
6
|
+
* never change, since renaming it would orphan existing persisted
|
|
7
|
+
* threads and break in-flight workflows.
|
|
8
|
+
*
|
|
9
|
+
* Re-exported from `zeitlich/adapters/thread/langchain` so downstream
|
|
10
|
+
* consumers can use the exact same literal the adapter uses internally,
|
|
11
|
+
* typed as the narrow string literal `"langChain"`.
|
|
12
|
+
*/
|
|
13
|
+
export const ADAPTER_ID = "langChain" as const;
|
|
14
|
+
|
|
15
|
+
/** Narrow string-literal type for {@link ADAPTER_ID}. */
|
|
16
|
+
export type AdapterId = typeof ADAPTER_ID;
|