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.
- package/README.md +64 -6
- package/dist/{activities-CyeiqK_f.d.cts → activities-CPwKoUlD.d.cts} +3 -3
- package/dist/{activities-Bm4TLTid.d.ts → activities-DlaBxNID.d.ts} +3 -3
- package/dist/adapters/thread/anthropic/index.cjs +105 -6
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +48 -9
- package/dist/adapters/thread/anthropic/index.d.ts +48 -9
- package/dist/adapters/thread/anthropic/index.js +104 -7
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +38 -22
- package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
- package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
- package/dist/adapters/thread/anthropic/workflow.js +38 -22
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +6 -5
- package/dist/adapters/thread/google-genai/index.d.ts +6 -5
- package/dist/adapters/thread/google-genai/workflow.cjs +38 -22
- package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +7 -5
- package/dist/adapters/thread/google-genai/workflow.d.ts +7 -5
- package/dist/adapters/thread/google-genai/workflow.js +38 -22
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +6 -5
- package/dist/adapters/thread/langchain/index.d.ts +6 -5
- package/dist/adapters/thread/langchain/workflow.cjs +38 -22
- package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
- package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
- package/dist/adapters/thread/langchain/workflow.js +38 -22
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/{cold-store-CFHwemBJ.d.ts → cold-store-BDgJpwLI.d.ts} +8 -11
- package/dist/{cold-store-BC5L5Z8A.d.cts → cold-store-Z2wvK2cV.d.cts} +8 -11
- package/dist/index.cjs +264 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -9
- package/dist/index.d.ts +21 -9
- package/dist/index.js +265 -93
- package/dist/index.js.map +1 -1
- package/dist/proxy-CDh3Rsa7.d.cts +40 -0
- package/dist/proxy-Du8ggERu.d.ts +40 -0
- package/dist/{thread-manager-D33SUmZa.d.cts → thread-manager-BjoYYXgd.d.cts} +2 -2
- package/dist/{thread-manager-9tezUcLW.d.cts → thread-manager-D8zKNFZ9.d.cts} +2 -2
- package/dist/{thread-manager-B-zy3xrs.d.ts → thread-manager-DtHYws2F.d.ts} +2 -2
- package/dist/{thread-manager-DduoSkvJ.d.ts → thread-manager-Dw96FKH1.d.ts} +2 -2
- package/dist/{types-oxt8GN97.d.cts → types-BMJrsHo0.d.cts} +1 -1
- package/dist/{types-L5bvbF-n.d.ts → types-CtdOquo3.d.ts} +1 -1
- package/dist/{types-CnuN9T6t.d.cts → types-DNEl5uxQ.d.cts} +16 -0
- package/dist/{types-CwN6_tAL.d.ts → types-qQVZfhoT.d.ts} +16 -0
- package/dist/{workflow-DIaIV7L2.d.cts → workflow-BH9ImDGq.d.cts} +17 -2
- package/dist/{workflow-B1TOcHbt.d.ts → workflow-Cdw3-RNB.d.ts} +17 -2
- package/dist/workflow.cjs +33 -3
- 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 +33 -4
- package/dist/workflow.js.map +1 -1
- package/package.json +9 -3
- package/src/adapters/thread/anthropic/activities.ts +18 -11
- package/src/adapters/thread/anthropic/index.ts +8 -0
- package/src/adapters/thread/anthropic/model-invoker.test.ts +110 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +26 -5
- package/src/adapters/thread/anthropic/prompt-cache.test.ts +134 -0
- package/src/adapters/thread/anthropic/prompt-cache.ts +163 -0
- package/src/adapters/thread/anthropic/proxy.ts +1 -0
- package/src/adapters/thread/google-genai/proxy.ts +1 -0
- package/src/adapters/thread/langchain/proxy.ts +1 -0
- package/src/index.ts +1 -1
- package/src/lib/subagent/define.ts +1 -0
- package/src/lib/subagent/handler.ts +11 -2
- package/src/lib/subagent/subagent.integration.test.ts +139 -0
- package/src/lib/subagent/types.ts +16 -0
- package/src/lib/thread/cold-store.test.ts +33 -5
- package/src/lib/thread/cold-store.ts +50 -31
- package/src/lib/thread/proxy.ts +79 -29
- package/src/tools/edit/handler.test.ts +177 -0
- package/src/tools/edit/handler.ts +249 -47
- package/src/tools/edit/tool.ts +40 -0
- package/src/tools/task-create/handler.ts +1 -1
- package/src/tools/task-update/handler.ts +1 -1
- package/src/workflow.ts +2 -2
- package/dist/proxy-BxFyd6cg.d.cts +0 -24
- 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
|
-
|
|
241
|
-
|
|
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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
*
|
|
73
|
-
* `send(...)`
|
|
74
|
-
* `@aws-sdk/
|
|
75
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
209
|
+
const { heartbeat } = getActivityContext();
|
|
210
|
+
const buf = await streamToBuffer(resp.Body, heartbeat);
|
|
195
211
|
const json = gzip
|
|
196
|
-
?
|
|
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 ?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
package/src/lib/thread/proxy.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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 -
|
|
23
|
-
* @param 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?:
|
|
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
|
|
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:
|
|
51
|
-
appendHumanMessage:
|
|
52
|
-
appendToolResult:
|
|
53
|
-
appendAgentMessage:
|
|
54
|
-
appendSystemMessage:
|
|
55
|
-
forkThread:
|
|
56
|
-
truncateThread:
|
|
57
|
-
loadThreadState:
|
|
58
|
-
saveThreadState:
|
|
59
|
-
hydrateThread:
|
|
60
|
-
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
|
}
|