zeitlich 0.2.46 → 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 (83) hide show
  1. package/README.md +64 -6
  2. package/dist/{activities-CyeiqK_f.d.cts → activities-CPwKoUlD.d.cts} +3 -3
  3. package/dist/{activities-Bm4TLTid.d.ts → activities-DlaBxNID.d.ts} +3 -3
  4. package/dist/adapters/thread/anthropic/index.cjs +105 -6
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +48 -9
  7. package/dist/adapters/thread/anthropic/index.d.ts +48 -9
  8. package/dist/adapters/thread/anthropic/index.js +104 -7
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +38 -22
  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 -22
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.d.cts +6 -5
  17. package/dist/adapters/thread/google-genai/index.d.ts +6 -5
  18. package/dist/adapters/thread/google-genai/workflow.cjs +38 -22
  19. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  20. package/dist/adapters/thread/google-genai/workflow.d.cts +7 -5
  21. package/dist/adapters/thread/google-genai/workflow.d.ts +7 -5
  22. package/dist/adapters/thread/google-genai/workflow.js +38 -22
  23. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  24. package/dist/adapters/thread/langchain/index.d.cts +6 -5
  25. package/dist/adapters/thread/langchain/index.d.ts +6 -5
  26. package/dist/adapters/thread/langchain/workflow.cjs +38 -22
  27. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  28. package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
  29. package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
  30. package/dist/adapters/thread/langchain/workflow.js +38 -22
  31. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  32. package/dist/{cold-store-CFHwemBJ.d.ts → cold-store-BDgJpwLI.d.ts} +8 -11
  33. package/dist/{cold-store-BC5L5Z8A.d.cts → cold-store-Z2wvK2cV.d.cts} +8 -11
  34. package/dist/index.cjs +264 -90
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +21 -9
  37. package/dist/index.d.ts +21 -9
  38. package/dist/index.js +265 -93
  39. package/dist/index.js.map +1 -1
  40. package/dist/proxy-CDh3Rsa7.d.cts +40 -0
  41. package/dist/proxy-Du8ggERu.d.ts +40 -0
  42. package/dist/{thread-manager-D33SUmZa.d.cts → thread-manager-BjoYYXgd.d.cts} +2 -2
  43. package/dist/{thread-manager-9tezUcLW.d.cts → thread-manager-D8zKNFZ9.d.cts} +2 -2
  44. package/dist/{thread-manager-B-zy3xrs.d.ts → thread-manager-DtHYws2F.d.ts} +2 -2
  45. package/dist/{thread-manager-DduoSkvJ.d.ts → thread-manager-Dw96FKH1.d.ts} +2 -2
  46. package/dist/{types-oxt8GN97.d.cts → types-BMJrsHo0.d.cts} +1 -1
  47. package/dist/{types-L5bvbF-n.d.ts → types-CtdOquo3.d.ts} +1 -1
  48. package/dist/{types-CnuN9T6t.d.cts → types-DNEl5uxQ.d.cts} +16 -0
  49. package/dist/{types-CwN6_tAL.d.ts → types-qQVZfhoT.d.ts} +16 -0
  50. package/dist/{workflow-DIaIV7L2.d.cts → workflow-BH9ImDGq.d.cts} +17 -2
  51. package/dist/{workflow-B1TOcHbt.d.ts → workflow-Cdw3-RNB.d.ts} +17 -2
  52. package/dist/workflow.cjs +33 -3
  53. package/dist/workflow.cjs.map +1 -1
  54. package/dist/workflow.d.cts +2 -2
  55. package/dist/workflow.d.ts +2 -2
  56. package/dist/workflow.js +33 -4
  57. package/dist/workflow.js.map +1 -1
  58. package/package.json +9 -3
  59. package/src/adapters/thread/anthropic/activities.ts +18 -11
  60. package/src/adapters/thread/anthropic/index.ts +8 -0
  61. package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
  62. package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
  63. package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
  64. package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
  65. package/src/adapters/thread/anthropic/proxy.ts +1 -0
  66. package/src/adapters/thread/google-genai/proxy.ts +1 -0
  67. package/src/adapters/thread/langchain/proxy.ts +1 -0
  68. package/src/index.ts +1 -1
  69. package/src/lib/subagent/define.ts +1 -0
  70. package/src/lib/subagent/handler.ts +11 -2
  71. package/src/lib/subagent/subagent.integration.test.ts +139 -0
  72. package/src/lib/subagent/types.ts +16 -0
  73. package/src/lib/thread/cold-store.test.ts +33 -5
  74. package/src/lib/thread/cold-store.ts +50 -31
  75. package/src/lib/thread/proxy.ts +79 -29
  76. package/src/tools/edit/handler.test.ts +177 -0
  77. package/src/tools/edit/handler.ts +249 -47
  78. package/src/tools/edit/tool.ts +40 -0
  79. package/src/tools/task-create/handler.ts +1 -1
  80. package/src/tools/task-update/handler.ts +1 -1
  81. package/src/workflow.ts +2 -2
  82. package/dist/proxy-BxFyd6cg.d.cts +0 -24
  83. package/dist/proxy-Cskmj4Yx.d.ts +0 -24
