zeitlich 0.2.45 → 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-CrN-ghLo.d.ts → activities-Bm4TLTid.d.ts} +22 -2
- package/dist/{activities-Coafq5zr.d.cts → activities-CyeiqK_f.d.cts} +22 -2
- 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 +170 -66
- 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 +170 -66
- 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 +226 -27
- 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 +220 -28
- package/dist/index.js.map +1 -1
- package/dist/{proxy-Bf7uI-Hw.d.cts → proxy-BxFyd6cg.d.cts} +1 -1
- package/dist/{proxy-COqA95FW.d.ts → proxy-Cskmj4Yx.d.ts} +1 -1
- package/dist/{thread-manager-BsLO3Fgc.d.cts → thread-manager-9tezUcLW.d.cts} +8 -2
- package/dist/{thread-manager-Bi1XlbpJ.d.ts → thread-manager-B-zy3xrs.d.ts} +8 -2
- package/dist/{thread-manager-wRVVBFgj.d.cts → thread-manager-D33SUmZa.d.cts} +8 -2
- package/dist/{thread-manager-BhkOyQ1I.d.ts → thread-manager-DduoSkvJ.d.ts} +8 -2
- package/dist/{types-CdALEF3z.d.cts → types-CnuN9T6t.d.cts} +22 -0
- package/dist/{types-ChAy_jSP.d.ts → types-CwN6_tAL.d.ts} +22 -0
- package/dist/{types-BkX4HLzi.d.ts → types-L5bvbF-n.d.ts} +17 -1
- package/dist/{types-C66-BVBr.d.cts → types-oxt8GN97.d.cts} +17 -1
- package/dist/{workflow-BwT5EybR.d.ts → workflow-B1TOcHbt.d.ts} +33 -2
- package/dist/{workflow-DMmiaw6w.d.cts → workflow-DIaIV7L2.d.cts} +33 -2
- package/dist/workflow.cjs +14 -1
- 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 +14 -1
- 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 +29 -0
- 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
|
@@ -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,6 +5,8 @@ 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
|
|
|
@@ -1038,7 +1040,9 @@ async function createSession(config) {
|
|
|
1038
1040
|
appendAgentMessage,
|
|
1039
1041
|
forkThread,
|
|
1040
1042
|
loadThreadState,
|
|
1041
|
-
saveThreadState
|
|
1043
|
+
saveThreadState,
|
|
1044
|
+
hydrateThread,
|
|
1045
|
+
flushThread
|
|
1042
1046
|
} = threadOps;
|
|
1043
1047
|
const plugins = [];
|
|
1044
1048
|
let destroySubagentSandboxes;
|
|
@@ -1182,10 +1186,12 @@ async function createSession(config) {
|
|
|
1182
1186
|
});
|
|
1183
1187
|
};
|
|
1184
1188
|
if (threadMode === "fork" && sourceThreadId) {
|
|
1189
|
+
await hydrateThread(sourceThreadId, threadKey);
|
|
1185
1190
|
await forkThread(sourceThreadId, threadId, threadKey);
|
|
1186
1191
|
const forkedSlice = await loadThreadState(threadId, threadKey);
|
|
1187
1192
|
if (forkedSlice) rehydrateFromSlice(forkedSlice);
|
|
1188
1193
|
} else if (threadMode === "continue") {
|
|
1194
|
+
await hydrateThread(threadId, threadKey);
|
|
1189
1195
|
const continuedSlice = await loadThreadState(threadId, threadKey);
|
|
1190
1196
|
if (continuedSlice) rehydrateFromSlice(continuedSlice);
|
|
1191
1197
|
} else {
|
|
@@ -1367,6 +1373,15 @@ async function createSession(config) {
|
|
|
1367
1373
|
error: persistError instanceof Error ? persistError.message : String(persistError)
|
|
1368
1374
|
});
|
|
1369
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
|
+
}
|
|
1370
1385
|
await callSessionEnd(exitReason, stateManager.getTurns());
|
|
1371
1386
|
if (sandboxOwned && sandboxId && sandboxOps) {
|
|
1372
1387
|
switch (resolvedShutdown) {
|
|
@@ -1450,6 +1465,9 @@ function getThreadMetaKey(threadKey, threadId) {
|
|
|
1450
1465
|
function getThreadStateKey(threadKey, threadId) {
|
|
1451
1466
|
return `${threadKey}:state:thread:${threadId}`;
|
|
1452
1467
|
}
|
|
1468
|
+
function getThreadDedupKey(threadId, dedupId) {
|
|
1469
|
+
return `dedup:${dedupId}:thread:${threadId}`;
|
|
1470
|
+
}
|
|
1453
1471
|
|
|
1454
1472
|
// src/lib/types.ts
|
|
1455
1473
|
function isTerminalStatus(status) {
|
|
@@ -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;
|