zeitlich 0.1.1 → 0.2.0

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 (58) hide show
  1. package/README.md +165 -180
  2. package/dist/index.cjs +1314 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +128 -0
  5. package/dist/index.d.ts +51 -75
  6. package/dist/index.js +741 -1091
  7. package/dist/index.js.map +1 -1
  8. package/dist/workflow-uVNF7zoe.d.cts +941 -0
  9. package/dist/workflow-uVNF7zoe.d.ts +941 -0
  10. package/dist/workflow.cjs +914 -0
  11. package/dist/workflow.cjs.map +1 -0
  12. package/dist/workflow.d.cts +5 -0
  13. package/dist/workflow.d.ts +2 -1
  14. package/dist/workflow.js +543 -423
  15. package/dist/workflow.js.map +1 -1
  16. package/package.json +19 -17
  17. package/src/activities.ts +112 -0
  18. package/src/index.ts +49 -0
  19. package/src/lib/fs.ts +80 -0
  20. package/src/lib/model-invoker.ts +75 -0
  21. package/src/lib/session.ts +216 -0
  22. package/src/lib/state-manager.ts +268 -0
  23. package/src/lib/thread-manager.ts +169 -0
  24. package/src/lib/tool-router.ts +717 -0
  25. package/src/lib/types.ts +354 -0
  26. package/src/plugin.ts +28 -0
  27. package/src/tools/ask-user-question/handler.ts +25 -0
  28. package/src/tools/ask-user-question/tool.ts +46 -0
  29. package/src/tools/bash/bash.test.ts +104 -0
  30. package/src/tools/bash/handler.ts +36 -0
  31. package/src/tools/bash/tool.ts +20 -0
  32. package/src/tools/edit/handler.ts +156 -0
  33. package/src/tools/edit/tool.ts +39 -0
  34. package/src/tools/glob/handler.ts +62 -0
  35. package/src/tools/glob/tool.ts +27 -0
  36. package/src/tools/grep/tool.ts +45 -0
  37. package/src/tools/read/tool.ts +33 -0
  38. package/src/tools/task/handler.ts +75 -0
  39. package/src/tools/task/tool.ts +96 -0
  40. package/src/tools/task-create/handler.ts +49 -0
  41. package/src/tools/task-create/tool.ts +66 -0
  42. package/src/tools/task-get/handler.ts +38 -0
  43. package/src/tools/task-get/tool.ts +11 -0
  44. package/src/tools/task-list/handler.ts +33 -0
  45. package/src/tools/task-list/tool.ts +9 -0
  46. package/src/tools/task-update/handler.ts +79 -0
  47. package/src/tools/task-update/tool.ts +20 -0
  48. package/src/tools/write/tool.ts +26 -0
  49. package/src/workflow.ts +138 -0
  50. package/tsup.config.ts +20 -0
  51. package/dist/index.d.mts +0 -152
  52. package/dist/index.mjs +0 -1587
  53. package/dist/index.mjs.map +0 -1
  54. package/dist/workflow-7_MT-5-w.d.mts +0 -1203
  55. package/dist/workflow-7_MT-5-w.d.ts +0 -1203
  56. package/dist/workflow.d.mts +0 -4
  57. package/dist/workflow.mjs +0 -739
  58. package/dist/workflow.mjs.map +0 -1
