zeitlich 0.2.32 → 0.2.34

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 (143) hide show
  1. package/README.md +28 -16
  2. package/dist/{activities-FIXVz7DT.d.ts → activities-JOqPfKP0.d.cts} +6 -5
  3. package/dist/{activities-DA-bQM12.d.cts → activities-WwMsjRwm.d.ts} +6 -5
  4. package/dist/adapters/sandbox/bedrock/index.cjs +2 -0
  5. package/dist/adapters/sandbox/bedrock/index.cjs.map +1 -1
  6. package/dist/adapters/sandbox/bedrock/index.d.cts +4 -3
  7. package/dist/adapters/sandbox/bedrock/index.d.ts +4 -3
  8. package/dist/adapters/sandbox/bedrock/index.js +2 -0
  9. package/dist/adapters/sandbox/bedrock/index.js.map +1 -1
  10. package/dist/adapters/sandbox/bedrock/workflow.cjs +1 -0
  11. package/dist/adapters/sandbox/bedrock/workflow.cjs.map +1 -1
  12. package/dist/adapters/sandbox/bedrock/workflow.d.cts +2 -2
  13. package/dist/adapters/sandbox/bedrock/workflow.d.ts +2 -2
  14. package/dist/adapters/sandbox/bedrock/workflow.js +1 -0
  15. package/dist/adapters/sandbox/bedrock/workflow.js.map +1 -1
  16. package/dist/adapters/sandbox/daytona/index.cjs +2 -0
  17. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  18. package/dist/adapters/sandbox/daytona/index.d.cts +2 -1
  19. package/dist/adapters/sandbox/daytona/index.d.ts +2 -1
  20. package/dist/adapters/sandbox/daytona/index.js +2 -0
  21. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  22. package/dist/adapters/sandbox/daytona/workflow.cjs +1 -0
  23. package/dist/adapters/sandbox/daytona/workflow.cjs.map +1 -1
  24. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  25. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  26. package/dist/adapters/sandbox/daytona/workflow.js +1 -0
  27. package/dist/adapters/sandbox/daytona/workflow.js.map +1 -1
  28. package/dist/adapters/sandbox/e2b/index.cjs +3 -0
  29. package/dist/adapters/sandbox/e2b/index.cjs.map +1 -1
  30. package/dist/adapters/sandbox/e2b/index.d.cts +2 -1
  31. package/dist/adapters/sandbox/e2b/index.d.ts +2 -1
  32. package/dist/adapters/sandbox/e2b/index.js +3 -0
  33. package/dist/adapters/sandbox/e2b/index.js.map +1 -1
  34. package/dist/adapters/sandbox/e2b/workflow.cjs +1 -0
  35. package/dist/adapters/sandbox/e2b/workflow.cjs.map +1 -1
  36. package/dist/adapters/sandbox/e2b/workflow.d.cts +1 -1
  37. package/dist/adapters/sandbox/e2b/workflow.d.ts +1 -1
  38. package/dist/adapters/sandbox/e2b/workflow.js +1 -0
  39. package/dist/adapters/sandbox/e2b/workflow.js.map +1 -1
  40. package/dist/adapters/sandbox/inmemory/index.cjs +2 -0
  41. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  42. package/dist/adapters/sandbox/inmemory/index.d.cts +2 -1
  43. package/dist/adapters/sandbox/inmemory/index.d.ts +2 -1
  44. package/dist/adapters/sandbox/inmemory/index.js +2 -0
  45. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  46. package/dist/adapters/sandbox/inmemory/workflow.cjs +1 -0
  47. package/dist/adapters/sandbox/inmemory/workflow.cjs.map +1 -1
  48. package/dist/adapters/sandbox/inmemory/workflow.d.cts +1 -1
  49. package/dist/adapters/sandbox/inmemory/workflow.d.ts +1 -1
  50. package/dist/adapters/sandbox/inmemory/workflow.js +1 -0
  51. package/dist/adapters/sandbox/inmemory/workflow.js.map +1 -1
  52. package/dist/adapters/thread/anthropic/index.cjs +18 -2
  53. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  54. package/dist/adapters/thread/anthropic/index.d.cts +12 -11
  55. package/dist/adapters/thread/anthropic/index.d.ts +12 -11
  56. package/dist/adapters/thread/anthropic/index.js +18 -2
  57. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  58. package/dist/adapters/thread/anthropic/workflow.d.cts +5 -5
  59. package/dist/adapters/thread/anthropic/workflow.d.ts +5 -5
  60. package/dist/adapters/thread/google-genai/index.cjs +29 -8
  61. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  62. package/dist/adapters/thread/google-genai/index.d.cts +8 -8
  63. package/dist/adapters/thread/google-genai/index.d.ts +8 -8
  64. package/dist/adapters/thread/google-genai/index.js +29 -8
  65. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  66. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -5
  67. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -5
  68. package/dist/adapters/thread/langchain/index.cjs +42 -23
  69. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  70. package/dist/adapters/thread/langchain/index.d.cts +13 -11
  71. package/dist/adapters/thread/langchain/index.d.ts +13 -11
  72. package/dist/adapters/thread/langchain/index.js +42 -23
  73. package/dist/adapters/thread/langchain/index.js.map +1 -1
  74. package/dist/adapters/thread/langchain/workflow.d.cts +5 -5
  75. package/dist/adapters/thread/langchain/workflow.d.ts +5 -5
  76. package/dist/index.cjs +148 -34
  77. package/dist/index.cjs.map +1 -1
  78. package/dist/index.d.cts +32 -16
  79. package/dist/index.d.ts +32 -16
  80. package/dist/index.js +147 -35
  81. package/dist/index.js.map +1 -1
  82. package/dist/{proxy-CTCYWjkr.d.cts → proxy-BesT2ioL.d.cts} +1 -1
  83. package/dist/{proxy-Br4unLTC.d.ts → proxy-Bz6wXYW-.d.ts} +1 -1
  84. package/dist/{thread-manager-Cv_BR28i.d.cts → thread-manager-CCVAOK8g.d.cts} +1 -1
  85. package/dist/{thread-manager-CUubPYPH.d.cts → thread-manager-Cf_34H8w.d.cts} +1 -1
  86. package/dist/{thread-manager-YJLoc1vH.d.ts → thread-manager-ClKAQx78.d.ts} +1 -1
  87. package/dist/{thread-manager-DKWxHUzD.d.ts → thread-manager-DarJIK_b.d.ts} +1 -1
  88. package/dist/{types-Bpq5fDI5.d.cts → types-BGLW5Zyj.d.ts} +35 -20
  89. package/dist/{types-BxiT8w9d.d.ts → types-BVUmLYpj.d.ts} +1 -1
  90. package/dist/{types-DUvEZSDe.d.cts → types-CBH54cwr.d.cts} +1 -1
  91. package/dist/{types-NJDyMyUx.d.cts → types-DPAZ3KCs.d.cts} +1 -1
  92. package/dist/{types-CheCTLeV.d.ts → types-DlLajQcu.d.cts} +35 -20
  93. package/dist/{types-AujBIMMn.d.cts → types-DxCpFNv_.d.cts} +4 -0
  94. package/dist/{types-AujBIMMn.d.ts → types-DxCpFNv_.d.ts} +4 -0
  95. package/dist/{types-DBk-C8zM.d.ts → types-wiGLvxWf.d.ts} +1 -1
  96. package/dist/{workflow-BWKQcz9d.d.cts → workflow-_ZGcacCK.d.ts} +32 -4
  97. package/dist/{workflow-D8wK7TJY.d.ts → workflow-hocXpLwg.d.cts} +32 -4
  98. package/dist/workflow.cjs +126 -30
  99. package/dist/workflow.cjs.map +1 -1
  100. package/dist/workflow.d.cts +3 -3
  101. package/dist/workflow.d.ts +3 -3
  102. package/dist/workflow.js +126 -31
  103. package/dist/workflow.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/adapters/sandbox/bedrock/index.ts +4 -0
  106. package/src/adapters/sandbox/bedrock/proxy.ts +1 -0
  107. package/src/adapters/sandbox/daytona/index.ts +4 -0
  108. package/src/adapters/sandbox/daytona/proxy.ts +1 -0
  109. package/src/adapters/sandbox/e2b/index.ts +4 -0
  110. package/src/adapters/sandbox/e2b/proxy.ts +1 -0
  111. package/src/adapters/sandbox/inmemory/index.ts +4 -0
  112. package/src/adapters/sandbox/inmemory/proxy.ts +1 -0
  113. package/src/adapters/thread/anthropic/activities.ts +4 -3
  114. package/src/adapters/thread/anthropic/model-invoker.ts +15 -5
  115. package/src/adapters/thread/google-genai/activities.ts +4 -3
  116. package/src/adapters/thread/google-genai/model-invoker.ts +24 -11
  117. package/src/adapters/thread/langchain/activities.ts +3 -3
  118. package/src/adapters/thread/langchain/model-invoker.ts +63 -34
  119. package/src/index.ts +1 -0
  120. package/src/lib/activity.ts +36 -9
  121. package/src/lib/lifecycle.ts +7 -3
  122. package/src/lib/model/helpers.ts +1 -0
  123. package/src/lib/model/index.ts +1 -0
  124. package/src/lib/model/proxy.ts +50 -0
  125. package/src/lib/sandbox/manager.ts +7 -0
  126. package/src/lib/sandbox/types.ts +4 -0
  127. package/src/lib/session/session-edge-cases.integration.test.ts +194 -0
  128. package/src/lib/session/session.integration.test.ts +5 -0
  129. package/src/lib/session/session.ts +9 -0
  130. package/src/lib/session/types.ts +5 -0
  131. package/src/lib/subagent/define.ts +1 -1
  132. package/src/lib/subagent/handler.ts +142 -32
  133. package/src/lib/subagent/index.ts +5 -1
  134. package/src/lib/subagent/signals.ts +8 -1
  135. package/src/lib/subagent/subagent.integration.test.ts +532 -25
  136. package/src/lib/subagent/types.ts +32 -15
  137. package/src/lib/subagent/workflow.ts +26 -13
  138. package/src/lib/virtual-fs/manager.ts +1 -1
  139. package/src/lib/virtual-fs/types.ts +2 -2
  140. package/src/lib/virtual-fs/virtual-fs.test.ts +2 -2
  141. package/src/workflow.ts +3 -0
  142. package/src/lib/.env +0 -1
  143. package/src/tools/bash/.env +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.32",
