zeitlich 0.2.44 → 0.2.46
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 +78 -10
- package/dist/{activities-CPIB2v2C.d.ts → activities-Bm4TLTid.d.ts} +24 -4
- package/dist/{activities-DnmNOnq4.d.cts → activities-CyeiqK_f.d.cts} +24 -4
- package/dist/adapters/sandbox/daytona/index.d.cts +2 -2
- package/dist/adapters/sandbox/daytona/index.d.ts +2 -2
- 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.d.cts +1 -1
- package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
- package/dist/adapters/thread/anthropic/index.cjs +171 -65
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +19 -4
- package/dist/adapters/thread/anthropic/index.d.ts +19 -4
- package/dist/adapters/thread/anthropic/index.js +171 -65
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +3 -1
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +4 -4
- package/dist/adapters/thread/anthropic/workflow.d.ts +4 -4
- package/dist/adapters/thread/anthropic/workflow.js +3 -1
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +171 -69
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +5 -4
- package/dist/adapters/thread/google-genai/index.d.ts +5 -4
- package/dist/adapters/thread/google-genai/index.js +171 -69
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +3 -1
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +5 -4
- package/dist/adapters/thread/google-genai/workflow.d.ts +5 -4
- package/dist/adapters/thread/google-genai/workflow.js +3 -1
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/langchain/index.cjs +181 -77
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +18 -4
- package/dist/adapters/thread/langchain/index.d.ts +18 -4
- package/dist/adapters/thread/langchain/index.js +182 -74
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +3 -1
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +4 -4
- package/dist/adapters/thread/langchain/workflow.d.ts +4 -4
- package/dist/adapters/thread/langchain/workflow.js +3 -1
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/cold-store-BC5L5Z8A.d.cts +117 -0
- package/dist/cold-store-CFHwemBJ.d.ts +117 -0
- package/dist/index.cjs +252 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -8
- package/dist/index.d.ts +138 -8
- package/dist/index.js +247 -54
- package/dist/index.js.map +1 -1
- package/dist/{proxy-DTnc5rqT.d.cts → proxy-BxFyd6cg.d.cts} +1 -1
- package/dist/{proxy-B7Xi1znZ.d.ts → proxy-Cskmj4Yx.d.ts} +1 -1
- package/dist/{thread-manager-BlX2TwRN.d.cts → thread-manager-9tezUcLW.d.cts} +9 -3
- package/dist/{thread-manager-BAv340mi.d.ts → thread-manager-B-zy3xrs.d.ts} +9 -3
- package/dist/{thread-manager-D2xorI-J.d.ts → thread-manager-D33SUmZa.d.cts} +10 -4
- package/dist/{thread-manager-BWv6ZXI3.d.cts → thread-manager-DduoSkvJ.d.ts} +10 -4
- package/dist/{types-C90VoEpt.d.cts → types-CjY93AWZ.d.cts} +1 -1
- package/dist/{types-4Wmk-wRq.d.cts → types-CnuN9T6t.d.cts} +23 -1
- package/dist/{types-DKsCdAtQ.d.ts → types-CwN6_tAL.d.ts} +23 -1
- package/dist/{types-Clhqautb.d.ts → types-L5bvbF-n.d.ts} +17 -1
- package/dist/{types-DpFD8ofR.d.ts → types-gVa5XCWD.d.ts} +1 -1
- package/dist/{types-DRJt1TMi.d.cts → types-oxt8GN97.d.cts} +17 -1
- package/dist/{workflow-D32TRMr-.d.ts → workflow-B1TOcHbt.d.ts} +33 -2
- package/dist/{workflow-XVt0ww8K.d.cts → workflow-DIaIV7L2.d.cts} +33 -2
- package/dist/workflow.cjs +29 -19
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +2 -2
- package/dist/workflow.d.ts +2 -2
- package/dist/workflow.js +29 -19
- package/dist/workflow.js.map +1 -1
- package/package.json +6 -1
- package/src/adapters/thread/anthropic/activities.ts +72 -36
- package/src/adapters/thread/anthropic/thread-manager.ts +9 -1
- package/src/adapters/thread/google-genai/activities.ts +64 -40
- package/src/adapters/thread/google-genai/thread-manager.ts +9 -1
- package/src/adapters/thread/langchain/activities.ts +63 -36
- package/src/adapters/thread/langchain/thread-manager.ts +9 -1
- package/src/index.ts +20 -1
- package/src/lib/session/session-edge-cases.integration.test.ts +12 -0
- package/src/lib/session/session.integration.test.ts +138 -0
- package/src/lib/session/session.ts +47 -22
- package/src/lib/session/types.ts +22 -0
- package/src/lib/thread/cold-store.test.ts +193 -0
- package/src/lib/thread/cold-store.ts +250 -0
- package/src/lib/thread/index.ts +32 -0
- package/src/lib/thread/keys.ts +20 -0
- package/src/lib/thread/manager.ts +16 -27
- package/src/lib/thread/proxy.ts +2 -0
- package/src/lib/thread/snapshot.test.ts +443 -0
- package/src/lib/thread/snapshot.ts +163 -0
- package/src/lib/thread/test-utils.ts +228 -0
- package/src/lib/thread/tiered.test.ts +281 -0
- package/src/lib/thread/tiered.ts +135 -0
- package/src/lib/thread/types.ts +16 -0
- package/src/lib/.env +0 -1
- package/src/tools/bash/.env +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { P as PersistedThreadState } from './types-CwN6_tAL.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pluggable cold-tier interface for thread archives.
|
|
5
|
+
*
|
|
6
|
+
* Zeitlich's thread manager is a Redis-backed hot tier optimized for
|
|
7
|
+
* the duration of a workflow run. A `ColdThreadStore` provides the
|
|
8
|
+
* durable archive: each thread is serialized to a single
|
|
9
|
+
* {@link ThreadSnapshot} blob (messages + persisted state slice +
|
|
10
|
+
* dedup-id ledger) at session-exit time, and restored at session-entry
|
|
11
|
+
* time when the workflow is resumed or forked.
|
|
12
|
+
*
|
|
13
|
+
* The contract is intentionally minimal — one read, one write, one
|
|
14
|
+
* delete keyed by `(threadKey, threadId)`. Any storage backend that
|
|
15
|
+
* can satisfy these three calls (S3, R2, GCS, Postgres, the local
|
|
16
|
+
* filesystem, etc.) can plug into the same tiered manager.
|
|
17
|
+
*
|
|
18
|
+
* Concurrency assumption: zeitlich assumes a single active session
|
|
19
|
+
* per `(threadKey, threadId)` at a time, so cold writes are
|
|
20
|
+
* last-writer-wins; no compare-and-swap is required.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serialized form of a thread that can be written to and read from a
|
|
25
|
+
* {@link ColdThreadStore}.
|
|
26
|
+
*
|
|
27
|
+
* `messages` is the list of raw-serialized message strings exactly as
|
|
28
|
+
* they were stored in the Redis list — keeping the cold tier opaque
|
|
29
|
+
* to the adapter-specific message envelope. `state` is the
|
|
30
|
+
* {@link PersistedThreadState} that the session writes via
|
|
31
|
+
* `saveThreadState` on every exit path, or `null` if none has been
|
|
32
|
+
* written yet. `dedupIds` lets the tiered manager re-prime the
|
|
33
|
+
* idempotent-append dedup markers when restoring, so a rewind retry
|
|
34
|
+
* after a continue cannot accidentally re-append a message.
|
|
35
|
+
*/
|
|
36
|
+
interface ThreadSnapshot {
|
|
37
|
+
v: 1;
|
|
38
|
+
messages: string[];
|
|
39
|
+
state: PersistedThreadState | null;
|
|
40
|
+
dedupIds: string[];
|
|
41
|
+
}
|
|
42
|
+
/** Pluggable cold archive for thread snapshots. */
|
|
43
|
+
interface ColdThreadStore {
|
|
44
|
+
/**
|
|
45
|
+
* Read the latest snapshot for `(threadKey, threadId)`, or return
|
|
46
|
+
* `null` if no snapshot has ever been written.
|
|
47
|
+
*/
|
|
48
|
+
read(threadKey: string, threadId: string): Promise<ThreadSnapshot | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Persist `snapshot` as the latest archive for `(threadKey,
|
|
51
|
+
* threadId)`. Overwrites any prior snapshot in place.
|
|
52
|
+
*/
|
|
53
|
+
write(threadKey: string, threadId: string, snapshot: ThreadSnapshot): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Permanently remove the archive for `(threadKey, threadId)`.
|
|
56
|
+
* No-op if no snapshot exists.
|
|
57
|
+
*/
|
|
58
|
+
delete(threadKey: string, threadId: string): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Compact, duck-typed shape of an S3 client. Zeitlich only needs the
|
|
62
|
+
* `send(...)` method; declaring this locally avoids forcing
|
|
63
|
+
* `@aws-sdk/client-s3` to be installed when the consumer is using a
|
|
64
|
+
* different cold-store backend.
|
|
65
|
+
*/
|
|
66
|
+
interface S3LikeClient {
|
|
67
|
+
send<TInput, TOutput>(command: {
|
|
68
|
+
input: TInput;
|
|
69
|
+
} & object): Promise<TOutput>;
|
|
70
|
+
}
|
|
71
|
+
/** Configuration for the built-in S3 cold store. */
|
|
72
|
+
interface S3ColdStoreConfig {
|
|
73
|
+
/** An `@aws-sdk/client-s3` `S3Client` (or duck-typed equivalent). */
|
|
74
|
+
s3: S3LikeClient;
|
|
75
|
+
/** S3 bucket that holds the archive. */
|
|
76
|
+
bucket: string;
|
|
77
|
+
/**
|
|
78
|
+
* Optional key prefix applied to every object. The final key layout
|
|
79
|
+
* is `${prefix}/${threadKey}/${threadId}.json[.gz]` with leading
|
|
80
|
+
* slashes stripped. Defaults to `"threads"`.
|
|
81
|
+
*/
|
|
82
|
+
prefix?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Gzip the JSON payload before uploading and assume gzip on read.
|
|
85
|
+
* Defaults to `true` — message lists are highly compressible.
|
|
86
|
+
*/
|
|
87
|
+
gzip?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Optional `Content-Type` override. Defaults to
|
|
90
|
+
* `application/json` (or `application/gzip` when `gzip: true`).
|
|
91
|
+
*/
|
|
92
|
+
contentType?: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Build an S3-backed {@link ColdThreadStore}.
|
|
96
|
+
*
|
|
97
|
+
* One object per thread at
|
|
98
|
+
* `${prefix}/${threadKey}/${threadId}.json[.gz]`, JSON-encoded and
|
|
99
|
+
* gzip-compressed by default. The consumer owns the `S3Client`
|
|
100
|
+
* instance, so credentials, region, and endpoint configuration live
|
|
101
|
+
* outside zeitlich.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* import { S3Client } from "@aws-sdk/client-s3";
|
|
106
|
+
* import { createS3ColdStore } from "zeitlich";
|
|
107
|
+
*
|
|
108
|
+
* const coldStore = createS3ColdStore({
|
|
109
|
+
* s3: new S3Client({ region: "us-east-1" }),
|
|
110
|
+
* bucket: "my-threads",
|
|
111
|
+
* prefix: "prod/threads",
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
declare function createS3ColdStore(config: S3ColdStoreConfig): ColdThreadStore;
|
|
116
|
+
|
|
117
|
+
export { type ColdThreadStore as C, type S3ColdStoreConfig as S, type ThreadSnapshot as T, type S3LikeClient as a, createS3ColdStore as c };
|
package/dist/index.cjs
CHANGED
|
@@ -5,13 +5,14 @@ var z14 = require('zod');
|
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var common = require('@temporalio/common');
|
|
7
7
|
var path = require('path');
|
|
8
|
+
var zlib = require('zlib');
|
|
9
|
+
var clientS3 = require('@aws-sdk/client-s3');
|
|
8
10
|
var activity = require('@temporalio/activity');
|
|
9
11
|
var fs = require('fs');
|
|
10
12
|
|
|
11
13
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
14
|
|
|
13
15
|
var z14__default = /*#__PURE__*/_interopDefault(z14);
|
|
14
|
-
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
15
16
|
|
|
16
17
|
// src/lib/session/session.ts
|
|
17
18
|
|
|
@@ -1039,7 +1040,9 @@ async function createSession(config) {
|
|
|
1039
1040
|
appendAgentMessage,
|
|
1040
1041
|
forkThread,
|
|
1041
1042
|
loadThreadState,
|
|
1042
|
-
saveThreadState
|
|
1043
|
+
saveThreadState,
|
|
1044
|
+
hydrateThread,
|
|
1045
|
+
flushThread
|
|
1043
1046
|
} = threadOps;
|
|
1044
1047
|
const plugins = [];
|
|
1045
1048
|
let destroySubagentSandboxes;
|
|
@@ -1099,10 +1102,7 @@ async function createSession(config) {
|
|
|
1099
1102
|
stateManager.run();
|
|
1100
1103
|
}
|
|
1101
1104
|
);
|
|
1102
|
-
const lifecycle = resolveSessionLifecycle(
|
|
1103
|
-
sandboxInit,
|
|
1104
|
-
sandboxShutdown
|
|
1105
|
-
);
|
|
1105
|
+
const lifecycle = resolveSessionLifecycle(sandboxInit, sandboxShutdown);
|
|
1106
1106
|
const sandboxMode = lifecycle.mode;
|
|
1107
1107
|
const resolvedShutdown = lifecycle.shutdown;
|
|
1108
1108
|
let sandboxId;
|
|
@@ -1177,20 +1177,6 @@ async function createSession(config) {
|
|
|
1177
1177
|
...baseSnapshot && { baseSnapshot }
|
|
1178
1178
|
});
|
|
1179
1179
|
}
|
|
1180
|
-
if (hooks.onSessionStart) {
|
|
1181
|
-
await hooks.onSessionStart({
|
|
1182
|
-
threadId,
|
|
1183
|
-
agentName,
|
|
1184
|
-
metadata
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
workflow.log.info("session started", {
|
|
1188
|
-
agentName,
|
|
1189
|
-
threadId,
|
|
1190
|
-
threadMode,
|
|
1191
|
-
maxTurns,
|
|
1192
|
-
...sandboxId && { sandboxId }
|
|
1193
|
-
});
|
|
1194
1180
|
const sessionStartMs = Date.now();
|
|
1195
1181
|
const systemPrompt = stateManager.getSystemPrompt();
|
|
1196
1182
|
const rehydrateFromSlice = (slice) => {
|
|
@@ -1200,10 +1186,12 @@ async function createSession(config) {
|
|
|
1200
1186
|
});
|
|
1201
1187
|
};
|
|
1202
1188
|
if (threadMode === "fork" && sourceThreadId) {
|
|
1189
|
+
await hydrateThread(sourceThreadId, threadKey);
|
|
1203
1190
|
await forkThread(sourceThreadId, threadId, threadKey);
|
|
1204
1191
|
const forkedSlice = await loadThreadState(threadId, threadKey);
|
|
1205
1192
|
if (forkedSlice) rehydrateFromSlice(forkedSlice);
|
|
1206
1193
|
} else if (threadMode === "continue") {
|
|
1194
|
+
await hydrateThread(threadId, threadKey);
|
|
1207
1195
|
const continuedSlice = await loadThreadState(threadId, threadKey);
|
|
1208
1196
|
if (continuedSlice) rehydrateFromSlice(continuedSlice);
|
|
1209
1197
|
} else {
|
|
@@ -1263,6 +1251,20 @@ async function createSession(config) {
|
|
|
1263
1251
|
);
|
|
1264
1252
|
let exitReason = "completed";
|
|
1265
1253
|
let finalMessage = null;
|
|
1254
|
+
if (hooks.onSessionStart) {
|
|
1255
|
+
await hooks.onSessionStart({
|
|
1256
|
+
threadId,
|
|
1257
|
+
agentName,
|
|
1258
|
+
metadata
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
workflow.log.info("session started", {
|
|
1262
|
+
agentName,
|
|
1263
|
+
threadId,
|
|
1264
|
+
threadMode,
|
|
1265
|
+
maxTurns,
|
|
1266
|
+
...sandboxId && { sandboxId }
|
|
1267
|
+
});
|
|
1266
1268
|
try {
|
|
1267
1269
|
let assistantId;
|
|
1268
1270
|
while (stateManager.isRunning() && !stateManager.isTerminal() && stateManager.getTurns() < maxTurns) {
|
|
@@ -1371,6 +1373,15 @@ async function createSession(config) {
|
|
|
1371
1373
|
error: persistError instanceof Error ? persistError.message : String(persistError)
|
|
1372
1374
|
});
|
|
1373
1375
|
}
|
|
1376
|
+
try {
|
|
1377
|
+
await flushThread(threadId, threadKey);
|
|
1378
|
+
} catch (flushError) {
|
|
1379
|
+
workflow.log.warn("failed to flush thread to cold tier", {
|
|
1380
|
+
agentName,
|
|
1381
|
+
threadId,
|
|
1382
|
+
error: flushError instanceof Error ? flushError.message : String(flushError)
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1374
1385
|
await callSessionEnd(exitReason, stateManager.getTurns());
|
|
1375
1386
|
if (sandboxOwned && sandboxId && sandboxOps) {
|
|
1376
1387
|
switch (resolvedShutdown) {
|
|
@@ -1454,6 +1465,9 @@ function getThreadMetaKey(threadKey, threadId) {
|
|
|
1454
1465
|
function getThreadStateKey(threadKey, threadId) {
|
|
1455
1466
|
return `${threadKey}:state:thread:${threadId}`;
|
|
1456
1467
|
}
|
|
1468
|
+
function getThreadDedupKey(threadId, dedupId) {
|
|
1469
|
+
return `dedup:${dedupId}:thread:${threadId}`;
|
|
1470
|
+
}
|
|
1457
1471
|
|
|
1458
1472
|
// src/lib/types.ts
|
|
1459
1473
|
function isTerminalStatus(status) {
|
|
@@ -1660,7 +1674,7 @@ function createAgentStateManager({
|
|
|
1660
1674
|
};
|
|
1661
1675
|
}
|
|
1662
1676
|
|
|
1663
|
-
//
|
|
1677
|
+
// node_modules/uuid/dist/esm/stringify.js
|
|
1664
1678
|
var byteToHex = [];
|
|
1665
1679
|
for (let i = 0; i < 256; ++i) {
|
|
1666
1680
|
byteToHex.push((i + 256).toString(16).slice(1));
|
|
@@ -1672,26 +1686,30 @@ var rnds8Pool = new Uint8Array(256);
|
|
|
1672
1686
|
var poolPtr = rnds8Pool.length;
|
|
1673
1687
|
function rng() {
|
|
1674
1688
|
if (poolPtr > rnds8Pool.length - 16) {
|
|
1675
|
-
|
|
1689
|
+
crypto.randomFillSync(rnds8Pool);
|
|
1676
1690
|
poolPtr = 0;
|
|
1677
1691
|
}
|
|
1678
1692
|
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
1679
1693
|
}
|
|
1680
|
-
var native_default = {
|
|
1681
|
-
randomUUID: crypto__default.default.randomUUID
|
|
1682
|
-
};
|
|
1694
|
+
var native_default = { randomUUID: crypto.randomUUID };
|
|
1683
1695
|
|
|
1684
|
-
//
|
|
1696
|
+
// node_modules/uuid/dist/esm/v4.js
|
|
1685
1697
|
function v4(options, buf, offset) {
|
|
1686
1698
|
if (native_default.randomUUID && !buf && !options) {
|
|
1687
1699
|
return native_default.randomUUID();
|
|
1688
1700
|
}
|
|
1689
1701
|
options = options || {};
|
|
1690
|
-
const rnds = options.random
|
|
1702
|
+
const rnds = options.random ?? options.rng?.() ?? rng();
|
|
1703
|
+
if (rnds.length < 16) {
|
|
1704
|
+
throw new Error("Random bytes length must be >= 16");
|
|
1705
|
+
}
|
|
1691
1706
|
rnds[6] = rnds[6] & 15 | 64;
|
|
1692
1707
|
rnds[8] = rnds[8] & 63 | 128;
|
|
1693
1708
|
if (buf) {
|
|
1694
1709
|
offset = offset || 0;
|
|
1710
|
+
if (offset < 0 || offset + 16 > buf.length) {
|
|
1711
|
+
throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
1712
|
+
}
|
|
1695
1713
|
for (let i = 0; i < 16; ++i) {
|
|
1696
1714
|
buf[offset + i] = rnds[i];
|
|
1697
1715
|
}
|
|
@@ -2594,9 +2612,6 @@ redis.call('EXPIRE', KEYS[2], tonumber(ARGV[1]))
|
|
|
2594
2612
|
redis.call('SET', KEYS[1], '1', 'EX', tonumber(ARGV[1]))
|
|
2595
2613
|
return 1
|
|
2596
2614
|
`;
|
|
2597
|
-
function getDedupKey(threadId, id) {
|
|
2598
|
-
return `dedup:${id}:thread:${threadId}`;
|
|
2599
|
-
}
|
|
2600
2615
|
function createThreadManager(config) {
|
|
2601
2616
|
const {
|
|
2602
2617
|
redis,
|
|
@@ -2604,11 +2619,13 @@ function createThreadManager(config) {
|
|
|
2604
2619
|
key = "messages",
|
|
2605
2620
|
serialize = (m) => JSON.stringify(m),
|
|
2606
2621
|
deserialize = (raw) => JSON.parse(raw),
|
|
2607
|
-
idOf
|
|
2622
|
+
idOf,
|
|
2623
|
+
ttlSeconds = THREAD_TTL_SECONDS
|
|
2608
2624
|
} = config;
|
|
2609
2625
|
const redisKey = getThreadListKey(key, threadId);
|
|
2610
2626
|
const metaKey = getThreadMetaKey(key, threadId);
|
|
2611
2627
|
const stateKey = getThreadStateKey(key, threadId);
|
|
2628
|
+
const dedupKey = (id) => getThreadDedupKey(threadId, id);
|
|
2612
2629
|
async function assertThreadExists() {
|
|
2613
2630
|
const exists = await redis.exists(metaKey);
|
|
2614
2631
|
if (!exists) {
|
|
@@ -2618,7 +2635,7 @@ function createThreadManager(config) {
|
|
|
2618
2635
|
return {
|
|
2619
2636
|
async initialize() {
|
|
2620
2637
|
await redis.del(redisKey);
|
|
2621
|
-
await redis.set(metaKey, "1", "EX",
|
|
2638
|
+
await redis.set(metaKey, "1", "EX", ttlSeconds);
|
|
2622
2639
|
},
|
|
2623
2640
|
async load() {
|
|
2624
2641
|
await assertThreadExists();
|
|
@@ -2630,18 +2647,17 @@ function createThreadManager(config) {
|
|
|
2630
2647
|
await assertThreadExists();
|
|
2631
2648
|
if (idOf) {
|
|
2632
2649
|
const dedupId = messages.map(idOf).join(":");
|
|
2633
|
-
const dedupKey = getDedupKey(threadId, dedupId);
|
|
2634
2650
|
await redis.eval(
|
|
2635
2651
|
APPEND_IDEMPOTENT_SCRIPT,
|
|
2636
2652
|
2,
|
|
2637
|
-
dedupKey,
|
|
2653
|
+
dedupKey(dedupId),
|
|
2638
2654
|
redisKey,
|
|
2639
|
-
String(
|
|
2655
|
+
String(ttlSeconds),
|
|
2640
2656
|
...messages.map(serialize)
|
|
2641
2657
|
);
|
|
2642
2658
|
} else {
|
|
2643
2659
|
await redis.rpush(redisKey, ...messages.map(serialize));
|
|
2644
|
-
await redis.expire(redisKey,
|
|
2660
|
+
await redis.expire(redisKey, ttlSeconds);
|
|
2645
2661
|
}
|
|
2646
2662
|
},
|
|
2647
2663
|
async fork(newThreadId) {
|
|
@@ -2656,11 +2672,11 @@ function createThreadManager(config) {
|
|
|
2656
2672
|
if (data.length > 0) {
|
|
2657
2673
|
const newKey = getThreadListKey(key, newThreadId);
|
|
2658
2674
|
await redis.rpush(newKey, ...data);
|
|
2659
|
-
await redis.expire(newKey,
|
|
2675
|
+
await redis.expire(newKey, ttlSeconds);
|
|
2660
2676
|
}
|
|
2661
2677
|
if (stateRaw != null) {
|
|
2662
2678
|
const newStateKey = getThreadStateKey(key, newThreadId);
|
|
2663
|
-
await redis.set(newStateKey, stateRaw, "EX",
|
|
2679
|
+
await redis.set(newStateKey, stateRaw, "EX", ttlSeconds);
|
|
2664
2680
|
}
|
|
2665
2681
|
return forked;
|
|
2666
2682
|
},
|
|
@@ -2675,15 +2691,13 @@ function createThreadManager(config) {
|
|
|
2675
2691
|
const existingIds = existing.map((raw) => idOf(deserialize(raw))).filter((id) => typeof id === "string");
|
|
2676
2692
|
await redis.del(redisKey);
|
|
2677
2693
|
if (existingIds.length > 0) {
|
|
2678
|
-
await redis.del(
|
|
2679
|
-
...existingIds.map((id) => getDedupKey(threadId, id))
|
|
2680
|
-
);
|
|
2694
|
+
await redis.del(...existingIds.map(dedupKey));
|
|
2681
2695
|
}
|
|
2682
2696
|
if (messages.length > 0) {
|
|
2683
2697
|
await redis.rpush(redisKey, ...messages.map(serialize));
|
|
2684
|
-
await redis.expire(redisKey,
|
|
2698
|
+
await redis.expire(redisKey, ttlSeconds);
|
|
2685
2699
|
}
|
|
2686
|
-
await redis.expire(metaKey,
|
|
2700
|
+
await redis.expire(metaKey, ttlSeconds);
|
|
2687
2701
|
},
|
|
2688
2702
|
async delete() {
|
|
2689
2703
|
await redis.del(redisKey, metaKey, stateKey);
|
|
@@ -2695,12 +2709,7 @@ function createThreadManager(config) {
|
|
|
2695
2709
|
},
|
|
2696
2710
|
async saveState(state) {
|
|
2697
2711
|
await assertThreadExists();
|
|
2698
|
-
await redis.set(
|
|
2699
|
-
stateKey,
|
|
2700
|
-
JSON.stringify(state),
|
|
2701
|
-
"EX",
|
|
2702
|
-
THREAD_TTL_SECONDS
|
|
2703
|
-
);
|
|
2712
|
+
await redis.set(stateKey, JSON.stringify(state), "EX", ttlSeconds);
|
|
2704
2713
|
},
|
|
2705
2714
|
async deleteState() {
|
|
2706
2715
|
await redis.del(stateKey);
|
|
@@ -2729,19 +2738,202 @@ function createThreadManager(config) {
|
|
|
2729
2738
|
if (idx === -1) return;
|
|
2730
2739
|
if (idx === 0) {
|
|
2731
2740
|
await redis.del(redisKey);
|
|
2732
|
-
await redis.expire(metaKey,
|
|
2741
|
+
await redis.expire(metaKey, ttlSeconds);
|
|
2733
2742
|
} else {
|
|
2734
2743
|
await redis.ltrim(redisKey, 0, idx - 1);
|
|
2735
|
-
await redis.expire(redisKey,
|
|
2744
|
+
await redis.expire(redisKey, ttlSeconds);
|
|
2736
2745
|
}
|
|
2737
2746
|
if (removedIds.length > 0) {
|
|
2738
|
-
await redis.del(
|
|
2739
|
-
|
|
2747
|
+
await redis.del(...removedIds.map(dedupKey));
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
function joinKey(parts) {
|
|
2753
|
+
return parts.map((p) => p.replace(/^\/+|\/+$/g, "")).filter((p) => p.length > 0).join("/");
|
|
2754
|
+
}
|
|
2755
|
+
function buildKey(prefix, threadKey, threadId, gzip) {
|
|
2756
|
+
const ext = gzip ? "json.gz" : "json";
|
|
2757
|
+
return joinKey([
|
|
2758
|
+
prefix ?? "threads",
|
|
2759
|
+
threadKey,
|
|
2760
|
+
`${threadId}.${ext}`
|
|
2761
|
+
]);
|
|
2762
|
+
}
|
|
2763
|
+
async function streamToBuffer(body) {
|
|
2764
|
+
if (body == null) return Buffer.alloc(0);
|
|
2765
|
+
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
2766
|
+
if (typeof body.transformToByteArray === "function") {
|
|
2767
|
+
const bytes = await body.transformToByteArray();
|
|
2768
|
+
return Buffer.from(bytes);
|
|
2769
|
+
}
|
|
2770
|
+
if (typeof body.arrayBuffer === "function") {
|
|
2771
|
+
const ab = await body.arrayBuffer();
|
|
2772
|
+
return Buffer.from(ab);
|
|
2773
|
+
}
|
|
2774
|
+
const chunks = [];
|
|
2775
|
+
for await (const chunk of body) {
|
|
2776
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2777
|
+
}
|
|
2778
|
+
return Buffer.concat(chunks);
|
|
2779
|
+
}
|
|
2780
|
+
function createS3ColdStore(config) {
|
|
2781
|
+
const { s3, bucket, prefix, gzip = true } = config;
|
|
2782
|
+
const contentType = config.contentType ?? (gzip ? "application/gzip" : "application/json");
|
|
2783
|
+
return {
|
|
2784
|
+
async read(threadKey, threadId) {
|
|
2785
|
+
const Key = buildKey(prefix, threadKey, threadId, gzip);
|
|
2786
|
+
try {
|
|
2787
|
+
const resp = await s3.send(
|
|
2788
|
+
new clientS3.GetObjectCommand({ Bucket: bucket, Key })
|
|
2740
2789
|
);
|
|
2790
|
+
const buf = await streamToBuffer(resp.Body);
|
|
2791
|
+
const json = gzip ? zlib.gunzipSync(buf).toString("utf8") : buf.toString("utf8");
|
|
2792
|
+
return JSON.parse(json);
|
|
2793
|
+
} catch (err) {
|
|
2794
|
+
if (isNotFound(err)) return null;
|
|
2795
|
+
throw err;
|
|
2741
2796
|
}
|
|
2797
|
+
},
|
|
2798
|
+
async write(threadKey, threadId, snapshot) {
|
|
2799
|
+
const Key = buildKey(prefix, threadKey, threadId, gzip);
|
|
2800
|
+
const json = JSON.stringify(snapshot);
|
|
2801
|
+
const body = gzip ? zlib.gzipSync(Buffer.from(json, "utf8")) : json;
|
|
2802
|
+
await s3.send(
|
|
2803
|
+
new clientS3.PutObjectCommand({
|
|
2804
|
+
Bucket: bucket,
|
|
2805
|
+
Key,
|
|
2806
|
+
Body: body,
|
|
2807
|
+
ContentType: contentType
|
|
2808
|
+
})
|
|
2809
|
+
);
|
|
2810
|
+
},
|
|
2811
|
+
async delete(threadKey, threadId) {
|
|
2812
|
+
const Key = buildKey(prefix, threadKey, threadId, gzip);
|
|
2813
|
+
await s3.send(new clientS3.DeleteObjectCommand({ Bucket: bucket, Key }));
|
|
2742
2814
|
}
|
|
2743
2815
|
};
|
|
2744
2816
|
}
|
|
2817
|
+
function isNotFound(err) {
|
|
2818
|
+
if (typeof err !== "object" || err === null) return false;
|
|
2819
|
+
const e = err;
|
|
2820
|
+
return e.name === "NoSuchKey" || e.Code === "NoSuchKey" || e.code === "NoSuchKey" || e.name === "NotFound" || e.$metadata?.httpStatusCode === 404;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/lib/thread/snapshot.ts
|
|
2824
|
+
async function encodeSnapshot(config) {
|
|
2825
|
+
const { redis, threadKey, threadId, idOf } = config;
|
|
2826
|
+
const metaKey = getThreadMetaKey(threadKey, threadId);
|
|
2827
|
+
if (await redis.exists(metaKey) === 0) {
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
const listKey = getThreadListKey(threadKey, threadId);
|
|
2831
|
+
const stateKey = getThreadStateKey(threadKey, threadId);
|
|
2832
|
+
const messages = await redis.lrange(listKey, 0, -1);
|
|
2833
|
+
const stateRaw = await redis.get(stateKey);
|
|
2834
|
+
const state = stateRaw == null ? null : JSON.parse(stateRaw);
|
|
2835
|
+
const dedupIds = idOf ? messages.map(idOf) : [];
|
|
2836
|
+
return { v: 1, messages, state, dedupIds };
|
|
2837
|
+
}
|
|
2838
|
+
async function applySnapshot(config) {
|
|
2839
|
+
const {
|
|
2840
|
+
redis,
|
|
2841
|
+
threadKey,
|
|
2842
|
+
threadId,
|
|
2843
|
+
snapshot,
|
|
2844
|
+
ttlSeconds = THREAD_TTL_SECONDS
|
|
2845
|
+
} = config;
|
|
2846
|
+
const metaKey = getThreadMetaKey(threadKey, threadId);
|
|
2847
|
+
if (await redis.exists(metaKey) === 1) {
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const listKey = getThreadListKey(threadKey, threadId);
|
|
2851
|
+
const stateKey = getThreadStateKey(threadKey, threadId);
|
|
2852
|
+
await redis.del(listKey, stateKey);
|
|
2853
|
+
const pipeline = redis.pipeline();
|
|
2854
|
+
if (snapshot.messages.length > 0) {
|
|
2855
|
+
pipeline.rpush(listKey, ...snapshot.messages);
|
|
2856
|
+
pipeline.expire(listKey, ttlSeconds);
|
|
2857
|
+
}
|
|
2858
|
+
if (snapshot.state != null) {
|
|
2859
|
+
pipeline.set(stateKey, JSON.stringify(snapshot.state), "EX", ttlSeconds);
|
|
2860
|
+
}
|
|
2861
|
+
for (const id of snapshot.dedupIds) {
|
|
2862
|
+
pipeline.set(getThreadDedupKey(threadId, id), "1", "EX", ttlSeconds);
|
|
2863
|
+
}
|
|
2864
|
+
const results = await pipeline.exec();
|
|
2865
|
+
if (results) {
|
|
2866
|
+
const firstErr = results.find(([err]) => err)?.[0] ?? null;
|
|
2867
|
+
if (firstErr) {
|
|
2868
|
+
await redis.del(
|
|
2869
|
+
listKey,
|
|
2870
|
+
stateKey,
|
|
2871
|
+
...snapshot.dedupIds.map((id) => getThreadDedupKey(threadId, id))
|
|
2872
|
+
).catch(() => void 0);
|
|
2873
|
+
throw firstErr;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
await redis.set(metaKey, "1", "EX", ttlSeconds);
|
|
2877
|
+
}
|
|
2878
|
+
async function clearHotTier(config) {
|
|
2879
|
+
const { redis, threadKey, threadId, dedupIds = [] } = config;
|
|
2880
|
+
const keys = [
|
|
2881
|
+
getThreadListKey(threadKey, threadId),
|
|
2882
|
+
getThreadMetaKey(threadKey, threadId),
|
|
2883
|
+
getThreadStateKey(threadKey, threadId),
|
|
2884
|
+
...dedupIds.map((id) => getThreadDedupKey(threadId, id))
|
|
2885
|
+
];
|
|
2886
|
+
await redis.del(...keys);
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// src/lib/thread/tiered.ts
|
|
2890
|
+
function createTieredThreadManager(config) {
|
|
2891
|
+
const {
|
|
2892
|
+
redis,
|
|
2893
|
+
threadId,
|
|
2894
|
+
key = "messages",
|
|
2895
|
+
coldStore,
|
|
2896
|
+
idOf,
|
|
2897
|
+
deserialize = (raw) => JSON.parse(raw),
|
|
2898
|
+
ttlSeconds = THREAD_TTL_SECONDS
|
|
2899
|
+
} = config;
|
|
2900
|
+
const base = createThreadManager(config);
|
|
2901
|
+
const rawIdOf = idOf ? (raw) => idOf(deserialize(raw)) : void 0;
|
|
2902
|
+
return Object.assign(base, {
|
|
2903
|
+
async hydrate() {
|
|
2904
|
+
if (!coldStore) return;
|
|
2905
|
+
const snapshot = await coldStore.read(key, threadId);
|
|
2906
|
+
if (!snapshot) return;
|
|
2907
|
+
await applySnapshot({
|
|
2908
|
+
redis,
|
|
2909
|
+
threadKey: key,
|
|
2910
|
+
threadId,
|
|
2911
|
+
snapshot,
|
|
2912
|
+
ttlSeconds
|
|
2913
|
+
});
|
|
2914
|
+
},
|
|
2915
|
+
async flush(opts) {
|
|
2916
|
+
if (!coldStore) return;
|
|
2917
|
+
const snapshot = await encodeSnapshot({
|
|
2918
|
+
redis,
|
|
2919
|
+
threadKey: key,
|
|
2920
|
+
threadId,
|
|
2921
|
+
...rawIdOf ? { idOf: rawIdOf } : {}
|
|
2922
|
+
});
|
|
2923
|
+
if (!snapshot) return;
|
|
2924
|
+
await coldStore.write(key, threadId, snapshot);
|
|
2925
|
+
const deleteHot = opts?.deleteHot ?? true;
|
|
2926
|
+
if (deleteHot) {
|
|
2927
|
+
await clearHotTier({
|
|
2928
|
+
redis,
|
|
2929
|
+
threadKey: key,
|
|
2930
|
+
threadId,
|
|
2931
|
+
dedupIds: snapshot.dedupIds
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2745
2937
|
function getActivityContext() {
|
|
2746
2938
|
try {
|
|
2747
2939
|
const ctx = activity.Context.current();
|
|
@@ -3685,10 +3877,12 @@ exports.SandboxNotFoundError = SandboxNotFoundError;
|
|
|
3685
3877
|
exports.SandboxNotSupportedError = SandboxNotSupportedError;
|
|
3686
3878
|
exports.THREAD_TTL_SECONDS = THREAD_TTL_SECONDS;
|
|
3687
3879
|
exports.VirtualFileSystem = VirtualFileSystem;
|
|
3880
|
+
exports.applySnapshot = applySnapshot;
|
|
3688
3881
|
exports.applyVirtualTreeMutations = applyVirtualTreeMutations;
|
|
3689
3882
|
exports.askUserQuestionTool = askUserQuestionTool;
|
|
3690
3883
|
exports.bashHandler = bashHandler;
|
|
3691
3884
|
exports.bashTool = bashTool;
|
|
3885
|
+
exports.clearHotTier = clearHotTier;
|
|
3692
3886
|
exports.composeHooks = composeHooks;
|
|
3693
3887
|
exports.createAgentStateManager = createAgentStateManager;
|
|
3694
3888
|
exports.createAskUserQuestionHandler = createAskUserQuestionHandler;
|
|
@@ -3697,12 +3891,14 @@ exports.createObservabilityHooks = createObservabilityHooks;
|
|
|
3697
3891
|
exports.createReadSkillHandler = createReadSkillHandler;
|
|
3698
3892
|
exports.createReadSkillTool = createReadSkillTool;
|
|
3699
3893
|
exports.createRunAgentActivity = createRunAgentActivity;
|
|
3894
|
+
exports.createS3ColdStore = createS3ColdStore;
|
|
3700
3895
|
exports.createSession = createSession;
|
|
3701
3896
|
exports.createTaskCreateHandler = createTaskCreateHandler;
|
|
3702
3897
|
exports.createTaskGetHandler = createTaskGetHandler;
|
|
3703
3898
|
exports.createTaskListHandler = createTaskListHandler;
|
|
3704
3899
|
exports.createTaskUpdateHandler = createTaskUpdateHandler;
|
|
3705
3900
|
exports.createThreadManager = createThreadManager;
|
|
3901
|
+
exports.createTieredThreadManager = createTieredThreadManager;
|
|
3706
3902
|
exports.createToolRouter = createToolRouter;
|
|
3707
3903
|
exports.createVirtualFsActivities = createVirtualFsActivities;
|
|
3708
3904
|
exports.defineSubagent = defineSubagent;
|
|
@@ -3711,12 +3907,15 @@ exports.defineTool = defineTool;
|
|
|
3711
3907
|
exports.defineWorkflow = defineWorkflow;
|
|
3712
3908
|
exports.editHandler = editHandler;
|
|
3713
3909
|
exports.editTool = editTool;
|
|
3910
|
+
exports.encodeSnapshot = encodeSnapshot;
|
|
3714
3911
|
exports.filesWithMimeType = filesWithMimeType;
|
|
3715
3912
|
exports.formatVirtualFileTree = formatVirtualFileTree;
|
|
3716
3913
|
exports.getActivityContext = getActivityContext;
|
|
3717
3914
|
exports.getShortId = getShortId;
|
|
3915
|
+
exports.getThreadDedupKey = getThreadDedupKey;
|
|
3718
3916
|
exports.getThreadListKey = getThreadListKey;
|
|
3719
3917
|
exports.getThreadMetaKey = getThreadMetaKey;
|
|
3918
|
+
exports.getThreadStateKey = getThreadStateKey;
|
|
3720
3919
|
exports.globHandler = globHandler;
|
|
3721
3920
|
exports.globTool = globTool;
|
|
3722
3921
|
exports.grepTool = grepTool;
|