@@ -237,8 +237,17 @@ export function createSubagentHandler<
237
237
 
238
238
  const threadMode = config.thread ?? "new";
239
239
  const allowsContinuation = threadMode !== "new";
240
- const continuationThreadId =
241
- args.threadId && allowsContinuation ? args.threadId : undefined;
240
+ // The parent agent's tool call wins. When `threadId` is omitted,
241
+ // `newThreadSource` decides what to fall back to: `"new"` (default)
242
+ // starts fresh, `"from-parent"` continues/forks the parent's own
243
+ // thread via `context.threadId`. Both paths still require
244
+ // `thread: "fork" | "continue"` — `thread: "new"` always starts
245
+ // fresh regardless of the source.
246
+ const newThreadSource = config.newThreadSource ?? "new";
247
+ const continuationThreadId = !allowsContinuation
248
+ ? undefined
249
+ : (args.threadId ??
250
+ (newThreadSource === "from-parent" ? context.threadId : undefined));
242
251
 
243
252
  // --- Build thread init ---
244
253
  let thread: ThreadInit | undefined;
@@ -635,6 +635,145 @@ describe("createSubagentHandler", () => {
635
635
  expect(workflowInput.thread).toBeUndefined();
636
636
  });
637
637
 
638
+ // --- newThreadSource: "from-parent" ---
639
+
640
+ it("uses the parent's threadId when fork + newThreadSource 'from-parent' and args.threadId is absent", async () => {
641
+ const { executeChild } = await import("@temporalio/workflow");
642
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
643
+
644
+ const subagent: SubagentConfig = {
645
+ agentName: "parent-fork",
646
+ description: "Forks parent thread by default",
647
+ workflow: mockWorkflow(),
648
+ thread: "fork",
649
+ newThreadSource: "from-parent",
650
+ };
651
+
652
+ const { handler } = createSubagentHandler([subagent]);
653
+
654
+ await handler(
655
+ { subagent: "parent-fork", description: "test", prompt: "test" },
656
+ { threadId: "parent-t", toolCallId: "tc", toolName: "Subagent" }
657
+ );
658
+
659
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
660
+ if (!lastCall) throw new Error("expected executeChild call");
661
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
662
+ expect(workflowInput.thread).toEqual({
663
+ mode: "fork",
664
+ threadId: "parent-t",
665
+ });
666
+ });
667
+
668
+ it("uses the parent's threadId when continue + newThreadSource 'from-parent' and args.threadId is absent", async () => {
669
+ const { executeChild } = await import("@temporalio/workflow");
670
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
671
+
672
+ const subagent: SubagentConfig = {
673
+ agentName: "parent-continue",
674
+ description: "Continues parent thread by default",
675
+ workflow: mockWorkflow(),
676
+ thread: "continue",
677
+ newThreadSource: "from-parent",
678
+ };
679
+
680
+ const { handler } = createSubagentHandler([subagent]);
681
+
682
+ await handler(
683
+ { subagent: "parent-continue", description: "test", prompt: "test" },
684
+ { threadId: "parent-t", toolCallId: "tc", toolName: "Subagent" }
685
+ );
686
+
687
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
688
+ if (!lastCall) throw new Error("expected executeChild call");
689
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
690
+ expect(workflowInput.thread).toEqual({
691
+ mode: "continue",
692
+ threadId: "parent-t",
693
+ });
694
+ });
695
+
696
+ it("prefers args.threadId over the parent source when both are available", async () => {
697
+ const { executeChild } = await import("@temporalio/workflow");
698
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
699
+
700
+ const subagent: SubagentConfig = {
701
+ agentName: "parent-fork-explicit",
702
+ description: "Forks parent thread by default",
703
+ workflow: mockWorkflow(),
704
+ thread: "fork",
705
+ newThreadSource: "from-parent",
706
+ };
707
+
708
+ const { handler } = createSubagentHandler([subagent]);
709
+
710
+ await handler(
711
+ {
712
+ subagent: "parent-fork-explicit",
713
+ description: "test",
714
+ prompt: "test",
715
+ threadId: "explicit-prev",
716
+ },
717
+ { threadId: "parent-t", toolCallId: "tc", toolName: "Subagent" }
718
+ );
719
+
720
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
721
+ if (!lastCall) throw new Error("expected executeChild call");
722
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
723
+ expect(workflowInput.thread).toEqual({
724
+ mode: "fork",
725
+ threadId: "explicit-prev",
726
+ });
727
+ });
728
+
729
+ it("ignores newThreadSource 'from-parent' when thread is new", async () => {
730
+ const { executeChild } = await import("@temporalio/workflow");
731
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
732
+
733
+ const subagent: SubagentConfig = {
734
+ agentName: "new-with-source",
735
+ description: "Should still start fresh",
736
+ workflow: mockWorkflow(),
737
+ newThreadSource: "from-parent",
738
+ };
739
+
740
+ const { handler } = createSubagentHandler([subagent]);
741
+
742
+ await handler(
743
+ { subagent: "new-with-source", description: "test", prompt: "test" },
744
+ { threadId: "parent-t", toolCallId: "tc", toolName: "Subagent" }
745
+ );
746
+
747
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
748
+ if (!lastCall) throw new Error("expected executeChild call");
749
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
750
+ expect(workflowInput.thread).toBeUndefined();
751
+ });
752
+
753
+ it("preserves prior behavior when fork is set with default newThreadSource and args.threadId is absent", async () => {
754
+ const { executeChild } = await import("@temporalio/workflow");
755
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
756
+
757
+ const subagent: SubagentConfig = {
758
+ agentName: "fork-default-source",
759
+ description: "Fork with default new-thread source",
760
+ workflow: mockWorkflow(),
761
+ thread: "fork",
762
+ };
763
+
764
+ const { handler } = createSubagentHandler([subagent]);
765
+
766
+ await handler(
767
+ { subagent: "fork-default-source", description: "test", prompt: "test" },
768
+ { threadId: "parent-t", toolCallId: "tc", toolName: "Subagent" }
769
+ );
770
+
771
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
772
+ if (!lastCall) throw new Error("expected executeChild call");
773
+ const workflowInput = lastCall[1].args[1] as SubagentWorkflowInput;
774
+ expect(workflowInput.thread).toBeUndefined();
775
+ });
776
+
638
777
  // --- Sandbox continuation ---
