@alexkroman1/aai 1.0.6 → 1.2.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 -11
- package/CHANGELOG.md +22 -0
- package/dist/_internal-types-CoDTiBd1.js +61 -0
- package/dist/host/_mock-ws.d.ts +0 -24
- package/dist/host/runtime-barrel.d.ts +0 -1
- package/dist/host/runtime-barrel.js +55 -5
- package/dist/host/runtime.d.ts +2 -0
- package/dist/host/tool-executor.d.ts +1 -0
- package/dist/host/ws-handler.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +90 -1
- package/dist/sdk/allowed-hosts.d.ts +34 -0
- package/dist/sdk/manifest-barrel.d.ts +3 -5
- package/dist/sdk/manifest-barrel.js +2 -52
- package/dist/sdk/manifest.d.ts +2 -0
- package/dist/sdk/protocol.d.ts +11 -28
- package/dist/sdk/protocol.js +6 -3
- package/dist/sdk/types.d.ts +2 -0
- package/host/_mock-ws.ts +0 -50
- package/host/_test-utils.ts +1 -0
- package/host/runtime-barrel.ts +0 -1
- package/host/runtime.ts +13 -1
- package/host/session-ctx.test.ts +387 -0
- package/host/session-fixture-replay.test.ts +2 -10
- package/host/session.test.ts +19 -41
- package/host/tool-executor.test.ts +36 -0
- package/host/tool-executor.ts +4 -0
- package/host/ws-handler.ts +3 -0
- package/index.ts +1 -0
- package/package.json +1 -1
- package/sdk/__snapshots__/exports.test.ts.snap +79 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +187 -0
- package/sdk/_test-matchers.test.ts +75 -0
- package/sdk/_test-matchers.ts +73 -0
- package/sdk/allowed-hosts.test.ts +236 -0
- package/sdk/allowed-hosts.ts +113 -0
- package/sdk/exports.test.ts +31 -0
- package/sdk/manifest-barrel.ts +13 -7
- package/sdk/manifest.test.ts +103 -2
- package/sdk/manifest.ts +19 -0
- package/sdk/protocol-compat.test.ts +0 -6
- package/sdk/protocol-snapshot.test.ts +7 -5
- package/sdk/protocol.test.ts +107 -21
- package/sdk/protocol.ts +7 -15
- package/sdk/schema-alignment.test.ts +1 -27
- package/sdk/schema-shapes.test.ts +103 -0
- package/sdk/tsconfig.json +1 -1
- package/sdk/types.test.ts +56 -1
- package/sdk/types.ts +2 -0
- package/sdk/ws-upgrade.test.ts +8 -8
- package/tsconfig.build.json +8 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
- package/dist/system-prompt-nik_iavo.js +0 -92
package/sdk/protocol.test.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fc from "fast-check";
|
|
2
|
+
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
3
|
+
import { z } from "zod";
|
|
2
4
|
import {
|
|
3
5
|
DEFAULT_STT_SAMPLE_RATE,
|
|
4
6
|
DEFAULT_TTS_SAMPLE_RATE,
|
|
5
7
|
TOOL_EXECUTION_TIMEOUT_MS,
|
|
6
8
|
} from "./constants.ts";
|
|
9
|
+
import type { ClientEvent, ServerMessage } from "./protocol.ts";
|
|
7
10
|
import {
|
|
8
|
-
AUDIO_FORMAT,
|
|
9
11
|
buildReadyConfig,
|
|
10
12
|
ClientEventSchema,
|
|
11
13
|
ClientMessageSchema,
|
|
12
14
|
KvRequestSchema,
|
|
15
|
+
lenientParse,
|
|
13
16
|
SessionErrorCodeSchema,
|
|
14
17
|
} from "./protocol.ts";
|
|
15
18
|
|
|
@@ -22,10 +25,6 @@ describe("protocol constants", () => {
|
|
|
22
25
|
expect(DEFAULT_TTS_SAMPLE_RATE).toBe(24_000);
|
|
23
26
|
});
|
|
24
27
|
|
|
25
|
-
test('AUDIO_FORMAT is "pcm16"', () => {
|
|
26
|
-
expect(AUDIO_FORMAT).toBe("pcm16");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
28
|
test("TOOL_EXECUTION_TIMEOUT_MS is 30000", () => {
|
|
30
29
|
expect(TOOL_EXECUTION_TIMEOUT_MS).toBe(30_000);
|
|
31
30
|
});
|
|
@@ -91,32 +90,23 @@ describe("SessionErrorCodeSchema", () => {
|
|
|
91
90
|
|
|
92
91
|
describe("ClientEventSchema", () => {
|
|
93
92
|
test("accepts speech_started", () => {
|
|
94
|
-
|
|
95
|
-
expect(result.success).toBe(true);
|
|
93
|
+
expect({ type: "speech_started" }).toBeValidClientEvent();
|
|
96
94
|
});
|
|
97
95
|
|
|
98
96
|
test("accepts user_transcript", () => {
|
|
99
|
-
|
|
100
|
-
type: "user_transcript",
|
|
101
|
-
text: "hello world",
|
|
102
|
-
});
|
|
103
|
-
expect(result.success).toBe(true);
|
|
97
|
+
expect({ type: "user_transcript", text: "hello world" }).toBeValidClientEvent();
|
|
104
98
|
});
|
|
105
99
|
|
|
106
100
|
test("accepts error event", () => {
|
|
107
|
-
|
|
101
|
+
expect({
|
|
108
102
|
type: "error",
|
|
109
103
|
code: "internal",
|
|
110
104
|
message: "something went wrong",
|
|
111
|
-
});
|
|
112
|
-
expect(result.success).toBe(true);
|
|
105
|
+
}).toBeValidClientEvent();
|
|
113
106
|
});
|
|
114
107
|
|
|
115
108
|
test("rejects unknown type", () => {
|
|
116
|
-
|
|
117
|
-
type: "unknown_event_type",
|
|
118
|
-
});
|
|
119
|
-
expect(result.success).toBe(false);
|
|
109
|
+
expect({ type: "unknown_event_type" }).not.toBeValidClientEvent();
|
|
120
110
|
});
|
|
121
111
|
});
|
|
122
112
|
|
|
@@ -156,7 +146,7 @@ describe("buildReadyConfig", () => {
|
|
|
156
146
|
test("builds config from sample rates", () => {
|
|
157
147
|
const config = buildReadyConfig({ inputSampleRate: 16_000, outputSampleRate: 24_000 });
|
|
158
148
|
expect(config).toEqual({
|
|
159
|
-
audioFormat:
|
|
149
|
+
audioFormat: "pcm16",
|
|
160
150
|
sampleRate: 16_000,
|
|
161
151
|
ttsSampleRate: 24_000,
|
|
162
152
|
});
|
|
@@ -168,3 +158,99 @@ describe("buildReadyConfig", () => {
|
|
|
168
158
|
expect(config.ttsSampleRate).toBe(48_000);
|
|
169
159
|
});
|
|
170
160
|
});
|
|
161
|
+
|
|
162
|
+
// ── Property-based tests ─────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
describe("property: lenientParse", () => {
|
|
165
|
+
test("never throws on arbitrary input", () => {
|
|
166
|
+
fc.assert(
|
|
167
|
+
fc.property(fc.anything(), (input) => {
|
|
168
|
+
const result = lenientParse(ClientEventSchema, input);
|
|
169
|
+
expect(result).toHaveProperty("ok");
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("valid ClientEvents round-trip through parse", () => {
|
|
175
|
+
const errorCodes = [
|
|
176
|
+
"stt",
|
|
177
|
+
"llm",
|
|
178
|
+
"tts",
|
|
179
|
+
"tool",
|
|
180
|
+
"protocol",
|
|
181
|
+
"connection",
|
|
182
|
+
"audio",
|
|
183
|
+
"internal",
|
|
184
|
+
] as const;
|
|
185
|
+
|
|
186
|
+
const speechStartedArb = fc.constant({ type: "speech_started" as const });
|
|
187
|
+
|
|
188
|
+
const userTranscriptArb = fc.record({
|
|
189
|
+
type: fc.constant("user_transcript" as const),
|
|
190
|
+
text: fc.string(),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const errorEventArb = fc.record({
|
|
194
|
+
type: fc.constant("error" as const),
|
|
195
|
+
code: fc.constantFrom(...errorCodes),
|
|
196
|
+
message: fc.string(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const clientEventArb = fc.oneof(speechStartedArb, userTranscriptArb, errorEventArb);
|
|
200
|
+
|
|
201
|
+
fc.assert(
|
|
202
|
+
fc.property(clientEventArb, (event) => {
|
|
203
|
+
const result = lenientParse(ClientEventSchema, event);
|
|
204
|
+
expect(result.ok).toBe(true);
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("objects without type field are malformed", () => {
|
|
210
|
+
const noTypeArb = fc.object().filter((obj) => !("type" in obj));
|
|
211
|
+
|
|
212
|
+
fc.assert(
|
|
213
|
+
fc.property(noTypeArb, (obj) => {
|
|
214
|
+
const result = lenientParse(ClientEventSchema, obj);
|
|
215
|
+
expect(result.ok).toBe(false);
|
|
216
|
+
if (!result.ok) {
|
|
217
|
+
expect(result.malformed).toBe(true);
|
|
218
|
+
}
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("protocol type contracts", () => {
|
|
225
|
+
test("ClientEvent narrows on user_transcript discriminant", () => {
|
|
226
|
+
type UserTranscript = Extract<ClientEvent, { type: "user_transcript" }>;
|
|
227
|
+
expectTypeOf<UserTranscript>().toHaveProperty("text");
|
|
228
|
+
expectTypeOf<UserTranscript["text"]>().toBeString();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("ClientEvent narrows on tool_call discriminant", () => {
|
|
232
|
+
type ToolCall = Extract<ClientEvent, { type: "tool_call" }>;
|
|
233
|
+
expectTypeOf<ToolCall>().toHaveProperty("toolCallId");
|
|
234
|
+
expectTypeOf<ToolCall>().toHaveProperty("toolName");
|
|
235
|
+
expectTypeOf<ToolCall>().toHaveProperty("args");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("ClientEvent narrows on error discriminant", () => {
|
|
239
|
+
type ErrorEvent = Extract<ClientEvent, { type: "error" }>;
|
|
240
|
+
expectTypeOf<ErrorEvent>().toHaveProperty("code");
|
|
241
|
+
expectTypeOf<ErrorEvent>().toHaveProperty("message");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("ServerMessage has type property on all variants", () => {
|
|
245
|
+
expectTypeOf<ServerMessage>().toHaveProperty("type");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("lenientParse returns ok/error discriminated union", () => {
|
|
249
|
+
const schema = z.object({ type: z.literal("test"), value: z.number() });
|
|
250
|
+
type Parsed = z.infer<typeof schema>;
|
|
251
|
+
const result = lenientParse(schema, {});
|
|
252
|
+
expectTypeOf(result).toEqualTypeOf<
|
|
253
|
+
{ ok: true; data: Parsed } | { ok: false; malformed: boolean; error: string }
|
|
254
|
+
>();
|
|
255
|
+
});
|
|
256
|
+
});
|
package/sdk/protocol.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { MAX_TOOL_RESULT_CHARS } from "./constants.ts";
|
|
|
14
14
|
*
|
|
15
15
|
* All audio frames are 16-bit signed PCM, little-endian, mono.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
const AUDIO_FORMAT = "pcm16";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Minimal envelope schema for two-phase message parsing.
|
|
@@ -24,7 +24,7 @@ export const AUDIO_FORMAT = "pcm16";
|
|
|
24
24
|
* *unrecognised* type (safe to ignore during rolling upgrades) or genuinely
|
|
25
25
|
* malformed (should be warned about).
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
const MessageEnvelopeSchema = z.object({ type: z.string() }).passthrough();
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Two-phase message parse: tries the strict schema first, then falls back to
|
|
@@ -128,6 +128,11 @@ export const ClientEventSchema = z.discriminatedUnion("type", [
|
|
|
128
128
|
ev("reset"),
|
|
129
129
|
ev("idle_timeout"),
|
|
130
130
|
z.object({ type: z.literal("error"), code: SessionErrorCodeSchema, message: z.string() }),
|
|
131
|
+
z.object({
|
|
132
|
+
type: z.literal("custom_event"),
|
|
133
|
+
event: z.string().min(1),
|
|
134
|
+
data: z.unknown(),
|
|
135
|
+
}),
|
|
131
136
|
]);
|
|
132
137
|
|
|
133
138
|
/** Discriminated union of all server→client session events. */
|
|
@@ -151,9 +156,6 @@ export interface ClientSink {
|
|
|
151
156
|
|
|
152
157
|
// ─── WebSocket message types ────────────────────────────────────────────────
|
|
153
158
|
|
|
154
|
-
/** Supported audio formats for the wire protocol. */
|
|
155
|
-
export type AudioFormatId = "pcm16";
|
|
156
|
-
|
|
157
159
|
/** Zod schema for {@link ReadyConfig}. */
|
|
158
160
|
export const ReadyConfigSchema = z.object({
|
|
159
161
|
audioFormat: z.enum(["pcm16"]),
|
|
@@ -211,13 +213,3 @@ export function buildReadyConfig(s2sConfig: {
|
|
|
211
213
|
ttsSampleRate: s2sConfig.outputSampleRate,
|
|
212
214
|
};
|
|
213
215
|
}
|
|
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>;
|
|
@@ -10,12 +10,7 @@
|
|
|
10
10
|
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
11
11
|
import type { z } from "zod";
|
|
12
12
|
import { type AgentConfig, AgentConfigSchema, ToolSchemaSchema } from "./_internal-types.ts";
|
|
13
|
-
import {
|
|
14
|
-
type ReadyConfig,
|
|
15
|
-
ReadyConfigSchema,
|
|
16
|
-
type TurnConfig,
|
|
17
|
-
TurnConfigSchema,
|
|
18
|
-
} from "./protocol.ts";
|
|
13
|
+
import { type ReadyConfig, ReadyConfigSchema } from "./protocol.ts";
|
|
19
14
|
import { type BuiltinTool, BuiltinToolSchema, type ToolChoice, ToolChoiceSchema } from "./types.ts";
|
|
20
15
|
|
|
21
16
|
// ── AgentConfigSchema ────────────────────────────────────────────────────
|
|
@@ -105,27 +100,6 @@ describe("ToolSchemaSchema", () => {
|
|
|
105
100
|
});
|
|
106
101
|
});
|
|
107
102
|
|
|
108
|
-
// ── TurnConfigSchema ─────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
describe("TurnConfigSchema", () => {
|
|
111
|
-
test("accepts empty config", () => {
|
|
112
|
-
expect(TurnConfigSchema.parse({})).toEqual({});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("accepts full config", () => {
|
|
116
|
-
const full: TurnConfig = { maxSteps: 3 };
|
|
117
|
-
expect(TurnConfigSchema.parse(full)).toEqual(full);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("rejects non-positive maxSteps", () => {
|
|
121
|
-
expect(TurnConfigSchema.safeParse({ maxSteps: 0 }).success).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("type derived from schema matches TurnConfig", () => {
|
|
125
|
-
expectTypeOf<z.infer<typeof TurnConfigSchema>>().toEqualTypeOf<TurnConfig>();
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
103
|
// ── ReadyConfigSchema ────────────────────────────────────────────────────
|
|
130
104
|
|
|
131
105
|
describe("ReadyConfigSchema", () => {
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
import { describe, expect, test } from "vitest";
|
|
16
|
+
import { AgentConfigSchema, ToolSchemaSchema } from "./_internal-types.ts";
|
|
17
|
+
import {
|
|
18
|
+
ClientEventSchema,
|
|
19
|
+
ClientMessageSchema,
|
|
20
|
+
KvDelSchema,
|
|
21
|
+
KvGetSchema,
|
|
22
|
+
KvSetSchema,
|
|
23
|
+
ReadyConfigSchema,
|
|
24
|
+
ServerMessageSchema,
|
|
25
|
+
} from "./protocol.ts";
|
|
26
|
+
|
|
27
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
type ZodObjectLike = { shape: Record<string, unknown> };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract shape keys (field names) from a ZodObject-like schema.
|
|
33
|
+
*/
|
|
34
|
+
function shapeKeys(schema: ZodObjectLike): string[] {
|
|
35
|
+
return Object.keys(schema.shape).sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
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
|
+
function discriminatedUnionShapes(schema: {
|
|
44
|
+
options: Array<{ shape: Record<string, unknown> }>;
|
|
45
|
+
}): Record<string, string[]> {
|
|
46
|
+
const result: Record<string, string[]> = {};
|
|
47
|
+
for (const option of schema.options) {
|
|
48
|
+
const typeSchema = option.shape?.type as
|
|
49
|
+
| { _def?: { value?: string; values?: string[] } }
|
|
50
|
+
| undefined;
|
|
51
|
+
const def = typeSchema?._def;
|
|
52
|
+
// Zod v4 uses `.values` (array); Zod v3 uses `.value` (scalar)
|
|
53
|
+
const raw = def?.values ?? def?.value;
|
|
54
|
+
const discriminatorValue = Array.isArray(raw) ? raw[0] : raw;
|
|
55
|
+
const key = String(discriminatorValue ?? "unknown");
|
|
56
|
+
result[key] = Object.keys(option.shape).sort();
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Protocol schemas ─────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe("protocol schema shapes", () => {
|
|
64
|
+
test("ClientEventSchema option shapes", () => {
|
|
65
|
+
expect(discriminatedUnionShapes(ClientEventSchema)).toMatchSnapshot();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("ServerMessageSchema option shapes", () => {
|
|
69
|
+
expect(discriminatedUnionShapes(ServerMessageSchema)).toMatchSnapshot();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("ClientMessageSchema option shapes", () => {
|
|
73
|
+
expect(discriminatedUnionShapes(ClientMessageSchema)).toMatchSnapshot();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("ReadyConfigSchema shape", () => {
|
|
77
|
+
expect(shapeKeys(ReadyConfigSchema)).toMatchSnapshot();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("KvGetSchema shape", () => {
|
|
81
|
+
expect(shapeKeys(KvGetSchema)).toMatchSnapshot();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("KvSetSchema shape", () => {
|
|
85
|
+
expect(shapeKeys(KvSetSchema)).toMatchSnapshot();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("KvDelSchema shape", () => {
|
|
89
|
+
expect(shapeKeys(KvDelSchema)).toMatchSnapshot();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Manifest schemas ──────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
describe("manifest schema shapes", () => {
|
|
96
|
+
test("AgentConfigSchema shape", () => {
|
|
97
|
+
expect(shapeKeys(AgentConfigSchema)).toMatchSnapshot();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("ToolSchemaSchema shape", () => {
|
|
101
|
+
expect(shapeKeys(ToolSchemaSchema)).toMatchSnapshot();
|
|
102
|
+
});
|
|
103
|
+
});
|
package/sdk/tsconfig.json
CHANGED
package/sdk/types.test.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
1
|
+
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { AgentDef, Kv, ToolDef } from "../index.ts";
|
|
4
|
+
import { agent, tool } from "../index.ts";
|
|
2
5
|
import { DEFAULT_GREETING, DEFAULT_SYSTEM_PROMPT } from "./types.ts";
|
|
3
6
|
|
|
4
7
|
describe("constants", () => {
|
|
@@ -12,3 +15,55 @@ describe("constants", () => {
|
|
|
12
15
|
expect(DEFAULT_GREETING.length).toBeGreaterThan(0);
|
|
13
16
|
});
|
|
14
17
|
});
|
|
18
|
+
|
|
19
|
+
describe("type contracts", () => {
|
|
20
|
+
test("agent() returns AgentDef", () => {
|
|
21
|
+
const def = agent({ name: "test" });
|
|
22
|
+
expectTypeOf(def).toEqualTypeOf<AgentDef>();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("tool() infers parameter type from Zod schema", () => {
|
|
26
|
+
const params = z.object({ city: z.string() });
|
|
27
|
+
const t = tool({
|
|
28
|
+
description: "weather",
|
|
29
|
+
parameters: params,
|
|
30
|
+
execute: (args) => {
|
|
31
|
+
expectTypeOf(args).toEqualTypeOf<{ city: string }>();
|
|
32
|
+
return "ok";
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
expectTypeOf(t).toMatchTypeOf<ToolDef<typeof params>>();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("tool() works without parameters", () => {
|
|
39
|
+
const t = tool({ description: "no params", execute: () => "ok" });
|
|
40
|
+
expectTypeOf(t).toMatchTypeOf<ToolDef>();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("agent() accepts tools record", () => {
|
|
44
|
+
const t = tool({
|
|
45
|
+
description: "echo",
|
|
46
|
+
parameters: z.object({ msg: z.string() }),
|
|
47
|
+
execute: ({ msg }) => msg,
|
|
48
|
+
});
|
|
49
|
+
const def = agent({ name: "with-tools", tools: { echo: t } });
|
|
50
|
+
expectTypeOf(def).toEqualTypeOf<AgentDef>();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("Kv.get returns Promise<unknown> by default", () => {
|
|
54
|
+
expectTypeOf<Kv["get"]>().returns.toEqualTypeOf<Promise<unknown>>();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("Kv.set accepts various value types", () => {
|
|
58
|
+
expectTypeOf<Kv["set"]>().toBeCallableWith("key", "string-value");
|
|
59
|
+
expectTypeOf<Kv["set"]>().toBeCallableWith("key", 42);
|
|
60
|
+
expectTypeOf<Kv["set"]>().toBeCallableWith("key", { nested: true });
|
|
61
|
+
expectTypeOf<Kv["set"]>().returns.toEqualTypeOf<Promise<void>>();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("Kv.delete accepts string or string[]", () => {
|
|
65
|
+
expectTypeOf<Kv["delete"]>().toBeCallableWith("single-key");
|
|
66
|
+
expectTypeOf<Kv["delete"]>().toBeCallableWith(["key1", "key2"]);
|
|
67
|
+
expectTypeOf<Kv["delete"]>().returns.toEqualTypeOf<Promise<void>>();
|
|
68
|
+
});
|
|
69
|
+
});
|
package/sdk/types.ts
CHANGED
|
@@ -83,6 +83,8 @@ export type ToolContext<S = Record<string, unknown>> = {
|
|
|
83
83
|
messages: readonly Message[];
|
|
84
84
|
/** Unique identifier for the current session. Useful for correlating logs across concurrent sessions. */
|
|
85
85
|
sessionId: string;
|
|
86
|
+
/** Push a custom event to the connected browser client. Fire-and-forget. */
|
|
87
|
+
send(event: string, data: unknown): void;
|
|
86
88
|
};
|
|
87
89
|
|
|
88
90
|
/**
|
package/sdk/ws-upgrade.test.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
2
|
|
|
3
|
-
import { describe, expect,
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
4
|
import { parseWsUpgradeParams } from "./ws-upgrade.ts";
|
|
5
5
|
|
|
6
6
|
describe("parseWsUpgradeParams", () => {
|
|
7
|
-
|
|
7
|
+
test("returns defaults for URL with no query params", () => {
|
|
8
8
|
const result = parseWsUpgradeParams("/websocket");
|
|
9
9
|
expect(result).toEqual({ resumeFrom: undefined, skipGreeting: false });
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
test("extracts sessionId and sets skipGreeting", () => {
|
|
13
13
|
const result = parseWsUpgradeParams("/ws?sessionId=abc-123");
|
|
14
14
|
expect(result.resumeFrom).toBe("abc-123");
|
|
15
15
|
expect(result.skipGreeting).toBe(true);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
test("sets skipGreeting when resume param is present", () => {
|
|
19
19
|
const result = parseWsUpgradeParams("/ws?resume=1");
|
|
20
20
|
expect(result.resumeFrom).toBeUndefined();
|
|
21
21
|
expect(result.skipGreeting).toBe(true);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
test("sessionId takes precedence for resumeFrom", () => {
|
|
25
25
|
const result = parseWsUpgradeParams("/ws?resume=1&sessionId=sess-42");
|
|
26
26
|
expect(result.resumeFrom).toBe("sess-42");
|
|
27
27
|
expect(result.skipGreeting).toBe(true);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
test("handles URL with no query string", () => {
|
|
31
31
|
const result = parseWsUpgradeParams("/websocket");
|
|
32
32
|
expect(result.resumeFrom).toBeUndefined();
|
|
33
33
|
expect(result.skipGreeting).toBe(false);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
test("handles full URL with query params", () => {
|
|
37
37
|
const result = parseWsUpgradeParams("ws://localhost:3000/websocket?sessionId=s1");
|
|
38
38
|
expect(result.resumeFrom).toBe("s1");
|
|
39
39
|
expect(result.skipGreeting).toBe(true);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
test("handles empty sessionId", () => {
|
|
43
43
|
const result = parseWsUpgradeParams("/ws?sessionId=");
|
|
44
44
|
// Empty string from URLSearchParams.get is truthy for ?? check
|
|
45
45
|
expect(result.resumeFrom).toBe("");
|
package/tsconfig.build.json
CHANGED
|
@@ -10,5 +10,12 @@
|
|
|
10
10
|
"allowImportingTsExtensions": false,
|
|
11
11
|
"rewriteRelativeImportExtensions": true
|
|
12
12
|
},
|
|
13
|
-
"exclude": [
|
|
13
|
+
"exclude": [
|
|
14
|
+
"node_modules",
|
|
15
|
+
"dist",
|
|
16
|
+
"**/*.test.ts",
|
|
17
|
+
"**/*.test-d.ts",
|
|
18
|
+
"**/_test-*.ts",
|
|
19
|
+
"vitest*.config.ts"
|
|
20
|
+
]
|
|
14
21
|
}
|
package/tsconfig.json
CHANGED
package/vitest.config.ts
CHANGED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { i as ToolChoiceSchema, r as DEFAULT_SYSTEM_PROMPT, t as BuiltinToolSchema } from "./types-Cfx_4QDK.js";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
//#region sdk/_internal-types.ts
|
|
4
|
-
/**
|
|
5
|
-
* Zod schema for serializable agent configuration sent over the wire.
|
|
6
|
-
*
|
|
7
|
-
* This is the JSON-safe subset of the agent definition that can be
|
|
8
|
-
* transmitted between the worker and the host process via structured clone.
|
|
9
|
-
*/
|
|
10
|
-
const AgentConfigSchema = z.object({
|
|
11
|
-
name: z.string().min(1),
|
|
12
|
-
systemPrompt: z.string(),
|
|
13
|
-
greeting: z.string(),
|
|
14
|
-
sttPrompt: z.string().optional(),
|
|
15
|
-
maxSteps: z.number().int().positive().optional(),
|
|
16
|
-
toolChoice: ToolChoiceSchema.optional(),
|
|
17
|
-
builtinTools: z.array(BuiltinToolSchema).readonly().optional(),
|
|
18
|
-
idleTimeoutMs: z.number().nonnegative().optional()
|
|
19
|
-
});
|
|
20
|
-
/** Extract the serializable {@link AgentConfig} subset from a source object. */
|
|
21
|
-
function toAgentConfig(src) {
|
|
22
|
-
const config = {
|
|
23
|
-
name: src.name,
|
|
24
|
-
systemPrompt: src.systemPrompt,
|
|
25
|
-
greeting: src.greeting
|
|
26
|
-
};
|
|
27
|
-
if (src.sttPrompt !== void 0) config.sttPrompt = src.sttPrompt;
|
|
28
|
-
if (src.maxSteps !== void 0) config.maxSteps = src.maxSteps;
|
|
29
|
-
if (src.toolChoice !== void 0) config.toolChoice = src.toolChoice;
|
|
30
|
-
if (src.builtinTools) config.builtinTools = [...src.builtinTools];
|
|
31
|
-
if (src.idleTimeoutMs !== void 0) config.idleTimeoutMs = src.idleTimeoutMs;
|
|
32
|
-
return config;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Zod schema for serialized tool definitions sent over the wire.
|
|
36
|
-
*
|
|
37
|
-
* `parameters` must be a valid JSON Schema object (with `type`, `properties`,
|
|
38
|
-
* etc.) — the Vercel AI SDK wraps it via `jsonSchema()`.
|
|
39
|
-
*/
|
|
40
|
-
const ToolSchemaSchema = z.object({
|
|
41
|
-
name: z.string().min(1),
|
|
42
|
-
description: z.string().min(1),
|
|
43
|
-
parameters: z.record(z.string(), z.unknown())
|
|
44
|
-
});
|
|
45
|
-
/** Empty Zod object schema used as default when tools have no parameters. */
|
|
46
|
-
const EMPTY_PARAMS = z.object({});
|
|
47
|
-
/**
|
|
48
|
-
* Convert agent tool definitions to JSON Schema format for wire transport.
|
|
49
|
-
*
|
|
50
|
-
* Transforms the Zod-based `parameters` of each tool into a plain JSON Schema
|
|
51
|
-
* object suitable for structured clone / JSON serialization.
|
|
52
|
-
*/
|
|
53
|
-
function agentToolsToSchemas(tools) {
|
|
54
|
-
return Object.entries(tools).map(([name, def]) => ({
|
|
55
|
-
name,
|
|
56
|
-
description: def.description,
|
|
57
|
-
parameters: z.toJSONSchema(def.parameters ?? EMPTY_PARAMS)
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
//#endregion
|
|
61
|
-
//#region sdk/system-prompt.ts
|
|
62
|
-
function getFormattedDate() {
|
|
63
|
-
return (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
64
|
-
weekday: "long",
|
|
65
|
-
year: "numeric",
|
|
66
|
-
month: "long",
|
|
67
|
-
day: "numeric"
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const VOICE_RULES = "\n\nCRITICAL OUTPUT RULES — you MUST follow these for EVERY response:\nYour response will be spoken aloud by a TTS system and displayed as plain text.\n- NEVER use markdown: no **, no *, no _, no #, no `, no [](), no ---\n- NEVER use bullet points (-, *, •) or numbered lists (1., 2.)\n- NEVER use code blocks or inline code\n- NEVER mention tools, search, APIs, or technical failures to the user. If a tool returns no results, just answer naturally without explaining why.\n- Write exactly as you would say it out loud to a friend\n- Use short conversational sentences. To list things, say \"First,\" \"Next,\" \"Finally,\"\n- Keep responses concise — 1 to 3 sentences max";
|
|
71
|
-
/**
|
|
72
|
-
* Build the system prompt sent to the LLM from the agent configuration.
|
|
73
|
-
*
|
|
74
|
-
* Assembles the default system prompt, today's date, agent-specific instructions,
|
|
75
|
-
* and optional sections for tool usage preamble and voice output rules.
|
|
76
|
-
*
|
|
77
|
-
* @param config - The serializable agent configuration (name, systemPrompt, etc.).
|
|
78
|
-
* @param opts.hasTools - When `true`, appends a preamble instructing the LLM to
|
|
79
|
-
* speak a brief phrase before each tool call to fill silence.
|
|
80
|
-
* @param opts.voice - When `true`, appends strict voice-specific output rules
|
|
81
|
-
* (no markdown, no bullet points, conversational tone, concise responses).
|
|
82
|
-
* @returns The assembled system prompt string.
|
|
83
|
-
*/
|
|
84
|
-
function buildSystemPrompt(config, opts) {
|
|
85
|
-
const { hasTools } = opts;
|
|
86
|
-
const agentInstructions = config.systemPrompt && config.systemPrompt !== DEFAULT_SYSTEM_PROMPT ? `\n\nAgent-Specific Instructions:\n${config.systemPrompt}` : "";
|
|
87
|
-
const toolPreamble = hasTools ? "\n\nWhen you decide to use a tool, ALWAYS say a brief natural phrase BEFORE the tool call (e.g. \"Let me look that up\" or \"One moment while I check\"). This fills silence while the tool executes. Keep preambles to one short sentence." : "";
|
|
88
|
-
const guidance = opts.toolGuidance && opts.toolGuidance.length > 0 ? `\n\nBuilt-in Tool Usage:\n${opts.toolGuidance.join("\n")}` : "";
|
|
89
|
-
return DEFAULT_SYSTEM_PROMPT + `\n\nToday's date is ${getFormattedDate()}.` + agentInstructions + toolPreamble + guidance + (opts.voice ? VOICE_RULES : "");
|
|
90
|
-
}
|
|
91
|
-
//#endregion
|
|
92
|
-
export { agentToolsToSchemas as a, ToolSchemaSchema as i, AgentConfigSchema as n, toAgentConfig as o, EMPTY_PARAMS as r, buildSystemPrompt as t };
|