@alexkroman1/aai 1.7.0 → 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.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +11 -9
  2. package/CHANGELOG.md +16 -0
  3. package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
  4. package/dist/constants-y68COEGj.js +29 -0
  5. package/dist/host/_base64.d.ts +2 -0
  6. package/dist/host/_mock-ws.d.ts +0 -61
  7. package/dist/host/_pipeline-test-fakes.d.ts +7 -4
  8. package/dist/host/_run-code.d.ts +0 -25
  9. package/dist/host/_runtime-conformance.d.ts +3 -34
  10. package/dist/host/memory-vector.d.ts +0 -11
  11. package/dist/host/providers/resolve-kv.d.ts +0 -7
  12. package/dist/host/providers/resolve-vector.d.ts +0 -8
  13. package/dist/host/providers/stt/assemblyai.d.ts +0 -14
  14. package/dist/host/providers/stt/deepgram.d.ts +2 -14
  15. package/dist/host/providers/stt/soniox.d.ts +0 -22
  16. package/dist/host/providers/tts/rime.d.ts +10 -31
  17. package/dist/host/runtime-barrel.js +628 -642
  18. package/dist/host/runtime-config.d.ts +9 -6
  19. package/dist/host/runtime.d.ts +3 -0
  20. package/dist/host/to-vercel-tools.d.ts +3 -33
  21. package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
  22. package/dist/host/unstorage-kv.d.ts +0 -26
  23. package/dist/index.js +3 -3
  24. package/dist/openai-realtime-cjPAHMMx.js +10 -0
  25. package/dist/sdk/_internal-types.d.ts +6 -55
  26. package/dist/sdk/allowed-hosts.d.ts +4 -3
  27. package/dist/sdk/constants.d.ts +4 -29
  28. package/dist/sdk/define.d.ts +7 -4
  29. package/dist/sdk/kv.d.ts +13 -37
  30. package/dist/sdk/manifest-barrel.js +1 -1
  31. package/dist/sdk/manifest.d.ts +8 -2
  32. package/dist/sdk/protocol.js +1 -1
  33. package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
  34. package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
  35. package/dist/sdk/providers/s2s-barrel.js +2 -0
  36. package/dist/sdk/providers/tts/rime.d.ts +1 -1
  37. package/dist/sdk/providers.d.ts +6 -2
  38. package/dist/sdk/types.d.ts +7 -1
  39. package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
  40. package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
  41. package/host/_base64.ts +9 -0
  42. package/host/_mock-ws.ts +0 -65
  43. package/host/_pipeline-test-fakes.ts +19 -31
  44. package/host/_run-code.ts +10 -53
  45. package/host/_runtime-conformance.ts +3 -44
  46. package/host/_test-utils.ts +20 -42
  47. package/host/builtin-tools.test.ts +127 -222
  48. package/host/builtin-tools.ts +6 -10
  49. package/host/cleanup.test.ts +30 -73
  50. package/host/integration/pipeline-reference.integration.test.ts +12 -17
  51. package/host/integration.test.ts +0 -7
  52. package/host/memory-vector.test.ts +3 -1
  53. package/host/memory-vector.ts +16 -21
  54. package/host/pinecone-vector.test.ts +14 -17
  55. package/host/pinecone-vector.ts +10 -19
  56. package/host/providers/providers.test-d.ts +5 -3
  57. package/host/providers/resolve-kv.ts +23 -41
  58. package/host/providers/resolve-vector.ts +3 -12
  59. package/host/providers/resolve.test.ts +15 -28
  60. package/host/providers/resolve.ts +24 -24
  61. package/host/providers/stt/assemblyai.test.ts +2 -14
  62. package/host/providers/stt/assemblyai.ts +12 -35
  63. package/host/providers/stt/deepgram.test.ts +23 -83
  64. package/host/providers/stt/deepgram.ts +15 -40
  65. package/host/providers/stt/elevenlabs.test.ts +26 -38
  66. package/host/providers/stt/elevenlabs.ts +10 -9
  67. package/host/providers/stt/soniox.test.ts +35 -85
  68. package/host/providers/stt/soniox.ts +8 -53
  69. package/host/providers/tts/cartesia.test.ts +19 -58
  70. package/host/providers/tts/cartesia.ts +36 -66
  71. package/host/providers/tts/rime.test.ts +12 -38
  72. package/host/providers/tts/rime.ts +23 -86
  73. package/host/runtime-config.test.ts +9 -9
  74. package/host/runtime-config.ts +16 -22
  75. package/host/runtime.test.ts +111 -73
  76. package/host/runtime.ts +138 -86
  77. package/host/s2s.test.ts +92 -191
  78. package/host/s2s.ts +56 -53
  79. package/host/server-shutdown.test.ts +9 -30
  80. package/host/server.test.ts +2 -13
  81. package/host/server.ts +85 -100
  82. package/host/session-core.test.ts +15 -30
  83. package/host/session-core.ts +10 -13
  84. package/host/session-prompt.test.ts +1 -5
  85. package/host/to-vercel-tools.test.ts +53 -72
  86. package/host/to-vercel-tools.ts +9 -39
  87. package/host/tool-executor.test.ts +25 -51
  88. package/host/tool-executor.ts +18 -12
  89. package/host/transports/openai-realtime-transport.test.ts +371 -0
  90. package/host/transports/openai-realtime-transport.ts +319 -0
  91. package/host/transports/pipeline-transport.test.ts +125 -298
  92. package/host/transports/pipeline-transport.ts +20 -68
  93. package/host/transports/s2s-transport-fixtures.test.ts +31 -92
  94. package/host/transports/s2s-transport.test.ts +65 -134
  95. package/host/transports/s2s-transport.ts +15 -43
  96. package/host/transports/types.test.ts +4 -8
  97. package/host/unstorage-kv.test.ts +3 -2
  98. package/host/unstorage-kv.ts +5 -35
  99. package/host/ws-handler.test.ts +72 -176
  100. package/host/ws-handler.ts +6 -12
  101. package/package.json +6 -1
  102. package/sdk/__snapshots__/exports.test.ts.snap +7 -0
  103. package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
  104. package/sdk/_internal-types.test.ts +6 -9
  105. package/sdk/_internal-types.ts +16 -57
  106. package/sdk/_test-matchers.ts +25 -15
  107. package/sdk/allowed-hosts.test.ts +50 -114
  108. package/sdk/allowed-hosts.ts +8 -14
  109. package/sdk/constants.ts +5 -52
  110. package/sdk/define.test.ts +7 -6
  111. package/sdk/define.ts +7 -3
  112. package/sdk/exports.test.ts +6 -1
  113. package/sdk/kv.ts +13 -37
  114. package/sdk/manifest.test-d.ts +5 -0
  115. package/sdk/manifest.test.ts +61 -9
  116. package/sdk/manifest.ts +11 -11
  117. package/sdk/protocol-compat.test.ts +66 -98
  118. package/sdk/protocol-snapshot.test.ts +2 -16
  119. package/sdk/protocol.test.ts +13 -22
  120. package/sdk/providers/s2s/openai-realtime.ts +36 -0
  121. package/sdk/providers/s2s-barrel.ts +12 -0
  122. package/sdk/providers/tts/rime.ts +1 -1
  123. package/sdk/providers.ts +24 -5
  124. package/sdk/schema-alignment.test.ts +25 -73
  125. package/sdk/schema-shapes.test.ts +1 -29
  126. package/sdk/system-prompt.test.ts +0 -1
  127. package/sdk/system-prompt.ts +17 -19
  128. package/sdk/types-inference.test.ts +10 -36
  129. package/sdk/types.ts +7 -0
  130. package/sdk/ws-upgrade.test.ts +24 -23
  131. package/sdk/ws-upgrade.ts +2 -3
  132. package/tsdown.config.ts +8 -11
  133. package/dist/constants-C2nirZUI.js +0 -54
