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.
- package/README.md +313 -126
- package/dist/adapters/langchain/index.cjs +270 -0
- package/dist/adapters/langchain/index.cjs.map +1 -0
- package/dist/adapters/langchain/index.d.cts +132 -0
- package/dist/adapters/langchain/index.d.ts +132 -0
- package/dist/adapters/langchain/index.js +265 -0
- package/dist/adapters/langchain/index.js.map +1 -0
- package/dist/index.cjs +89 -209
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -46
- package/dist/index.d.ts +62 -46
- package/dist/index.js +88 -208
- package/dist/index.js.map +1 -1
- package/dist/{workflow-BhjsEQc1.d.cts → model-invoker-y_zlyMqu.d.cts} +45 -482
- package/dist/{workflow-BhjsEQc1.d.ts → model-invoker-y_zlyMqu.d.ts} +45 -482
- package/dist/thread-manager-qc0g5Rvd.d.cts +39 -0
- package/dist/thread-manager-qc0g5Rvd.d.ts +39 -0
- package/dist/workflow.cjs +59 -27
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +459 -6
- package/dist/workflow.d.ts +459 -6
- package/dist/workflow.js +60 -29
- package/dist/workflow.js.map +1 -1
- package/package.json +17 -2
- package/src/adapters/langchain/activities.ts +120 -0
- package/src/adapters/langchain/index.ts +38 -0
- package/src/adapters/langchain/model-invoker.ts +102 -0
- package/src/adapters/langchain/thread-manager.ts +142 -0
- package/src/index.ts +24 -23
- package/src/lib/fs.ts +25 -0
- package/src/lib/model-invoker.ts +15 -75
- package/src/lib/session.ts +52 -21
- package/src/lib/state-manager.ts +23 -5
- package/src/lib/thread-id.ts +25 -0
- package/src/lib/thread-manager.ts +18 -142
- package/src/lib/tool-router.ts +12 -18
- package/src/lib/types.ts +26 -10
- package/src/lib/workflow-helpers.ts +50 -0
- package/src/tools/ask-user-question/handler.ts +25 -1
- package/src/tools/bash/handler.ts +13 -0
- package/src/tools/subagent/handler.ts +16 -5
- package/src/tools/subagent/tool.ts +34 -15
- package/src/workflow.ts +26 -7
- package/tsup.config.ts +1 -0
- package/src/activities.ts +0 -91
- package/src/plugin.ts +0 -28
package/src/lib/session.ts
CHANGED
|
@@ -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 "
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
246
|
-
* Call this in workflow code
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/state-manager.ts
CHANGED
|
@@ -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
|
|
156
|
-
*
|
|
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
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/tool-router.ts
CHANGED
|
@@ -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
|
-
|
|
957
|
-
|
|
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
|
-
|
|
966
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
108
|
-
/** The thread ID to use for the session */
|
|
109
|
-
threadId
|
|
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 =
|
|
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
|
|
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<
|