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.
- package/README.md +50 -0
- package/dist/adapters/sandbox/daytona/index.cjs +52 -23
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +10 -2
- package/dist/adapters/sandbox/daytona/index.d.ts +10 -2
- package/dist/adapters/sandbox/daytona/index.js +52 -23
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs +21 -16
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.ts +1 -1
- package/dist/adapters/sandbox/inmemory/index.js +21 -16
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/virtual/index.cjs +38 -38
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
- package/dist/adapters/sandbox/virtual/index.d.cts +6 -6
- package/dist/adapters/sandbox/virtual/index.d.ts +6 -6
- package/dist/adapters/sandbox/virtual/index.js +37 -37
- package/dist/adapters/sandbox/virtual/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +2 -2
- package/dist/adapters/thread/google-genai/index.d.ts +2 -2
- package/dist/adapters/thread/langchain/index.d.cts +2 -2
- package/dist/adapters/thread/langchain/index.d.ts +2 -2
- package/dist/index.cjs +2 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/{types-CDubRtad.d.cts → types-BMRzfELQ.d.cts} +2 -0
- package/dist/{types-CDubRtad.d.ts → types-BMRzfELQ.d.ts} +2 -0
- package/dist/{types-CwwgQ_9H.d.ts → types-BSOte_8s.d.ts} +6 -2
- package/dist/{types-BVP87m_W.d.cts → types-DCi2qXjN.d.cts} +6 -2
- package/dist/{types-Dje1TdH6.d.cts → types-Drli9aCK.d.cts} +1 -1
- package/dist/{types-BWvIYK28.d.ts → types-XPtivmSJ.d.ts} +1 -1
- package/dist/workflow.cjs +2 -3
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +6 -6
- package/dist/workflow.d.ts +6 -6
- package/dist/workflow.js +2 -3
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/sandbox/daytona/filesystem.ts +43 -19
- package/src/adapters/sandbox/daytona/index.ts +16 -3
- package/src/adapters/sandbox/daytona/types.ts +4 -0
- package/src/adapters/sandbox/inmemory/index.ts +22 -16
- package/src/adapters/sandbox/virtual/filesystem.ts +29 -31
- package/src/adapters/sandbox/virtual/index.ts +5 -3
- package/src/adapters/sandbox/virtual/provider.ts +5 -2
- package/src/adapters/sandbox/virtual/types.ts +3 -0
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +4 -3
- package/src/lib/sandbox/tree.integration.test.ts +153 -0
- package/src/lib/sandbox/types.ts +2 -0
- package/src/lib/session/session-edge-cases.integration.test.ts +962 -0
- package/src/lib/session/session.integration.test.ts +852 -0
- package/src/lib/session/session.ts +5 -4
- package/src/lib/skills/skills.integration.test.ts +308 -0
- package/src/lib/state/manager.integration.test.ts +342 -0
- package/src/lib/subagent/subagent.integration.test.ts +467 -0
- package/src/lib/thread/id.test.ts +50 -0
- package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +344 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +623 -0
- package/src/lib/tool-router/router.integration.test.ts +699 -0
- 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
|
|
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
|
+
});
|