@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.
Files changed (135) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/CHANGELOG.md +176 -0
  3. package/dist/constants-VTFoymJ-.js +47 -0
  4. package/dist/host/_run-code.d.ts +1 -1
  5. package/dist/host/_runtime-conformance.d.ts +4 -5
  6. package/dist/host/builtin-tools.d.ts +11 -9
  7. package/dist/host/runtime-barrel.d.ts +15 -0
  8. package/dist/{direct-executor-DRRrZUp0.js → host/runtime-barrel.js} +453 -348
  9. package/dist/host/runtime-config.d.ts +42 -0
  10. package/dist/host/runtime.d.ts +119 -35
  11. package/dist/host/s2s.d.ts +14 -38
  12. package/dist/host/server.d.ts +16 -8
  13. package/dist/host/session-ctx.d.ts +55 -0
  14. package/dist/host/session.d.ts +20 -70
  15. package/dist/host/tool-executor.d.ts +20 -0
  16. package/dist/host/unstorage-kv.d.ts +1 -1
  17. package/dist/host/ws-handler.d.ts +4 -2
  18. package/dist/index.d.ts +9 -20
  19. package/dist/index.js +63 -2
  20. package/dist/{isolate → sdk}/_internal-types.d.ts +5 -9
  21. package/dist/{isolate → sdk}/constants.d.ts +6 -4
  22. package/dist/sdk/define.d.ts +66 -0
  23. package/dist/{isolate → sdk}/kv.d.ts +1 -49
  24. package/dist/sdk/manifest-barrel.d.ts +8 -0
  25. package/dist/sdk/manifest-barrel.js +52 -0
  26. package/dist/sdk/manifest.d.ts +50 -0
  27. package/dist/{isolate → sdk}/protocol.d.ts +59 -36
  28. package/dist/sdk/protocol.js +163 -0
  29. package/dist/{isolate → sdk}/system-prompt.d.ts +2 -2
  30. package/dist/sdk/types.d.ts +201 -0
  31. package/dist/sdk/ws-upgrade.d.ts +5 -0
  32. package/dist/{system-prompt-DYAYFW99.js → system-prompt-nik_iavo.js} +10 -10
  33. package/dist/types-Cfx_4QDK.js +39 -0
  34. package/dist/ws-upgrade-BeOQ7fXL.js +30 -0
  35. package/exports-no-dev-deps.test.ts +62 -0
  36. package/host/_mock-ws.ts +185 -0
  37. package/host/_run-code.ts +217 -0
  38. package/host/_runtime-conformance.ts +143 -0
  39. package/host/_test-utils.ts +276 -0
  40. package/host/builtin-tools.test.ts +774 -0
  41. package/host/builtin-tools.ts +255 -0
  42. package/host/cleanup.test.ts +422 -0
  43. package/host/fixture-replay.test.ts +463 -0
  44. package/host/fixtures/README.md +40 -0
  45. package/host/fixtures/greeting-session-sequence.json +40 -0
  46. package/host/fixtures/reply-audio-samples.json +42 -0
  47. package/host/fixtures/reply-lifecycle.json +21 -0
  48. package/host/fixtures/session-ready.json +48 -0
  49. package/host/fixtures/session-updated.json +45 -0
  50. package/host/fixtures/simple-question-sequence.json +73 -0
  51. package/host/fixtures/tool-call-sequence.json +114 -0
  52. package/host/fixtures/tool-calls.json +11 -0
  53. package/host/fixtures/tool-config-session-sequence.json +51 -0
  54. package/host/fixtures/user-speech-recognition.json +30 -0
  55. package/host/fixtures/web-search-sequence.json +122 -0
  56. package/host/integration.test.ts +222 -0
  57. package/host/runtime-barrel.ts +25 -0
  58. package/host/runtime-config.test.ts +71 -0
  59. package/host/runtime-config.ts +99 -0
  60. package/host/runtime.test.ts +641 -0
  61. package/host/runtime.ts +308 -0
  62. package/host/s2s-fixtures.test.ts +237 -0
  63. package/host/s2s.test.ts +562 -0
  64. package/host/s2s.ts +310 -0
  65. package/host/server-shutdown.test.ts +76 -0
  66. package/host/server.test.ts +116 -0
  67. package/host/server.ts +223 -0
  68. package/host/session-ctx.ts +107 -0
  69. package/host/session-fixture-replay.test.ts +136 -0
  70. package/host/session-prompt.test.ts +77 -0
  71. package/host/session.test.ts +590 -0
  72. package/host/session.ts +370 -0
  73. package/host/tool-executor.test.ts +124 -0
  74. package/host/tool-executor.ts +80 -0
  75. package/host/unstorage-kv.test.ts +99 -0
  76. package/host/unstorage-kv.ts +69 -0
  77. package/host/ws-handler.test.ts +739 -0
  78. package/host/ws-handler.ts +255 -0
  79. package/index.ts +16 -0
  80. package/package.json +24 -72
  81. package/sdk/_internal-types.test.ts +34 -0
  82. package/sdk/_internal-types.ts +115 -0
  83. package/sdk/compat-fixtures/README.md +26 -0
  84. package/sdk/compat-fixtures/v1.json +68 -0
  85. package/sdk/constants.ts +77 -0
  86. package/sdk/define.test.ts +57 -0
  87. package/sdk/define.ts +88 -0
  88. package/sdk/kv.ts +60 -0
  89. package/sdk/manifest-barrel.ts +12 -0
  90. package/sdk/manifest.test.ts +56 -0
  91. package/sdk/manifest.ts +89 -0
  92. package/sdk/protocol-compat.test.ts +187 -0
  93. package/sdk/protocol-snapshot.test.ts +199 -0
  94. package/sdk/protocol.test.ts +170 -0
  95. package/sdk/protocol.ts +223 -0
  96. package/sdk/schema-alignment.test.ts +191 -0
  97. package/sdk/system-prompt.test.ts +111 -0
  98. package/sdk/system-prompt.ts +74 -0
  99. package/sdk/tsconfig.json +12 -0
  100. package/sdk/types-inference.test.ts +122 -0
  101. package/sdk/types.test.ts +14 -0
  102. package/sdk/types.ts +226 -0
  103. package/sdk/utils.test.ts +52 -0
  104. package/sdk/utils.ts +20 -0
  105. package/sdk/ws-upgrade.test.ts +48 -0
  106. package/sdk/ws-upgrade.ts +13 -0
  107. package/tsconfig.build.json +14 -0
  108. package/tsconfig.json +10 -0
  109. package/tsdown.config.ts +26 -0
  110. package/vitest.config.ts +17 -0
  111. package/dist/host/_test-utils.d.ts +0 -73
  112. package/dist/host/direct-executor.d.ts +0 -130
  113. package/dist/host/index.d.ts +0 -19
  114. package/dist/host/index.js +0 -165
  115. package/dist/host/matchers.d.ts +0 -20
  116. package/dist/host/matchers.js +0 -41
  117. package/dist/host/server.js +0 -164
  118. package/dist/host/testing.d.ts +0 -294
  119. package/dist/host/testing.js +0 -2
  120. package/dist/host/vite-plugin.d.ts +0 -15
  121. package/dist/host/vite-plugin.js +0 -83
  122. package/dist/isolate/_kv-utils.d.ts +0 -10
  123. package/dist/isolate/_utils.js +0 -17
  124. package/dist/isolate/hooks.d.ts +0 -44
  125. package/dist/isolate/hooks.js +0 -58
  126. package/dist/isolate/index.d.ts +0 -18
  127. package/dist/isolate/index.js +0 -6
  128. package/dist/isolate/kv.js +0 -1
  129. package/dist/isolate/protocol.js +0 -2
  130. package/dist/isolate/types.d.ts +0 -418
  131. package/dist/isolate/types.js +0 -175
  132. package/dist/protocol-rcOrz7T3.js +0 -183
  133. package/dist/testing-BreLdpq-.js +0 -513
  134. package/dist/types.test-d.d.ts +0 -7
  135. /package/dist/{isolate/_utils.d.ts → sdk/utils.d.ts} +0 -0
