@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
package/sdk/session.ts
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* S2S session — relays audio between the client and AssemblyAI's
|
|
4
|
+
* Speech-to-Speech API, intercepting only tool calls for local execution.
|
|
5
|
+
*
|
|
6
|
+
* Cross-runtime: accepts Logger, Metrics, and a WebSocket factory via
|
|
7
|
+
* dependency injection.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AgentConfig, ToolSchema } from "./_internal_types.ts";
|
|
13
|
+
import type { ClientSink } from "./protocol.ts";
|
|
14
|
+
import { HOOK_TIMEOUT_MS } from "./protocol.ts";
|
|
15
|
+
import type { Logger, Metrics, S2SConfig } from "./runtime.ts";
|
|
16
|
+
import { consoleLogger, noopMetrics } from "./runtime.ts";
|
|
17
|
+
import {
|
|
18
|
+
type CreateS2sWebSocket,
|
|
19
|
+
connectS2s,
|
|
20
|
+
type S2sHandle,
|
|
21
|
+
type S2sToolCall,
|
|
22
|
+
type S2sToolSchema,
|
|
23
|
+
} from "./s2s.ts";
|
|
24
|
+
import { buildSystemPrompt } from "./system_prompt.ts";
|
|
25
|
+
import type { Message, StepInfo } from "./types.ts";
|
|
26
|
+
import type { ExecuteTool } from "./worker_entry.ts";
|
|
27
|
+
|
|
28
|
+
/** A voice session managing the S2S connection for one client. */
|
|
29
|
+
export type Session = {
|
|
30
|
+
start(): Promise<void>;
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
onAudio(data: Uint8Array): void;
|
|
33
|
+
onAudioReady(): void;
|
|
34
|
+
onCancel(): void;
|
|
35
|
+
onReset(): void;
|
|
36
|
+
onHistory(incoming: readonly { role: "user" | "assistant"; text: string }[]): void;
|
|
37
|
+
waitForTurn(): Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Generic interface for invoking agent lifecycle hooks. */
|
|
41
|
+
export type HookInvoker = {
|
|
42
|
+
onConnect(sessionId: string, timeoutMs?: number): Promise<void>;
|
|
43
|
+
onDisconnect(sessionId: string, timeoutMs?: number): Promise<void>;
|
|
44
|
+
onTurn(sessionId: string, text: string, timeoutMs?: number): Promise<void>;
|
|
45
|
+
onError(sessionId: string, error: { message: string }, timeoutMs?: number): Promise<void>;
|
|
46
|
+
onStep(sessionId: string, step: StepInfo, timeoutMs?: number): Promise<void>;
|
|
47
|
+
resolveTurnConfig(
|
|
48
|
+
sessionId: string,
|
|
49
|
+
timeoutMs?: number,
|
|
50
|
+
): Promise<{ maxSteps?: number; activeTools?: string[] } | null>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Configuration options for creating a new session. */
|
|
54
|
+
export type SessionOptions = {
|
|
55
|
+
id: string;
|
|
56
|
+
agent: string;
|
|
57
|
+
client: ClientSink;
|
|
58
|
+
agentConfig: AgentConfig;
|
|
59
|
+
toolSchemas: readonly ToolSchema[];
|
|
60
|
+
apiKey: string;
|
|
61
|
+
s2sConfig: S2SConfig;
|
|
62
|
+
executeTool: ExecuteTool;
|
|
63
|
+
createWebSocket: CreateS2sWebSocket;
|
|
64
|
+
env?: Record<string, string | undefined>;
|
|
65
|
+
hookInvoker?: HookInvoker;
|
|
66
|
+
skipGreeting?: boolean;
|
|
67
|
+
logger?: Logger;
|
|
68
|
+
metrics?: Metrics;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const _internals = {
|
|
72
|
+
connectS2s,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Create an S2S-backed session with the same interface as the STT+LLM+TTS session. */
|
|
76
|
+
export function createS2sSession(opts: SessionOptions): Session {
|
|
77
|
+
const {
|
|
78
|
+
id,
|
|
79
|
+
agent,
|
|
80
|
+
client,
|
|
81
|
+
toolSchemas,
|
|
82
|
+
apiKey,
|
|
83
|
+
s2sConfig,
|
|
84
|
+
executeTool,
|
|
85
|
+
createWebSocket,
|
|
86
|
+
hookInvoker,
|
|
87
|
+
logger: log = consoleLogger,
|
|
88
|
+
metrics = noopMetrics,
|
|
89
|
+
} = opts;
|
|
90
|
+
|
|
91
|
+
const agentLabel = { agent };
|
|
92
|
+
const agentConfig = opts.skipGreeting ? { ...opts.agentConfig, greeting: "" } : opts.agentConfig;
|
|
93
|
+
|
|
94
|
+
// Build system prompt
|
|
95
|
+
const hasTools = toolSchemas.length > 0 || (agentConfig.builtinTools?.length ?? 0) > 0;
|
|
96
|
+
const systemPrompt = buildSystemPrompt(agentConfig, {
|
|
97
|
+
hasTools,
|
|
98
|
+
voice: true,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// toolSchemas already includes both agent-defined and builtin tools
|
|
102
|
+
const s2sTools: S2sToolSchema[] = toolSchemas.map((ts) => ({
|
|
103
|
+
type: "function" as const,
|
|
104
|
+
name: ts.name,
|
|
105
|
+
description: ts.description,
|
|
106
|
+
parameters: ts.parameters as Record<string, unknown>,
|
|
107
|
+
}));
|
|
108
|
+
let s2s: S2sHandle | null = null;
|
|
109
|
+
const sessionAbort = new AbortController();
|
|
110
|
+
let audioReady = false;
|
|
111
|
+
let toolCallCount = 0;
|
|
112
|
+
let turnPromise: Promise<void> | null = null;
|
|
113
|
+
let conversationMessages: Message[] = [];
|
|
114
|
+
let s2sSessionId: string | null = null;
|
|
115
|
+
|
|
116
|
+
// Accumulate tool results — send after reply.done per API docs.
|
|
117
|
+
type PendingTool = { call_id: string; result: string };
|
|
118
|
+
let pendingTools: PendingTool[] = [];
|
|
119
|
+
|
|
120
|
+
async function resolveTurnConfig(): Promise<{
|
|
121
|
+
maxSteps?: number;
|
|
122
|
+
activeTools?: string[];
|
|
123
|
+
} | null> {
|
|
124
|
+
if (!hookInvoker) return null;
|
|
125
|
+
try {
|
|
126
|
+
return await hookInvoker.resolveTurnConfig(id, HOOK_TIMEOUT_MS);
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function invokeHook(
|
|
133
|
+
hook: "onConnect" | "onDisconnect" | "onTurn" | "onError" | "onStep",
|
|
134
|
+
...args: unknown[]
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
if (!hookInvoker) return;
|
|
137
|
+
try {
|
|
138
|
+
switch (hook) {
|
|
139
|
+
case "onConnect":
|
|
140
|
+
await hookInvoker.onConnect(id, HOOK_TIMEOUT_MS);
|
|
141
|
+
break;
|
|
142
|
+
case "onDisconnect":
|
|
143
|
+
await hookInvoker.onDisconnect(id, HOOK_TIMEOUT_MS);
|
|
144
|
+
break;
|
|
145
|
+
case "onTurn":
|
|
146
|
+
await hookInvoker.onTurn(id, args[0] as string, HOOK_TIMEOUT_MS);
|
|
147
|
+
break;
|
|
148
|
+
case "onError":
|
|
149
|
+
await hookInvoker.onError(id, args[0] as { message: string }, HOOK_TIMEOUT_MS);
|
|
150
|
+
break;
|
|
151
|
+
case "onStep":
|
|
152
|
+
await hookInvoker.onStep(id, args[0] as StepInfo, HOOK_TIMEOUT_MS);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
log.warn(`${hook} hook failed`, {
|
|
157
|
+
err: err instanceof Error ? err.message : String(err),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleToolCall(detail: S2sToolCall): Promise<void> {
|
|
163
|
+
const { call_id, name, args: parsedArgs } = detail;
|
|
164
|
+
|
|
165
|
+
// Emit tool_call_start to client
|
|
166
|
+
client.event({
|
|
167
|
+
type: "tool_call_start",
|
|
168
|
+
toolCallId: call_id,
|
|
169
|
+
toolName: name,
|
|
170
|
+
args: parsedArgs,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Resolve turn config for maxSteps / activeTools
|
|
174
|
+
const turnConfig = await resolveTurnConfig();
|
|
175
|
+
const maxSteps = turnConfig?.maxSteps ?? agentConfig.maxSteps;
|
|
176
|
+
|
|
177
|
+
toolCallCount++;
|
|
178
|
+
|
|
179
|
+
// Check maxSteps
|
|
180
|
+
if (maxSteps !== undefined && toolCallCount > maxSteps) {
|
|
181
|
+
log.info("maxSteps exceeded, refusing tool call", {
|
|
182
|
+
toolCallCount,
|
|
183
|
+
maxSteps,
|
|
184
|
+
});
|
|
185
|
+
pendingTools.push({
|
|
186
|
+
call_id,
|
|
187
|
+
result: "Maximum tool steps reached. Please respond to the user now.",
|
|
188
|
+
});
|
|
189
|
+
client.event({ type: "tool_call_done", toolCallId: call_id, result: "" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check activeTools filter
|
|
194
|
+
if (turnConfig?.activeTools && !turnConfig.activeTools.includes(name)) {
|
|
195
|
+
log.info("Tool filtered by activeTools", { name });
|
|
196
|
+
const errResult = JSON.stringify({
|
|
197
|
+
error: `Tool "${name}" is not available at this step.`,
|
|
198
|
+
});
|
|
199
|
+
pendingTools.push({ call_id, result: errResult });
|
|
200
|
+
client.event({
|
|
201
|
+
type: "tool_call_done",
|
|
202
|
+
toolCallId: call_id,
|
|
203
|
+
result: errResult,
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fire onStep hook
|
|
209
|
+
invokeHook("onStep", {
|
|
210
|
+
stepNumber: toolCallCount - 1,
|
|
211
|
+
toolCalls: [{ toolName: name, args: parsedArgs }],
|
|
212
|
+
text: "",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
log.info("S2S tool call", { tool: name, call_id, args: parsedArgs, agent });
|
|
216
|
+
|
|
217
|
+
// Execute — all tools (custom + builtin) run via the executor
|
|
218
|
+
let result: string;
|
|
219
|
+
try {
|
|
220
|
+
result = await executeTool(name, parsedArgs, id, conversationMessages);
|
|
221
|
+
} catch (err: unknown) {
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
223
|
+
log.error("Tool execution failed", { tool: name, error: msg });
|
|
224
|
+
result = JSON.stringify({ error: msg });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
log.info("S2S tool result", {
|
|
228
|
+
tool: name,
|
|
229
|
+
call_id,
|
|
230
|
+
resultLength: result.length,
|
|
231
|
+
});
|
|
232
|
+
// Accumulate — don't send yet. Results are sent after reply.done.
|
|
233
|
+
pendingTools.push({ call_id, result });
|
|
234
|
+
client.event({ type: "tool_call_done", toolCallId: call_id, result });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function connectAndSetup(): Promise<void> {
|
|
238
|
+
try {
|
|
239
|
+
const handle = await _internals.connectS2s({
|
|
240
|
+
apiKey,
|
|
241
|
+
config: s2sConfig,
|
|
242
|
+
createWebSocket,
|
|
243
|
+
logger: log,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Send session.update immediately on connect — before session.ready.
|
|
247
|
+
if (s2sSessionId) {
|
|
248
|
+
log.info("Attempting S2S session resume", {
|
|
249
|
+
session_id: s2sSessionId,
|
|
250
|
+
});
|
|
251
|
+
handle.resumeSession(s2sSessionId);
|
|
252
|
+
}
|
|
253
|
+
// Send config without greeting first — greeting is deferred until
|
|
254
|
+
// the client's audio is ready (onAudioReady) to avoid a race where
|
|
255
|
+
// greeting audio arrives before the browser can play it.
|
|
256
|
+
handle.updateSession({
|
|
257
|
+
system_prompt: systemPrompt,
|
|
258
|
+
tools: s2sTools,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
handle.addEventListener("ready", ((e: CustomEvent<{ session_id: string }>) => {
|
|
262
|
+
s2sSessionId = e.detail.session_id;
|
|
263
|
+
log.info("S2S session ready", { session_id: s2sSessionId });
|
|
264
|
+
}) as EventListener);
|
|
265
|
+
|
|
266
|
+
handle.addEventListener("session_expired", (() => {
|
|
267
|
+
log.info("S2S session expired, reconnecting fresh");
|
|
268
|
+
s2sSessionId = null;
|
|
269
|
+
handle.close();
|
|
270
|
+
}) as EventListener);
|
|
271
|
+
|
|
272
|
+
handle.addEventListener("speech_started", () => {
|
|
273
|
+
client.event({ type: "speech_started" });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
handle.addEventListener("speech_stopped", () => {
|
|
277
|
+
client.event({ type: "speech_stopped" });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
handle.addEventListener("user_transcript_delta", ((e: CustomEvent<{ text: string }>) => {
|
|
281
|
+
client.event({
|
|
282
|
+
type: "transcript",
|
|
283
|
+
text: e.detail.text,
|
|
284
|
+
isFinal: false,
|
|
285
|
+
});
|
|
286
|
+
}) as EventListener);
|
|
287
|
+
|
|
288
|
+
handle.addEventListener("user_transcript", ((
|
|
289
|
+
e: CustomEvent<{ item_id: string; text: string }>,
|
|
290
|
+
) => {
|
|
291
|
+
const { text } = e.detail;
|
|
292
|
+
log.info("S2S user transcript", { text });
|
|
293
|
+
client.event({ type: "transcript", text, isFinal: true });
|
|
294
|
+
client.event({ type: "turn", text });
|
|
295
|
+
conversationMessages.push({ role: "user", content: text });
|
|
296
|
+
invokeHook("onTurn", text);
|
|
297
|
+
}) as EventListener);
|
|
298
|
+
|
|
299
|
+
handle.addEventListener("reply_started", () => {
|
|
300
|
+
toolCallCount = 0;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
handle.addEventListener("audio", ((e: CustomEvent<{ audio: Uint8Array }>) => {
|
|
304
|
+
client.playAudioChunk(e.detail.audio);
|
|
305
|
+
}) as EventListener);
|
|
306
|
+
|
|
307
|
+
handle.addEventListener("agent_transcript", ((e: CustomEvent<{ text: string }>) => {
|
|
308
|
+
const { text } = e.detail;
|
|
309
|
+
client.event({ type: "chat", text });
|
|
310
|
+
conversationMessages.push({ role: "assistant", content: text });
|
|
311
|
+
}) as EventListener);
|
|
312
|
+
|
|
313
|
+
handle.addEventListener("tool_call", ((e: CustomEvent<S2sToolCall>) => {
|
|
314
|
+
const p = handleToolCall(e.detail).catch((err: unknown) => {
|
|
315
|
+
log.error("Tool call handler failed", {
|
|
316
|
+
err: err instanceof Error ? err.message : String(err),
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
const prev = turnPromise;
|
|
320
|
+
turnPromise = (prev ?? Promise.resolve())
|
|
321
|
+
.then(() => p)
|
|
322
|
+
.finally(() => {
|
|
323
|
+
turnPromise = null;
|
|
324
|
+
});
|
|
325
|
+
}) as EventListener);
|
|
326
|
+
|
|
327
|
+
handle.addEventListener("reply_done", ((e: CustomEvent<{ status?: string }>) => {
|
|
328
|
+
if (e.detail.status === "interrupted") {
|
|
329
|
+
log.info("S2S reply interrupted (barge-in)");
|
|
330
|
+
// Discard pending tool results on interruption.
|
|
331
|
+
pendingTools = [];
|
|
332
|
+
client.event({ type: "cancelled" });
|
|
333
|
+
} else {
|
|
334
|
+
// Send all accumulated tool results after reply.done.
|
|
335
|
+
if (pendingTools.length > 0) {
|
|
336
|
+
for (const tool of pendingTools) {
|
|
337
|
+
s2s?.sendToolResult(tool.call_id, tool.result);
|
|
338
|
+
}
|
|
339
|
+
pendingTools = [];
|
|
340
|
+
} else {
|
|
341
|
+
client.playAudioDone();
|
|
342
|
+
client.event({ type: "tts_done" });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}) as EventListener);
|
|
346
|
+
|
|
347
|
+
handle.addEventListener("error", ((e: CustomEvent<{ code: string; message: string }>) => {
|
|
348
|
+
log.error("S2S error", {
|
|
349
|
+
code: e.detail.code,
|
|
350
|
+
message: e.detail.message,
|
|
351
|
+
});
|
|
352
|
+
client.event({
|
|
353
|
+
type: "error",
|
|
354
|
+
code: "internal",
|
|
355
|
+
message: e.detail.message,
|
|
356
|
+
});
|
|
357
|
+
}) as EventListener);
|
|
358
|
+
|
|
359
|
+
handle.addEventListener("close", () => {
|
|
360
|
+
log.info("S2S closed");
|
|
361
|
+
s2s = null;
|
|
362
|
+
if (!sessionAbort.signal.aborted) {
|
|
363
|
+
log.info("Attempting S2S reconnect");
|
|
364
|
+
connectAndSetup().catch((err: unknown) => {
|
|
365
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
366
|
+
log.error("S2S reconnect failed", { error: msg });
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
s2s = handle;
|
|
372
|
+
} catch (err: unknown) {
|
|
373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
374
|
+
log.error("S2S connect failed", { error: msg });
|
|
375
|
+
client.event({ type: "error", code: "internal", message: msg });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
async start(): Promise<void> {
|
|
381
|
+
metrics.sessionsTotal.inc(agentLabel);
|
|
382
|
+
metrics.sessionsActive.inc(agentLabel);
|
|
383
|
+
invokeHook("onConnect");
|
|
384
|
+
await connectAndSetup();
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
async stop(): Promise<void> {
|
|
388
|
+
if (sessionAbort.signal.aborted) return;
|
|
389
|
+
sessionAbort.abort();
|
|
390
|
+
metrics.sessionsActive.dec(agentLabel);
|
|
391
|
+
if (turnPromise) await turnPromise;
|
|
392
|
+
s2s?.close();
|
|
393
|
+
invokeHook("onDisconnect");
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
onAudio(data: Uint8Array): void {
|
|
397
|
+
s2s?.sendAudio(data);
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
onAudioReady(): void {
|
|
401
|
+
if (audioReady) return;
|
|
402
|
+
audioReady = true;
|
|
403
|
+
// Now that the client can play audio, send greeting via session.update.
|
|
404
|
+
if (agentConfig.greeting && s2s) {
|
|
405
|
+
s2s.updateSession({
|
|
406
|
+
system_prompt: systemPrompt,
|
|
407
|
+
tools: s2sTools,
|
|
408
|
+
greeting: agentConfig.greeting,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
onCancel(): void {
|
|
414
|
+
// S2S handles barge-in natively.
|
|
415
|
+
client.event({ type: "cancelled" });
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
onReset(): void {
|
|
419
|
+
conversationMessages = [];
|
|
420
|
+
toolCallCount = 0;
|
|
421
|
+
pendingTools = [];
|
|
422
|
+
s2sSessionId = null;
|
|
423
|
+
s2s?.close();
|
|
424
|
+
// Reconnect happens via the close handler.
|
|
425
|
+
client.event({ type: "reset" });
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
onHistory(incoming: readonly { role: "user" | "assistant"; text: string }[]): void {
|
|
429
|
+
for (const msg of incoming) {
|
|
430
|
+
conversationMessages.push({ role: msg.role, content: msg.text });
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
waitForTurn(): Promise<void> {
|
|
435
|
+
return turnPromise ?? Promise.resolve();
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright 2025 the AAI authors. MIT license.
|
|
2
|
+
|
|
3
|
+
import type { AgentConfig } from "./_internal_types.ts";
|
|
4
|
+
import { DEFAULT_INSTRUCTIONS } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const VOICE_RULES =
|
|
7
|
+
"\n\nCRITICAL OUTPUT RULES — you MUST follow these for EVERY response:\n" +
|
|
8
|
+
"Your response will be spoken aloud by a TTS system and displayed as plain text.\n" +
|
|
9
|
+
"- NEVER use markdown: no **, no *, no _, no #, no `, no [](), no ---\n" +
|
|
10
|
+
"- NEVER use bullet points (-, *, •) or numbered lists (1., 2.)\n" +
|
|
11
|
+
"- NEVER use code blocks or inline code\n" +
|
|
12
|
+
"- NEVER mention tools, search, APIs, or technical failures to the user. " +
|
|
13
|
+
"If a tool returns no results, just answer naturally without explaining why.\n" +
|
|
14
|
+
"- Write exactly as you would say it out loud to a friend\n" +
|
|
15
|
+
'- Use short conversational sentences. To list things, say "First," "Next," "Finally,"\n' +
|
|
16
|
+
"- Keep responses concise — 1 to 3 sentences max";
|
|
17
|
+
|
|
18
|
+
export function buildSystemPrompt(
|
|
19
|
+
config: AgentConfig,
|
|
20
|
+
opts: { hasTools: boolean; voice?: boolean },
|
|
21
|
+
): string {
|
|
22
|
+
const { hasTools } = opts;
|
|
23
|
+
const agentInstructions = config.instructions
|
|
24
|
+
? `\n\nAgent-Specific Instructions:\n${config.instructions}`
|
|
25
|
+
: "";
|
|
26
|
+
|
|
27
|
+
const toolPreamble = hasTools
|
|
28
|
+
? "\n\nWhen you decide to use a tool, ALWAYS say a brief natural phrase BEFORE the tool call " +
|
|
29
|
+
'(e.g. "Let me look that up" or "One moment while I check"). ' +
|
|
30
|
+
"This fills silence while the tool executes. Keep preambles to one short sentence."
|
|
31
|
+
: "";
|
|
32
|
+
|
|
33
|
+
const today = new Date().toLocaleDateString("en-US", {
|
|
34
|
+
weekday: "long",
|
|
35
|
+
year: "numeric",
|
|
36
|
+
month: "long",
|
|
37
|
+
day: "numeric",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
DEFAULT_INSTRUCTIONS +
|
|
42
|
+
`\n\nToday's date is ${today}.` +
|
|
43
|
+
agentInstructions +
|
|
44
|
+
toolPreamble +
|
|
45
|
+
(opts?.voice ? VOICE_RULES : "")
|
|
46
|
+
);
|
|
47
|
+
}
|