@alexkroman1/aai 0.10.3 → 0.10.4

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 (73) hide show
  1. package/dist/_internal-types.d.ts +8 -1
  2. package/dist/_runtime-conformance.d.ts +64 -0
  3. package/dist/_test-utils.d.ts +70 -0
  4. package/dist/_utils.d.ts +1 -8
  5. package/dist/_utils.js +49 -2
  6. package/dist/builtin-tools.d.ts +1 -5
  7. package/dist/constants-BbAOvKl_.js +47 -0
  8. package/dist/constants.d.ts +44 -0
  9. package/dist/direct-executor-BfHrDdPL.js +1589 -0
  10. package/dist/direct-executor.d.ts +90 -31
  11. package/dist/hooks.d.ts +44 -0
  12. package/dist/hooks.js +58 -0
  13. package/dist/index.d.ts +1 -2
  14. package/dist/index.js +2 -2
  15. package/dist/internal.d.ts +19 -0
  16. package/dist/internal.js +209 -0
  17. package/dist/kv.d.ts +1 -1
  18. package/dist/kv.js +32 -1
  19. package/dist/matchers.js +1 -1
  20. package/dist/protocol.d.ts +3 -29
  21. package/dist/protocol.js +140 -2
  22. package/dist/server.d.ts +27 -40
  23. package/dist/server.js +117 -145
  24. package/dist/session.d.ts +65 -44
  25. package/dist/{testing-BbitshLb.js → testing-BonJtfHJ.js} +25 -43
  26. package/dist/testing.d.ts +9 -14
  27. package/dist/testing.js +2 -2
  28. package/dist/types.d.ts +24 -226
  29. package/dist/types.js +176 -2
  30. package/dist/types.test-d.d.ts +7 -0
  31. package/dist/vite-plugin.d.ts +15 -0
  32. package/dist/vite-plugin.js +82 -0
  33. package/dist/ws-handler.d.ts +1 -2
  34. package/package.json +28 -88
  35. package/dist/_embeddings.d.ts +0 -31
  36. package/dist/_internal-types-IfPcaJd5.js +0 -61
  37. package/dist/_internal-types.js +0 -2
  38. package/dist/_session-ctx.d.ts +0 -73
  39. package/dist/_session-otel.d.ts +0 -43
  40. package/dist/_session-persist.d.ts +0 -30
  41. package/dist/_ssrf-DCp_27V4.js +0 -123
  42. package/dist/_ssrf.d.ts +0 -30
  43. package/dist/_ssrf.js +0 -2
  44. package/dist/_utils-DgzpOMSV.js +0 -61
  45. package/dist/direct-executor-B-5mq3cu.js +0 -570
  46. package/dist/kv-iXtikQmR.js +0 -32
  47. package/dist/middleware-core-BwyBIPed.js +0 -107
  48. package/dist/middleware-core.d.ts +0 -47
  49. package/dist/middleware-core.js +0 -2
  50. package/dist/middleware.d.ts +0 -37
  51. package/dist/protocol-B-H2Q4ox.js +0 -162
  52. package/dist/runtime-CxcwaK68.js +0 -58
  53. package/dist/runtime.js +0 -2
  54. package/dist/s2s-M7JqtgFw.js +0 -272
  55. package/dist/s2s.js +0 -2
  56. package/dist/session-BYlwcrya.js +0 -683
  57. package/dist/session.js +0 -2
  58. package/dist/telemetry-CJlaDFNc.js +0 -95
  59. package/dist/telemetry.d.ts +0 -49
  60. package/dist/telemetry.js +0 -2
  61. package/dist/types-D8ZBxTL_.js +0 -192
  62. package/dist/unstorage-kv-CDgP-frt.js +0 -64
  63. package/dist/unstorage-kv.js +0 -2
  64. package/dist/unstorage-vector-Cj5llNhg.js +0 -172
  65. package/dist/unstorage-vector.d.ts +0 -47
  66. package/dist/unstorage-vector.js +0 -2
  67. package/dist/vector.d.ts +0 -86
  68. package/dist/vector.js +0 -49
  69. package/dist/worker-entry-2jaiqIj0.js +0 -70
  70. package/dist/worker-entry.d.ts +0 -47
  71. package/dist/worker-entry.js +0 -2
  72. package/dist/ws-handler-C0Q6eSay.js +0 -207
  73. package/dist/ws-handler.js +0 -2