3
+ "version": "0.2.34",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -237,6 +237,10 @@ export class BedrockSandboxProvider
237
237
  throw new SandboxNotSupportedError("pause");
238
238
  }
239
239
 
240
+ async resume(_sandboxId: string): Promise<void> {
241
+ // Bedrock sandboxes don't support pause, so resume is a no-op
242
+ }
243
+
240
244
  async snapshot(_sandboxId: string): Promise<SandboxSnapshot> {
241
245
  throw new SandboxNotSupportedError("snapshot");
242
246
  }
@@ -50,6 +50,7 @@ export function proxyBedrockSandboxOps(
50
50
  createSandbox: acts[p("createSandbox")],
51
51
  destroySandbox: acts[p("destroySandbox")],
52
52
  pauseSandbox: acts[p("pauseSandbox")],
53
+ resumeSandbox: acts[p("resumeSandbox")],
53
54
  snapshotSandbox: acts[p("snapshotSandbox")],
54
55
  forkSandbox: acts[p("forkSandbox")],
55
56
  } as SandboxOps<BedrockSandboxCreateOptions>;
@@ -145,6 +145,10 @@ export class DaytonaSandboxProvider implements SandboxProvider<
145
145
  throw new SandboxNotSupportedError("pause");
146
146
  }
147
147
 
