@alexkroman1/aai-cli 13.2.0 → 14.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.
Files changed (152) hide show
  1. package/bin.mjs +1 -1
  2. package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
  3. package/dist/_agent.d.ts +23 -0
  4. package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
  5. package/dist/_cli-common.d.ts +45 -6
  6. package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
  7. package/dist/_config.d.ts +25 -1
  8. package/dist/_dev-env.d.ts +1 -1
  9. package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
  10. package/dist/_dev-vite-config.d.ts +3 -3
  11. package/dist/_e2e-test-utils.d.ts +15 -7
  12. package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
  13. package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
  14. package/dist/_output.d.ts +24 -7
  15. package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
  16. package/dist/_resource-commands.d.ts +9 -9
  17. package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
  18. package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
  19. package/dist/_slug-api.d.ts +19 -0
  20. package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
  21. package/dist/_studio-commands.d.ts +13 -13
  22. package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
  23. package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
  24. package/dist/_typecheck-gate.d.ts +3 -1
  25. package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
  26. package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
  27. package/dist/_utils.d.ts +35 -0
  28. package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
  29. package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
  30. package/dist/_workflow-determinism.d.ts +3 -3
  31. package/dist/build-Mxk8gWvX.mjs +108 -0
  32. package/dist/build.d.ts +5 -0
  33. package/dist/cli.mjs +172 -116
  34. package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
  35. package/dist/client-bundler.mjs +1 -1
  36. package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
  37. package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
  38. package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
  39. package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
  40. package/dist/eval.d.ts +2 -0
  41. package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
  42. package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
  43. package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
  44. package/dist/project-config.mjs +1 -1
  45. package/dist/scaffold/.env.example +17 -1
  46. package/dist/scaffold/CLAUDE.md +84 -56
  47. package/dist/scaffold/package.json +6 -5
  48. package/dist/scaffold/server.mjs +129 -25
  49. package/dist/secret-Cjp912yF.mjs +162 -0
  50. package/dist/secret.d.ts +48 -13
  51. package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
  52. package/dist/templates/briefing-desk/agent.test.ts +7 -3
  53. package/dist/templates/call-audit/agent.test.ts +25 -9
  54. package/dist/templates/call-audit/agent.ts +2 -2
  55. package/dist/templates/call-audit/client.tsx +40 -58
  56. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  57. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  58. package/dist/templates/call-audit/workflows/media.ts +12 -37
  59. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  60. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  61. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  62. package/dist/templates/dispatch-center/client.tsx +53 -61
  63. package/dist/templates/dispatch-center/shared.ts +0 -8
  64. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  65. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  66. package/dist/templates/embedded-assets/agent.test.ts +7 -1
  67. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  68. package/dist/templates/health-assistant/agent.test.ts +8 -4
  69. package/dist/templates/infocom-adventure/client.tsx +40 -36
  70. package/dist/templates/link-digest/agent.test.ts +10 -4
  71. package/dist/templates/link-digest/agent.ts +2 -2
  72. package/dist/templates/link-digest/client.tsx +42 -36
  73. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  74. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  75. package/dist/templates/night-owl/agent.test.ts +6 -1
  76. package/dist/templates/night-owl/client.tsx +8 -2
  77. package/dist/templates/pipeline-simple/agent.test.ts +33 -16
  78. package/dist/templates/pizza-ordering/client.tsx +5 -4
  79. package/dist/templates/pizza-ordering/shared.ts +3 -8
  80. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  81. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  82. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  83. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  84. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  85. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  86. package/dist/templates/plan-and-execute/client.tsx +2 -2
  87. package/dist/templates/podcast-digest/agent.test.ts +33 -14
  88. package/dist/templates/podcast-digest/client.tsx +27 -36
  89. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  90. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  91. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  92. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  93. package/dist/templates/recap-workflow/agent.test.ts +49 -32
  94. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  95. package/dist/templates/redline/agent.eval.test.ts +5 -8
  96. package/dist/templates/redline/agent.test.ts +8 -2
  97. package/dist/templates/redline/client.tsx +47 -48
  98. package/dist/templates/redline/workflows/redline.ts +17 -17
  99. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  100. package/dist/templates/research-workflow/agent.test.ts +17 -8
  101. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  102. package/dist/templates/retail/agent.test.ts +17 -15
  103. package/dist/templates/retail/cancel.ts +7 -6
  104. package/dist/templates/retail/client.tsx +39 -58
  105. package/dist/templates/retail/payment.ts +4 -3
  106. package/dist/templates/retail/pending.test.ts +19 -19
  107. package/dist/templates/retail/registry.test.ts +99 -58
  108. package/dist/templates/retail/store.test.ts +5 -5
  109. package/dist/templates/retail/swap.ts +8 -11
  110. package/dist/templates/simple/agent.test.ts +39 -6
  111. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  112. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  113. package/dist/templates/solo-rpg/client.tsx +3 -3
  114. package/dist/templates/spoken-summary/agent.test.ts +21 -12
  115. package/dist/templates/spoken-summary/agent.ts +3 -3
  116. package/dist/templates/spoken-summary/client.tsx +36 -47
  117. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  118. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  119. package/dist/templates/support-line/client.tsx +4 -3
  120. package/dist/templates/support-line/prompts.ts +0 -2
  121. package/dist/templates/transcription-workflow/agent.test.ts +36 -20
  122. package/dist/templates/transcription-workflow/agent.ts +2 -2
  123. package/dist/templates/transcription-workflow/client.tsx +34 -40
  124. package/dist/templates/transcription-workflow/recover.ts +1 -1
  125. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  126. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  127. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  128. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  129. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  130. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  131. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  132. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  133. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  134. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  135. package/dist/templates/travel-concierge/client.tsx +2 -2
  136. package/dist/templates/travel-concierge/shared.ts +10 -14
  137. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  138. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  139. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  140. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  141. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  142. package/dist/test-D032V2cA.mjs +242 -0
  143. package/dist/test.d.ts +97 -31
  144. package/dist/typecheck.mjs +2 -2
  145. package/dist/worker-bundler.mjs +3 -3
  146. package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
  147. package/package.json +10 -10
  148. package/dist/bin.d.mts +0 -2
  149. package/dist/build-BLlLOrng.mjs +0 -66
  150. package/dist/secret-B-3T5tvr.mjs +0 -61
  151. package/dist/test-CiLab-AA.mjs +0 -182
  152. package/dist/tsdown.config.d.ts +0 -2