639
778
 
640
779
  it("does not pass sandbox when thread is fork (own sandbox)", async () => {
@@ -632,6 +632,22 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
632
632
  * directly to the existing thread in-place.
633
633
  */
634
634
  thread?: "new" | "fork" | "continue";
635
+ /**
636
+ * Where the subagent's thread comes from when the parent's tool call
637
+ * omits `threadId`. Only meaningful in combination with
638
+ * `thread: "fork"` or `"continue"`.
639
+ *
640
+ * - `"new"` (default) — start a fresh thread (the prior behavior).
641
+ * - `"from-parent"` — use the parent's own `threadId` (from
642
+ * `RouterContext`). With `thread: "fork"` the parent's conversation
643
+ * is copied into a new thread; with `thread: "continue"` the
644
+ * subagent appends to the parent's thread in-place.
645
+ *
646
+ * Has no effect when `thread` is `"new"` (or omitted). A `threadId`
647
+ * supplied by the parent agent always wins — `newThreadSource` only
648
+ * applies when none is provided.
649
+ */
650
+ newThreadSource?: "new" | "from-parent";
635
651
  /**
636
652
  * Sandbox strategy for this subagent.
637
653
  *
@@ -16,9 +16,9 @@ interface CommandInput {
16
16
  }
17
17
 
18
18
  /**
19
- * Minimal in-memory S3-shaped client. The `send()` method dispatches
20
- * by the command's constructor name, mirroring the contract that
21
- * `@aws-sdk/client-s3` exposes.
19
+ * Minimal S3-shaped fake. `send()` dispatches by command constructor
20
+ * name; `config` provides the fields `@aws-sdk/lib-storage`'s
21
+ * single-part upload path inspects.
22
22
  */
23
23
  function createFakeS3(): {
24
24
  s3: S3LikeClient;
@@ -31,7 +31,12 @@ function createFakeS3(): {
31
31
  const compositeKey = (bucket: string, key: string): string =>
32
32
  `${bucket}/${key}`;
33
33
 
34
- const s3: S3LikeClient = {
34
+ const s3 = {
35
+ config: {
36
+ requestHandler: undefined,
37
+ forcePathStyle: false,
38
+ endpoint: async (): Promise<URL> => new URL("https://fake.s3.local"),
39
+ },
35
40
  async send<TInput, TOutput>(
36
41
  command: { input: TInput } & object
37
42
  ): Promise<TOutput> {
@@ -68,7 +73,7 @@ function createFakeS3(): {
68
73
  }
69
74
  throw new Error(`unknown command: ${name}`);
70
75
  },
71
- };
76
+ } as unknown as S3LikeClient;
72
77
 
73
78
  return { s3, store, calls };
74
79
  }
@@ -190,4 +195,27 @@ describe("createS3ColdStore", () => {
190
195
  });
191
196
  expect(await cold.read("messages", "t-1")).toEqual(sampleSnapshot);
192
197
  });
198
+
199
+ it("round-trips a large payload through the async gzip path", async () => {
200
+ // ~1 MB payload — regression guard that large payloads still
201
+ // encode/decode correctly through the promisified gzip path.
202
+ const big: ThreadSnapshot = {
203
+ v: 1,
204
+ messages: Array.from({ length: 500 }, (_, i) =>
205
+ JSON.stringify({
206
+ id: `m${i}`,
207
+ text: "x".repeat(2048),
208
+ })
209
+ ),
210
+ state: null,
211
+ dedupIds: Array.from({ length: 500 }, (_, i) => `m${i}`),
212
+ };
213
+
214
+ const cold = createS3ColdStore({
215
+ s3: fake.s3,
216
+ bucket: "test-bucket",
217
+ });
218
+ await cold.write("messages", "big", big);
219
+ expect(await cold.read("messages", "big")).toEqual(big);
220
+ });
193
221
  });