148
+ async resume(_sandboxId: string): Promise<void> {
149
+ // Daytona sandboxes don't support pause, so resume is a no-op
150
+ }
151
+
148
152
  async fork(_sandboxId: string): Promise<Sandbox> {
149
153
  throw new Error("Not implemented");
150
154
  }
@@ -50,6 +50,7 @@ export function proxyDaytonaSandboxOps(
50
50
  createSandbox: acts[p("createSandbox")],
51
51
  destroySandbox: acts[p("destroySandbox")],
52
52
  pauseSandbox: acts[p("pauseSandbox")],
53
+ resumeSandbox: acts[p("resumeSandbox")],
53
54
  snapshotSandbox: acts[p("snapshotSandbox")],
54
55
  forkSandbox: acts[p("forkSandbox")],
55
56
  } as SandboxOps<DaytonaSandboxCreateOptions>;
@@ -136,6 +136,10 @@ export class E2bSandboxProvider
136
136
  await sdkSandbox.pause();
137
137
  }
138
138
 
139
+ async resume(sandboxId: string): Promise<void> {
140
+ await E2bSdkSandbox.connect(sandboxId);
141
+ }
142
+
139
143
  async snapshot(_sandboxId: string): Promise<SandboxSnapshot> {
140
144
  throw new SandboxNotSupportedError("snapshot");
141
145
  }
@@ -50,6 +50,7 @@ export function proxyE2bSandboxOps(
50
50
  createSandbox: acts[p("createSandbox")],
51
51
  destroySandbox: acts[p("destroySandbox")],
52
52
  pauseSandbox: acts[p("pauseSandbox")],
53
+ resumeSandbox: acts[p("resumeSandbox")],
53
54
  snapshotSandbox: acts[p("snapshotSandbox")],
54
55
  forkSandbox: acts[p("forkSandbox")],
55
56
  } as SandboxOps<E2bSandboxCreateOptions>;
@@ -163,6 +163,10 @@ export class InMemorySandboxProvider implements SandboxProvider {
163
163
  // In-memory: nothing to pause
164
164
  }
165
165
 
