zeitlich 0.2.45 → 0.2.47

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.
Files changed (109) hide show
  1. package/README.md +137 -11
  2. package/dist/{activities-Coafq5zr.d.cts → activities-CPwKoUlD.d.cts} +22 -2
  3. package/dist/{activities-CrN-ghLo.d.ts → activities-DlaBxNID.d.ts} +22 -2
  4. package/dist/adapters/thread/anthropic/index.cjs +276 -71
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +62 -8
  7. package/dist/adapters/thread/anthropic/index.d.ts +62 -8
  8. package/dist/adapters/thread/anthropic/index.js +275 -72
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +38 -20
  11. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
  14. package/dist/adapters/thread/anthropic/workflow.js +38 -20
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.cjs +171 -69
  17. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  18. package/dist/adapters/thread/google-genai/index.d.cts +6 -4
  19. package/dist/adapters/thread/google-genai/index.d.ts +6 -4
  20. package/dist/adapters/thread/google-genai/index.js +171 -69
  21. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  22. package/dist/adapters/thread/google-genai/workflow.cjs +38 -20
  23. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/workflow.d.cts +7 -4
  25. package/dist/adapters/thread/google-genai/workflow.d.ts +7 -4
  26. package/dist/adapters/thread/google-genai/workflow.js +38 -20
  27. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  28. package/dist/adapters/thread/langchain/index.cjs +170 -66
  29. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  30. package/dist/adapters/thread/langchain/index.d.cts +19 -4
  31. package/dist/adapters/thread/langchain/index.d.ts +19 -4
  32. package/dist/adapters/thread/langchain/index.js +170 -66
  33. package/dist/adapters/thread/langchain/index.js.map +1 -1
  34. package/dist/adapters/thread/langchain/workflow.cjs +38 -20
  35. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  36. package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
  37. package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
  38. package/dist/adapters/thread/langchain/workflow.js +38 -20
  39. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  40. package/dist/cold-store-BDgJpwLI.d.ts +114 -0
  41. package/dist/cold-store-Z2wvK2cV.d.cts +114 -0
  42. package/dist/index.cjs +440 -67
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +150 -8
  45. package/dist/index.d.ts +150 -8
  46. package/dist/index.js +432 -68
  47. package/dist/index.js.map +1 -1
  48. package/dist/proxy-CDh3Rsa7.d.cts +40 -0
  49. package/dist/proxy-Du8ggERu.d.ts +40 -0
  50. package/dist/{thread-manager-wRVVBFgj.d.cts → thread-manager-BjoYYXgd.d.cts} +8 -2
  51. package/dist/{thread-manager-BsLO3Fgc.d.cts → thread-manager-D8zKNFZ9.d.cts} +8 -2
  52. package/dist/{thread-manager-Bi1XlbpJ.d.ts → thread-manager-DtHYws2F.d.ts} +8 -2
  53. package/dist/{thread-manager-BhkOyQ1I.d.ts → thread-manager-Dw96FKH1.d.ts} +8 -2
  54. package/dist/{types-C66-BVBr.d.cts → types-BMJrsHo0.d.cts} +17 -1
  55. package/dist/{types-BkX4HLzi.d.ts → types-CtdOquo3.d.ts} +17 -1
  56. package/dist/{types-CdALEF3z.d.cts → types-DNEl5uxQ.d.cts} +38 -0
  57. package/dist/{types-ChAy_jSP.d.ts → types-qQVZfhoT.d.ts} +38 -0
  58. package/dist/{workflow-DMmiaw6w.d.cts → workflow-BH9ImDGq.d.cts} +48 -2
  59. package/dist/{workflow-BwT5EybR.d.ts → workflow-Cdw3-RNB.d.ts} +48 -2
  60. package/dist/workflow.cjs +47 -4
  61. package/dist/workflow.cjs.map +1 -1
  62. package/dist/workflow.d.cts +2 -2
  63. package/dist/workflow.d.ts +2 -2
  64. package/dist/workflow.js +47 -5
  65. package/dist/workflow.js.map +1 -1
  66. package/package.json +14 -3
  67. package/src/adapters/thread/anthropic/activities.ts +82 -39
  68. package/src/adapters/thread/anthropic/index.ts +8 -0
  69. package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
  70. package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
  71. package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
  72. package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
  73. package/src/adapters/thread/anthropic/proxy.ts +1 -0
  74. package/src/adapters/thread/anthropic/thread-manager.ts +9 -1
  75. package/src/adapters/thread/google-genai/activities.ts +64 -40
  76. package/src/adapters/thread/google-genai/proxy.ts +1 -0
  77. package/src/adapters/thread/google-genai/thread-manager.ts +9 -1
  78. package/src/adapters/thread/langchain/activities.ts +63 -36
  79. package/src/adapters/thread/langchain/proxy.ts +1 -0
  80. package/src/adapters/thread/langchain/thread-manager.ts +9 -1
  81. package/src/index.ts +21 -2
  82. package/src/lib/session/session-edge-cases.integration.test.ts +12 -0
  83. package/src/lib/session/session.integration.test.ts +138 -0
  84. package/src/lib/session/session.ts +29 -0
  85. package/src/lib/session/types.ts +22 -0
  86. package/src/lib/subagent/define.ts +1 -0
  87. package/src/lib/subagent/handler.ts +11 -2
  88. package/src/lib/subagent/subagent.integration.test.ts +139 -0
  89. package/src/lib/subagent/types.ts +16 -0
  90. package/src/lib/thread/cold-store.test.ts +221 -0
  91. package/src/lib/thread/cold-store.ts +269 -0
  92. package/src/lib/thread/index.ts +32 -0
  93. package/src/lib/thread/keys.ts +20 -0
  94. package/src/lib/thread/manager.ts +16 -27
  95. package/src/lib/thread/proxy.ts +79 -27
  96. package/src/lib/thread/snapshot.test.ts +443 -0
  97. package/src/lib/thread/snapshot.ts +163 -0
  98. package/src/lib/thread/test-utils.ts +228 -0
  99. package/src/lib/thread/tiered.test.ts +281 -0
  100. package/src/lib/thread/tiered.ts +135 -0
  101. package/src/lib/thread/types.ts +16 -0
  102. package/src/tools/edit/handler.test.ts +177 -0
  103. package/src/tools/edit/handler.ts +249 -47
  104. package/src/tools/edit/tool.ts +40 -0
  105. package/src/tools/task-create/handler.ts +1 -1
  106. package/src/tools/task-update/handler.ts +1 -1
  107. package/src/workflow.ts +2 -2
  108. package/dist/proxy-Bf7uI-Hw.d.cts +0 -24
  109. package/dist/proxy-COqA95FW.d.ts +0 -24
