@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.
- package/LICENSE +21 -0
- package/dist/cli.js +3436 -0
- package/package.json +78 -0
- package/sdk/_internal_types.ts +89 -0
- package/sdk/_mock_ws.ts +172 -0
- package/sdk/_timeout.ts +24 -0
- package/sdk/builtin_tools.ts +309 -0
- package/sdk/capnweb.ts +341 -0
- package/sdk/define_agent.ts +70 -0
- package/sdk/direct_executor.ts +195 -0
- package/sdk/kv.ts +183 -0
- package/sdk/mod.ts +35 -0
- package/sdk/protocol.ts +313 -0
- package/sdk/runtime.ts +65 -0
- package/sdk/s2s.ts +271 -0
- package/sdk/server.ts +198 -0
- package/sdk/session.ts +438 -0
- package/sdk/system_prompt.ts +47 -0
- package/sdk/types.ts +406 -0
- package/sdk/vector.ts +133 -0
- package/sdk/winterc_server.ts +141 -0
- package/sdk/worker_entry.ts +99 -0
- package/sdk/worker_shim.ts +170 -0
- package/sdk/ws_handler.ts +190 -0
- package/templates/_shared/.env.example +5 -0
- package/templates/_shared/package.json +17 -0
- package/templates/code-interpreter/agent.ts +27 -0
- package/templates/code-interpreter/client.tsx +2 -0
- package/templates/dispatch-center/agent.ts +1536 -0
- package/templates/dispatch-center/client.tsx +504 -0
- package/templates/embedded-assets/agent.ts +49 -0
- package/templates/embedded-assets/client.tsx +2 -0
- package/templates/embedded-assets/knowledge.json +20 -0
- package/templates/health-assistant/agent.ts +160 -0
- package/templates/health-assistant/client.tsx +2 -0
- package/templates/infocom-adventure/agent.ts +164 -0
- package/templates/infocom-adventure/client.tsx +299 -0
- package/templates/math-buddy/agent.ts +21 -0
- package/templates/math-buddy/client.tsx +2 -0
- package/templates/memory-agent/agent.ts +74 -0
- package/templates/memory-agent/client.tsx +2 -0
- package/templates/night-owl/agent.ts +98 -0
- package/templates/night-owl/client.tsx +28 -0
- package/templates/personal-finance/agent.ts +26 -0
- package/templates/personal-finance/client.tsx +2 -0
- package/templates/simple/agent.ts +6 -0
- package/templates/simple/client.tsx +2 -0
- package/templates/smart-research/agent.ts +164 -0
- package/templates/smart-research/client.tsx +2 -0
- package/templates/support/README.md +62 -0
- package/templates/support/agent.ts +19 -0
- package/templates/support/client.tsx +2 -0
- package/templates/travel-concierge/agent.ts +29 -0
- package/templates/travel-concierge/client.tsx +2 -0
- package/templates/web-researcher/agent.ts +17 -0
- package/templates/web-researcher/client.tsx +2 -0
- package/ui/_components/app.tsx +37 -0
- package/ui/_components/chat_view.tsx +36 -0
- package/ui/_components/controls.tsx +32 -0
- package/ui/_components/error_banner.tsx +18 -0
- package/ui/_components/message_bubble.tsx +21 -0
- package/ui/_components/message_list.tsx +61 -0
- package/ui/_components/state_indicator.tsx +17 -0
- package/ui/_components/thinking_indicator.tsx +19 -0
- package/ui/_components/tool_call_block.tsx +110 -0
- package/ui/_components/tool_icons.tsx +101 -0
- package/ui/_components/transcript.tsx +20 -0
- package/ui/audio.ts +170 -0
- package/ui/components.ts +49 -0
- package/ui/components_mod.ts +37 -0
- package/ui/mod.ts +48 -0
- package/ui/mount.tsx +112 -0
- package/ui/mount_context.ts +19 -0
- package/ui/session.ts +456 -0
- package/ui/session_mod.ts +27 -0
- package/ui/signals.ts +111 -0
- package/ui/types.ts +50 -0
- package/ui/worklets/capture-processor.js +62 -0
- 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
|
+
}
|