@alexkroman1/aai 1.7.1 → 1.8.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.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -0
- package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
- package/dist/constants-y68COEGj.js +29 -0
- package/dist/host/_base64.d.ts +2 -0
- package/dist/host/_mock-ws.d.ts +0 -61
- package/dist/host/_pipeline-test-fakes.d.ts +7 -4
- package/dist/host/_run-code.d.ts +0 -25
- package/dist/host/_runtime-conformance.d.ts +3 -34
- package/dist/host/memory-vector.d.ts +0 -11
- package/dist/host/providers/resolve-kv.d.ts +0 -7
- package/dist/host/providers/resolve-vector.d.ts +0 -8
- package/dist/host/providers/stt/assemblyai.d.ts +0 -14
- package/dist/host/providers/stt/deepgram.d.ts +2 -14
- package/dist/host/providers/stt/soniox.d.ts +0 -22
- package/dist/host/providers/tts/rime.d.ts +10 -31
- package/dist/host/runtime-barrel.js +619 -630
- package/dist/host/runtime-config.d.ts +9 -6
- package/dist/host/runtime.d.ts +3 -0
- package/dist/host/to-vercel-tools.d.ts +3 -33
- package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
- package/dist/host/unstorage-kv.d.ts +0 -26
- package/dist/index.js +3 -3
- package/dist/openai-realtime-cjPAHMMx.js +10 -0
- package/dist/sdk/_internal-types.d.ts +6 -55
- package/dist/sdk/allowed-hosts.d.ts +4 -3
- package/dist/sdk/constants.d.ts +4 -29
- package/dist/sdk/define.d.ts +7 -4
- package/dist/sdk/kv.d.ts +13 -37
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/manifest.d.ts +8 -2
- package/dist/sdk/protocol.js +1 -1
- package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
- package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
- package/dist/sdk/providers/s2s-barrel.js +2 -0
- package/dist/sdk/providers/tts/rime.d.ts +1 -1
- package/dist/sdk/providers.d.ts +6 -2
- package/dist/sdk/types.d.ts +7 -1
- package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
- package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
- package/host/_base64.ts +9 -0
- package/host/_mock-ws.ts +0 -65
- package/host/_pipeline-test-fakes.ts +19 -31
- package/host/_run-code.ts +10 -53
- package/host/_runtime-conformance.ts +3 -44
- package/host/_test-utils.ts +20 -42
- package/host/builtin-tools.test.ts +127 -222
- package/host/builtin-tools.ts +6 -10
- package/host/cleanup.test.ts +30 -73
- package/host/integration/pipeline-reference.integration.test.ts +12 -17
- package/host/integration.test.ts +0 -7
- package/host/memory-vector.test.ts +3 -1
- package/host/memory-vector.ts +16 -21
- package/host/pinecone-vector.test.ts +14 -17
- package/host/pinecone-vector.ts +10 -19
- package/host/providers/providers.test-d.ts +5 -3
- package/host/providers/resolve-kv.ts +23 -41
- package/host/providers/resolve-vector.ts +3 -12
- package/host/providers/resolve.test.ts +15 -28
- package/host/providers/resolve.ts +24 -24
- package/host/providers/stt/assemblyai.test.ts +2 -14
- package/host/providers/stt/assemblyai.ts +12 -35
- package/host/providers/stt/deepgram.test.ts +23 -83
- package/host/providers/stt/deepgram.ts +15 -40
- package/host/providers/stt/elevenlabs.test.ts +26 -38
- package/host/providers/stt/elevenlabs.ts +10 -9
- package/host/providers/stt/soniox.test.ts +35 -85
- package/host/providers/stt/soniox.ts +8 -53
- package/host/providers/tts/cartesia.test.ts +19 -58
- package/host/providers/tts/cartesia.ts +36 -66
- package/host/providers/tts/rime.test.ts +12 -38
- package/host/providers/tts/rime.ts +23 -86
- package/host/runtime-config.test.ts +9 -9
- package/host/runtime-config.ts +16 -22
- package/host/runtime.test.ts +111 -73
- package/host/runtime.ts +138 -86
- package/host/s2s.test.ts +92 -191
- package/host/s2s.ts +55 -49
- package/host/server-shutdown.test.ts +9 -30
- package/host/server.test.ts +2 -13
- package/host/server.ts +85 -100
- package/host/session-core.test.ts +15 -30
- package/host/session-core.ts +10 -13
- package/host/session-prompt.test.ts +1 -5
- package/host/to-vercel-tools.test.ts +53 -72
- package/host/to-vercel-tools.ts +9 -39
- package/host/tool-executor.test.ts +25 -51
- package/host/tool-executor.ts +18 -12
- package/host/transports/openai-realtime-transport.test.ts +371 -0
- package/host/transports/openai-realtime-transport.ts +319 -0
- package/host/transports/pipeline-transport.test.ts +125 -298
- package/host/transports/pipeline-transport.ts +20 -68
- package/host/transports/s2s-transport-fixtures.test.ts +31 -92
- package/host/transports/s2s-transport.test.ts +65 -134
- package/host/transports/s2s-transport.ts +15 -43
- package/host/transports/types.test.ts +4 -8
- package/host/unstorage-kv.test.ts +3 -2
- package/host/unstorage-kv.ts +5 -35
- package/host/ws-handler.test.ts +72 -176
- package/host/ws-handler.ts +6 -12
- package/package.json +6 -1
- package/sdk/__snapshots__/exports.test.ts.snap +7 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.test.ts +6 -9
- package/sdk/_internal-types.ts +16 -57
- package/sdk/_test-matchers.ts +25 -15
- package/sdk/allowed-hosts.test.ts +50 -114
- package/sdk/allowed-hosts.ts +8 -14
- package/sdk/constants.ts +5 -52
- package/sdk/define.test.ts +7 -6
- package/sdk/define.ts +7 -3
- package/sdk/exports.test.ts +6 -1
- package/sdk/kv.ts +13 -37
- package/sdk/manifest.test-d.ts +5 -0
- package/sdk/manifest.test.ts +61 -9
- package/sdk/manifest.ts +11 -11
- package/sdk/protocol-compat.test.ts +66 -98
- package/sdk/protocol-snapshot.test.ts +2 -16
- package/sdk/protocol.test.ts +13 -22
- package/sdk/providers/s2s/openai-realtime.ts +36 -0
- package/sdk/providers/s2s-barrel.ts +12 -0
- package/sdk/providers/tts/rime.ts +1 -1
- package/sdk/providers.ts +24 -5
- package/sdk/schema-alignment.test.ts +25 -73
- package/sdk/schema-shapes.test.ts +1 -29
- package/sdk/system-prompt.test.ts +0 -1
- package/sdk/system-prompt.ts +17 -19
- package/sdk/types-inference.test.ts +10 -36
- package/sdk/types.ts +7 -0
- package/sdk/ws-upgrade.test.ts +24 -23
- package/sdk/ws-upgrade.ts +2 -3
- package/tsdown.config.ts +8 -11
- package/dist/constants-C2nirZUI.js +0 -54
package/sdk/define.test.ts
CHANGED
|
@@ -59,21 +59,22 @@ describe("agent()", () => {
|
|
|
59
59
|
expect(def.builtinTools).toEqual(["web_search"]);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
function pipelineAgent() {
|
|
63
63
|
const stt = assemblyAI({ model: "u3pro-rt" });
|
|
64
64
|
const tts = cartesia({ voice: "v" });
|
|
65
65
|
const llm = anthropic({ model: "claude-haiku-4-5" });
|
|
66
|
-
|
|
66
|
+
return { stt, llm, tts, def: agent({ name: "t", systemPrompt: "p", stt, llm, tts }) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("preserves stt/llm/tts providers on the returned def", () => {
|
|
70
|
+
const { stt, llm, tts, def } = pipelineAgent();
|
|
67
71
|
expect(def.stt).toBe(stt);
|
|
68
72
|
expect(def.llm).toBe(llm);
|
|
69
73
|
expect(def.tts).toBe(tts);
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
test("stt/llm/tts flow through parseManifest to mode 'pipeline'", () => {
|
|
73
|
-
const stt
|
|
74
|
-
const tts = cartesia({ voice: "v" });
|
|
75
|
-
const llm = anthropic({ model: "claude-haiku-4-5" });
|
|
76
|
-
const def = agent({ name: "t", systemPrompt: "p", stt, llm, tts });
|
|
77
|
+
const { stt, llm, tts, def } = pipelineAgent();
|
|
77
78
|
const parsed = parseManifest(def);
|
|
78
79
|
expect(parsed.mode).toBe("pipeline");
|
|
79
80
|
expect(parsed.stt).toStrictEqual(stt);
|
package/sdk/define.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
-
/**
|
|
3
|
-
* Helper functions for defining agents and tools with full type inference.
|
|
4
|
-
*/
|
|
5
2
|
|
|
6
3
|
import type { z } from "zod";
|
|
7
4
|
import type {
|
|
8
5
|
KvProvider,
|
|
9
6
|
LlmProvider,
|
|
7
|
+
S2sProvider,
|
|
10
8
|
SttProvider,
|
|
11
9
|
TtsProvider,
|
|
12
10
|
VectorProvider,
|
|
@@ -104,6 +102,12 @@ export function agent(def: {
|
|
|
104
102
|
* enable pipeline mode.
|
|
105
103
|
*/
|
|
106
104
|
tts?: TtsProvider;
|
|
105
|
+
/**
|
|
106
|
+
* Pluggable S2S provider descriptor. When set, overrides the implicit
|
|
107
|
+
* AssemblyAI default. Mutually exclusive with the `stt`/`llm`/`tts`
|
|
108
|
+
* pipeline triple.
|
|
109
|
+
*/
|
|
110
|
+
s2s?: S2sProvider;
|
|
107
111
|
/** Pluggable KV backend. Falls back to platform default when omitted. */
|
|
108
112
|
kv?: KvProvider;
|
|
109
113
|
/** Pluggable Vector backend. Falls back to platform default when omitted. */
|
package/sdk/exports.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
2
|
/**
|
|
3
|
-
* Export surface snapshot tests for all
|
|
3
|
+
* Export surface snapshot tests for all five aai subpath exports.
|
|
4
4
|
*
|
|
5
5
|
* These tests catch accidental export additions or removals. If a snapshot
|
|
6
6
|
* breaks, it signals a potentially breaking API change that should be
|
|
@@ -28,4 +28,9 @@ describe("export surface stability", () => {
|
|
|
28
28
|
const mod = await import("@alexkroman1/aai/runtime");
|
|
29
29
|
expect(Object.keys(mod).sort()).toMatchSnapshot();
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
test("@alexkroman1/aai/s2s export", async () => {
|
|
33
|
+
const mod = await import("@alexkroman1/aai/s2s");
|
|
34
|
+
expect(Object.keys(mod).sort()).toMatchSnapshot();
|
|
35
|
+
});
|
|
31
36
|
});
|
package/sdk/kv.ts
CHANGED
|
@@ -1,60 +1,36 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
-
/**
|
|
3
|
-
* Key-value storage interface and shared utilities.
|
|
4
|
-
*/
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
4
|
* Async key-value store interface used by agents.
|
|
8
5
|
*
|
|
9
|
-
* Agents access the KV store via `ToolContext.kv`. Values are JSON-serialized
|
|
10
|
-
* strings with an optional TTL.
|
|
6
|
+
* Agents access the KV store via `ToolContext.kv`. Values are JSON-serialized
|
|
7
|
+
* and stored as strings with an optional TTL.
|
|
11
8
|
*
|
|
12
9
|
* @example
|
|
13
10
|
* ```ts
|
|
14
|
-
*
|
|
15
|
-
* const
|
|
16
|
-
* description: "Save and retrieve data",
|
|
17
|
-
* execute: async (_args: unknown, ctx: { kv: Kv }) => {
|
|
18
|
-
* await ctx.kv.set("user:name", "Alice", { expireIn: 60_000 });
|
|
19
|
-
* const name = await ctx.kv.get<string>("user:name");
|
|
20
|
-
* return name; // "Alice"
|
|
21
|
-
* },
|
|
22
|
-
* };
|
|
11
|
+
* await ctx.kv.set("user:name", "Alice", { expireIn: 60_000 });
|
|
12
|
+
* const name = await ctx.kv.get<string>("user:name"); // "Alice"
|
|
23
13
|
* ```
|
|
24
14
|
*
|
|
25
15
|
* @public
|
|
26
16
|
*/
|
|
27
17
|
export type Kv = {
|
|
28
|
-
/**
|
|
29
|
-
* Get a value by key, or `null` if not found.
|
|
30
|
-
*
|
|
31
|
-
* @typeParam T - The expected type of the stored value.
|
|
32
|
-
* @param key - The key to look up.
|
|
33
|
-
* @returns The deserialized value, or `null` if the key does not exist
|
|
34
|
-
* or has expired.
|
|
35
|
-
*/
|
|
18
|
+
/** Get a value by key. Returns `null` if missing or expired. */
|
|
36
19
|
get<T = unknown>(key: string): Promise<T | null>;
|
|
37
20
|
/**
|
|
38
|
-
* Set a value, optionally with a TTL
|
|
21
|
+
* Set a value, optionally with a TTL.
|
|
39
22
|
*
|
|
40
|
-
* @param
|
|
41
|
-
*
|
|
42
|
-
* @
|
|
43
|
-
* (e.g. `60_000` for 1 minute). The entry is automatically removed after this duration.
|
|
44
|
-
* @throws Throws an Error if the serialized value exceeds 65,536 bytes.
|
|
23
|
+
* @param options.expireIn - Time-to-live in **milliseconds** (e.g. `60_000`
|
|
24
|
+
* for 1 minute). The entry is automatically removed after this duration.
|
|
25
|
+
* @throws If the serialized value exceeds 65,536 bytes.
|
|
45
26
|
*/
|
|
46
27
|
set(key: string, value: unknown, options?: { expireIn?: number }): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* Delete one or more keys.
|
|
49
|
-
*
|
|
50
|
-
* @param keys - A single key or array of keys to delete. No-op for keys that do not exist.
|
|
51
|
-
*/
|
|
28
|
+
/** Delete one or more keys. No-op for keys that do not exist. */
|
|
52
29
|
delete(keys: string | string[]): Promise<void>;
|
|
53
30
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* for implementations that hold no resources (e.g. in-memory stores).
|
|
31
|
+
* Release any held resources (intervals, database handles). After calling,
|
|
32
|
+
* the store must not be used. No-op for stateless implementations (e.g.
|
|
33
|
+
* in-memory stores).
|
|
58
34
|
*/
|
|
59
35
|
close?(): void;
|
|
60
36
|
};
|
package/sdk/manifest.test-d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
2
|
import { expectTypeOf, test } from "vitest";
|
|
3
3
|
import type { Manifest } from "./manifest.ts";
|
|
4
|
+
import type { S2sProvider } from "./providers.ts";
|
|
4
5
|
|
|
5
6
|
test("Manifest.stt/llm/tts are optional", () => {
|
|
6
7
|
expectTypeOf<Manifest["stt"]>().toBeNullable();
|
|
@@ -8,6 +9,10 @@ test("Manifest.stt/llm/tts are optional", () => {
|
|
|
8
9
|
expectTypeOf<Manifest["tts"]>().toBeNullable();
|
|
9
10
|
});
|
|
10
11
|
|
|
12
|
+
test("Manifest.s2s is optional and typed as S2sProvider", () => {
|
|
13
|
+
expectTypeOf<Manifest["s2s"]>().toEqualTypeOf<S2sProvider | undefined>();
|
|
14
|
+
});
|
|
15
|
+
|
|
11
16
|
test("parseManifest return includes mode", () => {
|
|
12
17
|
type Parsed = ReturnType<typeof import("./manifest.ts").parseManifest>;
|
|
13
18
|
expectTypeOf<Parsed["mode"]>().toEqualTypeOf<"s2s" | "pipeline">();
|
package/sdk/manifest.test.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { anthropic } from "./providers/llm/anthropic.ts";
|
|
|
9
9
|
import { assemblyAI } from "./providers/stt/assemblyai.ts";
|
|
10
10
|
import { cartesia } from "./providers/tts/cartesia.ts";
|
|
11
11
|
import { pinecone } from "./providers/vector/pinecone.ts";
|
|
12
|
+
import { assertProviderTriple } from "./providers.ts";
|
|
12
13
|
|
|
13
14
|
describe("parseManifest", () => {
|
|
14
15
|
test("minimal manifest requires only name", () => {
|
|
@@ -102,15 +103,16 @@ describe("parseManifest", () => {
|
|
|
102
103
|
});
|
|
103
104
|
});
|
|
104
105
|
|
|
105
|
-
// ── Property-based tests ─────────────────────────────────────────────────
|
|
106
|
-
|
|
107
106
|
describe("property: parseManifest", () => {
|
|
107
|
+
const optString = fc.option(fc.string(), { nil: undefined });
|
|
108
|
+
const optMaxSteps = fc.option(fc.integer({ min: 1, max: 100 }), { nil: undefined });
|
|
109
|
+
|
|
108
110
|
test("valid manifests always parse", () => {
|
|
109
111
|
const validManifestArb = fc.record({
|
|
110
112
|
name: fc.string({ minLength: 1 }),
|
|
111
|
-
systemPrompt:
|
|
112
|
-
greeting:
|
|
113
|
-
maxSteps:
|
|
113
|
+
systemPrompt: optString,
|
|
114
|
+
greeting: optString,
|
|
115
|
+
maxSteps: optMaxSteps,
|
|
114
116
|
toolChoice: fc.option(fc.constantFrom("auto" as const, "required" as const), {
|
|
115
117
|
nil: undefined,
|
|
116
118
|
}),
|
|
@@ -127,11 +129,10 @@ describe("property: parseManifest", () => {
|
|
|
127
129
|
});
|
|
128
130
|
|
|
129
131
|
test("missing name throws", () => {
|
|
130
|
-
// Generate objects that never have a `name` field
|
|
131
132
|
const noNameArb = fc.record({
|
|
132
|
-
systemPrompt:
|
|
133
|
-
greeting:
|
|
134
|
-
maxSteps:
|
|
133
|
+
systemPrompt: optString,
|
|
134
|
+
greeting: optString,
|
|
135
|
+
maxSteps: optMaxSteps,
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
fc.assert(
|
|
@@ -229,3 +230,54 @@ describe("parseManifest — mode classification", () => {
|
|
|
229
230
|
).toThrow(/stt, llm, and tts must be set together/);
|
|
230
231
|
});
|
|
231
232
|
});
|
|
233
|
+
|
|
234
|
+
describe("parseManifest s2s", () => {
|
|
235
|
+
test("parseManifest accepts s2s descriptor", () => {
|
|
236
|
+
const m = parseManifest({
|
|
237
|
+
name: "x",
|
|
238
|
+
s2s: { kind: "openai-realtime", options: { model: "gpt-realtime" } },
|
|
239
|
+
});
|
|
240
|
+
expect(m.s2s).toEqual({
|
|
241
|
+
kind: "openai-realtime",
|
|
242
|
+
options: { model: "gpt-realtime" },
|
|
243
|
+
});
|
|
244
|
+
expect(m.mode).toBe("s2s");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("parseManifest rejects s2s combined with pipeline triple", () => {
|
|
248
|
+
expect(() =>
|
|
249
|
+
parseManifest({
|
|
250
|
+
name: "x",
|
|
251
|
+
s2s: { kind: "openai-realtime", options: {} },
|
|
252
|
+
stt: { kind: "assemblyai", options: {} },
|
|
253
|
+
llm: { kind: "openai", options: {} },
|
|
254
|
+
tts: { kind: "cartesia", options: {} },
|
|
255
|
+
}),
|
|
256
|
+
).toThrow(/s2s.*pipeline|cannot.*together/i);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("assertProviderTriple with s2s", () => {
|
|
261
|
+
test("returns 's2s' when s2s descriptor is set and pipeline triple is empty", () => {
|
|
262
|
+
const s2s = { kind: "openai-realtime", options: {} };
|
|
263
|
+
expect(assertProviderTriple(undefined, undefined, undefined, s2s)).toBe("s2s");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("returns 's2s' when nothing is set (default fallback)", () => {
|
|
267
|
+
expect(assertProviderTriple(undefined, undefined, undefined, undefined)).toBe("s2s");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("returns 'pipeline' when triple is set and s2s is not", () => {
|
|
271
|
+
const stt = { kind: "x", options: {} };
|
|
272
|
+
expect(assertProviderTriple(stt, stt, stt, undefined)).toBe("pipeline");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("rejects setting s2s alongside any pipeline field", () => {
|
|
276
|
+
const d = { kind: "x", options: {} };
|
|
277
|
+
expect(() => assertProviderTriple(d, undefined, undefined, d)).toThrow(
|
|
278
|
+
/s2s.*pipeline|cannot.*together/i,
|
|
279
|
+
);
|
|
280
|
+
expect(() => assertProviderTriple(undefined, d, undefined, d)).toThrow();
|
|
281
|
+
expect(() => assertProviderTriple(undefined, undefined, d, d)).toThrow();
|
|
282
|
+
});
|
|
283
|
+
});
|
package/sdk/manifest.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
assertProviderTriple,
|
|
13
13
|
type KvProvider,
|
|
14
14
|
type LlmProvider,
|
|
15
|
+
type S2sProvider,
|
|
15
16
|
type SessionMode,
|
|
16
17
|
type SttProvider,
|
|
17
18
|
type TtsProvider,
|
|
@@ -70,13 +71,19 @@ export type Manifest = {
|
|
|
70
71
|
* enable pipeline mode.
|
|
71
72
|
*/
|
|
72
73
|
tts?: TtsProvider | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Pluggable S2S provider descriptor. When set, overrides the implicit
|
|
76
|
+
* AssemblyAI default. Mutually exclusive with the `stt`/`llm`/`tts`
|
|
77
|
+
* pipeline triple.
|
|
78
|
+
*/
|
|
79
|
+
s2s?: S2sProvider | undefined;
|
|
73
80
|
/** Pluggable KV backend descriptor. Falls back to platform default when omitted. */
|
|
74
81
|
kv?: KvProvider | undefined;
|
|
75
82
|
/** Pluggable Vector backend descriptor. Falls back to platform default when omitted. */
|
|
76
83
|
vector?: VectorProvider | undefined;
|
|
77
84
|
/**
|
|
78
85
|
* Session mode derived from provider fields:
|
|
79
|
-
* - `"s2s"
|
|
86
|
+
* - `"s2s"`: speech-to-speech path (default when no stt/llm/tts set, or when `s2s` is set).
|
|
80
87
|
* - `"pipeline"`: pluggable STT → LLM → TTS path (stt + llm + tts all set).
|
|
81
88
|
*/
|
|
82
89
|
mode: SessionMode;
|
|
@@ -127,6 +134,7 @@ const ManifestSchema = z.object({
|
|
|
127
134
|
stt: ProviderDescriptorSchema.optional(),
|
|
128
135
|
llm: ProviderDescriptorSchema.optional(),
|
|
129
136
|
tts: ProviderDescriptorSchema.optional(),
|
|
137
|
+
s2s: ProviderDescriptorSchema.optional(),
|
|
130
138
|
kv: ProviderDescriptorSchema.optional(),
|
|
131
139
|
vector: ProviderDescriptorSchema.optional(),
|
|
132
140
|
});
|
|
@@ -142,24 +150,16 @@ const ManifestSchema = z.object({
|
|
|
142
150
|
*/
|
|
143
151
|
export function parseManifest(input: unknown): Manifest {
|
|
144
152
|
const parsed = ManifestSchema.parse(input);
|
|
145
|
-
const mode = assertProviderTriple(parsed.stt, parsed.llm, parsed.tts);
|
|
153
|
+
const mode = assertProviderTriple(parsed.stt, parsed.llm, parsed.tts, parsed.s2s);
|
|
146
154
|
return {
|
|
147
|
-
|
|
155
|
+
...parsed,
|
|
148
156
|
systemPrompt: parsed.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
|
|
149
157
|
greeting: parsed.greeting ?? DEFAULT_GREETING,
|
|
150
|
-
sttPrompt: parsed.sttPrompt,
|
|
151
158
|
builtinTools: parsed.builtinTools ?? [],
|
|
152
159
|
maxSteps: parsed.maxSteps ?? 5,
|
|
153
160
|
toolChoice: parsed.toolChoice ?? "auto",
|
|
154
|
-
idleTimeoutMs: parsed.idleTimeoutMs,
|
|
155
|
-
theme: parsed.theme,
|
|
156
161
|
tools: parsed.tools ?? {},
|
|
157
162
|
allowedHosts: parsed.allowedHosts ?? [],
|
|
158
|
-
stt: parsed.stt,
|
|
159
|
-
llm: parsed.llm,
|
|
160
|
-
tts: parsed.tts,
|
|
161
|
-
kv: parsed.kv,
|
|
162
|
-
vector: parsed.vector,
|
|
163
163
|
mode,
|
|
164
164
|
};
|
|
165
165
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { readdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { describe, expect, test } from "vitest";
|
|
12
|
+
import type { ZodTypeAny } from "zod";
|
|
12
13
|
import {
|
|
13
14
|
DEFAULT_STT_SAMPLE_RATE,
|
|
14
15
|
DEFAULT_TTS_SAMPLE_RATE,
|
|
@@ -21,20 +22,7 @@ import {
|
|
|
21
22
|
SessionErrorCodeSchema,
|
|
22
23
|
} from "./protocol.ts";
|
|
23
24
|
|
|
24
|
-
// ── Load fixtures ─────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
25
|
const FIXTURE_DIR = join(import.meta.dirname, "compat-fixtures");
|
|
27
|
-
// Only load compat fixtures that have the expected schema-compat structure
|
|
28
|
-
// (ServerMessage, ClientMessage, KvRequest, constants). Wire-format fixtures
|
|
29
|
-
// like wire-v1.json use a different shape and are tested by wire.test.ts.
|
|
30
|
-
const fixtureFiles = readdirSync(FIXTURE_DIR)
|
|
31
|
-
.filter((f) => f.endsWith(".json"))
|
|
32
|
-
.filter((f) => {
|
|
33
|
-
const raw = readFileSync(join(FIXTURE_DIR, f), "utf-8");
|
|
34
|
-
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
35
|
-
return "ServerMessage" in parsed && "ClientMessage" in parsed;
|
|
36
|
-
})
|
|
37
|
-
.sort();
|
|
38
26
|
|
|
39
27
|
type Fixture = {
|
|
40
28
|
version: number;
|
|
@@ -53,6 +41,16 @@ function loadFixture(filename: string): Fixture {
|
|
|
53
41
|
return JSON.parse(readFileSync(join(FIXTURE_DIR, filename), "utf-8"));
|
|
54
42
|
}
|
|
55
43
|
|
|
44
|
+
// Wire-format fixtures (e.g. wire-v1.json) use a different shape and live in
|
|
45
|
+
// wire.test.ts; filter them out by checking for the schema-compat structure.
|
|
46
|
+
const fixtureFiles = readdirSync(FIXTURE_DIR)
|
|
47
|
+
.filter((f) => f.endsWith(".json"))
|
|
48
|
+
.filter((f) => {
|
|
49
|
+
const parsed = loadFixture(f) as unknown as Record<string, unknown>;
|
|
50
|
+
return "ServerMessage" in parsed && "ClientMessage" in parsed;
|
|
51
|
+
})
|
|
52
|
+
.sort();
|
|
53
|
+
|
|
56
54
|
function compatError(fixture: string, schema: string, msg: unknown, zodError: string): string {
|
|
57
55
|
return [
|
|
58
56
|
`PROTOCOL COMPATIBILITY BREAK (${fixture}, ${schema}):`,
|
|
@@ -68,104 +66,74 @@ function compatError(fixture: string, schema: string, msg: unknown, zodError: st
|
|
|
68
66
|
].join("\n");
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
function schemaAcceptsType(
|
|
77
|
-
schema: typeof ServerMessageSchema | typeof ClientMessageSchema,
|
|
78
|
-
type: string,
|
|
79
|
-
): boolean {
|
|
80
|
-
// Parse a minimal object with just the discriminant. If the discriminant
|
|
81
|
-
// is unrecognized, the error includes "invalid_union_discriminator" or
|
|
82
|
-
// similar. Any other failure (missing fields) means the variant exists.
|
|
83
|
-
const result = schema.safeParse({ type });
|
|
84
|
-
if (result.success) return true;
|
|
85
|
-
// Check error messages — the Zod issue code for discriminated union
|
|
86
|
-
// mismatches varies across versions, so we check the message text.
|
|
87
|
-
return !result.error.issues.some((i) => i.message.includes("Invalid discriminator"));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function kvSchemaAcceptsOp(op: string): boolean {
|
|
91
|
-
const result = KvRequestSchema.safeParse({ op });
|
|
69
|
+
// A minimal-discriminant parse fails either with "Invalid discriminator" (variant
|
|
70
|
+
// removed) or with missing-field errors (variant exists). The Zod issue code for
|
|
71
|
+
// discriminated-union mismatches varies across versions, so match on message text.
|
|
72
|
+
function schemaAcceptsDiscriminant(schema: ZodTypeAny, value: Record<string, unknown>): boolean {
|
|
73
|
+
const result = schema.safeParse(value);
|
|
92
74
|
if (result.success) return true;
|
|
93
75
|
return !result.error.issues.some((i) => i.message.includes("Invalid discriminator"));
|
|
94
76
|
}
|
|
95
77
|
|
|
96
|
-
|
|
78
|
+
type CompatGroup = {
|
|
79
|
+
label: string;
|
|
80
|
+
schema: ZodTypeAny;
|
|
81
|
+
messages: Record<string, unknown>[];
|
|
82
|
+
discriminant: "type" | "op";
|
|
83
|
+
};
|
|
97
84
|
|
|
98
85
|
describe.each(fixtureFiles)("compat fixture: %s", (filename) => {
|
|
99
86
|
const fixture = loadFixture(filename);
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
fixture.ServerMessage
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
88
|
+
const groups: CompatGroup[] = [
|
|
89
|
+
{
|
|
90
|
+
label: "ServerMessage",
|
|
91
|
+
schema: ServerMessageSchema,
|
|
92
|
+
messages: fixture.ServerMessage,
|
|
93
|
+
discriminant: "type",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: "ClientMessage",
|
|
97
|
+
schema: ClientMessageSchema,
|
|
98
|
+
messages: fixture.ClientMessage,
|
|
99
|
+
discriminant: "type",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
label: "KvRequest",
|
|
103
|
+
schema: KvRequestSchema,
|
|
104
|
+
messages: fixture.KvRequest,
|
|
105
|
+
discriminant: "op",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const { label, schema, messages, discriminant } of groups) {
|
|
110
|
+
describe(`${label} backward compat`, () => {
|
|
111
|
+
test.each(
|
|
112
|
+
messages.map((m, i) => [`${m[discriminant] as string}#${i}`, m]),
|
|
113
|
+
)("%s parses against current schema", (_label, msg) => {
|
|
114
|
+
const result = schema.safeParse(msg);
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
throw new Error(compatError(filename, label, msg, result.error.message));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
122
119
|
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
describe("KvRequest backward compat", () => {
|
|
126
|
-
test.each(
|
|
127
|
-
fixture.KvRequest.map((m, i) => [`${(m as { op: string }).op}#${i}`, m]),
|
|
128
|
-
)("%s parses against current schema", (_label, msg) => {
|
|
129
|
-
const result = KvRequestSchema.safeParse(msg);
|
|
130
|
-
if (!result.success) {
|
|
131
|
-
throw new Error(compatError(filename, "KvRequest", msg, result.error.message));
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ── Variant coverage: no types/ops removed ──────────────────────
|
|
120
|
+
}
|
|
137
121
|
|
|
138
122
|
describe("variant coverage", () => {
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (const t of fixtureTypes) {
|
|
152
|
-
expect(
|
|
153
|
-
schemaAcceptsType(ClientMessageSchema, t),
|
|
154
|
-
`ClientMessage variant "${t}" was removed`,
|
|
155
|
-
).toBe(true);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("no KvRequest ops removed", () => {
|
|
160
|
-
const fixtureOps = new Set(fixture.KvRequest.map((m) => (m as { op: string }).op));
|
|
161
|
-
for (const op of fixtureOps) {
|
|
162
|
-
expect(kvSchemaAcceptsOp(op), `KvRequest op "${op}" was removed`).toBe(true);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
123
|
+
for (const { label, schema, messages, discriminant } of groups) {
|
|
124
|
+
const noun = discriminant === "type" ? "variant" : "op";
|
|
125
|
+
test(`no ${label} ${noun}s removed`, () => {
|
|
126
|
+
const seen = new Set(messages.map((m) => m[discriminant] as string));
|
|
127
|
+
for (const value of seen) {
|
|
128
|
+
expect(
|
|
129
|
+
schemaAcceptsDiscriminant(schema, { [discriminant]: value }),
|
|
130
|
+
`${label} ${noun} "${value}" was removed`,
|
|
131
|
+
).toBe(true);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
165
135
|
});
|
|
166
136
|
|
|
167
|
-
// ── Constants stability ─────────────────────────────────────────
|
|
168
|
-
|
|
169
137
|
describe("constants stability", () => {
|
|
170
138
|
test("DEFAULT_STT_SAMPLE_RATE unchanged", () => {
|
|
171
139
|
expect(DEFAULT_STT_SAMPLE_RATE).toBe(fixture.constants.DEFAULT_STT_SAMPLE_RATE);
|
|
@@ -21,8 +21,6 @@ import {
|
|
|
21
21
|
VectorRequestSchema,
|
|
22
22
|
} from "./protocol.ts";
|
|
23
23
|
|
|
24
|
-
// ── Constants ────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
24
|
describe("protocol constants", () => {
|
|
27
25
|
test("sample rates", () => {
|
|
28
26
|
expect(DEFAULT_STT_SAMPLE_RATE).toMatchInlineSnapshot("16000");
|
|
@@ -49,8 +47,6 @@ describe("protocol constants", () => {
|
|
|
49
47
|
});
|
|
50
48
|
});
|
|
51
49
|
|
|
52
|
-
// ── Server → Client events (ClientEventSchema) ──────────────────────────
|
|
53
|
-
|
|
54
50
|
describe("server→client event wire format", () => {
|
|
55
51
|
const valid: [string, ClientEvent][] = [
|
|
56
52
|
["speech_started", { type: "speech_started" }],
|
|
@@ -77,8 +73,7 @@ describe("server→client event wire format", () => {
|
|
|
77
73
|
];
|
|
78
74
|
|
|
79
75
|
test.each(valid)("%s parses successfully", (_label, event) => {
|
|
80
|
-
|
|
81
|
-
expect(result.success).toBe(true);
|
|
76
|
+
expect(ClientEventSchema.safeParse(event).success).toBe(true);
|
|
82
77
|
});
|
|
83
78
|
|
|
84
79
|
test("rejects unknown event type", () => {
|
|
@@ -108,8 +103,6 @@ describe("server→client event wire format", () => {
|
|
|
108
103
|
});
|
|
109
104
|
});
|
|
110
105
|
|
|
111
|
-
// ── Client → Server messages (ClientMessageSchema) ──────────────────────
|
|
112
|
-
|
|
113
106
|
describe("client→server message wire format", () => {
|
|
114
107
|
const valid: [string, ClientMessage][] = [
|
|
115
108
|
["audio_ready", { type: "audio_ready" }],
|
|
@@ -128,8 +121,7 @@ describe("client→server message wire format", () => {
|
|
|
128
121
|
];
|
|
129
122
|
|
|
130
123
|
test.each(valid)("%s parses successfully", (_label, msg) => {
|
|
131
|
-
|
|
132
|
-
expect(result.success).toBe(true);
|
|
124
|
+
expect(ClientMessageSchema.safeParse(msg).success).toBe(true);
|
|
133
125
|
});
|
|
134
126
|
|
|
135
127
|
test("rejects unknown message type", () => {
|
|
@@ -154,8 +146,6 @@ describe("client→server message wire format", () => {
|
|
|
154
146
|
});
|
|
155
147
|
});
|
|
156
148
|
|
|
157
|
-
// ── ServerMessage union (type check) ────────────────────────────────────
|
|
158
|
-
|
|
159
149
|
describe("ServerMessage type covers all variants", () => {
|
|
160
150
|
test("config message shape", () => {
|
|
161
151
|
const msg: ServerMessage = {
|
|
@@ -178,8 +168,6 @@ describe("ServerMessage type covers all variants", () => {
|
|
|
178
168
|
});
|
|
179
169
|
});
|
|
180
170
|
|
|
181
|
-
// ── KvRequestSchema ─────────────────────────────────────────────────────
|
|
182
|
-
|
|
183
171
|
describe("KvRequest wire format", () => {
|
|
184
172
|
const valid = [
|
|
185
173
|
["get", { op: "get", key: "k1" }],
|
|
@@ -201,8 +189,6 @@ describe("KvRequest wire format", () => {
|
|
|
201
189
|
});
|
|
202
190
|
});
|
|
203
191
|
|
|
204
|
-
// ── VectorRequestSchema ─────────────────────────────────────────────────
|
|
205
|
-
|
|
206
192
|
describe("VectorRequest wire format", () => {
|
|
207
193
|
const valid = [
|
|
208
194
|
["upsert", { op: "upsert", id: "doc-1", text: "hello" }],
|
package/sdk/protocol.test.ts
CHANGED
|
@@ -70,17 +70,19 @@ describe("KvRequestSchema", () => {
|
|
|
70
70
|
});
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
const ERROR_CODES = [
|
|
74
|
+
"stt",
|
|
75
|
+
"llm",
|
|
76
|
+
"tts",
|
|
77
|
+
"tool",
|
|
78
|
+
"protocol",
|
|
79
|
+
"connection",
|
|
80
|
+
"audio",
|
|
81
|
+
"internal",
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
73
84
|
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) => {
|
|
85
|
+
test.each(ERROR_CODES)("accepts valid code: %s", (code) => {
|
|
84
86
|
expect(SessionErrorCodeSchema.safeParse(code).success).toBe(true);
|
|
85
87
|
});
|
|
86
88
|
|
|
@@ -173,17 +175,6 @@ describe("property: lenientParse", () => {
|
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
test("valid ClientEvents round-trip through parse", () => {
|
|
176
|
-
const errorCodes = [
|
|
177
|
-
"stt",
|
|
178
|
-
"llm",
|
|
179
|
-
"tts",
|
|
180
|
-
"tool",
|
|
181
|
-
"protocol",
|
|
182
|
-
"connection",
|
|
183
|
-
"audio",
|
|
184
|
-
"internal",
|
|
185
|
-
] as const;
|
|
186
|
-
|
|
187
178
|
const speechStartedArb = fc.constant({ type: "speech_started" as const });
|
|
188
179
|
|
|
189
180
|
const userTranscriptArb = fc.record({
|
|
@@ -193,7 +184,7 @@ describe("property: lenientParse", () => {
|
|
|
193
184
|
|
|
194
185
|
const errorEventArb = fc.record({
|
|
195
186
|
type: fc.constant("error" as const),
|
|
196
|
-
code: fc.constantFrom(...
|
|
187
|
+
code: fc.constantFrom(...ERROR_CODES),
|
|
197
188
|
message: fc.string(),
|
|
198
189
|
});
|
|
199
190
|
|