@alexkroman1/aai-cli 8.2.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/_dev-env.d.ts +15 -2
  2. package/dist/{_dev-server-Dt3TCjhj.mjs → _dev-server-BAR8Ya43.mjs} +182 -55
  3. package/dist/_dev-server.d.ts +5 -30
  4. package/dist/_dev-typecheck.d.ts +38 -0
  5. package/dist/_dev-vite-config.d.ts +74 -0
  6. package/dist/_e2e-test-utils.d.ts +33 -1
  7. package/dist/_init-BNotRgV1.mjs +163 -0
  8. package/dist/_resource-commands.d.ts +0 -1
  9. package/dist/{_slug-api-z-USxPtL.mjs → _slug-api--ZBmIGH-.mjs} +1 -1
  10. package/dist/{_studio-DR5yzZ1T.mjs → _studio-CKrsixd0.mjs} +2 -1
  11. package/dist/_studio.d.ts +3 -1
  12. package/dist/{build-BBxf8vom.mjs → build-DvDdSwAS.mjs} +1 -1
  13. package/dist/cli.mjs +26 -97
  14. package/dist/{delete-Dl_LSfvI.mjs → delete-8pjH8n8n.mjs} +3 -2
  15. package/dist/{deploy-DRC8jWr8.mjs → deploy-CjU3e-3P.mjs} +4 -4
  16. package/dist/{dev-QuuOOUtv.mjs → dev-dFzdvwa7.mjs} +3 -2
  17. package/dist/dev.d.ts +2 -0
  18. package/dist/{eval-DfQ8cm5p.mjs → eval-DwNwdqmS.mjs} +1 -1
  19. package/dist/{init-CUo87gcI.mjs → init-DK7GpIF9.mjs} +7 -6
  20. package/dist/{logs-CQ2T46dr.mjs → logs-D2Yhqe2w.mjs} +2 -2
  21. package/dist/scaffold/.env.example +7 -4
  22. package/dist/scaffold/CLAUDE.md +152 -97
  23. package/dist/scaffold/global.d.ts +12 -0
  24. package/dist/scaffold/package.json +4 -4
  25. package/dist/scaffold/server.mjs +49 -2
  26. package/dist/scaffold/tsconfig.json +1 -0
  27. package/dist/scaffold/vitest.config.ts +24 -0
  28. package/dist/{secret-Ct34kdgU.mjs → secret-CwgDLpSy.mjs} +2 -2
  29. package/dist/{studio-CzTzPqe1.mjs → studio-D3-HWIfk.mjs} +7 -5
  30. package/dist/templates/briefing-desk/agent.test.ts +3 -15
  31. package/dist/templates/briefing-desk/shared.ts +3 -3
  32. package/dist/templates/call-audit/agent.eval.test.ts +8 -1
  33. package/dist/templates/call-audit/agent.test.ts +0 -1
  34. package/dist/templates/call-audit/agent.ts +4 -3
  35. package/dist/templates/call-audit/client.tsx +4 -7
  36. package/dist/templates/call-audit/workflows/audit.ts +10 -1
  37. package/dist/templates/code-interpreter/agent.eval.test.ts +14 -43
  38. package/dist/templates/dispatch-center/agent.eval.test.ts +16 -26
  39. package/dist/templates/embedded-assets/agent.eval.test.ts +6 -16
  40. package/dist/templates/embedded-assets/agent.test.ts +3 -16
  41. package/dist/templates/health-assistant/agent.eval.test.ts +5 -15
  42. package/dist/templates/health-assistant/agent.test.ts +3 -14
  43. package/dist/templates/infocom-adventure/agent.eval.test.ts +6 -16
  44. package/dist/templates/infocom-adventure/agent.test.ts +3 -17
  45. package/dist/templates/link-digest/agent.test.ts +0 -14
  46. package/dist/templates/link-digest/agent.ts +9 -5
  47. package/dist/templates/link-digest/client.tsx +8 -6
  48. package/dist/templates/math-buddy/agent.eval.test.ts +11 -40
  49. package/dist/templates/night-owl/agent.eval.test.ts +15 -44
  50. package/dist/templates/night-owl/agent.test.ts +2 -15
  51. package/dist/templates/personal-finance/agent.eval.test.ts +10 -39
  52. package/dist/templates/pipeline-simple/agent.test.ts +73 -0
  53. package/dist/templates/pizza-ordering/agent.eval.test.ts +2 -17
  54. package/dist/templates/pizza-ordering/agent.test.ts +2 -17
  55. package/dist/templates/plan-and-execute/agent.eval.test.ts +2 -12
  56. package/dist/templates/plan-and-execute/agent.test.ts +3 -23
  57. package/dist/templates/plan-and-execute/shared.ts +1 -1
  58. package/dist/templates/podcast-digest/agent.test.ts +0 -1
  59. package/dist/templates/podcast-digest/client.tsx +3 -6
  60. package/dist/templates/podcast-digest/workflows/digest.ts +16 -8
  61. package/dist/templates/recap-workflow/agent.eval.test.ts +7 -14
  62. package/dist/templates/recap-workflow/agent.test.ts +3 -18
  63. package/dist/templates/recap-workflow/agent.ts +7 -5
  64. package/dist/templates/redline/agent.test.ts +0 -13
  65. package/dist/templates/redline/agent.ts +3 -2
  66. package/dist/templates/redline/client.tsx +9 -4
  67. package/dist/templates/research-workflow/agent.eval.test.ts +7 -14
  68. package/dist/templates/research-workflow/agent.test.ts +3 -15
  69. package/dist/templates/research-workflow/agent.ts +7 -5
  70. package/dist/templates/research-workflow/workflows/research.ts +8 -2
  71. package/dist/templates/retail/agent.eval.test.ts +10 -25
  72. package/dist/templates/retail/registry.test.ts +16 -13
  73. package/dist/templates/solo-rpg/agent.eval.test.ts +5 -15
  74. package/dist/templates/solo-rpg/agent.test.ts +17 -124
  75. package/dist/templates/solo-rpg/shared.ts +13 -61
  76. package/dist/templates/spoken-summary/agent.test.ts +0 -1
  77. package/dist/templates/spoken-summary/agent.ts +5 -4
  78. package/dist/templates/spoken-summary/client.tsx +4 -7
  79. package/dist/templates/support-line/agent.eval.test.ts +2 -12
  80. package/dist/templates/support-line/agent.test.ts +3 -22
  81. package/dist/templates/transcription-workflow/agent.ts +4 -4
  82. package/dist/templates/transcription-workflow/client.tsx +5 -5
  83. package/dist/templates/transcription-workflow/workflows/transcribe.ts +10 -1
  84. package/dist/templates/travel-concierge/agent.eval.test.ts +5 -15
  85. package/dist/templates/travel-concierge/agent.test.ts +3 -23
  86. package/dist/templates/web-researcher/agent.eval.test.ts +2 -5
  87. package/dist/test-CvwgeVSQ.mjs +181 -0
  88. package/dist/test.d.ts +33 -2
  89. package/package.json +4 -4
  90. package/dist/_init-BhEfQ6Yi.mjs +0 -93
  91. package/dist/storage-i_79ZwAR.mjs +0 -66
  92. package/dist/storage.d.ts +0 -22
  93. package/dist/templates/solo-rpg/tools/load_game.ts +0 -37
  94. package/dist/templates/solo-rpg/tools/save_game.ts +0 -32
  95. package/dist/test-BvyT6sAc.mjs +0 -104