package/dist/protocol.js CHANGED
@@ -1,2 +1,140 @@
1
- import { a as DEFAULT_TTS_SAMPLE_RATE, c as MAX_TOOL_RESULT_CHARS, d as SessionErrorCodeSchema, f as TOOL_EXECUTION_TIMEOUT_MS, i as DEFAULT_STT_SAMPLE_RATE, l as ReadyConfigSchema, m as buildReadyConfig, n as ClientEventSchema, o as HOOK_TIMEOUT_MS, p as TurnConfigSchema, r as ClientMessageSchema, s as KvRequestSchema, t as AUDIO_FORMAT, u as ServerMessageSchema } from "./protocol-B-H2Q4ox.js";
2
- export { AUDIO_FORMAT, ClientEventSchema, ClientMessageSchema, DEFAULT_STT_SAMPLE_RATE, DEFAULT_TTS_SAMPLE_RATE, HOOK_TIMEOUT_MS, KvRequestSchema, MAX_TOOL_RESULT_CHARS, ReadyConfigSchema, ServerMessageSchema, SessionErrorCodeSchema, TOOL_EXECUTION_TIMEOUT_MS, TurnConfigSchema, buildReadyConfig };
1
+ import { p as MAX_TOOL_RESULT_CHARS } from "./constants-BbAOvKl_.js";
2
+ import { z } from "zod";
3
+ //#region protocol.ts
4
+ /**
5
+ * WebSocket wire-format types shared by server and client.
6
+ *
7
+ * Note: this module is for internal use only and should not be used directly.
8
+ */
9
+ /**
10
+ * Audio codec identifier used in the wire protocol.
11
+ *
12
+ * All audio frames are 16-bit signed PCM, little-endian, mono.
13
+ */
14
+ const AUDIO_FORMAT = "pcm16";
15
+ /** Zod schema for KV operation requests from the worker to the host. */
16
+ const KvRequestSchema = z.discriminatedUnion("op", [
17
+ z.object({
18
+ op: z.literal("get"),
19
+ key: z.string().min(1)
20
+ }),
21
+ z.object({
22
+ op: z.literal("set"),
23
+ key: z.string().min(1),
24
+ value: z.string(),
25
+ expireIn: z.number().int().positive().optional()
26
+ }),
27
+ z.object({
28
+ op: z.literal("del"),
29
+ key: z.string().min(1)
30
+ }),
31
+ z.object({
32
+ op: z.literal("list"),
33
+ prefix: z.string(),
34
+ limit: z.number().int().positive().optional(),
35
+ reverse: z.boolean().optional()
36
+ }),
37
+ z.object({
38
+ op: z.literal("keys"),
39
+ pattern: z.string().optional()
40
+ })
41
+ ]);
42
+ /**
43
+ * Zod schema for session error codes.
44
+ * @public
45
+ */
46
+ const SessionErrorCodeSchema = z.enum([
47
+ "stt",
48
+ "llm",
49
+ "tts",
50
+ "tool",
51
+ "protocol",
52
+ "connection",
53
+ "audio",
54
+ "internal"
55
+ ]);
56
+ /** Helper: simple event with only a type field. */
57
+ const ev = (t) => z.object({ type: z.literal(t) });
58
+ /** Helper: event with type + text. */
59
+ const textEv = (t) => z.object({
60
+ type: z.literal(t),
61
+ text: z.string()
62
+ });
63
+ const turnOrder = z.number().int().nonnegative().optional();
64
+ /** Zod schema for {@link ClientEvent}. */
65
+ const ClientEventSchema = z.discriminatedUnion("type", [
66
+ ev("speech_started"),
67
+ ev("speech_stopped"),
68
+ z.object({
69
+ type: z.literal("transcript"),
70
+ text: z.string(),
71
+ isFinal: z.boolean(),
72
+ turnOrder
73
+ }),
74
+ textEv("turn").extend({ turnOrder }),
75
+ textEv("chat"),
76
+ textEv("chat_delta"),
77
+ z.object({
78
+ type: z.literal("tool_call_start"),
79
+ toolCallId: z.string(),
80
+ toolName: z.string(),
81
+ args: z.record(z.string(), z.unknown())
82
+ }),
83
+ z.object({
84
+ type: z.literal("tool_call_done"),
85
+ toolCallId: z.string(),
86
+ result: z.string().max(MAX_TOOL_RESULT_CHARS)
87
+ }),
88
+ ev("tts_done"),
89
+ ev("cancelled"),
90
+ ev("reset"),
91
+ ev("idle_timeout"),
92
+ z.object({
93
+ type: z.literal("error"),
94
+ code: SessionErrorCodeSchema,
95
+ message: z.string()
96
+ })
97
+ ]);
98
+ /** Zod schema for {@link ReadyConfig}. */
99
+ const ReadyConfigSchema = z.object({
100
+ audioFormat: z.enum(["pcm16"]),
101
+ sampleRate: z.number().int().positive(),
102
+ ttsSampleRate: z.number().int().positive()
103
+ });
104
+ /** Zod schema for server→client text messages. */
105
+ const ServerMessageSchema = z.discriminatedUnion("type", [
106
+ z.object({
107
+ type: z.literal("config"),
108
+ audioFormat: z.string(),
109
+ sampleRate: z.number(),
110
+ ttsSampleRate: z.number(),
111
+ sessionId: z.string().optional()
112
+ }),
113
+ ev("audio_done"),
114
+ ...ClientEventSchema.options
115
+ ]);
116
+ /** Zod schema for client→server text messages. */
117
+ const ClientMessageSchema = z.discriminatedUnion("type", [
118
+ ev("audio_ready"),
119
+ ev("cancel"),
120
+ ev("reset"),
121
+ z.object({
122
+ type: z.literal("history"),
123
+ messages: z.array(z.object({
124
+ role: z.enum(["user", "assistant"]),
125
+ content: z.string().max(1e5)
126
+ })).max(200)
127
+ })
128
+ ]);
129
+ /** Build the protocol-level session config from S2S sample rates. */
130
+ function buildReadyConfig(s2sConfig) {
131
+ return {
132
+ audioFormat: AUDIO_FORMAT,
133
+ sampleRate: s2sConfig.inputSampleRate,
134
+ ttsSampleRate: s2sConfig.outputSampleRate
135
+ };
136
+ }
137
+ /** Zod schema for {@link TurnConfig}. */
138
+ const TurnConfigSchema = z.object({ maxSteps: z.number().int().positive().optional() });
139
+ //#endregion
140
+ export { AUDIO_FORMAT, ClientEventSchema, ClientMessageSchema, KvRequestSchema, ReadyConfigSchema, ServerMessageSchema, SessionErrorCodeSchema, TurnConfigSchema, buildReadyConfig };
package/dist/server.d.ts CHANGED
@@ -1,61 +1,48 @@
1
1
  /**
2
2
  * Self-hostable agent server.
3
3
  *
4
- * `createServer()` returns a server with `listen()` for HTTP + WebSocket.
5
- * Calls `createDirectExecutor` + `wireSessionSocket` directly no
6
- * intermediary needed.
4
+ * {@link createServer} wraps a {@link Runtime} with an HTTP + WebSocket
5
+ * server using only `node:http` and `ws` (no framework dependencies).
7
6
  */