package/dist/index.cjs CHANGED
@@ -5,6 +5,10 @@ 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 util = require('util');
10
+ var clientS3 = require('@aws-sdk/client-s3');
11
+ var libStorage = require('@aws-sdk/lib-storage');
8
12
  var activity = require('@temporalio/activity');
9
13
  var fs = require('fs');
10
14
 
@@ -570,7 +574,8 @@ function createSubagentHandler(subagents) {
570
574
  }
571
575
  const threadMode = config.thread ?? "new";
572
576
  const allowsContinuation = threadMode !== "new";
573
- const continuationThreadId = args.threadId && allowsContinuation ? args.threadId : void 0;
577
+ const newThreadSource = config.newThreadSource ?? "new";
578
+ const continuationThreadId = !allowsContinuation ? void 0 : args.threadId ?? (newThreadSource === "from-parent" ? context.threadId : void 0);
574
579
  let thread;
575
580
  if (continuationThreadId) {
576
581
  thread = {
@@ -1038,7 +1043,9 @@ async function createSession(config) {
1038
1043
  appendAgentMessage,
1039
1044
  forkThread,
1040
1045
  loadThreadState,
1041
- saveThreadState
1046
+ saveThreadState,
1047
+ hydrateThread,
1048
+ flushThread
1042
1049
  } = threadOps;
1043
1050
  const plugins = [];
1044
1051
  let destroySubagentSandboxes;
@@ -1182,10 +1189,12 @@ async function createSession(config) {
1182
1189
  });
1183
1190
  };
1184
1191
  if (threadMode === "fork" && sourceThreadId) {
1192
+ await hydrateThread(sourceThreadId, threadKey);
1185
1193
  await forkThread(sourceThreadId, threadId, threadKey);
1186
1194
  const forkedSlice = await loadThreadState(threadId, threadKey);
1187
1195
  if (forkedSlice) rehydrateFromSlice(forkedSlice);
1188
1196
  } else if (threadMode === "continue") {
1197
+ await hydrateThread(threadId, threadKey);
1189
1198
  const continuedSlice = await loadThreadState(threadId, threadKey);
1190
1199
  if (continuedSlice) rehydrateFromSlice(continuedSlice);
1191
1200
  } else {
@@ -1367,6 +1376,15 @@ async function createSession(config) {
1367
1376
  error: persistError instanceof Error ? persistError.message : String(persistError)
1368
1377
  });
1369
1378
  }
1379
+ try {
1380
+ await flushThread(threadId, threadKey);
1381
+ } catch (flushError) {
1382
+ workflow.log.warn("failed to flush thread to cold tier", {
1383
+ agentName,
1384
+ threadId,
1385
+ error: flushError instanceof Error ? flushError.message : String(flushError)
1386
+ });
1387
+ }
1370
1388
  await callSessionEnd(exitReason, stateManager.getTurns());