@@ -0,0 +1,36 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /** OpenAI Realtime S2S descriptor — host transport resolves at session start. */
3
+
4
+ import type { S2sProvider } from "../../providers.ts";
5
+
6
+ export const OPENAI_REALTIME_KIND = "openai-realtime" as const;
7
+
8
+ export type OpenaiRealtimeVoice =
9
+ | "alloy"
10
+ | "ash"
11
+ | "ballad"
12
+ | "cedar"
13
+ | "coral"
14
+ | "echo"
15
+ | "marin"
16
+ | "sage"
17
+ | "shimmer"
18
+ | "verse";
19
+
20
+ export type OpenaiRealtimeOptions = {
21
+ /** Realtime model identifier. Default applied by the host (currently `"gpt-realtime-2"`). */
22
+ model?: string;
23
+ /** TTS voice. Default applied by the host (currently `"alloy"`). */
24
+ voice?: OpenaiRealtimeVoice;
25
+ /** Override the WebSocket base URL (testing/proxy). */
26
+ url?: string;
27
+ };
28
+
29
+ export type OpenaiRealtimeProvider = S2sProvider & {
30
+ readonly kind: typeof OPENAI_REALTIME_KIND;
31
+ readonly options: OpenaiRealtimeOptions;
32
+ };
33
+
34
+ export function openaiRealtime(opts: OpenaiRealtimeOptions = {}): OpenaiRealtimeProvider {
35
+ return { kind: OPENAI_REALTIME_KIND, options: { ...opts } };
36
+ }
@@ -0,0 +1,12 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * `@alexkroman1/aai/s2s` subpath barrel.
4
+ *
5
+ * Re-exports S2S descriptor factories. Importing this barrel does not
6
+ * pull in any provider SDK — the host resolver handles that at session
7
+ * start.
8
+ */
9
+
10
+ export type { S2sProvider } from "../providers.ts";
11
+ // biome-ignore lint/performance/noReExportAll: subpath barrel
12
+ export * from "./s2s/openai-realtime.ts";
@@ -29,7 +29,7 @@ export interface RimeOptions {
29
29
  * Rime model ID. Defaults to `"mistv2"` (Rime's most compatible model).
30
30
  * Common values: `"mistv2"`, `"arcana"`.
31
31
  */
32
- model?: "mistv2" | "arcana" | string;
32
+ model?: string;
33
33
  /**
34
34
  * Spoken language. Uses ISO 639-3 (three-letter codes).
35
35
  * Defaults to `"eng"` (English).
package/sdk/providers.ts CHANGED
@@ -48,6 +48,9 @@ export type LlmProvider = ProviderDescriptor<string, Record<string, unknown>>;
48
48
  /** Descriptor for a TTS provider. Returned by factories like `cartesia(...)`. */
49
49
  export type TtsProvider = ProviderDescriptor<string, Record<string, unknown>>;
50
50
 
51
+ /** Descriptor for an S2S provider. Returned by factories like `openaiRealtime(...)`. */
52
+ export type S2sProvider = ProviderDescriptor<string, Record<string, unknown>>;
53
+
51
54
  /** Descriptor for a KV backend. Returned by factories like `redisKv()`. */
52
55
  export type KvProvider = ProviderDescriptor<string, Record<string, unknown>>;
53
56
 
@@ -67,14 +70,30 @@ export type SessionMode = "s2s" | "pipeline";
67
70
  * Enforce the all-or-nothing provider rule and return the derived mode.
68
71
  *
69
72
  * Pipeline mode requires STT, LLM, and TTS all set; S2S mode requires
70
- * none of them. Anything in-between is a configuration error.
73
+ * none of them. Anything in-between is a configuration error. An optional
74
+ * `s2s` descriptor selects a non-default S2S provider — it must not be
75
+ * combined with any pipeline field.
71
76
  */
72
- export function assertProviderTriple(stt: unknown, llm: unknown, tts: unknown): SessionMode {
73
- const count = (stt != null ? 1 : 0) + (llm != null ? 1 : 0) + (tts != null ? 1 : 0);
74
- if (count !== 0 && count !== 3) {
77
+ export function assertProviderTriple(
78
+ stt: unknown,
79
+ llm: unknown,
80
+ tts: unknown,
81
+ s2s?: unknown,
82
+ ): SessionMode {
83
+ const hasStt = stt != null;
84
+ const hasLlm = llm != null;
85
+ const hasTts = tts != null;
86
+ const hasS2s = s2s != null;
87
+ const anyPipeline = hasStt || hasLlm || hasTts;
88
+ const allSet = hasStt && hasLlm && hasTts;
89
+ const noneSetPipeline = !anyPipeline;
90
+ if (hasS2s && anyPipeline) {
91
+ throw new Error("s2s and the stt/llm/tts pipeline cannot be set together");
92
+ }
93
+ if (!(allSet || noneSetPipeline)) {
75
94
  throw new Error("stt, llm, and tts must be set together");
76
95
  }
77
- return count === 3 ? "pipeline" : "s2s";
96
+ return allSet ? "pipeline" : "s2s";
78
97
  }
79
98
 
80
99
  // -------- STT openable (host-only) ------------------------------------------
@@ -1,20 +1,10 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * Schema–type alignment tests.
4
- *
5
- * These tests verify that Zod schemas and their derived TypeScript types
6
- * stay in sync, and that schemas correctly reject invalid data. When a
7
- * new field is added to a type but not its schema (or vice-versa), these
8
- * tests break — preventing silent API drift.
9
- */
10
2
  import { describe, expect, expectTypeOf, test } from "vitest";
11
3
  import type { z } from "zod";
12
4
  import { type AgentConfig, AgentConfigSchema, ToolSchemaSchema } from "./_internal-types.ts";
13
5
  import { type ReadyConfig, ReadyConfigSchema } from "./protocol.ts";
14
6
  import { type BuiltinTool, BuiltinToolSchema, type ToolChoice, ToolChoiceSchema } from "./types.ts";
15
7
 
16
- // ── AgentConfigSchema ────────────────────────────────────────────────────
17
-
18
8
  describe("AgentConfigSchema", () => {
19
9
  const valid: AgentConfig = {
20
10
  name: "test-agent",
@@ -37,22 +27,13 @@ describe("AgentConfigSchema", () => {
37
27
  expect(AgentConfigSchema.parse(full)).toEqual(full);
38
28
  });
39
29
 
40
- test("rejects empty name", () => {
41
- expect(AgentConfigSchema.safeParse({ ...valid, name: "" }).success).toBe(false);
42
- });
43
-
44
- test("rejects non-integer maxSteps", () => {
45
- expect(AgentConfigSchema.safeParse({ ...valid, maxSteps: 2.5 }).success).toBe(false);
46
- });
47
-
48
- test("rejects negative maxSteps", () => {
49
- expect(AgentConfigSchema.safeParse({ ...valid, maxSteps: -1 }).success).toBe(false);
50
- });
51
-
52
- test("rejects invalid builtinTools", () => {
53
- expect(AgentConfigSchema.safeParse({ ...valid, builtinTools: ["not_a_tool"] }).success).toBe(
54
- false,
55
- );
30
+ test.each([
31
+ ["empty name", { name: "" }],
32
+ ["non-integer maxSteps", { maxSteps: 2.5 }],
33
+ ["negative maxSteps", { maxSteps: -1 }],
34
+ ["invalid builtinTools", { builtinTools: ["not_a_tool"] }],
35
+ ])("rejects %s", (_label, override) => {
36
+ expect(AgentConfigSchema.safeParse({ ...valid, ...override }).success).toBe(false);
56
37
  });
57
38
 
58
39
  test("type derived from schema matches AgentConfig", () => {
@@ -60,9 +41,9 @@ describe("AgentConfigSchema", () => {
60
41
  });
61
42
  });
62
43
 
63
- // ── ToolSchemaSchema ─────────────────────────────────────────────────────
64
-
65
44
  describe("ToolSchemaSchema", () => {
45
+ const base = { type: "function" as const, name: "n", description: "d", parameters: {} };
46
+
66
47
  test("accepts valid tool schema", () => {
67
48
  const valid = {
68
49
  type: "function" as const,
@@ -73,47 +54,28 @@ describe("ToolSchemaSchema", () => {
73
54
  expect(ToolSchemaSchema.parse(valid)).toEqual(valid);
74
55
  });
75
56
 
76
- test("rejects empty name", () => {
77
- expect(
78
- ToolSchemaSchema.safeParse({
79
- type: "function",
80
- name: "",
81
- description: "d",
82
- parameters: {},
83
- }).success,
84
- ).toBe(false);
85
- });
86
-
87
- test("rejects empty description", () => {
88
- expect(
89
- ToolSchemaSchema.safeParse({
90
- type: "function",
91
- name: "n",
92
- description: "",
93
- parameters: {},
94
- }).success,
95
- ).toBe(false);
57
+ test.each([
58
+ ["empty name", { name: "" }],
59
+ ["empty description", { description: "" }],
60
+ ])("rejects %s", (_label, override) => {
61
+ expect(ToolSchemaSchema.safeParse({ ...base, ...override }).success).toBe(false);
96
62
  });
97
63
 
98
64
  test("ToolSchema is assignable from schema inference", () => {
99
- // ToolSchema uses JSONSchema7 for parameters which is more specific than
100
- // the runtime schema's Record<string, unknown>. Verify the direction:
101
- // a parsed result should be assignable to ToolSchema (narrow → wide).
65
+ // ToolSchema uses JSONSchema7 for parameters (narrower than the runtime
66
+ // Record<string, unknown>); verify a parsed result satisfies the wider shape.
102
67
  const parsed = ToolSchemaSchema.parse({
103
68
  type: "function",
104
69
  name: "test",
105
70
  description: "test",
106
71
  parameters: { type: "object" },
107
72
  });
108
- // Runtime check: shape matches
109
73
  expect(parsed).toHaveProperty("name");
110
74
  expect(parsed).toHaveProperty("description");
111
75
  expect(parsed).toHaveProperty("parameters");
112
76
  });
113
77
  });
114
78
 
115
- // ── ReadyConfigSchema ────────────────────────────────────────────────────
116
-
117
79
  describe("ReadyConfigSchema", () => {
118
80
  const valid: ReadyConfig = {
119
81
  audioFormat: "pcm16",
@@ -125,12 +87,11 @@ describe("ReadyConfigSchema", () => {
125
87
  expect(ReadyConfigSchema.parse(valid)).toEqual(valid);
126
88
  });
127
89
 
128
- test("rejects unknown audio format", () => {
129
- expect(ReadyConfigSchema.safeParse({ ...valid, audioFormat: "mp3" }).success).toBe(false);
130
- });
131
-
132
- test("rejects non-positive sampleRate", () => {
133
- expect(ReadyConfigSchema.safeParse({ ...valid, sampleRate: 0 }).success).toBe(false);
90
+ test.each([
91
+ ["unknown audio format", { audioFormat: "mp3" }],
92
+ ["non-positive sampleRate", { sampleRate: 0 }],
93
+ ])("rejects %s", (_label, override) => {
94
+ expect(ReadyConfigSchema.safeParse({ ...valid, ...override }).success).toBe(false);
134
95
  });
135
96
 
136
97
  test("type derived from schema matches ReadyConfig", () => {
@@ -138,13 +99,8 @@ describe("ReadyConfigSchema", () => {
138
99
  });
139
100
  });
140
101
 
141
- // ── BuiltinTool / ToolChoice drift guards ────────────────────────────────
142
-
143
102
  describe("type ↔ schema alignment", () => {
144
103
  test("BuiltinToolSchema values match BuiltinTool union", () => {
145
- // Schema values are the source of truth; if the type adds a member
146
- // without updating the schema (or vice versa), TypeScript will error
147
- // on the drift guards in types.ts. This test documents the enum values.
148
104
  expect(BuiltinToolSchema.options).toMatchInlineSnapshot(`
149
105
  [
150
106
  "web_search",
@@ -163,15 +119,11 @@ describe("type ↔ schema alignment", () => {
163
119
  expectTypeOf<z.infer<typeof ToolChoiceSchema>>().toEqualTypeOf<ToolChoice>();
164
120
  });
165
121
 
166
- test("ToolChoiceSchema accepts all ToolChoice variants", () => {
167
- const variants: ToolChoice[] = ["auto", "required"];
168
- for (const v of variants) {
169
- expect(ToolChoiceSchema.safeParse(v).success).toBe(true);
170
- }
122
+ test.each<ToolChoice>(["auto", "required"])("ToolChoiceSchema accepts %s", (v) => {
123
+ expect(ToolChoiceSchema.safeParse(v).success).toBe(true);
171
124
  });
172
125
 
173
- test("ToolChoiceSchema rejects invalid variants", () => {
174
- expect(ToolChoiceSchema.safeParse("invalid").success).toBe(false);
175
- expect(ToolChoiceSchema.safeParse("none").success).toBe(false);
126
+ test.each(["invalid", "none"])("ToolChoiceSchema rejects %s", (v) => {
127
+ expect(ToolChoiceSchema.safeParse(v).success).toBe(false);
176
128
  });
177
129
  });
@@ -1,17 +1,4 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * Schema shape snapshot tests for public Zod schemas.
4
- *
5
- * These tests snapshot the field names of public schemas exported from
6
- * `@alexkroman1/aai/protocol` and `@alexkroman1/aai/manifest`. A breaking
7
- * snapshot signals a wire-format change (field additions, removals, or renames)
8
- * that may require a changeset and protocol-version bump.
9
- *
10
- * For discriminated unions (ClientEventSchema, ServerMessageSchema,
11
- * ClientMessageSchema), each option's shape keys are keyed by its
12
- * discriminator value. For object schemas (AgentConfigSchema,
13
- * ToolSchemaSchema), the top-level shape keys are snapshotted directly.
14
- */
15
2
  import { describe, expect, test } from "vitest";
16
3
  import { AgentConfigSchema, ToolSchemaSchema } from "./_internal-types.ts";
17
4
  import {
@@ -24,22 +11,12 @@ import {
24
11
  ServerMessageSchema,
25
12
  } from "./protocol.ts";
26
13
 
27
- // ── Helpers ──────────────────────────────────────────────────────────────
28
-
29
14
  type ZodObjectLike = { shape: Record<string, unknown> };
30
15
 
31
- /**
32
- * Extract shape keys (field names) from a ZodObject-like schema.
33
- */
34
16
  function shapeKeys(schema: ZodObjectLike): string[] {
35
17
  return Object.keys(schema.shape).sort();
36
18
  }
37
19
 
38
- /**
39
- * Extract a map of { discriminatorValue → sorted shape keys } from a
40
- * ZodDiscriminatedUnion. The discriminator literal may be a single value
41
- * or an array (Zod v4 stores it as `.values`).
42
- */
43
20
  function discriminatedUnionShapes(schema: {
44
21
  options: Array<{ shape: Record<string, unknown> }>;
45
22
  }): Record<string, string[]> {
@@ -48,9 +25,8 @@ function discriminatedUnionShapes(schema: {
48
25
  const typeSchema = option.shape?.type as
49
26
  | { _def?: { value?: string; values?: string[] } }
50
27
  | undefined;
51
- const def = typeSchema?._def;
52
28
  // Zod v4 uses `.values` (array); Zod v3 uses `.value` (scalar)
53
- const raw = def?.values ?? def?.value;
29
+ const raw = typeSchema?._def?.values ?? typeSchema?._def?.value;
54
30
  const discriminatorValue = Array.isArray(raw) ? raw[0] : raw;
55
31
  const key = String(discriminatorValue ?? "unknown");
56
32
  result[key] = Object.keys(option.shape).sort();
@@ -58,8 +34,6 @@ function discriminatedUnionShapes(schema: {
58
34
  return result;
59
35
  }
60
36
 
61
- // ── Protocol schemas ─────────────────────────────────────────────────────
62
-
63
37
  describe("protocol schema shapes", () => {
64
38
  test("ClientEventSchema option shapes", () => {
65
39
  expect(discriminatedUnionShapes(ClientEventSchema)).toMatchSnapshot();
@@ -90,8 +64,6 @@ describe("protocol schema shapes", () => {
90
64
  });
91
65
  });
92
66
 
93
- // ── Manifest schemas ──────────────────────────────────────────────────────
94
-
95
67
  describe("manifest schema shapes", () => {
96
68
  test("AgentConfigSchema shape", () => {
97
69
  expect(shapeKeys(AgentConfigSchema)).toMatchSnapshot();
@@ -8,7 +8,6 @@ import { DEFAULT_SYSTEM_PROMPT } from "./types.ts";
8
8
  describe("buildSystemPrompt", () => {
9
9
  beforeEach(() => {
10
10
  vi.useFakeTimers();
11
- // Pin to Wednesday, January 15, 2025
12
11
  vi.setSystemTime(new Date("2025-01-15T12:00:00Z"));
13
12
  });
14
13
 
@@ -6,14 +6,17 @@
6
6
  import type { AgentConfig } from "./_internal-types.ts";
7
7
  import { DEFAULT_SYSTEM_PROMPT } from "./types.ts";
8
8
 
9
- function getFormattedDate(): string {
10
- return new Date().toLocaleDateString("en-US", {
11
- weekday: "long",
12
- year: "numeric",
13
- month: "long",
14
- day: "numeric",
15
- });
16
- }
9
+ const DATE_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
10
+ weekday: "long",
11
+ year: "numeric",
12
+ month: "long",
13
+ day: "numeric",
14
+ };
15
+
16
+ const TOOL_PREAMBLE =
17
+ "\n\nWhen you decide to use a tool, ALWAYS say a brief natural phrase BEFORE the tool call " +
18
+ '(e.g. "Let me look that up" or "One moment while I check"). ' +
19
+ "This fills silence while the tool executes. Keep preambles to one short sentence.";
17
20
 
18
21
  const VOICE_RULES =
19
22
  "\n\nCRITICAL OUTPUT RULES — you MUST follow these for EVERY response:\n" +
@@ -44,24 +47,19 @@ export function buildSystemPrompt(
44
47
  config: AgentConfig,
45
48
  opts: { hasTools: boolean; voice?: boolean; toolGuidance?: readonly string[] | undefined },
46
49
  ): string {
47
- const { hasTools } = opts;
48
- const agentInstructions =
49
- config.systemPrompt && config.systemPrompt !== DEFAULT_SYSTEM_PROMPT
50
- ? `\n\nAgent-Specific Instructions:\n${config.systemPrompt}`
51
- : "";
52
-
53
- const toolPreamble = hasTools
54
- ? "\n\nWhen you decide to use a tool, ALWAYS say a brief natural phrase BEFORE the tool call " +
55
- '(e.g. "Let me look that up" or "One moment while I check"). ' +
56
- "This fills silence while the tool executes. Keep preambles to one short sentence."
50
+ const hasCustomPrompt = config.systemPrompt && config.systemPrompt !== DEFAULT_SYSTEM_PROMPT;
51
+ const agentInstructions = hasCustomPrompt
52
+ ? `\n\nAgent-Specific Instructions:\n${config.systemPrompt}`
57
53
  : "";
58
54
 
55
+ const toolPreamble = opts.hasTools ? TOOL_PREAMBLE : "";
56
+
59
57
  const guidance =
60
58
  opts.toolGuidance && opts.toolGuidance.length > 0
61
59
  ? `\n\nBuilt-in Tool Usage:\n${opts.toolGuidance.join("\n")}`
62
60
  : "";
63
61
 
64
- const today = getFormattedDate();
62
+ const today = new Date().toLocaleDateString("en-US", DATE_FORMAT_OPTIONS);
65
63
 
66
64
  return (
67
65
  DEFAULT_SYSTEM_PROMPT +
@@ -1,16 +1,14 @@
1
1
  // Copyright 2025 the AAI authors. MIT license.
2
- /**
3
- * Type-level tests for AgentDef and ToolDef type inference.
4
- *
5
- * These use vitest's expectTypeOf to verify that TypeScript correctly
6
- * infers parameter types, state types, and context types without
7
- * any runtime assertions. A failing type test means a type refactor
8
- * broke inference for consumers.
9
- */
10
2
  import { describe, expectTypeOf, test } from "vitest";
11
3
  import { z } from "zod";
12
4
  import type { AgentDef, Message, ToolContext, ToolDef } from "./types.ts";
13
5
 
6
+ const baseAgent = {
7
+ systemPrompt: "Be helpful.",
8
+ greeting: "Hello!",
9
+ maxSteps: 5,
10
+ } as const;
11
+
14
12
  describe("ToolDef type inference", () => {
15
13
  test("infers parameter types in execute args", () => {
16
14
  const _t: ToolDef<z.ZodObject<{ name: z.ZodString; count: z.ZodNumber }>> = {
@@ -19,7 +17,6 @@ describe("ToolDef type inference", () => {
19
17
  execute: (args) => args,
20
18
  };
21
19
 
22
- // The execute function should receive typed args
23
20
  type Args = Parameters<typeof _t.execute>[0];
24
21
  expectTypeOf<Args>().toEqualTypeOf<{ name: string; count: number }>();
25
22
  });
@@ -31,7 +28,6 @@ describe("ToolDef type inference", () => {
31
28
  };
32
29
 
33
30
  type Args = Parameters<typeof _t.execute>[0];
34
- // Without parameters, args is inferred from the base ZodObject
35
31
  expectTypeOf<Args>().toBeObject();
36
32
  });
37
33
 
@@ -57,26 +53,16 @@ describe("ToolDef type inference", () => {
57
53
 
58
54
  describe("AgentDef type inference", () => {
59
55
  test("satisfies AgentDef type", () => {
60
- const agent: AgentDef = {
61
- name: "test",
62
- systemPrompt: "Be helpful.",
63
- greeting: "Hello!",
64
- maxSteps: 5,
65
- tools: {},
66
- };
56
+ const agent: AgentDef = { ...baseAgent, name: "test", tools: {} };
67
57
  expectTypeOf(agent).toMatchTypeOf<AgentDef>();
68
58
  });
69
59
 
70
60
  test("typed state flows through to tools", () => {
71
61
  type MyState = { counter: number; name: string };
72
62
 
73
- // This should compile without errors — state type flows
74
- // through to tool execute context
75
63
  const _agent: AgentDef<MyState> = {
64
+ ...baseAgent,
76
65
  name: "typed-state",
77
- systemPrompt: "Be helpful.",
78
- greeting: "Hello!",
79
- maxSteps: 5,
80
66
  state: () => ({ counter: 0, name: "test" }),
81
67
  tools: {
82
68
  inc: {
@@ -96,25 +82,13 @@ describe("AgentDef type inference", () => {
96
82
  execute: ({ name }: { name: string }) => `Hello, ${name}!`,
97
83
  };
98
84
 
99
- const agent: AgentDef = {
100
- name: "with-tool",
101
- systemPrompt: "Be helpful.",
102
- greeting: "Hello!",
103
- maxSteps: 5,
104
- tools: { greet },
105
- };
85
+ const agent: AgentDef = { ...baseAgent, name: "with-tool", tools: { greet } };
106
86
 
107
87
  expectTypeOf(agent.tools).toHaveProperty("greet");
108
88
  });
109
89
 
110
90
  test("required fields are present", () => {
111
- const agent: AgentDef = {
112
- name: "defaults",
113
- systemPrompt: "Be helpful.",
114
- greeting: "Hello!",
115
- maxSteps: 5,
116
- tools: {},
117
- };
91
+ const agent: AgentDef = { ...baseAgent, name: "defaults", tools: {} };
118
92
  expectTypeOf(agent.systemPrompt).toBeString();
119
93
  expectTypeOf(agent.greeting).toBeString();
120
94
  expectTypeOf(agent.tools).toBeObject();
package/sdk/types.ts CHANGED
@@ -8,6 +8,7 @@ import type { Kv } from "./kv.ts";
8
8
  import type {
9
9
  KvProvider,
10
10
  LlmProvider,
11
+ S2sProvider,
11
12
  SttProvider,
12
13
  TtsProvider,
13
14
  VectorProvider,
@@ -242,6 +243,12 @@ export type AgentDef<S = Record<string, unknown>> = {
242
243
  * pipeline mode.
243
244
  */
244
245
  tts?: TtsProvider;
246
+ /**
247
+ * Pluggable S2S provider descriptor. When set, overrides the implicit
248
+ * AssemblyAI default. Mutually exclusive with the `stt`/`llm`/`tts`
249
+ * pipeline triple.
250
+ */
251
+ s2s?: S2sProvider;
245
252
  /** Pluggable KV backend. Falls back to platform default when omitted. */
246
253
  kv?: KvProvider;
247
254
  /** Pluggable Vector backend. Falls back to platform default when omitted. */
@@ -5,44 +5,45 @@ import { parseWsUpgradeParams } from "./ws-upgrade.ts";
5
5
 
6
6
  describe("parseWsUpgradeParams", () => {
7
7
  test("returns defaults for URL with no query params", () => {
8
- const result = parseWsUpgradeParams("/websocket");
9
- expect(result).toEqual({ resumeFrom: undefined, skipGreeting: false });
8
+ expect(parseWsUpgradeParams("/websocket")).toEqual({
9
+ resumeFrom: undefined,
10
+ skipGreeting: false,
11
+ });
10
12
  });
11
13
 
12
14
  test("extracts sessionId and sets skipGreeting", () => {
13
- const result = parseWsUpgradeParams("/ws?sessionId=abc-123");
14
- expect(result.resumeFrom).toBe("abc-123");
15
- expect(result.skipGreeting).toBe(true);
15
+ expect(parseWsUpgradeParams("/ws?sessionId=abc-123")).toEqual({
16
+ resumeFrom: "abc-123",
17
+ skipGreeting: true,
18
+ });
16
19
  });
17
20
 
18
21
  test("sets skipGreeting when resume param is present", () => {
19
- const result = parseWsUpgradeParams("/ws?resume=1");
20
- expect(result.resumeFrom).toBeUndefined();
21
- expect(result.skipGreeting).toBe(true);
22
+ expect(parseWsUpgradeParams("/ws?resume=1")).toEqual({
23
+ resumeFrom: undefined,
24
+ skipGreeting: true,
25
+ });
22
26
  });
23
27
 
24
28
  test("sessionId takes precedence for resumeFrom", () => {
25
- const result = parseWsUpgradeParams("/ws?resume=1&sessionId=sess-42");
26
- expect(result.resumeFrom).toBe("sess-42");
27
- expect(result.skipGreeting).toBe(true);
28
- });
29
-
30
- test("handles URL with no query string", () => {
31
- const result = parseWsUpgradeParams("/websocket");
32
- expect(result.resumeFrom).toBeUndefined();
33
- expect(result.skipGreeting).toBe(false);
29
+ expect(parseWsUpgradeParams("/ws?resume=1&sessionId=sess-42")).toEqual({
30
+ resumeFrom: "sess-42",
31
+ skipGreeting: true,
32
+ });
34
33
  });
35
34
 
36
35
  test("handles full URL with query params", () => {
37
- const result = parseWsUpgradeParams("ws://localhost:3000/websocket?sessionId=s1");
38
- expect(result.resumeFrom).toBe("s1");
39
- expect(result.skipGreeting).toBe(true);
36
+ expect(parseWsUpgradeParams("ws://localhost:3000/websocket?sessionId=s1")).toEqual({
37
+ resumeFrom: "s1",
38
+ skipGreeting: true,
39
+ });
40
40
  });
41
41
 
42
42
  test("handles empty sessionId", () => {
43
- const result = parseWsUpgradeParams("/ws?sessionId=");
44
43
  // Empty string from URLSearchParams.get is truthy for ?? check
45
- expect(result.resumeFrom).toBe("");
46
- expect(result.skipGreeting).toBe(true);
44
+ expect(parseWsUpgradeParams("/ws?sessionId=")).toEqual({
45
+ resumeFrom: "",
46
+ skipGreeting: true,
47
+ });
47
48
  });
48
49
  });
package/sdk/ws-upgrade.ts CHANGED
@@ -5,9 +5,8 @@ export function parseWsUpgradeParams(rawUrl: string): {
5
5
  resumeFrom?: string;
6
6
  skipGreeting: boolean;
7
7
  } {
8
- const search = rawUrl.includes("?") ? (rawUrl.split("?")[1] ?? "") : "";
9
- const params = new URLSearchParams(search);
8
+ const params = new URLSearchParams(rawUrl.split("?")[1] ?? "");
10
9
  const resumeFrom = params.get("sessionId") ?? undefined;
11
- const skipGreeting = params.has("resume") || resumeFrom !== undefined;
10
+ const skipGreeting = resumeFrom !== undefined || params.has("resume");
12
11
  return resumeFrom !== undefined ? { resumeFrom, skipGreeting } : { skipGreeting };
13
12
  }
package/tsdown.config.ts CHANGED
@@ -2,17 +2,14 @@ import { readFileSync } from "node:fs";
2
2
  import { defineConfig } from "tsdown";
3
3
 
4
4
  // Derive build entries from package.json exports so they can never drift.
5
- const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
6
- const entry = [
7
- ...new Set(
8
- Object.values(pkg.exports as Record<string, Record<string, string>>)
9
- .filter(
10
- (v): v is { "@dev/source": string } =>
11
- typeof v === "object" && typeof v["@dev/source"] === "string",
12
- )
13
- .map((v) => v["@dev/source"].replace(/^\.\//, "")),
14
- ),
15
- ];
5
+ const pkg = JSON.parse(readFileSync("package.json", "utf-8")) as {
6
+ exports: Record<string, Record<string, string>>;
7
+ };
8
+ const sources = Object.values(pkg.exports)
9
+ .map((v) => v?.["@dev/source"])
10
+ .filter((s): s is string => typeof s === "string")
11
+ .map((s) => s.replace(/^\.\//, ""));
12
+ const entry = [...new Set(sources)];
16
13
 
17
14
  export default defineConfig({
18
15
  entry,