zeitlich 0.2.31 → 0.2.33

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 (87) hide show
  1. package/README.md +11 -10
  2. package/dist/{activities-DRSdt8Y3.d.ts → activities-YBD5BaHh.d.ts} +6 -5
  3. package/dist/{activities-qPkJDAiq.d.cts → activities-fnX8-vhR.d.cts} +6 -5
  4. package/dist/adapters/thread/anthropic/index.cjs +19 -47
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +12 -11
  7. package/dist/adapters/thread/anthropic/index.d.ts +12 -11
  8. package/dist/adapters/thread/anthropic/index.js +19 -47
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +1 -0
  11. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +4 -4
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +4 -4
  14. package/dist/adapters/thread/anthropic/workflow.js +1 -0
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.cjs +34 -53
  17. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  18. package/dist/adapters/thread/google-genai/index.d.cts +8 -8
  19. package/dist/adapters/thread/google-genai/index.d.ts +8 -8
  20. package/dist/adapters/thread/google-genai/index.js +34 -53
  21. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  22. package/dist/adapters/thread/google-genai/workflow.cjs +1 -0
  23. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/workflow.d.cts +4 -4
  25. package/dist/adapters/thread/google-genai/workflow.d.ts +4 -4
  26. package/dist/adapters/thread/google-genai/workflow.js +1 -0
  27. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  28. package/dist/adapters/thread/langchain/index.cjs +47 -24
  29. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  30. package/dist/adapters/thread/langchain/index.d.cts +13 -10
  31. package/dist/adapters/thread/langchain/index.d.ts +13 -10
  32. package/dist/adapters/thread/langchain/index.js +47 -24
  33. package/dist/adapters/thread/langchain/index.js.map +1 -1
  34. package/dist/adapters/thread/langchain/workflow.cjs +1 -0
  35. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  36. package/dist/adapters/thread/langchain/workflow.d.cts +4 -4
  37. package/dist/adapters/thread/langchain/workflow.d.ts +4 -4
  38. package/dist/adapters/thread/langchain/workflow.js +1 -0
  39. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  40. package/dist/index.cjs +42 -9
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +28 -13
  43. package/dist/index.d.ts +28 -13
  44. package/dist/index.js +41 -10
  45. package/dist/index.js.map +1 -1
  46. package/dist/{proxy-BkvkV2oU.d.ts → proxy-Br4unLTC.d.ts} +1 -1
  47. package/dist/{proxy-BDQ3Rj6R.d.cts → proxy-CTCYWjkr.d.cts} +1 -1
  48. package/dist/{thread-manager-BLgvv9Gf.d.cts → thread-manager-CUubPYPH.d.cts} +1 -1
  49. package/dist/{thread-manager-DowU4ntB.d.cts → thread-manager-Cv_BR28i.d.cts} +1 -1
  50. package/dist/{thread-manager-Cv82H1wi.d.ts → thread-manager-DKWxHUzD.d.ts} +1 -1
  51. package/dist/{thread-manager-HsAYkyAV.d.ts → thread-manager-YJLoc1vH.d.ts} +1 -1
  52. package/dist/{types-CjeGWQm1.d.cts → types-Bpq5fDI5.d.cts} +7 -4
  53. package/dist/{types-D6UKZZtj.d.ts → types-BxiT8w9d.d.ts} +1 -1
  54. package/dist/{types-BmS-Huc0.d.ts → types-CheCTLeV.d.ts} +7 -4
  55. package/dist/{types-e_38QaKo.d.cts → types-NJDyMyUx.d.cts} +1 -1
  56. package/dist/{workflow-CTcrPZAV.d.ts → workflow-D9nNERvs.d.ts} +30 -2
  57. package/dist/{workflow-CNshfqSO.d.cts → workflow-Od9vx5Jk.d.cts} +30 -2
  58. package/dist/workflow.cjs +22 -1
  59. package/dist/workflow.cjs.map +1 -1
  60. package/dist/workflow.d.cts +2 -2
  61. package/dist/workflow.d.ts +2 -2
  62. package/dist/workflow.js +22 -2
  63. package/dist/workflow.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/adapters/thread/anthropic/activities.ts +14 -3
  66. package/src/adapters/thread/anthropic/model-invoker.ts +15 -8
  67. package/src/adapters/thread/google-genai/activities.ts +18 -3
  68. package/src/adapters/thread/google-genai/model-invoker.ts +24 -14
  69. package/src/adapters/thread/langchain/activities.ts +14 -3
  70. package/src/adapters/thread/langchain/model-invoker.ts +63 -35
  71. package/src/index.ts +1 -0
  72. package/src/lib/activity.ts +36 -9
  73. package/src/lib/model/helpers.ts +1 -0
  74. package/src/lib/model/index.ts +1 -0
  75. package/src/lib/model/proxy.ts +50 -0
  76. package/src/lib/model/types.ts +3 -2
  77. package/src/lib/session/session-edge-cases.integration.test.ts +6 -0
  78. package/src/lib/session/session.integration.test.ts +3 -0
  79. package/src/lib/session/session.ts +4 -0
  80. package/src/lib/session/types.ts +7 -0
  81. package/src/lib/thread/proxy.ts +1 -0
  82. package/src/lib/types.ts +1 -0
  83. package/src/lib/virtual-fs/manager.ts +3 -3
  84. package/src/lib/virtual-fs/proxy.ts +3 -3
  85. package/src/lib/virtual-fs/types.ts +1 -2
  86. package/src/lib/virtual-fs/with-virtual-fs.ts +4 -4
  87. package/src/workflow.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zeitlich",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "[EXPERIMENTAL] An opinionated AI agent implementation for Temporal",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -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
  * }
