@alexkroman1/aai-cli 5.14.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +8 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -28,7 +28,16 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
|
28
28
|
`aai init --template <name>` scaffolds a fresh project from one. Closest
|
|
29
29
|
matches: `simple`, `pipeline-simple`, `web-researcher`, `solo-rpg`,
|
|
30
30
|
`pizza-ordering`, `retail` (the most complex — 15 tools over a
|
|
31
|
-
relational store, with a `syncState`-driven UI).
|
|
31
|
+
relational store, with a `syncState`-driven UI). Four are ports of agents you
|
|
32
|
+
may already know from LangChain/LangGraph, and each says in its own source
|
|
33
|
+
what had to change: `travel-concierge` (their customer-support bot —
|
|
34
|
+
specialist desks, and every booking staged for a spoken confirmation before
|
|
35
|
+
it applies), `support-line` (self-RAG/CRAG — retrieve, grade what came back,
|
|
36
|
+
rewrite the question, and refuse to speak an answer it cannot ground),
|
|
37
|
+
`plan-and-execute` (plan then work the plan — one step per tool call, so the
|
|
38
|
+
caller can redirect between them), and `redline` (the reflection agent — write,
|
|
39
|
+
critique, revise, which is too slow for a phone and so is a PAGE over a
|
|
40
|
+
durable run rather than a voice agent). When reading SDK
|
|
32
41
|
types under
|
|
33
42
|
`node_modules/@alexkroman1/aai*/dist/`, note the built entry points
|
|
34
43
|
re-export with source specifiers (`"./sdk/constants.ts"`,
|
|
@@ -56,22 +65,29 @@ are CLI-only.
|
|
|
56
65
|
|
|
57
66
|
## Running it yourself (`npm start`)
|
|
58
67
|
|
|
59
|
-
`server.mjs` serves this agent from a plain Node process — no
|
|
60
|
-
|
|
68
|
+
`server.mjs` serves this agent from a plain Node process — no platform
|
|
69
|
+
account, nothing managed. It is the deployment counterpart of `aai dev`:
|
|
61
70
|
|
|
62
71
|
```sh
|
|
63
72
|
npm start # http://127.0.0.1:3000
|
|
64
73
|
PORT=8080 HOST=0.0.0.0 npm start # bind every interface, e.g. in a container
|
|
65
74
|
```
|
|
66
75
|
|
|
76
|
+
`npm start` **builds first** (that is the `prestart` script) and then serves
|
|
77
|
+
the result: `server.mjs` boots `.aai/worker.mjs`, the same artifact
|
|
78
|
+
`aai publish` uploads. The build is what makes `tools/` work — a tool is
|
|
79
|
+
registered by existing, and the enumeration happens where the bundle is
|
|
80
|
+
assembled, so a server that loaded `agent.ts` directly would run an agent with
|
|
81
|
+
none of its tools. The same build produces your `client.tsx`, so a custom UI is
|
|
82
|
+
served with no extra step.
|
|
83
|
+
|
|
67
84
|
Secrets work the same as everywhere else: `ctx.env` holds the keys declared
|
|
68
85
|
in `.env` (or `.env.example`), and a real environment variable of that name
|
|
69
86
|
wins — so `docker run -e MY_API_KEY=…` needs no `.env` in the image.
|
|
70
87
|
|
|
71
|
-
|
|
88
|
+
One thing to know: it binds **loopback by default**, because this server has
|
|
72
89
|
no request authentication of its own; set `HOST=0.0.0.0` only behind your own
|
|
73
|
-
proxy or auth.
|
|
74
|
-
otherwise it serves the default UI and says so at startup.
|
|
90
|
+
proxy or auth.
|
|
75
91
|
|
|
76
92
|
Deleting `server.mjs` costs nothing: `aai dev`, `aai publish` and the managed
|
|
77
93
|
platform never read it. `run_code` is the one feature that does not follow —
|
|
@@ -85,8 +101,9 @@ my-agent/
|
|
|
85
101
|
agent.test.ts # Unit tests (optional)
|
|
86
102
|
client.tsx # Custom UI (optional, React)
|
|
87
103
|
shared.ts # Types shared between agent.ts and client.tsx
|
|
88
|
-
system-prompt.md #
|
|
89
|
-
tools/ #
|
|
104
|
+
system-prompt.md # The system prompt — discovered, not imported
|
|
105
|
+
tools/ # One file per tool — this is how a tool is declared
|
|
106
|
+
workflows/ # Durable workflow bodies (optional — see "Workflow apps")
|
|
90
107
|
package.json
|
|
91
108
|
tsconfig.json
|
|
92
109
|
.env # Local dev secrets (gitignored)
|
|
@@ -99,8 +116,9 @@ import { agent } from "@alexkroman1/aai";
|
|
|
99
116
|
|
|
100
117
|
export default agent({
|
|
101
118
|
name: string; // required — display name
|
|
102
|
-
systemPrompt?: string; //
|
|
103
|
-
//
|
|
119
|
+
systemPrompt?: string; // usually ABSENT — write system-prompt.md
|
|
120
|
+
// instead; declare it only to COMPOSE
|
|
121
|
+
// one (`system` is an accepted alias)
|
|
104
122
|
greeting?: string; // default: "Hey there..."
|
|
105
123
|
voice?: string; // TTS voice for the default pipeline, e.g. "michael"
|
|
106
124
|
// (shorthand for tts: assemblyAITts({ voice });
|
|
@@ -111,7 +129,8 @@ export default agent({
|
|
|
111
129
|
s2s?: S2sProvider; // explicit opt-in to speech-to-speech mode
|
|
112
130
|
sttPrompt?: string; // STT guidance for jargon/acronyms
|
|
113
131
|
builtinTools?: BuiltinTool[]; // see built-in tools table
|
|
114
|
-
|
|
132
|
+
// (there is no `tools` field — a tool is a FILE;
|
|
133
|
+
// see "A file in tools/ IS a tool")
|
|
115
134
|
maxSteps?: number; // default: 10 — max tool calls per turn
|
|
116
135
|
toolChoice?: ToolChoice; // "auto" (default) | "required" | "none"
|
|
117
136
|
// | { type: "tool", toolName }
|
|
@@ -122,10 +141,8 @@ export default agent({
|
|
|
122
141
|
interruptionMinDurationMs?: number; // pipeline only — sustained speech (ms) before an interim barge-in interrupts (default 500; 0 disables)
|
|
123
142
|
deadAirCoverMs?: number; // pipeline only — speak a short filler after this much silence in a turn (default 5000; 0 disables)
|
|
124
143
|
resumeFalseInterruption?: boolean; // pipeline only — resume an interrupted reply if no user turn commits (default true)
|
|
125
|
-
preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default
|
|
126
|
-
|
|
127
|
-
// (S is inferred; see "Typing ctx.state")
|
|
128
|
-
syncState?: (state: S) => unknown; // push a projection of state to the client
|
|
144
|
+
preemptiveGeneration?: boolean; // pipeline only — start the reply from a high-confidence interim (default false; true opts in)
|
|
145
|
+
syncState?: StateProjection; // show a slot to the client: slot.projection(view)
|
|
129
146
|
// (read it with useAgentState; see UI hooks)
|
|
130
147
|
});
|
|
131
148
|
```
|
|
@@ -180,21 +197,347 @@ you want the three stages visible in the config or EU data residency across
|
|
|
180
197
|
STT and the LLM gateway. Speech-to-speech (S2S) mode is an explicit opt-in
|
|
181
198
|
via the `s2s` field — see below.
|
|
182
199
|
|
|
183
|
-
|
|
200
|
+
### `system-prompt.md` IS the system prompt
|
|
201
|
+
|
|
202
|
+
**Write the prompt in `system-prompt.md` beside `agent.ts`, and declare
|
|
203
|
+
nothing.** The build discovers the file, so there is no import line and no
|
|
204
|
+
field — the same rule `tools/` follows, applied to the one part of an agent
|
|
205
|
+
that is a DOCUMENT rather than a value:
|
|
184
206
|
|
|
185
207
|
```ts
|
|
208
|
+
// agent.ts — nothing about the prompt appears here
|
|
209
|
+
import { agent } from "@alexkroman1/aai";
|
|
210
|
+
|
|
211
|
+
export default agent({ name: "My Agent" });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```markdown
|
|
215
|
+
<!-- system-prompt.md -->
|
|
216
|
+
You are a concise, friendly assistant.
|
|
217
|
+
|
|
218
|
+
- Keep replies to one or two sentences.
|
|
219
|
+
- Never read a URL aloud.
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Why the file rather than a string: a prompt is markdown — paragraphs, headings,
|
|
223
|
+
bulleted lists — and inline it becomes that document spelled as `\n\n` and `\n-`
|
|
224
|
+
escapes inside one string literal, with no wrapping, no preview, and a diff that
|
|
225
|
+
is one line no matter which bullet changed. Editing the prompt is the main loop
|
|
226
|
+
of building an agent, so it should land in the most reviewable place available,
|
|
227
|
+
not the least.
|
|
228
|
+
|
|
229
|
+
Three rules, each a build error naming the file:
|
|
230
|
+
|
|
231
|
+
- **A file nothing reads is an error.** If `system-prompt.md` exists and
|
|
232
|
+
`agent.ts` declares a DIFFERENT `systemPrompt`, the build fails rather than
|
|
233
|
+
ignoring the file — "I edited the prompt and nothing changed" is the failure
|
|
234
|
+
this mechanism exists to prevent.
|
|
235
|
+
- **An empty file is an error**, not a silent fall-through to the framework
|
|
236
|
+
default. Delete the file if that is what you want.
|
|
237
|
+
- **A `system-prompt/` directory is rejected.** One file, no concatenation
|
|
238
|
+
order to guess.
|
|
239
|
+
|
|
240
|
+
**Composing a prompt is still legal, and it is the one case you write the import
|
|
241
|
+
for.** When part of the prompt is computed — a menu, a catalogue, today's date —
|
|
242
|
+
import the file and build the field; the build sees its own text inside your
|
|
243
|
+
prompt and leaves what you built alone:
|
|
244
|
+
|
|
245
|
+
```ts no-check
|
|
246
|
+
// `no-check`: the prompt file and the menu module are the project's, not this
|
|
247
|
+
// guide's — which is the point of the example.
|
|
186
248
|
/// <reference types="vite/client" />
|
|
187
249
|
import { agent } from "@alexkroman1/aai";
|
|
188
250
|
import systemPrompt from "./system-prompt.md?raw";
|
|
189
|
-
|
|
251
|
+
import { menuText } from "./menu.ts";
|
|
252
|
+
|
|
253
|
+
export default agent({ name: "Pizza", systemPrompt: `${systemPrompt}\n${menuText()}` });
|
|
190
254
|
```
|
|
191
255
|
|
|
256
|
+
`greeting` stays a field, deliberately: it is one sentence with no structure to
|
|
257
|
+
lose, and it crosses the wire to the browser beside `name` and `page`. **A
|
|
258
|
+
document goes in a file, a value stays in the call.**
|
|
259
|
+
|
|
192
260
|
**JSON imports need no attribute.** `resolveJsonModule` is on, so
|
|
193
261
|
`import data from "./knowledge.json"` is all it takes. Do NOT write
|
|
194
262
|
`assert { type: "json" }` — import assertions were replaced by import
|
|
195
263
|
attributes and TypeScript rejects them (`TS2880`). If you want to be
|
|
196
264
|
explicit the modern spelling is `with { type: "json" }`, but plain is fine.
|
|
197
265
|
|
|
266
|
+
## Workflow apps — `workflowApp()`
|
|
267
|
+
|
|
268
|
+
Not every agent's front door is a microphone. When the product is a FORM —
|
|
269
|
+
submit a job, watch it run, read the result — declare it with `workflowApp()`
|
|
270
|
+
instead of `agent()`:
|
|
271
|
+
|
|
272
|
+
```ts no-check
|
|
273
|
+
import { workflow, workflowApp } from "@alexkroman1/aai";
|
|
274
|
+
import { z } from "zod";
|
|
275
|
+
import { digestFlow } from "./workflows/digest.ts";
|
|
276
|
+
|
|
277
|
+
export const digest = workflow({
|
|
278
|
+
description: "Summarize a link and file the digest",
|
|
279
|
+
input: z.object({ url: z.url().describe("The link to digest") }),
|
|
280
|
+
run: digestFlow,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export default workflowApp({
|
|
284
|
+
name: "Link Digest",
|
|
285
|
+
workflows: { digest },
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
That is the whole declaration, and the fields it does NOT take are the point:
|
|
290
|
+
a workflow app has no session and no LLM loop, so `systemPrompt`, `tools`,
|
|
291
|
+
`maxSteps`, `syncState`, `stt`/`llm`/`tts`/`s2s` and every voice knob
|
|
292
|
+
are **compile errors** here, not fields that quietly do nothing. `greeting` and
|
|
293
|
+
`requiredEnv` stay. `workflowApp()` is `agent({ …, page: "static" })` with the
|
|
294
|
+
discriminant already set — same definition object out, so `aai build`,
|
|
295
|
+
`aai dev` and `aai publish` treat it like any other agent.
|
|
296
|
+
|
|
297
|
+
Reach for it when the user asks for something that outlives a request: an
|
|
298
|
+
overnight job, an upload that takes minutes, anything waiting on a third-party
|
|
299
|
+
callback. Reach for `agent()` when someone is on the line — a voice agent can
|
|
300
|
+
also START a workflow from a tool (`ctx.workflows.start(def, input)`) and
|
|
301
|
+
answer the turn, which is the other shape.
|
|
302
|
+
|
|
303
|
+
**Requires storage** (`aai storage enable`, or `DATABASE_URL` under
|
|
304
|
+
`aai dev`): runs live in the database.
|
|
305
|
+
|
|
306
|
+
### Workflow bodies live in `workflows/`
|
|
307
|
+
|
|
308
|
+
The build transforms that directory and nothing else. A `"use workflow"` body
|
|
309
|
+
written in `agent.ts` is never transformed — it runs inline once, with no
|
|
310
|
+
durability and nothing saying so.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { sleep } from "workflow";
|
|
314
|
+
|
|
315
|
+
export async function digestFlow(input: { url: string }) {
|
|
316
|
+
"use workflow";
|
|
317
|
+
|
|
318
|
+
const digest = await summarize(input.url);
|
|
319
|
+
// Suspended, not blocked: the container is free to exit here and the run
|
|
320
|
+
// resumes when it comes due. `"6 hours"` works the same as `"10 seconds"`.
|
|
321
|
+
await sleep("10 seconds");
|
|
322
|
+
return { ...digest, filedAt: await file(digest) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function summarize(url: string) {
|
|
326
|
+
"use step";
|
|
327
|
+
// The whole Node runtime is available in a step: fetch, a model call, a
|
|
328
|
+
// database. Not in the body.
|
|
329
|
+
return { url, headline: `What ${new URL(url).hostname} says`, points: [] };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function file(digest: { url: string }) {
|
|
333
|
+
"use step";
|
|
334
|
+
return new Date().toISOString();
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Three rules, all of which fail silently if broken:
|
|
339
|
+
|
|
340
|
+
- **The body replays from the top on every resume**, so it holds no live handle
|
|
341
|
+
and makes no undurable decision — no `Date.now()`, no `Math.random()`, no
|
|
342
|
+
`fetch`. Those belong in a step, whose result is journaled and returned
|
|
343
|
+
unchanged on replay.
|
|
344
|
+
- **A step's arguments and return value cross a queue**, so they must be
|
|
345
|
+
JSON-shaped and small. Put bytes in storage and pass the key.
|
|
346
|
+
- **A step gets no tool context.** It is bundled and dispatched separately from
|
|
347
|
+
the agent, so there is no `ctx` in one — see below for how it reaches the
|
|
348
|
+
agent's env and a model anyway. `ctx.db` has no step-side equivalent yet.
|
|
349
|
+
|
|
350
|
+
### A step's env, and calling a model from one
|
|
351
|
+
|
|
352
|
+
A step has no `ctx`, so the two things tool code takes for granted come from
|
|
353
|
+
`@alexkroman1/aai/utils` instead. Import them from THERE and not from
|
|
354
|
+
`@alexkroman1/aai` — a `workflows/*.ts` module is bundled separately, and the
|
|
355
|
+
root barrel would drag the whole SDK into that bundle.
|
|
356
|
+
|
|
357
|
+
```ts no-check
|
|
358
|
+
import { requireStepEnv, stepEnv, StepGenerateError, stepGenerate } from "@alexkroman1/aai/utils";
|
|
359
|
+
import { FatalError } from "workflow";
|
|
360
|
+
|
|
361
|
+
async function summarize(url: string, text: string) {
|
|
362
|
+
"use step";
|
|
363
|
+
|
|
364
|
+
// The agent's env by name — the same values a tool reads from `ctx.env`.
|
|
365
|
+
// `requireStepEnv` fails naming the key; `stepEnv` returns undefined.
|
|
366
|
+
const style = stepEnv("DIGEST_STYLE") ?? "plain";
|
|
367
|
+
|
|
368
|
+
// One model call, on the agent's own ASSEMBLYAI_API_KEY and default model.
|
|
369
|
+
return await stepGenerate(`${style} summary of:\n\n${text}`, {
|
|
370
|
+
system: "Reply with two sentences and nothing else.",
|
|
371
|
+
}).catch(stopOrRetry);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// The DevKit retries a step that throws, so decide which failures deserve it.
|
|
375
|
+
// A rate limit does; a bad key does not.
|
|
376
|
+
function stopOrRetry(err: unknown): never {
|
|
377
|
+
if (err instanceof StepGenerateError && !err.retryable) throw new FatalError(err.message);
|
|
378
|
+
throw err;
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Two things to know. **The env is what `.env` and `aai secret put` declare** —
|
|
383
|
+
not your shell, even under `aai dev`, so that a step reads the same values
|
|
384
|
+
before and after a deploy. List what you read in `requiredEnv` and a deploy
|
|
385
|
+
checks it for you. And **`stepGenerate` is not `ctx.generate`**: it is one
|
|
386
|
+
request to the AssemblyAI LLM Gateway, with no tools and no structured output,
|
|
387
|
+
because bundling the AI SDK into a step artifact costs megabytes on every
|
|
388
|
+
deploy. Ask it for JSON and parse the reply if you need a shape.
|
|
389
|
+
|
|
390
|
+
### A step's HTTP: use `stepFetch`, not `fetch`
|
|
391
|
+
|
|
392
|
+
Any outbound request from a step goes through `stepFetch` (also
|
|
393
|
+
`@alexkroman1/aai/utils`). It is not a style preference — `fetch` is the wrong
|
|
394
|
+
call to make from a step, for a reason nothing at the call site shows:
|
|
395
|
+
|
|
396
|
+
```ts no-check
|
|
397
|
+
import { multipartBody, stepFetch, StepTransportError } from "@alexkroman1/aai/utils";
|
|
398
|
+
|
|
399
|
+
async function transcribeChunk(key: string, bytes: Uint8Array, index: number) {
|
|
400
|
+
"use step";
|
|
401
|
+
|
|
402
|
+
// Multipart as BYTES. Never a `FormData` — see below.
|
|
403
|
+
const part = multipartBody({
|
|
404
|
+
name: "audio",
|
|
405
|
+
filename: `chunk-${index}.wav`,
|
|
406
|
+
type: "audio/wav",
|
|
407
|
+
bytes,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const response = await stepFetch("https://sync.assemblyai.com/transcribe", {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: { Authorization: key, ...part.headers },
|
|
413
|
+
body: part.body,
|
|
414
|
+
// Nothing here has a deadline of its own, and a hung request inside a step
|
|
415
|
+
// is a run that never finishes rather than one that retries.
|
|
416
|
+
signal: AbortSignal.timeout(60_000),
|
|
417
|
+
});
|
|
418
|
+
if (!response.ok) throw stepFailure(response);
|
|
419
|
+
return await response.json();
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**`fetch` speaks HTTP/2, and a fan-out is the worst case for that.** Node's
|
|
424
|
+
global `fetch` offers `h2` in ALPN and the far side decides; a server that takes
|
|
425
|
+
it gets every concurrent request from your process multiplexed onto ONE TCP
|
|
426
|
+
connection, sharing one flow-control window. That is fine for small JSON calls
|
|
427
|
+
and pathological for `mapInBatches` over large bodies. Measured on 8 concurrent
|
|
428
|
+
17.66 MB uploads: `fetch` landed 14 of 16 at p50 8094ms, HTTP/1.1 landed 16 of
|
|
429
|
+
16 at p50 3037ms.
|
|
430
|
+
|
|
431
|
+
**And the two it lost are the reason this matters more than the latency.** On
|
|
432
|
+
HTTP/2 a capacity limit arrives as a *stream reset* — `NGHTTP2_ENHANCE_YOUR_CALM`
|
|
433
|
+
— and a stream error carries no HTTP status, so `isTransientStatus` and
|
|
434
|
+
`retryAfter` cannot see it. Every sibling in the batch then retries in lockstep
|
|
435
|
+
into the same reset, exhausts `maxRetries`, and fails the run with
|
|
436
|
+
`TypeError: fetch failed`, whose real cause is two `cause` hops down where
|
|
437
|
+
nothing prints it. Over HTTP/1.1 the identical limit arrives as `503` with
|
|
438
|
+
`retry-after`, which your retry policy already reads.
|
|
439
|
+
|
|
440
|
+
Three rules come with it:
|
|
441
|
+
|
|
442
|
+
- **Bodies are BYTES or a string.** Never hand a `FormData`, `Blob`, `File`,
|
|
443
|
+
`Headers` or `Request` to a step's fetch: those are branded objects, checked
|
|
444
|
+
against the classes of whichever undici the fetch came from, and a foreign one
|
|
445
|
+
is silently stringified — `Content-Type: text/plain` with the 17-byte body
|
|
446
|
+
`[object FormData]`, answered `415`. `multipartBody()` is how a file becomes
|
|
447
|
+
bytes.
|
|
448
|
+
- **A connection failure is a `StepTransportError`**, distinct from a response
|
|
449
|
+
with a bad status because only the first is unclassifiable. It names its whole
|
|
450
|
+
`cause` chain, and `err.codes` is what to branch on (`ECONNRESET`,
|
|
451
|
+
`ETIMEDOUT`, …).
|
|
452
|
+
- **Test it with `stubStepFetch`** (`@alexkroman1/aai/testing`), not
|
|
453
|
+
`vi.stubGlobal("fetch", …)`. The global stub passes — an unpublished slot falls
|
|
454
|
+
back to it — while asserting a path production does not take, and it cannot see
|
|
455
|
+
the request body as bytes.
|
|
456
|
+
|
|
457
|
+
`stepGenerate` already goes through this, so a step that only calls a model gets
|
|
458
|
+
it for free.
|
|
459
|
+
|
|
460
|
+
### A builtin's failure is its RESULT, so narrow it
|
|
461
|
+
|
|
462
|
+
`webSearch`, `visitWebpage` and `fetchJson` (`@alexkroman1/aai/tools`) answer
|
|
463
|
+
`T | ToolFailure` — they do not throw on an HTTP failure, a bot challenge or an
|
|
464
|
+
oversized body, because a tool usually wants to hand the model something useful
|
|
465
|
+
rather than fail the turn:
|
|
466
|
+
|
|
467
|
+
```ts no-check
|
|
468
|
+
import { webSearch } from "@alexkroman1/aai/tools";
|
|
469
|
+
import { isToolFailure } from "@alexkroman1/aai/utils";
|
|
470
|
+
|
|
471
|
+
const found = await webSearch<{ results?: { url?: string }[] }>({ query, max_results: 4 });
|
|
472
|
+
// NOT `(found.results ?? [])` — a REFUSED search would then read as an empty web.
|
|
473
|
+
if (isToolFailure(found)) return `That search failed: ${found.error}`;
|
|
474
|
+
return (found.results ?? []).map((one) => one.url);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**`?? []` is the mistake, and it is a quiet one.** Both shipped templates that
|
|
478
|
+
search wrote it, and one of them had a `catch` for this exact failure — which
|
|
479
|
+
never ran, because a `catch` cannot see a returned value. DuckDuckGo refuses
|
|
480
|
+
often enough that the empty answer is routine, and to the model "no results" and
|
|
481
|
+
"the search was blocked" are different facts: told the first, it concludes the
|
|
482
|
+
pages do not exist and tries again with different words until its budget is gone.
|
|
483
|
+
|
|
484
|
+
An UNTYPED call (`await fetchJson(url)`) stays loose and needs no narrowing —
|
|
485
|
+
naming a shape is what asks the compiler to make you handle the failure.
|
|
486
|
+
|
|
487
|
+
### The page
|
|
488
|
+
|
|
489
|
+
A workflow app's `client.tsx` mounts with `page()` rather than `client()` —
|
|
490
|
+
there is no session to build, so no socket, no audio graph and no microphone
|
|
491
|
+
request. Everything else is the same file, React and Tailwind included.
|
|
492
|
+
|
|
493
|
+
```tsx no-check
|
|
494
|
+
import { createWorkflowApi, page, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
495
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
496
|
+
import type { WorkflowOutputOf } from "@alexkroman1/aai";
|
|
497
|
+
import { useState } from "react";
|
|
498
|
+
import type { digest } from "./agent.ts";
|
|
499
|
+
|
|
500
|
+
// Hoisted: a client built in render is a new object every render.
|
|
501
|
+
const api = createWorkflowApi();
|
|
502
|
+
|
|
503
|
+
export function App() {
|
|
504
|
+
const [runId, setRunId] = useState<string>();
|
|
505
|
+
// The generic is what makes `run.output` typed rather than `unknown`.
|
|
506
|
+
const { run, polling } = useWorkflowRun<WorkflowOutputOf<typeof digest>>(runId, { api });
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<main>
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
onClick={async () => setRunId(await api.start("digest", { url: "https://example.com" }))}
|
|
513
|
+
>
|
|
514
|
+
Digest
|
|
515
|
+
</button>
|
|
516
|
+
{polling && <p>Working. You can close this tab — the run continues.</p>}
|
|
517
|
+
{run?.status === "completed" && <h2>{run.output.headline}</h2>}
|
|
518
|
+
</main>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
page({ name: "Link Digest", component: App });
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
`api.start()` resolves as soon as the RUN EXISTS, not when it finishes — that
|
|
526
|
+
is the whole mechanism. The `runId` is the entire client state, so it survives
|
|
527
|
+
a reload, a different device, or `curl`. Note the workflow is named by the key
|
|
528
|
+
it has in `workflows` above (`"digest"`); nothing else records that string, so
|
|
529
|
+
a rename there is a 400 here rather than a compile error.
|
|
530
|
+
|
|
531
|
+
The same routes are scriptable, which is the other half of having an API:
|
|
532
|
+
|
|
533
|
+
```text
|
|
534
|
+
GET /workflows → the declared workflows, with input schemas
|
|
535
|
+
POST /workflows/runs → { runId } body: { workflow, input?, key?, wait? }
|
|
536
|
+
GET /workflows/runs/:id → a run snapshot
|
|
537
|
+
DELETE /workflows/runs/:id → cancel
|
|
538
|
+
GET /workflows/runs/:id/events → SSE
|
|
539
|
+
```
|
|
540
|
+
|
|
198
541
|
## Pipeline mode
|
|
199
542
|
|
|
200
543
|
Pipeline mode is the default: omitting `stt`/`llm`/`tts` (and `s2s`) gives
|
|
@@ -299,7 +642,7 @@ one-word backchannels like "yeah" don't cut it off);
|
|
|
299
642
|
500 ms; `0` disables; interim transcripts only — committed turns always
|
|
300
643
|
land). End-of-turn detection (how long a pause ends the user's turn)
|
|
301
644
|
belongs to the STT provider: `assemblyAIStt({ minTurnSilenceMs })` (default
|
|
302
|
-
|
|
645
|
+
1600 ms) / `deepgram({ endpointing })` (default 1500 ms), so mid-utterance
|
|
303
646
|
pauses don't split a request.
|
|
304
647
|
`deadAirCoverMs` is how long a turn may go silent before the transport speaks
|
|
305
648
|
a short filler, so a long tool chain doesn't sound like a dropped call. It is
|
|
@@ -311,16 +654,20 @@ caller answers it and the answer barges in.
|
|
|
311
654
|
a barge-in turns out to be noise — no user turn ever commits. The wait is not
|
|
312
655
|
configurable: the resume fires once the transcript stream goes quiet with no
|
|
313
656
|
final, so it can never race a real turn the STT is still endpointing.
|
|
314
|
-
`preemptiveGeneration` (default
|
|
315
|
-
as transcription is confident the caller has finished, and uses that
|
|
657
|
+
`preemptiveGeneration` (default **`false`**) starts generating the reply as
|
|
658
|
+
soon as transcription is confident the caller has finished, and uses that
|
|
316
659
|
already-running answer if the committed transcript matches. It can shorten the
|
|
317
|
-
pause before the agent speaks, and
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
660
|
+
pause before the agent speaks, and it is off by default because the measurement
|
|
661
|
+
came back negative: over a tau2-bench retail run, 16 speculations started, 14
|
|
662
|
+
were adopted at a p50 head start of 0.44s, and 5 of those 14 (36%) were poisoned
|
|
663
|
+
after adoption by a tool call — discarded whole, each having burned p50 0.69s
|
|
664
|
+
first. Net **+8ms per caller turn**, for 44% of its LLM requests thrown away.
|
|
665
|
+
What bounds the downside either way is that a speculation never speaks, calls a
|
|
666
|
+
tool, or enters history until the real turn adopts it, so the worst case is a
|
|
667
|
+
wasted request and a turn that behaves exactly as it would with the flag off.
|
|
668
|
+
Set `preemptiveGeneration: true` to opt in — worth trying on a text-heavy agent,
|
|
669
|
+
since 36% poisoned is a tool-calling agent's number, and pointless on a
|
|
670
|
+
tool-heavy one, where a speculation that reaches a tool call is thrown away.
|
|
324
671
|
|
|
325
672
|
## Providers
|
|
326
673
|
|
|
@@ -465,7 +812,8 @@ same way in `aai dev` and deployed.
|
|
|
465
812
|
|
|
466
813
|
```ts no-check
|
|
467
814
|
ctx.env: Readonly<Record<string, string>> // secrets from .env / aai secret put
|
|
468
|
-
ctx.
|
|
815
|
+
ctx.slots: SlotStore // where sessionSlot() keeps this session's state —
|
|
816
|
+
// reach for the slot, never this (see "Session state")
|
|
469
817
|
ctx.db: Db // SQL database, needs storage enabled (see Database section)
|
|
470
818
|
ctx.messages: readonly Message[] // conversation history [{role, content}]
|
|
471
819
|
ctx.sessionId: string // unique session ID
|
|
@@ -495,17 +843,9 @@ export const lookup = tool({
|
|
|
495
843
|
});
|
|
496
844
|
```
|
|
497
845
|
|
|
498
|
-
**
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
```ts no-check
|
|
503
|
-
ctx.state.count++;
|
|
504
|
-
ctx.state.incidents.filter((i) => i.status === "open");
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
Write the code first. Do NOT add type annotations defensively — almost
|
|
508
|
-
nothing requires them, and time spent on them is time not spent on the agent.
|
|
846
|
+
**The project's tsconfig turns off `noImplicitAny`, so write the code first.**
|
|
847
|
+
Do NOT add type annotations defensively — almost nothing requires them, and time
|
|
848
|
+
spent on them is time not spent on the agent.
|
|
509
849
|
|
|
510
850
|
**The one exception, and it is not optional: annotate any variable you
|
|
511
851
|
declare empty.** With `noImplicitAny` off, TypeScript does not widen an empty
|
|
@@ -527,35 +867,74 @@ so the next push reports the next line, and you can burn a whole session
|
|
|
527
867
|
fixing one call site at a time. This is the single most common way a
|
|
528
868
|
generated agent fails to build.
|
|
529
869
|
|
|
530
|
-
|
|
531
|
-
it turns a misspelled field into a compile error instead of `undefined` at
|
|
532
|
-
runtime:
|
|
870
|
+
### Session state
|
|
533
871
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
872
|
+
**A `sessionSlot` is the only way to keep state across a session's tool calls**,
|
|
873
|
+
and it is one declaration in a shared module:
|
|
874
|
+
|
|
875
|
+
```ts no-check
|
|
876
|
+
// shared.ts — the one place the shape is written down.
|
|
877
|
+
import { sessionSlot } from "@alexkroman1/aai";
|
|
878
|
+
|
|
879
|
+
export type Incident = { id: string; status: "open" | "closed" };
|
|
538
880
|
|
|
539
|
-
|
|
540
|
-
|
|
881
|
+
export const incidentSlot = sessionSlot("incidents", () => ({ items: [] as Incident[] }));
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
```ts no-check
|
|
885
|
+
// tools/list_open.ts — `slot.tool` READS: the body is handed the value, typed.
|
|
886
|
+
import { incidentSlot } from "../shared.ts";
|
|
541
887
|
|
|
542
|
-
|
|
888
|
+
export default incidentSlot.tool({
|
|
543
889
|
description: "List open incidents",
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return ctx.state.incidents.filter((i) => i.status === "open");
|
|
547
|
-
},
|
|
890
|
+
// `i` infers as Incident, and `i.staus` would now be an error.
|
|
891
|
+
execute: (_args, incidents) => incidents.items.filter((i) => i.status === "open"),
|
|
548
892
|
});
|
|
893
|
+
```
|
|
549
894
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
895
|
+
```ts no-check
|
|
896
|
+
// tools/open_incident.ts — `slot.updateTool` WRITES: mutate what you are handed.
|
|
897
|
+
import { incidentSlot } from "../shared.ts";
|
|
898
|
+
import { z } from "zod";
|
|
899
|
+
|
|
900
|
+
export default incidentSlot.updateTool({
|
|
901
|
+
description: "Open an incident",
|
|
902
|
+
inputSchema: z.object({ id: z.string() }),
|
|
903
|
+
execute: ({ id }, incidents) => {
|
|
904
|
+
incidents.items.push({ id, status: "open" });
|
|
905
|
+
return { open: incidents.items.length };
|
|
906
|
+
},
|
|
554
907
|
});
|
|
555
908
|
```
|
|
556
909
|
|
|
557
|
-
|
|
558
|
-
|
|
910
|
+
Four rules, and each is an error rather than advice if you get it wrong:
|
|
911
|
+
|
|
912
|
+
- **`tool` reads, `updateTool` writes.** What a read is handed is FROZEN, so
|
|
913
|
+
mutating it throws instead of quietly going nowhere.
|
|
914
|
+
- **A write is SYNCHRONOUS.** The value you mutate is stored the moment your body
|
|
915
|
+
returns, so an `updateTool` body may not `await`. When you need a model call or
|
|
916
|
+
a fetch first, do it in an ordinary `tool()` and then mutate:
|
|
917
|
+
|
|
918
|
+
```ts no-check
|
|
919
|
+
execute: async (args, ctx) => {
|
|
920
|
+
const priced = await ctx.generate({ prompt: `price ${args.sku}` });
|
|
921
|
+
return cartSlot.update(ctx, (cart) => {
|
|
922
|
+
cart.total = Number(priced.text);
|
|
923
|
+
return { total: cart.total };
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
- **Hold plain data.** Objects, arrays, strings, numbers, booleans and null. A
|
|
929
|
+
`Map`, a `Set`, a `Date` or a class instance is refused with the field named,
|
|
930
|
+
because none of them survives being stored.
|
|
931
|
+
- **State is STORED when your app has a database** (`aai storage enable`, or a
|
|
932
|
+
`DATABASE_URL` in `.env`), so a crash or a redeploy no longer loses it. Without
|
|
933
|
+
one it lives in memory for the life of the process. You write the same code
|
|
934
|
+
either way; that is the reason for the rules above.
|
|
935
|
+
|
|
936
|
+
There is nothing to declare on `agent()` — the slot owns its own default. Use
|
|
937
|
+
`syncState: slot.projection(view)` to show state to a custom client.
|
|
559
938
|
|
|
560
939
|
**`verbatimModuleSyntax` applies to every type you import** — `ToolContext`,
|
|
561
940
|
`ToolDef`, `Message`, provider types. A plain
|
|
@@ -570,31 +949,30 @@ that's also how S2S agents use it. Pass a Zod schema as `schema` for typed
|
|
|
570
949
|
structured output (`generateObject`-style): the result's `object` carries
|
|
571
950
|
the parsed, typed value. A plain JSON Schema object also works.
|
|
572
951
|
|
|
573
|
-
###
|
|
952
|
+
### A tool that calls an API
|
|
574
953
|
|
|
575
954
|
```ts
|
|
576
|
-
|
|
955
|
+
// tools/get_weather.ts → the model calls this "get_weather"
|
|
956
|
+
import { tool } from "@alexkroman1/aai";
|
|
577
957
|
import { z } from "zod";
|
|
578
958
|
|
|
579
|
-
export default
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
`https://api.example.com/weather?q=${city}&key=${ctx.env.WEATHER_KEY}`,
|
|
590
|
-
);
|
|
591
|
-
return resp.json();
|
|
592
|
-
},
|
|
593
|
-
}),
|
|
959
|
+
export default tool({
|
|
960
|
+
description: "Get current weather for a city",
|
|
961
|
+
inputSchema: z.object({
|
|
962
|
+
city: z.string().describe("City name"),
|
|
963
|
+
}),
|
|
964
|
+
async execute({ city }, ctx) {
|
|
965
|
+
const resp = await fetch(
|
|
966
|
+
`https://api.example.com/weather?q=${city}&key=${ctx.env.WEATHER_KEY}`,
|
|
967
|
+
);
|
|
968
|
+
return resp.json();
|
|
594
969
|
},
|
|
595
970
|
});
|
|
596
971
|
```
|
|
597
972
|
|
|
973
|
+
Nothing else. `agent.ts` does not import it, does not list it, and takes no
|
|
974
|
+
`tools` field at all — see "A file in `tools/` IS a tool" below.
|
|
975
|
+
|
|
598
976
|
**Calling the network builtins from your own tool code.** `web_search`,
|
|
599
977
|
`visit_webpage` and `fetch_json` are declared to the MODEL — the LLM calls
|
|
600
978
|
them, and they are not on `ctx`. When your own `execute` needs one, import
|
|
@@ -650,16 +1028,18 @@ the tool also returns an error, because `Promise<DrugInfo>` does not accept
|
|
|
650
1028
|
`{ error: "not found" }`. Every such annotation eventually costs a build
|
|
651
1029
|
round to widen into a union. Let it infer.
|
|
652
1030
|
|
|
653
|
-
###
|
|
1031
|
+
### A file in `tools/` IS a tool — there is no registration step
|
|
654
1032
|
|
|
655
|
-
|
|
1033
|
+
**`tools/` is not a convention, it is the mechanism.** A file there is named for
|
|
1034
|
+
the tool the model calls, default-exports it, and is picked up by the build. It
|
|
1035
|
+
is not imported by `agent.ts` and not listed anywhere:
|
|
656
1036
|
|
|
657
1037
|
```ts
|
|
658
|
-
// tools/roll_dice.ts
|
|
1038
|
+
// tools/roll_dice.ts → the model calls this "roll_dice"
|
|
659
1039
|
import { tool } from "@alexkroman1/aai";
|
|
660
1040
|
import { z } from "zod";
|
|
661
1041
|
|
|
662
|
-
export
|
|
1042
|
+
export default tool({
|
|
663
1043
|
description: "Roll dice",
|
|
664
1044
|
inputSchema: z.object({ sides: z.number() }),
|
|
665
1045
|
execute({ sides }) {
|
|
@@ -669,16 +1049,42 @@ export const rollDice = tool({
|
|
|
669
1049
|
```
|
|
670
1050
|
|
|
671
1051
|
```ts no-check
|
|
672
|
-
// agent.ts
|
|
1052
|
+
// agent.ts — nothing about tools appears here
|
|
673
1053
|
import { agent } from "@alexkroman1/aai";
|
|
674
|
-
import { rollDice } from "./tools/roll_dice.ts";
|
|
675
1054
|
|
|
676
|
-
export default agent({
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1055
|
+
export default agent({ name: "Dice Agent" });
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
Three rules come with it, each a build error naming the file:
|
|
1059
|
+
|
|
1060
|
+
- **The file name is the tool name**, so it must be lowercase, start with a
|
|
1061
|
+
letter, and join words with `_` — `tools/incident_create.ts`, never
|
|
1062
|
+
`incident-create.ts`. Renaming the file renames the tool.
|
|
1063
|
+
- **The export is the DEFAULT export**, and it must be a `tool()` (or a
|
|
1064
|
+
`slot.tool()` / `slot.updateTool()`). A file exporting something else is
|
|
1065
|
+
named at build time rather than becoming a tool that fails per turn.
|
|
1066
|
+
- **`tools/` is flat.** A nested file is rejected, because a provider will not
|
|
1067
|
+
accept a tool name with a `/` in it and inventing a flattening rule would
|
|
1068
|
+
freeze a guess. This applies to a nested HELPER too, not just a nested tool —
|
|
1069
|
+
the build cannot tell them apart, so put shared helpers beside `agent.ts`
|
|
1070
|
+
rather than under `tools/`. The error names the file and both ways out.
|
|
1071
|
+
|
|
1072
|
+
A tool that closes over module-local state, or one built by your own wrapper,
|
|
1073
|
+
still gets its own file — the file names the instance and the factory lives
|
|
1074
|
+
beside it:
|
|
1075
|
+
|
|
1076
|
+
```ts no-check
|
|
1077
|
+
// tools/to_hotel_assistant.ts
|
|
1078
|
+
import { delegationTool } from "../routing.ts";
|
|
1079
|
+
|
|
1080
|
+
export default delegationTool("hotel");
|
|
680
1081
|
```
|
|
681
1082
|
|
|
1083
|
+
Why discovery rather than a map: the map was 62 lines across the shipped
|
|
1084
|
+
templates whose entire content was `snake_case_name: camelCaseImport`, and
|
|
1085
|
+
forgetting one line was **silent** — the file compiled, every check passed, and
|
|
1086
|
+
the tool simply never reached the model.
|
|
1087
|
+
|
|
682
1088
|
## Built-in tools
|
|
683
1089
|
|
|
684
1090
|
Enable via `builtinTools` in `agent()`. **When `builtinTools` is omitted, none
|
|
@@ -770,7 +1176,7 @@ const rows = await ctx.db.query<{ value: { name: string } }>(
|
|
|
770
1176
|
|
|
771
1177
|
Use `ctx.db` for data that must outlive the session (saves, filed records,
|
|
772
1178
|
user profiles). For scratch that only the current session needs, prefer
|
|
773
|
-
`
|
|
1179
|
+
a `sessionSlot` (per-session state, stored when the app has a database); the
|
|
774
1180
|
`remember`/`recall` builtins likewise remain for session-scoped notes the
|
|
775
1181
|
LLM manages itself.
|
|
776
1182
|
|
|
@@ -907,11 +1313,12 @@ also what you want for anything that can be a string, an array, or null.
|
|
|
907
1313
|
**`useAgentState`** — the agent's session state, pushed automatically:
|
|
908
1314
|
|
|
909
1315
|
```ts no-check
|
|
1316
|
+
// shared.ts — the slot owns the shape; `agent()` has no `state` field.
|
|
1317
|
+
export const cartSlot = sessionSlot("cart", () => ({ cart: [] as Item[], staffPin: "" }));
|
|
1318
|
+
|
|
910
1319
|
// agent.ts
|
|
911
1320
|
export default agent({
|
|
912
|
-
|
|
913
|
-
syncState: (s) => ({ cart: s.cart }), // staffPin never leaves the server
|
|
914
|
-
tools: { ... },
|
|
1321
|
+
syncState: cartSlot.projection((s) => ({ cart: s.cart })), // staffPin stays server-side
|
|
915
1322
|
});
|
|
916
1323
|
|
|
917
1324
|
// client.tsx
|
|
@@ -1085,7 +1492,7 @@ Common mistakes when working in aai projects:
|
|
|
1085
1492
|
preview redeploys itself, production on the next publish.
|
|
1086
1493
|
- **The database is per-app.** Rows are shared by every session of one
|
|
1087
1494
|
deployment — key them yourself if sessions must not see each other's data
|
|
1088
|
-
(or keep session-scoped data in `
|
|
1495
|
+
(or keep session-scoped data in a `sessionSlot`).
|
|
1089
1496
|
- **Rime language codes are ISO 639-3** (3-letter, e.g. `"eng"`), not
|
|
1090
1497
|
ISO 639-1 (`"en"`).
|
|
1091
1498
|
|