166
+ async resume(_sandboxId: string): Promise<void> {
167
+ // In-memory: nothing to resume
168
+ }
169
+
166
170
  async create(options?: SandboxCreateOptions): Promise<SandboxCreateResult> {
167
171
  const id = options?.id ?? getShortId();
168
172
  const initialFiles: InitialFiles = {};
@@ -47,6 +47,7 @@ export function proxyInMemorySandboxOps(
47
47
  createSandbox: acts[p("createSandbox")],
48
48
  destroySandbox: acts[p("destroySandbox")],
49
49
  pauseSandbox: acts[p("pauseSandbox")],
50
+ resumeSandbox: acts[p("resumeSandbox")],
50
51
  snapshotSandbox: acts[p("snapshotSandbox")],
51
52
  forkSandbox: acts[p("forkSandbox")],
52
53
  } as SandboxOps;
@@ -107,7 +107,7 @@ export interface AnthropicAdapter {
107
107
  * export function createActivities(temporalClient: WorkflowClient) {
108
108
  * return {
109
109
  * ...adapter.createActivities("codingAgent"),
110
- * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
110
+ * ...createRunAgentActivity(temporalClient, adapter.invoker, "codingAgent"),
111
111
  * };
112
112
  * }
113
113
  * ```
@@ -118,10 +118,11 @@ export interface AnthropicAdapter {
118
118
  * return {
119
119
  * ...adapter.createActivities("codingAgent"),
120
120
  * ...adapter.createActivities("researchAgent"),
121
- * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
122
- * runResearchAgent: createRunAgentActivity(
121
+ * ...createRunAgentActivity(temporalClient, adapter.invoker, "codingAgent"),
122
+ * ...createRunAgentActivity(
123
123
  * temporalClient,
124
124
  * adapter.createModelInvoker('claude-sonnet-4-20250514'),
125
+ * "researchAgent",
125
126
  * ),
126
127
  * };
127
128
  * }
@@ -3,6 +3,7 @@ import type Anthropic from "@anthropic-ai/sdk";
3
3
  import type { SerializableToolDefinition } from "../../../lib/types";
4
4
  import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
5
5
  import { createAnthropicThreadManager, type AnthropicThreadManagerHooks } from "./thread-manager";
6
+ import { getActivityContext } from "../../../lib/activity";
6
7
 
7
8
  export interface AnthropicModelInvokerConfig {
8
9
  redis: Redis;
@@ -27,8 +28,8 @@ function toAnthropicTools(
27
28
  * Creates an Anthropic model invoker that satisfies the generic
28
29
  * `ModelInvoker<Anthropic.Messages.Message>` contract.
29
30
  *
30
- * Loads the conversation thread from Redis, invokes the Claude model via
31
- * `client.messages.create`, and returns a normalised AgentResponse.
31
+ * Internally streams the response and emits Temporal heartbeats on each
32
+ * event so that long-running LLM calls remain visible to the scheduler.
32
33
  * The caller is responsible for appending the response to the thread.
33
34
  *
34
35
  * @example
@@ -44,7 +45,7 @@ function toAnthropicTools(
44
45
  * model: 'claude-sonnet-4-20250514',
45
46
  * });
46
47
  *
47
- * return { runAgent: createRunAgentActivity(client, invoker) };
48
+ * return { ...createRunAgentActivity(client, invoker, "myAgent") };
48
49
  * ```
49
50
  */
50
51
  export function createAnthropicModelInvoker({
@@ -58,6 +59,7 @@ export function createAnthropicModelInvoker({
58
59
  config: ModelInvokerConfig,
59
60
  ): Promise<AgentResponse<Anthropic.Messages.Message>> {
60
61
  const { threadId, threadKey, state } = config;
62
+ const { heartbeat, signal } = getActivityContext();
61
63
 
62
64
  const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey, hooks });
63
65
  const { messages, system } = await thread.prepareForInvocation();
@@ -65,13 +67,21 @@ export function createAnthropicModelInvoker({
65
67
  const anthropicTools = toAnthropicTools(state.tools);
66
68
  const tools = anthropicTools.length > 0 ? anthropicTools : undefined;
67
69
 
68
- const response = await client.messages.create({
70
+ const params: Anthropic.MessageCreateParams = {
69
71
  model,
70
72
  max_tokens: maxTokens,
71
73
  messages,
72
74
  ...(system ? { system } : {}),
73
75
  ...(tools ? { tools } : {}),
74
- });
76
+ };
77
+
78
+ const stream = client.messages.stream(params, { signal });
79
+
80
+ for await (const _event of stream) {
81
+ heartbeat?.();
82
+ }
83
+
84
+ const response: Anthropic.Messages.Message = await stream.finalMessage();
75
85
 
76
86
  const toolCalls = response.content.filter(
77
87
  (block): block is Anthropic.Messages.ToolUseBlock =>
@@ -113,7 +113,7 @@ export interface GoogleGenAIAdapter {
113
113
  * export function createActivities(temporalClient: WorkflowClient) {
114
114
  * return {
115
115
  * ...adapter.createActivities("codingAgent"),
116
- * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
116
+ * ...createRunAgentActivity(temporalClient, adapter.invoker, "codingAgent"),
117
117
  * };
118
118
  * }
119
119
  * ```
@@ -124,10 +124,11 @@ export interface GoogleGenAIAdapter {
124
124
  * return {
125
125
  * ...adapter.createActivities("codingAgent"),
126
126
  * ...adapter.createActivities("researchAgent"),
127
- * runCodingAgent: createRunAgentActivity(temporalClient, adapter.invoker),
128
- * runResearchAgent: createRunAgentActivity(
127
+ * ...createRunAgentActivity(temporalClient, adapter.invoker, "codingAgent"),
128
+ * ...createRunAgentActivity(
129
129
  * temporalClient,
130
130
  * adapter.createModelInvoker('gemini-2.5-pro'),
131
+ * "researchAgent",
131
132
  * ),
132
133
  * };
133
134
  * }
@@ -1,8 +1,9 @@
1
1
  import type Redis from "ioredis";
2
- import type { GoogleGenAI, Content, FunctionDeclaration } from "@google/genai";
2
+ import type { GoogleGenAI, Content, FunctionDeclaration, Part, GenerateContentResponse } from "@google/genai";
3
3
  import type { SerializableToolDefinition } from "../../../lib/types";
4
4
  import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
5
5
  import { createGoogleGenAIThreadManager, type GoogleGenAIThreadManagerHooks } from "./thread-manager";
6
+ import { getActivityContext } from "../../../lib/activity";
6
7
 
7
8
  export interface GoogleGenAIModelInvokerConfig {
8
9
  redis: Redis;
@@ -25,8 +26,8 @@ function toFunctionDeclarations(
25
26
  * Creates a Google GenAI model invoker that satisfies the generic
26
27
  * `ModelInvoker<Content>` contract.
27
28
  *
28
- * Loads the conversation thread from Redis, invokes the Gemini model via
29
- * `client.models.generateContent`, and returns a normalised AgentResponse.
29
+ * Internally streams the response and emits Temporal heartbeats on each
30
+ * chunk so that long-running LLM calls remain visible to the scheduler.
30
31
  * The caller is responsible for appending the response to the thread.
31
32
  *
32
33
  * @example
@@ -42,7 +43,7 @@ function toFunctionDeclarations(
42
43
  * model: 'gemini-2.5-flash',
43
44
  * });
44
45
  *
45
- * return { runAgent: createRunAgentActivity(client, invoker) };
46
+ * return { ...createRunAgentActivity(client, invoker, "myAgent") };
46
47
  * ```
47
48
  */
48
49
  export function createGoogleGenAIModelInvoker({
@@ -55,6 +56,7 @@ export function createGoogleGenAIModelInvoker({
55
56
  config: ModelInvokerConfig,
56
57
  ): Promise<AgentResponse<Content>> {
57
58
  const { threadId, threadKey, state } = config;
59
+ const { heartbeat, signal } = getActivityContext();
58
60
 
59
61
  const thread = createGoogleGenAIThreadManager({ redis, threadId, key: threadKey, hooks });
60
62
  const { contents, systemInstruction } =
@@ -64,19 +66,30 @@ export function createGoogleGenAIModelInvoker({
64
66
  const tools =
65
67
  functionDeclarations.length > 0 ? [{ functionDeclarations }] : undefined;
66
68
 
67
- const response = await client.models.generateContent({
69
+ const stream = await client.models.generateContentStream({
68
70
  model,
69
71
  contents,
70
72
  config: {
71
73
  ...(systemInstruction ? { systemInstruction } : {}),
72
74
  ...(tools ? { tools } : {}),
75
+ abortSignal: signal,
73
76
  },
74
77
  });
75
78
 
76
- const responseParts = response.candidates?.[0]?.content?.parts ?? [];
77
- const modelContent: Content = { role: "model", parts: responseParts };
79
+ const allParts: Part[] = [];
80
+ let lastChunk: GenerateContentResponse | undefined;
81
+ for await (const chunk of stream) {
82
+ lastChunk = chunk;
83
+ allParts.push(...(chunk.candidates?.[0]?.content?.parts ?? []));
84
+ heartbeat?.();
85
+ }
78
86
 
79
- const functionCalls = response.functionCalls ?? [];
87
+ if (!lastChunk) {
88
+ throw new Error("Google GenAI stream ended without producing any chunks");
89
+ }
90
+
91
+ const modelContent: Content = { role: "model", parts: allParts };
92
+ const functionCalls = lastChunk.functionCalls ?? [];
80
93
 
81
94
  return {
82
95
  message: modelContent,
@@ -86,9 +99,9 @@ export function createGoogleGenAIModelInvoker({
86
99
  args: fc.args ?? {},
87
100
  })),
88
101
  usage: {
89
- inputTokens: response.usageMetadata?.promptTokenCount,
90
- outputTokens: response.usageMetadata?.candidatesTokenCount,
91
- cachedReadTokens: response.usageMetadata?.cachedContentTokenCount,
102
+ inputTokens: lastChunk.usageMetadata?.promptTokenCount,
103
+ outputTokens: lastChunk.usageMetadata?.candidatesTokenCount,
104
+ cachedReadTokens: lastChunk.usageMetadata?.cachedContentTokenCount,
92
105
  },
93
106
  };
94
107
  };
@@ -94,7 +94,7 @@ export interface LangChainAdapter {
94
94
  * export function createActivities(client: WorkflowClient) {
95
95
  * return {
96
96
  * ...adapter.createActivities("codingAgent"),
97
- * runCodingAgent: createRunAgentActivity(client, adapter.invoker),
97
+ * ...createRunAgentActivity(client, adapter.invoker, "codingAgent"),
98
98
  * };
99
99
  * }
100
100
  * ```
@@ -105,8 +105,8 @@ export interface LangChainAdapter {
105
105
  * return {
106
106
  * ...adapter.createActivities("codingAgent"),
107
107
  * ...adapter.createActivities("researchAgent"),
108
- * runCodingAgent: createRunAgentActivity(client, adapter.invoker),
109
- * runResearchAgent: createRunAgentActivity(client, adapter.createModelInvoker(claude)),
108
+ * ...createRunAgentActivity(client, adapter.invoker, "codingAgent"),
109
+ * ...createRunAgentActivity(client, adapter.createModelInvoker(claude), "researchAgent"),
110
110
  * };
111
111
  * }
112
112
  * ```
@@ -3,10 +3,16 @@ import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
3
3
  import type { StoredMessage } from "@langchain/core/messages";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
  import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
6
- import { createLangChainThreadManager, type LangChainThreadManagerHooks } from "./thread-manager";
6
+ import {
7
+ createLangChainThreadManager,
8
+ type LangChainThreadManagerHooks,
9
+ } from "./thread-manager";
10
+ import { getActivityContext } from "../../../lib/activity";
7
11
 
8
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
- export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> = BaseChatModel<any>> {
13
+ export interface LangChainModelInvokerConfig<
14
+ TModel extends BaseChatModel<any> = BaseChatModel<any>,
15
+ > {
10
16
  redis: Redis;
11
17
  model: TModel;
12
18
  hooks?: LangChainThreadManagerHooks;
@@ -16,8 +22,10 @@ export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> =
16
22
  * Creates a LangChain-based model invoker that satisfies the generic
17
23
  * `ModelInvoker<StoredMessage>` contract.
18
24
  *
19
- * Loads the conversation thread from Redis, invokes a LangChain chat model,
20
- * and returns a normalised AgentResponse.
25
+ * Uses interval-based Temporal heartbeats during model.invoke() to keep
26
+ * long-running LLM calls visible to the scheduler. LangChain's streaming
27
+ * chunk accumulation is unreliable across providers (e.g. reasoning_content
28
+ * blocks don't merge correctly), so we use invoke() for correctness.
21
29
  * The caller is responsible for appending the response to the thread.
22
30
  *
23
31
  * @example
@@ -29,51 +37,70 @@ export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> =
29
37
  * const model = new ChatAnthropic({ model: "claude-sonnet-4-6" });
30
38
  * const invoker = createLangChainModelInvoker({ redis, model });
31
39
  *
32
- * return { runAgent: createRunAgentActivity(client, invoker) };
40
+ * return { ...createRunAgentActivity(client, invoker, "myAgent") };
33
41
  * ```
34
42
  */
35
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
- export function createLangChainModelInvoker<TModel extends BaseChatModel<any> = BaseChatModel<any>>(
37
- { redis, model, hooks }: LangChainModelInvokerConfig<TModel>,
38
- ) {
44
+ export function createLangChainModelInvoker<
45
+ TModel extends BaseChatModel<any> = BaseChatModel<any>,
46
+ >({ redis, model, hooks }: LangChainModelInvokerConfig<TModel>) {
39
47
  return async function invokeLangChainModel(
40
- config: ModelInvokerConfig,
48
+ config: ModelInvokerConfig
41
49
  ): Promise<AgentResponse<StoredMessage>> {
42
50
  const { threadId, threadKey, agentName, state, metadata } = config;
51
+ const { heartbeat, signal } = getActivityContext();
43
52
 
44
- const thread = createLangChainThreadManager({ redis, threadId, key: threadKey, hooks });
53
+ const thread = createLangChainThreadManager({
54
+ redis,
55
+ threadId,
56
+ key: threadKey,
57
+ hooks,
58
+ });
45
59
  const runId = uuidv4();
46
60
 
47
61
  const { messages } = await thread.prepareForInvocation();
48
- const response = await model.invoke(
49
- messages,
50
- {
62
+
63
+ const heartbeatInterval = heartbeat
64
+ ? setInterval(() => heartbeat(), 30_000)
65
+ : undefined;
66
+
67
+ try {
68
+ const response = await model.invoke(messages, {
51
69
  runName: agentName,
52
70
  runId,
53
71
  metadata: { thread_id: `${agentName}-${threadId}`, ...metadata },
54
72
  tools: state.tools,
55
- },
56
- );
73
+ signal,
74
+ });
75
+
76
+ const toolCalls = response.tool_calls ?? [];
57
77
 
58
- const toolCalls = response.tool_calls ?? [];
78
+ const providerUsage =
79
+ (response.response_metadata?.usage as Record<string, unknown>) ?? {};
59
80
 
60
- return {
61
- message: response.toDict(),
62
- rawToolCalls: toolCalls.map((tc) => ({
63
- id: tc.id,
64
- name: tc.name,
65
- args: tc.args,
66
- })),
67
- usage: {
68
- inputTokens: response.usage_metadata?.input_tokens,
69
- outputTokens: response.usage_metadata?.output_tokens,
70
- reasonTokens: response.usage_metadata?.output_token_details?.reasoning,
71
- cachedWriteTokens:
72
- response.usage_metadata?.input_token_details?.cache_creation,
73
- cachedReadTokens:
74
- response.usage_metadata?.input_token_details?.cache_read,
75
- },
76
- };
81
+ return {
82
+ message: response.toDict(),
83
+ rawToolCalls: toolCalls.map((tc) => ({
84
+ id: tc.id,
85
+ name: tc.name,
86
+ args: tc.args,
87
+ })),
88
+ usage: {
89
+ inputTokens: response.usage_metadata?.input_tokens,
90
+ outputTokens: response.usage_metadata?.output_tokens,
91
+ reasonTokens:
92
+ response.usage_metadata?.output_token_details?.reasoning,
93
+ cachedWriteTokens:
94
+ response.usage_metadata?.input_token_details?.cache_creation ||
95
+ (providerUsage.cacheWriteInputTokens as number | undefined),
96
+ cachedReadTokens:
97
+ response.usage_metadata?.input_token_details?.cache_read ||
98
+ (providerUsage.cacheReadInputTokens as number | undefined),
99
+ },
100
+ };
101
+ } finally {
102
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
103
+ }
77
104
  };
78
105
  }
79
106
 
@@ -83,7 +110,9 @@ export function createLangChainModelInvoker<TModel extends BaseChatModel<any> =
83
110
  * you don't need to reuse the invoker.
84
111
  */
85
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
- export async function invokeLangChainModel<TModel extends BaseChatModel<any> = BaseChatModel<any>>({
113
+ export async function invokeLangChainModel<
114
+ TModel extends BaseChatModel<any> = BaseChatModel<any>,
115
+ >({
87
116
  redis,
88
117
  model,
89
118
  hooks,
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export {
52
52
  queryParentWorkflowState,
53
53
  createRunAgentActivity,
54
54
  withParentWorkflowState,
55
+ getActivityContext,
55
56
  } from "./lib/activity";
56
57
  export type { AgentStateContext } from "./lib/activity";
57
58
 
@@ -8,6 +8,22 @@ import type {
8
8
  ToolHandlerResponse,
9
9
  } from "./tool-router/types";
10
10
 
11
+ /**
12
+ * Safely retrieve Temporal activity heartbeat and cancellation signal.
13
+ * Returns empty object when called outside a Temporal activity (e.g. tests).
14
+ */
15
+ export function getActivityContext(): {
16
+ heartbeat?: () => void;
17
+ signal?: AbortSignal;
18
+ } {
19
+ try {
20
+ const ctx = Context.current();
21
+ return { heartbeat: () => ctx.heartbeat(), signal: ctx.cancellationSignal };
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
11
27
  /**
12
28
  * Query the parent workflow's state from within an activity.
13
29
  * Resolves the workflow handle from the current activity context.
@@ -24,25 +40,36 @@ export async function queryParentWorkflowState<T>(
24
40
  }
25
41
 
26
42
  /**
27
- * Wraps a handler into a `RunAgentActivity` by auto-fetching the parent
28
- * workflow's agent state before each invocation.
43
+ * Wraps a handler into a scope-prefixed `RunAgentActivity` by auto-fetching
44
+ * the parent workflow's agent state before each invocation.
45
+ *
46
+ * Returns a `Record` with a single key `run<Scope>` so it can be spread
47
+ * into the activities object alongside adapter activities.
48
+ *
49
+ * @param scope - Workflow scope used to derive the activity name.
50
+ * `"myAgentWorkflow"` produces `{ runMyAgentWorkflow: fn }`.
29
51
  *
30
52
  * @example
31
53
  * ```typescript
32
54
  * import { createRunAgentActivity } from 'zeitlich';
33
- * import { createLangChainModelInvoker } from 'zeitlich/adapters/thread/langchain';
34
55
  *
35
- * const invoker = createLangChainModelInvoker({ redis, model });
36
- * return { runAgent: createRunAgentActivity(client, invoker) };
56
+ * return {
57
+ * ...adapter.createActivities("myAgentWorkflow"),
58
+ * ...createRunAgentActivity(client, adapter.invoker, "myAgentWorkflow"),
59
+ * };
37
60
  * ```
38
61
  */
39
62
  export function createRunAgentActivity<R, S extends BaseAgentState = BaseAgentState>(
40
63
  client: WorkflowClient,
41
64
  handler: (config: RunAgentConfig & { state: S }) => Promise<R>,
42
- ): (config: RunAgentConfig) => Promise<R> {
43
- return async (config: RunAgentConfig) => {
44
- const state = await queryParentWorkflowState<S>(client);
45
- return handler({ ...config, state });
65
+ scope: string,
66
+ ): Record<string, (config: RunAgentConfig) => Promise<R>> {
67
+ const name = `run${scope.charAt(0).toUpperCase()}${scope.slice(1)}`;
68
+ return {
69
+ [name]: async (config: RunAgentConfig) => {
70
+ const state = await queryParentWorkflowState<S>(client);
71
+ return handler({ ...config, state });
72
+ },
46
73
  };
47
74
  }
48
75
 
@@ -25,8 +25,9 @@ export type ThreadInit =
25
25
  * - `"new"` — create a fresh sandbox. Optionally pass `ctx` to
26
26
  * have the {@link SandboxManager}'s resolver produce creation options
27
27
  * (e.g. initial files) from workflow arguments.
28
- * - `"continue"` — resume a previously-paused sandbox (this session takes
29
- * ownership and the shutdown policy applies on exit).
28
+ * - `"continue"` — take ownership of an existing sandbox (paused or running).
29
+ * Paused sandboxes are automatically resumed. The shutdown policy applies
30
+ * on exit.
30
31
  * - `"fork"` — fork from an existing (or paused) sandbox; a new sandbox is
31
32
  * created and owned by this session.
32
33
  * - `"inherit"` — use a sandbox owned by someone else (e.g. a parent agent).
@@ -56,7 +57,10 @@ export type SandboxShutdown = "destroy" | "pause" | "keep";
56
57
  * Includes all base {@link SandboxShutdown} values plus:
57
58
  * - `"pause-until-parent-close"` — pause the sandbox on exit, then wait for
58
59
  * the parent workflow to signal when to destroy it.
60
+ * - `"keep-until-parent-close"` — leave the sandbox running on exit, then
61
+ * wait for the parent workflow to signal when to destroy it.
59
62
  */
60
63
  export type SubagentSandboxShutdown =
61
64
  | SandboxShutdown
62
- | "pause-until-parent-close";
65
+ | "pause-until-parent-close"
66
+ | "keep-until-parent-close";
@@ -2,5 +2,6 @@ export {
2
2
  queryParentWorkflowState,
3
3
  createRunAgentActivity,
4
4
  withParentWorkflowState,
5
+ getActivityContext,
5
6
  } from "../activity";
6
7
  export type { AgentStateContext } from "../activity";
@@ -2,6 +2,7 @@ export {
2
2
  queryParentWorkflowState,
3
3
  createRunAgentActivity,
4
4
  withParentWorkflowState,
5
+ getActivityContext,
5
6
  } from "./helpers";
6
7
  export type { AgentStateContext } from "./helpers";
7
8
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Workflow-safe proxy for runAgent activities with LLM-optimised defaults.
3
+ *
4
+ * Resolves the activity name from the scope using the same convention as
5
+ * {@link createRunAgentActivity}: `run<Scope>`.
6
+ * When no scope is provided, defaults to `workflowInfo().workflowType`.
7
+ *
8
+ * Import this from `zeitlich/workflow` in your Temporal workflow files.
9
+ *
10
+ * @typeParam M - SDK-native message type (e.g. `StoredMessage` for LangChain,
11
+ * `Anthropic.Messages.Message` for Anthropic, `Content` for Google GenAI).
12
+ * Must be provided for `SessionResult.finalMessage` to be correctly typed.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { proxyRunAgent } from 'zeitlich/workflow';
17
+ * import type { StoredMessage } from '@langchain/core/messages';
18
+ *
19
+ * // Auto-scoped to the current workflow name
20
+ * const runAgent = proxyRunAgent<StoredMessage>();
21
+ *
22
+ * // Explicit scope for subagents
23
+ * const runResearcher = proxyRunAgent<StoredMessage>("Researcher");
24
+ * ```
25
+ */
26
+ import { proxyActivities, workflowInfo } from "@temporalio/workflow";
27
+ import type { AgentResponse } from "./types";
28
+ import type { RunAgentConfig } from "../types";
29
+
30
+ export function proxyRunAgent<M = unknown>(
31
+ scope?: string,
32
+ options?: Parameters<typeof proxyActivities>[0],
33
+ ): (config: RunAgentConfig) => Promise<AgentResponse<M>> {
34
+ const resolvedScope = scope ?? workflowInfo().workflowType;
35
+ const name = `run${resolvedScope.charAt(0).toUpperCase()}${resolvedScope.slice(1)}`;
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const acts = proxyActivities<Record<string, (...args: any[]) => any>>(
38
+ options ?? {
39
+ startToCloseTimeout: "10m",
40
+ heartbeatTimeout: "1m",
41
+ retry: {
42
+ maximumAttempts: 3,
43
+ initialInterval: "10s",
44
+ maximumInterval: "2m",
45
+ backoffCoefficient: 3,
46
+ },
47
+ },
48
+ );
49
+ return acts[name] as (config: RunAgentConfig) => Promise<AgentResponse<M>>;
50
+ }
@@ -166,6 +166,10 @@ export class SandboxManager<
166
166
  await this.provider.pause(id, ttlSeconds);
167
167
  }
168
168
 
169
+ async resume(id: string): Promise<void> {
170
+ await this.provider.resume(id);
171
+ }
172
+
169
173
  async snapshot(id: string): Promise<SandboxSnapshot> {
170
174
  return this.provider.snapshot(id);
171
175
  }
@@ -222,6 +226,9 @@ export class SandboxManager<
222
226
  ): Promise<void> => {
223
227
  await this.pause(sandboxId, ttlSeconds);
224
228
  },
229
+ resumeSandbox: async (sandboxId: string): Promise<void> => {
230
+ await this.resume(sandboxId);
231
+ },
225
232
  snapshotSandbox: async (sandboxId: string): Promise<SandboxSnapshot> => {
226
233
  return this.snapshot(sandboxId);
227
234
  },