zeitlich 0.2.15 → 0.2.17

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 (78) hide show
  1. package/README.md +125 -64
  2. package/dist/adapters/sandbox/daytona/index.cjs +52 -23
  3. package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
  4. package/dist/adapters/sandbox/daytona/index.d.cts +10 -2
  5. package/dist/adapters/sandbox/daytona/index.d.ts +10 -2
  6. package/dist/adapters/sandbox/daytona/index.js +52 -23
  7. package/dist/adapters/sandbox/daytona/index.js.map +1 -1
  8. package/dist/adapters/sandbox/inmemory/index.cjs +21 -16
  9. package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
  10. package/dist/adapters/sandbox/inmemory/index.d.cts +1 -1
  11. package/dist/adapters/sandbox/inmemory/index.d.ts +1 -1
  12. package/dist/adapters/sandbox/inmemory/index.js +21 -16
  13. package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
  14. package/dist/adapters/sandbox/virtual/index.cjs +83 -38
  15. package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
  16. package/dist/adapters/sandbox/virtual/index.d.cts +6 -6
  17. package/dist/adapters/sandbox/virtual/index.d.ts +6 -6
  18. package/dist/adapters/sandbox/virtual/index.js +80 -38
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  20. package/dist/adapters/thread/google-genai/index.d.cts +2 -2
  21. package/dist/adapters/thread/google-genai/index.d.ts +2 -2
  22. package/dist/adapters/thread/langchain/index.cjs +2 -2
  23. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  24. package/dist/adapters/thread/langchain/index.d.cts +2 -2
  25. package/dist/adapters/thread/langchain/index.d.ts +2 -2
  26. package/dist/adapters/thread/langchain/index.js +2 -2
  27. package/dist/adapters/thread/langchain/index.js.map +1 -1
  28. package/dist/index.cjs +102 -10
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +6 -6
  31. package/dist/index.d.ts +6 -6
  32. package/dist/index.js +98 -11
  33. package/dist/index.js.map +1 -1
  34. package/dist/{types-CwwgQ_9H.d.ts → queries-BlC1I3DK.d.ts} +48 -3
  35. package/dist/{types-BVP87m_W.d.cts → queries-DlJ3jE48.d.cts} +48 -3
  36. package/dist/{types-CDubRtad.d.cts → types-BMRzfELQ.d.cts} +2 -0
  37. package/dist/{types-CDubRtad.d.ts → types-BMRzfELQ.d.ts} +2 -0
  38. package/dist/{types-Dje1TdH6.d.cts → types-Bh-BbfCp.d.cts} +31 -12
  39. package/dist/{types-BWvIYK28.d.ts → types-NkiAxU4t.d.ts} +31 -12
  40. package/dist/workflow.cjs +102 -10
  41. package/dist/workflow.cjs.map +1 -1
  42. package/dist/workflow.d.cts +114 -40
  43. package/dist/workflow.d.ts +114 -40
  44. package/dist/workflow.js +98 -11
  45. package/dist/workflow.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/adapters/sandbox/daytona/filesystem.ts +43 -19
  48. package/src/adapters/sandbox/daytona/index.ts +16 -3
  49. package/src/adapters/sandbox/daytona/types.ts +4 -0
  50. package/src/adapters/sandbox/inmemory/index.ts +22 -16
  51. package/src/adapters/sandbox/virtual/filesystem.ts +29 -31
  52. package/src/adapters/sandbox/virtual/index.ts +7 -3
  53. package/src/adapters/sandbox/virtual/provider.ts +5 -2
  54. package/src/adapters/sandbox/virtual/queries.ts +97 -0
  55. package/src/adapters/sandbox/virtual/types.ts +3 -0
  56. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +4 -3
  57. package/src/adapters/thread/langchain/activities.ts +7 -5
  58. package/src/lib/sandbox/tree.integration.test.ts +153 -0
  59. package/src/lib/sandbox/types.ts +2 -0
  60. package/src/lib/session/session-edge-cases.integration.test.ts +962 -0
  61. package/src/lib/session/session.integration.test.ts +853 -0
  62. package/src/lib/session/session.ts +5 -4
  63. package/src/lib/skills/skills.integration.test.ts +308 -0
  64. package/src/lib/state/manager.integration.test.ts +342 -0
  65. package/src/lib/subagent/define.ts +34 -47
  66. package/src/lib/subagent/handler.ts +9 -6
  67. package/src/lib/subagent/index.ts +4 -1
  68. package/src/lib/subagent/subagent.integration.test.ts +573 -0
  69. package/src/lib/subagent/types.ts +40 -10
  70. package/src/lib/subagent/workflow.ts +114 -0
  71. package/src/lib/thread/id.test.ts +50 -0
  72. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +344 -0
  73. package/src/lib/tool-router/router-edge-cases.integration.test.ts +623 -0
  74. package/src/lib/tool-router/router.integration.test.ts +699 -0
  75. package/src/lib/types.test.ts +29 -0
  76. package/src/lib/workflow.test.ts +131 -0
  77. package/src/lib/workflow.ts +45 -0
  78. package/src/workflow.ts +12 -2