@@ -1,18 +1,76 @@
1
- import { a as agentToolsToSchemas, o as toAgentConfig, r as EMPTY_PARAMS, t as buildSystemPrompt } from "./system-prompt-DYAYFW99.js";
2
- import { errorDetail, errorMessage, toolError } from "./isolate/_utils.js";
3
- import { C as MAX_VALUE_SIZE, S as MAX_TOOL_RESULT_CHARS, T as TOOL_EXECUTION_TIMEOUT_MS, _ as FETCH_TIMEOUT_MS, b as MAX_HTML_BYTES, g as DEFAULT_TTS_SAMPLE_RATE, h as DEFAULT_STT_SAMPLE_RATE, l as buildReadyConfig, m as DEFAULT_SHUTDOWN_TIMEOUT_MS, r as ClientMessageSchema, v as HOOK_TIMEOUT_MS, w as RUN_CODE_TIMEOUT_MS, x as MAX_PAGE_CHARS, y as MAX_GLOB_PATTERN_LENGTH } from "./protocol-rcOrz7T3.js";
4
- import { callResolveTurnConfig, createAgentHooks } from "./isolate/hooks.js";
1
+ import { a as DEFAULT_SHUTDOWN_TIMEOUT_MS, c as FETCH_TIMEOUT_MS, d as MAX_PAGE_CHARS, f as MAX_TOOL_RESULT_CHARS, g as TOOL_EXECUTION_TIMEOUT_MS, h as RUN_CODE_TIMEOUT_MS, l as MAX_HTML_BYTES, m as MAX_WS_PAYLOAD_BYTES, o as DEFAULT_STT_SAMPLE_RATE, p as MAX_VALUE_SIZE, s as DEFAULT_TTS_SAMPLE_RATE, t as AGENT_CSP } from "../constants-VTFoymJ-.js";
2
+ import { i as toolError, n as errorDetail, r as errorMessage, t as parseWsUpgradeParams } from "../ws-upgrade-BeOQ7fXL.js";
3
+ import { ClientMessageSchema, buildReadyConfig, lenientParse } from "../sdk/protocol.js";
4
+ import { a as agentToolsToSchemas, o as toAgentConfig, r as EMPTY_PARAMS, t as buildSystemPrompt } from "../system-prompt-nik_iavo.js";
5
5
  import { z } from "zod";
6
+ import { convert } from "html-to-text";
6
7
  import vm from "node:vm";
7
8
  import pTimeout from "p-timeout";
8
9
  import { createStorage, prefixStorage } from "unstorage";
9
10
  import { createNanoEvents } from "nanoevents";
10
- import WsWebSocket from "ws";
11
+ import WsWebSocket, { WebSocketServer } from "ws";
12
+ import fs from "node:fs";
13
+ import http from "node:http";
14
+ import path from "node:path";
15
+ import escapeHtml from "escape-html";
16
+ import { lookup } from "mime-types";
11
17
  //#region host/_run-code.ts
12
18
  /**
13
19
  * run_code built-in tool — executes user JavaScript in a fresh `node:vm`
14
20
  * context with no network, filesystem, or process access.
15
21
  */
22
+ const SKIPPED_CLASS_KEYS = new Set([
23
+ "constructor",
24
+ "prototype",
25
+ "length",
26
+ "name"
27
+ ]);
28
+ /**
29
+ * Copy static members from a class constructor to a wrapper function,
30
+ * skipping built-in keys that must not be forwarded.
31
+ */
32
+ function copyStaticMembers(src, dst) {
33
+ for (const key of Object.getOwnPropertyNames(src)) {
34
+ if (SKIPPED_CLASS_KEYS.has(key)) continue;
35
+ try {
36
+ const desc = Object.getOwnPropertyDescriptor(src, key);
37
+ if (desc) Object.defineProperty(dst, key, desc);
38
+ } catch {}
39
+ }
40
+ }
41
+ /**
42
+ * Neuter the `.constructor` chain on a host function or class constructor.
43
+ *
44
+ * For plain functions: wraps the function so calling `.constructor` or
45
+ * `.constructor.constructor` no longer exposes the host `Function`.
46
+ *
47
+ * For class constructors: additionally copies static methods and neutralizes
48
+ * `prototype.constructor` so instances created via `new` also cannot escape.
49
+ *
50
+ * This prevents sandbox code from reaching the host `Function` constructor
51
+ * via patterns like `fn.constructor.constructor('return process')()`.
52
+ */
53
+ function neutralizeConstructor(fn) {
54
+ const hasPrototype = typeof fn.prototype === "object" && fn.prototype !== null;
55
+ function Wrapper(...args) {
56
+ if (hasPrototype) return new fn(...args);
57
+ return fn(...args);
58
+ }
59
+ if (hasPrototype) {
60
+ copyStaticMembers(fn, Wrapper);
61
+ if (Wrapper.prototype) Object.defineProperty(Wrapper.prototype, "constructor", {
62
+ value: void 0,
63
+ writable: false,
64
+ configurable: false
65
+ });
66
+ }
67
+ Object.defineProperty(Wrapper, "constructor", {
68
+ value: void 0,
69
+ writable: false,
70
+ configurable: false
71
+ });
72
+ return Wrapper;
73
+ }
16
74
  const runCodeParams = z.object({ code: z.string().describe("JavaScript code to execute. Use console.log() for output.") });
17
75
  /**
18
76
  * Execute JavaScript code inside a fresh `node:vm` context.
@@ -48,34 +106,67 @@ function createRunCode() {
48
106
  async function executeInIsolate(code) {
49
107
  const output = [];
50
108
  const capture = (...args) => output.push(args.map(String).join(" "));
109
+ const activeTimers = /* @__PURE__ */ new Set();
110
+ const sandboxSetTimeout = (fn, delay, ...args) => {
111
+ const id = setTimeout((...a) => {
112
+ activeTimers.delete(id);
113
+ fn(...a);
114
+ }, delay, ...args);
115
+ activeTimers.add(id);
116
+ return id;
117
+ };
118
+ const sandboxClearTimeout = (id) => {
119
+ if (id !== void 0) {
120
+ activeTimers.delete(id);
121
+ clearTimeout(id);
122
+ }
123
+ };
124
+ const sandboxSetInterval = (fn, delay, ...args) => {
125
+ const id = setInterval(fn, delay, ...args);
126
+ activeTimers.add(id);
127
+ return id;
128
+ };
129
+ const sandboxClearInterval = (id) => {
130
+ if (id !== void 0) {
131
+ activeTimers.delete(id);
132
+ clearInterval(id);
133
+ }
134
+ };
51
135
  const context = vm.createContext({
52
- console: {
53
- log: capture,
54
- info: capture,
55
- warn: capture,
56
- error: capture,
57
- debug: capture
58
- },
59
- setTimeout,
60
- clearTimeout,
61
- setInterval,
62
- clearInterval,
63
- URL,
64
- URLSearchParams,
65
- TextEncoder,
66
- TextDecoder,
67
- atob,
68
- btoa,
69
- structuredClone,
70
- queueMicrotask
71
- });
136
+ console: Object.freeze({
137
+ log: neutralizeConstructor(capture),
138
+ info: neutralizeConstructor(capture),
139
+ warn: neutralizeConstructor(capture),
140
+ error: neutralizeConstructor(capture),
141
+ debug: neutralizeConstructor(capture)
142
+ }),
143
+ setTimeout: neutralizeConstructor(sandboxSetTimeout),
144
+ clearTimeout: neutralizeConstructor(sandboxClearTimeout),
145
+ setInterval: neutralizeConstructor(sandboxSetInterval),
146
+ clearInterval: neutralizeConstructor(sandboxClearInterval),
147
+ URL: neutralizeConstructor(URL),
148
+ URLSearchParams: neutralizeConstructor(URLSearchParams),
149
+ TextEncoder: neutralizeConstructor(TextEncoder),
150
+ TextDecoder: neutralizeConstructor(TextDecoder),
151
+ atob: neutralizeConstructor(atob),
152
+ btoa: neutralizeConstructor(btoa),
153
+ structuredClone: neutralizeConstructor(structuredClone)
154
+ }, { codeGeneration: {
155
+ strings: false,
156
+ wasm: false
157
+ } });
72
158
  try {
73
159
  const wrapped = `(async () => {\n${code}\n})()`;
74
- const promise = new vm.Script(wrapped, { filename: "run_code.js" }).runInContext(context, { timeout: RUN_CODE_TIMEOUT_MS });
75
- await Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("Code execution timed out")), RUN_CODE_TIMEOUT_MS))]);
160
+ await new vm.Script(wrapped, { filename: "run_code.js" }).runInContext(context, { timeout: RUN_CODE_TIMEOUT_MS });
76
161
  return output.join("\n").trim() || "Code ran successfully (no output)";
