@alexkroman1/aai-cli 13.2.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/bin.mjs +1 -1
- package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
- package/dist/_config.d.ts +25 -1
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +15 -7
- package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
- package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
- package/dist/_output.d.ts +24 -7
- package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
- package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
- package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
- package/dist/_utils.d.ts +35 -0
- package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
- package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-Mxk8gWvX.mjs +108 -0
- package/dist/build.d.ts +5 -0
- package/dist/cli.mjs +172 -116
- package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
- package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
- package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
- package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
- package/dist/eval.d.ts +2 -0
- package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
- package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
- package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/.env.example +17 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +6 -5
- package/dist/scaffold/server.mjs +129 -25
- package/dist/secret-Cjp912yF.mjs +162 -0
- package/dist/secret.d.ts +48 -13
- package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
- package/dist/templates/briefing-desk/agent.test.ts +7 -3
- package/dist/templates/call-audit/agent.test.ts +25 -9
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +40 -58
- 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/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +10 -4
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +42 -36
- 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/agent.test.ts +6 -1
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +33 -16
- 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 +33 -14
- package/dist/templates/podcast-digest/client.tsx +27 -36
- 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 +49 -32
- 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 +8 -2
- package/dist/templates/redline/client.tsx +47 -48
- 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 +17 -8
- 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/registry.test.ts +99 -58
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/simple/agent.test.ts +39 -6
- 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 +21 -12
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +36 -47
- 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 +36 -20
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +34 -40
- 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-D032V2cA.mjs +242 -0
- package/dist/test.d.ts +97 -31
- package/dist/typecheck.mjs +2 -2
- package/dist/worker-bundler.mjs +3 -3
- package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
- package/package.json +10 -10
- package/dist/bin.d.mts +0 -2
- package/dist/build-BLlLOrng.mjs +0 -66
- package/dist/secret-B-3T5tvr.mjs +0 -61
- package/dist/test-CiLab-AA.mjs +0 -182
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*
|
|
36
36
|
* ## Every stage REPORTS, and the report goes two places
|
|
37
37
|
*
|
|
38
|
-
* `
|
|
38
|
+
* `stepReport()` (`@alexkroman1/aai/step`) writes to the run's own stream — which
|
|
39
39
|
* `research_progress` reads back down the phone and a page renders — and to the
|
|
40
40
|
* server log, with the attempt number appended past the first. A pass that is
|
|
41
41
|
* retrying and one that is working print the same sentences otherwise.
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
* researcher CONCLUDED, which is exactly what the step returns.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import type {
|
|
53
|
-
import { mapConcurrent,
|
|
54
|
-
import {
|
|
52
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
53
|
+
import { mapConcurrent, stepReport } from "@alexkroman1/aai/step";
|
|
54
|
+
import { stepGenerateJsonOrFail, stepGenerateOrFail } from "@alexkroman1/aai/step-errors";
|
|
55
55
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
56
56
|
import { errorMessage, isToolFailure, plural } from "@alexkroman1/aai/utils";
|
|
57
57
|
import { z } from "zod";
|
|
@@ -107,7 +107,7 @@ export type Source = { title: string; url: string };
|
|
|
107
107
|
|
|
108
108
|
// ---- What each stage's model call has to come back as ------------------------
|
|
109
109
|
//
|
|
110
|
-
// `
|
|
110
|
+
// `stepGenerateJsonOrFail` validates against these, so a reply that missed
|
|
111
111
|
// is a plain throw and therefore a retry — where the hand-rolled `askJson<T>()`
|
|
112
112
|
// this replaces returned a value the compiler believed and nothing checked. They are
|
|
113
113
|
// deliberately LENIENT wherever the old hand-written coercion was: a model that
|
|
@@ -212,7 +212,7 @@ export type Findings = {
|
|
|
212
212
|
*/
|
|
213
213
|
export async function researchFlow(
|
|
214
214
|
input: { topic: string; requestedBy: string },
|
|
215
|
-
ctx:
|
|
215
|
+
ctx: WorkflowContext,
|
|
216
216
|
) {
|
|
217
217
|
const brief = await ctx.step("writeBrief", () => writeBrief(input.topic));
|
|
218
218
|
const angles = await ctx.step("planAngles", () => planAngles(brief));
|
|
@@ -270,11 +270,11 @@ export async function researchFlow(
|
|
|
270
270
|
* later model call.
|
|
271
271
|
*/
|
|
272
272
|
export async function writeBrief(topic: string): Promise<Brief> {
|
|
273
|
-
await
|
|
274
|
-
const parsed = await
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
);
|
|
273
|
+
await stepReport(`Working out what "${topic}" is really asking.`);
|
|
274
|
+
const parsed = await stepGenerateJsonOrFail(`Research request, as the caller said it: ${topic}`, {
|
|
275
|
+
system: BRIEF_SYSTEM,
|
|
276
|
+
schema: BriefReply,
|
|
277
|
+
});
|
|
278
278
|
return { brief: parsed.brief || topic, criteria: parsed.criteria.slice(0, MAX_ANGLES) };
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -286,7 +286,7 @@ export async function writeBrief(topic: string): Promise<Brief> {
|
|
|
286
286
|
* asking the model again and getting a different one.
|
|
287
287
|
*/
|
|
288
288
|
export async function planAngles(brief: Brief): Promise<string[]> {
|
|
289
|
-
const parsed = await
|
|
289
|
+
const parsed = await stepGenerateJsonOrFail(briefText(brief), {
|
|
290
290
|
system: PLAN_SYSTEM,
|
|
291
291
|
schema: AnglesReply,
|
|
292
292
|
});
|
|
@@ -294,10 +294,10 @@ export async function planAngles(brief: Brief): Promise<string[]> {
|
|
|
294
294
|
if (angles.length === 0) {
|
|
295
295
|
// Nothing to fan out over is a plan failure, not an empty result: the brief
|
|
296
296
|
// itself is the one angle that is always available.
|
|
297
|
-
await
|
|
297
|
+
await stepReport("No angles came back; researching the brief itself.");
|
|
298
298
|
return [brief.brief];
|
|
299
299
|
}
|
|
300
|
-
await
|
|
300
|
+
await stepReport(`Researching ${angles.length} ${plural(angles.length, "angle")}.`);
|
|
301
301
|
return angles;
|
|
302
302
|
}
|
|
303
303
|
|
|
@@ -309,7 +309,7 @@ export async function planAngles(brief: Brief): Promise<string[]> {
|
|
|
309
309
|
* at the end, which is where it becomes small enough to journal.
|
|
310
310
|
*/
|
|
311
311
|
export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
312
|
-
await
|
|
312
|
+
await stepReport(`Looking into: ${angle}`);
|
|
313
313
|
const seen: string[] = [];
|
|
314
314
|
const sources: Source[] = [];
|
|
315
315
|
|
|
@@ -323,7 +323,7 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
|
323
323
|
continue;
|
|
324
324
|
}
|
|
325
325
|
if (action.action === "read" && action.url) {
|
|
326
|
-
await
|
|
326
|
+
await stepReport(`Reading ${hostname(action.url)}`);
|
|
327
327
|
seen.push(`PAGE ${action.url}\n${await readPage(action.url)}`);
|
|
328
328
|
continue;
|
|
329
329
|
}
|
|
@@ -335,8 +335,6 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
|
335
335
|
return await compress(angle, seen, sources);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
/** UNUSED-ORPHAN-MARKER Retries beyond the default: the far side is a search engine and a model. */
|
|
339
|
-
|
|
340
338
|
/**
|
|
341
339
|
* The supervisor's second look.
|
|
342
340
|
*
|
|
@@ -346,12 +344,12 @@ export async function investigate(brief: Brief, angle: string): Promise<Note> {
|
|
|
346
344
|
*/
|
|
347
345
|
export async function findGaps(brief: Brief, notes: readonly Note[]): Promise<string[]> {
|
|
348
346
|
if (notes.length === 0) return [];
|
|
349
|
-
const parsed = await
|
|
347
|
+
const parsed = await stepGenerateJsonOrFail(
|
|
350
348
|
`${briefText(brief)}\n\nWhat came back:\n${notes.map(noteText).join("\n\n")}`,
|
|
351
349
|
{ system: GAPS_SYSTEM, schema: AnglesReply },
|
|
352
350
|
);
|
|
353
351
|
const gaps = parsed.angles.slice(0, MAX_ANGLES - 1);
|
|
354
|
-
await
|
|
352
|
+
await stepReport(
|
|
355
353
|
gaps.length === 0
|
|
356
354
|
? "The brief is covered; writing it up."
|
|
357
355
|
: `Following up ${gaps.length} ${plural(gaps.length, "gap")}.`,
|
|
@@ -371,12 +369,12 @@ export async function writeReport(
|
|
|
371
369
|
brief: Brief,
|
|
372
370
|
notes: readonly Note[],
|
|
373
371
|
): Promise<{ report: string; summary: string }> {
|
|
374
|
-
await
|
|
375
|
-
const written = await
|
|
372
|
+
await stepReport(`Writing up ${notes.length} ${plural(notes.length, "angle")}.`);
|
|
373
|
+
const written = await stepGenerateOrFail(
|
|
376
374
|
`${briefText(brief)}\n\nFindings:\n${notes.map(noteText).join("\n\n")}`,
|
|
377
375
|
{ system: REPORT_SYSTEM },
|
|
378
376
|
);
|
|
379
|
-
const summary = await
|
|
377
|
+
const summary = await stepGenerateOrFail(`Topic: ${topic}\n\nReport:\n${written}`, {
|
|
380
378
|
system: BRIEF_SUMMARY_SYSTEM,
|
|
381
379
|
});
|
|
382
380
|
return { report: written, summary };
|
|
@@ -390,7 +388,7 @@ export async function writeReport(
|
|
|
390
388
|
* parameters carry `_` for the same reason.
|
|
391
389
|
*/
|
|
392
390
|
export async function file(_requestedBy: string, _topic: string): Promise<string> {
|
|
393
|
-
await
|
|
391
|
+
await stepReport("Filing the findings.");
|
|
394
392
|
return "filed";
|
|
395
393
|
}
|
|
396
394
|
|
|
@@ -406,7 +404,7 @@ async function nextAction(
|
|
|
406
404
|
seen: readonly string[],
|
|
407
405
|
left: number,
|
|
408
406
|
): Promise<Action> {
|
|
409
|
-
return await
|
|
407
|
+
return await stepGenerateJsonOrFail(
|
|
410
408
|
`${briefText(brief)}\n\nYour angle: ${angle}\n` +
|
|
411
409
|
`Actions left: ${left}\n\n` +
|
|
412
410
|
(seen.length === 0 ? "You have not looked at anything yet." : seen.join("\n\n")),
|
|
@@ -423,7 +421,7 @@ async function nextAction(
|
|
|
423
421
|
* pages exist" and gets run again, differently worded, until the budget is gone.
|
|
424
422
|
*/
|
|
425
423
|
async function search(query: string): Promise<{ summary: string; sources: Source[] }> {
|
|
426
|
-
await
|
|
424
|
+
await stepReport(`Searching: ${query}`);
|
|
427
425
|
try {
|
|
428
426
|
const results = await webSearch<{ results?: { title?: string; url?: string }[] }>({
|
|
429
427
|
query,
|
|
@@ -445,7 +443,7 @@ async function search(query: string): Promise<{ summary: string; sources: Source
|
|
|
445
443
|
};
|
|
446
444
|
} catch (err: unknown) {
|
|
447
445
|
const summary = `That search failed: ${errorMessage(err)}`;
|
|
448
|
-
await
|
|
446
|
+
await stepReport(summary);
|
|
449
447
|
return { summary, sources: [] };
|
|
450
448
|
}
|
|
451
449
|
}
|
|
@@ -475,7 +473,7 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
|
|
|
475
473
|
if (seen.length === 0) {
|
|
476
474
|
return { angle, findings: "Nothing was found on this angle.", sources: [] };
|
|
477
475
|
}
|
|
478
|
-
const parsed = await
|
|
476
|
+
const parsed = await stepGenerateJsonOrFail(`Angle: ${angle}\n\n${seen.join("\n\n")}`, {
|
|
479
477
|
system: COMPRESS_SYSTEM,
|
|
480
478
|
schema: CompressReply,
|
|
481
479
|
});
|
|
@@ -492,11 +490,11 @@ async function compress(angle: string, seen: readonly string[], sources: Source[
|
|
|
492
490
|
//
|
|
493
491
|
// There is none left, and its absence is the point. This desk carried an `ask()`
|
|
494
492
|
// and an `askJson()` whose whole body was `.catch(throwStepError)`; the SDK's
|
|
495
|
-
// `
|
|
493
|
+
// `stepGenerateOrFail` and `stepGenerateJsonOrFail`
|
|
496
494
|
// (`@alexkroman1/aai/step-errors`) ARE that call — the `/step` one with the
|
|
497
495
|
// gateway's verdict classified, so a terminal failure stays terminal and a rate
|
|
498
496
|
// limit becomes a `RetryableError` carrying the delay the gateway itself named.
|
|
499
|
-
// `
|
|
497
|
+
// `stepGenerateJsonOrFail` also owns the four things every JSON stage used
|
|
500
498
|
// to re-derive — unwrap the fence, parse, reject a non-object, check the shape —
|
|
501
499
|
// and throws PLAINLY when any of them misses, which is what makes a malformed
|
|
502
500
|
// reply a retry rather than a failure.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
3
|
-
import { createToolContext,
|
|
3
|
+
import { createToolContext, expectToolOk } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
import type { AuthResult } from "./authenticate.ts";
|
|
6
6
|
import type { StagedResult } from "./pending.ts";
|
|
@@ -32,7 +32,7 @@ function makeCtx(): ToolContext {
|
|
|
32
32
|
/** A context already authenticated as `userId`, via the real tool. */
|
|
33
33
|
async function authedCtx(email: string): Promise<ToolContext> {
|
|
34
34
|
const ctx = makeCtx();
|
|
35
|
-
|
|
35
|
+
expectToolOk<AuthResult>(await findUserIdByEmail.execute({ email }, ctx));
|
|
36
36
|
return ctx;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -47,19 +47,19 @@ async function authedCtx(email: string): Promise<ToolContext> {
|
|
|
47
47
|
* would let this gate pass while carrying nothing to confirm.
|
|
48
48
|
*/
|
|
49
49
|
async function confirmed<R = unknown>(staged: unknown, ctx: ToolContext): Promise<R> {
|
|
50
|
-
const stage =
|
|
50
|
+
const stage = expectToolOk<StagedResult>(staged);
|
|
51
51
|
// A throw rather than an `expect`, which biome's `noMisplacedAssertion`
|
|
52
52
|
// rightly refuses outside a test body. A staged change with no sentence to
|
|
53
53
|
// read back is the one shape that would let the gate pass while carrying
|
|
54
54
|
// nothing to confirm, so it stops the spec here rather than downstream.
|
|
55
55
|
if (stage.read_back.length === 0) throw new Error(`${stage.staged} staged with no readback`);
|
|
56
|
-
return
|
|
56
|
+
return expectToolOk<R>(await confirmChange.execute({}, ctx));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
describe("authentication", () => {
|
|
60
60
|
test("find_user_id_by_email is case-insensitive and authenticates the session", async () => {
|
|
61
61
|
const ctx = makeCtx();
|
|
62
|
-
const result =
|
|
62
|
+
const result = expectToolOk<AuthResult>(
|
|
63
63
|
await findUserIdByEmail.execute({ email: "OLIVIA.ITO5204@EXAMPLE.COM" }, ctx),
|
|
64
64
|
);
|
|
65
65
|
expect(result.user_id).toBe("olivia_ito_3591");
|
|
@@ -81,7 +81,7 @@ describe("authentication", () => {
|
|
|
81
81
|
|
|
82
82
|
test("find_user_id_by_name_zip is case-insensitive on names and exact on zip", async () => {
|
|
83
83
|
const ctx = makeCtx();
|
|
84
|
-
const found =
|
|
84
|
+
const found = expectToolOk<AuthResult>(
|
|
85
85
|
await findUserIdByNameZip.execute(
|
|
86
86
|
{ first_name: "aarav", last_name: "ANDERSON", zip: "19031" },
|
|
87
87
|
ctx,
|
|
@@ -97,13 +97,13 @@ describe("authentication", () => {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
test("two customers share a first name — the zip is what separates them", async () => {
|
|
100
|
-
const a =
|
|
100
|
+
const a = expectToolOk<AuthResult>(
|
|
101
101
|
await findUserIdByNameZip.execute(
|
|
102
102
|
{ first_name: "Aarav", last_name: "Anderson", zip: "19031" },
|
|
103
103
|
makeCtx(),
|
|
104
104
|
),
|
|
105
105
|
);
|
|
106
|
-
const b =
|
|
106
|
+
const b = expectToolOk<AuthResult>(
|
|
107
107
|
await findUserIdByNameZip.execute(
|
|
108
108
|
{ first_name: "Aarav", last_name: "Gonzalez", zip: "78268" },
|
|
109
109
|
makeCtx(),
|
|
@@ -172,7 +172,7 @@ interface ProductTypesResult {
|
|
|
172
172
|
describe("read tools", () => {
|
|
173
173
|
test("get_user_details returns the caller's profile with gift-card balances", async () => {
|
|
174
174
|
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
175
|
-
const result =
|
|
175
|
+
const result = expectToolOk<UserDetailsResult>(
|
|
176
176
|
await getUserDetails.execute({ user_id: "olivia_ito_3591" }, ctx),
|
|
177
177
|
);
|
|
178
178
|
expect(result.email).toBe("olivia.ito5204@example.com");
|
|
@@ -189,7 +189,7 @@ describe("read tools", () => {
|
|
|
189
189
|
|
|
190
190
|
test("get_order_details resolves shorthand and sets focus", async () => {
|
|
191
191
|
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
192
|
-
const result =
|
|
192
|
+
const result = expectToolOk<OrderDetailsResult>(
|
|
193
193
|
await getOrderDetails.execute({ order_id: "the delivered one" }, ctx),
|
|
194
194
|
);
|
|
195
195
|
expect(result.order_id).toBe("#W5866402");
|
|
@@ -203,7 +203,7 @@ describe("read tools", () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
test("get_product_details lists variants and needs no authentication", async () => {
|
|
206
|
-
const result =
|
|
206
|
+
const result = expectToolOk<ProductDetailsResult>(
|
|
207
207
|
await getProductDetails.execute({ product_id: "9832717871" }, makeCtx()),
|
|
208
208
|
);
|
|
209
209
|
expect(result.name).toBe("Tea Kettle");
|
|
@@ -217,7 +217,7 @@ describe("read tools", () => {
|
|
|
217
217
|
});
|
|
218
218
|
|
|
219
219
|
test("get_item_details resolves an item without knowing its product", async () => {
|
|
220
|
-
const result =
|
|
220
|
+
const result = expectToolOk<ItemDetailsResult>(
|
|
221
221
|
await getItemDetails.execute({ item_id: "3909406921" }, makeCtx()),
|
|
222
222
|
);
|
|
223
223
|
expect(result.price).toBe(98.25);
|
|
@@ -225,7 +225,9 @@ describe("read tools", () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
test("list_all_product_types returns all 50, sorted by name", async () => {
|
|
228
|
-
const result =
|
|
228
|
+
const result = expectToolOk<ProductTypesResult>(
|
|
229
|
+
await listAllProductTypes.execute({}, makeCtx()),
|
|
230
|
+
);
|
|
229
231
|
expect(Object.keys(result.products)).toHaveLength(50);
|
|
230
232
|
const names = Object.keys(result.products);
|
|
231
233
|
// localeCompare, not the default lexicographic sort — the tool sorts for a
|
|
@@ -1148,7 +1150,7 @@ interface TransferResult {
|
|
|
1148
1150
|
|
|
1149
1151
|
describe("transfer_to_human_agents", () => {
|
|
1150
1152
|
test("works without authentication — the escape hatch cannot be gated", async () => {
|
|
1151
|
-
const result =
|
|
1153
|
+
const result = expectToolOk<TransferResult>(
|
|
1152
1154
|
await transferToHumanAgents.execute(
|
|
1153
1155
|
{ summary: "Caller wants to dispute a charge from 2019." },
|
|
1154
1156
|
makeCtx(),
|
|
@@ -1160,7 +1162,7 @@ describe("transfer_to_human_agents", () => {
|
|
|
1160
1162
|
|
|
1161
1163
|
test("the handoff is terminal, so the call cannot be worked afterwards", async () => {
|
|
1162
1164
|
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
1163
|
-
|
|
1165
|
+
expectToolOk(await transferToHumanAgents.execute({ summary: "wants a human" }, ctx));
|
|
1164
1166
|
|
|
1165
1167
|
const at = callFlow.position(ctx);
|
|
1166
1168
|
expect(at.state).toBe("transferred");
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
|
|
13
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
13
14
|
import { creditRefund, REFUND_DELAY_NOTE, REFUND_IMMEDIATE_NOTE } from "./refund.ts";
|
|
14
15
|
import { resolveOrder } from "./resolve.ts";
|
|
15
16
|
import type { RetailState } from "./shared.ts";
|
|
16
|
-
import {
|
|
17
|
+
import { money } from "./store.ts";
|
|
17
18
|
|
|
18
19
|
/** The only two reasons the store records. */
|
|
19
20
|
export const CANCEL_REASONS = ["no longer needed", "ordered by mistake"] as const;
|
|
@@ -42,9 +43,9 @@ export function planCancel(
|
|
|
42
43
|
spokenOrderId: string,
|
|
43
44
|
reason: CancelReason,
|
|
44
45
|
): CancelPlan | ToolFailure {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// No `authenticatedUser` gate here: `resolveOrder` opens with the identical
|
|
47
|
+
// call and returns the identical failure, so a second one is a second store
|
|
48
|
+
// lookup for a value this planner never reads.
|
|
48
49
|
const order = resolveOrder(state, spokenOrderId);
|
|
49
50
|
if (isToolFailure(order)) return order;
|
|
50
51
|
|
|
@@ -75,7 +76,7 @@ export function planCancel(
|
|
|
75
76
|
return {
|
|
76
77
|
readBack:
|
|
77
78
|
`cancel order ${order.order_id} (${items}) as '${reason}' and refund ` +
|
|
78
|
-
|
|
79
|
+
`${formatMoney(total)} to ${destinations}`,
|
|
79
80
|
orderId: order.order_id,
|
|
80
81
|
reason,
|
|
81
82
|
refunds,
|
|
@@ -109,7 +110,7 @@ export function applyCancel(state: RetailState, plan: CancelPlan) {
|
|
|
109
110
|
cancel_reason: plan.reason,
|
|
110
111
|
refunded: plan.total,
|
|
111
112
|
refund_immediate: immediate,
|
|
112
|
-
message: `Order ${plan.orderId} is cancelled and
|
|
113
|
+
message: `Order ${plan.orderId} is cancelled and ${formatMoney(plan.total)} is being refunded. ${
|
|
113
114
|
immediate ? REFUND_IMMEDIATE_NOTE : REFUND_DELAY_NOTE
|
|
114
115
|
}`,
|
|
115
116
|
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { formatMoney, plural } from "@alexkroman1/aai/utils";
|
|
1
2
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import type { AgentState, ConversationItem
|
|
3
|
+
import type { AgentState, ConversationItem } from "@alexkroman1/aai-ui";
|
|
3
4
|
import {
|
|
4
5
|
AutoScroll,
|
|
5
|
-
|
|
6
|
+
mountClient,
|
|
7
|
+
SessionErrorBanner,
|
|
6
8
|
useAgentState,
|
|
7
9
|
useConversation,
|
|
8
|
-
|
|
10
|
+
useSessionActions,
|
|
9
11
|
useSessionSelector,
|
|
12
|
+
useSessionStatus,
|
|
10
13
|
} from "@alexkroman1/aai-ui";
|
|
11
14
|
import type { ReactNode } from "react";
|
|
12
15
|
import type {
|
|
@@ -100,7 +103,7 @@ function MethodRow({ method }: { method: PaymentMethodView }) {
|
|
|
100
103
|
</span>
|
|
101
104
|
) : (
|
|
102
105
|
<span className="font-semibold tabular-nums" style={{ color: "#15803d" }}>
|
|
103
|
-
|
|
106
|
+
{formatMoney(method.balance)}
|
|
104
107
|
</span>
|
|
105
108
|
)}
|
|
106
109
|
</div>
|
|
@@ -132,8 +135,7 @@ function OrderCard({ order, focused }: { order: OrderView; focused: boolean }) {
|
|
|
132
135
|
</span>
|
|
133
136
|
</div>
|
|
134
137
|
<div className="text-[11px] mb-1 tabular-nums" style={{ color: "#71717a" }}>
|
|
135
|
-
|
|
136
|
-
{order.items.length === 1 ? "" : "s"}
|
|
138
|
+
{formatMoney(order.total)} · {order.items.length} {plural(order.items.length, "item")}
|
|
137
139
|
</div>
|
|
138
140
|
{order.items.slice(0, 3).map((item) => (
|
|
139
141
|
<div
|
|
@@ -246,7 +248,7 @@ function SwapOptions({ option }: { option: SwapOptionView }) {
|
|
|
246
248
|
>
|
|
247
249
|
<span>{Object.values(alternative.options).join(", ")}</span>
|
|
248
250
|
<span className="tabular-nums whitespace-nowrap" style={{ color: "#71717a" }}>
|
|
249
|
-
|
|
251
|
+
{formatMoney(alternative.price)}
|
|
250
252
|
</span>
|
|
251
253
|
</div>
|
|
252
254
|
))
|
|
@@ -344,8 +346,16 @@ function Conversation() {
|
|
|
344
346
|
{streaming}
|
|
345
347
|
</div>
|
|
346
348
|
)}
|
|
349
|
+
{/* Same contract as the shipped `MessageList`'s indicator: three pulsing
|
|
350
|
+
dots are the only sign the agent is working, and to a screen reader
|
|
351
|
+
they are punctuation. */}
|
|
347
352
|
{thinking && (
|
|
348
|
-
<div
|
|
353
|
+
<div
|
|
354
|
+
role="status"
|
|
355
|
+
aria-label="Agent is thinking"
|
|
356
|
+
className="self-start text-[11px] px-3.5"
|
|
357
|
+
style={{ color: "#a1a1aa" }}
|
|
358
|
+
>
|
|
349
359
|
<span style={{ animation: "rt-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
|
|
350
360
|
</div>
|
|
351
361
|
)}
|
|
@@ -370,7 +380,7 @@ function Conversation() {
|
|
|
370
380
|
/** The live status dot, on its own subscription rather than a field off a
|
|
371
381
|
* whole-page read — the header is the only thing here that wants it. */
|
|
372
382
|
function StatusReadout() {
|
|
373
|
-
const state =
|
|
383
|
+
const state = useSessionStatus();
|
|
374
384
|
return (
|
|
375
385
|
<>
|
|
376
386
|
<span
|
|
@@ -393,58 +403,29 @@ function StatusReadout() {
|
|
|
393
403
|
);
|
|
394
404
|
}
|
|
395
405
|
|
|
396
|
-
|
|
397
|
-
* The
|
|
406
|
+
/*
|
|
407
|
+
* The call controls.
|
|
398
408
|
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* `ConsoleShell` carries it, and this template's two-pane page cannot use that
|
|
404
|
-
* shell as its frame; the attribute is what it is here for.
|
|
409
|
+
* The methods come off `useSessionActions()`, which does not subscribe, and the
|
|
410
|
+
* two flags off one-field selectors. The whole-snapshot `useSession()` this
|
|
411
|
+
* replaced re-rendered these four buttons on every STT partial to read two
|
|
412
|
+
* booleans that change once a call.
|
|
405
413
|
*/
|
|
406
|
-
function ErrorBanner() {
|
|
407
|
-
const error = useSessionSelector((s) => s.error);
|
|
408
|
-
if (!error) return null;
|
|
409
|
-
return (
|
|
410
|
-
<div
|
|
411
|
-
role="alert"
|
|
412
|
-
className="px-4 py-2 text-xs"
|
|
413
|
-
style={{ background: "#fef2f2", color: "#b91c1c", borderTop: "1px solid #fecaca" }}
|
|
414
|
-
>
|
|
415
|
-
{error.message} ({error.code})
|
|
416
|
-
</div>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Start a fresh conversation without leaving the console.
|
|
422
|
-
*
|
|
423
|
-
* Written out here rather than reached for on the session, because there is no
|
|
424
|
-
* one method that does it: `reset()` clears the CONVERSATION and keeps the
|
|
425
|
-
* session, which is wrong for any agent that also keeps session-scoped state —
|
|
426
|
-
* this one's store would come back with the next tool call.
|
|
427
|
-
*/
|
|
428
|
-
function newConversation(session: Session): void {
|
|
429
|
-
session.end();
|
|
430
|
-
session.start();
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/** The call controls: the one place a whole-session read is what is wanted —
|
|
434
|
-
* `started`, `running` and four methods, in four buttons. */
|
|
435
414
|
function CallControls({ productCount }: { productCount: number }) {
|
|
436
|
-
const
|
|
415
|
+
const { start, toggle, restart, end } = useSessionActions();
|
|
416
|
+
const started = useSessionSelector((s) => s.started);
|
|
417
|
+
const running = useSessionSelector((s) => s.running);
|
|
437
418
|
return (
|
|
438
419
|
<div
|
|
439
420
|
className="flex items-center gap-2 px-4 py-3"
|
|
440
421
|
style={{ background: "#ffffff", borderTop: "1px solid #e4e4e7" }}
|
|
441
422
|
>
|
|
442
|
-
{!
|
|
423
|
+
{!started ? (
|
|
443
424
|
<button
|
|
444
425
|
type="button"
|
|
445
426
|
className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
|
|
446
427
|
style={{ background: "#2563eb" }}
|
|
447
|
-
onClick={() =>
|
|
428
|
+
onClick={() => start()}
|
|
448
429
|
>
|
|
449
430
|
Start call
|
|
450
431
|
</button>
|
|
@@ -454,12 +435,12 @@ function CallControls({ productCount }: { productCount: number }) {
|
|
|
454
435
|
type="button"
|
|
455
436
|
className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer"
|
|
456
437
|
style={{
|
|
457
|
-
background:
|
|
458
|
-
color:
|
|
438
|
+
background: running ? "#e4e4e7" : "#2563eb",
|
|
439
|
+
color: running ? "#18181b" : "#ffffff",
|
|
459
440
|
}}
|
|
460
|
-
onClick={() =>
|
|
441
|
+
onClick={() => toggle()}
|
|
461
442
|
>
|
|
462
|
-
{
|
|
443
|
+
{running ? "Hold" : "Resume"}
|
|
463
444
|
</button>
|
|
464
445
|
{/* The one-click new conversation the default shell's
|
|
465
446
|
`<Controls>` gives every other template — a custom
|
|
@@ -472,7 +453,7 @@ function CallControls({ productCount }: { productCount: number }) {
|
|
|
472
453
|
type="button"
|
|
473
454
|
className="px-4 py-2 rounded-md text-xs font-semibold cursor-pointer"
|
|
474
455
|
style={{ background: "#ffffff", color: "#18181b", border: "1px solid #e4e4e7" }}
|
|
475
|
-
onClick={
|
|
456
|
+
onClick={restart}
|
|
476
457
|
>
|
|
477
458
|
New Conversation
|
|
478
459
|
</button>
|
|
@@ -484,7 +465,7 @@ function CallControls({ productCount }: { productCount: number }) {
|
|
|
484
465
|
type="button"
|
|
485
466
|
className="px-4 py-2 border-none rounded-md text-xs font-semibold cursor-pointer text-white"
|
|
486
467
|
style={{ background: "#dc2626" }}
|
|
487
|
-
onClick={() =>
|
|
468
|
+
onClick={() => end()}
|
|
488
469
|
>
|
|
489
470
|
End
|
|
490
471
|
</button>
|
|
@@ -545,7 +526,7 @@ function App() {
|
|
|
545
526
|
style={{ borderRight: "1px solid #e4e4e7" }}
|
|
546
527
|
>
|
|
547
528
|
<Conversation />
|
|
548
|
-
<
|
|
529
|
+
<SessionErrorBanner className="rounded-none border-x-0 border-b-0" />
|
|
549
530
|
<CallControls productCount={view.productCount} />
|
|
550
531
|
</div>
|
|
551
532
|
|
|
@@ -642,7 +623,7 @@ function App() {
|
|
|
642
623
|
)}
|
|
643
624
|
</span>
|
|
644
625
|
<span className="tabular-nums whitespace-nowrap ml-3">
|
|
645
|
-
{view.callSeq}
|
|
626
|
+
{view.callSeq} {plural(view.callSeq, "call")}
|
|
646
627
|
</span>
|
|
647
628
|
</div>
|
|
648
629
|
</div>
|
|
@@ -650,7 +631,7 @@ function App() {
|
|
|
650
631
|
);
|
|
651
632
|
}
|
|
652
633
|
|
|
653
|
-
|
|
634
|
+
mountClient({
|
|
654
635
|
component: App,
|
|
655
636
|
theme: {
|
|
656
637
|
bg: "#f4f4f5",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
|
|
7
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
7
8
|
import { resolveOrder } from "./resolve.ts";
|
|
8
9
|
import type { RetailState } from "./shared.ts";
|
|
9
10
|
import { authenticatedUser, findPaymentMethod, isGiftCard, money } from "./store.ts";
|
|
@@ -53,13 +54,13 @@ export function planPayment(
|
|
|
53
54
|
const amount = original.amount;
|
|
54
55
|
if (isGiftCard(newMethod) && newMethod.balance < amount) {
|
|
55
56
|
return {
|
|
56
|
-
error: `Gift card ${newMethodId}'s balance (
|
|
57
|
+
error: `Gift card ${newMethodId}'s balance (${formatMoney(newMethod.balance)}) does not cover the ${formatMoney(amount)} order total.`,
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
return {
|
|
61
62
|
readBack:
|
|
62
|
-
`charge order ${order.order_id} —
|
|
63
|
+
`charge order ${order.order_id} — ${formatMoney(amount)} — to ${newMethodId} instead, ` +
|
|
63
64
|
`refunding ${original.payment_method_id}`,
|
|
64
65
|
orderId: order.order_id,
|
|
65
66
|
newMethodId,
|
|
@@ -102,6 +103,6 @@ export function applyPayment(state: RetailState, plan: PaymentPlan) {
|
|
|
102
103
|
amount: plan.amount,
|
|
103
104
|
paid_with: plan.newMethodId,
|
|
104
105
|
refunded_to: plan.oldMethodId,
|
|
105
|
-
message: `Order ${plan.orderId} is now charged to ${plan.newMethodId}, and
|
|
106
|
+
message: `Order ${plan.orderId} is now charged to ${plan.newMethodId}, and ${formatMoney(plan.amount)} is going back to ${plan.oldMethodId}.`,
|
|
106
107
|
};
|
|
107
108
|
}
|