@@ -164,6 +165,16 @@ export function createAnthropicAdapter(
164
165
  await thread.appendToolResult(id, toolCallId, toolName, content);
165
166
  },
166
167
 
168
+ async appendAgentMessage(
169
+ threadId: string,
170
+ id: string,
171
+ message: Anthropic.Messages.Message,
172
+ threadKey?: string,
173
+ ): Promise<void> {
174
+ const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey });
175
+ await thread.appendAssistantMessage(id, message.content);
176
+ },
177
+
167
178
  async forkThread(
168
179
  sourceThreadId: string,
169
180
  targetThreadId: string,
@@ -3,7 +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 { v4 as uuidv4 } from "uuid";
6
+ import { getActivityContext } from "../../../lib/activity";
7
7
 
8
8
  export interface AnthropicModelInvokerConfig {
9
9
  redis: Redis;
@@ -28,9 +28,9 @@ function toAnthropicTools(
28
28
  * Creates an Anthropic model invoker that satisfies the generic
29
29
  * `ModelInvoker<Anthropic.Messages.Message>` contract.
30
30
  *
31
- * Loads the conversation thread from Redis, invokes the Claude model via
32
- * `client.messages.create`, appends the AI response, and returns
33
- * 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.
33
+ * The caller is responsible for appending the response to the thread.
34
34
  *
35
35
  * @example
36
36
  * ```typescript
@@ -45,7 +45,7 @@ function toAnthropicTools(
45
45
  * model: 'claude-sonnet-4-20250514',
46
46
  * });
47
47
  *
48
- * return { runAgent: createRunAgentActivity(client, invoker) };
48
+ * return { ...createRunAgentActivity(client, invoker, "myAgent") };
49
49
  * ```
50
50
  */
51
51
  export function createAnthropicModelInvoker({
@@ -59,6 +59,7 @@ export function createAnthropicModelInvoker({
59
59
  config: ModelInvokerConfig,
60
60
  ): Promise<AgentResponse<Anthropic.Messages.Message>> {
61
61
  const { threadId, threadKey, state } = config;
62
+ const { heartbeat, signal } = getActivityContext();
62
63
 
63
64
  const thread = createAnthropicThreadManager({ redis, threadId, key: threadKey, hooks });
64
65
  const { messages, system } = await thread.prepareForInvocation();
@@ -66,15 +67,21 @@ export function createAnthropicModelInvoker({
66
67
  const anthropicTools = toAnthropicTools(state.tools);
67
68
  const tools = anthropicTools.length > 0 ? anthropicTools : undefined;
68
69
 
69
- const response = await client.messages.create({
70
+ const params: Anthropic.MessageCreateParams = {
70
71
  model,
71
72
  max_tokens: maxTokens,
72
73
  messages,
73
74
  ...(system ? { system } : {}),
74
75
  ...(tools ? { tools } : {}),
75
- });
76
+ };
77
+
78
+ const stream = client.messages.stream(params, { signal });
79
+
80
+ for await (const _event of stream) {
81
+ heartbeat?.();
82
+ }
76
83
 
77
- await thread.appendAssistantMessage(uuidv4(), response.content);
84
+ const response: Anthropic.Messages.Message = await stream.finalMessage();
78
85
 
79
86
  const toolCalls = response.content.filter(
80
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
  * }
@@ -194,6 +195,20 @@ export function createGoogleGenAIAdapter(
194
195
  );
195
196
  },
196
197
 
198
+ async appendAgentMessage(
199
+ threadId: string,
200
+ id: string,
201
+ message: Content,
202
+ threadKey?: string,
203
+ ): Promise<void> {
204
+ const thread = createGoogleGenAIThreadManager({
205
+ redis,
206
+ threadId,
207
+ key: threadKey,
208
+ });
209
+ await thread.appendModelContent(id, message.parts ?? []);
210
+ },
211
+
197
212
  async forkThread(
198
213
  sourceThreadId: string,
199
214
  targetThreadId: string,
@@ -1,9 +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 { v4 as uuidv4 } from "uuid";
6
+ import { getActivityContext } from "../../../lib/activity";
7
7
 
8
8
  export interface GoogleGenAIModelInvokerConfig {
9
9
  redis: Redis;
@@ -26,9 +26,9 @@ function toFunctionDeclarations(
26
26
  * Creates a Google GenAI model invoker that satisfies the generic
27
27
  * `ModelInvoker<Content>` contract.
28
28
  *
29
- * Loads the conversation thread from Redis, invokes the Gemini model via
30
- * `client.models.generateContent`, appends the AI response, and returns
31
- * 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.
31
+ * The caller is responsible for appending the response to the thread.
32
32
  *
33
33
  * @example
34
34
  * ```typescript
@@ -43,7 +43,7 @@ function toFunctionDeclarations(
43
43
  * model: 'gemini-2.5-flash',
44
44
  * });
45
45
  *
46
- * return { runAgent: createRunAgentActivity(client, invoker) };
46
+ * return { ...createRunAgentActivity(client, invoker, "myAgent") };
47
47
  * ```
48
48
  */
49
49
  export function createGoogleGenAIModelInvoker({
@@ -56,6 +56,7 @@ export function createGoogleGenAIModelInvoker({
56
56
  config: ModelInvokerConfig,
57
57
  ): Promise<AgentResponse<Content>> {
58
58
  const { threadId, threadKey, state } = config;
59
+ const { heartbeat, signal } = getActivityContext();
59
60
 
60
61
  const thread = createGoogleGenAIThreadManager({ redis, threadId, key: threadKey, hooks });
61
62
  const { contents, systemInstruction } =
@@ -65,21 +66,30 @@ export function createGoogleGenAIModelInvoker({
65
66
  const tools =
66
67
  functionDeclarations.length > 0 ? [{ functionDeclarations }] : undefined;
67
68
 
68
- const response = await client.models.generateContent({
69
+ const stream = await client.models.generateContentStream({
69
70
  model,
70
71
  contents,
71
72
  config: {
72
73
  ...(systemInstruction ? { systemInstruction } : {}),
73
74
  ...(tools ? { tools } : {}),
75
+ abortSignal: signal,
74
76
  },
75
77
  });
76
78
 
77
- const responseParts = response.candidates?.[0]?.content?.parts ?? [];
78
- 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
+ }
79
86
 
80
- await thread.appendModelContent(uuidv4(), responseParts);
87
+ if (!lastChunk) {
88
+ throw new Error("Google GenAI stream ended without producing any chunks");
89
+ }
81
90
 
82
- const functionCalls = response.functionCalls ?? [];
91
+ const modelContent: Content = { role: "model", parts: allParts };
92
+ const functionCalls = lastChunk.functionCalls ?? [];
83
93
 
84
94
  return {
85
95
  message: modelContent,
@@ -89,9 +99,9 @@ export function createGoogleGenAIModelInvoker({
89
99
  args: fc.args ?? {},
90
100
  })),
91
101
  usage: {
92
- inputTokens: response.usageMetadata?.promptTokenCount,
93
- outputTokens: response.usageMetadata?.candidatesTokenCount,
94
- cachedReadTokens: response.usageMetadata?.cachedContentTokenCount,
102
+ inputTokens: lastChunk.usageMetadata?.promptTokenCount,
103
+ outputTokens: lastChunk.usageMetadata?.candidatesTokenCount,
104
+ cachedReadTokens: lastChunk.usageMetadata?.cachedContentTokenCount,
95
105
  },
96
106
  };
97
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
  * ```
@@ -148,6 +148,17 @@ export function createLangChainAdapter(
148
148
  await thread.appendToolResult(id, toolCallId, "", content);
149
149
  },
150
150
 
151
+ async appendAgentMessage(
152
+ threadId: string,
153
+ id: string,
154
+ message: StoredMessage,
155
+ threadKey?: string,
156
+ ): Promise<void> {
157
+ const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
158
+ const patched = { ...message, data: { ...message.data, id } };
159
+ await thread.append([patched]);
160
+ },
161
+
151
162
  async forkThread(
152
163
  sourceThreadId: string,
153
164
  targetThreadId: string,
@@ -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,11 @@ 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
- * appends the AI response, 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.
29
+ * The caller is responsible for appending the response to the thread.
21
30
  *
22
31
  * @example
23
32
  * ```typescript
@@ -28,53 +37,70 @@ export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> =
28
37
  * const model = new ChatAnthropic({ model: "claude-sonnet-4-6" });
29
38
  * const invoker = createLangChainModelInvoker({ redis, model });
30
39
  *
31
- * return { runAgent: createRunAgentActivity(client, invoker) };
40
+ * return { ...createRunAgentActivity(client, invoker, "myAgent") };
32
41
  * ```
33
42
  */
34
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- export function createLangChainModelInvoker<TModel extends BaseChatModel<any> = BaseChatModel<any>>(
36
- { redis, model, hooks }: LangChainModelInvokerConfig<TModel>,
37
- ) {
44
+ export function createLangChainModelInvoker<
45
+ TModel extends BaseChatModel<any> = BaseChatModel<any>,
46
+ >({ redis, model, hooks }: LangChainModelInvokerConfig<TModel>) {
38
47
  return async function invokeLangChainModel(
39
- config: ModelInvokerConfig,
48
+ config: ModelInvokerConfig
40
49
  ): Promise<AgentResponse<StoredMessage>> {
41
50
  const { threadId, threadKey, agentName, state, metadata } = config;
51
+ const { heartbeat, signal } = getActivityContext();
42
52
 
43
- const thread = createLangChainThreadManager({ redis, threadId, key: threadKey, hooks });
53
+ const thread = createLangChainThreadManager({
54
+ redis,
55
+ threadId,
56
+ key: threadKey,
57
+ hooks,
58
+ });
44
59
  const runId = uuidv4();
45
60
 
46
61
  const { messages } = await thread.prepareForInvocation();
47
- const response = await model.invoke(
48
- messages,
49
- {
62
+
63
+ const heartbeatInterval = heartbeat
64
+ ? setInterval(() => heartbeat(), 30_000)
65
+ : undefined;
66
+
67
+ try {
68
+ const response = await model.invoke(messages, {
50
69
  runName: agentName,
51
70
  runId,
52
71
  metadata: { thread_id: `${agentName}-${threadId}`, ...metadata },
53
72
  tools: state.tools,
54
- },
55
- );
73
+ signal,
74
+ });
56
75
 
57
- await thread.append([response.toDict()]);
76
+ const toolCalls = response.tool_calls ?? [];
58
77
 
59
- const toolCalls = response.tool_calls ?? [];
78
+ const providerUsage =
79
+ (response.response_metadata?.usage as Record<string, unknown>) ?? {};
60
80
 
61
- return {
62
- message: response.toDict(),
63
- rawToolCalls: toolCalls.map((tc) => ({
64
- id: tc.id,
65
- name: tc.name,
66
- args: tc.args,
67
- })),
68
- usage: {
69
- inputTokens: response.usage_metadata?.input_tokens,
70
- outputTokens: response.usage_metadata?.output_tokens,
71
- reasonTokens: response.usage_metadata?.output_token_details?.reasoning,
72
- cachedWriteTokens:
73
- response.usage_metadata?.input_token_details?.cache_creation,
74
- cachedReadTokens:
75
- response.usage_metadata?.input_token_details?.cache_read,
76
- },
77
- };
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
+ }
78
104
  };
79
105
  }
80
106
 
@@ -84,7 +110,9 @@ export function createLangChainModelInvoker<TModel extends BaseChatModel<any> =
84
110
  * you don't need to reuse the invoker.
85
111
  */
86
112
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
- export async function invokeLangChainModel<TModel extends BaseChatModel<any> = BaseChatModel<any>>({
113
+ export async function invokeLangChainModel<
114
+ TModel extends BaseChatModel<any> = BaseChatModel<any>,
115
+ >({
88
116
  redis,
89
117
  model,
90
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
 
@@ -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
+ }
@@ -33,8 +33,9 @@ export interface ModelInvokerConfig {
33
33
 
34
34
  /**
35
35
  * Generic model invocation contract.
36
- * Implementations load the thread, call the LLM, append the response,
37
- * and return a normalised AgentResponse.
36
+ * Implementations load the thread, call the LLM, and return a normalised
37
+ * AgentResponse. The caller (workflow) is responsible for appending the
38
+ * response to the thread with a deterministic ID.
38
39
  *
39
40
  * Framework adapters (e.g. `zeitlich/langchain`) provide concrete
40
41
  * implementations of this type.
@@ -88,6 +88,9 @@ function createMockThreadOps() {
88
88
  appendSystemMessage: async (threadId, id, content) => {
89
89
  log.push({ op: "appendSystemMessage", args: [threadId, id, content] });
90
90
  },
91
+ appendAgentMessage: async (threadId, id, message) => {
92
+ log.push({ op: "appendAgentMessage", args: [threadId, id, message] });
93
+ },
91
94
  forkThread: async (source, target) => {
92
95
  log.push({ op: "forkThread", args: [source, target] });
93
96
  },
@@ -752,6 +755,9 @@ describe("createSession edge cases", () => {
752
755
  appendSystemMessage: async (threadId, id, content) => {
753
756
  log.push({ op: "appendSystemMessage", args: [threadId, id, content] });
754
757
  },
758
+ appendAgentMessage: async (threadId, id, message) => {
759
+ log.push({ op: "appendAgentMessage", args: [threadId, id, message] });
760
+ },
755
761
  forkThread: async (source, target) => {
756
762
  log.push({ op: "forkThread", args: [source, target] });
757
763
  },
@@ -99,6 +99,9 @@ function createMockThreadOps() {
99
99
  appendSystemMessage: async (threadId, id, content) => {
100
100
  log.push({ op: "appendSystemMessage", args: [threadId, id, content] });
101
101
  },
102
+ appendAgentMessage: async (threadId, id, message) => {
103
+ log.push({ op: "appendAgentMessage", args: [threadId, id, message] });
104
+ },
102
105
  forkThread: async (source, target) => {
103
106
  log.push({ op: "forkThread", args: [source, target] });
104
107
  },
@@ -139,6 +139,7 @@ export async function createSession<
139
139
  appendHumanMessage,
140
140
  initializeThread,
141
141
  appendSystemMessage,
142
+ appendAgentMessage,
142
143
  forkThread,
143
144
  } = threadOps;
144
145
 
@@ -285,6 +286,7 @@ export async function createSession<
285
286
  : result.fileTree;
286
287
  stateManager.mergeUpdate({
287
288
  fileTree,
289
+ virtualFsCtx: virtualFsConfig.ctx,
288
290
  ...(skillFiles && { inlineFiles: skillFiles }),
289
291
  } as Partial<AgentState<TState>>);
290
292
  }
@@ -355,6 +357,8 @@ export async function createSession<
355
357
  metadata,
356
358
  });
357
359
 
360
+ await appendAgentMessage(threadId, uuid4(), message, threadKey);
361
+
358
362
  if (usage) {
359
363
  stateManager.updateUsage(usage);
360
364
  }
@@ -39,6 +39,13 @@ export interface ThreadOps<TContent = string> {
39
39
  ): Promise<void>;
40
40
  /** Append a tool result to the thread */
41
41
  appendToolResult(id: string, config: ToolResultConfig): Promise<void>;
42
+ /** Append the model's response to the thread */
43
+ appendAgentMessage(
44
+ threadId: string,
45
+ id: string,
46
+ message: unknown,
47
+ threadKey?: string
48
+ ): Promise<void>;
42
49
  /** Append a system message to the thread */
43
50
  appendSystemMessage(
44
51
  threadId: string,