@alexkroman1/aai-cli 6.11.0 → 8.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/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -18,11 +18,51 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
|
18
18
|
agent that no longer exists. When a test fails after your change, decide
|
|
19
19
|
which side is stale: updating the test to match the new agent is a normal
|
|
20
20
|
fix, not a workaround. Do not delete a test to make it pass.
|
|
21
|
-
3. **Run `pnpm
|
|
21
|
+
3. **Run `pnpm eval` when you change what the agent DOES** — a test asserts
|
|
22
|
+
the agent's shape; an eval drives a real session and asserts what it did.
|
|
23
|
+
Cases live in `agent.eval.test.ts` (the `simple` template ships one):
|
|
24
|
+
|
|
25
|
+
```ts no-check
|
|
26
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
27
|
+
import { expect } from "vitest";
|
|
28
|
+
import agentDef from "./agent.ts";
|
|
29
|
+
|
|
30
|
+
describeEval(agentDef, (test) => {
|
|
31
|
+
test(
|
|
32
|
+
"looks the order up before answering",
|
|
33
|
+
async ({ session }) => {
|
|
34
|
+
// `say()` returns THAT turn — the reply, its tool calls, its events.
|
|
35
|
+
const turn = await session.say("where is order W1234?");
|
|
36
|
+
expect(turn.toolCalls.map((c) => c.name)).toContain("look_up");
|
|
37
|
+
expect(turn.text).toMatch(/shipped/i);
|
|
38
|
+
},
|
|
39
|
+
// What a SCRIPTED model answers with when there is no key (below).
|
|
40
|
+
{ stubReply: "Order W1234 shipped yesterday." },
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Everything is real except the microphone and the speaker: your tools run,
|
|
46
|
+
your prompt runs, the session's own event stream is what you assert over.
|
|
47
|
+
Two things to know before reading a green run:
|
|
48
|
+
|
|
49
|
+
- **With a provider key it uses a LIVE model** — it spends tokens, and it is
|
|
50
|
+
a noisy instrument. One failure is a question, not a verdict; re-run before
|
|
51
|
+
believing either answer.
|
|
52
|
+
- **Without one it uses a SCRIPTED model** answering each case's `stubReply`,
|
|
53
|
+
and says so. That still proves the agent boots, the tools resolve and the
|
|
54
|
+
session reaches a reply — it proves nothing about what the agent SAYS. Give
|
|
55
|
+
a case `{ live: true }` instead when no script could honestly stand in
|
|
56
|
+
(a tool the model has to choose for itself, a refusal, a judgement).
|
|
57
|
+
|
|
58
|
+
No eval can see anything below the audio boundary — when the agent decides
|
|
59
|
+
you stopped talking, barge-in, two sentences merging into one turn. Those
|
|
60
|
+
need `pnpm dev` and your own voice.
|
|
61
|
+
4. **Run `pnpm build` before declaring done** — bundles `agent.ts`,
|
|
22
62
|
type-checks, and validates the manifest. Catches issues `dev` won't.
|
|
23
|
-
|
|
63
|
+
5. **Make small, focused changes** — verify each one before stacking the
|
|
24
64
|
next.
|
|
25
|
-
|
|
65
|
+
6. **Look at templates before writing custom code** — the CLI ships working
|
|
26
66
|
examples inside its own package, at
|
|
27
67
|
`node_modules/@alexkroman1/aai-cli/dist/templates/`. Read them directly;
|
|
28
68
|
`aai init --template <name>` scaffolds a fresh project from one. Closest
|
|
@@ -51,6 +91,7 @@ npx @alexkroman1/aai-cli init # Scaffold a new agent
|
|
|
51
91
|
npx @alexkroman1/aai-cli templates # List available templates
|
|
52
92
|
npx @alexkroman1/aai-cli dev # Start local dev server
|
|
53
93
|
npx @alexkroman1/aai-cli test # Run agent.test.ts via vitest
|
|
94
|
+
npx @alexkroman1/aai-cli eval # Run agent.eval.test.ts against a model
|
|
54
95
|
npx @alexkroman1/aai-cli build # Bundle and validate
|
|
55
96
|
npx @alexkroman1/aai-cli deploy # Deploy to production
|
|
56
97
|
npx @alexkroman1/aai-cli delete # Remove deployed agent
|
|
@@ -59,9 +100,9 @@ npx @alexkroman1/aai-cli secret delete NAME
|
|
|
59
100
|
npx @alexkroman1/aai-cli secret list
|
|
60
101
|
```
|
|
61
102
|
|
|
62
|
-
The scaffold's `package.json` exposes `dev`, `build`, `test`,
|
|
63
|
-
as `pnpm <name>` shortcuts. Other commands (`init`, `templates`,
|
|
64
|
-
are CLI-only.
|
|
103
|
+
The scaffold's `package.json` exposes `dev`, `build`, `test`, `eval` and
|
|
104
|
+
`deploy` as `pnpm <name>` shortcuts. Other commands (`init`, `templates`,
|
|
105
|
+
`delete`, `secret`) are CLI-only.
|
|
65
106
|
|
|
66
107
|
## Running it yourself (`npm start`)
|
|
67
108
|
|
|
@@ -75,11 +116,15 @@ PORT=8080 HOST=0.0.0.0 npm start # bind every interface, e.g. in a container
|
|
|
75
116
|
|
|
76
117
|
`npm start` **builds first** (that is the `prestart` script) and then serves
|
|
77
118
|
the result: `server.mjs` boots `.aai/worker.mjs`, the same artifact
|
|
78
|
-
`aai publish` uploads.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
`aai publish` uploads. It serves your own `client.tsx` build when there is one
|
|
120
|
+
and falls back to `defaultClientDir()` (`@alexkroman1/aai-ui/client-dir`), the
|
|
121
|
+
prebuilt default UI shipped inside the package — the only export of `aai-ui`
|
|
122
|
+
that runs on Node rather than in the browser.
|
|
123
|
+
|
|
124
|
+
The build is what makes `tools/` work — a tool is registered by existing, and
|
|
125
|
+
the enumeration happens where the bundle is assembled, so a server that loaded
|
|
126
|
+
`agent.ts` directly would run an agent with none of its tools. The same build
|
|
127
|
+
produces your `client.tsx`, so a custom UI is served with no extra step.
|
|
83
128
|
|
|
84
129
|
Secrets work the same as everywhere else: `ctx.env` holds the keys declared
|
|
85
130
|
in `.env` (or `.env.example`), and a real environment variable of that name
|
|
@@ -99,6 +144,7 @@ it needs the platform's sandbox and refuses outside one.
|
|
|
99
144
|
my-agent/
|
|
100
145
|
agent.ts # Agent definition (required)
|
|
101
146
|
agent.test.ts # Unit tests (optional)
|
|
147
|
+
agent.eval.test.ts # Behaviour evals, run by `pnpm eval` (optional)
|
|
102
148
|
client.tsx # Custom UI (optional, React)
|
|
103
149
|
shared.ts # Types shared between agent.ts and client.tsx
|
|
104
150
|
system-prompt.md # The system prompt — discovered, not imported
|
|
@@ -127,6 +173,8 @@ export default agent({
|
|
|
127
173
|
llm?: LlmProvider | string; // unset stages default to AssemblyAI
|
|
128
174
|
tts?: TtsProvider; // (llm also takes a model-id string)
|
|
129
175
|
s2s?: S2sProvider; // explicit opt-in to speech-to-speech mode
|
|
176
|
+
// all four types are on "@alexkroman1/aai",
|
|
177
|
+
// and on their own stage subpath
|
|
130
178
|
sttPrompt?: string; // STT guidance for jargon/acronyms
|
|
131
179
|
builtinTools?: BuiltinTool[]; // see built-in tools table
|
|
132
180
|
// (there is no `tools` field — a tool is a FILE;
|
|
@@ -211,6 +259,19 @@ import { agent } from "@alexkroman1/aai";
|
|
|
211
259
|
export default agent({ name: "My Agent" });
|
|
212
260
|
```
|
|
213
261
|
|
|
262
|
+
Declare neither and the agent runs on `DEFAULT_SYSTEM_PROMPT`, exported from
|
|
263
|
+
`@alexkroman1/aai` so you can read what you are replacing — or compose against
|
|
264
|
+
it, rather than restating the voice rules at the bottom of this guide:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { DEFAULT_SYSTEM_PROMPT, agent } from "@alexkroman1/aai";
|
|
268
|
+
|
|
269
|
+
export default agent({
|
|
270
|
+
name: "My Agent",
|
|
271
|
+
systemPrompt: `${DEFAULT_SYSTEM_PROMPT}\n\nYou only ever discuss pizza.`,
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
214
275
|
```markdown
|
|
215
276
|
<!-- system-prompt.md -->
|
|
216
277
|
You are a concise, friendly assistant.
|
|
@@ -344,12 +405,15 @@ async function file(digest: { url: string }) {
|
|
|
344
405
|
}
|
|
345
406
|
```
|
|
346
407
|
|
|
347
|
-
Three rules
|
|
408
|
+
Three rules. The second and third fail silently if broken; the first is
|
|
409
|
+
warned about by `aai build` and `aai dev`, naming the file and the call:
|
|
348
410
|
|
|
349
411
|
- **The body replays from the top on every resume**, so it holds no live handle
|
|
350
412
|
and makes no undurable decision — no `Date.now()`, no `Math.random()`, no
|
|
351
|
-
`fetch`. Those belong in a step, whose result is
|
|
352
|
-
unchanged on replay.
|
|
413
|
+
`crypto.randomUUID()`, no `fetch`. Those belong in a step, whose result is
|
|
414
|
+
journaled and returned unchanged on replay. (The warning reads the built
|
|
415
|
+
workflow bundle, where step bodies have already been removed, so a step doing
|
|
416
|
+
any of this is not flagged — that is what a step is FOR.)
|
|
353
417
|
- **A step's arguments and return value cross a queue**, so they must be
|
|
354
418
|
JSON-shaped and small. Put bytes in storage and pass the key.
|
|
355
419
|
- **A step gets no tool context.** It is bundled and dispatched separately from
|
|
@@ -359,15 +423,15 @@ Three rules, all of which fail silently if broken:
|
|
|
359
423
|
### A step's env, and calling a model from one
|
|
360
424
|
|
|
361
425
|
A step has no `ctx`, so the two things tool code takes for granted come from
|
|
362
|
-
`@alexkroman1/aai/
|
|
426
|
+
`@alexkroman1/aai/step` instead. Import them from THERE and not from
|
|
363
427
|
`@alexkroman1/aai` — a `workflows/*.ts` module is bundled separately, and the
|
|
364
428
|
root barrel would drag the whole SDK into that bundle.
|
|
365
429
|
|
|
366
430
|
```ts no-check
|
|
367
|
-
import {
|
|
368
|
-
import {
|
|
431
|
+
import { stepEnv } from "@alexkroman1/aai/step";
|
|
432
|
+
import { stepGenerateClassified } from "@alexkroman1/aai/step-errors";
|
|
369
433
|
|
|
370
|
-
async function summarize(
|
|
434
|
+
async function summarize(text: string) {
|
|
371
435
|
"use step";
|
|
372
436
|
|
|
373
437
|
// The agent's env by name — the same values a tool reads from `ctx.env`.
|
|
@@ -375,16 +439,9 @@ async function summarize(url: string, text: string) {
|
|
|
375
439
|
const style = stepEnv("DIGEST_STYLE") ?? "plain";
|
|
376
440
|
|
|
377
441
|
// One model call, on the agent's own ASSEMBLYAI_API_KEY and default model.
|
|
378
|
-
return await
|
|
442
|
+
return await stepGenerateClassified(`${style} summary of:\n\n${text}`, {
|
|
379
443
|
system: "Reply with two sentences and nothing else.",
|
|
380
|
-
})
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// The DevKit retries a step that throws, so decide which failures deserve it.
|
|
384
|
-
// A rate limit does; a bad key does not.
|
|
385
|
-
function stopOrRetry(err: unknown): never {
|
|
386
|
-
if (err instanceof StepGenerateError && !err.retryable) throw new FatalError(err.message);
|
|
387
|
-
throw err;
|
|
444
|
+
});
|
|
388
445
|
}
|
|
389
446
|
```
|
|
390
447
|
|
|
@@ -394,16 +451,127 @@ before and after a deploy. List what you read in `requiredEnv` and a deploy
|
|
|
394
451
|
checks it for you. And **`stepGenerate` is not `ctx.generate`**: it is one
|
|
395
452
|
request to the AssemblyAI LLM Gateway, with no tools and no structured output,
|
|
396
453
|
because bundling the AI SDK into a step artifact costs megabytes on every
|
|
397
|
-
deploy.
|
|
454
|
+
deploy. Use `stepGenerateJsonClassified` with a Zod `schema` if you need a shape.
|
|
455
|
+
|
|
456
|
+
### From a step, reach for the `Classified` call
|
|
457
|
+
|
|
458
|
+
`@alexkroman1/aai/step-errors` publishes a wrapper for every `/step` call that
|
|
459
|
+
can fail against a remote service, and **inside a `"use step"` body the wrapper
|
|
460
|
+
is the one to use**:
|
|
461
|
+
|
|
462
|
+
| Raw, on `@alexkroman1/aai/step` | Use this instead, on `@alexkroman1/aai/step-errors` |
|
|
463
|
+
| --- | --- |
|
|
464
|
+
| `stepGenerate` | `stepGenerateClassified` |
|
|
465
|
+
| `stepGenerateJson` | `stepGenerateJsonClassified` |
|
|
466
|
+
| `stepFetch` | `stepFetchOk` |
|
|
467
|
+
| `stepTranscribeSync` | `stepTranscribeSyncClassified` |
|
|
468
|
+
| `stepTranscribeUpload` / `Submit` / `Poll` | the matching `*Classified` |
|
|
469
|
+
| `sendToChannel` (`/channels`) | `sendToChannelClassified` |
|
|
470
|
+
|
|
471
|
+
The whole of what a wrapper adds is `throwStepError`, and that is worth having
|
|
472
|
+
because the DevKit's retry policy is decided by WHICH error a step throws. Raw,
|
|
473
|
+
every failure looks the same to it: a bad API key is retried until the attempts
|
|
474
|
+
run out, and a rate limit backs off for the DevKit's default one second while
|
|
475
|
+
the delay the gateway itself named sits unread on the error. Classified, a
|
|
476
|
+
terminal failure raises `FatalError` and stops, and a transient one raises
|
|
477
|
+
`RetryableError` carrying the far side's own `Retry-After`. That matters most
|
|
478
|
+
where this SDK encourages a fan-out, because N steps hit a rate limit together
|
|
479
|
+
and a second later all N ask again.
|
|
480
|
+
|
|
481
|
+
**Reach for the raw call when the failure is not simply a failure** — a `404`
|
|
482
|
+
that means "already deleted", a `4xx` whose body decides which advice to print.
|
|
483
|
+
Then classify it yourself: `throwStepError(err)`, `throwFatalStepError(err)` to
|
|
484
|
+
stop outright, `toStepError(cause, message)` to build the error without throwing,
|
|
485
|
+
or `throwFfmpegStepError(err)` for a media failure, whose default runs the other
|
|
486
|
+
way (only a `timeout` or an `aborted` is worth another attempt).
|
|
487
|
+
|
|
488
|
+
**Why the split exists, since the wrapper is what you usually want:** this is
|
|
489
|
+
the one authoring module allowed to import the DevKit's `workflow` package, and
|
|
490
|
+
`/step` is not written only for a step — `mapConcurrent` bounds a rate-limited
|
|
491
|
+
call anywhere, `stepFetch` is an ordinary HTTP client, and your specs drive
|
|
492
|
+
exported steps directly. Putting `workflow` in `/step`'s graph would put it in
|
|
493
|
+
every one of those bundles. A step pays nothing for the extra import line.
|
|
494
|
+
|
|
495
|
+
### Media, big files, and transcription from a step
|
|
496
|
+
|
|
497
|
+
Three more subpaths a `workflows/*.ts` module can reach, all with the same
|
|
498
|
+
bundling rule as `/step` — import them there, never through the root barrel:
|
|
499
|
+
|
|
500
|
+
- **`@alexkroman1/aai/transcribe`** — `stepTranscribeSync(bytes)` for a short
|
|
501
|
+
recording, or `stepTranscribeUpload` → `stepTranscribeSubmit` →
|
|
502
|
+
`stepTranscribePoll` for a long one, plus `Transcript`, `TranscribeError` and
|
|
503
|
+
the `TRANSCRIBE_*` limits. Use the `Classified` wrappers above: a provider
|
|
504
|
+
refusal — a container it will not read, a recording with no speech — arrives
|
|
505
|
+
with `retryable: false`, and unclassified a step re-uploads the same bytes
|
|
506
|
+
until its attempts run out.
|
|
507
|
+
- **`@alexkroman1/aai/ffmpeg`** — `transcodeToWav(bytes, { sampleRate })`,
|
|
508
|
+
`runFfmpeg(args)`, `probeMedia(source)` for duration and stream info, and
|
|
509
|
+
`FfmpegError`/`isFfmpegError`. Under `aai dev` it needs ffmpeg on your PATH;
|
|
510
|
+
a `missing-binary` failure says so and carries the install line.
|
|
511
|
+
- **`@alexkroman1/aai/step-files`** — for a recording too big to hold in memory.
|
|
512
|
+
`readUploadToFile(uploadId, path)` streams an upload to disk,
|
|
513
|
+
`writeUploadFromFile(path)` streams one back, and `withTempDir(work)` gives
|
|
514
|
+
both a directory that is cleaned up even when the step throws.
|
|
515
|
+
|
|
516
|
+
```ts no-check
|
|
517
|
+
import { probeMedia, runFfmpeg } from "@alexkroman1/aai/ffmpeg";
|
|
518
|
+
import { throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
519
|
+
import { readUploadToFile, withTempDir } from "@alexkroman1/aai/step-files";
|
|
520
|
+
|
|
521
|
+
export async function measure(uploadId: string) {
|
|
522
|
+
"use step";
|
|
523
|
+
|
|
524
|
+
return await withTempDir(async (dir) => {
|
|
525
|
+
const path = `${dir}/input`;
|
|
526
|
+
// Read the upload ONCE. A five-step version reads it five times, and on a
|
|
527
|
+
// 700 MB recording that is the expensive part by an order of magnitude.
|
|
528
|
+
await readUploadToFile(uploadId, path);
|
|
529
|
+
const media = await probeMedia(path).catch(throwFfmpegStepError);
|
|
530
|
+
return { durationMs: media.durationMs };
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
`call-audit` is the worked example for all three at once.
|
|
536
|
+
|
|
537
|
+
### Posting somewhere — `@alexkroman1/aai/channels`
|
|
538
|
+
|
|
539
|
+
A run that finishes while nobody is on the line needs somewhere to put the
|
|
540
|
+
result. `slackChannel({ webhookUrl })` names a destination and
|
|
541
|
+
`sendToChannelClassified(channel, message)` posts to it:
|
|
542
|
+
|
|
543
|
+
```ts no-check
|
|
544
|
+
import { type ChannelMessage, slackChannel } from "@alexkroman1/aai/channels";
|
|
545
|
+
import { requireStepEnv } from "@alexkroman1/aai/step";
|
|
546
|
+
import { sendToChannelClassified } from "@alexkroman1/aai/step-errors";
|
|
547
|
+
|
|
548
|
+
export async function announce(headline: string, points: string[]) {
|
|
549
|
+
"use step";
|
|
550
|
+
|
|
551
|
+
const message: ChannelMessage = {
|
|
552
|
+
text: headline,
|
|
553
|
+
sections: points.map((point) => ({ text: point })),
|
|
554
|
+
};
|
|
555
|
+
return await sendToChannelClassified(slackChannel({ webhookUrl: requireStepEnv("SLACK_WEBHOOK_URL") }), message);
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The webhook URL is a secret like any other — declare it in `requiredEnv` and set
|
|
560
|
+
it with `aai secret put`. A channel's credential is its DESTINATION and is
|
|
561
|
+
passed in, which is why no channel reads an env var of its own. `ChannelMessage`
|
|
562
|
+
is rendered per platform, so the same message is legal on a channel kind added
|
|
563
|
+
later; `isSlackWebhookUrl` / `isSlackWorkflowTriggerUrl` validate a pasted URL
|
|
564
|
+
before a run depends on it, and `channelAdvice` turns a refusal into a sentence
|
|
565
|
+
a person can act on. `podcast-digest` is the worked example.
|
|
398
566
|
|
|
399
567
|
### A step's HTTP: use `stepFetch`, not `fetch`
|
|
400
568
|
|
|
401
569
|
Any outbound request from a step goes through `stepFetch` (also
|
|
402
|
-
`@alexkroman1/aai/
|
|
570
|
+
`@alexkroman1/aai/step`). It is not a style preference — `fetch` is the wrong
|
|
403
571
|
call to make from a step, for a reason nothing at the call site shows:
|
|
404
572
|
|
|
405
573
|
```ts no-check
|
|
406
|
-
import { multipartBody, stepFetch, StepTransportError } from "@alexkroman1/aai/
|
|
574
|
+
import { multipartBody, stepFetch, StepTransportError } from "@alexkroman1/aai/step";
|
|
407
575
|
|
|
408
576
|
async function transcribeChunk(key: string, bytes: Uint8Array, index: number) {
|
|
409
577
|
"use step";
|
|
@@ -433,7 +601,7 @@ async function transcribeChunk(key: string, bytes: Uint8Array, index: number) {
|
|
|
433
601
|
global `fetch` offers `h2` in ALPN and the far side decides; a server that takes
|
|
434
602
|
it gets every concurrent request from your process multiplexed onto ONE TCP
|
|
435
603
|
connection, sharing one flow-control window. That is fine for small JSON calls
|
|
436
|
-
and pathological for `
|
|
604
|
+
and pathological for `mapConcurrent` over large bodies. Measured on 8 concurrent
|
|
437
605
|
17.66 MB uploads: `fetch` landed 14 of 16 at p50 8094ms, HTTP/1.1 landed 16 of
|
|
438
606
|
16 at p50 3037ms.
|
|
439
607
|
|
|
@@ -470,11 +638,11 @@ it for free.
|
|
|
470
638
|
|
|
471
639
|
A workflow whose answer is a FILE — a summary read aloud, a rendered image, a
|
|
472
640
|
generated PDF — needs two things a first draft reaches for and does not find.
|
|
473
|
-
Both are on `@alexkroman1/aai/
|
|
641
|
+
Both are on `@alexkroman1/aai/step`, and `spoken-summary` is the template that
|
|
474
642
|
shows the whole round trip.
|
|
475
643
|
|
|
476
644
|
```ts no-check
|
|
477
|
-
import { stepSpeak, writeUpload } from "@alexkroman1/aai/
|
|
645
|
+
import { stepSpeak, writeUpload } from "@alexkroman1/aai/step";
|
|
478
646
|
|
|
479
647
|
export async function narrate(script: string) {
|
|
480
648
|
"use step";
|
|
@@ -491,9 +659,11 @@ and the session TTS surface would not help anyway: it is an event stream wired
|
|
|
491
659
|
into a live pipeline's playback, and a step has no turn to be part of and has to
|
|
492
660
|
return a value. So this is the smaller thing: text in, the whole utterance out
|
|
493
661
|
as a WAV, on the same `ASSEMBLYAI_API_KEY` everything else uses. Voices come
|
|
494
|
-
from `ASSEMBLYAI_TTS_VOICES` (`@alexkroman1/aai
|
|
495
|
-
than typing an id, because a wrong one is refused *after* the socket
|
|
496
|
-
produces silence rather than an error.
|
|
662
|
+
from `ASSEMBLYAI_TTS_VOICES` (`@alexkroman1/aai`, or `/tts`) — read that list
|
|
663
|
+
rather than typing an id, because a wrong one is refused *after* the socket
|
|
664
|
+
opens and produces silence rather than an error. The `AssemblyAITtsVoice` type
|
|
665
|
+
gives you autocomplete over it and nothing more: it accepts any string, so that
|
|
666
|
+
a voice the service adds after this release still compiles.
|
|
497
667
|
|
|
498
668
|
**`writeUpload` is `readUpload`'s other direction, and you need it.** A run's
|
|
499
669
|
output is read back as JSON, so audio cannot travel in one — the same rule that
|
|
@@ -560,7 +730,7 @@ request. Everything else is the same file, React and Tailwind included.
|
|
|
560
730
|
```tsx no-check
|
|
561
731
|
import { createWorkflowApi, page, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
562
732
|
import "@alexkroman1/aai-ui/styles.css";
|
|
563
|
-
import type { WorkflowOutputOf } from "@alexkroman1/aai";
|
|
733
|
+
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
564
734
|
import { useState } from "react";
|
|
565
735
|
import type { digest } from "./agent.ts";
|
|
566
736
|
|
|
@@ -678,14 +848,14 @@ descriptor (the descriptor owns its own voice). A raw config that skips
|
|
|
678
848
|
```ts
|
|
679
849
|
import { agent } from "@alexkroman1/aai";
|
|
680
850
|
import { assemblyAIStt } from "@alexkroman1/aai/stt";
|
|
681
|
-
import {
|
|
682
|
-
import {
|
|
851
|
+
import { anthropicLlm } from "@alexkroman1/aai/llm";
|
|
852
|
+
import { cartesiaTts } from "@alexkroman1/aai/tts";
|
|
683
853
|
|
|
684
854
|
export default agent({
|
|
685
855
|
name: "My Agent",
|
|
686
856
|
stt: assemblyAIStt({ model: "universal-3-5-pro" }),
|
|
687
|
-
llm:
|
|
688
|
-
tts:
|
|
857
|
+
llm: anthropicLlm({ model: "claude-haiku-4-5" }),
|
|
858
|
+
tts: cartesiaTts(),
|
|
689
859
|
});
|
|
690
860
|
```
|
|
691
861
|
|
|
@@ -709,7 +879,7 @@ one-word backchannels like "yeah" don't cut it off);
|
|
|
709
879
|
500 ms; `0` disables; interim transcripts only — committed turns always
|
|
710
880
|
land). End-of-turn detection (how long a pause ends the user's turn)
|
|
711
881
|
belongs to the STT provider: `assemblyAIStt({ minTurnSilenceMs })` (default
|
|
712
|
-
1600 ms) / `
|
|
882
|
+
1600 ms) / `deepgramStt({ endpointing })` (default 1500 ms), so mid-utterance
|
|
713
883
|
pauses don't split a request.
|
|
714
884
|
`deadAirCoverMs` is how long a turn may go silent before the transport speaks
|
|
715
885
|
a short filler, so a long tool chain doesn't sound like a dropped call. It is
|
|
@@ -746,12 +916,19 @@ for the providers you actually use.
|
|
|
746
916
|
| Factory | Default model | Env var |
|
|
747
917
|
| --------------- | ---------------------- | -------------------- |
|
|
748
918
|
| `assemblyAIStt` | `"universal-3-5-pro"` | `ASSEMBLYAI_API_KEY` |
|
|
749
|
-
| `
|
|
750
|
-
| `
|
|
751
|
-
| `
|
|
919
|
+
| `deepgramStt` | `"nova-3"` | `DEEPGRAM_API_KEY` |
|
|
920
|
+
| `elevenLabsStt` | `"scribe_v2_realtime"` | `ELEVENLABS_API_KEY` |
|
|
921
|
+
| `sonioxStt` | `"stt-rt-v3"` | `SONIOX_API_KEY` |
|
|
752
922
|
|
|
753
923
|
All STT factories accept `{ model?: string, ... }`. Bare calls
|
|
754
|
-
(`
|
|
924
|
+
(`deepgramStt()`, `sonioxStt()`, etc.) use the default model. Language is spelled
|
|
925
|
+
`language` where the vendor takes one code (`deepgramStt`, `elevenLabsStt`) and
|
|
926
|
+
`languages` where it takes a list (`assemblyAIStt`, `sonioxStt`) — and only
|
|
927
|
+
`deepgramStt`'s unset value means English; the other three auto-detect.
|
|
928
|
+
|
|
929
|
+
`elevenLabsStt` carries the stage in its name because ElevenLabs is
|
|
930
|
+
better known for TTS: when that stage arrives, `elevenLabs` is the name it
|
|
931
|
+
should get.
|
|
755
932
|
|
|
756
933
|
`assemblyAIStt` accepts an optional `region: "eu"` for EU data residency —
|
|
757
934
|
it routes streaming transcription to AssemblyAI's EU endpoints. EU-region
|
|
@@ -762,29 +939,32 @@ API keys require it; the US endpoints reject them. Example:
|
|
|
762
939
|
|
|
763
940
|
| Factory | SDK package | Env var |
|
|
764
941
|
| --------------- | ------------------- | ------------------------------ |
|
|
765
|
-
| `
|
|
766
|
-
| `
|
|
767
|
-
| `
|
|
768
|
-
| `
|
|
769
|
-
| `
|
|
770
|
-
| `
|
|
771
|
-
| `
|
|
772
|
-
| `
|
|
942
|
+
| `anthropicLlm` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |
|
|
943
|
+
| `openaiLlm` | `@ai-sdk/openai` | `OPENAI_API_KEY` |
|
|
944
|
+
| `googleLlm` | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
945
|
+
| `mistralLlm` | `@ai-sdk/mistral` | `MISTRAL_API_KEY` |
|
|
946
|
+
| `xaiLlm` | `@ai-sdk/xai` | `XAI_API_KEY` |
|
|
947
|
+
| `groqLlm` | `@ai-sdk/groq` | `GROQ_API_KEY` |
|
|
948
|
+
| `openrouterLlm` | `@ai-sdk/openai` | `OPENROUTER_API_KEY` |
|
|
949
|
+
| `gatewayLlm` | `ai` (built in) | `AI_GATEWAY_API_KEY` |
|
|
773
950
|
| `assemblyAILlm` | `@ai-sdk/openai` | `ASSEMBLYAI_API_KEY` |
|
|
774
951
|
|
|
775
|
-
LLM factories require `{ model: string }
|
|
776
|
-
|
|
952
|
+
LLM factories require `{ model: string }` — the `ModelOptions` interface,
|
|
953
|
+
shared by all of them except `assemblyAILlm`. Example:
|
|
954
|
+
`anthropicLlm({ model: "claude-haiku-4-5" })`. The argument is required
|
|
955
|
+
because a third-party vendor's catalog is not this SDK's to default from;
|
|
956
|
+
`assemblyAILlm()` is the one bare call, since it has a default model.
|
|
777
957
|
|
|
778
|
-
`
|
|
958
|
+
`openrouterLlm` routes through [OpenRouter](https://openrouter.ai) — an
|
|
779
959
|
OpenAI-compatible endpoint fronting hundreds of models addressed as
|
|
780
960
|
`"creator/model"`, e.g.
|
|
781
|
-
`
|
|
961
|
+
`openrouterLlm({ model: "meta-llama/llama-3.3-70b-instruct" })`. It needs
|
|
782
962
|
no extra SDK install (it reuses the `@ai-sdk/openai` client).
|
|
783
963
|
|
|
784
|
-
`
|
|
964
|
+
`gatewayLlm` routes through the [Vercel AI
|
|
785
965
|
Gateway](https://vercel.com/docs/ai-gateway) — one endpoint fronting
|
|
786
966
|
hundreds of models addressed as `"creator/model"`, e.g.
|
|
787
|
-
`
|
|
967
|
+
`gatewayLlm({ model: "zai/glm-4.6" })`. It needs no extra SDK install
|
|
788
968
|
(the gateway client ships inside the `ai` package).
|
|
789
969
|
|
|
790
970
|
`assemblyAILlm` routes through the [AssemblyAI LLM
|
|
@@ -809,12 +989,12 @@ Mixing providers works the same way — declare the stages you're changing:
|
|
|
809
989
|
|
|
810
990
|
```ts
|
|
811
991
|
import { agent } from "@alexkroman1/aai";
|
|
812
|
-
import {
|
|
992
|
+
import { cartesiaTts } from "@alexkroman1/aai/tts";
|
|
813
993
|
|
|
814
994
|
export default agent({
|
|
815
995
|
name: "My Agent",
|
|
816
996
|
llm: "claude-sonnet-4-6",
|
|
817
|
-
tts:
|
|
997
|
+
tts: cartesiaTts(),
|
|
818
998
|
});
|
|
819
999
|
```
|
|
820
1000
|
|
|
@@ -823,10 +1003,10 @@ export default agent({
|
|
|
823
1003
|
| Factory | Default voice | Env var |
|
|
824
1004
|
| --------------- | ---------------------------------------- | -------------------- |
|
|
825
1005
|
| `assemblyAITts` | `"jane"` | `ASSEMBLYAI_API_KEY` |
|
|
826
|
-
| `
|
|
827
|
-
| `
|
|
1006
|
+
| `cartesiaTts` | `"f786b574-daa5-4673-aa0c-cbe3e8534c02"` | `CARTESIA_API_KEY` |
|
|
1007
|
+
| `rimeTts` | `"cove"` (model `mistv2`) | `RIME_API_KEY` |
|
|
828
1008
|
|
|
829
|
-
Bare calls (`assemblyAITts()`, `
|
|
1009
|
+
Bare calls (`assemblyAITts()`, `cartesiaTts()`, `rimeTts()`) use the defaults.
|
|
830
1010
|
Override with `{ voice, model, language }`.
|
|
831
1011
|
|
|
832
1012
|
**AssemblyAI TTS** shares `ASSEMBLYAI_API_KEY` with AssemblyAI STT and the
|
|
@@ -851,7 +1031,11 @@ Set
|
|
|
851
1031
|
`language` only alongside a voice that speaks it, as an ISO 639-1 code —
|
|
852
1032
|
`"en"`, `"fr"`, `"de"`, `"it"`, `"pt"`, `"es"` are the six the catalog
|
|
853
1033
|
covers, and the SDK translates each to the full name the service wants.
|
|
854
|
-
|
|
1034
|
+
An unsupported code, and a code the declared voice does not speak, are both
|
|
1035
|
+
build errors naming the voices that do speak it — including the one you get by
|
|
1036
|
+
setting `language` alone, since the descriptor then fills in the default
|
|
1037
|
+
English voice. (A voice this release's catalog does not list is passed through:
|
|
1038
|
+
the catalog is the service's, so a voice it ships later still works.)
|
|
855
1039
|
|
|
856
1040
|
**Rime quirk:** language uses ISO 639-3 three-letter codes (e.g. `"eng"`
|
|
857
1041
|
not `"en"`).
|
|
@@ -887,6 +1071,8 @@ ctx.sessionId: string // unique session ID
|
|
|
887
1071
|
ctx.send(event: string, data: unknown): void // push custom event to browser client (silently dropped over 64 KB JSON)
|
|
888
1072
|
ctx.generate(opts): Promise<{ text, object? }> // one-shot LLM call (host-side)
|
|
889
1073
|
// with a `schema`, `object` is REQUIRED and typed by it
|
|
1074
|
+
ctx.delegate(sub, opts): Promise<DelegateResult> // run a subagent — a whole tool loop with its own
|
|
1075
|
+
// context window (see "Subagents")
|
|
890
1076
|
ctx.signal: AbortSignal // aborts on barge-in, reset, session stop, or this call's timeout
|
|
891
1077
|
```
|
|
892
1078
|
|
|
@@ -910,29 +1096,29 @@ export const lookup = tool({
|
|
|
910
1096
|
});
|
|
911
1097
|
```
|
|
912
1098
|
|
|
913
|
-
**
|
|
914
|
-
|
|
915
|
-
|
|
1099
|
+
**Write the code first; let inference do the work.** The project runs `strict`,
|
|
1100
|
+
so a variable declared empty and filled in the same scope widens from what you
|
|
1101
|
+
put in it — `const items = []` followed by `items.push(pick)` infers `Pick[]`
|
|
1102
|
+
with no annotation. Do NOT add type annotations defensively.
|
|
916
1103
|
|
|
917
|
-
**
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1104
|
+
**Annotate the DECLARATION when the first write is somewhere the compiler
|
|
1105
|
+
cannot follow** — inside a callback, or after the value has already been read.
|
|
1106
|
+
The widening only tracks straight-line code in one scope, so in those cases the
|
|
1107
|
+
declaration keeps its starting type:
|
|
921
1108
|
|
|
922
1109
|
```ts no-check
|
|
923
|
-
const items = [];
|
|
924
|
-
let best = null;
|
|
925
|
-
const [picks, set] = useState([]);
|
|
1110
|
+
const items = []; // stays never[] if the only push is in a callback
|
|
1111
|
+
let best = null; // stays null if the only assignment is in a callback
|
|
1112
|
+
const [picks, set] = useState([]); // never[] — useState's argument is read immediately
|
|
926
1113
|
|
|
927
|
-
const items: Pick[] = [];
|
|
928
|
-
let best: Pick | null = null;
|
|
1114
|
+
const items: Pick[] = []; // ✅ annotate the DECLARATION
|
|
1115
|
+
let best: Pick | null = null; // ✅
|
|
929
1116
|
const [picks, set] = useState<Pick[]>([]); // ✅
|
|
930
1117
|
```
|
|
931
1118
|
|
|
932
1119
|
Annotating the *use* instead does not help — the declaration is still wrong,
|
|
933
1120
|
so the next push reports the next line, and you can burn a whole session
|
|
934
|
-
fixing one call site at a time.
|
|
935
|
-
generated agent fails to build.
|
|
1121
|
+
fixing one call site at a time.
|
|
936
1122
|
|
|
937
1123
|
### Session state
|
|
938
1124
|
|
|
@@ -1016,6 +1202,129 @@ that's also how S2S agents use it. Pass a Zod schema as `schema` for typed
|
|
|
1016
1202
|
structured output (`generateObject`-style): the result's `object` carries
|
|
1017
1203
|
the parsed, typed value. A plain JSON Schema object also works.
|
|
1018
1204
|
|
|
1205
|
+
The option bag is `GenerateOptions` and the answer is `GenerateResult`
|
|
1206
|
+
(`GenerateObjectResult<T>` with a `schema`), both exported from
|
|
1207
|
+
`@alexkroman1/aai` — annotate a helper that wraps the call rather than
|
|
1208
|
+
re-describing the shape. `GenerateFn` is the type of `ctx.generate` itself,
|
|
1209
|
+
which is what a spec passes to `createToolContext({ generate })`.
|
|
1210
|
+
|
|
1211
|
+
### When the NEXT step is the hard part — `dialog()` and `procedure()`
|
|
1212
|
+
|
|
1213
|
+
Two declarations for flows, and the difference is who is driving.
|
|
1214
|
+
|
|
1215
|
+
**`dialog()` gates what the MODEL may do next.** A prompt asking the agent to
|
|
1216
|
+
collect an address before taking payment is a suggestion; a dialog is a rule.
|
|
1217
|
+
`dialog(key, spec)` takes `{ initial, states }`, each state carrying an
|
|
1218
|
+
`instruction` the agent is given while it is there and an `on` map of the events
|
|
1219
|
+
that leave it. It is a slot underneath, so the position is persisted with the
|
|
1220
|
+
rest of the session and survives a reconnect.
|
|
1221
|
+
|
|
1222
|
+
```ts no-check
|
|
1223
|
+
import { dialog } from "@alexkroman1/aai";
|
|
1224
|
+
|
|
1225
|
+
export const checkout = dialog("checkout", {
|
|
1226
|
+
initial: "collecting",
|
|
1227
|
+
states: {
|
|
1228
|
+
collecting: {
|
|
1229
|
+
instruction: "Take the order. Confirm it back before charging anything.",
|
|
1230
|
+
on: { CONFIRMED: "paying" },
|
|
1231
|
+
},
|
|
1232
|
+
paying: {
|
|
1233
|
+
instruction: "Take payment with charge_card. Do not add items now.",
|
|
1234
|
+
on: { PAID: "done" },
|
|
1235
|
+
},
|
|
1236
|
+
done: { instruction: "Read back the order number and say goodbye." },
|
|
1237
|
+
},
|
|
1238
|
+
});
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
A tool declared with `checkout.tool({...})` is REFUSED unless the dialog is in a
|
|
1242
|
+
state that allows it, and the refusal reaches the model as a `ToolFailure` it
|
|
1243
|
+
can recover from — the point being that the gate is enforced at EXECUTION
|
|
1244
|
+
rather than hoped for in a prompt. The states and events are inferred from the
|
|
1245
|
+
spec, so a misspelled `send` is a compile error. `dispatch-center` and
|
|
1246
|
+
`solo-rpg` are the worked examples.
|
|
1247
|
+
|
|
1248
|
+
**`procedure()` runs a flow YOU drive, with no model in the loop.** Where a
|
|
1249
|
+
dialog constrains a conversation, a procedure is an algorithm with branches,
|
|
1250
|
+
retries and a bounded budget — a grading loop, a retrieval-and-check cycle —
|
|
1251
|
+
expressed as a statechart rather than as a `while` with four early returns:
|
|
1252
|
+
|
|
1253
|
+
```ts no-check
|
|
1254
|
+
import { procedure } from "@alexkroman1/aai";
|
|
1255
|
+
|
|
1256
|
+
const answer = procedure(ragMachine);
|
|
1257
|
+
const result = await answer.run({ question }, { signal: ctx.signal });
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
`run` resolves with the machine's output, or throws `ProcedureNotFinishedError`
|
|
1261
|
+
if it stops without reaching a final state — which is what makes "we ran out of
|
|
1262
|
+
attempts" a state you declare and handle rather than an error. Options are
|
|
1263
|
+
`ProcedureRunOptions`; the machine is an XState machine, and `xstate` is already
|
|
1264
|
+
an SDK dependency. `support-line` is the worked example.
|
|
1265
|
+
|
|
1266
|
+
### Subagents (`ctx.delegate`)
|
|
1267
|
+
|
|
1268
|
+
`ctx.generate` is ONE prompt. When answering takes an unknown number of tool
|
|
1269
|
+
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
|
+
instructions, model, tools and — the whole point — its own context window.
|
|
1272
|
+
|
|
1273
|
+
```ts
|
|
1274
|
+
import { subagent, tool } from "@alexkroman1/aai";
|
|
1275
|
+
import { z } from "zod";
|
|
1276
|
+
|
|
1277
|
+
const researcher = subagent({
|
|
1278
|
+
name: "researcher",
|
|
1279
|
+
instructions:
|
|
1280
|
+
"Research the task with the tools you have. IMPORTANT: your final message " +
|
|
1281
|
+
"is the only thing the caller sees — end with a self-contained summary.",
|
|
1282
|
+
builtinTools: ["web_search", "visit_webpage"],
|
|
1283
|
+
maxSteps: 6,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
export default tool({
|
|
1287
|
+
description: "Research a question in depth",
|
|
1288
|
+
inputSchema: z.object({ question: z.string() }),
|
|
1289
|
+
execute: async ({ question }, ctx) => {
|
|
1290
|
+
const { text, toolCalls } = await ctx.delegate(researcher, { task: question });
|
|
1291
|
+
return { answer: text, lookups: toolCalls.length };
|
|
1292
|
+
},
|
|
1293
|
+
});
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
Four rules, each of which is how a subagent disappoints when you skip it:
|
|
1297
|
+
|
|
1298
|
+
- **Tell it to summarize.** You receive its FINAL message. A subagent that
|
|
1299
|
+
signs off with "Done." has thrown away everything it read.
|
|
1300
|
+
- **Write the task as a complete brief.** Its context is isolated — it has not
|
|
1301
|
+
heard the conversation. Anything it needs from the call goes in `task`, or in
|
|
1302
|
+
the optional `context` string.
|
|
1303
|
+
- **Give it a budget.** `maxSteps` (default: the framework's) bounds the loop;
|
|
1304
|
+
past it the subagent is asked for its answer with its tools withheld, so a
|
|
1305
|
+
capped run still answers. In a voice session the tool timeout bounds the
|
|
1306
|
+
whole thing, so keep it small.
|
|
1307
|
+
- **Say you are looking it up before you call.** A delegated run takes a
|
|
1308
|
+
moment, and a silent line is the worst thing on a phone call.
|
|
1309
|
+
|
|
1310
|
+
Runs are ordinary promises, so several fan out at once — this is the other
|
|
1311
|
+
reason to reach for a subagent:
|
|
1312
|
+
|
|
1313
|
+
```ts no-check
|
|
1314
|
+
const runs = await Promise.allSettled(
|
|
1315
|
+
angles.map((angle) => ctx.delegate(researcher, { task: angle })),
|
|
1316
|
+
);
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
A subagent may name its own `llm` (a cheaper model for a narrower job) and its
|
|
1320
|
+
own `tools` — an explicit map of `tool()` values, which is how you give one
|
|
1321
|
+
run a strictly smaller surface than the agent has. **Delegation is one level
|
|
1322
|
+
deep**: a subagent's own tools get a `ctx.delegate` that refuses.
|
|
1323
|
+
|
|
1324
|
+
In tests, `stubDelegate` from `@alexkroman1/aai/testing` fakes the capability,
|
|
1325
|
+
routed by subagent name; `createToolContext()` defaults `delegate` to a
|
|
1326
|
+
rejection so an unstubbed run cannot reach a real model.
|
|
1327
|
+
|
|
1019
1328
|
### A tool that calls an API
|
|
1020
1329
|
|
|
1021
1330
|
```ts
|
|
@@ -1200,6 +1509,30 @@ Reaching for the `fetch_json` builtin instead is a different design, not a
|
|
|
1200
1509
|
shortcut for the same one: it hands URL choice to the model. You cannot
|
|
1201
1510
|
call it from `execute` — see the builtin table above.
|
|
1202
1511
|
|
|
1512
|
+
## Small helpers — `@alexkroman1/aai/utils`
|
|
1513
|
+
|
|
1514
|
+
Zero-dependency helpers a tool body, a step or a client may reach for, so the
|
|
1515
|
+
same three lines are not rewritten per template. Import from `/utils`, which is
|
|
1516
|
+
safe from a `workflows/*.ts` module and from a browser bundle:
|
|
1517
|
+
|
|
1518
|
+
| Helper | For |
|
|
1519
|
+
| --- | --- |
|
|
1520
|
+
| `errorMessage(err)`, `errorDetail(err)` | Turning an unknown `catch` value into a sentence for the model or the log |
|
|
1521
|
+
| `responseErrorMessage(res, label)` | The same for a non-2xx `Response`, preferring a JSON `error` field over the bare status |
|
|
1522
|
+
| `safeJsonParse(text)` | A parse that answers `undefined` instead of throwing |
|
|
1523
|
+
| `formatBytes`, `formatDuration`, `countWords`, `plural` | Narration. Each returns ONE fixed shape, so a step's progress line and the page rendering the same run cannot disagree — they did, one template printing `1:04:09` from its workflow and `64:09` from its page |
|
|
1524
|
+
| `pushCapped(list, item, max)` | An append that keeps the last N, for a log a session accumulates |
|
|
1525
|
+
| `isRecord(x)`, `omitUndefined(obj)` | The object guard and the spread-free way to drop undefined fields |
|
|
1526
|
+
| `decodeHtmlEntities(text)` | Scraped text on its way to a model |
|
|
1527
|
+
| `createKeyedLock()` / `withLock(lock, key, work)` | Serializing async work per key |
|
|
1528
|
+
|
|
1529
|
+
**`createKeyedLock` is the one an agent most needs and least expects to.** The
|
|
1530
|
+
LLM loop runs a step's tool calls CONCURRENTLY, so two tools mutating the same
|
|
1531
|
+
external resource interleave at every `await`. A session-state mutation is NOT
|
|
1532
|
+
that case — `slot.update`'s window is synchronous — so reach for the lock when
|
|
1533
|
+
the thing being mutated is outside the session. `withLock` takes an optional
|
|
1534
|
+
acquire deadline and throws `KeyedLockTimeoutError` when it runs out.
|
|
1535
|
+
|
|
1203
1536
|
## Database API — `ctx.db`
|
|
1204
1537
|
|
|
1205
1538
|
Persistent SQL storage scoped per app, backed by the app's own Postgres
|
|
@@ -1466,6 +1799,27 @@ Available from `@alexkroman1/aai-ui`:
|
|
|
1466
1799
|
| `MessageList` | — | Messages with auto-scroll, tool calls, transcript |
|
|
1467
1800
|
| `Controls` | — | Stop/Resume + New Conversation buttons |
|
|
1468
1801
|
| `Button` | — | Styled button |
|
|
1802
|
+
| `UploadProgressBar` | `upload, onPause?, onResume?` | Bytes in flight, with pause/resume |
|
|
1803
|
+
|
|
1804
|
+
**Forms are declared, not written.** `<Form onSubmit>` collects typed values off
|
|
1805
|
+
the DOM and hands them over once the browser's own validation passes; the field
|
|
1806
|
+
components — `TextField`, `TextAreaField`, `NumberField`, `SelectField`,
|
|
1807
|
+
`CheckboxField`, `FileField` and `SubmitButton` — are plain named inputs, and
|
|
1808
|
+
`Field`/`FieldShell` are what a custom control wraps itself in to match them.
|
|
1809
|
+
For a workflow app there is usually no field markup at all: `<WorkflowFields
|
|
1810
|
+
workflow="name" />` fetches that workflow's input schema and renders a control
|
|
1811
|
+
per field, so a page written against one workflow serves another.
|
|
1812
|
+
|
|
1813
|
+
```tsx no-check
|
|
1814
|
+
import { Form, WorkflowFields } from "@alexkroman1/aai-ui";
|
|
1815
|
+
|
|
1816
|
+
<Form onSubmit={(values) => submit(values)} error={error}>
|
|
1817
|
+
<WorkflowFields workflow="digest" />
|
|
1818
|
+
</Form>;
|
|
1819
|
+
```
|
|
1820
|
+
|
|
1821
|
+
`transcription-workflow` is the all-declared version; `link-digest` writes its
|
|
1822
|
+
form by hand, which is what the two are for.
|
|
1469
1823
|
|
|
1470
1824
|
The usual shape — note `StartScreen` **wraps** the app rather than sitting
|
|
1471
1825
|
beside it; writing `<StartScreen ... />` self-closing is a `TS2741:
|
|
@@ -1546,6 +1900,14 @@ Never hardcode secrets in agent code.
|
|
|
1546
1900
|
- Define personality, tone, and specialty
|
|
1547
1901
|
- Include when and how to use each tool
|
|
1548
1902
|
|
|
1903
|
+
**Three helpers for the other direction — what the caller SAID.** Speech
|
|
1904
|
+
arrives as words, so `@alexkroman1/aai` publishes the conversions a tool
|
|
1905
|
+
otherwise re-derives: `spokenDigits("four one five")` gives `"415"` for an
|
|
1906
|
+
order number or a phone number, `spokenOrdinal("the third one")` gives `3`, and
|
|
1907
|
+
`resolveOne(candidates, spoken, opts)` picks the one item a phrase meant —
|
|
1908
|
+
answering a `ToolFailure` the model can act on when nothing matches or several
|
|
1909
|
+
do, which is the case a hand-written `.find()` gets wrong.
|
|
1910
|
+
|
|
1549
1911
|
Patterns by agent type:
|
|
1550
1912
|
|
|
1551
1913
|
- **Code execution:** "You MUST use run_code for ANY math, counting, or
|