@alexkroman1/aai-cli 8.2.1 → 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 (90) 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/{build-BBxf8vom.mjs → build-DvDdSwAS.mjs} +1 -1
  10. package/dist/cli.mjs +22 -93
  11. package/dist/{delete-CPf347lo.mjs → delete-8pjH8n8n.mjs} +2 -1
  12. package/dist/{deploy-BqV6ShIe.mjs → deploy-CjU3e-3P.mjs} +3 -3
  13. package/dist/{dev-QuuOOUtv.mjs → dev-dFzdvwa7.mjs} +3 -2
  14. package/dist/dev.d.ts +2 -0
  15. package/dist/{eval-DfQ8cm5p.mjs → eval-DwNwdqmS.mjs} +1 -1
  16. package/dist/{init-Cp5GZg0B.mjs → init-DK7GpIF9.mjs} +7 -6
  17. package/dist/scaffold/.env.example +7 -4
  18. package/dist/scaffold/CLAUDE.md +152 -97
  19. package/dist/scaffold/global.d.ts +12 -0
  20. package/dist/scaffold/package.json +4 -4
  21. package/dist/scaffold/server.mjs +49 -2
  22. package/dist/scaffold/tsconfig.json +1 -0
  23. package/dist/scaffold/vitest.config.ts +24 -0
  24. package/dist/{studio-DUfCH9I8.mjs → studio-D3-HWIfk.mjs} +3 -3
  25. package/dist/templates/briefing-desk/agent.test.ts +3 -15
  26. package/dist/templates/briefing-desk/shared.ts +3 -3
  27. package/dist/templates/call-audit/agent.eval.test.ts +8 -1
  28. package/dist/templates/call-audit/agent.test.ts +0 -1
  29. package/dist/templates/call-audit/agent.ts +4 -3
  30. package/dist/templates/call-audit/client.tsx +4 -7
  31. package/dist/templates/call-audit/workflows/audit.ts +10 -1
  32. package/dist/templates/code-interpreter/agent.eval.test.ts +14 -43
  33. package/dist/templates/dispatch-center/agent.eval.test.ts +16 -26
  34. package/dist/templates/embedded-assets/agent.eval.test.ts +6 -16
  35. package/dist/templates/embedded-assets/agent.test.ts +3 -16
  36. package/dist/templates/health-assistant/agent.eval.test.ts +5 -15
  37. package/dist/templates/health-assistant/agent.test.ts +3 -14
  38. package/dist/templates/infocom-adventure/agent.eval.test.ts +6 -16
  39. package/dist/templates/infocom-adventure/agent.test.ts +3 -17
  40. package/dist/templates/link-digest/agent.test.ts +0 -14
  41. package/dist/templates/link-digest/agent.ts +9 -5
  42. package/dist/templates/link-digest/client.tsx +8 -6
  43. package/dist/templates/math-buddy/agent.eval.test.ts +11 -40
  44. package/dist/templates/night-owl/agent.eval.test.ts +15 -44
  45. package/dist/templates/night-owl/agent.test.ts +2 -15
  46. package/dist/templates/personal-finance/agent.eval.test.ts +10 -39
  47. package/dist/templates/pipeline-simple/agent.test.ts +73 -0
  48. package/dist/templates/pizza-ordering/agent.eval.test.ts +2 -17
  49. package/dist/templates/pizza-ordering/agent.test.ts +2 -17
  50. package/dist/templates/plan-and-execute/agent.eval.test.ts +2 -12
  51. package/dist/templates/plan-and-execute/agent.test.ts +3 -23
  52. package/dist/templates/plan-and-execute/shared.ts +1 -1
  53. package/dist/templates/podcast-digest/agent.test.ts +0 -1
  54. package/dist/templates/podcast-digest/client.tsx +3 -6
  55. package/dist/templates/podcast-digest/workflows/digest.ts +16 -8
  56. package/dist/templates/recap-workflow/agent.eval.test.ts +7 -14
  57. package/dist/templates/recap-workflow/agent.test.ts +3 -18
  58. package/dist/templates/recap-workflow/agent.ts +7 -5
  59. package/dist/templates/redline/agent.test.ts +0 -13
  60. package/dist/templates/redline/agent.ts +3 -2
  61. package/dist/templates/redline/client.tsx +9 -4
  62. package/dist/templates/research-workflow/agent.eval.test.ts +7 -14
  63. package/dist/templates/research-workflow/agent.test.ts +3 -15
  64. package/dist/templates/research-workflow/agent.ts +7 -5
  65. package/dist/templates/research-workflow/workflows/research.ts +8 -2
  66. package/dist/templates/retail/agent.eval.test.ts +10 -25
  67. package/dist/templates/retail/registry.test.ts +16 -13
  68. package/dist/templates/solo-rpg/agent.eval.test.ts +5 -15
  69. package/dist/templates/solo-rpg/agent.test.ts +17 -124
  70. package/dist/templates/solo-rpg/shared.ts +13 -61
  71. package/dist/templates/spoken-summary/agent.test.ts +0 -1
  72. package/dist/templates/spoken-summary/agent.ts +5 -4
  73. package/dist/templates/spoken-summary/client.tsx +4 -7
  74. package/dist/templates/support-line/agent.eval.test.ts +2 -12
  75. package/dist/templates/support-line/agent.test.ts +3 -22
  76. package/dist/templates/transcription-workflow/agent.ts +4 -4
  77. package/dist/templates/transcription-workflow/client.tsx +5 -5
  78. package/dist/templates/transcription-workflow/workflows/transcribe.ts +10 -1
  79. package/dist/templates/travel-concierge/agent.eval.test.ts +5 -15
  80. package/dist/templates/travel-concierge/agent.test.ts +3 -23
  81. package/dist/templates/web-researcher/agent.eval.test.ts +2 -5
  82. package/dist/test-CvwgeVSQ.mjs +181 -0
  83. package/dist/test.d.ts +33 -2
  84. package/package.json +4 -4
  85. package/dist/_init-BhEfQ6Yi.mjs +0 -93
  86. package/dist/storage-Bfs7QQWq.mjs +0 -66
  87. package/dist/storage.d.ts +0 -22
  88. package/dist/templates/solo-rpg/tools/load_game.ts +0 -37
  89. package/dist/templates/solo-rpg/tools/save_game.ts +0 -32
  90. package/dist/test-BvyT6sAc.mjs +0 -104
