@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,6 +1,14 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
3
- import { AutoScroll, client, useSession, useUserTranscript } from "@alexkroman1/aai-ui";
2
+ import type { AgentState } from "@alexkroman1/aai-ui";
3
+ import {
4
+ AutoScroll,
5
+ client,
6
+ useConversation,
7
+ useSession,
8
+ useSessionSelector,
9
+ useTheme,
10
+ } from "@alexkroman1/aai-ui";
11
+ import type { ReactNode } from "react";
4
12
 
5
13
  const CSS = `
6
14
  @keyframes ic-flicker {
@@ -71,89 +79,187 @@ const STATE_COLORS = {
71
79
  error: GREEN_DARK,
72
80
  } satisfies Record<AgentState, string>;
73
81
 
74
- function InfocomAdventure() {
75
- const session = useSession();
76
- // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
77
- // is silence and `""` is speech detected with no words back yet, and reading
78
- // them as one falsy value hides the indicator at the start of every turn.
79
- const { speaking, text: userTranscript } = useUserTranscript();
82
+ /**
83
+ * The exchange, in the CRT idiom.
84
+ *
85
+ * `useConversation()` rather than `session.messages`: the four decisions
86
+ * `<MessageList>` makes the message/tool-call interleave, the streaming
87
+ * narrator line, the transcript's `null`-vs-`""` distinction and the thinking
88
+ * rule — are the hook's now, so this renders them instead of shipping a
89
+ * conversation missing all four. The old list read `messages` alone, so the
90
+ * eight `game_state_*` calls and every partial of the narrator's reply were
91
+ * invisible: the screen sat still until a whole utterance finalized.
92
+ *
93
+ * It also subscribes per FIELD, so a partial transcript no longer re-renders
94
+ * the status bar, the footer and the CRT overlays with it.
95
+ */
96
+ function Transcript() {
97
+ const { items, streaming, transcript, thinking } = useConversation();
98
+ // The one theme read left in this file, and the case `useTheme()` is still
99
+ // for: `scrollbar-color` takes TWO values and has no utility class, so it has
100
+ // to be a style — and reading it off the theme beats re-pinning the two hex
101
+ // codes the `client({ theme })` block below already declares.
102
+ const theme = useTheme();
103
+
104
+ return (
105
+ <AutoScroll
106
+ scrollClassName="ic-messages overflow-y-auto"
107
+ contentClassName="p-5"
108
+ style={{ scrollbarWidth: "thin", scrollbarColor: `${theme.primary} ${theme.surface}` }}
109
+ >
110
+ {items.map((item) =>
111
+ item.kind === "message" ? (
112
+ <div
113
+ key={item.message.id}
114
+ className={`mb-4 ${item.message.role === "user" ? "ic-user-msg" : ""}`}
115
+ style={{
116
+ textShadow:
117
+ item.message.role === "user"
118
+ ? "0 0 5px rgba(0,204,255,0.3)"
119
+ : "0 0 5px rgba(0,255,65,0.3)",
120
+ color: item.message.role === "user" ? CYAN : GREEN,
121
+ }}
122
+ >
123
+ {item.message.content}
124
+ </div>
125
+ ) : (
126
+ // The game engine's own bookkeeping, in the idiom the machine would
127
+ // have printed it in. Dim, because it is beneath the narration and
128
+ // not instead of it.
129
+ <div key={item.toolCall.callId} className="mb-4 text-[13px]" style={{ color: GREEN_DIM }}>
130
+ {`[ ${item.toolCall.name.replace(/^game_state_/, "").replace(/_/g, " ")}${
131
+ item.toolCall.status === "pending" ? " …" : ""
132
+ } ]`}
133
+ </div>
134
+ ),
135
+ )}
136
+ {streaming !== null && (
137
+ <div className="mb-4" style={{ color: GREEN, textShadow: "0 0 5px rgba(0,255,65,0.3)" }}>
138
+ {streaming}
139
+ </div>
140
+ )}
141
+ {transcript.speaking && (
142
+ <div
143
+ className="ic-transcript italic"
144
+ style={{ color: "#007a1e", textShadow: "0 0 5px rgba(0,255,65,0.15)" }}
145
+ >
146
+ {transcript.text}
147
+ </div>
148
+ )}
149
+ {thinking && (
150
+ <div className="animate-pulse" style={{ color: GREEN_DIM }}>
151
+ &#9612;
152
+ </div>
153
+ )}
154
+ </AutoScroll>
155
+ );
156
+ }
80
157
 
158
+ /** The turn counter — a NUMBER out of the selector, so a new message array with
159
+ * the same user-message count re-renders nothing. */
160
+ function TurnCount() {
161
+ const turns = useSessionSelector(
162
+ (snapshot) => snapshot.messages.filter((message) => message.role === "user").length,
163
+ );
164
+ return <span>Turns: {turns}</span>;
165
+ }
166
+
167
+ /** The live state dot and its label. */
168
+ function StatusDot() {
169
+ const state = useSessionSelector((snapshot) => snapshot.state);
170
+ const dotColor = STATE_COLORS[state];
81
171
  const stateLabel =
82
- session.state === "listening"
172
+ state === "listening"
83
173
  ? "Listening"
84
- : session.state === "speaking"
174
+ : state === "speaking"
85
175
  ? "Narrating"
86
- : session.state === "thinking"
176
+ : state === "thinking"
87
177
  ? "Thinking"
88
- : session.state === "connecting"
178
+ : state === "connecting"
89
179
  ? "Connecting"
90
- : session.state === "ready"
180
+ : state === "ready"
91
181
  ? "Ready"
92
182
  : "Idle";
93
183
 
94
- const msgCount = session.messages.filter((m: ChatMessage) => m.role === "user").length;
184
+ return (
185
+ <div
186
+ className="flex items-center gap-2.5 text-xs uppercase tracking-wider"
187
+ style={{ color: GREEN_DIM }}
188
+ >
189
+ <div
190
+ className="w-2 h-2 rounded-full"
191
+ style={{
192
+ background: dotColor,
193
+ boxShadow: dotColor !== GREEN_DARK ? `0 0 6px ${dotColor}` : "none",
194
+ }}
195
+ />
196
+ <span>{stateLabel}</span>
197
+ </div>
198
+ );
199
+ }
95
200
 
96
- const dotColor = STATE_COLORS[session.state];
201
+ /**
202
+ * The fault line.
203
+ *
204
+ * `role="alert"` for the reason `ConsoleShell` gives: once `session-core`
205
+ * latches a fatal error the state eyebrow goes back to reading like a live
206
+ * session, so this banner is the only remaining signal — and a screen reader is
207
+ * never told an unannounced one appeared.
208
+ */
209
+ function ErrorBanner() {
210
+ const error = useSessionSelector((snapshot) => snapshot.error);
211
+ if (!error) return null;
212
+ return (
213
+ <div
214
+ role="alert"
215
+ className="px-5 py-2 text-xs"
216
+ style={{ background: "#3a0000", color: "#ff4141" }}
217
+ >
218
+ ERROR: {error.message}
219
+ </div>
220
+ );
221
+ }
97
222
 
98
- if (!session.started) {
99
- return (
100
- <>
101
- <style>{CSS}</style>
102
- <div
103
- className="ic-crt fixed inset-0 overflow-hidden"
104
- style={{
105
- background: CRT_BG,
106
- color: GREEN,
107
- fontFamily: "monospace",
108
- fontSize: "15px",
109
- lineHeight: 1.6,
110
- animation: "ic-flicker 4s infinite",
111
- }}
223
+ /** Pause/resume and hang-up. The only place a whole-session subscription is
224
+ * still needed: the ACTIONS live on `useSession()`, and `useSessionCore` — the
225
+ * narrow way `<Controls>` reaches them — is not on the public surface. */
226
+ function Footer() {
227
+ const session = useSession();
228
+ return (
229
+ <div
230
+ className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
231
+ style={{ borderTop: `1px solid ${GREEN_DARK}`, background: "#001100" }}
232
+ >
233
+ <StatusDot />
234
+ <div className="flex gap-2">
235
+ <button
236
+ type="button"
237
+ className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
238
+ style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
239
+ onClick={session.toggle}
112
240
  >
113
- <div
114
- className="flex flex-col items-center justify-center h-full text-center p-10"
115
- style={{ animation: "ic-boot 1.5s ease-out" }}
116
- >
117
- <div
118
- className="text-[11px] whitespace-pre mb-8"
119
- style={{ textShadow: "0 0 10px rgba(0,255,65,0.5)" }}
120
- >
121
- {ASCII_LOGO}
122
- </div>
123
- <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
124
- AN INTERACTIVE FICTION
125
- </div>
126
- <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
127
- In the style of the classic text adventures.
128
- </div>
129
- <div className="text-[13px] mt-4" style={{ color: GREEN }}>
130
- VOICE-ENABLED EDITION
131
- </div>
132
- <button
133
- type="button"
134
- className="mt-10 px-12 py-3.5 bg-transparent cursor-pointer uppercase tracking-[3px] font-mono text-base"
135
- style={{
136
- color: GREEN,
137
- border: `1px solid ${GREEN}`,
138
- animation: "ic-pulse 2s ease-in-out infinite",
139
- }}
140
- onClick={session.start}
141
- >
142
- Begin Adventure
143
- </button>
144
- </div>
145
- <div
146
- className="fixed inset-0 pointer-events-none z-12"
147
- style={{
148
- background:
149
- "radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.4) 100%)",
150
- }}
151
- />
152
- </div>
153
- </>
154
- );
155
- }
241
+ {session.running ? "[P]ause" : "[R]esume"}
242
+ </button>
243
+ {/* end() hangs up and drops the sessionId, so the
244
+ session-scoped game state starts over and the title
245
+ screen returns. (session.reset() keeps the same
246
+ sessionId and would resume the old game.) */}
247
+ <button
248
+ type="button"
249
+ className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
250
+ style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
251
+ onClick={() => session.end()}
252
+ >
253
+ [N]ew Game
254
+ </button>
255
+ </div>
256
+ </div>
257
+ );
258
+ }
156
259
 
260
+ /** The CRT itself: the flicker, the scanlines and the vignette every screen
261
+ * sits inside. */
262
+ function Crt({ children }: { children: ReactNode }) {
157
263
  return (
158
264
  <>
159
265
  <style>{CSS}</style>
@@ -168,98 +274,7 @@ function InfocomAdventure() {
168
274
  animation: "ic-flicker 4s infinite",
169
275
  }}
170
276
  >
171
- <div className="flex flex-col h-full">
172
- {/* Status bar */}
173
- <div
174
- className="flex items-center justify-between px-5 py-2 text-[13px] font-bold tracking-wider shrink-0"
175
- style={{ background: GREEN, color: CRT_BG }}
176
- >
177
- <div className="flex gap-6">
178
- <span>CAVERN ADVENTURE</span>
179
- <span>Turns: {msgCount}</span>
180
- </div>
181
- <span>Voice Adventure</span>
182
- </div>
183
-
184
- {session.error && (
185
- <div className="px-5 py-2 text-xs" style={{ background: "#3a0000", color: "#ff4141" }}>
186
- ERROR: {session.error.message}
187
- </div>
188
- )}
189
-
190
- {/* Messages */}
191
- <AutoScroll
192
- scrollClassName="ic-messages overflow-y-auto"
193
- contentClassName="p-5"
194
- style={{ scrollbarWidth: "thin", scrollbarColor: `${GREEN} #001a00` }}
195
- >
196
- {session.messages.map((msg: ChatMessage, i: number) => (
197
- <div
198
- key={i}
199
- className={`mb-4 ${msg.role === "user" ? "ic-user-msg" : ""}`}
200
- style={{
201
- textShadow:
202
- msg.role === "user"
203
- ? "0 0 5px rgba(0,204,255,0.3)"
204
- : "0 0 5px rgba(0,255,65,0.3)",
205
- color: msg.role === "user" ? CYAN : GREEN,
206
- }}
207
- >
208
- {msg.content}
209
- </div>
210
- ))}
211
- {speaking && (
212
- <div
213
- className="ic-transcript italic"
214
- style={{ color: "#007a1e", textShadow: "0 0 5px rgba(0,255,65,0.15)" }}
215
- >
216
- {userTranscript}
217
- </div>
218
- )}
219
- </AutoScroll>
220
-
221
- {/* Footer controls */}
222
- <div
223
- className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
224
- style={{ borderTop: `1px solid ${GREEN_DARK}`, background: "#001100" }}
225
- >
226
- <div
227
- className="flex items-center gap-2.5 text-xs uppercase tracking-wider"
228
- style={{ color: GREEN_DIM }}
229
- >
230
- <div
231
- className="w-2 h-2 rounded-full"
232
- style={{
233
- background: dotColor,
234
- boxShadow: dotColor !== GREEN_DARK ? `0 0 6px ${dotColor}` : "none",
235
- }}
236
- />
237
- <span>{stateLabel}</span>
238
- </div>
239
- <div className="flex gap-2">
240
- <button
241
- type="button"
242
- className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
243
- style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
244
- onClick={session.toggle}
245
- >
246
- {session.running ? "[P]ause" : "[R]esume"}
247
- </button>
248
- {/* end() hangs up and drops the sessionId, so the
249
- session-scoped game state starts over and the title
250
- screen returns. (session.reset() keeps the same
251
- sessionId and would resume the old game.) */}
252
- <button
253
- type="button"
254
- className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
255
- style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
256
- onClick={() => session.end()}
257
- >
258
- [N]ew Game
259
- </button>
260
- </div>
261
- </div>
262
- </div>
277
+ {children}
263
278
  <div