@@ -18,9 +18,17 @@
18
18
  * last-writer-wins; no compare-and-swap is required.
19
19
  */
20
20
 
21
- import { gzipSync, gunzipSync } from "node:zlib";
21
+ import { gunzip as gunzipCb, gzip as gzipCb } from "node:zlib";
22
+ import { promisify } from "node:util";
22
23
  import type { PersistedThreadState } from "../state/types";
23
- import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
24
+ import { DeleteObjectCommand, GetObjectCommand, type S3Client } from "@aws-sdk/client-s3";
25
+ import { Upload } from "@aws-sdk/lib-storage";
26
+ import { getActivityContext } from "../activity";
27
+
28
+ // Async zlib so gzip/gunzip don't block the worker's event loop
29
+ // during compression of large snapshots.
30
+ const gzipAsync = promisify(gzipCb);
31
+ const gunzipAsync = promisify(gunzipCb);
24
32
 
25
33
  /**
26
34
  * Serialized form of a thread that can be written to and read from a
@@ -69,20 +77,16 @@ export interface ColdThreadStore {
69
77
  }
70
78
 
71
79
  /**
72
- * Compact, duck-typed shape of an S3 client. Zeitlich only needs the
73
- * `send(...)` method; declaring this locally avoids forcing
74
- * `@aws-sdk/client-s3` to be installed when the consumer is using a
75
- * different cold-store backend.
80
+ * Alias for `@aws-sdk/client-s3`'s `S3Client`. The built-in store
81
+ * calls `send(...)` and accesses `client.config` (read by
82
+ * `@aws-sdk/lib-storage`'s `Upload`) a duck-type with just `send`
83
+ * is not sufficient.
76
84
  */
