@alexkroman1/aai-cli 13.3.0 → 14.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-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
- package/dist/build.d.ts +5 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +117 -135
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +4 -4
- package/dist/scaffold/server.mjs +129 -25
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +27 -17
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +4 -4
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
* request rather than two and the value journaled by the last poll IS the result.
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
|
-
import type {
|
|
57
|
-
import {
|
|
56
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
57
|
+
import { stepReport, stepUploadInfo, TRANSCRIBE_API } from "@alexkroman1/aai/step";
|
|
58
58
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
stepTranscribePollOrFail,
|
|
60
|
+
stepTranscribeSubmitOrFail,
|
|
61
|
+
stepTranscribeUploadOrFail,
|
|
62
62
|
} from "@alexkroman1/aai/step-errors";
|
|
63
63
|
import { countWords, formatBytes } from "@alexkroman1/aai/utils";
|
|
64
64
|
import type { Transcript } from "./transcribe.ts";
|
|
@@ -78,7 +78,7 @@ const MAX_POLLS = 360;
|
|
|
78
78
|
/** Transcribe a recording through the async API. */
|
|
79
79
|
export async function transcribeBatchFlow(
|
|
80
80
|
input: { recording: string },
|
|
81
|
-
ctx:
|
|
81
|
+
ctx: WorkflowContext,
|
|
82
82
|
): Promise<Transcript> {
|
|
83
83
|
// Both at once: the clock does not depend on the upload, and issuing them
|
|
84
84
|
// together costs one round trip instead of two before a byte moves. Their issue
|
|
@@ -122,24 +122,24 @@ export async function transcribeBatchFlow(
|
|
|
122
122
|
* far smaller: if it expires before the next step runs, the run fails and a fresh one
|
|
123
123
|
* re-uploads — which is what would have happened anyway, once, instead of five times.
|
|
124
124
|
*
|
|
125
|
-
* The `
|
|
125
|
+
* The `OrFail` callers on `@alexkroman1/aai/step-errors` are the SDK's own
|
|
126
126
|
* `stepTranscribe*` plus `throwStepError` and nothing else, which is what turns the
|
|
127
127
|
* SDK's `TranscribeError` into the DevKit's verdict: a missing key and a 400 stop, a
|
|
128
128
|
* 429 waits as long as the service asked. Every step here ends the same way for the
|
|
129
129
|
* same reason.
|
|
130
130
|
*/
|
|
131
131
|
export async function uploadToProvider(uploadId: string): Promise<{ audioUrl: string }> {
|
|
132
|
-
const stored = await
|
|
133
|
-
await
|
|
132
|
+
const stored = await stepUploadInfo(uploadId);
|
|
133
|
+
await stepReport(
|
|
134
134
|
`Uploading ${stored.name || uploadId} (${formatBytes(stored.size)}) to the async API.`,
|
|
135
135
|
);
|
|
136
|
-
return await
|
|
136
|
+
return await stepTranscribeUploadOrFail(uploadId);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/** Create the transcription job, and answer with the id that outlives this run. */
|
|
140
140
|
export async function createJob(audioUrl: string): Promise<{ id: string }> {
|
|
141
|
-
const job = await
|
|
142
|
-
await
|
|
141
|
+
const job = await stepTranscribeSubmitOrFail(audioUrl);
|
|
142
|
+
await stepReport(`Submitted — job ${job.id}.`);
|
|
143
143
|
return job;
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -156,13 +156,13 @@ export async function pollTranscript(
|
|
|
156
156
|
id: string,
|
|
157
157
|
startedAt: number,
|
|
158
158
|
): Promise<{ done: false } | { done: true; transcript: Transcript }> {
|
|
159
|
-
const progress = await
|
|
159
|
+
const progress = await stepTranscribePollOrFail(id);
|
|
160
160
|
if (!progress.done) {
|
|
161
|
-
await
|
|
161
|
+
await stepReport(`Transcript ${id} is ${progress.status}.`);
|
|
162
162
|
return { done: false };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
const stored = await
|
|
165
|
+
const stored = await stepUploadInfo(uploadId);
|
|
166
166
|
const transcript = progress.transcript.text;
|
|
167
167
|
return {
|
|
168
168
|
done: true,
|
|
@@ -152,7 +152,7 @@ export function downsampleSegment(
|
|
|
152
152
|
const bind = sampleReaderFor(from);
|
|
153
153
|
const frame = blockAlign(from);
|
|
154
154
|
const inFrames = Math.floor(bytes.length / frame);
|
|
155
|
-
// A CALLER ERROR, and stated as one. `
|
|
155
|
+
// A CALLER ERROR, and stated as one. `stepReadUpload` clamps a window to what is
|
|
156
156
|
// stored, so the streaming flow can hand a short read down here; with no whole
|
|
157
157
|
// frame the averaging window below is zero wide, `total / 0` is `NaN`, and
|
|
158
158
|
// `setInt16` writes that as a 0 — a two-byte WAV of silence, transcribed and
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
72
|
import { basename, extname, join } from "node:path";
|
|
73
|
-
import { probeMedia, runFfmpeg, wavEncodeArgs } from "@alexkroman1/aai/ffmpeg";
|
|
74
|
-
import {
|
|
73
|
+
import { ffmpegBaseArgs, probeMedia, runFfmpeg, wavEncodeArgs } from "@alexkroman1/aai/ffmpeg";
|
|
74
|
+
import { stepReadUpload, stepReport, stepRequireCompleteUpload } from "@alexkroman1/aai/step";
|
|
75
75
|
import { throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
76
76
|
import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
|
|
77
77
|
import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
|
|
@@ -122,16 +122,16 @@ export type NormalizedRecording = {
|
|
|
122
122
|
* file that already exists instead of paying for a second one.
|
|
123
123
|
*/
|
|
124
124
|
export async function normalizeRecording(uploadId: string): Promise<NormalizedRecording> {
|
|
125
|
-
// `
|
|
125
|
+
// `stepRequireCompleteUpload`, not `stepUploadInfo`: `size` is the readable PREFIX, and
|
|
126
126
|
// every judgement below — cuttable, heavier-per-second, the byte count copied to
|
|
127
127
|
// disk — is about the WHOLE file.
|
|
128
|
-
const stored = await
|
|
129
|
-
const head = await
|
|
128
|
+
const stored = await stepRequireCompleteUpload(uploadId);
|
|
129
|
+
const head = await stepReadUpload(uploadId, { end: HEADER_PROBE_BYTES });
|
|
130
130
|
|
|
131
131
|
if (cuttable(head.bytes, stored.size) && !heavierThanNormalized(head.bytes, stored.size)) {
|
|
132
132
|
// No subprocess, no copy, no second upload. The overwhelmingly common case
|
|
133
133
|
// for a desk whose form says WAV, and the reason the check is a 64 KB read.
|
|
134
|
-
await
|
|
134
|
+
await stepReport(`${stored.name || uploadId} is already linear-PCM WAV — cutting it as it is.`);
|
|
135
135
|
return { recording: uploadId, converted: false };
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -144,7 +144,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
144
144
|
// WAV that is fine but too heavy to cut at this rate. Reporting "not a WAV we
|
|
145
145
|
// can cut" for the second one is a line that contradicts the file they
|
|
146
146
|
// uploaded.
|
|
147
|
-
await
|
|
147
|
+
await stepReport(
|
|
148
148
|
`Converting ${stored.name || uploadId} (${formatBytes(stored.size)}) — ` +
|
|
149
149
|
(cuttable(head.bytes, stored.size)
|
|
150
150
|
? `heavier per second than ${NORMALIZED_SAMPLE_RATE / 1000} kHz mono.`
|
|
@@ -164,7 +164,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
164
164
|
// `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
|
|
165
165
|
// completeness myself", which is what a body polling a still-arriving
|
|
166
166
|
// upload needs and is the opposite of what happened above: this step has
|
|
167
|
-
// already called `
|
|
167
|
+
// already called `stepRequireCompleteUpload`, so the file IS whole and the
|
|
168
168
|
// windows may land in any order. Omitting it lets `readUploadToFile`
|
|
169
169
|
// establish that for itself and fan out. The cost is one metadata round
|
|
170
170
|
// trip, against the dozens of window reads it overlaps.
|
|
@@ -177,22 +177,18 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
177
177
|
const info = await probeMedia(source, { timeoutMs: CONVERT_TIMEOUT_MS }).catch(
|
|
178
178
|
throwFfmpegStepError,
|
|
179
179
|
);
|
|
180
|
-
await
|
|
180
|
+
await stepReport(
|
|
181
181
|
`It is ${describeSource(info.audio?.codec, info.durationSec)} — re-encoding to ` +
|
|
182
182
|
`${NORMALIZED_SAMPLE_RATE / 1000} kHz mono WAV.`,
|
|
183
183
|
);
|
|
184
184
|
|
|
185
185
|
await runFfmpeg(
|
|
186
186
|
[
|
|
187
|
-
// The argv is the caller's, verbatim — `runFfmpeg` adds nothing
|
|
188
|
-
// standing flags are
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
"-loglevel",
|
|
193
|
-
"error",
|
|
194
|
-
"-nostdin",
|
|
195
|
-
"-y",
|
|
187
|
+
// The argv is the caller's, verbatim — `runFfmpeg` adds nothing — so
|
|
188
|
+
// the standing flags are the caller's too. This copy used to omit
|
|
189
|
+
// `-nostats`, which is the one that keeps ffmpeg's progress spam from
|
|
190
|
+
// evicting the error out of the captured stderr tail.
|
|
191
|
+
...ffmpegBaseArgs(),
|
|
196
192
|
"-i",
|
|
197
193
|
source,
|
|
198
194
|
...wavEncodeArgs({
|
|
@@ -212,7 +208,7 @@ export async function normalizeRecording(uploadId: string): Promise<NormalizedRe
|
|
|
212
208
|
type: "audio/wav",
|
|
213
209
|
});
|
|
214
210
|
|
|
215
|
-
await
|
|
211
|
+
await stepReport(
|
|
216
212
|
`Converted to ${formatBytes(written.size)} of WAV (from ${formatBytes(stored.size)}).`,
|
|
217
213
|
);
|
|
218
214
|
return { recording: written.id, converted: true };
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* first byte with `complete: false` and its `size` grows as bytes land.
|
|
23
23
|
*
|
|
24
24
|
* The reader needed almost nothing for this, which is why this flow is so close to
|
|
25
|
-
* the other one: `
|
|
25
|
+
* the other one: `stepReadUpload` already clamped its window to what is stored (so a
|
|
26
26
|
* plan computed from a header could end one byte past the file), and that clamp is
|
|
27
27
|
* exactly "read what has arrived". So `transcribeSegment` below is `transcribe.ts`'s
|
|
28
28
|
* OWN step, unchanged, called on windows this body has checked are present.
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* 0.9 MB/s: `size` was 0 at every poll for 45 seconds and then the whole file, so
|
|
51
51
|
* the run planned nothing, transcribed nothing, and did its entire fan-out after
|
|
52
52
|
* the upload — the classic flow, with extra steps. `segmentStored` reads `ranges`
|
|
53
|
-
* instead, and `
|
|
53
|
+
* instead, and `stepReadUpload` clamps to the run a read starts in rather than to the
|
|
54
54
|
* prefix, so a window that has landed is a window this flow can work on.
|
|
55
55
|
* - **The stall test on the prefix would then FAIL a healthy upload.** A parts
|
|
56
56
|
* upload moving at full speed reports the same prefix at every poll, which is
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
* what keeps that order a pure function of journaled values.
|
|
168
168
|
*/
|
|
169
169
|
|
|
170
|
-
import type {
|
|
170
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
171
171
|
import {
|
|
172
172
|
mapConcurrent,
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
stepReadUpload,
|
|
174
|
+
stepReport,
|
|
175
|
+
stepUploadInfo,
|
|
175
176
|
type UploadRange,
|
|
176
|
-
uploadInfo,
|
|
177
177
|
} from "@alexkroman1/aai/step";
|
|
178
178
|
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
179
179
|
import { formatDuration, omitUndefined, plural } from "@alexkroman1/aai/utils";
|
|
@@ -308,7 +308,7 @@ export type StreamPlan = {
|
|
|
308
308
|
* an upload id exactly as in the classic flow; what differs is that the client chose
|
|
309
309
|
* it and the bytes are still on their way.
|
|
310
310
|
*/
|
|
311
|
-
export async function transcribeStreamFlow(input: { recording: string }, ctx:
|
|
311
|
+
export async function transcribeStreamFlow(input: { recording: string }, ctx: WorkflowContext) {
|
|
312
312
|
// `ctx.now()`, not a step: the engine journals the read under its own key, so
|
|
313
313
|
// every walk of this line sees the instant the first one did.
|
|
314
314
|
const startedAt = await ctx.now();
|
|
@@ -364,7 +364,7 @@ export async function transcribeStreamFlow(input: { recording: string }, ctx: Wo
|
|
|
364
364
|
// A segment is READY when its whole window is stored — except once the upload
|
|
365
365
|
// is complete, where `at.size` is the true total and the plan came from the
|
|
366
366
|
// header's DECLARED length: a recording that came up short leaves a final
|
|
367
|
-
// segment ending past the file, and `
|
|
367
|
+
// segment ending past the file, and `stepReadUpload` clamping is what makes that
|
|
368
368
|
// the right answer rather than an error.
|
|
369
369
|
const ready = plan.segments.filter(
|
|
370
370
|
(segment) =>
|
|
@@ -443,7 +443,7 @@ export async function transcribeStreamFlow(input: { recording: string }, ctx: Wo
|
|
|
443
443
|
* from.
|
|
444
444
|
*/
|
|
445
445
|
export async function probeUpload(id: string): Promise<UploadProgressView> {
|
|
446
|
-
const info = await
|
|
446
|
+
const info = await stepUploadInfo(id);
|
|
447
447
|
return {
|
|
448
448
|
size: info.size,
|
|
449
449
|
complete: info.complete,
|
|
@@ -609,7 +609,7 @@ export function storedBytes(size: number, ranges: readonly UploadRange[] | undef
|
|
|
609
609
|
* only the prefix and the run has nothing to do until the upload is over, which
|
|
610
610
|
* is the entire wait this flow exists to remove.
|
|
611
611
|
*
|
|
612
|
-
* A window has to be covered WHOLE by one run: `
|
|
612
|
+
* A window has to be covered WHOLE by one run: `stepReadUpload` clamps to the run a
|
|
613
613
|
* read starts in, so a segment straddling a hole would come back short and be
|
|
614
614
|
* transcribed as a fragment. `rangesOf` merges adjacent windows, so a run really
|
|
615
615
|
* is a contiguous stretch and one containment test is the whole check.
|
|
@@ -636,7 +636,7 @@ export function segmentStored(segment: Segment, at: UploadProgressView): boolean
|
|
|
636
636
|
* which is what the classic flow is for.
|
|
637
637
|
*/
|
|
638
638
|
export async function planStreamed(id: string): Promise<StreamPlan> {
|
|
639
|
-
const head = await
|
|
639
|
+
const head = await stepReadUpload(id, { end: HEADER_PROBE_BYTES });
|
|
640
640
|
const format = fatalOnUnsupported(() => parseWav(head.bytes, Number.POSITIVE_INFINITY));
|
|
641
641
|
if (!Number.isFinite(format.dataEnd)) {
|
|
642
642
|
return throwFatalStepError(
|
|
@@ -647,7 +647,7 @@ export async function planStreamed(id: string): Promise<StreamPlan> {
|
|
|
647
647
|
);
|
|
648
648
|
}
|
|
649
649
|
const segments = fatalOnUnsupported(() => planSegments(format));
|
|
650
|
-
await
|
|
650
|
+
await stepReport(
|
|
651
651
|
`Planned ${formatDuration(segments.at(-1)?.endMs ?? 0)} of audio as ` +
|
|
652
652
|
`${segments.length} ${plural(segments.length, "segment")} while it uploads.`,
|
|
653
653
|
);
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* the same shape). It is called FROM steps, so it inherits their environment.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { stepTranscribeSyncOrFail } from "@alexkroman1/aai/step-errors";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Time one transcription, so the progress log carries LATENCY.
|
|
@@ -57,7 +57,7 @@ export function elapsed(ms: number): string {
|
|
|
57
57
|
* concatenated straight into the request body rather than into an intermediate
|
|
58
58
|
* buffer that doubles the segment's footprint.
|
|
59
59
|
*
|
|
60
|
-
* `
|
|
60
|
+
* `stepTranscribeSyncOrFail` — the SDK's own `stepTranscribeSync` plus
|
|
61
61
|
* `throwStepError`, and nothing else — is the whole of what this adds to the SDK
|
|
62
62
|
* call, and it is where the three-way call is made: a `FatalError` stops the DevKit retrying
|
|
63
63
|
* something that will answer the same way, a bare `RetryableError` retries in ONE
|
|
@@ -75,6 +75,6 @@ export async function transcribeWav(
|
|
|
75
75
|
filename: string,
|
|
76
76
|
label: string,
|
|
77
77
|
): Promise<string> {
|
|
78
|
-
const { text } = await
|
|
78
|
+
const { text } = await stepTranscribeSyncOrFail(bytes, { filename, label });
|
|
79
79
|
return text;
|
|
80
80
|
}
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* - **The audio is addressed by BYTE RANGE, never carried.** A workflow's input
|
|
49
49
|
* is journaled and replayed on every resume, so the recording lives in the
|
|
50
50
|
* app's own upload store and the run carries only its id; each step reads
|
|
51
|
-
* exactly its own window with `
|
|
51
|
+
* exactly its own window with `stepReadUpload`. Sixty steps therefore move the
|
|
52
52
|
* recording once between them, not sixty times.
|
|
53
53
|
* - **The fan-out is bounded by `mapConcurrent`, and the bound is not a detail.**
|
|
54
54
|
* The DevKit correlates a journal entry to a step call by the ORDER the call
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
* calls settle. Its module doc carries the argument; what matters here is that
|
|
58
58
|
* there is no barrier, so a slow segment costs only itself.
|
|
59
59
|
* - **The transcript STREAMS as it is produced.** Each segment is emitted the
|
|
60
|
-
* moment it lands (`
|
|
60
|
+
* moment it lands (`stepEmit(TRANSCRIPT_STREAM, …)`), so the page renders the
|
|
61
61
|
* answer growing rather than a status line and then everything at once. That is
|
|
62
62
|
* the difference a fan-out can make to a reader and a run output cannot: an
|
|
63
63
|
* `output` exists only when the last segment does.
|
|
64
64
|
*/
|
|
65
65
|
|
|
66
|
-
import type {
|
|
66
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
67
67
|
import {
|
|
68
|
-
emit,
|
|
69
68
|
mapConcurrent,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
stepEmit,
|
|
70
|
+
stepReadUpload,
|
|
71
|
+
stepReport,
|
|
72
|
+
stepRequireCompleteUpload,
|
|
73
|
+
stepUploadInfo,
|
|
74
74
|
wavHeader,
|
|
75
75
|
} from "@alexkroman1/aai/step";
|
|
76
76
|
import { throwFatalStepError } from "@alexkroman1/aai/step-errors";
|
|
@@ -248,7 +248,7 @@ export type SegmentTranscript = {
|
|
|
248
248
|
* The input is what `POST /workflows/runs` carries — see `agent.ts` for the
|
|
249
249
|
* schema it is validated against before a run exists.
|
|
250
250
|
*/
|
|
251
|
-
export async function transcribeFlow(input: { recording: string }, ctx:
|
|
251
|
+
export async function transcribeFlow(input: { recording: string }, ctx: WorkflowContext) {
|
|
252
252
|
// Both at once: neither needs the other, and issued together they are one
|
|
253
253
|
// round trip instead of two before any audio is read. The ORDER is still a
|
|
254
254
|
// pure function of this line — the two calls go out synchronously, left to
|
|
@@ -313,13 +313,13 @@ export async function splitRecording(uploadId: string): Promise<{
|
|
|
313
313
|
// half-arrived recording this planned a fan-out over the first half and the run
|
|
314
314
|
// returned a transcript of it, reporting success. `stream.ts` is the flow for a
|
|
315
315
|
// recording that is still landing; this one wants all of it.
|
|
316
|
-
const stored = await
|
|
317
|
-
const head = await
|
|
316
|
+
const stored = await stepRequireCompleteUpload(uploadId);
|
|
317
|
+
const head = await stepReadUpload(uploadId, { end: HEADER_PROBE_BYTES });
|
|
318
318
|
const format = fatalOnUnsupported(() => parseWav(head.bytes, stored.size));
|
|
319
319
|
const segments = fatalOnUnsupported(() => planSegments(format));
|
|
320
320
|
const durationMs = segments.at(-1)?.endMs ?? 0;
|
|
321
321
|
|
|
322
|
-
await
|
|
322
|
+
await stepReport(
|
|
323
323
|
`Split ${formatDuration(durationMs)} of audio into ${segments.length} ${plural(segments.length, "segment")}.`,
|
|
324
324
|
);
|
|
325
325
|
return { format, segments, durationMs };
|
|
@@ -345,12 +345,14 @@ export async function transcribeSegment(
|
|
|
345
345
|
// calls together, so their lines interleave by completion — the page renders a
|
|
346
346
|
// log, not a sequence, and `segment.index` is what puts the TRANSCRIPT back in
|
|
347
347
|
// order.
|
|
348
|
-
await
|
|
348
|
+
await stepReport(
|
|
349
|
+
`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`,
|
|
350
|
+
);
|
|
349
351
|
|
|
350
352
|
// `[start, end)`, the same half-open pair `planSegments` produced — the store
|
|
351
353
|
// owns the conversion to HTTP's inclusive range, so there is no `- 1` here to
|
|
352
354
|
// get wrong.
|
|
353
|
-
const audio = await
|
|
355
|
+
const audio = await stepReadUpload(uploadId, { start: segment.start, end: segment.end });
|
|
354
356
|
|
|
355
357
|
// The audio and nothing else. A `config` part carrying `language_code` used
|
|
356
358
|
// to ride along, and it is gone with the picker that fed it: the model detects
|
|
@@ -401,15 +403,15 @@ export async function transcribeSegment(
|
|
|
401
403
|
);
|
|
402
404
|
// The LATENCY, which is what says whether the concurrency bound or the endpoint
|
|
403
405
|
// is the thing limiting the run — see `timed`'s doc.
|
|
404
|
-
await
|
|
406
|
+
await stepReport(
|
|
405
407
|
`Transcribed ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)} in ${elapsed(ms)}.`,
|
|
406
408
|
);
|
|
407
409
|
// And the WORDS, into their own stream, which is what makes this run's answer
|
|
408
410
|
// streamable rather than only its narration: the page stitches whatever has
|
|
409
411
|
// arrived and renders the transcript growing, minutes before `output` exists.
|
|
410
|
-
// Its own namespace because `
|
|
411
|
-
// verbatim — see `
|
|
412
|
-
await
|
|
412
|
+
// Its own namespace because `stepReport`'s stream carries sentences a page prints
|
|
413
|
+
// verbatim — see `stepEmit`'s doc.
|
|
414
|
+
await stepEmit(TRANSCRIPT_STREAM, {
|
|
413
415
|
index: segment.index,
|
|
414
416
|
startMs: segment.startMs,
|
|
415
417
|
endMs: segment.endMs,
|
|
@@ -422,7 +424,7 @@ export async function transcribeSegment(
|
|
|
422
424
|
* Stitch the segments into one transcript.
|
|
423
425
|
*
|
|
424
426
|
* A step rather than a pure call in the body, and the reason is the narration:
|
|
425
|
-
* the body replays from the top on every resume, so a `
|
|
427
|
+
* the body replays from the top on every resume, so a `stepReport()` written there
|
|
426
428
|
* is re-emitted on each one. Journaling the finished transcript also means a
|
|
427
429
|
* caller re-reading a completed run gets the same bytes rather than a value
|
|
428
430
|
* recomputed from parts.
|
|
@@ -433,7 +435,7 @@ export async function mergeTranscript(
|
|
|
433
435
|
parts: readonly SegmentTranscript[],
|
|
434
436
|
startedAt: number,
|
|
435
437
|
): Promise<Transcript> {
|
|
436
|
-
await
|
|
438
|
+
await stepReport(`Stitching ${parts.length} ${plural(parts.length, "segment")} together.`);
|
|
437
439
|
|
|
438
440
|
// `mapConcurrent` resolves in ITEM order however the calls settled, so this is
|
|
439
441
|
// already ordered — sorted anyway, because the merge is where an ordering
|
|
@@ -443,7 +445,7 @@ export async function mergeTranscript(
|
|
|
443
445
|
|
|
444
446
|
// The FILENAME, not the id: the page prints this, and `upl_9f3…` tells a
|
|
445
447
|
// reader nothing about which recording they are looking at.
|
|
446
|
-
const source = (await
|
|
448
|
+
const source = (await stepUploadInfo(uploadId)).name || uploadId;
|
|
447
449
|
return {
|
|
448
450
|
source,
|
|
449
451
|
segments: parts.length,
|
|
@@ -457,19 +459,6 @@ export async function mergeTranscript(
|
|
|
457
459
|
};
|
|
458
460
|
}
|
|
459
461
|
|
|
460
|
-
// Re-exported rather than re-declared: `stream.ts` and `batch.ts` already import
|
|
461
|
-
// these from this module, and the split that let the PAGE stitch a partial
|
|
462
|
-
// transcript should not ripple through every flow. `clock` and `countWords` used
|
|
463
|
-
// to be in this list and are `formatDuration`/`countWords` on
|
|
464
|
-
// `@alexkroman1/aai/utils` now — a run narrates itself and the page renders the
|
|
465
|
-
// same run, so those two were a private copy of a formatter the SDK ships.
|
|
466
|
-
export {
|
|
467
|
-
stitchChunks,
|
|
468
|
-
stitchTranscript,
|
|
469
|
-
TRANSCRIPT_STREAM,
|
|
470
|
-
type TranscriptChunk,
|
|
471
|
-
} from "./stitch.ts";
|
|
472
|
-
|
|
473
462
|
// ---- I/O helpers ------------------------------------------------------------
|
|
474
463
|
|
|
475
464
|
/**
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import agentDef from "virtual:aai/agent";
|
|
32
32
|
import {
|
|
33
|
-
callsIn,
|
|
34
33
|
describeTurn,
|
|
35
34
|
type EvalSession,
|
|
36
35
|
type EvalToolCall,
|
|
37
36
|
lastStateIn,
|
|
37
|
+
toolCallsInTurns,
|
|
38
38
|
toolNames,
|
|
39
39
|
turnCalling,
|
|
40
40
|
} from "@alexkroman1/aai-runtime/eval";
|
|
@@ -116,7 +116,7 @@ describeEval(agentDef, (test) => {
|
|
|
116
116
|
// every turn's tool list AND tells the two findings apart: no
|
|
117
117
|
// `update_ticket` at all, or calls that were all refused by the desk gate.
|
|
118
118
|
const staging = turnCalling(turns, "update_ticket", stagedSomething);
|
|
119
|
-
const attempts =
|
|
119
|
+
const attempts = toolCallsInTurns(turns).filter((call) => call.name === "update_ticket");
|
|
120
120
|
// The staging call itself, by INDEX, because what follows it in the same
|
|
121
121
|
// turn is the subject of the assertion below.
|
|
122
122
|
const stagedAt = staging.toolCalls.findIndex(
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
|
|
2
2
|
import agentDef from "virtual:aai/agent";
|
|
3
3
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createToolContext,
|
|
6
|
+
expectDialogOk,
|
|
7
|
+
expectToolOk,
|
|
8
|
+
toolRunner,
|
|
9
|
+
} from "@alexkroman1/aai/testing";
|
|
5
10
|
import { describe, expect, test } from "vitest";
|
|
6
11
|
|
|
7
12
|
import {
|
|
@@ -196,11 +201,11 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
196
201
|
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
197
202
|
|
|
198
203
|
// A gated tool answers the flow's POSITION wrapped around the body's own
|
|
199
|
-
// return value, so the applied sentence is under `result`. `
|
|
204
|
+
// return value, so the applied sentence is under `result`. `expectDialogOk`
|
|
200
205
|
// keeps the position and THROWS on a refusal, quoting it — where the cast
|
|
201
206
|
// it replaces read `undefined` off the failure and died three assertions
|
|
202
207
|
// later on a property of undefined.
|
|
203
|
-
const confirmed =
|
|
208
|
+
const confirmed = expectDialogOk<{ applied: string; reference: string }>(
|
|
204
209
|
await run("confirm_action", ctx),
|
|
205
210
|
);
|
|
206
211
|
expect(confirmed.result.applied).toContain("Harborview Suites");
|
|
@@ -225,7 +230,7 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
225
230
|
const ctx = makeCtx();
|
|
226
231
|
await atDesk("flight", ctx);
|
|
227
232
|
await run("update_ticket", { flightId: "LX54" }, ctx);
|
|
228
|
-
const dropped =
|
|
233
|
+
const dropped = expectDialogOk<{ discarded: string }>(await run("cancel_action", ctx));
|
|
229
234
|
expect(dropped.result.discarded).toContain("LX54");
|
|
230
235
|
expect(dropped.state).toBe("browsing");
|
|
231
236
|
|
|
@@ -291,9 +296,9 @@ describe("sensitive tools stage rather than act", () => {
|
|
|
291
296
|
// A refused SECOND staging must not have moved the gate either — it was
|
|
292
297
|
// already `awaitingConfirmation` and the refusal changed nothing.
|
|
293
298
|
expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
|
|
294
|
-
// `
|
|
299
|
+
// `expectToolOk` is `expectDialogOk` with `.result` taken off: this assertion is about
|
|
295
300
|
// what the apply DID, not about where the gate landed.
|
|
296
|
-
const applied =
|
|
301
|
+
const applied = expectToolOk<{ applied: string }>(await run("confirm_action", ctx));
|
|
297
302
|
expect(applied.applied).toContain("LX52");
|
|
298
303
|
expect(stateOf(ctx).bookings).toEqual([]);
|
|
299
304
|
|
|
@@ -368,7 +373,10 @@ describe("search tools", () => {
|
|
|
368
373
|
const all = (await run("search_hotels", { city: "Boston" }, ctx)) as {
|
|
369
374
|
hotels: { perNight: string }[];
|
|
370
375
|
};
|
|
371
|
-
|
|
376
|
+
// Always to the cent: `formatMoney` is one shape at every desk, where
|
|
377
|
+
// this template's own `toLocaleString` copy dropped `.00` on a round
|
|
378
|
+
// number and kept it on a price with change.
|
|
379
|
+
expect(all.hotels.map((h) => h.perNight)).toEqual(["$180.00", "$265.00", "$340.00"]);
|
|
372
380
|
|
|
373
381
|
const cheap = (await run("search_hotels", { city: "Boston", maxPerNight: 200 }, ctx)) as {
|
|
374
382
|
hotels: { name: string }[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import { AutoScroll,
|
|
2
|
+
import { AutoScroll, mountClient, 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
|
|
|
@@ -118,7 +118,7 @@ function ItinerarySidebar() {
|
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
mountClient({
|
|
122
122
|
name: "Swiss Air Concierge",
|
|
123
123
|
sidebar: ItinerarySidebar,
|
|
124
124
|
theme: {
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
type ToolContext,
|
|
48
48
|
type ToolFailure,
|
|
49
49
|
} from "@alexkroman1/aai";
|
|
50
|
+
import { formatMoney, plural } from "@alexkroman1/aai/utils";
|
|
50
51
|
|
|
51
52
|
// ─── The booking world ───────────────────────────────────────────────────────
|
|
52
53
|
// Their notebook downloads a sqlite database of a real airline's schedule and
|
|
@@ -180,11 +181,6 @@ export const EXCURSIONS: Excursion[] = [
|
|
|
180
181
|
{ id: "E4", name: "Lake Zurich cruise", city: "Zurich", kind: "boat", price: 52 },
|
|
181
182
|
];
|
|
182
183
|
|
|
183
|
-
/** `$1,234` — one money format, so a spoken price and a rendered one agree. */
|
|
184
|
-
export function formatPrice(amount: number): string {
|
|
185
|
-
return `$${amount.toLocaleString("en-US")}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
184
|
// ─── The dialog stack ────────────────────────────────────────────────────────
|
|
189
185
|
|
|
190
186
|
export const SPECIALIST_IDS = ["flight", "hotel", "car_rental", "excursion"] as const;
|
|
@@ -504,24 +500,24 @@ export function describeAction(action: DeepReadonly<PendingAction>): string | To
|
|
|
504
500
|
case "update_ticket": {
|
|
505
501
|
const flight = FLIGHTS.find((f) => f.id === action.flightId);
|
|
506
502
|
if (!flight) return { error: `No flight ${action.flightId} in the schedule.` };
|
|
507
|
-
return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${
|
|
503
|
+
return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${formatMoney(flight.fare)}`;
|
|
508
504
|
}
|
|
509
505
|
case "cancel_ticket":
|
|
510
506
|
return "cancel your ticket entirely";
|
|
511
507
|
case "book_hotel": {
|
|
512
508
|
const hotel = HOTELS.find((h) => h.id === action.hotelId);
|
|
513
509
|
if (!hotel) return { error: `No hotel ${action.hotelId}.` };
|
|
514
|
-
return `book ${hotel.name} in ${hotel.area} for ${action.nights}
|
|
510
|
+
return `book ${hotel.name} in ${hotel.area} for ${action.nights} ${plural(action.nights, "night")}, ${formatMoney(hotel.pricePerNight * action.nights)} total`;
|
|
515
511
|
}
|
|
516
512
|
case "book_car": {
|
|
517
513
|
const car = CAR_RENTALS.find((c) => c.id === action.carId);
|
|
518
514
|
if (!car) return { error: `No car ${action.carId}.` };
|
|
519
|
-
return `reserve the ${car.tier} from ${car.vendor} for ${action.days}
|
|
515
|
+
return `reserve the ${car.tier} from ${car.vendor} for ${action.days} ${plural(action.days, "day")}, ${formatMoney(car.pricePerDay * action.days)} total`;
|
|
520
516
|
}
|
|
521
517
|
case "book_excursion": {
|
|
522
518
|
const excursion = EXCURSIONS.find((e) => e.id === action.excursionId);
|
|
523
519
|
if (!excursion) return { error: `No excursion ${action.excursionId}.` };
|
|
524
|
-
return `book ${excursion.name} at ${
|
|
520
|
+
return `book ${excursion.name} at ${formatMoney(excursion.price)}`;
|
|
525
521
|
}
|
|
526
522
|
// Unreachable while `PendingAction` is exhausted above — and the arm a new
|
|
527
523
|
// member of the union lands in until it has one of its own, which is a
|
|
@@ -627,11 +623,11 @@ export function applyPending(
|
|
|
627
623
|
state.bookings.push({
|
|
628
624
|
kind: "hotel",
|
|
629
625
|
reference,
|
|
630
|
-
summary: `${hotel.name} (${hotel.area}), ${action.nights}
|
|
626
|
+
summary: `${hotel.name} (${hotel.area}), ${action.nights} ${plural(action.nights, "night")}`,
|
|
631
627
|
price,
|
|
632
628
|
});
|
|
633
629
|
note(state, `Hotel booked: ${hotel.name} — ${reference}`);
|
|
634
|
-
return { applied: `${hotel.name} booked, ${
|
|
630
|
+
return { applied: `${hotel.name} booked, ${formatMoney(price)}.`, reference };
|
|
635
631
|
}
|
|
636
632
|
case "book_car": {
|
|
637
633
|
const car = CAR_RENTALS.find((c) => c.id === action.carId);
|
|
@@ -640,12 +636,12 @@ export function applyPending(
|
|
|
640
636
|
state.bookings.push({
|
|
641
637
|
kind: "car",
|
|
642
638
|
reference,
|
|
643
|
-
summary: `${car.vendor} ${car.tier}, ${action.days}
|
|
639
|
+
summary: `${car.vendor} ${car.tier}, ${action.days} ${plural(action.days, "day")}`,
|
|
644
640
|
price,
|
|
645
641
|
});
|
|
646
642
|
note(state, `Car reserved: ${car.vendor} ${car.tier} — ${reference}`);
|
|
647
643
|
return {
|
|
648
|
-
applied: `${car.tier} from ${car.vendor} reserved, ${
|
|
644
|
+
applied: `${car.tier} from ${car.vendor} reserved, ${formatMoney(price)}.`,
|
|
649
645
|
reference,
|
|
650
646
|
};
|
|
651
647
|
}
|
|
@@ -659,7 +655,7 @@ export function applyPending(
|
|
|
659
655
|
price: excursion.price,
|
|
660
656
|
});
|
|
661
657
|
note(state, `Excursion booked: ${excursion.name} — ${reference}`);
|
|
662
|
-
return { applied: `${excursion.name} booked, ${
|
|
658
|
+
return { applied: `${excursion.name} booked, ${formatMoney(excursion.price)}.`, reference };
|
|
663
659
|
}
|
|
664
660
|
// Same as `describeAction`: unreachable today, and a refusal rather than a
|
|
665
661
|
// silent no-op for whatever the union grows next.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
|
+
import { FLIGHTS, tripSlot } from "../shared.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Their `fetch_user_flight_information`, which the notebook runs ONCE and
|
|
@@ -22,13 +23,13 @@ export default tripSlot.tool({
|
|
|
22
23
|
route: flight.route,
|
|
23
24
|
departs: flight.departs,
|
|
24
25
|
arrives: flight.arrives,
|
|
25
|
-
fare:
|
|
26
|
+
fare: formatMoney(flight.fare),
|
|
26
27
|
}
|
|
27
28
|
: null,
|
|
28
29
|
bookings: trip.bookings.map((b) => ({
|
|
29
30
|
reference: b.reference,
|
|
30
31
|
what: b.summary,
|
|
31
|
-
price:
|
|
32
|
+
price: formatMoney(b.price),
|
|
32
33
|
})),
|
|
33
34
|
};
|
|
34
35
|
},
|