8
- import { type Storage } from "unstorage";
9
- import type { Logger, S2SConfig } from "./runtime.ts";
10
- import type { AgentDef } from "./types.ts";
7
+ import type { Runtime } from "./direct-executor.ts";
8
+ import type { Kv } from "./kv.ts";
9
+ import type { Logger } from "./runtime.ts";
10
+ export { createRuntime, type Runtime, type RuntimeOptions } from "./direct-executor.ts";
11
11
  /**
12
- * Configuration for a self-hosted agent server created by {@link createServer}.
13
- *
12
+ * Configuration for {@link createServer}.
14
13
  * @public
15
14
  */
16
15
  export type ServerOptions = {
17
- /** The agent definition returned by `defineAgent()`. */
18
- agent: AgentDef<any>;
19
- /** Environment variables. Defaults to `process.env`. */
20
- env?: Record<string, string>;
21
- /**
22
- * Unstorage instance for KV and vector storage. Defaults to in-memory.
23
- * Configure with an S3/R2/filesystem driver for persistence.
24
- */
25
- storage?: Storage;
26
- /** HTML to serve at `GET /`. */
16
+ runtime: Runtime;
17
+ name?: string;
18
+ kv?: Kv;
27
19
  clientHtml?: string;
28
- /** Directory containing built client files (index.html + assets/). */
29
20
  clientDir?: string;
30
- /** Logger. Defaults to console. */
31
21
  logger?: Logger;
32
- /** S2S configuration. Defaults to AssemblyAI production. */
33
- s2sConfig?: S2SConfig;
34
- /**
35
- * Timeout in ms for `session.start()` (S2S connection setup).
36
- * Defaults to 10 000 (10 s). If the session doesn't initialize within
37
- * this window the connection is cleaned up.
38
- */
39
- sessionStartTimeoutMs?: number;
40
- /**
41
- * Maximum time in milliseconds to wait for sessions to stop during
42
- * {@link AgentServer.close | close()}. Sessions still running after this
43
- * deadline are force-closed. Defaults to `30_000` (30 seconds).
44
- */
45
- shutdownTimeoutMs?: number;
46
22
  };
