zeitlich 0.2.14 → 0.2.16

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 (80) hide show
  1. package/README.md +62 -12
  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 +38 -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 +37 -37
  19. package/dist/adapters/sandbox/virtual/index.js.map +1 -1
  20. package/dist/adapters/thread/google-genai/index.cjs +22 -0
  21. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  22. package/dist/adapters/thread/google-genai/index.d.cts +3 -3
  23. package/dist/adapters/thread/google-genai/index.d.ts +3 -3
  24. package/dist/adapters/thread/google-genai/index.js +22 -0
  25. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  26. package/dist/adapters/thread/langchain/index.cjs +22 -0
  27. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  28. package/dist/adapters/thread/langchain/index.d.cts +3 -3
  29. package/dist/adapters/thread/langchain/index.d.ts +3 -3
  30. package/dist/adapters/thread/langchain/index.js +22 -0
  31. package/dist/adapters/thread/langchain/index.js.map +1 -1
  32. package/dist/index.cjs +38 -11
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +6 -6
  35. package/dist/index.d.ts +6 -6
  36. package/dist/index.js +38 -11
  37. package/dist/index.js.map +1 -1
  38. package/dist/{types-B9ljZewB.d.cts → types-35POpVfa.d.cts} +6 -0
  39. package/dist/{types-B9ljZewB.d.ts → types-35POpVfa.d.ts} +6 -0
  40. package/dist/{types-CDubRtad.d.cts → types-BMRzfELQ.d.cts} +2 -0
  41. package/dist/{types-CDubRtad.d.ts → types-BMRzfELQ.d.ts} +2 -0
  42. package/dist/{types-CwwgQ_9H.d.ts → types-BSOte_8s.d.ts} +6 -2
  43. package/dist/{types-BVP87m_W.d.cts → types-DCi2qXjN.d.cts} +6 -2
  44. package/dist/{types-GpMU4b0w.d.cts → types-Drli9aCK.d.cts} +3 -1
  45. package/dist/{types-B4C9txdq.d.ts → types-XPtivmSJ.d.ts} +3 -1
  46. package/dist/workflow.cjs +23 -11
  47. package/dist/workflow.cjs.map +1 -1
  48. package/dist/workflow.d.cts +6 -6
  49. package/dist/workflow.d.ts +6 -6
  50. package/dist/workflow.js +23 -11
  51. package/dist/workflow.js.map +1 -1
  52. package/package.json +7 -3
  53. package/src/adapters/sandbox/daytona/filesystem.ts +43 -19
  54. package/src/adapters/sandbox/daytona/index.ts +16 -3
  55. package/src/adapters/sandbox/daytona/types.ts +4 -0
  56. package/src/adapters/sandbox/inmemory/index.ts +22 -16
  57. package/src/adapters/sandbox/virtual/filesystem.ts +29 -31
  58. package/src/adapters/sandbox/virtual/index.ts +5 -3
  59. package/src/adapters/sandbox/virtual/provider.ts +5 -2
  60. package/src/adapters/sandbox/virtual/types.ts +3 -0
  61. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +4 -3
  62. package/src/adapters/thread/google-genai/activities.ts +11 -0
  63. package/src/adapters/thread/langchain/activities.ts +11 -0
  64. package/src/lib/sandbox/tree.integration.test.ts +153 -0
  65. package/src/lib/sandbox/types.ts +2 -0
  66. package/src/lib/session/session-edge-cases.integration.test.ts +962 -0
  67. package/src/lib/session/session.integration.test.ts +852 -0
  68. package/src/lib/session/session.ts +11 -5
  69. package/src/lib/session/types.ts +2 -0
  70. package/src/lib/skills/skills.integration.test.ts +308 -0
  71. package/src/lib/state/manager.integration.test.ts +342 -0
  72. package/src/lib/subagent/register.ts +22 -7
  73. package/src/lib/subagent/subagent.integration.test.ts +467 -0
  74. package/src/lib/thread/id.test.ts +50 -0
  75. package/src/lib/thread/manager.ts +20 -1
  76. package/src/lib/thread/types.ts +6 -0
  77. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +344 -0
  78. package/src/lib/tool-router/router-edge-cases.integration.test.ts +623 -0
  79. package/src/lib/tool-router/router.integration.test.ts +699 -0
  80. package/src/lib/types.test.ts +29 -0