77
162
  } catch (err) {
78
163
  return { error: errorMessage(err) };
164
+ } finally {
165
+ for (const id of activeTimers) {
166
+ clearTimeout(id);
167
+ clearInterval(id);
168
+ }
169
+ activeTimers.clear();
79
170
  }
80
171
  }
81
172
  //#endregion
@@ -88,10 +179,7 @@ async function executeInIsolate(code) {
88
179
  * Network requests go through the host's fetch proxy (with SSRF protection).
89
180
  */
90
181
  const fetchSignal = () => AbortSignal.timeout(FETCH_TIMEOUT_MS);
91
- /** Strip HTML tags and decode common entities. */
92
- function htmlToText(html) {
93
- return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\s{2,}/g, " ").trim();
94
- }
182
+ const htmlToText = (html) => convert(html, { wordwrap: false });
95
183
  const webSearchParams = z.object({
96
184
  query: z.string().describe("The search query"),
97
185
  max_results: z.number().describe("Maximum number of results to return (default 5)").optional()
@@ -226,40 +314,46 @@ function resolveBuiltin(name, opts) {
226
314
  }
227
315
  }
228
316
  /**
229
- * Create built-in tool definitions for the given tool names.
230
- * For runtime use.
317
+ * Resolve all builtin tools in one pass, returning defs, schemas, and guidance.
318
+ * Avoids redundant calls to `resolveBuiltin` and `z.toJSONSchema`.
231
319
  */
232
- function getBuiltinToolDefs(names, opts) {
320
+ function resolveAllBuiltins(names, opts) {
233
321
  const defs = {};
234
- for (const name of names) for (const [k, v] of resolveBuiltin(name, opts)) defs[k] = v;
235
- return defs;
236
- }
237
- /** Returns system prompt guidance strings for the specified builtin tools. */
238
- function getBuiltinToolGuidance(names) {
239
- return names.flatMap((name) => resolveBuiltin(name).map(([, def]) => def.guidance).filter((g) => Boolean(g)));
240
- }
241
- /** Returns JSON tool schemas for the specified builtin tools. */
242
- function getBuiltinToolSchemas(names) {
243
- return names.flatMap((name) => resolveBuiltin(name).map(([toolName, def]) => ({
244
- name: toolName,
245
- description: def.description,
246
- parameters: z.toJSONSchema(def.parameters ?? EMPTY_PARAMS)
247
- })));
322
+ const schemas = [];
323
+ const guidance = [];
324
+ for (const name of names) for (const [toolName, def] of resolveBuiltin(name, opts)) {
325
+ defs[toolName] = def;
326
+ schemas.push({
327
+ name: toolName,
328
+ description: def.description,
329
+ parameters: z.toJSONSchema(def.parameters ?? EMPTY_PARAMS)
330
+ });
331
+ const g = def.guidance;
332
+ if (g) guidance.push(g);
333
+ }
334
+ return {
335
+ defs,
336
+ schemas,
337
+ guidance
338
+ };
248
339
  }
249
340
  //#endregion
250
- //#region host/runtime.ts
341
+ //#region host/runtime-config.ts
251
342
  /**
252
343
  * Runtime dependencies injected into the session pipeline.
253
344
  *
254
345
  * Defines the {@link Logger} interface, a default {@link consoleLogger},
255
346
  * and the {@link S2SConfig} for Speech-to-Speech endpoint configuration.
256
347
  */
348
+ function consoleLog(fn) {
349
+ return (msg, ctx) => ctx ? fn(msg, ctx) : fn(msg);
350
+ }
257
351
  /** Default console-backed logger. */
258
352
  const consoleLogger = {
259
- info: (msg, ctx) => ctx ? console.log(msg, ctx) : console.log(msg),
260
- warn: (msg, ctx) => ctx ? console.warn(msg, ctx) : console.warn(msg),
261
- error: (msg, ctx) => ctx ? console.error(msg, ctx) : console.error(msg),
262
- debug: (msg, ctx) => ctx ? console.debug(msg, ctx) : console.debug(msg)
353
+ info: consoleLog(console.log),
354
+ warn: consoleLog(console.warn),
355
+ error: consoleLog(console.error),
356
+ debug: consoleLog(console.debug)
263
357
  };
264
358
  /**
265
359
  * Structured JSON logger for production diagnostics. Each log entry is a
@@ -285,7 +379,7 @@ const jsonLogger = {
285
379
  };
286
380
  /** Default S2S endpoint configuration. */
287
381
  const DEFAULT_S2S_CONFIG = {
288
- wssUrl: "wss://speech-to-speech.us.assemblyai.com/v1/realtime",
382
+ wssUrl: "wss://agents.assemblyai.com/v1/voice",
289
383
  inputSampleRate: DEFAULT_STT_SAMPLE_RATE,
290
384
  outputSampleRate: DEFAULT_TTS_SAMPLE_RATE
291
385
  };
@@ -293,7 +387,6 @@ const DEFAULT_S2S_CONFIG = {
293
387
  //#region host/s2s.ts
294
388
  const uint8ToBase64 = (bytes) => Buffer.from(bytes).toString("base64");
295
389
  const base64ToUint8 = (base64) => new Uint8Array(Buffer.from(base64, "base64"));
296
- const WS_OPEN = 1;
297
390
  const defaultCreateS2sWebSocket = (url, opts) => new WsWebSocket(url, { headers: opts.headers });
298
391
  const S2sMessageSchema = z.discriminatedUnion("type", [
299
392
  z.object({
@@ -303,10 +396,6 @@ const S2sMessageSchema = z.discriminatedUnion("type", [
303
396
  z.object({ type: z.literal("session.updated") }).passthrough(),
304
397
  z.object({ type: z.literal("input.speech.started") }),
305
398
  z.object({ type: z.literal("input.speech.stopped") }),
306
- z.object({
307
- type: z.literal("transcript.user.delta"),
308
- text: z.string()
309
- }),
310
399
  z.object({
311
400
  type: z.literal("transcript.user"),
312
401
  item_id: z.string(),
@@ -316,10 +405,6 @@ const S2sMessageSchema = z.discriminatedUnion("type", [
316
405
  type: z.literal("reply.started"),
317
406
  reply_id: z.string()
318
407
  }),
319
- z.object({
320
- type: z.literal("transcript.agent.delta"),
321
- delta: z.string()
322
- }).passthrough(),
323
408
  z.object({
324
409
  type: z.literal("transcript.agent"),
325
410
  text: z.string(),
@@ -327,8 +412,6 @@ const S2sMessageSchema = z.discriminatedUnion("type", [
327
412
  item_id: z.string().optional().default(""),
328
413
  interrupted: z.boolean().optional().default(false)
329
414
  }),
330
- z.object({ type: z.literal("reply.content_part.started") }).passthrough(),
331
- z.object({ type: z.literal("reply.content_part.done") }).passthrough(),
332
415
  z.object({
333
416
  type: z.literal("tool.call"),
334
417
  call_id: z.string(),
@@ -358,66 +441,48 @@ function dispatchS2sMessage(emitter, msg) {
358
441
  case "session.ready":
359
442
  emitter.emit("ready", { sessionId: msg.session_id });
360
443
  break;
361
- case "session.updated":
362
- emitter.emit("sessionUpdated", msg);
363
- break;
444
+ case "session.updated": break;
364
445
  case "input.speech.started":
365
- emitter.emit("speechStarted");
446
+ emitter.emit("event", { type: "speech_started" });
366
447
  break;
367
448
  case "input.speech.stopped":
368
- emitter.emit("speechStopped");
369
- break;
370
- case "transcript.user.delta":
371
- emitter.emit("userTranscriptDelta", { text: msg.text });
449
+ emitter.emit("event", { type: "speech_stopped" });
372
450
  break;
373
451
  case "transcript.user":
374
- emitter.emit("userTranscript", {
375
- itemId: msg.item_id,
452
+ emitter.emit("event", {
453
+ type: "user_transcript",
376
454
  text: msg.text
377
455
  });
378
456
  break;
379
457
  case "reply.started":
380
458
  emitter.emit("replyStarted", { replyId: msg.reply_id });
381
459
  break;
382
- case "transcript.agent.delta":
383
- emitter.emit("agentTranscriptDelta", { text: msg.delta });
384
- break;
385
460
  case "transcript.agent":
386
- emitter.emit("agentTranscript", {
461
+ emitter.emit("event", {
462
+ type: "agent_transcript",
387
463
  text: msg.text,
388
- replyId: msg.reply_id,
389
- itemId: msg.item_id,
390
- interrupted: msg.interrupted
464
+ _interrupted: msg.interrupted
391
465
  });
392
466
  break;
393
467
  case "tool.call":
394
- emitter.emit("toolCall", {
395
- callId: msg.call_id,
396
- name: msg.name,
468
+ emitter.emit("event", {
469
+ type: "tool_call",
470
+ toolCallId: msg.call_id,
471
+ toolName: msg.name,
397
472
  args: msg.args
398
473
  });
399
474
  break;
400
475
  case "reply.done":
401
- emitter.emit("replyDone", msg.status ? { status: msg.status } : {});
476
+ if (msg.status === "interrupted") emitter.emit("event", { type: "cancelled" });
477
+ else emitter.emit("event", { type: "reply_done" });
402
478
  break;
403
479
  case "session.error":
404
- if (msg.code === "session_not_found" || msg.code === "session_forbidden") emitter.emit("sessionExpired", {
405
- code: msg.code,
406
- message: msg.message
407
- });
408
- else emitter.emit("error", {
409
- code: msg.code,
410
- message: msg.message
411
- });
480
+ if (msg.code === "session_not_found" || msg.code === "session_forbidden") emitter.emit("sessionExpired");
481
+ else emitter.emit("error", new Error(msg.message));
412
482
  break;
413
483
  case "error":
414
- emitter.emit("error", {
415
- code: "connection",
416
- message: msg.message
417
- });
484
+ emitter.emit("error", new Error(msg.message));
418
485
  break;
419
- case "reply.content_part.started":
420
- case "reply.content_part.done": break;
421
486
  default: break;
422
487
  }
423
488
  }
@@ -429,18 +494,19 @@ function connectS2s(opts) {
429
494
  const emitter = createNanoEvents();
430
495
  let opened = false;
431
496
  function send(msg) {
432
- if (ws.readyState !== WS_OPEN) {
497
+ if (ws.readyState !== 1) {
433
498
  log.debug("S2S send dropped: socket not open", { type: msg.type });
434
499
  return;
435
500
  }
436
501
  const json = JSON.stringify(msg);
437
- if (msg.type !== "input.audio") log.info(`S2S >> ${msg.type}`, msg.type === "session.update" ? { payload: json } : void 0);
502
+ if (msg.type !== "input.audio") if (msg.type === "session.update") log.info(`S2S >> ${msg.type}`, { payload: json });
503
+ else log.info(`S2S >> ${msg.type}`);
438
504
  ws.send(json);
439
505
  }
440
506
  const handle = {
441
507
  on: emitter.on.bind(emitter),
442
508
  sendAudio(audio) {
443
- if (ws.readyState !== WS_OPEN) {
509
+ if (ws.readyState !== 1) {
444
510
  log.debug("S2S sendAudio dropped: socket not open");
445
511
  return;
446
512
  }
@@ -501,7 +567,7 @@ function connectS2s(opts) {
501
567
  }
502
568
  function logIncoming(obj) {
503
569
  if (obj.type === "reply.audio" || obj.type === "input.audio") return;
504
- log.info(`S2S << ${obj.type}`, obj.type === "transcript.agent.delta" ? { delta: obj.delta } : void 0);
570
+ log.info(`S2S << ${obj.type}`);
505
571
  }
506
572
  function handleS2sMessage(ev) {
507
573
  const raw = tryParseJson(ev.data);
@@ -522,32 +588,29 @@ function connectS2s(opts) {
522
588
  }
523
589
  ws.addEventListener("message", handleS2sMessage);
524
590
  ws.addEventListener("close", (ev) => {
591
+ const code = ev.code ?? 0;
592
+ const reason = ev.reason ?? "";
525
593
  log.info("S2S WebSocket closed", {
526
- code: ev.code ?? 0,
527
- reason: ev.reason ?? ""
594
+ code,
595
+ reason
528
596
  });
529
- if (!opened) reject(/* @__PURE__ */ new Error(`WebSocket closed before open (code: ${ev.code ?? 0})`));
530
- emitter.emit("close");
597
+ if (!opened) reject(/* @__PURE__ */ new Error(`WebSocket closed before open (code: ${code})`));
598
+ emitter.emit("close", code, reason);
531
599
  });
532
600
  ws.addEventListener("error", (ev) => {
533
601
  const message = typeof ev.message === "string" ? ev.message : "WebSocket error";
534
602
  const errObj = new Error(message);
535
603
  log.error("S2S WebSocket error", { error: errObj.message });
536
604
  if (!opened) reject(errObj);
537
- else emitter.emit("error", {
538
- code: "ws_error",
539
- message: errObj.message
540
- });
605
+ else emitter.emit("error", errObj);
541
606
  });
542
607
  });
543
608
  }
544
609
  //#endregion
545
- //#region host/session.ts
610
+ //#region host/session-ctx.ts
546
611
  function buildCtx(opts) {
547
- const { id, agentConfig, hooks, log } = opts;
612
+ const { agentConfig, log } = opts;
548
613
  const maxHistory = opts.maxHistory ?? 200;
549
- /** Track in-flight hook promises so they can be awaited during shutdown. */
550
- const pendingHooks = /* @__PURE__ */ new Set();
551
614
  const ctx = {
552
615
  ...opts,
553
616
  s2s: null,
@@ -559,12 +622,9 @@ function buildCtx(opts) {
559
622
  turnPromise: null,
560
623
  conversationMessages: [],
561
624
  maxHistory,
562
- resolveTurnConfig() {
563
- return callResolveTurnConfig(hooks, id, HOOK_TIMEOUT_MS);
564
- },
565
- consumeToolCallStep(turnConfig, _name, replyId) {
625
+ consumeToolCallStep(_name, replyId) {
566
626
  if (replyId === null || replyId !== ctx.reply.currentReplyId) return toolError("Reply was interrupted. Discarding stale tool call.");
567
- const maxSteps = turnConfig?.maxSteps ?? agentConfig.maxSteps;
627
+ const maxSteps = agentConfig.maxSteps;
568
628
  ctx.reply.toolCallCount++;
569
629
  if (maxSteps !== void 0 && ctx.reply.toolCallCount > maxSteps) {
570
630
  log.info("maxSteps exceeded, refusing tool call", {
@@ -575,32 +635,9 @@ function buildCtx(opts) {
575
635
  }
576
636
  return null;
577
637
  },
578
- fireHook(name, ...args) {
579
- if (!hooks) return;
580
- const notifyOnError = (err) => {
581
- log.warn(`${name} hook failed`, { err: errorMessage(err) });
582
- if (name !== "error") {
583
- const ep = hooks.callHook("error", id, { message: errorMessage(err) });
584
- if (ep && typeof ep.catch === "function") ep.catch((e) => {
585
- log.warn("error hook failed", { err: errorMessage(e) });
586
- });
587
- }
588
- };
589
- try {
590
- const result = hooks.callHook(name, ...args);
591
- if (result == null) return;
592
- const p = result.catch(notifyOnError).finally(() => pendingHooks.delete(p));
593
- pendingHooks.add(p);
594
- } catch (err) {
595
- notifyOnError(err);
596
- }
597
- },
598
- async drainHooks() {
599
- if (pendingHooks.size > 0) await Promise.all([...pendingHooks]);
600
- },
601
638
  pushMessages(...msgs) {
602
639
  ctx.conversationMessages.push(...msgs);
603
- if (maxHistory > 0 && ctx.conversationMessages.length > maxHistory) ctx.conversationMessages = ctx.conversationMessages.slice(-maxHistory);
640
+ if (maxHistory > 0 && ctx.conversationMessages.length > maxHistory) ctx.conversationMessages.splice(0, ctx.conversationMessages.length - maxHistory);
604
641
  },
605
642
  beginReply(replyId) {
606
643
  ctx.reply = {
@@ -623,8 +660,15 @@ function buildCtx(opts) {
623
660
  };
624
661
  return ctx;
625
662
  }
663
+ //#endregion
664
+ //#region host/session.ts
626
665
  /** @internal Not part of the public API. Exposed for testing only. */
627
666
  const _internals = { connectS2s };
667
+ /**
668
+ * Create an idle timer that closes the S2S connection after inactivity.
669
+ * Convention: `timeoutMs <= 0` disables the timer entirely (returns a no-op).
670
+ * This allows agents to opt out of idle timeout via `idleTimeoutMs: 0` in their config.
671
+ */
628
672
  function createIdleTimer(opts) {
629
673
  if (opts.timeoutMs <= 0) return {
630
674
  reset() {},
@@ -671,25 +715,11 @@ function finishToolCall(ctx, callId, result, replyId) {
671
715
  if (ctx.maxHistory > 0 && ctx.reply.pendingTools.length > ctx.maxHistory) ctx.reply.pendingTools.shift();
672
716
  }
673
717
  }
674
- async function handleToolCall(ctx, detail) {
675
- const { callId, name, args: parsedArgs } = detail;
718
+ async function handleToolCall(ctx, event) {
719
+ const { toolCallId: callId, toolName: name, args: parsedArgs } = event;
676
720
  const replyId = ctx.reply.currentReplyId;
677
- ctx.client.event({
678
- type: "tool_call_start",
679
- toolCallId: callId,
680
- toolName: name,
681
- args: parsedArgs
682
- });
683
- let turnConfig;
684
- try {
685
- turnConfig = await ctx.resolveTurnConfig();
686
- } catch (err) {
687
- const msg = `resolveTurnConfig hook error: ${errorMessage(err)}`;
688
- ctx.log.error(msg);
689
- finishToolCall(ctx, callId, toolError(msg), replyId);
690
- return;
691
- }
692
- const refused = ctx.consumeToolCallStep(turnConfig, name, replyId);
721
+ ctx.client.event(event);
722
+ const refused = ctx.consumeToolCallStep(name, replyId);
693
723
  if (refused !== null) {
694
724
  finishToolCall(ctx, callId, refused, replyId);
695
725
  return;
@@ -721,23 +751,17 @@ async function handleToolCall(ctx, detail) {
721
751
  function handleUserTranscript(ctx, text) {
722
752
  ctx.log.info("S2S user transcript", { text });
723
753
  ctx.client.event({
724
- type: "transcript",
725
- text,
726
- isFinal: true
727
- });
728
- ctx.client.event({
729
- type: "turn",
754
+ type: "user_transcript",
730
755
  text
731
756
  });
732
757
  ctx.pushMessages({
733
758
  role: "user",
734
759
  content: text
735
760
  });
736
- ctx.fireHook("turn", ctx.id, text, HOOK_TIMEOUT_MS);
737
761
  }
738
762
  function handleAgentTranscript(ctx, text, interrupted) {
739
763
  ctx.client.event({
740
- type: "chat",
764
+ type: "agent_transcript",
741
765
  text
742
766
  });
743
767
  if (!interrupted) ctx.pushMessages({
@@ -745,13 +769,12 @@ function handleAgentTranscript(ctx, text, interrupted) {
745
769
  content: text
746
770
  });
747
771
  }
748
- function handleReplyDone(ctx, status) {
749
- if (status === "interrupted") {
750
- ctx.log.info("S2S reply interrupted (barge-in)");
751
- ctx.cancelReply();
752
- ctx.client.event({ type: "cancelled" });
753
- return;
754
- }
772
+ function handleReplyCancelled(ctx) {
773
+ ctx.log.info("S2S reply interrupted (barge-in)");
774
+ ctx.cancelReply();
775
+ ctx.client.event({ type: "cancelled" });
776
+ }
777
+ function handleReplyDone(ctx) {
755
778
  const doneReplyId = ctx.reply.currentReplyId;
756
779
  const sendPending = () => {
757
780
  if (ctx.reply.currentReplyId !== doneReplyId) {
@@ -768,7 +791,7 @@ function handleReplyDone(ctx, status) {
768
791
  agent: ctx.agent
769
792
  });
770
793
  ctx.client.playAudioDone();
771
- ctx.client.event({ type: "tts_done" });
794
+ ctx.client.event({ type: "reply_done" });
772
795
  }
773
796
  };
774
797
  if (ctx.turnPromise !== null) ctx.turnPromise.then(sendPending);
@@ -776,54 +799,58 @@ function handleReplyDone(ctx, status) {
776
799
  }
777
800
  function setupListeners(ctx, handle) {
778
801
  handle.on("ready", ({ sessionId }) => ctx.log.info("S2S session ready", { sessionId }));
802
+ handle.on("replyStarted", ({ replyId }) => {
803
+ ctx.beginReply(replyId);
804
+ });
779
805
  handle.on("sessionExpired", () => {
780
806
  ctx.log.info("S2S session expired");
781
807
  handle.close();
782
808
  });
783
- handle.on("speechStarted", () => ctx.client.event({ type: "speech_started" }));
784
- handle.on("speechStopped", () => ctx.client.event({ type: "speech_stopped" }));
785
- handle.on("userTranscriptDelta", ({ text }) => ctx.client.event({
786
- type: "transcript",
787
- text,
788
- isFinal: false
789
- }));
790
- handle.on("userTranscript", ({ text }) => handleUserTranscript(ctx, text));
791
- handle.on("replyStarted", ({ replyId }) => {
792
- ctx.beginReply(replyId);
793
- });
794
809
  handle.on("audio", ({ audio }) => ctx.client.playAudioChunk(audio));
795
- handle.on("agentTranscriptDelta", ({ text }) => ctx.client.event({
796
- type: "chat_delta",
797
- text
798
- }));
799
- handle.on("agentTranscript", ({ text, interrupted }) => handleAgentTranscript(ctx, text, interrupted));
800
- handle.on("toolCall", (detail) => {
801
- const p = handleToolCall(ctx, detail).catch((err) => {
802
- ctx.log.error("Tool call handler failed", { err: errorMessage(err) });
803
- });
804
- ctx.chainTurn(p);
805
- });
806
- handle.on("replyDone", ({ status }) => handleReplyDone(ctx, status));
807
- handle.on("error", ({ code, message }) => {
808
- ctx.log.error("S2S error", {
809
- code,
810
- message
811
- });
810
+ handle.on("error", (err) => {
811
+ ctx.log.error("S2S error", { message: err.message });
812
812
  ctx.client.event({
813
813
  type: "error",
814
814
  code: "internal",
815
- message
815
+ message: err.message
816
816
  });
817
817
  handle.close();
818
818
  });
819
- handle.on("close", () => {
820
- ctx.log.info("S2S closed");
819
+ handle.on("close", (code, reason) => {
820
+ ctx.log.info("S2S closed", {
821
+ code,
822
+ reason
823
+ });
821
824
  ctx.s2s = null;
822
825
  ctx.cancelReply();
823
826
  });
827
+ handle.on("event", (event) => {
828
+ switch (event.type) {
829
+ case "user_transcript":
830
+ handleUserTranscript(ctx, event.text);
831
+ break;
832
+ case "agent_transcript":
833
+ handleAgentTranscript(ctx, event.text, event._interrupted ?? false);
834
+ break;
835
+ case "tool_call": {
836
+ const p = handleToolCall(ctx, event).catch((err) => {
837
+ ctx.log.error("Tool call handler failed", { err: errorMessage(err) });
838
+ });
839
+ ctx.chainTurn(p);
840
+ break;
841
+ }
842
+ case "reply_done":
843
+ handleReplyDone(ctx);
844
+ break;
845
+ case "cancelled":
846
+ handleReplyCancelled(ctx);
847
+ break;
848
+ default: ctx.client.event(event);
849
+ }
850
+ });
824
851
  }
825
852
  function createS2sSession(opts) {
826
- const { id, agent, client, toolSchemas, apiKey, s2sConfig, executeTool, createWebSocket = defaultCreateS2sWebSocket, hooks, logger: log = consoleLogger } = opts;
853
+ const { id, agent, client, toolSchemas, apiKey, s2sConfig, executeTool, createWebSocket = defaultCreateS2sWebSocket, logger: log = consoleLogger } = opts;
827
854
  const agentConfig = opts.skipGreeting ? {
828
855
  ...opts.agentConfig,
829
856
  greeting: ""
@@ -846,7 +873,6 @@ function createS2sSession(opts) {
846
873
  client,
847
874
  agentConfig,
848
875
  executeTool,
849
- hooks,
850
876
  log,
851
877
  maxHistory: opts.maxHistory
852
878
  });
@@ -893,7 +919,6 @@ function createS2sSession(opts) {
893
919
  }
894
920
  return {
895
921
  async start() {
896
- ctx.fireHook("connect", id, HOOK_TIMEOUT_MS);
897
922
  await connectAndSetup();
898
923
  },
899
924
  async stop() {
@@ -901,10 +926,7 @@ function createS2sSession(opts) {
901
926
  sessionAbort.abort();
902
927
  idle.clear();
903
928
  if (ctx.turnPromise !== null) await ctx.turnPromise;
904
- await ctx.drainHooks();
905
929
  ctx.s2s?.close();
906
- ctx.fireHook("disconnect", id, HOOK_TIMEOUT_MS);
907
- await ctx.drainHooks();
908
930
  },
909
931
  onAudio(data) {
910
932
  idle.reset();
@@ -936,33 +958,50 @@ function createS2sSession(opts) {
936
958
  };
937
959
  }
938
960
  //#endregion
939
- //#region isolate/_kv-utils.ts
940
- /** Internal KV helpers shared by kv.ts and unstorage-kv.ts. */
941
- /** Sort entries by key and apply reverse/limit options. Mutates the array. */
942
- function sortAndPaginate(entries, options) {
943
- entries.sort((a, b) => a.key.localeCompare(b.key));
944
- if (options?.reverse) entries.reverse();
945
- if (options?.limit && options.limit > 0) entries.length = Math.min(entries.length, options.limit);
946
- return entries;
961
+ //#region host/tool-executor.ts
962
+ /**
963
+ * Tool execution validates arguments and invokes tool handlers.
964
+ *
965
+ * {@link executeToolCall} is the single entry point used by both the
966
+ * direct (self-hosted) runtime and the platform sandbox sidecar.
967
+ */
968
+ const yieldTick = () => new Promise((r) => setTimeout(r, 0));
969
+ function buildToolContext(opts) {
970
+ const { env, state, kv, messages, sessionId } = opts;
971
+ return {
972
+ env,
973
+ state: state ?? {},
974
+ get kv() {
975
+ if (!kv) throw new Error("KV not available");
976
+ return kv;
977
+ },
978
+ messages: messages ?? [],
979
+ sessionId: sessionId ?? ""
980
+ };
947
981
  }
948
- /** Simple glob matcher supports `*` as a wildcard for any characters. */
949
- function matchGlob(key, pattern) {
950
- if (pattern.length > 1024) throw new Error(`Glob pattern exceeds maximum length of ${MAX_GLOB_PATTERN_LENGTH}`);
951
- const parts = pattern.split("*");
952
- if (parts.length === 1) return key === pattern;
953
- const first = parts[0];
954
- if (!key.startsWith(first)) return false;
955
- const last = parts.at(-1);
956
- if (key.length < first.length + last.length) return false;
957
- if (!key.endsWith(last)) return false;
958
- let pos = first.length;
959
- const end = key.length - last.length;
960
- for (const part of parts.slice(1, -1)) {
961
- const idx = key.indexOf(part, pos);
962
- if (idx === -1 || idx > end) return false;
963
- pos = idx + part.length;
982
+ async function executeToolCall(name, args, options) {
983
+ const { tool } = options;
984
+ const parsed = (tool.parameters ?? EMPTY_PARAMS).safeParse(args);
985
+ if (!parsed.success) return toolError(`Invalid arguments for tool "${name}": ${(parsed.error?.issues ?? []).map((i) => `${i.path.map(String).join(".")}: ${i.message}`).join(", ")}`);
986
+ try {
987
+ const ctx = buildToolContext(options);
988
+ await yieldTick();
989
+ const result = await pTimeout(Promise.resolve(tool.execute(parsed.data, ctx)), {
990
+ milliseconds: TOOL_EXECUTION_TIMEOUT_MS,
991
+ message: `Tool "${name}" timed out after ${TOOL_EXECUTION_TIMEOUT_MS}ms`
992
+ });
993
+ await yieldTick();
994
+ if (result == null) return "null";
995
+ return typeof result === "string" ? result : JSON.stringify(result);
996
+ } catch (err) {
997
+ const log = options.logger;
998
+ if (log) log.warn("Tool execution failed", {
999
+ tool: name,
1000
+ error: errorDetail(err)
1001
+ });
1002
+ else console.warn(`[tool-executor] Tool execution failed: ${name}`, err);
1003
+ return toolError(errorMessage(err));
964
1004
  }
965
- return pos <= end;
966
1005
  }
967
1006
  //#endregion
968
1007
  //#region host/unstorage-kv.ts
@@ -1003,23 +1042,6 @@ function createUnstorageKv(options) {
1003
1042
  const keyArray = Array.isArray(keys) ? keys : [keys];
1004
1043
  await Promise.all(keyArray.map((k) => store.removeItem(k)));
1005
1044
  },
1006
- async list(listPrefix, listOptions) {
1007
- const allKeys = await store.getKeys(listPrefix);
1008
- const entries = [];
1009
- for (const key of allKeys) {
1010
- const value = await store.getItem(key);
1011
- if (value != null) entries.push({
1012
- key,
1013
- value
1014
- });
1015
- }
1016
- return sortAndPaginate(entries, listOptions);
1017
- },
1018
- async keys(pattern) {
1019
- const allKeys = await store.getKeys();
1020
- if (!pattern) return allKeys.sort((a, b) => a.localeCompare(b));
1021
- return allKeys.filter((key) => matchGlob(key, pattern)).sort((a, b) => a.localeCompare(b));
1022
- },
1023
1045
  close() {
1024
1046
  store.dispose();
1025
1047
  }
@@ -1086,12 +1108,12 @@ function handleTextMessage(data, session, log, ctx, sid) {
1086
1108
  });
1087
1109
  return;
1088
1110
  }
1089
- const parsed = ClientMessageSchema.safeParse(json);
1090
- if (!parsed.success) {
1091
- log.warn("Invalid client message", {
1111
+ const parsed = lenientParse(ClientMessageSchema, json);
1112
+ if (!parsed.ok) {
1113
+ if (parsed.malformed) log.warn("Invalid client message", {
1092
1114
  ...ctx,
1093
1115
  sid,
1094
- error: parsed.error.message
1116
+ error: parsed.error
1095
1117
  });
1096
1118
  return;
1097
1119
  }
@@ -1183,7 +1205,7 @@ function wireSessionSocket(ws, opts) {
1183
1205
  ws.addEventListener("message", (event) => {
1184
1206
  if (!session) return;
1185
1207
  if (!sessionReady) {
1186
- messageBuffer?.push(event);
1208
+ if (messageBuffer && messageBuffer.length < 100) messageBuffer.push(event);
1187
1209
  return;
1188
1210
  }
1189
1211
  const { data } = event;
@@ -1203,6 +1225,7 @@ function wireSessionSocket(ws, opts) {
1203
1225
  });
1204
1226
  }).finally(() => {
1205
1227
  sessions.delete(sessionId);
1228
+ opts.onSessionEnd?.(sessionId);
1206
1229
  });
1207
1230
  opts.onClose?.();
1208
1231
  });
@@ -1216,7 +1239,7 @@ function wireSessionSocket(ws, opts) {
1216
1239
  });
1217
1240
  }
1218
1241
  //#endregion
1219
- //#region host/direct-executor.ts
1242
+ //#region host/runtime.ts
1220
1243
  /**
1221
1244
  * Agent runtime — the execution engine for voice agents.
1222
1245
  *
@@ -1224,45 +1247,6 @@ function wireSessionSocket(ws, opts) {
1224
1247
  * self-hosted servers and the platform sandbox. It wires up tool execution,
1225
1248
  * lifecycle hooks, and session management.
1226
1249
  */
1227
- const yieldTick = () => new Promise((r) => setTimeout(r, 0));
1228
- function buildToolContext(opts) {
1229
- const { env, state, kv, messages, fetch: fetchFn, sessionId } = opts;
1230
- return {
1231
- env: { ...env },
1232
- state: state ?? {},
1233
- get kv() {
1234
- if (!kv) throw new Error("KV not available");
1235
- return kv;
1236
- },
1237
- messages: messages ?? [],
1238
- fetch: fetchFn ?? globalThis.fetch,
1239
- sessionId: sessionId ?? ""
1240
- };
1241
- }
1242
- async function executeToolCall(name, args, options) {
1243
- const { tool } = options;
1244
- const parsed = (tool.parameters ?? EMPTY_PARAMS).safeParse(args);
1245
- if (!parsed.success) return toolError(`Invalid arguments for tool "${name}": ${(parsed.error?.issues ?? []).map((i) => `${i.path.map(String).join(".")}: ${i.message}`).join(", ")}`);
1246
- try {
1247
- const ctx = buildToolContext(options);
1248
- await yieldTick();
1249
- const result = await pTimeout(Promise.resolve(tool.execute(parsed.data, ctx)), {
1250
- milliseconds: TOOL_EXECUTION_TIMEOUT_MS,
1251
- message: `Tool "${name}" timed out after ${TOOL_EXECUTION_TIMEOUT_MS}ms`
1252
- });
1253
- await yieldTick();
1254
- if (result == null) return "null";
1255
- return typeof result === "string" ? result : JSON.stringify(result);
1256
- } catch (err) {
1257
- const log = options.logger;
1258
- if (log) log.warn("Tool execution failed", {
1259
- tool: name,
1260
- error: errorDetail(err)
1261
- });
1262
- else console.warn(`[tool-executor] Tool execution failed: ${name}`, err);
1263
- return toolError(errorMessage(err));
1264
- }
1265
- }
1266
1250
  /** Create an in-memory KV store (default for self-hosted). */
1267
1251
  function createLocalKv() {
1268
1252
  return createUnstorageKv({ storage: createStorage() });
@@ -1285,40 +1269,43 @@ function createRuntime(opts) {
1285
1269
  const sessions = /* @__PURE__ */ new Map();
1286
1270
  const readyConfig = buildReadyConfig(s2sConfig);
1287
1271
  let executeTool;
1288
- let hooks;
1289
1272
  let toolSchemas;
1290
1273
  let toolGuidance = [];
1291
- if (opts.executeTool && opts.hooks && opts.toolSchemas) {
1292
- executeTool = opts.executeTool;
1293
- hooks = opts.hooks;
1274
+ const builtinFetchOpt = opts.fetch ? { fetch: opts.fetch } : void 0;
1275
+ if (opts.executeTool && opts.toolSchemas) {
1276
+ const builtinDefs = opts.builtinDefs ?? resolveAllBuiltins(agent.builtinTools ?? [], builtinFetchOpt).defs;
1277
+ const rpcExecuteTool = opts.executeTool;
1278
+ const frozenEnv = Object.freeze({ ...env });
1279
+ executeTool = async (name, args, sessionId, messages) => {
1280
+ if (builtinDefs[name]) {
1281
+ const tool = builtinDefs[name];
1282
+ return executeToolCall(name, args, {
1283
+ tool,
1284
+ env: frozenEnv,
1285
+ sessionId: sessionId ?? "",
1286
+ kv,
1287
+ messages,
1288
+ logger
1289
+ });
1290
+ }
1291
+ return rpcExecuteTool(name, args, sessionId, messages);
1292
+ };
1294
1293
  toolSchemas = opts.toolSchemas;
1295
1294
  toolGuidance = opts.toolGuidance ?? [];
1296
1295
  } else {
1296
+ const builtins = resolveAllBuiltins(agent.builtinTools ?? [], builtinFetchOpt);
1297
1297
  const allTools = {
1298
- ...getBuiltinToolDefs(agent.builtinTools ?? []),
1298
+ ...builtins.defs,
1299
1299
  ...agent.tools
1300
1300
  };
1301
- const customSchemas = agentToolsToSchemas(agent.tools ?? {});
1302
- const builtinSchemas = getBuiltinToolSchemas(agent.builtinTools ?? []);
1303
- toolSchemas = [...customSchemas, ...builtinSchemas];
1304
- toolGuidance = getBuiltinToolGuidance(agent.builtinTools ?? []);
1301
+ toolSchemas = [...agentToolsToSchemas(agent.tools ?? {}), ...builtins.schemas];
1302
+ toolGuidance = builtins.guidance;
1305
1303
  const stateMap = /* @__PURE__ */ new Map();
1306
1304
  const getState = (sid) => {
1307
1305
  if (!stateMap.has(sid) && agent.state) stateMap.set(sid, agent.state());
1308
1306
  return stateMap.get(sid) ?? {};
1309
1307
  };
1310
1308
  const frozenEnv = Object.freeze({ ...env });
1311
- function makeHookContext(sessionId) {
1312
- return {
1313
- env: frozenEnv,
1314
- state: getState(sessionId),
1315
- sessionId,
1316
- get kv() {
1317
- return kv;
1318
- },
1319
- fetch: globalThis.fetch
1320
- };
1321
- }
1322
1309
  executeTool = async (name, args, sessionId, messages) => {
1323
1310
  const tool = allTools[name];
1324
1311
  if (!tool) return toolError(`Unknown tool: ${name}`);
@@ -1329,17 +1316,9 @@ function createRuntime(opts) {
1329
1316
  sessionId: sessionId ?? "",
1330
1317
  kv,
1331
1318
  messages,
1332
- logger,
1333
- fetch: globalThis.fetch
1319
+ logger
1334
1320
  });
1335
1321
  };
1336
- hooks = createAgentHooks({
1337
- agent,
1338
- makeCtx: makeHookContext
1339
- });
1340
- hooks.hook("disconnect", async (sessionId) => {
1341
- stateMap.delete(sessionId);
1342
- });
1343
1322
  }
1344
1323
  function createSession(sessionOpts) {
1345
1324
  const apiKey = env.ASSEMBLYAI_API_KEY ?? "";
@@ -1354,7 +1333,6 @@ function createRuntime(opts) {
1354
1333
  s2sConfig,
1355
1334
  executeTool,
1356
1335
  ...createWebSocket ? { createWebSocket } : {},
1357
- hooks,
1358
1336
  skipGreeting: sessionOpts.skipGreeting ?? false,
1359
1337
  logger,
1360
1338
  ...sessionOpts.resumeFrom ? { resumeFrom: sessionOpts.resumeFrom } : {}
@@ -1376,32 +1354,23 @@ function createRuntime(opts) {
1376
1354
  ...startOpts?.logContext ? { logContext: startOpts.logContext } : {},
1377
1355
  ...startOpts?.onOpen ? { onOpen: startOpts.onOpen } : {},
1378
1356
  ...startOpts?.onClose ? { onClose: startOpts.onClose } : {},
1357
+ ...startOpts?.onSessionEnd ? { onSessionEnd: startOpts.onSessionEnd } : {},
1379
1358
  ...sessionStartTimeoutMs !== void 0 ? { sessionStartTimeoutMs } : {},
1380
1359
  ...resumeFrom ? { resumeFrom } : {}
1381
1360
  });
1382
1361
  }
1383
1362
  async function shutdown() {
1384
1363
  if (sessions.size === 0) return;
1385
- let timer;
1386
- const timeout = new Promise((resolve) => {
1387
- timer = setTimeout(resolve, shutdownTimeoutMs, "timeout");
1388
- });
1389
- const graceful = Promise.allSettled([...sessions.values()].map((s) => s.stop())).then((results) => {
1390
- for (const r of results) if (r.status === "rejected") logger.warn(`Session stop failed during shutdown: ${r.reason}`);
1391
- return "done";
1392
- });
1393
- let outcome;
1394
1364
  try {
1395
- outcome = await Promise.race([graceful, timeout]);
1396
- } finally {
1397
- if (timer) clearTimeout(timer);
1365
+ const results = await pTimeout(Promise.allSettled([...sessions.values()].map((s) => s.stop())), { milliseconds: shutdownTimeoutMs });
1366
+ for (const r of results) if (r.status === "rejected") logger.warn(`Session stop failed during shutdown: ${r.reason}`);
1367
+ } catch {
1368
+ logger.warn(`Shutdown timeout (${shutdownTimeoutMs}ms) exceeded — force-closing ${sessions.size} remaining session(s)`);
1398
1369
  }
1399
- if (outcome === "timeout") logger.warn(`Shutdown timeout (${shutdownTimeoutMs}ms) exceeded — force-closing ${sessions.size} remaining session(s)`);
1400
1370
  sessions.clear();
1401
1371
  }
1402
1372
  return {
1403
1373
  executeTool,
1404
- hooks,
1405
1374
  toolSchemas,
1406
1375
  createSession,
1407
1376
  startSession,
@@ -1410,4 +1379,140 @@ function createRuntime(opts) {
1410
1379
  };
1411
1380
  }
1412
1381
  //#endregion
1413
- export { _internals as a, connectS2s as c, consoleLogger as d, jsonLogger as f, executeInIsolate as g, getBuiltinToolSchemas as h, createUnstorageKv as i, defaultCreateS2sWebSocket as l, getBuiltinToolGuidance as m, executeToolCall as n, buildCtx as o, getBuiltinToolDefs as p, wireSessionSocket as r, createS2sSession as s, createRuntime as t, DEFAULT_S2S_CONFIG as u };
1382
+ //#region host/server.ts
1383
+ /**
1384
+ * Agent HTTP+WebSocket server.
1385
+ *
1386
+ * {@link createServer} wraps a {@link Runtime} with an HTTP + WebSocket
1387
+ * server using only `node:http` and `ws` (no framework dependencies).
1388
+ *
1389
+ * **Internal module** — used by `aai-cli` dev server. Not a public API.
1390
+ * Import via `aai/host`.
1391
+ */
1392
+ async function serveStatic(dir, req, res) {
1393
+ const url = req.url?.split("?")[0] ?? "/";
1394
+ const filePath = path.join(dir, url === "/" ? "index.html" : url);
1395
+ const resolved = path.resolve(dir);
1396
+ if (!filePath.startsWith(resolved + path.sep) && filePath !== resolved) return false;
1397
+ try {
1398
+ const stat = await fs.promises.stat(filePath);
1399
+ if (!stat.isFile()) return false;
1400
+ const mime = lookup(path.extname(filePath).toLowerCase()) || "application/octet-stream";
1401
+ res.writeHead(200, {
1402
+ "Content-Type": mime,
1403
+ "Content-Length": stat.size
1404
+ });
1405
+ fs.createReadStream(filePath).pipe(res);
1406
+ return true;
1407
+ } catch {
1408
+ return false;
1409
+ }
1410
+ }
1411
+ function handleKvGet(kv, req, res) {
1412
+ const key = new URL(req.url ?? "/", "http://localhost").searchParams.get("key");
1413
+ if (!key) {
1414
+ res.writeHead(400, { "Content-Type": "application/json" });
1415
+ res.end(JSON.stringify({ error: "Missing key query parameter" }));
1416
+ return;
1417
+ }
1418
+ kv.get(key).then((value) => {
1419
+ if (value === null) {
1420
+ res.writeHead(404, { "Content-Type": "application/json" });
1421
+ res.end("null");
1422
+ } else {
1423
+ res.writeHead(200, { "Content-Type": "application/json" });
1424
+ res.end(JSON.stringify(value));
1425
+ }
1426
+ }).catch(() => {
1427
+ res.writeHead(500, { "Content-Type": "application/json" });
1428
+ res.end(JSON.stringify({ error: "KV error" }));
1429
+ });
1430
+ }
1431
+ /**
1432
+ * Create an HTTP + WebSocket server for an agent.
1433
+ *
1434
+ * @internal Used by aai-cli dev server.
1435
+ */
1436
+ function createServer(options) {
1437
+ const { runtime, clientHtml, clientDir, logger = consoleLogger, kv } = options;
1438
+ const name = options.name ?? "agent";
1439
+ if (clientHtml && clientDir) throw new Error("clientHtml and clientDir are mutually exclusive");
1440
+ const escapedName = escapeHtml(name);
1441
+ const defaultHtml = clientHtml ?? `<!DOCTYPE html><html><body><h1>${escapedName}</h1><p>Agent server running.</p></body></html>`;
1442
+ const httpServer = http.createServer((req, res) => {
1443
+ const url = req.url?.split("?")[0] ?? "/";
1444
+ const method = req.method ?? "GET";
1445
+ res.setHeader("Content-Security-Policy", AGENT_CSP);
1446
+ res.setHeader("X-Content-Type-Options", "nosniff");
1447
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
1448
+ if (method === "GET" && url === "/health") {
1449
+ res.writeHead(200, { "Content-Type": "application/json" });
1450
+ res.end(JSON.stringify({
1451
+ status: "ok",
1452
+ name
1453
+ }));
1454
+ return;
1455
+ }
1456
+ if (kv && method === "GET" && url === "/kv") {
1457
+ handleKvGet(kv, req, res);
1458
+ return;
1459
+ }
1460
+ handleRequest(req, res, url, method);
1461
+ });
1462
+ async function handleRequest(req, res, url, method) {
1463
+ if (clientDir && await serveStatic(clientDir, req, res)) return;
1464
+ if (method === "GET" && url === "/") {
1465
+ res.writeHead(200, { "Content-Type": "text/html" });
1466
+ res.end(defaultHtml);
1467
+ return;
1468
+ }
1469
+ logger.error(`${method} ${url} 404`);
1470
+ res.writeHead(404, { "Content-Type": "application/json" });
1471
+ res.end(JSON.stringify({ error: "Not found" }));
1472
+ }
1473
+ const wss = new WebSocketServer({
1474
+ noServer: true,
1475
+ maxPayload: MAX_WS_PAYLOAD_BYTES
1476
+ });
1477
+ httpServer.on("upgrade", (req, socket, head) => {
1478
+ const url = req.url?.split("?")[0] ?? "";
1479
+ if (!url.startsWith("/websocket")) return;
1480
+ wss.handleUpgrade(req, socket, head, (ws) => {
1481
+ const startOpts = parseWsUpgradeParams(req.url ?? "");
1482
+ logger.info(`WS upgrade ${url}${startOpts.skipGreeting ? " (resume)" : ""}`);
1483
+ runtime.startSession(ws, startOpts);
1484
+ });
1485
+ });
1486
+ let listenPort;
1487
+ return {
1488
+ get port() {
1489
+ return listenPort;
1490
+ },
1491
+ async listen(port = 3e3) {
1492
+ await new Promise((resolve, reject) => {
1493
+ httpServer.on("error", reject);
1494
+ httpServer.listen(port, () => {
1495
+ const addr = httpServer.address();
1496
+ listenPort = typeof addr === "object" && addr ? addr.port : port;
1497
+ resolve();
1498
+ });
1499
+ });
1500
+ },
1501
+ async close() {
1502
+ try {
1503
+ await runtime.shutdown();
1504
+ } finally {
1505
+ try {
1506
+ wss.close();
1507
+ } finally {
1508
+ if (listenPort !== void 0) await new Promise((resolve, reject) => {
1509
+ httpServer.close((err) => err ? reject(err) : resolve());
1510
+ });
1511
+ listenPort = void 0;
1512
+ }
1513
+ }
1514
+ }
1515
+ };
1516
+ }
1517
+ //#endregion
1518
+ export { DEFAULT_S2S_CONFIG, _internals, buildCtx, connectS2s, consoleLogger, createRuntime, createS2sSession, createServer, createUnstorageKv, defaultCreateS2sWebSocket, executeInIsolate, executeToolCall, jsonLogger, resolveAllBuiltins, wireSessionSocket };