@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,199 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Wire format snapshot tests for the WebSocket protocol.
|
|
4
|
+
*
|
|
5
|
+
* These ensure that changes to Zod schemas in protocol.ts don't
|
|
6
|
+
* accidentally alter the wire format. If a snapshot breaks, it
|
|
7
|
+
* signals a potentially breaking protocol change.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_STT_SAMPLE_RATE,
|
|
12
|
+
DEFAULT_TTS_SAMPLE_RATE,
|
|
13
|
+
TOOL_EXECUTION_TIMEOUT_MS,
|
|
14
|
+
} from "./constants.ts";
|
|
15
|
+
import type { ClientEvent, ClientMessage, ServerMessage } from "./protocol.ts";
|
|
16
|
+
import {
|
|
17
|
+
AUDIO_FORMAT,
|
|
18
|
+
ClientEventSchema,
|
|
19
|
+
ClientMessageSchema,
|
|
20
|
+
KvRequestSchema,
|
|
21
|
+
SessionErrorCodeSchema,
|
|
22
|
+
} from "./protocol.ts";
|
|
23
|
+
|
|
24
|
+
// ── Constants ────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe("protocol constants", () => {
|
|
27
|
+
test("audio format", () => {
|
|
28
|
+
expect(AUDIO_FORMAT).toMatchInlineSnapshot(`"pcm16"`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("sample rates", () => {
|
|
32
|
+
expect(DEFAULT_STT_SAMPLE_RATE).toMatchInlineSnapshot("16000");
|
|
33
|
+
expect(DEFAULT_TTS_SAMPLE_RATE).toMatchInlineSnapshot("24000");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("timeout constants", () => {
|
|
37
|
+
expect(TOOL_EXECUTION_TIMEOUT_MS).toMatchInlineSnapshot("30000");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("error codes", () => {
|
|
41
|
+
expect(SessionErrorCodeSchema.options).toMatchInlineSnapshot(`
|
|
42
|
+
[
|
|
43
|
+
"stt",
|
|
44
|
+
"llm",
|
|
45
|
+
"tts",
|
|
46
|
+
"tool",
|
|
47
|
+
"protocol",
|
|
48
|
+
"connection",
|
|
49
|
+
"audio",
|
|
50
|
+
"internal",
|
|
51
|
+
]
|
|
52
|
+
`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── Server → Client events (ClientEventSchema) ──────────────────────────
|
|
57
|
+
|
|
58
|
+
describe("server→client event wire format", () => {
|
|
59
|
+
const valid: [string, ClientEvent][] = [
|
|
60
|
+
["speech_started", { type: "speech_started" }],
|
|
61
|
+
["speech_stopped", { type: "speech_stopped" }],
|
|
62
|
+
["user_transcript", { type: "user_transcript", text: "hello" }],
|
|
63
|
+
["user_transcript (with order)", { type: "user_transcript", text: "hello", turnOrder: 1 }],
|
|
64
|
+
["agent_transcript", { type: "agent_transcript", text: "response" }],
|
|
65
|
+
[
|
|
66
|
+
"tool_call",
|
|
67
|
+
{
|
|
68
|
+
type: "tool_call",
|
|
69
|
+
toolCallId: "tc1",
|
|
70
|
+
toolName: "web_search",
|
|
71
|
+
args: { query: "weather" },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
["tool_call_done", { type: "tool_call_done", toolCallId: "tc1", result: "72F" }],
|
|
75
|
+
["reply_done", { type: "reply_done" }],
|
|
76
|
+
["cancelled", { type: "cancelled" }],
|
|
77
|
+
["reset", { type: "reset" }],
|
|
78
|
+
["idle_timeout", { type: "idle_timeout" }],
|
|
79
|
+
["error", { type: "error", code: "stt", message: "Speech recognition failed" }],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
test.each(valid)("%s parses successfully", (_label, event) => {
|
|
83
|
+
const result = ClientEventSchema.safeParse(event);
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("rejects unknown event type", () => {
|
|
88
|
+
expect(ClientEventSchema.safeParse({ type: "bogus" }).success).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("rejects invalid error code", () => {
|
|
92
|
+
expect(
|
|
93
|
+
ClientEventSchema.safeParse({ type: "error", code: "invalid_code", message: "x" }).success,
|
|
94
|
+
).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("rejects tool_call_done with oversized result", () => {
|
|
98
|
+
expect(
|
|
99
|
+
ClientEventSchema.safeParse({
|
|
100
|
+
type: "tool_call_done",
|
|
101
|
+
toolCallId: "tc1",
|
|
102
|
+
result: "x".repeat(4001),
|
|
103
|
+
}).success,
|
|
104
|
+
).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ── Client → Server messages (ClientMessageSchema) ──────────────────────
|
|
109
|
+
|
|
110
|
+
describe("client→server message wire format", () => {
|
|
111
|
+
const valid: [string, ClientMessage][] = [
|
|
112
|
+
["audio_ready", { type: "audio_ready" }],
|
|
113
|
+
["cancel", { type: "cancel" }],
|
|
114
|
+
["reset", { type: "reset" }],
|
|
115
|
+
[
|
|
116
|
+
"history",
|
|
117
|
+
{
|
|
118
|
+
type: "history",
|
|
119
|
+
messages: [
|
|
120
|
+
{ role: "user", content: "Hello" },
|
|
121
|
+
{ role: "assistant", content: "Hi" },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
test.each(valid)("%s parses successfully", (_label, msg) => {
|
|
128
|
+
const result = ClientMessageSchema.safeParse(msg);
|
|
129
|
+
expect(result.success).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("rejects unknown message type", () => {
|
|
133
|
+
expect(ClientMessageSchema.safeParse({ type: "bogus" }).success).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("rejects history with invalid role", () => {
|
|
137
|
+
expect(
|
|
138
|
+
ClientMessageSchema.safeParse({
|
|
139
|
+
type: "history",
|
|
140
|
+
messages: [{ role: "system", text: "nope" }],
|
|
141
|
+
}).success,
|
|
142
|
+
).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("rejects history exceeding 200 messages", () => {
|
|
146
|
+
const messages = Array.from({ length: 201 }, (_, i) => ({
|
|
147
|
+
role: "user" as const,
|
|
148
|
+
text: `msg ${i}`,
|
|
149
|
+
}));
|
|
150
|
+
expect(ClientMessageSchema.safeParse({ type: "history", messages }).success).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── ServerMessage union (type check) ────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe("ServerMessage type covers all variants", () => {
|
|
157
|
+
test("config message shape", () => {
|
|
158
|
+
const msg: ServerMessage = {
|
|
159
|
+
type: "config",
|
|
160
|
+
audioFormat: "pcm16",
|
|
161
|
+
sampleRate: 16_000,
|
|
162
|
+
ttsSampleRate: 24_000,
|
|
163
|
+
};
|
|
164
|
+
expect(msg.type).toBe("config");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("audio_done message shape", () => {
|
|
168
|
+
const msg: ServerMessage = { type: "audio_done" };
|
|
169
|
+
expect(msg.type).toBe("audio_done");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("ClientEvent is a valid ServerMessage", () => {
|
|
173
|
+
const msg: ServerMessage = { type: "speech_started" };
|
|
174
|
+
expect(msg.type).toBe("speech_started");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── KvRequestSchema ─────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe("KvRequest wire format", () => {
|
|
181
|
+
const valid = [
|
|
182
|
+
["get", { op: "get", key: "k1" }],
|
|
183
|
+
["set", { op: "set", key: "k1", value: "v1" }],
|
|
184
|
+
["set with expireIn", { op: "set", key: "k1", value: "v1", expireIn: 60_000 }],
|
|
185
|
+
["del", { op: "del", key: "k1" }],
|
|
186
|
+
] as const;
|
|
187
|
+
|
|
188
|
+
test.each(valid)("%s parses successfully", (_label, req) => {
|
|
189
|
+
expect(KvRequestSchema.safeParse(req).success).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects unknown op", () => {
|
|
193
|
+
expect(KvRequestSchema.safeParse({ op: "update", key: "k1" }).success).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("rejects empty key for get", () => {
|
|
197
|
+
expect(KvRequestSchema.safeParse({ op: "get", key: "" }).success).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_STT_SAMPLE_RATE,
|
|
4
|
+
DEFAULT_TTS_SAMPLE_RATE,
|
|
5
|
+
TOOL_EXECUTION_TIMEOUT_MS,
|
|
6
|
+
} from "./constants.ts";
|
|
7
|
+
import {
|
|
8
|
+
AUDIO_FORMAT,
|
|
9
|
+
buildReadyConfig,
|
|
10
|
+
ClientEventSchema,
|
|
11
|
+
ClientMessageSchema,
|
|
12
|
+
KvRequestSchema,
|
|
13
|
+
SessionErrorCodeSchema,
|
|
14
|
+
} from "./protocol.ts";
|
|
15
|
+
|
|
16
|
+
describe("protocol constants", () => {
|
|
17
|
+
test("DEFAULT_STT_SAMPLE_RATE is 16000", () => {
|
|
18
|
+
expect(DEFAULT_STT_SAMPLE_RATE).toBe(16_000);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("DEFAULT_TTS_SAMPLE_RATE is 24000", () => {
|
|
22
|
+
expect(DEFAULT_TTS_SAMPLE_RATE).toBe(24_000);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('AUDIO_FORMAT is "pcm16"', () => {
|
|
26
|
+
expect(AUDIO_FORMAT).toBe("pcm16");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("TOOL_EXECUTION_TIMEOUT_MS is 30000", () => {
|
|
30
|
+
expect(TOOL_EXECUTION_TIMEOUT_MS).toBe(30_000);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("KvRequestSchema", () => {
|
|
35
|
+
test("accepts valid get request", () => {
|
|
36
|
+
const result = KvRequestSchema.safeParse({
|
|
37
|
+
op: "get",
|
|
38
|
+
key: "my-key",
|
|
39
|
+
});
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("accepts valid set request with ttl", () => {
|
|
44
|
+
const result = KvRequestSchema.safeParse({
|
|
45
|
+
op: "set",
|
|
46
|
+
key: "my-key",
|
|
47
|
+
value: "my-value",
|
|
48
|
+
ttl: 3600,
|
|
49
|
+
});
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The Kv interface accepts `value: unknown` — ensure the schema matches.
|
|
54
|
+
test.each([
|
|
55
|
+
{ nested: true },
|
|
56
|
+
[1, 2, 3],
|
|
57
|
+
null,
|
|
58
|
+
42,
|
|
59
|
+
])("set accepts non-string value: %j", (value) => {
|
|
60
|
+
const result = KvRequestSchema.safeParse({ op: "set", key: "k", value });
|
|
61
|
+
expect(result.success).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("rejects empty key on get", () => {
|
|
65
|
+
const result = KvRequestSchema.safeParse({
|
|
66
|
+
op: "get",
|
|
67
|
+
key: "",
|
|
68
|
+
});
|
|
69
|
+
expect(result.success).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("SessionErrorCodeSchema", () => {
|
|
74
|
+
test.each([
|
|
75
|
+
"stt",
|
|
76
|
+
"llm",
|
|
77
|
+
"tts",
|
|
78
|
+
"tool",
|
|
79
|
+
"protocol",
|
|
80
|
+
"connection",
|
|
81
|
+
"audio",
|
|
82
|
+
"internal",
|
|
83
|
+
])("accepts valid code: %s", (code) => {
|
|
84
|
+
expect(SessionErrorCodeSchema.safeParse(code).success).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("rejects invalid code", () => {
|
|
88
|
+
expect(SessionErrorCodeSchema.safeParse("not_a_real_code").success).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("ClientEventSchema", () => {
|
|
93
|
+
test("accepts speech_started", () => {
|
|
94
|
+
const result = ClientEventSchema.safeParse({ type: "speech_started" });
|
|
95
|
+
expect(result.success).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("accepts user_transcript", () => {
|
|
99
|
+
const result = ClientEventSchema.safeParse({
|
|
100
|
+
type: "user_transcript",
|
|
101
|
+
text: "hello world",
|
|
102
|
+
});
|
|
103
|
+
expect(result.success).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("accepts error event", () => {
|
|
107
|
+
const result = ClientEventSchema.safeParse({
|
|
108
|
+
type: "error",
|
|
109
|
+
code: "internal",
|
|
110
|
+
message: "something went wrong",
|
|
111
|
+
});
|
|
112
|
+
expect(result.success).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("rejects unknown type", () => {
|
|
116
|
+
const result = ClientEventSchema.safeParse({
|
|
117
|
+
type: "unknown_event_type",
|
|
118
|
+
});
|
|
119
|
+
expect(result.success).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("ClientMessageSchema", () => {
|
|
124
|
+
test("accepts audio_ready", () => {
|
|
125
|
+
const result = ClientMessageSchema.safeParse({ type: "audio_ready" });
|
|
126
|
+
expect(result.success).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("accepts cancel", () => {
|
|
130
|
+
const result = ClientMessageSchema.safeParse({ type: "cancel" });
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("accepts reset", () => {
|
|
135
|
+
const result = ClientMessageSchema.safeParse({ type: "reset" });
|
|
136
|
+
expect(result.success).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("accepts history with messages", () => {
|
|
140
|
+
const result = ClientMessageSchema.safeParse({
|
|
141
|
+
type: "history",
|
|
142
|
+
messages: [{ role: "user", content: "hello" }],
|
|
143
|
+
});
|
|
144
|
+
expect(result.success).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("rejects unknown type", () => {
|
|
148
|
+
const result = ClientMessageSchema.safeParse({
|
|
149
|
+
type: "unknown_message_type",
|
|
150
|
+
});
|
|
151
|
+
expect(result.success).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("buildReadyConfig", () => {
|
|
156
|
+
test("builds config from sample rates", () => {
|
|
157
|
+
const config = buildReadyConfig({ inputSampleRate: 16_000, outputSampleRate: 24_000 });
|
|
158
|
+
expect(config).toEqual({
|
|
159
|
+
audioFormat: AUDIO_FORMAT,
|
|
160
|
+
sampleRate: 16_000,
|
|
161
|
+
ttsSampleRate: 24_000,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("uses custom sample rates", () => {
|
|
166
|
+
const config = buildReadyConfig({ inputSampleRate: 8000, outputSampleRate: 48_000 });
|
|
167
|
+
expect(config.sampleRate).toBe(8000);
|
|
168
|
+
expect(config.ttsSampleRate).toBe(48_000);
|
|
169
|
+
});
|
|
170
|
+
});
|
package/sdk/protocol.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket wire-format types shared by server and client.
|
|
4
|
+
*
|
|
5
|
+
* Note: this module is for internal use only and should not be used directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { MAX_TOOL_RESULT_CHARS } from "./constants.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Audio codec identifier used in the wire protocol.
|
|
14
|
+
*
|
|
15
|
+
* All audio frames are 16-bit signed PCM, little-endian, mono.
|
|
16
|
+
*/
|
|
17
|
+
export const AUDIO_FORMAT = "pcm16";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Minimal envelope schema for two-phase message parsing.
|
|
21
|
+
*
|
|
22
|
+
* When a strict schema (ServerMessageSchema / ClientMessageSchema) rejects a
|
|
23
|
+
* message, this schema determines whether the message is a valid but
|
|
24
|
+
* *unrecognised* type (safe to ignore during rolling upgrades) or genuinely
|
|
25
|
+
* malformed (should be warned about).
|
|
26
|
+
*/
|
|
27
|
+
export const MessageEnvelopeSchema = z.object({ type: z.string() }).passthrough();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Two-phase message parse: tries the strict schema first, then falls back to
|
|
31
|
+
* the envelope to distinguish unknown-but-valid types (safe to ignore during
|
|
32
|
+
* rolling upgrades) from genuinely malformed messages.
|
|
33
|
+
*
|
|
34
|
+
* Return value when `ok: false`:
|
|
35
|
+
* - `malformed: true` — message doesn't even have a `{ type: string }` shape;
|
|
36
|
+
* likely corrupt data, should warn
|
|
37
|
+
* - `malformed: false` — has a valid `type` field but the type is unrecognised;
|
|
38
|
+
* safe to ignore (e.g. new message type from a newer server version)
|
|
39
|
+
*/
|
|
40
|
+
export function lenientParse<T>(
|
|
41
|
+
schema: z.ZodType<T>,
|
|
42
|
+
json: unknown,
|
|
43
|
+
): { ok: true; data: T } | { ok: false; malformed: boolean; error: string } {
|
|
44
|
+
const result = schema.safeParse(json);
|
|
45
|
+
if (result.success) return { ok: true, data: result.data };
|
|
46
|
+
const malformed = !MessageEnvelopeSchema.safeParse(json).success;
|
|
47
|
+
return { ok: false, malformed, error: result.error.message };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Zod schema for the KV "get" operation. */
|
|
51
|
+
export const KvGetSchema = z.object({ op: z.literal("get"), key: z.string().min(1) });
|
|
52
|
+
|
|
53
|
+
/** Zod schema for the KV "set" operation. */
|
|
54
|
+
export const KvSetSchema = z.object({
|
|
55
|
+
op: z.literal("set"),
|
|
56
|
+
key: z.string().min(1),
|
|
57
|
+
value: z.unknown(),
|
|
58
|
+
/** Time-to-live in **milliseconds**. */
|
|
59
|
+
expireIn: z.number().int().positive().optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Zod schema for the KV "del" operation. */
|
|
63
|
+
export const KvDelSchema = z.object({ op: z.literal("del"), key: z.string().min(1) });
|
|
64
|
+
|
|
65
|
+
/** Zod schema for KV operation requests from the worker to the host. */
|
|
66
|
+
export const KvRequestSchema = z.discriminatedUnion("op", [KvGetSchema, KvSetSchema, KvDelSchema]);
|
|
67
|
+
|
|
68
|
+
/** KV operation request — discriminated union on the `op` field. */
|
|
69
|
+
export type KvRequest = z.infer<typeof KvRequestSchema>;
|
|
70
|
+
|
|
71
|
+
// ─── Error codes ───────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Zod schema for session error codes.
|
|
75
|
+
* @public
|
|
76
|
+
*/
|
|
77
|
+
export const SessionErrorCodeSchema = z.enum([
|
|
78
|
+
"stt",
|
|
79
|
+
"llm",
|
|
80
|
+
"tts",
|
|
81
|
+
"tool",
|
|
82
|
+
"protocol",
|
|
83
|
+
"connection",
|
|
84
|
+
"audio",
|
|
85
|
+
"internal",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Error codes for categorizing session errors on the wire.
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
export type SessionErrorCode = z.infer<typeof SessionErrorCodeSchema>;
|
|
94
|
+
|
|
95
|
+
// ─── Client events ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/** Helper: simple event with only a type field. */
|
|
98
|
+
const ev = <T extends string>(t: T) => z.object({ type: z.literal(t) });
|
|
99
|
+
|
|
100
|
+
const turnOrder = z.number().int().nonnegative().optional();
|
|
101
|
+
|
|
102
|
+
/** Zod schema for {@link ClientEvent}. */
|
|
103
|
+
export const ClientEventSchema = z.discriminatedUnion("type", [
|
|
104
|
+
ev("speech_started"),
|
|
105
|
+
ev("speech_stopped"),
|
|
106
|
+
z.object({
|
|
107
|
+
type: z.literal("user_transcript"),
|
|
108
|
+
text: z.string(),
|
|
109
|
+
turnOrder,
|
|
110
|
+
}),
|
|
111
|
+
z.object({
|
|
112
|
+
type: z.literal("agent_transcript"),
|
|
113
|
+
text: z.string(),
|
|
114
|
+
}),
|
|
115
|
+
z.object({
|
|
116
|
+
type: z.literal("tool_call"),
|
|
117
|
+
toolCallId: z.string(),
|
|
118
|
+
toolName: z.string(),
|
|
119
|
+
args: z.record(z.string(), z.unknown()),
|
|
120
|
+
}),
|
|
121
|
+
z.object({
|
|
122
|
+
type: z.literal("tool_call_done"),
|
|
123
|
+
toolCallId: z.string(),
|
|
124
|
+
result: z.string().max(MAX_TOOL_RESULT_CHARS),
|
|
125
|
+
}),
|
|
126
|
+
ev("reply_done"),
|
|
127
|
+
ev("cancelled"),
|
|
128
|
+
ev("reset"),
|
|
129
|
+
ev("idle_timeout"),
|
|
130
|
+
z.object({ type: z.literal("error"), code: SessionErrorCodeSchema, message: z.string() }),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
/** Discriminated union of all server→client session events. */
|
|
134
|
+
export type ClientEvent = z.infer<typeof ClientEventSchema>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Typed interface for pushing session events to a connected client.
|
|
138
|
+
*
|
|
139
|
+
* For WebSocket sessions this sends JSON text frames and binary audio frames.
|
|
140
|
+
*/
|
|
141
|
+
export interface ClientSink {
|
|
142
|
+
/** Whether the underlying connection is open and accepting calls. */
|
|
143
|
+
readonly open: boolean;
|
|
144
|
+
/** Push a session event to the client. */
|
|
145
|
+
event(e: ClientEvent): void;
|
|
146
|
+
/** Send a single TTS audio chunk to the client. */
|
|
147
|
+
playAudioChunk(chunk: Uint8Array): void;
|
|
148
|
+
/** Signal that TTS audio is complete. */
|
|
149
|
+
playAudioDone(): void;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── WebSocket message types ────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/** Supported audio formats for the wire protocol. */
|
|
155
|
+
export type AudioFormatId = "pcm16";
|
|
156
|
+
|
|
157
|
+
/** Zod schema for {@link ReadyConfig}. */
|
|
158
|
+
export const ReadyConfigSchema = z.object({
|
|
159
|
+
audioFormat: z.enum(["pcm16"]),
|
|
160
|
+
sampleRate: z.number().int().positive(),
|
|
161
|
+
ttsSampleRate: z.number().int().positive(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** Protocol-level session config returned to the client on connect. */
|
|
165
|
+
export type ReadyConfig = z.infer<typeof ReadyConfigSchema>;
|
|
166
|
+
|
|
167
|
+
/** Zod schema for server→client text messages. */
|
|
168
|
+
export const ServerMessageSchema = z.discriminatedUnion("type", [
|
|
169
|
+
z.object({
|
|
170
|
+
type: z.literal("config"),
|
|
171
|
+
audioFormat: z.string(),
|
|
172
|
+
sampleRate: z.number(),
|
|
173
|
+
ttsSampleRate: z.number(),
|
|
174
|
+
/** Session ID for this connection. Clients can reconnect with
|
|
175
|
+
* `?sessionId=<id>` to resume a persisted session. */
|
|
176
|
+
sessionId: z.string().optional(),
|
|
177
|
+
}),
|
|
178
|
+
ev("audio_done"),
|
|
179
|
+
...ClientEventSchema.options,
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
/** Server→client text messages (binary frames carry raw PCM16 audio). */
|
|
183
|
+
export type ServerMessage = z.infer<typeof ServerMessageSchema>;
|
|
184
|
+
|
|
185
|
+
/** Zod schema for client→server text messages. */
|
|
186
|
+
export const ClientMessageSchema = z.discriminatedUnion("type", [
|
|
187
|
+
ev("audio_ready"),
|
|
188
|
+
ev("cancel"),
|
|
189
|
+
ev("reset"),
|
|
190
|
+
z.object({
|
|
191
|
+
type: z.literal("history"),
|
|
192
|
+
messages: z
|
|
193
|
+
.array(z.object({ role: z.enum(["user", "assistant"]), content: z.string().max(100_000) }))
|
|
194
|
+
.max(200),
|
|
195
|
+
}),
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
/** Client→server text messages (binary frames carry raw PCM16 audio). */
|
|
199
|
+
export type ClientMessage = z.infer<typeof ClientMessageSchema>;
|
|
200
|
+
|
|
201
|
+
// ─── Ready config builder ───────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/** Build the protocol-level session config from S2S sample rates. */
|
|
204
|
+
export function buildReadyConfig(s2sConfig: {
|
|
205
|
+
inputSampleRate: number;
|
|
206
|
+
outputSampleRate: number;
|
|
207
|
+
}): ReadyConfig {
|
|
208
|
+
return {
|
|
209
|
+
audioFormat: AUDIO_FORMAT,
|
|
210
|
+
sampleRate: s2sConfig.inputSampleRate,
|
|
211
|
+
ttsSampleRate: s2sConfig.outputSampleRate,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─── Worker RPC interfaces ─────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/** Zod schema for {@link TurnConfig}. */
|
|
218
|
+
export const TurnConfigSchema = z.object({
|
|
219
|
+
maxSteps: z.number().int().positive().optional(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
/** Combined turn configuration resolved from the worker before a turn starts. */
|
|
223
|
+
export type TurnConfig = z.infer<typeof TurnConfigSchema>;
|