@alexkroman1/aai 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +12 -0
  3. package/dist/_internal-types-CoDTiBd1.js +61 -0
  4. package/dist/host/_mock-ws.d.ts +0 -24
  5. package/dist/host/runtime-barrel.d.ts +0 -1
  6. package/dist/host/runtime-barrel.js +55 -5
  7. package/dist/host/runtime.d.ts +2 -0
  8. package/dist/host/tool-executor.d.ts +1 -0
  9. package/dist/host/ws-handler.d.ts +2 -0
  10. package/dist/sdk/manifest-barrel.d.ts +3 -5
  11. package/dist/sdk/manifest-barrel.js +2 -52
  12. package/dist/sdk/protocol.d.ts +8 -25
  13. package/dist/sdk/protocol.js +6 -3
  14. package/dist/sdk/types.d.ts +2 -0
  15. package/host/_mock-ws.ts +0 -50
  16. package/host/_test-utils.ts +1 -0
  17. package/host/runtime-barrel.ts +0 -1
  18. package/host/runtime.ts +13 -1
  19. package/host/session-ctx.test.ts +387 -0
  20. package/host/session-fixture-replay.test.ts +2 -10
  21. package/host/session.test.ts +19 -41
  22. package/host/tool-executor.test.ts +36 -0
  23. package/host/tool-executor.ts +4 -0
  24. package/host/ws-handler.ts +3 -0
  25. package/package.json +1 -1
  26. package/sdk/__snapshots__/exports.test.ts.snap +77 -0
  27. package/sdk/__snapshots__/schema-shapes.test.ts.snap +187 -0
  28. package/sdk/_test-matchers.test.ts +75 -0
  29. package/sdk/_test-matchers.ts +73 -0
  30. package/sdk/exports.test.ts +31 -0
  31. package/sdk/manifest-barrel.ts +13 -7
  32. package/sdk/manifest.test.ts +66 -2
  33. package/sdk/protocol-compat.test.ts +0 -6
  34. package/sdk/protocol-snapshot.test.ts +7 -5
  35. package/sdk/protocol.test.ts +107 -21
  36. package/sdk/protocol.ts +7 -15
  37. package/sdk/schema-alignment.test.ts +1 -27
  38. package/sdk/schema-shapes.test.ts +103 -0
  39. package/sdk/tsconfig.json +1 -1
  40. package/sdk/types.test.ts +56 -1
  41. package/sdk/types.ts +2 -0
  42. package/sdk/ws-upgrade.test.ts +8 -8
  43. package/tsconfig.build.json +8 -1
  44. package/tsconfig.json +1 -1
  45. package/vitest.config.ts +1 -0
  46. package/dist/system-prompt-nik_iavo.js +0 -92
