@alexkroman1/aai 0.3.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/LICENSE +21 -0
- package/dist/cli.js +3436 -0
- package/package.json +78 -0
- package/sdk/_internal_types.ts +89 -0
- package/sdk/_mock_ws.ts +172 -0
- package/sdk/_timeout.ts +24 -0
- package/sdk/builtin_tools.ts +309 -0
- package/sdk/capnweb.ts +341 -0
- package/sdk/define_agent.ts +70 -0
- package/sdk/direct_executor.ts +195 -0
- package/sdk/kv.ts +183 -0
- package/sdk/mod.ts +35 -0
- package/sdk/protocol.ts +313 -0
- package/sdk/runtime.ts +65 -0
- package/sdk/s2s.ts +271 -0
- package/sdk/server.ts +198 -0
- package/sdk/session.ts +438 -0
- package/sdk/system_prompt.ts +47 -0
- package/sdk/types.ts +406 -0
- package/sdk/vector.ts +133 -0
- package/sdk/winterc_server.ts +141 -0
- package/sdk/worker_entry.ts +99 -0
- package/sdk/worker_shim.ts +170 -0
- package/sdk/ws_handler.ts +190 -0
- package/templates/_shared/.env.example +5 -0
- package/templates/_shared/package.json +17 -0
- package/templates/code-interpreter/agent.ts +27 -0
- package/templates/code-interpreter/client.tsx +2 -0
- package/templates/dispatch-center/agent.ts +1536 -0
- package/templates/dispatch-center/client.tsx +504 -0
- package/templates/embedded-assets/agent.ts +49 -0
- package/templates/embedded-assets/client.tsx +2 -0
- package/templates/embedded-assets/knowledge.json +20 -0
- package/templates/health-assistant/agent.ts +160 -0
- package/templates/health-assistant/client.tsx +2 -0
- package/templates/infocom-adventure/agent.ts +164 -0
- package/templates/infocom-adventure/client.tsx +299 -0
- package/templates/math-buddy/agent.ts +21 -0
- package/templates/math-buddy/client.tsx +2 -0
- package/templates/memory-agent/agent.ts +74 -0
- package/templates/memory-agent/client.tsx +2 -0
- package/templates/night-owl/agent.ts +98 -0
- package/templates/night-owl/client.tsx +28 -0
- package/templates/personal-finance/agent.ts +26 -0
- package/templates/personal-finance/client.tsx +2 -0
- package/templates/simple/agent.ts +6 -0
- package/templates/simple/client.tsx +2 -0
- package/templates/smart-research/agent.ts +164 -0
- package/templates/smart-research/client.tsx +2 -0
- package/templates/support/README.md +62 -0
- package/templates/support/agent.ts +19 -0
- package/templates/support/client.tsx +2 -0
- package/templates/travel-concierge/agent.ts +29 -0
- package/templates/travel-concierge/client.tsx +2 -0
- package/templates/web-researcher/agent.ts +17 -0
- package/templates/web-researcher/client.tsx +2 -0
- package/ui/_components/app.tsx +37 -0
- package/ui/_components/chat_view.tsx +36 -0
- package/ui/_components/controls.tsx +32 -0
- package/ui/_components/error_banner.tsx +18 -0
- package/ui/_components/message_bubble.tsx +21 -0
- package/ui/_components/message_list.tsx +61 -0
- package/ui/_components/state_indicator.tsx +17 -0
- package/ui/_components/thinking_indicator.tsx +19 -0
- package/ui/_components/tool_call_block.tsx +110 -0
- package/ui/_components/tool_icons.tsx +101 -0
- package/ui/_components/transcript.tsx +20 -0
- package/ui/audio.ts +170 -0
- package/ui/components.ts +49 -0
- package/ui/components_mod.ts +37 -0
- package/ui/mod.ts +48 -0
- package/ui/mount.tsx +112 -0
- package/ui/mount_context.ts +19 -0
- package/ui/session.ts +456 -0
- package/ui/session_mod.ts +27 -0
- package/ui/signals.ts +111 -0
- package/ui/types.ts +50 -0
- package/ui/worklets/capture-processor.js +62 -0
- package/ui/worklets/playback-processor.js +110 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Worker entry point — shared tool execution logic.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { Kv } from "./kv.ts";
|
|
10
|
+
import type { Message, ToolContext, ToolDef } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maximum time in milliseconds a tool handler may run before being aborted.
|
|
14
|
+
*
|
|
15
|
+
* If a tool's `execute` function exceeds this duration, it is cancelled via
|
|
16
|
+
* `AbortSignal.timeout` and an error message is returned to the LLM.
|
|
17
|
+
*/
|
|
18
|
+
export const TOOL_HANDLER_TIMEOUT = 30_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Function signature for executing a tool by name.
|
|
22
|
+
*
|
|
23
|
+
* @param name - The tool name to execute.
|
|
24
|
+
* @param args - Key-value arguments to pass to the tool handler.
|
|
25
|
+
* @param sessionId - Optional session identifier for stateful tools.
|
|
26
|
+
* @param messages - Optional conversation history for context-aware tools.
|
|
27
|
+
* @returns The tool's string result, or an error message string.
|
|
28
|
+
*/
|
|
29
|
+
export type ExecuteTool = (
|
|
30
|
+
name: string,
|
|
31
|
+
args: Readonly<Record<string, unknown>>,
|
|
32
|
+
sessionId?: string,
|
|
33
|
+
messages?: readonly Message[],
|
|
34
|
+
) => Promise<string>;
|
|
35
|
+
|
|
36
|
+
/** Options for {@linkcode executeToolCall}. */
|
|
37
|
+
export type ExecuteToolCallOptions = {
|
|
38
|
+
tool: ToolDef;
|
|
39
|
+
env: Readonly<Record<string, string>>;
|
|
40
|
+
sessionId?: string | undefined;
|
|
41
|
+
state?: unknown;
|
|
42
|
+
kv?: Kv | undefined;
|
|
43
|
+
messages?: readonly Message[] | undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Execute a tool call with argument validation, timeout, and error handling.
|
|
48
|
+
*
|
|
49
|
+
* Validates the provided arguments against the tool's Zod parameter schema,
|
|
50
|
+
* constructs a {@linkcode ToolContext}, invokes the tool's `execute` function,
|
|
51
|
+
* and serializes the result to a string. Errors and timeouts are caught and
|
|
52
|
+
* returned as `"Error: ..."` strings rather than thrown.
|
|
53
|
+
*
|
|
54
|
+
* @param name - The name of the tool being invoked.
|
|
55
|
+
* @param args - Raw arguments from the LLM to validate and pass to the tool.
|
|
56
|
+
* @param options - Tool definition, environment, and optional context.
|
|
57
|
+
* @returns The tool's result serialized as a string, or an error message.
|
|
58
|
+
*/
|
|
59
|
+
export async function executeToolCall(
|
|
60
|
+
name: string,
|
|
61
|
+
args: Readonly<Record<string, unknown>>,
|
|
62
|
+
options: ExecuteToolCallOptions,
|
|
63
|
+
): Promise<string> {
|
|
64
|
+
const { tool, env, sessionId, state, kv, messages } = options;
|
|
65
|
+
const schema = tool.parameters ?? z.object({});
|
|
66
|
+
const parsed = schema.safeParse(args);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
const issues = (parsed.error?.issues ?? [])
|
|
69
|
+
.map((i: z.ZodIssue) => `${i.path.map(String).join(".")}: ${i.message}`)
|
|
70
|
+
.join(", ");
|
|
71
|
+
return `Error: Invalid arguments for tool "${name}": ${issues}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const abortSignal = AbortSignal.timeout(TOOL_HANDLER_TIMEOUT);
|
|
76
|
+
const envCopy = { ...env };
|
|
77
|
+
const ctx: ToolContext = {
|
|
78
|
+
sessionId: sessionId ?? "",
|
|
79
|
+
env: envCopy,
|
|
80
|
+
abortSignal,
|
|
81
|
+
state: (state ?? {}) as Record<string, unknown>,
|
|
82
|
+
get kv(): Kv {
|
|
83
|
+
if (!kv) throw new Error("KV not available");
|
|
84
|
+
return kv;
|
|
85
|
+
},
|
|
86
|
+
messages: messages ?? [],
|
|
87
|
+
};
|
|
88
|
+
const result = await Promise.resolve(tool.execute(parsed.data, ctx));
|
|
89
|
+
if (result == null) return "null";
|
|
90
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
93
|
+
console.warn(`[tool-executor] Tool execution timed out: ${name}`);
|
|
94
|
+
return `Error: Tool "${name}" timed out after ${TOOL_HANDLER_TIMEOUT}ms`;
|
|
95
|
+
}
|
|
96
|
+
console.warn(`[tool-executor] Tool execution failed: ${name}`, err);
|
|
97
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Sandboxed worker entry point for platform mode.
|
|
4
|
+
*
|
|
5
|
+
* Called from the bundled `worker.js` inside a Deno Worker with all
|
|
6
|
+
* permissions false. Monkeypatches `globalThis.fetch` to proxy through
|
|
7
|
+
* capnweb RPC, creates capnweb-backed KV/vector/WebSocket, and delegates
|
|
8
|
+
* to a {@linkcode WintercServer} for session management.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
BridgedWebSocket,
|
|
15
|
+
CapnwebEndpoint,
|
|
16
|
+
type CapnwebPort,
|
|
17
|
+
createBridgedS2sWebSocket,
|
|
18
|
+
} from "./capnweb.ts";
|
|
19
|
+
import type { Kv } from "./kv.ts";
|
|
20
|
+
import type { CreateS2sWebSocket } from "./s2s.ts";
|
|
21
|
+
import type { AgentDef } from "./types.ts";
|
|
22
|
+
import { createWintercServer, type WintercServer } from "./winterc_server.ts";
|
|
23
|
+
|
|
24
|
+
declare const self: {
|
|
25
|
+
postMessage(msg: unknown, transfer?: Transferable[]): void;
|
|
26
|
+
onmessage: ((ev: MessageEvent) => any) | null; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize a sandboxed worker with the given agent definition.
|
|
31
|
+
*
|
|
32
|
+
* Sets up capnweb RPC, monkeypatches fetch, creates capability stubs,
|
|
33
|
+
* and waits for the host to send initialization data before creating
|
|
34
|
+
* the WinterTC server.
|
|
35
|
+
*/
|
|
36
|
+
export function initWorker(agent: AgentDef): void {
|
|
37
|
+
const endpoint = new CapnwebEndpoint(self as CapnwebPort);
|
|
38
|
+
|
|
39
|
+
// ─── Monkeypatch fetch to proxy through host ────────────────────────────
|
|
40
|
+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
41
|
+
let url: string;
|
|
42
|
+
let method = "GET";
|
|
43
|
+
let headers: Record<string, string> = {};
|
|
44
|
+
let body: string | undefined;
|
|
45
|
+
|
|
46
|
+
if (typeof input === "string") {
|
|
47
|
+
url = input;
|
|
48
|
+
} else if (input instanceof URL) {
|
|
49
|
+
url = input.toString();
|
|
50
|
+
} else {
|
|
51
|
+
url = input.url;
|
|
52
|
+
method = input.method;
|
|
53
|
+
headers = Object.fromEntries(input.headers);
|
|
54
|
+
if (input.body) {
|
|
55
|
+
body = await new Response(input.body).text();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (init?.method) method = init.method;
|
|
60
|
+
if (init?.headers) {
|
|
61
|
+
headers = Object.fromEntries(new Headers(init.headers as HeadersInit));
|
|
62
|
+
}
|
|
63
|
+
if (init?.body !== undefined) {
|
|
64
|
+
body = typeof init.body === "string" ? init.body : String(init.body);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = (await endpoint.call("host.fetch", [url, method, headers, body])) as {
|
|
68
|
+
status: number;
|
|
69
|
+
headers: Record<string, string>;
|
|
70
|
+
body: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return new Response(result.body, {
|
|
74
|
+
status: result.status,
|
|
75
|
+
headers: result.headers,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ─── Capnweb-backed KV ─────────────────────────────────────────────────
|
|
80
|
+
const kv: Kv = {
|
|
81
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
82
|
+
return (await endpoint.call("host.kv", ["get", key])) as T | null;
|
|
83
|
+
},
|
|
84
|
+
async set(key: string, value: unknown, options?: { expireIn?: number }): Promise<void> {
|
|
85
|
+
await endpoint.call("host.kv", ["set", key, value, options?.expireIn]);
|
|
86
|
+
},
|
|
87
|
+
async delete(key: string): Promise<void> {
|
|
88
|
+
await endpoint.call("host.kv", ["del", key]);
|
|
89
|
+
},
|
|
90
|
+
async list<T = unknown>(
|
|
91
|
+
prefix: string,
|
|
92
|
+
options?: { limit?: number; reverse?: boolean },
|
|
93
|
+
): Promise<{ key: string; value: T }[]> {
|
|
94
|
+
return (await endpoint.call("host.kv", [
|
|
95
|
+
"list",
|
|
96
|
+
prefix,
|
|
97
|
+
options?.limit,
|
|
98
|
+
options?.reverse,
|
|
99
|
+
])) as { key: string; value: T }[];
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ─── Capnweb-backed vector search ──────────────────────────────────────
|
|
104
|
+
const vectorSearch = async (query: string, topK: number): Promise<string> => {
|
|
105
|
+
return (await endpoint.call("host.vectorSearch", [query, topK])) as string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ─── Capnweb-backed S2S WebSocket factory ──────────────────────────────
|
|
109
|
+
const createWebSocket: CreateS2sWebSocket = (url, opts) => {
|
|
110
|
+
const { port1, port2 } = new MessageChannel();
|
|
111
|
+
endpoint.notify("host.createWebSocket", [url, JSON.stringify(opts.headers)], [port2]);
|
|
112
|
+
return createBridgedS2sWebSocket(port1);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ─── RPC handlers ──────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
let wintercServer: WintercServer | null = null;
|
|
118
|
+
|
|
119
|
+
// Handle init from host — creates the WinterTC server
|
|
120
|
+
endpoint.handle("worker.init", (args) => {
|
|
121
|
+
const [env, clientHtml] = args as [Record<string, string>, string | undefined];
|
|
122
|
+
|
|
123
|
+
wintercServer = createWintercServer({
|
|
124
|
+
agent,
|
|
125
|
+
env,
|
|
126
|
+
kv,
|
|
127
|
+
vectorSearch,
|
|
128
|
+
createWebSocket,
|
|
129
|
+
clientHtml,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return "ok";
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Handle HTTP request forwarding
|
|
136
|
+
endpoint.handle("worker.fetch", async (args) => {
|
|
137
|
+
if (!wintercServer) throw new Error("Worker not initialized");
|
|
138
|
+
const [url, method, headers, body] = args as [
|
|
139
|
+
string,
|
|
140
|
+
string,
|
|
141
|
+
Record<string, string>,
|
|
142
|
+
string | undefined,
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const request = new Request(url, {
|
|
146
|
+
method,
|
|
147
|
+
headers,
|
|
148
|
+
...(body ? { body } : {}),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const response = await wintercServer.fetch(request);
|
|
152
|
+
return {
|
|
153
|
+
status: response.status,
|
|
154
|
+
headers: Object.fromEntries(response.headers),
|
|
155
|
+
body: await response.text(),
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Handle new WebSocket client connection — port transferred from host
|
|
160
|
+
endpoint.handle("worker.handleWebSocket", (_args, ports) => {
|
|
161
|
+
if (!wintercServer) throw new Error("Worker not initialized");
|
|
162
|
+
const [skipGreeting] = _args as [boolean | undefined];
|
|
163
|
+
const port = ports[0];
|
|
164
|
+
if (!port) throw new Error("No port transferred");
|
|
165
|
+
|
|
166
|
+
const ws = new BridgedWebSocket(port);
|
|
167
|
+
wintercServer.handleWebSocket(ws, { skipGreeting });
|
|
168
|
+
return "ok";
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket session lifecycle handler.
|
|
4
|
+
*
|
|
5
|
+
* Cross-runtime: accepts a Logger parameter instead of importing `@std/log`.
|
|
6
|
+
* Audio validation is inlined (no dependency on server-side schemas).
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ClientMessage, ClientSink, ReadyConfig } from "./protocol.ts";
|
|
12
|
+
import { ClientMessageSchema } from "./protocol.ts";
|
|
13
|
+
import type { Logger } from "./runtime.ts";
|
|
14
|
+
import { consoleLogger } from "./runtime.ts";
|
|
15
|
+
import type { Session } from "./session.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Minimal WebSocket interface accepted by {@linkcode wireSessionSocket}.
|
|
19
|
+
*
|
|
20
|
+
* Satisfied by the standard `WebSocket`, `BridgedWebSocket` (capnweb),
|
|
21
|
+
* and the `ws` npm package's WebSocket.
|
|
22
|
+
*/
|
|
23
|
+
export type SessionWebSocket = {
|
|
24
|
+
readonly readyState: number;
|
|
25
|
+
send(data: string | ArrayBuffer | Uint8Array): void;
|
|
26
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Max size for a single audio chunk from the browser (1 MB). */
|
|
30
|
+
const MAX_AUDIO_CHUNK_BYTES = 1_048_576;
|
|
31
|
+
|
|
32
|
+
/** Validate a PCM16 audio chunk: non-empty, within size bounds, even byte length. */
|
|
33
|
+
function isValidAudioChunk(data: Uint8Array): boolean {
|
|
34
|
+
return (
|
|
35
|
+
data.byteLength > 0 && data.byteLength <= MAX_AUDIO_CHUNK_BYTES && data.byteLength % 2 === 0
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Options for wiring a WebSocket to a session. */
|
|
40
|
+
export type WsSessionOptions = {
|
|
41
|
+
/** Map of active sessions (session is added on open, removed on close). */
|
|
42
|
+
sessions: Map<string, Session>;
|
|
43
|
+
/** Factory function to create a session for a given ID and client sink. */
|
|
44
|
+
createSession: (sessionId: string, client: ClientSink) => Session;
|
|
45
|
+
/** Protocol config sent to the client immediately on connect. */
|
|
46
|
+
readyConfig: ReadyConfig;
|
|
47
|
+
/** Additional key-value pairs included in log messages. */
|
|
48
|
+
logContext?: Record<string, string>;
|
|
49
|
+
/** Callback invoked when the WebSocket connection opens. */
|
|
50
|
+
onOpen?: () => void;
|
|
51
|
+
/** Callback invoked when the WebSocket connection closes. */
|
|
52
|
+
onClose?: () => void;
|
|
53
|
+
/** Logger instance. Defaults to console. */
|
|
54
|
+
logger?: Logger;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a {@linkcode ClientSink} backed by a plain WebSocket.
|
|
59
|
+
*
|
|
60
|
+
* Text events are sent as JSON text frames; audio chunks are sent as
|
|
61
|
+
* binary frames (zero-copy).
|
|
62
|
+
*/
|
|
63
|
+
function createClientSink(ws: SessionWebSocket): ClientSink {
|
|
64
|
+
return {
|
|
65
|
+
get open() {
|
|
66
|
+
return ws.readyState === 1;
|
|
67
|
+
},
|
|
68
|
+
event(e) {
|
|
69
|
+
if (ws.readyState === 1) {
|
|
70
|
+
ws.send(JSON.stringify(e));
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
playAudioChunk(chunk) {
|
|
74
|
+
if (ws.readyState === 1) {
|
|
75
|
+
ws.send(chunk);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
playAudioDone() {
|
|
79
|
+
if (ws.readyState === 1) {
|
|
80
|
+
ws.send(JSON.stringify({ type: "audio_done" }));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Attaches session lifecycle handlers to a native WebSocket using
|
|
88
|
+
* plain JSON text frames and binary audio frames.
|
|
89
|
+
*
|
|
90
|
+
* Connection flow:
|
|
91
|
+
* 1. WebSocket opens → server sends `{ type: "config", ...ReadyConfig }`
|
|
92
|
+
* 2. Client sets up audio → sends `{ type: "audio_ready" }`
|
|
93
|
+
* 3. If reconnecting → client sends `{ type: "history", messages: [...] }`
|
|
94
|
+
*/
|
|
95
|
+
export function wireSessionSocket(ws: SessionWebSocket, opts: WsSessionOptions): void {
|
|
96
|
+
const { sessions, logger: log = consoleLogger } = opts;
|
|
97
|
+
const sessionId = crypto.randomUUID();
|
|
98
|
+
const sid = sessionId.slice(0, 8);
|
|
99
|
+
const ctx = opts.logContext ?? {};
|
|
100
|
+
|
|
101
|
+
let session: Session | null = null;
|
|
102
|
+
|
|
103
|
+
ws.addEventListener("open", () => {
|
|
104
|
+
opts.onOpen?.();
|
|
105
|
+
log.info("Session connected", { ...ctx, sid });
|
|
106
|
+
|
|
107
|
+
const client = createClientSink(ws);
|
|
108
|
+
session = opts.createSession(sessionId, client);
|
|
109
|
+
sessions.set(sessionId, session);
|
|
110
|
+
|
|
111
|
+
// Send config immediately — zero RTT
|
|
112
|
+
ws.send(JSON.stringify({ type: "config", ...opts.readyConfig }));
|
|
113
|
+
|
|
114
|
+
void session.start();
|
|
115
|
+
log.info("Session ready", { ...ctx, sid });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
ws.addEventListener("message", (event: Event) => {
|
|
119
|
+
if (!session) return;
|
|
120
|
+
const msgEvent = event as MessageEvent;
|
|
121
|
+
const { data } = msgEvent;
|
|
122
|
+
|
|
123
|
+
// Binary frame → raw PCM16 audio
|
|
124
|
+
if (data instanceof ArrayBuffer) {
|
|
125
|
+
const chunk = new Uint8Array(data);
|
|
126
|
+
if (!isValidAudioChunk(chunk)) {
|
|
127
|
+
log.warn("Invalid audio chunk, dropping", {
|
|
128
|
+
...ctx,
|
|
129
|
+
sid,
|
|
130
|
+
bytes: chunk.byteLength,
|
|
131
|
+
aligned: chunk.byteLength % 2 === 0,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
session.onAudio(chunk);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Text frame → JSON message
|
|
140
|
+
if (typeof data !== "string") return;
|
|
141
|
+
let json: unknown;
|
|
142
|
+
try {
|
|
143
|
+
json = JSON.parse(data);
|
|
144
|
+
} catch {
|
|
145
|
+
log.warn("Invalid JSON from client", { ...ctx, sid });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const parsed = ClientMessageSchema.safeParse(json);
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
log.warn("Invalid client message", {
|
|
152
|
+
...ctx,
|
|
153
|
+
sid,
|
|
154
|
+
error: parsed.error.message,
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const msg: ClientMessage = parsed.data;
|
|
160
|
+
switch (msg.type) {
|
|
161
|
+
case "audio_ready":
|
|
162
|
+
session.onAudioReady();
|
|
163
|
+
break;
|
|
164
|
+
case "cancel":
|
|
165
|
+
session.onCancel();
|
|
166
|
+
break;
|
|
167
|
+
case "reset":
|
|
168
|
+
session.onReset();
|
|
169
|
+
break;
|
|
170
|
+
case "history":
|
|
171
|
+
session.onHistory(msg.messages);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
ws.addEventListener("close", () => {
|
|
177
|
+
log.info("Session disconnected", { ...ctx, sid });
|
|
178
|
+
if (session) {
|
|
179
|
+
void session.stop().finally(() => {
|
|
180
|
+
sessions.delete(sessionId);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
opts.onClose?.();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
ws.addEventListener("error", (event) => {
|
|
187
|
+
const msg = event instanceof ErrorEvent ? event.message : "WebSocket error";
|
|
188
|
+
log.error("WebSocket error", { ...ctx, sid, error: msg });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "module",
|
|
3
|
+
"scripts": {
|
|
4
|
+
"dev": "aai dev",
|
|
5
|
+
"build": "aai deploy --dry-run",
|
|
6
|
+
"deploy": "aai deploy"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@alexkroman1/aai": "^0.3",
|
|
10
|
+
"preact": "^10",
|
|
11
|
+
"@preact/signals": "^2",
|
|
12
|
+
"zod": "^4"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineAgent } from "@alexkroman1/aai";
|
|
2
|
+
|
|
3
|
+
export default defineAgent({
|
|
4
|
+
name: "Coda",
|
|
5
|
+
instructions:
|
|
6
|
+
`You are Coda, a problem-solving assistant who answers questions by writing and running JavaScript code.
|
|
7
|
+
|
|
8
|
+
CRITICAL RULES:
|
|
9
|
+
- You MUST use the run_code tool for ANY question involving math, counting, string manipulation, data processing, logic, or anything that benefits from exact computation.
|
|
10
|
+
- NEVER do mental math or estimate. Always write code and report the exact result.
|
|
11
|
+
- Use console.log() to output intermediate steps. The last expression is captured automatically.
|
|
12
|
+
- If the code throws an error, fix it and try again.
|
|
13
|
+
- Explain what the code does briefly, then give the answer.
|
|
14
|
+
- Keep your spoken responses short — just say what the code found.
|
|
15
|
+
|
|
16
|
+
Examples of questions you MUST use code for:
|
|
17
|
+
- "What is 127 times 849?" → run_code
|
|
18
|
+
- "How many prime numbers are there below 1000?" → run_code
|
|
19
|
+
- "Reverse the string 'hello world'" → run_code
|
|
20
|
+
- "What's the 50th fibonacci number?" → run_code
|
|
21
|
+
- "Sort these numbers: 42, 17, 93, 8, 55" → run_code
|
|
22
|
+
- "What day of the week was January 1st, 2000?" → run_code
|
|
23
|
+
- "Convert 255 to binary" → run_code`,
|
|
24
|
+
greeting:
|
|
25
|
+
"Hey, I'm Coda. I solve problems by writing and running code. Try asking me something like, what's the 50th fibonacci number, or what day of the week was January 1st 2000.",
|
|
26
|
+
builtinTools: ["run_code"],
|
|
27
|
+
});
|