47
23
  /**
48
- * Handle returned by {@link createServer} with lifecycle methods to start
49
- * and stop the HTTP + WebSocket server.
50
- *
24
+ * Handle returned by {@link createServer}.
51
25
  * @public
52
26
  */
53
27
  export type AgentServer = {
54
- /** Start listening on the given port. */
55
28
  listen(port?: number): Promise<void>;
56
- /** Stop the server. */
57
29
  close(): Promise<void>;
58
- /** The port the server is listening on, or `undefined` before `listen()`. */
59
30
  port: number | undefined;
60
31
  };
32
+ /**
33
+ * Create an HTTP + WebSocket server for self-hosted agent deployments.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { defineAgent } from "@alexkroman1/aai";
38
+ * import { createRuntime, createServer } from "@alexkroman1/aai/server";
39
+ *
40
+ * const agent = defineAgent({ name: "my-agent" });
41
+ * const runtime = createRuntime({ agent, env: process.env });
42
+ * const server = createServer({ runtime, name: agent.name });
43
+ * await server.listen(3000);
44
+ * ```
45
+ *
46
+ * @public
47
+ */
61
48
  export declare function createServer(options: ServerOptions): AgentServer;
package/dist/server.js CHANGED
@@ -1,183 +1,155 @@
1
- import { t as createUnstorageVectorStore } from "./unstorage-vector-Cj5llNhg.js";
2
- import { i as filterEnv } from "./_utils-DgzpOMSV.js";
3
- import { t as createDirectExecutor } from "./direct-executor-B-5mq3cu.js";
4
- import { m as buildReadyConfig } from "./protocol-B-H2Q4ox.js";
5
- import { n as consoleLogger, t as DEFAULT_S2S_CONFIG } from "./runtime-CxcwaK68.js";
6
- import { t as createUnstorageKv } from "./unstorage-kv-CDgP-frt.js";
7
- import { t as wireSessionSocket } from "./ws-handler-C0Q6eSay.js";
8
- import { createStorage } from "unstorage";
9
- import { serve } from "@hono/node-server";
10
- import { serveStatic } from "@hono/node-server/serve-static";
11
- import { createNodeWebSocket } from "@hono/node-ws";
12
- import { Hono } from "hono";
13
- import { html } from "hono/html";
14
- import { secureHeaders } from "hono/secure-headers";
1
+ import { t as AGENT_CSP } from "./constants-BbAOvKl_.js";
2
+ import { _ as consoleLogger, t as createRuntime } from "./direct-executor-BfHrDdPL.js";
3
+ import fs from "node:fs";
4
+ import http from "node:http";
5
+ import path from "node:path";
6
+ import { WebSocketServer } from "ws";
15
7
  //#region server.ts
16
8
  /**
17
9
  * Self-hostable agent server.
18
10
  *
19
- * `createServer()` returns a server with `listen()` for HTTP + WebSocket.
20
- * Calls `createDirectExecutor` + `wireSessionSocket` directly no
21
- * intermediary needed.
11
+ * {@link createServer} wraps a {@link Runtime} with an HTTP + WebSocket
12
+ * server using only `node:http` and `ws` (no framework dependencies).
22
13
  */
