@alexkroman1/aai 0.12.3 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +20 -0
- package/CHANGELOG.md +176 -0
- package/dist/constants-VTFoymJ-.js +47 -0
- package/dist/host/_run-code.d.ts +1 -1
- package/dist/host/_runtime-conformance.d.ts +4 -5
- package/dist/host/builtin-tools.d.ts +11 -9
- package/dist/host/runtime-barrel.d.ts +15 -0
- package/dist/{direct-executor-DRRrZUp0.js → host/runtime-barrel.js} +453 -348
- package/dist/host/runtime-config.d.ts +42 -0
- package/dist/host/runtime.d.ts +119 -35
- package/dist/host/s2s.d.ts +14 -38
- package/dist/host/server.d.ts +16 -8
- package/dist/host/session-ctx.d.ts +55 -0
- package/dist/host/session.d.ts +20 -70
- package/dist/host/tool-executor.d.ts +20 -0
- package/dist/host/unstorage-kv.d.ts +1 -1
- package/dist/host/ws-handler.d.ts +4 -2
- package/dist/index.d.ts +9 -20
- package/dist/index.js +63 -2
- package/dist/{isolate → sdk}/_internal-types.d.ts +5 -9
- package/dist/{isolate → sdk}/constants.d.ts +6 -4
- package/dist/sdk/define.d.ts +66 -0
- package/dist/{isolate → sdk}/kv.d.ts +1 -49
- package/dist/sdk/manifest-barrel.d.ts +8 -0
- package/dist/sdk/manifest-barrel.js +52 -0
- package/dist/sdk/manifest.d.ts +50 -0
- package/dist/{isolate → sdk}/protocol.d.ts +59 -36
- package/dist/sdk/protocol.js +163 -0
- package/dist/{isolate → sdk}/system-prompt.d.ts +2 -2
- package/dist/sdk/types.d.ts +201 -0
- package/dist/sdk/ws-upgrade.d.ts +5 -0
- package/dist/{system-prompt-DYAYFW99.js → system-prompt-nik_iavo.js} +10 -10
- package/dist/types-Cfx_4QDK.js +39 -0
- package/dist/ws-upgrade-BeOQ7fXL.js +30 -0
- package/exports-no-dev-deps.test.ts +62 -0
- package/host/_mock-ws.ts +185 -0
- package/host/_run-code.ts +217 -0
- package/host/_runtime-conformance.ts +143 -0
- package/host/_test-utils.ts +276 -0
- package/host/builtin-tools.test.ts +774 -0
- package/host/builtin-tools.ts +255 -0
- package/host/cleanup.test.ts +422 -0
- package/host/fixture-replay.test.ts +463 -0
- package/host/fixtures/README.md +40 -0
- package/host/fixtures/greeting-session-sequence.json +40 -0
- package/host/fixtures/reply-audio-samples.json +42 -0
- package/host/fixtures/reply-lifecycle.json +21 -0
- package/host/fixtures/session-ready.json +48 -0
- package/host/fixtures/session-updated.json +45 -0
- package/host/fixtures/simple-question-sequence.json +73 -0
- package/host/fixtures/tool-call-sequence.json +114 -0
- package/host/fixtures/tool-calls.json +11 -0
- package/host/fixtures/tool-config-session-sequence.json +51 -0
- package/host/fixtures/user-speech-recognition.json +30 -0
- package/host/fixtures/web-search-sequence.json +122 -0
- package/host/integration.test.ts +222 -0
- package/host/runtime-barrel.ts +25 -0
- package/host/runtime-config.test.ts +71 -0
- package/host/runtime-config.ts +99 -0
- package/host/runtime.test.ts +641 -0
- package/host/runtime.ts +308 -0
- package/host/s2s-fixtures.test.ts +237 -0
- package/host/s2s.test.ts +562 -0
- package/host/s2s.ts +310 -0
- package/host/server-shutdown.test.ts +76 -0
- package/host/server.test.ts +116 -0
- package/host/server.ts +223 -0
- package/host/session-ctx.ts +107 -0
- package/host/session-fixture-replay.test.ts +136 -0
- package/host/session-prompt.test.ts +77 -0
- package/host/session.test.ts +590 -0
- package/host/session.ts +370 -0
- package/host/tool-executor.test.ts +124 -0
- package/host/tool-executor.ts +80 -0
- package/host/unstorage-kv.test.ts +99 -0
- package/host/unstorage-kv.ts +69 -0
- package/host/ws-handler.test.ts +739 -0
- package/host/ws-handler.ts +255 -0
- package/index.ts +16 -0
- package/package.json +24 -72
- package/sdk/_internal-types.test.ts +34 -0
- package/sdk/_internal-types.ts +115 -0
- package/sdk/compat-fixtures/README.md +26 -0
- package/sdk/compat-fixtures/v1.json +68 -0
- package/sdk/constants.ts +77 -0
- package/sdk/define.test.ts +57 -0
- package/sdk/define.ts +88 -0
- package/sdk/kv.ts +60 -0
- package/sdk/manifest-barrel.ts +12 -0
- package/sdk/manifest.test.ts +56 -0
- package/sdk/manifest.ts +89 -0
- package/sdk/protocol-compat.test.ts +187 -0
- package/sdk/protocol-snapshot.test.ts +199 -0
- package/sdk/protocol.test.ts +170 -0
- package/sdk/protocol.ts +223 -0
- package/sdk/schema-alignment.test.ts +191 -0
- package/sdk/system-prompt.test.ts +111 -0
- package/sdk/system-prompt.ts +74 -0
- package/sdk/tsconfig.json +12 -0
- package/sdk/types-inference.test.ts +122 -0
- package/sdk/types.test.ts +14 -0
- package/sdk/types.ts +226 -0
- package/sdk/utils.test.ts +52 -0
- package/sdk/utils.ts +20 -0
- package/sdk/ws-upgrade.test.ts +48 -0
- package/sdk/ws-upgrade.ts +13 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/dist/host/_test-utils.d.ts +0 -73
- package/dist/host/direct-executor.d.ts +0 -130
- package/dist/host/index.d.ts +0 -19
- package/dist/host/index.js +0 -165
- package/dist/host/matchers.d.ts +0 -20
- package/dist/host/matchers.js +0 -41
- package/dist/host/server.js +0 -164
- package/dist/host/testing.d.ts +0 -294
- package/dist/host/testing.js +0 -2
- package/dist/host/vite-plugin.d.ts +0 -15
- package/dist/host/vite-plugin.js +0 -83
- package/dist/isolate/_kv-utils.d.ts +0 -10
- package/dist/isolate/_utils.js +0 -17
- package/dist/isolate/hooks.d.ts +0 -44
- package/dist/isolate/hooks.js +0 -58
- package/dist/isolate/index.d.ts +0 -18
- package/dist/isolate/index.js +0 -6
- package/dist/isolate/kv.js +0 -1
- package/dist/isolate/protocol.js +0 -2
- package/dist/isolate/types.d.ts +0 -418
- package/dist/isolate/types.js +0 -175
- package/dist/protocol-rcOrz7T3.js +0 -183
- package/dist/testing-BreLdpq-.js +0 -513
- package/dist/types.test-d.d.ts +0 -7
- /package/dist/{isolate/_utils.d.ts → sdk/utils.d.ts} +0 -0
package/sdk/constants.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Centralised numeric constants — timeouts, size limits, sample rates.
|
|
4
|
+
*
|
|
5
|
+
* Every magic number that controls a timeout, buffer size, or threshold
|
|
6
|
+
* lives here so the values are discoverable in one place.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── Audio ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Default sample rate for speech-to-text audio in Hz (AssemblyAI). */
|
|
12
|
+
export const DEFAULT_STT_SAMPLE_RATE = 16_000;
|
|
13
|
+
|
|
14
|
+
/** Default sample rate for text-to-speech audio in Hz. */
|
|
15
|
+
export const DEFAULT_TTS_SAMPLE_RATE = 24_000;
|
|
16
|
+
|
|
17
|
+
// ─── Timeouts (ms) ───────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Default timeout for tool execution in the worker. */
|
|
20
|
+
export const TOOL_EXECUTION_TIMEOUT_MS = 30_000;
|
|
21
|
+
|
|
22
|
+
/** Timeout for session.start() (S2S connection setup). */
|
|
23
|
+
export const DEFAULT_SESSION_START_TIMEOUT_MS = 10_000;
|
|
24
|
+
|
|
25
|
+
/** S2S session idle timeout before auto-close. */
|
|
26
|
+
export const DEFAULT_IDLE_TIMEOUT_MS = 300_000; // 5 minutes
|
|
27
|
+
|
|
28
|
+
/** Per-fetch timeout for network tools (web_search, visit_webpage, fetch_json). */
|
|
29
|
+
export const FETCH_TIMEOUT_MS = 15_000;
|
|
30
|
+
|
|
31
|
+
/** Timeout for sandboxed run_code execution. */
|
|
32
|
+
export const RUN_CODE_TIMEOUT_MS = 5000;
|
|
33
|
+
|
|
34
|
+
/** Maximum time to wait for sessions to stop during graceful shutdown. */
|
|
35
|
+
export const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
36
|
+
|
|
37
|
+
// ─── Size / length limits ────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Maximum length for tool result strings sent to clients. */
|
|
40
|
+
export const MAX_TOOL_RESULT_CHARS = 4000;
|
|
41
|
+
|
|
42
|
+
/** Maximum chars for webpage text after HTML-to-text conversion. */
|
|
43
|
+
export const MAX_PAGE_CHARS = 10_000;
|
|
44
|
+
|
|
45
|
+
/** Maximum bytes to fetch from an HTML page before conversion. */
|
|
46
|
+
export const MAX_HTML_BYTES = 200_000;
|
|
47
|
+
|
|
48
|
+
/** Maximum value size for KV store entries (bytes). */
|
|
49
|
+
export const MAX_VALUE_SIZE = 65_536;
|
|
50
|
+
|
|
51
|
+
/** Maximum conversation messages to retain (sliding window). */
|
|
52
|
+
export const DEFAULT_MAX_HISTORY = 200;
|
|
53
|
+
|
|
54
|
+
/** Maximum WebSocket message payload size (bytes, 1 MiB). */
|
|
55
|
+
export const MAX_WS_PAYLOAD_BYTES = 1 * 1024 * 1024;
|
|
56
|
+
|
|
57
|
+
/** Maximum messages buffered while session.start() is pending. */
|
|
58
|
+
export const MAX_MESSAGE_BUFFER_SIZE = 100;
|
|
59
|
+
|
|
60
|
+
// ─── WebSocket ──────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/** WebSocket readyState value indicating the connection is open. */
|
|
63
|
+
export const WS_OPEN = 1;
|
|
64
|
+
|
|
65
|
+
// ─── Security ───────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Content-Security-Policy applied to agent UI pages (both self-hosted and
|
|
69
|
+
* platform). Single source of truth — used by `secureHeaders` middleware
|
|
70
|
+
* and per-response CSP headers.
|
|
71
|
+
*/
|
|
72
|
+
export const AGENT_CSP =
|
|
73
|
+
"default-src 'self'; script-src 'self' 'unsafe-eval' blob:; " +
|
|
74
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
|
|
75
|
+
"connect-src 'self' wss: ws:; img-src 'self' data:; " +
|
|
76
|
+
"font-src 'self' https://fonts.gstatic.com; " +
|
|
77
|
+
"object-src 'none'; base-uri 'self'";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { agent, tool } from "./define.ts";
|
|
5
|
+
|
|
6
|
+
describe("tool()", () => {
|
|
7
|
+
test("returns the definition unchanged", () => {
|
|
8
|
+
const def = tool({
|
|
9
|
+
description: "Greet someone",
|
|
10
|
+
parameters: z.object({ name: z.string() }),
|
|
11
|
+
execute: ({ name }) => `Hello, ${name}!`,
|
|
12
|
+
});
|
|
13
|
+
expect(def.description).toBe("Greet someone");
|
|
14
|
+
expect(def.execute({ name: "Alice" }, {} as never)).toBe("Hello, Alice!");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("works without parameters", () => {
|
|
18
|
+
const def = tool({
|
|
19
|
+
description: "No-param tool",
|
|
20
|
+
execute: () => "done",
|
|
21
|
+
});
|
|
22
|
+
expect(def.description).toBe("No-param tool");
|
|
23
|
+
expect(def.parameters).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("agent()", () => {
|
|
28
|
+
test("applies defaults", () => {
|
|
29
|
+
const def = agent({ name: "Test Agent" });
|
|
30
|
+
expect(def.name).toBe("Test Agent");
|
|
31
|
+
expect(def.systemPrompt).toContain("You are AAI");
|
|
32
|
+
expect(def.greeting).toContain("Hey there");
|
|
33
|
+
expect(def.maxSteps).toBe(5);
|
|
34
|
+
expect(def.tools).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("preserves explicit values", () => {
|
|
38
|
+
const greetTool = tool({
|
|
39
|
+
description: "Greet",
|
|
40
|
+
parameters: z.object({ name: z.string() }),
|
|
41
|
+
execute: ({ name }) => `Hi ${name}`,
|
|
42
|
+
});
|
|
43
|
+
const def = agent({
|
|
44
|
+
name: "Custom",
|
|
45
|
+
systemPrompt: "Be nice.",
|
|
46
|
+
greeting: "Hello!",
|
|
47
|
+
maxSteps: 10,
|
|
48
|
+
tools: { greet: greetTool },
|
|
49
|
+
builtinTools: ["web_search"],
|
|
50
|
+
});
|
|
51
|
+
expect(def.systemPrompt).toBe("Be nice.");
|
|
52
|
+
expect(def.greeting).toBe("Hello!");
|
|
53
|
+
expect(def.maxSteps).toBe(10);
|
|
54
|
+
expect(def.tools.greet).toBe(greetTool);
|
|
55
|
+
expect(def.builtinTools).toEqual(["web_search"]);
|
|
56
|
+
});
|
|
57
|
+
});
|
package/sdk/define.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Helper functions for defining agents and tools with full type inference.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
type AgentDef,
|
|
9
|
+
type BuiltinTool,
|
|
10
|
+
DEFAULT_GREETING,
|
|
11
|
+
DEFAULT_SYSTEM_PROMPT,
|
|
12
|
+
type ToolChoice,
|
|
13
|
+
type ToolContext,
|
|
14
|
+
type ToolDef,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Define a tool with typed parameters and execute function.
|
|
19
|
+
*
|
|
20
|
+
* Identity function for type inference — returns the input unchanged.
|
|
21
|
+
* Follows the Vercel AI SDK `tool()` pattern.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { tool } from "@alexkroman1/aai";
|
|
26
|
+
* import { z } from "zod";
|
|
27
|
+
*
|
|
28
|
+
* const greet = tool({
|
|
29
|
+
* description: "Greet someone by name",
|
|
30
|
+
* parameters: z.object({ name: z.string() }),
|
|
31
|
+
* execute: ({ name }) => `Hello, ${name}!`,
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export function tool<P extends z.ZodObject<z.ZodRawShape>>(def: {
|
|
38
|
+
description: string;
|
|
39
|
+
parameters?: P;
|
|
40
|
+
execute(args: z.infer<P>, ctx: ToolContext): Promise<unknown> | unknown;
|
|
41
|
+
}): ToolDef<P> {
|
|
42
|
+
return def;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Define an agent with tools, system prompt, and configuration.
|
|
47
|
+
*
|
|
48
|
+
* Applies sensible defaults for omitted fields. Export as the default
|
|
49
|
+
* export of your `agent.ts` file.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { agent, tool } from "@alexkroman1/aai";
|
|
54
|
+
* import { z } from "zod";
|
|
55
|
+
*
|
|
56
|
+
* const myTool = tool({
|
|
57
|
+
* description: "Echo a message",
|
|
58
|
+
* parameters: z.object({ message: z.string() }),
|
|
59
|
+
* execute: ({ message }) => message,
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* export default agent({
|
|
63
|
+
* name: "Echo Agent",
|
|
64
|
+
* tools: { echo: myTool },
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export function agent(def: {
|
|
71
|
+
name: string;
|
|
72
|
+
systemPrompt?: string;
|
|
73
|
+
greeting?: string;
|
|
74
|
+
tools?: Record<string, ToolDef>;
|
|
75
|
+
builtinTools?: BuiltinTool[];
|
|
76
|
+
maxSteps?: number;
|
|
77
|
+
toolChoice?: ToolChoice;
|
|
78
|
+
sttPrompt?: string;
|
|
79
|
+
idleTimeoutMs?: number;
|
|
80
|
+
}): AgentDef {
|
|
81
|
+
return {
|
|
82
|
+
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
83
|
+
greeting: DEFAULT_GREETING,
|
|
84
|
+
maxSteps: 5,
|
|
85
|
+
tools: {},
|
|
86
|
+
...def,
|
|
87
|
+
};
|
|
88
|
+
}
|
package/sdk/kv.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Key-value storage interface and shared utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Async key-value store interface used by agents.
|
|
8
|
+
*
|
|
9
|
+
* Agents access the KV store via `ToolContext.kv`. Values are JSON-serialized and stored as
|
|
10
|
+
* strings with an optional TTL.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Inside a tool execute function:
|
|
15
|
+
* const myTool = {
|
|
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
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
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
|
+
*/
|
|
36
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Set a value, optionally with a TTL in milliseconds.
|
|
39
|
+
*
|
|
40
|
+
* @param key - The key to store the value under.
|
|
41
|
+
* @param value - The value to store. Must be JSON-serializable.
|
|
42
|
+
* @param options - Optional settings. `expireIn` sets the time-to-live in **milliseconds**
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
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
|
+
*/
|
|
52
|
+
delete(keys: string | string[]): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Close the KV store, releasing any resources (intervals, database handles).
|
|
55
|
+
*
|
|
56
|
+
* After calling `close()`, the store must not be used. This is a no-op
|
|
57
|
+
* for implementations that hold no resources (e.g. in-memory stores).
|
|
58
|
+
*/
|
|
59
|
+
close?(): void;
|
|
60
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Manifest barrel — agent manifest parsing and tool schema conversion.
|
|
4
|
+
*
|
|
5
|
+
* Used by aai-cli (scanner, bundler) and aai-server (tests).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// biome-ignore-all lint/performance/noReExportAll: barrel file by design
|
|
9
|
+
|
|
10
|
+
export * from "./_internal-types.ts";
|
|
11
|
+
export * from "./manifest.ts";
|
|
12
|
+
export * from "./system-prompt.ts";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { parseManifest } from "./manifest.ts";
|
|
4
|
+
|
|
5
|
+
describe("parseManifest", () => {
|
|
6
|
+
test("minimal manifest requires only name", () => {
|
|
7
|
+
const result = parseManifest({ name: "Simple Agent" });
|
|
8
|
+
expect(result).toEqual({
|
|
9
|
+
name: "Simple Agent",
|
|
10
|
+
systemPrompt: expect.any(String),
|
|
11
|
+
greeting: expect.any(String),
|
|
12
|
+
maxSteps: 5,
|
|
13
|
+
toolChoice: "auto",
|
|
14
|
+
builtinTools: [],
|
|
15
|
+
tools: {},
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("full manifest passes through all fields", () => {
|
|
20
|
+
const input = {
|
|
21
|
+
name: "Weather Agent",
|
|
22
|
+
systemPrompt: "You are a weather bot.",
|
|
23
|
+
greeting: "What city?",
|
|
24
|
+
sttPrompt: "Celsius, Fahrenheit",
|
|
25
|
+
builtinTools: ["web_search"],
|
|
26
|
+
maxSteps: 10,
|
|
27
|
+
toolChoice: "required" as const,
|
|
28
|
+
idleTimeoutMs: 60_000,
|
|
29
|
+
theme: { bg: "#000", primary: "#fff" },
|
|
30
|
+
tools: {
|
|
31
|
+
get_weather: {
|
|
32
|
+
description: "Get weather",
|
|
33
|
+
parameters: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: { city: { type: "string" } },
|
|
36
|
+
required: ["city"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const result = parseManifest(input);
|
|
42
|
+
expect(result.name).toBe("Weather Agent");
|
|
43
|
+
expect(result.systemPrompt).toBe("You are a weather bot.");
|
|
44
|
+
expect(result.tools.get_weather?.description).toBe("Get weather");
|
|
45
|
+
expect(result.maxSteps).toBe(10);
|
|
46
|
+
expect(result.toolChoice).toBe("required");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("rejects manifest without name", () => {
|
|
50
|
+
expect(() => parseManifest({})).toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("rejects unknown builtinTools", () => {
|
|
54
|
+
expect(() => parseManifest({ name: "X", builtinTools: ["not_a_tool"] })).toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/sdk/manifest.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Canonical manifest format for directory-based agents.
|
|
4
|
+
*
|
|
5
|
+
* Flows from build → host → sdk. Validated via Zod at the boundary,
|
|
6
|
+
* then used as a plain typed object throughout the runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { BuiltinToolSchema, DEFAULT_GREETING, DEFAULT_SYSTEM_PROMPT } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tool definition as it appears in the serialized manifest JSON.
|
|
14
|
+
*
|
|
15
|
+
* This is the JSON-safe representation. Compare with `ToolDef` (in types.ts)
|
|
16
|
+
* which uses Zod schemas for parameters — `agentToolsToSchemas()` in
|
|
17
|
+
* `_internal-types.ts` converts ToolDef → ToolSchema (JSON Schema) for transport.
|
|
18
|
+
*/
|
|
19
|
+
export type ToolManifest = {
|
|
20
|
+
description: string;
|
|
21
|
+
parameters?: Record<string, unknown> | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Normalized agent manifest — all optional fields resolved to defaults. */
|
|
25
|
+
export type Manifest = {
|
|
26
|
+
/** Agent display name (from `agent({ name: "..." })`). */
|
|
27
|
+
name: string;
|
|
28
|
+
/** System prompt sent to the LLM. Defaults to {@link DEFAULT_SYSTEM_PROMPT}. */
|
|
29
|
+
systemPrompt: string;
|
|
30
|
+
/** Initial greeting spoken to the user on connect. Defaults to {@link DEFAULT_GREETING}. */
|
|
31
|
+
greeting: string;
|
|
32
|
+
/** Optional prompt hint for the STT engine (improves transcription of domain terms). */
|
|
33
|
+
sttPrompt?: string | undefined;
|
|
34
|
+
/** Enabled built-in tools: `web_search`, `visit_webpage`, `fetch_json`, `run_code`. */
|
|
35
|
+
builtinTools: string[];
|
|
36
|
+
/** Max tool calls per LLM reply. Prevents runaway loops. Default: 5. */
|
|
37
|
+
maxSteps: number;
|
|
38
|
+
/** `"auto"` = LLM decides when to use tools; `"required"` = always call a tool. */
|
|
39
|
+
toolChoice: "auto" | "required";
|
|
40
|
+
/** Idle timeout in ms before auto-closing the session. `undefined` = use default (5 min). */
|
|
41
|
+
idleTimeoutMs?: number | undefined;
|
|
42
|
+
/** CSS custom properties for agent UI theming. */
|
|
43
|
+
theme?: Record<string, string> | undefined;
|
|
44
|
+
/** Custom tool definitions keyed by tool name. */
|
|
45
|
+
tools: Record<string, ToolManifest>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ToolManifestSchema = z.object({
|
|
49
|
+
description: z.string(),
|
|
50
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const ManifestSchema = z.object({
|
|
54
|
+
name: z.string().min(1),
|
|
55
|
+
systemPrompt: z.string().optional(),
|
|
56
|
+
greeting: z.string().optional(),
|
|
57
|
+
sttPrompt: z.string().optional(),
|
|
58
|
+
builtinTools: z.array(BuiltinToolSchema).optional(),
|
|
59
|
+
maxSteps: z.number().int().positive().optional(),
|
|
60
|
+
toolChoice: z.enum(["auto", "required"]).optional(),
|
|
61
|
+
idleTimeoutMs: z.number().int().positive().optional(),
|
|
62
|
+
theme: z.record(z.string(), z.string()).optional(),
|
|
63
|
+
tools: z.record(z.string(), ToolManifestSchema).optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse and normalize a raw agent manifest, applying defaults for all
|
|
68
|
+
* optional fields. Input is typically the JSON from a bundled agent.ts.
|
|
69
|
+
*
|
|
70
|
+
* Key defaults:
|
|
71
|
+
* - `maxSteps`: 5 — prevents runaway tool-call loops in a single reply
|
|
72
|
+
* - `toolChoice`: "auto" — LLM decides when to use tools vs respond directly
|
|
73
|
+
* - `builtinTools`: [] — no built-in tools unless explicitly opted in
|
|
74
|
+
*/
|
|
75
|
+
export function parseManifest(input: unknown): Manifest {
|
|
76
|
+
const parsed = ManifestSchema.parse(input);
|
|
77
|
+
return {
|
|
78
|
+
name: parsed.name,
|
|
79
|
+
systemPrompt: parsed.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
|
|
80
|
+
greeting: parsed.greeting ?? DEFAULT_GREETING,
|
|
81
|
+
sttPrompt: parsed.sttPrompt,
|
|
82
|
+
builtinTools: parsed.builtinTools ?? [],
|
|
83
|
+
maxSteps: parsed.maxSteps ?? 5,
|
|
84
|
+
toolChoice: parsed.toolChoice ?? "auto",
|
|
85
|
+
idleTimeoutMs: parsed.idleTimeoutMs,
|
|
86
|
+
theme: parsed.theme,
|
|
87
|
+
tools: parsed.tools ?? {},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Protocol compatibility tests.
|
|
4
|
+
*
|
|
5
|
+
* These test pinned fixture messages against the current Zod schemas to
|
|
6
|
+
* catch breaking wire-format changes. Unlike snapshot tests, fixtures
|
|
7
|
+
* are never auto-updated — a failure here means deployed code would break.
|
|
8
|
+
*/
|
|
9
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_STT_SAMPLE_RATE,
|
|
14
|
+
DEFAULT_TTS_SAMPLE_RATE,
|
|
15
|
+
MAX_TOOL_RESULT_CHARS,
|
|
16
|
+
} from "./constants.ts";
|
|
17
|
+
import {
|
|
18
|
+
AUDIO_FORMAT,
|
|
19
|
+
ClientMessageSchema,
|
|
20
|
+
KvRequestSchema,
|
|
21
|
+
ServerMessageSchema,
|
|
22
|
+
SessionErrorCodeSchema,
|
|
23
|
+
} from "./protocol.ts";
|
|
24
|
+
|
|
25
|
+
// ── Load fixtures ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const FIXTURE_DIR = join(import.meta.dirname, "compat-fixtures");
|
|
28
|
+
const fixtureFiles = readdirSync(FIXTURE_DIR)
|
|
29
|
+
.filter((f) => f.endsWith(".json"))
|
|
30
|
+
.sort();
|
|
31
|
+
|
|
32
|
+
type Fixture = {
|
|
33
|
+
version: number;
|
|
34
|
+
ServerMessage: Record<string, unknown>[];
|
|
35
|
+
ClientMessage: Record<string, unknown>[];
|
|
36
|
+
KvRequest: Record<string, unknown>[];
|
|
37
|
+
constants: {
|
|
38
|
+
AUDIO_FORMAT: string;
|
|
39
|
+
DEFAULT_STT_SAMPLE_RATE: number;
|
|
40
|
+
DEFAULT_TTS_SAMPLE_RATE: number;
|
|
41
|
+
MAX_TOOL_RESULT_CHARS: number;
|
|
42
|
+
SessionErrorCodes: string[];
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function loadFixture(filename: string): Fixture {
|
|
47
|
+
return JSON.parse(readFileSync(join(FIXTURE_DIR, filename), "utf-8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compatError(fixture: string, schema: string, msg: unknown, zodError: string): string {
|
|
51
|
+
return [
|
|
52
|
+
`PROTOCOL COMPATIBILITY BREAK (${fixture}, ${schema}):`,
|
|
53
|
+
"",
|
|
54
|
+
"A deployed client/server sending this message would fail:",
|
|
55
|
+
` ${JSON.stringify(msg)}`,
|
|
56
|
+
"",
|
|
57
|
+
`Zod error: ${zodError}`,
|
|
58
|
+
"",
|
|
59
|
+
"To resolve:",
|
|
60
|
+
" 1. If UNINTENTIONAL: revert the schema change.",
|
|
61
|
+
" 2. If INTENTIONAL: create a new fixture version and document the breaking change.",
|
|
62
|
+
].join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a discriminated union schema accepts a given discriminant value
|
|
67
|
+
* by testing whether a minimal object with that value passes the first
|
|
68
|
+
* discriminant check (the full parse may fail, but the type/op is recognized).
|
|
69
|
+
*/
|
|
70
|
+
function schemaAcceptsType(
|
|
71
|
+
schema: typeof ServerMessageSchema | typeof ClientMessageSchema,
|
|
72
|
+
type: string,
|
|
73
|
+
): boolean {
|
|
74
|
+
// Parse a minimal object with just the discriminant. If the discriminant
|
|
75
|
+
// is unrecognized, the error includes "invalid_union_discriminator" or
|
|
76
|
+
// similar. Any other failure (missing fields) means the variant exists.
|
|
77
|
+
const result = schema.safeParse({ type });
|
|
78
|
+
if (result.success) return true;
|
|
79
|
+
// Check error messages — the Zod issue code for discriminated union
|
|
80
|
+
// mismatches varies across versions, so we check the message text.
|
|
81
|
+
return !result.error.issues.some((i) => i.message.includes("Invalid discriminator"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function kvSchemaAcceptsOp(op: string): boolean {
|
|
85
|
+
const result = KvRequestSchema.safeParse({ op });
|
|
86
|
+
if (result.success) return true;
|
|
87
|
+
return !result.error.issues.some((i) => i.message.includes("Invalid discriminator"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe.each(fixtureFiles)("compat fixture: %s", (filename) => {
|
|
93
|
+
const fixture = loadFixture(filename);
|
|
94
|
+
|
|
95
|
+
// ── Backward compat: every fixture message must still parse ──────
|
|
96
|
+
|
|
97
|
+
describe("ServerMessage backward compat", () => {
|
|
98
|
+
test.each(
|
|
99
|
+
fixture.ServerMessage.map((m, i) => [`${(m as { type: string }).type}#${i}`, m]),
|
|
100
|
+
)("%s parses against current schema", (_label, msg) => {
|
|
101
|
+
const result = ServerMessageSchema.safeParse(msg);
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
throw new Error(compatError(filename, "ServerMessage", msg, result.error.message));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("ClientMessage backward compat", () => {
|
|
109
|
+
test.each(
|
|
110
|
+
fixture.ClientMessage.map((m, i) => [`${(m as { type: string }).type}#${i}`, m]),
|
|
111
|
+
)("%s parses against current schema", (_label, msg) => {
|
|
112
|
+
const result = ClientMessageSchema.safeParse(msg);
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
throw new Error(compatError(filename, "ClientMessage", msg, result.error.message));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("KvRequest backward compat", () => {
|
|
120
|
+
test.each(
|
|
121
|
+
fixture.KvRequest.map((m, i) => [`${(m as { op: string }).op}#${i}`, m]),
|
|
122
|
+
)("%s parses against current schema", (_label, msg) => {
|
|
123
|
+
const result = KvRequestSchema.safeParse(msg);
|
|
124
|
+
if (!result.success) {
|
|
125
|
+
throw new Error(compatError(filename, "KvRequest", msg, result.error.message));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Variant coverage: no types/ops removed ──────────────────────
|
|
131
|
+
|
|
132
|
+
describe("variant coverage", () => {
|
|
133
|
+
test("no ServerMessage types removed", () => {
|
|
134
|
+
const fixtureTypes = new Set(fixture.ServerMessage.map((m) => (m as { type: string }).type));
|
|
135
|
+
for (const t of fixtureTypes) {
|
|
136
|
+
expect(
|
|
137
|
+
schemaAcceptsType(ServerMessageSchema, t),
|
|
138
|
+
`ServerMessage variant "${t}" was removed`,
|
|
139
|
+
).toBe(true);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("no ClientMessage types removed", () => {
|
|
144
|
+
const fixtureTypes = new Set(fixture.ClientMessage.map((m) => (m as { type: string }).type));
|
|
145
|
+
for (const t of fixtureTypes) {
|
|
146
|
+
expect(
|
|
147
|
+
schemaAcceptsType(ClientMessageSchema, t),
|
|
148
|
+
`ClientMessage variant "${t}" was removed`,
|
|
149
|
+
).toBe(true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("no KvRequest ops removed", () => {
|
|
154
|
+
const fixtureOps = new Set(fixture.KvRequest.map((m) => (m as { op: string }).op));
|
|
155
|
+
for (const op of fixtureOps) {
|
|
156
|
+
expect(kvSchemaAcceptsOp(op), `KvRequest op "${op}" was removed`).toBe(true);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── Constants stability ─────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
describe("constants stability", () => {
|
|
164
|
+
test("AUDIO_FORMAT unchanged", () => {
|
|
165
|
+
expect(AUDIO_FORMAT).toBe(fixture.constants.AUDIO_FORMAT);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("DEFAULT_STT_SAMPLE_RATE unchanged", () => {
|
|
169
|
+
expect(DEFAULT_STT_SAMPLE_RATE).toBe(fixture.constants.DEFAULT_STT_SAMPLE_RATE);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("DEFAULT_TTS_SAMPLE_RATE unchanged", () => {
|
|
173
|
+
expect(DEFAULT_TTS_SAMPLE_RATE).toBe(fixture.constants.DEFAULT_TTS_SAMPLE_RATE);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("MAX_TOOL_RESULT_CHARS unchanged", () => {
|
|
177
|
+
expect(MAX_TOOL_RESULT_CHARS).toBe(fixture.constants.MAX_TOOL_RESULT_CHARS);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("SessionErrorCodes is superset of fixture", () => {
|
|
181
|
+
const currentCodes = new Set<string>(SessionErrorCodeSchema.options);
|
|
182
|
+
for (const code of fixture.constants.SessionErrorCodes) {
|
|
183
|
+
expect(currentCodes.has(code), `SessionErrorCode "${code}" was removed`).toBe(true);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|