77
- export interface S3LikeClient {
78
- send<TInput, TOutput>(
79
- command: { input: TInput } & object
80
- ): Promise<TOutput>;
81
- }
85
+ export type S3LikeClient = S3Client;
82
86
 
83
87
  /** Configuration for the built-in S3 cold store. */
84
88
  export interface S3ColdStoreConfig {
85
- /** An `@aws-sdk/client-s3` `S3Client` (or duck-typed equivalent). */
89
+ /** An `@aws-sdk/client-s3` `S3Client`. */
86
90
  s3: S3LikeClient;
87
91
  /** S3 bucket that holds the archive. */
88
92
  bucket: string;
@@ -126,10 +130,26 @@ function buildKey(
126
130
  }
127
131
 
128
132
  async function streamToBuffer(
129
- body: unknown
133
+ body: unknown,
134
+ onChunk?: () => void
130
135
  ): Promise<Buffer> {
131
136
  if (body == null) return Buffer.alloc(0);
132
137
  if (body instanceof Uint8Array) return Buffer.from(body);
138
+ // Prefer async iteration so `onChunk` fires per chunk. Node S3
139
+ // bodies (`SdkStream<Readable>`) iterate; bulk-read fallbacks
140
+ // below cover browser body shapes.
141
+ if (
142
+ typeof (body as { [Symbol.asyncIterator]?: unknown })[
143
+ Symbol.asyncIterator
144
+ ] === "function"
145
+ ) {
146
+ const chunks: Buffer[] = [];
147
+ for await (const chunk of body as AsyncIterable<Buffer | Uint8Array>) {
148
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
149
+ onChunk?.();
150
+ }
151
+ return Buffer.concat(chunks);
152
+ }
133
153
  if (typeof (body as { transformToByteArray?: () => Promise<Uint8Array> })
134
154
  .transformToByteArray === "function") {
135
155
  const bytes = await (
@@ -144,12 +164,7 @@ async function streamToBuffer(
144
164
  ).arrayBuffer();
145
165
  return Buffer.from(ab);
146
166
  }
147
- // Node.js Readable stream fallback
148
- const chunks: Buffer[] = [];
149
- for await (const chunk of body as AsyncIterable<Buffer | Uint8Array>) {
150
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
151
- }
152
- return Buffer.concat(chunks);
167
+ return Buffer.alloc(0);
153
168
  }
154
169
 
155
170
  /**
@@ -191,9 +206,10 @@ export function createS3ColdStore(
191
206
  const resp = (await s3.send(
192
207
  new GetObjectCommand({ Bucket: bucket, Key })
193
208
  )) as { Body?: unknown };
194
- const buf = await streamToBuffer(resp.Body);
209
+ const { heartbeat } = getActivityContext();
210
+ const buf = await streamToBuffer(resp.Body, heartbeat);
195
211
  const json = gzip
196
- ? gunzipSync(buf).toString("utf8")
212
+ ? (await gunzipAsync(buf)).toString("utf8")
197
213
  : buf.toString("utf8");
198
214
  return JSON.parse(json) as ThreadSnapshot;
199
215
  } catch (err) {
@@ -209,16 +225,19 @@ export function createS3ColdStore(
209
225
  ): Promise<void> {
210
226
  const Key = buildKey(prefix, threadKey, threadId, gzip);
211
227
  const json = JSON.stringify(snapshot);
212
- const body = gzip ? gzipSync(Buffer.from(json, "utf8")) : json;
213
-
214
- await s3.send(
215
- new PutObjectCommand({
216
- Bucket: bucket,
217
- Key,
218
- Body: body,
219
- ContentType: contentType,
220
- })
221
- );
228
+ const body = gzip ? await gzipAsync(Buffer.from(json, "utf8")) : json;
229
+
230
+ const upload = new Upload({
231
+ client: s3,
232
+ params: { Bucket: bucket, Key, Body: body, ContentType: contentType },
233
+ });
234
+
235
+ // Heartbeat per S3 part completion so a stalled upload trips
236
+ // `heartbeatTimeout` instead of `startToCloseTimeout`.
237
+ const { heartbeat } = getActivityContext();
238
+ if (heartbeat) upload.on("httpUploadProgress", heartbeat);
239
+
240
+ await upload.done();
222
241
  },
223
242
 
224
243
  async delete(
@@ -8,55 +8,105 @@ import {
8
8
  proxyActivities,
9
9
  workflowInfo,
10
10
  type ActivityInterfaceFor,
11
+ type ActivityOptions,
11
12
  } from "@temporalio/workflow";
12
13
  import type { ThreadOps } from "../session/types";
13
14
 
15
+ type OpName = keyof ThreadOps;
16
+
17
+ /** Tight `startToCloseTimeout` so a sick Redis surfaces quickly via retry. */
18
+ const DEFAULT_OPTIONS: ActivityOptions = {
19
+ startToCloseTimeout: "10s",
20
+ retry: {
21
+ maximumAttempts: 6,
22
+ initialInterval: "5s",
23
+ maximumInterval: "15m",
24
+ backoffCoefficient: 4,
25
+ },
26
+ };
27
+
14
28
  /**
15
- * Creates a workflow-safe Temporal activity proxy for {@link ThreadOps}.
29
+ * `heartbeatTimeout` assumes the built-in S3 cold store's progress
30
+ * events (multipart `Upload` + chunked stream read). Stalls trip via
31
+ * heartbeat rather than `startToCloseTimeout`. Custom backends without
32
+ * progress events should override via `perOp`. Harmless on Redis-only
33
+ * deployments — the activities no-op.
34
+ */
35
+ const BUILTIN_PER_OP: Partial<Record<OpName, ActivityOptions>> = {
36
+ hydrateThread: { startToCloseTimeout: "60s", heartbeatTimeout: "15s" },
37
+ flushThread: { startToCloseTimeout: "60s", heartbeatTimeout: "15s" },
38
+ };
39
+
40
+ /**
41
+ * `perOp[op]` layers shallow-rightmost over `defaults` and the
42
+ * built-in cold-tier overlay (`hydrateThread` / `flushThread`).
43
+ * A bare {@link ActivityOptions} is also accepted (treated as `{ defaults }`).
16
44
  *
17
- * The proxy resolves activity names by combining the adapter prefix with
18
- * the workflow scope, so each adapter + workflow combination gets its own
19
- * namespace.
45
+ * @example
46
+ * ```typescript
47
+ * proxyAnthropicThreadOps(undefined, {
48
+ * defaults: { startToCloseTimeout: "5s" },
49
+ * perOp: {
50
+ * flushThread: { startToCloseTimeout: "180s" }, // heartbeatTimeout still inherited
51
+ * },
52
+ * });
53
+ * ```
54
+ */
55
+ export interface ThreadOpsProxyOptions {
56
+ defaults?: ActivityOptions;
57
+ perOp?: Partial<Record<OpName, ActivityOptions>>;
58
+ }
59
+
60
+ function isProxyOptionsShape(o: object): o is ThreadOpsProxyOptions {
61
+ return "defaults" in o || "perOp" in o;
62
+ }
63
+
64
+ /**
65
+ * Creates a workflow-safe Temporal activity proxy for {@link ThreadOps}.
20
66
  *
21
67
  * @param adapterPrefix - Adapter identifier (e.g. "anthropic", "googleGenAI", "langChain")
22
- * @param scope - Optional workflow scope override. Defaults to `workflowInfo().workflowType`.
23
- * @param options - Optional Temporal `proxyActivities` options.
68
+ * @param scope - Workflow scope. Defaults to `workflowInfo().workflowType`.
69
+ * @param options - {@link ThreadOpsProxyOptions} or a bare {@link ActivityOptions}.
24
70
  */
25
71
  export function createThreadOpsProxy(
26
72
  adapterPrefix: string,
27
73
  scope?: string,
28
- options?: Parameters<typeof proxyActivities>[0]
74
+ options?: ActivityOptions | ThreadOpsProxyOptions
29
75
  ): ActivityInterfaceFor<ThreadOps> {
30
76
  const resolvedScope = scope ?? workflowInfo().workflowType;
31
77
 
78
+ const opts: ThreadOpsProxyOptions =
79
+ options && isProxyOptionsShape(options) ? options : { defaults: options };
80
+
81
+ const base = opts.defaults ?? DEFAULT_OPTIONS;
32
82
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
- const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
34
- options ?? {
35
- startToCloseTimeout: "10s",
36
- retry: {
37
- maximumAttempts: 6,
38
- initialInterval: "5s",
39
- maximumInterval: "15m",
40
- backoffCoefficient: 4,
41
- },
42
- }
43
- );
83
+ const baseActs = proxyActivities<Record<string, (...args: any[]) => any>>(base);
44
84
 
45
85
  const prefix = `${adapterPrefix}${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
46
86
  const p = (key: string): string =>
47
87
  `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`;
48
88
 
89
+ const pick = (op: OpName): unknown => {
90
+ const overlay = { ...BUILTIN_PER_OP[op], ...opts.perOp?.[op] };
91
+ if (Object.keys(overlay).length === 0) return baseActs[p(op)];
92
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
+ return proxyActivities<Record<string, (...args: any[]) => any>>({
94
+ ...base,
95
+ ...overlay,
96
+ })[p(op)];
97
+ };
98
+
49
99
  return {
50
- initializeThread: acts[p("initializeThread")],
51
- appendHumanMessage: acts[p("appendHumanMessage")],
52
- appendToolResult: acts[p("appendToolResult")],
53
- appendAgentMessage: acts[p("appendAgentMessage")],
54
- appendSystemMessage: acts[p("appendSystemMessage")],
55
- forkThread: acts[p("forkThread")],
56
- truncateThread: acts[p("truncateThread")],
57
- loadThreadState: acts[p("loadThreadState")],
58
- saveThreadState: acts[p("saveThreadState")],
59
- hydrateThread: acts[p("hydrateThread")],
60
- flushThread: acts[p("flushThread")],
100
+ initializeThread: pick("initializeThread"),
101
+ appendHumanMessage: pick("appendHumanMessage"),
102
+ appendToolResult: pick("appendToolResult"),
103
+ appendAgentMessage: pick("appendAgentMessage"),
104
+ appendSystemMessage: pick("appendSystemMessage"),
105
+ forkThread: pick("forkThread"),
106
+ truncateThread: pick("truncateThread"),
107
+ loadThreadState: pick("loadThreadState"),
108
+ saveThreadState: pick("saveThreadState"),
109
+ hydrateThread: pick("hydrateThread"),
110
+ flushThread: pick("flushThread"),
61
111
  } as ActivityInterfaceFor<ThreadOps>;
62
112
  }