@@ -5,23 +5,29 @@ import type {
5
5
  ToolMap,
6
6
  } from "../tool-router/types";
7
7
  import type { SubagentConfig, SubagentHooks } from "./types";
8
- import { createSubagentTool, type SubagentArgs } from "./tool";
8
+ import type { z } from "zod";
9
+ import { createSubagentTool, SUBAGENT_TOOL_NAME, type SubagentArgs } from "./tool";
9
10
  import { createSubagentHandler } from "./handler";
10
11
 
11
12
  /**
12
13
  * Builds a fully wired tool entry for the Subagent tool,
13
14
  * including per-subagent hook delegation.
14
15
  *
15
- * Returns null if no enabled subagents are configured.
16
+ * Uses getters for `enabled`, `description`, and `schema` so that
17
+ * dynamic changes to SubagentConfig.enabled are re-evaluated each
18
+ * time getToolDefinitions() is called.
19
+ *
20
+ * Returns null if no subagents are configured.
16
21
  */
17
22
  export function buildSubagentRegistration(
18
23
  subagents: SubagentConfig[]
19
24
  ): ToolMap[string] | null {
20
- const enabled = subagents.filter((s) => s.enabled ?? true);
21
- if (enabled.length === 0) return null;
25
+ if (subagents.length === 0) return null;
26
+
27
+ const getEnabled = (): SubagentConfig[] => subagents.filter((s) => s.enabled ?? true);
22
28
 
23
29
  const subagentHooksMap = new Map<string, SubagentHooks>();
24
- for (const s of enabled) {
30
+ for (const s of subagents) {
25
31
  if (s.hooks) subagentHooksMap.set(s.agentName, s.hooks);
26
32
  }
27
33
 
@@ -29,8 +35,17 @@ export function buildSubagentRegistration(
29
35
  (args as SubagentArgs).subagent;
30
36
 
31
37
  return {
32
- ...createSubagentTool(enabled),
33
- handler: createSubagentHandler(enabled),
38
+ name: SUBAGENT_TOOL_NAME,
39
+ get enabled(): boolean {
40
+ return getEnabled().length > 0;
41
+ },
42
+ get description(): string {
43
+ return createSubagentTool(getEnabled()).description;
44
+ },
45
+ get schema(): z.ZodObject<z.ZodRawShape> {
46
+ return createSubagentTool(getEnabled()).schema;
47
+ },
48
+ handler: createSubagentHandler(subagents),
34
49
  ...(subagentHooksMap.size > 0 && {
35
50
  hooks: {
36
51
  onPreToolUse: async (ctx): Promise<PreToolUseHookResult> => {
@@ -0,0 +1,467 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { z } from "zod";
3
+
4
+ vi.mock("@temporalio/workflow", () => {
5
+ let counter = 0;
6
+ return {
7
+ workflowInfo: () => ({ taskQueue: "default-queue" }),
8
+ executeChild: vi.fn(async (_workflow: unknown, opts: { args: unknown[] }) => {
9
+ const input = (opts.args as [{ prompt: string }])[0];
10
+ return {
11
+ toolResponse: `Response to: ${input.prompt}`,
12
+ data: { result: "child-data" },
13
+ threadId: "child-thread-1",
14
+ usage: { inputTokens: 100, outputTokens: 50 },
15
+ };
16
+ }),
17
+ uuid4: () => {
18
+ counter++;
19
+ const bytes = Array.from({ length: 16 }, (_, i) =>
20
+ ((counter * 31 + i * 7) & 0xff).toString(16).padStart(2, "0"),
21
+ ).join("");
22
+ return `${bytes.slice(0, 8)}-${bytes.slice(8, 12)}-${bytes.slice(12, 16)}-${bytes.slice(16, 20)}-${bytes.slice(20, 32)}`;
23
+ },
24
+ };
25
+ });
26
+
27
+ import { createSubagentTool, SUBAGENT_TOOL_NAME } from "./tool";
28
+ import { createSubagentHandler } from "./handler";
29
+ import { buildSubagentRegistration } from "./register";
30
+ import type { SubagentConfig } from "./types";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // createSubagentTool
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("createSubagentTool", () => {
37
+ it("creates tool with correct name and schema for single subagent", () => {
38
+ const tool = createSubagentTool([
39
+ {
40
+ agentName: "researcher",
41
+ description: "Researches topics",
42
+ workflow: "researcherWorkflow",
43
+ },
44
+ ]);
45
+
46
+ expect(tool.name).toBe(SUBAGENT_TOOL_NAME);
47
+ expect(tool.description).toContain("researcher");
48
+ expect(tool.description).toContain("Researches topics");
49
+
50
+ const valid = tool.schema.safeParse({
51
+ subagent: "researcher",
52
+ description: "Research something",
53
+ prompt: "Find info about X",
54
+ });
55
+ expect(valid.success).toBe(true);
56
+ });
57
+
58
+ it("creates enum schema for multiple subagents", () => {
59
+ const tool = createSubagentTool([
60
+ {
61
+ agentName: "researcher",
62
+ description: "Researches",
63
+ workflow: "researcherWorkflow",
64
+ },
65
+ {
66
+ agentName: "writer",
67
+ description: "Writes",
68
+ workflow: "writerWorkflow",
69
+ },
70
+ ]);
71
+
72
+ const validResearcher = tool.schema.safeParse({
73
+ subagent: "researcher",
74
+ description: "desc",
75
+ prompt: "prompt",
76
+ });
77
+ expect(validResearcher.success).toBe(true);
78
+
79
+ const validWriter = tool.schema.safeParse({
80
+ subagent: "writer",
81
+ description: "desc",
82
+ prompt: "prompt",
83
+ });
84
+ expect(validWriter.success).toBe(true);
85
+
86
+ const invalidAgent = tool.schema.safeParse({
87
+ subagent: "nonexistent",
88
+ description: "desc",
89
+ prompt: "prompt",
90
+ });
91
+ expect(invalidAgent.success).toBe(false);
92
+ });
93
+
94
+ it("adds threadId field when allowThreadContinuation is set", () => {
95
+ const tool = createSubagentTool([
96
+ {
97
+ agentName: "agent",
98
+ description: "supports continuation",
99
+ workflow: "workflow",
100
+ allowThreadContinuation: true,
101
+ },
102
+ ]);
103
+
104
+ const withThread = tool.schema.safeParse({
105
+ subagent: "agent",
106
+ description: "desc",
107
+ prompt: "prompt",
108
+ threadId: "some-thread",
109
+ });
110
+ expect(withThread.success).toBe(true);
111
+
112
+ const withNull = tool.schema.safeParse({
113
+ subagent: "agent",
114
+ description: "desc",
115
+ prompt: "prompt",
116
+ threadId: null,
117
+ });
118
+ expect(withNull.success).toBe(true);
119
+ });
120
+
121
+ it("does not include threadId field when no subagent has allowThreadContinuation", () => {
122
+ const tool = createSubagentTool([
123
+ {
124
+ agentName: "basic",
125
+ description: "basic agent",
126
+ workflow: "workflow",
127
+ },
128
+ ]);
129
+
130
+ const result = tool.schema.safeParse({
131
+ subagent: "basic",
132
+ description: "desc",
133
+ prompt: "prompt",
134
+ threadId: "should-strip",
135
+ });
136
+ expect(result.success).toBe(true);
137
+ if (result.success) {
138
+ expect(result.data).not.toHaveProperty("threadId");
139
+ }
140
+ });
141
+
142
+ it("throws when no subagents are provided", () => {
143
+ expect(() => createSubagentTool([])).toThrow(
144
+ "createSubagentTool requires at least one subagent",
145
+ );
146
+ });
147
+
148
+ it("includes thread continuation note in description", () => {
149
+ const tool = createSubagentTool([
150
+ {
151
+ agentName: "cont-agent",
152
+ description: "Supports continuation",
153
+ workflow: "workflow",
154
+ allowThreadContinuation: true,
155
+ },
156
+ ]);
157
+
158
+ expect(tool.description).toContain("thread continuation");
159
+ });
160
+ });
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // createSubagentHandler
164
+ // ---------------------------------------------------------------------------
165
+
166
+ describe("createSubagentHandler", () => {
167
+ const basicSubagent: SubagentConfig = {
168
+ agentName: "researcher",
169
+ description: "Researches topics",
170
+ workflow: "researcherWorkflow",
171
+ };
172
+
173
+ it("executes child workflow and returns response", async () => {
174
+ const handler = createSubagentHandler([basicSubagent]);
175
+
176
+ const result = await handler(
177
+ { subagent: "researcher", description: "test", prompt: "Find info" },
178
+ { threadId: "parent-thread", toolCallId: "tc-1", toolName: "Subagent" },
179
+ );
180
+
181
+ expect(result.toolResponse).toContain("Response to: Find info");
182
+ expect(result.data).toEqual({ result: "child-data" });
183
+ });
184
+
185
+ it("throws for unknown subagent name", async () => {
186
+ const handler = createSubagentHandler([basicSubagent]);
187
+
188
+ await expect(
189
+ handler(
190
+ { subagent: "nonexistent", description: "test", prompt: "test" },
191
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" },
192
+ ),
193
+ ).rejects.toThrow("Unknown subagent: nonexistent");
194
+ });
195
+
196
+ it("includes available subagent names in error message", async () => {
197
+ const handler = createSubagentHandler([
198
+ basicSubagent,
199
+ {
200
+ agentName: "writer",
201
+ description: "Writes",
202
+ workflow: "writerWorkflow",
203
+ },
204
+ ]);
205
+
206
+ await expect(
207
+ handler(
208
+ { subagent: "bad", description: "test", prompt: "test" },
209
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" },
210
+ ),
211
+ ).rejects.toThrow(/researcher.*writer/);
212
+ });
213
+
214
+ it("validates result against resultSchema", async () => {
215
+ const { executeChild } = await import("@temporalio/workflow");
216
+ (executeChild as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
217
+ toolResponse: "result",
218
+ data: { invalid: "data" },
219
+ threadId: "child-t",
220
+ });
221
+
222
+ const validatedSubagent: SubagentConfig = {
223
+ agentName: "validated",
224
+ description: "Has validation",
225
+ workflow: "workflow",
226
+ resultSchema: z.object({ expected: z.string() }),
227
+ };
228
+
229
+ const handler = createSubagentHandler([validatedSubagent]);
230
+
231
+ const result = await handler(
232
+ { subagent: "validated", description: "test", prompt: "test" },
233
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" },
234
+ );
235
+
236
+ expect(result.toolResponse).toContain("invalid data");
237
+ expect(result.data).toBeNull();
238
+ });
239
+
240
+ it("appends thread ID when allowThreadContinuation is set", async () => {
241
+ const { executeChild } = await import("@temporalio/workflow");
242
+ (executeChild as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
243
+ toolResponse: "Some response",
244
+ data: null,
245
+ threadId: "child-thread-99",
246
+ });
247
+
248
+ const contSubagent: SubagentConfig = {
249
+ agentName: "cont",
250
+ description: "Continues threads",
251
+ workflow: "workflow",
252
+ allowThreadContinuation: true,
253
+ };
254
+
255
+ const handler = createSubagentHandler([contSubagent]);
256
+
257
+ const result = await handler(
258
+ { subagent: "cont", description: "test", prompt: "test" },
259
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" },
260
+ );
261
+
262
+ expect(result.toolResponse).toContain("Thread ID: child-thread-99");
263
+ });
264
+
265
+ it("returns fallback when child workflow returns no toolResponse", async () => {
266
+ const { executeChild } = await import("@temporalio/workflow");
267
+ (executeChild as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
268
+ toolResponse: null,
269
+ data: null,
270
+ threadId: "child-t",
271
+ });
272
+
273
+ const handler = createSubagentHandler([basicSubagent]);
274
+
275
+ const result = await handler(
276
+ { subagent: "researcher", description: "test", prompt: "test" },
277
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent" },
278
+ );
279
+
280
+ expect(result.toolResponse).toContain("no response");
281
+ expect(result.data).toBeNull();
282
+ });
283
+
284
+ it("passes sandboxId to child when sandbox is inherit", async () => {
285
+ const { executeChild } = await import("@temporalio/workflow");
286
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
287
+ execMock.mockResolvedValueOnce({
288
+ toolResponse: "ok",
289
+ data: null,
290
+ threadId: "child-t",
291
+ });
292
+
293
+ const inheritSubagent: SubagentConfig = {
294
+ agentName: "inherit-agent",
295
+ description: "Inherits sandbox",
296
+ workflow: "workflow",
297
+ sandbox: "inherit",
298
+ };
299
+
300
+ const handler = createSubagentHandler([inheritSubagent]);
301
+
302
+ await handler(
303
+ { subagent: "inherit-agent", description: "test", prompt: "test" },
304
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent", sandboxId: "parent-sb" },
305
+ );
306
+
307
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
308
+ if (!lastCall) throw new Error("expected exec call");
309
+ const input = lastCall[1].args[0];
310
+ expect(input.sandboxId).toBe("parent-sb");
311
+ });
312
+
313
+ it("does not pass sandboxId when sandbox is own", async () => {
314
+ const { executeChild } = await import("@temporalio/workflow");
315
+ const execMock = executeChild as ReturnType<typeof vi.fn>;
316
+ execMock.mockResolvedValueOnce({
317
+ toolResponse: "ok",
318
+ data: null,
319
+ threadId: "child-t",
320
+ });
321
+
322
+ const ownSubagent: SubagentConfig = {
323
+ agentName: "own-agent",
324
+ description: "Own sandbox",
325
+ workflow: "workflow",
326
+ sandbox: "own",
327
+ };
328
+
329
+ const handler = createSubagentHandler([ownSubagent]);
330
+
331
+ await handler(
332
+ { subagent: "own-agent", description: "test", prompt: "test" },
333
+ { threadId: "t", toolCallId: "tc", toolName: "Subagent", sandboxId: "parent-sb" },
334
+ );
335
+
336
+ const lastCall = execMock.mock.calls[execMock.mock.calls.length - 1];
337
+ if (!lastCall) throw new Error("expected exec call");
338
+ const input = lastCall[1].args[0];
339
+ expect(input.sandboxId).toBeUndefined();
340
+ });
341
+ });
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // buildSubagentRegistration
345
+ // ---------------------------------------------------------------------------
346
+
347
+ describe("buildSubagentRegistration", () => {
348
+ it("returns null for empty array", () => {
349
+ expect(buildSubagentRegistration([])).toBeNull();
350
+ });
351
+
352
+ it("creates registration with correct tool name", () => {
353
+ const reg = buildSubagentRegistration([
354
+ {
355
+ agentName: "agent",
356
+ description: "An agent",
357
+ workflow: "workflow",
358
+ },
359
+ ]);
360
+
361
+ expect(reg).not.toBeNull();
362
+ expect(reg).toBeDefined();
363
+ if (reg) {
364
+ expect(reg.name).toBe(SUBAGENT_TOOL_NAME);
365
+ expect(typeof reg.handler).toBe("function");
366
+ }
367
+ });
368
+
369
+ it("enabled getter reflects dynamic subagent state", () => {
370
+ const config: SubagentConfig = {
371
+ agentName: "toggle",
372
+ description: "Toggleable",
373
+ workflow: "workflow",
374
+ enabled: true,
375
+ };
376
+
377
+ const reg = buildSubagentRegistration([config]);
378
+ expect(reg).toBeDefined();
379
+ if (!reg) return;
380
+ expect(reg.enabled).toBe(true);
381
+
382
+ config.enabled = false;
383
+ expect(reg.enabled).toBe(false);
384
+ });
385
+
386
+ it("disabled when all subagents are disabled", () => {
387
+ const reg = buildSubagentRegistration([
388
+ {
389
+ agentName: "off",
390
+ description: "Disabled",
391
+ workflow: "workflow",
392
+ enabled: false,
393
+ },
394
+ ]);
395
+
396
+ expect(reg).toBeDefined();
397
+ if (reg) {
398
+ expect(reg.enabled).toBe(false);
399
+ }
400
+ });
401
+
402
+ it("includes hooks when subagents have hooks configured", () => {
403
+ const hookSpy = vi.fn(async () => ({}));
404
+
405
+ const reg = buildSubagentRegistration([
406
+ {
407
+ agentName: "hooked",
408
+ description: "Has hooks",
409
+ workflow: "workflow",
410
+ hooks: {
411
+ onPreExecution: hookSpy,
412
+ },
413
+ },
414
+ ]);
415
+
416
+ expect(reg).toBeDefined();
417
+ if (reg) {
418
+ expect(reg.hooks).toBeDefined();
419
+ if (reg.hooks) {
420
+ expect(reg.hooks.onPreToolUse).toBeDefined();
421
+ }
422
+ }
423
+ });
424
+
425
+ it("does not include hooks when no subagents have hooks", () => {
426
+ const reg = buildSubagentRegistration([
427
+ {
428
+ agentName: "plain",
429
+ description: "No hooks",
430
+ workflow: "workflow",
431
+ },
432
+ ]);
433
+
434
+ expect(reg).toBeDefined();
435
+ if (reg) {
436
+ expect(reg.hooks).toBeUndefined();
437
+ }
438
+ });
439
+
440
+ it("dynamic schema/description updates when subagents change enabled state", () => {
441
+ const config1: SubagentConfig = {
442
+ agentName: "a",
443
+ description: "Agent A",
444
+ workflow: "workflow",
445
+ enabled: true,
446
+ };
447
+ const config2: SubagentConfig = {
448
+ agentName: "b",
449
+ description: "Agent B",
450
+ workflow: "workflow",
451
+ enabled: true,
452
+ };
453
+
454
+ const reg = buildSubagentRegistration([config1, config2]);
455
+
456
+ expect(reg).toBeDefined();
457
+ if (reg) {
458
+ expect(reg.description).toContain("Agent A");
459
+ expect(reg.description).toContain("Agent B");
460
+
461
+ config2.enabled = false;
462
+
463
+ expect(reg.description).toContain("Agent A");
464
+ expect(reg.description).not.toContain("Agent B");
465
+ }
466
+ });
467
+ });
@@ -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
+ });
@@ -1,4 +1,7 @@
1
- import type { ThreadManagerConfig, BaseThreadManager } from "./types";
1
+ import type {
2
+ ThreadManagerConfig,
3
+ BaseThreadManager,
4
+ } from "./types";
2
5
 
3
6
  const THREAD_TTL_SECONDS = 60 * 60 * 24 * 90; // 90 days
4
7
 
@@ -85,6 +88,22 @@ export function createThreadManager<T>(
85
88
  }
86
89
  },
87
90
 
91
+ async fork(newThreadId: string): Promise<BaseThreadManager<T>> {
92
+ await assertThreadExists();
93
+ const data = await redis.lrange(redisKey, 0, -1);
94
+ const forked = createThreadManager({
95
+ ...config,
96
+ threadId: newThreadId,
97
+ });
98
+ await forked.initialize();
99
+ if (data.length > 0) {
100
+ const newKey = getThreadKey(newThreadId, key);
101
+ await redis.rpush(newKey, ...data);
102
+ await redis.expire(newKey, THREAD_TTL_SECONDS);
103
+ }
104
+ return forked;
105
+ },
106
+
88
107
  async delete(): Promise<void> {
89
108
  await redis.del(redisKey, metaKey);
90
109
  },
@@ -28,6 +28,12 @@ export interface BaseThreadManager<T> {
28
28
  * same message ids are atomically skipped via a Redis Lua script.
29
29
  */
30
30
  append(messages: T[]): Promise<void>;
31
+ /**
32
+ * Copy all messages from this thread into a new thread, leaving the
33
+ * original intact. Returns the new thread's manager. Safe for parallel
34
+ * forks — each call creates an independent copy.
35
+ */
36
+ fork(newThreadId: string): Promise<BaseThreadManager<T>>;
31
37
  /** Delete the thread */
32
38
  delete(): Promise<void>;
33
39
  }