1371
1389
  if (sandboxOwned && sandboxId && sandboxOps) {
1372
1390
  switch (resolvedShutdown) {
@@ -1450,6 +1468,9 @@ function getThreadMetaKey(threadKey, threadId) {
1450
1468
  function getThreadStateKey(threadKey, threadId) {
1451
1469
  return `${threadKey}:state:thread:${threadId}`;
1452
1470
  }
1471
+ function getThreadDedupKey(threadId, dedupId) {
1472
+ return `dedup:${dedupId}:thread:${threadId}`;
1473
+ }
1453
1474
 
1454
1475
  // src/lib/types.ts
1455
1476
  function isTerminalStatus(status) {
@@ -2202,6 +2223,13 @@ IMPORTANT:
2202
2223
  }),
2203
2224
  strict: true
2204
2225
  };
2226
+ var textEditSchema = z14.z.object({
2227
+ old_string: z14.z.string().describe("The exact text to replace"),
2228
+ new_string: z14.z.string().describe("The text to replace it with"),
2229
+ replace_all: z14.z.boolean().optional().describe(
2230
+ "If true, replace all occurrences of old_string for this edit (default: false)"
2231
+ )
2232
+ });
2205
2233
  var editTool = {
2206
2234
  name: "FileEdit",
2207
2235
  description: `Edit specific sections of a file by replacing text.
@@ -2230,6 +2258,27 @@ IMPORTANT:
2230
2258
  }),
2231
2259
  strict: true
2232
2260
  };
2261
+ var multiEditTool = {
2262
+ name: "FileMultiEdit",
2263
+ description: `Apply multiple exact text replacements to one file in order.
2264
+
2265
+ Usage:
2266
+ - Use this when a task needs several related edits in the same file
2267
+ - Each edit is applied to the file content produced by the prior edit
2268
+ - The operation is atomic: if any edit fails, the file is left unchanged
2269
+
2270
+ IMPORTANT:
2271
+ - You must read the file first (in this session) before editing it
2272
+ - Each old_string must match exactly (whitespace-sensitive)
2273
+ - Each old_string must be unique unless that edit uses replace_all: true
2274
+ - old_string and new_string must be different for every edit
2275
+ `,
2276
+ schema: z14.z.object({
2277
+ file_path: z14.z.string().describe("The absolute virtual path to the file to modify"),
2278
+ edits: z14.z.array(textEditSchema).min(1).describe("Exact replacements to apply sequentially to the file")
2279
+ }),
2280
+ strict: true
2281
+ };
2233
2282
  var taskCreateTool = {
2234
2283
  name: "TaskCreate",
2235
2284
  description: `Use this tool to create a structured task list. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
@@ -2300,7 +2349,7 @@ function createTaskCreateHandler(stateManager) {
2300
2349
  };
2301
2350
  stateManager.setTask(task);
2302
2351
  return {
2303
- toolResponse: JSON.stringify(task, null, 2),
2352
+ toolResponse: `Task ${task.id} created`,
2304
2353
  data: task
2305
2354
  };
2306
2355
  };
@@ -2399,7 +2448,7 @@ function createTaskUpdateHandler(stateManager) {
2399
2448
  }
2400
2449
  stateManager.setTask(task);
2401
2450
  return {
2402
- toolResponse: JSON.stringify(task, null, 2),
2451
+ toolResponse: `Task ${task.id} updated`,
2403
2452
  data: task
2404
2453
  };
2405
2454
  };
@@ -2594,9 +2643,6 @@ redis.call('EXPIRE', KEYS[2], tonumber(ARGV[1]))
2594
2643
  redis.call('SET', KEYS[1], '1', 'EX', tonumber(ARGV[1]))
2595
2644
  return 1