@@ -0,0 +1,77 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`export surface stability > @alexkroman1/aai main export 1`] = `
4
+ [
5
+ "AGENT_CSP",
6
+ "BuiltinToolSchema",
7
+ "DEFAULT_GREETING",
8
+ "DEFAULT_IDLE_TIMEOUT_MS",
9
+ "DEFAULT_MAX_HISTORY",
10
+ "DEFAULT_SESSION_START_TIMEOUT_MS",
11
+ "DEFAULT_SHUTDOWN_TIMEOUT_MS",
12
+ "DEFAULT_STT_SAMPLE_RATE",
13
+ "DEFAULT_SYSTEM_PROMPT",
14
+ "DEFAULT_TTS_SAMPLE_RATE",
15
+ "FETCH_TIMEOUT_MS",
16
+ "MAX_HTML_BYTES",
17
+ "MAX_MESSAGE_BUFFER_SIZE",
18
+ "MAX_PAGE_CHARS",
19
+ "MAX_TOOL_RESULT_CHARS",
20
+ "MAX_VALUE_SIZE",
21
+ "MAX_WS_PAYLOAD_BYTES",
22
+ "RUN_CODE_TIMEOUT_MS",
23
+ "TOOL_EXECUTION_TIMEOUT_MS",
24
+ "ToolChoiceSchema",
25
+ "WS_OPEN",
26
+ "agent",
27
+ "errorDetail",
28
+ "errorMessage",
29
+ "parseWsUpgradeParams",
30
+ "tool",
31
+ "toolError",
32
+ ]
33
+ `;
34
+
35
+ exports[`export surface stability > @alexkroman1/aai/manifest export 1`] = `
36
+ [
37
+ "AgentConfigSchema",
38
+ "EMPTY_PARAMS",
39
+ "ToolSchemaSchema",
40
+ "agentToolsToSchemas",
41
+ "toAgentConfig",
42
+ ]
43
+ `;
44
+
45
+ exports[`export surface stability > @alexkroman1/aai/protocol export 1`] = `
46
+ [
47
+ "ClientEventSchema",
48
+ "ClientMessageSchema",
49
+ "KvDelSchema",
50
+ "KvGetSchema",
51
+ "KvRequestSchema",
52
+ "KvSetSchema",
53
+ "ReadyConfigSchema",
54
+ "ServerMessageSchema",
55
+ "SessionErrorCodeSchema",
56
+ "buildReadyConfig",
57
+ "lenientParse",
58
+ ]
59
+ `;
60
+
61
+ exports[`export surface stability > @alexkroman1/aai/runtime export 1`] = `
62
+ [
63
+ "DEFAULT_S2S_CONFIG",
64
+ "_internals",
65
+ "buildCtx",
66
+ "consoleLogger",
67
+ "createRuntime",
68
+ "createS2sSession",
69
+ "createServer",
70
+ "createUnstorageKv",
71
+ "executeInIsolate",
72
+ "executeToolCall",
73
+ "jsonLogger",
74
+ "resolveAllBuiltins",
75
+ "wireSessionSocket",
76
+ ]
77
+ `;
@@ -0,0 +1,187 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`manifest schema shapes > AgentConfigSchema shape 1`] = `
4
+ [
5
+ "builtinTools",
6
+ "greeting",
7
+ "idleTimeoutMs",
8
+ "maxSteps",
9
+ "name",
10
+ "sttPrompt",
11
+ "systemPrompt",
12
+ "toolChoice",
13
+ ]
14
+ `;
15
+
16
+ exports[`manifest schema shapes > ToolSchemaSchema shape 1`] = `
17
+ [
18
+ "description",
19
+ "name",
20
+ "parameters",
21
+ ]
22
+ `;
23
+
24
+ exports[`protocol schema shapes > ClientEventSchema option shapes 1`] = `
25
+ {
26
+ "agent_transcript": [
27
+ "text",
28
+ "type",
29
+ ],
30
+ "cancelled": [
31
+ "type",
32
+ ],
33
+ "custom_event": [
34
+ "data",
35
+ "event",
36
+ "type",
37
+ ],
38
+ "error": [
39
+ "code",
40
+ "message",
41
+ "type",
42
+ ],
43
+ "idle_timeout": [
44
+ "type",
45
+ ],
46
+ "reply_done": [
47
+ "type",
48
+ ],
49
+ "reset": [
50
+ "type",
51
+ ],
52
+ "speech_started": [
53
+ "type",
54
+ ],
55
+ "speech_stopped": [
56
+ "type",
57
+ ],
58
+ "tool_call": [
59
+ "args",
60
+ "toolCallId",
61
+ "toolName",
62
+ "type",
63
+ ],
64
+ "tool_call_done": [
65
+ "result",
66
+ "toolCallId",
67
+ "type",
68
+ ],
69
+ "user_transcript": [
70
+ "text",
71
+ "turnOrder",
72
+ "type",
73
+ ],
74
+ }
75
+ `;
76
+
77
+ exports[`protocol schema shapes > ClientMessageSchema option shapes 1`] = `
78
+ {
79
+ "audio_ready": [
80
+ "type",
81
+ ],
82
+ "cancel": [
83
+ "type",
84
+ ],
85
+ "history": [
86
+ "messages",
87
+ "type",
88
+ ],
89
+ "reset": [
90
+ "type",
91
+ ],
92
+ }
93
+ `;
94
+
95
+ exports[`protocol schema shapes > KvDelSchema shape 1`] = `
96
+ [
97
+ "key",
98
+ "op",
99
+ ]
100
+ `;
101
+
102
+ exports[`protocol schema shapes > KvGetSchema shape 1`] = `
103
+ [
104
+ "key",
105
+ "op",
106
+ ]
107
+ `;
108
+
109
+ exports[`protocol schema shapes > KvSetSchema shape 1`] = `
110
+ [
111
+ "expireIn",
112
+ "key",
113
+ "op",
114
+ "value",
115
+ ]
116
+ `;
117
+
118
+ exports[`protocol schema shapes > ReadyConfigSchema shape 1`] = `
119
+ [
120
+ "audioFormat",
121
+ "sampleRate",
122
+ "ttsSampleRate",
123
+ ]
124
+ `;
125
+
126
+ exports[`protocol schema shapes > ServerMessageSchema option shapes 1`] = `
127
+ {
128
+ "agent_transcript": [
129
+ "text",
130
+ "type",
131
+ ],
132
+ "audio_done": [
133
+ "type",
134
+ ],
135
+ "cancelled": [
136
+ "type",
137
+ ],
138
+ "config": [
139
+ "audioFormat",
140
+ "sampleRate",
141
+ "sessionId",
142
+ "ttsSampleRate",
143
+ "type",
144
+ ],
145
+ "custom_event": [
146
+ "data",
147
+ "event",
148
+ "type",
149
+ ],
150
+ "error": [
151
+ "code",
152
+ "message",
153
+ "type",
154
+ ],
155
+ "idle_timeout": [
156
+ "type",
157
+ ],
158
+ "reply_done": [
159
+ "type",
160
+ ],
161
+ "reset": [
162
+ "type",
163
+ ],
164
+ "speech_started": [
165
+ "type",
166
+ ],
167
+ "speech_stopped": [
168
+ "type",
169
+ ],
170
+ "tool_call": [
171
+ "args",
172
+ "toolCallId",
173
+ "toolName",
174
+ "type",
175
+ ],
176
+ "tool_call_done": [
177
+ "result",
178
+ "toolCallId",
179
+ "type",
180
+ ],
181
+ "user_transcript": [
182
+ "text",
183
+ "turnOrder",
184
+ "type",
185
+ ],
186
+ }
187
+ `;
@@ -0,0 +1,75 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import "./_test-matchers.ts";
5
+
6
+ describe("toBeValidClientEvent", () => {
7
+ it("passes for a valid event", () => {
8
+ expect({ type: "speech_started" }).toBeValidClientEvent();
9
+ });
10
+
11
+ it("passes for a valid event with fields", () => {
12
+ expect({
13
+ type: "user_transcript",
14
+ text: "hello world",
15
+ }).toBeValidClientEvent();
16
+ });
17
+
18
+ it("fails for an invalid event", () => {
19
+ expect(() => {
20
+ expect({ type: "not_a_real_event" }).toBeValidClientEvent();
21
+ }).toThrow(/expected value to be a valid ClientEvent/);
22
+ });
23
+
24
+ it("fails for a missing type field", () => {
25
+ expect(() => {
26
+ expect({ text: "no type" }).toBeValidClientEvent();
27
+ }).toThrow(/expected value to be a valid ClientEvent/);
28
+ });
29
+ });
30
+
31
+ describe("toContainEvent", () => {
32
+ const events = [
33
+ { type: "speech_started" },
34
+ { type: "user_transcript", text: "hello" },
35
+ { type: "tool_call", toolCallId: "tc1", toolName: "search", args: { q: "test" } },
36
+ { type: "reply_done" },
37
+ ];
38
+
39
+ it("finds a matching event by type", () => {
40
+ expect(events).toContainEvent("speech_started");
41
+ });
42
+
43
+ it("finds a matching event with fields", () => {
44
+ expect(events).toContainEvent("tool_call", { toolName: "search" });
45
+ });
46
+
47
+ it("matches a subset of fields", () => {
48
+ expect(events).toContainEvent("tool_call", {
49
+ toolName: "search",
50
+ args: { q: "test" },
51
+ });
52
+ });
53
+
54
+ it("fails when event type is not found", () => {
55
+ expect(() => {
56
+ expect(events).toContainEvent("cancelled");
57
+ }).toThrow(/expected array to contain event of type "cancelled"/);
58
+ });
59
+
60
+ it("fails when fields do not match", () => {
61
+ expect(() => {
62
+ expect(events).toContainEvent("tool_call", { toolName: "visit_webpage" });
63
+ }).toThrow(/expected array to contain event of type "tool_call"/);
64
+ });
65
+
66
+ it("fails when received value is not an array", () => {
67
+ expect(() => {
68
+ expect("not-an-array").toContainEvent("speech_started");
69
+ }).toThrow(/expected an array of events/);
70
+ });
71
+
72
+ it("supports .not negation", () => {
73
+ expect(events).not.toContainEvent("cancelled");
74
+ });
75
+ });
@@ -0,0 +1,73 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Custom Vitest matchers for AAI domain types.
4
+ *
5
+ * Registered via `expect.extend()` — add this file to the vitest `setupFiles`
6
+ * for the `aai` project so matchers are available in every test.
7
+ */
8
+
9
+ import { isDeepStrictEqual } from "node:util";
10
+ import { expect } from "vitest";
11
+ import { ClientEventSchema } from "./protocol.ts";
12
+
13
+ type MatcherResult = { pass: boolean; message: () => string };
14
+
15
+ // ─── Matcher implementations ────────────────────────────────────────────────
16
+
17
+ function toBeValidClientEvent(received: unknown): MatcherResult {
18
+ const result = ClientEventSchema.safeParse(received);
19
+ return {
20
+ pass: result.success,
21
+ message: () =>
22
+ result.success
23
+ ? "expected value NOT to be a valid ClientEvent, but it parsed successfully"
24
+ : `expected value to be a valid ClientEvent\n\nZod errors:\n${result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`,
25
+ };
26
+ }
27
+
28
+ function toContainEvent(
29
+ received: unknown,
30
+ type: string,
31
+ fields?: Record<string, unknown>,
32
+ ): MatcherResult {
33
+ if (!Array.isArray(received)) {
34
+ return {
35
+ pass: false,
36
+ message: () => `expected an array of events, but received ${typeof received}`,
37
+ };
38
+ }
39
+
40
+ const match = received.some((event: Record<string, unknown>) => {
41
+ if (event?.type !== type) return false;
42
+ if (!fields) return true;
43
+ return Object.entries(fields).every(([key, value]) => isDeepStrictEqual(event[key], value));
44
+ });
45
+
46
+ return {
47
+ pass: match,
48
+ message: () =>
49
+ match
50
+ ? `expected array NOT to contain event of type "${type}"${fields ? ` with fields ${JSON.stringify(fields)}` : ""}`
51
+ : `expected array to contain event of type "${type}"${fields ? ` with fields ${JSON.stringify(fields)}` : ""}\n\nReceived event types: [${received.map((e: Record<string, unknown>) => `"${e?.type}"`).join(", ")}]`,
52
+ };
53
+ }
54
+
55
+ // ─── Register matchers ──────────────────────────────────────────────────────
56
+
57
+ expect.extend({
58
+ toBeValidClientEvent,
59
+ toContainEvent,
60
+ });
61
+
62
+ // ─── Type augmentation ──────────────────────────────────────────────────────
63
+
64
+ declare module "vitest" {
65
+ interface Assertion<T> {
66
+ toBeValidClientEvent(): void;
67
+ toContainEvent(type: string, fields?: Record<string, unknown>): void;
68
+ }
69
+ interface AsymmetricMatchersContaining {
70
+ toBeValidClientEvent(): void;
71
+ toContainEvent(type: string, fields?: Record<string, unknown>): void;
72
+ }
73
+ }
@@ -0,0 +1,31 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Export surface snapshot tests for all four aai subpath exports.
4
+ *
5
+ * These tests catch accidental export additions or removals. If a snapshot
6
+ * breaks, it signals a potentially breaking API change that should be
7
+ * reviewed and documented with a changeset.
8
+ */
9
+ import { describe, expect, test } from "vitest";
10
+
11
+ describe("export surface stability", () => {
12
+ test("@alexkroman1/aai main export", async () => {
13
+ const mod = await import("@alexkroman1/aai");
14
+ expect(Object.keys(mod).sort()).toMatchSnapshot();
15
+ });
16
+
17
+ test("@alexkroman1/aai/protocol export", async () => {
18
+ const mod = await import("@alexkroman1/aai/protocol");
19
+ expect(Object.keys(mod).sort()).toMatchSnapshot();
20
+ });
21
+
22
+ test("@alexkroman1/aai/manifest export", async () => {
23
+ const mod = await import("@alexkroman1/aai/manifest");
24
+ expect(Object.keys(mod).sort()).toMatchSnapshot();
25
+ });
26
+
27
+ test("@alexkroman1/aai/runtime export", async () => {
28
+ const mod = await import("@alexkroman1/aai/runtime");
29
+ expect(Object.keys(mod).sort()).toMatchSnapshot();
30
+ });
31
+ });
@@ -1,12 +1,18 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
2
  /**
3
- * Manifest barrel — agent manifest parsing and tool schema conversion.
3
+ * Manifest barrel — agent config conversion and tool schema handling.
4
4
  *
5
- * Used by aai-cli (scanner, bundler) and aai-server (tests).
5
+ * Used by aai-cli (bundler) and aai-server (rpc-schemas).
6
6
  */