14
+ const MIME_TYPES = {
15
+ ".html": "text/html",
16
+ ".js": "application/javascript",
17
+ ".mjs": "application/javascript",
18
+ ".css": "text/css",
19
+ ".json": "application/json",
20
+ ".svg": "image/svg+xml",
21
+ ".png": "image/png",
22
+ ".jpg": "image/jpeg",
23
+ ".ico": "image/x-icon",
24
+ ".woff2": "font/woff2",
25
+ ".woff": "font/woff",
26
+ ".map": "application/json"
27
+ };
28
+ function serveStatic(dir, req, res) {
29
+ const url = req.url?.split("?")[0] ?? "/";
30
+ const filePath = path.join(dir, url === "/" ? "index.html" : url);
31
+ if (!filePath.startsWith(dir)) return false;
32
+ try {
33
+ const stat = fs.statSync(filePath);
34
+ if (!stat.isFile()) return false;
35
+ const mime = MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
36
+ res.writeHead(200, {
37
+ "Content-Type": mime,
38
+ "Content-Length": stat.size
39
+ });
40
+ fs.createReadStream(filePath).pipe(res);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
23
46
  /**
24
47
  * Create an HTTP + WebSocket server for self-hosted agent deployments.
25
48
  *
26
- * Sets up a Hono HTTP server with a `/health` endpoint and WebSocket upgrade
27
- * handling. Agent tools execute directly in-process via {@link createDirectExecutor}.
28
- *
29
- * @param options - Server configuration including the agent definition, optional
30
- * KV store, client assets, logger, and S2S config. See {@link ServerOptions}.
31
- * @returns An {@link AgentServer} with `listen()` and `close()` lifecycle methods.
32
- *
33
49
  * @example
34
50
  * ```ts
35
51
  * import { defineAgent } from "@alexkroman1/aai";
36
- * import { createServer } from "@alexkroman1/aai/server";
52
+ * import { createRuntime, createServer } from "@alexkroman1/aai/server";
37
53
  *
38
54
  * const agent = defineAgent({ name: "my-agent" });
39
- * const server = createServer({ agent });
55
+ * const runtime = createRuntime({ agent, env: process.env });
56
+ * const server = createServer({ runtime, name: agent.name });
40
57
  * await server.listen(3000);
41
58
  * ```
42
59
  *
43
60
  * @public
44
61
  */
45
- async function drainSessions(sessions, shutdownTimeoutMs, logger) {
46
- if (sessions.size === 0) return;
47
- let timer;
48
- const timeout = new Promise((resolve) => {
49
- timer = setTimeout(resolve, shutdownTimeoutMs, "timeout");
50
- });
51
- const graceful = Promise.allSettled([...sessions.values()].map((s) => s.stop())).then((results) => {
52
- for (const r of results) if (r.status === "rejected") logger.warn(`Session stop failed during close: ${r.reason}`);
53
- return "done";
54
- });
55
- const outcome = await Promise.race([graceful, timeout]);
56
- if (timer) clearTimeout(timer);
57
- if (outcome === "timeout") logger.warn(`Shutdown timeout (${shutdownTimeoutMs}ms) exceeded — force-closing ${sessions.size} remaining session(s)`);
58
- sessions.clear();
59
- }
60
62
  function createServer(options) {
61
- if (options.clientHtml && options.clientDir) throw new Error("ServerOptions: clientHtml and clientDir are mutually exclusive provide one or the other, not both.");
62
- const { agent, clientHtml, clientDir, logger = consoleLogger, s2sConfig = DEFAULT_S2S_CONFIG, shutdownTimeoutMs = 3e4 } = options;
63
- const env = filterEnv(options.env ?? (typeof process !== "undefined" ? process.env : {}));
64
- const storage = options.storage ?? createStorage();
65
- const kv = createUnstorageKv({ storage });
66
- const executor = createDirectExecutor({
67
- agent,
68
- env,
69
- kv,
70
- vector: createUnstorageVectorStore({ storage }),
71
- logger,
72
- s2sConfig
63
+ const { runtime, clientHtml, clientDir, logger = consoleLogger, kv } = options;
64
+ const name = options.name ?? "agent";
65
+ if (clientHtml && clientDir) throw new Error("clientHtml and clientDir are mutually exclusive");
66
+ const httpServer = http.createServer((req, res) => {
67
+ const url = req.url?.split("?")[0] ?? "/";
68
+ const method = req.method ?? "GET";
69
+ res.setHeader("Content-Security-Policy", AGENT_CSP);
70
+ res.setHeader("X-Content-Type-Options", "nosniff");
71
+ res.setHeader("X-Frame-Options", "SAMEORIGIN");
72
+ if (method === "GET" && url === "/health") {
73
+ res.writeHead(200, { "Content-Type": "application/json" });
74
+ res.end(JSON.stringify({
75
+ status: "ok",
76
+ name
77
+ }));
78
+ return;
79
+ }
80
+ if (kv && method === "GET" && url === "/kv") {
81
+ const key = new URL(req.url ?? "/", "http://localhost").searchParams.get("key");
82
+ if (!key) {
83
+ res.writeHead(400, { "Content-Type": "application/json" });
84
+ res.end(JSON.stringify({ error: "Missing key query parameter" }));
85
+ return;
86
+ }
87
+ kv.get(key).then((value) => {
88
+ if (value === null) {
89
+ res.writeHead(404, { "Content-Type": "application/json" });
90
+ res.end("null");
91
+ } else {
92
+ res.writeHead(200, { "Content-Type": "application/json" });
93
+ res.end(JSON.stringify(value));
94
+ }
95
+ }).catch(() => {
96
+ res.writeHead(500, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({ error: "KV error" }));
98
+ });
99
+ return;
100
+ }
101
+ if (clientDir && serveStatic(clientDir, req, res)) return;
102
+ if (method === "GET" && url === "/") {
103
+ const escaped = name.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
104
+ const body = clientHtml ?? `<!DOCTYPE html><html><body><h1>${escaped}</h1><p>Agent server running.</p></body></html>`;
105
+ res.writeHead(200, { "Content-Type": "text/html" });
106
+ res.end(body);
107
+ return;
108
+ }
109
+ logger.error(`${method} ${url} 404`);
110
+ res.writeHead(404, { "Content-Type": "application/json" });
111
+ res.end(JSON.stringify({ error: "Not found" }));
73
112
  });
74
- const sessions = /* @__PURE__ */ new Map();
75
- const readyConfig = buildReadyConfig(s2sConfig);
76
- function handleWs(ws, skipGreeting, resumeFrom) {
77
- wireSessionSocket(ws, {
78
- sessions,
79
- createSession: (sid, client) => executor.createSession({
80
- id: sid,
81
- agent: agent.name,
82
- client,
113
+ const wss = new WebSocketServer({ noServer: true });
114
+ httpServer.on("upgrade", (req, socket, head) => {
115
+ const url = req.url?.split("?")[0] ?? "";
116
+ if (!url.startsWith("/websocket")) return;
117
+ wss.handleUpgrade(req, socket, head, (ws) => {
118
+ const search = req.url?.includes("?") ? req.url.split("?")[1] ?? "" : "";
119
+ const params = new URLSearchParams(search);
120
+ const resumeFrom = params.get("sessionId") ?? void 0;
121
+ const skipGreeting = params.has("resume") || resumeFrom !== void 0;
122
+ logger.info(`WS upgrade ${url}${skipGreeting ? " (resume)" : ""}`);
123
+ runtime.startSession(ws, {
83
124
  skipGreeting,
84
125
  ...resumeFrom ? { resumeFrom } : {}
85
- }),
86
- readyConfig,
87
- logger,
88
- ...options.sessionStartTimeoutMs !== void 0 ? { sessionStartTimeoutMs: options.sessionStartTimeoutMs } : {},
89
- ...resumeFrom ? { resumeFrom } : {}
126
+ });
90
127
  });
91
- }
92
- let serverHandle = null;
128
+ });
93
129
  let listenPort;
94
130
  return {
95
131
  get port() {
96
132
  return listenPort;
97
133
  },
98
134
  async listen(port = 3e3) {
99
- if (serverHandle) throw new Error("Server is already listening");
100
- const app = new Hono();
101
- const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
102
- app.onError((err, c) => {
103
- logger.error(`${c.req.method} ${c.req.path} error: ${err.message}`);
104
- return c.json({ error: "Internal Server Error" }, 500);
105
- });
106
- app.use("/*", async (c, next) => {
107
- const start = Date.now();
108
- await next();
109
- const ms = Date.now() - start;
110
- const { status } = c.res;
111
- const method = c.req.method;
112
- const path = c.req.path;
113
- if (status >= 400) logger.error(`${method} ${path} ${status} ${ms}ms`);
114
- else logger.info(`${method} ${path} ${status} ${ms}ms`);
115
- });
116
- app.use("*", secureHeaders({ contentSecurityPolicy: {
117
- defaultSrc: ["'self'"],
118
- scriptSrc: ["'self'", "blob:"],
119
- styleSrc: [
120
- "'self'",
121
- "'unsafe-inline'",
122
- "https://fonts.googleapis.com"
123
- ],
124
- connectSrc: [
125
- "'self'",
126
- "wss:",
127
- "ws:"
128
- ],
129
- imgSrc: ["'self'", "data:"],
130
- fontSrc: ["'self'", "https://fonts.gstatic.com"],
131
- objectSrc: ["'none'"],
132
- baseUri: ["'self'"]
133
- } }));
134
- app.get("/health", (c) => c.json({
135
- status: "ok",
136
- name: agent.name
137
- }));
138
- app.get("/kv", async (c) => {
139
- const key = c.req.query("key");
140
- if (!key) return c.json({ error: "Missing key query parameter" }, 400);
141
- const value = await kv.get(key);
142
- if (value === null) return c.json(null, 404);
143
- return c.json(value);
144
- });
145
- if (clientDir) app.use("/*", serveStatic({ root: clientDir }));
146
- app.get("/", (c) => {
147
- if (clientHtml) return c.html(clientHtml);
148
- return c.html(html`<!DOCTYPE html><html><body><h1>${agent.name}</h1><p>Agent server running.</p></body></html>`);
149
- });
150
- app.get("/websocket", upgradeWebSocket((c) => {
151
- const resumeFrom = c.req.query("sessionId") ?? void 0;
152
- const skipGreeting = c.req.query("resume") !== void 0 || resumeFrom !== void 0;
153
- logger.info(`WS upgrade ${c.req.path}${skipGreeting ? " (resume)" : ""}`);
154
- return { onOpen(_evt, ws) {
155
- if (ws.raw) handleWs(ws.raw, skipGreeting, resumeFrom);
156
- } };
157
- }));
158
- const nodeServer = serve({
159
- fetch: app.fetch,
160
- port
161
- });
162
- injectWebSocket(nodeServer);
163
135
  await new Promise((resolve, reject) => {
164
- nodeServer.on("listening", resolve);
165
- nodeServer.on("error", reject);
166
- });
167
- const addr = nodeServer.address();
168
- listenPort = typeof addr === "object" && addr ? addr.port : port;
169
- serverHandle = { async shutdown() {
170
- await new Promise((resolve, reject) => {
171
- nodeServer.close((err) => err ? reject(err) : resolve());
136
+ httpServer.on("error", reject);
137
+ httpServer.listen(port, () => {
138
+ const addr = httpServer.address();
139
+ listenPort = typeof addr === "object" && addr ? addr.port : port;
140
+ resolve();
172
141
  });
173
- } };
142
+ });
174
143
  },
175
144
  async close() {
176
- await drainSessions(sessions, shutdownTimeoutMs, logger);
177
- await serverHandle?.shutdown();
145
+ await runtime.shutdown();
146
+ wss.close();
147
+ if (listenPort !== void 0) await new Promise((resolve, reject) => {
148
+ httpServer.close((err) => err ? reject(err) : resolve());
149
+ });
178
150
  listenPort = void 0;
179
151
  }
180
152
  };
181
153
  }
182
154
  //#endregion
183
- export { createServer };
155
+ export { createRuntime, createServer };
package/dist/session.d.ts CHANGED
@@ -1,15 +1,70 @@
1
1
  /** S2S session — relays audio between client and AssemblyAI S2S API. */
2
- import type { AgentConfig, ToolSchema } from "./_internal-types.ts";
3
- import { type SessionPersistence } from "./_session-persist.ts";
4
- import type { HookInvoker } from "./middleware.ts";
2
+ import type { AgentConfig, ExecuteTool, ToolSchema } from "./_internal-types.ts";
3
+ import type { AgentHookMap, AgentHooks } from "./hooks.ts";
5
4
  import type { ClientSink } from "./protocol.ts";
6
5
  import type { Logger, S2SConfig } from "./runtime.ts";
7
- import { type CreateS2sWebSocket, connectS2s } from "./s2s.ts";
8
- import type { ExecuteTool } from "./worker-entry.ts";
9
- export type { S2sSessionCtx } from "./_session-ctx.ts";
10
- export type { PersistedSession, SessionPersistence } from "./_session-persist.ts";
11
- export { persistKey } from "./_session-persist.ts";
12
- export type { HookInvoker, ToolInterceptResult } from "./middleware.ts";
6
+ import { type CreateS2sWebSocket, connectS2s, type S2sHandle } from "./s2s.ts";
7
+ import type { Message } from "./types.ts";
8
+ export type { S2sHandle } from "./s2s.ts";
9
+ type PendingTool = {
10
+ callId: string;
11
+ result: string;
12
+ };
13
+ /** Per-reply mutable state — reset on beginReply/cancelReply. */
14
+ export type ReplyState = {
15
+ pendingTools: PendingTool[];
16
+ toolCallCount: number;
17
+ currentReplyId: string | null;
18
+ };
19
+ /** Immutable dependencies injected at session creation. */
20
+ export type SessionDeps = {
21
+ readonly id: string;
22
+ readonly agent: string;
23
+ readonly client: ClientSink;
24
+ readonly agentConfig: AgentConfig;
25
+ readonly executeTool: ExecuteTool;
26
+ readonly hooks: AgentHooks | undefined;
27
+ readonly log: Logger;
28
+ readonly maxHistory: number;
29
+ };
30
+ /**
31
+ * Session context threaded through event handlers.
32
+ *
33
+ * Split into three layers:
34
+ * - {@link SessionDeps} — immutable dependencies (set once)
35
+ * - {@link ReplyState} via `reply` — per-reply mutable state (reset on beginReply/cancelReply)
36
+ * - Remaining fields — connection, conversation, and lifecycle methods
37
+ */
38
+ export type S2sSessionCtx = SessionDeps & {
39
+ s2s: S2sHandle | null;
40
+ reply: ReplyState;
41
+ turnPromise: Promise<void> | null;
42
+ conversationMessages: Message[];
43
+ resolveTurnConfig(): Promise<{
44
+ maxSteps?: number;
45
+ } | null>;
46
+ consumeToolCallStep(turnConfig: {
47
+ maxSteps?: number;
48
+ } | null, name: string, replyId: string | null): string | null;
49
+ fireHook(name: keyof AgentHookMap, ...args: unknown[]): void;
50
+ drainHooks(): Promise<void>;
51
+ pushMessages(...msgs: Message[]): void;
52
+ beginReply(replyId: string): void;
53
+ cancelReply(): void;
54
+ chainTurn(p: Promise<void>): void;
55
+ };
56
+ export declare function buildCtx(opts: {
57
+ id: string;
58
+ agent: string;
59
+ client: ClientSink;
60
+ agentConfig: AgentConfig;
61
+ executeTool: ExecuteTool;
62
+ hooks: AgentHooks | undefined;
63
+ log: Logger;
64
+ maxHistory?: number | undefined;
65
+ }): S2sSessionCtx;
66
+ export type { AgentHookMap, AgentHooks } from "./hooks.ts";
67
+ export { callResolveTurnConfig, createAgentHooks } from "./hooks.ts";
13
68
  export { buildSystemPrompt } from "./system-prompt.ts";
14
69
  /**
15
70
  * A voice session managing the Speech-to-Speech connection for one client.
@@ -20,27 +75,16 @@ export { buildSystemPrompt } from "./system-prompt.ts";
20
75
  * @internal Exported for use by `ws-handler.ts`, `server.ts`, and `direct-executor.ts`.
21
76
  */
22
77
  export type Session = {
23
- /** Open the S2S connection and fire the `onConnect` hook. */
24
78
  start(): Promise<void>;
25
- /** Gracefully shut down: wait for in-flight turns, close the S2S socket, fire `onDisconnect`. */
26
79
  stop(): Promise<void>;
27
- /** Forward raw PCM audio from the client microphone to the S2S connection. */
28
80
  onAudio(data: Uint8Array): void;
29
- /** Called when the client has finished setting up its audio pipeline. For S2S sessions this is a no-op since the greeting comes automatically. */
30
81
  onAudioReady(): void;
31
- /** Handle a client-initiated cancellation (barge-in). Sends a `cancelled` event. */
32
82
  onCancel(): void;
33
- /** Reset the session: clear conversation history, bump generation counters, reconnect S2S. */
34
83
  onReset(): void;
35
- /**
36
- * Inject conversation history from the client (e.g. on reconnect).
37
- * @param incoming - Messages with `{role, content}` fields.
38
- */
39
84
  onHistory(incoming: readonly {
40
85
  role: "user" | "assistant";
41
86
  content: string;
42
87
  }[]): void;
43
- /** Returns a promise that resolves when the current in-flight turn completes, or resolves immediately if no turn is active. */
44
88
  waitForTurn(): Promise<void>;
45
89
  };
46
90
  /** Configuration options for creating a new session. */
@@ -55,36 +99,13 @@ export type S2sSessionOptions = {
55
99
  executeTool: ExecuteTool;
56
100
  createWebSocket?: CreateS2sWebSocket;
57
101
  env?: Record<string, string | undefined>;
58
- hookInvoker?: HookInvoker;
102
+ hooks?: AgentHooks;
59
103
  skipGreeting?: boolean;
60
104
  logger?: Logger;
61
- /** Maximum number of conversation messages to retain. Older messages are
62
- * dropped (sliding window) to bound memory in long-running sessions.
63
- * Defaults to 200. Set to 0 or Infinity to disable trimming. */
64
105
  maxHistory?: number;
65
- /** Persistence configuration for auto-saving/restoring session data. */
66
- persistence?: SessionPersistence;
67
- /** Old session ID to resume from. Loads persisted state/messages from KV
68
- * and attempts S2S session resume. */
69
- resumeFrom?: string;
70
106
  };
71
107
  /** @internal Not part of the public API. Exposed for testing only. */
72
108
  export declare const _internals: {
73
109
  connectS2s: typeof connectS2s;
74
110
  };
75
- /**
76
- * Create a Speech-to-Speech backed session implementing the {@link Session} interface.
77
- *
78
- * Connects to AssemblyAI's S2S WebSocket, configures the system prompt and tools,
79
- * and wires up event listeners for user transcripts, agent replies, tool calls,
80
- * barge-ins, and session lifecycle. Manages reconnection on `onReset` via a
81
- * `connectGeneration` guard that prevents stale connection attempts from overwriting
82
- * newer ones during rapid resets. A `sessionAbort` AbortController is used to
83
- * coordinate cleanup on `stop()`.
84
- *
85
- * @param opts - Session configuration. See {@link S2sSessionOptions} for all fields
86
- * including the agent config, tool schemas, API key, and optional hooks.
87
- * @returns A {@link Session} with `start`, `stop`, `onAudio`, `onReset`, and other
88
- * lifecycle methods.
89
- */
90
111
  export declare function createS2sSession(opts: S2sSessionOptions): Session;