zeitlich 0.2.15 → 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 (64) hide show
  1. package/README.md +50 -0
  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.d.cts +2 -2
  21. package/dist/adapters/thread/google-genai/index.d.ts +2 -2
  22. package/dist/adapters/thread/langchain/index.d.cts +2 -2
  23. package/dist/adapters/thread/langchain/index.d.ts +2 -2
  24. package/dist/index.cjs +2 -3
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +5 -5
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +2 -3
  29. package/dist/index.js.map +1 -1
  30. package/dist/{types-CDubRtad.d.cts → types-BMRzfELQ.d.cts} +2 -0
  31. package/dist/{types-CDubRtad.d.ts → types-BMRzfELQ.d.ts} +2 -0
  32. package/dist/{types-CwwgQ_9H.d.ts → types-BSOte_8s.d.ts} +6 -2
  33. package/dist/{types-BVP87m_W.d.cts → types-DCi2qXjN.d.cts} +6 -2
  34. package/dist/{types-Dje1TdH6.d.cts → types-Drli9aCK.d.cts} +1 -1
  35. package/dist/{types-BWvIYK28.d.ts → types-XPtivmSJ.d.ts} +1 -1
  36. package/dist/workflow.cjs +2 -3
  37. package/dist/workflow.cjs.map +1 -1
  38. package/dist/workflow.d.cts +6 -6
  39. package/dist/workflow.d.ts +6 -6
  40. package/dist/workflow.js +2 -3
  41. package/dist/workflow.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/adapters/sandbox/daytona/filesystem.ts +43 -19
  44. package/src/adapters/sandbox/daytona/index.ts +16 -3
  45. package/src/adapters/sandbox/daytona/types.ts +4 -0
  46. package/src/adapters/sandbox/inmemory/index.ts +22 -16
  47. package/src/adapters/sandbox/virtual/filesystem.ts +29 -31
  48. package/src/adapters/sandbox/virtual/index.ts +5 -3
  49. package/src/adapters/sandbox/virtual/provider.ts +5 -2
  50. package/src/adapters/sandbox/virtual/types.ts +3 -0
  51. package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +4 -3
  52. package/src/lib/sandbox/tree.integration.test.ts +153 -0
  53. package/src/lib/sandbox/types.ts +2 -0
  54. package/src/lib/session/session-edge-cases.integration.test.ts +962 -0
  55. package/src/lib/session/session.integration.test.ts +852 -0
  56. package/src/lib/session/session.ts +5 -4
  57. package/src/lib/skills/skills.integration.test.ts +308 -0
  58. package/src/lib/state/manager.integration.test.ts +342 -0
  59. package/src/lib/subagent/subagent.integration.test.ts +467 -0
  60. package/src/lib/thread/id.test.ts +50 -0
  61. package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +344 -0
  62. package/src/lib/tool-router/router-edge-cases.integration.test.ts +623 -0
  63. package/src/lib/tool-router/router.integration.test.ts +699 -0
  64. package/src/lib/types.test.ts +29 -0