@@ -45,11 +45,11 @@ export const MAX_RESEARCH_STEPS = 6;
45
45
  * "Finish with a summary" is not politeness. The parent gets the subagent's
46
46
  * FINAL message, so a run that ends by saying "Done." has thrown away
47
47
  * everything it read and no budget recovers it — this is the failure mode the
48
- * `SubagentDef.instructions` contract warns about, stated once, here.
48
+ * `SubagentDef.systemPrompt` contract warns about, stated once, here.
49
49
  */
50
50
  export const researcher = subagent({
51
51
  name: "researcher",
52
- instructions: [
52
+ systemPrompt: [
53
53
  "You are a research agent working one angle of a briefing.",
54
54
  "",
55
55
  "Search, then open the two or three most promising pages and read them.",
@@ -77,7 +77,7 @@ export const researcher = subagent({
77
77
  */
78
78
  export const factChecker = subagent({
79
79
  name: "fact-checker",
80
- instructions: [
80
+ systemPrompt: [
81
81
  "You check ONE claim against what you can find on the web.",
82
82
  "",
83
83
  "Search for it. Answer in one sentence, starting with one of",
@@ -101,7 +101,14 @@ const describeWorkflowEvalWithFfmpeg: typeof describeWorkflowEval = (agent, defi
101
101
  if ((process.env.AAI_REQUIRE_FFMPEG ?? "") !== "") {
102
102
  throw new Error(`AAI_REQUIRE_FFMPEG is set but no ffmpeg was found.\n${HOW_TO}`);
103
103
  }
104
- console.warn(`\n[skipped: no ffmpeg] ${agent.name} eval not run.\n${HOW_TO}\n`);
104
+ // A direct stderr write, not `console.warn`: vitest intercepts `console`, and
105
+ // which reporter it hands the capture to is chosen for you — unset
106
+ // `reporters` resolves to vitest's AGENT reporter when it detects one, and
107
+ // that reporter prints a passing file's output nowhere. A skip passes. So the
108
+ // announcement that keeps this skip from being silent was dropped for exactly
109
+ // the reader most likely to miss it. `announceEvalMode` in the SDK carries the
110
+ // measurement.
111
+ process.stderr.write(`\n[skipped: no ffmpeg] ${agent.name} eval not run.\n${HOW_TO}\n`);
105
112
  skipSuite(agent.name, () => {
106
113
  // Named `test`, not aliased: Biome's `noMisplacedAssertion` matches the
107
114
  // CALLEE IDENTIFIER, so an `expect` inside a `vitestTest(…)` is an error.
@@ -171,7 +171,6 @@ describe("the declaration", () => {
171
171
  // The page calls `api.start("audit", …)`, so a rename here is a runtime 400
172
172
  // rather than a compile error. This is what pins it.
173
173
  expect(Object.keys(agentDef.workflows ?? {})).toEqual(["audit"]);
174
- expect(agentDef.page).toBe("static");
175
174
  });
176
175
 
177
176
  test("declares no providers and exactly the one credential its steps read", () => {
@@ -57,9 +57,10 @@
57
57
  * `aai secret put ASSEMBLYAI_API_KEY` once deployed. `requiredEnv` below is what
58
58
  * makes a deploy check for it rather than letting the first run find out. One key
59
59
  * covers transcription, the model and the voice alike.
60
- * - **Storage** (`aai storage enable`, Settings Database in the studio, or
61
- * `DATABASE_URL` under `aai dev`). REQUIRED, unlike most workflow apps: an
62
- * upload's record is a row, and this desk both reads an upload and writes two.
60
+ * - **A `DATABASE_URL`** a secret when deployed, `.env` under `aai dev`, and a
61
+ * Postgres you bring since the platform provisions none. REQUIRED, unlike most
62
+ * workflow apps: an upload's record is a row, and this desk both reads an
63
+ * upload and writes two.
63
64
  * - **ffmpeg** — every deployed guest's image installs it (and `ffprobe` with it).
64
65
  * Under `aai dev` it is whatever is on `PATH`, or what `AAI_FFMPEG_PATH` /
65
66
  * `AAI_FFPROBE_PATH` name. That is the one place dev/prod parity is partial, so
@@ -29,7 +29,6 @@ import "@alexkroman1/aai-ui/styles.css";
29
29
  // nothing — and it is what stops this file restating a shape `workflows/audit.ts`
30
30
  // already declares.
31
31
  import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
32
- import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
33
32
  import {
34
33
  createWorkflowApi,
35
34
  Form,
@@ -43,9 +42,6 @@ import {
43
42
  } from "@alexkroman1/aai-ui";
44
43
  import type { audit } from "./agent.ts";
45
44
 
46
- /** What a completed run reports, derived from the workflow rather than restated. */
47
- type Audit = WorkflowOutputOf<typeof audit>;
48
-
49
45
  /**
50
46
  * The workflow's name, as a page starts a run by one.
51
47
  *
@@ -91,8 +87,9 @@ function Findings({ title, items }: { title: string; items: string[] }) {
91
87
  export function App() {
92
88
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
93
89
  // `run.output` instead of `unknown`.
94
- const { submit, run, pending, upload, pauseUpload, resumeUpload, error } =
95
- useWorkflowSubmit<Audit>(WORKFLOW, { api });
90
+ const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } = useWorkflowSubmit<
91
+ typeof audit
92
+ >(WORKFLOW, { api });
96
93
  const output = run?.status === "completed" ? run.output : undefined;
97
94
  // `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
98
95
  // bytes have to be FETCHED and handed to the element as an object URL — and the
@@ -109,7 +106,7 @@ export function App() {
109
106
  </p>
110
107
  </header>
111
108
 
112
- <Form onSubmit={submit} error={error} className="flex flex-col gap-4">
109
+ <Form onSubmit={submitForm} error={error} className="flex flex-col gap-4">
113
110
  {/* Every control, from the workflow's own input schema. See the module doc. */}
114
111
  <WorkflowFields workflow={WORKFLOW} />
115
112
  <SubmitButton pending={pending} pendingLabel="Auditing…">
@@ -78,7 +78,16 @@ import { transcribeSpan } from "./sync-api.ts";
78
78
  * template measured as the point where the endpoint starts returning `503`s. So
79
79
  * the byte bound never binds and what is left is the endpoint's own knee, which it
80
80
  * measured at 32. Its `BYTES_IN_FLIGHT` and `MAX_SEGMENT_CONCURRENCY` docs carry
81
- * both measurements; this is the one number that survives them.
81
+ * both measurements; this is the one number that survives them. *
82
+ * **What EXECUTES at this width is the world's call, not this number's.**
83
+ * `mapConcurrent` bounds how many step calls the body has in flight; how many
84
+ * run at once is the workflow world's worker concurrency, which on the
85
+ * `DATABASE_URL` path defaults to three — so on a default deployment a width
86
+ * above three is inert while still costing a queued job per item. That makes
87
+ * this the FAR SIDE's knee and the width to use once an operator has raised
88
+ * the ceiling, not a promise about a stock deployment. See "The WINDOW is not
89
+ * the concurrency" in `@alexkroman1/aai/step`'s `mapConcurrent`; the numbers
90
+ * above were measured against the endpoint and say nothing about that layer.
82
91
  */
83
92
  export const SEGMENT_CONCURRENCY = 32;
84
93
 
@@ -19,19 +19,14 @@
19
19
  // model-written JavaScript in the host process. That is right, and it left this
20
20
  // template's whole subject assertable as a CALL and never as an answer: a
21
21
  // `toBeDefined()` on the result is satisfied by the refusal itself. So the suite
22
- // passes `runCode` (below) — a developer's own machine may, a deployment may
23
- // not — and every case here asserts BOTH halves: that Coda reached for code, and
24
- // what the code came back with.
25
- import { runInNewContext } from "node:vm";
26
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
27
- import { errorMessage } from "@alexkroman1/aai/utils";
28
- import type { RunCodeExecutor } from "@alexkroman1/aai-runtime/eval";
22
+ // passes `createVmRunCode()` — a developer's own machine may evaluate generated
23
+ // code, a deployment may not — and every case here asserts BOTH halves: that
24
+ // Coda reached for code, and what the code came back with.
25
+
26
+ import agentDef from "virtual:aai/agent";
27
+ import { createVmRunCode, toolResultsIn } from "@alexkroman1/aai-runtime/eval";
29
28
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
30
29
  import { expect } from "vitest";
31
- import authored from "./agent.ts";
32
- import systemPrompt from "./system-prompt.md?raw";
33
-
34
- const agentDef = withSystemPrompt(authored, systemPrompt);
35
30
 
36
31
  /** The code every `run_code` call in this turn carried, joined. */
37
32
  const codeIn = (turn: { toolCalls: readonly { name: string; args: Record<string, unknown> }[] }) =>
@@ -45,37 +40,13 @@ const codeIn = (turn: { toolCalls: readonly { name: string; args: Record<string,
45
40
  *
46
41
  * The builtin refuses without one — the Modal container is the security
47
42
  * boundary, and off-platform there is none — so a case could assert the CALL and
48
- * the code it carried, and never what the code came back with. A `node:vm`
49
- * context with a capturing `console.log` is what a developer would reach for on
50
- * their own machine, and it is enough here: what runs is arithmetic, not a
51
- * program. It is NOT a sandbox and does not pretend to be one; a deployed agent
52
- * still gets the refusal.
53
- *
54
- * A template eval imports from `@alexkroman1/aai-runtime/eval` and
55
- * `/eval/vitest` and nowhere else — the ROOT barrel drags the host runtime's
56
- * node-reaching module graph into this project's TypeScript program, which is
57
- * three errors in runtime files no eval ever calls. `RunCodeExecutor` is
58
- * re-exported from `/eval` for exactly that reason.
43
+ * the code it carried, and never what the code came back with.
44
+ * `createVmRunCode()` is a `node:vm` context with a capturing `console.log`,
45
+ * which is enough here: what runs is arithmetic, not a program. It is NOT a
46
+ * sandbox and does not pretend to be one; a deployed agent still gets the
47
+ * refusal.
59
48
  */
60
- const runCode: RunCodeExecutor = async (code) => {
61
- const lines: string[] = [];
62
- const log = (...args: unknown[]): void => {
63
- lines.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
64
- };
65
- try {
66
- runInNewContext(code, { console: { log } }, { timeout: 1000 });
67
- } catch (err) {
68
- return { error: errorMessage(err) };
69
- }
70
- return lines.join("\n");
71
- };
72
-
73
- /** What every `run_code` call in this turn PRINTED, joined. */
74
- const outputIn = (turn: { toolCalls: readonly { name: string; result?: string }[] }) =>
75
- turn.toolCalls
76
- .filter((c) => c.name === "run_code")
77
- .map((c) => c.result ?? "")
78
- .join("\n");
49
+ const runCode = createVmRunCode();
79
50
 
80
51
  describeEval(
81
52
  agentDef,
@@ -97,7 +68,7 @@ describeEval(
97
68
  // none, `run_code` answers "only available in the sandboxed runtime", so
98
69
  // every claim above passes for an agent that then does the sum in its
99
70
  // head — which is the exact regression the CRITICAL RULE exists to stop.
100
- const output = outputIn(turn);
71
+ const output = toolResultsIn(turn.toolCalls, "run_code").join("\n");
101
72
  expect(output, `run_code printed: ${output}`).toContain("107823");
102
73
 
103
74
  // "Report RESULTS, never intentions": the call goes out before Coda says
@@ -129,7 +100,7 @@ describeEval(
129
100
  // Greenwich (measured: this printed "Friday"). Asserting the weekday
130
101
  // would measure the machine running the eval. The two arithmetic cases
131
102
  // in this suite have no such dependency and do assert their answers.
132
- const output = outputIn(turn);
103
+ const output = toolResultsIn(turn.toolCalls, "run_code").join("\n");
133
104
  expect(output, `run_code printed: ${output}`).not.toMatch(
134
105
  /only available in the sandboxed runtime/,
135
106
  );
@@ -1,8 +1,20 @@
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
+ * BOTH wrappers are load-bearing and neither is applied by `agent()` — they are
6
+ * applied by the BUILD (`aai build` enumerates `tools/` and resolves the prompt
7
+ * file), so an eval driving the raw default export would measure a twelve-tool
8
+ * desk with no tools and the FRAMEWORK DEFAULT prompt. For this template that
9
+ * is the whole subject: "location is always the first priority", the radio
10
+ * style and "never leave a critical incident without a resource" are all in
11
+ * that file.
12
+ *
13
+ * The glob is written here rather than reached for from a shared helper because
14
+ * this file SHIPS — it is what a scaffolded project runs.
15
+ */
16
+ import dispatchAgent from "virtual:aai/agent";
4
17
  import type { SessionEvent } from "@alexkroman1/aai/protocol";
5
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
6
18
  // An EVAL: does the desk actually behave? Run it with `aai eval`.
7
19
  //
8
20
  // `agent.test.ts` drives each tool directly. What it cannot ask is whether the
@@ -26,28 +38,6 @@ import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-r
26
38
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
27
39
  import { expect } from "vitest";
28
40
  import { z } from "zod";
29
- import authoredAgent from "./agent.ts";
30
- import systemPrompt from "./system-prompt.md?raw";
31
-
32
- /**
33
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares, plus
34
- * what `system-prompt.md` says.
35
- *
36
- * BOTH wrappers are load-bearing and neither is applied by `agent()` — they are
37
- * applied by the BUILD (`aai build` enumerates `tools/` and resolves the prompt
38
- * file), so an eval driving the raw default export would measure a twelve-tool
39
- * desk with no tools and the FRAMEWORK DEFAULT prompt. For this template that
40
- * is the whole subject: "location is always the first priority", the radio
41
- * style and "never leave a critical incident without a resource" are all in
42
- * that file.
43
- *
44
- * The glob is written here rather than reached for from a shared helper because
45
- * this file SHIPS — it is what a scaffolded project runs.
46
- */
47
- const dispatchAgent = withSystemPrompt(
48
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
49
- systemPrompt,
50
- );
51
41
 
52
42
  /** The six tools gated on `working` — the ones that refuse until something has
53
43
  * been logged. Listed here so ADDING an ungated mutating tool is a deliberate
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the FAQ bot actually look things up?
4
2
  //
5
3
  // `agent.test.ts` scores the search index directly, which settles what
@@ -12,16 +10,6 @@
12
10
  // SCRIPTED model (its `stubReply`) — the real session and the real tools, a
13
11
  // fake reply — which proves the wiring and says nothing about the discipline.
14
12
 
15
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
16
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
17
- import { toolResultIn } from "@alexkroman1/aai-runtime/eval";
18
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
19
- import { expect } from "vitest";
20
- import { z } from "zod";
21
- import authoredAgent from "./agent.ts";
22
- import { faqs } from "./shared.ts";
23
- import systemPrompt from "./system-prompt.md?raw";
24
-
25
13
  /**
26
14
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
27
15
  *
@@ -36,10 +24,12 @@ import systemPrompt from "./system-prompt.md?raw";
36
24
  * drives it measures a different agent than the one that deploys, and every
37
25
  * tool-choice claim below then passes or fails for the wrong reason.
38
26
  */
39
- const agentDef = withSystemPrompt(
40
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
41
- systemPrompt,
42
- );
27
+ import agentDef from "virtual:aai/agent";
28
+ import { toolResultIn } from "@alexkroman1/aai-runtime/eval";
29
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
30
+ import { expect } from "vitest";
31
+ import { z } from "zod";
32
+ import { faqs } from "./shared.ts";
43
33
 
44
34
  /**
45
35
  * A knowledge-base HIT, as the model saw it — `tool.completed` carries it
@@ -1,22 +1,9 @@
1
- /// <reference types="vite/client" />
2
-
3
- import { toolRunner, withDiscoveredTools } 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 { toolRunner } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test } from "vitest";
5
- import authoredAgent from "./agent.ts";
6
5
  import { faqs, searchable } from "./shared.ts";
7
6
 
8
- /**
9
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
10
- *
11
- * The glob is written HERE rather than reached for from a shared helper because
12
- * this file SHIPS — a scaffolded project has no `../../_discovery.ts` to import,
13
- * and `import.meta.glob` is expanded against the file containing it either way.
14
- */
15
- const agentDef = withDiscoveredTools(
16
- authoredAgent,
17
- import.meta.glob("./tools/*.ts", { eager: true }),
18
- );
19
-
20
7
  /**
21
8
  * Both tools here are stateless, so no call passes a context: `runTool` builds
22
9
  * a fresh one, which is a distinct session with empty slots — right for a tool
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the desk read the label, or does it remember?
4
2
  //
5
3
  // `agent.test.ts` drives both tools against a faked openFDA, which settles the
@@ -21,15 +19,6 @@
21
19
  // unreachable openFDA reads as a drug that could not be resolved, which this
22
20
  // template already refuses on).
23
21
 
24
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
25
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
26
- import { toolResultIn } from "@alexkroman1/aai-runtime/eval";
27
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
28
- import { expect } from "vitest";
29
- import { z } from "zod";
30
- import authoredAgent from "./agent.ts";
31
- import systemPrompt from "./system-prompt.md?raw";
32
-
33
22
  /**
34
23
  * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
35
24
  *
@@ -44,10 +33,11 @@ import systemPrompt from "./system-prompt.md?raw";
44
33
  * drives it measures a different agent than the one that deploys, and every
45
34
  * tool-choice claim below then passes or fails for the wrong reason.
46
35
  */
47
- const agentDef = withSystemPrompt(
48
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
49
- systemPrompt,
50
- );
36
+ import agentDef from "virtual:aai/agent";
37
+ import { toolResultIn } from "@alexkroman1/aai-runtime/eval";
38
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
39
+ import { expect } from "vitest";
40
+ import { z } from "zod";
51
41
 
52
42
  /** The drugs an interaction check was actually asked about, lowercased. */
53
43
  const drugsIn = (args: Record<string, unknown>): string[] =>
@@ -1,9 +1,6 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  import { isToolFailure } from "@alexkroman1/aai";
4
- import { toolInputIssues, toolRunner, withDiscoveredTools } from "@alexkroman1/aai/testing";
2
+ import { toolInputIssues, toolRunner } from "@alexkroman1/aai/testing";
5
3
  import { beforeEach, describe, expect, test, vi } from "vitest";
6
- import authoredAgent from "./agent.ts";
7
4
  import { excerptAround, type FdaLabel, toDrugInfo } from "./fda.ts";
8
5
 
9
6
  /**
@@ -24,16 +21,8 @@ vi.mock("./fda.ts", async (importOriginal) => ({
24
21
  const { fetchFdaLabel } = await import("./fda.ts");
25
22
  const label = vi.mocked(fetchFdaLabel);
26
23
 
27
- /**
28
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
29
- *
30
- * The glob is written HERE rather than reached for from a shared helper because
31
- * this file SHIPS — a scaffolded project has no repo helper to import.
32
- */
33
- const agentDef = withDiscoveredTools(
34
- authoredAgent,
35
- import.meta.glob("./tools/*.ts", { eager: true }),
36
- );
24
+ /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
25
+ import agentDef from "virtual:aai/agent";
37
26
 
38
27
  /**
39
28
  * Every tool here takes arguments and none of them touches session state, so no
@@ -1,5 +1,3 @@
1
- /// <reference types="vite/client" />
2
-
3
1
  // An EVAL: does the game engine actually keep its world? Run it with `aai eval`.
4
2
  //
5
3
  // `agent.test.ts` drives the tools directly, one call at a time, against a
@@ -21,16 +19,6 @@
21
19
  // What no eval here can see: anything below the audio boundary — endpointing,
22
20
  // barge-in, two commands merging into one turn. Those need real paced audio.
23
21
 
24
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
25
- import { withDiscoveredTools } from "@alexkroman1/aai/testing";
26
- import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
27
- import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
28
- import { expect } from "vitest";
29
- import { z } from "zod";
30
- import authoredAgent from "./agent.ts";
31
- import { DEFAULT_GAME_STATE } from "./shared.ts";
32
- import systemPrompt from "./system-prompt.md?raw";
33
-
34
22
  /**
35
23
  * The def a DEPLOYED agent runs, assembled the way the build assembles it: the
36
24
  * authored export, plus what `tools/` declares, plus `system-prompt.md`.
@@ -41,10 +29,12 @@ import systemPrompt from "./system-prompt.md?raw";
41
29
  * `game_state_take` — an eval run against the framework default prompt measures
42
30
  * an agent nobody deployed.
43
31
  */
44
- const agentDef = withSystemPrompt(
45
- withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
46
- systemPrompt,
47
- );
32
+ import agentDef from "virtual:aai/agent";
33
+ import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
34
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
35
+ import { expect } from "vitest";
36
+ import { z } from "zod";
37
+ import { DEFAULT_GAME_STATE } from "./shared.ts";
48
38
 
49
39
  /**
50
40
  * What each of the three tools this file drives answers, off the wire.
@@ -1,21 +1,7 @@
1
- /// <reference types="vite/client" />
2
-
3
- import { createToolContext, toolRunner, withDiscoveredTools } 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, toolRunner } from "@alexkroman1/aai/testing";
4
4
  import { describe, expect, test } from "vitest";
5
- import authoredAgent from "./agent.ts";
6
-
7
- /**
8
- * The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
9
- *
10
- * The glob is written HERE rather than reached for from a shared helper because
11
- * this file SHIPS: it is what a scaffolded project runs, so it may not import
12
- * anything outside its own template, and `import.meta.glob` is expanded against
13
- * the file containing it either way. This is the pattern a user writes.
14
- */
15
- const agentDef = withDiscoveredTools(
16
- authoredAgent,
17
- import.meta.glob("./tools/*.ts", { eager: true }),
18
- );
19
5
 
20
6
  import { DEFAULT_GAME_STATE, gameSlot, MAX_HISTORY, REPORTED_HISTORY } from "./shared.ts";
21
7
 
@@ -28,20 +28,6 @@ import agentDef, { digest } from "./agent.ts";
28
28
  import { extractText, extractTitle, fetchArticle, summarize } from "./workflows/digest.ts";
29
29
 
30
30
  describe("the agent declares itself a workflow app", () => {
31
- test("its front door is a page, not a microphone", () => {
32
- // Not decoration: `createServer` declines `/websocket` with a reason for a
33
- // static agent, and telephony defaults off.
34
- expect(agentDef.page).toBe("static");
35
- });
36
-
37
- test("it declares no voice pipeline and no tools, because nothing talks", () => {
38
- expect(agentDef.stt).toBeUndefined();
39
- expect(agentDef.llm).toBeUndefined();
40
- expect(agentDef.tts).toBeUndefined();
41
- expect(agentDef.s2s).toBeUndefined();
42
- expect(agentDef.tools).toEqual({});
43
- });
44
-
45
31
  test("under the name the page starts a run by", () => {
46
32
  // `api.start("digest", …)` in client.tsx names this key. Nothing else
47
33
  // records it, so a rename here is a 400 there rather than a compile error.
@@ -44,11 +44,15 @@
44
44
  * doc for the one thing that changes under `aai dev` (the key has to be in
45
45
  * `.env`, not just your shell).
46
46
  *
47
- * Storage is what makes it DURABLE (`aai storage enable`, Settings Database
48
- * in the studio, or `DATABASE_URL` under `aai dev`) runs and the
49
- * correlation-key index both live there. Without it both live in the process,
50
- * so a run in flight is lost when the agent restarts, redeploys or goes idle —
51
- * which is fine while you are building one.
47
+ * Runs are DURABLE on the platform with nothing to configure a deployed app's
48
+ * runs live on the platform's own database and survive a restart, a redeploy and
49
+ * an idle sandbox.
50
+ *
51
+ * A `DATABASE_URL` (a secret when deployed, `.env` under `aai dev`) still buys
52
+ * one thing here: the correlation-key index, which is what lets `find()` resolve
53
+ * a run by key. Without one that index is in memory, so it is forgotten on a
54
+ * restart even though the runs themselves are not. Under `aai dev` with no
55
+ * `DATABASE_URL` the runs go too — fine while you are building.
52
56
  */
53
57
 
54
58
  import { workflow, workflowApp } from "@alexkroman1/aai";
@@ -42,13 +42,9 @@ import "@alexkroman1/aai-ui/styles.css";
42
42
  // ERASED at build time, so naming the agent's own type costs the browser bundle
43
43
  // nothing — and it is what stops this file restating a shape `workflows/
44
44
  // digest.ts` already declares.
45
- import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
46
45
  import { useState } from "react";
47
46
  import type { digest } from "./agent.ts";
48
47
 
49
- /** What a completed run reports, derived from the workflow rather than restated. */
50
- type Digest = WorkflowOutputOf<typeof digest>;
51
-
52
48
  export function App() {
53
49
  const [url, setUrl] = useState("");
54
50
  // The generic is what makes `run.status === "completed"` narrow to a TYPED
@@ -56,7 +52,7 @@ export function App() {
56
52
  // rejected input, which is better copy than anything this page could write, and
57
53
  // `wake` is bound to whatever run the hook is following — the whole reason this
58
54
  // page no longer holds a `createWorkflowApi()` of its own.
59
- const { submit, run, pending, error, wake } = useWorkflowSubmit<Digest>("digest");
55
+ const { submit, run, pending, error, wake } = useWorkflowSubmit<typeof digest>("digest");
60
56
 
61
57
  // `submit()` resolves as soon as the run exists — deliberately not when it
62
58
  // finishes. That is the whole mechanism: the digest sleeps for a while, and
@@ -76,7 +72,13 @@ export function App() {
76
72
  required
77
73
  value={url}
78
74
  onChange={(e) => setUrl(e.target.value)}
79
- placeholder="https://example.com/article"
75
+ // A REAL article, because a placeholder is a suggestion and this one
76
+ // gets typed. `https://example.com/article` 404s, and the bare
77
+ // `example.com` a reader trims it to has no prose on it — so the first
78
+ // run of the template failed with the agent's own
79
+ // "returned no readable text — is the page rendered in JS?", which
80
+ // reads as the template being broken rather than the URL being empty.
81
+ placeholder="https://en.wikipedia.org/wiki/Speech_recognition"
80
82
  className="flex-1 rounded-md border px-3 py-2"
81
83
  />
82
84
  <button type="submit" disabled={pending} className="rounded-md border px-4 py-2">
@@ -12,16 +12,11 @@
12
12
  // different agent; and `run_code` refuses unless the EVAL supplies an executor,
13
13
  // which this suite does — so the cases below assert the answer the code came
14
14
  // back with as well as the code the tutor wrote.
15
- import { runInNewContext } from "node:vm";
16
- import { withSystemPrompt } from "@alexkroman1/aai/manifest";
17
- import { errorMessage } from "@alexkroman1/aai/utils";
18
- import type { RunCodeExecutor } from "@alexkroman1/aai-runtime/eval";
15
+
16
+ import agentDef from "virtual:aai/agent";
17
+ import { createVmRunCode, toolResultsIn } from "@alexkroman1/aai-runtime/eval";
19
18
  import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
20
19
  import { expect } from "vitest";
21
- import authored from "./agent.ts";
22
- import systemPrompt from "./system-prompt.md?raw";
23
-
24
- const agentDef = withSystemPrompt(authored, systemPrompt);
25
20
 
26
21
  /** The code every `run_code` call in this turn carried, joined. */
27
22
  const codeIn = (turn: { toolCalls: readonly { name: string; args: Record<string, unknown> }[] }) =>
@@ -35,37 +30,13 @@ const codeIn = (turn: { toolCalls: readonly { name: string; args: Record<string,
35
30
  *
36
31
  * The builtin refuses without one — the Modal container is the security
37
32
  * boundary, and off-platform there is none — so a case could assert the CALL and
38
- * the code it carried, and never what the code came back with. A `node:vm`
39
- * context with a capturing `console.log` is what a developer would reach for on
40
- * their own machine, and it is enough here: what runs is arithmetic, not a
41
- * program. It is NOT a sandbox and does not pretend to be one; a deployed agent
42
- * still gets the refusal.
43
- *
44
- * A template eval imports from `@alexkroman1/aai-runtime/eval` and
45
- * `/eval/vitest` and nowhere else — the ROOT barrel drags the host runtime's
46
- * node-reaching module graph into this project's TypeScript program, which is
47
- * three errors in runtime files no eval ever calls. `RunCodeExecutor` is
48
- * re-exported from `/eval` for exactly that reason.
33
+ * the code it carried, and never what the code came back with.
34
+ * `createVmRunCode()` is a `node:vm` context with a capturing `console.log`,
35
+ * which is enough here: what runs is arithmetic, not a program. It is NOT a
36
+ * sandbox and does not pretend to be one; a deployed agent still gets the
37
+ * refusal.
49
38
  */
50
- const runCode: RunCodeExecutor = async (code) => {
51
- const lines: string[] = [];
52
- const log = (...args: unknown[]): void => {
53
- lines.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
54
- };
55
- try {
56
- runInNewContext(code, { console: { log } }, { timeout: 1000 });
57
- } catch (err) {
58
- return { error: errorMessage(err) };
59
- }
60
- return lines.join("\n");
61
- };
62
-
63
- /** What every `run_code` call in this turn PRINTED, joined. */
64
- const outputIn = (turn: { toolCalls: readonly { name: string; result?: string }[] }) =>
65
- turn.toolCalls
66
- .filter((c) => c.name === "run_code")
67
- .map((c) => c.result ?? "")
68
- .join("\n");
39
+ const runCode = createVmRunCode();
69
40
 
70
41
  describeEval(
71
42
  agentDef,
@@ -84,7 +55,7 @@ describeEval(
84
55
  expect(code).toMatch(/1\.60|1\.61|0\.621|8\.04/);
85
56
  // And the factor was applied rather than merely mentioned: five miles is
86
57
  // 8.0467 km, so whatever rounding the tutor chose the answer starts 8.0.
87
- const output = outputIn(turn);
58
+ const output = toolResultsIn(turn.toolCalls, "run_code").join("\n");
88
59
  expect(output, `run_code printed: ${output}`).toMatch(/8\.0/);
89
60
  },
90
61
  { live: true },
@@ -108,7 +79,7 @@ describeEval(
108
79
  // face of a twenty-sided die. `Math.random` in the code says the tutor
109
80
  // asked for a roll; this says it GOT one — a `run_code` that refused
110
81
  // prints a sentence with no dice in it at all.
111
- const output = outputIn(turn);
82
+ const output = toolResultsIn(turn.toolCalls, "run_code").join("\n");
112
83
  const rolled = [...output.matchAll(/\d+/g)].map((m) => Number(m[0]));
113
84
  expect(rolled.length, `run_code printed: ${output}`).toBeGreaterThanOrEqual(3);
114
85
  for (const face of rolled) {