@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
@@ -34,7 +34,6 @@ import {
34
34
  parseSchemaInput,
35
35
  schemaInputIssues,
36
36
  toolRunner,
37
- withDiscoveredTools,
38
37
  } from "@alexkroman1/aai/testing";
39
38
  import {
40
39
  installStubStepFetch,
@@ -44,7 +43,6 @@ import {
44
43
  import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
45
44
  import { beforeEach, describe, expect, test, vi } from "vitest";
46
45
  import { createHook, type Hook, sleep } from "workflow";
47
- import authoredAgent from "./agent.ts";
48
46
  import { recap } from "./shared.ts";
49
47
  import {
50
48
  askWhetherToKeep,
@@ -72,18 +70,8 @@ vi.mock("workflow", async (importActual) => ({
72
70
  createHook: vi.fn(),
73
71
  }));
74
72
 
75
- /**
76
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
77
- *
78
- * The glob is written HERE rather than reached for from a shared helper because
79
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
80
- * anything outside its own template, and `import.meta.glob` is expanded against
81
- * the file containing it either way. This is the pattern a user writes.
82
- */
83
- const agentDef = withDiscoveredTools(
84
- authoredAgent,
85
- import.meta.glob("./tools/*.ts", { eager: true }),
86
- );
73
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
74
+ import agentDef from "virtual:aai/agent";
87
75
 
88
76
  /**
89
77
  * Every tool here is driven through the agent's own table, by the name the model
@@ -151,10 +139,7 @@ describe("the agent declares its workflow", () => {
151
139
  expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
152
140
  });
153
141
 
154
- test("as a VOICE agent, not a workflow app", () => {
155
- // The distinction this template exists on: a caller is on the line, so the
156
- // run is a handoff. `link-digest` is the other shape.
157
- expect(agentDef.page).toBeUndefined();
142
+ test("discovers every tool in tools/, by file name", () => {
158
143
  // Discovered, not declared: every name here is a file in `tools/`.
159
144
  expect(Object.keys(agentDef.tools).sort()).toEqual([
160
145
  "cancel_recap",
@@ -65,11 +65,13 @@
65
65
  * the run really submits the recording, really polls it, and really deletes it
66
66
  * when it has to.
67
67
  *
68
- * Storage is what makes it DURABLE (`aai storage enable`, Settings Database
69
- * in the studio, or `DATABASE_URL` under `aai dev`) runs and the
70
- * correlation-key index both live there. Without it both live in the process:
71
- * the flow still runs, and a recap parked on a callback does not survive the
72
- * agent restarting, redeploying or going idle.
68
+ * Runs are DURABLE on the platform with nothing to configure, so a recap parked
69
+ * on a callback survives a restart, a redeploy and an idle sandbox.
70
+ *
71
+ * A `DATABASE_URL` you supply moves the correlation-key index out of memory,
72
+ * which is what lets `find()` resolve that parked run by key afterwards. Under
73
+ * `aai dev` with none, both the runs and the index go with the process: the flow
74
+ * still runs, it just does not outlive the server.
73
75
  */
74
76
 
75
77
  import { agent } from "@alexkroman1/aai";
@@ -42,19 +42,6 @@ const INPUT: RedlineInput = {
42
42
  // ─── 1. The declaration ──────────────────────────────────────────────────────
43
43
 
44
44
  describe("the agent declares itself a workflow app", () => {
45
- test("its front door is a page, not a microphone", () => {
46
- // The port's own decision: seven long-form model calls in sequence is not
47
- // something anyone holds a phone for.
48
- expect(agentDef.page).toBe("static");
49
- });
50
-
51
- test("it declares no voice pipeline and no tools, because nothing talks", () => {
52
- expect(agentDef.stt).toBeUndefined();
53
- expect(agentDef.llm).toBeUndefined();
54
- expect(agentDef.tts).toBeUndefined();
55
- expect(agentDef.tools).toEqual({});
56
- });
57
-
58
45
  test("under the name the page starts a run by", () => {
59
46
  // `useWorkflowSubmit("redline")` in client.tsx names this key. Nothing else
60
47
  // records it, so a rename here is a 400 there rather than a compile error.
@@ -25,8 +25,9 @@
25
25
  * `ToolContext`, so it reads that key with `requireStepEnv` (inside
26
26
  * `stepGenerate`); `requiredEnv` below is what makes a deploy check for it
27
27
  * rather than letting the first run find out.
28
- * - **Storage** (`aai storage enable`, or `DATABASE_URL` under `aai dev`) runs
29
- * live there.
28
+ * - **Nothing for durability.** A deployed app's runs live on the platform. A
29
+ * `DATABASE_URL` (yours, if you set one) only moves the correlation-key index
30
+ * out of memory.
30
31
  */
31
32
 
32
33
  import { workflow, workflowApp } from "@alexkroman1/aai";
@@ -28,7 +28,7 @@
28
28
 
29
29
  import "@alexkroman1/aai-ui/styles.css";
30
30
  import { plural } from "@alexkroman1/aai/utils";
31
- import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
31
+ import type { WorkflowInputOf, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
32
32
  import {
33
33
  Form,
34
34
  type FormValues,
@@ -62,10 +62,15 @@ const WORKFLOW = "redline";
62
62
  * thing that otherwise gets half-done in three places. Blank lines go, so a
63
63
  * trailing newline is not a requirement to cover "".
64
64
  */
65
- export function toInput(values: FormValues): FormValues {
65
+ export function toInput(values: FormValues): WorkflowInputOf<typeof redline> {
66
66
  const raw = typeof values.mustCover === "string" ? values.mustCover : "";
67
+ // The scalars ride through as the form collected them — strings from the DOM,
68
+ // which the WORKFLOW's schema coerces and validates server-side. Only
69
+ // `mustCover` is reshaped here, because no control renders a `string[]`.
70
+ // The assertion is on the scalars alone and is what `submitForm` exists to
71
+ // avoid needing anywhere a page is not doing this reshaping deliberately.
67
72
  return {
68
- ...values,
73
+ ...(values as Omit<WorkflowInputOf<typeof redline>, "mustCover">),
69
74
  mustCover: raw
70
75
  .split("\n")
71
76
  .map((line) => line.trim())
@@ -74,7 +79,7 @@ export function toInput(values: FormValues): FormValues {
74
79
  }
75
80
 
76
81
  function RedlineDesk() {
77
- const { submit, run, pending, error, reset } = useWorkflowSubmit<Redline>(WORKFLOW);
82
+ const { submit, run, pending, error, reset } = useWorkflowSubmit<typeof redline>(WORKFLOW);
78
83
 
79
84
  return (
80
85
  <main className="mx-auto flex max-w-2xl flex-col gap-8 p-8">
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does this desk actually behave? Run it with `aai eval`.
4
2
  //
5
3
  // `agent.test.ts` drives the four tools against a STUBBED `ctx.workflows` and
@@ -29,14 +27,6 @@
29
27
  //
30
28
  // And what no eval here can see at all: anything below the audio boundary —
31
29
  // endpointing, barge-in, whether two sentences merged into one turn.
32
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
33
- import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
34
- import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
35
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
36
- import { expect } from "vitest";
37
- import { z } from "zod";
38
- import authoredAgent from "./agent.ts";
39
- import { research } from "./shared.ts";
40
30
 
41
31
  /**
42
32
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
@@ -50,10 +40,13 @@ import { research } from "./shared.ts";
50
40
  * The glob is written here rather than reached for from a shared helper because
51
41
  * this file SHIPS — see `agent.test.ts`.
52
42
  */
53
- const agentDef = withDiscoveredTools(
54
- authoredAgent,
55
- import.meta.glob("./tools/*.ts", { eager: true }),
56
- );
43
+ import agentDef from "virtual:aai/agent";
44
+ import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
45
+ import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
46
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
47
+ import { expect } from "vitest";
48
+ import { z } from "zod";
49
+ import { research } from "./shared.ts";
57
50
 
58
51
  /**
59
52
  * The key the run's steps read with `requireStepEnv`.
@@ -28,14 +28,12 @@ import {
28
28
  type StubGatewayCall,
29
29
  schemaInputIssues,
30
30
  toolRunner,
31
- withDiscoveredTools,
32
31
  } from "@alexkroman1/aai/testing";
33
32
  import { mockWorkflows, installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
34
33
  import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
35
34
  import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
36
35
  import { beforeEach, describe, expect, test, vi } from "vitest";
37
36
  import { FatalError, RetryableError } from "workflow";
38
- import authoredAgent from "./agent.ts";
39
37
  import { research } from "./shared.ts";
40
38
  import {
41
39
  countSources,
@@ -63,18 +61,8 @@ vi.mock("@alexkroman1/aai/tools", () => ({
63
61
  visitWebpage: vi.fn(async () => ({ content: "The page body." })),
64
62
  }));
65
63
 
66
- /**
67
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
68
- *
69
- * The glob is written HERE rather than reached for from a shared helper because
70
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
71
- * anything outside its own template, and `import.meta.glob` is expanded against
72
- * the file containing it either way. This is the pattern a user writes.
73
- */
74
- const agentDef = withDiscoveredTools(
75
- authoredAgent,
76
- import.meta.glob("./tools/*.ts", { eager: true }),
77
- );
64
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
65
+ import agentDef from "virtual:aai/agent";
78
66
 
79
67
  /**
80
68
  * Every tool here is driven through the agent's own table, by the name the model
@@ -404,7 +392,7 @@ describe("the steps that research", () => {
404
392
 
405
393
  const note = await investigate(brief, "Tool use");
406
394
 
407
- expect(webSearch).toHaveBeenCalledWith({ query: "otter tool use", max_results: 5 });
395
+ expect(webSearch).toHaveBeenCalledWith({ query: "otter tool use", maxResults: 5 });
408
396
  expect(visitWebpage).toHaveBeenCalledWith("https://otters.example/tools");
409
397
  expect(note.findings).toContain("crack shellfish");
410
398
  expect(note.sources).toEqual([{ title: "Otters", url: "https://otters.example/tools" }]);
@@ -69,11 +69,13 @@
69
69
  * thing that changes under `aai dev` (the key has to be in `.env`, not just your
70
70
  * shell).
71
71
  *
72
- * Storage is what makes it DURABLE (`aai storage enable`, Settings Database
73
- * in the studio, or `DATABASE_URL` under `aai dev`) runs and the key index
74
- * both live there. Without it both live in the process, so a run in flight is
75
- * lost when the agent restarts, redeploys or goes idle; everything below still
76
- * works, which is what lets you try it before provisioning anything.
72
+ * Runs are DURABLE on the platform with nothing to configure they live on the
73
+ * platform's own database and survive a restart, a redeploy and an idle sandbox.
74
+ *
75
+ * A `DATABASE_URL` you supply (a secret when deployed, `.env` under `aai dev`)
76
+ * buys the key index, which is what lets `find()` resolve a run by key across a
77
+ * restart. Under `aai dev` with none, the runs go with the process too —
78
+ * everything below still works, which is what lets you try it first.
77
79
  */
78
80
 
79
81
  import { agent } from "@alexkroman1/aai";
@@ -65,7 +65,13 @@ import {
65
65
  RESEARCH_SYSTEM,
66
66
  } from "./prompts.ts";
67
67
 
68
- /** Angles investigated at once. The far side of every one is a rate limit. */
68
+ /**
69
+ * Angles investigated at once. The far side of every one is a rate limit.
70
+ *
71
+ * Inside the workflow world's default worker concurrency (three on the
72
+ * `DATABASE_URL` path), so this width is what really runs — see "The WINDOW is
73
+ * not the concurrency" in `mapConcurrent`.
74
+ */
69
75
  const ANGLE_CONCURRENCY = 2;
70
76
 
71
77
  /**
@@ -424,7 +430,7 @@ async function search(query: string): Promise<{ summary: string; sources: Source
424
430
  try {
425
431
  const results = await webSearch<{ results?: { title?: string; url?: string }[] }>({
426
432
  query,
427
- max_results: SEARCH_RESULTS,
433
+ maxResults: SEARCH_RESULTS,
428
434
  });
429
435
  // The `catch` below was written for exactly this and could not reach it:
430
436
  // `webSearch` ANSWERS with `{ error }` rather than throwing, so a refused
@@ -1,8 +1,14 @@
1
- /// <reference types="vite/client" />
2
-
3
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
1
+ /**
2
+ * The def a DEPLOYED agent runs: authored, plus what `tools/` declares, plus
3
+ * what `system-prompt.md` says.
4
+ *
5
+ * Driving the raw default export instead would measure a seventeen-tool desk
6
+ * with no tools and the FRAMEWORK DEFAULT prompt — and for this template that
7
+ * is the whole subject: the authenticate-first discipline, the three-step
8
+ * readback and the one-customer-per-call rule all live in that file.
9
+ */
10
+ import retailAgent from "virtual:aai/agent";
4
11
  import type { SessionEvent } from "@alexkroman1/aai/protocol";
5
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
6
12
  // An EVAL: does this desk actually behave? Run it with `aai eval`.
7
13
  //
8
14
  // `agent.test.ts` drives each tool directly and asserts about its result;
@@ -29,27 +35,6 @@ import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-r
29
35
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
30
36
  import { expect } from "vitest";
31
37
  import { z } from "zod";
32
- import authoredAgent from "./agent.ts";
33
- import systemPrompt from "./system-prompt.md?raw";
34
-
35
- /**
36
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares, plus
37
- * what `system-prompt.md` says.
38
- *
39
- * BOTH wrappers are load-bearing and neither is applied by `agent()` — they are
40
- * applied by the BUILD (`aai build` enumerates `tools/` and resolves the prompt
41
- * file), so an eval driving the raw default export would measure a
42
- * seventeen-tool desk with no tools and the FRAMEWORK DEFAULT prompt. For this
43
- * template that is the whole subject: the authenticate-first discipline, the
44
- * three-step readback and the one-customer-per-call rule are all in that file.
45
- *
46
- * The glob is written here rather than reached for from a shared helper because
47
- * this file SHIPS — see `registry.test.ts`.
48
- */
49
- const retailAgent = withSystemPrompt(
50
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
51
- systemPrompt,
52
- );
53
38
 
54
39
  /** Harper Brown: exactly ONE pending order, which is what makes "cancel my
55
40
  * pending order" unambiguous — `resolveOrder` refuses a reference that matches
@@ -1,9 +1,6 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, withDiscoveredTools } from "@alexkroman1/aai/testing";
2
+ import { createToolContext, deployedAgent } from "@alexkroman1/aai/testing";
5
3
  import { describe, expect, test } from "vitest";
6
- import authoredAgent from "./agent.ts";
7
4
  import { callFlow, retailSlot } from "./store.ts";
8
5
 
9
6
  /** Tools that legitimately run before the caller is identified — the six
@@ -20,17 +17,23 @@ const PUBLIC_TOOLS = new Set([
20
17
  ]);
21
18
 
22
19
  /**
23
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
20
+ * The def a DEPLOYED agent runs, lowered BY HAND — the one place in the
21
+ * templates that still does.
24
22
  *
25
- * The glob is written HERE rather than reached for from a shared helper because
26
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
27
- * anything outside its own template, and `import.meta.glob` is expanded against
28
- * the file containing it either way. This is the pattern a user writes.
23
+ * `virtual:aai/agent` is what every other spec imports and what a user should
24
+ * reach for. This file is the exception on purpose: its whole subject is the
25
+ * tool REGISTRY, so doing the discovery explicitly is the thing under test
26
+ * rather than setup around it. It is also the worked example for a project
27
+ * whose runner is not vitest, and so cannot register the plugin.
29
28
  */
30
- const retailAgent = withDiscoveredTools(
31
- authoredAgent,
32
- import.meta.glob("./tools/*.ts", { eager: true }),
33
- );
29
+ import authoredAgent from "./agent.ts";
30
+ import systemPrompt from "./system-prompt.md?raw";
31
+
32
+ const retailAgent = deployedAgent(authoredAgent, {
33
+ tools: import.meta.glob("./tools/*.ts", { eager: true }),
34
+ systemPrompt,
35
+ });
36
+
34
37
  const registry = Object.entries(retailAgent.tools);
35
38
 
36
39
  // `createToolContext()` rather than a cast: it carries a real slot store (the
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the story machine actually hold? Run it with `aai eval`.
4
2
  //
5
3
  // `agent.test.ts` drives each tool directly against a context it made itself,
@@ -25,15 +23,6 @@
25
23
  // What no eval here can see: anything below the audio boundary — endpointing,
26
24
  // barge-in, a sentence split across two turns. Those need real paced audio.
27
25
 
28
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
29
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
30
- import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
31
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
32
- import { expect } from "vitest";
33
- import { z } from "zod";
34
- import authoredAgent from "./agent.ts";
35
- import systemPrompt from "./system-prompt.md?raw";
36
-
37
26
  /**
38
27
  * The def a DEPLOYED agent runs, assembled the way the build assembles it: the
39
28
  * authored export, plus what `tools/` declares, plus `system-prompt.md`.
@@ -43,10 +32,11 @@ import systemPrompt from "./system-prompt.md?raw";
43
32
  * eval run against the framework default prompt would measure an agent nobody
44
33
  * deployed.
45
34
  */
46
- const agentDef = withSystemPrompt(
47
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
48
- systemPrompt,
49
- );
35
+ import agentDef from "virtual:aai/agent";
36
+ import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
37
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
38
+ import { expect } from "vitest";
39
+ import { z } from "zod";
50
40
 
51
41
  /**
52
42
  * What each tool this file drives answers, off the wire.
@@ -1,4 +1,4 @@
1
- import type { Db, ToolContext, ToolDef, ToolInputSchema } from "@alexkroman1/aai";
1
+ import type { ToolContext, ToolDef, ToolInputSchema } from "@alexkroman1/aai";
2
2
  import { isToolFailure } from "@alexkroman1/aai";
3
3
  import { createToolContext, ok } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test, vi } from "vitest";
@@ -18,44 +18,17 @@ import {
18
18
  import actionRoll from "./tools/action_roll.ts";
19
19
  import burnMomentum from "./tools/burn_momentum.ts";
20
20
  import checkState from "./tools/check_state.ts";
21
- import loadGame from "./tools/load_game.ts";
22
21
  import oracle from "./tools/oracle.ts";
23
- import saveGame from "./tools/save_game.ts";
24
22
  import setupCharacter from "./tools/setup_character.ts";
25
23
  import updateState from "./tools/update_state.ts";
26
24
 
27
25
  // ── Test doubles ─────────────────────────────────────────────────────────────
28
26
 
29
- /**
30
- * Map-backed fake of the app database, implementing exactly the three SQL
31
- * statements the shared save-slot helpers emit (create table / select /
32
- * upsert). Values are stored parsed, the way a postgres driver returns jsonb.
33
- */
34
- function makeDb(): { db: Db; rows: Map<string, unknown> } {
35
- const rows = new Map<string, unknown>();
36
- const db: Db = {
37
- async query<T>(sql: string, params: unknown[] = []): Promise<T[]> {
38
- if (sql.startsWith("create table if not exists app_state")) return [];
39
- if (sql.startsWith("select value from app_state")) {
40
- const key = params[0] as string;
41
- return rows.has(key) ? ([{ value: structuredClone(rows.get(key)) }] as T[]) : [];
42
- }
43
- if (sql.startsWith("insert into app_state")) {
44
- const [key, json] = params as [string, string];
45
- rows.set(key, JSON.parse(json)); // $2::jsonb — parsed like postgres would
46
- return [];
47
- }
48
- throw new Error(`unexpected SQL in test: ${sql}`);
49
- },
50
- };
51
- return { db, rows };
52
- }
53
-
54
27
  /** `send` is a spy rather than the recorder `createToolContext` installs,
55
28
  * because this suite asserts call counts on it. Each call gets its own slot
56
29
  * store, which is what makes two contexts two games. */
57
- function makeCtx(db: Db = makeDb().db): ToolContext {
58
- return createToolContext({ db, send: vi.fn() });
30
+ function makeCtx(): ToolContext {
31
+ return createToolContext({ send: vi.fn() });
59
32
  }
60
33
 
61
34
  const SETUP_ARGS = {
@@ -588,65 +561,13 @@ describe("update_state", () => {
588
561
  expect(
589
562
  params.parse({ addClockSegments: 6, updateNpcBond: 4, timeOfDay: "night" }),
590
563
  ).toBeTruthy();
591
-
592
- const slotParams = saveGame.inputSchema!;
593
- expect(() => slotParams.parse({ slot: "../../etc" })).toThrow();
594
- expect(() => slotParams.parse({ slot: "a".repeat(40) })).toThrow();
595
- expect(slotParams.parse({ slot: "chapter-2" })).toBeTruthy();
596
564
  });
597
565
  });
598
566
 
599
- // ── save_game / load_game: cross-session persistence via ctx.db ──────────────
600
-
601
- describe("save_game / load_game", () => {
602
- test("a save made in one session loads in a later session", async () => {
603
- const { db, rows } = makeDb();
604
-
605
- // Session A plays and saves.
606
- const sessionA = makeCtx(db);
607
- const played = playingState();
608
- played.playerName = "Kael";
609
- played.sceneCount = 7;
610
- seedPlaying(sessionA, played);
611
- const saved = ok<{ saved: boolean; slot: string }>(
612
- await saveGame.execute({ slot: "chapter-2" }, sessionA),
613
- );
614
- expect(saved.saved).toBe(true);
615
- expect(saved.slot).toBe("chapter-2");
616
- expect(rows.get("save:chapter-2")).toMatchObject({ playerName: "Kael", sceneCount: 7 });
617
-
618
- // Session B (a fresh game slot, the same app db) resumes it.
619
- const sessionB = makeCtx(db);
620
- // `load_game` is a plain `tool()`, so its result is the union its body
621
- // writes rather than a position envelope — the guard is what picks the
622
- // arm, where the old `as Record<string, unknown>` picked neither.
623
- const loaded = await loadGame.execute({ slot: "chapter-2" }, sessionB);
624
- if (isToolFailure(loaded)) throw new Error(`load refused: ${loaded.error}`);
625
- expect(loaded.loaded).toBe(true);
626
- expect(loaded.playerName).toBe("Kael");
627
- expect(loaded.sceneCount).toBe(7);
628
- expect(gameSlot.get(sessionB).playerName).toBe("Kael");
629
- });
630
-
631
- test("loading a missing slot reports an error instead of resetting the game", async () => {
632
- const ctx = makeCtx();
633
- const result = await loadGame.execute({ slot: "nope" }, ctx);
634
- expect(isToolFailure(result) && result.error).toMatch(/No save found/);
635
- });
636
-
637
- test("saving twice to one slot upserts — the newer save wins", async () => {
638
- const { db, rows } = makeDb();
639
- const ctx = makeCtx(db);
640
- seedPlaying(ctx);
641
- await saveGame.execute({}, ctx); // autosave
642
- gameSlot.update(ctx, (game) => {
643
- game.sceneCount = 9;
644
- });
645
- await saveGame.execute({}, ctx);
646
- expect(rows.size).toBe(1);
647
- expect(rows.get("save:autosave")).toMatchObject({ sceneCount: 9 });
648
- });
649
- });
567
+ // A `save_game / load_game` suite stood here, driving cross-session persistence
568
+ // through a map-backed fake of `ctx.db`. Both tools are gone: `ctx.db` is gone,
569
+ // and a shipped template cannot reach a database (see `shared.ts`). This
570
+ // adventure is single-session now.
650
571
 
651
572
  // ── the story flow ───────────────────────────────────────────────────────────
652
573
 
@@ -655,10 +576,11 @@ describe("the story flow", () => {
655
576
  const ctx = makeCtx();
656
577
  expect(storyFlow.position(ctx).state).toBe("awaitingSetup");
657
578
 
658
- // All three of these used to RUN before a character existed: `action_roll`
659
- // rolled 2d6 against the stats of nobody and applied consequences to a game
660
- // that was not there, and `save_game` wrote an empty campaign to a slot
661
- // `load_game` would later restore over a real one.
579
+ // All of these used to RUN before a character existed: `action_roll` rolled
580
+ // 2d6 against the stats of nobody and applied consequences to a game that was
581
+ // not there. (`save_game` was in this list too, writing an empty campaign to a
582
+ // slot `load_game` would later restore over a real one — both are gone with
583
+ // `ctx.db`.)
662
584
  for (const call of [
663
585
  actionRoll.execute(
664
586
  { move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "swing" },
@@ -666,7 +588,6 @@ describe("the story flow", () => {
666
588
  ),
667
589
  updateState.execute({ location: "Nowhere" }, ctx),
668
590
  callNoArgs(burnMomentum, ctx),
669
- saveGame.execute({}, ctx),
670
591
  ]) {
671
592
  const refusal = await call;
672
593
  expect(isToolFailure(refusal)).toBe(true);
@@ -740,37 +661,9 @@ describe("the story flow", () => {
740
661
  expect(restarted.state).toBe("playing.awaitingRoll");
741
662
  });
742
663
 
743
- test("a loaded game resumes in play rather than awaiting setup", async () => {
744
- const { db } = makeDb();
745
- const sessionA = makeCtx(db);
746
- seedPlaying(sessionA);
747
- ok(await saveGame.execute({ slot: "resume" }, sessionA));
748
-
749
- const sessionB = makeCtx(db);
750
- expect(storyFlow.position(sessionB).state).toBe("awaitingSetup");
751
- const loaded = await loadGame.execute({ slot: "resume" }, sessionB);
752
- if (isToolFailure(loaded)) throw new Error(`load refused: ${loaded.error}`);
753
- expect(loaded.state).toBe("playing.awaitingRoll");
754
-
755
- // And the play tools are available in the resumed session.
756
- ok(await updateState.execute({ location: "Back at the Docks" }, sessionB));
757
- });
758
-
759
- test("a save whose game was over resumes as over", async () => {
760
- const { db } = makeDb();
761
- const sessionA = makeCtx(db);
762
- const dead = playingState();
763
- dead.health = 0;
764
- dead.spirit = 0;
765
- dead.gameOver = true;
766
- dead.crisisMode = true;
767
- seedPlaying(sessionA, dead);
768
- ok(await saveGame.execute({ slot: "ended" }, sessionA));
769
-
770
- const sessionB = makeCtx(db);
771
- const loaded = await loadGame.execute({ slot: "ended" }, sessionB);
772
- if (isToolFailure(loaded)) throw new Error(`load refused: ${loaded.error}`);
773
- expect(loaded.state).toBe("gameOver");
774
- expect(storyFlow.position(sessionB).done).toBe(true);
775
- });
664
+ // Two resume tests stood here — a saved game reopening in play, and one saved
665
+ // after the ending reopening as over. Both drove `save_game`/`load_game`, which
666
+ // are gone with `ctx.db`. What they proved about the FLOW (a resumed position is
667
+ // restored rather than recomputed) has no path left to exercise it: a session
668
+ // is the whole life of a game now.
776
669
  });