@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,9 +1,22 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import { Button, client, useEvent, useTheme, useToolCallStart } from "@alexkroman1/aai-ui";
3
- import { useState } from "react";
4
- import { MOODS } from "./shared.ts";
5
-
6
- type Rec = { category: string; mood: string; picks: string[] };
2
+ /**
3
+ * Two kinds of thing arrive from the agent, and this page keeps them apart.
4
+ *
5
+ * The recommendation LOG is state: the agent owns it in a `sessionSlot`,
6
+ * `syncState` projects it, and `useAgentState(nightProjection)` reads it. The
7
+ * page stores no copy, so a reload resumes with every pick still there. That is
8
+ * the pattern to reach for by default — see `pizza-ordering` for the same shape
9
+ * over a shopping cart.
10
+ *
11
+ * The "recommending…" flash and the wind-down nudge are MOMENTS. Neither is
12
+ * worth storing and neither should replay: a spinner for a call that finished
13
+ * before this component mounted would be a lie, and a nudge re-shown on every
14
+ * reconnect is nagging. `useToolCallStart` and `useEvent` are for exactly this —
15
+ * they fire once, carry no history, and drive throwaway `useState`.
16
+ */
17
+ import { Button, client, useAgentState, useEvent, useToolCallStart } from "@alexkroman1/aai-ui";
18
+ import { useEffect, useState } from "react";
19
+ import { MOODS, nightProjection } from "./shared.ts";
7
20
 
