@alexkroman1/aai-cli 5.2.0 → 5.4.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/README.md +51 -0
- package/dist/{_bundler-DFS4xxqE.mjs → _bundler-Cjaxa2wi.mjs} +1 -1
- package/dist/{_dev-server-BBUWo0sb.mjs → _dev-server-cigzayJe.mjs} +2 -2
- package/dist/{_init-DU-sXH6S.mjs → _init-_G8l4lh2.mjs} +2 -55
- package/dist/_templates-DW9CvuDN.mjs +69 -0
- package/dist/_templates.d.ts +6 -0
- package/dist/{build-Xq1xxoSg.mjs → build-D_PgQOD4.mjs} +1 -1
- package/dist/cli.mjs +35 -10
- package/dist/{client-bundler-BWDkUeEP.mjs → client-bundler-yiWoXrgb.mjs} +3 -0
- package/dist/client-bundler.d.ts +9 -0
- package/dist/client-bundler.mjs +1 -1
- package/dist/{deploy-Ci0X1gp2.mjs → deploy-C0VLnnJe.mjs} +1 -1
- package/dist/{dev-g7sGt8qX.mjs → dev-2oUsvgRX.mjs} +1 -1
- package/dist/{init-BjeK8crW.mjs → init-BkW9BuZF.mjs} +2 -2
- package/dist/scaffold/CLAUDE.md +174 -101
- package/dist/scaffold/package.json +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +3 -3
- package/dist/templates/dispatch-center/agent.ts +2 -2
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_create.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_get.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_triage.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +1 -1
- package/dist/templates/embedded-assets/agent.ts +1 -1
- package/dist/templates/health-assistant/agent.ts +2 -2
- package/dist/templates/infocom-adventure/agent.ts +7 -7
- package/dist/templates/math-buddy/agent.ts +1 -1
- package/dist/templates/night-owl/agent.ts +1 -1
- package/dist/templates/pizza-ordering/agent.test.ts +13 -11
- package/dist/templates/pizza-ordering/agent.ts +4 -4
- package/dist/templates/simple/agent.test.ts +18 -0
- package/dist/templates/simple/agent.ts +2 -0
- package/dist/templates/solo-rpg/agent.test.ts +2 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +1 -1
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +1 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +1 -1
- package/dist/typecheck.d.ts +18 -0
- package/dist/typecheck.mjs +8 -0
- package/dist/worker-bundler.d.ts +9 -1
- package/dist/worker-bundler.mjs +8 -0
- package/package.json +3 -3
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -11,8 +11,9 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
|
11
11
|
agent to verify behavior end-to-end. This is the primary feedback loop.
|
|
12
12
|
2. **Run `pnpm test` after logic changes** — vitest. Co-locate tests as
|
|
13
13
|
`agent.test.ts` (see `pipeline-simple` template for a reference).
|
|
14
|
-
**
|
|
15
|
-
|
|
14
|
+
**When the project has an `agent.test.ts` (the default `simple`
|
|
15
|
+
template and several others ship one), it is yours to maintain.** It
|
|
16
|
+
asserts the agent's shape — name, providers, tool names —
|
|
16
17
|
so rewriting the agent without updating it leaves a test asserting an
|
|
17
18
|
agent that no longer exists. When a test fails after your change, decide
|
|
18
19
|
which side is stale: updating the test to match the new agent is a normal
|
|
@@ -26,17 +27,21 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
|
26
27
|
`node_modules/@alexkroman1/aai-cli/dist/templates/`. Read them directly;
|
|
27
28
|
`aai init --template <name>` scaffolds a fresh project from one. Closest
|
|
28
29
|
matches: `simple`, `pipeline-simple`, `web-researcher`, `solo-rpg`,
|
|
29
|
-
`pizza-ordering`.
|
|
30
|
+
`pizza-ordering`. When reading SDK types under
|
|
31
|
+
`node_modules/@alexkroman1/aai*/dist/`, note the built entry points
|
|
32
|
+
re-export with source specifiers (`"./sdk/constants.ts"`,
|
|
33
|
+
`"./components/button.tsx"`) — rewrite `.ts`/`.tsx` to `.d.ts` to find
|
|
34
|
+
the shipped file.
|
|
30
35
|
|
|
31
36
|
## CLI
|
|
32
37
|
|
|
33
38
|
```sh
|
|
34
39
|
npx @alexkroman1/aai-cli init # Scaffold a new agent
|
|
40
|
+
npx @alexkroman1/aai-cli templates # List available templates
|
|
35
41
|
npx @alexkroman1/aai-cli dev # Start local dev server
|
|
36
42
|
npx @alexkroman1/aai-cli test # Run agent.test.ts via vitest
|
|
37
43
|
npx @alexkroman1/aai-cli build # Bundle and validate
|
|
38
44
|
npx @alexkroman1/aai-cli deploy # Deploy to production
|
|
39
|
-
npx @alexkroman1/aai-cli deploy -y # Deploy without prompts
|
|
40
45
|
npx @alexkroman1/aai-cli delete # Remove deployed agent
|
|
41
46
|
npx @alexkroman1/aai-cli secret put NAME # Set a secret
|
|
42
47
|
npx @alexkroman1/aai-cli secret delete NAME
|
|
@@ -44,7 +49,7 @@ npx @alexkroman1/aai-cli secret list
|
|
|
44
49
|
```
|
|
45
50
|
|
|
46
51
|
The scaffold's `package.json` exposes `dev`, `build`, `test`, and `deploy`
|
|
47
|
-
as `pnpm <name>` shortcuts. Other commands (`init`, `delete`, `secret`)
|
|
52
|
+
as `pnpm <name>` shortcuts. Other commands (`init`, `templates`, `delete`, `secret`)
|
|
48
53
|
are CLI-only.
|
|
49
54
|
|
|
50
55
|
## Project structure
|
|
@@ -64,23 +69,25 @@ my-agent/
|
|
|
64
69
|
|
|
65
70
|
## `agent()` API
|
|
66
71
|
|
|
67
|
-
```ts
|
|
72
|
+
```ts no-check
|
|
68
73
|
import { agent } from "@alexkroman1/aai";
|
|
69
74
|
|
|
70
75
|
export default agent({
|
|
71
76
|
name: string; // required — display name
|
|
72
77
|
systemPrompt?: string; // default: general voice assistant
|
|
78
|
+
// (`system` is an accepted alias)
|
|
73
79
|
greeting?: string; // default: "Hey there..."
|
|
74
80
|
sttPrompt?: string; // STT guidance for jargon/acronyms
|
|
75
81
|
builtinTools?: BuiltinTool[]; // see built-in tools table
|
|
76
82
|
tools?: Record<string, ToolDef>;
|
|
77
83
|
maxSteps?: number; // default: 10 — max tool calls per turn
|
|
78
|
-
toolChoice?: "auto" | "required"
|
|
84
|
+
toolChoice?: ToolChoice; // "auto" (default) | "required" | "none"
|
|
85
|
+
// | { type: "tool", toolName }
|
|
79
86
|
idleTimeoutMs?: number; // disconnect after inactivity (ms)
|
|
80
87
|
silenceTimeoutMs?: number; // pipeline only — assistant speaks up after this much user silence (ms)
|
|
81
88
|
silencePrompt?: string; // instruction injected on silence timeout (requires silenceTimeoutMs)
|
|
82
89
|
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
|
|
90
|
+
interruptionMinDurationMs?: number; // pipeline only — sustained speech (ms) before an interim barge-in interrupts (default 500; 0 disables)
|
|
84
91
|
holdPhrase?: string; // pipeline only — spoken before a silent tool-call turn (default "One moment."; "" disables)
|
|
85
92
|
falseInterruptionTimeoutMs?: number; // pipeline only — resume an interrupted reply if no user turn commits (default 2000; 0 disables)
|
|
86
93
|
state?: () => S; // per-session mutable state, exposed as ctx.state
|
|
@@ -91,7 +98,10 @@ export default agent({
|
|
|
91
98
|
```
|
|
92
99
|
|
|
93
100
|
> When `stt`, `llm`, and `tts` are all provided, the agent runs in
|
|
94
|
-
> **Pipeline mode** — see the section below.
|
|
101
|
+
> **Pipeline mode** — see the section below. `llm` also accepts a model-id
|
|
102
|
+
> string: `"creator/model"` routes through the Vercel AI Gateway
|
|
103
|
+
> (`AI_GATEWAY_API_KEY`), a bare id through the AssemblyAI LLM Gateway
|
|
104
|
+
> (`ASSEMBLYAI_API_KEY`).
|
|
95
105
|
|
|
96
106
|
Minimal agent — a cascaded pipeline, which is what you should build unless
|
|
97
107
|
the user asks for the speech-to-speech API:
|
|
@@ -111,7 +121,7 @@ by setting it after the spread — everything else stays as the preset put it:
|
|
|
111
121
|
|
|
112
122
|
```ts
|
|
113
123
|
import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
|
|
114
|
-
import {
|
|
124
|
+
import { assemblyAITts } from "@alexkroman1/aai/tts";
|
|
115
125
|
|
|
116
126
|
export default agent({
|
|
117
127
|
name: "My Agent",
|
|
@@ -120,11 +130,27 @@ export default agent({
|
|
|
120
130
|
});
|
|
121
131
|
```
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
The same pattern changes the gateway LLM model — `llm` accepts the model id
|
|
134
|
+
as a plain string:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { agent, assemblyAIPipeline } from "@alexkroman1/aai";
|
|
138
|
+
|
|
139
|
+
export default agent({
|
|
140
|
+
name: "My Agent",
|
|
141
|
+
...assemblyAIPipeline(),
|
|
142
|
+
llm: "claude-sonnet-4-6",
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`agent({ name })` alone is legal and equivalent — an agent that declares no
|
|
147
|
+
providers gets `assemblyAIPipeline()` injected as the default. Speech-to-speech
|
|
148
|
+
(S2S) mode is an explicit opt-in via the `s2s` field — see below.
|
|
124
149
|
|
|
125
150
|
System prompt from file:
|
|
126
151
|
|
|
127
152
|
```ts
|
|
153
|
+
/// <reference types="vite/client" />
|
|
128
154
|
import { agent } from "@alexkroman1/aai";
|
|
129
155
|
import systemPrompt from "./system-prompt.md?raw";
|
|
130
156
|
export default agent({ name: "My Agent", systemPrompt });
|
|
@@ -138,14 +164,32 @@ explicit the modern spelling is `with { type: "json" }`, but plain is fine.
|
|
|
138
164
|
|
|
139
165
|
## Pipeline mode
|
|
140
166
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
Pipeline mode is the default: omitting `stt`/`llm`/`tts` (and `s2s`) gives
|
|
168
|
+
you the all-AssemblyAI pipeline, exactly as if you had spread
|
|
169
|
+
`assemblyAIPipeline()` yourself.
|
|
144
170
|
|
|
145
|
-
**
|
|
171
|
+
**S2S mode is an explicit opt-in.** Setting `s2s: assemblyAIS2s()` (imported
|
|
172
|
+
from `@alexkroman1/aai`, next to `agent()`) selects AssemblyAI's
|
|
173
|
+
speech-to-speech Voice Agent API: STT, the LLM loop, and TTS run
|
|
174
|
+
service-side in one socket. Fewer moving parts, but you cannot choose the
|
|
175
|
+
model, swap a provider, or tune a stage. There is no way to reach S2S by
|
|
176
|
+
omission — only the `s2s` field selects it, and it is mutually exclusive
|
|
177
|
+
with the `stt`/`llm`/`tts` triple.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { agent, assemblyAIS2s } from "@alexkroman1/aai";
|
|
181
|
+
|
|
182
|
+
export default agent({
|
|
183
|
+
name: "My Agent",
|
|
184
|
+
s2s: assemblyAIS2s(),
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Prefer pipeline mode** — the default — unless the user specifically
|
|
146
189
|
asks for the speech-to-speech API. Nearly every template ships this way, and
|
|
147
190
|
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
|
|
191
|
+
(Vercel AI SDK) with your chosen STT, LLM, and TTS. You want explicit
|
|
192
|
+
providers when:
|
|
149
193
|
|
|
150
194
|
- you want a specific LLM (Anthropic, OpenAI, Gemini, Mistral, xAI, Groq,
|
|
151
195
|
hundreds of models via OpenRouter, or 25+ models via the AssemblyAI
|
|
@@ -153,18 +197,21 @@ it is what the App Builder defaults to. The host runs the LLM loop locally
|
|
|
153
197
|
- you want a specific STT model or TTS voice
|
|
154
198
|
- you need to swap providers without changing agent code
|
|
155
199
|
|
|
156
|
-
**The rule:** set all three of `stt`, `llm`, `tts` together, or none.
|
|
157
|
-
partial
|
|
200
|
+
**The rule:** set all three of `stt`, `llm`, `tts` together, or none.
|
|
201
|
+
`agent()`'s parameter type enforces this — a partial triple is a compile
|
|
202
|
+
error ("missing: llm, tts"), as is combining `s2s` with any pipeline
|
|
203
|
+
provider or pipeline-only tuning field. A raw config that skips `agent()`
|
|
204
|
+
is still rejected at parse time.
|
|
158
205
|
|
|
159
206
|
```ts
|
|
160
207
|
import { agent } from "@alexkroman1/aai";
|
|
161
|
-
import {
|
|
208
|
+
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
162
209
|
import { anthropic } from "@alexkroman1/aai/llm";
|
|
163
210
|
import { cartesia } from "@alexkroman1/aai/tts";
|
|
164
211
|
|
|
165
212
|
export default agent({
|
|
166
213
|
name: "My Agent",
|
|
167
|
-
stt:
|
|
214
|
+
stt: assemblyAIStt({ model: "universal-3-5-pro" }),
|
|
168
215
|
llm: anthropic({ model: "claude-haiku-4-5" }),
|
|
169
216
|
tts: cartesia(),
|
|
170
217
|
});
|
|
@@ -186,11 +233,12 @@ user speaks again.
|
|
|
186
233
|
**Voice-UX tuning (pipeline only):** `minBargeInWords` controls how many
|
|
187
234
|
words of user speech interrupt the assistant mid-reply (default 2, so
|
|
188
235
|
one-word backchannels like "yeah" don't cut it off);
|
|
189
|
-
`interruptionMinDurationMs` adds
|
|
190
|
-
|
|
191
|
-
detection (how long a pause ends the user's turn)
|
|
192
|
-
provider: `
|
|
193
|
-
|
|
236
|
+
`interruptionMinDurationMs` adds a sustained-speech gate on top (default
|
|
237
|
+
500 ms; `0` disables; interim transcripts only — committed turns always
|
|
238
|
+
land). End-of-turn detection (how long a pause ends the user's turn)
|
|
239
|
+
belongs to the STT provider: `assemblyAIStt({ minTurnSilenceMs })` (default
|
|
240
|
+
2000 ms) / `deepgram({ endpointing })` (default 1500 ms), so mid-utterance
|
|
241
|
+
pauses don't split a request.
|
|
194
242
|
`holdPhrase` is spoken when a turn opens with a tool call and no speech.
|
|
195
243
|
`falseInterruptionTimeoutMs` resumes an interrupted reply when a barge-in
|
|
196
244
|
turns out to be noise (no user turn commits within the window).
|
|
@@ -202,34 +250,34 @@ for the providers you actually use.
|
|
|
202
250
|
|
|
203
251
|
### STT — `@alexkroman1/aai/stt`
|
|
204
252
|
|
|
205
|
-
| Factory
|
|
206
|
-
|
|
|
207
|
-
| `
|
|
208
|
-
| `deepgram`
|
|
209
|
-
| `elevenlabs`
|
|
210
|
-
| `soniox`
|
|
253
|
+
| Factory | Default model | Env var |
|
|
254
|
+
| --------------- | ---------------------- | -------------------- |
|
|
255
|
+
| `assemblyAIStt` | `"universal-3-5-pro"` | `ASSEMBLYAI_API_KEY` |
|
|
256
|
+
| `deepgram` | `"nova-3"` | `DEEPGRAM_API_KEY` |
|
|
257
|
+
| `elevenlabs` | `"scribe_v2_realtime"` | `ELEVENLABS_API_KEY` |
|
|
258
|
+
| `soniox` | `"stt-rt-v3"` | `SONIOX_API_KEY` |
|
|
211
259
|
|
|
212
260
|
All STT factories accept `{ model?: string, ... }`. Bare calls
|
|
213
261
|
(`deepgram()`, `soniox()`, etc.) use the default model.
|
|
214
262
|
|
|
215
|
-
`
|
|
263
|
+
`assemblyAIStt` accepts an optional `region: "eu"` for EU data residency —
|
|
216
264
|
it routes streaming transcription to AssemblyAI's EU endpoints. EU-region
|
|
217
265
|
API keys require it; the US endpoints reject them. Example:
|
|
218
|
-
`
|
|
266
|
+
`assemblyAIStt({ model: "universal-3-5-pro", region: "eu" })`.
|
|
219
267
|
|
|
220
268
|
### LLM — `@alexkroman1/aai/llm`
|
|
221
269
|
|
|
222
|
-
| Factory
|
|
223
|
-
|
|
|
224
|
-
| `anthropic`
|
|
225
|
-
| `openai`
|
|
226
|
-
| `google`
|
|
227
|
-
| `mistral`
|
|
228
|
-
| `xai`
|
|
229
|
-
| `groq`
|
|
230
|
-
| `openrouter
|
|
231
|
-
| `gateway`
|
|
232
|
-
| `
|
|
270
|
+
| Factory | SDK package | Env var |
|
|
271
|
+
| --------------- | ------------------- | ------------------------------ |
|
|
272
|
+
| `anthropic` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |
|
|
273
|
+
| `openai` | `@ai-sdk/openai` | `OPENAI_API_KEY` |
|
|
274
|
+
| `google` | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
275
|
+
| `mistral` | `@ai-sdk/mistral` | `MISTRAL_API_KEY` |
|
|
276
|
+
| `xai` | `@ai-sdk/xai` | `XAI_API_KEY` |
|
|
277
|
+
| `groq` | `@ai-sdk/groq` | `GROQ_API_KEY` |
|
|
278
|
+
| `openrouter` | `@ai-sdk/openai` | `OPENROUTER_API_KEY` |
|
|
279
|
+
| `gateway` | `ai` (built in) | `AI_GATEWAY_API_KEY` |
|
|
280
|
+
| `assemblyAILlm` | `@ai-sdk/openai` | `ASSEMBLYAI_API_KEY` |
|
|
233
281
|
|
|
234
282
|
LLM factories require `{ model: string }`. Example:
|
|
235
283
|
`anthropic({ model: "claude-haiku-4-5" })`.
|
|
@@ -246,50 +294,52 @@ hundreds of models addressed as `"creator/model"`, e.g.
|
|
|
246
294
|
`gateway({ model: "zai/glm-4.6" })`. It needs no extra SDK install
|
|
247
295
|
(the gateway client ships inside the `ai` package).
|
|
248
296
|
|
|
249
|
-
`
|
|
297
|
+
`assemblyAILlm` routes through the [AssemblyAI LLM
|
|
250
298
|
Gateway](https://www.assemblyai.com/docs/llm-gateway) — an
|
|
251
299
|
OpenAI-compatible endpoint fronting 25+ models (Claude, GPT, Gemini,
|
|
252
|
-
etc.) with the same API key as AssemblyAI STT
|
|
253
|
-
`region: "eu"` for EU data residency. It shares its name with the STT
|
|
254
|
-
factory, so alias one when using both:
|
|
300
|
+
etc.) with the same API key as AssemblyAI STT:
|
|
255
301
|
|
|
256
302
|
```ts
|
|
257
|
-
import {
|
|
258
|
-
import {
|
|
303
|
+
import { agent } from "@alexkroman1/aai";
|
|
304
|
+
import { assemblyAILlm } from "@alexkroman1/aai/llm";
|
|
305
|
+
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
306
|
+
import { cartesia } from "@alexkroman1/aai/tts";
|
|
259
307
|
|
|
260
308
|
export default agent({
|
|
261
309
|
name: "My Agent",
|
|
262
|
-
stt:
|
|
310
|
+
stt: assemblyAIStt({ model: "universal-3-5-pro" }),
|
|
263
311
|
llm: assemblyAILlm({ model: "claude-sonnet-4-6" }),
|
|
264
312
|
tts: cartesia(),
|
|
265
313
|
});
|
|
266
314
|
```
|
|
267
315
|
|
|
316
|
+
It accepts an optional `region: "eu"` for EU data residency.
|
|
317
|
+
|
|
268
318
|
An all-AssemblyAI pipeline — one provider, one key:
|
|
269
319
|
|
|
270
320
|
```ts
|
|
271
321
|
import { agent } from "@alexkroman1/aai";
|
|
272
|
-
import {
|
|
273
|
-
import {
|
|
274
|
-
import {
|
|
322
|
+
import { assemblyAILlm } from "@alexkroman1/aai/llm";
|
|
323
|
+
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
324
|
+
import { assemblyAITts } from "@alexkroman1/aai/tts";
|
|
275
325
|
|
|
276
326
|
export default agent({
|
|
277
327
|
name: "My Agent",
|
|
278
|
-
stt:
|
|
328
|
+
stt: assemblyAIStt({ model: "universal-3-5-pro" }),
|
|
279
329
|
llm: assemblyAILlm({ model: "gemini-2.5-flash-lite" }),
|
|
280
|
-
tts: assemblyAITts({ voice: "
|
|
330
|
+
tts: assemblyAITts({ voice: "jane" }),
|
|
281
331
|
});
|
|
282
332
|
```
|
|
283
333
|
|
|
284
334
|
### TTS — `@alexkroman1/aai/tts`
|
|
285
335
|
|
|
286
|
-
| Factory
|
|
287
|
-
|
|
|
288
|
-
| `
|
|
289
|
-
| `cartesia`
|
|
290
|
-
| `rime`
|
|
336
|
+
| Factory | Default voice | Env var |
|
|
337
|
+
| --------------- | ---------------------------------------- | -------------------- |
|
|
338
|
+
| `assemblyAITts` | `"jane"` | `ASSEMBLYAI_API_KEY` |
|
|
339
|
+
| `cartesia` | `"f786b574-daa5-4673-aa0c-cbe3e8534c02"` | `CARTESIA_API_KEY` |
|
|
340
|
+
| `rime` | `"cove"` (model `mistv2`) | `RIME_API_KEY` |
|
|
291
341
|
|
|
292
|
-
Bare calls (`
|
|
342
|
+
Bare calls (`assemblyAITts()`, `cartesia()`, `rime()`) use the defaults.
|
|
293
343
|
Override with `{ voice, model, language }`.
|
|
294
344
|
|
|
295
345
|
**AssemblyAI TTS** shares `ASSEMBLYAI_API_KEY` with AssemblyAI STT and the
|
|
@@ -300,8 +350,8 @@ connected, "ready", and permanently silent**, so pick one from here rather
|
|
|
300
350
|
than guessing a plausible name:
|
|
301
351
|
|
|
302
352
|
- **English, US accent**: `alba`, `anna`, `charles`, `eve`, `george`,
|
|
303
|
-
`jane
|
|
304
|
-
- **English, UK accent**: `paul`, `vera`
|
|
353
|
+
`jane` (the default), `jean`, `mary`, `michael`
|
|
354
|
+
- **English, UK accent**: `paul`, `vera`
|
|
305
355
|
- **Native accent, code-switches with English**: `estelle` (fr),
|
|
306
356
|
`giovanni` (it), `juergen` (de), `lola` (es), `rafael` (pt)
|
|
307
357
|
|
|
@@ -312,8 +362,7 @@ Set
|
|
|
312
362
|
`language` only alongside a voice that speaks it, as an ISO 639-1 code —
|
|
313
363
|
`"en"`, `"fr"`, `"de"`, `"it"`, `"pt"`, `"es"` are the six the catalog
|
|
314
364
|
covers, and the SDK translates each to the full name the service wants.
|
|
315
|
-
Anything else fails at session start.
|
|
316
|
-
`assemblyAI` in `/stt`, `/llm`, and `/tts`, alias on import.
|
|
365
|
+
Anything else fails at session start.
|
|
317
366
|
|
|
318
367
|
**Rime quirk:** language uses ISO 639-3 three-letter codes (e.g. `"eng"`
|
|
319
368
|
not `"en"`).
|
|
@@ -323,13 +372,13 @@ Set provider keys the same way as any secret: `.env` for local dev,
|
|
|
323
372
|
|
|
324
373
|
## `tool()` API
|
|
325
374
|
|
|
326
|
-
```ts
|
|
375
|
+
```ts no-check
|
|
327
376
|
import { tool } from "@alexkroman1/aai";
|
|
328
377
|
import { z } from "zod";
|
|
329
378
|
|
|
330
379
|
const myTool = tool({
|
|
331
380
|
description: string; // shown to LLM — decides when to call
|
|
332
|
-
|
|
381
|
+
inputSchema?: z.ZodObject; // Zod schema (omit for no-arg tools)
|
|
333
382
|
execute(args, ctx): unknown; // sync or async
|
|
334
383
|
});
|
|
335
384
|
```
|
|
@@ -339,13 +388,13 @@ same way in `aai dev` and deployed.
|
|
|
339
388
|
|
|
340
389
|
### `ctx` (ToolContext)
|
|
341
390
|
|
|
342
|
-
```ts
|
|
391
|
+
```ts no-check
|
|
343
392
|
ctx.env: Readonly<Record<string, string>> // secrets from .env / aai secret put
|
|
344
393
|
ctx.state: S // per-session mutable state (agent's `state` factory)
|
|
345
394
|
ctx.db: Db // SQL database, needs storage enabled (see Database section)
|
|
346
395
|
ctx.messages: readonly Message[] // conversation history [{role, content}]
|
|
347
396
|
ctx.sessionId: string // unique session ID
|
|
348
|
-
ctx.send(event: string, data: unknown): void // push custom event to browser client
|
|
397
|
+
ctx.send(event: string, data: unknown): void // push custom event to browser client (silently dropped over 64 KB JSON)
|
|
349
398
|
ctx.generate(opts): Promise<{ text, object? }> // one-shot LLM call (host-side)
|
|
350
399
|
```
|
|
351
400
|
|
|
@@ -353,7 +402,7 @@ ctx.generate(opts): Promise<{ text, object? }> // one-shot LLM call (host-side)
|
|
|
353
402
|
the project's tsconfig turns off `noImplicitAny`, so both of these compile
|
|
354
403
|
with no annotations and no errors:
|
|
355
404
|
|
|
356
|
-
```ts
|
|
405
|
+
```ts no-check
|
|
357
406
|
ctx.state.count++;
|
|
358
407
|
ctx.state.incidents.filter((i) => i.status === "open");
|
|
359
408
|
```
|
|
@@ -366,7 +415,7 @@ declare empty.** With `noImplicitAny` off, TypeScript does not widen an empty
|
|
|
366
415
|
initializer from what you later assign, so `[]` stays `never[]` and `null`
|
|
367
416
|
stays `null` — forever, whether or not a callback is involved:
|
|
368
417
|
|
|
369
|
-
```ts
|
|
418
|
+
```ts no-check
|
|
370
419
|
const items = []; // never[] → items.push(x) is an error
|
|
371
420
|
let best = null; // null → best = {...} is an error
|
|
372
421
|
const [picks, set] = useState([]); // never[] in a client, same thing
|
|
@@ -418,10 +467,11 @@ compile error, which is the point.
|
|
|
418
467
|
|
|
419
468
|
`ctx.generate({ prompt, system?, llm?, schema?, temperature?, maxOutputTokens? })`
|
|
420
469
|
runs one LLM generation on the host. It defaults to the agent's pipeline
|
|
421
|
-
`llm`; pass an `llm` descriptor (from `@alexkroman1/aai/llm`)
|
|
422
|
-
provider whose API key is in the agent's secrets —
|
|
423
|
-
agents use it.
|
|
424
|
-
|
|
470
|
+
`llm`; pass an `llm` descriptor (from `@alexkroman1/aai/llm`) or a model-id
|
|
471
|
+
string to use another provider whose API key is in the agent's secrets —
|
|
472
|
+
that's also how S2S agents use it. Pass a Zod schema as `schema` for typed
|
|
473
|
+
structured output (`generateObject`-style): the result's `object` carries
|
|
474
|
+
the parsed, typed value. A plain JSON Schema object also works.
|
|
425
475
|
|
|
426
476
|
### Inline tool example
|
|
427
477
|
|
|
@@ -434,7 +484,7 @@ export default agent({
|
|
|
434
484
|
tools: {
|
|
435
485
|
get_weather: tool({
|
|
436
486
|
description: "Get current weather for a city",
|
|
437
|
-
|
|
487
|
+
inputSchema: z.object({
|
|
438
488
|
city: z.string().describe("City name"),
|
|
439
489
|
}),
|
|
440
490
|
async execute({ city }, ctx) {
|
|
@@ -453,7 +503,7 @@ export default agent({
|
|
|
453
503
|
them, and they are not on `ctx`. When your own `execute` needs one, import
|
|
454
504
|
it:
|
|
455
505
|
|
|
456
|
-
```ts
|
|
506
|
+
```ts no-check
|
|
457
507
|
import { fetchJson, visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
458
508
|
|
|
459
509
|
execute: async ({ city }) => await fetchJson(`https://api.example.com/${city}`),
|
|
@@ -482,18 +532,18 @@ Wrapping `webSearch` in a single custom tool is the mistake to avoid — it
|
|
|
482
532
|
replaces "the model searches as needed" with one fixed query-and-summarize
|
|
483
533
|
pipeline, and no amount of prompting gets the flexibility back.
|
|
484
534
|
|
|
485
|
-
**`
|
|
486
|
-
but its VALUE must be a plain `z.object(...)` — so all of these
|
|
487
|
-
errors:
|
|
535
|
+
**`inputSchema` is a Zod object, or absent.** The field itself is
|
|
536
|
+
optional, but its VALUE must be a plain `z.object(...)` — so all of these
|
|
537
|
+
are type errors:
|
|
488
538
|
|
|
489
|
-
```ts
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
539
|
+
```ts no-check
|
|
540
|
+
inputSchema: z.undefined(), // ✗ ZodUndefined
|
|
541
|
+
inputSchema: z.void(), // ✗
|
|
542
|
+
inputSchema: z.object({ q: z.string() }).optional(), // ✗ ZodOptional
|
|
493
543
|
```
|
|
494
544
|
|
|
495
545
|
For a tool with no arguments write `tool({ description, execute })`, or
|
|
496
|
-
`
|
|
546
|
+
`inputSchema: z.object({})` if you prefer it explicit. To make an individual
|
|
497
547
|
argument optional, put `.optional()` on the FIELD, never on the object:
|
|
498
548
|
`z.object({ notes: z.string().optional() })`.
|
|
499
549
|
|
|
@@ -514,14 +564,14 @@ import { z } from "zod";
|
|
|
514
564
|
|
|
515
565
|
export const rollDice = tool({
|
|
516
566
|
description: "Roll dice",
|
|
517
|
-
|
|
567
|
+
inputSchema: z.object({ sides: z.number() }),
|
|
518
568
|
execute({ sides }) {
|
|
519
569
|
return Math.floor(Math.random() * sides) + 1;
|
|
520
570
|
},
|
|
521
571
|
});
|
|
522
572
|
```
|
|
523
573
|
|
|
524
|
-
```ts
|
|
574
|
+
```ts no-check
|
|
525
575
|
// agent.ts
|
|
526
576
|
import { agent } from "@alexkroman1/aai";
|
|
527
577
|
import { rollDice } from "./tools/roll_dice.ts";
|
|
@@ -584,7 +634,7 @@ call it from `execute` — see the builtin table above.
|
|
|
584
634
|
Persistent SQL storage scoped per app, backed by the app's own Postgres
|
|
585
635
|
schema. Access via `ctx.db`:
|
|
586
636
|
|
|
587
|
-
```ts
|
|
637
|
+
```ts no-check
|
|
588
638
|
ctx.db.query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>
|
|
589
639
|
```
|
|
590
640
|
|
|
@@ -602,7 +652,7 @@ A query returning more than 1000 rows throws — always bound reads with
|
|
|
602
652
|
|
|
603
653
|
Create tables lazily from tool code and upsert with `on conflict`:
|
|
604
654
|
|
|
605
|
-
```ts
|
|
655
|
+
```ts no-check
|
|
606
656
|
await ctx.db.query(`create table if not exists app_state (
|
|
607
657
|
key text primary key,
|
|
608
658
|
value jsonb not null,
|
|
@@ -633,6 +683,7 @@ Always import `"@alexkroman1/aai-ui/styles.css"` first.
|
|
|
633
683
|
### Tier 1 — config only (default UI)
|
|
634
684
|
|
|
635
685
|
```tsx
|
|
686
|
+
/// <reference types="vite/client" />
|
|
636
687
|
import "@alexkroman1/aai-ui/styles.css";
|
|
637
688
|
import { client } from "@alexkroman1/aai-ui";
|
|
638
689
|
|
|
@@ -642,6 +693,7 @@ client({ name: "My Agent" });
|
|
|
642
693
|
### Tier 1 with sidebar
|
|
643
694
|
|
|
644
695
|
```tsx
|
|
696
|
+
/// <reference types="vite/client" />
|
|
645
697
|
import "@alexkroman1/aai-ui/styles.css";
|
|
646
698
|
import { client, useEvent } from "@alexkroman1/aai-ui";
|
|
647
699
|
import { useState } from "react";
|
|
@@ -664,6 +716,7 @@ client({ name: "My Agent", sidebar: Sidebar });
|
|
|
664
716
|
### Tier 2 — full custom component
|
|
665
717
|
|
|
666
718
|
```tsx
|
|
719
|
+
/// <reference types="vite/client" />
|
|
667
720
|
import "@alexkroman1/aai-ui/styles.css";
|
|
668
721
|
import { client, useSession } from "@alexkroman1/aai-ui";
|
|
669
722
|
|
|
@@ -714,7 +767,7 @@ to put it in.
|
|
|
714
767
|
| `state` | `AgentState` | `"disconnected"` `"connecting"` `"ready"` `"listening"` `"thinking"` `"speaking"` `"error"` |
|
|
715
768
|
| `messages` | `ChatMessage[]` | `{ role, content }` |
|
|
716
769
|
| `toolCalls` | `ToolCallInfo[]` | `{ callId, name, args, status, result? }` |
|
|
717
|
-
| `customEvents` | `
|
|
770
|
+
| `customEvents` | `AgentCustomEvent[]` | `{ id, event, data }` from `ctx.send()` |
|
|
718
771
|
| `userTranscript` | `string \| null` | `null` = not speaking, `""` = speech detected, string = text |
|
|
719
772
|
| `agentTranscript` | `string \| null` | `null` = not speaking, string = streaming response |
|
|
720
773
|
| `error` | `SessionError \| null` | `{ code, message }` |
|
|
@@ -729,7 +782,7 @@ Methods: `start()`, `toggle()`, `reset()`, `cancel()`, `disconnect()`,
|
|
|
729
782
|
**`useToolResult`** — fires once per completed tool call (deduplicates by
|
|
730
783
|
callId):
|
|
731
784
|
|
|
732
|
-
```ts
|
|
785
|
+
```ts no-check
|
|
733
786
|
useToolResult("tool_name", (result, toolCall) => { ... }) // one tool
|
|
734
787
|
useToolResult((toolName, result, toolCall) => { ... }) // all tools
|
|
735
788
|
useToolResult<ResultType>("tool_name", (result) => { ... }) // typed (optional)
|
|
@@ -746,7 +799,7 @@ also what you want for anything that can be a string, an array, or null.
|
|
|
746
799
|
|
|
747
800
|
**`useAgentState`** — the agent's session state, pushed automatically:
|
|
748
801
|
|
|
749
|
-
```ts
|
|
802
|
+
```ts no-check
|
|
750
803
|
// agent.ts
|
|
751
804
|
export default agent({
|
|
752
805
|
state: () => ({ cart: [] as Item[], staffPin: "" }),
|
|
@@ -772,7 +825,7 @@ after every tool call and is sent only when the result changed.
|
|
|
772
825
|
|
|
773
826
|
**`useEvent`** — fires for custom events from `ctx.send()`:
|
|
774
827
|
|
|
775
|
-
```ts
|
|
828
|
+
```ts no-check
|
|
776
829
|
useEvent<DataType>("event_name", (data) => { ... })
|
|
777
830
|
```
|
|
778
831
|
|
|
@@ -793,13 +846,33 @@ Available from `@alexkroman1/aai-ui`:
|
|
|
793
846
|
|
|
794
847
|
| Component | Props | Description |
|
|
795
848
|
| --- | --- | --- |
|
|
796
|
-
| `StartScreen` | `children
|
|
849
|
+
| `StartScreen` | `children` (**required**)`, icon?, title?, subtitle?, buttonText?` | **Wrapper, never self-closing.** Shows the start card, then renders `children` — your whole app — once the session starts |
|
|
797
850
|
| `ChatView` | `icon?, title?` | Chat interface (header + messages + controls) |
|
|
798
851
|
| `SidebarLayout` | `sidebar, children, sidebarWidth?, sidebarPosition?` | Two-column layout |
|
|
799
852
|
| `MessageList` | — | Messages with auto-scroll, tool calls, transcript |
|
|
800
853
|
| `Controls` | — | Stop/Resume + New Conversation buttons |
|
|
801
854
|
| `Button` | — | Styled button |
|
|
802
855
|
|
|
856
|
+
The usual shape — note `StartScreen` **wraps** the app rather than sitting
|
|
857
|
+
beside it; writing `<StartScreen ... />` self-closing is a `TS2741:
|
|
858
|
+
Property 'children' is missing` build error:
|
|
859
|
+
|
|
860
|
+
```tsx
|
|
861
|
+
/// <reference types="vite/client" />
|
|
862
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
863
|
+
import { ChatView, client, StartScreen } from "@alexkroman1/aai-ui";
|
|
864
|
+
|
|
865
|
+
function PizzaApp() {
|
|
866
|
+
return (
|
|
867
|
+
<StartScreen title="Pizza Palace" subtitle="Voice-powered ordering">
|
|
868
|
+
<ChatView />
|
|
869
|
+
</StartScreen>
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
client({ component: PizzaApp });
|
|
874
|
+
```
|
|
875
|
+
|
|
803
876
|
## Styling
|
|
804
877
|
|
|
805
878
|
- **Tailwind CSS v4** — compiled at bundle time, configured via CSS.
|
|
@@ -877,8 +950,8 @@ Common mistakes when working in aai projects:
|
|
|
877
950
|
values are injected into LLM context. Truncate, summarize, or extract
|
|
878
951
|
only what the model needs.
|
|
879
952
|
- **Pipeline mode requires all three of `stt` / `llm` / `tts`.** Partial
|
|
880
|
-
configs are rejected at parse time.
|
|
881
|
-
|
|
953
|
+
configs are rejected at parse time. Omit all three for the default
|
|
954
|
+
AssemblyAI pipeline; S2S needs an explicit `s2s: assemblyAIS2s()`.
|
|
882
955
|
- **Never hardcode secrets.** Use `ctx.env.MY_KEY`. `.env` for local dev,
|
|
883
956
|
`aai secret put` for production.
|
|
884
957
|
- **Don't use `useEffect` + `toolCalls` to derive state.** Use
|
|
@@ -906,10 +979,10 @@ Common mistakes when working in aai projects:
|
|
|
906
979
|
|
|
907
980
|
## Constraints
|
|
908
981
|
|
|
909
|
-
- Tool `execute` return values go into LLM context
|
|
910
|
-
large API responses
|
|
911
|
-
- `fetch`
|
|
912
|
-
(SSRF
|
|
982
|
+
- Tool `execute` return values go into LLM context, capped at 4000 chars
|
|
983
|
+
(a truncation marker replaces the tail) — filter large API responses
|
|
984
|
+
- Tool code uses plain `fetch` with open egress; the keyless web builtins
|
|
985
|
+
screen private/internal IPs (SSRF) when running outside a sandbox
|
|
913
986
|
- Agent code runs in a sandboxed worker — use `fetch` for HTTP, `ctx.env`
|
|
914
987
|
for secrets
|
|
915
988
|
- Tool execution timeout: 30 seconds
|
|
@@ -11,15 +11,15 @@
|
|
|
11
11
|
"deploy": "aai deploy"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@alexkroman1/aai": "^5.
|
|
15
|
-
"@alexkroman1/aai-ui": "^5.
|
|
14
|
+
"@alexkroman1/aai": "^5.4.0",
|
|
15
|
+
"@alexkroman1/aai-ui": "^5.4.0",
|
|
16
16
|
"react": "^19.2.8",
|
|
17
17
|
"react-dom": "^19.2.8",
|
|
18
18
|
"tailwindcss": "^4.0.0",
|
|
19
19
|
"zod": "^4.4.3"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@alexkroman1/aai-cli": "^5.
|
|
22
|
+
"@alexkroman1/aai-cli": "^5.4.0",
|
|
23
23
|
"@tailwindcss/vite": "^4.3.3",
|
|
24
24
|
"@types/node": "^26.1.1",
|
|
25
25
|
"@types/react": "^19.2.17",
|
|
@@ -90,17 +90,17 @@ describe("dispatch-center template", () => {
|
|
|
90
90
|
|
|
91
91
|
test("negative casualty counts are rejected by the parameter schemas", () => {
|
|
92
92
|
expect(
|
|
93
|
-
incidentTriage.
|
|
93
|
+
incidentTriage.inputSchema?.safeParse({ incidentId: "INC-0001", casualtyUpdate: -5 }).success,
|
|
94
94
|
).toBe(false);
|
|
95
95
|
expect(
|
|
96
|
-
incidentCreate.
|
|
96
|
+
incidentCreate.inputSchema?.safeParse({
|
|
97
97
|
location: "1 First St",
|
|
98
98
|
description: "fire",
|
|
99
99
|
estimatedCasualties: -1,
|
|
100
100
|
}).success,
|
|
101
101
|
).toBe(false);
|
|
102
102
|
expect(
|
|
103
|
-
incidentUpdateStatus.
|
|
103
|
+
incidentUpdateStatus.inputSchema?.safeParse({
|
|
104
104
|
incidentId: "INC-0001",
|
|
105
105
|
status: "on_scene",
|
|
106
106
|
casualtyUpdate: { confirmed: -2 },
|