@alexkroman1/aai-cli 6.10.1 → 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 +93 -14
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +13 -4
- package/dist/scaffold/vite.config.ts +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 +818 -0
- package/dist/templates/call-audit/agent.ts +171 -0
- package/dist/templates/call-audit/client.tsx +198 -0
- package/dist/templates/call-audit/workflows/audit.ts +298 -0
- package/dist/templates/call-audit/workflows/ingest.ts +251 -0
- package/dist/templates/call-audit/workflows/media.ts +637 -0
- package/dist/templates/call-audit/workflows/summarize.ts +201 -0
- package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
- 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 +107 -96
- 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 +48 -41
- 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 +314 -0
- package/dist/templates/spoken-summary/agent.ts +155 -0
- package/dist/templates/spoken-summary/client.tsx +185 -0
- package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
- package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
- 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 +246 -123
- package/dist/templates/transcription-workflow/agent.ts +21 -7
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
- package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
- package/dist/templates/transcription-workflow/workflows/wav.ts +31 -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/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,11 +1,12 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import type { AgentState,
|
|
2
|
+
import type { AgentState, ConversationItem } from "@alexkroman1/aai-ui";
|
|
3
3
|
import {
|
|
4
4
|
AutoScroll,
|
|
5
5
|
client,
|
|
6
6
|
useAgentState,
|
|
7
|
+
useConversation,
|
|
7
8
|
useSession,
|
|
8
|
-
|
|
9
|
+
useSessionSelector,
|
|
9
10
|
} from "@alexkroman1/aai-ui";
|
|
10
11
|
import type { DispatchState, IncidentSummary, Severity, Status } from "./shared.ts";
|
|
11
12
|
import { dashboardProjection } from "./shared.ts";
|
|
@@ -139,15 +140,242 @@ function IncidentCard({ inc }: { inc: IncidentSummary }) {
|
|
|
139
140
|
);
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
/** One radio line: a message bubble, or the tool call that came after it. */
|
|
144
|
+
function Row({ item }: { item: ConversationItem }) {
|
|
145
|
+
if (item.kind === "tool") {
|
|
146
|
+
const { name, status } = item.toolCall;
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
className="self-start rounded-md px-2.5 py-1.5 text-[11px] font-mono flex items-center gap-2"
|
|
150
|
+
style={{
|
|
151
|
+
background: "#111827",
|
|
152
|
+
border: "1px solid #1e293b",
|
|
153
|
+
color: status === "pending" ? "#eab308" : "#64748b",
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<span
|
|
157
|
+
className="w-1.5 h-1.5 rounded-full inline-block"
|
|
158
|
+
style={{
|
|
159
|
+
background: status === "pending" ? "#eab308" : "#22c55e",
|
|
160
|
+
animation: status === "pending" ? "dc-pulse 1s ease-in-out infinite" : "none",
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
{name}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const { role, content } = item.message;
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5"
|
|
171
|
+
style={{
|
|
172
|
+
lineHeight: 1.6,
|
|
173
|
+
alignSelf: role === "assistant" ? "flex-start" : "flex-end",
|
|
174
|
+
background: role === "assistant" ? "#1e293b" : "#172554",
|
|
175
|
+
animation: "dc-slide-in 0.2s ease-out",
|
|
176
|
+
borderLeft: role === "assistant" ? "3px solid #3b82f6" : "none",
|
|
177
|
+
borderRight: role !== "assistant" ? "3px solid #22d3ee" : "none",
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: "#64748b" }}>
|
|
181
|
+
{role === "assistant" ? "DISPATCH" : "OPERATOR"}
|
|
182
|
+
</div>
|
|
183
|
+
{content}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The radio log.
|
|
190
|
+
*
|
|
191
|
+
* `useConversation()` rather than a `session.messages.map(...)`: the messages
|
|
192
|
+
* were only half of it. This board runs eight tools and rendered NONE of them,
|
|
193
|
+
* because tool calls live in a second array this page never read — and it also
|
|
194
|
+
* dropped the streaming reply and the thinking indicator. The hook owns the
|
|
195
|
+
* interleave (a tool row follows its anchor message), the `null`-vs-`""`
|
|
196
|
+
* transcript distinction and the thinking-suppression rule; what stays here is
|
|
197
|
+
* the markup, which is the part this template exists to show.
|
|
198
|
+
*
|
|
199
|
+
* It also subscribes per FIELD, so the conversation re-renders at the
|
|
200
|
+
* conversation's rate. The whole-page `useSession()` this replaced re-rendered
|
|
201
|
+
* the incident board on every STT partial.
|
|
202
|
+
*/
|
|
203
|
+
function Conversation() {
|
|
204
|
+
const { items, streaming, transcript, thinking } = useConversation();
|
|
205
|
+
return (
|
|
206
|
+
<>
|
|
207
|
+
<AutoScroll
|
|
208
|
+
scrollClassName="dc-messages overflow-y-auto"
|
|
209
|
+
contentClassName="p-4 flex flex-col gap-2"
|
|
210
|
+
>
|
|
211
|
+
{items.length === 0 && !streaming && (
|
|
212
|
+
<div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
|
|
213
|
+
Dispatch Command Center standing by. Click START to begin operations.
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
{items.map((item) => (
|
|
217
|
+
<Row
|
|
218
|
+
key={item.kind === "message" ? `m${item.message.id}` : item.toolCall.callId}
|
|
219
|
+
item={item}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
{streaming !== null && (
|
|
223
|
+
<div
|
|
224
|
+
className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5 self-start"
|
|
225
|
+
style={{ lineHeight: 1.6, background: "#1e293b", borderLeft: "3px solid #3b82f6" }}
|
|
226
|
+
>
|
|
227
|
+
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: "#64748b" }}>
|
|
228
|
+
DISPATCH
|
|
229
|
+
</div>
|
|
230
|
+
{streaming}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
{thinking && (
|
|
234
|
+
<div className="self-start text-[11px] px-3.5" style={{ color: "#64748b" }}>
|
|
235
|
+
<span style={{ animation: "dc-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</AutoScroll>
|
|
239
|
+
|
|
240
|
+
{transcript.speaking && (
|
|
241
|
+
<div
|
|
242
|
+
className="flex items-center px-4 py-2 text-xs italic min-h-8"
|
|
243
|
+
style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
|
|
244
|
+
>
|
|
245
|
+
<span
|
|
246
|
+
className="w-2.5 h-2.5 rounded-full inline-block mr-2"
|
|
247
|
+
style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
|
|
248
|
+
/>
|
|
249
|
+
{transcript.text}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The live status readout, on its own subscription.
|
|
258
|
+
*
|
|
259
|
+
* `useSessionSelector` rather than a field off a whole-page `useSession()`: the
|
|
260
|
+
* header is the only thing here that cares about the session state, and reading
|
|
261
|
+
* it one level up dragged the incident board through every snapshot change.
|
|
262
|
+
*/
|
|
263
|
+
function StatusReadout() {
|
|
264
|
+
const state = useSessionSelector((s) => s.state);
|
|
265
|
+
return (
|
|
266
|
+
<>
|
|
267
|
+
<span
|
|
268
|
+
className="w-2.5 h-2.5 rounded-full inline-block"
|
|
269
|
+
style={{
|
|
270
|
+
background: STATE_COLORS[state],
|
|
271
|
+
animation:
|
|
272
|
+
state === "listening"
|
|
273
|
+
? "dc-pulse 1.5s ease-in-out infinite"
|
|
274
|
+
: state === "thinking"
|
|
275
|
+
? "dc-pulse 0.8s ease-in-out infinite"
|
|
276
|
+
: "none",
|
|
277
|
+
}}
|
|
278
|
+
title={state}
|
|
279
|
+
/>
|
|
280
|
+
<span className="text-[11px] font-normal normal-case" style={{ color: "#64748b" }}>
|
|
281
|
+
{state === "listening"
|
|
282
|
+
? "LISTENING"
|
|
283
|
+
: state === "thinking"
|
|
284
|
+
? "PROCESSING"
|
|
285
|
+
: state === "speaking"
|
|
286
|
+
? "TRANSMITTING"
|
|
287
|
+
: state.toUpperCase()}
|
|
288
|
+
</span>
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The fatal-error banner.
|
|
295
|
+
*
|
|
296
|
+
* `role="alert"` is the part that is easy to leave out and is load-bearing: per
|
|
297
|
+
* the `fatalError` latch in the session core this banner is the only remaining
|
|
298
|
+
* signal — the status eyebrow beside it goes back to reading like a live
|
|
299
|
+
* session — and a screen reader is never told an unannounced one appeared. It
|
|
300
|
+
* is what `ConsoleShell` carries, which this template's full-bleed two-pane
|
|
301
|
+
* board cannot use as its frame.
|
|
302
|
+
*/
|
|
303
|
+
function ErrorBanner() {
|
|
304
|
+
const error = useSessionSelector((s) => s.error);
|
|
305
|
+
if (!error) return null;
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
role="alert"
|
|
309
|
+
className="px-4 py-2 text-xs"
|
|
310
|
+
style={{ background: "#450a0a", color: "#fca5a5", borderTop: "1px solid #991b1b" }}
|
|
311
|
+
>
|
|
312
|
+
ERROR: {error.message} ({error.code})
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The shift controls. The one place a whole-session read is what is wanted:
|
|
318
|
+
* it needs `started`, `running` and three methods, and it is three buttons. */
|
|
319
|
+
function ShiftControls({ logged }: { logged: number }) {
|
|
143
320
|
const session = useSession();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
className="flex items-center gap-2.5 px-4 py-3"
|
|
324
|
+
style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
|
|
325
|
+
>
|
|
326
|
+
{!session.started ? (
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
330
|
+
style={{ background: "#2563eb" }}
|
|
331
|
+
onClick={() => session.start()}
|
|
332
|
+
>
|
|
333
|
+
Start Dispatch
|
|
334
|
+
</button>
|
|
335
|
+
) : (
|
|
336
|
+
<>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
|
|
340
|
+
style={{
|
|
341
|
+
background: session.running ? "#334155" : "#2563eb",
|
|
342
|
+
color: session.running ? "#e2e8f0" : "white",
|
|
343
|
+
}}
|
|
344
|
+
onClick={() => session.toggle()}
|
|
345
|
+
>
|
|
346
|
+
{session.running ? "Pause" : "Resume"}
|
|
347
|
+
</button>
|
|
348
|
+
{/* end() hangs up and flips `started` back, so the UI
|
|
349
|
+
returns to "Start Dispatch" and the next start is a
|
|
350
|
+
brand-new shift (fresh incident board, greeting
|
|
351
|
+
included). reset() would keep the call live — the
|
|
352
|
+
buttons never toggle back. */}
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
356
|
+
style={{ background: "#dc2626" }}
|
|
357
|
+
onClick={() => session.end()}
|
|
358
|
+
>
|
|
359
|
+
End Shift
|
|
360
|
+
</button>
|
|
361
|
+
</>
|
|
362
|
+
)}
|
|
363
|
+
<div className="flex-1" />
|
|
364
|
+
<span className="text-[10px]" style={{ color: "#475569" }}>
|
|
365
|
+
{logged} incident{logged !== 1 ? "s" : ""} logged
|
|
366
|
+
</span>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function App() {
|
|
148
372
|
// The agent's own board, projected by `syncState`. This replaced a
|
|
149
373
|
// useState mirror that merged incident deltas out of tool events — the
|
|
150
374
|
// projection is already the complete list, so there is nothing to merge.
|
|
375
|
+
//
|
|
376
|
+
// It is the ONLY subscription at this level: the session reads that used to
|
|
377
|
+
// sit beside it moved into the four components above, so a partial transcript
|
|
378
|
+
// no longer re-renders the incident cards.
|
|
151
379
|
const dash = useAgentState(dashboardProjection);
|
|
152
380
|
|
|
153
381
|
const incidentList = [...dash.incidents].reverse();
|
|
@@ -179,28 +407,7 @@ function App() {
|
|
|
179
407
|
>
|
|
180
408
|
<span style={{ color: "#3b82f6" }}>◆</span>
|
|
181
409
|
Dispatch Command Center
|
|
182
|
-
<
|
|
183
|
-
className="w-2.5 h-2.5 rounded-full inline-block"
|
|
184
|
-
style={{
|
|
185
|
-
background: STATE_COLORS[session.state],
|
|
186
|
-
animation:
|
|
187
|
-
session.state === "listening"
|
|
188
|
-
? "dc-pulse 1.5s ease-in-out infinite"
|
|
189
|
-
: session.state === "thinking"
|
|
190
|
-
? "dc-pulse 0.8s ease-in-out infinite"
|
|
191
|
-
: "none",
|
|
192
|
-
}}
|
|
193
|
-
title={session.state}
|
|
194
|
-
/>
|
|
195
|
-
<span className="text-[11px] font-normal normal-case" style={{ color: "#64748b" }}>
|
|
196
|
-
{session.state === "listening"
|
|
197
|
-
? "LISTENING"
|
|
198
|
-
: session.state === "thinking"
|
|
199
|
-
? "PROCESSING"
|
|
200
|
-
: session.state === "speaking"
|
|
201
|
-
? "TRANSMITTING"
|
|
202
|
-
: session.state.toUpperCase()}
|
|
203
|
-
</span>
|
|
410
|
+
<StatusReadout />
|
|
204
411
|
</div>
|
|
205
412
|
<div className="flex gap-2 items-center">
|
|
206
413
|
<span className="text-[10px] tracking-wider" style={{ color: "#64748b" }}>
|
|
@@ -229,106 +436,9 @@ function App() {
|
|
|
229
436
|
className="flex flex-col overflow-hidden"
|
|
230
437
|
style={{ borderRight: "1px solid #1e293b" }}
|
|
231
438
|
>
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
>
|
|
236
|
-
{session.messages.length === 0 && (
|
|
237
|
-
<div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
|
|
238
|
-
Dispatch Command Center standing by. Click START to begin operations.
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
241
|
-
{session.messages.map((m: ChatMessage, i: number) => (
|
|
242
|
-
<div
|
|
243
|
-
key={i}
|
|
244
|
-
className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5"
|
|
245
|
-
style={{
|
|
246
|
-
lineHeight: 1.6,
|
|
247
|
-
alignSelf: m.role === "assistant" ? "flex-start" : "flex-end",
|
|
248
|
-
background: m.role === "assistant" ? "#1e293b" : "#172554",
|
|
249
|
-
animation: "dc-slide-in 0.2s ease-out",
|
|
250
|
-
borderLeft: m.role === "assistant" ? "3px solid #3b82f6" : "none",
|
|
251
|
-
borderRight: m.role !== "assistant" ? "3px solid #22d3ee" : "none",
|
|
252
|
-
}}
|
|
253
|
-
>
|
|
254
|
-
<div
|
|
255
|
-
className="text-[10px] uppercase tracking-wider mb-1"
|
|
256
|
-
style={{ color: "#64748b" }}
|
|
257
|
-
>
|
|
258
|
-
{m.role === "assistant" ? "DISPATCH" : "OPERATOR"}
|
|
259
|
-
</div>
|
|
260
|
-
{m.content}
|
|
261
|
-
</div>
|
|
262
|
-
))}
|
|
263
|
-
</AutoScroll>
|
|
264
|
-
|
|
265
|
-
{speaking && (
|
|
266
|
-
<div
|
|
267
|
-
className="flex items-center px-4 py-2 text-xs italic min-h-8"
|
|
268
|
-
style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
|
|
269
|
-
>
|
|
270
|
-
<span
|
|
271
|
-
className="w-2.5 h-2.5 rounded-full inline-block mr-2"
|
|
272
|
-
style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
|
|
273
|
-
/>
|
|
274
|
-
{userTranscript}
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
{session.error && (
|
|
278
|
-
<div
|
|
279
|
-
className="px-4 py-2 text-xs"
|
|
280
|
-
style={{ background: "#450a0a", color: "#fca5a5", borderTop: "1px solid #991b1b" }}
|
|
281
|
-
>
|
|
282
|
-
ERROR: {session.error.message} ({session.error.code})
|
|
283
|
-
</div>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
<div
|
|
287
|
-
className="flex items-center gap-2.5 px-4 py-3"
|
|
288
|
-
style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
|
|
289
|
-
>
|
|
290
|
-
{!session.started ? (
|
|
291
|
-
<button
|
|
292
|
-
type="button"
|
|
293
|
-
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
294
|
-
style={{ background: "#2563eb" }}
|
|
295
|
-
onClick={() => session.start()}
|
|
296
|
-
>
|
|
297
|
-
Start Dispatch
|
|
298
|
-
</button>
|
|
299
|
-
) : (
|
|
300
|
-
<>
|
|
301
|
-
<button
|
|
302
|
-
type="button"
|
|
303
|
-
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
|
|
304
|
-
style={{
|
|
305
|
-
background: session.running ? "#334155" : "#2563eb",
|
|
306
|
-
color: session.running ? "#e2e8f0" : "white",
|
|
307
|
-
}}
|
|
308
|
-
onClick={() => session.toggle()}
|
|
309
|
-
>
|
|
310
|
-
{session.running ? "Pause" : "Resume"}
|
|
311
|
-
</button>
|
|
312
|
-
{/* end() hangs up and flips `started` back, so the UI
|
|
313
|
-
returns to "Start Dispatch" and the next start is a
|
|
314
|
-
brand-new shift (fresh incident board, greeting
|
|
315
|
-
included). reset() would keep the call live — the
|
|
316
|
-
buttons never toggle back. */}
|
|
317
|
-
<button
|
|
318
|
-
type="button"
|
|
319
|
-
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
320
|
-
style={{ background: "#dc2626" }}
|
|
321
|
-
onClick={() => session.end()}
|
|
322
|
-
>
|
|
323
|
-
End Shift
|
|
324
|
-
</button>
|
|
325
|
-
</>
|
|
326
|
-
)}
|
|
327
|
-
<div className="flex-1" />
|
|
328
|
-
<span className="text-[10px]" style={{ color: "#475569" }}>
|
|
329
|
-
{incidentList.length} incident{incidentList.length !== 1 ? "s" : ""} logged
|
|
330
|
-
</span>
|
|
331
|
-
</div>
|
|
439
|
+
<Conversation />
|
|
440
|
+
<ErrorBanner />
|
|
441
|
+
<ShiftControls logged={incidentList.length} />
|
|
332
442
|
</div>
|
|
333
443
|
|
|
334
444
|
{/* Right: sidebar dashboard */}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
3
|
import type { DeepReadonly, ToolFailure } from "@alexkroman1/aai";
|
|
4
|
-
import { pushCapped, sessionSlot } from "@alexkroman1/aai";
|
|
4
|
+
import { dialog, pushCapped, sessionSlot } from "@alexkroman1/aai";
|
|
5
5
|
|
|
6
6
|
export const SEVERITIES = ["critical", "urgent", "moderate", "minor"] as const;
|
|
7
7
|
export type Severity = (typeof SEVERITIES)[number];
|
|
@@ -218,6 +218,104 @@ export const dispatchSlot = sessionSlot("dispatch", createDefaultState, {
|
|
|
218
218
|
/** The projection BOTH ends use: `syncState` on the agent, `useAgentState` in the client. */
|
|
219
219
|
export const dashboardProjection = dispatchSlot.projection(dashboardView);
|
|
220
220
|
|
|
221
|
+
// ─── The call in hand ────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Where the dispatcher is on the call in hand, as a plain state map.
|
|
225
|
+
*
|
|
226
|
+
* Declared as a {@link DialogSpec} — `{ initial, states }`, each state carrying
|
|
227
|
+
* its own `instruction` and `on` map — rather than an XState machine. This
|
|
228
|
+
* dialog used exactly the four things a spec can say, and the `setup({ types:
|
|
229
|
+
* {} as { events: … } })` block it used to carry restated the four event names
|
|
230
|
+
* already written in the `on` maps below. The instruction is a TYPED field now
|
|
231
|
+
* instead of an untyped `meta` bag, which matters because a misspelling there
|
|
232
|
+
* produced a refusal with no recovery text and no error anywhere.
|
|
233
|
+
*
|
|
234
|
+
* The system prompt used to carry this as prose — "incident_triage: After
|
|
235
|
+
* creating, assess severity", "Critical incidents get immediate dispatch",
|
|
236
|
+
* "Never leave a critical incident without at least one resource dispatched" —
|
|
237
|
+
* which is advice to a model rather than a property of the agent. Two things
|
|
238
|
+
* change by declaring it.
|
|
239
|
+
*
|
|
240
|
+
* **The gate is real.** Six mutating tools take an incident id and did their own
|
|
241
|
+
* "does this exist" check through {@link findIncident}, whose answer on an empty
|
|
242
|
+
* board is `Incident INC-0001 not found` — a data answer to a positional
|
|
243
|
+
* question, and the wrong sentence for a dispatcher who has logged nothing yet.
|
|
244
|
+
* `when: "working"` is that check now, and the SDK's refusal names where the
|
|
245
|
+
* shift actually is and quotes the state's instruction.
|
|
246
|
+
*
|
|
247
|
+
* **The instruction rides every result.** A flow tool answers with the position
|
|
248
|
+
* it landed in, so `incident_create`'s result now ends with "confirm the
|
|
249
|
+
* severity and type with incident_triage" and `incident_triage`'s with "assign
|
|
250
|
+
* units" — the tool-by-tool sequencing the prompt was spending paragraphs on,
|
|
251
|
+
* arriving in the last thing the model reads before it speaks.
|
|
252
|
+
*
|
|
253
|
+
* **What this position is NOT is per-incident.** A flow is bound to a session
|
|
254
|
+
* and holds one position, and this board holds many incidents at once — so
|
|
255
|
+
* `working.monitoring` means "the incident this dispatcher last touched has
|
|
256
|
+
* units on it", not "every incident does". Per-incident status stays where it
|
|
257
|
+
* belongs, on {@link Incident.status}, and the tools stay addressed by id. That
|
|
258
|
+
* is also why `working` is a PARENT state: what every gated tool actually needs
|
|
259
|
+
* is "something has been logged on this shift", and the three children exist to
|
|
260
|
+
* carry the instruction for the step in front of the dispatcher rather than to
|
|
261
|
+
* gate anything.
|
|
262
|
+
*
|
|
263
|
+
* `LOGGED` is accepted from every state because a new 911 call is always legal —
|
|
264
|
+
* the one transition that is not a progression.
|
|
265
|
+
*
|
|
266
|
+
* `as const` is what keeps the `on` keys literal, so `DialogEvent` synthesizes
|
|
267
|
+
* the event union from them and a misspelled `send` is a compile error.
|
|
268
|
+
*/
|
|
269
|
+
const callSpec = {
|
|
270
|
+
initial: "standby",
|
|
271
|
+
states: {
|
|
272
|
+
standby: {
|
|
273
|
+
instruction:
|
|
274
|
+
"Nothing is logged on this shift. Take the call — location first, then the " +
|
|
275
|
+
"nature of the emergency, then the caller's name and callback — and log it " +
|
|
276
|
+
"with incident_create.",
|
|
277
|
+
on: { LOGGED: "working" },
|
|
278
|
+
},
|
|
279
|
+
working: {
|
|
280
|
+
initial: "triaging",
|
|
281
|
+
// Declared on the PARENT so all three children inherit it, which is what
|
|
282
|
+
// makes a new call legal mid-incident without repeating the transition
|
|
283
|
+
// three times. The children re-target each other below.
|
|
284
|
+
on: { LOGGED: ".triaging" },
|
|
285
|
+
states: {
|
|
286
|
+
triaging: {
|
|
287
|
+
instruction:
|
|
288
|
+
"Logged, not yet triaged. Confirm or override the severity and type with " +
|
|
289
|
+
"incident_triage. For a critical call, dispatch first and triage alongside it.",
|
|
290
|
+
on: { TRIAGED: "dispatching", DISPATCHED: "monitoring", ESCALATED: "dispatching" },
|
|
291
|
+
},
|
|
292
|
+
dispatching: {
|
|
293
|
+
instruction:
|
|
294
|
+
"Triaged, nothing rolling. Assign units with resources_dispatch — never " +
|
|
295
|
+
"leave a critical incident without at least one.",
|
|
296
|
+
on: { DISPATCHED: "monitoring", TRIAGED: "dispatching", ESCALATED: "dispatching" },
|
|
297
|
+
},
|
|
298
|
+
monitoring: {
|
|
299
|
+
instruction:
|
|
300
|
+
"Units are assigned. Work it — status updates as they radio in, notes as " +
|
|
301
|
+
"the picture changes, escalate if it outgrows the response, and close it " +
|
|
302
|
+
"with incident_update_status.",
|
|
303
|
+
on: { TRIAGED: "dispatching", DISPATCHED: "monitoring", ESCALATED: "dispatching" },
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
} as const;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The flow. Its own slot key beside {@link dispatchSlot}: the flow holds the
|
|
312
|
+
* POSITION and the board holds the incidents, because the position is one fact
|
|
313
|
+
* about the conversation and the board is many facts about the world. One tool
|
|
314
|
+
* call moves both — every converted tool opens `dispatchSlot.update` inside its
|
|
315
|
+
* `execute` and lets the flow's own `send` follow it.
|
|
316
|
+
*/
|
|
317
|
+
export const callFlow = dialog("call", callSpec);
|
|
318
|
+
|
|
221
319
|
/**
|
|
222
320
|
* The board as a READ hands it out: deep-frozen, and typed to say so.
|
|
223
321
|
*
|
|
@@ -2,11 +2,13 @@ You are the AI-powered Emergency Dispatch Command Center. You coordinate emergen
|
|
|
2
2
|
|
|
3
3
|
Your role combines call-taker, dispatcher, and incident commander. You speak like an experienced dispatcher: calm, precise, and authoritative. Never panic. Use brevity codes and dispatch terminology naturally.
|
|
4
4
|
|
|
5
|
+
WHERE THE CALL IS: every incident and resource tool answers with the position the call is in and what that position expects next — read it. Those tools refuse outright until something has been logged, and the refusal tells you what to do first. Nothing here has to be remembered between turns.
|
|
6
|
+
|
|
5
7
|
Your tools:
|
|
6
8
|
|
|
7
9
|
INCIDENT MANAGEMENT:
|
|
8
10
|
- incident_create: Log a new incident. Ask for location first, then nature of emergency, then caller info. Speed matters for critical calls.
|
|
9
|
-
- incident_triage:
|
|
11
|
+
- incident_triage: Assess severity. The system recommends severity, type, and protocols. Review and confirm or override.
|
|
10
12
|
- incident_update_status: Move incidents through the workflow (en_route, on_scene, resolved, escalated).
|
|
11
13
|
- incident_get: Get details on a specific incident.
|
|
12
14
|
- incident_escalate: Escalate when an incident exceeds current capacity or severity increases.
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { dispatchSlot, findIncident, logEvent } from "../shared.ts";
|
|
3
|
+
import { callFlow, dispatchSlot, findIncident, logEvent } from "../shared.ts";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Gated on `working` — there is nothing to annotate before anything is logged. */
|
|
6
|
+
export default callFlow.tool({
|
|
6
7
|
description: "Add a situational update note to an incident's timeline.",
|
|
8
|
+
when: "working",
|
|
7
9
|
inputSchema: z.object({
|
|
8
10
|
incidentId: z.string().max(20).describe("The incident ID"),
|
|
9
11
|
note: z.string().max(1000).describe("The note to add"),
|
|
10
12
|
source: z.string().max(100).describe("Who reported this — unit callsign or caller").optional(),
|
|
11
13
|
}),
|
|
12
|
-
execute(args,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
execute: (args, ctx) =>
|
|
15
|
+
dispatchSlot.update(ctx, (state) => {
|
|
16
|
+
const inc = findIncident(state, args.incidentId);
|
|
17
|
+
if (isToolFailure(inc)) return inc;
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const entry = args.source ? `[${args.source}] ${args.note}` : args.note;
|
|
20
|
+
logEvent(inc, entry);
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
return {
|
|
23
|
+
incidentId: args.incidentId,
|
|
24
|
+
noteAdded: entry,
|
|
25
|
+
timelineEntries: inc.timeline.length,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
25
28
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
3
|
calculateTriageScore,
|
|
4
|
+
callFlow,
|
|
4
5
|
createIncident,
|
|
5
6
|
dispatchSlot,
|
|
6
7
|
getApplicableProtocols,
|
|
@@ -10,6 +11,18 @@ import {
|
|
|
10
11
|
resourceBrief,
|
|
11
12
|
} from "../shared.ts";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* **Not a `callFlow.tool`, deliberately.** A new 911 call is legal in every
|
|
16
|
+
* state, so a `when` listing all of them would be a gate that gates nothing —
|
|
17
|
+
* the same call `plan-and-execute`'s `start_plan` makes. It sends `LOGGED`
|
|
18
|
+
* itself, which is what `dialog.send` is public for, and reports the position it
|
|
19
|
+
* landed in so the model reads "confirm the severity and type" as part of this
|
|
20
|
+
* result rather than having to remember it from the prompt.
|
|
21
|
+
*
|
|
22
|
+
* The `send` sits INSIDE the board's mutation window, which is safe because it
|
|
23
|
+
* is a different slot: the re-entrancy guard is per slot, and neither window
|
|
24
|
+
* spans an await, so the whole call commits atomically.
|
|
25
|
+
*/
|
|
13
26
|
export default dispatchSlot.updateTool({
|
|
14
27
|
description: "Create a new incident from an incoming emergency call.",
|
|
15
28
|
inputSchema: z.object({
|
|
@@ -29,7 +42,7 @@ export default dispatchSlot.updateTool({
|
|
|
29
42
|
.describe("Known hazards: fire, chemical, electrical, structural, weapons")
|
|
30
43
|
.optional(),
|
|
31
44
|
}),
|
|
32
|
-
execute(args, state) {
|
|
45
|
+
execute(args, state, ctx) {
|
|
33
46
|
const recSeverity = recommendSeverity(args.description);
|
|
34
47
|
const recType = recommendType(args.description);
|
|
35
48
|
const triageScore = calculateTriageScore(
|
|
@@ -62,6 +75,11 @@ export default dispatchSlot.updateTool({
|
|
|
62
75
|
|
|
63
76
|
return {
|
|
64
77
|
incidentId: id,
|
|
78
|
+
// Spread the position rather than renaming its fields: this tool is
|
|
79
|
+
// ungated, so the `{ state, instruction }` a gated tool's result carries
|
|
80
|
+
// has to come from here — and under two different key sets the model
|
|
81
|
+
// reads its own position two different ways within one agent.
|
|
82
|
+
...callFlow.send(ctx, { type: "LOGGED" }),
|
|
65
83
|
recommendedSeverity: recSeverity,
|
|
66
84
|
recommendedType: recType,
|
|
67
85
|
triageScore,
|