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
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolResultConfig, TokenUsage } from "../types";
|
|
4
|
+
import type { ThreadOps } from "./types";
|
|
5
|
+
import type { RunAgentActivity } from "../model/types";
|
|
6
|
+
import type { RawToolCall } from "../tool-router/types";
|
|
7
|
+
import type { SandboxOps } from "../sandbox/types";
|
|
8
|
+
|
|
9
|
+
let idCounter = 0;
|
|
10
|
+
|
|
11
|
+
vi.mock("@temporalio/workflow", () => {
|
|
12
|
+
class MockApplicationFailure extends Error {
|
|
13
|
+
nonRetryable?: boolean;
|
|
14
|
+
static create({
|
|
15
|
+
message,
|
|
16
|
+
nonRetryable,
|
|
17
|
+
}: {
|
|
18
|
+
message: string;
|
|
19
|
+
nonRetryable?: boolean;
|
|
20
|
+
}) {
|
|
21
|
+
const err = new MockApplicationFailure(message);
|
|
22
|
+
err.nonRetryable = nonRetryable;
|
|
23
|
+
return err;
|
|
24
|
+
}
|
|
25
|
+
static fromError(error: unknown) {
|
|
26
|
+
const src = error instanceof Error ? error : new Error(String(error));
|
|
27
|
+
return new MockApplicationFailure(src.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
proxyActivities: <T>() => ({}) as T,
|
|
33
|
+
condition: async (fn: () => boolean) => fn(),
|
|
34
|
+
defineUpdate: (name: string) => ({ __type: "update", name }),
|
|
35
|
+
defineQuery: (name: string) => ({ __type: "query", name }),
|
|
36
|
+
setHandler: (_def: unknown, _handler: unknown) => {},
|
|
37
|
+
uuid4: () =>
|
|
38
|
+
`00000000-0000-0000-0000-${String(++idCounter).padStart(12, "0")}`,
|
|
39
|
+
ApplicationFailure: MockApplicationFailure,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
import { createSession } from "./session";
|
|
44
|
+
import { createAgentStateManager } from "../state/manager";
|
|
45
|
+
import { defineTool } from "../tool-router/router";
|
|
46
|
+
import type { ToolHandlerResponse, RouterContext } from "../tool-router/types";
|
|
47
|
+
|
|
48
|
+
type TurnScript = {
|
|
49
|
+
message: unknown;
|
|
50
|
+
toolCalls: RawToolCall[];
|
|
51
|
+
usage?: TokenUsage;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function createMockThreadOps() {
|
|
55
|
+
const log: { op: string; args: unknown[] }[] = [];
|
|
56
|
+
const ops: ThreadOps = {
|
|
57
|
+
initializeThread: async (threadId) => {
|
|
58
|
+
log.push({ op: "initializeThread", args: [threadId] });
|
|
59
|
+
},
|
|
60
|
+
appendHumanMessage: async (threadId, content) => {
|
|
61
|
+
log.push({ op: "appendHumanMessage", args: [threadId, content] });
|
|
62
|
+
},
|
|
63
|
+
appendToolResult: async (config) => {
|
|
64
|
+
log.push({ op: "appendToolResult", args: [config] });
|
|
65
|
+
},
|
|
66
|
+
appendSystemMessage: async (threadId, content) => {
|
|
67
|
+
log.push({ op: "appendSystemMessage", args: [threadId, content] });
|
|
68
|
+
},
|
|
69
|
+
forkThread: async (source, target) => {
|
|
70
|
+
log.push({ op: "forkThread", args: [source, target] });
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return { ops, log };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createScriptedRunAgent(turns: TurnScript[]): RunAgentActivity<unknown> {
|
|
77
|
+
let call = 0;
|
|
78
|
+
return async () => {
|
|
79
|
+
const turn = turns[call++];
|
|
80
|
+
if (!turn) {
|
|
81
|
+
return { message: "done", rawToolCalls: [], usage: undefined };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
message: turn.message,
|
|
85
|
+
rawToolCalls: turn.toolCalls,
|
|
86
|
+
usage: turn.usage,
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createEchoTool() {
|
|
92
|
+
return defineTool({
|
|
93
|
+
name: "Echo" as const,
|
|
94
|
+
description: "echoes input",
|
|
95
|
+
schema: z.object({ text: z.string() }),
|
|
96
|
+
handler: async (
|
|
97
|
+
args: { text: string },
|
|
98
|
+
_ctx: RouterContext,
|
|
99
|
+
): Promise<ToolHandlerResponse<{ echoed: string }>> => ({
|
|
100
|
+
toolResponse: `Echo: ${args.text}`,
|
|
101
|
+
data: { echoed: args.text },
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("createSession edge cases", () => {
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
idCounter = 0;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// --- WAITING_FOR_INPUT flow (condition returns false = timeout) ---
|
|
112
|
+
|
|
113
|
+
it("cancels session when WAITING_FOR_INPUT times out (condition returns false)", async () => {
|
|
114
|
+
const { ops } = createMockThreadOps();
|
|
115
|
+
let endReason: string | undefined;
|
|
116
|
+
const capturedRef: {
|
|
117
|
+
stateManager: ReturnType<typeof createAgentStateManager> | undefined;
|
|
118
|
+
} = { stateManager: undefined };
|
|
119
|
+
|
|
120
|
+
const waitTool = defineTool({
|
|
121
|
+
name: "AskUser" as const,
|
|
122
|
+
description: "asks user",
|
|
123
|
+
schema: z.object({}),
|
|
124
|
+
handler: async (_args: Record<string, never>, _ctx: RouterContext) => {
|
|
125
|
+
capturedRef.stateManager?.waitForInput();
|
|
126
|
+
return {
|
|
127
|
+
toolResponse: "Please provide input.",
|
|
128
|
+
data: null,
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const session = await createSession({
|
|
134
|
+
agentName: "TestAgent",
|
|
135
|
+
threadId: "thread-1",
|
|
136
|
+
runAgent: createScriptedRunAgent([
|
|
137
|
+
{
|
|
138
|
+
message: "Need user input",
|
|
139
|
+
toolCalls: [{ id: "tc-1", name: "AskUser", args: {} }],
|
|
140
|
+
},
|
|
141
|
+
]),
|
|
142
|
+
threadOps: ops,
|
|
143
|
+
tools: { AskUser: waitTool },
|
|
144
|
+
buildContextMessage: () => "go",
|
|
145
|
+
hooks: {
|
|
146
|
+
onSessionEnd: async ({ exitReason }) => {
|
|
147
|
+
endReason = exitReason;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const stateManager = createAgentStateManager({
|
|
153
|
+
initialState: { systemPrompt: "test" },
|
|
154
|
+
});
|
|
155
|
+
capturedRef.stateManager = stateManager;
|
|
156
|
+
|
|
157
|
+
const result = await session.runSession({ stateManager });
|
|
158
|
+
|
|
159
|
+
expect(result.exitReason).toBe("cancelled");
|
|
160
|
+
expect(result.finalMessage).toBeNull();
|
|
161
|
+
expect(endReason).toBe("cancelled");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// --- All tool calls are invalid ---
|
|
165
|
+
|
|
166
|
+
it("continues looping when all tool calls in a turn are invalid", async () => {
|
|
167
|
+
const { ops, log } = createMockThreadOps();
|
|
168
|
+
|
|
169
|
+
const session = await createSession({
|
|
170
|
+
agentName: "TestAgent",
|
|
171
|
+
threadId: "thread-1",
|
|
172
|
+
runAgent: createScriptedRunAgent([
|
|
173
|
+
{
|
|
174
|
+
message: "bad calls",
|
|
175
|
+
toolCalls: [
|
|
176
|
+
{ id: "tc-1", name: "Nonexistent", args: {} },
|
|
177
|
+
{ id: "tc-2", name: "AlsoNonexistent", args: {} },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
message: "all done",
|
|
182
|
+
toolCalls: [],
|
|
183
|
+
},
|
|
184
|
+
]),
|
|
185
|
+
threadOps: ops,
|
|
186
|
+
tools: { Echo: createEchoTool() },
|
|
187
|
+
buildContextMessage: () => "go",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const stateManager = createAgentStateManager({
|
|
191
|
+
initialState: { systemPrompt: "test" },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await session.runSession({ stateManager });
|
|
195
|
+
|
|
196
|
+
expect(result.exitReason).toBe("completed");
|
|
197
|
+
expect(result.finalMessage).toBe("all done");
|
|
198
|
+
|
|
199
|
+
const errorResults = log.filter((l) => {
|
|
200
|
+
if (l.op !== "appendToolResult") return false;
|
|
201
|
+
const config = l.args[0] as ToolResultConfig;
|
|
202
|
+
return typeof config.content === "string" && config.content.includes("Invalid tool call");
|
|
203
|
+
});
|
|
204
|
+
expect(errorResults).toHaveLength(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// --- Tool call with missing id ---
|
|
208
|
+
|
|
209
|
+
it("handles tool call with missing id gracefully", async () => {
|
|
210
|
+
const { ops, log } = createMockThreadOps();
|
|
211
|
+
|
|
212
|
+
const session = await createSession({
|
|
213
|
+
agentName: "TestAgent",
|
|
214
|
+
threadId: "thread-1",
|
|
215
|
+
runAgent: createScriptedRunAgent([
|
|
216
|
+
{
|
|
217
|
+
message: "no id",
|
|
218
|
+
toolCalls: [{ name: "Echo", args: { text: "hello" } }],
|
|
219
|
+
},
|
|
220
|
+
{ message: "done", toolCalls: [] },
|
|
221
|
+
]),
|
|
222
|
+
threadOps: ops,
|
|
223
|
+
tools: { Echo: createEchoTool() },
|
|
224
|
+
buildContextMessage: () => "go",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const stateManager = createAgentStateManager({
|
|
228
|
+
initialState: { systemPrompt: "test" },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const result = await session.runSession({ stateManager });
|
|
232
|
+
expect(result.exitReason).toBe("completed");
|
|
233
|
+
|
|
234
|
+
const toolResults = log.filter((l) => l.op === "appendToolResult");
|
|
235
|
+
expect(toolResults.length).toBeGreaterThan(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// --- No tools registered but rawToolCalls returned ---
|
|
239
|
+
|
|
240
|
+
it("completes immediately when no tools are registered even if rawToolCalls are returned", async () => {
|
|
241
|
+
const { ops } = createMockThreadOps();
|
|
242
|
+
|
|
243
|
+
const session = await createSession({
|
|
244
|
+
agentName: "TestAgent",
|
|
245
|
+
threadId: "thread-1",
|
|
246
|
+
runAgent: createScriptedRunAgent([
|
|
247
|
+
{
|
|
248
|
+
message: "I tried calling a tool",
|
|
249
|
+
toolCalls: [{ id: "tc-1", name: "Something", args: {} }],
|
|
250
|
+
},
|
|
251
|
+
]),
|
|
252
|
+
threadOps: ops,
|
|
253
|
+
buildContextMessage: () => "go",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const stateManager = createAgentStateManager({
|
|
257
|
+
initialState: { systemPrompt: "test" },
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const result = await session.runSession({ stateManager });
|
|
261
|
+
|
|
262
|
+
expect(result.exitReason).toBe("completed");
|
|
263
|
+
expect(result.finalMessage).toBe("I tried calling a tool");
|
|
264
|
+
expect(result.usage.turns).toBe(1);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// --- Tool handler throws but session continues via failure hook ---
|
|
268
|
+
|
|
269
|
+
it("session continues when tool handler throws and failure hook recovers", async () => {
|
|
270
|
+
const { ops } = createMockThreadOps();
|
|
271
|
+
|
|
272
|
+
const failTool = defineTool({
|
|
273
|
+
name: "Fail" as const,
|
|
274
|
+
description: "always fails",
|
|
275
|
+
schema: z.object({}),
|
|
276
|
+
handler: async (): Promise<ToolHandlerResponse<null>> => {
|
|
277
|
+
throw new Error("tool exploded");
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const session = await createSession({
|
|
282
|
+
agentName: "TestAgent",
|
|
283
|
+
threadId: "thread-1",
|
|
284
|
+
runAgent: createScriptedRunAgent([
|
|
285
|
+
{
|
|
286
|
+
message: "calling fail",
|
|
287
|
+
toolCalls: [{ id: "tc-1", name: "Fail", args: {} }],
|
|
288
|
+
},
|
|
289
|
+
{ message: "recovered", toolCalls: [] },
|
|
290
|
+
]),
|
|
291
|
+
threadOps: ops,
|
|
292
|
+
tools: { Fail: failTool },
|
|
293
|
+
buildContextMessage: () => "go",
|
|
294
|
+
hooks: {
|
|
295
|
+
onPostToolUseFailure: async () => ({
|
|
296
|
+
fallbackContent: "Tool failed, but recovered",
|
|
297
|
+
}),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const stateManager = createAgentStateManager({
|
|
302
|
+
initialState: { systemPrompt: "test" },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const result = await session.runSession({ stateManager });
|
|
306
|
+
|
|
307
|
+
expect(result.exitReason).toBe("completed");
|
|
308
|
+
expect(result.finalMessage).toBe("recovered");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// --- Tool handler throws without recovery ---
|
|
312
|
+
|
|
313
|
+
it("session fails when tool handler throws with no failure hook", async () => {
|
|
314
|
+
const { ops } = createMockThreadOps();
|
|
315
|
+
let endReason: string | undefined;
|
|
316
|
+
|
|
317
|
+
const failTool = defineTool({
|
|
318
|
+
name: "Fail" as const,
|
|
319
|
+
description: "always fails",
|
|
320
|
+
schema: z.object({}),
|
|
321
|
+
handler: async (): Promise<ToolHandlerResponse<null>> => {
|
|
322
|
+
throw new Error("unrecoverable tool");
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const session = await createSession({
|
|
327
|
+
agentName: "TestAgent",
|
|
328
|
+
threadId: "thread-1",
|
|
329
|
+
runAgent: createScriptedRunAgent([
|
|
330
|
+
{
|
|
331
|
+
message: "calling fail",
|
|
332
|
+
toolCalls: [{ id: "tc-1", name: "Fail", args: {} }],
|
|
333
|
+
},
|
|
334
|
+
]),
|
|
335
|
+
threadOps: ops,
|
|
336
|
+
tools: { Fail: failTool },
|
|
337
|
+
buildContextMessage: () => "go",
|
|
338
|
+
hooks: {
|
|
339
|
+
onSessionEnd: async ({ exitReason }) => {
|
|
340
|
+
endReason = exitReason;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const stateManager = createAgentStateManager({
|
|
346
|
+
initialState: { systemPrompt: "test" },
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await expect(session.runSession({ stateManager })).rejects.toThrow(
|
|
350
|
+
"unrecoverable tool",
|
|
351
|
+
);
|
|
352
|
+
expect(endReason).toBe("failed");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// --- Metadata passed through to hooks ---
|
|
356
|
+
|
|
357
|
+
it("passes metadata to session hooks", async () => {
|
|
358
|
+
const { ops } = createMockThreadOps();
|
|
359
|
+
let capturedStartMeta: Record<string, unknown> | undefined;
|
|
360
|
+
let capturedEndMeta: Record<string, unknown> | undefined;
|
|
361
|
+
|
|
362
|
+
const session = await createSession({
|
|
363
|
+
agentName: "TestAgent",
|
|
364
|
+
threadId: "thread-1",
|
|
365
|
+
metadata: { env: "test", version: 42 },
|
|
366
|
+
runAgent: createScriptedRunAgent([
|
|
367
|
+
{ message: "done", toolCalls: [] },
|
|
368
|
+
]),
|
|
369
|
+
threadOps: ops,
|
|
370
|
+
buildContextMessage: () => "go",
|
|
371
|
+
hooks: {
|
|
372
|
+
onSessionStart: async ({ metadata }) => {
|
|
373
|
+
capturedStartMeta = metadata;
|
|
374
|
+
},
|
|
375
|
+
onSessionEnd: async ({ metadata }) => {
|
|
376
|
+
capturedEndMeta = metadata;
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const stateManager = createAgentStateManager({
|
|
382
|
+
initialState: { systemPrompt: "test" },
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await session.runSession({ stateManager });
|
|
386
|
+
|
|
387
|
+
expect(capturedStartMeta).toEqual({ env: "test", version: 42 });
|
|
388
|
+
expect(capturedEndMeta).toEqual({ env: "test", version: 42 });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// --- Sandbox error during create ---
|
|
392
|
+
|
|
393
|
+
it("propagates sandbox creation error", async () => {
|
|
394
|
+
const { ops } = createMockThreadOps();
|
|
395
|
+
|
|
396
|
+
const sandboxOps: SandboxOps = {
|
|
397
|
+
createSandbox: async () => {
|
|
398
|
+
throw new Error("sandbox creation failed");
|
|
399
|
+
},
|
|
400
|
+
destroySandbox: async () => {},
|
|
401
|
+
snapshotSandbox: async () => ({
|
|
402
|
+
sandboxId: "sb-1",
|
|
403
|
+
providerId: "test",
|
|
404
|
+
data: null,
|
|
405
|
+
createdAt: new Date().toISOString(),
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const session = await createSession({
|
|
410
|
+
agentName: "TestAgent",
|
|
411
|
+
threadId: "thread-1",
|
|
412
|
+
runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
|
|
413
|
+
threadOps: ops,
|
|
414
|
+
buildContextMessage: () => "go",
|
|
415
|
+
sandbox: sandboxOps,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const stateManager = createAgentStateManager({
|
|
419
|
+
initialState: { systemPrompt: "test" },
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await expect(session.runSession({ stateManager })).rejects.toThrow(
|
|
423
|
+
"sandbox creation failed",
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// --- Sandbox is destroyed even after session error ---
|
|
428
|
+
|
|
429
|
+
it("destroys sandbox in finally block even when session fails", async () => {
|
|
430
|
+
const { ops } = createMockThreadOps();
|
|
431
|
+
const sandboxLog: string[] = [];
|
|
432
|
+
|
|
433
|
+
const sandboxOps: SandboxOps = {
|
|
434
|
+
createSandbox: async () => {
|
|
435
|
+
sandboxLog.push("create");
|
|
436
|
+
return { sandboxId: "sb-cleanup" };
|
|
437
|
+
},
|
|
438
|
+
destroySandbox: async (id: string) => {
|
|
439
|
+
sandboxLog.push(`destroy:${id}`);
|
|
440
|
+
},
|
|
441
|
+
snapshotSandbox: async () => ({
|
|
442
|
+
sandboxId: "sb-1",
|
|
443
|
+
providerId: "test",
|
|
444
|
+
data: null,
|
|
445
|
+
createdAt: new Date().toISOString(),
|
|
446
|
+
}),
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const session = await createSession({
|
|
450
|
+
agentName: "TestAgent",
|
|
451
|
+
threadId: "thread-1",
|
|
452
|
+
runAgent: async () => {
|
|
453
|
+
throw new Error("LLM crash");
|
|
454
|
+
},
|
|
455
|
+
threadOps: ops,
|
|
456
|
+
buildContextMessage: () => "go",
|
|
457
|
+
sandbox: sandboxOps,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const stateManager = createAgentStateManager({
|
|
461
|
+
initialState: { systemPrompt: "test" },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await expect(session.runSession({ stateManager })).rejects.toThrow(
|
|
465
|
+
"LLM crash",
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
expect(sandboxLog).toContain("create");
|
|
469
|
+
expect(sandboxLog).toContain("destroy:sb-cleanup");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// --- Empty system prompt (whitespace only) ---
|
|
473
|
+
|
|
474
|
+
it("throws when system prompt is whitespace-only", async () => {
|
|
475
|
+
const { ops } = createMockThreadOps();
|
|
476
|
+
|
|
477
|
+
const session = await createSession({
|
|
478
|
+
agentName: "TestAgent",
|
|
479
|
+
threadId: "thread-1",
|
|
480
|
+
runAgent: createScriptedRunAgent([]),
|
|
481
|
+
threadOps: ops,
|
|
482
|
+
buildContextMessage: () => "hi",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const stateManager = createAgentStateManager({
|
|
486
|
+
initialState: { systemPrompt: " " },
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await expect(session.runSession({ stateManager })).rejects.toThrow(
|
|
490
|
+
"No system prompt in state",
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// --- Tool that returns usage ---
|
|
495
|
+
|
|
496
|
+
it("accumulates usage from both runAgent and tool handler results", async () => {
|
|
497
|
+
const { ops } = createMockThreadOps();
|
|
498
|
+
|
|
499
|
+
const usageTool = defineTool({
|
|
500
|
+
name: "SubAgent" as const,
|
|
501
|
+
description: "returns usage",
|
|
502
|
+
schema: z.object({}),
|
|
503
|
+
handler: async () => ({
|
|
504
|
+
toolResponse: "ok",
|
|
505
|
+
data: null,
|
|
506
|
+
usage: { inputTokens: 200, outputTokens: 100 },
|
|
507
|
+
}),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const session = await createSession({
|
|
511
|
+
agentName: "TestAgent",
|
|
512
|
+
threadId: "thread-1",
|
|
513
|
+
runAgent: createScriptedRunAgent([
|
|
514
|
+
{
|
|
515
|
+
message: "t1",
|
|
516
|
+
toolCalls: [
|
|
517
|
+
{ id: "tc-1", name: "SubAgent", args: {} },
|
|
518
|
+
{ id: "tc-2", name: "SubAgent", args: {} },
|
|
519
|
+
],
|
|
520
|
+
usage: { inputTokens: 50, outputTokens: 25 },
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
message: "done",
|
|
524
|
+
toolCalls: [],
|
|
525
|
+
usage: { inputTokens: 50, outputTokens: 25 },
|
|
526
|
+
},
|
|
527
|
+
]),
|
|
528
|
+
threadOps: ops,
|
|
529
|
+
tools: { SubAgent: usageTool },
|
|
530
|
+
buildContextMessage: () => "go",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const stateManager = createAgentStateManager({
|
|
534
|
+
initialState: { systemPrompt: "test" },
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const result = await session.runSession({ stateManager });
|
|
538
|
+
|
|
539
|
+
expect(result.usage.totalInputTokens).toBe(100);
|
|
540
|
+
expect(result.usage.totalOutputTokens).toBe(50);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// --- continueThread with no source thread ---
|
|
544
|
+
|
|
545
|
+
it("continueThread generates new threadId and forks when source is provided", async () => {
|
|
546
|
+
const { ops, log } = createMockThreadOps();
|
|
547
|
+
|
|
548
|
+
const session = await createSession({
|
|
549
|
+
agentName: "TestAgent",
|
|
550
|
+
threadId: "original-thread",
|
|
551
|
+
continueThread: true,
|
|
552
|
+
runAgent: createScriptedRunAgent([
|
|
553
|
+
{ message: "continued", toolCalls: [] },
|
|
554
|
+
]),
|
|
555
|
+
threadOps: ops,
|
|
556
|
+
buildContextMessage: () => "continue",
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const stateManager = createAgentStateManager({
|
|
560
|
+
initialState: { systemPrompt: "test" },
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const result = await session.runSession({ stateManager });
|
|
564
|
+
|
|
565
|
+
expect(result.exitReason).toBe("completed");
|
|
566
|
+
expect(result.threadId).not.toBe("original-thread");
|
|
567
|
+
|
|
568
|
+
const forkOps = log.filter((l) => l.op === "forkThread");
|
|
569
|
+
expect(forkOps).toHaveLength(1);
|
|
570
|
+
const forkOp = forkOps[0];
|
|
571
|
+
if (!forkOp) throw new Error("expected fork op");
|
|
572
|
+
expect(forkOp.args[0]).toBe("original-thread");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// --- maxTurns of 1 ---
|
|
576
|
+
|
|
577
|
+
it("stops after exactly 1 turn when maxTurns is 1", async () => {
|
|
578
|
+
const { ops } = createMockThreadOps();
|
|
579
|
+
|
|
580
|
+
const session = await createSession({
|
|
581
|
+
agentName: "TestAgent",
|
|
582
|
+
threadId: "thread-1",
|
|
583
|
+
maxTurns: 1,
|
|
584
|
+
runAgent: createScriptedRunAgent([
|
|
585
|
+
{
|
|
586
|
+
message: "turn 1",
|
|
587
|
+
toolCalls: [{ id: "tc-1", name: "Echo", args: { text: "hi" } }],
|
|
588
|
+
},
|
|
589
|
+
{ message: "turn 2", toolCalls: [] },
|
|
590
|
+
]),
|
|
591
|
+
threadOps: ops,
|
|
592
|
+
tools: { Echo: createEchoTool() },
|
|
593
|
+
buildContextMessage: () => "go",
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const stateManager = createAgentStateManager({
|
|
597
|
+
initialState: { systemPrompt: "test" },
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const result = await session.runSession({ stateManager });
|
|
601
|
+
|
|
602
|
+
expect(result.exitReason).toBe("max_turns");
|
|
603
|
+
expect(result.finalMessage).toBeNull();
|
|
604
|
+
expect(result.usage.turns).toBe(1);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// --- processToolsInParallel false ---
|
|
608
|
+
|
|
609
|
+
it("processes tools sequentially when processToolsInParallel is false", async () => {
|
|
610
|
+
const { ops } = createMockThreadOps();
|
|
611
|
+
const order: string[] = [];
|
|
612
|
+
|
|
613
|
+
const slowTool = defineTool({
|
|
614
|
+
name: "Slow" as const,
|
|
615
|
+
description: "slow",
|
|
616
|
+
schema: z.object({ id: z.string() }),
|
|
617
|
+
handler: async (args: { id: string }) => {
|
|
618
|
+
order.push(`start-${args.id}`);
|
|
619
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
620
|
+
order.push(`end-${args.id}`);
|
|
621
|
+
return { toolResponse: "ok", data: null };
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const session = await createSession({
|
|
626
|
+
agentName: "TestAgent",
|
|
627
|
+
threadId: "thread-1",
|
|
628
|
+
processToolsInParallel: false,
|
|
629
|
+
runAgent: createScriptedRunAgent([
|
|
630
|
+
{
|
|
631
|
+
message: "two calls",
|
|
632
|
+
toolCalls: [
|
|
633
|
+
{ id: "tc-1", name: "Slow", args: { id: "a" } },
|
|
634
|
+
{ id: "tc-2", name: "Slow", args: { id: "b" } },
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
{ message: "done", toolCalls: [] },
|
|
638
|
+
]),
|
|
639
|
+
threadOps: ops,
|
|
640
|
+
tools: { Slow: slowTool },
|
|
641
|
+
buildContextMessage: () => "go",
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const stateManager = createAgentStateManager({
|
|
645
|
+
initialState: { systemPrompt: "test" },
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
await session.runSession({ stateManager });
|
|
649
|
+
|
|
650
|
+
expect(order).toEqual(["start-a", "end-a", "start-b", "end-b"]);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// --- Mix of valid and unknown tool calls in single turn ---
|
|
654
|
+
|
|
655
|
+
it("processes valid tool calls even when some are unknown in same turn", async () => {
|
|
656
|
+
const { ops, log } = createMockThreadOps();
|
|
657
|
+
|
|
658
|
+
const session = await createSession({
|
|
659
|
+
agentName: "TestAgent",
|
|
660
|
+
threadId: "thread-1",
|
|
661
|
+
runAgent: createScriptedRunAgent([
|
|
662
|
+
{
|
|
663
|
+
message: "mixed",
|
|
664
|
+
toolCalls: [
|
|
665
|
+
{ id: "tc-1", name: "Echo", args: { text: "valid" } },
|
|
666
|
+
{ id: "tc-2", name: "Unknown", args: {} },
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
{ message: "done", toolCalls: [] },
|
|
670
|
+
]),
|
|
671
|
+
threadOps: ops,
|
|
672
|
+
tools: { Echo: createEchoTool() },
|
|
673
|
+
buildContextMessage: () => "go",
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const stateManager = createAgentStateManager({
|
|
677
|
+
initialState: { systemPrompt: "test" },
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const result = await session.runSession({ stateManager });
|
|
681
|
+
expect(result.exitReason).toBe("completed");
|
|
682
|
+
|
|
683
|
+
const toolResults = log.filter((l) => l.op === "appendToolResult");
|
|
684
|
+
const echoResult = toolResults.find((l) => {
|
|
685
|
+
const config = l.args[0] as ToolResultConfig;
|
|
686
|
+
return config.toolName === "Echo";
|
|
687
|
+
});
|
|
688
|
+
expect(echoResult).toBeDefined();
|
|
689
|
+
if (echoResult) {
|
|
690
|
+
expect((echoResult.args[0] as ToolResultConfig).content).toBe("Echo: valid");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const unknownResult = toolResults.find((l) => {
|
|
694
|
+
const config = l.args[0] as ToolResultConfig;
|
|
695
|
+
return config.toolName === "Unknown";
|
|
696
|
+
});
|
|
697
|
+
expect(unknownResult).toBeDefined();
|
|
698
|
+
const unknownContent = unknownResult
|
|
699
|
+
? (unknownResult.args[0] as ToolResultConfig).content
|
|
700
|
+
: undefined;
|
|
701
|
+
expect(
|
|
702
|
+
typeof unknownContent === "string" && unknownContent.includes("Invalid tool call"),
|
|
703
|
+
).toBe(true);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// --- buildContextMessage returns ContentPart[] ---
|
|
707
|
+
|
|
708
|
+
it("handles buildContextMessage returning ContentPart array", async () => {
|
|
709
|
+
const { ops, log } = createMockThreadOps();
|
|
710
|
+
|
|
711
|
+
const session = await createSession({
|
|
712
|
+
agentName: "TestAgent",
|
|
713
|
+
threadId: "thread-1",
|
|
714
|
+
runAgent: createScriptedRunAgent([
|
|
715
|
+
{ message: "done", toolCalls: [] },
|
|
716
|
+
]),
|
|
717
|
+
threadOps: ops,
|
|
718
|
+
buildContextMessage: () => [
|
|
719
|
+
{ type: "text", text: "Hello" },
|
|
720
|
+
{ type: "image_url", url: "https://example.com/img.png" },
|
|
721
|
+
],
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const stateManager = createAgentStateManager({
|
|
725
|
+
initialState: { systemPrompt: "test" },
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
await session.runSession({ stateManager });
|
|
729
|
+
|
|
730
|
+
const humanOps = log.filter((l) => l.op === "appendHumanMessage");
|
|
731
|
+
expect(humanOps).toHaveLength(1);
|
|
732
|
+
const humanOp = humanOps[0];
|
|
733
|
+
if (!humanOp) throw new Error("expected human op");
|
|
734
|
+
const content = humanOp.args[1];
|
|
735
|
+
expect(Array.isArray(content)).toBe(true);
|
|
736
|
+
const firstContent = (content as { type: string }[])[0];
|
|
737
|
+
if (!firstContent) throw new Error("expected content item");
|
|
738
|
+
expect(firstContent.type).toBe("text");
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// --- onSessionEnd always called (even on success) ---
|
|
742
|
+
|
|
743
|
+
it("onSessionEnd receives correct turns count", async () => {
|
|
744
|
+
const { ops } = createMockThreadOps();
|
|
745
|
+
let endTurns: number | undefined;
|
|
746
|
+
|
|
747
|
+
const session = await createSession({
|
|
748
|
+
agentName: "TestAgent",
|
|
749
|
+
threadId: "thread-1",
|
|
750
|
+
runAgent: createScriptedRunAgent([
|
|
751
|
+
{
|
|
752
|
+
message: "t1",
|
|
753
|
+
toolCalls: [{ id: "tc-1", name: "Echo", args: { text: "a" } }],
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
message: "t2",
|
|
757
|
+
toolCalls: [{ id: "tc-2", name: "Echo", args: { text: "b" } }],
|
|
758
|
+
},
|
|
759
|
+
{ message: "final", toolCalls: [] },
|
|
760
|
+
]),
|
|
761
|
+
threadOps: ops,
|
|
762
|
+
tools: { Echo: createEchoTool() },
|
|
763
|
+
buildContextMessage: () => "go",
|
|
764
|
+
hooks: {
|
|
765
|
+
onSessionEnd: async ({ turns }) => {
|
|
766
|
+
endTurns = turns;
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const stateManager = createAgentStateManager({
|
|
772
|
+
initialState: { systemPrompt: "test" },
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
await session.runSession({ stateManager });
|
|
776
|
+
|
|
777
|
+
expect(endTurns).toBe(3);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// --- Tool handler returns resultAppended: true ---
|
|
781
|
+
|
|
782
|
+
it("skips appendToolResult when handler sets resultAppended", async () => {
|
|
783
|
+
const { ops, log } = createMockThreadOps();
|
|
784
|
+
|
|
785
|
+
const selfAppendTool = defineTool({
|
|
786
|
+
name: "SelfAppend" as const,
|
|
787
|
+
description: "appends itself",
|
|
788
|
+
schema: z.object({}),
|
|
789
|
+
handler: async () => ({
|
|
790
|
+
toolResponse: "self-managed",
|
|
791
|
+
data: null,
|
|
792
|
+
resultAppended: true,
|
|
793
|
+
}),
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const session = await createSession({
|
|
797
|
+
agentName: "TestAgent",
|
|
798
|
+
threadId: "thread-1",
|
|
799
|
+
runAgent: createScriptedRunAgent([
|
|
800
|
+
{
|
|
801
|
+
message: "self",
|
|
802
|
+
toolCalls: [{ id: "tc-1", name: "SelfAppend", args: {} }],
|
|
803
|
+
},
|
|
804
|
+
{ message: "done", toolCalls: [] },
|
|
805
|
+
]),
|
|
806
|
+
threadOps: ops,
|
|
807
|
+
tools: { SelfAppend: selfAppendTool },
|
|
808
|
+
buildContextMessage: () => "go",
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const stateManager = createAgentStateManager({
|
|
812
|
+
initialState: { systemPrompt: "test" },
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
await session.runSession({ stateManager });
|
|
816
|
+
|
|
817
|
+
const toolResults = log.filter((l) => {
|
|
818
|
+
if (l.op !== "appendToolResult") return false;
|
|
819
|
+
const config = l.args[0] as ToolResultConfig;
|
|
820
|
+
return config.toolName === "SelfAppend";
|
|
821
|
+
});
|
|
822
|
+
expect(toolResults).toHaveLength(0);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// --- Pre-hook skips tool in session context ---
|
|
826
|
+
|
|
827
|
+
it("pre-hook skip works within full session flow", async () => {
|
|
828
|
+
const { ops, log } = createMockThreadOps();
|
|
829
|
+
|
|
830
|
+
const session = await createSession({
|
|
831
|
+
agentName: "TestAgent",
|
|
832
|
+
threadId: "thread-1",
|
|
833
|
+
runAgent: createScriptedRunAgent([
|
|
834
|
+
{
|
|
835
|
+
message: "calling",
|
|
836
|
+
toolCalls: [{ id: "tc-1", name: "Echo", args: { text: "skip-me" } }],
|
|
837
|
+
},
|
|
838
|
+
{ message: "done", toolCalls: [] },
|
|
839
|
+
]),
|
|
840
|
+
threadOps: ops,
|
|
841
|
+
tools: { Echo: createEchoTool() },
|
|
842
|
+
buildContextMessage: () => "go",
|
|
843
|
+
hooks: {
|
|
844
|
+
onPreToolUse: async ({ toolCall }) => {
|
|
845
|
+
if (toolCall.args && (toolCall.args as { text: string }).text === "skip-me") {
|
|
846
|
+
return { skip: true };
|
|
847
|
+
}
|
|
848
|
+
return {};
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const stateManager = createAgentStateManager({
|
|
854
|
+
initialState: { systemPrompt: "test" },
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
await session.runSession({ stateManager });
|
|
858
|
+
|
|
859
|
+
const toolResults = log.filter((l) => l.op === "appendToolResult");
|
|
860
|
+
expect(toolResults).toHaveLength(1);
|
|
861
|
+
const toolResult = toolResults[0];
|
|
862
|
+
if (!toolResult) throw new Error("expected tool result");
|
|
863
|
+
const content = (toolResult.args[0] as ToolResultConfig).content;
|
|
864
|
+
expect(typeof content === "string" && content.includes("Skipped")).toBe(true);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// --- Sandbox snapshot is not called on normal flow ---
|
|
868
|
+
|
|
869
|
+
it("sandbox snapshotSandbox is not called during normal session lifecycle", async () => {
|
|
870
|
+
const { ops } = createMockThreadOps();
|
|
871
|
+
const snapshotSpy = vi.fn(async () => ({
|
|
872
|
+
sandboxId: "sb-1",
|
|
873
|
+
providerId: "test",
|
|
874
|
+
data: null,
|
|
875
|
+
createdAt: new Date().toISOString(),
|
|
876
|
+
}));
|
|
877
|
+
|
|
878
|
+
const sandboxOps: SandboxOps = {
|
|
879
|
+
createSandbox: async () => ({ sandboxId: "sb-test" }),
|
|
880
|
+
destroySandbox: async () => {},
|
|
881
|
+
snapshotSandbox: snapshotSpy,
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const session = await createSession({
|
|
885
|
+
agentName: "TestAgent",
|
|
886
|
+
threadId: "thread-1",
|
|
887
|
+
runAgent: createScriptedRunAgent([{ message: "done", toolCalls: [] }]),
|
|
888
|
+
threadOps: ops,
|
|
889
|
+
buildContextMessage: () => "go",
|
|
890
|
+
sandbox: sandboxOps,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
const stateManager = createAgentStateManager({
|
|
894
|
+
initialState: { systemPrompt: "test" },
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
await session.runSession({ stateManager });
|
|
898
|
+
|
|
899
|
+
expect(snapshotSpy).not.toHaveBeenCalled();
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// --- Thread operations order ---
|
|
903
|
+
|
|
904
|
+
it("calls thread operations in correct order: system → human → [loop]", async () => {
|
|
905
|
+
const { ops, log } = createMockThreadOps();
|
|
906
|
+
|
|
907
|
+
const session = await createSession({
|
|
908
|
+
agentName: "TestAgent",
|
|
909
|
+
threadId: "thread-1",
|
|
910
|
+
runAgent: createScriptedRunAgent([
|
|
911
|
+
{
|
|
912
|
+
message: "t1",
|
|
913
|
+
toolCalls: [{ id: "tc-1", name: "Echo", args: { text: "a" } }],
|
|
914
|
+
},
|
|
915
|
+
{ message: "done", toolCalls: [] },
|
|
916
|
+
]),
|
|
917
|
+
threadOps: ops,
|
|
918
|
+
tools: { Echo: createEchoTool() },
|
|
919
|
+
buildContextMessage: () => "context message",
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
const stateManager = createAgentStateManager({
|
|
923
|
+
initialState: { systemPrompt: "System prompt here" },
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
await session.runSession({ stateManager });
|
|
927
|
+
|
|
928
|
+
const opNames = log.map((l) => l.op);
|
|
929
|
+
const systemIdx = opNames.indexOf("appendSystemMessage");
|
|
930
|
+
const humanIdx = opNames.indexOf("appendHumanMessage");
|
|
931
|
+
const toolResultIdx = opNames.indexOf("appendToolResult");
|
|
932
|
+
|
|
933
|
+
expect(systemIdx).toBeLessThan(humanIdx);
|
|
934
|
+
expect(humanIdx).toBeLessThan(toolResultIdx);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
// --- maxTurns = 0 exits immediately ---
|
|
938
|
+
|
|
939
|
+
it("exits with max_turns when maxTurns is 0", async () => {
|
|
940
|
+
const { ops } = createMockThreadOps();
|
|
941
|
+
|
|
942
|
+
const session = await createSession({
|
|
943
|
+
agentName: "TestAgent",
|
|
944
|
+
threadId: "thread-1",
|
|
945
|
+
maxTurns: 0,
|
|
946
|
+
runAgent: createScriptedRunAgent([]),
|
|
947
|
+
threadOps: ops,
|
|
948
|
+
tools: { Echo: createEchoTool() },
|
|
949
|
+
buildContextMessage: () => "go",
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
const stateManager = createAgentStateManager({
|
|
953
|
+
initialState: { systemPrompt: "test" },
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const result = await session.runSession({ stateManager });
|
|
957
|
+
|
|
958
|
+
expect(result.exitReason).toBe("max_turns");
|
|
959
|
+
expect(result.usage.turns).toBe(0);
|
|
960
|
+
expect(result.finalMessage).toBeNull();
|
|
961
|
+
});
|
|
962
|
+
});
|