@@ -77,7 +77,8 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
77
77
  you stopped talking, barge-in, two sentences merging into one turn. Those
78
78
  need `pnpm dev` and your own voice.
79
79
  4. **Run `pnpm build` before declaring done** — bundles `agent.ts`,
80
- type-checks, and validates the manifest. Catches issues `dev` won't.
80
+ type-checks, validates the manifest, and runs the WHOLE spec suite first.
81
+ Catches issues `dev` won't.
81
82
  5. **Make small, focused changes** — verify each one before stacking the
82
83
  next.
83
84
  6. **Look at templates before writing custom code** — the CLI ships working
@@ -109,6 +110,7 @@ npx @alexkroman1/aai-cli init # Scaffold a new agent
109
110
  npx @alexkroman1/aai-cli templates # List available templates
110
111
  npx @alexkroman1/aai-cli dev # Start local dev server
111
112
  npx @alexkroman1/aai-cli test # Run agent.test.ts via vitest
113
+ npx @alexkroman1/aai-cli test --all # ...or every spec in the project
112
114
  npx @alexkroman1/aai-cli eval # Run agent.eval.test.ts against a model
113
115
  npx @alexkroman1/aai-cli build # Bundle and validate
114
116
  npx @alexkroman1/aai-cli deploy # Deploy to production
@@ -122,6 +124,13 @@ The scaffold's `package.json` exposes `dev`, `build`, `test`, `eval` and
122
124
  `deploy` as `pnpm <name>` shortcuts. Other commands (`init`, `templates`,
123
125
  `delete`, `secret`) are CLI-only.
124
126
 