7
7
 
8
- // biome-ignore-all lint/performance/noReExportAll: barrel file by design
9
-
10
- export * from "./_internal-types.ts";
11
- export * from "./manifest.ts";
12
- export * from "./system-prompt.ts";
8
+ export {
9
+ type AgentConfig,
10
+ AgentConfigSchema,
11
+ type AgentConfigSource,
12
+ agentToolsToSchemas,
13
+ EMPTY_PARAMS,
14
+ type ExecuteTool,
15
+ type ToolSchema,
16
+ ToolSchemaSchema,
17
+ toAgentConfig,
18
+ } from "./_internal-types.ts";
@@ -1,6 +1,9 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- import { describe, expect, test } from "vitest";
3
- import { parseManifest } from "./manifest.ts";
2
+ import fc from "fast-check";
3
+ import { describe, expect, expectTypeOf, test } from "vitest";
4
+ import { type Manifest, parseManifest } from "./manifest.ts";
5
+ import type { AgentConfig, ToolSchema } from "./manifest-barrel.ts";
6
+ import { agentToolsToSchemas, toAgentConfig } from "./manifest-barrel.ts";
4
7
 
5
8
  describe("parseManifest", () => {
6
9
  test("minimal manifest requires only name", () => {
@@ -54,3 +57,64 @@ describe("parseManifest", () => {
54
57
  expect(() => parseManifest({ name: "X", builtinTools: ["not_a_tool"] })).toThrow();
55
58
  });
56
59
  });
60
+
61
+ // ── Property-based tests ─────────────────────────────────────────────────
62
+
63
+ describe("property: parseManifest", () => {
64
+ test("valid manifests always parse", () => {
65
+ const validManifestArb = fc.record({
66
+ name: fc.string({ minLength: 1 }),
67
+ systemPrompt: fc.option(fc.string(), { nil: undefined }),
68
+ greeting: fc.option(fc.string(), { nil: undefined }),
69
+ maxSteps: fc.option(fc.integer({ min: 1, max: 100 }), { nil: undefined }),
70
+ toolChoice: fc.option(fc.constantFrom("auto" as const, "required" as const), {
71
+ nil: undefined,
72
+ }),
73
+ });
74
+
75
+ fc.assert(
76
+ fc.property(validManifestArb, (manifest) => {
77
+ const result = parseManifest(manifest);
78
+ expect(result.name).toBe(manifest.name);
79
+ expect(result.maxSteps).toBeGreaterThan(0);
80
+ expect(["auto", "required"]).toContain(result.toolChoice);
81
+ }),
82
+ );
83
+ });
84
+
85
+ test("missing name throws", () => {
86
+ // Generate objects that never have a `name` field
87
+ const noNameArb = fc.record({
88
+ systemPrompt: fc.option(fc.string(), { nil: undefined }),
89
+ greeting: fc.option(fc.string(), { nil: undefined }),
90
+ maxSteps: fc.option(fc.integer({ min: 1, max: 100 }), { nil: undefined }),
91
+ });
92
+
93
+ fc.assert(
94
+ fc.property(noNameArb, (obj) => {
95
+ expect(() => parseManifest(obj)).toThrow();
96
+ }),
97
+ );
98
+ });
99
+ });
100
+
101
+ describe("manifest type contracts", () => {
102
+ test("parseManifest returns Manifest", () => {
103
+ const result = parseManifest({ name: "test" });
104
+ expectTypeOf(result).toEqualTypeOf<Manifest>();
105
+ });
106
+
107
+ test("parseManifest accepts unknown input", () => {
108
+ expectTypeOf(parseManifest).parameter(0).toBeUnknown();
109
+ });
110
+
111
+ test("toAgentConfig returns AgentConfig", () => {
112
+ const config = toAgentConfig({ name: "test", systemPrompt: "p", greeting: "g" });
113
+ expectTypeOf(config).toEqualTypeOf<AgentConfig>();
114
+ });
115
+
116
+ test("agentToolsToSchemas returns ToolSchema[]", () => {
117
+ const schemas = agentToolsToSchemas({});
118
+ expectTypeOf(schemas).toEqualTypeOf<ToolSchema[]>();
119
+ });
120
+ });
@@ -15,7 +15,6 @@ import {
15
15
  MAX_TOOL_RESULT_CHARS,
16
16
  } from "./constants.ts";
17
17
  import {
18
- AUDIO_FORMAT,
19
18
  ClientMessageSchema,
20
19
  KvRequestSchema,
21
20
  ServerMessageSchema,
@@ -35,7 +34,6 @@ type Fixture = {
35
34
  ClientMessage: Record<string, unknown>[];
36
35
  KvRequest: Record<string, unknown>[];
37
36
  constants: {
38
- AUDIO_FORMAT: string;
39
37
  DEFAULT_STT_SAMPLE_RATE: number;
40
38
  DEFAULT_TTS_SAMPLE_RATE: number;
41
39
  MAX_TOOL_RESULT_CHARS: number;
@@ -161,10 +159,6 @@ describe.each(fixtureFiles)("compat fixture: %s", (filename) => {
161
159
  // ── Constants stability ─────────────────────────────────────────
162
160
 
163
161
  describe("constants stability", () => {
164
- test("AUDIO_FORMAT unchanged", () => {
165
- expect(AUDIO_FORMAT).toBe(fixture.constants.AUDIO_FORMAT);
166
- });
167
-
168
162
  test("DEFAULT_STT_SAMPLE_RATE unchanged", () => {
169
163
  expect(DEFAULT_STT_SAMPLE_RATE).toBe(fixture.constants.DEFAULT_STT_SAMPLE_RATE);
170
164
  });
@@ -14,7 +14,6 @@ import {
14
14
  } from "./constants.ts";
15
15
  import type { ClientEvent, ClientMessage, ServerMessage } from "./protocol.ts";
16
16
  import {
17
- AUDIO_FORMAT,
18
17
  ClientEventSchema,
19
18
  ClientMessageSchema,
20
19
  KvRequestSchema,
@@ -24,10 +23,6 @@ import {
24
23
  // ── Constants ────────────────────────────────────────────────────────────
25
24
 
26
25
  describe("protocol constants", () => {
27
- test("audio format", () => {
28
- expect(AUDIO_FORMAT).toMatchInlineSnapshot(`"pcm16"`);
29
- });
30
-
31
26
  test("sample rates", () => {
32
27
  expect(DEFAULT_STT_SAMPLE_RATE).toMatchInlineSnapshot("16000");
33
28
  expect(DEFAULT_TTS_SAMPLE_RATE).toMatchInlineSnapshot("24000");
@@ -77,6 +72,7 @@ describe("server→client event wire format", () => {
77
72
  ["reset", { type: "reset" }],
78
73
  ["idle_timeout", { type: "idle_timeout" }],
79
74
  ["error", { type: "error", code: "stt", message: "Speech recognition failed" }],
75
+ ["custom_event", { type: "custom_event", event: "game_state", data: { hp: 10 } }],
80
76
  ];
81
77
 
82
78
  test.each(valid)("%s parses successfully", (_label, event) => {
@@ -94,6 +90,12 @@ describe("server→client event wire format", () => {
94
90
  ).toBe(false);
95
91
  });
96
92
 
93
+ test("rejects custom_event with empty event name", () => {
94
+ expect(
95
+ ClientEventSchema.safeParse({ type: "custom_event", event: "", data: null }).success,
96
+ ).toBe(false);
97
+ });
98
+
97
99
  test("rejects tool_call_done with oversized result", () => {
98
100
  expect(
99
101
  ClientEventSchema.safeParse({