@alexkroman1/aai 0.3.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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +3436 -0
  3. package/package.json +78 -0
  4. package/sdk/_internal_types.ts +89 -0
  5. package/sdk/_mock_ws.ts +172 -0
  6. package/sdk/_timeout.ts +24 -0
  7. package/sdk/builtin_tools.ts +309 -0
  8. package/sdk/capnweb.ts +341 -0
  9. package/sdk/define_agent.ts +70 -0
  10. package/sdk/direct_executor.ts +195 -0
  11. package/sdk/kv.ts +183 -0
  12. package/sdk/mod.ts +35 -0
  13. package/sdk/protocol.ts +313 -0
  14. package/sdk/runtime.ts +65 -0
  15. package/sdk/s2s.ts +271 -0
  16. package/sdk/server.ts +198 -0
  17. package/sdk/session.ts +438 -0
  18. package/sdk/system_prompt.ts +47 -0
  19. package/sdk/types.ts +406 -0
  20. package/sdk/vector.ts +133 -0
  21. package/sdk/winterc_server.ts +141 -0
  22. package/sdk/worker_entry.ts +99 -0
  23. package/sdk/worker_shim.ts +170 -0
  24. package/sdk/ws_handler.ts +190 -0
  25. package/templates/_shared/.env.example +5 -0
  26. package/templates/_shared/package.json +17 -0
  27. package/templates/code-interpreter/agent.ts +27 -0
  28. package/templates/code-interpreter/client.tsx +2 -0
  29. package/templates/dispatch-center/agent.ts +1536 -0
  30. package/templates/dispatch-center/client.tsx +504 -0
  31. package/templates/embedded-assets/agent.ts +49 -0
  32. package/templates/embedded-assets/client.tsx +2 -0
  33. package/templates/embedded-assets/knowledge.json +20 -0
  34. package/templates/health-assistant/agent.ts +160 -0
  35. package/templates/health-assistant/client.tsx +2 -0
  36. package/templates/infocom-adventure/agent.ts +164 -0
  37. package/templates/infocom-adventure/client.tsx +299 -0
  38. package/templates/math-buddy/agent.ts +21 -0
  39. package/templates/math-buddy/client.tsx +2 -0
  40. package/templates/memory-agent/agent.ts +74 -0
  41. package/templates/memory-agent/client.tsx +2 -0
  42. package/templates/night-owl/agent.ts +98 -0
  43. package/templates/night-owl/client.tsx +28 -0
  44. package/templates/personal-finance/agent.ts +26 -0
  45. package/templates/personal-finance/client.tsx +2 -0
  46. package/templates/simple/agent.ts +6 -0
  47. package/templates/simple/client.tsx +2 -0
  48. package/templates/smart-research/agent.ts +164 -0
  49. package/templates/smart-research/client.tsx +2 -0
  50. package/templates/support/README.md +62 -0
  51. package/templates/support/agent.ts +19 -0
  52. package/templates/support/client.tsx +2 -0
  53. package/templates/travel-concierge/agent.ts +29 -0
  54. package/templates/travel-concierge/client.tsx +2 -0
  55. package/templates/web-researcher/agent.ts +17 -0
  56. package/templates/web-researcher/client.tsx +2 -0
  57. package/ui/_components/app.tsx +37 -0
  58. package/ui/_components/chat_view.tsx +36 -0
  59. package/ui/_components/controls.tsx +32 -0
  60. package/ui/_components/error_banner.tsx +18 -0
  61. package/ui/_components/message_bubble.tsx +21 -0
  62. package/ui/_components/message_list.tsx +61 -0
  63. package/ui/_components/state_indicator.tsx +17 -0
  64. package/ui/_components/thinking_indicator.tsx +19 -0
  65. package/ui/_components/tool_call_block.tsx +110 -0
  66. package/ui/_components/tool_icons.tsx +101 -0
  67. package/ui/_components/transcript.tsx +20 -0
  68. package/ui/audio.ts +170 -0
  69. package/ui/components.ts +49 -0
  70. package/ui/components_mod.ts +37 -0
  71. package/ui/mod.ts +48 -0
  72. package/ui/mount.tsx +112 -0
  73. package/ui/mount_context.ts +19 -0
  74. package/ui/session.ts +456 -0
  75. package/ui/session_mod.ts +27 -0
  76. package/ui/signals.ts +111 -0
  77. package/ui/types.ts +50 -0
  78. package/ui/worklets/capture-processor.js +62 -0
  79. package/ui/worklets/playback-processor.js +110 -0
