@alexkroman1/aai-cli 6.11.0 → 8.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 (234) hide show
  1. package/README.md +1 -0
  2. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  3. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
  4. package/dist/_dev-env.d.ts +22 -2
  5. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
  6. package/dist/_dev-server.d.ts +0 -17
  7. package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
  8. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  9. package/dist/_preflight.d.ts +1 -1
  10. package/dist/_resource-commands.d.ts +23 -0
  11. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
  12. package/dist/_slug-api.d.ts +5 -2
  13. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  14. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
  15. package/dist/_templates.d.ts +3 -4
  16. package/dist/_workflow-scan.d.ts +108 -0
  17. package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
  18. package/dist/cli.mjs +346 -349
  19. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  20. package/dist/client-bundler.mjs +1 -1
  21. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  22. package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
  23. package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
  24. package/dist/eval-DfQ8cm5p.mjs +56 -0
  25. package/dist/eval.d.ts +43 -0
  26. package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
  27. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  28. package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
  29. package/dist/project-config.mjs +1 -1
  30. package/dist/scaffold/CLAUDE.md +442 -80
  31. package/dist/scaffold/package.json +6 -3
  32. package/dist/scaffold/server.mjs +11 -4
  33. package/dist/scaffold/tsconfig.json +0 -1
  34. package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
  35. package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
  36. package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
  37. package/dist/templates/briefing-desk/agent.test.ts +350 -0
  38. package/dist/templates/briefing-desk/agent.ts +30 -0
  39. package/dist/templates/briefing-desk/shared.ts +200 -0
  40. package/dist/templates/briefing-desk/system-prompt.md +39 -0
  41. package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
  42. package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
  43. package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
  44. package/dist/templates/call-audit/agent.eval.test.ts +474 -0
  45. package/dist/templates/call-audit/agent.test.ts +57 -204
  46. package/dist/templates/call-audit/agent.ts +32 -19
  47. package/dist/templates/call-audit/client.tsx +17 -54
  48. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  49. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  50. package/dist/templates/call-audit/workflows/media.ts +2 -12
  51. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  52. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  53. package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
  54. package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
  55. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  56. package/dist/templates/dispatch-center/client.tsx +239 -129
  57. package/dist/templates/dispatch-center/shared.ts +99 -1
  58. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  59. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  60. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  61. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  62. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  63. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  64. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  65. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  66. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  67. package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
  68. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  69. package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
  70. package/dist/templates/health-assistant/agent.test.ts +22 -4
  71. package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
  72. package/dist/templates/infocom-adventure/agent.test.ts +53 -24
  73. package/dist/templates/infocom-adventure/agent.ts +21 -0
  74. package/dist/templates/infocom-adventure/client.tsx +246 -164
  75. package/dist/templates/infocom-adventure/shared.ts +20 -9
  76. package/dist/templates/infocom-adventure/system-prompt.md +1 -1
  77. package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
  78. package/dist/templates/link-digest/agent.eval.test.ts +195 -0
  79. package/dist/templates/link-digest/agent.test.ts +24 -19
  80. package/dist/templates/link-digest/client.tsx +47 -61
  81. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  82. package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
  83. package/dist/templates/night-owl/agent.eval.test.ts +272 -0
  84. package/dist/templates/night-owl/agent.test.ts +70 -19
  85. package/dist/templates/night-owl/agent.ts +5 -0
  86. package/dist/templates/night-owl/client.tsx +56 -56
  87. package/dist/templates/night-owl/shared.ts +24 -0
  88. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  89. package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
  90. package/dist/templates/personal-finance/system-prompt.md +4 -0
  91. package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
  92. package/dist/templates/pipeline-simple/agent.ts +2 -2
  93. package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
  94. package/dist/templates/pizza-ordering/agent.test.ts +42 -27
  95. package/dist/templates/pizza-ordering/client.tsx +9 -26
  96. package/dist/templates/pizza-ordering/shared.ts +20 -1
  97. package/dist/templates/pizza-ordering/system-prompt.md +6 -0
  98. package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
  99. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  100. package/dist/templates/plan-and-execute/agent.ts +1 -1
  101. package/dist/templates/plan-and-execute/client.tsx +12 -15
  102. package/dist/templates/plan-and-execute/shared.ts +71 -2
  103. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  104. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  105. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  106. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  107. package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
  108. package/dist/templates/podcast-digest/agent.test.ts +717 -0
  109. package/dist/templates/podcast-digest/agent.ts +139 -0
  110. package/dist/templates/podcast-digest/client.tsx +154 -0
  111. package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
  112. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  113. package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
  114. package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
  115. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  116. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  117. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  118. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  119. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  120. package/dist/templates/redline/agent.eval.test.ts +209 -0
  121. package/dist/templates/redline/agent.test.ts +17 -15
  122. package/dist/templates/redline/client.tsx +12 -12
  123. package/dist/templates/redline/workflows/redline.ts +19 -31
  124. package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
  125. package/dist/templates/research-workflow/agent.test.ts +60 -59
  126. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  127. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  128. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  129. package/dist/templates/retail/address.ts +87 -1
  130. package/dist/templates/retail/agent.eval.test.ts +314 -0
  131. package/dist/templates/retail/agent.test.ts +195 -112
  132. package/dist/templates/retail/agent.ts +35 -1
  133. package/dist/templates/retail/cancel.ts +116 -0
  134. package/dist/templates/retail/client.tsx +265 -117
  135. package/dist/templates/retail/payment.ts +107 -0
  136. package/dist/templates/retail/pending.test.ts +196 -0
  137. package/dist/templates/retail/pending.ts +137 -0
  138. package/dist/templates/retail/registry.test.ts +150 -7
  139. package/dist/templates/retail/returns.ts +102 -0
  140. package/dist/templates/retail/shared.test.ts +42 -0
  141. package/dist/templates/retail/shared.ts +35 -0
  142. package/dist/templates/retail/store.test.ts +84 -15
  143. package/dist/templates/retail/store.ts +231 -48
  144. package/dist/templates/retail/swap.test.ts +4 -4
  145. package/dist/templates/retail/swap.ts +248 -15
  146. package/dist/templates/retail/system-prompt.md +69 -28
  147. package/dist/templates/retail/tools/cancel_change.ts +33 -0
  148. package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
  149. package/dist/templates/retail/tools/confirm_change.ts +42 -0
  150. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
  151. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  152. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  153. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  154. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  155. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  156. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  157. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  158. package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
  159. package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
  160. package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
  161. package/dist/templates/retail/tools/modify_user_address.ts +13 -16
  162. package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
  163. package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
  164. package/dist/templates/simple/agent.eval.test.ts +54 -0
  165. package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
  166. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  167. package/dist/templates/solo-rpg/client.tsx +38 -37
  168. package/dist/templates/solo-rpg/shared.ts +145 -19
  169. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  170. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  171. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  172. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  173. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  174. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  175. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  176. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  177. package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
  178. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  179. package/dist/templates/spoken-summary/agent.ts +30 -17
  180. package/dist/templates/spoken-summary/client.tsx +10 -50
  181. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  182. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  183. package/dist/templates/support-line/agent.eval.test.ts +273 -0
  184. package/dist/templates/support-line/agent.test.ts +11 -16
  185. package/dist/templates/support-line/agent.ts +1 -1
  186. package/dist/templates/support-line/client.tsx +9 -9
  187. package/dist/templates/support-line/nodes.ts +100 -0
  188. package/dist/templates/support-line/procedure.ts +407 -0
  189. package/dist/templates/support-line/system-prompt.md +2 -1
  190. package/dist/templates/support-line/tools/answer_question.ts +66 -4
  191. package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
  192. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  193. package/dist/templates/transcription-workflow/agent.ts +1 -1
  194. package/dist/templates/transcription-workflow/client.tsx +17 -42
  195. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  196. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  197. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  198. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  199. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  200. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  201. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  202. package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
  203. package/dist/templates/travel-concierge/agent.test.ts +174 -43
  204. package/dist/templates/travel-concierge/agent.ts +18 -9
  205. package/dist/templates/travel-concierge/client.tsx +11 -23
  206. package/dist/templates/travel-concierge/routing.ts +34 -15
  207. package/dist/templates/travel-concierge/shared.ts +180 -12
  208. package/dist/templates/travel-concierge/system-prompt.md +7 -1
  209. package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
  210. package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
  211. package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
  212. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
  213. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
  214. package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
  215. package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
  216. package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
  217. package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
  218. package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
  219. package/dist/templates/web-researcher/system-prompt.md +15 -4
  220. package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
  221. package/dist/test.d.ts +29 -5
  222. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
  223. package/dist/worker-bundler.mjs +1 -1
  224. package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
  225. package/dist/workflow-bundler.d.ts +6 -1
  226. package/dist/workflow.d.ts +1 -1
  227. package/package.json +5 -4
  228. package/dist/eject-31gjtaHF.mjs +0 -116
  229. package/dist/eject.d.ts +0 -60
  230. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  231. package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
  232. package/dist/templates/support-line/graph.ts +0 -224
  233. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  234. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
