zeitlich 0.2.49 → 0.2.51
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 +26 -23
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +3 -3
- package/dist/adapters/sandbox/daytona/index.d.ts +3 -3
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/daytona/workflow.d.cts +2 -2
- package/dist/adapters/sandbox/daytona/workflow.d.ts +2 -2
- package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
- package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
- 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/thread/anthropic/index.cjs +60 -55
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +20 -15
- package/dist/adapters/thread/anthropic/index.d.ts +20 -15
- package/dist/adapters/thread/anthropic/index.js +60 -55
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +7 -7
- package/dist/adapters/thread/anthropic/workflow.d.ts +7 -7
- package/dist/adapters/thread/google-genai/index.cjs +135 -66
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +200 -26
- package/dist/adapters/thread/google-genai/index.d.ts +200 -26
- package/dist/adapters/thread/google-genai/index.js +135 -66
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +8 -8
- package/dist/adapters/thread/google-genai/workflow.d.ts +8 -8
- package/dist/adapters/thread/langchain/index.cjs +67 -55
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +20 -15
- package/dist/adapters/thread/langchain/index.d.ts +20 -15
- package/dist/adapters/thread/langchain/index.js +67 -55
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +7 -7
- package/dist/adapters/thread/langchain/workflow.d.ts +7 -7
- package/dist/{cold-store-DKMAO1Dd.d.ts → cold-store-DyHodfAB.d.ts} +1 -1
- package/dist/{cold-store-CkWoNtMh.d.cts → cold-store-YOx9nmgR.d.cts} +1 -1
- package/dist/index.cjs +15050 -420
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -83
- package/dist/index.d.ts +79 -83
- package/dist/index.js +15051 -417
- package/dist/index.js.map +1 -1
- package/dist/{proxy-B7CWEV-T.d.cts → proxy-2htgGQrc.d.cts} +1 -1
- package/dist/{proxy-ByFHMVRX.d.ts → proxy-CmiTP4pp.d.ts} +1 -1
- package/dist/{thread-manager-nK-WcFzM.d.ts → thread-manager-BJ5pz5Cx.d.cts} +6 -7
- package/dist/{thread-manager-7AW4rhfu.d.ts → thread-manager-BQAbrYXH.d.cts} +6 -7
- package/dist/{thread-manager-Cibe0X5m.d.cts → thread-manager-CcvltOuq.d.ts} +6 -7
- package/dist/{thread-manager-B9rtMEVn.d.cts → thread-manager-DHAbncHX.d.ts} +6 -7
- package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
- package/dist/{types-XUUFvrJ9.d.cts → types-BjdqxKYp.d.cts} +709 -709
- package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.cts} +3 -3
- package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.ts} +3 -3
- package/dist/{types-DO4Tkwxo.d.ts → types-DEbkLA06.d.ts} +3 -3
- package/dist/{types-DeVNWqlb.d.ts → types-DiI7mZhI.d.ts} +709 -709
- package/dist/{types-BR-k7h0e.d.cts → types-N_LTWe4b.d.cts} +3 -3
- package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
- package/dist/{workflow-uhOIj9D-.d.ts → workflow-CcgD6EUB.d.cts} +34 -3
- package/dist/{workflow-KbGsxpfh.d.cts → workflow-DBjPOKBr.d.ts} +34 -3
- package/dist/workflow.cjs +15008 -377
- 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 +15009 -374
- package/dist/workflow.js.map +1 -1
- package/package.json +10 -37
- package/src/adapters/thread/anthropic/activities.test.ts +115 -0
- package/src/adapters/thread/anthropic/activities.ts +11 -19
- package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
- package/src/adapters/thread/anthropic/model-invoker.test.ts +54 -3
- package/src/adapters/thread/anthropic/model-invoker.ts +11 -1
- package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +3 -4
- package/src/adapters/thread/google-genai/activities.test.ts +162 -0
- package/src/adapters/thread/google-genai/activities.ts +38 -15
- package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
- package/src/adapters/thread/google-genai/model-invoker.test.ts +386 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +118 -23
- package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +3 -4
- package/src/adapters/thread/langchain/activities.test.ts +88 -0
- package/src/adapters/thread/langchain/activities.ts +15 -12
- package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
- package/src/adapters/thread/langchain/model-invoker.test.ts +74 -0
- package/src/adapters/thread/langchain/model-invoker.ts +16 -3
- package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
- package/src/adapters/thread/langchain/thread-manager.ts +3 -4
- package/src/index.ts +2 -2
- package/src/lib/sandbox/capability-types.test.ts +2 -2
- package/src/lib/sandbox/manager.ts +2 -6
- package/src/lib/sandbox/sandbox.test.ts +1 -1
- package/src/lib/sandbox/types.ts +2 -2
- package/src/lib/session/session.integration.test.ts +92 -0
- package/src/lib/session/session.ts +23 -11
- package/src/lib/thread/keys.test.ts +9 -9
- package/src/lib/thread/keys.ts +1 -1
- package/src/lib/thread/manager.test.ts +24 -14
- package/src/lib/thread/manager.ts +19 -23
- package/src/lib/thread/snapshot.test.ts +51 -43
- package/src/lib/thread/snapshot.ts +54 -32
- package/src/lib/thread/test-utils.ts +106 -59
- package/src/lib/thread/tiered.test.ts +1 -1
- package/src/lib/thread/types.ts +2 -2
- package/src/lib/tool-router/router.integration.test.ts +44 -0
- package/src/lib/tool-router/router.ts +140 -32
- package/src/lib/workflow.ts +49 -0
- package/src/{adapters/sandbox/inmemory/proxy.ts → test-utils/in-memory-sandbox-proxy.ts} +5 -16
- package/src/{adapters/sandbox/inmemory/index.ts → test-utils/in-memory-sandbox.ts} +11 -3
- package/src/tools/bash/bash.test.ts +1 -1
- package/src/tools/edit/handler.test.ts +1 -1
- package/tsup.config.ts +2 -4
- package/dist/activities-7OcT_vdR.d.cts +0 -162
- package/dist/activities-zG_FBoY2.d.ts +0 -162
- package/dist/adapters/sandbox/inmemory/index.cjs +0 -214
- package/dist/adapters/sandbox/inmemory/index.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +0 -40
- package/dist/adapters/sandbox/inmemory/index.d.ts +0 -40
- package/dist/adapters/sandbox/inmemory/index.js +0 -211
- package/dist/adapters/sandbox/inmemory/index.js.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.cjs +0 -36
- package/dist/adapters/sandbox/inmemory/workflow.cjs.map +0 -1
- package/dist/adapters/sandbox/inmemory/workflow.d.cts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.d.ts +0 -27
- package/dist/adapters/sandbox/inmemory/workflow.js +0 -34
- package/dist/adapters/sandbox/inmemory/workflow.js.map +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AIMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
+
import { createLangChainAdapter } from "./activities";
|
|
4
|
+
import { THREAD_TTL_SECONDS } from "../../../lib/thread/keys";
|
|
5
|
+
|
|
6
|
+
function createMockRedis(stored: unknown[]) {
|
|
7
|
+
return {
|
|
8
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
+
lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
|
+
lTrim: vi.fn().mockResolvedValue("OK"),
|
|
11
|
+
del: vi.fn().mockResolvedValue(1),
|
|
12
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
13
|
+
rPush: vi.fn().mockResolvedValue(1),
|
|
14
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
15
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createMockModel() {
|
|
20
|
+
const response = {
|
|
21
|
+
tool_calls: [],
|
|
22
|
+
response_metadata: {},
|
|
23
|
+
usage_metadata: { input_tokens: 1, output_tokens: 1 },
|
|
24
|
+
toDict: () => ({ type: "ai", data: { content: "ok" } }),
|
|
25
|
+
};
|
|
26
|
+
return { invoke: vi.fn().mockResolvedValue(response) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Tail stored under the `assistantMessageId`, so the invoker's
|
|
30
|
+
// `truncateFromId` trims it and re-stamps the surviving list key's TTL.
|
|
31
|
+
const retriedThread = [
|
|
32
|
+
new HumanMessage({ id: "msg-1", content: "hi" }).toDict(),
|
|
33
|
+
new AIMessage({ id: "assistant-1", content: "prior" }).toDict(),
|
|
34
|
+
];
|
|
35
|
+
const listKey = "messages:thread:thread-1";
|
|
36
|
+
const metaKey = "messages:meta:thread:thread-1";
|
|
37
|
+
const invokerCall = {
|
|
38
|
+
threadId: "thread-1",
|
|
39
|
+
assistantMessageId: "assistant-1",
|
|
40
|
+
state: { tools: [] } as never,
|
|
41
|
+
agentName: "TestAgent",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe("createLangChainAdapter — TTL propagation", () => {
|
|
45
|
+
it("forwards adapter ttlSeconds to a created invoker's writes", async () => {
|
|
46
|
+
const redis = createMockRedis(retriedThread);
|
|
47
|
+
const model = createMockModel();
|
|
48
|
+
const adapter = createLangChainAdapter({
|
|
49
|
+
redis: redis as never,
|
|
50
|
+
ttlSeconds: 3600,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await adapter.createModelInvoker(model as never)(invokerCall);
|
|
54
|
+
|
|
55
|
+
expect(redis.expire).toHaveBeenCalledWith(listKey, 3600);
|
|
56
|
+
expect(redis.expire).not.toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("forwards adapter ttlSeconds to thread-op writes", async () => {
|
|
60
|
+
const redis = createMockRedis([]);
|
|
61
|
+
const adapter = createLangChainAdapter({
|
|
62
|
+
redis: redis as never,
|
|
63
|
+
ttlSeconds: 3600,
|
|
64
|
+
});
|
|
65
|
+
const acts = adapter.createActivities() as unknown as Record<
|
|
66
|
+
string,
|
|
67
|
+
(threadId: string, threadKey?: string) => Promise<void>
|
|
68
|
+
>;
|
|
69
|
+
const initialize = Object.entries(acts).find(([k]) =>
|
|
70
|
+
k.endsWith("InitializeThread")
|
|
71
|
+
)?.[1];
|
|
72
|
+
if (!initialize) throw new Error("initializeThread activity not found");
|
|
73
|
+
|
|
74
|
+
await initialize("thread-1");
|
|
75
|
+
|
|
76
|
+
expect(redis.set).toHaveBeenCalledWith(metaKey, "1", { EX: 3600 });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("defaults to THREAD_TTL_SECONDS when adapter ttlSeconds is omitted", async () => {
|
|
80
|
+
const redis = createMockRedis(retriedThread);
|
|
81
|
+
const model = createMockModel();
|
|
82
|
+
const adapter = createLangChainAdapter({ redis: redis as never });
|
|
83
|
+
|
|
84
|
+
await adapter.createModelInvoker(model as never)(invokerCall);
|
|
85
|
+
|
|
86
|
+
expect(redis.expire).toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Redis from "
|
|
1
|
+
import type { RedisClientType as Redis } from "redis";
|
|
2
2
|
import type { ToolResultConfig } from "../../../lib/types";
|
|
3
3
|
import type { PersistedThreadState } from "../../../lib/state/types";
|
|
4
4
|
import type { MessageContent } from "@langchain/core/messages";
|
|
@@ -46,9 +46,8 @@ export interface LangChainAdapterConfig {
|
|
|
46
46
|
*/
|
|
47
47
|
coldStore?: ColdThreadStore;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* more appropriate.
|
|
49
|
+
* Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
|
|
50
|
+
* value (hours) with a cold tier.
|
|
52
51
|
*/
|
|
53
52
|
ttlSeconds?: number;
|
|
54
53
|
}
|
|
@@ -133,25 +132,26 @@ export function createLangChainAdapter(
|
|
|
133
132
|
): LangChainAdapter {
|
|
134
133
|
const { redis } = config;
|
|
135
134
|
|
|
136
|
-
|
|
135
|
+
// Single source for the adapter's `redis` handle and configured TTL, spread
|
|
136
|
+
// into every internal thread manager so all of them share one configuration.
|
|
137
|
+
const base = {
|
|
138
|
+
redis,
|
|
137
139
|
...(config.ttlSeconds !== undefined && { ttlSeconds: config.ttlSeconds }),
|
|
138
140
|
};
|
|
139
141
|
|
|
140
142
|
const makeProviderThread = (threadId: string, threadKey?: string) =>
|
|
141
143
|
createLangChainThreadManager({
|
|
142
|
-
|
|
144
|
+
...base,
|
|
143
145
|
threadId,
|
|
144
146
|
key: threadKey,
|
|
145
|
-
...baseExtras,
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
const makeTieredBase = (threadId: string, threadKey?: string) =>
|
|
149
150
|
createTieredThreadManager<StoredMessage>({
|
|
150
|
-
|
|
151
|
+
...base,
|
|
151
152
|
threadId,
|
|
152
153
|
key: threadKey,
|
|
153
154
|
idOf: storedMessageId,
|
|
154
|
-
...baseExtras,
|
|
155
155
|
...(config.coldStore && { coldStore: config.coldStore }),
|
|
156
156
|
});
|
|
157
157
|
|
|
@@ -207,11 +207,10 @@ export function createLangChainAdapter(
|
|
|
207
207
|
threadKey?: string
|
|
208
208
|
): Promise<void> {
|
|
209
209
|
const thread = createLangChainThreadManager({
|
|
210
|
-
|
|
210
|
+
...base,
|
|
211
211
|
threadId: sourceThreadId,
|
|
212
212
|
key: threadKey,
|
|
213
213
|
hooks: config.hooks,
|
|
214
|
-
...baseExtras,
|
|
215
214
|
});
|
|
216
215
|
await thread.fork(targetThreadId);
|
|
217
216
|
},
|
|
@@ -275,7 +274,11 @@ export function createLangChainAdapter(
|
|
|
275
274
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
275
|
model: BaseChatModel<any>
|
|
277
276
|
): ModelInvoker<StoredMessage> =>
|
|
278
|
-
createLangChainModelInvoker({
|
|
277
|
+
createLangChainModelInvoker({
|
|
278
|
+
...base,
|
|
279
|
+
model,
|
|
280
|
+
hooks: config.hooks,
|
|
281
|
+
});
|
|
279
282
|
|
|
280
283
|
const invoker: ModelInvoker<StoredMessage> = config.model
|
|
281
284
|
? makeInvoker(config.model)
|
|
@@ -11,27 +11,27 @@ function createStatefulRedis() {
|
|
|
11
11
|
const strings = new Map<string, string>();
|
|
12
12
|
|
|
13
13
|
return {
|
|
14
|
-
exists: vi.fn(async (
|
|
15
|
-
keys.reduce(
|
|
14
|
+
exists: vi.fn(async (keys: string | string[]) =>
|
|
15
|
+
(Array.isArray(keys) ? keys : [keys]).reduce(
|
|
16
16
|
(acc, k) => acc + (lists.has(k) || strings.has(k) ? 1 : 0),
|
|
17
17
|
0
|
|
18
18
|
)
|
|
19
19
|
),
|
|
20
|
-
|
|
20
|
+
lRange: vi.fn(async (key: string, start: number, stop: number) => {
|
|
21
21
|
const list = lists.get(key) ?? [];
|
|
22
22
|
const end = stop === -1 ? list.length : stop + 1;
|
|
23
23
|
return list.slice(start, end);
|
|
24
24
|
}),
|
|
25
|
-
|
|
25
|
+
rPush: vi.fn(async (key: string, element: string | string[]) => {
|
|
26
26
|
const list = lists.get(key) ?? [];
|
|
27
|
-
list.push(...
|
|
27
|
+
list.push(...(Array.isArray(element) ? element : [element]));
|
|
28
28
|
lists.set(key, list);
|
|
29
29
|
return list.length;
|
|
30
30
|
}),
|
|
31
|
-
|
|
32
|
-
del: vi.fn(async (
|
|
31
|
+
lTrim: vi.fn(async () => "OK"),
|
|
32
|
+
del: vi.fn(async (keys: string | string[]) => {
|
|
33
33
|
let removed = 0;
|
|
34
|
-
for (const k of keys) {
|
|
34
|
+
for (const k of Array.isArray(keys) ? keys : [keys]) {
|
|
35
35
|
if (lists.delete(k)) removed++;
|
|
36
36
|
if (strings.delete(k)) removed++;
|
|
37
37
|
}
|
|
@@ -43,10 +43,16 @@ function createStatefulRedis() {
|
|
|
43
43
|
}),
|
|
44
44
|
get: vi.fn(async (key: string) => strings.get(key) ?? null),
|
|
45
45
|
expire: vi.fn(async () => 1),
|
|
46
|
-
|
|
46
|
+
lLen: vi.fn(async (key: string) => (lists.get(key) ?? []).length),
|
|
47
47
|
eval: vi.fn(
|
|
48
|
-
async (
|
|
49
|
-
|
|
48
|
+
async (
|
|
49
|
+
_script: string,
|
|
50
|
+
options: { keys?: string[]; arguments?: string[] }
|
|
51
|
+
) => {
|
|
52
|
+
const keys = options.keys ?? [];
|
|
53
|
+
const argv = options.arguments ?? [];
|
|
54
|
+
const [dedupKey, listKey] = keys;
|
|
55
|
+
const serialised = argv.slice(1);
|
|
50
56
|
if (!dedupKey || !listKey) return 0;
|
|
51
57
|
if (strings.has(dedupKey)) return 0;
|
|
52
58
|
const list = lists.get(listKey) ?? [];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { AIMessage, HumanMessage } from "@langchain/core/messages";
|
|
3
|
+
import { createLangChainModelInvoker } from "./model-invoker";
|
|
4
|
+
import { THREAD_TTL_SECONDS } from "../../../lib/thread/keys";
|
|
5
|
+
|
|
6
|
+
function createMockRedis(stored: unknown[]) {
|
|
7
|
+
return {
|
|
8
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
+
lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
|
+
lTrim: vi.fn().mockResolvedValue("OK"),
|
|
11
|
+
del: vi.fn().mockResolvedValue(1),
|
|
12
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
13
|
+
rPush: vi.fn().mockResolvedValue(1),
|
|
14
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
15
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createMockModel() {
|
|
20
|
+
const response = {
|
|
21
|
+
tool_calls: [],
|
|
22
|
+
response_metadata: {},
|
|
23
|
+
usage_metadata: { input_tokens: 1, output_tokens: 1 },
|
|
24
|
+
toDict: () => ({ type: "ai", data: { content: "ok" } }),
|
|
25
|
+
};
|
|
26
|
+
return { invoke: vi.fn().mockResolvedValue(response) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("createLangChainModelInvoker thread TTL", () => {
|
|
30
|
+
// The tail message is stored under `assistant-1`, so the invoker's
|
|
31
|
+
// `truncateFromId(assistant-1)` trims it and re-stamps the surviving
|
|
32
|
+
// list key's TTL.
|
|
33
|
+
const retriedThread = [
|
|
34
|
+
new HumanMessage({ id: "msg-1", content: "hi" }).toDict(),
|
|
35
|
+
new AIMessage({ id: "assistant-1", content: "prior" }).toDict(),
|
|
36
|
+
];
|
|
37
|
+
const listKey = "messages:thread:thread-1";
|
|
38
|
+
const invokerConfig = {
|
|
39
|
+
threadId: "thread-1",
|
|
40
|
+
assistantMessageId: "assistant-1",
|
|
41
|
+
state: { tools: [] } as never,
|
|
42
|
+
agentName: "Agent",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
it("re-stamps trimmed hot keys at the configured ttlSeconds", async () => {
|
|
46
|
+
const redis = createMockRedis(retriedThread);
|
|
47
|
+
const model = createMockModel();
|
|
48
|
+
const invoker = createLangChainModelInvoker({
|
|
49
|
+
redis: redis as never,
|
|
50
|
+
model: model as never,
|
|
51
|
+
ttlSeconds: 3600,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await invoker(invokerConfig);
|
|
55
|
+
|
|
56
|
+
expect(redis.lTrim).toHaveBeenCalledWith(listKey, 0, 0);
|
|
57
|
+
expect(redis.expire).toHaveBeenCalledWith(listKey, 3600);
|
|
58
|
+
expect(redis.expire).not.toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("defaults to THREAD_TTL_SECONDS when ttlSeconds is omitted", async () => {
|
|
62
|
+
const redis = createMockRedis(retriedThread);
|
|
63
|
+
const model = createMockModel();
|
|
64
|
+
const invoker = createLangChainModelInvoker({
|
|
65
|
+
redis: redis as never,
|
|
66
|
+
model: model as never,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await invoker(invokerConfig);
|
|
70
|
+
|
|
71
|
+
expect(redis.lTrim).toHaveBeenCalledWith(listKey, 0, 0);
|
|
72
|
+
expect(redis.expire).toHaveBeenCalledWith(listKey, THREAD_TTL_SECONDS);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Redis from "
|
|
1
|
+
import type { RedisClientType as Redis } from "redis";
|
|
2
2
|
import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
|
|
3
3
|
import type { StoredMessage } from "@langchain/core/messages";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
@@ -16,6 +16,11 @@ export interface LangChainModelInvokerConfig<
|
|
|
16
16
|
redis: Redis;
|
|
17
17
|
model: TModel;
|
|
18
18
|
hooks?: LangChainThreadManagerHooks;
|
|
19
|
+
/**
|
|
20
|
+
* Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
|
|
21
|
+
* value (hours) with a cold tier.
|
|
22
|
+
*/
|
|
23
|
+
ttlSeconds?: number;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -43,7 +48,7 @@ export interface LangChainModelInvokerConfig<
|
|
|
43
48
|
|
|
44
49
|
export function createLangChainModelInvoker<
|
|
45
50
|
TModel extends BaseChatModel<any> = BaseChatModel<any>,
|
|
46
|
-
>({ redis, model, hooks }: LangChainModelInvokerConfig<TModel>) {
|
|
51
|
+
>({ redis, model, hooks, ttlSeconds }: LangChainModelInvokerConfig<TModel>) {
|
|
47
52
|
return async function invokeLangChainModel(
|
|
48
53
|
config: ModelInvokerConfig
|
|
49
54
|
): Promise<AgentResponse<StoredMessage>> {
|
|
@@ -56,6 +61,7 @@ export function createLangChainModelInvoker<
|
|
|
56
61
|
threadId,
|
|
57
62
|
key: threadKey,
|
|
58
63
|
hooks,
|
|
64
|
+
...(ttlSeconds !== undefined && { ttlSeconds }),
|
|
59
65
|
});
|
|
60
66
|
const runId = uuidv4();
|
|
61
67
|
|
|
@@ -122,13 +128,20 @@ export async function invokeLangChainModel<
|
|
|
122
128
|
redis,
|
|
123
129
|
model,
|
|
124
130
|
hooks,
|
|
131
|
+
ttlSeconds,
|
|
125
132
|
config,
|
|
126
133
|
}: {
|
|
127
134
|
redis: Redis;
|
|
128
135
|
config: ModelInvokerConfig;
|
|
129
136
|
model: TModel;
|
|
130
137
|
hooks?: LangChainThreadManagerHooks;
|
|
138
|
+
ttlSeconds?: number;
|
|
131
139
|
}): Promise<AgentResponse<StoredMessage>> {
|
|
132
|
-
const invoker = createLangChainModelInvoker({
|
|
140
|
+
const invoker = createLangChainModelInvoker({
|
|
141
|
+
redis,
|
|
142
|
+
model,
|
|
143
|
+
hooks,
|
|
144
|
+
...(ttlSeconds !== undefined && { ttlSeconds }),
|
|
145
|
+
});
|
|
133
146
|
return invoker(config);
|
|
134
147
|
}
|
|
@@ -9,10 +9,10 @@ import { createLangChainThreadManager } from "./thread-manager";
|
|
|
9
9
|
function createMockRedis(stored: StoredMessage[]) {
|
|
10
10
|
return {
|
|
11
11
|
exists: vi.fn().mockResolvedValue(1),
|
|
12
|
-
|
|
12
|
+
lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
13
13
|
del: vi.fn().mockResolvedValue(1),
|
|
14
14
|
set: vi.fn().mockResolvedValue("OK"),
|
|
15
|
-
|
|
15
|
+
rPush: vi.fn().mockResolvedValue(1),
|
|
16
16
|
expire: vi.fn().mockResolvedValue(1),
|
|
17
17
|
eval: vi.fn().mockResolvedValue(1),
|
|
18
18
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type Redis from "
|
|
1
|
+
import type { RedisClientType as Redis } from "redis";
|
|
2
2
|
import type { JsonValue } from "../../../lib/state/types";
|
|
3
3
|
import {
|
|
4
4
|
AIMessage,
|
|
@@ -35,9 +35,8 @@ export interface LangChainThreadManagerConfig {
|
|
|
35
35
|
key?: string;
|
|
36
36
|
hooks?: LangChainThreadManagerHooks;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* typically more appropriate.
|
|
38
|
+
* Redis TTL for the thread's keys; defaults to 90 days. Use a shorter
|
|
39
|
+
* value (hours) with a cold tier.
|
|
41
40
|
*/
|
|
42
41
|
ttlSeconds?: number;
|
|
43
42
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
* toTree,
|
|
18
18
|
* } from 'zeitlich';
|
|
19
19
|
*
|
|
20
|
-
* //
|
|
21
|
-
* import {
|
|
20
|
+
* // Sandbox adapter
|
|
21
|
+
* import { DaytonaSandboxProvider } from 'zeitlich/adapters/sandbox/daytona';
|
|
22
22
|
*
|
|
23
23
|
* // LangChain adapter
|
|
24
24
|
* import { createLangChainAdapter } from 'zeitlich/adapters/thread/langchain';
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
SandboxProvider,
|
|
28
28
|
SandboxSnapshot,
|
|
29
29
|
} from "./types";
|
|
30
|
-
import { InMemorySandboxProvider } from "../../
|
|
30
|
+
import { InMemorySandboxProvider } from "../../test-utils/in-memory-sandbox";
|
|
31
31
|
import { DaytonaSandboxProvider } from "../../adapters/sandbox/daytona/index";
|
|
32
32
|
import type { E2bSandboxProvider } from "../../adapters/sandbox/e2b/index";
|
|
33
33
|
|
|
@@ -303,7 +303,7 @@ class _ImplWithoutDeclProvider {
|
|
|
303
303
|
import type { SubagentSandboxConfig } from "../subagent/types";
|
|
304
304
|
import { proxyDaytonaSandboxOps } from "../../adapters/sandbox/daytona/proxy";
|
|
305
305
|
import { proxyE2bSandboxOps } from "../../adapters/sandbox/e2b/proxy";
|
|
306
|
-
import { proxyInMemorySandboxOps } from "../../
|
|
306
|
+
import { proxyInMemorySandboxOps } from "../../test-utils/in-memory-sandbox-proxy";
|
|
307
307
|
|
|
308
308
|
// Helper that pins the matrix cell type to `SubagentSandboxConfig` so
|
|
309
309
|
// `@ts-expect-error` directives consistently land on the call line. The
|
|
@@ -102,12 +102,12 @@ export interface SandboxManagerHooks<
|
|
|
102
102
|
*
|
|
103
103
|
* @example
|
|
104
104
|
* ```typescript
|
|
105
|
-
* const manager = new SandboxManager(new
|
|
105
|
+
* const manager = new SandboxManager(new DaytonaSandboxProvider(config));
|
|
106
106
|
* const activities = {
|
|
107
107
|
* ...manager.createActivities("CodingAgent"),
|
|
108
108
|
* bashHandler: withSandbox(manager, bashHandler),
|
|
109
109
|
* };
|
|
110
|
-
* // registers:
|
|
110
|
+
* // registers: daytonaCodingAgentCreateSandbox, …
|
|
111
111
|
* ```
|
|
112
112
|
*
|
|
113
113
|
* @example
|
|
@@ -336,10 +336,6 @@ export class SandboxManager<
|
|
|
336
336
|
*
|
|
337
337
|
* @example
|
|
338
338
|
* ```typescript
|
|
339
|
-
* const manager = new SandboxManager(new InMemorySandboxProvider());
|
|
340
|
-
* manager.createActivities("CodingAgent");
|
|
341
|
-
* // registers: inMemoryCodingAgentCreateSandbox, inMemoryCodingAgentDestroySandbox, …
|
|
342
|
-
*
|
|
343
339
|
* const dmgr = new SandboxManager(new DaytonaSandboxProvider(config));
|
|
344
340
|
* dmgr.createActivities("CodingAgent");
|
|
345
341
|
* // registers: daytonaCodingAgentCreateSandbox, daytonaCodingAgentDestroySandbox
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
2
|
import { SandboxManager } from "./manager";
|
|
3
|
-
import { InMemorySandboxProvider } from "../../
|
|
3
|
+
import { InMemorySandboxProvider } from "../../test-utils/in-memory-sandbox";
|
|
4
4
|
import {
|
|
5
5
|
SandboxNotFoundError,
|
|
6
6
|
type Sandbox,
|
package/src/lib/sandbox/types.ts
CHANGED
|
@@ -342,8 +342,8 @@ export type SandboxOps<
|
|
|
342
342
|
*
|
|
343
343
|
* @example
|
|
344
344
|
* ```typescript
|
|
345
|
-
* type
|
|
346
|
-
* // → {
|
|
345
|
+
* type E2bOps = PrefixedSandboxOps<"e2b">;
|
|
346
|
+
* // → { e2bCreateSandbox, e2bDestroySandbox, e2bSnapshotSandbox, … }
|
|
347
347
|
* ```
|
|
348
348
|
*/
|
|
349
349
|
export type PrefixedSandboxOps<
|
|
@@ -681,6 +681,98 @@ describe("createSession integration", () => {
|
|
|
681
681
|
expect(capturedSandboxId).toBe("my-sandbox");
|
|
682
682
|
});
|
|
683
683
|
|
|
684
|
+
// --- persistThreadState dedupe ---
|
|
685
|
+
|
|
686
|
+
it("memoizes persistThreadState across parallel tool calls in one turn", async () => {
|
|
687
|
+
const { ops, log } = createMockThreadOps();
|
|
688
|
+
|
|
689
|
+
const persistTool = defineTool({
|
|
690
|
+
name: "Persist" as const,
|
|
691
|
+
description: "calls persistThreadState",
|
|
692
|
+
schema: z.object({}),
|
|
693
|
+
handler: async (_args: Record<string, never>, ctx: RouterContext) => {
|
|
694
|
+
await ctx.persistThreadState?.();
|
|
695
|
+
return { toolResponse: "ok", data: null };
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const session = await createSession({
|
|
700
|
+
agentName: "TestAgent",
|
|
701
|
+
thread: { mode: "new", threadId: "thread-1" },
|
|
702
|
+
runAgent: createScriptedRunAgent([
|
|
703
|
+
{
|
|
704
|
+
message: "fan out",
|
|
705
|
+
toolCalls: [
|
|
706
|
+
{ id: "tc-1", name: "Persist", args: {} },
|
|
707
|
+
{ id: "tc-2", name: "Persist", args: {} },
|
|
708
|
+
{ id: "tc-3", name: "Persist", args: {} },
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
{ message: "done", toolCalls: [] },
|
|
712
|
+
]),
|
|
713
|
+
threadOps: ops,
|
|
714
|
+
tools: { Persist: persistTool },
|
|
715
|
+
buildContextMessage: () => "go",
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
const stateManager = createAgentStateManager({
|
|
719
|
+
initialState: { systemPrompt: "test" },
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
await session.runSession({ stateManager });
|
|
723
|
+
|
|
724
|
+
const saves = log.filter((l) => l.op === "saveThreadState");
|
|
725
|
+
expect(saves).toHaveLength(2);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("re-persists across separate turns even when handlers call persistThreadState", async () => {
|
|
729
|
+
const { ops, log } = createMockThreadOps();
|
|
730
|
+
|
|
731
|
+
const persistTool = defineTool({
|
|
732
|
+
name: "Persist" as const,
|
|
733
|
+
description: "calls persistThreadState",
|
|
734
|
+
schema: z.object({}),
|
|
735
|
+
handler: async (_args: Record<string, never>, ctx: RouterContext) => {
|
|
736
|
+
await ctx.persistThreadState?.();
|
|
737
|
+
return { toolResponse: "ok", data: null };
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const session = await createSession({
|
|
742
|
+
agentName: "TestAgent",
|
|
743
|
+
thread: { mode: "new", threadId: "thread-1" },
|
|
744
|
+
runAgent: createScriptedRunAgent([
|
|
745
|
+
{
|
|
746
|
+
message: "turn 1",
|
|
747
|
+
toolCalls: [
|
|
748
|
+
{ id: "tc-1", name: "Persist", args: {} },
|
|
749
|
+
{ id: "tc-2", name: "Persist", args: {} },
|
|
750
|
+
],
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
message: "turn 2",
|
|
754
|
+
toolCalls: [
|
|
755
|
+
{ id: "tc-3", name: "Persist", args: {} },
|
|
756
|
+
{ id: "tc-4", name: "Persist", args: {} },
|
|
757
|
+
],
|
|
758
|
+
},
|
|
759
|
+
{ message: "done", toolCalls: [] },
|
|
760
|
+
]),
|
|
761
|
+
threadOps: ops,
|
|
762
|
+
tools: { Persist: persistTool },
|
|
763
|
+
buildContextMessage: () => "go",
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
const stateManager = createAgentStateManager({
|
|
767
|
+
initialState: { systemPrompt: "test" },
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
await session.runSession({ stateManager });
|
|
771
|
+
|
|
772
|
+
const saves = log.filter((l) => l.op === "saveThreadState");
|
|
773
|
+
expect(saves).toHaveLength(3);
|
|
774
|
+
});
|
|
775
|
+
|
|
684
776
|
// --- Error propagation ---
|
|
685
777
|
|
|
686
778
|
it("propagates runAgent errors and calls onSessionEnd with failed reason", async () => {
|
|
@@ -577,6 +577,28 @@ export async function createSession<
|
|
|
577
577
|
}
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
+
// Hand handlers a way to persist the parent's slice mid-loop
|
|
581
|
+
// (subagents that fork or continue the parent's thread need
|
|
582
|
+
// this — otherwise the child loads a stale snapshot from the
|
|
583
|
+
// prior session, since `saveThreadState` would otherwise only
|
|
584
|
+
// run in the `finally` below).
|
|
585
|
+
//
|
|
586
|
+
// Memoized per-batch so a single assistant message that emits
|
|
587
|
+
// N parallel subagent calls only writes the slice once.
|
|
588
|
+
// Persisting again later in the same turn is a no-op anyway
|
|
589
|
+
// (the slice doesn't mutate between handler dispatch and the
|
|
590
|
+
// batch's last `executeChild`), and Redis/cold-store writes
|
|
591
|
+
// aren't free.
|
|
592
|
+
let persistInflight: Promise<void> | undefined;
|
|
593
|
+
const persistThreadStateOnce = (): Promise<void> => {
|
|
594
|
+
persistInflight ??= saveThreadState(
|
|
595
|
+
threadId,
|
|
596
|
+
stateManager.getPersistedSlice(),
|
|
597
|
+
threadKey
|
|
598
|
+
);
|
|
599
|
+
return persistInflight;
|
|
600
|
+
};
|
|
601
|
+
|
|
580
602
|
const toolCallResults = await toolRouter.processToolCalls(
|
|
581
603
|
parsedToolCalls,
|
|
582
604
|
{
|
|
@@ -585,17 +607,7 @@ export async function createSession<
|
|
|
585
607
|
...(assistantId !== undefined && {
|
|
586
608
|
assistantMessageId: assistantId,
|
|
587
609
|
}),
|
|
588
|
-
|
|
589
|
-
// mid-loop (subagents that fork or continue the parent's
|
|
590
|
-
// thread need this — otherwise the child loads a stale
|
|
591
|
-
// snapshot from the prior session, since `saveThreadState`
|
|
592
|
-
// would otherwise only run in the `finally` below).
|
|
593
|
-
persistThreadState: () =>
|
|
594
|
-
saveThreadState(
|
|
595
|
-
threadId,
|
|
596
|
-
stateManager.getPersistedSlice(),
|
|
597
|
-
threadKey
|
|
598
|
-
),
|
|
610
|
+
persistThreadState: persistThreadStateOnce,
|
|
599
611
|
}
|
|
600
612
|
);
|
|
601
613
|
|
|
@@ -35,29 +35,29 @@ describe("createThreadManager ↔ public key helpers round-trip", () => {
|
|
|
35
35
|
const redis = {
|
|
36
36
|
exists: vi.fn(async (k: string) => (meta.has(k) ? 1 : 0)),
|
|
37
37
|
set: vi.fn(
|
|
38
|
-
async (k: string, v: string,
|
|
38
|
+
async (k: string, v: string, options?: { EX?: number }) => {
|
|
39
39
|
meta.set(k, v);
|
|
40
|
-
writtenMetaExpires.set(k,
|
|
40
|
+
if (options?.EX !== undefined) writtenMetaExpires.set(k, options.EX);
|
|
41
41
|
return "OK";
|
|
42
42
|
}
|
|
43
43
|
),
|
|
44
|
-
del: vi.fn(async (
|
|
44
|
+
del: vi.fn(async (keys: string | string[]) => {
|
|
45
45
|
let n = 0;
|
|
46
|
-
for (const k of keys) {
|
|
46
|
+
for (const k of Array.isArray(keys) ? keys : [keys]) {
|
|
47
47
|
if (store.delete(k)) n++;
|
|
48
48
|
if (meta.delete(k)) n++;
|
|
49
49
|
}
|
|
50
50
|
return n;
|
|
51
51
|
}),
|
|
52
|
-
|
|
52
|
+
rPush: vi.fn(async (k: string, element: string | string[]) => {
|
|
53
53
|
const list = store.get(k) ?? [];
|
|
54
|
-
list.push(...
|
|
54
|
+
list.push(...(Array.isArray(element) ? element : [element]));
|
|
55
55
|
store.set(k, list);
|
|
56
56
|
return list.length;
|
|
57
57
|
}),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
lRange: vi.fn(async (k: string) => store.get(k) ?? []),
|
|
59
|
+
lLen: vi.fn(async (k: string) => (store.get(k) ?? []).length),
|
|
60
|
+
lTrim: vi.fn(async () => "OK"),
|
|
61
61
|
expire: vi.fn(async (k: string, ttl: number) => {
|
|
62
62
|
if (store.has(k)) writtenListExpires.set(k, ttl);
|
|
63
63
|
if (meta.has(k)) writtenMetaExpires.set(k, ttl);
|
package/src/lib/thread/keys.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const THREAD_TTL_SECONDS = 60 * 60 * 24 * 90;
|
|
|
38
38
|
* Build the Redis list key that holds a thread's serialized messages.
|
|
39
39
|
*
|
|
40
40
|
* Mirrors the exact key used internally by zeitlich's thread manager,
|
|
41
|
-
* so a consumer calling `redis.
|
|
41
|
+
* so a consumer calling `redis.lRange(getThreadListKey(key, id), 0, -1)`
|
|
42
42
|
* sees the same data the writer wrote.
|
|
43
43
|
*
|
|
44
44
|
* @param threadKey - Thread key (defaults to `"messages"` inside the
|