@alexkroman1/aai 1.7.0 → 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 +16 -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 +628 -642
- 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 +56 -53
- 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
|
@@ -17,10 +17,9 @@ function toolError(message) {
|
|
|
17
17
|
//#region sdk/ws-upgrade.ts
|
|
18
18
|
/** Parse WebSocket upgrade query params into session start options. */
|
|
19
19
|
function parseWsUpgradeParams(rawUrl) {
|
|
20
|
-
const
|
|
21
|
-
const params = new URLSearchParams(search);
|
|
20
|
+
const params = new URLSearchParams(rawUrl.split("?")[1] ?? "");
|
|
22
21
|
const resumeFrom = params.get("sessionId") ?? void 0;
|
|
23
|
-
const skipGreeting = params.has("resume")
|
|
22
|
+
const skipGreeting = resumeFrom !== void 0 || params.has("resume");
|
|
24
23
|
return resumeFrom !== void 0 ? {
|
|
25
24
|
resumeFrom,
|
|
26
25
|
skipGreeting
|
package/host/_base64.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
|
|
3
|
+
export function uint8ToBase64(bytes: Uint8Array): string {
|
|
4
|
+
return Buffer.from(bytes).toString("base64");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function base64ToUint8(base64: string): Uint8Array {
|
|
8
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
9
|
+
}
|
package/host/_mock-ws.ts
CHANGED
|
@@ -1,46 +1,16 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* A mock WebSocket implementation for testing.
|
|
5
|
-
*
|
|
6
|
-
* Extends `EventTarget` to simulate WebSocket behavior without a real
|
|
7
|
-
* network connection. Records all sent messages in the {@link sent}
|
|
8
|
-
* array and provides helper methods to simulate incoming messages,
|
|
9
|
-
* connection events, and errors.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* const ws = new MockWebSocket("wss://example.com");
|
|
14
|
-
* ws.send(JSON.stringify({ type: "ping" }));
|
|
15
|
-
* ws.simulateMessage(JSON.stringify({ type: "pong" }));
|
|
16
|
-
* assertEquals(ws.sentJson(), [{ type: "ping" }]);
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
3
|
export class MockWebSocket extends EventTarget {
|
|
20
|
-
// mirrors the WebSocket API
|
|
21
4
|
static readonly CONNECTING = 0;
|
|
22
|
-
// mirrors the WebSocket API
|
|
23
5
|
static readonly OPEN = 1;
|
|
24
|
-
// mirrors the WebSocket API
|
|
25
6
|
static readonly CLOSING = 2;
|
|
26
|
-
// mirrors the WebSocket API
|
|
27
7
|
static readonly CLOSED = 3;
|
|
28
8
|
|
|
29
9
|
readyState = MockWebSocket.CONNECTING;
|
|
30
10
|
binaryType = "arraybuffer";
|
|
31
|
-
/** All messages passed to {@link send}, in order. */
|
|
32
11
|
sent: (string | ArrayBuffer | Uint8Array)[] = [];
|
|
33
12
|
url: string;
|
|
34
13
|
|
|
35
|
-
/**
|
|
36
|
-
* Create a new MockWebSocket.
|
|
37
|
-
*
|
|
38
|
-
* Automatically transitions to `OPEN` state on the next microtask,
|
|
39
|
-
* dispatching an `"open"` event.
|
|
40
|
-
*
|
|
41
|
-
* @param url - The WebSocket URL.
|
|
42
|
-
* @param _protocols - Ignored; accepted for API compatibility.
|
|
43
|
-
*/
|
|
44
14
|
constructor(url: string | URL, _protocols?: string | string[] | Record<string, unknown>) {
|
|
45
15
|
super();
|
|
46
16
|
this.url = typeof url === "string" ? url : url.toString();
|
|
@@ -64,71 +34,36 @@ export class MockWebSocket extends EventTarget {
|
|
|
64
34
|
super.addEventListener(type, listener);
|
|
65
35
|
}
|
|
66
36
|
|
|
67
|
-
/**
|
|
68
|
-
* Record a sent message without transmitting it.
|
|
69
|
-
*
|
|
70
|
-
* @param data - The message data to record.
|
|
71
|
-
*/
|
|
72
37
|
send(data: string | ArrayBuffer | Uint8Array) {
|
|
73
38
|
this.sent.push(data);
|
|
74
39
|
}
|
|
75
40
|
|
|
76
|
-
/**
|
|
77
|
-
* Transition to `CLOSED` state and dispatch a `"close"` event.
|
|
78
|
-
*
|
|
79
|
-
* @param code - The close code (defaults to 1000).
|
|
80
|
-
* @param _reason - Ignored; accepted for API compatibility.
|
|
81
|
-
*/
|
|
82
41
|
close(code?: number, _reason?: string) {
|
|
83
42
|
this.readyState = MockWebSocket.CLOSED;
|
|
84
43
|
this.dispatchEvent(Object.assign(new Event("close"), { code: code ?? 1000 }));
|
|
85
44
|
}
|
|
86
45
|
|
|
87
|
-
/**
|
|
88
|
-
* Simulate receiving a message from the server.
|
|
89
|
-
*
|
|
90
|
-
* @param data - The message data (string or binary).
|
|
91
|
-
*/
|
|
92
46
|
simulateMessage(data: string | ArrayBuffer) {
|
|
93
47
|
this.dispatchEvent(new MessageEvent("message", { data }));
|
|
94
48
|
}
|
|
95
49
|
|
|
96
|
-
/** Transition to `OPEN` state and dispatch an `"open"` event. */
|
|
97
50
|
open() {
|
|
98
51
|
this.readyState = MockWebSocket.OPEN;
|
|
99
52
|
this.dispatchEvent(new Event("open"));
|
|
100
53
|
}
|
|
101
54
|
|
|
102
|
-
/**
|
|
103
|
-
* Shorthand for {@link simulateMessage}.
|
|
104
|
-
*
|
|
105
|
-
* @param data - The message data to dispatch.
|
|
106
|
-
*/
|
|
107
55
|
msg(data: string | ArrayBuffer) {
|
|
108
56
|
this.simulateMessage(data);
|
|
109
57
|
}
|
|
110
58
|
|
|
111
|
-
/**
|
|
112
|
-
* Simulate a connection close from the server.
|
|
113
|
-
*
|
|
114
|
-
* @param code - The close code (defaults to 1000).
|
|
115
|
-
*/
|
|
116
59
|
disconnect(code = 1000) {
|
|
117
60
|
this.dispatchEvent(Object.assign(new Event("close"), { code }));
|
|
118
61
|
}
|
|
119
62
|
|
|
120
|
-
/** Dispatch an `"error"` event on this socket. */
|
|
121
63
|
error() {
|
|
122
64
|
this.dispatchEvent(new Event("error"));
|
|
123
65
|
}
|
|
124
66
|
|
|
125
|
-
/**
|
|
126
|
-
* Return all sent string messages parsed as JSON objects.
|
|
127
|
-
*
|
|
128
|
-
* Binary messages are filtered out.
|
|
129
|
-
*
|
|
130
|
-
* @returns An array of parsed JSON objects from sent string messages.
|
|
131
|
-
*/
|
|
132
67
|
sentJson(): Record<string, unknown>[] {
|
|
133
68
|
return this.sent.filter((d): d is string => typeof d === "string").map((s) => JSON.parse(s));
|
|
134
69
|
}
|
|
@@ -28,8 +28,14 @@ import type {
|
|
|
28
28
|
TtsSession,
|
|
29
29
|
} from "../sdk/providers.ts";
|
|
30
30
|
|
|
31
|
+
function makeCodedError<C extends string>(code: C, message: string): Error & { code: C } {
|
|
32
|
+
return Object.assign(new Error(message), { code });
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
// ─── Fake STT ───────────────────────────────────────────────────────────────
|
|
32
36
|
|
|
37
|
+
type SttErrorCode = "stt_stream_error" | "stt_connect_failed" | "stt_auth_failed";
|
|
38
|
+
|
|
33
39
|
export type FakeSttSession = SttSession & {
|
|
34
40
|
readonly emitter: Emitter<SttEvents>;
|
|
35
41
|
readonly opts: SttOpenOptions;
|
|
@@ -37,10 +43,7 @@ export type FakeSttSession = SttSession & {
|
|
|
37
43
|
readonly closed: { value: boolean };
|
|
38
44
|
firePartial(text: string): void;
|
|
39
45
|
fireFinal(text: string): void;
|
|
40
|
-
fireError(
|
|
41
|
-
code: "stt_stream_error" | "stt_connect_failed" | "stt_auth_failed",
|
|
42
|
-
message: string,
|
|
43
|
-
): void;
|
|
46
|
+
fireError(code: SttErrorCode, message: string): void;
|
|
44
47
|
};
|
|
45
48
|
|
|
46
49
|
export type FakeSttProvider = SttOpener & {
|
|
@@ -71,17 +74,14 @@ export function createFakeSttProvider(): FakeSttProvider {
|
|
|
71
74
|
close: vi.fn(async () => {
|
|
72
75
|
closed.value = true;
|
|
73
76
|
}),
|
|
74
|
-
firePartial(text
|
|
77
|
+
firePartial(text) {
|
|
75
78
|
emitter.emit("partial", text);
|
|
76
79
|
},
|
|
77
|
-
fireFinal(text
|
|
80
|
+
fireFinal(text) {
|
|
78
81
|
emitter.emit("final", text);
|
|
79
82
|
},
|
|
80
83
|
fireError(code, message) {
|
|
81
|
-
|
|
82
|
-
SttEvents["error"]
|
|
83
|
-
>[0];
|
|
84
|
-
emitter.emit("error", err);
|
|
84
|
+
emitter.emit("error", makeCodedError(code, message) as Parameters<SttEvents["error"]>[0]);
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
sessions.push(session);
|
|
@@ -92,6 +92,8 @@ export function createFakeSttProvider(): FakeSttProvider {
|
|
|
92
92
|
|
|
93
93
|
// ─── Fake TTS ───────────────────────────────────────────────────────────────
|
|
94
94
|
|
|
95
|
+
type TtsErrorCode = "tts_stream_error" | "tts_connect_failed" | "tts_auth_failed";
|
|
96
|
+
|
|
95
97
|
export type FakeTtsSession = TtsSession & {
|
|
96
98
|
readonly emitter: Emitter<TtsEvents>;
|
|
97
99
|
readonly opts: TtsOpenOptions;
|
|
@@ -101,10 +103,7 @@ export type FakeTtsSession = TtsSession & {
|
|
|
101
103
|
readonly flush: ReturnType<typeof vi.fn<() => void>>;
|
|
102
104
|
readonly cancel: ReturnType<typeof vi.fn<() => void>>;
|
|
103
105
|
fireAudio(pcm: Int16Array): void;
|
|
104
|
-
fireError(
|
|
105
|
-
code: "tts_stream_error" | "tts_connect_failed" | "tts_auth_failed",
|
|
106
|
-
message: string,
|
|
107
|
-
): void;
|
|
106
|
+
fireError(code: TtsErrorCode, message: string): void;
|
|
108
107
|
};
|
|
109
108
|
|
|
110
109
|
export type FakeTtsProvider = TtsOpener & {
|
|
@@ -152,14 +151,11 @@ export function createFakeTtsProvider(
|
|
|
152
151
|
close: vi.fn(async () => {
|
|
153
152
|
closed.value = true;
|
|
154
153
|
}),
|
|
155
|
-
fireAudio(pcm
|
|
154
|
+
fireAudio(pcm) {
|
|
156
155
|
emitter.emit("audio", pcm);
|
|
157
156
|
},
|
|
158
157
|
fireError(code, message) {
|
|
159
|
-
|
|
160
|
-
TtsEvents["error"]
|
|
161
|
-
>[0];
|
|
162
|
-
emitter.emit("error", err);
|
|
158
|
+
emitter.emit("error", makeCodedError(code, message) as Parameters<TtsEvents["error"]>[0]);
|
|
163
159
|
},
|
|
164
160
|
};
|
|
165
161
|
sessions.push(session);
|
|
@@ -172,15 +168,11 @@ export function createFakeTtsProvider(
|
|
|
172
168
|
* Fake STT provider that throws on `open()` with a given error code. Used to
|
|
173
169
|
* test atomic provider open — TTS should not be opened at all when STT fails.
|
|
174
170
|
*/
|
|
175
|
-
export function createFailingSttProvider(
|
|
176
|
-
code: "stt_connect_failed" | "stt_auth_failed" | "stt_stream_error",
|
|
177
|
-
message: string,
|
|
178
|
-
): SttOpener {
|
|
171
|
+
export function createFailingSttProvider(code: SttErrorCode, message: string): SttOpener {
|
|
179
172
|
return {
|
|
180
173
|
name: "failing-stt",
|
|
181
174
|
async open(): Promise<SttSession> {
|
|
182
|
-
|
|
183
|
-
throw err;
|
|
175
|
+
throw makeCodedError(code, message);
|
|
184
176
|
},
|
|
185
177
|
};
|
|
186
178
|
}
|
|
@@ -189,15 +181,11 @@ export function createFailingSttProvider(
|
|
|
189
181
|
* Fake TTS provider that throws on `open()` with a given error code. Used to
|
|
190
182
|
* test atomic provider open — STT should be closed when TTS fails.
|
|
191
183
|
*/
|
|
192
|
-
export function createFailingTtsProvider(
|
|
193
|
-
code: "tts_connect_failed" | "tts_auth_failed" | "tts_stream_error",
|
|
194
|
-
message: string,
|
|
195
|
-
): TtsOpener {
|
|
184
|
+
export function createFailingTtsProvider(code: TtsErrorCode, message: string): TtsOpener {
|
|
196
185
|
return {
|
|
197
186
|
name: "failing-tts",
|
|
198
187
|
async open(): Promise<TtsSession> {
|
|
199
|
-
|
|
200
|
-
throw err;
|
|
188
|
+
throw makeCodedError(code, message);
|
|
201
189
|
},
|
|
202
190
|
};
|
|
203
191
|
}
|
package/host/_run-code.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
-
/**
|
|
3
|
-
* run_code built-in tool — executes user JavaScript in a fresh `node:vm`
|
|
4
|
-
* context with no network, filesystem, or process access.
|
|
5
|
-
*/
|
|
6
2
|
|
|
7
3
|
import vm from "node:vm";
|
|
8
4
|
import { z } from "zod";
|
|
@@ -12,10 +8,6 @@ import { errorMessage } from "../sdk/utils.ts";
|
|
|
12
8
|
|
|
13
9
|
const SKIPPED_CLASS_KEYS = new Set(["constructor", "prototype", "length", "name"]);
|
|
14
10
|
|
|
15
|
-
/**
|
|
16
|
-
* Copy static members from a class constructor to a wrapper function,
|
|
17
|
-
* skipping built-in keys that must not be forwarded.
|
|
18
|
-
*/
|
|
19
11
|
// biome-ignore lint/complexity/noBannedTypes: copying descriptors from arbitrary class constructors
|
|
20
12
|
function copyStaticMembers(src: Function, dst: Function): void {
|
|
21
13
|
for (const key of Object.getOwnPropertyNames(src)) {
|
|
@@ -24,22 +16,16 @@ function copyStaticMembers(src: Function, dst: Function): void {
|
|
|
24
16
|
const desc = Object.getOwnPropertyDescriptor(src, key);
|
|
25
17
|
if (desc) Object.defineProperty(dst, key, desc);
|
|
26
18
|
} catch {
|
|
27
|
-
// Skip non-configurable properties
|
|
19
|
+
// Skip non-configurable properties.
|
|
28
20
|
}
|
|
29
21
|
}
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* For class constructors: additionally copies static methods and neutralizes
|
|
39
|
-
* `prototype.constructor` so instances created via `new` also cannot escape.
|
|
40
|
-
*
|
|
41
|
-
* This prevents sandbox code from reaching the host `Function` constructor
|
|
42
|
-
* via patterns like `fn.constructor.constructor('return process')()`.
|
|
25
|
+
* Prevents sandbox code from reaching the host `Function` constructor via
|
|
26
|
+
* `fn.constructor.constructor('return process')()`. For class constructors
|
|
27
|
+
* we also copy static members and neuter `prototype.constructor` so
|
|
28
|
+
* instances created via `new` cannot escape either.
|
|
43
29
|
*/
|
|
44
30
|
// biome-ignore lint/complexity/noBannedTypes: wrapping arbitrary functions and class constructors
|
|
45
31
|
function neutralizeConstructor<T extends Function>(fn: T): T {
|
|
@@ -54,7 +40,6 @@ function neutralizeConstructor<T extends Function>(fn: T): T {
|
|
|
54
40
|
|
|
55
41
|
if (hasPrototype) {
|
|
56
42
|
copyStaticMembers(fn, Wrapper);
|
|
57
|
-
// Neuter prototype.constructor so instances can't escape either.
|
|
58
43
|
if (Wrapper.prototype) {
|
|
59
44
|
Object.defineProperty(Wrapper.prototype, "constructor", {
|
|
60
45
|
value: undefined,
|
|
@@ -77,19 +62,6 @@ const runCodeParams = z.object({
|
|
|
77
62
|
code: z.string().describe("JavaScript code to execute. Use console.log() for output."),
|
|
78
63
|
});
|
|
79
64
|
|
|
80
|
-
/**
|
|
81
|
-
* Execute JavaScript code inside a fresh `node:vm` context.
|
|
82
|
-
*
|
|
83
|
-
* Each invocation creates a disposable VM context with:
|
|
84
|
-
* - No filesystem access (`node:fs` and other built-ins unavailable)
|
|
85
|
-
* - No network access (`fetch`, `http` unavailable)
|
|
86
|
-
* - No child process spawning
|
|
87
|
-
* - No environment variable access (`process` unavailable)
|
|
88
|
-
* - Execution timeout (default 5 s)
|
|
89
|
-
*
|
|
90
|
-
* The context is discarded after execution, so no state leaks between
|
|
91
|
-
* invocations or across sessions.
|
|
92
|
-
*/
|
|
93
65
|
export function createRunCode(): ToolDef<typeof runCodeParams> & { guidance: string } {
|
|
94
66
|
return {
|
|
95
67
|
guidance:
|
|
@@ -105,19 +77,11 @@ export function createRunCode(): ToolDef<typeof runCodeParams> & { guidance: str
|
|
|
105
77
|
};
|
|
106
78
|
}
|
|
107
79
|
|
|
108
|
-
/**
|
|
109
|
-
* Execute user code in a fresh `node:vm` context.
|
|
110
|
-
*
|
|
111
|
-
* @remarks
|
|
112
|
-
* The VM context only exposes standard ECMAScript globals and a console
|
|
113
|
-
* object that captures output. Node.js APIs (`process`, `require`,
|
|
114
|
-
* `import()`) are not available inside the sandbox.
|
|
115
|
-
*/
|
|
116
80
|
export async function executeInIsolate(code: string): Promise<string | { error: string }> {
|
|
117
81
|
const output: string[] = [];
|
|
118
82
|
const capture = (...args: unknown[]) => output.push(args.map(String).join(" "));
|
|
119
83
|
|
|
120
|
-
//
|
|
84
|
+
// Tracked so timers can't fire into the host loop after execution ends.
|
|
121
85
|
const activeTimers = new Set<ReturnType<typeof setTimeout>>();
|
|
122
86
|
|
|
123
87
|
const sandboxSetTimeout = (
|
|
@@ -161,9 +125,10 @@ export async function executeInIsolate(code: string): Promise<string | { error:
|
|
|
161
125
|
}
|
|
162
126
|
};
|
|
163
127
|
|
|
128
|
+
// Every host function/class is wrapped with neutralizeConstructor to block
|
|
129
|
+
// the `fn.constructor.constructor('return process')()` escape to host Function.
|
|
164
130
|
const context = vm.createContext(
|
|
165
131
|
{
|
|
166
|
-
// Console methods wrapped to prevent .constructor escape to host Function.
|
|
167
132
|
console: Object.freeze({
|
|
168
133
|
log: neutralizeConstructor(capture),
|
|
169
134
|
info: neutralizeConstructor(capture),
|
|
@@ -171,12 +136,10 @@ export async function executeInIsolate(code: string): Promise<string | { error:
|
|
|
171
136
|
error: neutralizeConstructor(capture),
|
|
172
137
|
debug: neutralizeConstructor(capture),
|
|
173
138
|
}),
|
|
174
|
-
// Wrapped timers — neutralized to prevent .constructor escape.
|
|
175
139
|
setTimeout: neutralizeConstructor(sandboxSetTimeout),
|
|
176
140
|
clearTimeout: neutralizeConstructor(sandboxClearTimeout),
|
|
177
141
|
setInterval: neutralizeConstructor(sandboxSetInterval),
|
|
178
142
|
clearInterval: neutralizeConstructor(sandboxClearInterval),
|
|
179
|
-
// Standard web-compat globals — constructor chain neutered.
|
|
180
143
|
URL: neutralizeConstructor(URL),
|
|
181
144
|
URLSearchParams: neutralizeConstructor(URLSearchParams),
|
|
182
145
|
TextEncoder: neutralizeConstructor(TextEncoder),
|
|
@@ -186,28 +149,22 @@ export async function executeInIsolate(code: string): Promise<string | { error:
|
|
|
186
149
|
structuredClone: neutralizeConstructor(structuredClone),
|
|
187
150
|
},
|
|
188
151
|
{
|
|
189
|
-
// Block string-based code generation within the sandbox realm.
|
|
190
152
|
codeGeneration: { strings: false, wasm: false },
|
|
191
153
|
},
|
|
192
154
|
);
|
|
193
155
|
|
|
194
156
|
try {
|
|
195
|
-
//
|
|
157
|
+
// Async IIFE so user code can use top-level `await`.
|
|
196
158
|
const wrapped = `(async () => {\n${code}\n})()`;
|
|
197
159
|
const script = new vm.Script(wrapped, { filename: "run_code.js" });
|
|
198
160
|
|
|
199
|
-
|
|
200
|
-
const result = await script.runInContext(context, { timeout: RUN_CODE_TIMEOUT_MS });
|
|
201
|
-
void result;
|
|
161
|
+
await script.runInContext(context, { timeout: RUN_CODE_TIMEOUT_MS });
|
|
202
162
|
|
|
203
163
|
const text = output.join("\n").trim();
|
|
204
164
|
return text || "Code ran successfully (no output)";
|
|
205
165
|
} catch (err: unknown) {
|
|
206
166
|
return { error: errorMessage(err) };
|
|
207
167
|
} finally {
|
|
208
|
-
// Cancel all sandbox timers that are still pending. This prevents
|
|
209
|
-
// setInterval/setTimeout callbacks from running in the host event loop
|
|
210
|
-
// after the sandbox execution has completed or timed out.
|
|
211
168
|
for (const id of activeTimers) {
|
|
212
169
|
clearTimeout(id);
|
|
213
170
|
clearInterval(id);
|
|
@@ -7,27 +7,6 @@
|
|
|
7
7
|
* reusable test suite that can be wired to either runtime.
|
|
8
8
|
*
|
|
9
9
|
* Inspired by Nitro's `testNitro()` pattern: one test fixture, many runtimes.
|
|
10
|
-
*
|
|
11
|
-
* @example Direct executor (unit test)
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { testRuntime } from "./_runtime-conformance.ts";
|
|
14
|
-
*
|
|
15
|
-
* testRuntime("direct", () => {
|
|
16
|
-
* const exec = createRuntime({ agent: CONFORMANCE_AGENT, env: { MY_VAR: "test-value" } });
|
|
17
|
-
* return { executeTool: exec.executeTool, hooks: exec.hooks };
|
|
18
|
-
* });
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* @example Sandbox (integration test in aai-server)
|
|
22
|
-
* ```ts
|
|
23
|
-
* // Internal module — import the .ts source directly from this package.
|
|
24
|
-
* import { testRuntime } from "../../aai/host/_runtime-conformance.ts";
|
|
25
|
-
*
|
|
26
|
-
* testRuntime("sandbox", async () => {
|
|
27
|
-
* // ... start isolate with a bundled agent
|
|
28
|
-
* return { executeTool: buildExecuteTool(...), hooks: buildHookInvoker(...) };
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
10
|
*/
|
|
32
11
|
|
|
33
12
|
import { describe, expect, test } from "vitest";
|
|
@@ -35,21 +14,10 @@ import { z } from "zod";
|
|
|
35
14
|
import type { ExecuteTool } from "../sdk/_internal-types.ts";
|
|
36
15
|
import type { AgentDef } from "../sdk/types.ts";
|
|
37
16
|
|
|
38
|
-
// ── Shared context type ────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Minimal runtime surface needed for conformance tests.
|
|
42
|
-
*
|
|
43
|
-
* Both `Runtime` and `buildExecuteTool`/`buildHookInvoker` from the
|
|
44
|
-
* sandbox produce objects that satisfy this interface.
|
|
45
|
-
*/
|
|
46
17
|
export type RuntimeTestContext = {
|
|
47
18
|
executeTool: ExecuteTool;
|
|
48
19
|
};
|
|
49
20
|
|
|
50
|
-
// ── Conformance agent ──────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
/** Agent definition used by the conformance suite (direct executor path). */
|
|
53
21
|
export const CONFORMANCE_AGENT: AgentDef = {
|
|
54
22
|
name: "conformance-test",
|
|
55
23
|
systemPrompt: "Conformance test agent.",
|
|
@@ -96,22 +64,15 @@ export const CONFORMANCE_AGENT: AgentDef = {
|
|
|
96
64
|
},
|
|
97
65
|
};
|
|
98
66
|
|
|
99
|
-
// ── Shared conformance suite ───────────────────────────────────────────────
|
|
100
|
-
|
|
101
67
|
/**
|
|
102
68
|
* Run the runtime conformance test suite against a given runtime context.
|
|
103
69
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* All tests assume the runtime was created with {@link CONFORMANCE_AGENT}
|
|
109
|
-
* (or its bundle equivalent) and `env: { MY_VAR: "test-value" }`.
|
|
70
|
+
* `getContext` is invoked once per test so callers can lazily set up the
|
|
71
|
+
* runtime in a `beforeAll`. All tests assume the runtime was created with
|
|
72
|
+
* {@link CONFORMANCE_AGENT} and `env: { MY_VAR: "test-value" }`.
|
|
110
73
|
*/
|
|
111
74
|
export function testRuntime(label: string, getContext: () => RuntimeTestContext): void {
|
|
112
75
|
describe(`runtime conformance: ${label}`, () => {
|
|
113
|
-
// ── Tool execution ───────────────────────────────────────────────
|
|
114
|
-
|
|
115
76
|
test("executes tool and returns result", async () => {
|
|
116
77
|
const { executeTool } = getContext();
|
|
117
78
|
const result = await executeTool("echo", { text: "hello" }, "s1", []);
|
|
@@ -151,8 +112,6 @@ export function testRuntime(label: string, getContext: () => RuntimeTestContext)
|
|
|
151
112
|
expect(vectorResult).toBe("conformance-input");
|
|
152
113
|
});
|
|
153
114
|
|
|
154
|
-
// ── Session state ────────────────────────────────────────────────
|
|
155
|
-
|
|
156
115
|
test("session state is initialized from factory", async () => {
|
|
157
116
|
const { executeTool } = getContext();
|
|
158
117
|
const result = await executeTool("get_state", {}, "state-init", []);
|
package/host/_test-utils.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { vi } from "vitest";
|
|
6
6
|
import type { AgentConfig } from "../sdk/_internal-types.ts";
|
|
7
|
-
import type { ClientSink } from "../sdk/protocol.ts";
|
|
7
|
+
import type { ClientEvent, ClientSink } from "../sdk/protocol.ts";
|
|
8
8
|
import type { AgentDef, ToolContext, ToolDef } from "../sdk/types.ts";
|
|
9
9
|
import { DEFAULT_SYSTEM_PROMPT } from "../sdk/types.ts";
|
|
10
10
|
import { createRuntime } from "./runtime.ts";
|
|
@@ -111,12 +111,7 @@ export function makeClientSink(overrides?: Partial<ClientSink>): ClientSink {
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export const silentLogger
|
|
115
|
-
info: (...args: unknown[]) => void;
|
|
116
|
-
warn: (...args: unknown[]) => void;
|
|
117
|
-
error: (...args: unknown[]) => void;
|
|
118
|
-
debug: (...args: unknown[]) => void;
|
|
119
|
-
} = {
|
|
114
|
+
export const silentLogger = {
|
|
120
115
|
info: vi.fn(),
|
|
121
116
|
warn: vi.fn(),
|
|
122
117
|
error: vi.fn(),
|
|
@@ -153,11 +148,11 @@ export type TrackingClientSink = ClientSink & {
|
|
|
153
148
|
userTranscripts: string[];
|
|
154
149
|
toolCallEvents: { callId: string; name: string; args: unknown }[];
|
|
155
150
|
audioChunks: Uint8Array[];
|
|
156
|
-
replyDoneCount: number;
|
|
157
|
-
cancelledCount: number;
|
|
158
|
-
speechStartedCount: number;
|
|
159
|
-
speechStoppedCount: number;
|
|
160
|
-
events:
|
|
151
|
+
readonly replyDoneCount: number;
|
|
152
|
+
readonly cancelledCount: number;
|
|
153
|
+
readonly speechStartedCount: number;
|
|
154
|
+
readonly speechStoppedCount: number;
|
|
155
|
+
events: ClientEvent[];
|
|
161
156
|
};
|
|
162
157
|
|
|
163
158
|
export function makeTrackingClient(): TrackingClientSink {
|
|
@@ -165,11 +160,13 @@ export function makeTrackingClient(): TrackingClientSink {
|
|
|
165
160
|
const userTranscripts: string[] = [];
|
|
166
161
|
const toolCallEvents: { callId: string; name: string; args: unknown }[] = [];
|
|
167
162
|
const audioChunks: Uint8Array[] = [];
|
|
168
|
-
const events:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
const events: ClientEvent[] = [];
|
|
164
|
+
|
|
165
|
+
function countByType(type: ClientEvent["type"]): number {
|
|
166
|
+
let n = 0;
|
|
167
|
+
for (const e of events) if (e.type === type) n++;
|
|
168
|
+
return n;
|
|
169
|
+
}
|
|
173
170
|
|
|
174
171
|
return {
|
|
175
172
|
open: true,
|
|
@@ -179,18 +176,18 @@ export function makeTrackingClient(): TrackingClientSink {
|
|
|
179
176
|
audioChunks,
|
|
180
177
|
events,
|
|
181
178
|
get replyDoneCount() {
|
|
182
|
-
return
|
|
179
|
+
return countByType("reply_done");
|
|
183
180
|
},
|
|
184
181
|
get cancelledCount() {
|
|
185
|
-
return
|
|
182
|
+
return countByType("cancelled");
|
|
186
183
|
},
|
|
187
184
|
get speechStartedCount() {
|
|
188
|
-
return
|
|
185
|
+
return countByType("speech_started");
|
|
189
186
|
},
|
|
190
187
|
get speechStoppedCount() {
|
|
191
|
-
return
|
|
188
|
+
return countByType("speech_stopped");
|
|
192
189
|
},
|
|
193
|
-
event: vi.fn((e:
|
|
190
|
+
event: vi.fn((e: ClientEvent) => {
|
|
194
191
|
events.push(e);
|
|
195
192
|
switch (e.type) {
|
|
196
193
|
case "agent_transcript":
|
|
@@ -202,18 +199,6 @@ export function makeTrackingClient(): TrackingClientSink {
|
|
|
202
199
|
case "tool_call":
|
|
203
200
|
toolCallEvents.push({ callId: e.toolCallId, name: e.toolName, args: e.args });
|
|
204
201
|
break;
|
|
205
|
-
case "reply_done":
|
|
206
|
-
replyDoneCount++;
|
|
207
|
-
break;
|
|
208
|
-
case "cancelled":
|
|
209
|
-
cancelledCount++;
|
|
210
|
-
break;
|
|
211
|
-
case "speech_started":
|
|
212
|
-
speechStartedCount++;
|
|
213
|
-
break;
|
|
214
|
-
case "speech_stopped":
|
|
215
|
-
speechStoppedCount++;
|
|
216
|
-
break;
|
|
217
202
|
default:
|
|
218
203
|
break;
|
|
219
204
|
}
|
|
@@ -297,14 +282,7 @@ export function createFixtureSession(
|
|
|
297
282
|
opts?: { env?: Record<string, string> },
|
|
298
283
|
) {
|
|
299
284
|
let capturedCallbacks: S2sCallbacks | null = null;
|
|
300
|
-
const fakeHandle
|
|
301
|
-
sendAudio: vi.fn(),
|
|
302
|
-
sendAudioRaw: vi.fn(),
|
|
303
|
-
sendToolResult: vi.fn(),
|
|
304
|
-
updateSession: vi.fn(),
|
|
305
|
-
resumeSession: vi.fn(),
|
|
306
|
-
close: vi.fn(),
|
|
307
|
-
};
|
|
285
|
+
const fakeHandle = makeMockHandle();
|
|
308
286
|
|
|
309
287
|
const connectSpy = vi
|
|
310
288
|
.spyOn(s2sTransportInternals, "connectS2s")
|