@@ -1,5 +1,4 @@
1
- import { type DeepReadonly, dialog, sessionSlot, type ToolContext } from "@alexkroman1/aai";
2
- import { z } from "zod";
1
+ import { type DeepReadonly, dialog, sessionSlot } from "@alexkroman1/aai";
3
2
 
4
3
  // ── Tuning Constants ─────────────────────────────────────────────────────────
5
4
  export const MAX_SESSION_LOG = 50;
@@ -390,7 +389,7 @@ export const gameProjection = gameSlot.projection((game) => game);
390
389
  * `storyFlow.reset` — and it is what lets `gameOver` be a genuinely `final`
391
390
  * state, which is the whole point of having one: `position().done` means the
392
391
  * story ended, and XState delivers no events to a done actor, so an `on: {
393
- * SETUP }` there would have been dead config that looked live. `load_game`
392
+ * SETUP }` there would have been dead config that looked live. A resumed run
394
393
  * resets for the same reason. `SETUP` therefore appears once, on the only state
395
394
  * that can be transitioned out of.
396
395
  */
@@ -450,65 +449,18 @@ export const storyFlow = dialog("story", storySpec);
450
449
  */
451
450
  export type FrozenGameState = DeepReadonly<GameState>;
452
451
 
453
- // ── Persistent save slots (ctx.db) ───────────────────────────────────────────
454
- // save_game / load_game are genuine cross-session persistence, so they use
455
- // the app's SQL database. Requires storage: `aai storage enable` (or
456
- // Settings Database in the studio); under `aai dev`, set DATABASE_URL in .env.
452
+ // ── No cross-session saves, and why ─────────────────────────────────────────
453
+ // `save_game` / `load_game` stood here, keyed by slot name and backed by an
454
+ // `app_state` table through `ctx.db`. Both are gone with `ctx.db` itself: the
455
+ // platform provides tool code no database, and a template cannot reach one — the
456
+ // scaffold ships no Postgres client, and shipped template code cannot import
457
+ // `@alexkroman1/aai-runtime` (templates type-check under the scaffold tsconfig,
458
+ // which that package's source is not clean under).
457
459
  //
458
- // Slots are keyed by name alone the whole point of a save is loading it in
459
- // a LATER session, whose sessionId differs, so the key can't embed one. The
460
- // storage is per app, so every player of one deployment shares the slot
461
- // namespace; without player identity that is the price of resumability.
462
- export function saveSlotKey(slot?: string): string {
463
- return `save:${slot ?? "autosave"}`;
464
- }
465
-
466
- /** The slot-name grammar, shared by save_game and load_game so a name that
467
- * can be saved can always be loaded. */
468
- export const saveSlotParam = z
469
- .string()
470
- .regex(/^[A-Za-z0-9_-]{1,32}$/, "letters, digits, dashes, underscores; max 32 chars")
471
- .describe("Save slot name, defaults to autosave")
472
- .optional();
473
-
474
- const ENSURE_APP_STATE = `create table if not exists app_state (
475
- key text primary key,
476
- value jsonb not null,
477
- updated_at timestamptz not null default now()
478
- )`;
479
-
480
- // Memoized per process (each session's tools run in a fresh sandbox, so this
481
- // is at most one round-trip per session); a failure clears the memo so the
482
- // next call retries instead of caching the error forever.
483
- let ensureP: Promise<unknown> | null = null;
484
- function ensureTable(ctx: ToolContext): Promise<unknown> {
485
- ensureP ??= ctx.db.query(ENSURE_APP_STATE).catch((err) => {
486
- ensureP = null;
487
- throw err;
488
- });
489
- return ensureP;
490
- }
491
-
492
- /** Read one saved value. jsonb columns come back from the postgres driver
493
- * already parsed, so the value needs no JSON.parse here. */
494
- export async function loadState<T>(ctx: ToolContext, key: string): Promise<T | null> {
495
- await ensureTable(ctx);
496
- const rows = await ctx.db.query<{ value: T }>("select value from app_state where key = $1", [
497
- key,
498
- ]);
499
- return rows[0]?.value ?? null;
500
- }
501
-
502
- /** Upsert one value. Serialized explicitly and cast with `::jsonb` so the
503
- * write is driver-agnostic about object parameters. */
504
- export async function saveState(ctx: ToolContext, key: string, value: unknown): Promise<void> {
505
- await ensureTable(ctx);
506
- await ctx.db.query(
507
- "insert into app_state (key, value, updated_at) values ($1, $2::jsonb, now()) " +
508
- "on conflict (key) do update set value = excluded.value, updated_at = now()",
509
- [key, JSON.stringify(value)],
510
- );
511
- }
460
+ // So this adventure is SINGLE-SESSION: everything lives in `sessionSlot`s and
461
+ // ends when the call does. An author who wants saves adds a client of their own
462
+ // (`postgres`, `pg`, a provider SDK) and a `DATABASE_URL` secret which is the
463
+ // supported pattern, just not one a shipped template can demonstrate.
512
464
 
513
465
  // ── Helpers ──────────────────────────────────────────────────────────────────
514
466
  export function d(sides: number): number {
@@ -54,7 +54,6 @@ describe("the declaration", () => {
54
54
  // The page calls `api.start("spokenSummary", …)`, so a rename here is a
55
55
  // runtime 400 rather than a compile error. This is what pins it.
56
56
  expect(Object.keys(agentDef.workflows ?? {})).toEqual(["spokenSummary"]);
57
- expect(agentDef.page).toBe("static");
58
57
  });
59
58
 
60
59
  test("declares no providers and exactly the one credential its steps read", () => {
@@ -41,10 +41,11 @@
41
41
  * three services this uses: transcription, the LLM Gateway, and the voice.
42
42
  * `requiredEnv` below is what makes a deploy check for it rather than letting
43
43
  * the first run find out.
44
- * - **Storage** (`aai storage enable`, Settings Database in the studio, or
45
- * `DATABASE_URL` under `aai dev`). REQUIRED here, and more so than for most
46
- * workflow apps: an upload's record is a row, and this app uses uploads at
47
- * BOTH ends — the recording coming in and the summary going out.
44
+ * - **A `DATABASE_URL`** a secret when deployed, `.env` under `aai dev`, and a
45
+ * Postgres you bring since the platform provisions none. REQUIRED here, and
46
+ * more so than for most workflow apps: an upload's record is a row, and this
47
+ * app uses uploads at BOTH ends — the recording coming in and the summary
48
+ * going out.
48
49
  *
49
50
  * ## The recording is UPLOADED, and the run carries its id
50
51
  *
@@ -37,7 +37,6 @@ import "@alexkroman1/aai-ui/styles.css";
37
37
  // nothing — and it is what stops this file restating a shape
38
38
  // `workflows/summarize.ts` already declares.
39
39
  import { formatDuration } from "@alexkroman1/aai/utils";
40
- import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
41
40
  import {
42
41
  createWorkflowApi,
43
42
  Form,
@@ -51,9 +50,6 @@ import {
51
50
  } from "@alexkroman1/aai-ui";
52
51
  import type { spokenSummary } from "./agent.ts";
53
52
 
54
- /** What a completed run reports, derived from the workflow rather than restated. */
55
- type Summary = WorkflowOutputOf<typeof spokenSummary>;
56
-
57
53
  /**
58
54
  * The workflow's name, as a page starts a run by one.
59
55
  *
@@ -88,8 +84,9 @@ function captionsUrl(text: string, durationMs: number): string {
88
84
  export function App() {
89
85
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
90
86
  // `run.output` instead of `unknown`.
91
- const { submit, run, pending, upload, pauseUpload, resumeUpload, error } =
92
- useWorkflowSubmit<Summary>(WORKFLOW, { api });
87
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
88
+ typeof spokenSummary
89
+ >(WORKFLOW, { api });
93
90
  const output = run?.status === "completed" ? run.output : undefined;
94
91
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
95
92
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -105,7 +102,7 @@ export function App() {
105
102
  </p>
106
103
  </header>
107
104
 
108
- <Form onSubmit={submit} error={error} className="flex flex-col gap-4">
105
+ <Form onSubmit={submitForm} error={error} className="flex flex-col gap-4">
109
106
  {/* Every control, from the workflow's own input schema. See the module doc. */}
110
107
  <WorkflowFields workflow={WORKFLOW} />
111
108
  <SubmitButton pending={pending} pendingLabel="Working…">
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the line really refuse to say what it cannot ground? Run it
4
2
  // with `aai eval`.
5
3
  //
@@ -27,20 +25,12 @@
27
25
  // product questions from its own knowledge and skips the tools the prompt exists
28
26
  // to route it through, so a case run that way measures nothing it claims to.
29
27
 
30
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
31
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
28
+ /** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
29
+ import agentDef from "virtual:aai/agent";
32
30
  import { type EvalSession, toolResultIn } from "@alexkroman1/aai-runtime/eval";
33
31
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
34
32
  import { expect } from "vitest";
35
33
  import { z } from "zod";
36
- import authoredAgent from "./agent.ts";
37
- import systemPrompt from "./system-prompt.md?raw";
38
-
39
- /** The def a DEPLOYED agent runs — see `agent.test.ts` on why the glob is here. */
40
- const agentDef = withSystemPrompt(
41
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
42
- systemPrompt,
43
- );
44
34
 
45
35
  /**
46
36
  * What the BROWSER is sent, as this eval reads it.
@@ -1,26 +1,7 @@
1
- /// <reference types="vite/client" />
2
-
3
- import {
4
- createToolContext,
5
- stubGenerate,
6
- toolRunner,
7
- withDiscoveredTools,
8
- } from "@alexkroman1/aai/testing";
1
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { createToolContext, stubGenerate, toolRunner } from "@alexkroman1/aai/testing";
9
4
  import { describe, expect, test } from "vitest";
10
- import authoredAgent from "./agent.ts";
11
-
12
- /**
13
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
14
- *
15
- * The glob is written HERE rather than reached for from a shared helper because
16
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
17
- * anything outside its own template, and `import.meta.glob` is expanded against
18
- * the file containing it either way. This is the pattern a user writes.
19
- */
20
- const agentDef = withDiscoveredTools(
21
- authoredAgent,
22
- import.meta.glob("./tools/*.ts", { eager: true }),
23
- );
24
5
 
25
6
  import { MAX_ATTEMPTS, runCorrectiveRag } from "./procedure.ts";
26
7
  import {
@@ -17,10 +17,10 @@
17
17
  * `aai secret put ASSEMBLYAI_API_KEY` once deployed. `requiredEnv` below is
18
18
  * what makes a deploy check for it rather than letting the first run find out.
19
19
  * A step reads it with `requireStepEnv`; see `@alexkroman1/aai/step`.
20
- * - **Storage** (`aai storage enable`, Settings Database in the studio, or
21
- * `DATABASE_URL` under `aai dev`). REQUIRED here, unlike most workflow apps:
22
- * a run survives without it, but an UPLOAD's record is a row, so the form
23
- * below refuses by name until storage is on.
20
+ * - **A `DATABASE_URL`** a secret when deployed, `.env` under `aai dev` — and
21
+ * the platform provisions none, so it is a Postgres you bring. REQUIRED here,
22
+ * unlike most workflow apps: runs are durable on the platform either way, but
23
+ * an UPLOAD's record is a row, so the form below refuses by name without one.
24
24
  * - **ffmpeg, under `aai dev` only.** A deployed guest's image installs it; on a
25
25
  * laptop it is whatever is on `PATH` (or `AAI_FFMPEG_PATH`). The `transcribe`
26
26
  * flow needs it for anything that is not already a linear-PCM WAV, because the
@@ -359,14 +359,14 @@ function TranscriptionDesk() {
359
359
  // ALL THREE hooks are called every render, because a hook may not be conditional —
360
360
  // and that costs nothing here: none of them does anything until its `submit` is
361
361
  // called, and `useWorkflowRun` underneath them holds no id until then either.
362
- const streamed = useWorkflowStream<Transcript>(WORKFLOWS.streaming, { parallel });
363
- const stored = useWorkflowSubmit<Transcript>(WORKFLOWS.classic, { parallel });
364
- const batched = useWorkflowSubmit<Transcript>(WORKFLOWS.batch, { parallel });
362
+ const streamed = useWorkflowStream<typeof transcribe>(WORKFLOWS.streaming, { parallel });
363
+ const stored = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.classic, { parallel });
364
+ const batched = useWorkflowSubmit<typeof transcribe>(WORKFLOWS.batch, { parallel });
365
365
  // The batch flow uploads the same way the classic one does — the id comes from the
366
366
  // store — so it is the SAME hook against a different workflow. Only the streaming
367
367
  // mode needs the other one, because only it needs the id before the bytes.
368
368
  const active = mode === "streaming" ? streamed : mode === "batch" ? batched : stored;
369
- const { submit, run, upload, pending, error, reset, pauseUpload, resumeUpload } = active;
369
+ const { submitForm, run, upload, pending, error, reset, pauseUpload, resumeUpload } = active;
370
370
  // History is per WORKFLOW, so the list follows the mode: two flows that produce
371
371
  // the same output are still two different things to have run, and merging them
372
372
  // would put a run under a heading that cannot explain it.
@@ -422,7 +422,7 @@ function TranscriptionDesk() {
422
422
  <Form
423
423
  onSubmit={(values) => {
424
424
  total.start();
425
- return submit(values);
425
+ return submitForm(values);
426
426
  }}
427
427
  error={error}
428
428
  >
@@ -167,7 +167,16 @@ export const BYTES_IN_FLIGHT = 640 * 1024 * 1024;
167
167
  *
168
168
  * The table above was measured under the old per-round barrier. Re-measuring it is
169
169
  * worth doing before this number moves again: the window makes a wide fan-out
170
- * cheaper at the tail, which if anything argues for a HIGHER knee.
170
+ * cheaper at the tail, which if anything argues for a HIGHER knee. *
171
+ * **What EXECUTES at this width is the world's call, not this number's.**
172
+ * `mapConcurrent` bounds how many step calls the body has in flight; how many
173
+ * run at once is the workflow world's worker concurrency, which on the
174
+ * `DATABASE_URL` path defaults to three — so on a default deployment a width
175
+ * above three is inert while still costing a queued job per item. That makes
176
+ * this the FAR SIDE's knee and the width to use once an operator has raised
177
+ * the ceiling, not a promise about a stock deployment. See "The WINDOW is not
178
+ * the concurrency" in `@alexkroman1/aai/step`'s `mapConcurrent`; the numbers
179
+ * above were measured against the endpoint and say nothing about that layer.
171
180
  */
172
181
  export const MAX_SEGMENT_CONCURRENCY = 32;
173
182
 
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the confirmation gate actually gate? Run it with `aai eval`.
4
2
  //
5
3
  // `agent.test.ts` drives each tool directly and asserts about the state it
@@ -22,15 +20,6 @@
22
20
  // product questions from its own knowledge and skips the tools the prompt exists
23
21
  // to route it through, so a case run that way measures nothing it claims to.
24
22
 
25
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
26
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
27
- import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
28
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
29
- import { expect } from "vitest";
30
- import { z } from "zod";
31
- import authoredAgent from "./agent.ts";
32
- import systemPrompt from "./system-prompt.md?raw";
33
-
34
23
  /**
35
24
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
36
25
  *
@@ -39,10 +28,11 @@ import systemPrompt from "./system-prompt.md?raw";
39
28
  * outside its own template. An eval that forgot it would run an agent with NO
40
29
  * tools and read as a model that refuses to act.
41
30
  */
42
- const agentDef = withSystemPrompt(
43
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
44
- systemPrompt,
45
- );
31
+ import agentDef from "virtual:aai/agent";
32
+ import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
33
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
34
+ import { expect } from "vitest";
35
+ import { z } from "zod";
46
36
 
47
37
  /**
48
38
  * What the BROWSER is sent, as this eval reads it.
@@ -1,28 +1,8 @@
1
- /// <reference types="vite/client" />
2
-
1
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
+ import agentDef from "virtual:aai/agent";
3
3
  import type { ToolContext } from "@alexkroman1/aai";
4
- import {
5
- createToolContext,
6
- ok,
7
- okPosition,
8
- toolRunner,
9
- withDiscoveredTools,
10
- } from "@alexkroman1/aai/testing";
4
+ import { createToolContext, ok, okPosition, toolRunner } from "@alexkroman1/aai/testing";
11
5
  import { describe, expect, test } from "vitest";
12
- import authoredAgent from "./agent.ts";
13
-
14
- /**
15
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
16
- *
17
- * The glob is written HERE rather than reached for from a shared helper because
18
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
19
- * anything outside its own template, and `import.meta.glob` is expanded against
20
- * the file containing it either way. This is the pattern a user writes.
21
- */
22
- const agentDef = withDiscoveredTools(
23
- authoredAgent,
24
- import.meta.glob("./tools/*.ts", { eager: true }),
25
- );
26
6
 
27
7
  import {
28
8
  activeAssistant,
@@ -12,13 +12,10 @@
12
12
  // `agent.ts` alone would measure Scout with none of its own rules — and its
13
13
  // rules are the entire subject of this file. The reasoning is spelled out in
14
14
  // `../code-interpreter/agent.eval.test.ts`.
15
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
15
+
16
+ import agentDef from "virtual:aai/agent";
16
17
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
17
18
  import { expect } from "vitest";
18
- import authored from "./agent.ts";
19
- import systemPrompt from "./system-prompt.md?raw";
20
-
21
- const agentDef = withSystemPrompt(authored, systemPrompt);
22
19
 
23
20
  /**
24
21
  * The registrable label of every host this turn's tool results mentioned —
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ import { a as ok, n as fail } from "./_output-CKkmWs7i.mjs";
3
+ import { n as log, r as notify } from "./_ui-u7T4YooX.mjs";
4
+ import { i as errorCode, o as errorMessage$1, r as binFromPackageJson } from "./_utils-B8QmtFhK.mjs";
5
+ import { createRequire } from "node:module";
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { omitUndefined } from "@alexkroman1/aai/utils";
9
+ import { execaSync } from "execa";
10
+ //#region test.ts
11
+ /**
12
+ * `aai test` — run agent tests via vitest.
13
+ */
14
+ /**
15
+ * Resolve the agent project's own vitest binary so tests run without the
16
+ * npx resolution overhead (and its potential network fetch of vitest).
17
+ *
18
+ * Resolves `vitest/package.json` from the agent directory, derives the bin
19
+ * script, and runs it with the current Node executable. Falls back to
20
+ * `npx vitest` only when no local install is resolvable.
21
+ */
22
+ function resolveVitestCommand(cwd, resolve = createRequire(path.join(cwd, "package.json")).resolve) {
23
+ try {
24
+ const bin = binFromPackageJson(resolve("vitest/package.json"), "vitest");
25
+ if (bin) return {
26
+ cmd: process.execPath,
27
+ args: [bin]
28
+ };
29
+ } catch {}
30
+ return {
31
+ cmd: "npx",
32
+ args: ["vitest"]
33
+ };
34
+ }
35
+ /** The files `aai test` runs, in preference order. */
36
+ const TEST_FILES = ["agent.test.ts", "agent.test.js"];
37
+ /**
38
+ * Run vitest over one of `candidates` in the given project directory.
39
+ *
40
+ * Returns the FILE it ran, or `false` if none of the candidate files exists.
41
+ * Throws on failure.
42
+ *
43
+ * A vitest FILTER, not an include glob: the argument is matched as a substring
44
+ * against the paths vitest's own include globs already found, which is why the
45
+ * candidates are named `*.test.ts` — `agent.test.ts` cannot match
46
+ * `agent.eval.test.ts` and vice versa, so the two commands stay disjoint
47
+ * without either one having to exclude the other's file.
48
+ */
49
+ function runVitest(cwd, opts = { candidates: TEST_FILES }) {
50
+ const testFile = opts.candidates.find((name) => existsSync(path.join(cwd, name)));
51
+ if (!testFile) return false;
52
+ const { cmd, args } = resolveVitestCommand(cwd);
53
+ execaSync(cmd, [
54
+ ...args,
55
+ "run",
56
+ "--root",
57
+ ".",
58
+ ...opts.extraArgs ?? [],
59
+ testFile
60
+ ], {
61
+ cwd,
62
+ stdio: "inherit",
63
+ ...omitUndefined({ env: opts.env ? {
64
+ ...process.env,
65
+ ...opts.env
66
+ } : void 0 })
67
+ });
68
+ return testFile;
69
+ }
70
+ /**
71
+ * Classify a {@link runVitest} failure. execaSync throws an ENOENT-coded
72
+ * error when the binary itself couldn't be spawned (infrastructure problem)
73
+ * and an exit-code error when vitest ran and the tests failed.
74
+ */
75
+ function classifyVitestError(err, label = "Tests") {
76
+ if (errorCode(err) === "ENOENT") return {
77
+ code: "spawn_failed",
78
+ message: `Could not launch the test runner: ${errorMessage$1(err)} — is the binary on your PATH?`
79
+ };
80
+ return {
81
+ code: "test_failed",
82
+ message: `${label} failed: ${errorMessage$1(err)}`
83
+ };
84
+ }
85
+ /** Directories a project's own specs never live in. */
86
+ const UNSCANNED_DIRS = /* @__PURE__ */ new Set([
87
+ "node_modules",
88
+ ".aai",
89
+ ".git",
90
+ "dist",
91
+ ".workflow-data"
92
+ ]);
93
+ /** What counts as a spec file. */
94
+ const SPEC_FILE_RE = /\.test\.(ts|js|tsx|mts|cts)$/;
95
+ /**
96
+ * Spec files in the project that `aai test` did NOT run.
97
+ *
98
+ * `runVitest` passes ONE filename as a vitest FILTER, which is what keeps `test`
99
+ * and `eval` disjoint without either excluding the other's file — see its doc.
100
+ * The cost is that every other `*.test.ts` in the project is skipped, and the
101
+ * skip was SILENT: the shipped `retail` template carries seven of them, so
102
+ * `aai test` there ran 1 file / 67 tests, printed "Tests passed", and left
103
+ * 211 of the project's 278 tests unrun with nothing saying so.
104
+ *
105
+ * A silent skip is the worst outcome available, so the skip is announced rather
106
+ * than the filter widened: which files `aai test` runs is a documented contract
107
+ * (the scaffold guide says "Run agent.test.ts via vitest"), and running a
108
+ * project's other specs by default could reach ones that are slow or want
109
+ * credentials. Naming them costs nothing and is what a reader needs.
110
+ *
111
+ * Eval files are excluded because they have their OWN command, named in the
112
+ * message.
113
+ */
114
+ function unrunSpecFiles(cwd, ran) {
115
+ const found = [];
116
+ collectSpecs(cwd, "", ran, found);
117
+ return found.sort(compareCodeUnits);
118
+ }
119
+ /** Code-unit comparison — see {@link unrunSpecFiles} for why not `localeCompare`. */
120
+ function compareCodeUnits(a, b) {
121
+ if (a < b) return -1;
122
+ return a > b ? 1 : 0;
123
+ }
124
+ /** One directory of {@link unrunSpecFiles}, recursing into the ones that count. */
125
+ function collectSpecs(dir, prefix, ran, out) {
126
+ let entries;
127
+ try {
128
+ entries = readdirSync(dir, { withFileTypes: true });
129
+ } catch {
130
+ return;
131
+ }
132
+ for (const e of entries) {
133
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
134
+ if (e.isDirectory()) {
135
+ if (!(UNSCANNED_DIRS.has(e.name) || e.name.startsWith("."))) collectSpecs(path.join(dir, e.name), rel, ran, out);
136
+ } else if (isUnrunSpec(e.name, rel, ran)) out.push(rel);
137
+ }
138
+ }
139
+ /** A spec file this run did not cover. The `.eval.` INFIX is the tier convention. */
140
+ function isUnrunSpec(name, rel, ran) {
141
+ if (!SPEC_FILE_RE.test(name)) return false;
142
+ return rel !== ran && !name.includes(".eval.test.");
143
+ }
144
+ /**
145
+ * Warn, once, naming the spec files this run did not cover.
146
+ *
147
+ * `ran` is `false` when there was no `agent.test.ts` to run, and that case
148
+ * needs the warning MORE rather than less: `aai test` then prints "No test file
149
+ * found" while the project's spec files sit right there unrun, which reads as
150
+ * "this project has no tests". Measured on a project whose only spec was
151
+ * `tools/echo_back.test.ts` — `{"passed":true,"skipped":true}` and not a word
152
+ * about it. It stays silent when there is nothing to name, in both arms.
153
+ */
154
+ function warnUnrunSpecs(cwd, ran) {
155
+ const skipped = unrunSpecFiles(cwd, ran === false ? "" : ran);
156
+ if (skipped.length === 0) return;
157
+ notify("warn", `${ran === false ? `\`aai test\` found no agent.test.ts, so it ran nothing. ${skipped.length} spec file(s) exist and were NOT run:` : `\`aai test\` ran ${ran} only. ${skipped.length} other spec file(s) were NOT run:`} ${skipped.join(", ")}. Run them with your own vitest (\`npx vitest run\`); behaviour evals have their own command (\`aai eval\`).`);
158
+ }
159
+ /** Execute agent tests and return structured result. */
160
+ async function executeTest(cwd) {
161
+ log.step("Running agent tests");
162
+ try {
163
+ const ran = runVitest(cwd);
164
+ if (!ran) {
165
+ log.info("No test file found. Create agent.test.ts to add tests.");
166
+ warnUnrunSpecs(cwd, ran);
167
+ return ok({
168
+ passed: true,
169
+ skipped: true
170
+ });
171
+ }
172
+ log.success("Tests passed");
173
+ warnUnrunSpecs(cwd, ran);
174
+ return ok({ passed: true });
175
+ } catch (err) {
176
+ const { code, message } = classifyVitestError(err);
177
+ return fail(code, message);
178
+ }
179
+ }
180
+ //#endregion
181
+ export { classifyVitestError, executeTest, runVitest };
package/dist/test.d.ts CHANGED
@@ -37,7 +37,7 @@ export type VitestRunOptions = {
37
37
  /**
38
38
  * Run vitest over one of `candidates` in the given project directory.
39
39
  *
40
- * Returns `true` if it ran, `false` if none of the candidate files exists.
40
+ * Returns the FILE it ran, or `false` if none of the candidate files exists.
41
41
  * Throws on failure.
42
42
  *
43
43
  * A vitest FILTER, not an include glob: the argument is matched as a substring
@@ -46,7 +46,7 @@ export type VitestRunOptions = {
46
46
  * `agent.eval.test.ts` and vice versa, so the two commands stay disjoint
47
47
  * without either one having to exclude the other's file.
48
48
  */
49
- export declare function runVitest(cwd: string, opts?: VitestRunOptions): boolean;
49
+ export declare function runVitest(cwd: string, opts?: VitestRunOptions): string | false;
50
50
  /**
51
51
  * Classify a {@link runVitest} failure. execaSync throws an ENOENT-coded
52
52
  * error when the binary itself couldn't be spawned (infrastructure problem)
@@ -58,6 +58,37 @@ label?: string): {
58
58
  code: "spawn_failed" | "test_failed";
59
59
  message: string;
60
60
  };
61
+ /**
62
+ * Spec files in the project that `aai test` did NOT run.
63
+ *
64
+ * `runVitest` passes ONE filename as a vitest FILTER, which is what keeps `test`
65
+ * and `eval` disjoint without either excluding the other's file — see its doc.
66
+ * The cost is that every other `*.test.ts` in the project is skipped, and the
67
+ * skip was SILENT: the shipped `retail` template carries seven of them, so
68
+ * `aai test` there ran 1 file / 67 tests, printed "Tests passed", and left
69
+ * 211 of the project's 278 tests unrun with nothing saying so.
70
+ *
71
+ * A silent skip is the worst outcome available, so the skip is announced rather
72
+ * than the filter widened: which files `aai test` runs is a documented contract
73
+ * (the scaffold guide says "Run agent.test.ts via vitest"), and running a
74
+ * project's other specs by default could reach ones that are slow or want
75
+ * credentials. Naming them costs nothing and is what a reader needs.
76
+ *
77
+ * Eval files are excluded because they have their OWN command, named in the
78
+ * message.
79
+ */
80
+ export declare function unrunSpecFiles(cwd: string, ran: string): string[];
81
+ /**
82
+ * Warn, once, naming the spec files this run did not cover.
83
+ *
84
+ * `ran` is `false` when there was no `agent.test.ts` to run, and that case
85
+ * needs the warning MORE rather than less: `aai test` then prints "No test file
86
+ * found" while the project's spec files sit right there unrun, which reads as
87
+ * "this project has no tests". Measured on a project whose only spec was
88
+ * `tools/echo_back.test.ts` — `{"passed":true,"skipped":true}` and not a word
89
+ * about it. It stays silent when there is nothing to name, in both arms.
90
+ */
91
+ export declare function warnUnrunSpecs(cwd: string, ran: string | false): void;
61
92
  /** Execute agent tests and return structured result. */
62
93
  export declare function executeTest(cwd: string): Promise<CommandResult<TestData>>;
63
94
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "8.2.0",
3
+ "version": "9.0.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "aai": "bin.mjs"
@@ -44,9 +44,9 @@
44
44
  "p-timeout": "^7.0.1",
45
45
  "vite": "^8.2.1",
46
46
  "zod": "^4.4.3",
47
- "@alexkroman1/aai-ui": "8.2.0",
48
- "@alexkroman1/aai": "8.2.0",
49
- "@alexkroman1/aai-runtime": "8.2.0"
47
+ "@alexkroman1/aai": "9.0.0",
48
+ "@alexkroman1/aai-runtime": "9.0.0",
49
+ "@alexkroman1/aai-ui": "9.0.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "playwright": "^1.62.1",