@@ -0,0 +1,112 @@
1
+ import type Redis from "ioredis";
2
+ import { createThreadManager } from "./lib/thread-manager";
3
+ import type { ToolResultConfig } from "./lib/types";
4
+ import {
5
+ type AIMessage,
6
+ mapStoredMessageToChatMessage,
7
+ type MessageContent,
8
+ type StoredMessage,
9
+ } from "@langchain/core/messages";
10
+ import type { RawToolCall } from "./lib/tool-router";
11
+ /**
12
+ * Shared Zeitlich activities - thread management and message handling
13
+ * Note: runAgent is workflow-specific and should be created per-workflow
14
+ */
15
+ export interface ZeitlichSharedActivities {
16
+ appendSystemMessage(threadId: string, content: string): Promise<void>;
17
+ /**
18
+ * Append a tool result to the thread.
19
+ * Handles JSON serialization and optional cache points.
20
+ */
21
+ appendToolResult(config: ToolResultConfig): Promise<void>;
22
+
23
+ /**
24
+ * Initialize an empty thread.
25
+ */
26
+ initializeThread(threadId: string): Promise<void>;
27
+
28
+ /**
29
+ * Append a system message to a thread.
30
+ */
31
+ appendSystemMessage(threadId: string, content: string): Promise<void>;
32
+
33
+ /**
34
+ * Append messages to a thread.
35
+ */
36
+ appendThreadMessages(
37
+ threadId: string,
38
+ messages: StoredMessage[]
39
+ ): Promise<void>;
40
+
41
+ /**
42
+ * Append a human message to a thread.
43
+ */
44
+ appendHumanMessage(
45
+ threadId: string,
46
+ content: string | MessageContent
47
+ ): Promise<void>;
48
+
49
+ /**
50
+ * Extract raw tool calls from a stored message.
51
+ * Returns unvalidated tool calls - use toolRegistry.parseToolCall() to validate.
52
+ */
53
+ parseToolCalls(storedMessage: StoredMessage): Promise<RawToolCall[]>;
54
+ }
55
+
56
+ /**
57
+ * Creates shared Temporal activities for thread management
58
+ *
59
+ * @returns An object containing the shared activity functions
60
+ *
61
+ * @experimental The Zeitlich integration is an experimental feature; APIs may change without notice.
62
+ */
63
+ export function createSharedActivities(redis: Redis): ZeitlichSharedActivities {
64
+ return {
65
+ async appendSystemMessage(
66
+ threadId: string,
67
+ content: string
68
+ ): Promise<void> {
69
+ const thread = createThreadManager({ redis, threadId });
70
+ await thread.appendSystemMessage(content);
71
+ },
72
+
73
+ async appendToolResult(config: ToolResultConfig): Promise<void> {
74
+ const { threadId, toolCallId, content } = config;
75
+ const thread = createThreadManager({ redis, threadId });
76
+
77
+ await thread.appendToolMessage(content, toolCallId);
78
+ },
79
+
80
+ async initializeThread(threadId: string): Promise<void> {
81
+ const thread = createThreadManager({ redis, threadId });
82
+ await thread.initialize();
83
+ },
84
+
85
+ async appendThreadMessages(
86
+ threadId: string,
87
+ messages: StoredMessage[]
88
+ ): Promise<void> {
89
+ const thread = createThreadManager({ redis, threadId });
90
+ await thread.append(messages);
91
+ },
92
+
93
+ async appendHumanMessage(
94
+ threadId: string,
95
+ content: string | MessageContent
96
+ ): Promise<void> {
97
+ const thread = createThreadManager({ redis, threadId });
98
+ await thread.appendHumanMessage(content);
99
+ },
100
+
101
+ async parseToolCalls(storedMessage: StoredMessage): Promise<RawToolCall[]> {
102
+ const message = mapStoredMessageToChatMessage(storedMessage) as AIMessage;
103
+ const toolCalls = message.tool_calls ?? [];
104
+
105
+ return toolCalls.map((toolCall) => ({
106
+ id: toolCall.id,
107
+ name: toolCall.name,
108
+ args: toolCall.args,
109
+ }));
110
+ },
111
+ };
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Activity-side exports for use in Temporal activity code and worker setup.
3
+ *
4
+ * Import from '@bead-ai/zeitlich' in activity files and worker setup.
5
+ * These exports may have external dependencies (Redis, LangChain).
6
+ *
7
+ * For workflow code, use '@bead-ai/zeitlich/workflow' instead.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // In your activities file
12
+ * import { invokeModel, globHandler } from '@bead-ai/zeitlich';
13
+ *
14
+ * // In your worker file
15
+ * import { ZeitlichPlugin } from '@bead-ai/zeitlich';
16
+ * ```
17
+ */
18
+
19
+ // Re-export all workflow-safe exports for convenience
20
+ // (Activities can use these too)
21
+ export * from "./workflow";
22
+
23
+ // Plugin (requires Redis)
24
+ export { ZeitlichPlugin } from "./plugin";
25
+ export type { ZeitlichPluginOptions } from "./plugin";
26
+
27
+ // Shared activities (requires Redis)
28
+ export { createSharedActivities } from "./activities";
29
+ export type { ZeitlichSharedActivities } from "./activities";
30
+
31
+ // Model invocation (requires Redis, LangChain)
32
+ export { invokeModel } from "./lib/model-invoker";
33
+ export type { InvokeModelConfig } from "./lib/model-invoker";
34
+
35
+ // Tool handlers (activity implementations)
36
+ // These are direct functions that accept scopedNodes per-call for dynamic file trees
37
+ export { handleAskUserQuestionToolResult } from "./tools/ask-user-question/handler";
38
+ export { globHandler } from "./tools/glob/handler";
39
+
40
+ export { editHandler } from "./tools/edit/handler";
41
+ export type {
42
+ EditResult,
43
+ EditHandlerResponse,
44
+ EditHandlerOptions,
45
+ } from "./tools/edit/handler";
46
+
47
+ export { handleBashTool } from "./tools/bash/handler";
48
+
49
+ export { toTree } from "./lib/fs";
package/src/lib/fs.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { IFileSystem } from "just-bash";
2
+
3
+ const basename = (path: string, separator: string): string => {
4
+ if (path[path.length - 1] === separator) path = path.slice(0, -1);
5
+ const lastSlashIndex = path.lastIndexOf(separator);
6
+ return lastSlashIndex === -1 ? path : path.slice(lastSlashIndex + 1);
7
+ };
8
+
9
+ const printTree = async (
10
+ tab = "",
11
+ children: ((tab: string) => Promise<string | null>)[]
12
+ ): Promise<string> => {
13
+ let str = "";
14
+ let last = children.length - 1;
15
+ for (; last >= 0; last--) if (children[last]) break;
16
+ for (let i = 0; i <= last; i++) {
17
+ const fn = children[i];
18
+ if (!fn) continue;
19
+ const isLast = i === last;
20
+ const child = await fn(tab + (isLast ? " " : "│") + " ");
21
+ const branch = child ? (isLast ? "└─" : "├─") : "│";
22
+ str += "\n" + tab + branch + (child ? " " + child : "");
23
+ }
24
+ return str;
25
+ };
26
+
27
+ export const toTree = async (
28
+ fs: IFileSystem,
29
+ opts: {
30
+ dir?: string;
31
+ separator?: "/" | "\\";
32
+ depth?: number;
33
+ tab?: string;
34
+ sort?: boolean;
35
+ } = {}
36
+ ): Promise<string> => {
37
+ const separator = opts.separator || "/";
38
+ let dir = opts.dir || separator;
39
+ if (dir[dir.length - 1] !== separator) dir += separator;
40
+ const tab = opts.tab || "";
41
+ const depth = opts.depth ?? 10;
42
+ const sort = opts.sort ?? true;
43
+ let subtree = " (...)";
44
+ if (depth > 0) {
45
+ const list = (await fs.readdirWithFileTypes?.(dir)) || [];
46
+ if (sort) {
47
+ list.sort((a, b) => {
48
+ if (a.isDirectory && b.isDirectory) {
49
+ return a.name.toString().localeCompare(b.name.toString());
50
+ } else if (a.isDirectory) {
51
+ return -1;
52
+ } else if (b.isDirectory) {
53
+ return 1;
54
+ } else {
55
+ return a.name.toString().localeCompare(b.name.toString());
56
+ }
57
+ });
58
+ }
59
+ subtree = await printTree(
60
+ tab,
61
+ list.map((entry) => async (tab): Promise<string | null> => {
62
+ if (entry.isDirectory) {
63
+ return toTree(fs, {
64
+ dir: dir + entry.name,
65
+ depth: depth - 1,
66
+ tab,
67
+ });
68
+ } else if (entry.isSymbolicLink) {
69
+ return (
70
+ "" + entry.name + " → " + (await fs.readlink(dir + entry.name))
71
+ );
72
+ } else {
73
+ return "" + entry.name;
74
+ }
75
+ })
76
+ );
77
+ }
78
+ const base = basename(dir, separator) + separator;
79
+ return base + subtree;
80
+ };
@@ -0,0 +1,75 @@
1
+ import type Redis from "ioredis";
2
+ import { createThreadManager } from "./thread-manager";
3
+ import type { AgentResponse } from "./types";
4
+ import { Context } from "@temporalio/activity";
5
+ import type { WorkflowClient } from "@temporalio/client";
6
+ import { mapStoredMessagesToChatMessages } from "@langchain/core/messages";
7
+ import { v4 as uuidv4 } from "uuid";
8
+ import type {
9
+ BaseChatModel,
10
+ BaseChatModelCallOptions,
11
+ BindToolsInput,
12
+ } from "@langchain/core/language_models/chat_models";
13
+ import { getStateQuery } from "./state-manager";
14
+
15
+ /**
16
+ * Configuration for invoking the model
17
+ */
18
+ export interface InvokeModelConfig {
19
+ threadId: string;
20
+ agentName: string;
21
+ }
22
+
23
+ /**
24
+ * Core model invocation logic - shared utility for workflow-specific activities
25
+ *
26
+ * @param redis - Redis client for thread management
27
+ * @param config - Model invocation configuration
28
+ * @param model - Pre-instantiated LangChain chat model
29
+ * @param invocationConfig - Per-invocation configuration (system prompt, etc.)
30
+ * @returns Agent response with message and metadata
31
+ */
32
+ export async function invokeModel({
33
+ redis,
34
+ model,
35
+ client,
36
+ config: { threadId, agentName },
37
+ }: {
38
+ redis: Redis;
39
+ client: WorkflowClient;
40
+ config: InvokeModelConfig;
41
+ model: BaseChatModel<BaseChatModelCallOptions & { tools?: BindToolsInput }>;
42
+ }): Promise<AgentResponse> {
43
+ const thread = createThreadManager({ redis, threadId });
44
+ const runId = uuidv4();
45
+
46
+ const info = Context.current().info; // Activity info
47
+ const parentWorkflowId = info.workflowExecution.workflowId;
48
+ const parentRunId = info.workflowExecution.runId;
49
+
50
+ const handle = client.getHandle(parentWorkflowId, parentRunId);
51
+ const { tools } = await handle.query(getStateQuery);
52
+
53
+ const messages = await thread.load();
54
+ const response = await model.invoke(
55
+ [...mapStoredMessagesToChatMessages(messages)],
56
+ {
57
+ runName: agentName,
58
+ runId,
59
+ metadata: { thread_id: threadId },
60
+ tools,
61
+ }
62
+ );
63
+
64
+ await thread.append([response.toDict()]);
65
+
66
+ return {
67
+ message: response.toDict(),
68
+ stopReason: (response.response_metadata?.stop_reason as string) ?? null,
69
+ usage: {
70
+ input_tokens: response.usage_metadata?.input_tokens,
71
+ output_tokens: response.usage_metadata?.output_tokens,
72
+ total_tokens: response.usage_metadata?.total_tokens,
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,216 @@
1
+ import { proxyActivities } from "@temporalio/workflow";
2
+ import type { ZeitlichSharedActivities } from "../activities";
3
+ import type {
4
+ ZeitlichAgentConfig,
5
+ SessionStartHook,
6
+ SessionEndHook,
7
+ SessionExitReason,
8
+ SubagentConfig,
9
+ } from "./types";
10
+ import { type AgentStateManager, type JsonSerializable } from "./state-manager";
11
+ import {
12
+ createToolRouter,
13
+ type ParsedToolCall,
14
+ type ParsedToolCallUnion,
15
+ type RawToolCall,
16
+ type ToolMap,
17
+ } from "./tool-router";
18
+ import type { StoredMessage } from "@langchain/core/messages";
19
+ import { createTaskTool, type TaskToolSchemaType } from "../tools/task/tool";
20
+
21
+ export interface ZeitlichSession {
22
+ runSession<T extends JsonSerializable<T>>(args: {
23
+ stateManager: AgentStateManager<T>;
24
+ }): Promise<StoredMessage | null>;
25
+ }
26
+
27
+ async function resolvePrompt(
28
+ prompt: string | (() => string | Promise<string>)
29
+ ): Promise<string> {
30
+ if (typeof prompt === "function") {
31
+ return prompt();
32
+ }
33
+ return prompt;
34
+ }
35
+
36
+ /**
37
+ * Session-level hooks for lifecycle events
38
+ */
39
+ export interface SessionLifecycleHooks {
40
+ /** Called when session starts */
41
+ onSessionStart?: SessionStartHook;
42
+ /** Called when session ends */
43
+ onSessionEnd?: SessionEndHook;
44
+ }
45
+
46
+ export const createSession = async <T extends ToolMap>({
47
+ threadId,
48
+ agentName,
49
+ maxTurns = 50,
50
+ metadata = {},
51
+ runAgent,
52
+ baseSystemPrompt,
53
+ instructionsPrompt,
54
+ buildContextMessage,
55
+ buildFileTree = async (): Promise<string> => "",
56
+ subagents,
57
+ tools = {} as T,
58
+ processToolsInParallel = true,
59
+ buildInTools = {},
60
+ hooks = {},
61
+ }: ZeitlichAgentConfig<T>): Promise<ZeitlichSession> => {
62
+ const {
63
+ initializeThread,
64
+ appendHumanMessage,
65
+ parseToolCalls,
66
+ appendToolResult,
67
+ appendSystemMessage,
68
+ } = proxyActivities<ZeitlichSharedActivities>({
69
+ startToCloseTimeout: "30m",
70
+ retry: {
71
+ maximumAttempts: 6,
72
+ initialInterval: "5s",
73
+ maximumInterval: "15m",
74
+ backoffCoefficient: 4,
75
+ },
76
+ heartbeatTimeout: "5m",
77
+ });
78
+
79
+ const fileTree = await buildFileTree();
80
+
81
+ const toolRouter = createToolRouter({
82
+ tools,
83
+ appendToolResult,
84
+ threadId,
85
+ hooks,
86
+ buildInTools,
87
+ fileTree,
88
+ subagents,
89
+ parallel: processToolsInParallel,
90
+ });
91
+
92
+ // Helper to call session end hook
93
+ const callSessionEnd = async (
94
+ exitReason: SessionExitReason,
95
+ turns: number
96
+ ): Promise<void> => {
97
+ if (hooks.onSessionEnd) {
98
+ await hooks.onSessionEnd({
99
+ threadId,
100
+ agentName,
101
+ exitReason,
102
+ turns,
103
+ metadata,
104
+ });
105
+ }
106
+ };
107
+
108
+ return {
109
+ runSession: async ({ stateManager }): Promise<StoredMessage | null> => {
110
+ if (hooks.onSessionStart) {
111
+ await hooks.onSessionStart({
112
+ threadId,
113
+ agentName,
114
+ metadata,
115
+ });
116
+ }
117
+
118
+ stateManager.setTools(toolRouter.getToolDefinitions());
119
+
120
+ await initializeThread(threadId);
121
+ await appendSystemMessage(
122
+ threadId,
123
+ [
124
+ await resolvePrompt(baseSystemPrompt),
125
+ await resolvePrompt(instructionsPrompt),
126
+ ].join("\n")
127
+ );
128
+ await appendHumanMessage(threadId, await buildContextMessage());
129
+
130
+ let exitReason: SessionExitReason = "completed";
131
+
132
+ try {
133
+ while (
134
+ stateManager.isRunning() &&
135
+ !stateManager.isTerminal() &&
136
+ stateManager.getTurns() < maxTurns
137
+ ) {
138
+ stateManager.incrementTurns();
139
+ const currentTurn = stateManager.getTurns();
140
+
141
+ const { message, stopReason } = await runAgent({
142
+ threadId,
143
+ agentName,
144
+ metadata,
145
+ });
146
+
147
+ if (stopReason === "end_turn") {
148
+ stateManager.complete();
149
+ exitReason = "completed";
150
+ return message;
151
+ }
152
+
153
+ // No tools configured - treat any non-end_turn as completed
154
+ if (!toolRouter.hasTools()) {
155
+ stateManager.complete();
156
+ exitReason = "completed";
157
+ return message;
158
+ }
159
+
160
+ const rawToolCalls: RawToolCall[] = await parseToolCalls(message);
161
+ const parsedToolCalls = rawToolCalls
162
+ .filter((tc: RawToolCall) => tc.name !== "Task")
163
+ .map((tc: RawToolCall) => toolRouter.parseToolCall(tc));
164
+ const taskToolCalls =
165
+ subagents && subagents.length > 0
166
+ ? rawToolCalls
167
+ .filter((tc: RawToolCall) => tc.name === "Task")
168
+ .map((tc: RawToolCall) => {
169
+ // Parse and validate args using the tool's schema
170
+ const parsedArgs = createTaskTool(subagents).schema.parse(
171
+ tc.args
172
+ );
173
+
174
+ return {
175
+ id: tc.id ?? "",
176
+ name: tc.name,
177
+ args: parsedArgs,
178
+ } as ParsedToolCall<
179
+ "Task",
180
+ TaskToolSchemaType<SubagentConfig[]>
181
+ >;
182
+ })
183
+ : [];
184
+
185
+ // Hooks can call stateManager.waitForInput() to pause the session
186
+ await toolRouter.processToolCalls(
187
+ [...parsedToolCalls, ...taskToolCalls] as ParsedToolCallUnion<
188
+ T & { Task: TaskToolSchemaType<SubagentConfig[]> }
189
+ >[],
190
+ {
191
+ turn: currentTurn,
192
+ }
193
+ );
194
+
195
+ if (stateManager.getStatus() === "WAITING_FOR_INPUT") {
196
+ exitReason = "waiting_for_input";
197
+ break;
198
+ }
199
+ }
200
+
201
+ // Check if we hit max turns
202
+ if (stateManager.getTurns() >= maxTurns && stateManager.isRunning()) {
203
+ exitReason = "max_turns";
204
+ }
205
+ } catch (error) {
206
+ exitReason = "failed";
207
+ throw error;
208
+ } finally {
209
+ // SessionEnd hook - always called
210
+ await callSessionEnd(exitReason, stateManager.getTurns());
211
+ }
212
+
213
+ return null;
214
+ },
215
+ };
216
+ };