zeitlich 0.2.48 → 0.2.50
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/{activities-DCaIPQBT.d.ts → activities-IuOIvPHO.d.ts} +6 -6
- package/dist/{activities-BlQR5gX4.d.cts → activities-cIlq1y1y.d.cts} +6 -6
- 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 +45 -42
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +10 -10
- package/dist/adapters/thread/anthropic/index.d.ts +10 -10
- package/dist/adapters/thread/anthropic/index.js +45 -42
- 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 +117 -54
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +27 -23
- package/dist/adapters/thread/google-genai/index.d.ts +27 -23
- package/dist/adapters/thread/google-genai/index.js +117 -54
- 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 +45 -42
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +10 -10
- package/dist/adapters/thread/langchain/index.d.ts +10 -10
- package/dist/adapters/thread/langchain/index.js +45 -42
- 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-UL13Sstw.d.cts → cold-store-C0uvYTSi.d.cts} +1 -1
- package/dist/{cold-store-aD4TSKlU.d.ts → cold-store-CCnZYWjx.d.ts} +1 -1
- package/dist/index.cjs +15063 -405
- 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 +15064 -402
- package/dist/index.js.map +1 -1
- package/dist/{proxy-BAty3CWM.d.cts → proxy-BVznA2_p.d.cts} +1 -1
- package/dist/{proxy-mbnwBhHw.d.ts → proxy-C4J1pNUk.d.ts} +1 -1
- package/dist/{thread-manager-CICj68PI.d.ts → thread-manager-BqjzWsP7.d.ts} +4 -4
- package/dist/{thread-manager-R6c3lnJy.d.cts → thread-manager-CzIs47uG.d.cts} +4 -4
- package/dist/{thread-manager-DsXvJ5cJ.d.cts → thread-manager-Dzl1fHhV.d.cts} +4 -4
- package/dist/{thread-manager-DtEtbUkp.d.ts → thread-manager-SkSWRPRc.d.ts} +4 -4
- package/dist/{types-gVa5XCWD.d.ts → types-BQvXWcft.d.ts} +1 -1
- package/dist/{types-DF4wzWQG.d.ts → types-CbPnU4RM.d.ts} +3 -3
- package/dist/{types-CJ7tCdl6.d.cts → types-D8W5TnSa.d.cts} +3 -3
- package/dist/{types-CJ7tCdl6.d.ts → types-D8W5TnSa.d.ts} +3 -3
- package/dist/{types-DwBYd0ij.d.ts → types-DZnUqCAP.d.cts} +709 -686
- package/dist/{types-CjY93AWZ.d.cts → types-OEN1xrFg.d.cts} +1 -1
- package/dist/{types-DWeyCTYK.d.cts → types-YNesmGKV.d.ts} +709 -686
- package/dist/{types-DDLPnxBh.d.cts → types-d2RvEP6v.d.cts} +3 -3
- package/dist/{workflow-DdaU7_j4.d.ts → workflow-B3oTe2_D.d.cts} +34 -3
- package/dist/{workflow-DVNPR7eX.d.cts → workflow-Bkzg0cjB.d.ts} +34 -3
- package/dist/workflow.cjs +15021 -362
- 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 +15022 -359
- package/dist/workflow.js.map +1 -1
- package/package.json +10 -37
- package/src/adapters/thread/anthropic/activities.ts +1 -1
- package/src/adapters/thread/anthropic/fork-transform.test.ts +17 -11
- package/src/adapters/thread/anthropic/model-invoker.test.ts +4 -3
- package/src/adapters/thread/anthropic/model-invoker.ts +1 -1
- package/src/adapters/thread/anthropic/thread-manager.test.ts +2 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +1 -1
- package/src/adapters/thread/google-genai/activities.ts +1 -1
- package/src/adapters/thread/google-genai/fork-transform.test.ts +17 -11
- package/src/adapters/thread/google-genai/model-invoker.test.ts +337 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +107 -23
- package/src/adapters/thread/google-genai/thread-manager.test.ts +2 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +1 -1
- package/src/adapters/thread/langchain/activities.ts +1 -1
- package/src/adapters/thread/langchain/fork-transform.test.ts +17 -11
- package/src/adapters/thread/langchain/model-invoker.ts +1 -1
- package/src/adapters/thread/langchain/thread-manager.test.ts +2 -2
- package/src/adapters/thread/langchain/thread-manager.ts +1 -1
- 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 -0
- package/src/lib/subagent/handler.ts +23 -0
- package/src/lib/subagent/subagent.integration.test.ts +198 -0
- 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 +149 -33
- package/src/lib/tool-router/types.ts +23 -0
- 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/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
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type Redis from "
|
|
1
|
+
import type { RedisClientType as Redis } from "redis";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import type {
|
|
3
4
|
GoogleGenAI,
|
|
4
5
|
Content,
|
|
5
6
|
FunctionDeclaration,
|
|
7
|
+
GenerateContentConfig,
|
|
6
8
|
Part,
|
|
7
9
|
GenerateContentResponse,
|
|
8
10
|
} from "@google/genai";
|
|
@@ -19,6 +21,18 @@ export interface GoogleGenAIModelInvokerConfig {
|
|
|
19
21
|
client: GoogleGenAI;
|
|
20
22
|
model: string;
|
|
21
23
|
hooks?: GoogleGenAIThreadManagerHooks;
|
|
24
|
+
/** Passed through to `generateContentStream().config`.
|
|
25
|
+
* `systemInstruction`, `tools`, and `abortSignal` are managed by the
|
|
26
|
+
* invoker and will override any values set here. */
|
|
27
|
+
config?: GenerateContentConfig;
|
|
28
|
+
/** Caches the first `splitIndex` messages server-side (with
|
|
29
|
+
* `systemInstruction`, `tools`, and `toolConfig`). Skipped when
|
|
30
|
+
* `contents.length <= splitIndex`. */
|
|
31
|
+
cache?: {
|
|
32
|
+
splitIndex: number;
|
|
33
|
+
/** Default: 300. */
|
|
34
|
+
ttlSeconds?: number;
|
|
35
|
+
};
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
function toFunctionDeclarations(
|
|
@@ -32,12 +46,7 @@ function toFunctionDeclarations(
|
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/**
|
|
35
|
-
*
|
|
36
|
-
* `ModelInvoker<Content>` contract.
|
|
37
|
-
*
|
|
38
|
-
* Internally streams the response and emits Temporal heartbeats on each
|
|
39
|
-
* chunk so that long-running LLM calls remain visible to the scheduler.
|
|
40
|
-
* The caller is responsible for appending the response to the thread.
|
|
49
|
+
* The caller is responsible for appending the returned response to the thread.
|
|
41
50
|
*
|
|
42
51
|
* @example
|
|
43
52
|
* ```typescript
|
|
@@ -60,6 +69,8 @@ export function createGoogleGenAIModelInvoker({
|
|
|
60
69
|
client,
|
|
61
70
|
model,
|
|
62
71
|
hooks,
|
|
72
|
+
config: generationConfig,
|
|
73
|
+
cache: cacheConfig,
|
|
63
74
|
}: GoogleGenAIModelInvokerConfig) {
|
|
64
75
|
return async function invokeGoogleGenAIModel(
|
|
65
76
|
config: ModelInvokerConfig
|
|
@@ -78,19 +89,77 @@ export function createGoogleGenAIModelInvoker({
|
|
|
78
89
|
// retry / Temporal reset it wipes the prior attempt's assistant
|
|
79
90
|
// + tool results so the LLM sees the original pre-call state.
|
|
80
91
|
await thread.truncateFromId(assistantMessageId);
|
|
81
|
-
const { contents, systemInstruction } =
|
|
82
|
-
await thread.prepareForInvocation();
|
|
92
|
+
const { contents, systemInstruction } = await thread.prepareForInvocation();
|
|
83
93
|
|
|
84
94
|
const functionDeclarations = toFunctionDeclarations(state.tools);
|
|
85
95
|
const tools =
|
|
86
96
|
functionDeclarations.length > 0 ? [{ functionDeclarations }] : undefined;
|
|
87
97
|
|
|
98
|
+
const {
|
|
99
|
+
systemInstruction: _si,
|
|
100
|
+
tools: _t,
|
|
101
|
+
abortSignal: _as,
|
|
102
|
+
cachedContent: callerCachedContent,
|
|
103
|
+
toolConfig: callerToolConfig,
|
|
104
|
+
...callerConfig
|
|
105
|
+
} = generationConfig ?? {};
|
|
106
|
+
|
|
107
|
+
let liveContents = contents;
|
|
108
|
+
let cachedContentName: string | undefined;
|
|
109
|
+
let cachedWriteTokens: number | undefined;
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
cacheConfig &&
|
|
113
|
+
cacheConfig.splitIndex > 0 &&
|
|
114
|
+
contents.length > cacheConfig.splitIndex
|
|
115
|
+
) {
|
|
116
|
+
liveContents = contents.slice(cacheConfig.splitIndex);
|
|
117
|
+
const ttl = cacheConfig.ttlSeconds ?? 300;
|
|
118
|
+
const cacheRedisKey = `${threadKey ?? "messages"}:gemini-cache:${model}:${cacheConfig.splitIndex}:thread:${threadId}`;
|
|
119
|
+
|
|
120
|
+
cachedContentName = (await redis.get(cacheRedisKey)) ?? undefined;
|
|
121
|
+
|
|
122
|
+
if (!cachedContentName) {
|
|
123
|
+
const cacheInstance = await client.caches.create({
|
|
124
|
+
model,
|
|
125
|
+
config: {
|
|
126
|
+
contents: contents.slice(0, cacheConfig.splitIndex),
|
|
127
|
+
...(systemInstruction ? { systemInstruction } : {}),
|
|
128
|
+
...(tools ? { tools } : {}),
|
|
129
|
+
...(callerToolConfig ? { toolConfig: callerToolConfig } : {}),
|
|
130
|
+
ttl: `${ttl}s`,
|
|
131
|
+
abortSignal: signal,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
if (!cacheInstance?.name) {
|
|
135
|
+
throw new Error("Gemini cache creation did not return a cache name");
|
|
136
|
+
}
|
|
137
|
+
cachedContentName = cacheInstance.name;
|
|
138
|
+
cachedWriteTokens =
|
|
139
|
+
cacheInstance.usageMetadata?.totalTokenCount ?? undefined;
|
|
140
|
+
const redisTtl = ttl - 5;
|
|
141
|
+
if (redisTtl > 0) {
|
|
142
|
+
await redis.set(cacheRedisKey, cachedContentName, { EX: redisTtl });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
88
147
|
const stream = await client.models.generateContentStream({
|
|
89
148
|
model,
|
|
90
|
-
contents,
|
|
149
|
+
contents: liveContents,
|
|
91
150
|
config: {
|
|
92
|
-
...
|
|
93
|
-
...(
|
|
151
|
+
...callerConfig,
|
|
152
|
+
...(cachedContentName
|
|
153
|
+
? { cachedContent: cachedContentName }
|
|
154
|
+
: {
|
|
155
|
+
...(callerCachedContent
|
|
156
|
+
? { cachedContent: callerCachedContent }
|
|
157
|
+
: {
|
|
158
|
+
...(systemInstruction ? { systemInstruction } : {}),
|
|
159
|
+
...(tools ? { tools } : {}),
|
|
160
|
+
}),
|
|
161
|
+
...(callerToolConfig ? { toolConfig: callerToolConfig } : {}),
|
|
162
|
+
}),
|
|
94
163
|
abortSignal: signal,
|
|
95
164
|
},
|
|
96
165
|
});
|
|
@@ -107,48 +176,63 @@ export function createGoogleGenAIModelInvoker({
|
|
|
107
176
|
throw new Error("Google GenAI stream ended without producing any chunks");
|
|
108
177
|
}
|
|
109
178
|
|
|
179
|
+
for (const part of allParts) {
|
|
180
|
+
if (part.functionCall && !part.functionCall.id) {
|
|
181
|
+
part.functionCall.id = randomBytes(8).toString("hex");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
110
185
|
const modelContent: Content = { role: "model", parts: allParts };
|
|
111
|
-
const functionCalls = lastChunk.functionCalls ?? [];
|
|
112
186
|
|
|
113
187
|
return {
|
|
114
188
|
message: modelContent,
|
|
115
|
-
rawToolCalls:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
189
|
+
rawToolCalls: allParts
|
|
190
|
+
.filter(
|
|
191
|
+
(
|
|
192
|
+
p
|
|
193
|
+
): p is Part & { functionCall: NonNullable<Part["functionCall"]> } =>
|
|
194
|
+
!!p.functionCall
|
|
195
|
+
)
|
|
196
|
+
.map((p) => ({
|
|
197
|
+
id: p.functionCall.id,
|
|
198
|
+
name: p.functionCall.name ?? "",
|
|
199
|
+
args: p.functionCall.args ?? {},
|
|
200
|
+
})),
|
|
120
201
|
usage: {
|
|
121
202
|
inputTokens: lastChunk.usageMetadata?.promptTokenCount,
|
|
122
203
|
outputTokens: lastChunk.usageMetadata?.candidatesTokenCount,
|
|
204
|
+
cachedWriteTokens,
|
|
123
205
|
cachedReadTokens: lastChunk.usageMetadata?.cachedContentTokenCount,
|
|
206
|
+
reasonTokens: lastChunk.usageMetadata?.thoughtsTokenCount,
|
|
124
207
|
},
|
|
125
208
|
};
|
|
126
209
|
};
|
|
127
210
|
}
|
|
128
211
|
|
|
129
|
-
/**
|
|
130
|
-
* Standalone function for one-shot Google GenAI model invocation.
|
|
131
|
-
* Convenience wrapper around createGoogleGenAIModelInvoker for cases
|
|
132
|
-
* where you don't need to reuse the invoker.
|
|
133
|
-
*/
|
|
134
212
|
export async function invokeGoogleGenAIModel({
|
|
135
213
|
redis,
|
|
136
214
|
client,
|
|
137
215
|
model,
|
|
138
216
|
hooks,
|
|
139
217
|
config,
|
|
218
|
+
generationConfig,
|
|
219
|
+
cache,
|
|
140
220
|
}: {
|
|
141
221
|
redis: Redis;
|
|
142
222
|
client: GoogleGenAI;
|
|
143
223
|
model: string;
|
|
144
224
|
hooks?: GoogleGenAIThreadManagerHooks;
|
|
145
225
|
config: ModelInvokerConfig;
|
|
226
|
+
generationConfig?: GenerateContentConfig;
|
|
227
|
+
cache?: GoogleGenAIModelInvokerConfig["cache"];
|
|
146
228
|
}): Promise<AgentResponse<Content>> {
|
|
147
229
|
const invoker = createGoogleGenAIModelInvoker({
|
|
148
230
|
redis,
|
|
149
231
|
client,
|
|
150
232
|
model,
|
|
151
233
|
hooks,
|
|
234
|
+
config: generationConfig,
|
|
235
|
+
cache,
|
|
152
236
|
});
|
|
153
237
|
return invoker(config);
|
|
154
238
|
}
|
|
@@ -6,10 +6,10 @@ import { createGoogleGenAIThreadManager } from "./thread-manager";
|
|
|
6
6
|
function createMockRedis(stored: StoredContent[]) {
|
|
7
7
|
return {
|
|
8
8
|
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
-
|
|
9
|
+
lRange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
10
|
del: vi.fn().mockResolvedValue(1),
|
|
11
11
|
set: vi.fn().mockResolvedValue("OK"),
|
|
12
|
-
|
|
12
|
+
rPush: vi.fn().mockResolvedValue(1),
|
|
13
13
|
expire: vi.fn().mockResolvedValue(1),
|
|
14
14
|
eval: vi.fn().mockResolvedValue(1),
|
|
15
15
|
};
|
|
@@ -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";
|
|
@@ -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) ?? [];
|
|
@@ -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";
|
|
@@ -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
|
};
|
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,6 +607,7 @@ export async function createSession<
|
|
|
585
607
|
...(assistantId !== undefined && {
|
|
586
608
|
assistantMessageId: assistantId,
|
|
587
609
|
}),
|
|
610
|
+
persistThreadState: persistThreadStateOnce,
|
|
588
611
|
}
|
|
589
612
|
);
|
|
590
613
|
|
|
@@ -439,6 +439,29 @@ export function createSubagentHandler<
|
|
|
439
439
|
snapshotBaseCreatorAgent.set(childWorkflowId, config.agentName);
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
// The parent's `PersistedThreadState` slice (`tasks` + custom
|
|
443
|
+
// state) only lands in storage in the session's `finally`. When
|
|
444
|
+
// the child reads from the parent's thread (`from-parent`
|
|
445
|
+
// fallback or an explicit args.threadId pointing at the parent),
|
|
446
|
+
// that `loadThreadState` would otherwise see the prior session's
|
|
447
|
+
// snapshot. Flush the live slice now via the session-supplied
|
|
448
|
+
// callback so the child sees the parent's current state.
|
|
449
|
+
if (
|
|
450
|
+
continuationThreadId &&
|
|
451
|
+
continuationThreadId === context.threadId &&
|
|
452
|
+
context.persistThreadState
|
|
453
|
+
) {
|
|
454
|
+
try {
|
|
455
|
+
await context.persistThreadState();
|
|
456
|
+
} catch (err) {
|
|
457
|
+
log.warn("failed to persist parent thread state for subagent", {
|
|
458
|
+
subagent: config.agentName,
|
|
459
|
+
childWorkflowId,
|
|
460
|
+
error: err instanceof Error ? err.message : String(err),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
442
465
|
log.info("subagent spawned", {
|
|
443
466
|
subagent: config.agentName,
|
|
444
467
|
childWorkflowId,
|