264
279
  className="fixed inset-0 pointer-events-none z-12"
265
280
  style={{
@@ -271,6 +286,73 @@ function InfocomAdventure() {
271
286
  );
272
287
  }
273
288
 
289
+ function TitleScreen() {
290
+ const session = useSession();
291
+ return (
292
+ <Crt>
293
+ <div
294
+ className="flex flex-col items-center justify-center h-full text-center p-10"
295
+ style={{ animation: "ic-boot 1.5s ease-out" }}
296
+ >
297
+ <div
298
+ className="text-[11px] whitespace-pre mb-8"
299
+ style={{ textShadow: "0 0 10px rgba(0,255,65,0.5)" }}
300
+ >
301
+ {ASCII_LOGO}
302
+ </div>
303
+ <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
304
+ AN INTERACTIVE FICTION
305
+ </div>
306
+ <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
307
+ In the style of the classic text adventures.
308
+ </div>
309
+ <div className="text-[13px] mt-4" style={{ color: GREEN }}>
310
+ VOICE-ENABLED EDITION
311
+ </div>
312
+ <button
313
+ type="button"
314
+ className="mt-10 px-12 py-3.5 bg-transparent cursor-pointer uppercase tracking-[3px] font-mono text-base"
315
+ style={{
316
+ color: GREEN,
317
+ border: `1px solid ${GREEN}`,
318
+ animation: "ic-pulse 2s ease-in-out infinite",
319
+ }}
320
+ onClick={session.start}
321
+ >
322
+ Begin Adventure
323
+ </button>
324
+ </div>
325
+ </Crt>
326
+ );
327
+ }
328
+
329
+ function InfocomAdventure() {
330
+ const started = useSessionSelector((snapshot) => snapshot.started);
331
+ if (!started) return <TitleScreen />;
332
+
333
+ return (
334
+ <Crt>
335
+ <div className="flex flex-col h-full">
336
+ {/* Status bar */}
337
+ <div
338
+ className="flex items-center justify-between px-5 py-2 text-[13px] font-bold tracking-wider shrink-0"
339
+ style={{ background: GREEN, color: CRT_BG }}
340
+ >
341
+ <div className="flex gap-6">
342
+ <span>CAVERN ADVENTURE</span>
343
+ <TurnCount />
344
+ </div>
345
+ <span>Voice Adventure</span>
346
+ </div>
347
+
348
+ <ErrorBanner />
349
+ <Transcript />
350
+ <Footer />
351
+ </div>
352
+ </Crt>
353
+ );
354
+ }
355
+
274
356
  client({
275
357
  component: InfocomAdventure,
276
358
  theme: {
@@ -26,13 +26,7 @@ export const DEFAULT_GAME_STATE: GameState = {
26
26
  */
27
27
  export const MAX_HISTORY = 50;
28
28
 
29
- /**
30
- * How many of those commands a tool reports back to the model.
31
- *
32
- * Named because two tools answer with it (`game_state_get` and
33
- * `game_state_history`), and they live in separate files now — an inline `-5`
34
- * in each is a number that can disagree with itself.
35
- */
29
+ /** How many of those commands `game_state_get` reports back to the model. */
36
30
  export const REPORTED_HISTORY = 5;
37
31
 
38
32
  // The game lives in one `sessionSlot`, keyed per session — each session is its
@@ -52,7 +46,24 @@ export const REPORTED_HISTORY = 5;
52
46
  // `tool()`.
53
47
  export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_GAME_STATE));
54
48
 
55
- /** Log a player command, holding {@link MAX_HISTORY}. */
56
- export function recordCommand(game: GameState, command: string): void {
49
+ /**
50
+ * Log a player command and count the turn, holding {@link MAX_HISTORY}.
51
+ *
52
+ * **Nothing the MODEL can call runs this** — `agent.ts` declares it as a
53
+ * `user-transcript.committed` hook, so it runs once per thing the player says,
54
+ * whether or not the narrator cooperates. It replaced a `game_state_history`
55
+ * TOOL whose `value` argument was the player's own command: the framework
56
+ * already had the transcript, and the tool existed to hand it back. That cost a
57
+ * model call per turn and desynced `moves` and `history` from the game every
58
+ * time the model forgot the system prompt's instruction to call it.
59
+ *
60
+ * Which is also why `moves` is counted HERE and not in `game_state_move`. It
61
+ * used to be both, so a turn where the narrator moved the player AND logged the
62
+ * command counted twice, and a turn where it did neither counted nothing. A
63
+ * MOVE is a room change; a TURN is the player saying something, and only one of
64
+ * those is a thing the game can miscount.
65
+ */
66
+ export function recordTurn(game: GameState, command: string): void {
57
67
  pushCapped(game.history, command, MAX_HISTORY);
68
+ game.moves++;
58
69
  }
@@ -33,7 +33,7 @@ COMMAND INTERPRETATION:
33
33
  - "start over" / "new game" / "restart" = restart
34
34
  - Accept natural conversational commands and map them to game actions
35
35
 
36
- Use the game state tools to track inventory, location, score, and flags. Use game_state_get to read the current state, game_state_move to change rooms, game_state_take to pick up items, game_state_drop to drop items, game_state_score to add points, game_state_flag to set game flags, and game_state_history to log commands. When the player asks to restart, quit, or start a new game, call game_state_restart, then narrate the opening scene again. Always update state when the player takes an item, moves rooms, or triggers an event. Check state before responding to ensure consistency.
36
+ Use the game state tools to track inventory, location, score, and flags. Use game_state_get to read the current state, game_state_move to change rooms, game_state_take to pick up items, game_state_drop to drop items, game_state_score to add points, and game_state_flag to set game flags. You do not log commands or count moves — the game does that for you on every turn. When the player asks to restart, quit, or start a new game, call game_state_restart, then narrate the opening scene again. Always update state when the player takes an item, moves rooms, or triggers an event. Check state before responding to ensure consistency.
37
37
 
38
38
  ATMOSPHERE:
39
39
  - Underground areas should feel dark and foreboding with the lantern lit, and terrifying in pitch blackness
@@ -2,13 +2,14 @@ import { z } from "zod";
2
2
  import { gameSlot } from "../shared.ts";
3
3
 
4
4
  export default gameSlot.updateTool({
5
- description: "Move the player to a new room and increment the move counter.",
5
+ description: "Move the player to a new room.",
6
6
  inputSchema: z.object({
7
7
  value: z.string().describe("Room name to move to"),
8
8
  }),
9
9
  execute(args, game) {
10
+ // `moves` is NOT bumped here: a turn is counted once, by the
11
+ // `user-transcript.committed` hook in `agent.ts`. See `recordTurn`.
10
12
  game.currentRoom = args.value;
11
- game.moves++;
12
13
  return { currentRoom: game.currentRoom, moves: game.moves };
13
14
  },
14
15
  });