@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
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
* every directive body obeys — replayed from the top, so no live handles and no
|
|
8
8
|
* undurable decisions; step arguments and return values are serialized, so pass
|
|
9
9
|
* an id and not a payload — and both hold here unchanged. What this template
|
|
10
|
-
* adds is the shape a real provider limit forces on a workflow, and it is
|
|
10
|
+
* adds is the shape a real provider limit forces on a workflow, and it is four
|
|
11
11
|
* steps in a straight line:
|
|
12
12
|
*
|
|
13
13
|
* ```text
|
|
14
|
+
* normalizeRecording one step → an upload id in a format that can be cut
|
|
14
15
|
* splitRecording one step → the format + a byte range per segment
|
|
15
16
|
* transcribeSegment N steps → one sync API request each, bounded
|
|
16
17
|
* mergeTranscript one step → the stitched transcript
|
|
17
18
|
* ```
|
|
18
19
|
*
|
|
20
|
+
* The first is the newest and the one a reader is least likely to expect, since
|
|
21
|
+
* everything below it is arithmetic over a WAV and real recordings are not WAVs.
|
|
22
|
+
* `normalize.ts` is where ffmpeg enters, and its module doc carries why the
|
|
23
|
+
* conversion is file-to-file and why a temp file may not outlive its step.
|
|
24
|
+
*
|
|
19
25
|
* ## Why the SYNC endpoint, and why that forces a fan-out
|
|
20
26
|
*
|
|
21
27
|
* AssemblyAI has two pre-recorded APIs. The BATCH one takes a job and a webhook
|
|
@@ -35,7 +41,7 @@
|
|
|
35
41
|
* ## Three properties this leans on
|
|
36
42
|
*
|
|
37
43
|
* - **A step can read the agent's env now.** `stepEnv`/`requireStepEnv`
|
|
38
|
-
* (`@alexkroman1/aai/
|
|
44
|
+
* (`@alexkroman1/aai/step`) is what makes any of this real: a step is
|
|
39
45
|
* dispatched separately from the agent bundle and is handed no `ToolContext`,
|
|
40
46
|
* so before that seam existed no step anywhere could authenticate an outbound
|
|
41
47
|
* call, and every workflow template's I/O was a fixture saying so.
|
|
@@ -57,18 +63,22 @@
|
|
|
57
63
|
* `output` exists only when the last segment does.
|
|
58
64
|
*/
|
|
59
65
|
|
|
60
|
-
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
61
|
-
import { emit, mapConcurrent, readUpload, report, uploadInfo } from "@alexkroman1/aai/utils";
|
|
62
66
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
emit,
|
|
68
|
+
encodeWav,
|
|
69
|
+
mapConcurrent,
|
|
70
|
+
readUpload,
|
|
71
|
+
report,
|
|
72
|
+
uploadInfo,
|
|
73
|
+
} from "@alexkroman1/aai/step";
|
|
74
|
+
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
75
|
+
import { countWords, formatDuration, plural } from "@alexkroman1/aai/utils";
|
|
76
|
+
import { normalizeRecording } from "./normalize.ts";
|
|
77
|
+
import { stitchTranscript, TRANSCRIPT_STREAM, type TranscriptChunk } from "./stitch.ts";
|
|
69
78
|
import { elapsed, timed, transcribeWav } from "./sync-api.ts";
|
|
70
79
|
import {
|
|
71
80
|
bytesPerSecond,
|
|
81
|
+
HEADER_PROBE_BYTES,
|
|
72
82
|
parseWav,
|
|
73
83
|
planSegments,
|
|
74
84
|
SEGMENT_OVERLAP_SECONDS,
|
|
@@ -76,7 +86,6 @@ import {
|
|
|
76
86
|
type Segment,
|
|
77
87
|
UnsupportedRecordingError,
|
|
78
88
|
type WavFormat,
|
|
79
|
-
wavWithHeader,
|
|
80
89
|
} from "./wav.ts";
|
|
81
90
|
|
|
82
91
|
/**
|
|
@@ -186,15 +195,6 @@ export function segmentConcurrency(format: WavFormat): number {
|
|
|
186
195
|
return Math.max(1, Math.min(MAX_SEGMENT_CONCURRENCY, Math.floor(BYTES_IN_FLIGHT / perSegment)));
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
/**
|
|
190
|
-
* Bytes probed for the WAV header.
|
|
191
|
-
*
|
|
192
|
-
* The canonical header is 44 bytes; a recorder that writes a `LIST` or `bext`
|
|
193
|
-
* chunk in front of the samples pushes the `data` chunk further out, and 64 KB
|
|
194
|
-
* covers every such file anyone has produced by accident.
|
|
195
|
-
*/
|
|
196
|
-
const HEADER_PROBE_BYTES = 64 * 1024;
|
|
197
|
-
|
|
198
198
|
/**
|
|
199
199
|
* What a finished run reports, whichever flow produced it.
|
|
200
200
|
*
|
|
@@ -234,7 +234,16 @@ export async function transcribeFlow(input: { recording: string }) {
|
|
|
234
234
|
// round trip instead of two before any audio is read. The ORDER is still a
|
|
235
235
|
// pure function of this line — the two calls go out synchronously, left to
|
|
236
236
|
// right — which is what a replay reproduces.
|
|
237
|
-
|
|
237
|
+
//
|
|
238
|
+
// The clock starts before the conversion rather than after it, because a
|
|
239
|
+
// reader comparing the three flows over one file is comparing what the desk
|
|
240
|
+
// COST them, and re-encoding an m4a is part of that.
|
|
241
|
+
const [startedAt, ready] = await Promise.all([startClock(), normalizeRecording(input.recording)]);
|
|
242
|
+
|
|
243
|
+
// `ready.recording` from here on, not `input.recording`: a converted file is a
|
|
244
|
+
// DIFFERENT upload, and cutting the original by offsets planned against the
|
|
245
|
+
// converted one is a fan-out of garbage that still reports success.
|
|
246
|
+
const plan = await splitRecording(ready.recording);
|
|
238
247
|
|
|
239
248
|
// One step per segment, bounded, in an order a replay reproduces exactly.
|
|
240
249
|
// A failed segment fails the RUN, deliberately: every sibling that finished is
|
|
@@ -242,11 +251,12 @@ export async function transcribeFlow(input: { recording: string }) {
|
|
|
242
251
|
// what is missing, where catching here to salvage a partial transcript would
|
|
243
252
|
// return a recording with a silent hole in it and report success.
|
|
244
253
|
const parts = await mapConcurrent(plan.segments, segmentConcurrency(plan.format), (segment) =>
|
|
245
|
-
transcribeSegment(
|
|
254
|
+
transcribeSegment(ready.recording, plan.format, segment),
|
|
246
255
|
);
|
|
247
256
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
257
|
+
// The ORIGINAL id, and only here: `mergeTranscript` uses it for the filename a
|
|
258
|
+
// reader sees, and `standup.m4a` is the recording they uploaded — where the
|
|
259
|
+
// converted copy is an artifact of how the desk works.
|
|
250
260
|
return await mergeTranscript(input.recording, plan.durationMs, parts, startedAt);
|
|
251
261
|
}
|
|
252
262
|
|
|
@@ -272,7 +282,7 @@ export async function splitRecording(uploadId: string): Promise<{
|
|
|
272
282
|
const durationMs = segments.at(-1)?.endMs ?? 0;
|
|
273
283
|
|
|
274
284
|
await report(
|
|
275
|
-
`Split ${
|
|
285
|
+
`Split ${formatDuration(durationMs)} of audio into ${segments.length} ${plural(segments.length, "segment")}.`,
|
|
276
286
|
);
|
|
277
287
|
return { format, segments, durationMs };
|
|
278
288
|
}
|
|
@@ -299,7 +309,7 @@ export async function transcribeSegment(
|
|
|
299
309
|
// calls together, so their lines interleave by completion — the page renders a
|
|
300
310
|
// log, not a sequence, and `segment.index` is what puts the TRANSCRIPT back in
|
|
301
311
|
// order.
|
|
302
|
-
await report(`Transcribing ${
|
|
312
|
+
await report(`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`);
|
|
303
313
|
|
|
304
314
|
// `[start, end)`, the same half-open pair `planSegments` produced — the store
|
|
305
315
|
// owns the conversion to HTTP's inclusive range, so there is no `- 1` here to
|
|
@@ -312,20 +322,25 @@ export async function transcribeSegment(
|
|
|
312
322
|
// answers better — and getting it wrong is a whole transcript in the wrong
|
|
313
323
|
// language. Add one back only for a desk that really knows.
|
|
314
324
|
//
|
|
315
|
-
// `
|
|
325
|
+
// `encodeWav` is what makes a WINDOW decodable: the endpoint decodes each
|
|
316
326
|
// request independently, so a slice of the middle of a recording is a headerless
|
|
317
327
|
// tail until one is put back on it. The streaming flow needs no equivalent — its
|
|
318
|
-
// parts were cut with a header each.
|
|
328
|
+
// parts were cut with a header each. The header is the SDK's rather than this
|
|
329
|
+
// template's: a `WavFormat` is structurally a `PcmFormat`, and 22 lines of
|
|
330
|
+
// `DataView` writes with a comment about which of the two declared lengths a
|
|
331
|
+
// decoder trusts is not a thing worth a second copy of.
|
|
319
332
|
const { value: text, ms } = await timed(() =>
|
|
320
333
|
transcribeWav(
|
|
321
|
-
|
|
334
|
+
encodeWav(audio.bytes, format),
|
|
322
335
|
`segment-${segment.index}.wav`,
|
|
323
|
-
`Segment ${segment.index} (${
|
|
336
|
+
`Segment ${segment.index} (${formatDuration(segment.startMs)})`,
|
|
324
337
|
),
|
|
325
338
|
);
|
|
326
339
|
// The LATENCY, which is what says whether the concurrency bound or the endpoint
|
|
327
340
|
// is the thing limiting the run — see `timed`'s doc.
|
|
328
|
-
await report(
|
|
341
|
+
await report(
|
|
342
|
+
`Transcribed ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)} in ${elapsed(ms)}.`,
|
|
343
|
+
);
|
|
329
344
|
// And the WORDS, into their own stream, which is what makes this run's answer
|
|
330
345
|
// streamable rather than only its narration: the page stitches whatever has
|
|
331
346
|
// arrived and renders the transcript growing, minutes before `output` exists.
|
|
@@ -363,7 +378,7 @@ export async function mergeTranscript(
|
|
|
363
378
|
): Promise<Transcript> {
|
|
364
379
|
"use step";
|
|
365
380
|
|
|
366
|
-
await report(`Stitching ${parts.length}
|
|
381
|
+
await report(`Stitching ${parts.length} ${plural(parts.length, "segment")} together.`);
|
|
367
382
|
|
|
368
383
|
// `mapConcurrent` resolves in ITEM order however the calls settled, so this is
|
|
369
384
|
// already ordered — sorted anyway, because the merge is where an ordering
|
|
@@ -408,12 +423,13 @@ export async function startClock(): Promise<number> {
|
|
|
408
423
|
return Date.now();
|
|
409
424
|
}
|
|
410
425
|
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
426
|
+
// Re-exported rather than re-declared: `stream.ts` and `batch.ts` already import
|
|
427
|
+
// these from this module, and the split that let the PAGE stitch a partial
|
|
428
|
+
// transcript should not ripple through every flow. `clock` and `countWords` used
|
|
429
|
+
// to be in this list and are `formatDuration`/`countWords` on
|
|
430
|
+
// `@alexkroman1/aai/utils` now — a run narrates itself and the page renders the
|
|
431
|
+
// same run, so those two were a private copy of a formatter the SDK ships.
|
|
414
432
|
export {
|
|
415
|
-
clock,
|
|
416
|
-
countWords,
|
|
417
433
|
stitchChunks,
|
|
418
434
|
stitchTranscript,
|
|
419
435
|
TRANSCRIPT_STREAM,
|
|
@@ -425,9 +441,8 @@ export {
|
|
|
425
441
|
/**
|
|
426
442
|
* Run a `wav.ts` helper, turning its "cannot cut this" into a terminal failure.
|
|
427
443
|
*
|
|
428
|
-
* Exported
|
|
429
|
-
*
|
|
430
|
-
* byte.
|
|
444
|
+
* Exported because `stream.ts` plans with the same `wav.ts` helpers and owes the
|
|
445
|
+
* same classification, and it had this byte for byte.
|
|
431
446
|
*/
|
|
432
447
|
export function fatalOnUnsupported<T>(read: () => T): T {
|
|
433
448
|
try {
|
|
@@ -26,6 +26,21 @@
|
|
|
26
26
|
* anything else by name rather than trying to be clever — an MP3 or an M4A frame
|
|
27
27
|
* boundary cannot be found by arithmetic, and finding it means shipping a
|
|
28
28
|
* decoder into a step.
|
|
29
|
+
*
|
|
30
|
+
* ## Who READS the refusals below
|
|
31
|
+
*
|
|
32
|
+
* {@link UnsupportedRecordingError} is raised from here and handled in two
|
|
33
|
+
* completely different ways, which is worth knowing before editing a message:
|
|
34
|
+
*
|
|
35
|
+
* - The plain `transcribe` flow CONVERTS rather than refusing. `normalize.ts`
|
|
36
|
+
* calls {@link parseWav} as a QUESTION — a throw is its signal to hand the file
|
|
37
|
+
* to ffmpeg — so on that path no message here reaches a person, and a file this
|
|
38
|
+
* module rejects for a huge {@link MAX_BYTES_PER_SECOND} is one that
|
|
39
|
+
* normalization fixes by downsampling.
|
|
40
|
+
* - `transcribeStream` still refuses, and has to: it cuts a recording while the
|
|
41
|
+
* bytes are still arriving, and a partial file is not something ffmpeg can
|
|
42
|
+
* transcode. There the sentences below are the whole of what a person is told,
|
|
43
|
+
* so they keep naming the `ffmpeg` line that fixes the file.
|
|
29
44
|
*/
|
|
30
45
|
|
|
31
46
|
/** What the sync endpoint will accept in one request. */
|
|
@@ -57,6 +72,22 @@ export const MAX_SEGMENT_BYTES = 40 * 1024 * 1024;
|
|
|
57
72
|
/** The sync endpoint refuses audio shorter than this. */
|
|
58
73
|
export const MIN_SEGMENT_MS = 80;
|
|
59
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Bytes probed for the WAV header.
|
|
77
|
+
*
|
|
78
|
+
* The canonical header is 44 bytes; a recorder that writes a `LIST` or `bext`
|
|
79
|
+
* chunk in front of the samples pushes the `data` chunk further out, and 64 KB
|
|
80
|
+
* covers every such file anyone has produced by accident.
|
|
81
|
+
*
|
|
82
|
+
* Declared HERE because three callers need the same window, and the third one is
|
|
83
|
+
* what made a shared constant the rule rather than a preference: `splitRecording`
|
|
84
|
+
* cuts on it, `planFromHead` in `stream.ts` cuts on it, and `normalize.ts` decides
|
|
85
|
+
* whether to CONVERT on it. Two of those probing a different amount than the third
|
|
86
|
+
* is a file the desk converts and then cannot cut, or refuses and then converts —
|
|
87
|
+
* both silent, and neither reproducible from any one module.
|
|
88
|
+
*/
|
|
89
|
+
export const HEADER_PROBE_BYTES = 64 * 1024;
|
|
90
|
+
|
|
60
91
|
/**
|
|
61
92
|
* The largest `bytesPerSecond` this desk will cut, and the reason it is checked
|
|
62
93
|
* at PARSE time rather than at cut time.
|
|
@@ -281,34 +312,3 @@ export function planSegments(format: WavFormat): Segment[] {
|
|
|
281
312
|
}
|
|
282
313
|
return segments;
|
|
283
314
|
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Put a canonical 44-byte WAV header in front of a slice of samples.
|
|
287
|
-
*
|
|
288
|
-
* A byte range of a WAV is raw PCM with no header, and the sync endpoint reads
|
|
289
|
-
* the rate and channel count out of one. Sending `audio/pcm` with a `config`
|
|
290
|
-
* part instead would work equally well; a header keeps the two parts of the
|
|
291
|
-
* request from being able to disagree.
|
|
292
|
-
*/
|
|
293
|
-
export function wavWithHeader(format: WavFormat, samples: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
294
|
-
const out = new Uint8Array(44 + samples.length);
|
|
295
|
-
const view = new DataView(out.buffer);
|
|
296
|
-
const write = (at: number, text: string) => {
|
|
297
|
-
for (let i = 0; i < text.length; i++) view.setUint8(at + i, text.charCodeAt(i));
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
write(0, "RIFF");
|
|
301
|
-
view.setUint32(4, 36 + samples.length, true);
|
|
302
|
-
write(8, "WAVEfmt ");
|
|
303
|
-
view.setUint32(16, 16, true); // `fmt ` payload length
|
|
304
|
-
view.setUint16(20, 1, true); // linear PCM
|
|
305
|
-
view.setUint16(22, format.channels, true);
|
|
306
|
-
view.setUint32(24, format.sampleRate, true);
|
|
307
|
-
view.setUint32(28, bytesPerSecond(format), true);
|
|
308
|
-
view.setUint16(32, blockAlign(format), true);
|
|
309
|
-
view.setUint16(34, format.bitsPerSample, true);
|
|
310
|
-
write(36, "data");
|
|
311
|
-
view.setUint32(40, samples.length, true);
|
|
312
|
-
out.set(samples, 44);
|
|
313
|
-
return out;
|
|
314
|
-
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
3
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createToolContext,
|
|
6
|
+
ok,
|
|
7
|
+
okPosition,
|
|
8
|
+
toolRunner,
|
|
9
|
+
withDiscoveredTools,
|
|
10
|
+
} from "@alexkroman1/aai/testing";
|
|
5
11
|
import { describe, expect, test } from "vitest";
|
|
6
12
|
import authoredAgent from "./agent.ts";
|
|
7
13
|
|
|
@@ -18,7 +24,14 @@ const agentDef = withDiscoveredTools(
|
|
|
18
24
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
19
25
|
);
|
|
20
26
|
|
|
21
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
activeAssistant,
|
|
29
|
+
FLIGHTS,
|
|
30
|
+
gateFlow,
|
|
31
|
+
tripProjection,
|
|
32
|
+
tripSlot,
|
|
33
|
+
tripView,
|
|
34
|
+
} from "./shared.ts";
|
|
22
35
|
|
|
23
36
|
// ─── Harness ─────────────────────────────────────────────────────────────────
|
|
24
37
|
|
|
@@ -26,11 +39,11 @@ import { activeAssistant, FLIGHTS, tripProjection, tripSlot, tripView } from "./
|
|
|
26
39
|
* below rests on. */
|
|
27
40
|
const makeCtx = (): ToolContext => createToolContext();
|
|
28
41
|
|
|
29
|
-
/** A tool by the name the model calls it by, bound to this agent. The lookup
|
|
30
|
-
*
|
|
31
|
-
* what is local is only which
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
/** A tool by the name the model calls it by, bound to this agent. The lookup,
|
|
43
|
+
* its "no such tool" message and the args-or-context shape are all
|
|
44
|
+
* `toolRunner`'s (`@alexkroman1/aai/testing`); what is local is only which
|
|
45
|
+
* agent it runs against. */
|
|
46
|
+
const run = toolRunner(agentDef);
|
|
34
47
|
|
|
35
48
|
/** The state a tool just mutated, read back the way `syncState` reads it. */
|
|
36
49
|
function stateOf(ctx: ToolContext) {
|
|
@@ -116,36 +129,47 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
116
129
|
const ctx = makeCtx();
|
|
117
130
|
await run("book_hotel", { hotelId: "H1", nights: 3 }, ctx);
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
// Staging moved the gate, in the same window it wrote `pending`.
|
|
133
|
+
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
134
|
+
|
|
135
|
+
// A gated tool answers the flow's POSITION wrapped around the body's own
|
|
136
|
+
// return value, so the applied sentence is under `result`. `okPosition`
|
|
137
|
+
// keeps the position and THROWS on a refusal, quoting it — where the cast
|
|
138
|
+
// it replaces read `undefined` off the failure and died three assertions
|
|
139
|
+
// later on a property of undefined.
|
|
140
|
+
const confirmed = okPosition<{ applied: string; reference: string }>(
|
|
141
|
+
await run("confirm_action", ctx),
|
|
142
|
+
);
|
|
143
|
+
expect(confirmed.result.applied).toContain("Harborview Suites");
|
|
124
144
|
// 3 nights at $265.
|
|
125
|
-
expect(
|
|
145
|
+
expect(confirmed.result.applied).toContain("$795");
|
|
146
|
+
// SETTLED: the gate re-armed as part of the same call.
|
|
147
|
+
expect(confirmed.state).toBe("browsing");
|
|
126
148
|
|
|
127
149
|
const state = stateOf(ctx);
|
|
128
150
|
expect(state.pending).toBeNull();
|
|
129
151
|
expect(state.bookings).toHaveLength(1);
|
|
130
|
-
expect(state.bookings[0]?.reference).toBe(
|
|
152
|
+
expect(state.bookings[0]?.reference).toBe(confirmed.result.reference);
|
|
131
153
|
|
|
132
|
-
// The gate re-arms
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
// The gate re-arms, and now REFUSES rather than reporting an empty apply —
|
|
155
|
+
// `when: "awaitingConfirmation"` is what a second confirm meets.
|
|
156
|
+
expect(await run("confirm_action", ctx)).toMatchObject({
|
|
157
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
135
158
|
});
|
|
136
159
|
});
|
|
137
160
|
|
|
138
161
|
test("cancel_action drops the staged change and leaves the booking alone", async () => {
|
|
139
162
|
const ctx = makeCtx();
|
|
140
163
|
await run("update_ticket", { flightId: "LX54" }, ctx);
|
|
141
|
-
const dropped = (await run("cancel_action",
|
|
142
|
-
expect(dropped.discarded).toContain("LX54");
|
|
164
|
+
const dropped = okPosition<{ discarded: string }>(await run("cancel_action", ctx));
|
|
165
|
+
expect(dropped.result.discarded).toContain("LX54");
|
|
166
|
+
expect(dropped.state).toBe("browsing");
|
|
143
167
|
|
|
144
168
|
const state = stateOf(ctx);
|
|
145
169
|
expect(state.pending).toBeNull();
|
|
146
170
|
expect(state.ticket?.flightId).toBe("LX40");
|
|
147
|
-
expect(await run("cancel_action",
|
|
148
|
-
|
|
171
|
+
expect(await run("cancel_action", ctx)).toMatchObject({
|
|
172
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
149
173
|
});
|
|
150
174
|
});
|
|
151
175
|
|
|
@@ -161,14 +185,14 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
161
185
|
|
|
162
186
|
test("cancelling the ticket makes a later ticket change impossible", async () => {
|
|
163
187
|
const ctx = makeCtx();
|
|
164
|
-
await run("cancel_ticket",
|
|
165
|
-
await run("confirm_action",
|
|
188
|
+
await run("cancel_ticket", ctx);
|
|
189
|
+
await run("confirm_action", ctx);
|
|
166
190
|
expect(stateOf(ctx).ticket).toBeNull();
|
|
167
191
|
|
|
168
192
|
expect(await run("update_ticket", { flightId: "LX52" }, ctx)).toEqual({
|
|
169
193
|
error: "This caller has no ticket to move — it was cancelled on this call.",
|
|
170
194
|
});
|
|
171
|
-
expect(await run("cancel_ticket",
|
|
195
|
+
expect(await run("cancel_ticket", ctx)).toEqual({ error: "There is no ticket to cancel." });
|
|
172
196
|
});
|
|
173
197
|
|
|
174
198
|
test("a second staging is REFUSED rather than overwriting the first", async () => {
|
|
@@ -193,7 +217,12 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
193
217
|
|
|
194
218
|
// The first staging is untouched, and it is what a yes applies.
|
|
195
219
|
expect(stateOf(ctx).pending).toEqual({ kind: "update_ticket", flightId: "LX52" });
|
|
196
|
-
|
|
220
|
+
// A refused SECOND staging must not have moved the gate either — it was
|
|
221
|
+
// already `awaitingConfirmation` and the refusal changed nothing.
|
|
222
|
+
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
223
|
+
// `ok` is `okPosition` with `.result` taken off: this assertion is about
|
|
224
|
+
// what the apply DID, not about where the gate landed.
|
|
225
|
+
const applied = ok<{ applied: string }>(await run("confirm_action", ctx));
|
|
197
226
|
expect(applied.applied).toContain("LX52");
|
|
198
227
|
expect(stateOf(ctx).bookings).toEqual([]);
|
|
199
228
|
|
|
@@ -207,7 +236,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
207
236
|
test("cancel_action clears the block, so a declined change does not wedge the desk", async () => {
|
|
208
237
|
const ctx = makeCtx();
|
|
209
238
|
await run("book_car_rental", { carId: "C2", days: 3 }, ctx);
|
|
210
|
-
await run("cancel_action",
|
|
239
|
+
await run("cancel_action", ctx);
|
|
211
240
|
const staged = (await run("book_excursion", { excursionId: "E2" }, ctx)) as {
|
|
212
241
|
awaitingConfirmation: boolean;
|
|
213
242
|
};
|
|
@@ -225,12 +254,14 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
225
254
|
|
|
226
255
|
await run("to_hotel_assistant", { request: "a room" }, first);
|
|
227
256
|
await run("book_hotel", { hotelId: "H3", nights: 1 }, first);
|
|
228
|
-
await run("confirm_action",
|
|
257
|
+
await run("confirm_action", first);
|
|
229
258
|
|
|
230
259
|
expect(activeAssistant(stateOf(second))).toBe("primary");
|
|
231
260
|
expect(stateOf(second).bookings).toEqual([]);
|
|
232
|
-
|
|
233
|
-
|
|
261
|
+
// The GATE is per-session too, not just the trip.
|
|
262
|
+
expect(gateFlow.position(second).state).toBe("browsing");
|
|
263
|
+
expect(await run("confirm_action", second)).toMatchObject({
|
|
264
|
+
error: expect.stringContaining('this conversation is at "browsing"'),
|
|
234
265
|
});
|
|
235
266
|
expect(stateOf(first).bookings).toHaveLength(1);
|
|
236
267
|
});
|
|
@@ -283,8 +314,8 @@ describe("search tools", () => {
|
|
|
283
314
|
test("lookup_booking reports the ticket the caller is actually holding", async () => {
|
|
284
315
|
const ctx = makeCtx();
|
|
285
316
|
await run("update_ticket", { flightId: "LX52" }, ctx);
|
|
286
|
-
await run("confirm_action",
|
|
287
|
-
const booking = (await run("lookup_booking",
|
|
317
|
+
await run("confirm_action", ctx);
|
|
318
|
+
const booking = (await run("lookup_booking", ctx)) as {
|
|
288
319
|
passenger: string;
|
|
289
320
|
ticket: { flight: string; departs: string } | null;
|
|
290
321
|
};
|
|
@@ -327,9 +358,9 @@ describe("tripView projection", () => {
|
|
|
327
358
|
test("totals every confirmed booking", async () => {
|
|
328
359
|
const ctx = makeCtx();
|
|
329
360
|
await run("book_hotel", { hotelId: "H3", nights: 2 }, ctx); // 2 × 180
|
|
330
|
-
await run("confirm_action",
|
|
361
|
+
await run("confirm_action", ctx);
|
|
331
362
|
await run("book_excursion", { excursionId: "E1" }, ctx); // 35
|
|
332
|
-
await run("confirm_action",
|
|
363
|
+
await run("confirm_action", ctx);
|
|
333
364
|
|
|
334
365
|
const view = tripView(stateOf(ctx));
|
|
335
366
|
expect(view.total).toBe(395);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll, client, useAgentState
|
|
2
|
+
import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
3
|
import type { TripView } from "./shared.ts";
|
|
4
4
|
import { SPECIALIST_IDS, SPECIALISTS, tripProjection } from "./shared.ts";
|
|
5
5
|
|
|
@@ -15,7 +15,6 @@ function deskLabel(id: (typeof DESKS)[number]): string {
|
|
|
15
15
|
* the transcript — the whole delegation mechanism is otherwise invisible.
|
|
16
16
|
*/
|
|
17
17
|
function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
18
|
-
const theme = useTheme();
|
|
19
18
|
return (
|
|
20
19
|
<div className="flex flex-wrap gap-1.5">
|
|
21
20
|
{DESKS.map((id) => {
|
|
@@ -23,12 +22,9 @@ function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
|
23
22
|
return (
|
|
24
23
|
<span
|
|
25
24
|
key={id}
|
|
26
|
-
className=
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
color: on ? theme.bg : theme.text,
|
|
30
|
-
opacity: on ? 1 : 0.55,
|
|
31
|
-
}}
|
|
25
|
+
className={`rounded-full px-2.5 py-1 text-[11px] font-medium capitalize ${
|
|
26
|
+
on ? "bg-aai-primary text-aai-bg" : "bg-aai-surface text-aai-text opacity-55"
|
|
27
|
+
}`}
|
|
32
28
|
>
|
|
33
29
|
{deskLabel(id)}
|
|
34
30
|
</span>
|
|
@@ -39,11 +35,10 @@ function DeskStrip({ active }: { active: TripView["assistant"] }) {
|
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
function ItinerarySidebar() {
|
|
42
|
-
const theme = useTheme();
|
|
43
38
|
const trip = useAgentState(tripProjection);
|
|
44
39
|
|
|
45
40
|
return (
|
|
46
|
-
<div className="flex flex-col gap-4 p-4 h-full min-h-0
|
|
41
|
+
<div className="flex flex-col gap-4 p-4 h-full min-h-0 text-aai-text">
|
|
47
42
|
<div className="flex flex-col gap-2">
|
|
48
43
|
<h3 className="text-sm font-bold opacity-60 uppercase tracking-wide">{trip.passenger}</h3>
|
|
49
44
|
<DeskStrip active={trip.assistant} />
|
|
@@ -52,10 +47,7 @@ function ItinerarySidebar() {
|
|
|
52
47
|
{/* The staged action — the browser half of the confirmation gate. What is
|
|
53
48
|
on screen is exactly what the concierge just asked out loud. */}
|
|
54
49
|
{trip.pending && (
|
|
55
|
-
<div
|
|
56
|
-
className="rounded-lg p-3 text-sm"
|
|
57
|
-
style={{ background: theme.surface, border: `1px solid ${theme.primary}` }}
|
|
58
|
-
>
|
|
50
|
+
<div className="rounded-lg p-3 text-sm bg-aai-surface border border-aai-primary">
|
|
59
51
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">
|
|
60
52
|
Waiting on your yes
|
|
61
53
|
</p>
|
|
@@ -66,7 +58,7 @@ function ItinerarySidebar() {
|
|
|
66
58
|
<div className="flex flex-col gap-2">
|
|
67
59
|
<p className="text-[11px] font-bold uppercase tracking-wide opacity-60">Ticket</p>
|
|
68
60
|
{trip.ticket ? (
|
|
69
|
-
<div className="rounded-lg p-3
|
|
61
|
+
<div className="rounded-lg p-3 bg-aai-surface">
|
|
70
62
|
<p className="text-sm font-medium">
|
|
71
63
|
{trip.ticket.flightId} · {trip.ticket.route}
|
|
72
64
|
</p>
|
|
@@ -85,24 +77,20 @@ function ItinerarySidebar() {
|
|
|
85
77
|
{trip.bookings.map((booking) => (
|
|
86
78
|
<div
|
|
87
79
|
key={booking.reference}
|
|
88
|
-
className="flex items-center justify-between gap-3 rounded-lg p-3"
|
|
89
|
-
style={{ background: theme.surface }}
|
|
80
|
+
className="flex items-center justify-between gap-3 rounded-lg p-3 bg-aai-surface"
|
|
90
81
|
>
|
|
91
82
|
<div className="min-w-0">
|
|
92
83
|
<p className="truncate text-sm">{booking.summary}</p>
|
|
93
84
|
<p className="text-xs opacity-60">{booking.reference}</p>
|
|
94
85
|
</div>
|
|
95
|
-
<span className="text-sm font-bold
|
|
86
|
+
<span className="text-sm font-bold text-aai-primary">
|
|
96
87
|
${booking.price.toLocaleString("en-US")}
|
|
97
88
|
</span>
|
|
98
89
|
</div>
|
|
99
90
|
))}
|
|
100
|
-
<div
|
|
101
|
-
className="flex justify-between border-t pt-2 text-sm font-bold"
|
|
102
|
-
style={{ borderColor: theme.border }}
|
|
103
|
-
>
|
|
91
|
+
<div className="flex justify-between border-t border-aai-border pt-2 text-sm font-bold">
|
|
104
92
|
<span>Total</span>
|
|
105
|
-
<span
|
|
93
|
+
<span className="text-aai-primary">${trip.total.toLocaleString("en-US")}</span>
|
|
106
94
|
</div>
|
|
107
95
|
</div>
|
|
108
96
|
)}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
activeAssistant,
|
|
25
25
|
applyPending,
|
|
26
26
|
describeAction,
|
|
27
|
+
gateFlow,
|
|
27
28
|
note,
|
|
28
29
|
SPECIALISTS,
|
|
29
30
|
type SpecialistId,
|
|
@@ -111,28 +112,46 @@ export const completeOrEscalate = tripSlot.updateTool({
|
|
|
111
112
|
* halts before a sensitive tool and resumes on approval; the halt here is that
|
|
112
113
|
* every sensitive tool stages instead of acting, so the approval has somewhere
|
|
113
114
|
* to arrive.
|
|
115
|
+
*
|
|
116
|
+
* **`when: "awaitingConfirmation"` is the other side of that halt.** It used to
|
|
117
|
+
* be a null check inside {@link applyPending} — "Nothing is waiting for
|
|
118
|
+
* confirmation. Use the booking tool first." — which is the question "where is
|
|
119
|
+
* this conversation", asked of the payload. The gate asks the machine instead,
|
|
120
|
+
* and its refusal carries the state's own instruction. `SETTLED` fires only when
|
|
121
|
+
* the body did NOT answer with a {@link ToolFailure}, so an application that
|
|
122
|
+
* failed leaves the change staged and the caller still being asked.
|
|
114
123
|
*/
|
|
115
|
-
export const confirmAction =
|
|
124
|
+
export const confirmAction = gateFlow.tool({
|
|
116
125
|
description:
|
|
117
126
|
"Apply the change the caller has just confirmed out loud. Only call this " +
|
|
118
127
|
"after you have read the change back and heard a clear yes.",
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
when: "awaitingConfirmation",
|
|
129
|
+
send: { type: "SETTLED" },
|
|
130
|
+
execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
|
|
122
131
|
});
|
|
123
132
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
133
|
+
/**
|
|
134
|
+
* `cancel_action` — the caller said no. Drops the staged action, changes nothing.
|
|
135
|
+
*
|
|
136
|
+
* Gated for the same reason as `confirm_action`, and its own "nothing was
|
|
137
|
+
* waiting" arm is gone with the same argument.
|
|
138
|
+
*/
|
|
139
|
+
export const cancelAction = gateFlow.tool({
|
|
126
140
|
description:
|
|
127
141
|
"Discard the change the caller just declined. Call this when they say no, " +
|
|
128
142
|
"or when they want to change the details before confirming.",
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
when: "awaitingConfirmation",
|
|
144
|
+
send: { type: "SETTLED" },
|
|
145
|
+
execute: (_args, ctx) =>
|
|
146
|
+
tripSlot.update(ctx, (trip) => {
|
|
147
|
+
const action = trip.pending;
|
|
148
|
+
// Reachable only if the position and the payload disagree; see
|
|
149
|
+
// `applyPending`. Reported rather than thrown, mid-call.
|
|
150
|
+
if (!action) return { discarded: null, message: "Nothing was staged after all." };
|
|
151
|
+
trip.pending = null;
|
|
152
|
+
const described = describeAction(action);
|
|
153
|
+
const summary = typeof described === "string" ? described : action.kind;
|
|
154
|
+
note(trip, `Declined: ${summary}`);
|
|
155
|
+
return { discarded: summary, message: "Nothing was changed." };
|
|
156
|
+
}),
|
|
138
157
|
});
|