@alexkroman1/aai 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +11 -9
- package/CHANGELOG.md +10 -0
- package/dist/{_internal-types-CrnTi9Ew.js → _internal-types-CfOAbK6V.js} +22 -35
- package/dist/constants-y68COEGj.js +29 -0
- package/dist/host/_base64.d.ts +2 -0
- package/dist/host/_mock-ws.d.ts +0 -61
- package/dist/host/_pipeline-test-fakes.d.ts +7 -4
- package/dist/host/_run-code.d.ts +0 -25
- package/dist/host/_runtime-conformance.d.ts +3 -34
- package/dist/host/memory-vector.d.ts +0 -11
- package/dist/host/providers/resolve-kv.d.ts +0 -7
- package/dist/host/providers/resolve-vector.d.ts +0 -8
- package/dist/host/providers/stt/assemblyai.d.ts +0 -14
- package/dist/host/providers/stt/deepgram.d.ts +2 -14
- package/dist/host/providers/stt/soniox.d.ts +0 -22
- package/dist/host/providers/tts/rime.d.ts +10 -31
- package/dist/host/runtime-barrel.js +619 -630
- package/dist/host/runtime-config.d.ts +9 -6
- package/dist/host/runtime.d.ts +3 -0
- package/dist/host/to-vercel-tools.d.ts +3 -33
- package/dist/host/transports/openai-realtime-transport.d.ts +43 -0
- package/dist/host/unstorage-kv.d.ts +0 -26
- package/dist/index.js +3 -3
- package/dist/openai-realtime-cjPAHMMx.js +10 -0
- package/dist/sdk/_internal-types.d.ts +6 -55
- package/dist/sdk/allowed-hosts.d.ts +4 -3
- package/dist/sdk/constants.d.ts +4 -29
- package/dist/sdk/define.d.ts +7 -4
- package/dist/sdk/kv.d.ts +13 -37
- package/dist/sdk/manifest-barrel.js +1 -1
- package/dist/sdk/manifest.d.ts +8 -2
- package/dist/sdk/protocol.js +1 -1
- package/dist/sdk/providers/s2s/openai-realtime.d.ts +17 -0
- package/dist/sdk/providers/s2s-barrel.d.ts +9 -0
- package/dist/sdk/providers/s2s-barrel.js +2 -0
- package/dist/sdk/providers/tts/rime.d.ts +1 -1
- package/dist/sdk/providers.d.ts +6 -2
- package/dist/sdk/types.d.ts +7 -1
- package/dist/{types-KUgezM6u.js → types-DOWVZhb9.js} +1 -7
- package/dist/{ws-upgrade-BeOQ7fXL.js → ws-upgrade-CG8-by1n.js} +2 -3
- package/host/_base64.ts +9 -0
- package/host/_mock-ws.ts +0 -65
- package/host/_pipeline-test-fakes.ts +19 -31
- package/host/_run-code.ts +10 -53
- package/host/_runtime-conformance.ts +3 -44
- package/host/_test-utils.ts +20 -42
- package/host/builtin-tools.test.ts +127 -222
- package/host/builtin-tools.ts +6 -10
- package/host/cleanup.test.ts +30 -73
- package/host/integration/pipeline-reference.integration.test.ts +12 -17
- package/host/integration.test.ts +0 -7
- package/host/memory-vector.test.ts +3 -1
- package/host/memory-vector.ts +16 -21
- package/host/pinecone-vector.test.ts +14 -17
- package/host/pinecone-vector.ts +10 -19
- package/host/providers/providers.test-d.ts +5 -3
- package/host/providers/resolve-kv.ts +23 -41
- package/host/providers/resolve-vector.ts +3 -12
- package/host/providers/resolve.test.ts +15 -28
- package/host/providers/resolve.ts +24 -24
- package/host/providers/stt/assemblyai.test.ts +2 -14
- package/host/providers/stt/assemblyai.ts +12 -35
- package/host/providers/stt/deepgram.test.ts +23 -83
- package/host/providers/stt/deepgram.ts +15 -40
- package/host/providers/stt/elevenlabs.test.ts +26 -38
- package/host/providers/stt/elevenlabs.ts +10 -9
- package/host/providers/stt/soniox.test.ts +35 -85
- package/host/providers/stt/soniox.ts +8 -53
- package/host/providers/tts/cartesia.test.ts +19 -58
- package/host/providers/tts/cartesia.ts +36 -66
- package/host/providers/tts/rime.test.ts +12 -38
- package/host/providers/tts/rime.ts +23 -86
- package/host/runtime-config.test.ts +9 -9
- package/host/runtime-config.ts +16 -22
- package/host/runtime.test.ts +111 -73
- package/host/runtime.ts +138 -86
- package/host/s2s.test.ts +92 -191
- package/host/s2s.ts +55 -49
- package/host/server-shutdown.test.ts +9 -30
- package/host/server.test.ts +2 -13
- package/host/server.ts +85 -100
- package/host/session-core.test.ts +15 -30
- package/host/session-core.ts +10 -13
- package/host/session-prompt.test.ts +1 -5
- package/host/to-vercel-tools.test.ts +53 -72
- package/host/to-vercel-tools.ts +9 -39
- package/host/tool-executor.test.ts +25 -51
- package/host/tool-executor.ts +18 -12
- package/host/transports/openai-realtime-transport.test.ts +371 -0
- package/host/transports/openai-realtime-transport.ts +319 -0
- package/host/transports/pipeline-transport.test.ts +125 -298
- package/host/transports/pipeline-transport.ts +20 -68
- package/host/transports/s2s-transport-fixtures.test.ts +31 -92
- package/host/transports/s2s-transport.test.ts +65 -134
- package/host/transports/s2s-transport.ts +15 -43
- package/host/transports/types.test.ts +4 -8
- package/host/unstorage-kv.test.ts +3 -2
- package/host/unstorage-kv.ts +5 -35
- package/host/ws-handler.test.ts +72 -176
- package/host/ws-handler.ts +6 -12
- package/package.json +6 -1
- package/sdk/__snapshots__/exports.test.ts.snap +7 -0
- package/sdk/__snapshots__/schema-shapes.test.ts.snap +1 -0
- package/sdk/_internal-types.test.ts +6 -9
- package/sdk/_internal-types.ts +16 -57
- package/sdk/_test-matchers.ts +25 -15
- package/sdk/allowed-hosts.test.ts +50 -114
- package/sdk/allowed-hosts.ts +8 -14
- package/sdk/constants.ts +5 -52
- package/sdk/define.test.ts +7 -6
- package/sdk/define.ts +7 -3
- package/sdk/exports.test.ts +6 -1
- package/sdk/kv.ts +13 -37
- package/sdk/manifest.test-d.ts +5 -0
- package/sdk/manifest.test.ts +61 -9
- package/sdk/manifest.ts +11 -11
- package/sdk/protocol-compat.test.ts +66 -98
- package/sdk/protocol-snapshot.test.ts +2 -16
- package/sdk/protocol.test.ts +13 -22
- package/sdk/providers/s2s/openai-realtime.ts +36 -0
- package/sdk/providers/s2s-barrel.ts +12 -0
- package/sdk/providers/tts/rime.ts +1 -1
- package/sdk/providers.ts +24 -5
- package/sdk/schema-alignment.test.ts +25 -73
- package/sdk/schema-shapes.test.ts +1 -29
- package/sdk/system-prompt.test.ts +0 -1
- package/sdk/system-prompt.ts +17 -19
- package/sdk/types-inference.test.ts +10 -36
- package/sdk/types.ts +7 -0
- package/sdk/ws-upgrade.test.ts +24 -23
- package/sdk/ws-upgrade.ts +2 -3
- package/tsdown.config.ts +8 -11
- package/dist/constants-C2nirZUI.js +0 -54
package/host/s2s.ts
CHANGED
|
@@ -8,12 +8,10 @@ import WsWebSocket from "ws";
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { WS_OPEN } from "../sdk/constants.ts";
|
|
10
10
|
import type { ClientEvent } from "../sdk/protocol.ts";
|
|
11
|
+
import { base64ToUint8, uint8ToBase64 } from "./_base64.ts";
|
|
11
12
|
import type { Logger, S2SConfig } from "./runtime-config.ts";
|
|
12
13
|
import { consoleLogger } from "./runtime-config.ts";
|
|
13
14
|
|
|
14
|
-
const uint8ToBase64 = (bytes: Uint8Array): string => Buffer.from(bytes).toString("base64");
|
|
15
|
-
const base64ToUint8 = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));
|
|
16
|
-
|
|
17
15
|
export type S2sWebSocket = {
|
|
18
16
|
readonly readyState: number;
|
|
19
17
|
send(data: string): void;
|
|
@@ -32,8 +30,7 @@ export type CreateS2sWebSocket = (
|
|
|
32
30
|
opts: { headers: Record<string, string> },
|
|
33
31
|
) => S2sWebSocket;
|
|
34
32
|
|
|
35
|
-
// Node's native WebSocket doesn't support custom headers.
|
|
36
|
-
// Use the `ws` package which accepts { headers } in the constructor.
|
|
33
|
+
// Node's native WebSocket doesn't support custom headers; the `ws` package does.
|
|
37
34
|
export const defaultCreateS2sWebSocket: CreateS2sWebSocket = (url, opts) =>
|
|
38
35
|
new WsWebSocket(url, { headers: opts.headers }) as unknown as S2sWebSocket;
|
|
39
36
|
|
|
@@ -41,7 +38,12 @@ export const defaultCreateS2sWebSocket: CreateS2sWebSocket = (url, opts) =>
|
|
|
41
38
|
|
|
42
39
|
const S2sMessageSchema = z.discriminatedUnion("type", [
|
|
43
40
|
z.object({ type: z.literal("session.ready"), session_id: z.string() }).passthrough(),
|
|
44
|
-
z
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
type: z.literal("session.updated"),
|
|
44
|
+
config: z.object({ id: z.string().optional() }).passthrough().optional(),
|
|
45
|
+
})
|
|
46
|
+
.passthrough(),
|
|
45
47
|
z.object({ type: z.literal("input.speech.started") }),
|
|
46
48
|
z.object({ type: z.literal("input.speech.stopped") }),
|
|
47
49
|
z.object({ type: z.literal("transcript.user"), item_id: z.string(), text: z.string() }),
|
|
@@ -86,12 +88,14 @@ export type S2sEvent = ClientEvent & { _interrupted?: boolean };
|
|
|
86
88
|
type DispatchState = { speechActive: boolean };
|
|
87
89
|
|
|
88
90
|
type DispatchContext = {
|
|
89
|
-
/** Logger used for diagnostic `S2S <<` arrival logs. */
|
|
90
91
|
log: Logger;
|
|
91
|
-
/** Session id threaded through diagnostic logs; omitted when undefined. */
|
|
92
92
|
sid?: string;
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
function sidFields(ctx: DispatchContext): { sid?: string } {
|
|
96
|
+
return ctx.sid !== undefined ? { sid: ctx.sid } : {};
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
function dispatchS2sMessage(
|
|
96
100
|
callbacks: S2sCallbacks,
|
|
97
101
|
msg: S2sServerMessage,
|
|
@@ -103,6 +107,10 @@ function dispatchS2sMessage(
|
|
|
103
107
|
callbacks.onSessionReady(msg.session_id);
|
|
104
108
|
break;
|
|
105
109
|
case "session.updated":
|
|
110
|
+
// The S2S API conveys the session id via `config.id` in the success
|
|
111
|
+
// path (no separate `session.ready` is emitted); capturing it here is
|
|
112
|
+
// required for resume on transient close.
|
|
113
|
+
if (msg.config?.id !== undefined) callbacks.onSessionReady(msg.config.id);
|
|
106
114
|
break;
|
|
107
115
|
case "input.speech.started":
|
|
108
116
|
if (!state.speechActive) {
|
|
@@ -129,20 +137,21 @@ function dispatchS2sMessage(
|
|
|
129
137
|
callbacks.onToolCall(msg.call_id, msg.name, msg.args);
|
|
130
138
|
break;
|
|
131
139
|
case "reply.done":
|
|
132
|
-
// Log every raw reply.done
|
|
133
|
-
//
|
|
134
|
-
// stalled sessions actually received reply.done for their turn.
|
|
140
|
+
// Log every raw reply.done before client-facing dedup so we can
|
|
141
|
+
// cross-check which stalled sessions actually got one.
|
|
135
142
|
ctx.log.info("S2S << reply.done", {
|
|
136
|
-
...(ctx
|
|
143
|
+
...sidFields(ctx),
|
|
137
144
|
status: msg.status ?? "completed",
|
|
138
145
|
});
|
|
139
|
-
if (msg.status === "interrupted")
|
|
140
|
-
|
|
141
|
-
} else {
|
|
142
|
-
callbacks.onReplyDone();
|
|
143
|
-
}
|
|
146
|
+
if (msg.status === "interrupted") callbacks.onCancelled();
|
|
147
|
+
else callbacks.onReplyDone();
|
|
144
148
|
break;
|
|
145
149
|
case "session.error":
|
|
150
|
+
ctx.log.warn("S2S << session.error", {
|
|
151
|
+
...sidFields(ctx),
|
|
152
|
+
code: msg.code,
|
|
153
|
+
message: msg.message,
|
|
154
|
+
});
|
|
146
155
|
if (msg.code === "session_not_found" || msg.code === "session_forbidden") {
|
|
147
156
|
callbacks.onSessionExpired();
|
|
148
157
|
} else {
|
|
@@ -234,12 +243,10 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
|
|
|
234
243
|
return;
|
|
235
244
|
}
|
|
236
245
|
const json = JSON.stringify(msg);
|
|
237
|
-
if (msg.type
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
log.info(`S2S >> ${msg.type}`);
|
|
242
|
-
}
|
|
246
|
+
if (msg.type === "session.update") {
|
|
247
|
+
log.info(`S2S >> ${msg.type}`, { payload: json });
|
|
248
|
+
} else if (msg.type !== "input.audio") {
|
|
249
|
+
log.info(`S2S >> ${msg.type}`);
|
|
243
250
|
}
|
|
244
251
|
ws.send(json);
|
|
245
252
|
}
|
|
@@ -281,42 +288,41 @@ export function connectS2s(opts: ConnectS2sOptions): Promise<S2sHandle> {
|
|
|
281
288
|
resolve(handle);
|
|
282
289
|
});
|
|
283
290
|
|
|
284
|
-
function
|
|
285
|
-
try {
|
|
286
|
-
return JSON.parse(String(data));
|
|
287
|
-
} catch {
|
|
288
|
-
log.warn("S2S << invalid JSON", { data: String(data).slice(0, 200) });
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function handleAudioFastPath(obj: { type?: unknown; data?: unknown }): boolean {
|
|
293
|
-
if (obj.type === "reply.audio" && typeof obj.data === "string") {
|
|
294
|
-
callbacks.onAudio(base64ToUint8(obj.data));
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function logIncoming(obj: { type?: unknown }): void {
|
|
291
|
+
function logIncoming(type: unknown): void {
|
|
301
292
|
// reply.audio and input.audio are ~95% of traffic — skip logging.
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
293
|
+
// reply.done and session.error get richer logs inside dispatch;
|
|
294
|
+
// skip here to avoid a duplicate line.
|
|
295
|
+
if (
|
|
296
|
+
type === "reply.audio" ||
|
|
297
|
+
type === "input.audio" ||
|
|
298
|
+
type === "reply.done" ||
|
|
299
|
+
type === "session.error"
|
|
300
|
+
) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
log.info(`S2S << ${type}`);
|
|
307
304
|
}
|
|
308
305
|
|
|
309
306
|
ws.addEventListener("message", (ev) => {
|
|
310
|
-
|
|
311
|
-
|
|
307
|
+
let raw: unknown;
|
|
308
|
+
try {
|
|
309
|
+
raw = JSON.parse(String(ev.data));
|
|
310
|
+
} catch {
|
|
311
|
+
log.warn("S2S << invalid JSON", { data: String(ev.data).slice(0, 200) });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
312
314
|
|
|
313
315
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
314
316
|
log.warn("S2S << non-object JSON message", { type: typeof raw });
|
|
315
317
|
return;
|
|
316
318
|
}
|
|
317
319
|
const obj = raw as Record<string, unknown>;
|
|
318
|
-
logIncoming(obj);
|
|
319
|
-
|
|
320
|
+
logIncoming(obj.type);
|
|
321
|
+
|
|
322
|
+
if (obj.type === "reply.audio" && typeof obj.data === "string") {
|
|
323
|
+
callbacks.onAudio(base64ToUint8(obj.data));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
320
326
|
|
|
321
327
|
const parsed = parseS2sMessage(obj);
|
|
322
328
|
if (!parsed) {
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
-
/**
|
|
3
|
-
* Tests for server shutdown timeout behavior.
|
|
4
|
-
*
|
|
5
|
-
* Creates a mock runtime with controlled shutdown behavior, then exercises
|
|
6
|
-
* the timeout and graceful paths in close().
|
|
7
|
-
*/
|
|
8
2
|
|
|
9
3
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
10
4
|
import { silentLogger } from "./_test-utils.ts";
|
|
@@ -24,35 +18,25 @@ function createMockRuntime(): Runtime {
|
|
|
24
18
|
};
|
|
25
19
|
}
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
async function startServer(): Promise<ReturnType<typeof createServer>> {
|
|
22
|
+
const server = createServer({ runtime: createMockRuntime(), logger: silentLogger });
|
|
23
|
+
await server.listen(0);
|
|
24
|
+
return server;
|
|
25
|
+
}
|
|
29
26
|
|
|
27
|
+
describe("server shutdown timeout", () => {
|
|
30
28
|
afterEach(() => {
|
|
31
29
|
mockShutdown = vi.fn<() => Promise<void>>().mockResolvedValue(undefined);
|
|
32
|
-
server = null;
|
|
33
30
|
});
|
|
34
31
|
|
|
35
32
|
test("close calls runtime.shutdown()", async () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
server = createServer({
|
|
39
|
-
runtime: createMockRuntime(),
|
|
40
|
-
logger: silentLogger,
|
|
41
|
-
});
|
|
42
|
-
await server.listen(0);
|
|
43
|
-
|
|
33
|
+
const server = await startServer();
|
|
44
34
|
await server.close();
|
|
45
35
|
expect(mockShutdown).toHaveBeenCalledOnce();
|
|
46
36
|
}, 10_000);
|
|
47
37
|
|
|
48
38
|
test("close resolves quickly when runtime.shutdown() resolves", async () => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
server = createServer({
|
|
52
|
-
runtime: createMockRuntime(),
|
|
53
|
-
logger: silentLogger,
|
|
54
|
-
});
|
|
55
|
-
await server.listen(0);
|
|
39
|
+
const server = await startServer();
|
|
56
40
|
|
|
57
41
|
const start = Date.now();
|
|
58
42
|
await server.close();
|
|
@@ -65,12 +49,7 @@ describe("server shutdown timeout", () => {
|
|
|
65
49
|
test("close propagates when runtime.shutdown() rejects", async () => {
|
|
66
50
|
mockShutdown = vi.fn<() => Promise<void>>().mockRejectedValue(new Error("boom"));
|
|
67
51
|
|
|
68
|
-
server =
|
|
69
|
-
runtime: createMockRuntime(),
|
|
70
|
-
logger: silentLogger,
|
|
71
|
-
});
|
|
72
|
-
await server.listen(0);
|
|
73
|
-
|
|
52
|
+
const server = await startServer();
|
|
74
53
|
await expect(server.close()).rejects.toThrow("boom");
|
|
75
54
|
}, 10_000);
|
|
76
55
|
});
|
package/host/server.test.ts
CHANGED
|
@@ -36,16 +36,12 @@ describe("createServer", () => {
|
|
|
36
36
|
const { runtime } = makeRuntime({ name: "health-agent" });
|
|
37
37
|
server = createServer({ runtime, name: "health-agent", logger: silentLogger });
|
|
38
38
|
await server.listen(0);
|
|
39
|
-
await server.close();
|
|
40
|
-
server = null;
|
|
41
39
|
});
|
|
42
40
|
|
|
43
41
|
test("listen and close lifecycle works", async () => {
|
|
44
42
|
const { runtime } = makeRuntime();
|
|
45
43
|
server = createServer({ runtime, logger: silentLogger });
|
|
46
44
|
await server.listen(0);
|
|
47
|
-
await server.close();
|
|
48
|
-
server = null;
|
|
49
45
|
});
|
|
50
46
|
|
|
51
47
|
test("/ returns default HTML with escaped agent name", async () => {
|
|
@@ -77,11 +73,7 @@ describe("createServer", () => {
|
|
|
77
73
|
|
|
78
74
|
test("/health returns JSON with agent name", async () => {
|
|
79
75
|
const { runtime } = makeRuntime({ name: "my-agent" });
|
|
80
|
-
server = createServer({
|
|
81
|
-
runtime,
|
|
82
|
-
name: "my-agent",
|
|
83
|
-
logger: silentLogger,
|
|
84
|
-
});
|
|
76
|
+
server = createServer({ runtime, name: "my-agent", logger: silentLogger });
|
|
85
77
|
await server.listen(0);
|
|
86
78
|
|
|
87
79
|
const res = await fetch(`http://localhost:${server.port}/health`);
|
|
@@ -91,10 +83,7 @@ describe("createServer", () => {
|
|
|
91
83
|
|
|
92
84
|
test("404 triggers error-level logging", async () => {
|
|
93
85
|
const { runtime } = makeRuntime();
|
|
94
|
-
server = createServer({
|
|
95
|
-
runtime,
|
|
96
|
-
logger: silentLogger,
|
|
97
|
-
});
|
|
86
|
+
server = createServer({ runtime, logger: silentLogger });
|
|
98
87
|
await server.listen(0);
|
|
99
88
|
|
|
100
89
|
await fetch(`http://localhost:${server.port}/nonexistent-path`);
|
package/host/server.ts
CHANGED
|
@@ -51,7 +51,12 @@ export type AgentServer = {
|
|
|
51
51
|
port: number | undefined;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
const JSON_HEADERS = { "Content-Type": "application/json" } as const;
|
|
55
|
+
|
|
56
|
+
function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
|
|
57
|
+
res.writeHead(status, JSON_HEADERS);
|
|
58
|
+
res.end(JSON.stringify(body));
|
|
59
|
+
}
|
|
55
60
|
|
|
56
61
|
async function serveStatic(
|
|
57
62
|
dir: string,
|
|
@@ -61,8 +66,8 @@ async function serveStatic(
|
|
|
61
66
|
const url = req.url?.split("?")[0] ?? "/";
|
|
62
67
|
const filePath = path.join(dir, url === "/" ? "index.html" : url);
|
|
63
68
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
69
|
+
// Use resolved dir + separator to avoid prefix collisions
|
|
70
|
+
// (e.g. dir="/app/static" matching "/app/static-secrets/…").
|
|
66
71
|
const resolved = path.resolve(dir);
|
|
67
72
|
if (!filePath.startsWith(resolved + path.sep) && filePath !== resolved) return false;
|
|
68
73
|
|
|
@@ -79,77 +84,72 @@ async function serveStatic(
|
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
async function readBody(req: http.IncomingMessage): Promise<string> {
|
|
88
|
+
let body = "";
|
|
89
|
+
for await (const chunk of req) body += chunk;
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
83
92
|
|
|
84
|
-
function handleVectorPost(
|
|
93
|
+
async function handleVectorPost(
|
|
85
94
|
vector: Vector,
|
|
86
95
|
req: http.IncomingMessage,
|
|
87
96
|
res: http.ServerResponse,
|
|
88
|
-
): void {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
case "delete":
|
|
116
|
-
await vector.delete(op.ids);
|
|
117
|
-
result = "OK";
|
|
118
|
-
break;
|
|
119
|
-
default:
|
|
120
|
-
break;
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = VectorRequestSchema.safeParse(JSON.parse(await readBody(req)));
|
|
100
|
+
if (!parsed.success) {
|
|
101
|
+
sendJson(res, 400, { error: parsed.error.message });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const op = parsed.data;
|
|
105
|
+
let result: unknown;
|
|
106
|
+
switch (op.op) {
|
|
107
|
+
case "upsert":
|
|
108
|
+
await vector.upsert(op.id, op.text, op.metadata);
|
|
109
|
+
result = "OK";
|
|
110
|
+
break;
|
|
111
|
+
case "query":
|
|
112
|
+
result = await vector.query(op.text, {
|
|
113
|
+
...(op.topK !== undefined ? { topK: op.topK } : {}),
|
|
114
|
+
...(op.filter !== undefined ? { filter: op.filter } : {}),
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
case "delete":
|
|
118
|
+
await vector.delete(op.ids);
|
|
119
|
+
result = "OK";
|
|
120
|
+
break;
|
|
121
|
+
default: {
|
|
122
|
+
const _exhaustive: never = op;
|
|
123
|
+
return _exhaustive;
|
|
121
124
|
}
|
|
122
|
-
res.statusCode = 200;
|
|
123
|
-
res.end(JSON.stringify({ result }));
|
|
124
|
-
} catch (err) {
|
|
125
|
-
res.statusCode = 500;
|
|
126
|
-
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
127
125
|
}
|
|
128
|
-
|
|
126
|
+
sendJson(res, 200, { result });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
129
|
+
}
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
function handleKvGet(
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
async function handleKvGet(
|
|
133
|
+
kv: Kv,
|
|
134
|
+
req: http.IncomingMessage,
|
|
135
|
+
res: http.ServerResponse,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
const key = new URL(req.url ?? "/", "http://localhost").searchParams.get("key");
|
|
134
138
|
if (!key) {
|
|
135
|
-
res
|
|
136
|
-
res.end(JSON.stringify({ error: "Missing key query parameter" }));
|
|
139
|
+
sendJson(res, 400, { error: "Missing key query parameter" });
|
|
137
140
|
return;
|
|
138
141
|
}
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
151
|
-
res.end(JSON.stringify({ error: "KV error" }));
|
|
152
|
-
});
|
|
142
|
+
try {
|
|
143
|
+
const value = await kv.get(key);
|
|
144
|
+
if (value === null) {
|
|
145
|
+
res.writeHead(404, JSON_HEADERS);
|
|
146
|
+
res.end("null");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
sendJson(res, 200, value);
|
|
150
|
+
} catch {
|
|
151
|
+
sendJson(res, 500, { error: "KV error" });
|
|
152
|
+
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
/**
|
|
@@ -165,67 +165,52 @@ export function createServer(options: ServerOptions): AgentServer {
|
|
|
165
165
|
throw new Error("clientHtml and clientDir are mutually exclusive");
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// Pre-compute the default HTML page once (the agent name never changes).
|
|
169
|
-
const escapedName = escapeHtml(name);
|
|
170
168
|
const defaultHtml =
|
|
171
169
|
clientHtml ??
|
|
172
|
-
`<!DOCTYPE html><html><body><h1>${
|
|
170
|
+
`<!DOCTYPE html><html><body><h1>${escapeHtml(name)}</h1><p>Agent server running.</p></body></html>`;
|
|
171
|
+
|
|
172
|
+
async function handleRequest(
|
|
173
|
+
req: http.IncomingMessage,
|
|
174
|
+
res: http.ServerResponse,
|
|
175
|
+
url: string,
|
|
176
|
+
method: string,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
if (clientDir && (await serveStatic(clientDir, req, res))) return;
|
|
179
|
+
|
|
180
|
+
if (method === "GET" && url === "/") {
|
|
181
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
182
|
+
res.end(defaultHtml);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
logger.error(`${method} ${url} 404`);
|
|
187
|
+
sendJson(res, 404, { error: "Not found" });
|
|
188
|
+
}
|
|
173
189
|
|
|
174
190
|
const httpServer = http.createServer((req, res) => {
|
|
175
191
|
const url = req.url?.split("?")[0] ?? "/";
|
|
176
192
|
const method = req.method ?? "GET";
|
|
177
193
|
|
|
178
|
-
// Security headers
|
|
179
194
|
res.setHeader("Content-Security-Policy", AGENT_CSP);
|
|
180
195
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
181
196
|
res.setHeader("X-Frame-Options", "SAMEORIGIN");
|
|
182
197
|
|
|
183
|
-
// Health endpoint
|
|
184
198
|
if (method === "GET" && url === "/health") {
|
|
185
|
-
res
|
|
186
|
-
res.end(JSON.stringify({ status: "ok", name }));
|
|
199
|
+
sendJson(res, 200, { status: "ok", name });
|
|
187
200
|
return;
|
|
188
201
|
}
|
|
189
|
-
|
|
190
|
-
// KV endpoint
|
|
191
202
|
if (kv && method === "GET" && url === "/kv") {
|
|
192
|
-
handleKvGet(kv, req, res);
|
|
203
|
+
void handleKvGet(kv, req, res);
|
|
193
204
|
return;
|
|
194
205
|
}
|
|
195
|
-
|
|
196
|
-
// Vector endpoint
|
|
197
206
|
if (vector && method === "POST" && url === "/vector") {
|
|
198
|
-
handleVectorPost(vector, req, res);
|
|
207
|
+
void handleVectorPost(vector, req, res);
|
|
199
208
|
return;
|
|
200
209
|
}
|
|
201
210
|
|
|
202
|
-
// Routes that may need async handling
|
|
203
211
|
void handleRequest(req, res, url, method);
|
|
204
212
|
});
|
|
205
213
|
|
|
206
|
-
async function handleRequest(
|
|
207
|
-
req: http.IncomingMessage,
|
|
208
|
-
res: http.ServerResponse,
|
|
209
|
-
url: string,
|
|
210
|
-
method: string,
|
|
211
|
-
): Promise<void> {
|
|
212
|
-
// Static files from client dir
|
|
213
|
-
if (clientDir && (await serveStatic(clientDir, req, res))) return;
|
|
214
|
-
|
|
215
|
-
// Default HTML
|
|
216
|
-
if (method === "GET" && url === "/") {
|
|
217
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
218
|
-
res.end(defaultHtml);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 404
|
|
223
|
-
logger.error(`${method} ${url} 404`);
|
|
224
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
225
|
-
res.end(JSON.stringify({ error: "Not found" }));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// WebSocket upgrade via ws
|
|
229
214
|
const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_PAYLOAD_BYTES });
|
|
230
215
|
|
|
231
216
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import type { AgentConfig } from "../sdk/_internal-types.ts";
|
|
2
3
|
import type { ClientEvent, ClientSink } from "../sdk/protocol.ts";
|
|
3
4
|
import { DEFAULT_SYSTEM_PROMPT } from "../sdk/types.ts";
|
|
4
5
|
import { flush } from "./_test-utils.ts";
|
|
@@ -9,7 +10,7 @@ import type { Transport } from "./transports/types.ts";
|
|
|
9
10
|
function makeSink(): {
|
|
10
11
|
events: ClientEvent[];
|
|
11
12
|
audioChunks: Uint8Array[];
|
|
12
|
-
audioDoneCount: number;
|
|
13
|
+
readonly audioDoneCount: number;
|
|
13
14
|
sink: ClientSink;
|
|
14
15
|
} {
|
|
15
16
|
const events: ClientEvent[] = [];
|
|
@@ -22,9 +23,7 @@ function makeSink(): {
|
|
|
22
23
|
return audioDoneCount;
|
|
23
24
|
},
|
|
24
25
|
sink: {
|
|
25
|
-
|
|
26
|
-
return true;
|
|
27
|
-
},
|
|
26
|
+
open: true,
|
|
28
27
|
event: (e) => {
|
|
29
28
|
events.push(e);
|
|
30
29
|
},
|
|
@@ -34,13 +33,13 @@ function makeSink(): {
|
|
|
34
33
|
playAudioDone: () => {
|
|
35
34
|
audioDoneCount++;
|
|
36
35
|
},
|
|
37
|
-
}
|
|
36
|
+
},
|
|
38
37
|
};
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
function makeTransport(): Transport & { starts: number; stops: number } {
|
|
42
|
-
let starts = 0
|
|
43
|
-
|
|
40
|
+
function makeTransport(): Transport & { readonly starts: number; readonly stops: number } {
|
|
41
|
+
let starts = 0;
|
|
42
|
+
let stops = 0;
|
|
44
43
|
return {
|
|
45
44
|
start: async () => {
|
|
46
45
|
starts++;
|
|
@@ -60,6 +59,10 @@ function makeTransport(): Transport & { starts: number; stops: number } {
|
|
|
60
59
|
};
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
function makeAgentConfig(overrides: Partial<AgentConfig> = {}): AgentConfig {
|
|
63
|
+
return { name: "test", systemPrompt: DEFAULT_SYSTEM_PROMPT, greeting: "", ...overrides };
|
|
64
|
+
}
|
|
65
|
+
|
|
63
66
|
function makeCore(overrides: Partial<SessionCoreOptions> = {}): {
|
|
64
67
|
core: SessionCore;
|
|
65
68
|
sink: ReturnType<typeof makeSink>;
|
|
@@ -71,7 +74,7 @@ function makeCore(overrides: Partial<SessionCoreOptions> = {}): {
|
|
|
71
74
|
id: "s-test",
|
|
72
75
|
agent: "test-agent",
|
|
73
76
|
client: sink.sink,
|
|
74
|
-
agentConfig:
|
|
77
|
+
agentConfig: makeAgentConfig(),
|
|
75
78
|
executeTool: vi.fn(async () => "ok"),
|
|
76
79
|
transport,
|
|
77
80
|
...overrides,
|
|
@@ -98,12 +101,7 @@ describe("createSessionCore — lifecycle", () => {
|
|
|
98
101
|
vi.useFakeTimers();
|
|
99
102
|
try {
|
|
100
103
|
const { core, sink } = makeCore({
|
|
101
|
-
agentConfig: {
|
|
102
|
-
name: "test",
|
|
103
|
-
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
104
|
-
greeting: "",
|
|
105
|
-
idleTimeoutMs: 1000,
|
|
106
|
-
} as unknown as SessionCoreOptions["agentConfig"],
|
|
104
|
+
agentConfig: makeAgentConfig({ idleTimeoutMs: 1000 }),
|
|
107
105
|
});
|
|
108
106
|
await core.start();
|
|
109
107
|
await core.stop();
|
|
@@ -190,10 +188,8 @@ describe("createSessionCore — tool call pending results", () => {
|
|
|
190
188
|
await core.start();
|
|
191
189
|
core.onReplyStarted("r1");
|
|
192
190
|
core.onToolCall("cid", "my_tool", {});
|
|
193
|
-
// Let the async tool IIFE settle and push to pendingTools
|
|
194
191
|
await flush();
|
|
195
192
|
core.onReplyDone();
|
|
196
|
-
// Poll until tool results are forwarded and toolCallDone fires
|
|
197
193
|
await vi.waitFor(() =>
|
|
198
194
|
expect(transport.sendToolResult).toHaveBeenCalledWith("cid", "tool-output"),
|
|
199
195
|
);
|
|
@@ -206,12 +202,7 @@ describe("createSessionCore — idle timeout", () => {
|
|
|
206
202
|
vi.useFakeTimers();
|
|
207
203
|
try {
|
|
208
204
|
const { core, sink } = makeCore({
|
|
209
|
-
agentConfig: {
|
|
210
|
-
name: "t",
|
|
211
|
-
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
212
|
-
greeting: "",
|
|
213
|
-
idleTimeoutMs: 1000,
|
|
214
|
-
} as unknown as SessionCoreOptions["agentConfig"],
|
|
205
|
+
agentConfig: makeAgentConfig({ name: "t", idleTimeoutMs: 1000 }),
|
|
215
206
|
});
|
|
216
207
|
await core.start();
|
|
217
208
|
expect(sink.events.filter((e) => e.type === "idle_timeout")).toHaveLength(0);
|
|
@@ -225,12 +216,7 @@ describe("createSessionCore — idle timeout", () => {
|
|
|
225
216
|
vi.useFakeTimers();
|
|
226
217
|
try {
|
|
227
218
|
const { core, sink } = makeCore({
|
|
228
|
-
agentConfig: {
|
|
229
|
-
name: "t",
|
|
230
|
-
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
|
231
|
-
greeting: "",
|
|
232
|
-
idleTimeoutMs: 1000,
|
|
233
|
-
} as unknown as SessionCoreOptions["agentConfig"],
|
|
219
|
+
agentConfig: makeAgentConfig({ name: "t", idleTimeoutMs: 1000 }),
|
|
234
220
|
});
|
|
235
221
|
await core.start();
|
|
236
222
|
vi.advanceTimersByTime(500);
|
|
@@ -251,7 +237,6 @@ describe("createSessionCore — history", () => {
|
|
|
251
237
|
await core.start();
|
|
252
238
|
core.onHistory([{ role: "user", content: "prior" }]);
|
|
253
239
|
core.onUserTranscript("now");
|
|
254
|
-
// No direct introspection — but onReset clears history and replay should see no effect on subsequent behavior.
|
|
255
240
|
core.onReset();
|
|
256
241
|
});
|
|
257
242
|
});
|