@alexkroman1/aai-cli 6.11.0 → 7.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.
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +40 -19
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.test.ts +746 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/tools/answer_question.ts +17 -4
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.test.ts +64 -33
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /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
|
|
3
|
-
import {
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
▌
|
|
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
|
-
|
|
172
|
+
state === "listening"
|
|
83
173
|
? "Listening"
|
|
84
|
-
:
|
|
174
|
+
: state === "speaking"
|
|
85
175
|
? "Narrating"
|
|
86
|
-
:
|
|
176
|
+
: state === "thinking"
|
|
87
177
|
? "Thinking"
|
|
88
|
-
:
|
|
178
|
+
: state === "connecting"
|
|
89
179
|
? "Connecting"
|
|
90
|
-
:
|
|
180
|
+
: state === "ready"
|
|
91
181
|
? "Ready"
|
|
92
182
|
: "Idle";
|
|
93
183
|
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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: {
|
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
* `FatalError` guards are all testable.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
|
|
21
|
+
import { schemaInputIssues } from "@alexkroman1/aai/testing";
|
|
22
|
+
import {
|
|
23
|
+
installStubStepFetch,
|
|
24
|
+
installStubGateway as stubGateway,
|
|
25
|
+
} from "@alexkroman1/aai/testing/vitest";
|
|
26
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
24
27
|
import agentDef, { digest } from "./agent.ts";
|
|
25
28
|
import { extractText, extractTitle, fetchArticle, summarize } from "./workflows/digest.ts";
|
|
26
29
|
|
|
@@ -49,13 +52,16 @@ describe("the agent declares itself a workflow app", () => {
|
|
|
49
52
|
|
|
50
53
|
describe("the input schema", () => {
|
|
51
54
|
test("accepts a URL", async () => {
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
// `schemaInputIssues` rather than `digest.input?.["~standard"].validate`:
|
|
56
|
+
// the vendor interface is a wire contract, and `.validate` may answer
|
|
57
|
+
// synchronously or with a promise depending on it — the half a hand-rolled
|
|
58
|
+
// reach gets wrong, since a missing `await` leaves `.issues` undefined and
|
|
59
|
+
// the refusing test below then passes for the wrong reason.
|
|
60
|
+
expect(await schemaInputIssues(digest.input, { url: "https://example.com/a" })).toBeUndefined();
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
test("rejects a non-URL at the CALL SITE rather than three steps into a run", async () => {
|
|
57
|
-
|
|
58
|
-
expect(result?.issues).toBeDefined();
|
|
64
|
+
expect(await schemaInputIssues(digest.input, { url: "not a url" })).toBeDefined();
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
test("carries a description, which is what a rendered form labels the field with", () => {
|
|
@@ -111,28 +117,23 @@ describe("extractTitle", () => {
|
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
describe("fetchArticle", () => {
|
|
114
|
-
/** Unpublished between specs — a fetch left behind reaches the next file. */
|
|
115
|
-
let restore: (() => void) | undefined;
|
|
116
|
-
afterEach(() => {
|
|
117
|
-
restore?.();
|
|
118
|
-
restore = undefined;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
120
|
/**
|
|
122
121
|
* A page server answering `html` with `status`.
|
|
123
122
|
*
|
|
124
123
|
* Published into `stepFetch`'s own slot rather than over `globalThis.fetch`:
|
|
125
124
|
* the step calls `stepFetch`, and stubbing the global would pass while
|
|
126
125
|
* exercising the fallback path production never takes.
|
|
126
|
+
*
|
|
127
|
+
* `installStubStepFetch` unpublishes it on `onTestFinished`, which is why
|
|
128
|
+
* there is no `afterEach` here — a fetch left behind reaches the next file,
|
|
129
|
+
* and a hand-kept restore registry is the thing that forgets.
|
|
127
130
|
*/
|
|
128
131
|
function stubPage(html: string, status = 200) {
|
|
129
|
-
|
|
132
|
+
return installStubStepFetch(() => ({
|
|
130
133
|
status,
|
|
131
134
|
body: html,
|
|
132
135
|
headers: { "Content-Type": "text/html" },
|
|
133
136
|
}));
|
|
134
|
-
restore = stub.restore;
|
|
135
|
-
return stub;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
test("returns the page's title and its readable text", async () => {
|
|
@@ -157,10 +158,14 @@ describe("fetchArticle", () => {
|
|
|
157
158
|
});
|
|
158
159
|
|
|
159
160
|
test("fails FATALLY on a 404 and plainly on a 503", async () => {
|
|
161
|
+
// `stepFetchOk` builds the message, so it is the SDK's one spelling for a
|
|
162
|
+
// bad response — the REQUEST, the status, and a preview of any body.
|
|
160
163
|
stubPage("", 404);
|
|
161
|
-
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(
|
|
164
|
+
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(
|
|
165
|
+
/GET https:\/\/example\.com\/gone 404/,
|
|
166
|
+
);
|
|
162
167
|
stubPage("", 503);
|
|
163
|
-
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/
|
|
168
|
+
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/503/);
|
|
164
169
|
});
|
|
165
170
|
});
|
|
166
171
|
|