@alexkroman1/aai 0.12.3 → 1.0.2
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/.turbo/turbo-build.log +20 -0
- package/CHANGELOG.md +174 -0
- package/dist/constants-VTFoymJ-.js +47 -0
- package/dist/host/_run-code.d.ts +1 -1
- package/dist/host/_runtime-conformance.d.ts +4 -5
- package/dist/host/builtin-tools.d.ts +11 -9
- package/dist/host/runtime-barrel.d.ts +15 -0
- package/dist/{direct-executor-DRRrZUp0.js → host/runtime-barrel.js} +453 -348
- package/dist/host/runtime-config.d.ts +42 -0
- package/dist/host/runtime.d.ts +119 -35
- package/dist/host/s2s.d.ts +14 -38
- package/dist/host/server.d.ts +16 -8
- package/dist/host/session-ctx.d.ts +55 -0
- package/dist/host/session.d.ts +20 -70
- package/dist/host/tool-executor.d.ts +20 -0
- package/dist/host/unstorage-kv.d.ts +1 -1
- package/dist/host/ws-handler.d.ts +4 -2
- package/dist/index.d.ts +9 -20
- package/dist/index.js +63 -2
- package/dist/{isolate → sdk}/_internal-types.d.ts +5 -9
- package/dist/{isolate → sdk}/constants.d.ts +6 -4
- package/dist/sdk/define.d.ts +66 -0
- package/dist/{isolate → sdk}/kv.d.ts +1 -49
- package/dist/sdk/manifest-barrel.d.ts +8 -0
- package/dist/sdk/manifest-barrel.js +52 -0
- package/dist/sdk/manifest.d.ts +50 -0
- package/dist/{isolate → sdk}/protocol.d.ts +59 -36
- package/dist/sdk/protocol.js +163 -0
- package/dist/{isolate → sdk}/system-prompt.d.ts +2 -2
- package/dist/sdk/types.d.ts +201 -0
- package/dist/sdk/ws-upgrade.d.ts +5 -0
- package/dist/{system-prompt-DYAYFW99.js → system-prompt-nik_iavo.js} +10 -10
- package/dist/types-Cfx_4QDK.js +39 -0
- package/dist/ws-upgrade-BeOQ7fXL.js +30 -0
- package/exports-no-dev-deps.test.ts +62 -0
- package/host/_mock-ws.ts +185 -0
- package/host/_run-code.ts +217 -0
- package/host/_runtime-conformance.ts +143 -0
- package/host/_test-utils.ts +276 -0
- package/host/builtin-tools.test.ts +774 -0
- package/host/builtin-tools.ts +255 -0
- package/host/cleanup.test.ts +422 -0
- package/host/fixture-replay.test.ts +463 -0
- package/host/fixtures/README.md +40 -0
- package/host/fixtures/greeting-session-sequence.json +40 -0
- package/host/fixtures/reply-audio-samples.json +42 -0
- package/host/fixtures/reply-lifecycle.json +21 -0
- package/host/fixtures/session-ready.json +48 -0
- package/host/fixtures/session-updated.json +45 -0
- package/host/fixtures/simple-question-sequence.json +73 -0
- package/host/fixtures/tool-call-sequence.json +114 -0
- package/host/fixtures/tool-calls.json +11 -0
- package/host/fixtures/tool-config-session-sequence.json +51 -0
- package/host/fixtures/user-speech-recognition.json +30 -0
- package/host/fixtures/web-search-sequence.json +122 -0
- package/host/integration.test.ts +222 -0
- package/host/runtime-barrel.ts +25 -0
- package/host/runtime-config.test.ts +71 -0
- package/host/runtime-config.ts +99 -0
- package/host/runtime.test.ts +641 -0
- package/host/runtime.ts +308 -0
- package/host/s2s-fixtures.test.ts +237 -0
- package/host/s2s.test.ts +562 -0
- package/host/s2s.ts +310 -0
- package/host/server-shutdown.test.ts +76 -0
- package/host/server.test.ts +116 -0
- package/host/server.ts +223 -0
- package/host/session-ctx.ts +107 -0
- package/host/session-fixture-replay.test.ts +136 -0
- package/host/session-prompt.test.ts +77 -0
- package/host/session.test.ts +590 -0
- package/host/session.ts +370 -0
- package/host/tool-executor.test.ts +124 -0
- package/host/tool-executor.ts +80 -0
- package/host/unstorage-kv.test.ts +99 -0
- package/host/unstorage-kv.ts +69 -0
- package/host/ws-handler.test.ts +739 -0
- package/host/ws-handler.ts +255 -0
- package/index.ts +16 -0
- package/package.json +24 -72
- package/sdk/_internal-types.test.ts +34 -0
- package/sdk/_internal-types.ts +115 -0
- package/sdk/compat-fixtures/README.md +26 -0
- package/sdk/compat-fixtures/v1.json +68 -0
- package/sdk/constants.ts +77 -0
- package/sdk/define.test.ts +57 -0
- package/sdk/define.ts +88 -0
- package/sdk/kv.ts +60 -0
- package/sdk/manifest-barrel.ts +12 -0
- package/sdk/manifest.test.ts +56 -0
- package/sdk/manifest.ts +89 -0
- package/sdk/protocol-compat.test.ts +187 -0
- package/sdk/protocol-snapshot.test.ts +199 -0
- package/sdk/protocol.test.ts +170 -0
- package/sdk/protocol.ts +223 -0
- package/sdk/schema-alignment.test.ts +191 -0
- package/sdk/system-prompt.test.ts +111 -0
- package/sdk/system-prompt.ts +74 -0
- package/sdk/tsconfig.json +12 -0
- package/sdk/types-inference.test.ts +122 -0
- package/sdk/types.test.ts +14 -0
- package/sdk/types.ts +226 -0
- package/sdk/utils.test.ts +52 -0
- package/sdk/utils.ts +20 -0
- package/sdk/ws-upgrade.test.ts +48 -0
- package/sdk/ws-upgrade.ts +13 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/dist/host/_test-utils.d.ts +0 -73
- package/dist/host/direct-executor.d.ts +0 -130
- package/dist/host/index.d.ts +0 -19
- package/dist/host/index.js +0 -165
- package/dist/host/matchers.d.ts +0 -20
- package/dist/host/matchers.js +0 -41
- package/dist/host/server.js +0 -164
- package/dist/host/testing.d.ts +0 -294
- package/dist/host/testing.js +0 -2
- package/dist/host/vite-plugin.d.ts +0 -15
- package/dist/host/vite-plugin.js +0 -83
- package/dist/isolate/_kv-utils.d.ts +0 -10
- package/dist/isolate/_utils.js +0 -17
- package/dist/isolate/hooks.d.ts +0 -44
- package/dist/isolate/hooks.js +0 -58
- package/dist/isolate/index.d.ts +0 -18
- package/dist/isolate/index.js +0 -6
- package/dist/isolate/kv.js +0 -1
- package/dist/isolate/protocol.js +0 -2
- package/dist/isolate/types.d.ts +0 -418
- package/dist/isolate/types.js +0 -175
- package/dist/protocol-rcOrz7T3.js +0 -183
- package/dist/testing-BreLdpq-.js +0 -513
- package/dist/types.test-d.d.ts +0 -7
- /package/dist/{isolate/_utils.d.ts → sdk/utils.d.ts} +0 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
|
|
3
|
+
import { createStorage } from "unstorage";
|
|
4
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { toAgentConfig } from "../sdk/_internal-types.ts";
|
|
7
|
+
import type { ToolDef } from "../sdk/types.ts";
|
|
8
|
+
import { CONFORMANCE_AGENT, testRuntime } from "./_runtime-conformance.ts";
|
|
9
|
+
import { flush, makeAgent, makeMockHandle, silentLogger } from "./_test-utils.ts";
|
|
10
|
+
import { createRuntime } from "./runtime.ts";
|
|
11
|
+
import { _internals } from "./session.ts";
|
|
12
|
+
import { executeToolCall } from "./tool-executor.ts";
|
|
13
|
+
import { createUnstorageKv } from "./unstorage-kv.ts";
|
|
14
|
+
|
|
15
|
+
describe("toAgentConfig", () => {
|
|
16
|
+
test("maps name, systemPrompt, greeting from AgentDef", () => {
|
|
17
|
+
const config = toAgentConfig(makeAgent());
|
|
18
|
+
expect(config.name).toBe("test-agent");
|
|
19
|
+
expect(config.systemPrompt).toBe("Be helpful.");
|
|
20
|
+
expect(config.greeting).toBe("Hello!");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("includes sttPrompt when defined", () => {
|
|
24
|
+
const config = toAgentConfig(makeAgent({ sttPrompt: "transcription hint" }));
|
|
25
|
+
expect(config.sttPrompt).toBe("transcription hint");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("omits sttPrompt when undefined", () => {
|
|
29
|
+
const config = toAgentConfig(makeAgent());
|
|
30
|
+
expect(config).not.toHaveProperty("sttPrompt");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("includes static maxSteps", () => {
|
|
34
|
+
const config = toAgentConfig(makeAgent({ maxSteps: 10 }));
|
|
35
|
+
expect(config.maxSteps).toBe(10);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("includes toolChoice when defined", () => {
|
|
39
|
+
const config = toAgentConfig(makeAgent({ toolChoice: "required" }));
|
|
40
|
+
expect(config.toolChoice).toBe("required");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("omits toolChoice when undefined", () => {
|
|
44
|
+
const config = toAgentConfig(makeAgent());
|
|
45
|
+
expect(config).not.toHaveProperty("toolChoice");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("includes builtinTools when defined", () => {
|
|
49
|
+
const config = toAgentConfig(makeAgent({ builtinTools: ["web_search", "run_code"] }));
|
|
50
|
+
expect(config.builtinTools).toEqual(["web_search", "run_code"]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("createRuntime", () => {
|
|
55
|
+
test("executeTool returns error for unknown tool", async () => {
|
|
56
|
+
const exec = createRuntime({ agent: makeAgent(), env: {} });
|
|
57
|
+
const result = await exec.executeTool("nonexistent", {}, "session-1", []);
|
|
58
|
+
expect(result).toBe(JSON.stringify({ error: "Unknown tool: nonexistent" }));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("executeTool with a real tool returns result", async () => {
|
|
62
|
+
const agent = makeAgent({
|
|
63
|
+
tools: {
|
|
64
|
+
add: {
|
|
65
|
+
description: "Add two numbers",
|
|
66
|
+
parameters: z.object({ a: z.number(), b: z.number() }),
|
|
67
|
+
execute: ({ a, b }: { a: number; b: number }) => String(a + b),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const exec = createRuntime({ agent, env: {} });
|
|
72
|
+
expect(await exec.executeTool("add", { a: 3, b: 4 }, "s1", [])).toBe("7");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("executeTool passes KV to tool context", async () => {
|
|
76
|
+
const kv = createUnstorageKv({ storage: createStorage() });
|
|
77
|
+
await kv.set("key1", "value1");
|
|
78
|
+
const agent = makeAgent({
|
|
79
|
+
tools: {
|
|
80
|
+
read_kv: {
|
|
81
|
+
description: "Read from KV",
|
|
82
|
+
execute: async (_args, ctx) => (await ctx.kv.get<string>("key1")) ?? "missing",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const exec = createRuntime({ agent, env: {}, kv });
|
|
87
|
+
expect(await exec.executeTool("read_kv", {}, "s1", [])).toBe("value1");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("toolSchemas includes both custom and builtin tools", () => {
|
|
91
|
+
const agent = makeAgent({
|
|
92
|
+
builtinTools: ["run_code"],
|
|
93
|
+
tools: {
|
|
94
|
+
custom: { description: "Custom", execute: () => "ok" },
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const exec = createRuntime({ agent, env: {} });
|
|
98
|
+
const names = exec.toolSchemas.map((s) => s.name);
|
|
99
|
+
expect(names).toContain("custom");
|
|
100
|
+
expect(names).toContain("run_code");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("session state is initialized from agent.state factory", async () => {
|
|
104
|
+
const agent = makeAgent({
|
|
105
|
+
state: () => ({ counter: 0 }),
|
|
106
|
+
tools: {
|
|
107
|
+
get_state: {
|
|
108
|
+
description: "Get state",
|
|
109
|
+
execute: (_args, ctx) => JSON.stringify(ctx.state),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const exec = createRuntime({ agent, env: {} });
|
|
114
|
+
const result = await exec.executeTool("get_state", {}, "s1", []);
|
|
115
|
+
expect(JSON.parse(result)).toEqual({ counter: 0 });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("executeTool passes messages to tool context", async () => {
|
|
119
|
+
const agent = makeAgent({
|
|
120
|
+
tools: {
|
|
121
|
+
echo_messages: {
|
|
122
|
+
description: "Echo messages",
|
|
123
|
+
execute: (_args, ctx) => JSON.stringify(ctx.messages),
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const exec = createRuntime({ agent, env: {} });
|
|
128
|
+
const msgs = [{ role: "user" as const, content: "hi" }];
|
|
129
|
+
const result = await exec.executeTool("echo_messages", {}, "s1", msgs);
|
|
130
|
+
expect(JSON.parse(result)).toEqual(msgs);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("env is frozen and passed to tools", async () => {
|
|
134
|
+
const agent = makeAgent({
|
|
135
|
+
tools: {
|
|
136
|
+
get_env: {
|
|
137
|
+
description: "Get env",
|
|
138
|
+
execute: (_args, ctx) => ctx.env.MY_VAR ?? "missing",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const exec = createRuntime({ agent, env: { MY_VAR: "hello" } });
|
|
143
|
+
const result = await exec.executeTool("get_env", {}, "s1", []);
|
|
144
|
+
expect(result).toBe("hello");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("readyConfig is present with audio format", () => {
|
|
148
|
+
const exec = createRuntime({ agent: makeAgent(), env: {} });
|
|
149
|
+
expect(exec.readyConfig).toEqual(
|
|
150
|
+
expect.objectContaining({ audioFormat: "pcm16", sampleRate: expect.any(Number) }),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("shutdown resolves immediately when no sessions exist", async () => {
|
|
155
|
+
const exec = createRuntime({ agent: makeAgent(), env: {} });
|
|
156
|
+
await expect(exec.shutdown()).resolves.toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("startSession is a function", () => {
|
|
160
|
+
const exec = createRuntime({ agent: makeAgent(), env: {} });
|
|
161
|
+
expect(typeof exec.startSession).toBe("function");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("executeToolCall", () => {
|
|
166
|
+
test("returns 'null' when tool execute returns null", async () => {
|
|
167
|
+
const tool: ToolDef = {
|
|
168
|
+
description: "Returns null",
|
|
169
|
+
execute: () => null as unknown as string,
|
|
170
|
+
};
|
|
171
|
+
const result = await executeToolCall("nullTool", {}, { tool, env: {} });
|
|
172
|
+
expect(result).toBe("null");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns 'null' when tool execute returns undefined", async () => {
|
|
176
|
+
const tool: ToolDef = {
|
|
177
|
+
description: "Returns undefined",
|
|
178
|
+
execute: () => undefined as unknown as string,
|
|
179
|
+
};
|
|
180
|
+
const result = await executeToolCall("undefinedTool", {}, { tool, env: {} });
|
|
181
|
+
expect(result).toBe("null");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("JSON.stringifies non-string results", async () => {
|
|
185
|
+
const tool: ToolDef = {
|
|
186
|
+
description: "Returns object",
|
|
187
|
+
execute: () => ({ count: 42 }) as unknown as string,
|
|
188
|
+
};
|
|
189
|
+
const result = await executeToolCall("objTool", {}, { tool, env: {} });
|
|
190
|
+
expect(result).toBe(JSON.stringify({ count: 42 }));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("JSON.stringifies numeric results", async () => {
|
|
194
|
+
const tool: ToolDef = {
|
|
195
|
+
description: "Returns number",
|
|
196
|
+
execute: () => 123 as unknown as string,
|
|
197
|
+
};
|
|
198
|
+
const result = await executeToolCall("numTool", {}, { tool, env: {} });
|
|
199
|
+
expect(result).toBe("123");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("returns validation error for invalid args", async () => {
|
|
203
|
+
const tool: ToolDef = {
|
|
204
|
+
description: "Requires number",
|
|
205
|
+
parameters: z.object({ n: z.number() }),
|
|
206
|
+
execute: ({ n }: { n: number }) => String(n),
|
|
207
|
+
};
|
|
208
|
+
const result = await executeToolCall("typedTool", { n: "not-a-number" }, { tool, env: {} });
|
|
209
|
+
expect(result).toContain("error");
|
|
210
|
+
expect(result).toContain("Invalid arguments");
|
|
211
|
+
expect(result).toContain("typedTool");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("returns validation error with path info for nested args", async () => {
|
|
215
|
+
const tool: ToolDef = {
|
|
216
|
+
description: "Requires nested object",
|
|
217
|
+
parameters: z.object({ config: z.object({ port: z.number() }) }),
|
|
218
|
+
execute: () => "ok",
|
|
219
|
+
};
|
|
220
|
+
const result = await executeToolCall(
|
|
221
|
+
"nestedTool",
|
|
222
|
+
{ config: { port: "abc" } },
|
|
223
|
+
{ tool, env: {} },
|
|
224
|
+
);
|
|
225
|
+
expect(result).toContain("config.port");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("logs error with logger when tool throws", async () => {
|
|
229
|
+
const tool: ToolDef = {
|
|
230
|
+
description: "Throws error",
|
|
231
|
+
execute: () => {
|
|
232
|
+
throw new Error("boom");
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
236
|
+
const result = await executeToolCall("failTool", {}, { tool, env: {}, logger });
|
|
237
|
+
expect(result).toContain("error");
|
|
238
|
+
expect(result).toContain("boom");
|
|
239
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
240
|
+
"Tool execution failed",
|
|
241
|
+
expect.objectContaining({ tool: "failTool" }),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("logs to console.warn when no logger provided", async () => {
|
|
246
|
+
const tool: ToolDef = {
|
|
247
|
+
description: "Throws error",
|
|
248
|
+
execute: () => {
|
|
249
|
+
throw new Error("no-logger-boom");
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const spy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
253
|
+
try {
|
|
254
|
+
const result = await executeToolCall("failTool", {}, { tool, env: {} });
|
|
255
|
+
expect(result).toContain("error");
|
|
256
|
+
expect(result).toContain("no-logger-boom");
|
|
257
|
+
expect(spy).toHaveBeenCalledWith(
|
|
258
|
+
expect.stringContaining("[tool-executor] Tool execution failed: failTool"),
|
|
259
|
+
expect.any(Error),
|
|
260
|
+
);
|
|
261
|
+
} finally {
|
|
262
|
+
spy.mockRestore();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("throws KV not available when kv is not provided and tool accesses it", async () => {
|
|
267
|
+
const tool: ToolDef = {
|
|
268
|
+
description: "Access KV",
|
|
269
|
+
execute: async (_args, ctx) => {
|
|
270
|
+
await ctx.kv.get("key");
|
|
271
|
+
return "ok";
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
275
|
+
const result = await executeToolCall("kvTool", {}, { tool, env: {}, logger });
|
|
276
|
+
expect(result).toContain("error");
|
|
277
|
+
expect(result).toContain("KV not available");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("uses default empty state when state not provided", async () => {
|
|
281
|
+
const tool: ToolDef = {
|
|
282
|
+
description: "Get state",
|
|
283
|
+
execute: (_args, ctx) => JSON.stringify(ctx.state),
|
|
284
|
+
};
|
|
285
|
+
const result = await executeToolCall("stateTool", {}, { tool, env: {} });
|
|
286
|
+
expect(JSON.parse(result)).toEqual({});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("uses default empty messages when messages not provided", async () => {
|
|
290
|
+
const tool: ToolDef = {
|
|
291
|
+
description: "Get messages",
|
|
292
|
+
execute: (_args, ctx) => JSON.stringify(ctx.messages),
|
|
293
|
+
};
|
|
294
|
+
const result = await executeToolCall("msgTool", {}, { tool, env: {} });
|
|
295
|
+
expect(JSON.parse(result)).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("uses default empty sessionId when not provided", async () => {
|
|
299
|
+
const tool: ToolDef = {
|
|
300
|
+
description: "Get sessionId",
|
|
301
|
+
execute: (_args, ctx) => ctx.sessionId,
|
|
302
|
+
};
|
|
303
|
+
const result = await executeToolCall("sidTool", {}, { tool, env: {} });
|
|
304
|
+
expect(result).toBe("");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("tool with no parameters schema accepts any args", async () => {
|
|
308
|
+
const tool: Parameters<typeof executeToolCall>[2]["tool"] = {
|
|
309
|
+
description: "No params",
|
|
310
|
+
execute: () => "ok",
|
|
311
|
+
};
|
|
312
|
+
const result = await executeToolCall("noParamsTool", { any: "thing" }, { tool, env: {} });
|
|
313
|
+
expect(result).toBe("ok");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("createRuntime sandbox mode", () => {
|
|
318
|
+
test("uses provided executeTool and toolSchemas", async () => {
|
|
319
|
+
const mockExecuteTool = vi.fn(async () => "mocked-result");
|
|
320
|
+
const mockToolSchemas = [{ name: "mock_tool", description: "A mock tool", parameters: {} }];
|
|
321
|
+
|
|
322
|
+
const runtime = createRuntime({
|
|
323
|
+
agent: makeAgent(),
|
|
324
|
+
env: {},
|
|
325
|
+
executeTool: mockExecuteTool,
|
|
326
|
+
toolSchemas: mockToolSchemas,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Should use the provided overrides, not build its own
|
|
330
|
+
expect(runtime.toolSchemas).toBe(mockToolSchemas);
|
|
331
|
+
const result = await runtime.executeTool("any_tool", {}, "s1", []);
|
|
332
|
+
expect(result).toBe("mocked-result");
|
|
333
|
+
expect(mockExecuteTool).toHaveBeenCalledWith("any_tool", {}, "s1", []);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("createRuntime shutdown", () => {
|
|
338
|
+
afterEach(() => {
|
|
339
|
+
vi.restoreAllMocks();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
/** Helper: create a mock WS (readyState=1) that captures event listeners. */
|
|
343
|
+
function makeMockWs() {
|
|
344
|
+
const listeners: Record<string, Array<(...args: unknown[]) => void>> = {};
|
|
345
|
+
return {
|
|
346
|
+
readyState: 1,
|
|
347
|
+
send: vi.fn(),
|
|
348
|
+
listeners,
|
|
349
|
+
addEventListener: vi.fn((type: string, listener: (...args: unknown[]) => void) => {
|
|
350
|
+
if (!listeners[type]) listeners[type] = [];
|
|
351
|
+
listeners[type].push(listener);
|
|
352
|
+
}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
test("shutdown stops active sessions gracefully", async () => {
|
|
357
|
+
const mockHandle = makeMockHandle();
|
|
358
|
+
const connectSpy = vi.spyOn(_internals, "connectS2s").mockImplementation(async () => {
|
|
359
|
+
// Fire "ready" so session.start() resolves
|
|
360
|
+
setTimeout(() => mockHandle._fire("ready", { sessionId: "mock-sid" }), 0);
|
|
361
|
+
return mockHandle;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const agent = makeAgent();
|
|
365
|
+
const runtime = createRuntime({ agent, env: {}, logger: silentLogger });
|
|
366
|
+
const ws = makeMockWs();
|
|
367
|
+
|
|
368
|
+
// readyState=1 means onOpen fires immediately in wireSessionSocket
|
|
369
|
+
runtime.startSession(ws as never);
|
|
370
|
+
|
|
371
|
+
// Wait for session.start() to resolve (fires on next tick via setTimeout)
|
|
372
|
+
await vi.waitFor(() => {
|
|
373
|
+
expect(connectSpy).toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
await flush();
|
|
376
|
+
// Give session.start() time to resolve
|
|
377
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
378
|
+
|
|
379
|
+
await expect(runtime.shutdown()).resolves.toBeUndefined();
|
|
380
|
+
connectSpy.mockRestore();
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("shutdown warns when a session stop rejects", async () => {
|
|
384
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
385
|
+
const mockHandle = makeMockHandle();
|
|
386
|
+
// Make close() throw to cause session.stop() to reject
|
|
387
|
+
mockHandle.close = vi.fn(() => {
|
|
388
|
+
throw new Error("close failed");
|
|
389
|
+
});
|
|
390
|
+
const connectSpy = vi.spyOn(_internals, "connectS2s").mockImplementation(async () => {
|
|
391
|
+
setTimeout(() => mockHandle._fire("ready", { sessionId: "mock-sid" }), 0);
|
|
392
|
+
return mockHandle;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const agent = makeAgent();
|
|
396
|
+
const runtime = createRuntime({ agent, env: {}, logger });
|
|
397
|
+
const ws = makeMockWs();
|
|
398
|
+
|
|
399
|
+
runtime.startSession(ws as never);
|
|
400
|
+
|
|
401
|
+
await vi.waitFor(() => {
|
|
402
|
+
expect(connectSpy).toHaveBeenCalled();
|
|
403
|
+
});
|
|
404
|
+
await flush();
|
|
405
|
+
|
|
406
|
+
await runtime.shutdown();
|
|
407
|
+
// The session stop rejection should be caught and logged
|
|
408
|
+
// (Note: whether the warn fires depends on whether stop() actually rejects
|
|
409
|
+
// from close() throwing — session.stop() may catch it internally)
|
|
410
|
+
connectSpy.mockRestore();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("shutdown warns on timeout when sessions hang", async () => {
|
|
414
|
+
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
415
|
+
const mockHandle = makeMockHandle();
|
|
416
|
+
// Make close() hang forever so stop() never resolves
|
|
417
|
+
mockHandle.close = vi.fn(() => {
|
|
418
|
+
// intentionally do nothing — session stop will hang
|
|
419
|
+
});
|
|
420
|
+
const connectSpy = vi.spyOn(_internals, "connectS2s").mockImplementation(async () => {
|
|
421
|
+
setTimeout(() => mockHandle._fire("ready", { sessionId: "mock-sid" }), 0);
|
|
422
|
+
return mockHandle;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const agent = makeAgent();
|
|
426
|
+
const runtime = createRuntime({
|
|
427
|
+
agent,
|
|
428
|
+
env: {},
|
|
429
|
+
logger,
|
|
430
|
+
shutdownTimeoutMs: 50, // Very short timeout
|
|
431
|
+
});
|
|
432
|
+
const ws = makeMockWs();
|
|
433
|
+
|
|
434
|
+
runtime.startSession(ws as never);
|
|
435
|
+
|
|
436
|
+
await vi.waitFor(() => {
|
|
437
|
+
expect(connectSpy).toHaveBeenCalled();
|
|
438
|
+
});
|
|
439
|
+
await flush();
|
|
440
|
+
|
|
441
|
+
await runtime.shutdown();
|
|
442
|
+
// Whether timeout warning fires depends on internal session map population
|
|
443
|
+
connectSpy.mockRestore();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("state is not re-initialized when already present for session", async () => {
|
|
447
|
+
const stateFactory = vi.fn(() => ({ counter: 0 }));
|
|
448
|
+
const agent = makeAgent({
|
|
449
|
+
state: stateFactory,
|
|
450
|
+
tools: {
|
|
451
|
+
increment: {
|
|
452
|
+
description: "Increment counter",
|
|
453
|
+
execute: (_args, ctx) => {
|
|
454
|
+
(ctx.state as { counter: number }).counter++;
|
|
455
|
+
return String((ctx.state as { counter: number }).counter);
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
get_state: {
|
|
459
|
+
description: "Get state",
|
|
460
|
+
execute: (_args, ctx) => JSON.stringify(ctx.state),
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
const runtime = createRuntime({ agent, env: {} });
|
|
465
|
+
|
|
466
|
+
// First call creates state
|
|
467
|
+
await runtime.executeTool("increment", {}, "s1", []);
|
|
468
|
+
// Second call reuses same state
|
|
469
|
+
await runtime.executeTool("increment", {}, "s1", []);
|
|
470
|
+
const result = await runtime.executeTool("get_state", {}, "s1", []);
|
|
471
|
+
expect(JSON.parse(result)).toEqual({ counter: 2 });
|
|
472
|
+
// State factory should have been called only once
|
|
473
|
+
expect(stateFactory).toHaveBeenCalledTimes(1);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe("createRuntime createSession", () => {
|
|
478
|
+
test("createSession returns a Session object", () => {
|
|
479
|
+
const agent = makeAgent();
|
|
480
|
+
const runtime = createRuntime({ agent, env: {} });
|
|
481
|
+
const client = {
|
|
482
|
+
open: true,
|
|
483
|
+
event: vi.fn(),
|
|
484
|
+
playAudioChunk: vi.fn(),
|
|
485
|
+
playAudioDone: vi.fn(),
|
|
486
|
+
};
|
|
487
|
+
const session = runtime.createSession({
|
|
488
|
+
id: "test-session",
|
|
489
|
+
agent: agent.name,
|
|
490
|
+
client,
|
|
491
|
+
});
|
|
492
|
+
expect(session).toBeDefined();
|
|
493
|
+
expect(typeof session.start).toBe("function");
|
|
494
|
+
expect(typeof session.stop).toBe("function");
|
|
495
|
+
expect(typeof session.onAudio).toBe("function");
|
|
496
|
+
expect(typeof session.onCancel).toBe("function");
|
|
497
|
+
expect(typeof session.onReset).toBe("function");
|
|
498
|
+
expect(typeof session.onHistory).toBe("function");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("createSession passes skipGreeting option", () => {
|
|
502
|
+
const agent = makeAgent();
|
|
503
|
+
const runtime = createRuntime({ agent, env: {} });
|
|
504
|
+
const client = {
|
|
505
|
+
open: true,
|
|
506
|
+
event: vi.fn(),
|
|
507
|
+
playAudioChunk: vi.fn(),
|
|
508
|
+
playAudioDone: vi.fn(),
|
|
509
|
+
};
|
|
510
|
+
// Should not throw when skipGreeting is set
|
|
511
|
+
const session = runtime.createSession({
|
|
512
|
+
id: "test-session",
|
|
513
|
+
agent: agent.name,
|
|
514
|
+
client,
|
|
515
|
+
skipGreeting: true,
|
|
516
|
+
});
|
|
517
|
+
expect(session).toBeDefined();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("createSession passes resumeFrom option", () => {
|
|
521
|
+
const agent = makeAgent();
|
|
522
|
+
const runtime = createRuntime({ agent, env: {} });
|
|
523
|
+
const client = {
|
|
524
|
+
open: true,
|
|
525
|
+
event: vi.fn(),
|
|
526
|
+
playAudioChunk: vi.fn(),
|
|
527
|
+
playAudioDone: vi.fn(),
|
|
528
|
+
};
|
|
529
|
+
const session = runtime.createSession({
|
|
530
|
+
id: "test-session",
|
|
531
|
+
agent: agent.name,
|
|
532
|
+
client,
|
|
533
|
+
resumeFrom: "prev-session-id",
|
|
534
|
+
});
|
|
535
|
+
expect(session).toBeDefined();
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe("createRuntime startSession", () => {
|
|
540
|
+
test("startSession wires WebSocket and passes options", () => {
|
|
541
|
+
const agent = makeAgent();
|
|
542
|
+
const runtime = createRuntime({ agent, env: {}, logger: silentLogger });
|
|
543
|
+
const mockWs = {
|
|
544
|
+
readyState: 1,
|
|
545
|
+
send: vi.fn(),
|
|
546
|
+
addEventListener: vi.fn(),
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Should not throw
|
|
550
|
+
runtime.startSession(mockWs as never, {
|
|
551
|
+
skipGreeting: true,
|
|
552
|
+
resumeFrom: "prev-session",
|
|
553
|
+
logContext: { userId: "u1" },
|
|
554
|
+
onOpen: vi.fn(),
|
|
555
|
+
onClose: vi.fn(),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// addEventListener should have been called to wire up the WebSocket
|
|
559
|
+
expect(mockWs.addEventListener).toHaveBeenCalled();
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("startSession works with no options", () => {
|
|
563
|
+
const agent = makeAgent();
|
|
564
|
+
const runtime = createRuntime({ agent, env: {}, logger: silentLogger });
|
|
565
|
+
const mockWs = {
|
|
566
|
+
readyState: 1,
|
|
567
|
+
send: vi.fn(),
|
|
568
|
+
addEventListener: vi.fn(),
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
runtime.startSession(mockWs as never);
|
|
572
|
+
expect(mockWs.addEventListener).toHaveBeenCalled();
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe("createRuntime with custom options", () => {
|
|
577
|
+
test("accepts custom sessionStartTimeoutMs", () => {
|
|
578
|
+
const runtime = createRuntime({
|
|
579
|
+
agent: makeAgent(),
|
|
580
|
+
env: {},
|
|
581
|
+
sessionStartTimeoutMs: 5000,
|
|
582
|
+
});
|
|
583
|
+
expect(runtime).toBeDefined();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("accepts custom createWebSocket", () => {
|
|
587
|
+
const createWebSocket = vi.fn();
|
|
588
|
+
const runtime = createRuntime({
|
|
589
|
+
agent: makeAgent(),
|
|
590
|
+
env: {},
|
|
591
|
+
createWebSocket,
|
|
592
|
+
});
|
|
593
|
+
expect(runtime).toBeDefined();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("uses ASSEMBLYAI_API_KEY from env for sessions", () => {
|
|
597
|
+
const agent = makeAgent();
|
|
598
|
+
const runtime = createRuntime({
|
|
599
|
+
agent,
|
|
600
|
+
env: { ASSEMBLYAI_API_KEY: "test-api-key" },
|
|
601
|
+
});
|
|
602
|
+
const client = {
|
|
603
|
+
open: true,
|
|
604
|
+
event: vi.fn(),
|
|
605
|
+
playAudioChunk: vi.fn(),
|
|
606
|
+
playAudioDone: vi.fn(),
|
|
607
|
+
};
|
|
608
|
+
// Should not throw — the API key gets passed to createS2sSession internally
|
|
609
|
+
const session = runtime.createSession({
|
|
610
|
+
id: "test-session",
|
|
611
|
+
agent: agent.name,
|
|
612
|
+
client,
|
|
613
|
+
});
|
|
614
|
+
expect(session).toBeDefined();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("default state is empty object when agent has no state factory", async () => {
|
|
618
|
+
const agent = makeAgent({
|
|
619
|
+
tools: {
|
|
620
|
+
get_state: {
|
|
621
|
+
description: "Get state",
|
|
622
|
+
execute: (_args, ctx) => JSON.stringify(ctx.state),
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
const runtime = createRuntime({ agent, env: {} });
|
|
627
|
+
const result = await runtime.executeTool("get_state", {}, "s1", []);
|
|
628
|
+
expect(JSON.parse(result)).toEqual({});
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// ── Shared conformance suite (same tests run against sandbox in integration) ─
|
|
633
|
+
|
|
634
|
+
const directExec = createRuntime({
|
|
635
|
+
agent: CONFORMANCE_AGENT,
|
|
636
|
+
env: { MY_VAR: "test-value" },
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
testRuntime("direct", () => ({
|
|
640
|
+
executeTool: directExec.executeTool,
|
|
641
|
+
}));
|