@alexkroman1/aai-cli 8.2.0 → 9.0.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/_dev-env.d.ts +15 -2
- package/dist/{_dev-server-Dt3TCjhj.mjs → _dev-server-BAR8Ya43.mjs} +182 -55
- package/dist/_dev-server.d.ts +5 -30
- package/dist/_dev-typecheck.d.ts +38 -0
- package/dist/_dev-vite-config.d.ts +74 -0
- package/dist/_e2e-test-utils.d.ts +33 -1
- package/dist/_init-BNotRgV1.mjs +163 -0
- package/dist/_resource-commands.d.ts +0 -1
- package/dist/{_slug-api-z-USxPtL.mjs → _slug-api--ZBmIGH-.mjs} +1 -1
- package/dist/{_studio-DR5yzZ1T.mjs → _studio-CKrsixd0.mjs} +2 -1
- package/dist/_studio.d.ts +3 -1
- package/dist/{build-BBxf8vom.mjs → build-DvDdSwAS.mjs} +1 -1
- package/dist/cli.mjs +26 -97
- package/dist/{delete-Dl_LSfvI.mjs → delete-8pjH8n8n.mjs} +3 -2
- package/dist/{deploy-DRC8jWr8.mjs → deploy-CjU3e-3P.mjs} +4 -4
- package/dist/{dev-QuuOOUtv.mjs → dev-dFzdvwa7.mjs} +3 -2
- package/dist/dev.d.ts +2 -0
- package/dist/{eval-DfQ8cm5p.mjs → eval-DwNwdqmS.mjs} +1 -1
- package/dist/{init-CUo87gcI.mjs → init-DK7GpIF9.mjs} +7 -6
- package/dist/{logs-CQ2T46dr.mjs → logs-D2Yhqe2w.mjs} +2 -2
- package/dist/scaffold/.env.example +7 -4
- package/dist/scaffold/CLAUDE.md +152 -97
- package/dist/scaffold/global.d.ts +12 -0
- package/dist/scaffold/package.json +4 -4
- package/dist/scaffold/server.mjs +49 -2
- package/dist/scaffold/tsconfig.json +1 -0
- package/dist/scaffold/vitest.config.ts +24 -0
- package/dist/{secret-Ct34kdgU.mjs → secret-CwgDLpSy.mjs} +2 -2
- package/dist/{studio-CzTzPqe1.mjs → studio-D3-HWIfk.mjs} +7 -5
- package/dist/templates/briefing-desk/agent.test.ts +3 -15
- package/dist/templates/briefing-desk/shared.ts +3 -3
- package/dist/templates/call-audit/agent.eval.test.ts +8 -1
- package/dist/templates/call-audit/agent.test.ts +0 -1
- package/dist/templates/call-audit/agent.ts +4 -3
- package/dist/templates/call-audit/client.tsx +4 -7
- package/dist/templates/call-audit/workflows/audit.ts +10 -1
- package/dist/templates/code-interpreter/agent.eval.test.ts +14 -43
- package/dist/templates/dispatch-center/agent.eval.test.ts +16 -26
- package/dist/templates/embedded-assets/agent.eval.test.ts +6 -16
- package/dist/templates/embedded-assets/agent.test.ts +3 -16
- package/dist/templates/health-assistant/agent.eval.test.ts +5 -15
- package/dist/templates/health-assistant/agent.test.ts +3 -14
- package/dist/templates/infocom-adventure/agent.eval.test.ts +6 -16
- package/dist/templates/infocom-adventure/agent.test.ts +3 -17
- package/dist/templates/link-digest/agent.test.ts +0 -14
- package/dist/templates/link-digest/agent.ts +9 -5
- package/dist/templates/link-digest/client.tsx +8 -6
- package/dist/templates/math-buddy/agent.eval.test.ts +11 -40
- package/dist/templates/night-owl/agent.eval.test.ts +15 -44
- package/dist/templates/night-owl/agent.test.ts +2 -15
- package/dist/templates/personal-finance/agent.eval.test.ts +10 -39
- package/dist/templates/pipeline-simple/agent.test.ts +73 -0
- package/dist/templates/pizza-ordering/agent.eval.test.ts +2 -17
- package/dist/templates/pizza-ordering/agent.test.ts +2 -17
- package/dist/templates/plan-and-execute/agent.eval.test.ts +2 -12
- package/dist/templates/plan-and-execute/agent.test.ts +3 -23
- package/dist/templates/plan-and-execute/shared.ts +1 -1
- package/dist/templates/podcast-digest/agent.test.ts +0 -1
- package/dist/templates/podcast-digest/client.tsx +3 -6
- package/dist/templates/podcast-digest/workflows/digest.ts +16 -8
- package/dist/templates/recap-workflow/agent.eval.test.ts +7 -14
- package/dist/templates/recap-workflow/agent.test.ts +3 -18
- package/dist/templates/recap-workflow/agent.ts +7 -5
- package/dist/templates/redline/agent.test.ts +0 -13
- package/dist/templates/redline/agent.ts +3 -2
- package/dist/templates/redline/client.tsx +9 -4
- package/dist/templates/research-workflow/agent.eval.test.ts +7 -14
- package/dist/templates/research-workflow/agent.test.ts +3 -15
- package/dist/templates/research-workflow/agent.ts +7 -5
- package/dist/templates/research-workflow/workflows/research.ts +8 -2
- package/dist/templates/retail/agent.eval.test.ts +10 -25
- package/dist/templates/retail/registry.test.ts +16 -13
- package/dist/templates/solo-rpg/agent.eval.test.ts +5 -15
- package/dist/templates/solo-rpg/agent.test.ts +17 -124
- package/dist/templates/solo-rpg/shared.ts +13 -61
- package/dist/templates/spoken-summary/agent.test.ts +0 -1
- package/dist/templates/spoken-summary/agent.ts +5 -4
- package/dist/templates/spoken-summary/client.tsx +4 -7
- package/dist/templates/support-line/agent.eval.test.ts +2 -12
- package/dist/templates/support-line/agent.test.ts +3 -22
- package/dist/templates/transcription-workflow/agent.ts +4 -4
- package/dist/templates/transcription-workflow/client.tsx +5 -5
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +10 -1
- package/dist/templates/travel-concierge/agent.eval.test.ts +5 -15
- package/dist/templates/travel-concierge/agent.test.ts +3 -23
- package/dist/templates/web-researcher/agent.eval.test.ts +2 -5
- package/dist/test-CvwgeVSQ.mjs +181 -0
- package/dist/test.d.ts +33 -2
- package/package.json +4 -4
- package/dist/_init-BhEfQ6Yi.mjs +0 -93
- package/dist/storage-i_79ZwAR.mjs +0 -66
- package/dist/storage.d.ts +0 -22
- package/dist/templates/solo-rpg/tools/load_game.ts +0 -37
- package/dist/templates/solo-rpg/tools/save_game.ts +0 -32
- package/dist/test-BvyT6sAc.mjs +0 -104
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -7,8 +7,12 @@ You are helping build a voice agent using the **aai** framework.
|
|
|
7
7
|
The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
8
8
|
`pnpm test` (logic) → `pnpm build` (validate bundle).
|
|
9
9
|
|
|
10
|
-
1. **Iterate in `pnpm dev`** —
|
|
11
|
-
|
|
10
|
+
1. **Iterate in `pnpm dev`** — browser UI, and `pnpm dev -- --watch` to
|
|
11
|
+
rebuild and restart on every save. Speak to the agent to verify behavior
|
|
12
|
+
end-to-end. This is the primary feedback loop. Watching is OPT-IN because a
|
|
13
|
+
restart ends in-flight voice sessions, which is right while you are editing
|
|
14
|
+
and wrong while something is driving the agent for twenty minutes;
|
|
15
|
+
`AAI_DEV_WATCH=1` is the same switch for a process supervisor.
|
|
12
16
|
2. **Run `pnpm test` after logic changes** — vitest. Co-locate tests as
|
|
13
17
|
`agent.test.ts` (see `pipeline-simple` template for a reference).
|
|
14
18
|
**When the project has an `agent.test.ts` (the default `simple`
|
|
@@ -18,6 +22,20 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
|
18
22
|
agent that no longer exists. When a test fails after your change, decide
|
|
19
23
|
which side is stale: updating the test to match the new agent is a normal
|
|
20
24
|
fix, not a workaround. Do not delete a test to make it pass.
|
|
25
|
+
|
|
26
|
+
**A spec that needs the agent as DEPLOYED imports one module:**
|
|
27
|
+
|
|
28
|
+
```ts no-check
|
|
29
|
+
import agentDef from "virtual:aai/agent";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That is `agent.ts` with its `tools/` directory discovered and its
|
|
33
|
+
`system-prompt.md` applied — the same lowering `aai build` does, so a spec
|
|
34
|
+
measures the agent that ships rather than the raw default export (which has
|
|
35
|
+
no tools and the framework's default prompt). `vitest.config.ts` registers
|
|
36
|
+
the plugin that serves it; a scaffolded project already has it. For a runner
|
|
37
|
+
that is not vitest, `deployedAgent` on `@alexkroman1/aai/testing` is the same
|
|
38
|
+
thing written out.
|
|
21
39
|
3. **Run `pnpm eval` when you change what the agent DOES** — a test asserts
|
|
22
40
|
the agent's shape; an eval drives a real session and asserts what it did.
|
|
23
41
|
Cases live in `agent.eval.test.ts` (the `simple` template ships one):
|
|
@@ -163,8 +181,8 @@ import { agent } from "@alexkroman1/aai";
|
|
|
163
181
|
export default agent({
|
|
164
182
|
name: string; // required — display name
|
|
165
183
|
systemPrompt?: string; // usually ABSENT — write system-prompt.md
|
|
166
|
-
// instead; declare it only to COMPOSE
|
|
167
|
-
//
|
|
184
|
+
// instead; declare it only to COMPOSE one.
|
|
185
|
+
// There is no `system` alias — one name.
|
|
168
186
|
greeting?: string; // default: "Hey there..."
|
|
169
187
|
voice?: string; // TTS voice for the default pipeline, e.g. "michael"
|
|
170
188
|
// (shorthand for tts: assemblyAITts({ voice });
|
|
@@ -180,6 +198,10 @@ export default agent({
|
|
|
180
198
|
// (there is no `tools` field — a tool is a FILE;
|
|
181
199
|
// see "A file in tools/ IS a tool")
|
|
182
200
|
maxSteps?: number; // default: 10 — max tool calls per turn
|
|
201
|
+
temperature?: number; // sampling temperature for the agent's OWN model calls
|
|
202
|
+
// (pipeline and text). Unset = the model's default; some
|
|
203
|
+
// models ignore it and warn. S2S REFUSES it — the model
|
|
204
|
+
// runs in the provider's service and never sees this.
|
|
183
205
|
toolChoice?: ToolChoice; // "auto" (default) | "required" | "none"
|
|
184
206
|
// | { type: "tool", toolName }
|
|
185
207
|
idleTimeoutMs?: number; // disconnect after inactivity (ms)
|
|
@@ -192,6 +214,18 @@ export default agent({
|
|
|
192
214
|
preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default false; true opts in)
|
|
193
215
|
syncState?: StateProjection; // show a slot to the client: slot.projection(view)
|
|
194
216
|
// (read it with useAgentState; see UI hooks)
|
|
217
|
+
minTurnSilenceMs?: number; // pipeline only — pause (ms) that ENDS a user turn once the
|
|
218
|
+
// text reads complete (default 560)
|
|
219
|
+
maxTurnSilenceMs?: number; // pipeline only — pause (ms) that ends a turn REGARDLESS of
|
|
220
|
+
// content (default 1600). The endpointing knob to reach for:
|
|
221
|
+
// it bounds the utterances that never read as finished.
|
|
222
|
+
// Both are shorthand for the same options on the default
|
|
223
|
+
// assemblyAIStt() stage — invalid with an explicit `stt`.
|
|
224
|
+
requiredEnv?: string[]; // env vars this agent reads. A deploy CHECKS them, so a
|
|
225
|
+
// missing key fails at `aai push` instead of mid-call.
|
|
226
|
+
// Declare every key any tool or step reads.
|
|
227
|
+
text?: true; // text-only agent: no STT, no TTS, `llm` is the one stage
|
|
228
|
+
events?: SessionEventHandlers; // observe the session (see "Watching the session")
|
|
195
229
|
});
|
|
196
230
|
```
|
|
197
231
|
|
|
@@ -361,17 +395,20 @@ callback. Reach for `agent()` when someone is on the line — a voice agent can
|
|
|
361
395
|
also START a workflow from a tool (`ctx.workflows.start(def, input)`) and
|
|
362
396
|
answer the turn, which is the other shape.
|
|
363
397
|
|
|
364
|
-
**
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
**Runs are DURABLE on the platform with no setup.** A deployed app's runs live
|
|
399
|
+
on the platform's own database, so they survive a restart, a redeploy and an
|
|
400
|
+
idle sandbox. There is nothing to enable.
|
|
401
|
+
|
|
402
|
+
Under `aai dev` without a `DATABASE_URL` they live in the process that started
|
|
403
|
+
them — you can submit the form, watch the run and read its result, and
|
|
404
|
+
everything in flight is lost when that process goes away. That is the honest
|
|
405
|
+
tradeoff, and it is what
|
|
370
406
|
lets you build a workflow app before provisioning anything.
|
|
371
407
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
408
|
+
One thing does need a database whatever the run does: a workflow **upload**. An
|
|
409
|
+
upload's record is a row, so `api.upload` and the file-taking form hooks refuse
|
|
410
|
+
by name without a `DATABASE_URL`. (`ctx.db` was the other; there is no `ctx.db`
|
|
411
|
+
— see "Persisting data" below.)
|
|
375
412
|
|
|
376
413
|
### Workflow bodies live in `workflows/`
|
|
377
414
|
|
|
@@ -418,7 +455,8 @@ warned about by `aai build` and `aai dev`, naming the file and the call:
|
|
|
418
455
|
JSON-shaped and small. Put bytes in storage and pass the key.
|
|
419
456
|
- **A step gets no tool context.** It is bundled and dispatched separately from
|
|
420
457
|
the agent, so there is no `ctx` in one — see below for how it reaches the
|
|
421
|
-
agent's env and a model anyway.
|
|
458
|
+
agent's env and a model anyway. A step reaches a database the same way a tool
|
|
459
|
+
does now — its own client, its own credential from `requireStepEnv`.
|
|
422
460
|
|
|
423
461
|
### A step's env, and calling a model from one
|
|
424
462
|
|
|
@@ -468,6 +506,11 @@ is the one to use**:
|
|
|
468
506
|
| `stepTranscribeUpload` / `Submit` / `Poll` | the matching `*Classified` |
|
|
469
507
|
| `sendToChannel` (`/channels`) | `sendToChannelClassified` |
|
|
470
508
|
|
|
509
|
+
`stepFetchOk` is the one that is not spelled `*Classified`, and the name is the
|
|
510
|
+
difference: the others turn an already-thrown failure into a classified one,
|
|
511
|
+
while this also turns a NON-2XX RESPONSE into a throw — `stepFetch` resolves
|
|
512
|
+
with a `404` rather than raising it. Two changes, so two names.
|
|
513
|
+
|
|
471
514
|
The whole of what a wrapper adds is `throwStepError`, and that is worth having
|
|
472
515
|
because the DevKit's retry policy is decided by WHICH error a step throws. Raw,
|
|
473
516
|
every failure looks the same to it: a bad API key is retried until the attempts
|
|
@@ -497,11 +540,13 @@ every one of those bundles. A step pays nothing for the extra import line.
|
|
|
497
540
|
Three more subpaths a `workflows/*.ts` module can reach, all with the same
|
|
498
541
|
bundling rule as `/step` — import them there, never through the root barrel:
|
|
499
542
|
|
|
500
|
-
- **`@alexkroman1/aai/
|
|
543
|
+
- **`@alexkroman1/aai/step`** — `stepTranscribeSync(bytes)` for a short
|
|
501
544
|
recording, or `stepTranscribeUpload` → `stepTranscribeSubmit` →
|
|
502
545
|
`stepTranscribePoll` for a long one, plus `Transcript`, `TranscribeError` and
|
|
503
|
-
the `TRANSCRIBE_*` limits.
|
|
504
|
-
|
|
546
|
+
the `TRANSCRIBE_*` limits. (There is no `/transcribe` subpath; transcription
|
|
547
|
+
lives on `/step` with the other step primitives.) Use the `Classified`
|
|
548
|
+
wrappers above: a provider refusal — a container it will not read, a
|
|
549
|
+
recording with no speech — arrives
|
|
505
550
|
with `retryable: false`, and unclassified a step re-uploads the same bytes
|
|
506
551
|
until its attempts run out.
|
|
507
552
|
- **`@alexkroman1/aai/ffmpeg`** — `transcodeToWav(bytes, { sampleRate })`,
|
|
@@ -705,7 +750,7 @@ rather than fail the turn:
|
|
|
705
750
|
import { webSearch } from "@alexkroman1/aai/tools";
|
|
706
751
|
import { isToolFailure } from "@alexkroman1/aai/utils";
|
|
707
752
|
|
|
708
|
-
const found = await webSearch<{ results?: { url?: string }[] }>({ query,
|
|
753
|
+
const found = await webSearch<{ results?: { url?: string }[] }>({ query, maxResults: 4 });
|
|
709
754
|
// NOT `(found.results ?? [])` — a REFUSED search would then read as an empty web.
|
|
710
755
|
if (isToolFailure(found)) return `That search failed: ${found.error}`;
|
|
711
756
|
return (found.results ?? []).map((one) => one.url);
|
|
@@ -862,8 +907,39 @@ export default agent({
|
|
|
862
907
|
Tools, the database, `ctx`, and the UI all behave identically across modes.
|
|
863
908
|
Only the audio + LLM transport differs.
|
|
864
909
|
|
|
865
|
-
**
|
|
866
|
-
|
|
910
|
+
**Four front doors, each one field on `agent()`.** Omit them all for PIPELINE
|
|
911
|
+
(voice, cascaded STT → LLM → TTS) — the default, and the mode this guide
|
|
912
|
+
assumes. `s2s:` selects speech-to-speech. **`text: true` selects a text-only
|
|
913
|
+
agent**: no STT, no TTS, `llm` is the one stage, and the host runs it with
|
|
914
|
+
`createTextAgent` from `@alexkroman1/aai-runtime`. `workflowApp()` (see
|
|
915
|
+
"Workflow apps") builds a form with no session at all. Setting a field from the
|
|
916
|
+
wrong arm is a compile error naming the rule, so the modes cannot be mixed by
|
|
917
|
+
accident. Every pipeline agent must declare a real TTS provider — that is a
|
|
918
|
+
statement about pipeline mode, not about the SDK.
|
|
919
|
+
|
|
920
|
+
### Answering a phone call
|
|
921
|
+
|
|
922
|
+
A deployed voice agent already serves carrier media streams — there is nothing
|
|
923
|
+
to switch on. `createServer` mounts `WS /phone` whenever the agent is a voice
|
|
924
|
+
agent (`telephony` defaults to `true`, and to `false` for a `page: "static"`
|
|
925
|
+
workflow app, which has no stages to put on a call). Point the carrier at it
|
|
926
|
+
with a `carrier` query parameter naming who is dialling:
|
|
927
|
+
|
|
928
|
+
```text
|
|
929
|
+
wss://<your-agent-url>/phone?carrier=twilio
|
|
930
|
+
wss://<your-agent-url>/phone?carrier=telnyx
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
Twilio and Telnyx are the two carriers this build decodes (`CARRIER_CODECS`);
|
|
934
|
+
an unknown `carrier` is declined at the upgrade. Both speak 8 kHz mu-law, which
|
|
935
|
+
the bridge transcodes in both directions, so the agent, its tools and its slots
|
|
936
|
+
behave exactly as they do in the browser — a phone call is a transport, not a
|
|
937
|
+
mode. Nothing about `agent.ts` changes to support one.
|
|
938
|
+
|
|
939
|
+
Turn the route off with `telephony: false` on `createServer`. If you are
|
|
940
|
+
embedding the runtime yourself rather than deploying, the pieces are
|
|
941
|
+
`createTelephonyBridge`, `startTelephonySession`, `TELEPHONY_PATH` and
|
|
942
|
+
`carrierByName`, all on `@alexkroman1/aai-runtime`.
|
|
867
943
|
|
|
868
944
|
**Silence nudge (pipeline only):** set `silenceTimeoutMs` to make the
|
|
869
945
|
assistant proactively take a turn after that much user silence (e.g.
|
|
@@ -1062,10 +1138,14 @@ same way in `aai dev` and deployed.
|
|
|
1062
1138
|
### `ctx` (ToolContext)
|
|
1063
1139
|
|
|
1064
1140
|
```ts no-check
|
|
1065
|
-
ctx.env: Readonly<Record<string, string
|
|
1141
|
+
ctx.env: Readonly<Partial<Record<string, string>>> // secrets from .env / aai secret put.
|
|
1142
|
+
// Partial: every read is `string | undefined`.
|
|
1143
|
+
// Use requireEnv(ctx, "KEY") to fail by NAME
|
|
1144
|
+
// instead of throwing a TypeError at the model.
|
|
1145
|
+
ctx.workflows: WorkflowClient // start / signal / wake / find / stream a durable run
|
|
1146
|
+
// from a tool (see "Workflows")
|
|
1066
1147
|
ctx.slots: SlotStore // where sessionSlot() keeps this session's state —
|
|
1067
1148
|
// reach for the slot, never this (see "Session state")
|
|
1068
|
-
ctx.db: Db // SQL database, needs storage enabled (see Database section)
|
|
1069
1149
|
ctx.messages: readonly Message[] // conversation history [{role, content}]
|
|
1070
1150
|
ctx.sessionId: string // unique session ID
|
|
1071
1151
|
ctx.send(event: string, data: unknown): void // push custom event to browser client (silently dropped over 64 KB JSON)
|
|
@@ -1181,9 +1261,10 @@ Four rules, and each is an error rather than advice if you get it wrong:
|
|
|
1181
1261
|
- **Hold plain data.** Objects, arrays, strings, numbers, booleans and null. A
|
|
1182
1262
|
`Map`, a `Set`, a `Date` or a class instance is refused with the field named,
|
|
1183
1263
|
because none of them survives being stored.
|
|
1184
|
-
- **State is STORED
|
|
1185
|
-
|
|
1186
|
-
|
|
1264
|
+
- **State is STORED on the platform**, so a crash or a redeploy no longer loses
|
|
1265
|
+
it — the platform keeps a session's slots on its own database and there is
|
|
1266
|
+
nothing to enable. Under `aai dev` it lives in memory for the life of the
|
|
1267
|
+
process unless you set a `DATABASE_URL` in `.env`. You write the same code
|
|
1187
1268
|
either way; that is the reason for the rules above.
|
|
1188
1269
|
|
|
1189
1270
|
There is nothing to declare on `agent()` — the slot owns its own default. Use
|
|
@@ -1267,8 +1348,8 @@ an SDK dependency. `support-line` is the worked example.
|
|
|
1267
1348
|
|
|
1268
1349
|
`ctx.generate` is ONE prompt. When answering takes an unknown number of tool
|
|
1269
1350
|
calls whose intermediate results the conversation has no reason to carry,
|
|
1270
|
-
delegate to a **subagent** instead: a second tool loop with its own
|
|
1271
|
-
|
|
1351
|
+
delegate to a **subagent** instead: a second tool loop with its own system
|
|
1352
|
+
prompt, model, tools and — the whole point — its own context window.
|
|
1272
1353
|
|
|
1273
1354
|
```ts
|
|
1274
1355
|
import { subagent, tool } from "@alexkroman1/aai";
|
|
@@ -1276,7 +1357,9 @@ import { z } from "zod";
|
|
|
1276
1357
|
|
|
1277
1358
|
const researcher = subagent({
|
|
1278
1359
|
name: "researcher",
|
|
1279
|
-
|
|
1360
|
+
// `systemPrompt`, the same field name `agent()` uses — a subagent is a
|
|
1361
|
+
// field-for-field smaller agent, so nothing about it is spelled differently.
|
|
1362
|
+
systemPrompt:
|
|
1280
1363
|
"Research the task with the tools you have. IMPORTANT: your final message " +
|
|
1281
1364
|
"is the only thing the caller sees — end with a self-contained summary.",
|
|
1282
1365
|
builtinTools: ["web_search", "visit_webpage"],
|
|
@@ -1470,7 +1553,7 @@ something it has to notice and switch off.
|
|
|
1470
1553
|
|
|
1471
1554
|
| Tool | Description | Params |
|
|
1472
1555
|
| --- | --- | --- |
|
|
1473
|
-
| `web_search` | Search the web (DuckDuckGo) — no API key required | `query`, `
|
|
1556
|
+
| `web_search` | Search the web (DuckDuckGo) — no API key required | `query`, `maxResults?` (default 5) |
|
|
1474
1557
|
| `visit_webpage` | Fetch URL to plain text | `url` |
|
|
1475
1558
|
| `get_page_design` | Fetch URL's raw HTML + CSS (style blocks and linked stylesheets) to study/mimic a site's design | `url` |
|
|
1476
1559
|
| `fetch_json` | HTTP GET a JSON API | `url`, `headers?` |
|
|
@@ -1533,75 +1616,49 @@ that case — `slot.update`'s window is synchronous — so reach for the lock wh
|
|
|
1533
1616
|
the thing being mutated is outside the session. `withLock` takes an optional
|
|
1534
1617
|
acquire deadline and throws `KeyedLockTimeoutError` when it runs out.
|
|
1535
1618
|
|
|
1536
|
-
##
|
|
1619
|
+
## Persisting data — bring your own client
|
|
1537
1620
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1621
|
+
**There is no `ctx.db`.** It was a SQL handle on the tool context, backed first
|
|
1622
|
+
by a Postgres the platform provisioned per app and later by a `DATABASE_URL` an
|
|
1623
|
+
author set. The platform provisions no database, and no longer hands tool code
|
|
1624
|
+
one either.
|
|
1540
1625
|
|
|
1541
|
-
|
|
1542
|
-
ctx.db.query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>
|
|
1543
|
-
```
|
|
1626
|
+
So a tool that needs to persist anything uses a client of its own:
|
|
1544
1627
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
**The database must be enabled** or accessing `ctx.db` throws:
|
|
1552
|
-
|
|
1553
|
-
- CLI: `aai storage enable`
|
|
1554
|
-
- Studio: Settings pane → Database → Enable database (covers both the
|
|
1555
|
-
preview and published agents, each with its own schema)
|
|
1556
|
-
- `aai dev`: set `DATABASE_URL` in the project `.env`
|
|
1628
|
+
```ts no-check
|
|
1629
|
+
// tools/save_note.ts — a driver you added, a credential you set.
|
|
1630
|
+
import { tool } from "@alexkroman1/aai";
|
|
1631
|
+
import postgres from "postgres";
|
|
1632
|
+
import { z } from "zod";
|
|
1557
1633
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
bucket you point it at, and the Supabase CLI prints the two values
|
|
1561
|
-
(`supabase start`, then `supabase status -o env`):
|
|
1634
|
+
// Module scope, so one pool serves every call in this sandbox.
|
|
1635
|
+
const sql = postgres(process.env.DATABASE_URL ?? "");
|
|
1562
1636
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1637
|
+
export default tool({
|
|
1638
|
+
description: "Save a note.",
|
|
1639
|
+
inputSchema: z.object({ body: z.string() }),
|
|
1640
|
+
execute: async ({ body }) => {
|
|
1641
|
+
await sql`insert into notes (body) values (${body})`;
|
|
1642
|
+
return "saved";
|
|
1643
|
+
},
|
|
1644
|
+
});
|
|
1568
1645
|
```
|
|
1569
1646
|
|
|
1570
|
-
|
|
1571
|
-
(`
|
|
1572
|
-
|
|
1573
|
-
`blobs/<sha256>` deploy artifacts. Nothing creates a bucket for you.
|
|
1574
|
-
|
|
1575
|
-
`.env.example` in a scaffolded project carries this block commented out.
|
|
1647
|
+
Add the driver to your project's `package.json` and the URL with `aai secret put
|
|
1648
|
+
DATABASE_URL …` (or in `.env` under `aai dev`). Nothing here is privileged — an
|
|
1649
|
+
HTTP API, a provider SDK or a hosted KV works the same way.
|
|
1576
1650
|
|
|
1577
|
-
|
|
1578
|
-
never quietly into a directory, which is what it used to do and then lose by the
|
|
1579
|
-
time a resumed run read it.
|
|
1651
|
+
**What the platform DOES persist for you**, with no setup:
|
|
1580
1652
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
value jsonb not null,
|
|
1587
|
-
updated_at timestamptz not null default now()
|
|
1588
|
-
)`);
|
|
1589
|
-
await ctx.db.query(
|
|
1590
|
-
"insert into app_state (key, value, updated_at) values ($1, $2::jsonb, now()) " +
|
|
1591
|
-
"on conflict (key) do update set value = excluded.value, updated_at = now()",
|
|
1592
|
-
["user:123", JSON.stringify({ name: "Alex" })],
|
|
1593
|
-
);
|
|
1594
|
-
const rows = await ctx.db.query<{ value: { name: string } }>(
|
|
1595
|
-
"select value from app_state where key = $1",
|
|
1596
|
-
["user:123"],
|
|
1597
|
-
);
|
|
1598
|
-
```
|
|
1653
|
+
- **`sessionSlot`** — this session's state, durable across a crash or a
|
|
1654
|
+
redeploy. Reach for it before reaching for a database; most agents need
|
|
1655
|
+
nothing else.
|
|
1656
|
+
- **Durable workflow runs** — a run survives the sandbox recycling, every
|
|
1657
|
+
redeploy, and a multi-day `sleep()`.
|
|
1599
1658
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
`remember`/`recall` builtins likewise remain for session-scoped notes the
|
|
1604
|
-
LLM manages itself.
|
|
1659
|
+
Those two cover almost everything an agent wants. A database is for data that
|
|
1660
|
+
must outlive a session AND be queryable: a ledger, filed records, cross-session
|
|
1661
|
+
saves.
|
|
1605
1662
|
|
|
1606
1663
|
## Custom UI — `client()`
|
|
1607
1664
|
|
|
@@ -1947,15 +2004,13 @@ Common mistakes when working in aai projects:
|
|
|
1947
2004
|
platform's Modal/Deno sandbox; the self-hosted `aai dev` server has no
|
|
1948
2005
|
sandbox, so there `run_code` refuses with an error result. Deploy to test
|
|
1949
2006
|
it end-to-end, or use the `calculate` builtin for simple arithmetic in dev.
|
|
1950
|
-
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
deployment — key them yourself if sessions must not see each other's data
|
|
1958
|
-
(or keep session-scoped data in a `sessionSlot`).
|
|
2007
|
+
- **There is no `ctx.db`.** The platform provisions no database and hands tool
|
|
2008
|
+
code none, so a tool that persists brings its own client — see "Persisting
|
|
2009
|
+
data". Two consequences worth knowing before you do: a deployed agent reads a
|
|
2010
|
+
secret when its sandbox is BUILT, so a newly set `DATABASE_URL` reaches it on
|
|
2011
|
+
the next deploy rather than immediately; and a database you bring is shared by
|
|
2012
|
+
every session of the deployment, so key rows yourself if sessions must not see
|
|
2013
|
+
each other's data (or keep session-scoped data in a `sessionSlot`).
|
|
1959
2014
|
- **Rime language codes are ISO 639-3** (3-letter, e.g. `"eng"`), not
|
|
1960
2015
|
ISO 639-1 (`"en"`).
|
|
1961
2016
|
|
|
@@ -1 +1,13 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `virtual:aai/agent` — the agent as `aai build` lowers it: `agent.ts` with its
|
|
5
|
+
* `tools/` directory discovered and its `system-prompt.md` applied.
|
|
6
|
+
*
|
|
7
|
+
* Served by `aaiAgentPlugin()` in `vitest.config.ts`, which resolves it against
|
|
8
|
+
* the importing spec's own directory. See `@alexkroman1/aai/testing/vite`.
|
|
9
|
+
*/
|
|
10
|
+
declare module "virtual:aai/agent" {
|
|
11
|
+
const agentDef: import("@alexkroman1/aai").AgentDef;
|
|
12
|
+
export default agentDef;
|
|
13
|
+
}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"publish:agent": "aai publish"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@alexkroman1/aai": "^
|
|
18
|
-
"@alexkroman1/aai-runtime": "^
|
|
19
|
-
"@alexkroman1/aai-ui": "^
|
|
17
|
+
"@alexkroman1/aai": "^9.0.0",
|
|
18
|
+
"@alexkroman1/aai-runtime": "^9.0.0",
|
|
19
|
+
"@alexkroman1/aai-ui": "^9.0.0",
|
|
20
20
|
"@workflow/world-postgres": "4.3.3",
|
|
21
21
|
"react": "^19.2.8",
|
|
22
22
|
"react-dom": "^19.2.8",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"zod": "^4.4.3"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@alexkroman1/aai-cli": "^
|
|
29
|
+
"@alexkroman1/aai-cli": "^9.0.0",
|
|
30
30
|
"@tailwindcss/vite": "^4.3.3",
|
|
31
31
|
"@types/node": "^26.2.0",
|
|
32
32
|
"@types/react": "^19.2.18",
|
package/dist/scaffold/server.mjs
CHANGED
|
@@ -17,7 +17,11 @@ import { readFile } from "node:fs/promises";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
import { parseEnv } from "node:util";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createAgentServer,
|
|
22
|
+
ensureSessionStateSchema,
|
|
23
|
+
withHostCredentialFallback,
|
|
24
|
+
} from "@alexkroman1/aai-runtime";
|
|
21
25
|
import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
|
|
22
26
|
|
|
23
27
|
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -54,7 +58,22 @@ if (!existsSync(workerPath)) {
|
|
|
54
58
|
}
|
|
55
59
|
// A file: URL rather than a relative specifier, so the path is correct on
|
|
56
60
|
// Windows, where a bare POSIX-looking path is not a valid module specifier.
|
|
57
|
-
const
|
|
61
|
+
const worker = await import(pathToFileURL(workerPath).href);
|
|
62
|
+
const agent = worker.default;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The compiled workflow surface, carried on the bundle as two string exports.
|
|
66
|
+
*
|
|
67
|
+
* A `"use workflow"` body has to go through the DevKit's compiler, which happens
|
|
68
|
+
* at BUILD time — a deployed agent is handed one ESM string and has no bundler —
|
|
69
|
+
* so `aai build` leaves the result here as data. Passing them is what makes
|
|
70
|
+
* durable workflows actually run: without them the server accepts a run and no
|
|
71
|
+
* world is ever started to execute it, so it sits `pending` with no error
|
|
72
|
+
* anywhere. Both are absent for a project with no `workflows/` directory.
|
|
73
|
+
*/
|
|
74
|
+
const workflowCode =
|
|
75
|
+
typeof worker.__aaiWorkflowCode === "string" ? worker.__aaiWorkflowCode : undefined;
|
|
76
|
+
const stepCode = typeof worker.__aaiStepCode === "string" ? worker.__aaiStepCode : undefined;
|
|
58
77
|
|
|
59
78
|
/**
|
|
60
79
|
* Parse a dotenv-syntax file into a record; `{}` when it does not exist.
|
|
@@ -132,6 +151,24 @@ const env = await resolveAgentEnv();
|
|
|
132
151
|
*/
|
|
133
152
|
const publicUrl = process.env.PUBLIC_URL?.trim();
|
|
134
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Create the session-state tables, when this agent has a database.
|
|
156
|
+
*
|
|
157
|
+
* A `DATABASE_URL` puts session state in Postgres, and those tables come with
|
|
158
|
+
* whoever OWNS the database — which for a self-hosted agent is you, with no
|
|
159
|
+
* migration step anywhere to hang them off. Without this the server starts,
|
|
160
|
+
* reports `sessionState: postgres, durable: true`, and then every session dies
|
|
161
|
+
* at start with a fatal error the browser shows as "Session failed to start",
|
|
162
|
+
* the real reason (`relation "aai_session_events" does not exist`) appearing
|
|
163
|
+
* only in this process's log.
|
|
164
|
+
*
|
|
165
|
+
* Best-effort: if a real migration already created them and this role may not
|
|
166
|
+
* CREATE, it warns and the server starts anyway.
|
|
167
|
+
*/
|
|
168
|
+
if (env.DATABASE_URL) {
|
|
169
|
+
await ensureSessionStateSchema({ url: env.DATABASE_URL, logger: console });
|
|
170
|
+
}
|
|
171
|
+
|
|
135
172
|
const server = createAgentServer({
|
|
136
173
|
agent,
|
|
137
174
|
env,
|
|
@@ -141,6 +178,16 @@ const server = createAgentServer({
|
|
|
141
178
|
providerEnv: withHostCredentialFallback(env),
|
|
142
179
|
clientDir: resolveClientDir(),
|
|
143
180
|
...(publicUrl ? { publicUrl } : {}),
|
|
181
|
+
// Durable workflows. A `DATABASE_URL` in `env` puts the runs in Postgres and
|
|
182
|
+
// they survive a restart; without one they live in a per-process directory and
|
|
183
|
+
// do not, which is the same trade `aai dev` makes.
|
|
184
|
+
//
|
|
185
|
+
// Passed straight through rather than conditionally spread: both fields accept
|
|
186
|
+
// `undefined` explicitly, so a project with no `workflows/` directory needs no
|
|
187
|
+
// guard here — and a truthiness-guarded spread is what `guard-invariants`
|
|
188
|
+
// rule 22 counts.
|
|
189
|
+
workflowCode,
|
|
190
|
+
stepCode,
|
|
144
191
|
});
|
|
145
192
|
|
|
146
193
|
// Loopback by default: this server has no request authentication of its own,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { aaiAgentPlugin } from "@alexkroman1/aai/testing/vite";
|
|
1
2
|
import { defineConfig } from "vitest/config";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -16,9 +17,32 @@ import { defineConfig } from "vitest/config";
|
|
|
16
17
|
* explicit `import { test } from "vitest"`. Both spellings are common, both
|
|
17
18
|
* are correct, and tsconfig's `types: ["vitest/globals"]` already promises
|
|
18
19
|
* the un-imported one — this makes the runtime match the types.
|
|
20
|
+
*
|
|
21
|
+
* `reporters` is PINNED, and left unset it is not merely a default — it is a
|
|
22
|
+
* different reporter depending on who is running. Vitest 4 resolves an unset
|
|
23
|
+
* value to `std-env`'s `isAgent ? "agent" : "default"`, and the agent reporter
|
|
24
|
+
* prints a passing file's captured console output nowhere. So `aai test` run by
|
|
25
|
+
* a coding agent — this project's own studio agent, or a CLI agent in your
|
|
26
|
+
* terminal — swallowed every `console.log` from a test that passed, which is
|
|
27
|
+
* exactly where you put one while working out what an agent said or which tool
|
|
28
|
+
* it reached for. Measured on a scaffolded project, vitest 4.1.10: with the
|
|
29
|
+
* agent markers in the environment a module-scope `console.warn` printed
|
|
30
|
+
* nothing; with them stripped it printed; pinning this restored it either way.
|
|
31
|
+
*
|
|
32
|
+
* That reporter exists to keep an agent's output small, so this is a real
|
|
33
|
+
* trade — it is pinned because a debugging line you cannot see costs more than
|
|
34
|
+
* the tokens it saves, and because a test run that behaves differently
|
|
35
|
+
* depending on who typed the command is the harder thing to reason about. Drop
|
|
36
|
+
* the line if you would rather have the terser output.
|
|
37
|
+
*
|
|
38
|
+
* `aaiAgentPlugin` serves `virtual:aai/agent` — the agent lowered the way
|
|
39
|
+
* `aai build` lowers it, so a spec imports one module instead of rebuilding it
|
|
40
|
+
* out of a glob, a `?raw` read and `deployedAgent`.
|
|
19
41
|
*/
|
|
20
42
|
export default defineConfig({
|
|
43
|
+
plugins: [aaiAgentPlugin()],
|
|
21
44
|
test: {
|
|
22
45
|
globals: true,
|
|
46
|
+
reporters: ["default"],
|
|
23
47
|
},
|
|
24
48
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, n as fail } from "./_output-CKkmWs7i.mjs";
|
|
3
3
|
import { n as log, s as unwrapCancel } from "./_ui-u7T4YooX.mjs";
|
|
4
|
-
import { f as checkedResponse, p as isStringArray } from "./_studio-
|
|
5
|
-
import { t as secretRequest } from "./_slug-api
|
|
4
|
+
import { f as checkedResponse, p as isStringArray } from "./_studio-CKrsixd0.mjs";
|
|
5
|
+
import { t as secretRequest } from "./_slug-api--ZBmIGH-.mjs";
|
|
6
6
|
import { isRecord } from "@alexkroman1/aai/utils";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
8
|
import { text } from "node:stream/consumers";
|
|
@@ -5,9 +5,10 @@ import { s as updateProjectConfig } from "./_config-CmJOFsAP.mjs";
|
|
|
5
5
|
import { t as resolveServerEnv } from "./_server-common-6e0QI4mq.mjs";
|
|
6
6
|
import { a as resolveDeployTarget } from "./_agent-BTwZJu4P.mjs";
|
|
7
7
|
import { layerScaffold } from "./_templates-4WcKOjS5.mjs";
|
|
8
|
-
import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-
|
|
8
|
+
import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-CKrsixd0.mjs";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
10
|
-
import { isRecord } from "@alexkroman1/aai/utils";
|
|
11
|
+
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
11
12
|
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
12
13
|
//#region studio.ts
|
|
13
14
|
/**
|
|
@@ -74,7 +75,7 @@ async function executePull(opts) {
|
|
|
74
75
|
serverUrl,
|
|
75
76
|
studioProject: opts.project,
|
|
76
77
|
studioSourceHash: remote.sourceHash,
|
|
77
|
-
...
|
|
78
|
+
...omitUndefined({ slug: remote.deployedSlug })
|
|
78
79
|
});
|
|
79
80
|
const count = Object.keys(remote.files).length;
|
|
80
81
|
log.success(`Pulled ${opts.project} (${count} files) into ${target}`);
|
|
@@ -95,6 +96,7 @@ async function pushProject(opts) {
|
|
|
95
96
|
const { files, warnings } = await collectSourceFiles(opts.cwd);
|
|
96
97
|
for (const warning of warnings) log.warn(warning);
|
|
97
98
|
if (Object.keys(files).length === 0) throw new Error("Nothing to push — this directory has no project files.");
|
|
99
|
+
if (!files["agent.ts"] && existsSync(path.join(opts.cwd, "agent.ts"))) throw new CliError("entry_not_synced", warnings.find((w) => w.startsWith("agent.ts ")) ?? "agent.ts exists locally but was not synced.", "The entry file must sync to deploy — reduce its size or fix its encoding.");
|
|
98
100
|
let project = config?.studioProject;
|
|
99
101
|
let baseHash = config?.studioSourceHash;
|
|
100
102
|
let slug = config?.slug;
|
|
@@ -114,7 +116,7 @@ async function pushProject(opts) {
|
|
|
114
116
|
serverUrl,
|
|
115
117
|
studioProject: project,
|
|
116
118
|
studioSourceHash: result.sourceHash,
|
|
117
|
-
...
|
|
119
|
+
...omitUndefined({ slug })
|
|
118
120
|
});
|
|
119
121
|
return {
|
|
120
122
|
project,
|
|
@@ -166,7 +168,7 @@ async function executePublish(opts) {
|
|
|
166
168
|
const hadSlug = pushed.slug !== void 0;
|
|
167
169
|
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, project);
|
|
168
170
|
log.step(`Publishing ${project} (builds in the project's sandbox)…`);
|
|
169
|
-
const result = checkedResponse(await publishStudioProject(serverUrl, apiKey, project), (value) => isRecord(value) && typeof value.slug === "string" && typeof value.output === "string", `the publish route at ${serverUrl}`);
|
|
171
|
+
const result = checkedResponse(await publishStudioProject(serverUrl, apiKey, project, { skipTypecheck: opts.skipTypecheck }), (value) => isRecord(value) && typeof value.slug === "string" && typeof value.output === "string", `the publish route at ${serverUrl}`);
|
|
170
172
|
if (result.output.trim()) log.message(result.output.trim());
|
|
171
173
|
await updateProjectConfig(opts.cwd, {
|
|
172
174
|
serverUrl,
|