3
+ import { isToolFailure } from "@alexkroman1/aai";
4
+ import { toolInputIssues, toolRunner, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
5
  import { beforeEach, describe, expect, test, vi } from "vitest";
6
6
  import authoredAgent from "./agent.ts";
7
7
  import { excerptAround, type FdaLabel, toDrugInfo } from "./fda.ts";
@@ -35,8 +35,13 @@ const agentDef = withDiscoveredTools(
35
35
  import.meta.glob("./tools/*.ts", { eager: true }),
36
36
  );
37
37
 
38
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext = createToolContext()) =>
39
- runTool(agentDef, name, args, ctx);
38
+ /**
39
+ * Every tool here takes arguments and none of them touches session state, so no
40
+ * call passes a context: `runTool` builds a fresh one per call, which is a
41
+ * distinct session — right for a stateless tool, and never what two calls
42
+ * sharing state want.
43
+ */
44
+ const run = toolRunner(agentDef);
40
45
 
41
46
  const IBUPROFEN: FdaLabel = {
42
47
  openfda: { generic_name: ["IBUPROFEN"], brand_name: ["Advil"], manufacturer_name: ["Acme"] },
@@ -150,6 +155,19 @@ describe("check_drug_interaction", () => {
150
155
  expect(isToolFailure(result) && result.error).toContain("at least two");
151
156
  expect(label).not.toHaveBeenCalled();
152
157
  });
158
+
159
+ test("the schema itself accepts those names, which is why the body re-checks", async () => {
160
+ // The other half of the claim above, asked of the schema directly rather
161
+ // than through `~standard`: `min(2)` counts ENTRIES and `min(1)` counts
162
+ // CHARACTERS, so `" "` is a valid entry and the refusal is the body's.
163
+ expect(
164
+ await toolInputIssues(agentDef, "check_drug_interaction", { drugs: ["ibuprofen", " "] }),
165
+ ).toBeUndefined();
166
+ // And the schema is still doing its own half — one drug is not a check.
167
+ expect(
168
+ await toolInputIssues(agentDef, "check_drug_interaction", { drugs: ["ibuprofen"] }),
169
+ ).toBeDefined();
170
+ });
153
171
  });