@@ -67,7 +67,9 @@ export const createSession = async <T extends ToolMap, M = unknown>({
67
67
  }: SessionConfig<T, M>): Promise<ZeitlichSession<M>> => {
68
68
  const sourceThreadId = continueThread ? providedThreadId : undefined;
69
69
  const threadId =
70
- continueThread && providedThreadId ? getShortId() : (providedThreadId ?? getShortId());
70
+ continueThread && providedThreadId
71
+ ? getShortId()
72
+ : (providedThreadId ?? getShortId());
71
73
 
72
74
  const {
73
75
  appendToolResult,
@@ -149,9 +151,7 @@ export const createSession = async <T extends ToolMap, M = unknown>({
149
151
  const result = await sandboxOps.createSandbox({ id: threadId });
150
152
  sandboxId = result.sandboxId;
151
153
  if (result.stateUpdate) {
152
- stateManager.mergeUpdate(
153
- result.stateUpdate as Partial<TState>,
154
- );
154
+ stateManager.mergeUpdate(result.stateUpdate as Partial<TState>);
155
155
  }
156
156
  }
157
157
 
@@ -253,6 +253,7 @@ export const createSession = async <T extends ToolMap, M = unknown>({
253
253
  );
254
254
  if (!conditionMet) {
255
255
  stateManager.cancel();
256
+ exitReason = "cancelled";
256
257
  await condition(() => false, "2s");
257
258
  break;
258
259
  }
@@ -0,0 +1,308 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseSkillFile } from "./parse";
3
+ import { createReadSkillTool } from "./tool";
4
+ import { createReadSkillHandler } from "./handler";
5
+ import { buildSkillRegistration } from "./register";
6
+ import type { Skill } from "./types";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // parseSkillFile
10
+ // ---------------------------------------------------------------------------
11
+
12
+ describe("parseSkillFile", () => {
13
+ it("parses a minimal SKILL.md with name and description", () => {
14
+ const raw = `---
15
+ name: my-skill
16
+ description: Does useful things
17
+ ---
18
+ # Instructions
19
+ Do the thing.`;
20
+
21
+ const { frontmatter, body } = parseSkillFile(raw);
22
+
23
+ expect(frontmatter.name).toBe("my-skill");
24
+ expect(frontmatter.description).toBe("Does useful things");
25
+ expect(body).toBe("# Instructions\nDo the thing.");
26
+ });
27
+
28
+ it("parses full frontmatter with all optional fields", () => {
29
+ const raw = `---
30
+ name: advanced-skill
31
+ description: A more complex skill
32
+ license: MIT
33
+ compatibility: linux-only
34
+ allowed-tools: bash grep read-file
35
+ metadata:
36
+ author: test-author
37
+ version: 1.0
38
+ ---
39
+ Body content here.`;
40
+
41
+ const { frontmatter, body } = parseSkillFile(raw);
42
+
43
+ expect(frontmatter.name).toBe("advanced-skill");
44
+ expect(frontmatter.description).toBe("A more complex skill");
45
+ expect(frontmatter.license).toBe("MIT");
46
+ expect(frontmatter.compatibility).toBe("linux-only");
47
+ expect(frontmatter.allowedTools).toEqual(["bash", "grep", "read-file"]);
48
+ expect(frontmatter.metadata).toEqual({ author: "test-author", version: "1.0" });
49
+ expect(body).toBe("Body content here.");
50
+ });
51
+
52
+ it("strips BOM from input", () => {
53
+ const raw = `\uFEFF---
54
+ name: bom-skill
55
+ description: Has BOM
56
+ ---
57
+ Content`;
58
+
59
+ const { frontmatter } = parseSkillFile(raw);
60
+ expect(frontmatter.name).toBe("bom-skill");
61
+ });
62
+
63
+ it("handles quoted values in frontmatter", () => {
64
+ const raw = `---
65
+ name: "quoted-skill"
66
+ description: 'single quoted description'
67
+ ---
68
+ Body`;
69
+
70
+ const { frontmatter } = parseSkillFile(raw);
71
+ expect(frontmatter.name).toBe("quoted-skill");
72
+ expect(frontmatter.description).toBe("single quoted description");
73
+ });
74
+
75
+ it("throws when frontmatter is missing", () => {
76
+ expect(() => parseSkillFile("No frontmatter here")).toThrow(
77
+ "SKILL.md must start with YAML frontmatter",
78
+ );
79
+ });
80
+
81
+ it("throws when name is missing", () => {
82
+ const raw = `---
83
+ description: Missing name
84
+ ---
85
+ Body`;
86
+
87
+ expect(() => parseSkillFile(raw)).toThrow(
88
+ "SKILL.md frontmatter must include a 'name' field",
89
+ );
90
+ });
91
+
92
+ it("throws when description is missing", () => {
93
+ const raw = `---
94
+ name: no-desc
95
+ ---
96
+ Body`;
97
+
98
+ expect(() => parseSkillFile(raw)).toThrow(
99
+ "SKILL.md frontmatter must include a 'description' field",
100
+ );
101
+ });
102
+
103
+ it("handles empty body", () => {
104
+ const raw = `---
105
+ name: empty-body
106
+ description: No body content
107
+ ---
108
+ `;
109
+
110
+ const { body } = parseSkillFile(raw);
111
+ expect(body).toBe("");
112
+ });
113
+
114
+ it("handles CRLF line endings", () => {
115
+ const raw = "---\r\nname: crlf-skill\r\ndescription: CRLF test\r\n---\r\nBody with CRLF";
116
+
117
+ const { frontmatter, body } = parseSkillFile(raw);
118
+ expect(frontmatter.name).toBe("crlf-skill");
119
+ expect(body).toBe("Body with CRLF");
120
+ });
121
+
122
+ it("ignores comment lines in frontmatter", () => {
123
+ const raw = `---
124
+ name: commented
125
+ # This is a comment
126
+ description: Has comments
127
+ ---
128
+ Body`;
129
+
130
+ const { frontmatter } = parseSkillFile(raw);
131
+ expect(frontmatter.name).toBe("commented");
132
+ expect(frontmatter.description).toBe("Has comments");
133
+ });
134
+
135
+ it("handles metadata with empty map (key with no value)", () => {
136
+ const raw = `---
137
+ name: meta-skill
138
+ description: Has metadata section
139
+ metadata:
140
+ key1: value1
141
+ key2: value2
142
+ ---
143
+ Body`;
144
+
145
+ const { frontmatter } = parseSkillFile(raw);
146
+ expect(frontmatter.metadata).toEqual({ key1: "value1", key2: "value2" });
147
+ });
148
+
149
+ it("trims body whitespace", () => {
150
+ const raw = `---
151
+ name: trimmed
152
+ description: Trims body
153
+ ---
154
+
155
+
156
+ Body with leading whitespace
157
+ And trailing
158
+ `;
159
+
160
+ const { body } = parseSkillFile(raw);
161
+ expect(body).toBe("Body with leading whitespace\n And trailing");
162
+ });
163
+ });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // createReadSkillTool
167
+ // ---------------------------------------------------------------------------
168
+
169
+ describe("createReadSkillTool", () => {
170
+ const skills: Skill[] = [
171
+ {
172
+ name: "skill-a",
173
+ description: "First skill",
174
+ instructions: "Do A",
175
+ },
176
+ {
177
+ name: "skill-b",
178
+ description: "Second skill",
179
+ instructions: "Do B",
180
+ },
181
+ ];
182
+
183
+ it("creates a tool with correct name and dynamic schema", () => {
184
+ const tool = createReadSkillTool(skills);
185
+
186
+ expect(tool.name).toBe("ReadSkill");
187
+ expect(tool.description).toContain("skill-a");
188
+ expect(tool.description).toContain("skill-b");
189
+ expect(tool.description).toContain("First skill");
190
+ expect(tool.description).toContain("Second skill");
191
+ });
192
+
193
+ it("schema validates skill_name enum", () => {
194
+ const tool = createReadSkillTool(skills);
195
+
196
+ const validResult = tool.schema.safeParse({ skill_name: "skill-a" });
197
+ expect(validResult.success).toBe(true);
198
+
199
+ const invalidResult = tool.schema.safeParse({ skill_name: "nonexistent" });
200
+ expect(invalidResult.success).toBe(false);
201
+ });
202
+
203
+ it("throws when no skills are provided", () => {
204
+ expect(() => createReadSkillTool([])).toThrow(
205
+ "createReadSkillTool requires at least one skill",
206
+ );
207
+ });
208
+ });
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // createReadSkillHandler
212
+ // ---------------------------------------------------------------------------
213
+
214
+ describe("createReadSkillHandler", () => {
215
+ const skills: Skill[] = [
216
+ {
217
+ name: "skill-a",
218
+ description: "First skill",
219
+ instructions: "Instructions for A",
220
+ },
221
+ {
222
+ name: "skill-b",
223
+ description: "Second skill",
224
+ instructions: "Instructions for B",
225
+ },
226
+ ];
227
+
228
+ it("returns skill instructions for a valid skill name", () => {
229
+ const handler = createReadSkillHandler(skills);
230
+ const result = handler({ skill_name: "skill-a" });
231
+
232
+ expect(result.toolResponse).toBe("Instructions for A");
233
+ expect(result.data).toBeNull();
234
+ });
235
+
236
+ it("returns error for unknown skill name", () => {
237
+ const handler = createReadSkillHandler(skills);
238
+ const result = handler({ skill_name: "nonexistent" });
239
+
240
+ expect(typeof result.toolResponse).toBe("string");
241
+ expect((result.toolResponse as string)).toContain("not found");
242
+ expect(result.data).toBeNull();
243
+ });
244
+
245
+ it("handles single skill", () => {
246
+ const firstSkill = skills[0];
247
+ if (!firstSkill) throw new Error("expected skill");
248
+ const handler = createReadSkillHandler([firstSkill]);
249
+ const result = handler({ skill_name: "skill-a" });
250
+ expect(result.toolResponse).toBe("Instructions for A");
251
+ });
252
+ });
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // buildSkillRegistration
256
+ // ---------------------------------------------------------------------------
257
+
258
+ describe("buildSkillRegistration", () => {
259
+ it("returns null for empty skills array", () => {
260
+ expect(buildSkillRegistration([])).toBeNull();
261
+ });
262
+
263
+ it("returns a complete tool entry with handler", () => {
264
+ const skills: Skill[] = [
265
+ {
266
+ name: "my-skill",
267
+ description: "My skill",
268
+ instructions: "Do things",
269
+ },
270
+ ];
271
+
272
+ const registration = buildSkillRegistration(skills);
273
+
274
+ expect(registration).not.toBeNull();
275
+ expect(registration).toBeDefined();
276
+ if (registration) {
277
+ expect(registration.name).toBe("ReadSkill");
278
+ expect(registration.handler).toBeDefined();
279
+ expect(typeof registration.handler).toBe("function");
280
+ }
281
+ });
282
+
283
+ it("registered handler works end-to-end", () => {
284
+ const skills: Skill[] = [
285
+ {
286
+ name: "test-skill",
287
+ description: "Test",
288
+ instructions: "Test instructions content",
289
+ },
290
+ ];
291
+
292
+ const registration = buildSkillRegistration(skills);
293
+ expect(registration).toBeDefined();
294
+ if (!registration) return;
295
+ const result = registration.handler(
296
+ { skill_name: "test-skill" },
297
+ { threadId: "t-1", toolCallId: "tc-1", toolName: "ReadSkill" },
298
+ );
299
+
300
+ if (result instanceof Promise) {
301
+ return result.then((r) => {
302
+ expect(r.toolResponse).toBe("Test instructions content");
303
+ });
304
+ }
305
+ expect(result.toolResponse).toBe("Test instructions content");
306
+ return;
307
+ });
308
+ });
@@ -0,0 +1,342 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+ import { z } from "zod";
3
+
4
+ let idCounter = 0;
5
+
6
+ vi.mock("@temporalio/workflow", () => {
7
+ return {
8
+ condition: async (fn: () => boolean) => fn(),
9
+ defineUpdate: (name: string) => ({ __type: "update", name }),
10
+ defineQuery: (name: string) => ({ __type: "query", name }),
11
+ setHandler: (_def: unknown, _handler: unknown) => {},
12
+ uuid4: () =>
13
+ `00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
14
+ };
15
+ });
16
+
17
+ import { createAgentStateManager } from "./manager";
18
+ import type { WorkflowTask } from "../types";
19
+
20
+ describe("createAgentStateManager integration", () => {
21
+ beforeEach(() => {
22
+ idCounter = 0;
23
+ });
24
+
25
+ // --- Default initial state ---
26
+
27
+ it("initializes with default values when no initialState given", () => {
28
+ const sm = createAgentStateManager({});
29
+
30
+ expect(sm.getStatus()).toBe("RUNNING");
31
+ expect(sm.getTurns()).toBe(0);
32
+ expect(sm.getVersion()).toBe(0);
33
+ expect(sm.isRunning()).toBe(true);
34
+ expect(sm.isTerminal()).toBe(false);
35
+ expect(sm.getSystemPrompt()).toBeUndefined();
36
+ expect(sm.getTasks()).toEqual([]);
37
+ expect(sm.getTotalUsage()).toEqual({
38
+ totalInputTokens: 0,
39
+ totalOutputTokens: 0,
40
+ totalCachedWriteTokens: 0,
41
+ totalCachedReadTokens: 0,
42
+ totalReasonTokens: 0,
43
+ turns: 0,
44
+ });
45
+ });
46
+
47
+ // --- Status transitions ---
48
+
49
+ it("transitions through all status states and increments version", () => {
50
+ const sm = createAgentStateManager({});
51
+
52
+ expect(sm.getStatus()).toBe("RUNNING");
53
+ expect(sm.getVersion()).toBe(0);
54
+
55
+ sm.waitForInput();
56
+ expect(sm.getStatus()).toBe("WAITING_FOR_INPUT");
57
+ expect(sm.getVersion()).toBe(1);
58
+ expect(sm.isRunning()).toBe(false);
59
+ expect(sm.isTerminal()).toBe(false);
60
+
61
+ sm.run();
62
+ expect(sm.getStatus()).toBe("RUNNING");
63
+ expect(sm.getVersion()).toBe(2);
64
+ expect(sm.isRunning()).toBe(true);
65
+
66
+ sm.complete();
67
+ expect(sm.getStatus()).toBe("COMPLETED");
68
+ expect(sm.isTerminal()).toBe(true);
69
+
70
+ sm.fail();
71
+ expect(sm.getStatus()).toBe("FAILED");
72
+ expect(sm.isTerminal()).toBe(true);
73
+
74
+ sm.cancel();
75
+ expect(sm.getStatus()).toBe("CANCELLED");
76
+ expect(sm.isTerminal()).toBe(true);
77
+ });
78
+
79
+ // --- Turns ---
80
+
81
+ it("increments turns independently of version", () => {
82
+ const sm = createAgentStateManager({});
83
+
84
+ sm.incrementTurns();
85
+ sm.incrementTurns();
86
+ sm.incrementTurns();
87
+ expect(sm.getTurns()).toBe(3);
88
+ expect(sm.getVersion()).toBe(0);
89
+ });
90
+
91
+ // --- Custom state ---
92
+
93
+ it("manages custom state via get/set with version increments", () => {
94
+ const sm = createAgentStateManager<{ score: number; label: string }>({
95
+ initialState: { systemPrompt: "test", score: 0, label: "init" },
96
+ });
97
+
98
+ expect(sm.get("score")).toBe(0);
99
+ expect(sm.get("label")).toBe("init");
100
+
101
+ sm.set("score", 42);
102
+ expect(sm.get("score")).toBe(42);
103
+ expect(sm.getVersion()).toBe(1);
104
+
105
+ sm.set("label", "updated");
106
+ expect(sm.get("label")).toBe("updated");
107
+ expect(sm.getVersion()).toBe(2);
108
+ });
109
+
110
+ it("mergeUpdate bulk-updates custom state", () => {
111
+ const sm = createAgentStateManager<{ a: string; b: number }>({
112
+ initialState: { systemPrompt: "test", a: "old", b: 0 },
113
+ });
114
+
115
+ sm.mergeUpdate({ a: "new", b: 99 });
116
+ expect(sm.get("a")).toBe("new");
117
+ expect(sm.get("b")).toBe(99);
118
+ expect(sm.getVersion()).toBe(1);
119
+ });
120
+
121
+ it("mergeUpdate with partial fields only updates provided keys", () => {
122
+ const sm = createAgentStateManager<{ x: string; y: string }>({
123
+ initialState: { systemPrompt: "test", x: "orig-x", y: "orig-y" },
124
+ });
125
+
126
+ sm.mergeUpdate({ x: "changed" });
127
+ expect(sm.get("x")).toBe("changed");
128
+ expect(sm.get("y")).toBe("orig-y");
129
+ });
130
+
131
+ // --- System prompt ---
132
+
133
+ it("manages system prompt lifecycle", () => {
134
+ const sm = createAgentStateManager({
135
+ initialState: { systemPrompt: "initial prompt" },
136
+ });
137
+
138
+ expect(sm.getSystemPrompt()).toBe("initial prompt");
139
+
140
+ sm.setSystemPrompt("updated prompt");
141
+ expect(sm.getSystemPrompt()).toBe("updated prompt");
142
+ });
143
+
144
+ // --- Token usage accumulation ---
145
+
146
+ it("accumulates token usage across multiple updates", () => {
147
+ const sm = createAgentStateManager({});
148
+
149
+ sm.updateUsage({ inputTokens: 100, outputTokens: 50 });
150
+ sm.updateUsage({ inputTokens: 200, outputTokens: 100, cachedWriteTokens: 30 });
151
+ sm.updateUsage({ cachedReadTokens: 20, reasonTokens: 10 });
152
+
153
+ expect(sm.getTotalUsage()).toEqual({
154
+ totalInputTokens: 300,
155
+ totalOutputTokens: 150,
156
+ totalCachedWriteTokens: 30,
157
+ totalCachedReadTokens: 20,
158
+ totalReasonTokens: 10,
159
+ turns: 0,
160
+ });
161
+ });
162
+
163
+ it("handles updateUsage with undefined fields gracefully", () => {
164
+ const sm = createAgentStateManager({});
165
+
166
+ sm.updateUsage({});
167
+ sm.updateUsage({ inputTokens: undefined, outputTokens: undefined });
168
+
169
+ expect(sm.getTotalUsage().totalInputTokens).toBe(0);
170
+ expect(sm.getTotalUsage().totalOutputTokens).toBe(0);
171
+ });
172
+
173
+ // --- Task management ---
174
+
175
+ it("CRUD operations on tasks", () => {
176
+ const sm = createAgentStateManager({});
177
+
178
+ const task: WorkflowTask = {
179
+ id: "task-1",
180
+ subject: "Test task",
181
+ description: "A test task",
182
+ activeForm: "Testing",
183
+ status: "pending",
184
+ metadata: {},
185
+ blockedBy: [],
186
+ blocks: [],
187
+ };
188
+
189
+ sm.setTask(task);
190
+ expect(sm.getTasks()).toHaveLength(1);
191
+ expect(sm.getTask("task-1")).toEqual(task);
192
+ expect(sm.getVersion()).toBe(1);
193
+
194
+ sm.setTask({ ...task, status: "in_progress" });
195
+ expect(sm.getTask("task-1")?.status).toBe("in_progress");
196
+ expect(sm.getVersion()).toBe(2);
197
+
198
+ expect(sm.deleteTask("task-1")).toBe(true);
199
+ expect(sm.getTasks()).toHaveLength(0);
200
+ expect(sm.getTask("task-1")).toBeUndefined();
201
+ expect(sm.getVersion()).toBe(3);
202
+ });
203
+
204
+ it("deleteTask returns false for nonexistent task and does not increment version", () => {
205
+ const sm = createAgentStateManager({});
206
+
207
+ expect(sm.deleteTask("nonexistent")).toBe(false);
208
+ expect(sm.getVersion()).toBe(0);
209
+ });
210
+
211
+ it("manages multiple tasks simultaneously", () => {
212
+ const sm = createAgentStateManager({});
213
+
214
+ const makeTask = (id: string, subject: string): WorkflowTask => ({
215
+ id,
216
+ subject,
217
+ description: "",
218
+ activeForm: "",
219
+ status: "pending",
220
+ metadata: {},
221
+ blockedBy: [],
222
+ blocks: [],
223
+ });
224
+
225
+ sm.setTask(makeTask("t1", "Task 1"));
226
+ sm.setTask(makeTask("t2", "Task 2"));
227
+ sm.setTask(makeTask("t3", "Task 3"));
228
+
229
+ expect(sm.getTasks()).toHaveLength(3);
230
+ sm.deleteTask("t2");
231
+ expect(sm.getTasks()).toHaveLength(2);
232
+ expect(sm.getTask("t2")).toBeUndefined();
233
+ expect(sm.getTask("t1")).toBeDefined();
234
+ expect(sm.getTask("t3")).toBeDefined();
235
+ });
236
+
237
+ // --- getCurrentState ---
238
+
239
+ it("getCurrentState returns a snapshot including custom state", () => {
240
+ const sm = createAgentStateManager<{ mood: string }>({
241
+ initialState: { systemPrompt: "test", mood: "happy", status: "RUNNING" },
242
+ });
243
+
244
+ sm.incrementTurns();
245
+ sm.incrementTurns();
246
+
247
+ const state = sm.getCurrentState();
248
+ expect(state.status).toBe("RUNNING");
249
+ expect(state.turns).toBe(2);
250
+ expect(state.mood).toBe("happy");
251
+ });
252
+
253
+ // --- shouldReturnFromWait ---
254
+
255
+ it("shouldReturnFromWait returns true when version advanced", () => {
256
+ const sm = createAgentStateManager({});
257
+
258
+ expect(sm.shouldReturnFromWait(0)).toBe(false);
259
+ sm.incrementVersion();
260
+ expect(sm.shouldReturnFromWait(0)).toBe(true);
261
+ expect(sm.shouldReturnFromWait(1)).toBe(false);
262
+ });
263
+
264
+ it("shouldReturnFromWait returns true in terminal state regardless of version", () => {
265
+ const sm = createAgentStateManager({});
266
+
267
+ sm.complete();
268
+ expect(sm.shouldReturnFromWait(999)).toBe(true);
269
+ });
270
+
271
+ // --- Initial state with pre-set tasks ---
272
+
273
+ it("initializes with provided tasks map", () => {
274
+ const tasks = new Map<string, WorkflowTask>([
275
+ [
276
+ "preloaded",
277
+ {
278
+ id: "preloaded",
279
+ subject: "Preloaded task",
280
+ description: "",
281
+ activeForm: "",
282
+ status: "completed",
283
+ metadata: {},
284
+ blockedBy: [],
285
+ blocks: [],
286
+ },
287
+ ],
288
+ ]);
289
+
290
+ const sm = createAgentStateManager({
291
+ initialState: { systemPrompt: "test", tasks },
292
+ });
293
+
294
+ expect(sm.getTasks()).toHaveLength(1);
295
+ expect(sm.getTask("preloaded")?.status).toBe("completed");
296
+ });
297
+
298
+ // --- setTools ---
299
+
300
+ it("setTools stores serializable tool definitions in state", () => {
301
+ const sm = createAgentStateManager({});
302
+
303
+ sm.setTools([
304
+ {
305
+ name: "TestTool",
306
+ description: "A test tool",
307
+ schema: z.object({ input: z.string() }),
308
+ },
309
+ ]);
310
+
311
+ const state = sm.getCurrentState();
312
+ expect(state.tools).toHaveLength(1);
313
+ const firstTool = state.tools[0];
314
+ if (!firstTool) throw new Error("expected tool");
315
+ expect(firstTool.name).toBe("TestTool");
316
+ expect(firstTool.description).toBe("A test tool");
317
+ expect(firstTool.schema).toBeDefined();
318
+ expect(typeof firstTool.schema).toBe("object");
319
+ });
320
+
321
+ // --- incrementVersion standalone ---
322
+
323
+ it("incrementVersion works independently", () => {
324
+ const sm = createAgentStateManager({});
325
+
326
+ sm.incrementVersion();
327
+ sm.incrementVersion();
328
+ sm.incrementVersion();
329
+ expect(sm.getVersion()).toBe(3);
330
+ expect(sm.getStatus()).toBe("RUNNING");
331
+ });
332
+
333
+ // --- Usage turns are linked to incrementTurns ---
334
+
335
+ it("getTotalUsage.turns reflects incrementTurns count", () => {
336
+ const sm = createAgentStateManager({});
337
+
338
+ sm.incrementTurns();
339
+ sm.incrementTurns();
340
+ expect(sm.getTotalUsage().turns).toBe(2);
341
+ });
342
+ });