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.
Files changed (125) hide show
  1. package/README.md +18 -0
  2. package/dist/{activities-BKhMtKDd.d.ts → activities-Bmu7XnaG.d.ts} +4 -6
  3. package/dist/{activities-CDcwkRZs.d.cts → activities-ByBFLvm2.d.cts} +4 -6
  4. package/dist/adapter-id-BB-mmrts.d.cts +17 -0
  5. package/dist/adapter-id-BB-mmrts.d.ts +17 -0
  6. package/dist/adapter-id-CMwVrVqv.d.cts +17 -0
  7. package/dist/adapter-id-CMwVrVqv.d.ts +17 -0
  8. package/dist/adapter-id-CbY2zeSt.d.cts +17 -0
  9. package/dist/adapter-id-CbY2zeSt.d.ts +17 -0
  10. package/dist/adapters/thread/anthropic/index.cjs +140 -23
  11. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/index.d.cts +8 -7
  13. package/dist/adapters/thread/anthropic/index.d.ts +8 -7
  14. package/dist/adapters/thread/anthropic/index.js +140 -24
  15. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  16. package/dist/adapters/thread/anthropic/workflow.cjs +8 -3
  17. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  18. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -4
  19. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -4
  20. package/dist/adapters/thread/anthropic/workflow.js +8 -4
  21. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  22. package/dist/adapters/thread/google-genai/index.cjs +140 -23
  23. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/index.d.cts +5 -4
  25. package/dist/adapters/thread/google-genai/index.d.ts +5 -4
  26. package/dist/adapters/thread/google-genai/index.js +140 -24
  27. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  28. package/dist/adapters/thread/google-genai/workflow.cjs +8 -3
  29. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  30. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -4
  31. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -4
  32. package/dist/adapters/thread/google-genai/workflow.js +8 -4
  33. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  34. package/dist/adapters/thread/index.cjs +16 -0
  35. package/dist/adapters/thread/index.cjs.map +1 -0
  36. package/dist/adapters/thread/index.d.cts +34 -0
  37. package/dist/adapters/thread/index.d.ts +34 -0
  38. package/dist/adapters/thread/index.js +12 -0
  39. package/dist/adapters/thread/index.js.map +1 -0
  40. package/dist/adapters/thread/langchain/index.cjs +139 -24
  41. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  42. package/dist/adapters/thread/langchain/index.d.cts +8 -7
  43. package/dist/adapters/thread/langchain/index.d.ts +8 -7
  44. package/dist/adapters/thread/langchain/index.js +139 -25
  45. package/dist/adapters/thread/langchain/index.js.map +1 -1
  46. package/dist/adapters/thread/langchain/workflow.cjs +8 -3
  47. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  48. package/dist/adapters/thread/langchain/workflow.d.cts +5 -4
  49. package/dist/adapters/thread/langchain/workflow.d.ts +5 -4
  50. package/dist/adapters/thread/langchain/workflow.js +8 -4
  51. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  52. package/dist/index.cjs +266 -48
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.cts +6 -6
  55. package/dist/index.d.ts +6 -6
  56. package/dist/index.js +263 -49
  57. package/dist/index.js.map +1 -1
  58. package/dist/{proxy-D_3x7RN4.d.cts → proxy-BAKzNGRq.d.cts} +1 -1
  59. package/dist/{proxy-CUlKSvZS.d.ts → proxy-DO_MXbY4.d.ts} +1 -1
  60. package/dist/{thread-manager-CVu7o2cs.d.ts → thread-manager-CcRXasqs.d.ts} +2 -4
  61. package/dist/{thread-manager-HSwyh28L.d.cts → thread-manager-ClwSaUnj.d.cts} +2 -4
  62. package/dist/{thread-manager-c1gPopAG.d.ts → thread-manager-D-7lp1JK.d.ts} +2 -4
  63. package/dist/{thread-manager-wGi-LqIP.d.cts → thread-manager-Y8Ucf0Tf.d.cts} +2 -4
  64. package/dist/{types-C06FwR96.d.cts → types-Bcbiq8iv.d.cts} +162 -44
  65. package/dist/{types-BH_IRryz.d.ts → types-DpHTX-iO.d.ts} +54 -6
  66. package/dist/{types-DNr31FzL.d.ts → types-Dt8-HBBT.d.ts} +162 -44
  67. package/dist/{types-BaOw4hKI.d.cts → types-hFFi-Zd9.d.cts} +54 -6
  68. package/dist/{workflow-CSCkpwAL.d.ts → workflow-Bmf9EtDW.d.ts} +82 -2
  69. package/dist/{workflow-DuvMZ8Vm.d.cts → workflow-Bx9utBwb.d.cts} +82 -2
  70. package/dist/workflow.cjs +188 -37
  71. package/dist/workflow.cjs.map +1 -1
  72. package/dist/workflow.d.cts +2 -2
  73. package/dist/workflow.d.ts +2 -2
  74. package/dist/workflow.js +185 -38
  75. package/dist/workflow.js.map +1 -1
  76. package/package.json +11 -1
  77. package/src/adapters/thread/adapter-id.test.ts +42 -0
  78. package/src/adapters/thread/anthropic/activities.ts +33 -7
  79. package/src/adapters/thread/anthropic/adapter-id.ts +16 -0
  80. package/src/adapters/thread/anthropic/fork-transform.test.ts +291 -0
  81. package/src/adapters/thread/anthropic/index.ts +3 -0
  82. package/src/adapters/thread/anthropic/model-invoker.ts +8 -4
  83. package/src/adapters/thread/anthropic/proxy.ts +3 -2
  84. package/src/adapters/thread/anthropic/thread-manager.ts +27 -4
  85. package/src/adapters/thread/google-genai/activities.ts +33 -7
  86. package/src/adapters/thread/google-genai/adapter-id.ts +16 -0
  87. package/src/adapters/thread/google-genai/fork-transform.test.ts +149 -0
  88. package/src/adapters/thread/google-genai/index.ts +3 -0
  89. package/src/adapters/thread/google-genai/model-invoker.ts +7 -3
  90. package/src/adapters/thread/google-genai/proxy.ts +3 -2
  91. package/src/adapters/thread/google-genai/thread-manager.ts +27 -4
  92. package/src/adapters/thread/index.ts +39 -0
  93. package/src/adapters/thread/langchain/activities.ts +33 -7
  94. package/src/adapters/thread/langchain/adapter-id.ts +16 -0
  95. package/src/adapters/thread/langchain/fork-transform.test.ts +142 -0
  96. package/src/adapters/thread/langchain/index.ts +3 -0
  97. package/src/adapters/thread/langchain/model-invoker.ts +8 -3
  98. package/src/adapters/thread/langchain/proxy.ts +3 -2
  99. package/src/adapters/thread/langchain/thread-manager.ts +27 -4
  100. package/src/lib/lifecycle.ts +3 -1
  101. package/src/lib/model/types.ts +7 -10
  102. package/src/lib/session/session-edge-cases.integration.test.ts +131 -63
  103. package/src/lib/session/session.integration.test.ts +174 -5
  104. package/src/lib/session/session.ts +68 -28
  105. package/src/lib/session/types.ts +60 -9
  106. package/src/lib/state/index.ts +1 -0
  107. package/src/lib/state/manager.integration.test.ts +109 -0
  108. package/src/lib/state/manager.ts +38 -8
  109. package/src/lib/state/types.ts +25 -0
  110. package/src/lib/subagent/handler.ts +124 -11
  111. package/src/lib/subagent/index.ts +5 -1
  112. package/src/lib/subagent/subagent.integration.test.ts +528 -0
  113. package/src/lib/subagent/types.ts +63 -14
  114. package/src/lib/subagent/workflow.ts +29 -2
  115. package/src/lib/thread/index.ts +5 -0
  116. package/src/lib/thread/keys.test.ts +101 -0
  117. package/src/lib/thread/keys.ts +94 -0
  118. package/src/lib/thread/manager.test.ts +139 -0
  119. package/src/lib/thread/manager.ts +92 -14
  120. package/src/lib/thread/proxy.ts +2 -0
  121. package/src/lib/thread/types.ts +60 -6
  122. package/src/lib/tool-router/types.ts +16 -8
  123. package/src/lib/types.ts +12 -0
  124. package/src/workflow.ts +12 -1
  125. 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
- /** Called by the session as soon as the sandbox is created, before the agent loop starts. */
232
- onSandboxReady?: (sandboxId: string) => void;
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
  }