154
172
 
155
173
  describe("fda.ts helpers", () => {
@@ -0,0 +1,158 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // An EVAL: does the game engine actually keep its world? Run it with `aai eval`.
4
+ //
5
+ // `agent.test.ts` drives the tools directly, one call at a time, against a
6
+ // context it made itself. This drives the AGENT: a real session, the real tool
7
+ // executor, the real event stream, and — the part no unit test can reach — MORE
8
+ // THAN ONE TURN through the same session slot. Everything worth asserting here
9
+ // is a claim about state that has to survive a turn boundary, or about the world
10
+ // really being replaced when the player asks to start over.
11
+ //
12
+ // Two modes, and `describeEval` announces which it picked:
13
+ //
14
+ // * with ASSEMBLYAI_API_KEY — a LIVE model. It really has to map "I pick up
15
+ // the rusted lantern" onto `game_state_take`, which is the behaviour the
16
+ // system prompt spends a whole section on.
17
+ // * without one — a SCRIPTED model, whose tool calls REALLY EXECUTE. The
18
+ // state changes for real, so a scripted take followed by a scripted read is
19
+ // a genuine two-turn state test with no model involved.
20
+ //
21
+ // What no eval here can see: anything below the audio boundary — endpointing,
22
+ // barge-in, two commands merging into one turn. Those need real paced audio.
23
+
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
+ /**
35
+ * The def a DEPLOYED agent runs, assembled the way the build assembles it: the
36
+ * authored export, plus what `tools/` declares, plus `system-prompt.md`.
37
+ *
38
+ * Both wrappers matter here in a way they do not in `agent.test.ts`. A tool
39
+ * missing from the registry is a tool the model cannot call, and the prompt is
40
+ * the only thing that tells a live model that "grab the rope" means
41
+ * `game_state_take` — an eval run against the framework default prompt measures
42
+ * an agent nobody deployed.
43
+ */
44
+ const agentDef = withSystemPrompt(
45
+ withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
46
+ systemPrompt,
47
+ );
48
+
49
+ /**
50
+ * What each of the three tools this file drives answers, off the wire.
51
+ *
52
+ * `tool.completed` carries a tool result as a JSON STRING, so a case either
53
+ * casts it or validates it — and a cast is silent exactly when a tool's result
54
+ * changed shape underneath the eval, which is the regression an eval exists to
55
+ * catch. `toolResultIn` takes a schema for that reason, and these name only the
56
+ * fields asserted below.
57
+ */
58
+ const Carried = z.object({ inventory: z.array(z.string()) });
59
+ const Restarted = z.object({ restarted: z.boolean() });
60
+ const Status = z.object({
61
+ inventory: z.array(z.string()),
62
+ score: z.number(),
63
+ moves: z.number(),
64
+ currentRoom: z.string(),
65
+ });
66
+
67
+ /**
68
+ * What the ONE call to `name` answered on this turn.
69
+ *
70
+ * `toolResultIn` is the SDK's reader: it throws rather than returning
71
+ * undefined, and names what the agent called instead — "it called a different
72
+ * tool" is the finding, and a case that read `undefined` off a missing call
73
+ * would assert against nothing. A turn that called it TWICE is refused too,
74
+ * rather than silently answered with the first.
75
+ */
76
+ const answerOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T =>
77
+ toolResultIn(turn.toolCalls, name, schema);
78
+
79
+ describeEval(agentDef, (test) => {
80
+ test(
81
+ "an item taken on one turn is still carried on the next",
82
+ async ({ session }) => {
83
+ const taken = await session.say("I pick up the rusted lantern.");
84
+ // The write really wrote. `game_state_take` is a `gameSlot.updateTool`,
85
+ // and it shipped once as the READING half — pushing to a deep-frozen
86
+ // array, which throws on every call. A tool that threw answers with an
87
+ // error here, not an inventory.
88
+ expect(answerOf(taken, "game_state_take", Carried).inventory).toContain("rusted lantern");
89
+
90
+ // A SECOND turn, which is the whole point: the slot is keyed per session,
91
+ // so this is the only place the take can be shown to have outlived the
92
+ // turn that made it.
93
+ const status = await session.say("Check the game state. What am I carrying?");
94
+ expect(answerOf(status, "game_state_get", Status).inventory).toContain("rusted lantern");
95
+ expect(status.completed).toBe(true);
96
+ },
97
+ {
98
+ stubReply: [
99
+ { tool: "game_state_take", args: { value: "rusted lantern" } },
100
+ "You lift the rusted lantern from its iron hook. It is heavier than it looks.",
101
+ { tool: "game_state_get" },
102
+ "You are carrying the rusted lantern, and nothing else.",
103
+ ],
104
+ },
105
+ );
106
+
107
+ test(
108
+ "starting over really empties the world",
109
+ async ({ session }) => {
110
+ const taken = await session.say("I pick up the rusted lantern.");
111
+ expect(answerOf(taken, "game_state_take", Carried).inventory).toContain("rusted lantern");
112
+
113
+ const again = await session.say("Forget all that. Start a new game from the beginning.");
114
+ expect(answerOf(again, "game_state_restart", Restarted).restarted).toBe(true);
115
+
116
+ // `game_state_restart` is the one tool that REPLACES the slot's value
117
+ // (`gameSlot.reset`), so what has to be checked is the state a LATER turn
118
+ // reads — a reset that only rebuilt the value it returned would satisfy
119
+ // the assertion above and leave the lantern in the player's hands.
120
+ const status = await session.say("Check the game state. What am I carrying, and where am I?");
121
+ const fresh = answerOf(status, "game_state_get", Status);
122
+ expect(fresh.inventory).toEqual([]);
123
+ expect(fresh.score).toBe(0);
124
+ expect(fresh.currentRoom).toBe(DEFAULT_GAME_STATE.currentRoom);
125
+ // ONE, not zero — and this is the assertion that proves the turn counter
126
+ // is the framework's. The reset emptied it, then the player said the line
127
+ // above, and the `user-transcript.committed` hook counted it before the
128
+ // narrator took its turn. No tool call is involved anywhere in that.
129
+ expect(fresh.moves).toBe(1);
130
+ },
131
+ {
132
+ stubReply: [
133
+ { tool: "game_state_take", args: { value: "rusted lantern" } },
134
+ "You lift the rusted lantern from its iron hook.",
135
+ { tool: "game_state_restart" },
136
+ "Very well. We begin again at the mouth of the cave.",
137
+ { tool: "game_state_get" },
138
+ "You carry nothing. Your score is zero, and you stand at the cave mouth.",
139
+ ],
140
+ },
141
+ );
142
+
143
+ test(
144
+ "a restart is narrated, not merely recorded",
145
+ async ({ session }) => {
146
+ // LIVE only: the claim is about what the narrator SAYS after the reset,
147
+ // and a scripted reply is a line this file wrote. The prompt asks for the
148
+ // opening scene again — a restart that answers "done" leaves a voice
149
+ // player with no idea where they are.
150
+ const again = await session.say("Start over. New game, please.");
151
+
152
+ expect(answerOf(again, "game_state_restart", Restarted).restarted).toBe(true);
153
+ expect(again.text).toMatch(/cave|cavern|forest|entrance|lantern/i);
154
+ expect(again.completed).toBe(true);
155
+ },
156
+ { live: true },
157
+ );
158
+ });
@@ -1,7 +1,6 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import type { ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
3
+ import { createToolContext, toolRunner, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
4
  import { describe, expect, test } from "vitest";
6
5
  import authoredAgent from "./agent.ts";
7
6
 
@@ -21,8 +20,22 @@ const agentDef = withDiscoveredTools(
21
20
  import { DEFAULT_GAME_STATE, gameSlot, MAX_HISTORY, REPORTED_HISTORY } from "./shared.ts";
22
21
 
23
22
  /** A tool by the name the model calls it by, bound to this agent. */
24
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
25
- runTool(agentDef, name, args, ctx);
23
+ const run = toolRunner(agentDef);
24
+
25
+ /**
26
+ * What the player said, delivered the way the RUNTIME delivers it.
27
+ *
28
+ * A session event handler is a plain function on the def, so a template can
29
+ * drive one with no harness — which is the point of asserting on it here rather
30
+ * than trusting the wiring: `moves` and `history` are now maintained by
31
+ * something the model never calls, so nothing else in this file would notice if
32
+ * the hook stopped running.
33
+ */
34
+ const say = (text: string, ctx: ReturnType<typeof makeCtx>) =>
35
+ agentDef.events?.["user-transcript.committed"]?.(
36
+ { type: "user-transcript.committed", text, meta: { id: "evt_1", at: 0 } },
37
+ ctx,
38
+ );
26
39
 
27
40
  /** Each context owns its OWN slot store, which is what makes two playthroughs
28
41
  * independent by construction. */
@@ -99,13 +112,15 @@ describe("the adventure's tools", () => {
99
112
  expect(nothing.inventory).toEqual(["rope"]);
100
113
  });
101
114
 
102
- test("move sets the room and counts the move", async () => {
115
+ test("move sets the room and reports the turn count without touching it", async () => {
103
116
  const ctx = makeCtx();
104
117
  const moved = (await run("game_state_move", { value: "Echo Chamber" }, ctx)) as {
105
118
  currentRoom: string;
106
119
  moves: number;
107
120
  };
108
- expect(moved).toEqual({ currentRoom: "Echo Chamber", moves: 1 });
121
+ // `moves` is 0 because nobody has SAID anything — see `recordTurn`. It is
122
+ // still reported, because it is what the narrator wants back.
123
+ expect(moved).toEqual({ currentRoom: "Echo Chamber", moves: 0 });
109
124
  expect(gameSlot.get(ctx).currentRoom).toBe("Echo Chamber");
110
125
  });
111
126
 
@@ -116,26 +131,42 @@ describe("the adventure's tools", () => {
116
131
  expect(total.score).toBe(15);
117
132
  });
118
133
 
119
- test("history logs the command, counts the move, and reports only the recent ones", async () => {
134
+ test("what the player SAYS logs the command and counts the turn", async () => {
120
135
  const ctx = makeCtx();
121
- for (let i = 1; i <= REPORTED_HISTORY + 2; i++) {
122
- await run("game_state_history", { value: `command ${i}` }, ctx);
123
- }
136
+ for (let i = 1; i <= REPORTED_HISTORY + 2; i++) say(`command ${i}`, ctx);
137
+ say("look", ctx);
124
138
 
125
- const last = (await run("game_state_history", { value: "look" }, ctx)) as {
139
+ const game = gameSlot.get(ctx);
140
+ expect(game.moves).toBe(REPORTED_HISTORY + 3);
141
+ expect(game.history.at(-1)).toBe("look");
142
+
143
+ // And the narrator reads it back through the ordinary state tool — the hook
144
+ // writes, the model reads, and the two never have to agree about who counts.
145
+ const read = (await run("game_state_get", {}, ctx)) as {
126
146
  moves: number;
127
147
  recentHistory: string[];
128
148
  };
129
- expect(last.moves).toBe(REPORTED_HISTORY + 3);
130
- expect(last.recentHistory).toHaveLength(REPORTED_HISTORY);
131
- expect(last.recentHistory.at(-1)).toBe("look");
149
+ expect(read.moves).toBe(REPORTED_HISTORY + 3);
150
+ expect(read.recentHistory).toHaveLength(REPORTED_HISTORY);
132
151
  });
133
152
 
134
- test("the history is capped, so a long playthrough does not grow without bound", async () => {
153
+ test("a turn is counted once, even when the narrator also moves the player", async () => {
135
154
  const ctx = makeCtx();
136
- for (let i = 0; i < MAX_HISTORY + 10; i++) {
137
- await run("game_state_history", { value: `command ${i}` }, ctx);
138
- }
155
+ say("go north", ctx);
156
+ await run("game_state_move", { value: "Echo Chamber" }, ctx);
157
+
158
+ // Both used to bump `moves`, so this turn scored 2 — and a turn where the
159
+ // narrator called neither scored 0. A move is a room change; a turn is the
160
+ // player saying something.
161
+ const game = gameSlot.get(ctx);
162
+ expect(game.moves).toBe(1);
163
+ expect(game.currentRoom).toBe("Echo Chamber");
164
+ });
165
+
166
+ test("the history is capped, so a long playthrough does not grow without bound", () => {
167
+ const ctx = makeCtx();
168
+ for (let i = 0; i < MAX_HISTORY + 10; i++) say(`command ${i}`, ctx);
169
+
139
170
  const game = gameSlot.get(ctx);
140
171
  expect(game.history).toHaveLength(MAX_HISTORY);
141
172
  // The cap drops the OLDEST — the newest command is the one a narrator needs.
@@ -149,15 +180,13 @@ describe("the adventure's tools", () => {
149
180
  await run("game_state_flag", { value: "gate_opened" }, ctx);
150
181
  await run("game_state_move", { value: "Echo Chamber" }, ctx);
151
182
  await run("game_state_score", { value: 7 }, ctx);
152
- for (let i = 0; i < REPORTED_HISTORY + 3; i++) {
153
- await run("game_state_history", { value: `command ${i}` }, ctx);
154
- }
183
+ for (let i = 0; i < REPORTED_HISTORY + 3; i++) say(`command ${i}`, ctx);
155
184
 
156
- expect(await run("game_state_get", {}, ctx)).toEqual({
185
+ expect(await run("game_state_get", ctx)).toEqual({
157
186
  currentRoom: "Echo Chamber",
158
187
  inventory: ["lantern"],
159
188
  score: 7,
160
- moves: REPORTED_HISTORY + 4,
189
+ moves: REPORTED_HISTORY + 3,
161
190
  flags: { gate_opened: true },
162
191
  recentHistory: Array.from({ length: REPORTED_HISTORY }, (_, i) => `command ${i + 3}`),
163
192
  });
@@ -169,7 +198,7 @@ describe("the adventure's tools", () => {
169
198
  await run("game_state_score", { value: 30 }, ctx);
170
199
  await run("game_state_move", { value: "Echo Chamber" }, ctx);
171
200
 
172
- const restarted = (await run("game_state_restart", {}, ctx)) as {
201
+ const restarted = (await run("game_state_restart", ctx)) as {
173
202
  restarted: boolean;
174
203
  currentRoom: string;
175
204
  };
@@ -1,4 +1,5 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
+ import { gameSlot, recordTurn } from "./shared.ts";
2
3
 
3
4
  export default agent({
4
5
  name: "Cavern Adventure",
@@ -11,4 +12,24 @@ export default agent({
11
12
  // (shared.ts) and the world map in system-prompt.md.
12
13
  greeting:
13
14
  "Welcome, adventurer. You are standing at the mouth of a weathered cave at the edge of a pine forest. A cold wind carries the smell of damp stone up from the darkness below. A rusted lantern hangs from an iron hook beside the entrance. What would you like to do?",
15
+ /**
16
+ * The turn counter and the command log are the FRAMEWORK's, not the model's.
17
+ *
18
+ * Both used to be a `game_state_history` tool the system prompt told the
19
+ * narrator to call on every turn, handing back the player's own words — which
20
+ * the runtime already had. A hook is strictly better on all three counts a
21
+ * template is meant to teach: it costs no model call, it cannot be forgotten,
22
+ * and it needs no prose in the prompt to enforce it.
23
+ *
24
+ * `.committed` rather than `.updated`: partials arrive several times per
25
+ * utterance and would count one sentence as a dozen turns.
26
+ *
27
+ * It writes and does not speak, which is the whole line a session event hook
28
+ * draws — nothing here can decide what the narrator says next. The narrator
29
+ * reads the result on its next `game_state_get`.
30
+ */
31
+ events: {
32
+ "user-transcript.committed": (event, ctx) =>
33
+ gameSlot.update(ctx, (game) => recordTurn(game, event.text)),
34
+ },
14
35
  });