zeitlich 0.2.38 → 0.2.39
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 +18 -0
- package/dist/{activities-BKhMtKDd.d.ts → activities-Bmu7XnaG.d.ts} +4 -6
- package/dist/{activities-CDcwkRZs.d.cts → activities-ByBFLvm2.d.cts} +4 -6
- package/dist/adapter-id-BB-mmrts.d.cts +17 -0
- package/dist/adapter-id-BB-mmrts.d.ts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
- package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
- package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
- package/dist/adapters/thread/anthropic/index.cjs +140 -23
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +8 -7
- package/dist/adapters/thread/anthropic/index.d.ts +8 -7
- package/dist/adapters/thread/anthropic/index.js +140 -24
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.cjs +8 -3
- 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 +8 -4
- package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.cjs +140 -23
- 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 +140 -24
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.cjs +8 -3
- 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 +8 -4
- package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
- package/dist/adapters/thread/index.cjs +16 -0
- package/dist/adapters/thread/index.cjs.map +1 -0
- package/dist/adapters/thread/index.d.cts +34 -0
- package/dist/adapters/thread/index.d.ts +34 -0
- package/dist/adapters/thread/index.js +12 -0
- package/dist/adapters/thread/index.js.map +1 -0
- package/dist/adapters/thread/langchain/index.cjs +139 -24
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +8 -7
- package/dist/adapters/thread/langchain/index.d.ts +8 -7
- package/dist/adapters/thread/langchain/index.js +139 -25
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.cjs +8 -3
- 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 +8 -4
- package/dist/adapters/thread/langchain/workflow.js.map +1 -1
- package/dist/index.cjs +266 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +263 -49
- package/dist/index.js.map +1 -1
- package/dist/{proxy-D_3x7RN4.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
- package/dist/{proxy-CUlKSvZS.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
- package/dist/{thread-manager-CVu7o2cs.d.ts → thread-manager-CcRXasqs.d.ts} +2 -4
- package/dist/{thread-manager-HSwyh28L.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -4
- package/dist/{thread-manager-c1gPopAG.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -4
- package/dist/{thread-manager-wGi-LqIP.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -4
- package/dist/{types-C06FwR96.d.cts → types-Bcbiq8iv.d.cts} +162 -44
- package/dist/{types-BH_IRryz.d.ts → types-DpHTX-iO.d.ts} +54 -6
- package/dist/{types-DNr31FzL.d.ts → types-Dt8-HBBT.d.ts} +162 -44
- package/dist/{types-BaOw4hKI.d.cts → types-hFFi-Zd9.d.cts} +54 -6
- package/dist/{workflow-CSCkpwAL.d.ts → workflow-Bmf9EtDW.d.ts} +82 -2
- package/dist/{workflow-DuvMZ8Vm.d.cts → workflow-Bx9utBwb.d.cts} +82 -2
- package/dist/workflow.cjs +188 -37
- 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 +185 -38
- package/dist/workflow.js.map +1 -1
- package/package.json +11 -1
- package/src/adapters/thread/adapter-id.test.ts +42 -0
- package/src/adapters/thread/anthropic/activities.ts +33 -7
- package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
- package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
- package/src/adapters/thread/anthropic/index.ts +3 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +8 -4
- package/src/adapters/thread/anthropic/proxy.ts +3 -2
- package/src/adapters/thread/anthropic/thread-manager.ts +27 -4
- package/src/adapters/thread/google-genai/activities.ts +33 -7
- package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
- package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
- package/src/adapters/thread/google-genai/index.ts +3 -0
- package/src/adapters/thread/google-genai/model-invoker.ts +7 -3
- package/src/adapters/thread/google-genai/proxy.ts +3 -2
- package/src/adapters/thread/google-genai/thread-manager.ts +27 -4
- package/src/adapters/thread/index.ts +39 -0
- package/src/adapters/thread/langchain/activities.ts +33 -7
- package/src/adapters/thread/langchain/adapter-id.ts +16 -0
- package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
- package/src/adapters/thread/langchain/index.ts +3 -0
- package/src/adapters/thread/langchain/model-invoker.ts +8 -3
- package/src/adapters/thread/langchain/proxy.ts +3 -2
- package/src/adapters/thread/langchain/thread-manager.ts +27 -4
- package/src/lib/lifecycle.ts +3 -1
- package/src/lib/model/types.ts +7 -10
- package/src/lib/session/session-edge-cases.integration.test.ts +131 -63
- package/src/lib/session/session.integration.test.ts +174 -5
- package/src/lib/session/session.ts +68 -28
- package/src/lib/session/types.ts +60 -9
- package/src/lib/state/index.ts +1 -0
- package/src/lib/state/manager.integration.test.ts +109 -0
- package/src/lib/state/manager.ts +38 -8
- package/src/lib/state/types.ts +25 -0
- package/src/lib/subagent/handler.ts +124 -11
- package/src/lib/subagent/index.ts +5 -1
- package/src/lib/subagent/subagent.integration.test.ts +528 -0
- package/src/lib/subagent/types.ts +63 -14
- package/src/lib/subagent/workflow.ts +29 -2
- package/src/lib/thread/index.ts +5 -0
- package/src/lib/thread/keys.test.ts +101 -0
- package/src/lib/thread/keys.ts +94 -0
- package/src/lib/thread/manager.test.ts +139 -0
- package/src/lib/thread/manager.ts +92 -14
- package/src/lib/thread/proxy.ts +2 -0
- package/src/lib/thread/types.ts +60 -6
- package/src/lib/tool-router/types.ts +16 -8
- package/src/lib/types.ts +12 -0
- package/src/workflow.ts +12 -1
- package/tsup.config.ts +1 -0
|
@@ -1810,6 +1810,173 @@ describe("createSubagentHandler", () => {
|
|
|
1810
1810
|
expect(deletedTags.sort()).toEqual(["base", "exit-2"]);
|
|
1811
1811
|
});
|
|
1812
1812
|
|
|
1813
|
+
it("publishes persistentBaseSnapshot from childSandboxReadySignal before child completes", async () => {
|
|
1814
|
+
const { setHandler, executeChild } = await import("@temporalio/workflow");
|
|
1815
|
+
const setHandlerMock = setHandler as ReturnType<typeof vi.fn>;
|
|
1816
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
1817
|
+
|
|
1818
|
+
const signalBase = {
|
|
1819
|
+
sandboxId: "sb-signal",
|
|
1820
|
+
providerId: "test",
|
|
1821
|
+
data: { tag: "signal-base" },
|
|
1822
|
+
createdAt: new Date().toISOString(),
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
const config: SubagentConfig = {
|
|
1826
|
+
agentName: "snap-agent",
|
|
1827
|
+
description: "Snapshot-driven",
|
|
1828
|
+
workflow: mockWorkflow(),
|
|
1829
|
+
thread: "continue",
|
|
1830
|
+
sandbox: {
|
|
1831
|
+
source: "own",
|
|
1832
|
+
init: "once",
|
|
1833
|
+
continuation: "snapshot",
|
|
1834
|
+
proxy: noopSandboxProxy,
|
|
1835
|
+
},
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
const { handler } = createSubagentHandler([config]);
|
|
1839
|
+
|
|
1840
|
+
// Fire the signal from inside executeChild so persistentBaseSnapshot is
|
|
1841
|
+
// set before the child's own result is returned. The child result itself
|
|
1842
|
+
// intentionally omits baseSnapshot — only the signal path can populate
|
|
1843
|
+
// the map for this call.
|
|
1844
|
+
execMock.mockImplementationOnce(
|
|
1845
|
+
async (_wf: unknown, opts: { workflowId: string; args: unknown[] }) => {
|
|
1846
|
+
const reg = setHandlerMock.mock.calls
|
|
1847
|
+
.filter(
|
|
1848
|
+
([sig]) => (sig as { name?: string })?.name === "childSandboxReady"
|
|
1849
|
+
)
|
|
1850
|
+
.at(-1);
|
|
1851
|
+
const signalHandler = reg?.[1] as
|
|
1852
|
+
| ((p: {
|
|
1853
|
+
childWorkflowId: string;
|
|
1854
|
+
sandboxId: string;
|
|
1855
|
+
baseSnapshot?: unknown;
|
|
1856
|
+
}) => void)
|
|
1857
|
+
| undefined;
|
|
1858
|
+
signalHandler?.({
|
|
1859
|
+
childWorkflowId: opts.workflowId,
|
|
1860
|
+
sandboxId: "sb-signal",
|
|
1861
|
+
baseSnapshot: signalBase,
|
|
1862
|
+
});
|
|
1863
|
+
return {
|
|
1864
|
+
toolResponse: "first",
|
|
1865
|
+
data: null,
|
|
1866
|
+
threadId: "child-sig-1",
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
await handler(
|
|
1872
|
+
{ subagent: "snap-agent", description: "test", prompt: "first" },
|
|
1873
|
+
{ threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
|
|
1874
|
+
);
|
|
1875
|
+
|
|
1876
|
+
// Second call on a new thread must boot from the signal-published base.
|
|
1877
|
+
nextStartChildResult = () => ({
|
|
1878
|
+
toolResponse: "second",
|
|
1879
|
+
data: null,
|
|
1880
|
+
threadId: "child-sig-2",
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
await handler(
|
|
1884
|
+
{ subagent: "snap-agent", description: "test", prompt: "second" },
|
|
1885
|
+
{ threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
|
|
1886
|
+
);
|
|
1887
|
+
|
|
1888
|
+
const secondCall = execMock.mock.calls.at(-1);
|
|
1889
|
+
if (!secondCall) throw new Error("expected executeChild call");
|
|
1890
|
+
const secondInput = secondCall[1].args[1] as SubagentWorkflowInput;
|
|
1891
|
+
expect(secondInput.sandbox).toEqual({
|
|
1892
|
+
mode: "from-snapshot",
|
|
1893
|
+
snapshot: expect.objectContaining({ data: { tag: "signal-base" } }),
|
|
1894
|
+
});
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
it("ignores signal baseSnapshot for non-snapshot-base creators", async () => {
|
|
1898
|
+
const { setHandler, executeChild } = await import("@temporalio/workflow");
|
|
1899
|
+
const setHandlerMock = setHandler as ReturnType<typeof vi.fn>;
|
|
1900
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
1901
|
+
|
|
1902
|
+
const config: SubagentConfig = {
|
|
1903
|
+
agentName: "lazy-fork-agent",
|
|
1904
|
+
description: "Lazy fork (not snapshot)",
|
|
1905
|
+
workflow: mockWorkflow(),
|
|
1906
|
+
sandbox: {
|
|
1907
|
+
source: "own",
|
|
1908
|
+
init: "once",
|
|
1909
|
+
continuation: "fork",
|
|
1910
|
+
proxy: noopSandboxProxy,
|
|
1911
|
+
},
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
const { handler } = createSubagentHandler([config]);
|
|
1915
|
+
|
|
1916
|
+
execMock.mockImplementationOnce(
|
|
1917
|
+
async (_wf: unknown, opts: { workflowId: string; args: unknown[] }) => {
|
|
1918
|
+
const reg = setHandlerMock.mock.calls
|
|
1919
|
+
.filter(
|
|
1920
|
+
([sig]) => (sig as { name?: string })?.name === "childSandboxReady"
|
|
1921
|
+
)
|
|
1922
|
+
.at(-1);
|
|
1923
|
+
const signalHandler = reg?.[1] as
|
|
1924
|
+
| ((p: {
|
|
1925
|
+
childWorkflowId: string;
|
|
1926
|
+
sandboxId: string;
|
|
1927
|
+
baseSnapshot?: unknown;
|
|
1928
|
+
}) => void)
|
|
1929
|
+
| undefined;
|
|
1930
|
+
// Stray baseSnapshot on a non-snapshot path must not land in
|
|
1931
|
+
// persistentBaseSnapshot, or it would corrupt a different agent's
|
|
1932
|
+
// snapshot flow.
|
|
1933
|
+
signalHandler?.({
|
|
1934
|
+
childWorkflowId: opts.workflowId,
|
|
1935
|
+
sandboxId: "sb-lazy",
|
|
1936
|
+
baseSnapshot: {
|
|
1937
|
+
sandboxId: "sb-lazy",
|
|
1938
|
+
providerId: "test",
|
|
1939
|
+
data: { tag: "should-be-ignored" },
|
|
1940
|
+
createdAt: new Date().toISOString(),
|
|
1941
|
+
},
|
|
1942
|
+
});
|
|
1943
|
+
return {
|
|
1944
|
+
toolResponse: "first",
|
|
1945
|
+
data: null,
|
|
1946
|
+
threadId: "child-lazy-1",
|
|
1947
|
+
sandboxId: "sb-lazy",
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
);
|
|
1951
|
+
|
|
1952
|
+
await handler(
|
|
1953
|
+
{ subagent: "lazy-fork-agent", description: "test", prompt: "first" },
|
|
1954
|
+
{ threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
// Second call should fork from the lazy-published sandbox, not restore
|
|
1958
|
+
// from any snapshot.
|
|
1959
|
+
nextStartChildResult = () => ({
|
|
1960
|
+
toolResponse: "second",
|
|
1961
|
+
data: null,
|
|
1962
|
+
threadId: "child-lazy-2",
|
|
1963
|
+
sandboxId: "sb-lazy",
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
await handler(
|
|
1967
|
+
{ subagent: "lazy-fork-agent", description: "test", prompt: "second" },
|
|
1968
|
+
{ threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1971
|
+
const secondCall = execMock.mock.calls.at(-1);
|
|
1972
|
+
if (!secondCall) throw new Error("expected executeChild call");
|
|
1973
|
+
const secondInput = secondCall[1].args[1] as SubagentWorkflowInput;
|
|
1974
|
+
expect(secondInput.sandbox).toEqual({
|
|
1975
|
+
mode: "fork",
|
|
1976
|
+
sandboxId: "sb-lazy",
|
|
1977
|
+
});
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1813
1980
|
it("does not call deleteSandboxSnapshot for children that produced no snapshots", async () => {
|
|
1814
1981
|
const opsMock = makeMockSandboxOps();
|
|
1815
1982
|
const config: SubagentConfig = {
|
|
@@ -1843,6 +2010,195 @@ describe("createSubagentHandler", () => {
|
|
|
1843
2010
|
|
|
1844
2011
|
expect(opsMock.deleteSandboxSnapshot).not.toHaveBeenCalled();
|
|
1845
2012
|
});
|
|
2013
|
+
|
|
2014
|
+
// -------------------------------------------------------------------------
|
|
2015
|
+
// Child workflow failure propagation
|
|
2016
|
+
// -------------------------------------------------------------------------
|
|
2017
|
+
|
|
2018
|
+
it("propagates executeChild failure to the caller instead of hanging", async () => {
|
|
2019
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
2020
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
2021
|
+
execMock.mockImplementationOnce(async () => {
|
|
2022
|
+
throw new Error("Child Workflow execution failed: timeout");
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
const { handler } = createSubagentHandler([
|
|
2026
|
+
{
|
|
2027
|
+
agentName: "researcher",
|
|
2028
|
+
description: "Researches topics",
|
|
2029
|
+
workflow: mockWorkflow("researcherWorkflow"),
|
|
2030
|
+
},
|
|
2031
|
+
]);
|
|
2032
|
+
|
|
2033
|
+
await expect(
|
|
2034
|
+
handler(
|
|
2035
|
+
{ subagent: "researcher", description: "test", prompt: "hi" },
|
|
2036
|
+
{ threadId: "t", toolCallId: "tc", toolName: "Subagent" }
|
|
2037
|
+
)
|
|
2038
|
+
).rejects.toThrow("Child Workflow execution failed: timeout");
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
it("applies a default workflowRunTimeout when workflowOptions is omitted", async () => {
|
|
2042
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
2043
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
2044
|
+
|
|
2045
|
+
const { DEFAULT_SUBAGENT_WORKFLOW_RUN_TIMEOUT } = await import("./handler");
|
|
2046
|
+
|
|
2047
|
+
const { handler } = createSubagentHandler([
|
|
2048
|
+
{
|
|
2049
|
+
agentName: "researcher",
|
|
2050
|
+
description: "Researches topics",
|
|
2051
|
+
workflow: mockWorkflow("researcherWorkflow"),
|
|
2052
|
+
},
|
|
2053
|
+
]);
|
|
2054
|
+
|
|
2055
|
+
await handler(
|
|
2056
|
+
{ subagent: "researcher", description: "test", prompt: "hi" },
|
|
2057
|
+
{ threadId: "t", toolCallId: "tc", toolName: "Subagent" }
|
|
2058
|
+
);
|
|
2059
|
+
|
|
2060
|
+
const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
|
|
2061
|
+
if (!lastCall) throw new Error("expected executeChild call");
|
|
2062
|
+
expect(lastCall[1].workflowRunTimeout).toBe(
|
|
2063
|
+
DEFAULT_SUBAGENT_WORKFLOW_RUN_TIMEOUT
|
|
2064
|
+
);
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
it("lets workflowOptions.workflowRunTimeout override the default", async () => {
|
|
2068
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
2069
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
2070
|
+
|
|
2071
|
+
const { handler } = createSubagentHandler([
|
|
2072
|
+
{
|
|
2073
|
+
agentName: "researcher",
|
|
2074
|
+
description: "Researches topics",
|
|
2075
|
+
workflow: mockWorkflow("researcherWorkflow"),
|
|
2076
|
+
workflowOptions: { workflowRunTimeout: "30s" },
|
|
2077
|
+
},
|
|
2078
|
+
]);
|
|
2079
|
+
|
|
2080
|
+
await handler(
|
|
2081
|
+
{ subagent: "researcher", description: "test", prompt: "hi" },
|
|
2082
|
+
{ threadId: "t", toolCallId: "tc", toolName: "Subagent" }
|
|
2083
|
+
);
|
|
2084
|
+
|
|
2085
|
+
const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
|
|
2086
|
+
if (!lastCall) throw new Error("expected executeChild call");
|
|
2087
|
+
expect(lastCall[1].workflowRunTimeout).toBe("30s");
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
it("forwards workflowOptions to executeChild", async () => {
|
|
2091
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
2092
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
2093
|
+
|
|
2094
|
+
const { handler } = createSubagentHandler([
|
|
2095
|
+
{
|
|
2096
|
+
agentName: "researcher",
|
|
2097
|
+
description: "Researches topics",
|
|
2098
|
+
workflow: mockWorkflow("researcherWorkflow"),
|
|
2099
|
+
workflowOptions: {
|
|
2100
|
+
workflowRunTimeout: "5m",
|
|
2101
|
+
workflowTaskTimeout: "30s",
|
|
2102
|
+
retry: { maximumAttempts: 1 },
|
|
2103
|
+
},
|
|
2104
|
+
},
|
|
2105
|
+
]);
|
|
2106
|
+
|
|
2107
|
+
await handler(
|
|
2108
|
+
{ subagent: "researcher", description: "test", prompt: "hi" },
|
|
2109
|
+
{ threadId: "t", toolCallId: "tc", toolName: "Subagent" }
|
|
2110
|
+
);
|
|
2111
|
+
|
|
2112
|
+
const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
|
|
2113
|
+
if (!lastCall) throw new Error("expected executeChild call");
|
|
2114
|
+
expect(lastCall[1]).toMatchObject({
|
|
2115
|
+
workflowRunTimeout: "5m",
|
|
2116
|
+
workflowTaskTimeout: "30s",
|
|
2117
|
+
retry: { maximumAttempts: 1 },
|
|
2118
|
+
});
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
it("does not let workflowOptions override workflowId, taskQueue, or args", async () => {
|
|
2122
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
2123
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
2124
|
+
|
|
2125
|
+
const { handler } = createSubagentHandler([
|
|
2126
|
+
{
|
|
2127
|
+
agentName: "researcher",
|
|
2128
|
+
description: "Researches topics",
|
|
2129
|
+
workflow: mockWorkflow("researcherWorkflow"),
|
|
2130
|
+
taskQueue: "my-queue",
|
|
2131
|
+
workflowOptions: {
|
|
2132
|
+
// Intentionally violates the public Omit<> type to prove the
|
|
2133
|
+
// handler still wins at runtime. Cast removes the type error.
|
|
2134
|
+
...({
|
|
2135
|
+
workflowId: "forbidden-id",
|
|
2136
|
+
taskQueue: "forbidden-queue",
|
|
2137
|
+
args: ["forbidden"],
|
|
2138
|
+
} as Record<string, unknown>),
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
]);
|
|
2142
|
+
|
|
2143
|
+
await handler(
|
|
2144
|
+
{ subagent: "researcher", description: "test", prompt: "hello" },
|
|
2145
|
+
{ threadId: "t", toolCallId: "tc", toolName: "Subagent" }
|
|
2146
|
+
);
|
|
2147
|
+
|
|
2148
|
+
const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
|
|
2149
|
+
if (!lastCall) throw new Error("expected executeChild call");
|
|
2150
|
+
expect(lastCall[1].workflowId).not.toBe("forbidden-id");
|
|
2151
|
+
expect(lastCall[1].workflowId).toMatch(/^researcher-/);
|
|
2152
|
+
expect(lastCall[1].taskQueue).toBe("my-queue");
|
|
2153
|
+
expect(lastCall[1].args[0]).toBe("hello");
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
it("clears lazy-creator bookkeeping on failure so the next call can re-try", async () => {
|
|
2157
|
+
const opsMock = makeMockSandboxOps();
|
|
2158
|
+
const { executeChild } = await import("@temporalio/workflow");
|
|
2159
|
+
const execMock = executeChild as ReturnType<typeof vi.fn>;
|
|
2160
|
+
|
|
2161
|
+
const lazySubagent: SubagentConfig = {
|
|
2162
|
+
agentName: "lazy",
|
|
2163
|
+
description: "Lazy sandbox init",
|
|
2164
|
+
workflow: mockWorkflow(),
|
|
2165
|
+
sandbox: {
|
|
2166
|
+
source: "own",
|
|
2167
|
+
init: "once",
|
|
2168
|
+
continuation: "fork",
|
|
2169
|
+
proxy: () => opsMock,
|
|
2170
|
+
},
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
const { handler } = createSubagentHandler([lazySubagent]);
|
|
2174
|
+
|
|
2175
|
+
execMock.mockImplementationOnce(async () => {
|
|
2176
|
+
throw new Error("init failed");
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
await expect(
|
|
2180
|
+
handler(
|
|
2181
|
+
{ subagent: "lazy", description: "test", prompt: "first" },
|
|
2182
|
+
{ threadId: "t", toolCallId: "tc-1", toolName: "Subagent" }
|
|
2183
|
+
)
|
|
2184
|
+
).rejects.toThrow("init failed");
|
|
2185
|
+
|
|
2186
|
+
// A second call must be able to take the creator role again (no stranded
|
|
2187
|
+
// "creating" flag) and succeed.
|
|
2188
|
+
nextStartChildResult = () => ({
|
|
2189
|
+
toolResponse: "ok",
|
|
2190
|
+
data: null,
|
|
2191
|
+
threadId: "child-t-2",
|
|
2192
|
+
sandboxId: "child-sb-2",
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
const result = await handler(
|
|
2196
|
+
{ subagent: "lazy", description: "test", prompt: "second" },
|
|
2197
|
+
{ threadId: "t", toolCallId: "tc-2", toolName: "Subagent" }
|
|
2198
|
+
);
|
|
2199
|
+
|
|
2200
|
+
expect(result.toolResponse).toBe("ok");
|
|
2201
|
+
});
|
|
1846
2202
|
});
|
|
1847
2203
|
|
|
1848
2204
|
// ---------------------------------------------------------------------------
|
|
@@ -2050,6 +2406,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2050
2406
|
sandboxShutdown: "destroy",
|
|
2051
2407
|
thread: { mode: "fork", threadId: "prev-42" },
|
|
2052
2408
|
onSandboxReady: expect.any(Function),
|
|
2409
|
+
onSessionExit: expect.any(Function),
|
|
2053
2410
|
});
|
|
2054
2411
|
});
|
|
2055
2412
|
|
|
@@ -2069,6 +2426,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2069
2426
|
sandboxShutdown: "destroy",
|
|
2070
2427
|
sandbox: { mode: "inherit", sandboxId: "sb-123" },
|
|
2071
2428
|
onSandboxReady: expect.any(Function),
|
|
2429
|
+
onSessionExit: expect.any(Function),
|
|
2072
2430
|
});
|
|
2073
2431
|
});
|
|
2074
2432
|
|
|
@@ -2088,6 +2446,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2088
2446
|
sandboxShutdown: "destroy",
|
|
2089
2447
|
sandbox: { mode: "fork", sandboxId: "prev-sb-1" },
|
|
2090
2448
|
onSandboxReady: expect.any(Function),
|
|
2449
|
+
onSessionExit: expect.any(Function),
|
|
2091
2450
|
});
|
|
2092
2451
|
});
|
|
2093
2452
|
|
|
@@ -2111,6 +2470,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2111
2470
|
thread: { mode: "fork", threadId: "prev-t" },
|
|
2112
2471
|
sandbox: { mode: "fork", sandboxId: "prev-sb" },
|
|
2113
2472
|
onSandboxReady: expect.any(Function),
|
|
2473
|
+
onSessionExit: expect.any(Function),
|
|
2114
2474
|
});
|
|
2115
2475
|
});
|
|
2116
2476
|
|
|
@@ -2181,6 +2541,7 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2181
2541
|
agentName: "test",
|
|
2182
2542
|
sandboxShutdown: "destroy",
|
|
2183
2543
|
onSandboxReady: expect.any(Function),
|
|
2544
|
+
onSessionExit: expect.any(Function),
|
|
2184
2545
|
});
|
|
2185
2546
|
});
|
|
2186
2547
|
|
|
@@ -2203,4 +2564,171 @@ describe("defineSubagentWorkflow", () => {
|
|
|
2203
2564
|
|
|
2204
2565
|
expect(capturedSession?.sandboxShutdown).toBe("keep-until-parent-close");
|
|
2205
2566
|
});
|
|
2567
|
+
|
|
2568
|
+
// -------------------------------------------------------------------------
|
|
2569
|
+
// Auto-forwarding of session outputs + signal payload
|
|
2570
|
+
// -------------------------------------------------------------------------
|
|
2571
|
+
|
|
2572
|
+
it("auto-forwards baseSnapshot captured via onSandboxReady", async () => {
|
|
2573
|
+
const baseSnapshot = {
|
|
2574
|
+
sandboxId: "sb-1",
|
|
2575
|
+
providerId: "test",
|
|
2576
|
+
data: { tag: "base" },
|
|
2577
|
+
createdAt: new Date().toISOString(),
|
|
2578
|
+
};
|
|
2579
|
+
const workflow = defineSubagentWorkflow(
|
|
2580
|
+
{ name: "test", description: "test agent" },
|
|
2581
|
+
async (_prompt, sessionInput) => {
|
|
2582
|
+
sessionInput.onSandboxReady?.({ sandboxId: "sb-1", baseSnapshot });
|
|
2583
|
+
return { toolResponse: "ok", data: null, threadId: "t" };
|
|
2584
|
+
}
|
|
2585
|
+
);
|
|
2586
|
+
|
|
2587
|
+
const result = await workflow("go", {});
|
|
2588
|
+
expect(result.baseSnapshot).toEqual(baseSnapshot);
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
it("auto-forwards sandboxId and snapshot captured via onSessionExit", async () => {
|
|
2592
|
+
const snapshot = {
|
|
2593
|
+
sandboxId: "sb-1",
|
|
2594
|
+
providerId: "test",
|
|
2595
|
+
data: { tag: "exit" },
|
|
2596
|
+
createdAt: new Date().toISOString(),
|
|
2597
|
+
};
|
|
2598
|
+
const workflow = defineSubagentWorkflow(
|
|
2599
|
+
{ name: "test", description: "test agent" },
|
|
2600
|
+
async (_prompt, sessionInput) => {
|
|
2601
|
+
sessionInput.onSessionExit?.({
|
|
2602
|
+
sandboxId: "sb-1",
|
|
2603
|
+
snapshot,
|
|
2604
|
+
threadId: "t",
|
|
2605
|
+
});
|
|
2606
|
+
return { toolResponse: "ok", data: null, threadId: "t" };
|
|
2607
|
+
}
|
|
2608
|
+
);
|
|
2609
|
+
|
|
2610
|
+
const result = await workflow("go", {});
|
|
2611
|
+
expect(result.sandboxId).toBe("sb-1");
|
|
2612
|
+
expect(result.snapshot).toEqual(snapshot);
|
|
2613
|
+
});
|
|
2614
|
+
|
|
2615
|
+
it("fn-explicit sandbox outputs win over captured session outputs", async () => {
|
|
2616
|
+
const workflow = defineSubagentWorkflow(
|
|
2617
|
+
{ name: "test", description: "test agent" },
|
|
2618
|
+
async (_prompt, sessionInput) => {
|
|
2619
|
+
sessionInput.onSessionExit?.({
|
|
2620
|
+
sandboxId: "session-sb",
|
|
2621
|
+
snapshot: {
|
|
2622
|
+
sandboxId: "session-sb",
|
|
2623
|
+
providerId: "test",
|
|
2624
|
+
data: { tag: "session" },
|
|
2625
|
+
createdAt: new Date().toISOString(),
|
|
2626
|
+
},
|
|
2627
|
+
threadId: "t",
|
|
2628
|
+
});
|
|
2629
|
+
return {
|
|
2630
|
+
toolResponse: "ok",
|
|
2631
|
+
data: null,
|
|
2632
|
+
threadId: "t",
|
|
2633
|
+
sandboxId: "explicit-sb",
|
|
2634
|
+
snapshot: {
|
|
2635
|
+
sandboxId: "explicit-sb",
|
|
2636
|
+
providerId: "test",
|
|
2637
|
+
data: { tag: "explicit" },
|
|
2638
|
+
createdAt: new Date().toISOString(),
|
|
2639
|
+
},
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
);
|
|
2643
|
+
|
|
2644
|
+
const result = await workflow("go", {});
|
|
2645
|
+
expect(result.sandboxId).toBe("explicit-sb");
|
|
2646
|
+
expect(
|
|
2647
|
+
(result.snapshot as { data: { tag: string } } | undefined)?.data
|
|
2648
|
+
).toEqual({ tag: "explicit" });
|
|
2649
|
+
});
|
|
2650
|
+
|
|
2651
|
+
it("signals parent with baseSnapshot via childSandboxReadySignal", async () => {
|
|
2652
|
+
const { getExternalWorkflowHandle } = await import("@temporalio/workflow");
|
|
2653
|
+
const ghMock = getExternalWorkflowHandle as ReturnType<typeof vi.fn>;
|
|
2654
|
+
const baseSnapshot = {
|
|
2655
|
+
sandboxId: "sb-1",
|
|
2656
|
+
providerId: "test",
|
|
2657
|
+
data: { tag: "base" },
|
|
2658
|
+
createdAt: new Date().toISOString(),
|
|
2659
|
+
};
|
|
2660
|
+
|
|
2661
|
+
const workflow = defineSubagentWorkflow(
|
|
2662
|
+
{ name: "test", description: "test agent" },
|
|
2663
|
+
async (_prompt, sessionInput) => {
|
|
2664
|
+
sessionInput.onSandboxReady?.({ sandboxId: "sb-1", baseSnapshot });
|
|
2665
|
+
return { toolResponse: "ok", data: null, threadId: "t" };
|
|
2666
|
+
}
|
|
2667
|
+
);
|
|
2668
|
+
|
|
2669
|
+
await workflow("go", {});
|
|
2670
|
+
|
|
2671
|
+
const handle = ghMock.mock.results.at(-1)?.value as {
|
|
2672
|
+
signal: ReturnType<typeof vi.fn>;
|
|
2673
|
+
};
|
|
2674
|
+
expect(handle.signal).toHaveBeenCalledWith(
|
|
2675
|
+
expect.objectContaining({ name: "childSandboxReady" }),
|
|
2676
|
+
expect.objectContaining({
|
|
2677
|
+
childWorkflowId: "child-wf-1",
|
|
2678
|
+
sandboxId: "sb-1",
|
|
2679
|
+
baseSnapshot,
|
|
2680
|
+
})
|
|
2681
|
+
);
|
|
2682
|
+
});
|
|
2683
|
+
|
|
2684
|
+
it("omits baseSnapshot from signal when session did not capture one", async () => {
|
|
2685
|
+
const { getExternalWorkflowHandle } = await import("@temporalio/workflow");
|
|
2686
|
+
const ghMock = getExternalWorkflowHandle as ReturnType<typeof vi.fn>;
|
|
2687
|
+
|
|
2688
|
+
const workflow = defineSubagentWorkflow(
|
|
2689
|
+
{ name: "test", description: "test agent" },
|
|
2690
|
+
async (_prompt, sessionInput) => {
|
|
2691
|
+
sessionInput.onSandboxReady?.({ sandboxId: "sb-1" });
|
|
2692
|
+
return { toolResponse: "ok", data: null, threadId: "t" };
|
|
2693
|
+
}
|
|
2694
|
+
);
|
|
2695
|
+
|
|
2696
|
+
await workflow("go", {});
|
|
2697
|
+
|
|
2698
|
+
const handle = ghMock.mock.results.at(-1)?.value as {
|
|
2699
|
+
signal: ReturnType<typeof vi.fn>;
|
|
2700
|
+
};
|
|
2701
|
+
const payload = handle.signal.mock.calls.at(-1)?.[1] as {
|
|
2702
|
+
childWorkflowId: string;
|
|
2703
|
+
sandboxId: string;
|
|
2704
|
+
baseSnapshot?: unknown;
|
|
2705
|
+
};
|
|
2706
|
+
expect(payload).toEqual({
|
|
2707
|
+
childWorkflowId: "child-wf-1",
|
|
2708
|
+
sandboxId: "sb-1",
|
|
2709
|
+
});
|
|
2710
|
+
expect(payload.baseSnapshot).toBeUndefined();
|
|
2711
|
+
});
|
|
2712
|
+
|
|
2713
|
+
it("skips the signal when the sandbox is reused (continue mode)", async () => {
|
|
2714
|
+
const { getExternalWorkflowHandle } = await import("@temporalio/workflow");
|
|
2715
|
+
const ghMock = getExternalWorkflowHandle as ReturnType<typeof vi.fn>;
|
|
2716
|
+
|
|
2717
|
+
const workflow = defineSubagentWorkflow(
|
|
2718
|
+
{ name: "test", description: "test agent" },
|
|
2719
|
+
async (_prompt, sessionInput) => {
|
|
2720
|
+
sessionInput.onSandboxReady?.({ sandboxId: "sb-1" });
|
|
2721
|
+
return { toolResponse: "ok", data: null, threadId: "t" };
|
|
2722
|
+
}
|
|
2723
|
+
);
|
|
2724
|
+
|
|
2725
|
+
await workflow("go", {
|
|
2726
|
+
sandbox: { mode: "continue", sandboxId: "sb-1" },
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
const handle = ghMock.mock.results.at(-1)?.value as {
|
|
2730
|
+
signal: ReturnType<typeof vi.fn>;
|
|
2731
|
+
};
|
|
2732
|
+
expect(handle.signal).not.toHaveBeenCalled();
|
|
2733
|
+
});
|
|
2206
2734
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import type { ChildWorkflowOptions } from "@temporalio/workflow";
|
|
2
3
|
import type { JsonValue } from "../state/types";
|
|
3
4
|
import type {
|
|
4
5
|
ToolHandlerResponse,
|
|
@@ -12,22 +13,27 @@ import type {
|
|
|
12
13
|
} from "../lifecycle";
|
|
13
14
|
import type { SandboxOps, SandboxSnapshot } from "../sandbox/types";
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Subset of {@link ChildWorkflowOptions} that callers may override when a
|
|
18
|
+
* subagent is invoked. `workflowId`, `taskQueue`, and `args` are managed by
|
|
19
|
+
* the subagent handler itself and therefore cannot be set here.
|
|
20
|
+
*
|
|
21
|
+
* Configuring `workflowRunTimeout` (or `workflowExecutionTimeout`) is strongly
|
|
22
|
+
* recommended: it is the only reliable way to guarantee that a child workflow
|
|
23
|
+
* which fails during initialization or repeatedly fails workflow tasks will
|
|
24
|
+
* eventually be terminated, allowing the parent's `Subagent` tool call to fail
|
|
25
|
+
* deterministically instead of hanging forever waiting for a result.
|
|
26
|
+
*/
|
|
27
|
+
export type SubagentChildWorkflowOptions = Omit<
|
|
28
|
+
ChildWorkflowOptions,
|
|
29
|
+
"workflowId" | "taskQueue" | "args"
|
|
30
|
+
>;
|
|
31
|
+
|
|
15
32
|
/** ToolHandlerResponse with threadId required (subagents must always surface their thread) */
|
|
16
33
|
export type SubagentHandlerResponse<
|
|
17
34
|
TResult = null,
|
|
18
35
|
TToolResponse = JsonValue,
|
|
19
|
-
> = ToolHandlerResponse<TResult, TToolResponse
|
|
20
|
-
threadId: string;
|
|
21
|
-
sandboxId?: string;
|
|
22
|
-
/** Snapshot captured on session exit when `sandboxShutdown === "snapshot"`. */
|
|
23
|
-
snapshot?: SandboxSnapshot;
|
|
24
|
-
/**
|
|
25
|
-
* Snapshot captured immediately after the sandbox was seeded (before the
|
|
26
|
-
* first agent turn) when `continuation === "snapshot"`. Only set on the
|
|
27
|
-
* first call that actually created the sandbox.
|
|
28
|
-
*/
|
|
29
|
-
baseSnapshot?: SandboxSnapshot;
|
|
30
|
-
};
|
|
36
|
+
> = ToolHandlerResponse<TResult, TToolResponse>;
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
39
|
* Raw workflow input fields passed from parent to child workflow.
|
|
@@ -124,6 +130,24 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
|
|
|
124
130
|
workflow: SubagentWorkflow<TResult>;
|
|
125
131
|
/** Optional task queue - defaults to parent's queue if not specified */
|
|
126
132
|
taskQueue?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Optional child workflow options forwarded to `executeChild` when the
|
|
135
|
+
* subagent is spawned. Use this to configure timeouts, retry policies, or
|
|
136
|
+
* parent-close behavior for the child workflow.
|
|
137
|
+
*
|
|
138
|
+
* **Recommended:** configure a `workflowRunTimeout` (or
|
|
139
|
+
* `workflowExecutionTimeout`) so that a child workflow that fails to
|
|
140
|
+
* initialize — or repeatedly fails workflow tasks without ever reaching a
|
|
141
|
+
* terminal state — is eventually terminated by the Temporal server. Without
|
|
142
|
+
* such a timeout, the parent's `Subagent` tool call can hang indefinitely
|
|
143
|
+
* waiting for the child to finish. When Temporal terminates the child, the
|
|
144
|
+
* tool call fails with a structured `ChildWorkflowFailure` that the router's
|
|
145
|
+
* failure hooks can handle just like any other tool error.
|
|
146
|
+
*
|
|
147
|
+
* `workflowId`, `taskQueue`, and `args` are managed by the subagent handler
|
|
148
|
+
* and cannot be overridden here.
|
|
149
|
+
*/
|
|
150
|
+
workflowOptions?: SubagentChildWorkflowOptions;
|
|
127
151
|
/** Optional Zod schema to validate the child workflow's result. If omitted, result is passed through as-is. */
|
|
128
152
|
resultSchema?: TResult;
|
|
129
153
|
/** Optional context passed to the subagent — a static object or a function evaluated at invocation time */
|
|
@@ -214,6 +238,13 @@ export type SubagentFnResult<
|
|
|
214
238
|
export interface ChildSandboxReadySignalPayload {
|
|
215
239
|
childWorkflowId: string;
|
|
216
240
|
sandboxId: string;
|
|
241
|
+
/**
|
|
242
|
+
* Present only when the session captured a seed snapshot on this run
|
|
243
|
+
* (`continuation === "snapshot"` + fresh creation). Allows the parent to
|
|
244
|
+
* publish the reusable base snapshot to concurrent waiters without
|
|
245
|
+
* blocking on the child workflow's completion.
|
|
246
|
+
*/
|
|
247
|
+
baseSnapshot?: SandboxSnapshot;
|
|
217
248
|
}
|
|
218
249
|
|
|
219
250
|
/**
|
|
@@ -228,6 +259,24 @@ export interface SubagentSessionInput {
|
|
|
228
259
|
sandbox?: SandboxInit;
|
|
229
260
|
/** Sandbox shutdown policy (default: "destroy") */
|
|
230
261
|
sandboxShutdown?: SubagentSandboxShutdown;
|
|
231
|
-
/**
|
|
232
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Called by the session as soon as the sandbox is created, before the
|
|
264
|
+
* agent loop starts. `baseSnapshot` is populated only when the session
|
|
265
|
+
* captured a seed snapshot (fresh creation + `sandboxShutdown === "snapshot"`).
|
|
266
|
+
*/
|
|
267
|
+
onSandboxReady?: (args: {
|
|
268
|
+
sandboxId: string;
|
|
269
|
+
baseSnapshot?: SandboxSnapshot;
|
|
270
|
+
}) => void;
|
|
271
|
+
/**
|
|
272
|
+
* Called by the session right before `runSession` returns. Installed by
|
|
273
|
+
* `defineSubagentWorkflow` to capture sandbox outputs and auto-forward
|
|
274
|
+
* them to the subagent's final result so user code never has to thread
|
|
275
|
+
* `sandboxId` / `snapshot` manually.
|
|
276
|
+
*/
|
|
277
|
+
onSessionExit?: (result: {
|
|
278
|
+
sandboxId?: string;
|
|
279
|
+
snapshot?: SandboxSnapshot;
|
|
280
|
+
threadId: string;
|
|
281
|
+
}) => void;
|
|
233
282
|
}
|