@alexkroman1/aai 0.12.2 → 1.0.2

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 +174 -0
  3. package/dist/constants-VTFoymJ-.js +47 -0
  4. package/dist/host/_run-code.d.ts +4 -2
  5. package/dist/host/_runtime-conformance.d.ts +4 -5
  6. package/dist/host/builtin-tools.d.ts +11 -7
  7. package/dist/host/runtime-barrel.d.ts +15 -0
  8. package/dist/{direct-executor-ZUU0Ke4j.js → host/runtime-barrel.js} +463 -345
  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 +21 -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 +6 -10
  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 +3 -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-CVJSQJiA.js → system-prompt-nik_iavo.js} +11 -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 +28 -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 -128
  113. package/dist/host/index.d.ts +0 -18
  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-Bb2B5Uob.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-CVJSQJiA.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";
7
+ import vm from "node:vm";
6
8
  import pTimeout from "p-timeout";
7
9
  import { createStorage, prefixStorage } from "unstorage";
8
- import vm from "node:vm";
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.
@@ -29,6 +87,7 @@ const runCodeParams = z.object({ code: z.string().describe("JavaScript code to e
29
87
  */
30
88
  function createRunCode() {
31
89
  return {
90
+ guidance: "You MUST use the run_code tool for ANY question involving math, counting, calculations, data processing, or code. NEVER do mental math or recite code verbally. run_code executes JavaScript (not Python). Always write JavaScript.",
32
91
  description: "Execute JavaScript code in a sandbox and return the output. Use this for calculations, data transformations, string manipulation, or any task that benefits from running code. Output is captured from console.log(). No network or filesystem access.",
33
92
  parameters: runCodeParams,
34
93
  async execute(args) {
@@ -47,34 +106,67 @@ function createRunCode() {
47
106
  async function executeInIsolate(code) {
48
107
  const output = [];
49
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
+ };
50
135
  const context = vm.createContext({
51
- console: {
52
- log: capture,
53
- info: capture,
54
- warn: capture,
55
- error: capture,
56
- debug: capture
57
- },
58
- setTimeout,
59
- clearTimeout,
60
- setInterval,
61
- clearInterval,
62
- URL,
63
- URLSearchParams,
64
- TextEncoder,
65
- TextDecoder,
66
- atob,
67
- btoa,
68
- structuredClone,
69
- queueMicrotask
70
- });
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
+ } });
71
158
  try {
72
159
  const wrapped = `(async () => {\n${code}\n})()`;
73
- const promise = new vm.Script(wrapped, { filename: "run_code.js" }).runInContext(context, { timeout: RUN_CODE_TIMEOUT_MS });
74
- 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 });
75
161
  return output.join("\n").trim() || "Code ran successfully (no output)";
76
162
  } catch (err) {
77
163
  return { error: errorMessage(err) };
164
+ } finally {
165
+ for (const id of activeTimers) {
166
+ clearTimeout(id);
167
+ clearInterval(id);
168
+ }
169
+ activeTimers.clear();
78
170
  }
79
171
  }
80
172
  //#endregion
@@ -87,10 +179,7 @@ async function executeInIsolate(code) {
87
179
  * Network requests go through the host's fetch proxy (with SSRF protection).
88
180
  */
89
181
  const fetchSignal = () => AbortSignal.timeout(FETCH_TIMEOUT_MS);
90
- /** Strip HTML tags and decode common entities. */
91
- function htmlToText(html) {
92
- 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();
93
- }
182
+ const htmlToText = (html) => convert(html, { wordwrap: false });
94
183
  const webSearchParams = z.object({
95
184
  query: z.string().describe("The search query"),
96
185
  max_results: z.number().describe("Maximum number of results to return (default 5)").optional()
@@ -103,6 +192,7 @@ const BraveSearchResponseSchema = z.object({ web: z.object({ results: z.array(z.
103
192
  })) }).optional() });
104
193
  function createWebSearch(fetchFn = globalThis.fetch) {
105
194
  return {
195
+ guidance: "Use web_search for factual questions, current events, or anything you are unsure about. Search first rather than guessing.",
106
196
  description: "Search the web for current information, facts, news, or answers to questions. Returns a list of results with title, URL, and description. Use this when the user asks about something you don't know, need up-to-date information, or want to verify facts.",
107
197
  parameters: webSearchParams,
108
198
  async execute(args, ctx) {
@@ -132,6 +222,7 @@ function createWebSearch(fetchFn = globalThis.fetch) {
132
222
  const visitWebpageParams = z.object({ url: z.string().describe("The full URL to fetch (e.g., 'https://example.com/page')") });
133
223
  function createVisitWebpage(fetchFn = globalThis.fetch) {
134
224
  return {
225
+ guidance: "Use visit_webpage to read the full content of a URL when search snippets are not detailed enough.",
135
226
  description: "Fetch a webpage and return its content as clean text. Use this to read the full content of a URL found via web_search, or any link the user shares. Good for reading articles, documentation, blog posts, or product pages.",
136
227
  parameters: visitWebpageParams,
137
228
  async execute(args, _ctx) {
@@ -187,6 +278,7 @@ function sanitizeHeaders(raw) {
187
278
  }
188
279
  function createFetchJson(fetchFn = globalThis.fetch) {
189
280
  return {
281
+ guidance: "Use fetch_json to call REST APIs and retrieve structured JSON data.",
190
282
  description: "Call a REST API endpoint via HTTP GET and return the JSON response. Use this to fetch structured data from APIs — for example, weather data, stock prices, exchange rates, or any public JSON API. Supports custom headers for authenticated APIs.",
191
283
  parameters: fetchJsonParams,
192
284
  async execute(args, _ctx) {
@@ -222,36 +314,46 @@ function resolveBuiltin(name, opts) {
222
314
  }
223
315
  }
224
316
  /**
225
- * Create built-in tool definitions for the given tool names.
226
- * 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`.
227
319
  */
228
- function getBuiltinToolDefs(names, opts) {
320
+ function resolveAllBuiltins(names, opts) {
229
321
  const defs = {};
230
- for (const name of names) for (const [k, v] of resolveBuiltin(name, opts)) defs[k] = v;
231
- return defs;
232
- }
233
- /** Returns JSON tool schemas for the specified builtin tools. */
234
- function getBuiltinToolSchemas(names) {
235
- return names.flatMap((name) => resolveBuiltin(name).map(([toolName, def]) => ({
236
- name: toolName,
237
- description: def.description,
238
- parameters: z.toJSONSchema(def.parameters ?? EMPTY_PARAMS)
239
- })));
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
+ };
240
339
  }
241
340
  //#endregion
242
- //#region host/runtime.ts
341
+ //#region host/runtime-config.ts
243
342
  /**
244
343
  * Runtime dependencies injected into the session pipeline.
245
344
  *
246
345
  * Defines the {@link Logger} interface, a default {@link consoleLogger},
247
346
  * and the {@link S2SConfig} for Speech-to-Speech endpoint configuration.
248
347
  */
348
+ function consoleLog(fn) {
349
+ return (msg, ctx) => ctx ? fn(msg, ctx) : fn(msg);
350
+ }
249
351
  /** Default console-backed logger. */
250
352
  const consoleLogger = {
251
- info: (msg, ctx) => ctx ? console.log(msg, ctx) : console.log(msg),
252
- warn: (msg, ctx) => ctx ? console.warn(msg, ctx) : console.warn(msg),
253
- error: (msg, ctx) => ctx ? console.error(msg, ctx) : console.error(msg),
254
- 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)
255
357
  };
256
358
  /**
257
359
  * Structured JSON logger for production diagnostics. Each log entry is a
@@ -277,7 +379,7 @@ const jsonLogger = {
277
379
  };
278
380
  /** Default S2S endpoint configuration. */
279
381
  const DEFAULT_S2S_CONFIG = {
280
- wssUrl: "wss://speech-to-speech.us.assemblyai.com/v1/realtime",
382
+ wssUrl: "wss://agents.assemblyai.com/v1/voice",
281
383
  inputSampleRate: DEFAULT_STT_SAMPLE_RATE,
282
384
  outputSampleRate: DEFAULT_TTS_SAMPLE_RATE
283
385
  };
@@ -285,7 +387,6 @@ const DEFAULT_S2S_CONFIG = {
285
387
  //#region host/s2s.ts
286
388
  const uint8ToBase64 = (bytes) => Buffer.from(bytes).toString("base64");
287
389
  const base64ToUint8 = (base64) => new Uint8Array(Buffer.from(base64, "base64"));
288
- const WS_OPEN = 1;
289
390
  const defaultCreateS2sWebSocket = (url, opts) => new WsWebSocket(url, { headers: opts.headers });
290
391
  const S2sMessageSchema = z.discriminatedUnion("type", [
291
392
  z.object({
@@ -295,10 +396,6 @@ const S2sMessageSchema = z.discriminatedUnion("type", [
295
396
  z.object({ type: z.literal("session.updated") }).passthrough(),
296
397
  z.object({ type: z.literal("input.speech.started") }),
297
398
  z.object({ type: z.literal("input.speech.stopped") }),
298
- z.object({
299
- type: z.literal("transcript.user.delta"),
300
- text: z.string()
301
- }),
302
399
  z.object({
303
400
  type: z.literal("transcript.user"),
304
401
  item_id: z.string(),
@@ -308,10 +405,6 @@ const S2sMessageSchema = z.discriminatedUnion("type", [
308
405
  type: z.literal("reply.started"),
309
406
  reply_id: z.string()
310
407
  }),
311
- z.object({
312
- type: z.literal("transcript.agent.delta"),
313
- delta: z.string()
314
- }).passthrough(),
315
408
  z.object({
316
409
  type: z.literal("transcript.agent"),
317
410
  text: z.string(),
@@ -319,8 +412,6 @@ const S2sMessageSchema = z.discriminatedUnion("type", [
319
412
  item_id: z.string().optional().default(""),
320
413
  interrupted: z.boolean().optional().default(false)
321
414
  }),
322
- z.object({ type: z.literal("reply.content_part.started") }).passthrough(),
323
- z.object({ type: z.literal("reply.content_part.done") }).passthrough(),
324
415
  z.object({
325
416
  type: z.literal("tool.call"),
326
417
  call_id: z.string(),
@@ -350,66 +441,48 @@ function dispatchS2sMessage(emitter, msg) {
350
441
  case "session.ready":
351
442
  emitter.emit("ready", { sessionId: msg.session_id });
352
443
  break;
353
- case "session.updated":
354
- emitter.emit("sessionUpdated", msg);
355
- break;
444
+ case "session.updated": break;
356
445
  case "input.speech.started":
357
- emitter.emit("speechStarted");
446
+ emitter.emit("event", { type: "speech_started" });
358
447
  break;
359
448
  case "input.speech.stopped":
360
- emitter.emit("speechStopped");
361
- break;
362
- case "transcript.user.delta":
363
- emitter.emit("userTranscriptDelta", { text: msg.text });
449
+ emitter.emit("event", { type: "speech_stopped" });
364
450
  break;
365
451
  case "transcript.user":
366
- emitter.emit("userTranscript", {
367
- itemId: msg.item_id,
452
+ emitter.emit("event", {
453
+ type: "user_transcript",
368
454
  text: msg.text
369
455
  });
370
456
  break;
371
457
  case "reply.started":
372
458
  emitter.emit("replyStarted", { replyId: msg.reply_id });
373
459
  break;
374
- case "transcript.agent.delta":
375
- emitter.emit("agentTranscriptDelta", { text: msg.delta });
376
- break;
377
460
  case "transcript.agent":
378
- emitter.emit("agentTranscript", {
461
+ emitter.emit("event", {
462
+ type: "agent_transcript",
379
463
  text: msg.text,
380
- replyId: msg.reply_id,
381
- itemId: msg.item_id,
382
- interrupted: msg.interrupted
464
+ _interrupted: msg.interrupted
383
465
  });
384
466
  break;
385
467
  case "tool.call":
386
- emitter.emit("toolCall", {
387
- callId: msg.call_id,
388
- name: msg.name,
468
+ emitter.emit("event", {
469
+ type: "tool_call",
470
+ toolCallId: msg.call_id,
471
+ toolName: msg.name,
389
472
  args: msg.args
390
473
  });
391
474
  break;
392
475
  case "reply.done":
393
- 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" });
394
478
  break;
395
479
  case "session.error":
396
- if (msg.code === "session_not_found" || msg.code === "session_forbidden") emitter.emit("sessionExpired", {
397
- code: msg.code,
398
- message: msg.message
399
- });
400
- else emitter.emit("error", {
401
- code: msg.code,
402
- message: msg.message
403
- });
480
+ if (msg.code === "session_not_found" || msg.code === "session_forbidden") emitter.emit("sessionExpired");
481
+ else emitter.emit("error", new Error(msg.message));
404
482
  break;
405
483
  case "error":
406
- emitter.emit("error", {
407
- code: "connection",
408
- message: msg.message
409
- });
484
+ emitter.emit("error", new Error(msg.message));
410
485
  break;
411
- case "reply.content_part.started":
412
- case "reply.content_part.done": break;
413
486
  default: break;
414
487
  }
415
488
  }
@@ -421,18 +494,19 @@ function connectS2s(opts) {
421
494
  const emitter = createNanoEvents();
422
495
  let opened = false;
423
496
  function send(msg) {
424
- if (ws.readyState !== WS_OPEN) {
497
+ if (ws.readyState !== 1) {
425
498
  log.debug("S2S send dropped: socket not open", { type: msg.type });
426
499
  return;
427
500
  }
428
501
  const json = JSON.stringify(msg);
429
- 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}`);
430
504
  ws.send(json);
431
505
  }
432
506
  const handle = {
433
507
  on: emitter.on.bind(emitter),
434
508
  sendAudio(audio) {
435
- if (ws.readyState !== WS_OPEN) {
509
+ if (ws.readyState !== 1) {
436
510
  log.debug("S2S sendAudio dropped: socket not open");
437
511
  return;
438
512
  }
@@ -493,7 +567,7 @@ function connectS2s(opts) {
493
567
  }
494
568
  function logIncoming(obj) {
495
569
  if (obj.type === "reply.audio" || obj.type === "input.audio") return;
496
- log.info(`S2S << ${obj.type}`, obj.type === "transcript.agent.delta" ? { delta: obj.delta } : void 0);
570
+ log.info(`S2S << ${obj.type}`);
497
571
  }
498
572
  function handleS2sMessage(ev) {
499
573
  const raw = tryParseJson(ev.data);
@@ -514,32 +588,29 @@ function connectS2s(opts) {
514
588
  }
515
589
  ws.addEventListener("message", handleS2sMessage);
516
590
  ws.addEventListener("close", (ev) => {
591
+ const code = ev.code ?? 0;
592
+ const reason = ev.reason ?? "";
517
593
  log.info("S2S WebSocket closed", {
518
- code: ev.code ?? 0,
519
- reason: ev.reason ?? ""
594
+ code,
595
+ reason
520
596
  });
521
- if (!opened) reject(/* @__PURE__ */ new Error(`WebSocket closed before open (code: ${ev.code ?? 0})`));
522
- emitter.emit("close");
597
+ if (!opened) reject(/* @__PURE__ */ new Error(`WebSocket closed before open (code: ${code})`));
598
+ emitter.emit("close", code, reason);
523
599
  });
524
600
  ws.addEventListener("error", (ev) => {
525
601
  const message = typeof ev.message === "string" ? ev.message : "WebSocket error";
526
602
  const errObj = new Error(message);
527
603
  log.error("S2S WebSocket error", { error: errObj.message });
528
604
  if (!opened) reject(errObj);
529
- else emitter.emit("error", {
530
- code: "ws_error",
531
- message: errObj.message
532
- });
605
+ else emitter.emit("error", errObj);
533
606
  });
534
607
  });
535
608
  }
536
609
  //#endregion
537
- //#region host/session.ts
610
+ //#region host/session-ctx.ts
538
611
  function buildCtx(opts) {
539
- const { id, agentConfig, hooks, log } = opts;
612
+ const { agentConfig, log } = opts;
540
613
  const maxHistory = opts.maxHistory ?? 200;
541
- /** Track in-flight hook promises so they can be awaited during shutdown. */
542
- const pendingHooks = /* @__PURE__ */ new Set();
543
614
  const ctx = {
544
615
  ...opts,
545
616
  s2s: null,
@@ -551,12 +622,9 @@ function buildCtx(opts) {
551
622
  turnPromise: null,
552
623
  conversationMessages: [],
553
624
  maxHistory,
554
- resolveTurnConfig() {
555
- return callResolveTurnConfig(hooks, id, HOOK_TIMEOUT_MS);
556
- },
557
- consumeToolCallStep(turnConfig, _name, replyId) {
625
+ consumeToolCallStep(_name, replyId) {
558
626
  if (replyId === null || replyId !== ctx.reply.currentReplyId) return toolError("Reply was interrupted. Discarding stale tool call.");
559
- const maxSteps = turnConfig?.maxSteps ?? agentConfig.maxSteps;
627
+ const maxSteps = agentConfig.maxSteps;
560
628
  ctx.reply.toolCallCount++;
561
629
  if (maxSteps !== void 0 && ctx.reply.toolCallCount > maxSteps) {
562
630
  log.info("maxSteps exceeded, refusing tool call", {
@@ -567,32 +635,9 @@ function buildCtx(opts) {
567
635
  }
568
636
  return null;
569
637
  },
570
- fireHook(name, ...args) {
571
- if (!hooks) return;
572
- const notifyOnError = (err) => {
573
- log.warn(`${name} hook failed`, { err: errorMessage(err) });
574
- if (name !== "error") {
575
- const ep = hooks.callHook("error", id, { message: errorMessage(err) });
576
- if (ep && typeof ep.catch === "function") ep.catch((e) => {
577
- log.warn("error hook failed", { err: errorMessage(e) });
578
- });
579
- }
580
- };
581
- try {
582
- const result = hooks.callHook(name, ...args);
583
- if (result == null) return;
584
- const p = result.catch(notifyOnError).finally(() => pendingHooks.delete(p));
585
- pendingHooks.add(p);
586
- } catch (err) {
587
- notifyOnError(err);
588
- }
589
- },
590
- async drainHooks() {
591
- if (pendingHooks.size > 0) await Promise.all([...pendingHooks]);
592
- },
593
638
  pushMessages(...msgs) {
594
639
  ctx.conversationMessages.push(...msgs);
595
- 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);
596
641
  },
597
642
  beginReply(replyId) {
598
643
  ctx.reply = {
@@ -615,8 +660,15 @@ function buildCtx(opts) {
615
660
  };
616
661
  return ctx;
617
662
  }
663
+ //#endregion
664
+ //#region host/session.ts
618
665
  /** @internal Not part of the public API. Exposed for testing only. */
619
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
+ */
620
672
  function createIdleTimer(opts) {
621
673
  if (opts.timeoutMs <= 0) return {
622
674
  reset() {},
@@ -663,25 +715,11 @@ function finishToolCall(ctx, callId, result, replyId) {
663
715
  if (ctx.maxHistory > 0 && ctx.reply.pendingTools.length > ctx.maxHistory) ctx.reply.pendingTools.shift();
664
716
  }
665
717
  }
666
- async function handleToolCall(ctx, detail) {
667
- const { callId, name, args: parsedArgs } = detail;
718
+ async function handleToolCall(ctx, event) {
719
+ const { toolCallId: callId, toolName: name, args: parsedArgs } = event;
668
720
  const replyId = ctx.reply.currentReplyId;
669
- ctx.client.event({
670
- type: "tool_call_start",
671
- toolCallId: callId,
672
- toolName: name,
673
- args: parsedArgs
674
- });
675
- let turnConfig;
676
- try {
677
- turnConfig = await ctx.resolveTurnConfig();
678
- } catch (err) {
679
- const msg = `resolveTurnConfig hook error: ${errorMessage(err)}`;
680
- ctx.log.error(msg);
681
- finishToolCall(ctx, callId, toolError(msg), replyId);
682
- return;
683
- }
684
- const refused = ctx.consumeToolCallStep(turnConfig, name, replyId);
721
+ ctx.client.event(event);
722
+ const refused = ctx.consumeToolCallStep(name, replyId);
685
723
  if (refused !== null) {
686
724
  finishToolCall(ctx, callId, refused, replyId);
687
725
  return;
@@ -713,23 +751,17 @@ async function handleToolCall(ctx, detail) {
713
751
  function handleUserTranscript(ctx, text) {
714
752
  ctx.log.info("S2S user transcript", { text });
715
753
  ctx.client.event({
716
- type: "transcript",
717
- text,
718
- isFinal: true
719
- });
720
- ctx.client.event({
721
- type: "turn",
754
+ type: "user_transcript",
722
755
  text
723
756
  });
724
757
  ctx.pushMessages({
725
758
  role: "user",
726
759
  content: text
727
760
  });
728
- ctx.fireHook("turn", ctx.id, text, HOOK_TIMEOUT_MS);
729
761
  }
730
762
  function handleAgentTranscript(ctx, text, interrupted) {
731
763
  ctx.client.event({
732
- type: "chat",
764
+ type: "agent_transcript",
733
765
  text
734
766
  });
735
767
  if (!interrupted) ctx.pushMessages({
@@ -737,13 +769,12 @@ function handleAgentTranscript(ctx, text, interrupted) {
737
769
  content: text
738
770
  });
739
771
  }
740
- function handleReplyDone(ctx, status) {
741
- if (status === "interrupted") {
742
- ctx.log.info("S2S reply interrupted (barge-in)");
743
- ctx.cancelReply();
744
- ctx.client.event({ type: "cancelled" });
745
- return;
746
- }
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) {
747
778
  const doneReplyId = ctx.reply.currentReplyId;
748
779
  const sendPending = () => {
749
780
  if (ctx.reply.currentReplyId !== doneReplyId) {
@@ -760,7 +791,7 @@ function handleReplyDone(ctx, status) {
760
791
  agent: ctx.agent
761
792
  });
762
793
  ctx.client.playAudioDone();
763
- ctx.client.event({ type: "tts_done" });
794
+ ctx.client.event({ type: "reply_done" });
764
795
  }
765
796
  };
766
797
  if (ctx.turnPromise !== null) ctx.turnPromise.then(sendPending);
@@ -768,61 +799,66 @@ function handleReplyDone(ctx, status) {
768
799
  }
769
800
  function setupListeners(ctx, handle) {
770
801
  handle.on("ready", ({ sessionId }) => ctx.log.info("S2S session ready", { sessionId }));
802
+ handle.on("replyStarted", ({ replyId }) => {
803
+ ctx.beginReply(replyId);
804
+ });
771
805
  handle.on("sessionExpired", () => {
772
806
  ctx.log.info("S2S session expired");
773
807
  handle.close();
774
808
  });
775
- handle.on("speechStarted", () => ctx.client.event({ type: "speech_started" }));
776
- handle.on("speechStopped", () => ctx.client.event({ type: "speech_stopped" }));
777
- handle.on("userTranscriptDelta", ({ text }) => ctx.client.event({
778
- type: "transcript",
779
- text,
780
- isFinal: false
781
- }));
782
- handle.on("userTranscript", ({ text }) => handleUserTranscript(ctx, text));
783
- handle.on("replyStarted", ({ replyId }) => {
784
- ctx.beginReply(replyId);
785
- });
786
809
  handle.on("audio", ({ audio }) => ctx.client.playAudioChunk(audio));
787
- handle.on("agentTranscriptDelta", ({ text }) => ctx.client.event({
788
- type: "chat_delta",
789
- text
790
- }));
791
- handle.on("agentTranscript", ({ text, interrupted }) => handleAgentTranscript(ctx, text, interrupted));
792
- handle.on("toolCall", (detail) => {
793
- const p = handleToolCall(ctx, detail).catch((err) => {
794
- ctx.log.error("Tool call handler failed", { err: errorMessage(err) });
795
- });
796
- ctx.chainTurn(p);
797
- });
798
- handle.on("replyDone", ({ status }) => handleReplyDone(ctx, status));
799
- handle.on("error", ({ code, message }) => {
800
- ctx.log.error("S2S error", {
801
- code,
802
- message
803
- });
810
+ handle.on("error", (err) => {
811
+ ctx.log.error("S2S error", { message: err.message });
804
812
  ctx.client.event({
805
813
  type: "error",
806
814
  code: "internal",
807
- message
815
+ message: err.message
808
816
  });
809
817
  handle.close();
810
818
  });
811
- handle.on("close", () => {
812
- ctx.log.info("S2S closed");
819
+ handle.on("close", (code, reason) => {
820
+ ctx.log.info("S2S closed", {
821
+ code,
822
+ reason
823
+ });
813
824
  ctx.s2s = null;
814
825
  ctx.cancelReply();
815
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
+ });
816
851
  }
817
852
  function createS2sSession(opts) {
818
- 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;
819
854
  const agentConfig = opts.skipGreeting ? {
820
855
  ...opts.agentConfig,
821
856
  greeting: ""
822
857
  } : opts.agentConfig;
823
858
  const systemPrompt = buildSystemPrompt(agentConfig, {
824
859
  hasTools: toolSchemas.length > 0 || (agentConfig.builtinTools?.length ?? 0) > 0,
825
- voice: true
860
+ voice: true,
861
+ toolGuidance: opts.toolGuidance
826
862
  });
827
863
  const s2sTools = toolSchemas.map((ts) => ({
828
864
  type: "function",
@@ -837,7 +873,6 @@ function createS2sSession(opts) {
837
873
  client,
838
874
  agentConfig,
839
875
  executeTool,
840
- hooks,
841
876
  log,
842
877
  maxHistory: opts.maxHistory
843
878
  });
@@ -884,7 +919,6 @@ function createS2sSession(opts) {
884
919
  }
885
920
  return {
886
921
  async start() {
887
- ctx.fireHook("connect", id, HOOK_TIMEOUT_MS);
888
922
  await connectAndSetup();
889
923
  },
890
924
  async stop() {
@@ -892,10 +926,7 @@ function createS2sSession(opts) {
892
926
  sessionAbort.abort();
893
927
  idle.clear();
894
928
  if (ctx.turnPromise !== null) await ctx.turnPromise;
895
- await ctx.drainHooks();
896
929
  ctx.s2s?.close();
897
- ctx.fireHook("disconnect", id, HOOK_TIMEOUT_MS);
898
- await ctx.drainHooks();
899
930
  },
900
931
  onAudio(data) {
901
932
  idle.reset();
@@ -927,33 +958,50 @@ function createS2sSession(opts) {
927
958
  };
928
959
  }
929
960
  //#endregion
930
- //#region isolate/_kv-utils.ts
931
- /** Internal KV helpers shared by kv.ts and unstorage-kv.ts. */
932
- /** Sort entries by key and apply reverse/limit options. Mutates the array. */
933
- function sortAndPaginate(entries, options) {
934
- entries.sort((a, b) => a.key.localeCompare(b.key));
935
- if (options?.reverse) entries.reverse();
936
- if (options?.limit && options.limit > 0) entries.length = Math.min(entries.length, options.limit);
937
- 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
+ };
938
981
  }
939
- /** Simple glob matcher supports `*` as a wildcard for any characters. */
940
- function matchGlob(key, pattern) {
941
- if (pattern.length > 1024) throw new Error(`Glob pattern exceeds maximum length of ${MAX_GLOB_PATTERN_LENGTH}`);
942
- const parts = pattern.split("*");
943
- if (parts.length === 1) return key === pattern;
944
- const first = parts[0];
945
- if (!key.startsWith(first)) return false;
946
- const last = parts.at(-1);
947
- if (key.length < first.length + last.length) return false;
948
- if (!key.endsWith(last)) return false;
949
- let pos = first.length;
950
- const end = key.length - last.length;
951
- for (const part of parts.slice(1, -1)) {
952
- const idx = key.indexOf(part, pos);
953
- if (idx === -1 || idx > end) return false;
954
- 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));
955
1004
  }
956
- return pos <= end;
957
1005
  }
958
1006
  //#endregion
959
1007
  //#region host/unstorage-kv.ts
@@ -994,23 +1042,6 @@ function createUnstorageKv(options) {
994
1042
  const keyArray = Array.isArray(keys) ? keys : [keys];
995
1043
  await Promise.all(keyArray.map((k) => store.removeItem(k)));
996
1044
  },
997
- async list(listPrefix, listOptions) {
998
- const allKeys = await store.getKeys(listPrefix);
999
- const entries = [];
1000
- for (const key of allKeys) {
1001
- const value = await store.getItem(key);
1002
- if (value != null) entries.push({
1003
- key,
1004
- value
1005
- });
1006
- }
1007
- return sortAndPaginate(entries, listOptions);
1008
- },
1009
- async keys(pattern) {
1010
- const allKeys = await store.getKeys();
1011
- if (!pattern) return allKeys.sort((a, b) => a.localeCompare(b));
1012
- return allKeys.filter((key) => matchGlob(key, pattern)).sort((a, b) => a.localeCompare(b));
1013
- },
1014
1045
  close() {
1015
1046
  store.dispose();
1016
1047
  }
@@ -1077,12 +1108,12 @@ function handleTextMessage(data, session, log, ctx, sid) {
1077
1108
  });
1078
1109
  return;
1079
1110
  }
1080
- const parsed = ClientMessageSchema.safeParse(json);
1081
- if (!parsed.success) {
1082
- log.warn("Invalid client message", {
1111
+ const parsed = lenientParse(ClientMessageSchema, json);
1112
+ if (!parsed.ok) {
1113
+ if (parsed.malformed) log.warn("Invalid client message", {
1083
1114
  ...ctx,
1084
1115
  sid,
1085
- error: parsed.error.message
1116
+ error: parsed.error
1086
1117
  });
1087
1118
  return;
1088
1119
  }
@@ -1174,7 +1205,7 @@ function wireSessionSocket(ws, opts) {
1174
1205
  ws.addEventListener("message", (event) => {
1175
1206
  if (!session) return;
1176
1207
  if (!sessionReady) {
1177
- messageBuffer?.push(event);
1208
+ if (messageBuffer && messageBuffer.length < 100) messageBuffer.push(event);
1178
1209
  return;
1179
1210
  }
1180
1211
  const { data } = event;
@@ -1194,6 +1225,7 @@ function wireSessionSocket(ws, opts) {
1194
1225
  });
1195
1226
  }).finally(() => {
1196
1227
  sessions.delete(sessionId);
1228
+ opts.onSessionEnd?.(sessionId);
1197
1229
  });
1198
1230
  opts.onClose?.();
1199
1231
  });
@@ -1207,7 +1239,7 @@ function wireSessionSocket(ws, opts) {
1207
1239
  });
1208
1240
  }
1209
1241
  //#endregion
1210
- //#region host/direct-executor.ts
1242
+ //#region host/runtime.ts
1211
1243
  /**
1212
1244
  * Agent runtime — the execution engine for voice agents.
1213
1245
  *
@@ -1215,45 +1247,6 @@ function wireSessionSocket(ws, opts) {
1215
1247
  * self-hosted servers and the platform sandbox. It wires up tool execution,
1216
1248
  * lifecycle hooks, and session management.
1217
1249
  */
1218
- const yieldTick = () => new Promise((r) => setTimeout(r, 0));
1219
- function buildToolContext(opts) {
1220
- const { env, state, kv, messages, fetch: fetchFn, sessionId } = opts;
1221
- return {
1222
- env: { ...env },
1223
- state: state ?? {},
1224
- get kv() {
1225
- if (!kv) throw new Error("KV not available");
1226
- return kv;
1227
- },
1228
- messages: messages ?? [],
1229
- fetch: fetchFn ?? globalThis.fetch,
1230
- sessionId: sessionId ?? ""
1231
- };
1232
- }
1233
- async function executeToolCall(name, args, options) {
1234
- const { tool } = options;
1235
- const parsed = (tool.parameters ?? EMPTY_PARAMS).safeParse(args);
1236
- if (!parsed.success) return toolError(`Invalid arguments for tool "${name}": ${(parsed.error?.issues ?? []).map((i) => `${i.path.map(String).join(".")}: ${i.message}`).join(", ")}`);
1237
- try {
1238
- const ctx = buildToolContext(options);
1239
- await yieldTick();
1240
- const result = await pTimeout(Promise.resolve(tool.execute(parsed.data, ctx)), {
1241
- milliseconds: TOOL_EXECUTION_TIMEOUT_MS,
1242
- message: `Tool "${name}" timed out after ${TOOL_EXECUTION_TIMEOUT_MS}ms`
1243
- });
1244
- await yieldTick();
1245
- if (result == null) return "null";
1246
- return typeof result === "string" ? result : JSON.stringify(result);
1247
- } catch (err) {
1248
- const log = options.logger;
1249
- if (log) log.warn("Tool execution failed", {
1250
- tool: name,
1251
- error: errorDetail(err)
1252
- });
1253
- else console.warn(`[tool-executor] Tool execution failed: ${name}`, err);
1254
- return toolError(errorMessage(err));
1255
- }
1256
- }
1257
1250
  /** Create an in-memory KV store (default for self-hosted). */
1258
1251
  function createLocalKv() {
1259
1252
  return createUnstorageKv({ storage: createStorage() });
@@ -1276,37 +1269,43 @@ function createRuntime(opts) {
1276
1269
  const sessions = /* @__PURE__ */ new Map();
1277
1270
  const readyConfig = buildReadyConfig(s2sConfig);
1278
1271
  let executeTool;
1279
- let hooks;
1280
1272
  let toolSchemas;
1281
- if (opts.executeTool && opts.hooks && opts.toolSchemas) {
1282
- executeTool = opts.executeTool;
1283
- hooks = opts.hooks;
1273
+ let toolGuidance = [];
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
+ };
1284
1293
  toolSchemas = opts.toolSchemas;
1294
+ toolGuidance = opts.toolGuidance ?? [];
1285
1295
  } else {
1296
+ const builtins = resolveAllBuiltins(agent.builtinTools ?? [], builtinFetchOpt);
1286
1297
  const allTools = {
1287
- ...getBuiltinToolDefs(agent.builtinTools ?? []),
1298
+ ...builtins.defs,
1288
1299
  ...agent.tools
1289
1300
  };
1290
- const customSchemas = agentToolsToSchemas(agent.tools ?? {});
1291
- const builtinSchemas = getBuiltinToolSchemas(agent.builtinTools ?? []);
1292
- toolSchemas = [...customSchemas, ...builtinSchemas];
1301
+ toolSchemas = [...agentToolsToSchemas(agent.tools ?? {}), ...builtins.schemas];
1302
+ toolGuidance = builtins.guidance;
1293
1303
  const stateMap = /* @__PURE__ */ new Map();
1294
1304
  const getState = (sid) => {
1295
1305
  if (!stateMap.has(sid) && agent.state) stateMap.set(sid, agent.state());
1296
1306
  return stateMap.get(sid) ?? {};
1297
1307
  };
1298
1308
  const frozenEnv = Object.freeze({ ...env });
1299
- function makeHookContext(sessionId) {
1300
- return {
1301
- env: frozenEnv,
1302
- state: getState(sessionId),
1303
- sessionId,
1304
- get kv() {
1305
- return kv;
1306
- },
1307
- fetch: globalThis.fetch
1308
- };
1309
- }
1310
1309
  executeTool = async (name, args, sessionId, messages) => {
1311
1310
  const tool = allTools[name];
1312
1311
  if (!tool) return toolError(`Unknown tool: ${name}`);
@@ -1317,17 +1316,9 @@ function createRuntime(opts) {
1317
1316
  sessionId: sessionId ?? "",
1318
1317
  kv,
1319
1318
  messages,
1320
- logger,
1321
- fetch: globalThis.fetch
1319
+ logger
1322
1320
  });
1323
1321
  };
1324
- hooks = createAgentHooks({
1325
- agent,
1326
- makeCtx: makeHookContext
1327
- });
1328
- hooks.hook("disconnect", async (sessionId) => {
1329
- stateMap.delete(sessionId);
1330
- });
1331
1322
  }
1332
1323
  function createSession(sessionOpts) {
1333
1324
  const apiKey = env.ASSEMBLYAI_API_KEY ?? "";
@@ -1337,11 +1328,11 @@ function createRuntime(opts) {
1337
1328
  client: sessionOpts.client,
1338
1329
  agentConfig,
1339
1330
  toolSchemas,
1331
+ toolGuidance,
1340
1332
  apiKey,
1341
1333
  s2sConfig,
1342
1334
  executeTool,
1343
1335
  ...createWebSocket ? { createWebSocket } : {},
1344
- hooks,
1345
1336
  skipGreeting: sessionOpts.skipGreeting ?? false,
1346
1337
  logger,
1347
1338
  ...sessionOpts.resumeFrom ? { resumeFrom: sessionOpts.resumeFrom } : {}
@@ -1363,32 +1354,23 @@ function createRuntime(opts) {
1363
1354
  ...startOpts?.logContext ? { logContext: startOpts.logContext } : {},
1364
1355
  ...startOpts?.onOpen ? { onOpen: startOpts.onOpen } : {},
1365
1356
  ...startOpts?.onClose ? { onClose: startOpts.onClose } : {},
1357
+ ...startOpts?.onSessionEnd ? { onSessionEnd: startOpts.onSessionEnd } : {},
1366
1358
  ...sessionStartTimeoutMs !== void 0 ? { sessionStartTimeoutMs } : {},
1367
1359
  ...resumeFrom ? { resumeFrom } : {}
1368
1360
  });
1369
1361
  }
1370
1362
  async function shutdown() {
1371
1363
  if (sessions.size === 0) return;
1372
- let timer;
1373
- const timeout = new Promise((resolve) => {
1374
- timer = setTimeout(resolve, shutdownTimeoutMs, "timeout");
1375
- });
1376
- const graceful = Promise.allSettled([...sessions.values()].map((s) => s.stop())).then((results) => {
1377
- for (const r of results) if (r.status === "rejected") logger.warn(`Session stop failed during shutdown: ${r.reason}`);
1378
- return "done";
1379
- });
1380
- let outcome;
1381
1364
  try {
1382
- outcome = await Promise.race([graceful, timeout]);
1383
- } finally {
1384
- 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)`);
1385
1369
  }
1386
- if (outcome === "timeout") logger.warn(`Shutdown timeout (${shutdownTimeoutMs}ms) exceeded — force-closing ${sessions.size} remaining session(s)`);
1387
1370
  sessions.clear();
1388
1371
  }
1389
1372
  return {
1390
1373
  executeTool,
1391
- hooks,
1392
1374
  toolSchemas,
1393
1375
  createSession,
1394
1376
  startSession,
@@ -1397,4 +1379,140 @@ function createRuntime(opts) {
1397
1379
  };
1398
1380
  }
1399
1381
  //#endregion
1400
- export { _internals as a, connectS2s as c, consoleLogger as d, jsonLogger as f, createUnstorageKv as i, defaultCreateS2sWebSocket as l, executeToolCall as n, buildCtx as o, 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 };