@@ -0,0 +1,114 @@
1
+ import type { z } from "zod";
2
+ import type {
3
+ SubagentDefinition,
4
+ SubagentHandlerResponse,
5
+ SubagentWorkflowInput,
6
+ SubagentSessionInput,
7
+ } from "./types";
8
+
9
+ /**
10
+ * Defines a subagent workflow with embedded metadata (name, description, resultSchema).
11
+ * The returned value can be passed directly to `defineSubagent` — no need to repeat
12
+ * the name, description, or resultSchema in the parent workflow.
13
+ *
14
+ * Internally maps `SubagentWorkflowInput` fields to session-compatible `SubagentSessionInput`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import {
19
+ * defineSubagentWorkflow,
20
+ * defineSubagent,
21
+ * createSession,
22
+ * createAgentStateManager,
23
+ * } from 'zeitlich/workflow';
24
+ *
25
+ * // Define once — carries name, description, resultSchema
26
+ * export const researcherWorkflow = defineSubagentWorkflow(
27
+ * {
28
+ * name: "researcher",
29
+ * description: "Researches topics on the web",
30
+ * resultSchema: z.object({ findings: z.string() }),
31
+ * },
32
+ * async (prompt, sessionInput) => {
33
+ * const stateManager = createAgentStateManager({
34
+ * initialState: { systemPrompt: "You are a researcher." },
35
+ * });
36
+ *
37
+ * const session = await createSession({
38
+ * ...sessionInput,
39
+ * agentName: "researcher",
40
+ * runAgent: runAgentActivity,
41
+ * buildContextMessage: () => [{ type: "text", text: prompt }],
42
+ * });
43
+ *
44
+ * const { finalMessage, threadId } = await session.runSession({ stateManager });
45
+ * return { toolResponse: finalMessage ?? "No response", data: null, threadId };
46
+ * },
47
+ * );
48
+ *
49
+ * // Use in parent — only configure what's parent-specific
50
+ * export const researcher = defineSubagent(researcherWorkflow, {
51
+ * hooks: { onPostExecution: ({ result }) => console.log(result) },
52
+ * });
53
+ * ```
54
+ */
55
+ // Without resultSchema — data is null
56
+ export function defineSubagentWorkflow<
57
+ TContext extends Record<string, unknown> = Record<string, unknown>,
58
+ >(
59
+ config: { name: string; description: string },
60
+ fn: (
61
+ prompt: string,
62
+ sessionInput: SubagentSessionInput,
63
+ context: TContext
64
+ ) => Promise<SubagentHandlerResponse<null>>
65
+ ): SubagentDefinition<z.ZodNull, TContext>;
66
+ // With resultSchema — data is inferred from the schema
67
+ export function defineSubagentWorkflow<
68
+ TResult extends z.ZodType,
69
+ TContext extends Record<string, unknown> = Record<string, unknown>,
70
+ >(
71
+ config: { name: string; description: string; resultSchema: TResult },
72
+ fn: (
73
+ prompt: string,
74
+ sessionInput: SubagentSessionInput,
75
+ context: TContext
76
+ ) => Promise<SubagentHandlerResponse<z.infer<TResult> | null>>
77
+ ): SubagentDefinition<TResult, TContext>;
78
+ export function defineSubagentWorkflow(
79
+ config: { name: string; description: string; resultSchema?: z.ZodType },
80
+ fn: (
81
+ prompt: string,
82
+ sessionInput: SubagentSessionInput,
83
+ context: Record<string, unknown>
84
+ ) => Promise<SubagentHandlerResponse<unknown>>
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ ): SubagentDefinition<any, any> {
87
+ const workflow = async (
88
+ prompt: string,
89
+ workflowInput: SubagentWorkflowInput,
90
+ context?: Record<string, unknown>
91
+ ): Promise<SubagentHandlerResponse<unknown>> => {
92
+ const sessionInput: SubagentSessionInput = {
93
+ agentName: config.name,
94
+ ...(workflowInput.previousThreadId && {
95
+ threadId: workflowInput.previousThreadId,
96
+ continueThread: true,
97
+ }),
98
+ ...(workflowInput.sandboxId && { sandboxId: workflowInput.sandboxId }),
99
+ };
100
+ return fn(prompt, sessionInput, context ?? {});
101
+ };
102
+
103
+ // for temporal workflow name
104
+ Object.defineProperty(workflow, "name", { value: config.name });
105
+
106
+ return Object.assign(workflow, {
107
+ agentName: config.name,
108
+ description: config.description,
109
+ ...(config.resultSchema !== undefined && {
110
+ resultSchema: config.resultSchema,
111
+ }),
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ }) as SubagentDefinition<any, any>;
114
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+
3
+ let uuidCounter = 0;
4
+
5
+ vi.mock("@temporalio/workflow", () => ({
6
+ uuid4: () => {
7
+ uuidCounter++;
8
+ const bytes = Array.from({ length: 16 }, (_, i) =>
9
+ ((uuidCounter * 31 + i * 7 + uuidCounter * i) & 0xff)
10
+ .toString(16)
11
+ .padStart(2, "0"),
12
+ ).join("");
13
+ return `${bytes.slice(0, 8)}-${bytes.slice(8, 12)}-${bytes.slice(12, 16)}-${bytes.slice(16, 20)}-${bytes.slice(20, 32)}`;
14
+ },
15
+ }));
16
+
17
+ import { getShortId } from "./id";
18
+
19
+ describe("getShortId", () => {
20
+ beforeEach(() => {
21
+ uuidCounter = 0;
22
+ });
23
+
24
+ it("returns a string of default length 12", () => {
25
+ const id = getShortId();
26
+ expect(id).toHaveLength(12);
27
+ });
28
+
29
+ it("returns a string of custom length", () => {
30
+ const id = getShortId(6);
31
+ expect(id).toHaveLength(6);
32
+ });
33
+
34
+ it("contains only base-62 characters", () => {
35
+ const base62Regex = /^[A-Za-z0-9]+$/;
36
+ for (let i = 0; i < 10; i++) {
37
+ expect(getShortId()).toMatch(base62Regex);
38
+ }
39
+ });
40
+
41
+ it("generates unique IDs on successive calls", () => {
42
+ const ids = new Set(Array.from({ length: 20 }, () => getShortId()));
43
+ expect(ids.size).toBe(20);
44
+ });
45
+
46
+ it("returns empty string for length 0", () => {
47
+ const id = getShortId(0);
48
+ expect(id).toBe("");
49
+ });
50
+ });
@@ -0,0 +1,344 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { withAutoAppend } from "./auto-append";
3
+ import { withSandbox } from "./with-sandbox";
4
+ import type { RouterContext, ToolHandlerResponse } from "./types";
5
+ import type { ToolResultConfig } from "../types";
6
+ import type { Sandbox } from "../sandbox/types";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // withAutoAppend
10
+ // ---------------------------------------------------------------------------
11
+
12
+ describe("withAutoAppend", () => {
13
+ it("appends tool result via threadHandler and sets resultAppended", async () => {
14
+ const appended: ToolResultConfig[] = [];
15
+ const threadHandler = async (config: ToolResultConfig) => {
16
+ appended.push(config);
17
+ };
18
+
19
+ const innerHandler = async (
20
+ args: { text: string },
21
+ _ctx: RouterContext,
22
+ ): Promise<ToolHandlerResponse<{ echoed: string }>> => ({
23
+ toolResponse: `Echo: ${args.text}`,
24
+ data: { echoed: args.text },
25
+ });
26
+
27
+ const wrapped = withAutoAppend(threadHandler, innerHandler);
28
+
29
+ const result = await wrapped(
30
+ { text: "hello" },
31
+ {
32
+ threadId: "thread-1",
33
+ toolCallId: "tc-1",
34
+ toolName: "Echo",
35
+ },
36
+ );
37
+
38
+ expect(result.resultAppended).toBe(true);
39
+ expect(result.toolResponse).toBe("Response appended via withAutoAppend");
40
+ expect(result.data).toEqual({ echoed: "hello" });
41
+
42
+ expect(appended).toHaveLength(1);
43
+ const firstAppended = appended.at(0);
44
+ if (!firstAppended) throw new Error("expected appended item");
45
+ expect(firstAppended.threadId).toBe("thread-1");
46
+ expect(firstAppended.toolCallId).toBe("tc-1");
47
+ expect(firstAppended.toolName).toBe("Echo");
48
+ expect(firstAppended.content).toBe("Echo: hello");
49
+ });
50
+
51
+ it("preserves original data but replaces toolResponse", async () => {
52
+ const threadHandler = async () => {};
53
+
54
+ const innerHandler = async (): Promise<
55
+ ToolHandlerResponse<{ large: string }>
56
+ > => ({
57
+ toolResponse: "A".repeat(10000),
58
+ data: { large: "summary" },
59
+ });
60
+
61
+ const wrapped = withAutoAppend(threadHandler, innerHandler);
62
+
63
+ const result = await wrapped(
64
+ {},
65
+ { threadId: "t", toolCallId: "tc", toolName: "BigTool" },
66
+ );
67
+
68
+ expect(result.toolResponse).toBe("Response appended via withAutoAppend");
69
+ expect(result.data).toEqual({ large: "summary" });
70
+ expect(result.resultAppended).toBe(true);
71
+ });
72
+
73
+ it("propagates handler errors without appending", async () => {
74
+ const appendSpy = vi.fn();
75
+ const threadHandler = appendSpy;
76
+
77
+ const innerHandler = async (): Promise<ToolHandlerResponse<null>> => {
78
+ throw new Error("handler failed");
79
+ };
80
+
81
+ const wrapped = withAutoAppend(threadHandler, innerHandler);
82
+
83
+ await expect(
84
+ wrapped(
85
+ {},
86
+ { threadId: "t", toolCallId: "tc", toolName: "Fail" },
87
+ ),
88
+ ).rejects.toThrow("handler failed");
89
+
90
+ expect(appendSpy).not.toHaveBeenCalled();
91
+ });
92
+
93
+ it("uses correct context fields for thread handler config", async () => {
94
+ let capturedConfig: ToolResultConfig | null = null;
95
+ const threadHandler = async (config: ToolResultConfig) => {
96
+ capturedConfig = config;
97
+ };
98
+
99
+ const innerHandler = async (): Promise<ToolHandlerResponse<null>> => ({
100
+ toolResponse: "result content",
101
+ data: null,
102
+ });
103
+
104
+ const wrapped = withAutoAppend(threadHandler, innerHandler);
105
+
106
+ await wrapped(
107
+ {},
108
+ {
109
+ threadId: "my-thread",
110
+ toolCallId: "my-tc",
111
+ toolName: "MyTool",
112
+ sandboxId: "sb-1",
113
+ },
114
+ );
115
+
116
+ expect(capturedConfig).toEqual({
117
+ threadId: "my-thread",
118
+ toolCallId: "my-tc",
119
+ toolName: "MyTool",
120
+ content: "result content",
121
+ });
122
+ });
123
+ });
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // withSandbox
127
+ // ---------------------------------------------------------------------------
128
+
129
+ describe("withSandbox", () => {
130
+ function createMockSandbox(): Sandbox {
131
+ return {
132
+ id: "mock-sandbox",
133
+ capabilities: {
134
+ filesystem: true,
135
+ execution: true,
136
+ persistence: false,
137
+ },
138
+ fs: {
139
+ workspaceBase: "/",
140
+ exists: async () => false,
141
+ stat: async () => ({
142
+ isFile: false,
143
+ isDirectory: false,
144
+ isSymbolicLink: false,
145
+ size: 0,
146
+ mtime: new Date(),
147
+ }),
148
+ readdir: async () => [],
149
+ readdirWithFileTypes: async () => [],
150
+ readFile: async () => "",
151
+ readFileBuffer: async () => new Uint8Array(),
152
+ writeFile: async () => {},
153
+ appendFile: async () => {},
154
+ mkdir: async () => {},
155
+ rm: async () => {},
156
+ cp: async () => {},
157
+ mv: async () => {},
158
+ readlink: async () => "",
159
+ resolvePath: (base: string, path: string) => base + "/" + path,
160
+ },
161
+ exec: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
162
+ destroy: async () => {},
163
+ };
164
+ }
165
+
166
+ it("resolves sandbox and passes it to handler", async () => {
167
+ const mockSandbox = createMockSandbox();
168
+ const manager = {
169
+ getSandbox: async (id: string) => {
170
+ expect(id).toBe("sb-42");
171
+ return mockSandbox;
172
+ },
173
+ };
174
+
175
+ let capturedSandbox: Sandbox | null = null;
176
+ let capturedSandboxId: string | null = null;
177
+
178
+ const handler = async (
179
+ _args: { text: string },
180
+ ctx: RouterContext & { sandbox: Sandbox; sandboxId: string },
181
+ ): Promise<ToolHandlerResponse<null>> => {
182
+ capturedSandbox = ctx.sandbox;
183
+ capturedSandboxId = ctx.sandboxId;
184
+ return { toolResponse: "ok", data: null };
185
+ };
186
+
187
+ const wrapped = withSandbox(manager, handler);
188
+
189
+ const result = await wrapped(
190
+ { text: "hello" },
191
+ {
192
+ threadId: "thread-1",
193
+ toolCallId: "tc-1",
194
+ toolName: "Test",
195
+ sandboxId: "sb-42",
196
+ },
197
+ );
198
+
199
+ expect(result.toolResponse).toBe("ok");
200
+ expect(capturedSandbox).toBe(mockSandbox);
201
+ expect(capturedSandboxId).toBe("sb-42");
202
+ });
203
+
204
+ it("returns error when no sandboxId is present", async () => {
205
+ const manager = {
206
+ getSandbox: vi.fn(),
207
+ };
208
+
209
+ const handler = async (): Promise<ToolHandlerResponse<string>> => ({
210
+ toolResponse: "should not run",
211
+ data: "nope",
212
+ });
213
+
214
+ const wrapped = withSandbox(manager, handler);
215
+
216
+ const result = await wrapped(
217
+ {},
218
+ {
219
+ threadId: "thread-1",
220
+ toolCallId: "tc-1",
221
+ toolName: "Bash",
222
+ },
223
+ );
224
+
225
+ expect(result.toolResponse).toContain("No sandbox configured");
226
+ expect(result.toolResponse).toContain("Bash");
227
+ expect(result.data).toBeNull();
228
+ expect(manager.getSandbox).not.toHaveBeenCalled();
229
+ });
230
+
231
+ it("returns error when sandboxId is undefined explicitly", async () => {
232
+ const manager = {
233
+ getSandbox: vi.fn(),
234
+ };
235
+
236
+ const handler = async (): Promise<ToolHandlerResponse<null>> => ({
237
+ toolResponse: "nope",
238
+ data: null,
239
+ });
240
+
241
+ const wrapped = withSandbox(manager, handler);
242
+
243
+ const result = await wrapped(
244
+ {},
245
+ {
246
+ threadId: "t",
247
+ toolCallId: "tc",
248
+ toolName: "Grep",
249
+ sandboxId: undefined,
250
+ },
251
+ );
252
+
253
+ expect(result.toolResponse).toContain("No sandbox configured");
254
+ expect(result.data).toBeNull();
255
+ });
256
+
257
+ it("propagates getSandbox errors", async () => {
258
+ const manager = {
259
+ getSandbox: async () => {
260
+ throw new Error("sandbox not found");
261
+ },
262
+ };
263
+
264
+ const handler = async (): Promise<ToolHandlerResponse<null>> => ({
265
+ toolResponse: "ok",
266
+ data: null,
267
+ });
268
+
269
+ const wrapped = withSandbox(manager, handler);
270
+
271
+ await expect(
272
+ wrapped(
273
+ {},
274
+ {
275
+ threadId: "t",
276
+ toolCallId: "tc",
277
+ toolName: "Test",
278
+ sandboxId: "sb-missing",
279
+ },
280
+ ),
281
+ ).rejects.toThrow("sandbox not found");
282
+ });
283
+
284
+ it("passes all RouterContext fields through to inner handler", async () => {
285
+ const mockSandbox = createMockSandbox();
286
+ const manager = { getSandbox: async () => mockSandbox };
287
+
288
+ let capturedCtx: (RouterContext & { sandbox: Sandbox; sandboxId: string }) | null = null;
289
+
290
+ const handler = async (
291
+ _args: unknown,
292
+ ctx: RouterContext & { sandbox: Sandbox; sandboxId: string },
293
+ ): Promise<ToolHandlerResponse<null>> => {
294
+ capturedCtx = ctx;
295
+ return { toolResponse: "ok", data: null };
296
+ };
297
+
298
+ const wrapped = withSandbox(manager, handler);
299
+
300
+ await wrapped(
301
+ {},
302
+ {
303
+ threadId: "my-thread",
304
+ toolCallId: "my-tc",
305
+ toolName: "MyTool",
306
+ sandboxId: "my-sandbox",
307
+ },
308
+ );
309
+
310
+ expect(capturedCtx).toEqual(
311
+ expect.objectContaining({
312
+ threadId: "my-thread",
313
+ toolCallId: "my-tc",
314
+ toolName: "MyTool",
315
+ sandboxId: "my-sandbox",
316
+ sandbox: mockSandbox,
317
+ }),
318
+ );
319
+ });
320
+
321
+ it("handles sandboxId with empty string as falsy", async () => {
322
+ const manager = { getSandbox: vi.fn() };
323
+
324
+ const handler = async (): Promise<ToolHandlerResponse<null>> => ({
325
+ toolResponse: "nope",
326
+ data: null,
327
+ });
328
+
329
+ const wrapped = withSandbox(manager, handler);
330
+
331
+ const result = await wrapped(
332
+ {},
333
+ {
334
+ threadId: "t",
335
+ toolCallId: "tc",
336
+ toolName: "Test",
337
+ sandboxId: "",
338
+ },
339
+ );
340
+
341
+ expect(result.toolResponse).toContain("No sandbox configured");
342
+ expect(manager.getSandbox).not.toHaveBeenCalled();
343
+ });
344
+ });