package/sdk/capnweb.ts ADDED
@@ -0,0 +1,341 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * MessagePort RPC + WebSocket bridge for capnweb sandbox communication.
4
+ *
5
+ * Provides bidirectional RPC over MessagePort/Worker and WebSocket
6
+ * bridging for both standard WebSocket (client connections) and
7
+ * S2sWebSocket (.on()-style API for S2S connections).
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { S2sWebSocket } from "./s2s.ts";
13
+
14
+ // ─── MessagePort RPC ─────────────────────────────────────────────────────────
15
+
16
+ /** Minimal port interface for CapnwebEndpoint. Works with Worker, MessagePort, or worker self. */
17
+ export type CapnwebPort = {
18
+ postMessage(msg: unknown, transfer?: Transferable[]): void;
19
+ onmessage: ((ev: MessageEvent) => any) | null; // eslint-disable-line @typescript-eslint/no-explicit-any -- varies across Worker/MessagePort/self
20
+ };
21
+
22
+ /** RPC handler function. Receives call arguments and any transferred ports. */
23
+ export type RpcHandler = (
24
+ args: unknown[],
25
+ ports: readonly MessagePort[],
26
+ ) => unknown | Promise<unknown>;
27
+
28
+ /**
29
+ * Bidirectional RPC endpoint over MessagePort or Worker.
30
+ *
31
+ * Both sides can send calls and handle incoming calls on the same channel.
32
+ *
33
+ * Wire protocol:
34
+ * - Call: `{$: 0, id: number, m: string, a: unknown[]}`
35
+ * - Result: `{$: 1, id: number, v?: unknown, e?: string}`
36
+ * - Notify (fire-and-forget): id = -1, no response sent
37
+ */
38
+ export class CapnwebEndpoint {
39
+ private nextId = 1;
40
+ private pending = new Map<
41
+ number,
42
+ { resolve: (v: unknown) => void; reject: (e: Error) => void }
43
+ >();
44
+ private handlers = new Map<string, RpcHandler>();
45
+ private port: CapnwebPort;
46
+
47
+ constructor(port: CapnwebPort) {
48
+ this.port = port;
49
+ port.onmessage = (ev: MessageEvent) => this.onMessage(ev);
50
+ }
51
+
52
+ /** Register an RPC handler for incoming calls with the given method name. */
53
+ handle(method: string, handler: RpcHandler): void {
54
+ this.handlers.set(method, handler);
55
+ }
56
+
57
+ /** Call a remote method and wait for the result. */
58
+ call(method: string, args: unknown[], transfer?: Transferable[]): Promise<unknown> {
59
+ const id = this.nextId++;
60
+ return new Promise((resolve, reject) => {
61
+ this.pending.set(id, { resolve, reject });
62
+ this.send({ $: 0, id, m: method, a: args }, transfer);
63
+ });
64
+ }
65
+
66
+ /** Fire-and-forget: call a remote method without waiting for a response. */
67
+ notify(method: string, args: unknown[], transfer?: Transferable[]): void {
68
+ this.send({ $: 0, id: -1, m: method, a: args }, transfer);
69
+ }
70
+
71
+ private send(msg: unknown, transfer?: Transferable[]): void {
72
+ if (transfer && transfer.length > 0) {
73
+ this.port.postMessage(msg, transfer);
74
+ } else {
75
+ this.port.postMessage(msg);
76
+ }
77
+ }
78
+
79
+ private onMessage(ev: MessageEvent): void {
80
+ const msg = ev.data;
81
+ if (!msg || typeof msg !== "object" || !("$" in msg)) return;
82
+
83
+ if (msg.$ === 0) {
84
+ // Incoming call
85
+ const { id, m, a } = msg as { id: number; m: string; a: unknown[] };
86
+ const handler = this.handlers.get(m);
87
+ if (!handler) {
88
+ if (id >= 0) this.send({ $: 1, id, e: `No handler for ${m}` });
89
+ return;
90
+ }
91
+ const ports = [...ev.ports];
92
+ Promise.resolve()
93
+ .then(() => handler(a, ports))
94
+ .then((v) => {
95
+ if (id >= 0) this.send({ $: 1, id, v });
96
+ })
97
+ .catch((err: unknown) => {
98
+ if (id >= 0) {
99
+ this.send({
100
+ $: 1,
101
+ id,
102
+ e: err instanceof Error ? err.message : String(err),
103
+ });
104
+ }
105
+ });
106
+ } else if (msg.$ === 1) {
107
+ // Incoming result
108
+ const { id, v, e } = msg as { id: number; v?: unknown; e?: string };
109
+ const pending = this.pending.get(id);
110
+ if (!pending) return;
111
+ this.pending.delete(id);
112
+ if (e !== undefined) {
113
+ pending.reject(new Error(e));
114
+ } else {
115
+ pending.resolve(v);
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ // ─── WebSocket Bridge Protocol ───────────────────────────────────────────────
122
+
123
+ /**
124
+ * Bridge message format for WebSocket-over-MessagePort:
125
+ * - `{k:0, d:string}` — text frame
126
+ * - `{k:1, d:ArrayBuffer}` — binary frame (transferred zero-copy)
127
+ * - `{k:2, code?, reason?}` — close
128
+ * - `{k:3}` — open
129
+ * - `{k:4, m:string}` — error
130
+ */
131
+ type BridgeMsg =
132
+ | { k: 0; d: string }
133
+ | { k: 1; d: ArrayBuffer }
134
+ | { k: 2; code?: number; reason?: string }
135
+ | { k: 3 }
136
+ | { k: 4; m: string };
137
+
138
+ // ─── BridgedWebSocket (standard EventTarget-based) ───────────────────────────
139
+
140
+ /**
141
+ * Wraps a MessagePort as a standard WebSocket (extends EventTarget).
142
+ * Used in the worker for client connections bridged from the host.
143
+ */
144
+ export class BridgedWebSocket extends EventTarget {
145
+ readyState = 0;
146
+ private port: MessagePort;
147
+
148
+ constructor(port: MessagePort) {
149
+ super();
150
+ this.port = port;
151
+ port.onmessage = (ev: MessageEvent) => {
152
+ const msg = ev.data as BridgeMsg;
153
+ switch (msg.k) {
154
+ case 0:
155
+ this.dispatchEvent(new MessageEvent("message", { data: msg.d }));
156
+ break;
157
+ case 1:
158
+ this.dispatchEvent(new MessageEvent("message", { data: msg.d }));
159
+ break;
160
+ case 2:
161
+ this.readyState = 3;
162
+ this.dispatchEvent(
163
+ new CloseEvent("close", {
164
+ ...(msg.code !== undefined ? { code: msg.code } : {}),
165
+ ...(msg.reason !== undefined ? { reason: msg.reason } : {}),
166
+ }),
167
+ );
168
+ break;
169
+ case 3:
170
+ this.readyState = 1;
171
+ this.dispatchEvent(new Event("open"));
172
+ break;
173
+ case 4:
174
+ this.dispatchEvent(new ErrorEvent("error", { message: msg.m }));
175
+ break;
176
+ }
177
+ };
178
+ }
179
+
180
+ send(data: string | ArrayBuffer | Uint8Array): void {
181
+ if (this.readyState !== 1) return;
182
+ if (typeof data === "string") {
183
+ this.port.postMessage({ k: 0, d: data });
184
+ } else {
185
+ const ab =
186
+ data instanceof ArrayBuffer
187
+ ? data
188
+ : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
189
+ this.port.postMessage({ k: 1, d: ab }, [ab]);
190
+ }
191
+ }
192
+
193
+ close(code?: number, reason?: string): void {
194
+ if (this.readyState >= 2) return;
195
+ this.readyState = 2;
196
+ this.port.postMessage({ k: 2, code, reason });
197
+ }
198
+ }
199
+
200
+ // ─── BridgedS2sWebSocket (.on()-style API) ───────────────────────────────────
201
+
202
+ /**
203
+ * Wraps a MessagePort as an {@linkcode S2sWebSocket} (.on() event API).
204
+ * Used in the worker for S2S connections bridged from the host.
205
+ */
206
+ export function createBridgedS2sWebSocket(port: MessagePort): S2sWebSocket {
207
+ let readyState = 0;
208
+ const handlers = new Map<string, ((...args: unknown[]) => void)[]>();
209
+
210
+ function emit(event: string, ...args: unknown[]): void {
211
+ for (const h of handlers.get(event) ?? []) h(...args);
212
+ }
213
+
214
+ port.onmessage = (ev: MessageEvent) => {
215
+ const msg = ev.data as BridgeMsg;
216
+ switch (msg.k) {
217
+ case 0:
218
+ emit("message", msg.d);
219
+ break;
220
+ case 2:
221
+ readyState = 3;
222
+ emit("close", msg.code, msg.reason);
223
+ break;
224
+ case 3:
225
+ readyState = 1;
226
+ emit("open");
227
+ break;
228
+ case 4:
229
+ emit("error", new Error(msg.m));
230
+ break;
231
+ }
232
+ };
233
+
234
+ return {
235
+ get readyState() {
236
+ return readyState;
237
+ },
238
+ send(data: string): void {
239
+ if (readyState !== 1) return;
240
+ port.postMessage({ k: 0, d: data });
241
+ },
242
+ close(): void {
243
+ if (readyState >= 2) return;
244
+ readyState = 2;
245
+ port.postMessage({ k: 2 });
246
+ },
247
+ on(event: string, handler: (...args: unknown[]) => void): void {
248
+ if (!handlers.has(event)) handlers.set(event, []);
249
+ handlers.get(event)!.push(handler);
250
+ },
251
+ };
252
+ }
253
+
254
+ // ─── Host-side bridges ───────────────────────────────────────────────────────
255
+
256
+ /**
257
+ * Bridges a standard WebSocket (e.g. from `Deno.upgradeWebSocket`) to a
258
+ * MessagePort. Used on the host side for client connections.
259
+ */
260
+ export function bridgeWebSocketToPort(ws: WebSocket, port: MessagePort): void {
261
+ ws.binaryType = "arraybuffer";
262
+
263
+ ws.addEventListener("open", () => {
264
+ port.postMessage({ k: 3 });
265
+ });
266
+
267
+ ws.addEventListener("message", (ev: Event) => {
268
+ const { data } = ev as MessageEvent;
269
+ if (typeof data === "string") {
270
+ port.postMessage({ k: 0, d: data });
271
+ } else if (data instanceof ArrayBuffer) {
272
+ port.postMessage({ k: 1, d: data }, [data]);
273
+ }
274
+ });
275
+
276
+ ws.addEventListener("close", (ev: Event) => {
277
+ const e = ev as CloseEvent;
278
+ port.postMessage({ k: 2, code: e.code, reason: e.reason });
279
+ });
280
+
281
+ ws.addEventListener("error", (ev: Event) => {
282
+ const msg = ev instanceof ErrorEvent ? ev.message : "WebSocket error";
283
+ port.postMessage({ k: 4, m: msg });
284
+ });
285
+
286
+ // Messages from worker → real WebSocket
287
+ port.onmessage = (ev: MessageEvent) => {
288
+ const msg = ev.data as BridgeMsg;
289
+ switch (msg.k) {
290
+ case 0:
291
+ if (ws.readyState === 1) ws.send(msg.d);
292
+ break;
293
+ case 1:
294
+ if (ws.readyState === 1) ws.send(msg.d);
295
+ break;
296
+ case 2:
297
+ ws.close(msg.code, msg.reason);
298
+ break;
299
+ }
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Bridges a ws-style {@linkcode S2sWebSocket} to a MessagePort.
305
+ * Used on the host side for S2S connections to AssemblyAI.
306
+ */
307
+ export function bridgeS2sWebSocketToPort(ws: S2sWebSocket, port: MessagePort): void {
308
+ ws.on("open", () => {
309
+ port.postMessage({ k: 3 });
310
+ });
311
+
312
+ ws.on("message", (data: unknown) => {
313
+ port.postMessage({ k: 0, d: String(data) });
314
+ });
315
+
316
+ ws.on("close", (code: unknown, reason: unknown) => {
317
+ port.postMessage({
318
+ k: 2,
319
+ code: typeof code === "number" ? code : undefined,
320
+ reason: String(reason ?? ""),
321
+ });
322
+ });
323
+
324
+ ws.on("error", (err: unknown) => {
325
+ const msg = err instanceof Error ? err.message : String(err);
326
+ port.postMessage({ k: 4, m: msg });
327
+ });
328
+
329
+ // Messages from worker → real S2S WebSocket
330
+ port.onmessage = (ev: MessageEvent) => {
331
+ const msg = ev.data as BridgeMsg;
332
+ switch (msg.k) {
333
+ case 0:
334
+ if (ws.readyState === 1) ws.send(msg.d);
335
+ break;
336
+ case 2:
337
+ ws.close();
338
+ break;
339
+ }
340
+ };
341
+ }
@@ -0,0 +1,70 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Agent definition factory.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import {
9
+ type AgentDef,
10
+ type AgentOptions,
11
+ DEFAULT_GREETING,
12
+ DEFAULT_INSTRUCTIONS,
13
+ normalizeTransport,
14
+ } from "./types.ts";
15
+
16
+ /**
17
+ * Create an agent definition from the given options, applying sensible defaults.
18
+ *
19
+ * This is the main entry point for defining a voice agent. The returned
20
+ * {@linkcode AgentDef} is consumed by the AAI server at deploy time.
21
+ *
22
+ * @param options Configuration for the agent including name, instructions,
23
+ * tools, hooks, and transport settings.
24
+ * @returns A fully resolved agent definition with all defaults applied.
25
+ *
26
+ * @example Basic agent with a custom tool
27
+ * ```ts
28
+ * import { defineAgent } from "aai";
29
+ * import { z } from "zod";
30
+ *
31
+ * export default defineAgent({
32
+ * name: "greeter",
33
+ * instructions: "You greet people warmly.",
34
+ * tools: {
35
+ * greet: {
36
+ * description: "Greet a user by name",
37
+ * parameters: z.object({ name: z.string() }),
38
+ * execute: ({ name }) => `Hello, ${name}!`,
39
+ * },
40
+ * },
41
+ * });
42
+ * ```
43
+ */
44
+ export function defineAgent<S>(options: AgentOptions<S>): AgentDef {
45
+ // AgentDef erases the S generic (it's a runtime artifact consumed by the
46
+ // server which doesn't need the compile-time state type). The cast is safe
47
+ // because AgentDef's hooks/tools use the same shapes with `any`/`unknown`.
48
+ return {
49
+ name: options.name,
50
+ env: options.env ?? ["ASSEMBLYAI_API_KEY"],
51
+ transport: normalizeTransport(options.transport),
52
+ mode: options.mode ?? "s2s",
53
+ instructions: options.instructions ?? DEFAULT_INSTRUCTIONS,
54
+ greeting: options.greeting ?? DEFAULT_GREETING,
55
+ voice: options.voice ?? "",
56
+ ...(options.sttPrompt !== undefined && { sttPrompt: options.sttPrompt }),
57
+ maxSteps: options.maxSteps ?? 5,
58
+ ...(options.toolChoice !== undefined && { toolChoice: options.toolChoice }),
59
+ ...(options.builtinTools !== undefined && { builtinTools: options.builtinTools }),
60
+ ...(options.activeTools !== undefined && { activeTools: options.activeTools }),
61
+ tools: options.tools ?? {},
62
+ ...(options.state !== undefined && { state: options.state }),
63
+ ...(options.onConnect !== undefined && { onConnect: options.onConnect }),
64
+ ...(options.onDisconnect !== undefined && { onDisconnect: options.onDisconnect }),
65
+ ...(options.onError !== undefined && { onError: options.onError }),
66
+ ...(options.onTurn !== undefined && { onTurn: options.onTurn }),
67
+ ...(options.onStep !== undefined && { onStep: options.onStep }),
68
+ ...(options.onBeforeStep !== undefined && { onBeforeStep: options.onBeforeStep }),
69
+ } as AgentDef;
70
+ }
@@ -0,0 +1,195 @@
1
+ // Copyright 2025 the AAI authors. MIT license.
2
+ /**
3
+ * Direct tool execution for self-hosted mode.
4
+ *
5
+ * In self-hosted mode, agent code is trusted (you're running your own code).
6
+ * Tools execute directly in-process — no sandbox, no RPC.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { type AgentConfig, agentToolsToSchemas, type ToolSchema } from "./_internal_types.ts";
12
+ import { getBuiltinToolDefs, getBuiltinToolSchemas } from "./builtin_tools.ts";
13
+ import type { Kv } from "./kv.ts";
14
+ import { createMemoryKv } from "./kv.ts";
15
+ import type { ClientSink } from "./protocol.ts";
16
+ import type { Logger, Metrics, S2SConfig } from "./runtime.ts";
17
+ import { consoleLogger, DEFAULT_S2S_CONFIG, noopMetrics } from "./runtime.ts";
18
+ import type { CreateS2sWebSocket } from "./s2s.ts";
19
+ import { createS2sSession, type HookInvoker, type Session } from "./session.ts";
20
+ import type { AgentDef, HookContext, StepInfo } from "./types.ts";
21
+ import type { ExecuteTool } from "./worker_entry.ts";
22
+ import { executeToolCall } from "./worker_entry.ts";
23
+
24
+ export type DirectExecutorOptions = {
25
+ agent: AgentDef;
26
+ env: Record<string, string>;
27
+ kv?: Kv | undefined;
28
+ vectorSearch?: ((query: string, topK: number) => Promise<string>) | undefined;
29
+ createWebSocket?: CreateS2sWebSocket | undefined;
30
+ logger?: Logger | undefined;
31
+ metrics?: Metrics | undefined;
32
+ s2sConfig?: S2SConfig | undefined;
33
+ };
34
+
35
+ export type DirectExecutor = {
36
+ executeTool: ExecuteTool;
37
+ hookInvoker: HookInvoker;
38
+ toolSchemas: ToolSchema[];
39
+ createSession(opts: {
40
+ id: string;
41
+ agent: string;
42
+ client: ClientSink;
43
+ skipGreeting?: boolean;
44
+ }): Session;
45
+ };
46
+
47
+ /** Build a serializable AgentConfig from an AgentDef. */
48
+ export function buildAgentConfig(agent: AgentDef): AgentConfig {
49
+ const config: AgentConfig = {
50
+ name: agent.name,
51
+ instructions: agent.instructions,
52
+ greeting: agent.greeting,
53
+ voice: agent.voice,
54
+ };
55
+ if (agent.sttPrompt !== undefined) config.sttPrompt = agent.sttPrompt;
56
+ if (typeof agent.maxSteps !== "function") config.maxSteps = agent.maxSteps;
57
+ if (agent.toolChoice !== undefined) config.toolChoice = agent.toolChoice;
58
+ if (agent.builtinTools) config.builtinTools = [...agent.builtinTools];
59
+ if (agent.activeTools) config.activeTools = [...agent.activeTools];
60
+ return config;
61
+ }
62
+
63
+ /** Create a direct (in-process) tool executor and hook invoker for an agent. */
64
+ export function createDirectExecutor(opts: DirectExecutorOptions): DirectExecutor {
65
+ const {
66
+ agent,
67
+ env,
68
+ kv = createMemoryKv(),
69
+ vectorSearch,
70
+ createWebSocket,
71
+ logger = consoleLogger,
72
+ metrics = noopMetrics,
73
+ s2sConfig = DEFAULT_S2S_CONFIG,
74
+ } = opts;
75
+ const agentConfig = buildAgentConfig(agent);
76
+
77
+ // Merge custom + builtin tool definitions
78
+ const builtinDefs = getBuiltinToolDefs(
79
+ agent.builtinTools ?? [],
80
+ vectorSearch ? { vectorSearch } : undefined,
81
+ );
82
+ const allTools: Record<string, AgentDef["tools"][string]> = {
83
+ ...builtinDefs,
84
+ ...agent.tools,
85
+ };
86
+
87
+ // Build tool schemas for the S2S API
88
+ const customSchemas = agentToolsToSchemas(agent.tools ?? {});
89
+ const builtinSchemas = getBuiltinToolSchemas(agent.builtinTools ?? []);
90
+ const toolSchemas: ToolSchema[] = [...customSchemas, ...builtinSchemas];
91
+
92
+ // Per-session mutable state
93
+ const sessionState = new Map<string, unknown>();
94
+ const frozenEnv = Object.freeze({ ...env });
95
+
96
+ function getState(sessionId: string): unknown {
97
+ if (!sessionState.has(sessionId) && agent.state) {
98
+ sessionState.set(sessionId, agent.state());
99
+ }
100
+ return sessionState.get(sessionId) ?? {};
101
+ }
102
+
103
+ function makeHookContext(sessionId: string): HookContext {
104
+ return {
105
+ sessionId,
106
+ env: frozenEnv,
107
+ state: getState(sessionId) as Record<string, unknown>,
108
+ get kv() {
109
+ return kv;
110
+ },
111
+ };
112
+ }
113
+
114
+ const executeTool: ExecuteTool = async (name, args, sessionId, messages) => {
115
+ const tool = allTools[name];
116
+ if (!tool) return JSON.stringify({ error: `Unknown tool: ${name}` });
117
+
118
+ return executeToolCall(name, args, {
119
+ tool,
120
+ env: frozenEnv,
121
+ sessionId,
122
+ state: getState(sessionId ?? ""),
123
+ kv,
124
+ messages,
125
+ });
126
+ };
127
+
128
+ const hookInvoker: HookInvoker = {
129
+ async onConnect(sessionId) {
130
+ await agent.onConnect?.(makeHookContext(sessionId));
131
+ },
132
+ async onDisconnect(sessionId) {
133
+ await agent.onDisconnect?.(makeHookContext(sessionId));
134
+ sessionState.delete(sessionId);
135
+ },
136
+ async onTurn(sessionId, text) {
137
+ await agent.onTurn?.(text, makeHookContext(sessionId));
138
+ },
139
+ async onError(sessionId, error) {
140
+ await agent.onError?.(new Error(error.message), makeHookContext(sessionId));
141
+ },
142
+ async onStep(sessionId, step: StepInfo) {
143
+ await agent.onStep?.(step, makeHookContext(sessionId));
144
+ },
145
+ async resolveTurnConfig(sessionId) {
146
+ const ctx = makeHookContext(sessionId);
147
+ let maxSteps: number | undefined;
148
+ let activeTools: string[] | undefined;
149
+
150
+ if (typeof agent.maxSteps === "function") {
151
+ maxSteps = (await agent.maxSteps(ctx)) ?? undefined;
152
+ }
153
+
154
+ if (agent.onBeforeStep) {
155
+ const result = await agent.onBeforeStep(0, ctx);
156
+ activeTools = result?.activeTools;
157
+ }
158
+
159
+ if (maxSteps === undefined && activeTools === undefined) return null;
160
+ const config: { maxSteps?: number; activeTools?: string[] } = {};
161
+ if (maxSteps !== undefined) config.maxSteps = maxSteps;
162
+ if (activeTools !== undefined) config.activeTools = activeTools;
163
+ return config;
164
+ },
165
+ };
166
+
167
+ function createSession(sessionOpts: {
168
+ id: string;
169
+ agent: string;
170
+ client: ClientSink;
171
+ skipGreeting?: boolean;
172
+ }): Session {
173
+ if (!createWebSocket) {
174
+ throw new Error("createWebSocket not provided — pass it in DirectExecutorOptions");
175
+ }
176
+ const apiKey = frozenEnv.ASSEMBLYAI_API_KEY ?? "";
177
+ return createS2sSession({
178
+ id: sessionOpts.id,
179
+ agent: sessionOpts.agent,
180
+ client: sessionOpts.client,
181
+ agentConfig,
182
+ toolSchemas,
183
+ apiKey,
184
+ s2sConfig,
185
+ executeTool,
186
+ createWebSocket,
187
+ hookInvoker,
188
+ skipGreeting: sessionOpts.skipGreeting ?? false,
189
+ logger,
190
+ metrics,
191
+ });
192
+ }
193
+
194
+ return { executeTool, hookInvoker, toolSchemas, createSession };
195
+ }