@alexkroman1/aai-cli 5.1.1 → 5.2.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/dist/_agent.d.ts +39 -0
- package/dist/_api-client.d.ts +37 -0
- package/dist/_bundler.d.ts +46 -0
- package/dist/_config.d.ts +57 -0
- package/dist/_default-html.d.ts +12 -0
- package/dist/_deploy.d.ts +28 -0
- package/dist/_dev-server.d.ts +74 -0
- package/dist/{_init-B-LEVTHx.mjs → _init-DU-sXH6S.mjs} +44 -65
- package/dist/_init.d.ts +7 -0
- package/dist/_mock-api.d.ts +22 -0
- package/dist/_mock-registry.d.ts +17 -0
- package/dist/_output.d.ts +50 -0
- package/dist/_server-common.d.ts +17 -0
- package/dist/_slug-api.d.ts +15 -0
- package/dist/_templates.d.ts +21 -0
- package/dist/_typecheck-gate.d.ts +7 -0
- package/dist/_ui.d.ts +22 -0
- package/dist/_utils.d.ts +48 -0
- package/dist/_vite-env.d.ts +8 -0
- package/dist/build.d.ts +16 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +1 -1
- package/dist/client-bundler.d.ts +21 -0
- package/dist/delete.d.ts +18 -0
- package/dist/deploy.d.ts +15 -0
- package/dist/dev.d.ts +13 -0
- package/dist/{init-tc3Rbgrf.mjs → init-BjeK8crW.mjs} +1 -1
- package/dist/init.d.ts +24 -0
- package/dist/scaffold/.env.example +11 -0
- package/dist/scaffold/CLAUDE.md +919 -0
- package/dist/scaffold/global.d.ts +1 -0
- package/dist/scaffold/package.json +32 -0
- package/dist/scaffold/pnpm-workspace.yaml +13 -0
- package/dist/scaffold/tsconfig.json +19 -0
- package/dist/scaffold/vite.config.ts +14 -0
- package/dist/scaffold/vitest.config.ts +24 -0
- package/dist/secret.d.ts +25 -0
- package/dist/storage.d.ts +22 -0
- package/dist/templates/code-interpreter/agent.ts +11 -0
- package/dist/templates/code-interpreter/system-prompt.md +18 -0
- package/dist/templates/dispatch-center/agent.test.ts +153 -0
- package/dist/templates/dispatch-center/agent.ts +50 -0
- package/dist/templates/dispatch-center/client.tsx +378 -0
- package/dist/templates/dispatch-center/shared.ts +631 -0
- package/dist/templates/dispatch-center/system-prompt.md +39 -0
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +27 -0
- package/dist/templates/dispatch-center/tools/incident_create.ts +79 -0
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +88 -0
- package/dist/templates/dispatch-center/tools/incident_get.ts +36 -0
- package/dist/templates/dispatch-center/tools/incident_triage.ts +78 -0
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +68 -0
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +52 -0
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +27 -0
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +149 -0
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +97 -0
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +36 -0
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +62 -0
- package/dist/templates/embedded-assets/agent.ts +55 -0
- package/dist/templates/embedded-assets/knowledge.json +20 -0
- package/dist/templates/health-assistant/agent.ts +174 -0
- package/dist/templates/health-assistant/system-prompt.md +17 -0
- package/dist/templates/infocom-adventure/agent.ts +119 -0
- package/dist/templates/infocom-adventure/client.tsx +279 -0
- package/dist/templates/infocom-adventure/shared.ts +38 -0
- package/dist/templates/infocom-adventure/system-prompt.md +43 -0
- package/dist/templates/math-buddy/agent.ts +16 -0
- package/dist/templates/math-buddy/system-prompt.md +12 -0
- package/dist/templates/night-owl/agent.ts +76 -0
- package/dist/templates/night-owl/client.tsx +130 -0
- package/dist/templates/night-owl/shared.ts +5 -0
- package/dist/templates/personal-finance/agent.ts +11 -0
- package/dist/templates/personal-finance/system-prompt.md +17 -0
- package/dist/templates/pipeline-simple/agent.test.ts +32 -0
- package/dist/templates/pipeline-simple/agent.ts +13 -0
- package/dist/templates/pizza-ordering/agent.test.ts +235 -0
- package/dist/templates/pizza-ordering/agent.ts +174 -0
- package/dist/templates/pizza-ordering/client.tsx +120 -0
- package/dist/templates/pizza-ordering/shared.ts +142 -0
- package/dist/templates/pizza-ordering/system-prompt.md +17 -0
- package/dist/templates/simple/agent.ts +5 -0
- package/dist/templates/solo-rpg/agent.test.ts +477 -0
- package/dist/templates/solo-rpg/agent.ts +37 -0
- package/dist/templates/solo-rpg/client.tsx +809 -0
- package/dist/templates/solo-rpg/shared.ts +747 -0
- package/dist/templates/solo-rpg/system-prompt.md +115 -0
- package/dist/templates/solo-rpg/tools/action_roll.ts +98 -0
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +69 -0
- package/dist/templates/solo-rpg/tools/check_state.ts +11 -0
- package/dist/templates/solo-rpg/tools/load_game.ts +25 -0
- package/dist/templates/solo-rpg/tools/oracle.ts +248 -0
- package/dist/templates/solo-rpg/tools/save_game.ts +20 -0
- package/dist/templates/solo-rpg/tools/setup_character.ts +208 -0
- package/dist/templates/solo-rpg/tools/update_state.ts +170 -0
- package/dist/templates/web-researcher/agent.ts +11 -0
- package/dist/test.d.ts +39 -0
- package/dist/tsdown.config.d.ts +2 -0
- package/dist/typecheck.d.ts +26 -0
- package/dist/worker-bundler.d.ts +29 -0
- package/package.json +9 -6
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
# aai Voice Agent
|
|
2
|
+
|
|
3
|
+
You are helping build a voice agent using the **aai** framework.
|
|
4
|
+
|
|
5
|
+
## Workflow
|
|
6
|
+
|
|
7
|
+
The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
8
|
+
`pnpm test` (logic) → `pnpm build` (validate bundle).
|
|
9
|
+
|
|
10
|
+
1. **Iterate in `pnpm dev`** — hot reload + browser UI. Speak to the
|
|
11
|
+
agent to verify behavior end-to-end. This is the primary feedback loop.
|
|
12
|
+
2. **Run `pnpm test` after logic changes** — vitest. Co-locate tests as
|
|
13
|
+
`agent.test.ts` (see `pipeline-simple` template for a reference).
|
|
14
|
+
**The project starts with an `agent.test.ts`, and it is yours to
|
|
15
|
+
maintain.** It asserts the agent's shape — name, providers, tool names —
|
|
16
|
+
so rewriting the agent without updating it leaves a test asserting an
|
|
17
|
+
agent that no longer exists. When a test fails after your change, decide
|
|
18
|
+
which side is stale: updating the test to match the new agent is a normal
|
|
19
|
+
fix, not a workaround. Do not delete a test to make it pass.
|
|
20
|
+
3. **Run `pnpm build` before declaring done** — bundles `agent.ts`,
|
|
21
|
+
type-checks, and validates the manifest. Catches issues `dev` won't.
|
|
22
|
+
4. **Make small, focused changes** — verify each one before stacking the
|
|
23
|
+
next.
|
|
24
|
+
5. **Look at templates before writing custom code** — the CLI ships working
|
|
25
|
+
examples inside its own package, at
|
|
26
|
+
`node_modules/@alexkroman1/aai-cli/dist/templates/`. Read them directly;
|
|
27
|
+
`aai init --template <name>` scaffolds a fresh project from one. Closest
|
|
28
|
+
matches: `simple`, `pipeline-simple`, `web-researcher`, `solo-rpg`,
|
|
29
|
+
`pizza-ordering`.
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx @alexkroman1/aai-cli init # Scaffold a new agent
|
|
35
|
+
npx @alexkroman1/aai-cli dev # Start local dev server
|
|
36
|
+
npx @alexkroman1/aai-cli test # Run agent.test.ts via vitest
|
|
37
|
+
npx @alexkroman1/aai-cli build # Bundle and validate
|
|
38
|
+
npx @alexkroman1/aai-cli deploy # Deploy to production
|
|
39
|
+
npx @alexkroman1/aai-cli deploy -y # Deploy without prompts
|
|
40
|
+
npx @alexkroman1/aai-cli delete # Remove deployed agent
|
|
41
|
+
npx @alexkroman1/aai-cli secret put NAME # Set a secret
|
|
42
|
+
npx @alexkroman1/aai-cli secret delete NAME
|
|
43
|
+
npx @alexkroman1/aai-cli secret list
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The scaffold's `package.json` exposes `dev`, `build`, `test`, and `deploy`
|
|
47
|
+
as `pnpm <name>` shortcuts. Other commands (`init`, `delete`, `secret`)
|
|
48
|
+
are CLI-only.
|
|
49
|
+
|
|
50
|
+
## Project structure
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
my-agent/
|
|
54
|
+
agent.ts # Agent definition (required)
|
|
55
|
+
agent.test.ts # Unit tests (optional)
|
|
56
|
+
client.tsx # Custom UI (optional, React)
|
|
57
|
+
shared.ts # Types shared between agent.ts and client.tsx
|
|
58
|
+
system-prompt.md # Long system prompts (optional, imported)
|
|
59
|
+
tools/ # Tool files when too large for inline (optional)
|
|
60
|
+
package.json
|
|
61
|
+
tsconfig.json
|
|
62
|
+
.env # Local dev secrets (gitignored)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## `agent()` API
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { agent } from "@alexkroman1/aai";
|
|
69
|
+
|
|
70
|
+
export default agent({
|
|
71
|
+
name: string; // required — display name
|
|
72
|
+
systemPrompt?: string; // default: general voice assistant
|
|
73
|
+
greeting?: string; // default: "Hey there..."
|
|
74
|
+
sttPrompt?: string; // STT guidance for jargon/acronyms
|
|
75
|
+
builtinTools?: BuiltinTool[]; // see built-in tools table
|
|
76
|
+
tools?: Record<string, ToolDef>;
|
|
77
|
+
maxSteps?: number; // default: 10 — max tool calls per turn
|
|
78
|
+
toolChoice?: "auto" | "required"; // default: "auto"
|
|
79
|
+
idleTimeoutMs?: number; // disconnect after inactivity (ms)
|
|
80
|
+
silenceTimeoutMs?: number; // pipeline only — assistant speaks up after this much user silence (ms)
|
|
81
|
+
silencePrompt?: string; // instruction injected on silence timeout (requires silenceTimeoutMs)
|
|
82
|
+
minBargeInWords?: number; // pipeline only — words before user speech interrupts the reply (default 2)
|
|
83
|
+
interruptionMinDurationMs?: number; // pipeline only — sustained speech (ms) before an interim barge-in interrupts (default 0 = off)
|
|
84
|
+
holdPhrase?: string; // pipeline only — spoken before a silent tool-call turn (default "One moment."; "" disables)
|
|
85
|
+
falseInterruptionTimeoutMs?: number; // pipeline only — resume an interrupted reply if no user turn commits (default 2000; 0 disables)
|
|
86
|
+
state?: () => S; // per-session mutable state, exposed as ctx.state
|
|
87
|
+
// (S is inferred; see "Typing ctx.state")
|
|
88
|
+
syncState?: (state: S) => unknown; // push a projection of state to the client
|
|
89
|
+
// (read it with useAgentState; see UI hooks)
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
> When `stt`, `llm`, and `tts` are all provided, the agent runs in
|
|
94
|
+
> **Pipeline mode** — see the section below.
|
|
95
|
+
|
|
96
|
+
Minimal agent — a cascaded pipeline, which is what you should build unless
|
|
97
|
+
the user asks for the speech-to-speech API:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
|
|
101
|
+
|
|
102
|
+
export default agent({
|
|
103
|
+
name: "My Agent",
|
|
104
|
+
...assemblyAIPipeline(),
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`assemblyAIPipeline()` sets all three stages to AssemblyAI, which bill to
|
|
109
|
+
the one key a published agent is guaranteed to have. Override a single stage
|
|
110
|
+
by setting it after the spread — everything else stays as the preset put it:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
|
|
114
|
+
import { assemblyAI as assemblyAITts } from "@alexkroman1/aai/tts";
|
|
115
|
+
|
|
116
|
+
export default agent({
|
|
117
|
+
name: "My Agent",
|
|
118
|
+
...assemblyAIPipeline(),
|
|
119
|
+
tts: assemblyAITts({ voice: "paul" }),
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`agent({ name })` alone is legal and gives you S2S mode instead — see below.
|
|
124
|
+
|
|
125
|
+
System prompt from file:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { agent } from "@alexkroman1/aai";
|
|
129
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
130
|
+
export default agent({ name: "My Agent", systemPrompt });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**JSON imports need no attribute.** `resolveJsonModule` is on, so
|
|
134
|
+
`import data from "./knowledge.json"` is all it takes. Do NOT write
|
|
135
|
+
`assert { type: "json" }` — import assertions were replaced by import
|
|
136
|
+
attributes and TypeScript rejects them (`TS2880`). If you want to be
|
|
137
|
+
explicit the modern spelling is `with { type: "json" }`, but plain is fine.
|
|
138
|
+
|
|
139
|
+
## Pipeline mode
|
|
140
|
+
|
|
141
|
+
Omitting `stt`/`llm`/`tts` gives **S2S mode**: AssemblyAI's speech-to-speech
|
|
142
|
+
service handles STT, the LLM loop, and TTS in one socket. Fewer moving
|
|
143
|
+
parts, but you cannot choose the model, swap a provider, or tune a stage.
|
|
144
|
+
|
|
145
|
+
**Prefer pipeline mode** — declare all three — unless the user specifically
|
|
146
|
+
asks for the speech-to-speech API. Nearly every template ships this way, and
|
|
147
|
+
it is what the App Builder defaults to. The host runs the LLM loop locally
|
|
148
|
+
(Vercel AI SDK) with your chosen STT, LLM, and TTS. You need it when:
|
|
149
|
+
|
|
150
|
+
- you want a specific LLM (Anthropic, OpenAI, Gemini, Mistral, xAI, Groq,
|
|
151
|
+
hundreds of models via OpenRouter, or 25+ models via the AssemblyAI
|
|
152
|
+
LLM Gateway)
|
|
153
|
+
- you want a specific STT model or TTS voice
|
|
154
|
+
- you need to swap providers without changing agent code
|
|
155
|
+
|
|
156
|
+
**The rule:** set all three of `stt`, `llm`, `tts` together, or none. A
|
|
157
|
+
partial config is rejected at parse time.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { agent } from "@alexkroman1/aai";
|
|
161
|
+
import { assemblyAI } from "@alexkroman1/aai/stt";
|
|
162
|
+
import { anthropic } from "@alexkroman1/aai/llm";
|
|
163
|
+
import { cartesia } from "@alexkroman1/aai/tts";
|
|
164
|
+
|
|
165
|
+
export default agent({
|
|
166
|
+
name: "My Agent",
|
|
167
|
+
stt: assemblyAI({ model: "universal-3-5-pro" }),
|
|
168
|
+
llm: anthropic({ model: "claude-haiku-4-5" }),
|
|
169
|
+
tts: cartesia(),
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Tools, the database, `ctx`, and the UI all behave identically across modes.
|
|
174
|
+
Only the audio + LLM transport differs.
|
|
175
|
+
|
|
176
|
+
**There is no text-only agent mode.** An agent is a voice conversation —
|
|
177
|
+
every pipeline agent must declare a real TTS provider.
|
|
178
|
+
|
|
179
|
+
**Silence nudge (pipeline only):** set `silenceTimeoutMs` to make the
|
|
180
|
+
assistant proactively take a turn after that much user silence (e.g.
|
|
181
|
+
"Are you still there?"). Customize the injected instruction with
|
|
182
|
+
`silencePrompt`. The nudge never appears as a user transcript, and the
|
|
183
|
+
assistant stops nudging after 3 consecutive unanswered nudges until the
|
|
184
|
+
user speaks again.
|
|
185
|
+
|
|
186
|
+
**Voice-UX tuning (pipeline only):** `minBargeInWords` controls how many
|
|
187
|
+
words of user speech interrupt the assistant mid-reply (default 2, so
|
|
188
|
+
one-word backchannels like "yeah" don't cut it off);
|
|
189
|
+
`interruptionMinDurationMs` adds an optional sustained-speech gate on top
|
|
190
|
+
(interim transcripts only — committed turns always land). End-of-turn
|
|
191
|
+
detection (how long a pause ends the user's turn) belongs to the STT
|
|
192
|
+
provider: `assemblyAI({ minTurnSilenceMs })` / `deepgram({ endpointing })`,
|
|
193
|
+
both defaulting to 1500 ms so mid-utterance pauses don't split a request.
|
|
194
|
+
`holdPhrase` is spoken when a turn opens with a tool call and no speech.
|
|
195
|
+
`falseInterruptionTimeoutMs` resumes an interrupted reply when a barge-in
|
|
196
|
+
turns out to be noise (no user turn commits within the window).
|
|
197
|
+
|
|
198
|
+
## Providers
|
|
199
|
+
|
|
200
|
+
Provider SDKs are **optional peer dependencies**. Install only the SDKs
|
|
201
|
+
for the providers you actually use.
|
|
202
|
+
|
|
203
|
+
### STT — `@alexkroman1/aai/stt`
|
|
204
|
+
|
|
205
|
+
| Factory | Default model | Env var |
|
|
206
|
+
| ------------- | ------------------------ | -------------------- |
|
|
207
|
+
| `assemblyAI` | `"universal-3-5-pro"` | `ASSEMBLYAI_API_KEY` |
|
|
208
|
+
| `deepgram` | `"nova-3"` | `DEEPGRAM_API_KEY` |
|
|
209
|
+
| `elevenlabs` | `"scribe_v2_realtime"` | `ELEVENLABS_API_KEY` |
|
|
210
|
+
| `soniox` | `"stt-rt-v3"` | `SONIOX_API_KEY` |
|
|
211
|
+
|
|
212
|
+
All STT factories accept `{ model?: string, ... }`. Bare calls
|
|
213
|
+
(`deepgram()`, `soniox()`, etc.) use the default model.
|
|
214
|
+
|
|
215
|
+
`assemblyAI` accepts an optional `region: "eu"` for EU data residency —
|
|
216
|
+
it routes streaming transcription to AssemblyAI's EU endpoints. EU-region
|
|
217
|
+
API keys require it; the US endpoints reject them. Example:
|
|
218
|
+
`assemblyAI({ model: "universal-3-5-pro", region: "eu" })`.
|
|
219
|
+
|
|
220
|
+
### LLM — `@alexkroman1/aai/llm`
|
|
221
|
+
|
|
222
|
+
| Factory | SDK package | Env var |
|
|
223
|
+
| ----------- | --------------------- | -------------------------------- |
|
|
224
|
+
| `anthropic` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |
|
|
225
|
+
| `openai` | `@ai-sdk/openai` | `OPENAI_API_KEY` |
|
|
226
|
+
| `google` | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
227
|
+
| `mistral` | `@ai-sdk/mistral` | `MISTRAL_API_KEY` |
|
|
228
|
+
| `xai` | `@ai-sdk/xai` | `XAI_API_KEY` |
|
|
229
|
+
| `groq` | `@ai-sdk/groq` | `GROQ_API_KEY` |
|
|
230
|
+
| `openrouter`| `@ai-sdk/openai` | `OPENROUTER_API_KEY` |
|
|
231
|
+
| `gateway` | `ai` (built in) | `AI_GATEWAY_API_KEY` |
|
|
232
|
+
| `assemblyAI`| `@ai-sdk/openai` | `ASSEMBLYAI_API_KEY` |
|
|
233
|
+
|
|
234
|
+
LLM factories require `{ model: string }`. Example:
|
|
235
|
+
`anthropic({ model: "claude-haiku-4-5" })`.
|
|
236
|
+
|
|
237
|
+
`openrouter` routes through [OpenRouter](https://openrouter.ai) — an
|
|
238
|
+
OpenAI-compatible endpoint fronting hundreds of models addressed as
|
|
239
|
+
`"creator/model"`, e.g.
|
|
240
|
+
`openrouter({ model: "meta-llama/llama-3.3-70b-instruct" })`. It needs
|
|
241
|
+
no extra SDK install (it reuses the `@ai-sdk/openai` client).
|
|
242
|
+
|
|
243
|
+
`gateway` routes through the [Vercel AI
|
|
244
|
+
Gateway](https://vercel.com/docs/ai-gateway) — one endpoint fronting
|
|
245
|
+
hundreds of models addressed as `"creator/model"`, e.g.
|
|
246
|
+
`gateway({ model: "zai/glm-4.6" })`. It needs no extra SDK install
|
|
247
|
+
(the gateway client ships inside the `ai` package).
|
|
248
|
+
|
|
249
|
+
`assemblyAI` routes through the [AssemblyAI LLM
|
|
250
|
+
Gateway](https://www.assemblyai.com/docs/llm-gateway) — an
|
|
251
|
+
OpenAI-compatible endpoint fronting 25+ models (Claude, GPT, Gemini,
|
|
252
|
+
etc.) with the same API key as AssemblyAI STT. It accepts an optional
|
|
253
|
+
`region: "eu"` for EU data residency. It shares its name with the STT
|
|
254
|
+
factory, so alias one when using both:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { assemblyAI } from "@alexkroman1/aai/stt";
|
|
258
|
+
import { assemblyAI as assemblyAILlm } from "@alexkroman1/aai/llm";
|
|
259
|
+
|
|
260
|
+
export default agent({
|
|
261
|
+
name: "My Agent",
|
|
262
|
+
stt: assemblyAI({ model: "universal-3-5-pro" }),
|
|
263
|
+
llm: assemblyAILlm({ model: "claude-sonnet-4-6" }),
|
|
264
|
+
tts: cartesia(),
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
An all-AssemblyAI pipeline — one provider, one key:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { agent } from "@alexkroman1/aai";
|
|
272
|
+
import { assemblyAI } from "@alexkroman1/aai/stt";
|
|
273
|
+
import { assemblyAI as assemblyAILlm } from "@alexkroman1/aai/llm";
|
|
274
|
+
import { assemblyAI as assemblyAITts } from "@alexkroman1/aai/tts";
|
|
275
|
+
|
|
276
|
+
export default agent({
|
|
277
|
+
name: "My Agent",
|
|
278
|
+
stt: assemblyAI({ model: "universal-3-5-pro" }),
|
|
279
|
+
llm: assemblyAILlm({ model: "gemini-2.5-flash-lite" }),
|
|
280
|
+
tts: assemblyAITts({ voice: "vera" }),
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### TTS — `@alexkroman1/aai/tts`
|
|
285
|
+
|
|
286
|
+
| Factory | Default voice | Env var |
|
|
287
|
+
| ------------ | ---------------------------------------- | -------------------- |
|
|
288
|
+
| `assemblyAI` | `"vera"` | `ASSEMBLYAI_API_KEY` |
|
|
289
|
+
| `cartesia` | `"f786b574-daa5-4673-aa0c-cbe3e8534c02"` | `CARTESIA_API_KEY` |
|
|
290
|
+
| `rime` | `"cove"` (model `mistv2`) | `RIME_API_KEY` |
|
|
291
|
+
|
|
292
|
+
Bare calls (`assemblyAI()`, `cartesia()`, `rime()`) use the defaults.
|
|
293
|
+
Override with `{ voice, model, language }`.
|
|
294
|
+
|
|
295
|
+
**AssemblyAI TTS** shares `ASSEMBLYAI_API_KEY` with AssemblyAI STT and the
|
|
296
|
+
LLM Gateway, so an all-AssemblyAI pipeline needs exactly one secret. Each
|
|
297
|
+
voice speaks one language, and this is the whole catalog — **a voice not on
|
|
298
|
+
this list is rejected after the socket opens, which leaves the agent
|
|
299
|
+
connected, "ready", and permanently silent**, so pick one from here rather
|
|
300
|
+
than guessing a plausible name:
|
|
301
|
+
|
|
302
|
+
- **English, US accent**: `alba`, `anna`, `charles`, `eve`, `george`,
|
|
303
|
+
`jane`, `jean`, `mary`, `michael`
|
|
304
|
+
- **English, UK accent**: `paul`, `vera` (the default)
|
|
305
|
+
- **Native accent, code-switches with English**: `estelle` (fr),
|
|
306
|
+
`giovanni` (it), `juergen` (de), `lola` (es), `rafael` (pt)
|
|
307
|
+
|
|
308
|
+
There is no separate age/gender/style axis — match the persona by picking a
|
|
309
|
+
name and accent, and put the delivery in the system prompt instead.
|
|
310
|
+
|
|
311
|
+
Set
|
|
312
|
+
`language` only alongside a voice that speaks it, as an ISO 639-1 code —
|
|
313
|
+
`"en"`, `"fr"`, `"de"`, `"it"`, `"pt"`, `"es"` are the six the catalog
|
|
314
|
+
covers, and the SDK translates each to the full name the service wants.
|
|
315
|
+
Anything else fails at session start. Because the factory is named
|
|
316
|
+
`assemblyAI` in `/stt`, `/llm`, and `/tts`, alias on import.
|
|
317
|
+
|
|
318
|
+
**Rime quirk:** language uses ISO 639-3 three-letter codes (e.g. `"eng"`
|
|
319
|
+
not `"en"`).
|
|
320
|
+
|
|
321
|
+
Set provider keys the same way as any secret: `.env` for local dev,
|
|
322
|
+
`aai secret put` for production.
|
|
323
|
+
|
|
324
|
+
## `tool()` API
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { tool } from "@alexkroman1/aai";
|
|
328
|
+
import { z } from "zod";
|
|
329
|
+
|
|
330
|
+
const myTool = tool({
|
|
331
|
+
description: string; // shown to LLM — decides when to call
|
|
332
|
+
parameters?: z.ZodObject; // Zod schema (omit for no-arg tools)
|
|
333
|
+
execute(args, ctx): unknown; // sync or async
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`execute` may call `fetch` directly — tool code reaches external APIs the
|
|
338
|
+
same way in `aai dev` and deployed.
|
|
339
|
+
|
|
340
|
+
### `ctx` (ToolContext)
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
ctx.env: Readonly<Record<string, string>> // secrets from .env / aai secret put
|
|
344
|
+
ctx.state: S // per-session mutable state (agent's `state` factory)
|
|
345
|
+
ctx.db: Db // SQL database, needs storage enabled (see Database section)
|
|
346
|
+
ctx.messages: readonly Message[] // conversation history [{role, content}]
|
|
347
|
+
ctx.sessionId: string // unique session ID
|
|
348
|
+
ctx.send(event: string, data: unknown): void // push custom event to browser client
|
|
349
|
+
ctx.generate(opts): Promise<{ text, object? }> // one-shot LLM call (host-side)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Typing `ctx.state` is optional.** `ctx.state` is untyped by default, and
|
|
353
|
+
the project's tsconfig turns off `noImplicitAny`, so both of these compile
|
|
354
|
+
with no annotations and no errors:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
ctx.state.count++;
|
|
358
|
+
ctx.state.incidents.filter((i) => i.status === "open");
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Write the code first. Do NOT add type annotations defensively — almost
|
|
362
|
+
nothing requires them, and time spent on them is time not spent on the agent.
|
|
363
|
+
|
|
364
|
+
**The one exception, and it is not optional: annotate any variable you
|
|
365
|
+
declare empty.** With `noImplicitAny` off, TypeScript does not widen an empty
|
|
366
|
+
initializer from what you later assign, so `[]` stays `never[]` and `null`
|
|
367
|
+
stays `null` — forever, whether or not a callback is involved:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
const items = []; // never[] → items.push(x) is an error
|
|
371
|
+
let best = null; // null → best = {...} is an error
|
|
372
|
+
const [picks, set] = useState([]); // never[] in a client, same thing
|
|
373
|
+
|
|
374
|
+
const items: Pick[] = []; // ✅ annotate the DECLARATION
|
|
375
|
+
let best: Pick | null = null; // ✅
|
|
376
|
+
const [picks, set] = useState<Pick[]>([]); // ✅
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Annotating the *use* instead does not help — the declaration is still wrong,
|
|
380
|
+
so the next push reports the next line, and you can burn a whole session
|
|
381
|
+
fixing one call site at a time. This is the single most common way a
|
|
382
|
+
generated agent fails to build.
|
|
383
|
+
|
|
384
|
+
Declaring a state type is still worth it once the shape is settled, because
|
|
385
|
+
it turns a misspelled field into a compile error instead of `undefined` at
|
|
386
|
+
runtime:
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import { agent, tool } from "@alexkroman1/aai";
|
|
390
|
+
import type { ToolContext } from "@alexkroman1/aai"; // types need `import type`
|
|
391
|
+
import { z } from "zod";
|
|
392
|
+
|
|
393
|
+
type Incident = { id: string; status: "open" | "closed" };
|
|
394
|
+
type State = { incidents: Incident[] };
|
|
395
|
+
|
|
396
|
+
const listOpen = tool({
|
|
397
|
+
description: "List open incidents",
|
|
398
|
+
execute: (_args, ctx: ToolContext<State>) => {
|
|
399
|
+
// `i` infers as Incident, and `i.staus` would now be an error.
|
|
400
|
+
return ctx.state.incidents.filter((i) => i.status === "open");
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
export default agent({
|
|
405
|
+
name: "Dispatch",
|
|
406
|
+
state: (): State => ({ incidents: [] }),
|
|
407
|
+
tools: { listOpen },
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
A tool annotated with a state shape the agent's factory doesn't produce is a
|
|
412
|
+
compile error, which is the point.
|
|
413
|
+
|
|
414
|
+
**`verbatimModuleSyntax` applies to every type you import** — `ToolContext`,
|
|
415
|
+
`ToolDef`, `Message`, provider types. A plain
|
|
416
|
+
`import { ToolContext }` fails; use `import type { ToolContext }`, or
|
|
417
|
+
`import { agent, type ToolContext }` to combine with value imports.
|
|
418
|
+
|
|
419
|
+
`ctx.generate({ prompt, system?, llm?, schema?, temperature?, maxOutputTokens? })`
|
|
420
|
+
runs one LLM generation on the host. It defaults to the agent's pipeline
|
|
421
|
+
`llm`; pass an `llm` descriptor (from `@alexkroman1/aai/llm`) to use another
|
|
422
|
+
provider whose API key is in the agent's secrets — that's also how S2S
|
|
423
|
+
agents use it. `schema` must be a **plain JSON Schema object** (use
|
|
424
|
+
`z.toJSONSchema(...)`), never a Zod schema.
|
|
425
|
+
|
|
426
|
+
### Inline tool example
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { agent, tool } from "@alexkroman1/aai";
|
|
430
|
+
import { z } from "zod";
|
|
431
|
+
|
|
432
|
+
export default agent({
|
|
433
|
+
name: "Weather Agent",
|
|
434
|
+
tools: {
|
|
435
|
+
get_weather: tool({
|
|
436
|
+
description: "Get current weather for a city",
|
|
437
|
+
parameters: z.object({
|
|
438
|
+
city: z.string().describe("City name"),
|
|
439
|
+
}),
|
|
440
|
+
async execute({ city }, ctx) {
|
|
441
|
+
const resp = await fetch(
|
|
442
|
+
`https://api.example.com/weather?q=${city}&key=${ctx.env.WEATHER_KEY}`,
|
|
443
|
+
);
|
|
444
|
+
return resp.json();
|
|
445
|
+
},
|
|
446
|
+
}),
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Calling the network builtins from your own tool code.** `web_search`,
|
|
452
|
+
`visit_webpage` and `fetch_json` are declared to the MODEL — the LLM calls
|
|
453
|
+
them, and they are not on `ctx`. When your own `execute` needs one, import
|
|
454
|
+
it:
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import { fetchJson, visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
458
|
+
|
|
459
|
+
execute: async ({ city }) => await fetchJson(`https://api.example.com/${city}`),
|
|
460
|
+
// Reading fields off the result needs no cast. Pass a shape when you want
|
|
461
|
+
// it checked: `await fetchJson<Forecast>(url)`.
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Same implementations the builtins use, so you get URL screening, credential-
|
|
465
|
+
header stripping, size caps and timeouts rather than a bare `fetch`. Plain
|
|
466
|
+
`fetch` still works when you want none of that. There is no callable
|
|
467
|
+
`run_code`: it exists to run code the model wrote, and tool code that wants
|
|
468
|
+
to compute something can just compute it.
|
|
469
|
+
|
|
470
|
+
**But prefer the BUILTIN when the model should decide.** These two are not
|
|
471
|
+
interchangeable:
|
|
472
|
+
|
|
473
|
+
- If the agent's job is to search or browse — a research assistant, anything
|
|
474
|
+
that follows a link the user mentions — declare
|
|
475
|
+
`builtinTools: ["web_search", "visit_webpage"]` and let the model call
|
|
476
|
+
them. It can then search several times with different queries, or read one
|
|
477
|
+
specific page, as the conversation needs.
|
|
478
|
+
- Import from `/tools` when YOUR tool's own logic needs a fetch: a currency
|
|
479
|
+
tool hitting one known API, a price checker with a fixed endpoint.
|
|
480
|
+
|
|
481
|
+
Wrapping `webSearch` in a single custom tool is the mistake to avoid — it
|
|
482
|
+
replaces "the model searches as needed" with one fixed query-and-summarize
|
|
483
|
+
pipeline, and no amount of prompting gets the flexibility back.
|
|
484
|
+
|
|
485
|
+
**`parameters` is a Zod object, or absent.** The field itself is optional,
|
|
486
|
+
but its VALUE must be a plain `z.object(...)` — so all of these are type
|
|
487
|
+
errors:
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
parameters: z.undefined(), // ✗ ZodUndefined
|
|
491
|
+
parameters: z.void(), // ✗
|
|
492
|
+
parameters: z.object({ q: z.string() }).optional(), // ✗ ZodOptional
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
For a tool with no arguments write `tool({ description, execute })`, or
|
|
496
|
+
`parameters: z.object({})` if you prefer it explicit. To make an individual
|
|
497
|
+
argument optional, put `.optional()` on the FIELD, never on the object:
|
|
498
|
+
`z.object({ notes: z.string().optional() })`.
|
|
499
|
+
|
|
500
|
+
**Do not annotate `execute`'s return type.** Nothing needs it — the result
|
|
501
|
+
is serialized to the model either way — and it reliably breaks the moment
|
|
502
|
+
the tool also returns an error, because `Promise<DrugInfo>` does not accept
|
|
503
|
+
`{ error: "not found" }`. Every such annotation eventually costs a build
|
|
504
|
+
round to widen into a union. Let it infer.
|
|
505
|
+
|
|
506
|
+
### Separate file pattern
|
|
507
|
+
|
|
508
|
+
For complex tools — `tools/` is a convention, any import path works:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
// tools/roll_dice.ts
|
|
512
|
+
import { tool } from "@alexkroman1/aai";
|
|
513
|
+
import { z } from "zod";
|
|
514
|
+
|
|
515
|
+
export const rollDice = tool({
|
|
516
|
+
description: "Roll dice",
|
|
517
|
+
parameters: z.object({ sides: z.number() }),
|
|
518
|
+
execute({ sides }) {
|
|
519
|
+
return Math.floor(Math.random() * sides) + 1;
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
// agent.ts
|
|
526
|
+
import { agent } from "@alexkroman1/aai";
|
|
527
|
+
import { rollDice } from "./tools/roll_dice.ts";
|
|
528
|
+
|
|
529
|
+
export default agent({
|
|
530
|
+
name: "Dice Agent",
|
|
531
|
+
tools: { roll_dice: rollDice },
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Built-in tools
|
|
536
|
+
|
|
537
|
+
Enable via `builtinTools` in `agent()`. When `builtinTools` is omitted, the
|
|
538
|
+
cognitive defaults (`think`, `remember`, `recall`, `calculate`) are enabled;
|
|
539
|
+
set `builtinTools` explicitly (including `[]`) to override.
|
|
540
|
+
|
|
541
|
+
| Tool | Description | Params |
|
|
542
|
+
| --- | --- | --- |
|
|
543
|
+
| `web_search` | Search the web (DuckDuckGo) — no API key required | `query`, `max_results?` (default 5) |
|
|
544
|
+
| `visit_webpage` | Fetch URL to plain text | `url` |
|
|
545
|
+
| `get_page_design` | Fetch URL's raw HTML + CSS (style blocks and linked stylesheets) to study/mimic a site's design | `url` |
|
|
546
|
+
| `fetch_json` | HTTP GET a JSON API | `url`, `headers?` |
|
|
547
|
+
| `run_code` | Execute JS in the agent's sandbox — same authority as the agent's own tool code, output is what it logs (5s timeout) | `code` |
|
|
548
|
+
| `think` | Private reasoning scratchpad, no side effects (on by default) | `thought` |
|
|
549
|
+
| `remember` | Save a confirmed fact to session notes (on by default) | `key`, `value` |
|
|
550
|
+
| `recall` | Read session notes saved with `remember` (on by default) | `key?` |
|
|
551
|
+
| `calculate` | Safe arithmetic evaluator, no code execution (on by default) | `expression` |
|
|
552
|
+
|
|
553
|
+
**Every builtin in this table is a tool the MODEL calls — not a function
|
|
554
|
+
your code can call.** Listing one in `builtinTools` adds it to the model's
|
|
555
|
+
tool set; it does not import anything into `agent.ts`. There is no
|
|
556
|
+
`fetch_json()` you can call from a tool's `execute`.
|
|
557
|
+
|
|
558
|
+
So the two ways to reach an API are genuinely different designs, and both
|
|
559
|
+
are valid:
|
|
560
|
+
|
|
561
|
+
- **Declare the builtin** (`builtinTools: ["fetch_json"]`) when the MODEL
|
|
562
|
+
should decide the URL and read the JSON — general lookups you cannot
|
|
563
|
+
enumerate ahead of time.
|
|
564
|
+
- **Write your own tool** whose `execute` calls `fetch` when YOU own the
|
|
565
|
+
URL and the shape — a specific endpoint, auth, or a response you want to
|
|
566
|
+
reshape before the model sees it.
|
|
567
|
+
|
|
568
|
+
The network builtins take model-controlled URLs, so they are SSRF-screened
|
|
569
|
+
when the runtime is not inside a container (private/loopback blocked). Your
|
|
570
|
+
own tool code has open egress either way.
|
|
571
|
+
|
|
572
|
+
## Calling an external API from your own tool code
|
|
573
|
+
|
|
574
|
+
`fetch` inside a tool's `execute` works directly — no declaration needed,
|
|
575
|
+
identical under `aai dev` and deployed. This is the right choice when your
|
|
576
|
+
code owns the URL.
|
|
577
|
+
|
|
578
|
+
Reaching for the `fetch_json` builtin instead is a different design, not a
|
|
579
|
+
shortcut for the same one: it hands URL choice to the model. You cannot
|
|
580
|
+
call it from `execute` — see the builtin table above.
|
|
581
|
+
|
|
582
|
+
## Database API — `ctx.db`
|
|
583
|
+
|
|
584
|
+
Persistent SQL storage scoped per app, backed by the app's own Postgres
|
|
585
|
+
schema. Access via `ctx.db`:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
ctx.db.query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
One parameterized statement per call, `$1, $2…` placeholders — never
|
|
592
|
+
interpolate values into the SQL string. The rows come back as plain objects;
|
|
593
|
+
`jsonb` columns are returned already parsed (no `JSON.parse` needed).
|
|
594
|
+
A query returning more than 1000 rows throws — always bound reads with
|
|
595
|
+
`LIMIT` (paginate with `LIMIT`/`OFFSET`).
|
|
596
|
+
|
|
597
|
+
**Storage must be enabled** or accessing `ctx.db` throws:
|
|
598
|
+
|
|
599
|
+
- CLI: `aai storage enable`
|
|
600
|
+
- Studio: the Storage toggle
|
|
601
|
+
- `aai dev`: set `DATABASE_URL` in the project `.env`
|
|
602
|
+
|
|
603
|
+
Create tables lazily from tool code and upsert with `on conflict`:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
await ctx.db.query(`create table if not exists app_state (
|
|
607
|
+
key text primary key,
|
|
608
|
+
value jsonb not null,
|
|
609
|
+
updated_at timestamptz not null default now()
|
|
610
|
+
)`);
|
|
611
|
+
await ctx.db.query(
|
|
612
|
+
"insert into app_state (key, value, updated_at) values ($1, $2::jsonb, now()) " +
|
|
613
|
+
"on conflict (key) do update set value = excluded.value, updated_at = now()",
|
|
614
|
+
["user:123", JSON.stringify({ name: "Alex" })],
|
|
615
|
+
);
|
|
616
|
+
const rows = await ctx.db.query<{ value: { name: string } }>(
|
|
617
|
+
"select value from app_state where key = $1",
|
|
618
|
+
["user:123"],
|
|
619
|
+
);
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
Use `ctx.db` for data that must outlive the session (saves, filed records,
|
|
623
|
+
user profiles). For scratch that only the current session needs, prefer
|
|
624
|
+
`ctx.state` (per-session mutable state — no storage required); the
|
|
625
|
+
`remember`/`recall` builtins likewise remain for session-scoped notes the
|
|
626
|
+
LLM manages itself.
|
|
627
|
+
|
|
628
|
+
## Custom UI — `client()`
|
|
629
|
+
|
|
630
|
+
File: `client.tsx` alongside `agent.ts`. Uses **React** (not Preact).
|
|
631
|
+
Always import `"@alexkroman1/aai-ui/styles.css"` first.
|
|
632
|
+
|
|
633
|
+
### Tier 1 — config only (default UI)
|
|
634
|
+
|
|
635
|
+
```tsx
|
|
636
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
637
|
+
import { client } from "@alexkroman1/aai-ui";
|
|
638
|
+
|
|
639
|
+
client({ name: "My Agent" });
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Tier 1 with sidebar
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
646
|
+
import { client, useEvent } from "@alexkroman1/aai-ui";
|
|
647
|
+
import { useState } from "react";
|
|
648
|
+
|
|
649
|
+
function Sidebar() {
|
|
650
|
+
const [items, setItems] = useState<string[]>([]);
|
|
651
|
+
useEvent<{ item: string }>("new_item", (data) => {
|
|
652
|
+
setItems((prev) => [...prev, data.item]);
|
|
653
|
+
});
|
|
654
|
+
return (
|
|
655
|
+
<div className="p-4">
|
|
656
|
+
{items.map((it, i) => <p key={i}>{it}</p>)}
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
client({ name: "My Agent", sidebar: Sidebar });
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Tier 2 — full custom component
|
|
665
|
+
|
|
666
|
+
```tsx
|
|
667
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
668
|
+
import { client, useSession } from "@alexkroman1/aai-ui";
|
|
669
|
+
|
|
670
|
+
function MyApp() {
|
|
671
|
+
const { messages, userTranscript, started, running, start, toggle, reset } =
|
|
672
|
+
useSession();
|
|
673
|
+
return (
|
|
674
|
+
<div>
|
|
675
|
+
{messages.map((m, i) => <p key={i}>{m.content}</p>)}
|
|
676
|
+
{userTranscript != null && <p>{userTranscript || "..."}</p>}
|
|
677
|
+
{!started ? (
|
|
678
|
+
<button onClick={start}>Start</button>
|
|
679
|
+
) : (
|
|
680
|
+
<>
|
|
681
|
+
<button onClick={toggle}>{running ? "Stop" : "Resume"}</button>
|
|
682
|
+
<button onClick={reset}>Reset</button>
|
|
683
|
+
</>
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
client({ component: MyApp });
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### `client()` config
|
|
693
|
+
|
|
694
|
+
| Field | Type | Default | Description |
|
|
695
|
+
| --- | --- | --- | --- |
|
|
696
|
+
| `name` | `string` | — | Header/start screen title (tier 1) |
|
|
697
|
+
| `component` | `ComponentType` | — | Custom root component (tier 2) |
|
|
698
|
+
| `sidebar` | `ComponentType` | — | Sidebar alongside default chat (tier 1) |
|
|
699
|
+
| `sidebarWidth` | `string` | `"18rem"` | CSS width of sidebar |
|
|
700
|
+
| `theme` | `ClientTheme` | — | `{ bg, primary, text, surface, border }` |
|
|
701
|
+
| `target` | `string \| HTMLElement` | `"#app"` | Mount target |
|
|
702
|
+
| `tools` | `ToolDisplayConfig` | — | Icon/label overrides per tool name |
|
|
703
|
+
|
|
704
|
+
**The two tiers are mostly exclusive.** `sidebar`, `sidebarWidth`, and
|
|
705
|
+
`tools` configure the default shell, so passing any of them alongside
|
|
706
|
+
`component` is a type error. `name` is the exception — it is allowed with a
|
|
707
|
+
custom component and becomes the page title, since there is no shell header
|
|
708
|
+
to put it in.
|
|
709
|
+
|
|
710
|
+
### `useSession()` return type
|
|
711
|
+
|
|
712
|
+
| Field | Type | Description |
|
|
713
|
+
| --- | --- | --- |
|
|
714
|
+
| `state` | `AgentState` | `"disconnected"` `"connecting"` `"ready"` `"listening"` `"thinking"` `"speaking"` `"error"` |
|
|
715
|
+
| `messages` | `ChatMessage[]` | `{ role, content }` |
|
|
716
|
+
| `toolCalls` | `ToolCallInfo[]` | `{ callId, name, args, status, result? }` |
|
|
717
|
+
| `customEvents` | `CustomEvent[]` | `{ id, event, data }` from `ctx.send()` |
|
|
718
|
+
| `userTranscript` | `string \| null` | `null` = not speaking, `""` = speech detected, string = text |
|
|
719
|
+
| `agentTranscript` | `string \| null` | `null` = not speaking, string = streaming response |
|
|
720
|
+
| `error` | `SessionError \| null` | `{ code, message }` |
|
|
721
|
+
| `started` | `boolean` | Whether session started |
|
|
722
|
+
| `running` | `boolean` | Whether session active |
|
|
723
|
+
|
|
724
|
+
Methods: `start()`, `toggle()`, `reset()`, `cancel()`, `disconnect()`,
|
|
725
|
+
`resetState()`.
|
|
726
|
+
|
|
727
|
+
## UI hooks
|
|
728
|
+
|
|
729
|
+
**`useToolResult`** — fires once per completed tool call (deduplicates by
|
|
730
|
+
callId):
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
useToolResult("tool_name", (result, toolCall) => { ... }) // one tool
|
|
734
|
+
useToolResult((toolName, result, toolCall) => { ... }) // all tools
|
|
735
|
+
useToolResult<ResultType>("tool_name", (result) => { ... }) // typed (optional)
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
`result` is the tool's return value, already JSON-parsed and untyped — read
|
|
739
|
+
fields off it directly (`result.price`). The type parameter is optional; add
|
|
740
|
+
it only when you want the shape checked.
|
|
741
|
+
|
|
742
|
+
**There is no global `JSX` namespace.** React 19 removed it, so
|
|
743
|
+
`JSX.Element` is `Cannot find namespace 'JSX'` (`TS2503`). Type a component's
|
|
744
|
+
return as `ReactNode` — `import type { ReactNode } from "react"` — which is
|
|
745
|
+
also what you want for anything that can be a string, an array, or null.
|
|
746
|
+
|
|
747
|
+
**`useAgentState`** — the agent's session state, pushed automatically:
|
|
748
|
+
|
|
749
|
+
```ts
|
|
750
|
+
// agent.ts
|
|
751
|
+
export default agent({
|
|
752
|
+
state: () => ({ cart: [] as Item[], staffPin: "" }),
|
|
753
|
+
syncState: (s) => ({ cart: s.cart }), // staffPin never leaves the server
|
|
754
|
+
tools: { ... },
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// client.tsx
|
|
758
|
+
const view = useAgentState<{ cart: Item[] }>(); // null until the first push
|
|
759
|
+
return <Cart items={view?.cart ?? []} />;
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
**Reach for this before wiring `useToolResult` into `useState`.** Without
|
|
763
|
+
it the pattern is: return a cart snapshot from every tool, declare a type
|
|
764
|
+
describing what those tools return, and mirror it into `useState` — three
|
|
765
|
+
things to keep in step, and the usual source of drift when you add a tool
|
|
766
|
+
and forget to return the snapshot from it.
|
|
767
|
+
|
|
768
|
+
`syncState` is a projection, not a flag, because state often holds things
|
|
769
|
+
that should not reach a browser (keys, PINs, scratch) or cannot be
|
|
770
|
+
serialized. Whatever it returns is exactly what the client receives. It runs
|
|
771
|
+
after every tool call and is sent only when the result changed.
|
|
772
|
+
|
|
773
|
+
**`useEvent`** — fires for custom events from `ctx.send()`:
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
useEvent<DataType>("event_name", (data) => { ... })
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
Server: `ctx.send("order", { total: "$14.99" })` —
|
|
780
|
+
Client: `useEvent("order", (data) => ...)`.
|
|
781
|
+
|
|
782
|
+
**`useTheme`** — returns `{ bg, primary, text, surface, border }`.
|
|
783
|
+
|
|
784
|
+
**`useToolCallStart`** — fires when a tool call begins (status `"pending"`).
|
|
785
|
+
|
|
786
|
+
**Anti-pattern:** Do NOT use `useEffect` + `toolCalls` to build derived
|
|
787
|
+
state. Use `useToolResult` — it deduplicates. The `useEffect` pattern
|
|
788
|
+
re-processes every tool call on every render, causing duplicates.
|
|
789
|
+
|
|
790
|
+
## Components
|
|
791
|
+
|
|
792
|
+
Available from `@alexkroman1/aai-ui`:
|
|
793
|
+
|
|
794
|
+
| Component | Props | Description |
|
|
795
|
+
| --- | --- | --- |
|
|
796
|
+
| `StartScreen` | `children, icon?, title?, subtitle?, buttonText?` | Centered start card; renders children after start |
|
|
797
|
+
| `ChatView` | `icon?, title?` | Chat interface (header + messages + controls) |
|
|
798
|
+
| `SidebarLayout` | `sidebar, children, sidebarWidth?, sidebarPosition?` | Two-column layout |
|
|
799
|
+
| `MessageList` | — | Messages with auto-scroll, tool calls, transcript |
|
|
800
|
+
| `Controls` | — | Stop/Resume + New Conversation buttons |
|
|
801
|
+
| `Button` | — | Styled button |
|
|
802
|
+
|
|
803
|
+
## Styling
|
|
804
|
+
|
|
805
|
+
- **Tailwind CSS v4** — compiled at bundle time, configured via CSS.
|
|
806
|
+
Do NOT create `tailwind.config.js` — it will be ignored.
|
|
807
|
+
- Use Tailwind classes for layout, `useTheme()` for dynamic colors.
|
|
808
|
+
- Set theme: `client({ theme: { bg, primary, text, surface, border } })`.
|
|
809
|
+
- Override CSS custom properties for extra tokens:
|
|
810
|
+
`--color-aai-*`, `--radius-aai`, `--font-aai`.
|
|
811
|
+
- Always import `"@alexkroman1/aai-ui/styles.css"` at the top of `client.tsx`.
|
|
812
|
+
|
|
813
|
+
### Design guidelines
|
|
814
|
+
|
|
815
|
+
A custom UI should look deliberate, not like boilerplate. When building or
|
|
816
|
+
restyling a `client.tsx`:
|
|
817
|
+
|
|
818
|
+
- **Color:** pick one primary brand color, 2-3 neutrals (white/grays/black
|
|
819
|
+
variants), and at most 1-2 accents — 3-5 colors total. Avoid gradients
|
|
820
|
+
unless asked. If you override an element's background color, also set its
|
|
821
|
+
text color so contrast holds.
|
|
822
|
+
- **Typography:** at most 2 font families — one for headings, one for body.
|
|
823
|
+
Body text 14px or larger with a relaxed line height (`leading-relaxed`).
|
|
824
|
+
- **Layout:** design mobile-first, then enhance with responsive prefixes
|
|
825
|
+
(`md:`, `lg:`). Prefer flexbox (`flex items-center justify-between`);
|
|
826
|
+
use grid only for genuinely two-dimensional layouts; avoid absolute
|
|
827
|
+
positioning unless nothing else works.
|
|
828
|
+
- **Tailwind:** stay on the spacing scale (`p-4`, never `p-[16px]`), use
|
|
829
|
+
`gap-*` between siblings rather than per-child margins, and wrap headings
|
|
830
|
+
and key copy in `text-balance` or `text-pretty`.
|
|
831
|
+
- **Accessibility:** semantic elements (`main`, `header`, `button`), alt
|
|
832
|
+
text on meaningful images, `sr-only` labels on icon-only buttons.
|
|
833
|
+
- **No filler:** no emojis as icons, no decorative gradient blobs or
|
|
834
|
+
abstract placeholder shapes, no lorem-ipsum-looking content.
|
|
835
|
+
|
|
836
|
+
## Secrets
|
|
837
|
+
|
|
838
|
+
Never hardcode secrets in agent code.
|
|
839
|
+
|
|
840
|
+
- **Local dev:** `.env` in project root. Only declared keys available via
|
|
841
|
+
`ctx.env`.
|
|
842
|
+
- **Production:** `npx @alexkroman1/aai-cli secret put NAME`
|
|
843
|
+
- **Access:** `ctx.env.MY_KEY` in tool execute functions.
|
|
844
|
+
- **AssemblyAI key:** CLI prompts on first use, stores globally. No `.env`
|
|
845
|
+
entry needed. For CI, set `ASSEMBLYAI_API_KEY` env var.
|
|
846
|
+
|
|
847
|
+
## Voice rules for systemPrompt
|
|
848
|
+
|
|
849
|
+
- Short, punchy sentences — optimize for speech, not text
|
|
850
|
+
- Never mention "search results" or "sources" — speak as if knowledge is
|
|
851
|
+
your own
|
|
852
|
+
- No visual formatting (bullets, bold) — use "First", "Next", "Finally"
|
|
853
|
+
- Lead with the most important information
|
|
854
|
+
- Keep answers to 1-3 sentences
|
|
855
|
+
- No exclamation points — calm, conversational tone
|
|
856
|
+
- No hedging ("It seems that", "I believe")
|
|
857
|
+
- Define personality, tone, and specialty
|
|
858
|
+
- Include when and how to use each tool
|
|
859
|
+
|
|
860
|
+
Patterns by agent type:
|
|
861
|
+
|
|
862
|
+
- **Code execution:** "You MUST use run_code for ANY math, counting, or
|
|
863
|
+
data processing. NEVER do mental math."
|
|
864
|
+
- **Research:** "Search first. Never guess or rely on memory for factual
|
|
865
|
+
questions."
|
|
866
|
+
- **FAQ/support:** "Base answers strictly on your knowledge — don't guess."
|
|
867
|
+
- **Game/interactive:** "You ARE the game. Keep descriptions to 2-4
|
|
868
|
+
sentences. No visual formatting."
|
|
869
|
+
|
|
870
|
+
## Gotchas
|
|
871
|
+
|
|
872
|
+
Common mistakes when working in aai projects:
|
|
873
|
+
|
|
874
|
+
- **Tool execute must return a value.** A missing return = `undefined` in
|
|
875
|
+
LLM context = the model thinks the tool failed.
|
|
876
|
+
- **Filter large API responses before returning them from tools.** Return
|
|
877
|
+
values are injected into LLM context. Truncate, summarize, or extract
|
|
878
|
+
only what the model needs.
|
|
879
|
+
- **Pipeline mode requires all three of `stt` / `llm` / `tts`.** Partial
|
|
880
|
+
configs are rejected at parse time. Use S2S (omit all three) if you
|
|
881
|
+
don't need provider control.
|
|
882
|
+
- **Never hardcode secrets.** Use `ctx.env.MY_KEY`. `.env` for local dev,
|
|
883
|
+
`aai secret put` for production.
|
|
884
|
+
- **Don't use `useEffect` + `toolCalls` to derive state.** Use
|
|
885
|
+
`useToolResult` — it deduplicates by callId. The useEffect pattern
|
|
886
|
+
re-fires on every render and produces duplicates.
|
|
887
|
+
- **Always import `"@alexkroman1/aai-ui/styles.css"` first** in
|
|
888
|
+
`client.tsx`. Missing this = unstyled UI.
|
|
889
|
+
- **Don't create `tailwind.config.js`.** Tailwind v4 is configured via
|
|
890
|
+
CSS; the config file is ignored.
|
|
891
|
+
- **Voice prompts ≠ chat prompts.** No bullets, no bold, no exclamation
|
|
892
|
+
points. See "Voice rules" above.
|
|
893
|
+
- **`fetch` to private IPs is blocked** (SSRF protection). Use public URLs.
|
|
894
|
+
- **`run_code` only executes on the deployed platform.** It runs inside the
|
|
895
|
+
platform's Modal/Deno sandbox; the self-hosted `aai dev` server has no
|
|
896
|
+
sandbox, so there `run_code` refuses with an error result. Deploy to test
|
|
897
|
+
it end-to-end, or use the `calculate` builtin for simple arithmetic in dev.
|
|
898
|
+
- **`ctx.db` throws until storage is enabled.** Enable it with
|
|
899
|
+
`aai storage enable` (CLI), the Storage toggle (studio), or `DATABASE_URL`
|
|
900
|
+
in `.env` (`aai dev`) before shipping tools that persist data.
|
|
901
|
+
- **The database is per-app.** Rows are shared by every session of one
|
|
902
|
+
deployment — key them yourself if sessions must not see each other's data
|
|
903
|
+
(or keep session-scoped data in `ctx.state`).
|
|
904
|
+
- **Rime language codes are ISO 639-3** (3-letter, e.g. `"eng"`), not
|
|
905
|
+
ISO 639-1 (`"en"`).
|
|
906
|
+
|
|
907
|
+
## Constraints
|
|
908
|
+
|
|
909
|
+
- Tool `execute` return values go into LLM context — filter and truncate
|
|
910
|
+
large API responses
|
|
911
|
+
- `fetch` is proxied through the host; private/internal IPs are blocked
|
|
912
|
+
(SSRF protection)
|
|
913
|
+
- Agent code runs in a sandboxed worker — use `fetch` for HTTP, `ctx.env`
|
|
914
|
+
for secrets
|
|
915
|
+
- Tool execution timeout: 30 seconds
|
|
916
|
+
- `maxSteps` limits tool calls per turn (default 10) — increase for
|
|
917
|
+
multi-tool workflows
|
|
918
|
+
- Tool returns `undefined` if execute function has no return statement —
|
|
919
|
+
always return a value
|