2596
2645
  `;
2597
- function getDedupKey(threadId, id) {
2598
- return `dedup:${id}:thread:${threadId}`;
2599
- }
2600
2646
  function createThreadManager(config) {
2601
2647
  const {
2602
2648
  redis,
@@ -2604,11 +2650,13 @@ function createThreadManager(config) {
2604
2650
  key = "messages",
2605
2651
  serialize = (m) => JSON.stringify(m),
2606
2652
  deserialize = (raw) => JSON.parse(raw),
2607
- idOf
2653
+ idOf,
2654
+ ttlSeconds = THREAD_TTL_SECONDS
2608
2655
  } = config;
2609
2656
  const redisKey = getThreadListKey(key, threadId);
2610
2657
  const metaKey = getThreadMetaKey(key, threadId);
2611
2658
  const stateKey = getThreadStateKey(key, threadId);
2659
+ const dedupKey = (id) => getThreadDedupKey(threadId, id);
2612
2660
  async function assertThreadExists() {
2613
2661
  const exists = await redis.exists(metaKey);
2614
2662
  if (!exists) {
@@ -2618,7 +2666,7 @@ function createThreadManager(config) {
2618
2666
  return {
2619
2667
  async initialize() {
2620
2668
  await redis.del(redisKey);
2621
- await redis.set(metaKey, "1", "EX", THREAD_TTL_SECONDS);
2669
+ await redis.set(metaKey, "1", "EX", ttlSeconds);
2622
2670
  },
2623
2671
  async load() {
2624
2672
  await assertThreadExists();
@@ -2630,18 +2678,17 @@ function createThreadManager(config) {
2630
2678
  await assertThreadExists();
2631
2679
  if (idOf) {
2632
2680
  const dedupId = messages.map(idOf).join(":");
2633
- const dedupKey = getDedupKey(threadId, dedupId);
2634
2681
  await redis.eval(
2635
2682
  APPEND_IDEMPOTENT_SCRIPT,
2636
2683
  2,
2637
- dedupKey,
2684
+ dedupKey(dedupId),
2638
2685
  redisKey,
2639
- String(THREAD_TTL_SECONDS),
2686
+ String(ttlSeconds),
2640
2687
  ...messages.map(serialize)
2641
2688
  );
2642
2689
  } else {
2643
2690
  await redis.rpush(redisKey, ...messages.map(serialize));
2644
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
2691
+ await redis.expire(redisKey, ttlSeconds);
2645
2692
  }
2646
2693
  },
2647
2694
  async fork(newThreadId) {
@@ -2656,11 +2703,11 @@ function createThreadManager(config) {
2656
2703
  if (data.length > 0) {
2657
2704
  const newKey = getThreadListKey(key, newThreadId);
2658
2705
  await redis.rpush(newKey, ...data);
2659
- await redis.expire(newKey, THREAD_TTL_SECONDS);
2706
+ await redis.expire(newKey, ttlSeconds);
2660
2707
  }
2661
2708
  if (stateRaw != null) {
2662
2709
  const newStateKey = getThreadStateKey(key, newThreadId);
2663
- await redis.set(newStateKey, stateRaw, "EX", THREAD_TTL_SECONDS);
2710
+ await redis.set(newStateKey, stateRaw, "EX", ttlSeconds);
2664
2711
  }
2665
2712
  return forked;
2666
2713
  },
@@ -2675,15 +2722,13 @@ function createThreadManager(config) {
2675
2722
  const existingIds = existing.map((raw) => idOf(deserialize(raw))).filter((id) => typeof id === "string");
2676
2723
  await redis.del(redisKey);
2677
2724
  if (existingIds.length > 0) {
2678
- await redis.del(
2679
- ...existingIds.map((id) => getDedupKey(threadId, id))
2680
- );
2725
+ await redis.del(...existingIds.map(dedupKey));
2681
2726
  }
2682
2727
  if (messages.length > 0) {
2683
2728
  await redis.rpush(redisKey, ...messages.map(serialize));
2684
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
2729
+ await redis.expire(redisKey, ttlSeconds);
2685
2730
  }
2686
- await redis.expire(metaKey, THREAD_TTL_SECONDS);
2731
+ await redis.expire(metaKey, ttlSeconds);
2687
2732
  },
2688
2733
  async delete() {
2689
2734
  await redis.del(redisKey, metaKey, stateKey);
@@ -2695,12 +2740,7 @@ function createThreadManager(config) {
2695
2740
  },
2696
2741
  async saveState(state) {
2697
2742
  await assertThreadExists();
2698
- await redis.set(
2699
- stateKey,
2700
- JSON.stringify(state),
2701
- "EX",
2702
- THREAD_TTL_SECONDS
2703
- );
2743
+ await redis.set(stateKey, JSON.stringify(state), "EX", ttlSeconds);
2704
2744
  },
2705
2745
  async deleteState() {
2706
2746
  await redis.del(stateKey);
@@ -2729,15 +2769,13 @@ function createThreadManager(config) {
2729
2769
  if (idx === -1) return;
2730
2770
  if (idx === 0) {
2731
2771
  await redis.del(redisKey);
2732
- await redis.expire(metaKey, THREAD_TTL_SECONDS);
2772
+ await redis.expire(metaKey, ttlSeconds);
2733
2773
  } else {
2734
2774
  await redis.ltrim(redisKey, 0, idx - 1);
2735
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
2775
+ await redis.expire(redisKey, ttlSeconds);
2736
2776
  }
2737
2777
  if (removedIds.length > 0) {
2738
- await redis.del(
2739
- ...removedIds.map((id) => getDedupKey(threadId, id))
2740
- );
2778
+ await redis.del(...removedIds.map(dedupKey));
2741
2779
  }
2742
2780
  }
2743
2781
  };
@@ -2777,6 +2815,199 @@ function withParentWorkflowState(client, handler) {
2777
2815
  };
2778
2816
  }
2779
2817
 
2818
+ // src/lib/thread/cold-store.ts
2819
+ var gzipAsync = util.promisify(zlib.gzip);
2820
+ var gunzipAsync = util.promisify(zlib.gunzip);
2821
+ function joinKey(parts) {
2822
+ return parts.map((p) => p.replace(/^\/+|\/+$/g, "")).filter((p) => p.length > 0).join("/");
2823
+ }
2824
+ function buildKey(prefix, threadKey, threadId, gzip) {
2825
+ const ext = gzip ? "json.gz" : "json";
2826
+ return joinKey([
2827
+ prefix ?? "threads",
2828
+ threadKey,
2829
+ `${threadId}.${ext}`
2830
+ ]);
2831
+ }
2832
+ async function streamToBuffer(body, onChunk) {
2833
+ if (body == null) return Buffer.alloc(0);
2834
+ if (body instanceof Uint8Array) return Buffer.from(body);
2835
+ if (typeof body[Symbol.asyncIterator] === "function") {
2836
+ const chunks = [];
2837
+ for await (const chunk of body) {
2838
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2839
+ onChunk?.();
2840
+ }
2841
+ return Buffer.concat(chunks);
2842
+ }
2843
+ if (typeof body.transformToByteArray === "function") {
2844
+ const bytes = await body.transformToByteArray();
2845
+ return Buffer.from(bytes);
2846
+ }
2847
+ if (typeof body.arrayBuffer === "function") {
2848
+ const ab = await body.arrayBuffer();
2849
+ return Buffer.from(ab);
2850
+ }
2851
+ return Buffer.alloc(0);
2852
+ }
2853
+ function createS3ColdStore(config) {
2854
+ const { s3, bucket, prefix, gzip = true } = config;
2855
+ const contentType = config.contentType ?? (gzip ? "application/gzip" : "application/json");
2856
+ return {
2857
+ async read(threadKey, threadId) {
2858
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
2859
+ try {
2860
+ const resp = await s3.send(
2861
+ new clientS3.GetObjectCommand({ Bucket: bucket, Key })
2862
+ );
2863
+ const { heartbeat } = getActivityContext();
2864
+ const buf = await streamToBuffer(resp.Body, heartbeat);
2865
+ const json = gzip ? (await gunzipAsync(buf)).toString("utf8") : buf.toString("utf8");
2866
+ return JSON.parse(json);
2867
+ } catch (err) {
2868
+ if (isNotFound(err)) return null;
2869
+ throw err;
2870
+ }
2871
+ },
2872
+ async write(threadKey, threadId, snapshot) {
2873
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
2874
+ const json = JSON.stringify(snapshot);
2875
+ const body = gzip ? await gzipAsync(Buffer.from(json, "utf8")) : json;
2876
+ const upload = new libStorage.Upload({
2877
+ client: s3,
2878
+ params: { Bucket: bucket, Key, Body: body, ContentType: contentType }
2879
+ });
2880
+ const { heartbeat } = getActivityContext();
2881
+ if (heartbeat) upload.on("httpUploadProgress", heartbeat);
2882
+ await upload.done();
2883
+ },
2884
+ async delete(threadKey, threadId) {
2885
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
2886
+ await s3.send(new clientS3.DeleteObjectCommand({ Bucket: bucket, Key }));
2887
+ }
2888
+ };
2889
+ }
2890
+ function isNotFound(err) {
2891
+ if (typeof err !== "object" || err === null) return false;
2892
+ const e = err;
2893
+ return e.name === "NoSuchKey" || e.Code === "NoSuchKey" || e.code === "NoSuchKey" || e.name === "NotFound" || e.$metadata?.httpStatusCode === 404;
2894
+ }
2895
+
2896
+ // src/lib/thread/snapshot.ts
2897
+ async function encodeSnapshot(config) {
2898
+ const { redis, threadKey, threadId, idOf } = config;
2899
+ const metaKey = getThreadMetaKey(threadKey, threadId);
2900
+ if (await redis.exists(metaKey) === 0) {
2901
+ return null;
2902
+ }
2903
+ const listKey = getThreadListKey(threadKey, threadId);
2904
+ const stateKey = getThreadStateKey(threadKey, threadId);
2905
+ const messages = await redis.lrange(listKey, 0, -1);
2906
+ const stateRaw = await redis.get(stateKey);
2907
+ const state = stateRaw == null ? null : JSON.parse(stateRaw);
2908
+ const dedupIds = idOf ? messages.map(idOf) : [];
2909
+ return { v: 1, messages, state, dedupIds };
2910
+ }
2911
+ async function applySnapshot(config) {
2912
+ const {
2913
+ redis,
2914
+ threadKey,
2915
+ threadId,
2916
+ snapshot,
2917
+ ttlSeconds = THREAD_TTL_SECONDS
2918
+ } = config;
2919
+ const metaKey = getThreadMetaKey(threadKey, threadId);
2920
+ if (await redis.exists(metaKey) === 1) {
2921
+ return;
2922
+ }
2923
+ const listKey = getThreadListKey(threadKey, threadId);
2924
+ const stateKey = getThreadStateKey(threadKey, threadId);
2925
+ await redis.del(listKey, stateKey);
2926
+ const pipeline = redis.pipeline();
2927
+ if (snapshot.messages.length > 0) {
2928
+ pipeline.rpush(listKey, ...snapshot.messages);
2929
+ pipeline.expire(listKey, ttlSeconds);
2930
+ }
2931
+ if (snapshot.state != null) {
2932
+ pipeline.set(stateKey, JSON.stringify(snapshot.state), "EX", ttlSeconds);
2933
+ }
2934
+ for (const id of snapshot.dedupIds) {
2935
+ pipeline.set(getThreadDedupKey(threadId, id), "1", "EX", ttlSeconds);
2936
+ }
2937
+ const results = await pipeline.exec();
2938
+ if (results) {
2939
+ const firstErr = results.find(([err]) => err)?.[0] ?? null;
2940
+ if (firstErr) {
2941
+ await redis.del(
2942
+ listKey,
2943
+ stateKey,
2944
+ ...snapshot.dedupIds.map((id) => getThreadDedupKey(threadId, id))
2945
+ ).catch(() => void 0);
2946
+ throw firstErr;
2947
+ }
2948
+ }
2949
+ await redis.set(metaKey, "1", "EX", ttlSeconds);
2950
+ }
2951
+ async function clearHotTier(config) {
2952
+ const { redis, threadKey, threadId, dedupIds = [] } = config;
2953
+ const keys = [
2954
+ getThreadListKey(threadKey, threadId),
2955
+ getThreadMetaKey(threadKey, threadId),
2956
+ getThreadStateKey(threadKey, threadId),
2957
+ ...dedupIds.map((id) => getThreadDedupKey(threadId, id))
2958
+ ];
2959
+ await redis.del(...keys);
2960
+ }
2961
+
2962
+ // src/lib/thread/tiered.ts
2963
+ function createTieredThreadManager(config) {
2964
+ const {
2965
+ redis,
2966
+ threadId,
2967
+ key = "messages",
2968
+ coldStore,
2969
+ idOf,
2970
+ deserialize = (raw) => JSON.parse(raw),
2971
+ ttlSeconds = THREAD_TTL_SECONDS
2972
+ } = config;
2973
+ const base = createThreadManager(config);
2974
+ const rawIdOf = idOf ? (raw) => idOf(deserialize(raw)) : void 0;
2975
+ return Object.assign(base, {
2976
+ async hydrate() {
2977
+ if (!coldStore) return;
2978
+ const snapshot = await coldStore.read(key, threadId);
2979
+ if (!snapshot) return;
2980
+ await applySnapshot({
2981
+ redis,
2982
+ threadKey: key,
2983
+ threadId,
2984
+ snapshot,
2985
+ ttlSeconds
2986
+ });
2987
+ },
2988
+ async flush(opts) {
2989
+ if (!coldStore) return;
2990
+ const snapshot = await encodeSnapshot({
2991
+ redis,
2992
+ threadKey: key,
2993
+ threadId,
2994
+ ...rawIdOf ? { idOf: rawIdOf } : {}
2995
+ });
2996
+ if (!snapshot) return;
2997
+ await coldStore.write(key, threadId, snapshot);
2998
+ const deleteHot = opts?.deleteHot ?? true;
2999
+ if (deleteHot) {
3000
+ await clearHotTier({
3001
+ redis,
3002
+ threadKey: key,
3003
+ threadId,
3004
+ dedupIds: snapshot.dedupIds
3005
+ });
3006
+ }
3007
+ }
3008
+ });
3009
+ }
3010
+
2780
3011
  // src/lib/sandbox/manager.ts
2781
3012
  var CAP_METHOD_TO_CAPABILITY = [
2782
3013
  { method: "pause", capability: "pause" },
@@ -3447,57 +3678,190 @@ ${result.stderr}`,
3447
3678
  };
