@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.
- package/dist/_internal-types.d.ts +8 -1
- package/dist/_runtime-conformance.d.ts +64 -0
- package/dist/_test-utils.d.ts +70 -0
- package/dist/_utils.d.ts +1 -8
- package/dist/_utils.js +49 -2
- package/dist/builtin-tools.d.ts +1 -5
- package/dist/constants-BbAOvKl_.js +47 -0
- package/dist/constants.d.ts +44 -0
- package/dist/direct-executor-BfHrDdPL.js +1589 -0
- package/dist/direct-executor.d.ts +90 -31
- package/dist/hooks.d.ts +44 -0
- package/dist/hooks.js +58 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +2 -2
- package/dist/internal.d.ts +19 -0
- package/dist/internal.js +209 -0
- package/dist/kv.d.ts +1 -1
- package/dist/kv.js +32 -1
- package/dist/matchers.js +1 -1
- package/dist/protocol.d.ts +3 -29
- package/dist/protocol.js +140 -2
- package/dist/server.d.ts +27 -40
- package/dist/server.js +117 -145
- package/dist/session.d.ts +65 -44
- package/dist/{testing-BbitshLb.js → testing-BonJtfHJ.js} +25 -43
- package/dist/testing.d.ts +9 -14
- package/dist/testing.js +2 -2
- package/dist/types.d.ts +24 -226
- package/dist/types.js +176 -2
- package/dist/types.test-d.d.ts +7 -0
- package/dist/vite-plugin.d.ts +15 -0
- package/dist/vite-plugin.js +82 -0
- package/dist/ws-handler.d.ts +1 -2
- package/package.json +28 -88
- package/dist/_embeddings.d.ts +0 -31
- package/dist/_internal-types-IfPcaJd5.js +0 -61
- package/dist/_internal-types.js +0 -2
- package/dist/_session-ctx.d.ts +0 -73
- package/dist/_session-otel.d.ts +0 -43
- package/dist/_session-persist.d.ts +0 -30
- package/dist/_ssrf-DCp_27V4.js +0 -123
- package/dist/_ssrf.d.ts +0 -30
- package/dist/_ssrf.js +0 -2
- package/dist/_utils-DgzpOMSV.js +0 -61
- package/dist/direct-executor-B-5mq3cu.js +0 -570
- package/dist/kv-iXtikQmR.js +0 -32
- package/dist/middleware-core-BwyBIPed.js +0 -107
- package/dist/middleware-core.d.ts +0 -47
- package/dist/middleware-core.js +0 -2
- package/dist/middleware.d.ts +0 -37
- package/dist/protocol-B-H2Q4ox.js +0 -162
- package/dist/runtime-CxcwaK68.js +0 -58
- package/dist/runtime.js +0 -2
- package/dist/s2s-M7JqtgFw.js +0 -272
- package/dist/s2s.js +0 -2
- package/dist/session-BYlwcrya.js +0 -683
- package/dist/session.js +0 -2
- package/dist/telemetry-CJlaDFNc.js +0 -95
- package/dist/telemetry.d.ts +0 -49
- package/dist/telemetry.js +0 -2
- package/dist/types-D8ZBxTL_.js +0 -192
- package/dist/unstorage-kv-CDgP-frt.js +0 -64
- package/dist/unstorage-kv.js +0 -2
- package/dist/unstorage-vector-Cj5llNhg.js +0 -172
- package/dist/unstorage-vector.d.ts +0 -47
- package/dist/unstorage-vector.js +0 -2
- package/dist/vector.d.ts +0 -86
- package/dist/vector.js +0 -49
- package/dist/worker-entry-2jaiqIj0.js +0 -70
- package/dist/worker-entry.d.ts +0 -47
- package/dist/worker-entry.js +0 -2
- package/dist/ws-handler-C0Q6eSay.js +0 -207
- package/dist/ws-handler.js +0 -2
package/dist/protocol.js
CHANGED
|
@@ -1,2 +1,140 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
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
|
|
13
|
-
*
|
|
12
|
+
* Configuration for {@link createServer}.
|
|
14
13
|
* @public
|
|
15
14
|
*/
|
|
16
15
|
export type ServerOptions = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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}
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
177
|
-
|
|
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 {
|
|
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 {
|
|
9
|
-
export type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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;
|