zeitlich 0.2.11 → 0.2.13

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 (46) hide show
  1. package/README.md +313 -126
  2. package/dist/adapters/langchain/index.cjs +270 -0
  3. package/dist/adapters/langchain/index.cjs.map +1 -0
  4. package/dist/adapters/langchain/index.d.cts +132 -0
  5. package/dist/adapters/langchain/index.d.ts +132 -0
  6. package/dist/adapters/langchain/index.js +265 -0
  7. package/dist/adapters/langchain/index.js.map +1 -0
  8. package/dist/index.cjs +89 -209
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +62 -46
  11. package/dist/index.d.ts +62 -46
  12. package/dist/index.js +88 -208
  13. package/dist/index.js.map +1 -1
  14. package/dist/{workflow-BhjsEQc1.d.cts → model-invoker-y_zlyMqu.d.cts} +45 -482
  15. package/dist/{workflow-BhjsEQc1.d.ts → model-invoker-y_zlyMqu.d.ts} +45 -482
  16. package/dist/thread-manager-qc0g5Rvd.d.cts +39 -0
  17. package/dist/thread-manager-qc0g5Rvd.d.ts +39 -0
  18. package/dist/workflow.cjs +59 -27
  19. package/dist/workflow.cjs.map +1 -1
  20. package/dist/workflow.d.cts +459 -6
  21. package/dist/workflow.d.ts +459 -6
  22. package/dist/workflow.js +60 -29
  23. package/dist/workflow.js.map +1 -1
  24. package/package.json +17 -2
  25. package/src/adapters/langchain/activities.ts +120 -0
  26. package/src/adapters/langchain/index.ts +38 -0
  27. package/src/adapters/langchain/model-invoker.ts +102 -0
  28. package/src/adapters/langchain/thread-manager.ts +142 -0
  29. package/src/index.ts +24 -23
  30. package/src/lib/fs.ts +25 -0
  31. package/src/lib/model-invoker.ts +15 -75
  32. package/src/lib/session.ts +52 -21
  33. package/src/lib/state-manager.ts +23 -5
  34. package/src/lib/thread-id.ts +25 -0
  35. package/src/lib/thread-manager.ts +18 -142
  36. package/src/lib/tool-router.ts +12 -18
  37. package/src/lib/types.ts +26 -10
  38. package/src/lib/workflow-helpers.ts +50 -0
  39. package/src/tools/ask-user-question/handler.ts +25 -1
  40. package/src/tools/bash/handler.ts +13 -0
  41. package/src/tools/subagent/handler.ts +16 -5
  42. package/src/tools/subagent/tool.ts +34 -15
  43. package/src/workflow.ts +26 -7
  44. package/tsup.config.ts +1 -0
  45. package/src/activities.ts +0 -91
  46. package/src/plugin.ts +0 -28
@@ -5,7 +5,6 @@ import {
5
5
  setHandler,
6
6
  ApplicationFailure,
7
7
  } from "@temporalio/workflow";