3448
3679
 
3449
3680
  // src/tools/edit/handler.ts
3450
- function escapeRegExp(str) {
3451
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3681
+ function splitLines(text) {
3682
+ if (text.length === 0) return [];
3683
+ return text.replace(/\r\n/g, "\n").split("\n");
3684
+ }
3685
+ function lineNumberAt(content, index) {
3686
+ let line = 1;
3687
+ for (let i = 0; i < index; i++) {
3688
+ if (content.charCodeAt(i) === 10) line++;
3689
+ }
3690
+ return line;
3691
+ }
3692
+ function lineEnd(startLine, lines) {
3693
+ return lines.length === 0 ? startLine : startLine + lines.length - 1;
3694
+ }
3695
+ function indicesOf(content, needle) {
3696
+ const indices = [];
3697
+ let cursor = 0;
3698
+ while (cursor <= content.length) {
3699
+ const index = content.indexOf(needle, cursor);
3700
+ if (index === -1) break;
3701
+ indices.push(index);
3702
+ cursor = index + needle.length;
3703
+ }
3704
+ return indices;
3705
+ }
3706
+ function makeHunk(editIndex, beforeContent, replacementIndex, oldString, newString) {
3707
+ const oldStartLine = lineNumberAt(beforeContent, replacementIndex);
3708
+ const oldLines = splitLines(oldString);
3709
+ const newLines = splitLines(newString);
3710
+ return {
3711
+ editIndex,
3712
+ oldStartLine,
3713
+ oldEndLine: lineEnd(oldStartLine, oldLines),
3714
+ newStartLine: oldStartLine,
3715
+ newEndLine: lineEnd(oldStartLine, newLines),
3716
+ oldLines,
3717
+ newLines
3718
+ };
3719
+ }
3720
+ function applyOneEdit(content, edit, editIndex) {
3721
+ const { old_string, new_string, replace_all = false } = edit;
3722
+ if (old_string.length === 0) {
3723
+ return {
3724
+ ok: false,
3725
+ editIndex,
3726
+ message: `Error: old_string for edit ${editIndex} must not be empty.`
3727
+ };
3728
+ }
3729
+ if (old_string === new_string) {
3730
+ return {
3731
+ ok: false,
3732
+ editIndex,
3733
+ message: `Error: old_string and new_string must be different for edit ${editIndex}.`
3734
+ };
3735
+ }
3736
+ const matches = indicesOf(content, old_string);
3737
+ if (matches.length === 0) {
3738
+ return {
3739
+ ok: false,
3740
+ editIndex,
3741
+ message: `Error: Could not find old_string for edit ${editIndex}. Make sure it matches exactly (whitespace-sensitive).`
3742
+ };
3743
+ }
3744
+ if (!replace_all && matches.length > 1) {
3745
+ return {
3746
+ ok: false,
3747
+ editIndex,
3748
+ message: `Error: old_string for edit ${editIndex} appears ${matches.length} times. Provide more context to make it unique, or use replace_all: true for that edit.`
3749
+ };
3750
+ }
3751
+ if (replace_all) {
3752
+ const hunks = matches.map(
3753
+ (index2) => makeHunk(editIndex, content, index2, old_string, new_string)
3754
+ );
3755
+ return {
3756
+ ok: true,
3757
+ content: content.split(old_string).join(new_string),
3758
+ replacements: matches.length,
3759
+ hunks
3760
+ };
3761
+ }
3762
+ const index = matches[0];
3763
+ if (index === void 0) {
3764
+ return {
3765
+ ok: false,
3766
+ editIndex,
3767
+ message: `Error: Could not find old_string for edit ${editIndex}.`
3768
+ };
3769
+ }
3770
+ return {
3771
+ ok: true,
3772
+ content: content.slice(0, index) + new_string + content.slice(index + old_string.length),
3773
+ replacements: 1,
3774
+ hunks: [makeHunk(editIndex, content, index, old_string, new_string)]
3775
+ };
3776
+ }
3777
+ function applyEditPlan(content, edits) {
3778
+ if (edits.length === 0) {
3779
+ return {
3780
+ ok: false,
3781
+ message: "Error: edits must contain at least one edit."
3782
+ };
3783
+ }
3784
+ let current = content;
3785
+ let replacements = 0;
3786
+ const hunks = [];
3787
+ for (const [index, edit] of edits.entries()) {
3788
+ const result = applyOneEdit(current, edit, index);
3789
+ if (!result.ok) return result;
3790
+ current = result.content;
3791
+ replacements += result.replacements;
3792
+ hunks.push(...result.hunks);
3793
+ }
3794
+ return { ok: true, content: current, replacements, hunks };
3795
+ }
3796
+ function editFailureResult(filePath, message) {
3797
+ return {
3798
+ toolResponse: message,
3799
+ data: { path: filePath, success: false, replacements: 0 }
3800
+ };
3452
3801
  }
3453
3802
  var editHandler = async (args, { sandbox }) => {
3454
3803
  const { fs } = sandbox;
3455
3804
  const { file_path, old_string, new_string, replace_all = false } = args;
3456
- if (old_string === new_string) {
3805
+ try {
3806
+ const exists = await fs.exists(file_path);
3807
+ if (!exists) {
3808
+ return editFailureResult(
3809
+ file_path,
3810
+ `Error: File "${file_path}" does not exist.`
3811
+ );
3812
+ }
3813
+ const content = await fs.readFile(file_path);
3814
+ const result = applyEditPlan(content, [
3815
+ { old_string, new_string, replace_all }
3816
+ ]);
3817
+ if (!result.ok) {
3818
+ return editFailureResult(file_path, result.message);
3819
+ }
3820
+ await fs.writeFile(file_path, result.content);
3821
+ const summary = replace_all ? `Replaced ${result.replacements} occurrence(s)` : `Replaced 1 occurrence`;
3457
3822
  return {
3458
- toolResponse: `Error: old_string and new_string must be different.`,
3823
+ toolResponse: `${summary} in ${file_path}`,
3824
+ data: {
3825
+ path: file_path,
3826
+ success: true,
3827
+ replacements: result.replacements,
3828
+ hunks: result.hunks
3829
+ }
3830
+ };
3831
+ } catch (error) {
3832
+ const message = error instanceof Error ? error.message : "Unknown error";
3833
+ return {
3834
+ toolResponse: `Error editing file "${file_path}": ${message}`,
3459
3835
  data: { path: file_path, success: false, replacements: 0 }
3460
3836
  };
3461
3837
  }
3838
+ };
3839
+ var multiEditHandler = async (args, { sandbox }) => {
3840
+ const { fs } = sandbox;
3841
+ const { file_path, edits } = args;
3462
3842
  try {
3463
3843
  const exists = await fs.exists(file_path);
3464
3844
  if (!exists) {
3465
- return {
3466
- toolResponse: `Error: File "${file_path}" does not exist.`,
3467
- data: { path: file_path, success: false, replacements: 0 }
3468
- };
3845
+ return editFailureResult(
3846
+ file_path,
3847
+ `Error: File "${file_path}" does not exist.`
3848
+ );
3469
3849
  }
3470
3850
  const content = await fs.readFile(file_path);
3471
- if (!content.includes(old_string)) {
3472
- return {
3473
- toolResponse: `Error: Could not find the specified text in "${file_path}". Make sure old_string matches exactly (whitespace-sensitive).`,
3474
- data: { path: file_path, success: false, replacements: 0 }
3475
- };
3851
+ const result = applyEditPlan(content, edits);
3852
+ if (!result.ok) {
3853
+ const suffix = result.editIndex === void 0 ? "" : ` in ${file_path}`;
3854
+ return editFailureResult(file_path, `${result.message}${suffix}`);
3476
3855
  }
3477
- const escapedOldString = escapeRegExp(old_string);
3478
- const globalRegex = new RegExp(escapedOldString, "g");
3479
- const occurrences = (content.match(globalRegex) || []).length;
3480
- if (!replace_all && occurrences > 1) {
3481
- return {
3482
- toolResponse: `Error: old_string appears ${occurrences} times in "${file_path}". Either provide more context to make it unique, or use replace_all: true.`,
3483
- data: { path: file_path, success: false, replacements: 0 }
3484
- };
3485
- }
3486
- let newContent;
3487
- let replacements;
3488
- if (replace_all) {
3489
- newContent = content.split(old_string).join(new_string);
3490
- replacements = occurrences;
3491
- } else {
3492
- const index = content.indexOf(old_string);
3493
- newContent = content.slice(0, index) + new_string + content.slice(index + old_string.length);
3494
- replacements = 1;
3495
- }
3496
- await fs.writeFile(file_path, newContent);
3497
- const summary = replace_all ? `Replaced ${replacements} occurrence(s)` : `Replaced 1 occurrence`;
3856
+ await fs.writeFile(file_path, result.content);
3498
3857
  return {
3499
- toolResponse: `${summary} in ${file_path}`,
3500
- data: { path: file_path, success: true, replacements }
3858
+ toolResponse: `Applied ${edits.length} edit(s), ${result.replacements} replacement(s) in ${file_path}`,
3859
+ data: {
3860
+ path: file_path,
3861
+ success: true,
3862
+ replacements: result.replacements,
3863
+ hunks: result.hunks
3864
+ }
3501
3865
  };
3502
3866
  } catch (error) {
3503
3867
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -3685,10 +4049,12 @@ exports.SandboxNotFoundError = SandboxNotFoundError;
3685
4049
  exports.SandboxNotSupportedError = SandboxNotSupportedError;
3686
4050
  exports.THREAD_TTL_SECONDS = THREAD_TTL_SECONDS;
3687
4051
  exports.VirtualFileSystem = VirtualFileSystem;
4052
+ exports.applySnapshot = applySnapshot;
3688
4053
  exports.applyVirtualTreeMutations = applyVirtualTreeMutations;
3689
4054
  exports.askUserQuestionTool = askUserQuestionTool;
3690
4055
  exports.bashHandler = bashHandler;
3691
4056
  exports.bashTool = bashTool;
4057
+ exports.clearHotTier = clearHotTier;
3692
4058
  exports.composeHooks = composeHooks;
3693
4059
  exports.createAgentStateManager = createAgentStateManager;
3694
4060
  exports.createAskUserQuestionHandler = createAskUserQuestionHandler;
@@ -3697,12 +4063,14 @@ exports.createObservabilityHooks = createObservabilityHooks;
3697
4063
  exports.createReadSkillHandler = createReadSkillHandler;
3698
4064
  exports.createReadSkillTool = createReadSkillTool;
3699
4065
  exports.createRunAgentActivity = createRunAgentActivity;
4066
+ exports.createS3ColdStore = createS3ColdStore;
3700
4067
  exports.createSession = createSession;
3701
4068
  exports.createTaskCreateHandler = createTaskCreateHandler;
3702
4069
  exports.createTaskGetHandler = createTaskGetHandler;
3703
4070
  exports.createTaskListHandler = createTaskListHandler;
3704
4071
  exports.createTaskUpdateHandler = createTaskUpdateHandler;
3705
4072
  exports.createThreadManager = createThreadManager;
4073
+ exports.createTieredThreadManager = createTieredThreadManager;
3706
4074
  exports.createToolRouter = createToolRouter;
3707
4075
  exports.createVirtualFsActivities = createVirtualFsActivities;
3708
4076
  exports.defineSubagent = defineSubagent;
@@ -3711,12 +4079,15 @@ exports.defineTool = defineTool;
3711
4079
  exports.defineWorkflow = defineWorkflow;
3712
4080
  exports.editHandler = editHandler;
3713
4081
  exports.editTool = editTool;
4082
+ exports.encodeSnapshot = encodeSnapshot;
3714
4083
  exports.filesWithMimeType = filesWithMimeType;
3715
4084
  exports.formatVirtualFileTree = formatVirtualFileTree;
3716
4085
  exports.getActivityContext = getActivityContext;
3717
4086
  exports.getShortId = getShortId;
4087
+ exports.getThreadDedupKey = getThreadDedupKey;
3718
4088
  exports.getThreadListKey = getThreadListKey;
3719
4089
  exports.getThreadMetaKey = getThreadMetaKey;
4090
+ exports.getThreadStateKey = getThreadStateKey;
3720
4091
  exports.globHandler = globHandler;
3721
4092
  exports.globTool = globTool;
3722
4093
  exports.grepTool = grepTool;
@@ -3724,6 +4095,8 @@ exports.hasDirectory = hasDirectory;
3724
4095
  exports.hasFileWithMimeType = hasFileWithMimeType;
3725
4096
  exports.hasNoOtherToolCalls = hasNoOtherToolCalls;
3726
4097
  exports.isTerminalStatus = isTerminalStatus;
4098
+ exports.multiEditHandler = multiEditHandler;
4099
+ exports.multiEditTool = multiEditTool;
3727
4100
  exports.parseSkillFile = parseSkillFile;
3728
4101
  exports.proxyRunAgent = proxyRunAgent;
3729
4102
  exports.proxyVirtualFsOps = proxyVirtualFsOps;