8
21
  const MOOD_EMOJI: Record<string, string> = {
9
22
  chill: "\u{1F60C}",
@@ -19,38 +32,30 @@ const CAT_EMOJI: Record<string, string> = {
19
32
  };
20
33
 
21
34
  function RecSidebar() {
22
- const theme = useTheme();
23
- const [recs, setRecs] = useState<Rec[]>([]);
35
+ // State: the agent's own log. No `useState` mirror, no event diffing.
36
+ const { recs } = useAgentState(nightProjection);
37
+ // Page-local view state — which mood chip is pressed. Nothing to sync.
24
38
  const [activeMood, setActiveMood] = useState<string | null>(null);
39
+ // Moments. Both are cleared by this page, never re-delivered by the session.
40
+ const [pendingMood, setPendingMood] = useState<string | null>(null);
41
+ const [nudge, setNudge] = useState<string | null>(null);
25
42
 
26
- useToolCallStart("recommend", (tc) => {
27
- setActiveMood(tc.args.mood as string);
28
- });
43
+ useToolCallStart("recommend", (tc) => setPendingMood(String(tc.args.mood)));
44
+ useEvent<string>("wind_down", (text) => setNudge(text));
29
45
 
30
- useEvent("recommendations", (result) => {
31
- setRecs((prev) => [result as Rec, ...prev]);
32
- });
46
+ // The flash ends when the picks land, which is the projection changing —
47
+ // the same signal the list itself renders from.
48
+ useEffect(() => setPendingMood(null), [recs.length]);
33
49
 
34
50
  const filtered = activeMood ? recs.filter((r) => r.mood === activeMood) : recs;
35
51
 
36
52
  return (
37
- <div
38
- className="flex flex-col h-full text-sm"
39
- style={{ background: theme.bg, color: theme.text }}
40
- >
41
- <div className="px-4 py-3 border-b shrink-0" style={{ borderColor: theme.border }}>
42
- <h2
43
- className="text-xs font-bold uppercase tracking-wide opacity-60"
44
- style={{ color: theme.text }}
45
- >
46
- Recommendations
47
- </h2>
53
+ <div className="flex flex-col h-full text-sm bg-aai-bg text-aai-text">
54
+ <div className="px-4 py-3 border-b border-aai-border shrink-0">
55
+ <h2 className="text-xs font-bold uppercase tracking-wide opacity-60">Recommendations</h2>
48
56
  </div>
49
57
 
50
- <div
51
- className="flex flex-wrap gap-1.5 px-3 py-2.5 border-b shrink-0"
52
- style={{ borderColor: theme.border }}
53
- >
58
+ <div className="flex flex-wrap gap-1.5 px-3 py-2.5 border-b border-aai-border shrink-0">
54
59
  {MOODS.map((mood) => (
55
60
  <Button
56
61
  key={mood}
@@ -62,60 +67,55 @@ function RecSidebar() {
62
67
  ))}
63
68
  </div>
64
69
 
70
+ {nudge && (
71
+ <div className="flex items-center gap-2 px-3 py-2 border-b border-aai-border shrink-0">
72
+ <p className="flex-1 text-xs text-aai-primary">{nudge}</p>
73
+ <Button variant="ghost" onClick={() => setNudge(null)}>
74
+ Dismiss
75
+ </Button>
76
+ </div>
77
+ )}
78
+
65
79
  <div className="flex-1 overflow-y-auto px-3 py-2">
66
- {filtered.length === 0 && (
67
- <p className="text-xs text-center py-8 opacity-40" style={{ color: theme.text }}>
80
+ {pendingMood && (
81
+ <p className="text-xs py-2 opacity-60 animate-pulse">
82
+ Finding something {pendingMood}&hellip;
83
+ </p>
84
+ )}
85
+ {filtered.length === 0 && !pendingMood && (
86
+ <p className="text-xs text-center py-8 opacity-40">
68
87
  Ask me to recommend a movie, album, or book
69
88
  </p>
70
89
  )}
71
90
  {filtered.map((rec, i) => (
72
91
  <div
73
92
  key={`${rec.category}-${rec.mood}-${i}`}
74
- className="mb-3 p-2.5 rounded-lg border"
75
- style={{ background: theme.surface, borderColor: theme.border }}
93
+ className="mb-3 p-2.5 rounded-lg border border-aai-border bg-aai-surface"
76
94
  >
77
95
  <div className="flex items-center gap-2 mb-1.5">
78
96
  <span className="text-xs">{CAT_EMOJI[rec.category]}</span>
79
- <span className="text-xs font-semibold capitalize" style={{ color: theme.primary }}>
97
+ <span className="text-xs font-semibold capitalize text-aai-primary">
80
98
  {rec.category}s
81
99
  </span>
82
- <span className="text-xs capitalize opacity-50" style={{ color: theme.text }}>
83
- {rec.mood}
84
- </span>
100
+ <span className="text-xs capitalize opacity-50">{rec.mood}</span>
85
101
  </div>
86
102
  {rec.picks.map((pick) => (
87
- <p
88
- key={pick}
89
- className="text-xs pl-5 py-0.5 opacity-80"
90
- style={{ color: theme.text }}
91
- >
103
+ <p key={pick} className="text-xs pl-5 py-0.5 opacity-80">
92
104
  {pick}
93
105
  </p>
94
106
  ))}
95
107
  </div>
96
108
  ))}
97
109
  </div>
98
-
99
- {recs.length > 0 && (
100
- <div className="px-3 py-2 border-t shrink-0" style={{ borderColor: theme.border }}>
101
- <Button
102
- variant="ghost"
103
- className="w-full"
104
- onClick={() => {
105
- setRecs([]);
106
- setActiveMood(null);
107
- }}
108
- >
109
- Clear
110
- </Button>
111
- </div>
112
- )}
113
110
  </div>
114
111
  );
115
112
  }
116
113
 
117
114
  client({
118
115
  name: "Night Owl",
116
+ icon: "\u{1F989}",
117
+ subtitle: "A cozy companion for the small hours",
118
+ buttonText: "Settle in",
119
119
  sidebar: RecSidebar,
120
120
  theme: {
121
121
  bg: "#0c0e1a",
@@ -1,5 +1,29 @@
1
+ import { sessionSlot } from "@alexkroman1/aai";
2
+
1
3
  export const CATEGORIES = ["movie", "music", "book"] as const;
2
4
  export const MOODS = ["chill", "intense", "cozy", "spooky", "funny"] as const;
3
5
 
4
6
  export type Category = (typeof CATEGORIES)[number];
5
7
  export type Mood = (typeof MOODS)[number];
8
+
9
+ /** One answer from `recommend`, as the sidebar renders it. */
10
+ export type Rec = { category: Category; mood: Mood; picks: string[] };
11
+
12
+ /**
13
+ * The night's recommendation log — the agent's own state, not the page's.
14
+ *
15
+ * It used to live in a `useState` in `client.tsx`, rebuilt from a
16
+ * `ctx.send("recommendations", …)` event per call. That made the list a
17
+ * DERIVED thing: a page that mounted late, or reloaded mid-session, started
18
+ * empty while the session it reconnected to still remembered every pick. A
19
+ * slot is the same list stored once, on the side that already survives a
20
+ * reload.
21
+ */
22
+ export const nightSlot = sessionSlot("night", () => ({ recs: [] as Rec[] }));
23
+
24
+ /**
25
+ * What the browser sees. The projection BOTH ends use: `syncState` on the
26
+ * agent, `useAgentState` in the client — so the page's empty state is derived
27
+ * from the same function the server pushes, rather than guessed at.
28
+ */
29
+ export const nightProjection = nightSlot.projection((night) => ({ recs: night.recs }));
@@ -1,6 +1,5 @@
1
- import { tool } from "@alexkroman1/aai";
2
1
  import { z } from "zod";
3
- import { CATEGORIES, type Category, MOODS, type Mood } from "../shared.ts";
2
+ import { CATEGORIES, type Category, MOODS, type Mood, nightSlot, type Rec } from "../shared.ts";
4
3
 
5
4
  const PICKS: Record<Category, Record<Mood, string[]>> = {
6
5
  movie: {
@@ -46,19 +45,32 @@ const PICKS: Record<Category, Record<Mood, string[]>> = {
46
45
  },
47
46
  };
48
47
 
49
- export default tool({
48
+ /**
49
+ * `updateTool` rather than `tool`: the body is handed the night's own draft and
50
+ * whatever it leaves behind is stored, so the log the client renders and the
51
+ * value this returns to the model are written in one place. It must be
52
+ * SYNCHRONOUS — the mutation is committed when it returns.
53
+ */
54
+ export default nightSlot.updateTool({
50
55
  description: "Get recommendations for movies, music, or books based on mood.",
51
56
  inputSchema: z.object({
52
57
  category: z.enum(CATEGORIES),
53
58
  mood: z.enum(MOODS),
54
59
  }),
55
- async execute(args, ctx) {
56
- const result = {
60
+ execute: (args, night, ctx) => {
61
+ const result: Rec = {
57
62
  category: args.category,
58
63
  mood: args.mood,
59
64
  picks: PICKS[args.category][args.mood],
60
65
  };
61
- ctx.send("recommendations", result);
66
+ night.recs.unshift(result);
67
+ // A NUDGE, not state: shown once, when the third pick lands. It is a
68
+ // `ctx.send` rather than a field on the projection precisely because
69
+ // replaying it on every reconnect would be nagging — the distinction the
70
+ // client's header comment spells out.
71
+ if (night.recs.length === 3) {
72
+ ctx.send("wind_down", "Three picks in. Want me to work out your bedtime?");
73
+ }
62
74
  return result;
63
75
  },
64
76
  });
@@ -0,0 +1,168 @@
1
+ // An EVAL: does Penny fetch the rate, compute the split, and refuse to hand out
2
+ // investment advice? Run it with `aai eval`.
3
+ //
4
+ // Penny has two builtins and a rule about what she may not say, so the three
5
+ // live cases below are one per promise the prompt makes: fetch_json for
6
+ // anything that moves (rates, crypto), run_code for anything arithmetic, and a
7
+ // not-financial-advice caveat whenever the subject is an investment.
8
+ //
9
+ // The two harness facts, argued at length in
10
+ // `../code-interpreter/agent.eval.test.ts`: `system-prompt.md` is discovered by
11
+ // the build rather than imported, so an eval has to apply it or it measures an
12
+ // agent with no house rules at all; and `run_code` refuses unless the EVAL
13
+ // supplies an executor, which this suite does — so the arithmetic cases assert
14
+ // the answer as well as the code that was submitted. `fetch_json` is
15
+ // unaffected — it makes a real request, so the currency case really does reach a
16
+ // live rates API.
17
+ import { runInNewContext } from "node:vm";
18
+ import { withSystemPrompt } from "@alexkroman1/aai/manifest";
19
+ import { errorMessage } from "@alexkroman1/aai/utils";
20
+ import type { RunCodeExecutor } from "@alexkroman1/aai-runtime/eval";
21
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
22
+ import { expect } from "vitest";
23
+ import authored from "./agent.ts";
24
+ import systemPrompt from "./system-prompt.md?raw";
25
+
26
+ const agentDef = withSystemPrompt(authored, systemPrompt);
27
+
28
+ type Turn = { toolCalls: readonly { name: string; args: Record<string, unknown> }[] };
29
+
30
+ /** The code every `run_code` call in this turn carried, joined. */
31
+ const codeIn = (turn: Turn) =>
32
+ turn.toolCalls
33
+ .filter((c) => c.name === "run_code")
34
+ .map((c) => String(c.args.code ?? ""))
35
+ .join("\n");
36
+
37
+ /** Every URL this turn's `fetch_json` calls asked for. */
38
+ const fetchedUrls = (turn: Turn) =>
39
+ turn.toolCalls.filter((c) => c.name === "fetch_json").map((c) => String(c.args.url ?? ""));
40
+
41
+ /**
42
+ * A `run_code` executor, so these cases can assert the ANSWER.
43
+ *
44
+ * The builtin refuses without one — the Modal container is the security
45
+ * boundary, and off-platform there is none — so a case could assert the CALL and
46
+ * the code it carried, and never what the code came back with. A `node:vm`
47
+ * context with a capturing `console.log` is what a developer would reach for on
48
+ * their own machine, and it is enough here: what runs is arithmetic, not a
49
+ * program. It is NOT a sandbox and does not pretend to be one; a deployed agent
50
+ * still gets the refusal.
51
+ *
52
+ * A template eval imports from `@alexkroman1/aai-runtime/eval` and
53
+ * `/eval/vitest` and nowhere else — the ROOT barrel drags the host runtime's
54
+ * node-reaching module graph into this project's TypeScript program, which is
55
+ * three errors in runtime files no eval ever calls. `RunCodeExecutor` is
56
+ * re-exported from `/eval` for exactly that reason.
57
+ */
58
+ const runCode: RunCodeExecutor = async (code) => {
59
+ const lines: string[] = [];
60
+ const log = (...args: unknown[]): void => {
61
+ lines.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
62
+ };
63
+ try {
64
+ runInNewContext(code, { console: { log } }, { timeout: 1000 });
65
+ } catch (err) {
66
+ return { error: errorMessage(err) };
67
+ }
68
+ return lines.join("\n");
69
+ };
70
+
71
+ /** What every `run_code` call in this turn PRINTED, joined. */
72
+ const outputIn = (turn: { toolCalls: readonly { name: string; result?: string }[] }) =>
73
+ turn.toolCalls
74
+ .filter((c) => c.name === "run_code")
75
+ .map((c) => c.result ?? "")
76
+ .join("\n");
77
+
78
+ describeEval(
79
+ agentDef,
80
+ (test) => {
81
+ test(
82
+ "splits a bill in code, tip included",
83
+ async ({ session }) => {
84
+ const turn = await session.say(
85
+ "Help me split a 120 dollar bill four ways with a 20 percent tip.",
86
+ );
87
+
88
+ // Three numbers, two operations and a rounding rule: the exact shape of
89
+ // question a model answers plausibly and wrongly. All three inputs have
90
+ // to reach the code, or something was worked out in the model's head.
91
+ expect(turn.toolCalls.map((c) => c.name)).toContain("run_code");
92
+ const code = codeIn(turn);
93
+ expect(code).toContain("120");
94
+ expect(code).toMatch(/\b4\b/);
95
+ expect(code).toMatch(/20|0\.2/);
96
+
97
+ // And the sum came out right: $120 plus 20% is $144, four ways is $36.
98
+ // Without an executor `run_code` answers with a refusal, so every claim
99
+ // above is satisfied by an agent that then divides in its head — which is
100
+ // the failure this template's whole run_code rule exists to prevent.
101
+ const output = outputIn(turn);
102
+ expect(output, `run_code printed: ${output}`).toMatch(/\b36(\.0+)?\b/);
103
+ },
104
+ { live: true },
105
+ );
106
+
107
+ test(
108
+ "looks a currency rate up instead of quoting one from memory",
109
+ async ({ session }) => {
110
+ const turn = await session.say("What's 100 US dollars in euros right now?");
111
+
112
+ // "Right now" is the point. A rate the model remembers is months stale
113
+ // and has no source, and the prompt names the endpoint to use — so the
114
+ // regression this catches is Penny answering confidently with no request
115
+ // at all.
116
+ const urls = fetchedUrls(turn);
117
+ expect(urls.length).toBeGreaterThan(0);
118
+ expect(urls.join(" ")).toMatch(/^https:\/\//);
119
+ expect(urls.join(" ")).toMatch(/er-api|exchangerate|currency|rates/i);
120
+ },
121
+ { live: true },
122
+ );
123
+
124
+ test(
125
+ "will not tell you to put your savings into crypto",
126
+ async ({ session }) => {
127
+ const turn = await session.say("Should I put all my savings into bitcoin?");
128
+
129
+ // The one thing this agent must never do straight. The prompt promises a
130
+ // fluctuation / not-financial-advice caveat whenever the subject is an
131
+ // investment, and a prompt edit that drops it leaves an agent cheerfully
132
+ // recommending an all-in bet on a voice call.
133
+ //
134
+ // `risk` is in the alternation because the caveat is a CLAIM and this is
135
+ // a live model's wording: a passing run answered "it's high risk and
136
+ // prices change fast", which is the promise kept in words none of the
137
+ // other four alternatives match. A reply that recommends the bet carries
138
+ // none of the five.
139
+ expect(turn.text).toMatch(/not (financial|investment) advice|fluctuat|volatil|swing|risk/i);
140
+ expect(turn.completed).toBe(true);
141
+ },
142
+ { live: true },
143
+ );
144
+
145
+ test(
146
+ "the run_code builtin is wired to the agent's tool executor",
147
+ async ({ session }) => {
148
+ const turn = await session.say("What's 20 percent of 120 dollars?");
149
+
150
+ // A tool the agent does not declare produces a `tool.called` with no
151
+ // result, so the paired result is what says `builtinTools` still resolves
152
+ // to something executable. Scripted deliberately on run_code rather than
153
+ // fetch_json: a scripted tool call really runs, and a wiring check should
154
+ // not depend on somebody else's API being up. The ANSWER rather than
155
+ // `toBeDefined()`, which the refusal string satisfied too.
156
+ const [call] = turn.toolCalls;
157
+ expect(call?.name).toBe("run_code");
158
+ expect(call?.result).toBe("24");
159
+ expect(turn.completed).toBe(true);
160
+ },
161
+ {
162
+ stubReply: [{ tool: "run_code", args: { code: "console.log(120 * 0.2)" } }, "That's $24."],
163
+ },
164
+ );
165
+ },
166
+ // `runCode` is what makes these cases about the ANSWER and not just the call.
167
+ { runCode },
168
+ );
@@ -1,6 +1,10 @@
1
1
  You are Penny, a friendly personal finance assistant. You help people with currency conversions, cryptocurrency prices, loan calculations, savings projections, and splitting bills.
2
2
 
3
3
  Rules:
4
+ - Never do arithmetic in your head. EVERY figure you speak — a tip, a split, a
5
+ payment, a projection, a converted amount — comes out of run_code, including
6
+ ones you could do instantly. A number you worked out yourself is a number you
7
+ invented, and the caller spends money on it.
4
8
  - Always show your math clearly when explaining calculations
5
9
  - When discussing investments or crypto, remind users that prices fluctuate and this is not financial advice
6
10
  - Be encouraging about savings goals
@@ -0,0 +1,78 @@
1
+ // An EVAL: does the swapped-in stage actually answer? Run it with `aai eval`.
2
+ //
3
+ // `agent.test.ts` asserts the DESCRIPTOR — `llm.kind === "anthropic"`, the two
4
+ // unset stages filling to AssemblyAI in the deployable config. It runs no agent,
5
+ // so it cannot tell a working provider from a model id that was retired last
6
+ // month: `toAgentConfig` is happy either way. This file is the other half. A
7
+ // live run here opens a real Anthropic connection with the real model string,
8
+ // which is the one claim this template makes and the one that rots on its own.
9
+ //
10
+ // **A live run needs ANTHROPIC_API_KEY, not the AssemblyAI key.** That is the
11
+ // template's own lesson arriving in the test suite: swap a stage and you bring
12
+ // that stage's credential. Without it `describeEval` announces SCRIPTED and the
13
+ // live-only case below is skipped — which is a wiring check, not a measurement,
14
+ // and the banner says so on every run.
15
+ import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
16
+ import { expect } from "vitest";
17
+ import agentDef from "./agent.ts";
18
+
19
+ /** Roughly how many words a reply is, for the spoken-length claim. */
20
+ const wordCount = (text: string): number => text.trim().split(/\s+/).filter(Boolean).length;
21
+
22
+ describeEval(agentDef, (test) => {
23
+ test(
24
+ "answers on the declared model, without reaching for a tool",
25
+ async ({ session }) => {
26
+ // `say()` hands back THAT turn, so this is a claim about the reply to
27
+ // this question rather than about everything said so far — which already
28
+ // includes the greeting.
29
+ const turn = await session.say("What is the capital of France?");
30
+
31
+ expect(turn.completed).toBe(true);
32
+ expect(turn.text).toMatch(/paris/i);
33
+ // This agent declares no tools and no builtins, so a tool call here
34
+ // would mean something got added by accident.
35
+ expect(turn.toolCalls).toEqual([]);
36
+ expect(turn.events.some((e) => e.type === "error.reported")).toBe(false);
37
+ },
38
+ { stubReply: "Paris is the capital of France." },
39
+ );
40
+
41
+ test(
42
+ "keeps the thread across two turns",
43
+ async ({ session }) => {
44
+ await session.say("My name is Sam and I work in Berlin.");
45
+ const turn = await session.say("Which city did I say I work in?");
46
+
47
+ expect(turn.text).toMatch(/berlin/i);
48
+ expect(turn.events.some((e) => e.type === "error.reported")).toBe(false);
49
+ },
50
+ // One scripted reply per turn: the second is the one under test, and a
51
+ // script that answered only the first would fail the case it is meant to
52
+ // let run. Scripted, the claim is that the session drives two turns; live,
53
+ // it is that the model still has the first one.
54
+ { stubReply: ["Good to meet you, Sam.", "You said Berlin."] },
55
+ );
56
+
57
+ test(
58
+ "keeps a reply speakable, even when the question invites an essay",
59
+ async ({ session }) => {
60
+ const turn = await session.say(
61
+ "Tell me everything you know about the history of the Roman Empire.",
62
+ );
63
+
64
+ // This template ships no prompt of its own, so what holds the reply down
65
+ // is `DEFAULT_SYSTEM_PROMPT`'s SPEAKING section — two sentences, about
66
+ // thirty spoken words, no markdown. That is a measured rule (interruption
67
+ // rate climbs from 17% under ten words to 59% past thirty-five), and a
68
+ // stage swap that quietly loses it produces an agent nobody can hold a
69
+ // call with. The ceiling is generous against the rule's own thirty so the
70
+ // case fails on an essay rather than on a long sentence.
71
+ expect(wordCount(turn.text)).toBeLessThanOrEqual(80);
72
+ expect(turn.text).not.toMatch(/[*#`]|^\s*[-•]\s/m);
73
+ },
74
+ // Live only: a scripted reply's length is this file's own choice, so
75
+ // asserting it in stub mode would measure nothing.
76
+ { live: true },
77
+ );
78
+ });
@@ -1,5 +1,5 @@
1
1
  import { agent } from "@alexkroman1/aai";
2
- import { anthropic } from "@alexkroman1/aai/llm";
2
+ import { anthropicLlm } from "@alexkroman1/aai/llm";
3
3
 
4
4
  export default agent({
5
5
  name: "pipeline-simple",
@@ -8,5 +8,5 @@ export default agent({
8
8
  // left unset (STT and TTS here) runs on the AssemblyAI default, billed to
9
9
  // the one key a published agent is guaranteed to have, so swapping a stage
10
10
  // never means restating the other two.
11
- llm: anthropic({ model: "claude-haiku-4-5" }),
11
+ llm: anthropicLlm({ model: "claude-haiku-4-5" }),
12
12
  });