8
- import type { ZeitlichSharedActivities } from "../activities";
9
8
  import type {
10
9
  ThreadOps,
11
10
  AgentConfig,
@@ -20,7 +19,8 @@ import {
20
19
  type ParsedToolCallUnion,
21
20
  type ToolMap,
22
21
  } from "./tool-router";
23
- import type { MessageContent } from "@langchain/core/messages";
22
+ import type { MessageContent } from "./types";
23
+ import { getShortId } from "./thread-id";
24
24
 
25
25
  export interface ZeitlichSession<M = unknown> {
26
26
  runSession<T extends JsonSerializable<T>>(args: {
@@ -42,8 +42,39 @@ export interface SessionLifecycleHooks {
42
42
  onSessionEnd?: SessionEndHook;
43
43
  }
44
44
 
45
+ /**
46
+ * Creates an agent session that manages the agent loop: LLM invocation,
47
+ * tool routing, subagent coordination, and lifecycle hooks.
48
+ *
49
+ * @param config - Session and agent configuration (merged `SessionConfig` and `AgentConfig`)
50
+ * @returns A session object with `runSession()` to start the agent loop
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * import { createSession, createAgentStateManager, defineTool, bashTool } from 'zeitlich/workflow';
55
+ *
56
+ * const stateManager = createAgentStateManager({
57
+ * initialState: { systemPrompt: "You are a helpful assistant." },
58
+ * agentName: "my-agent",
59
+ * });
60
+ *
61
+ * const session = await createSession({
62
+ * agentName: "my-agent",
63
+ * maxTurns: 20,
64
+ * threadId: runId,
65
+ * runAgent: runAgentActivity,
66
+ * buildContextMessage: () => [{ type: "text", text: prompt }],
67
+ * subagents: [researcherSubagent],
68
+ * tools: {
69
+ * Bash: defineTool({ ...bashTool, handler: bashHandlerActivity }),
70
+ * },
71
+ * });
72
+ *
73
+ * const { finalMessage, exitReason } = await session.runSession({ stateManager });
74
+ * ```
75
+ */
45
76
  export const createSession = async <T extends ToolMap, M = unknown>({
46
- threadId,
77
+ threadId: providedThreadId,
47
78
  agentName,
48
79
  maxTurns = 50,
49
80
  metadata = {},
@@ -56,8 +87,11 @@ export const createSession = async <T extends ToolMap, M = unknown>({
56
87
  processToolsInParallel = true,
57
88
  hooks = {},
58
89
  appendSystemPrompt = true,
90
+ continueThread = false,
59
91
  waitForInputTimeout = "48h",
60
92
  }: SessionConfig<T, M> & AgentConfig): Promise<ZeitlichSession<M>> => {
93
+ const threadId = providedThreadId ?? getShortId();
94
+
61
95
  const {
62
96
  appendToolResult,
63
97
  appendHumanMessage,
@@ -129,15 +163,18 @@ export const createSession = async <T extends ToolMap, M = unknown>({
129
163
 
130
164
  const systemPrompt = stateManager.getSystemPrompt();
131
165
 
132
- await initializeThread(threadId);
133
- if (appendSystemPrompt) {
134
- if (!systemPrompt || systemPrompt.trim() === "") {
135
- throw ApplicationFailure.create({
136
- message: "No system prompt in state",
137
- nonRetryable: true,
138
- });
166
+ if (!continueThread) {
167
+ if (appendSystemPrompt) {
168
+ if (!systemPrompt || systemPrompt.trim() === "") {
169
+ throw ApplicationFailure.create({
170
+ message: "No system prompt in state",
171
+ nonRetryable: true,
172
+ });
173
+ }
174
+ await appendSystemMessage(threadId, systemPrompt);
175
+ } else {
176
+ await initializeThread(threadId);
139
177
  }
140
- await appendSystemMessage(threadId, systemPrompt);
141
178
  }
142
179
  await appendHumanMessage(threadId, await buildContextMessage());
143
180
 
@@ -242,8 +279,9 @@ export const createSession = async <T extends ToolMap, M = unknown>({
242
279
  };
243
280
 
244
281
  /**
245
- * Proxy the default ZeitlichSharedActivities as ThreadOps<StoredMessage>.
246
- * Call this in workflow code for the standard LangChain/StoredMessage setup.
282
+ * Proxy the adapter's thread operations as Temporal activities.
283
+ * Call this in workflow code to delegate thread operations to the
284
+ * adapter-provided activities registered on the worker.
247
285
  *
248
286
  * @example
249
287
  * ```typescript
@@ -256,7 +294,7 @@ export const createSession = async <T extends ToolMap, M = unknown>({
256
294
  export function proxyDefaultThreadOps(
257
295
  options?: Parameters<typeof proxyActivities>[0]
258
296
  ): ThreadOps {
259
- const activities = proxyActivities<ZeitlichSharedActivities>(
297
+ return proxyActivities<ThreadOps>(
260
298
  options ?? {
261
299
  startToCloseTimeout: "10s",
262
300
  retry: {
@@ -267,11 +305,4 @@ export function proxyDefaultThreadOps(
267
305
  },
268
306
  }
269
307
  );
270
-
271
- return {
272
- initializeThread: activities.initializeThread,
273
- appendHumanMessage: activities.appendHumanMessage,
274
- appendToolResult: activities.appendToolResult,
275
- appendSystemMessage: activities.appendSystemMessage,
276
- };
277
308
  }
@@ -151,13 +151,31 @@ export interface AgentStateManager<TCustom extends JsonSerializable<TCustom>> {
151
151
 
152
152
  /**
153
153
  * Creates an agent state manager for tracking workflow state.
154
+ * Automatically registers Temporal query and update handlers for the agent.
154
155
  *
155
- * @param initialState - Optional initial values for base and custom state
156
- * Base state defaults: status="RUNNING", version=0, turns=0, tasks=empty, fileTree=[]
156
+ * @param options.agentName - Unique agent name, used to derive query/update handler names
157
+ * @param options.initialState - Optional initial values for base and custom state.
158
+ * Use `systemPrompt` here to set the agent's system prompt.
159
+ * Base state defaults: status="RUNNING", version=0, turns=0, tasks=empty
157
160
  *
158
- * Note: Due to Temporal's workflow isolation, handlers must be set up
159
- * in the workflow file using defineQuery/defineUpdate and setHandler.
160
- * This manager provides the state and logic needed for those handlers.
161
+ * @example
162
+ * ```typescript
163
+ * const stateManager = createAgentStateManager({
164
+ * initialState: {
165
+ * systemPrompt: "You are a helpful assistant.",
166
+ * },
167
+ * agentName: "my-agent",
168
+ * });
169
+ *
170
+ * // With custom state fields
171
+ * const stateManager = createAgentStateManager({
172
+ * initialState: {
173
+ * systemPrompt: agentConfig.systemPrompt,
174
+ * customField: "value",
175
+ * },
176
+ * agentName: agentConfig.agentName,
177
+ * });
178
+ * ```
161
179
  */
162
180
  export function createAgentStateManager<
163
181
  TCustom extends JsonSerializable<TCustom> = Record<string, never>,
@@ -0,0 +1,25 @@
1
+ import { uuid4 } from "@temporalio/workflow";
2
+
3
+ const BASE62 =
4
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
5
+
6
+ /**
7
+ * Generate a compact, workflow-deterministic identifier.
8
+ *
9
+ * Uses Temporal's `uuid4()` internally (seeded by the workflow's RNG),
10
+ * then re-encodes the hex bytes into a base-62 alphabet for a shorter,
11
+ * more token-efficient identifier (~3 tokens vs ~10 for a full UUID).
12
+ *
13
+ * Suitable for thread IDs, child workflow IDs, or any workflow-scoped identifier.
14
+ *
15
+ * @param length - Number of base-62 characters (default 12, ~71 bits of entropy)
16
+ */
17
+ export function getShortId(length = 12): string {
18
+ const hex = uuid4().replace(/-/g, "");
19
+ let result = "";
20
+ for (let i = 0; i < length; i++) {
21
+ const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
22
+ result += BASE62[byte % BASE62.length];
23
+ }
24
+ return result;
25
+ }
@@ -1,17 +1,5 @@
1
1
  import type Redis from "ioredis";
2
2
 
3
- import {
4
- type $InferMessageContent,
5
- AIMessage,
6
- HumanMessage,
7
- type MessageContent,
8
- type MessageStructure,
9
- type StoredMessage,
10
- SystemMessage,
11
- ToolMessage,
12
- } from "@langchain/core/messages";
13
- import { v4 as uuidv4 } from "uuid";
14
-
15
3
  const THREAD_TTL_SECONDS = 60 * 60 * 24 * 90; // 90 days
16
4
 
17
5
  /**
@@ -39,13 +27,7 @@ function getThreadKey(threadId: string, key: string): string {
39
27
  return `thread:${threadId}:${key}`;
40
28
  }
41
29
 
42
- /**
43
- * Content for a tool message response.
44
- * Can be a simple string or complex content parts (text, images, cache points, etc.)
45
- */
46
- export type ToolMessageContent = $InferMessageContent<MessageStructure, "tool">;
47
-
48
- export interface ThreadManagerConfig<T = StoredMessage> {
30
+ export interface ThreadManagerConfig<T> {
49
31
  redis: Redis;
50
32
  threadId: string;
51
33
  /** Thread key, defaults to 'messages' */
@@ -57,7 +39,6 @@ export interface ThreadManagerConfig<T = StoredMessage> {
57
39
  /**
58
40
  * Extract a unique id from a message for idempotent appends.
59
41
  * When provided, `append` uses an atomic Lua script to skip duplicate writes.
60
- * Defaults to `StoredMessage.data.id` for the standard ThreadManager.
61
42
  */
62
43
  idOf?: (message: T) => string;
63
44
  }
@@ -78,51 +59,12 @@ export interface BaseThreadManager<T> {
78
59
  delete(): Promise<void>;
79
60
  }
80
61
 
81
- /** Thread manager with StoredMessage convenience helpers */
82
- export interface ThreadManager extends BaseThreadManager<StoredMessage> {
83
- /** Create a HumanMessage (returns StoredMessage for storage) */
84
- createHumanMessage(content: string | MessageContent): StoredMessage;
85
- /** Create a SystemMessage (returns StoredMessage for storage) */
86
- createSystemMessage(content: string): StoredMessage;
87
- /** Create an AIMessage with optional additional kwargs */
88
- createAIMessage(
89
- content: string | MessageContent,
90
- kwargs?: { header?: string; options?: string[]; multiSelect?: boolean }
91
- ): StoredMessage;
92
- /** Create a ToolMessage */
93
- createToolMessage(
94
- content: ToolMessageContent,
95
- toolCallId: string
96
- ): StoredMessage;
97
- /** Create and append a HumanMessage */
98
- appendHumanMessage(content: string | MessageContent): Promise<void>;
99
- /** Create and append a SystemMessage */
100
- appendSystemMessage(content: string): Promise<void>;
101
- /** Create and append a ToolMessage */
102
- appendToolMessage(
103
- content: ToolMessageContent,
104
- toolCallId: string
105
- ): Promise<void>;
106
- /** Create and append an AIMessage */
107
- appendAIMessage(content: string | MessageContent): Promise<void>;
108
- }
109
-
110
- /** Default id extractor for StoredMessage */
111
- function storedMessageId(msg: StoredMessage): string {
112
- return msg.data.id ?? "";
113
- }
114
-
115
62
  /**
116
- * Creates a thread manager for handling conversation state in Redis.
117
- * Without generic args, returns a full ThreadManager with StoredMessage helpers.
118
- * With a custom type T, returns a BaseThreadManager<T>.
63
+ * Creates a generic thread manager for handling conversation state in Redis.
64
+ * Framework-agnostic works with any serializable message type.
119
65
  */
120
- export function createThreadManager(config: ThreadManagerConfig): ThreadManager;
121
- export function createThreadManager<T>(
122
- config: ThreadManagerConfig<T>
123
- ): BaseThreadManager<T>;
124
66
  export function createThreadManager<T>(
125
- config: ThreadManagerConfig<T>
67
+ config: ThreadManagerConfig<T>,
126
68
  ): BaseThreadManager<T> {
127
69
  const {
128
70
  redis,
@@ -130,28 +72,33 @@ export function createThreadManager<T>(
130
72
  key = "messages",
131
73
  serialize = (m: T): string => JSON.stringify(m),
132
74
  deserialize = (raw: string): T => JSON.parse(raw) as T,
75
+ idOf,
133
76
  } = config;
134
77
  const redisKey = getThreadKey(threadId, key);
78
+ const metaKey = getThreadKey(threadId, `${key}:meta`);
135
79
 
136
- // Default idOf for StoredMessage when no custom serialization is used
137
- const idOf =
138
- config.idOf ??
139
- (!config.serialize
140
- ? (storedMessageId as unknown as (m: T) => string)
141
- : undefined);
80
+ async function assertThreadExists(): Promise<void> {
81
+ const exists = await redis.exists(metaKey);
82
+ if (!exists) {
83
+ throw new Error(`Thread "${threadId}" (key: ${key}) does not exist`);
84
+ }
85
+ }
142
86
 
143
- const base: BaseThreadManager<T> = {
87
+ return {
144
88
  async initialize(): Promise<void> {
145
89
  await redis.del(redisKey);
90
+ await redis.set(metaKey, "1", "EX", THREAD_TTL_SECONDS);
146
91
  },
147
92
 
148
93
  async load(): Promise<T[]> {
94
+ await assertThreadExists();
149
95
  const data = await redis.lrange(redisKey, 0, -1);
150
96
  return data.map(deserialize);
151
97
  },
152
98
 
153
99
  async append(messages: T[]): Promise<void> {
154
100
  if (messages.length === 0) return;
101
+ await assertThreadExists();
155
102
 
156
103
  if (idOf) {
157
104
  const dedupId = messages.map(idOf).join(":");
@@ -162,7 +109,7 @@ export function createThreadManager<T>(
162
109
  dedupKey,
163
110
  redisKey,
164
111
  String(THREAD_TTL_SECONDS),
165
- ...messages.map(serialize)
112
+ ...messages.map(serialize),
166
113
  );
167
114
  } else {
168
115
  await redis.rpush(redisKey, ...messages.map(serialize));
@@ -171,78 +118,7 @@ export function createThreadManager<T>(
171
118
  },
172
119
 
173
120
  async delete(): Promise<void> {
174
- await redis.del(redisKey);
175
- },
176
- };
177
-
178
- // If no custom serialize/deserialize were provided and T defaults to StoredMessage,
179
- // the overload guarantees the caller gets ThreadManager with convenience helpers.
180
- const helpers = {
181
- createHumanMessage(content: string | MessageContent): StoredMessage {
182
- return new HumanMessage({
183
- id: uuidv4(),
184
- content: content as string,
185
- }).toDict();
186
- },
187
-
188
- createSystemMessage(content: string): StoredMessage {
189
- return new SystemMessage({
190
- id: uuidv4(),
191
- content: content as string,
192
- }).toDict();
193
- },
194
-
195
- createAIMessage(
196
- content: string,
197
- kwargs?: { header?: string; options?: string[]; multiSelect?: boolean }
198
- ): StoredMessage {
199
- return new AIMessage({
200
- id: uuidv4(),
201
- content,
202
- additional_kwargs: kwargs
203
- ? {
204
- header: kwargs.header,
205
- options: kwargs.options,
206
- multiSelect: kwargs.multiSelect,
207
- }
208
- : undefined,
209
- }).toDict();
210
- },
211
-
212
- createToolMessage(
213
- content: ToolMessageContent,
214
- toolCallId: string
215
- ): StoredMessage {
216
- return new ToolMessage({
217
- id: uuidv4(),
218
- content: content as MessageContent,
219
- tool_call_id: toolCallId,
220
- }).toDict();
221
- },
222
-
223
- async appendHumanMessage(content: string | MessageContent): Promise<void> {
224
- const message = helpers.createHumanMessage(content);
225
- await (base as BaseThreadManager<StoredMessage>).append([message]);
226
- },
227
-
228
- async appendToolMessage(
229
- content: ToolMessageContent,
230
- toolCallId: string
231
- ): Promise<void> {
232
- const message = helpers.createToolMessage(content, toolCallId);
233
- await (base as BaseThreadManager<StoredMessage>).append([message]);
234
- },
235
-
236
- async appendAIMessage(content: string | MessageContent): Promise<void> {
237
- const message = helpers.createAIMessage(content as string);
238
- await (base as BaseThreadManager<StoredMessage>).append([message]);
239
- },
240
-
241
- async appendSystemMessage(content: string): Promise<void> {
242
- const message = helpers.createSystemMessage(content);
243
- await (base as BaseThreadManager<StoredMessage>).append([message]);
121
+ await redis.del(redisKey, metaKey);
244
122
  },
245
123
  };
246
-
247
- return Object.assign(base, helpers);
248
124
  }
@@ -1,6 +1,5 @@
1
- import type { MessageToolDefinition } from "@langchain/core/messages";
2
- import type { ToolMessageContent } from "./thread-manager";
3
1
  import type {
2
+ ToolMessageContent,
4
3
  Hooks,
5
4
  PostToolUseFailureHookResult,
6
5
  PreToolUseHookResult,
@@ -23,8 +22,6 @@ import {
23
22
  import { createReadSkillHandler } from "../tools/read-skill/handler";
24
23
  import { ApplicationFailure } from "@temporalio/workflow";
25
24
 
26
- export type { ToolMessageContent };
27
-
28
25
  // ============================================================================
29
26
  // Tool Definition Types (merged from tool-registry.ts)
30
27
  // ============================================================================
@@ -90,16 +87,6 @@ export type ToolMap = Record<
90
87
  }
91
88
  >;
92
89
 
93
- /**
94
- * Converts a ToolMap to MessageStructure-compatible tools type.
95
- * Maps each tool's name to a MessageToolDefinition with inferred input type from the schema.
96
- */
97
- export type ToolMapToMessageTools<T extends ToolMap> = {
98
- [K in keyof T as T[K]["name"]]: MessageToolDefinition<
99
- z.infer<T[K]["schema"]>
100
- >;
101
- };
102
-
103
90
  /**
104
91
  * Extract the tool names from a tool map (uses the tool's name property, not the key).
105
92
  */
@@ -163,6 +150,8 @@ export interface ToolHandlerResponse<TResult = null> {
163
150
  resultAppended?: boolean;
164
151
  /** Token usage from the tool execution (e.g. child agent invocations) */
165
152
  usage?: TokenUsage;
153
+ /** Thread ID used by the handler (surfaced to the LLM for subagent thread continuation) */
154
+ threadId?: string;
166
155
  }
167
156
 
168
157
  /**
@@ -953,8 +942,10 @@ export function defineSubagent<
953
942
  config: Omit<SubagentConfig<TResult>, "hooks" | "workflow" | "context"> & {
954
943
  workflow:
955
944
  | string
956
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
957
- | ((input: { prompt: string; context: TContext }) => Promise<any>);
945
+ | ((input: {
946
+ prompt: string;
947
+ context: TContext;
948
+ }) => Promise<ToolHandlerResponse<z.infer<TResult> | null>>);
958
949
  context: TContext;
959
950
  hooks?: SubagentHooks<SubagentArgs, z.infer<TResult>>;
960
951
  }
@@ -962,8 +953,11 @@ export function defineSubagent<
962
953
  // Without context — verifies workflow accepts { prompt }
963
954
  export function defineSubagent<TResult extends z.ZodType = z.ZodType>(
964
955
  config: Omit<SubagentConfig<TResult>, "hooks" | "workflow"> & {
965
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
966
- workflow: string | ((input: { prompt: string }) => Promise<any>);
956
+ workflow:
957
+ | string
958
+ | ((input: {
959
+ prompt: string;
960
+ }) => Promise<ToolHandlerResponse<z.infer<TResult> | null>>);
967
961
  hooks?: SubagentHooks<SubagentArgs, z.infer<TResult>>;
968
962
  }
969
963
  ): SubagentConfig<TResult>;
package/src/lib/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { ToolMessageContent } from "./thread-manager";
2
1
  import type {
3
2
  InferToolResults,
4
3
  ParsedToolCallUnion,
@@ -9,10 +8,22 @@ import type {
9
8
  } from "./tool-router";
10
9
  import type { Skill } from "./skills/types";
11
10
 
12
- import type { MessageContent, StoredMessage } from "@langchain/core/messages";
13
11
  import type { Duration } from "@temporalio/common";
14
12
  import type { z } from "zod";
15
13
 
14
+ // ============================================================================
15
+ // Framework-agnostic message types
16
+ // ============================================================================
17
+
18
+ /** A single content part within a structured message (text, image, etc.) */
19
+ export type ContentPart = { type: string; [key: string]: unknown };
20
+
21
+ /** Message content — plain string or an array of structured content parts */
22
+ export type MessageContent = string | ContentPart[];
23
+
24
+ /** Content returned by a tool handler */
25
+ export type ToolMessageContent = MessageContent;
26
+
16
27
  /**
17
28
  * Agent execution status
18
29
  */
@@ -36,7 +47,7 @@ export interface BaseAgentState {
36
47
  totalInputTokens: number;
37
48
  totalOutputTokens: number;
38
49
  cachedWriteTokens: number;
39
- cachedReadtTokens: number;
50
+ cachedReadTokens: number;
40
51
  }
41
52
 
42
53
  /**
@@ -66,7 +77,7 @@ export interface TokenUsage {
66
77
  /**
67
78
  * Agent response from LLM invocation
68
79
  */
69
- export interface AgentResponse<M = StoredMessage> {
80
+ export interface AgentResponse<M = unknown> {
70
81
  message: M;
71
82
  rawToolCalls: RawToolCall[];
72
83
  usage?: TokenUsage;
@@ -75,7 +86,6 @@ export interface AgentResponse<M = StoredMessage> {
75
86
  /**
76
87
  * Thread operations required by a session.
77
88
  * Consumers provide these — typically by wrapping Temporal activities.
78
- * Use `proxyDefaultThreadOps()` for the default StoredMessage implementation.
79
89
  */
80
90
  export interface ThreadOps {
81
91
  /** Initialize an empty thread */
@@ -104,9 +114,9 @@ export interface AgentConfig {
104
114
  /**
105
115
  * Configuration for a Zeitlich agent session
106
116
  */
107
- export interface SessionConfig<T extends ToolMap, M = StoredMessage> {
108
- /** The thread ID to use for the session */
109
- threadId: string;
117
+ export interface SessionConfig<T extends ToolMap, M = unknown> {
118
+ /** The thread ID to use for the session (defaults to a short generated ID) */
119
+ threadId?: string;
110
120
  /** Metadata for the session */
111
121
  metadata?: Record<string, unknown>;
112
122
  /** Whether to append the system prompt as message to the thread */
@@ -132,6 +142,8 @@ export interface SessionConfig<T extends ToolMap, M = StoredMessage> {
132
142
  * Returns MessageContent array for the initial HumanMessage.
133
143
  */
134
144
  buildContextMessage: () => MessageContent | Promise<MessageContent>;
145
+ /** When true, skip thread initialization and system prompt — append only the new human message to the existing thread. */
146
+ continueThread?: boolean;
135
147
  /** How long to wait for input before cancelling the workflow */
136
148
  waitForInputTimeout?: Duration;
137
149
  }
@@ -162,7 +174,7 @@ export interface RunAgentConfig extends AgentConfig {
162
174
  /**
163
175
  * Type signature for workflow-specific runAgent activity
164
176
  */
165
- export type RunAgentActivity<M = StoredMessage> = (
177
+ export type RunAgentActivity<M = unknown> = (
166
178
  config: RunAgentConfig
167
179
  ) => Promise<AgentResponse<M>>;
168
180
 
@@ -184,7 +196,7 @@ export interface ToolResultConfig {
184
196
 
185
197
  export type SubagentWorkflow<TResult extends z.ZodType = z.ZodType> = (
186
198
  input: SubagentInput
187
- ) => Promise<ToolHandlerResponse<TResult | null>>;
199
+ ) => Promise<ToolHandlerResponse<z.infer<TResult> | null>>;
188
200
 
189
201
  /** Infer the z.infer'd result type from a SubagentConfig, or null if no schema */
190
202
  export type InferSubagentResult<T extends SubagentConfig> =
@@ -210,6 +222,8 @@ export interface SubagentConfig<TResult extends z.ZodType = z.ZodType> {
210
222
  resultSchema?: TResult;
211
223
  /** Optional static context passed to the subagent on every invocation */
212
224
  context?: Record<string, unknown>;
225
+ /** Allow the parent agent to pass a threadId for this subagent to continue (default: false) */
226
+ allowThreadContinuation?: boolean;
213
227
  /** Per-subagent lifecycle hooks */
214
228
  hooks?: SubagentHooks;
215
229
  }
@@ -250,6 +264,8 @@ export interface SubagentInput {
250
264
  prompt: string;
251
265
  /** Optional context parameters passed from the parent agent */
252
266
  context?: Record<string, unknown>;
267
+ /** When set, the subagent should continue this thread instead of starting a new one */
268
+ threadId?: string;
253
269
  }
254
270
 
255
271
  // ============================================================================
@@ -0,0 +1,50 @@
1
+ import { Context } from "@temporalio/activity";
2
+ import type { WorkflowClient } from "@temporalio/client";
3
+ import type { ModelInvoker } from "./model-invoker";
4
+ import type { AgentResponse, BaseAgentState, RunAgentConfig } from "./types";
5
+ import { agentQueryName } from "./types";
6
+
7
+ /**
8
+ * Query the parent workflow's state from within an activity.
9
+ * Resolves the workflow handle from the current activity context.
10
+ */
11
+ export async function queryParentWorkflowState<T>(
12
+ client: WorkflowClient,
13
+ queryName: string
14
+ ): Promise<T> {
15
+ const { workflowExecution } = Context.current().info;
16
+ const handle = client.getHandle(
17
+ workflowExecution.workflowId,
18
+ workflowExecution.runId
19
+ );
20
+ return handle.query<T>(queryName);
21
+ }
22
+
23
+ /**
24
+ * Wraps a `ModelInvoker` into a `RunAgentActivity` by automatically
25
+ * loading tool definitions from the parent workflow state via query.
26
+ *
27
+ * This is the generic bridge between any provider-specific model invoker
28
+ * and the session's `runAgent` contract.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { createRunAgentActivity } from 'zeitlich';
33
+ * import { createLangChainModelInvoker } from 'zeitlich/adapters/langchain';
34
+ *
35
+ * const invoker = createLangChainModelInvoker({ redis, model });
36
+ * return { runAgent: createRunAgentActivity(client, invoker) };
37
+ * ```
38
+ */
39
+ export function createRunAgentActivity<M>(
40
+ client: WorkflowClient,
41
+ invoker: ModelInvoker<M>
42
+ ): (config: RunAgentConfig) => Promise<AgentResponse<M>> {
43
+ return async (config: RunAgentConfig) => {
44
+ const state = await queryParentWorkflowState<BaseAgentState>(
45
+ client,
46
+ agentQueryName(config.agentName)
47
+ );
48
+ return invoker({ ...config, state });
49
+ };
50
+ }
@@ -2,7 +2,31 @@ import type { ActivityToolHandler } from "../../lib/tool-router";
2
2
  import type { AskUserQuestionArgs } from "./tool";
3
3
 
4
4
  /**
5
- * Creates handler for user interaction tool - creates AI messages for display.
5
+ * Creates a handler for the AskUserQuestion tool.
6
+ * Returns question data for display to the user via your UI layer.
7
+ *
8
+ * Typically paired with `stateManager.waitForInput()` in a `hooks.onPostToolUse`
9
+ * callback to pause the agent loop until the user responds.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { createAskUserQuestionHandler } from 'zeitlich';
14
+ * import { askUserQuestionTool, defineTool } from 'zeitlich/workflow';
15
+ *
16
+ * // In activities
17
+ * const askUserQuestionHandlerActivity = createAskUserQuestionHandler();
18
+ *
19
+ * // In workflow
20
+ * tools: {
21
+ * AskUserQuestion: defineTool({
22
+ * ...askUserQuestionTool,
23
+ * handler: askUserQuestionHandlerActivity,
24
+ * hooks: {
25
+ * onPostToolUse: () => { stateManager.waitForInput(); },
26
+ * },
27
+ * }),
28
+ * }
29
+ * ```
6
30
  */
7
31
  export const createAskUserQuestionHandler =
8
32
  (): ActivityToolHandler<