127
+ **`aai test` targets `agent.test.ts` and nothing else**, which `--all` widens.
128
+ What matters is that a narrowed run does not report itself as a pass: when the
129
+ project holds spec files the run did not cover, it FAILS and names them rather
130
+ than printing a green line that says nothing about `tools/*.test.ts`. `pnpm
131
+ test` (the scaffold's own script) already runs the whole suite, and so does the
132
+ gate in front of `aai build`.
133
+
125
134
  ## Running it yourself (`npm start`)
126
135
 
127
136
  `server.mjs` serves this agent from a plain Node process — no platform
@@ -416,14 +425,14 @@ below.
416
425
 
417
426
  ### Workflow bodies live in `workflows/`
418
427
 
419
- A body is an ordinary exported async function of its input and a `WorkflowCtx`.
428
+ A body is an ordinary exported async function of its input and a `WorkflowContext`.
420
429
  There is no directive and no compile step of its own — the agent bundle compiles
421
430
  `workflows/` like any other source file — and durability is a method call:
422
431
 
423
432
  ```ts
424
- import type { WorkflowCtx } from "@alexkroman1/aai";
433
+ import type { WorkflowContext } from "@alexkroman1/aai";
425
434
 
426
- export async function digestFlow(input: { url: string }, ctx: WorkflowCtx) {
435
+ export async function digestFlow(input: { url: string }, ctx: WorkflowContext) {
427
436
  const digest = await ctx.step("summarize", () => summarize(input.url));
428
437
 
429
438
  // Suspended, not blocked: the container is free to exit here and the run
@@ -515,11 +524,11 @@ The three undurable reads a body most often wants, each journaled — read once
515
524
  the first reach, and the same value on every later walk:
516
525
 
517
526
  ```ts
518
- import type { WorkflowCtx } from "@alexkroman1/aai";
527
+ import type { WorkflowContext } from "@alexkroman1/aai";
519
528
 
520
529
  declare function charge(amount: number, idempotencyKey: string, jitter: number): Promise<void>;
521
530
 
522
- export async function chargeFlow(input: { amount: number }, ctx: WorkflowCtx) {
531
+ export async function chargeFlow(input: { amount: number }, ctx: WorkflowContext) {
523
532
  const startedAt = await ctx.now(); // epoch ms, decided once
524
533
  const idempotencyKey = await ctx.uuid(); // still the same id after a crash
525
534
  const jitter = await ctx.random(); // one float in [0, 1), journaled per call
@@ -601,6 +610,22 @@ Five things worth knowing:
601
610
  which is how a "send it now" tool cuts a scheduled wait short. Naming no ids
602
611
  wakes every outstanding SLEEP and deliberately not a `waitFor` deadline, so
603
612
  cutting a schedule short cannot also close an approval window.
613
+ - **A SUSPEND is not free, so `ctx.sleep` is not a `setTimeout`.** A wait costs
614
+ a journal write to record it, a queued delivery to bring the run back, and a
615
+ fresh WALK of the body — measured on a deployed agent at roughly a second of
616
+ overhead around the sleep itself, on top of whatever you asked for. There is a
617
+ cliff at the bottom of the range worth knowing about: a sleep shorter
618
+ than the round trip that records it never suspends AT ALL — `ctx.sleep("beat",
619
+ 100)` and `ctx.sleep("beat", 0)` are both simply free — while anything longer
620
+ pays the whole cost. Measured, `nap(100)` and `nap(0)` came back within 30 ms
621
+ of each other on a run whose total was 2.1 s.
622
+
623
+ So a sub-second pause is not what this is for. For a short backoff inside a
624
+ step, use an ordinary timer (`sleep` from `@alexkroman1/aai/internal`) — a step
625
+ body may not call `ctx.sleep` anyway, and the engine refuses one that does. Use
626
+ `ctx.sleep` for a wait you want to SURVIVE the process, which is the thing a
627
+ timer cannot do. A body that polls in a loop pays the suspend per iteration,
628
+ which is the strongest argument for the next section: park on the callback.
604
629
 
605
630
  #### A third-party callback is an OPTIMIZATION over a reconciling read
606
631
 
@@ -656,15 +681,15 @@ trap somebody has already paid for:
656
681
  Steps are ordinary exported functions, so a spec imports and calls them. The
657
682
  BODY needs an engine, and there are two, for two different questions.
658
683
 
659
- **"What did the body ask for?"** — `createWorkflowCtx` from
684
+ **"What did the body ask for?"** — `createWorkflowContext` from
660
685
  `@alexkroman1/aai/testing`. It runs the steps and records the names, the retry
661
686
  policies and the sleeps, over one walk with no journal. Nothing replays, so a
662
687
  spec built on it must not claim to test durability.
663
688
 
664
689
  ```ts no-check
665
- import { createWorkflowCtx } from "@alexkroman1/aai/testing";
690
+ import { createWorkflowContext } from "@alexkroman1/aai/testing";
666
691
 
667
- const ctx = createWorkflowCtx({ runSteps: false });
692
+ const ctx = createWorkflowContext({ runSteps: false });
668
693
  await digestFlow({ url: "https://example.com/a" }, ctx);
669
694
 
670
695
  expect(ctx.steps.map((s) => s.name)).toEqual(["fetchArticle", "summarize", "file"]);
@@ -729,7 +754,7 @@ root barrel would drag the whole SDK into that bundle.
729
754
 
730
755
  ```ts
731
756
  import { stepEnv } from "@alexkroman1/aai/step";
732
- import { stepGenerateClassified } from "@alexkroman1/aai/step-errors";
757
+ import { stepGenerateOrFail } from "@alexkroman1/aai/step-errors";
733
758
 
734
759
  async function summarize(text: string) {
735
760
  // The agent's env by name — the same values a tool reads from `ctx.env`.
@@ -737,7 +762,7 @@ async function summarize(text: string) {
737
762
  const style = stepEnv("DIGEST_STYLE") ?? "plain";
738
763
 
739
764
  // One model call, on the agent's own ASSEMBLYAI_API_KEY and default model.
740
- return await stepGenerateClassified(`${style} summary of:\n\n${text}`, {
765
+ return await stepGenerateOrFail(`${style} summary of:\n\n${text}`, {
741
766
  system: "Reply with two sentences and nothing else.",
742
767
  });
743
768
  }
@@ -749,9 +774,9 @@ before and after a deploy. List what you read in `requiredEnv` and a deploy
749
774
  checks it for you. And **`stepGenerate` is not `ctx.generate`**: it is one
750
775
  request to the AssemblyAI LLM Gateway, with no tools and no structured output,
751
776
  because bundling the AI SDK into a step artifact costs megabytes on every
752
- deploy. Use `stepGenerateJsonClassified` with a Zod `schema` if you need a shape.
777
+ deploy. Use `stepGenerateJsonOrFail` with a Zod `schema` if you need a shape.
753
778
 
754
- ### From a step, reach for the `Classified` call
779
+ ### From a step, reach for the `OrFail` call
755
780
 
756
781
  `@alexkroman1/aai/step-errors` publishes a wrapper for every `/step` call that
757
782
  can fail against a remote service, and **inside a step the wrapper is the one to
@@ -759,14 +784,14 @@ use**:
759
784
 
760
785
  | Raw, on `@alexkroman1/aai/step` | Use this instead, on `@alexkroman1/aai/step-errors` |
761
786
  | --- | --- |
762
- | `stepGenerate` | `stepGenerateClassified` |
763
- | `stepGenerateJson` | `stepGenerateJsonClassified` |
764
- | `stepFetch` | `stepFetchOk` |
765
- | `stepTranscribeSync` | `stepTranscribeSyncClassified` |
766
- | `stepTranscribeUpload` / `Submit` / `Poll` | the matching `*Classified` |
767
- | `sendToChannel` (`/channels`) | `sendToChannelClassified` |
768
-
769
- `stepFetchOk` is the one that is not spelled `*Classified`, and the name is the
787
+ | `stepGenerate` | `stepGenerateOrFail` |
788
+ | `stepGenerateJson` | `stepGenerateJsonOrFail` |
789
+ | `stepFetch` | `stepFetchOrFail` |
790
+ | `stepTranscribeSync` | `stepTranscribeSyncOrFail` |
791
+ | `stepTranscribeUpload` / `Submit` / `Poll` | the matching `*OrFail` |
792
+ | `sendToChannel` (`/channels`) | `sendToChannelOrFail` |
793
+
794
+ `stepFetchOrFail` is the one that is not spelled `*OrFail`, and the name is the
770
795
  difference: the others turn an already-thrown failure into a classified one,
771
796
  while this also turns a NON-2XX RESPONSE into a throw — `stepFetch` resolves
772
797
  with a `404` rather than raising it. Two changes, so two names.
@@ -804,7 +829,7 @@ bundling rule as `/step` — import them there, never through the root barrel:
804
829
  recording, or `stepTranscribeUpload` → `stepTranscribeSubmit` →
805
830
  `stepTranscribePoll` for a long one, plus `Transcript`, `TranscribeError` and
806
831
  the `TRANSCRIBE_*` limits. (There is no `/transcribe` subpath; transcription
807
- lives on `/step` with the other step primitives.) Use the `Classified`
832
+ lives on `/step` with the other step primitives.) Use the `OrFail`
808
833
  wrappers above: a provider refusal — a container it will not read, a
809
834
  recording with no speech — arrives
810
835
  with `retryable: false`, and unclassified a step re-uploads the same bytes
@@ -841,19 +866,19 @@ export async function measure(uploadId: string) {
841
866
 
842
867
  A run that finishes while nobody is on the line needs somewhere to put the
843
868
  result. `slackChannel({ webhookUrl })` names a destination and
844
- `sendToChannelClassified(channel, message)` posts to it:
869
+ `sendToChannelOrFail(channel, message)` posts to it:
845
870
 
846
871
  ```ts no-check
847
872
  import { type ChannelMessage, slackChannel } from "@alexkroman1/aai/channels";
848
873
  import { requireStepEnv } from "@alexkroman1/aai/step";
849
- import { sendToChannelClassified } from "@alexkroman1/aai/step-errors";
874
+ import { sendToChannelOrFail } from "@alexkroman1/aai/step-errors";
850
875
 
851
876
  export async function announce(headline: string, points: string[]) {
852
877
  const message: ChannelMessage = {
853
878
  text: headline,
854
879
  sections: points.map((point) => ({ text: point })),
855
880
  };
856
- return await sendToChannelClassified(slackChannel({ webhookUrl: requireStepEnv("SLACK_WEBHOOK_URL") }), message);
881
+ return await sendToChannelOrFail(slackChannel({ webhookUrl: requireStepEnv("SLACK_WEBHOOK_URL") }), message);
857
882
  }
858
883
  ```
859
884
 
@@ -862,7 +887,7 @@ it with `aai secret put`. A channel's credential is its DESTINATION and is
862
887
  passed in, which is why no channel reads an env var of its own. `ChannelMessage`
863
888
  is rendered per platform, so the same message is legal on a channel kind added
864
889
  later; `isSlackWebhookUrl` / `isSlackWorkflowTriggerUrl` validate a pasted URL
865
- before a run depends on it, and `channelAdvice` turns a refusal into a sentence
890
+ before a run depends on it, and `explainChannelFailure` turns a refusal into a sentence
866
891
  a person can act on. `podcast-digest` is the worked example.
867
892
 
868
893
  ### A step's HTTP: use `stepFetch`, not `fetch`
@@ -941,11 +966,11 @@ Both are on `@alexkroman1/aai/step`, and `spoken-summary` is the template that
941
966
  shows the whole round trip.
942
967
 
943
968
  ```ts
944
- import { stepSpeak, writeUpload } from "@alexkroman1/aai/step";
969
+ import { stepSpeak, stepWriteUpload } from "@alexkroman1/aai/step";
945
970
 
946
971
  export async function narrate(script: string) {
947
972
  const spoken = await stepSpeak(script, { voice: "jane" });
948
- const stored = await writeUpload(spoken.audio, { name: "summary.wav", type: "audio/wav" });
973
+ const stored = await stepWriteUpload(spoken.audio, { name: "summary.wav", type: "audio/wav" });
949
974
  return { audio: stored.id, durationMs: spoken.durationMs };
950
975
  }
951
976
  ```
@@ -962,11 +987,13 @@ opens and produces silence rather than an error. The `AssemblyAITtsVoice` type
962
987
  gives you autocomplete over it and nothing more: it accepts any string, so that
963
988
  a voice the service adds after this release still compiles.
964
989
 
965
- **`writeUpload` is `readUpload`'s other direction, and you need it.** A run's
966
- output is read back as JSON, so audio cannot travel in one — the same rule that
967
- keeps an uploaded recording's bytes out of a run's INPUT, arriving at the other
968
- end of the run. Store the bytes, return the **id**, and let the page fetch it
969
- with `api.download(id)`.
990
+ **`stepWriteUpload` is `stepReadUpload`'s other direction, and you need it.** A
991
+ run's output is read back as JSON, so audio cannot travel in one — the same rule
992
+ that keeps an uploaded recording's bytes out of a run's INPUT, arriving at the
993
+ other end of the run. Store the bytes, return the **id**, and let the page fetch
994
+ it with `api.download(id)`. A step that wants the record rather than the bytes
995
+ reads it with `stepUploadInfo(id)`, which answers an `UploadInfo` — the name,
996
+ the size stored so far, and whether that is all of it.
970
997
 
971
998
  Three rules come with it:
972
999
 
@@ -1020,12 +1047,13 @@ naming a shape is what asks the compiler to make you handle the failure.
1020
1047
 
1021
1048
  ### The page
1022
1049
 
1023
- A workflow app's `client.tsx` mounts with `page()` rather than `client()` —
1024
- there is no session to build, so no socket, no audio graph and no microphone
1025
- request. Everything else is the same file, React and Tailwind included.
1050
+ A workflow app's `client.tsx` mounts with `mountPage()` rather than
1051
+ `mountClient()` — there is no session to build, so no socket, no audio graph
1052
+ and no microphone request. Everything else is the same file, React and
1053
+ Tailwind included.
1026
1054
 
1027
1055
  ```tsx no-check
1028
- import { createWorkflowApi, page, useWorkflowRun } from "@alexkroman1/aai-ui";
1056
+ import { createWorkflowApi, mountPage, useWorkflowRun } from "@alexkroman1/aai-ui";
1029
1057
  import "@alexkroman1/aai-ui/styles.css";
1030
1058
  import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
1031
1059
  import { useState } from "react";
@@ -1053,7 +1081,7 @@ export function App() {
1053
1081
  );
1054
1082
  }
1055
1083
 
1056
- page({ name: "Link Digest", component: App });
1084
+ mountPage({ name: "Link Digest", component: App });
1057
1085
  ```
1058
1086
 
1059
1087
  `api.start()` resolves as soon as the RUN EXISTS, not when it finishes — that
@@ -1172,7 +1200,7 @@ statement about pipeline mode, not about the SDK.
1172
1200
  ### Answering a phone call
1173
1201
 
1174
1202
  A deployed voice agent already serves carrier media streams — there is nothing
1175
- to switch on. `createServer` mounts `WS /phone` whenever the agent is a voice
1203
+ to switch on. `createRuntimeServer` mounts `WS /phone` whenever the agent is a voice
1176
1204
  agent (`telephony` defaults to `true`, and to `false` for a `page: "static"`
1177
1205
  workflow app, which has no stages to put on a call). Point the carrier at it
1178
1206
  with a `carrier` query parameter naming who is dialling:
@@ -1188,7 +1216,7 @@ the bridge transcodes in both directions, so the agent, its tools and its slots
1188
1216
  behave exactly as they do in the browser — a phone call is a transport, not a
1189
1217
  mode. Nothing about `agent.ts` changes to support one.
1190
1218
 
1191
- Turn the route off with `telephony: false` on `createServer`. If you are
1219
+ Turn the route off with `telephony: false` on `createRuntimeServer`. If you are
1192
1220
  embedding the runtime yourself rather than deploying, the pieces are
1193
1221
  `createTelephonyBridge`, `startTelephonySession`, `TELEPHONY_PATH` and
1194
1222
  `carrierByName`, all on `@alexkroman1/aai-runtime`.
@@ -1268,12 +1296,12 @@ API keys require it; the US endpoints reject them. Example:
1268
1296
  | Factory | SDK package | Env var |
1269
1297
  | --------------- | ------------------- | ------------------------------ |
1270
1298
  | `anthropicLlm` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |
1271
- | `openaiLlm` | `@ai-sdk/openai` | `OPENAI_API_KEY` |
1299
+ | `openAILlm` | `@ai-sdk/openai` | `OPENAI_API_KEY` |
1272
1300
  | `googleLlm` | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |
1273
1301
  | `mistralLlm` | `@ai-sdk/mistral` | `MISTRAL_API_KEY` |
1274
- | `xaiLlm` | `@ai-sdk/xai` | `XAI_API_KEY` |
1302
+ | `xAILlm` | `@ai-sdk/xai` | `XAI_API_KEY` |
1275
1303
  | `groqLlm` | `@ai-sdk/groq` | `GROQ_API_KEY` |
1276
- | `openrouterLlm` | `@ai-sdk/openai` | `OPENROUTER_API_KEY` |
1304
+ | `openRouterLlm` | `@ai-sdk/openai` | `OPENROUTER_API_KEY` |
1277
1305
  | `gatewayLlm` | `ai` (built in) | `AI_GATEWAY_API_KEY` |
1278
1306
  | `assemblyAILlm` | `@ai-sdk/openai` | `ASSEMBLYAI_API_KEY` |
1279
1307
 
@@ -1283,10 +1311,10 @@ shared by all of them except `assemblyAILlm`. Example:
1283
1311
  because a third-party vendor's catalog is not this SDK's to default from;
1284
1312
  `assemblyAILlm()` is the one bare call, since it has a default model.
1285
1313
 
1286
- `openrouterLlm` routes through [OpenRouter](https://openrouter.ai) — an
1314
+ `openRouterLlm` routes through [OpenRouter](https://openrouter.ai) — an
1287
1315
  OpenAI-compatible endpoint fronting hundreds of models addressed as
1288
1316
  `"creator/model"`, e.g.
1289
- `openrouterLlm({ model: "meta-llama/llama-3.3-70b-instruct" })`. It needs
1317
+ `openRouterLlm({ model: "meta-llama/llama-3.3-70b-instruct" })`. It needs
1290
1318
  no extra SDK install (it reuses the `@ai-sdk/openai` client).
1291
1319
 
1292
1320
  `gatewayLlm` routes through the [Vercel AI
@@ -1947,7 +1975,7 @@ Those three cover almost everything an agent wants. A database is for data that
1947
1975
  must outlive a session AND be queryable: a ledger, filed records, cross-session
1948
1976
  saves.
1949
1977
 
1950
- ## Custom UI — `client()`
1978
+ ## Custom UI — `mountClient()`
1951
1979
 
1952
1980
  File: `client.tsx` alongside `agent.ts`. Uses **React** (not Preact).
1953
1981
  Always import `"@alexkroman1/aai-ui/styles.css"` first.
@@ -1957,9 +1985,9 @@ Always import `"@alexkroman1/aai-ui/styles.css"` first.
1957
1985
  ```tsx
1958
1986
  /// <reference types="vite/client" />
1959
1987
  import "@alexkroman1/aai-ui/styles.css";
1960
- import { client } from "@alexkroman1/aai-ui";
1988
+ import { mountClient } from "@alexkroman1/aai-ui";
1961
1989
 
1962
- client({ name: "My Agent" });
1990
+ mountClient({ name: "My Agent" });
1963
1991
  ```
1964
1992
 
1965
1993
  ### Tier 1 with sidebar
@@ -1967,7 +1995,7 @@ client({ name: "My Agent" });
1967
1995
  ```tsx
1968
1996
  /// <reference types="vite/client" />
1969
1997
  import "@alexkroman1/aai-ui/styles.css";
1970
- import { client, useEvent } from "@alexkroman1/aai-ui";
1998
+ import { mountClient, useEvent } from "@alexkroman1/aai-ui";
1971
1999
  import { useState } from "react";
1972
2000
 
1973
2001
  function Sidebar() {
@@ -1982,7 +2010,7 @@ function Sidebar() {
1982
2010
  );
1983
2011
  }
1984
2012
 
1985
- client({ name: "My Agent", sidebar: Sidebar });
2013
+ mountClient({ name: "My Agent", sidebar: Sidebar });
1986
2014
  ```
1987
2015
 
1988
2016
  ### Tier 2 — full custom component
@@ -1990,7 +2018,7 @@ client({ name: "My Agent", sidebar: Sidebar });
1990
2018
  ```tsx
1991
2019
  /// <reference types="vite/client" />
1992
2020
  import "@alexkroman1/aai-ui/styles.css";
1993
- import { client, useSession } from "@alexkroman1/aai-ui";
2021
+ import { mountClient, useSession } from "@alexkroman1/aai-ui";
1994
2022
 
1995
2023
  function MyApp() {
1996
2024
  const { messages, userTranscript, started, running, start, toggle, end } =
@@ -2011,10 +2039,10 @@ function MyApp() {
2011
2039
  );
2012
2040
  }
2013
2041
 
2014
- client({ component: MyApp });
2042
+ mountClient({ component: MyApp });
2015
2043
  ```
2016
2044
 
2017
- ### `client()` config
2045
+ ### `mountClient()` config
2018
2046
 
2019
2047
  | Field | Type | Default | Description |
2020
2048
  | --- | --- | --- | --- |
@@ -2172,7 +2200,7 @@ Property 'children' is missing` build error:
2172
2200
  ```tsx
2173
2201
  /// <reference types="vite/client" />
2174
2202
  import "@alexkroman1/aai-ui/styles.css";
2175
- import { ChatView, client, StartScreen } from "@alexkroman1/aai-ui";
2203
+ import { ChatView, mountClient, StartScreen } from "@alexkroman1/aai-ui";
2176
2204
 
2177
2205
  function PizzaApp() {
2178
2206
  return (
@@ -2182,7 +2210,7 @@ function PizzaApp() {
2182
2210
  );
2183
2211
  }
2184
2212
 
2185
- client({ component: PizzaApp });
2213
+ mountClient({ component: PizzaApp });
2186
2214
  ```
2187
2215
 
2188
2216
  ## Styling
@@ -2190,7 +2218,7 @@ client({ component: PizzaApp });
2190
2218
  - **Tailwind CSS v4** — compiled at bundle time, configured via CSS.
2191
2219
  Do NOT create `tailwind.config.js` — it will be ignored.
2192
2220
  - Use Tailwind classes for layout, `useTheme()` for dynamic colors.
2193
- - Set theme: `client({ theme: { bg, primary, text, surface, border } })`.
2221
+ - Set theme: `mountClient({ theme: { bg, primary, text, surface, border } })`.
2194
2222
  - Override CSS custom properties for extra tokens:
2195
2223
  `--color-aai-*`, `--radius-aai`, `--font-aai`.
2196
2224
  - Always import `"@alexkroman1/aai-ui/styles.css"` at the top of `client.tsx`.
@@ -9,14 +9,15 @@
9
9
  "prestart": "aai build --skip-tests",
10
10
  "start": "node server.mjs",
11
11
  "build": "aai build",
12
- "test": "aai test",
12
+ "test": "vitest run --exclude \"**/*.eval.test.*\"",
13
+ "test:agent": "aai test",
13
14
  "eval": "aai eval",
14
15
  "publish:agent": "aai publish"
15
16
  },
16
17
  "dependencies": {
17
- "@alexkroman1/aai": "^13.2.0",
18
- "@alexkroman1/aai-runtime": "^13.2.0",
19
- "@alexkroman1/aai-ui": "^13.2.0",
18
+ "@alexkroman1/aai": "^14.0.0",
19
+ "@alexkroman1/aai-runtime": "^14.0.0",
20
+ "@alexkroman1/aai-ui": "^14.0.0",
20
21
  "react": "^19.2.8",
21
22
  "react-dom": "^19.2.8",
22
23
  "tailwindcss": "^4.0.0",
@@ -24,7 +25,7 @@
24
25
  "zod": "^4.5.4"
25
26
  },
26
27
  "devDependencies": {
27
- "@alexkroman1/aai-cli": "^13.2.0",
28
+ "@alexkroman1/aai-cli": "^14.0.0",
28
29
  "@tailwindcss/vite": "^4.3.3",
29
30
  "@types/node": "^26.4.1",
30
31
  "@types/react": "^19.2.18",
@@ -122,6 +122,71 @@ function resolveClientDir() {
122
122
  return defaultClientDir();
123
123
  }
124
124
 
125
+ /**
126
+ * The error classes that mean a DEFECT in this code rather than a mistake in
127
+ * the configuration.
128
+ *
129
+ * Everything below turns a boot failure into two lines and a non-zero exit,
130
+ * which is right for "ASSEMBLYAI_API_KEY is not set" and wrong for a
131
+ * `TypeError`, where the traceback is the only thing that can locate the bug.
132
+ * Those are re-thrown untouched.
133
+ */
134
+ const BUG_ERRORS = [TypeError, ReferenceError, RangeError, SyntaxError];
135
+
136
+ /**
137
+ * Every distinct message on an error and its `cause` chain, outermost first.
138
+ *
139
+ * A driver failure states the useful half one hop down — `connect to database
140
+ * failed: getaddrinfo ENOTFOUND db` — so printing only the top message is how a
141
+ * tidy envelope ends up less informative than the stack it replaced.
142
+ *
143
+ * @param {unknown} err
144
+ * @returns {string}
145
+ */
146
+ function errorText(err) {
147
+ /** @type {string[]} */
148
+ const messages = [];
149
+ /** @type {unknown} */
150
+ let cursor = err;
151
+ while (cursor instanceof Error) {
152
+ if (cursor.message !== "" && !messages.includes(cursor.message)) messages.push(cursor.message);
153
+ cursor = cursor.cause;
154
+ }
155
+ return messages.length > 0 ? messages.join(": ") : String(err);
156
+ }
157
+
158
+ /**
159
+ * Run one step of BOOT, and answer a failure the way this file already answers
160
+ * a missing build artifact: what is wrong, then what to do, then exit 1.
161
+ *
162
+ * Without this, a missing provider key — the commonest way a first `npm start`
163
+ * fails — killed the process with a ten-frame traceback pointing into
164
+ * `node_modules/@alexkroman1/aai-runtime/dist/host-env-*.js`. The MESSAGE was
165
+ * already good ("AssemblyAI LLM: missing API key. Set ASSEMBLYAI_API_KEY in the
166
+ * agent env."); what it arrived wrapped in was a crash report about somebody
167
+ * else's bundle, in a container that then restarted and did it again.
168
+ *
169
+ * Exiting is deliberate rather than binding anyway and serving an unhealthy
170
+ * `/health`: a process that stays up tells an orchestrator it started, and a
171
+ * misconfigured deployment that reports itself healthy is worse than one that
172
+ * refuses to run. The non-zero exit is what a supervisor, a `docker run`, and
173
+ * CI all already read.
174
+ *
175
+ * @template T
176
+ * @param {string} fix - What the operator should change, in one sentence.
177
+ * @param {() => T | Promise<T>} work
178
+ * @returns {Promise<T>}
179
+ */
180
+ async function bootOrExit(fix, work) {
181
+ try {
182
+ return await work();
183
+ } catch (err) {
184
+ if (BUG_ERRORS.some((kind) => err instanceof kind)) throw err;
185
+ console.error(`Cannot start the agent: ${errorText(err)}\n${fix}`);
186
+ process.exit(1);
187
+ }
188
+ }
189
+
125
190
  const env = await resolveAgentEnv();
126
191
 
127
192
  /**
@@ -152,38 +217,77 @@ const publicUrl = process.env.PUBLIC_URL?.trim();
152
217
  * Best-effort: if a real migration already created them and this role may not
153
218
  * CREATE, it warns and the server starts anyway.
154
219
  */
155
- if (env.DATABASE_URL) {
156
- await ensureSessionStateSchema({ url: env.DATABASE_URL, logger: console });
157
- // And the durable-run journal's, which is a separate set of tables owned by
158
- // the same deployment. Without it a project with a `DATABASE_URL` boots
159
- // claiming durable runs and fails on the first one.
160
- await ensureWorkflowJournalSchema({ url: env.DATABASE_URL, logger: console });
220
+ // Read into a const: `env.DATABASE_URL` is a record lookup, so its narrowing
221
+ // does not survive into the callback below.
222
+ const databaseUrl = env.DATABASE_URL;
223
+ if (databaseUrl) {
224
+ await bootOrExit(
225
+ "Check DATABASE_URL: this server has to reach that database at boot to create the tables it owns.",
226
+ async () => {
227
+ await ensureSessionStateSchema({ url: databaseUrl, logger: console });
228
+ // And the durable-run journal's, which is a separate set of tables owned
229
+ // by the same deployment. Without it a project with a `DATABASE_URL`
230
+ // boots claiming durable runs and fails on the first one.
231
+ await ensureWorkflowJournalSchema({ url: databaseUrl, logger: console });
232
+ },
233
+ );
234
+ } else {
235
+ /**
236
+ * Say that this process is the only place the state lives, because the next
237
+ * thing an operator does with a container is run two of them.
238
+ *
239
+ * Session state (slots, the event log) is keyed by session id and held in
240
+ * memory here — the boot line below reports it as `sessionState: { backend:
241
+ * 'memory', durable: false }`, which is true and easy to read as being about
242
+ * restarts alone. It is also about REPLICAS: the browser reconnects with
243
+ * `?sessionId=<id>`, so a reconnect that lands on a different process resumes
244
+ * a session that process has never heard of and the agent's context is gone
245
+ * mid-call. One replica has no such problem, which is exactly why nothing
246
+ * catches this until the deployment grows a second one.
247
+ */
248
+ console.warn(
249
+ "No DATABASE_URL: session state and durable runs live in THIS process's memory.\n" +
250
+ "One replica is fine. Behind a load balancer, enable sticky sessions so a reconnect " +
251
+ "(the client re-dials with ?sessionId=) reaches the same process — or set DATABASE_URL " +
252
+ "and let every replica share the state.",
253
+ );
161
254
  }
162
255
 
163
- const server = createAgentServer({
164
- agent,
165
- env,
166
- // Provider credentials may ALSO arrive straight from the environment without
167
- // being declared, and without becoming ctx.env the ordinary way to hand
168
- // ASSEMBLYAI_API_KEY to a container. Anything in `env` still wins.
169
- providerEnv: withHostCredentialFallback(env),
170
- clientDir: resolveClientDir(),
171
- ...(publicUrl ? { publicUrl } : {}),
172
- // Durable workflows need nothing passed here. A `DATABASE_URL` in `env` puts
173
- // the runs in Postgres and they survive a restart; without one they live in a
174
- // per-process directory and do not, which is the same trade `aai dev` makes.
175
- //
176
- // Two options used to sit here — the compiled workflow surface, carried on the
177
- // bundle as `__aaiWorkflowCode`/`__aaiStepCode` because a `"use workflow"` body
178
- // had to go through a compiler at BUILD time. The engine reads the agent's own
179
- // `workflows` declaration instead, so there is no artifact to hand over.
180
- });
256
+ const server = await bootOrExit(
257
+ // The commonest first-run failure, and the one whose stack this replaces: a
258
+ // provider credential that is not there. `.env` is what `aai dev` reads too,
259
+ // so the fix is the same one in both places.
260
+ "Set the missing value in .env, or pass it as a real environment variable (`docker run -e NAME=value`), then start again.",
261
+ () =>
262
+ createAgentServer({
263
+ agent,
264
+ env,
265
+ // Provider credentials may ALSO arrive straight from the environment without
266
+ // being declared, and without becoming ctx.env the ordinary way to hand
267
+ // ASSEMBLYAI_API_KEY to a container. Anything in `env` still wins.
268
+ providerEnv: withHostCredentialFallback(env),
269
+ clientDir: resolveClientDir(),
270
+ ...(publicUrl ? { publicUrl } : {}),
271
+ // Durable workflows need nothing passed here. A `DATABASE_URL` in `env` puts
272
+ // the runs in Postgres and they survive a restart; without one they live in a
273
+ // per-process directory and do not, which is the same trade `aai dev` makes.
274
+ //
275
+ // Two options used to sit here — the compiled workflow surface, carried on the
276
+ // bundle as `__aaiWorkflowCode`/`__aaiStepCode` because a `"use workflow"` body
277
+ // had to go through a compiler at BUILD time. The engine reads the agent's own
278
+ // `workflows` declaration instead, so there is no artifact to hand over.
279
+ }),
280
+ );
181
281
 
182
282
  // Loopback by default: this server has no request authentication of its own,
183
283
  // so exposing it is a deliberate act. Set HOST=0.0.0.0 to bind every interface
184
284
  // behind your own proxy or auth. An empty HOST means unset, not "everything".
185
285
  const host = process.env.HOST?.trim() || undefined;
186
- await server.listen(Number(process.env.PORT ?? 3000), host);
286
+ const port = Number(process.env.PORT ?? 3000);
287
+ await bootOrExit(
288
+ `Nothing is listening yet — port ${port} is in use, or this process may not bind it. Set PORT to a free one.`,
289
+ () => server.listen(port, host),
290
+ );
187
291
  console.log(`${agent.name} listening on http://${host ?? "127.0.0.1"}:${server.port}`);
188
292
 
189
293
  for (const signal of ["SIGINT", "SIGTERM"]) {