@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
|
@@ -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
|
}
|
|
@@ -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, expectDialogOk, expectToolOk } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
import type { StagedResult } from "./pending.ts";
|
|
6
6
|
import { retailSlot } from "./store.ts";
|
|
@@ -17,7 +17,7 @@ import transferToHumanAgents from "./tools/transfer_to_human_agents.ts";
|
|
|
17
17
|
* one delivered order (#W4316152), which is every shape these cases need. */
|
|
18
18
|
async function aaravCtx(): Promise<ToolContext> {
|
|
19
19
|
const ctx = createToolContext();
|
|
20
|
-
|
|
20
|
+
expectToolOk(await findUserIdByEmail.execute({ email: "aarav.anderson9752@example.com" }, ctx));
|
|
21
21
|
return ctx;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ describe("staging", () => {
|
|
|
29
29
|
const ctx = await aaravCtx();
|
|
30
30
|
const before = structuredClone(retailSlot.get(ctx).store.orders["#W9300146"]);
|
|
31
31
|
|
|
32
|
-
const staged =
|
|
32
|
+
const staged = expectDialogOk<StagedResult>(await stageCancel(ctx));
|
|
33
33
|
|
|
34
34
|
expect(staged.state).toBe("serving.awaitingConfirmation");
|
|
35
35
|
expect(staged.result.staged).toBe("cancel_pending_order");
|
|
@@ -43,7 +43,7 @@ describe("staging", () => {
|
|
|
43
43
|
|
|
44
44
|
test("the readback names the order, the items, the amount and where the money goes", async () => {
|
|
45
45
|
const ctx = await aaravCtx();
|
|
46
|
-
const staged =
|
|
46
|
+
const staged = expectToolOk<StagedResult>(await stageCancel(ctx));
|
|
47
47
|
expect(staged.read_back).toContain("#W9300146");
|
|
48
48
|
expect(staged.read_back).toContain("153.23");
|
|
49
49
|
expect(staged.read_back).toContain("gift_card_7245904");
|
|
@@ -52,14 +52,14 @@ describe("staging", () => {
|
|
|
52
52
|
|
|
53
53
|
test("the result says out loud that nothing has happened", async () => {
|
|
54
54
|
const ctx = await aaravCtx();
|
|
55
|
-
const staged =
|
|
55
|
+
const staged = expectToolOk<StagedResult>(await stageCancel(ctx));
|
|
56
56
|
expect(staged.message).toContain("NOTHING HAS CHANGED YET");
|
|
57
57
|
expect(staged.message).toContain("confirm_change");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("a second stage is refused, and the refusal names the one already waiting", async () => {
|
|
61
61
|
const ctx = await aaravCtx();
|
|
62
|
-
|
|
62
|
+
expectToolOk(await stageCancel(ctx));
|
|
63
63
|
|
|
64
64
|
// A DIFFERENT change, so this is not a repeat of the first call.
|
|
65
65
|
const second = await modifyUserAddress.execute(
|
|
@@ -89,9 +89,9 @@ describe("staging", () => {
|
|
|
89
89
|
describe("confirming", () => {
|
|
90
90
|
test("confirm_change applies the staged change and returns to helping", async () => {
|
|
91
91
|
const ctx = await aaravCtx();
|
|
92
|
-
|
|
92
|
+
expectToolOk(await stageCancel(ctx));
|
|
93
93
|
|
|
94
|
-
const done =
|
|
94
|
+
const done = expectDialogOk<{ confirmed: string; status: string }>(
|
|
95
95
|
await confirmChange.execute({}, ctx),
|
|
96
96
|
);
|
|
97
97
|
expect(done.state).toBe("serving.helping");
|
|
@@ -114,8 +114,8 @@ describe("confirming", () => {
|
|
|
114
114
|
|
|
115
115
|
test("confirming twice is refused — the second call has nothing staged", async () => {
|
|
116
116
|
const ctx = await aaravCtx();
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
expectToolOk(await stageCancel(ctx));
|
|
118
|
+
expectToolOk(await confirmChange.execute({}, ctx));
|
|
119
119
|
|
|
120
120
|
const again = await confirmChange.execute({}, ctx);
|
|
121
121
|
expect(isToolFailure(again)).toBe(true);
|
|
@@ -130,9 +130,9 @@ describe("confirming", () => {
|
|
|
130
130
|
describe("cancelling a staged change", () => {
|
|
131
131
|
test("cancel_change drops it, changes nothing, and frees the call", async () => {
|
|
132
132
|
const ctx = await aaravCtx();
|
|
133
|
-
|
|
133
|
+
expectToolOk(await stageCancel(ctx));
|
|
134
134
|
|
|
135
|
-
const dropped =
|
|
135
|
+
const dropped = expectDialogOk<{ dropped: string | null; message: string }>(
|
|
136
136
|
await cancelChange.execute({}, ctx),
|
|
137
137
|
);
|
|
138
138
|
expect(dropped.state).toBe("serving.helping");
|
|
@@ -144,12 +144,12 @@ describe("cancelling a staged change", () => {
|
|
|
144
144
|
|
|
145
145
|
test("a corrected change can be staged straight after", async () => {
|
|
146
146
|
const ctx = await aaravCtx();
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
expectToolOk(await stageCancel(ctx));
|
|
148
|
+
expectToolOk(await cancelChange.execute({}, ctx));
|
|
149
149
|
|
|
150
150
|
// The caller changed their mind about which order — the very thing
|
|
151
151
|
// `cancel_change` exists for.
|
|
152
|
-
const restaged =
|
|
152
|
+
const restaged = expectToolOk<StagedResult>(
|
|
153
153
|
await returnDeliveredOrderItems.execute(
|
|
154
154
|
{
|
|
155
155
|
order_id: "#W4316152",
|
|
@@ -166,9 +166,9 @@ describe("cancelling a staged change", () => {
|
|
|
166
166
|
describe("what stays legal while a change waits", () => {
|
|
167
167
|
test("a read is still answerable — 'what was the total again?'", async () => {
|
|
168
168
|
const ctx = await aaravCtx();
|
|
169
|
-
|
|
169
|
+
expectToolOk(await stageCancel(ctx));
|
|
170
170
|
// `when: "serving"` matches both children, which is what makes this work.
|
|
171
|
-
const read =
|
|
171
|
+
const read = expectDialogOk<{ order_id: string }>(
|
|
172
172
|
await getOrderDetails.execute({ order_id: "#W9300146" }, ctx),
|
|
173
173
|
);
|
|
174
174
|
expect(read.result.order_id).toBe("#W9300146");
|
|
@@ -178,9 +178,9 @@ describe("what stays legal while a change waits", () => {
|
|
|
178
178
|
|
|
179
179
|
test("a caller can still ask for a human, and everything refuses afterwards", async () => {
|
|
180
180
|
const ctx = await aaravCtx();
|
|
181
|
-
|
|
181
|
+
expectToolOk(await stageCancel(ctx));
|
|
182
182
|
|
|
183
|
-
const transferred =
|
|
183
|
+
const transferred = expectDialogOk(
|
|
184
184
|
await transferToHumanAgents.execute({ summary: "wants a human" }, ctx),
|
|
185
185
|
);
|
|
186
186
|
// `TRANSFERRED` is declared on the `serving` PARENT, which is what lets it
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
2
|
-
import { createToolContext,
|
|
2
|
+
import { createToolContext, expectDialogOk, expectToolOk } from "@alexkroman1/aai/testing";
|
|
3
3
|
import { describe, expect, test } from "vitest";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import {
|
|
@@ -237,16 +237,16 @@ describe("retailTool", () => {
|
|
|
237
237
|
test("runs once the flow says serving", async () => {
|
|
238
238
|
const ctx = makeCtx();
|
|
239
239
|
serve(ctx);
|
|
240
|
-
expect(
|
|
240
|
+
expect(expectToolOk(await gated.execute({}, ctx))).toEqual({ ok: true });
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
test("the result carries the position the call landed in", async () => {
|
|
244
244
|
const ctx = makeCtx();
|
|
245
245
|
serve(ctx);
|
|
246
|
-
// `
|
|
246
|
+
// `expectDialogOk` rather than a cast to `{ instruction?: string }`: it keeps
|
|
247
247
|
// the envelope this test is about, and a refusal fails here naming what the
|
|
248
248
|
// flow said instead of reading `undefined` off a field nobody assigned.
|
|
249
|
-
const answered =
|
|
249
|
+
const answered = expectDialogOk<{ ok: boolean }>(await gated.execute({}, ctx));
|
|
250
250
|
expect(answered).toMatchObject({ state: "serving.helping", done: false });
|
|
251
251
|
expect(answered.instruction).toMatch(/one identified customer/);
|
|
252
252
|
});
|
|
@@ -307,7 +307,7 @@ describe("the call flow", () => {
|
|
|
307
307
|
summary: () => "ran",
|
|
308
308
|
execute: () => ({ ok: true }),
|
|
309
309
|
});
|
|
310
|
-
expect(
|
|
310
|
+
expect(expectToolOk(await anywhere.execute({}, ctx))).toEqual({ ok: true });
|
|
311
311
|
|
|
312
312
|
const at = callFlow.send(ctx, { type: "TRANSFERRED" });
|
|
313
313
|
expect(at.state).toBe("transferred");
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
|
|
10
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
10
11
|
import { resolveOrder } from "./resolve.ts";
|
|
11
12
|
import type { Order, OrderItem, RetailState, User, Variant } from "./shared.ts";
|
|
12
13
|
import {
|
|
@@ -48,7 +49,6 @@ export interface SwapLine {
|
|
|
48
49
|
fromOptions: Record<string, string>;
|
|
49
50
|
fromPrice: number;
|
|
50
51
|
toItemId: string;
|
|
51
|
-
toName: string;
|
|
52
52
|
toOptions: Record<string, string>;
|
|
53
53
|
toPrice: number;
|
|
54
54
|
}
|
|
@@ -146,9 +146,6 @@ export function toSwapLines(plan: SwapPlan): SwapLine[] {
|
|
|
146
146
|
fromOptions: { ...pair.item.options },
|
|
147
147
|
fromPrice: pair.item.price,
|
|
148
148
|
toItemId: pair.newVariant.item_id,
|
|
149
|
-
// Same product by construction — a swap can only reach another option of
|
|
150
|
-
// it — so the name is the item's own and the OPTIONS are what differ.
|
|
151
|
-
toName: pair.item.name,
|
|
152
149
|
toOptions: { ...pair.newVariant.options },
|
|
153
150
|
toPrice: pair.newVariant.price,
|
|
154
151
|
}));
|
|
@@ -163,7 +160,7 @@ export function assertCanCoverDiff(user: User, methodId: string, diff: number):
|
|
|
163
160
|
if (isToolFailure(method)) return method;
|
|
164
161
|
if (isGiftCard(method) && method.balance < diff) {
|
|
165
162
|
return {
|
|
166
|
-
error: `Gift card ${methodId}'s balance (
|
|
163
|
+
error: `Gift card ${methodId}'s balance (${formatMoney(method.balance)}) does not cover the ${formatMoney(diff)} difference. Ask for another payment method.`,
|
|
167
164
|
};
|
|
168
165
|
}
|
|
169
166
|
return null;
|
|
@@ -190,8 +187,8 @@ function describeLine(line: SwapLine): string {
|
|
|
190
187
|
}
|
|
191
188
|
|
|
192
189
|
function describeDiff(diff: number, methodId: string): string {
|
|
193
|
-
if (diff > 0) return
|
|
194
|
-
if (diff < 0) return
|
|
190
|
+
if (diff > 0) return `${formatMoney(diff)} charged to ${methodId}`;
|
|
191
|
+
if (diff < 0) return `${formatMoney(Math.abs(diff))} refunded to ${methodId}`;
|
|
195
192
|
return "no price difference";
|
|
196
193
|
}
|
|
197
194
|
|
|
@@ -275,8 +272,8 @@ export function applyModifyItems(state: RetailState, plan: ModifyItemsPlan) {
|
|
|
275
272
|
})),
|
|
276
273
|
message:
|
|
277
274
|
plan.diff > 0
|
|
278
|
-
? `Done.
|
|
279
|
-
: `Done.
|
|
275
|
+
? `Done. ${formatMoney(plan.diff)} was charged to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`
|
|
276
|
+
: `Done. ${formatMoney(Math.abs(plan.diff))} is being refunded to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`,
|
|
280
277
|
};
|
|
281
278
|
}
|
|
282
279
|
|
|
@@ -358,7 +355,7 @@ export function applyExchange(state: RetailState, plan: ExchangePlan) {
|
|
|
358
355
|
})),
|
|
359
356
|
message:
|
|
360
357
|
plan.diff > 0
|
|
361
|
-
? `Exchange requested on ${plan.orderId}.
|
|
362
|
-
: `Exchange requested on ${plan.orderId}.
|
|
358
|
+
? `Exchange requested on ${plan.orderId}. ${formatMoney(plan.diff)} will be charged to ${plan.paymentMethodId}. An email with return instructions is on its way.`
|
|
359
|
+
: `Exchange requested on ${plan.orderId}. ${formatMoney(Math.abs(plan.diff))} will be refunded to ${plan.paymentMethodId}. An email with return instructions is on its way.`,
|
|
363
360
|
};
|
|
364
361
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* deployed.
|
|
34
34
|
*/
|
|
35
35
|
import agentDef from "virtual:aai/agent";
|
|
36
|
-
import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
36
|
+
import { type EvalTurn, toolResultIn, toolResultsIn } from "@alexkroman1/aai-runtime/eval";
|
|
37
37
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
38
38
|
import { expect } from "vitest";
|
|
39
39
|
import { z } from "zod";
|
|
@@ -82,20 +82,6 @@ const Refusal = z.object({ error: z.string() });
|
|
|
82
82
|
const answerOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T =>
|
|
83
83
|
toolResultIn(turn.toolCalls, name, schema);
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* What EVERY call to `name` answered on this turn, in call order.
|
|
87
|
-
*
|
|
88
|
-
* The plural form is for a claim about calls that may legitimately not have
|
|
89
|
-
* happened — a refusal the narrator may have pre-empted by not calling at all.
|
|
90
|
-
* `toolResultIn` over a ONE-CALL list per call: the name is that call's own, so
|
|
91
|
-
* the reader's "no such call" and "two calls" throws are unreachable and what
|
|
92
|
-
* is left is the parse, the schema, and the "never completed" failure.
|
|
93
|
-
*/
|
|
94
|
-
const answersOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T[] =>
|
|
95
|
-
turn.toolCalls
|
|
96
|
-
.filter((c) => c.name === name)
|
|
97
|
-
.map((call) => toolResultIn([call], call.name, schema));
|
|
98
|
-
|
|
99
85
|
/**
|
|
100
86
|
* Every field `setup_character` requires, for the SCRIPTED runs.
|
|
101
87
|
*
|
|
@@ -267,7 +253,7 @@ describeEval(agentDef, (test) => {
|
|
|
267
253
|
expect(answerOf(down, "update_state", Settled).state).toBe("gameOver");
|
|
268
254
|
|
|
269
255
|
const after = await session.say("I refuse to die. Roll to fight on.");
|
|
270
|
-
const attempts =
|
|
256
|
+
const attempts = toolResultsIn(after.toolCalls, "action_roll", Refusal);
|
|
271
257
|
// The gate is only OBSERVABLE if something calls the gated tool, and the
|
|
272
258
|
// script is what guarantees that — hence the exact count. `gameOver` was
|
|
273
259
|
// once a flag nothing acted on, so a player with both tracks empty could
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolContext, ToolDef, ToolInputSchema } 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, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
6
|
applyConsequences,
|
|
@@ -24,11 +24,11 @@ import updateState from "./tools/update_state.ts";
|
|
|
24
24
|
|
|
25
25
|
// ── Test doubles ─────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
/** Each call gets its own slot store, which is what makes two contexts two
|
|
28
|
+
* games. `send` is the recorder `createToolContext` installs — no test here
|
|
29
|
+
* asserts on it, and overriding it would only throw the recording away. */
|
|
30
30
|
function makeCtx(): ToolContext {
|
|
31
|
-
return createToolContext(
|
|
31
|
+
return createToolContext();
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const SETUP_ARGS = {
|
|
@@ -288,7 +288,7 @@ describe("burn_momentum", () => {
|
|
|
288
288
|
const ctx = makeCtx();
|
|
289
289
|
seedRolledState(8, ctx); // 8 beats both dice (3, 5)
|
|
290
290
|
|
|
291
|
-
const result =
|
|
291
|
+
const result = expectToolOk<Record<string, unknown>>(await callNoArgs(burnMomentum, ctx));
|
|
292
292
|
expect(result.burned).toBe(true);
|
|
293
293
|
expect(result.newResultCode).toBe("STRONG_HIT");
|
|
294
294
|
|
|
@@ -302,7 +302,7 @@ describe("burn_momentum", () => {
|
|
|
302
302
|
test("momentum beating only one die upgrades a MISS to WEAK_HIT", async () => {
|
|
303
303
|
const ctx = makeCtx();
|
|
304
304
|
seedRolledState(4, ctx); // beats 3, not 5
|
|
305
|
-
const result =
|
|
305
|
+
const result = expectToolOk<{ newResultCode: string }>(await callNoArgs(burnMomentum, ctx));
|
|
306
306
|
expect(result.newResultCode).toBe("WEAK_HIT");
|
|
307
307
|
});
|
|
308
308
|
|
|
@@ -528,7 +528,7 @@ describe("update_state", () => {
|
|
|
528
528
|
state.clocks[0]!.filled = 3; // 3 of 4
|
|
529
529
|
seedPlaying(ctx, state);
|
|
530
530
|
|
|
531
|
-
const result =
|
|
531
|
+
const result = expectToolOk<{ clockEvents: { clock: string; trigger: string }[] }>(
|
|
532
532
|
await updateState.execute({ advanceClockName: "Doom" }, ctx),
|
|
533
533
|
);
|
|
534
534
|
expect(result.clockEvents).toEqual([{ clock: "Doom", trigger: "The doom arrives" }]);
|
|
@@ -542,7 +542,7 @@ describe("update_state", () => {
|
|
|
542
542
|
}
|
|
543
543
|
seedPlaying(ctx, state);
|
|
544
544
|
|
|
545
|
-
const result =
|
|
545
|
+
const result = expectToolOk<{ warnings?: string[] }>(
|
|
546
546
|
await updateState.execute({ addNpcName: "One Too Many" }, ctx),
|
|
547
547
|
);
|
|
548
548
|
expect(result.warnings?.[0]).toMatch(/NPC limit/);
|
|
@@ -609,7 +609,7 @@ describe("the story flow", () => {
|
|
|
609
609
|
expect(created.state).toBe("playing.awaitingRoll");
|
|
610
610
|
expect(created.instruction).toMatch(/action_roll/);
|
|
611
611
|
|
|
612
|
-
|
|
612
|
+
expectToolOk(
|
|
613
613
|
await actionRoll.execute(
|
|
614
614
|
{ move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "swing" },
|
|
615
615
|
ctx,
|
|
@@ -618,7 +618,7 @@ describe("the story flow", () => {
|
|
|
618
618
|
expect(storyFlow.position(ctx).state).toBe("playing.rollResolved");
|
|
619
619
|
|
|
620
620
|
// Moving the scene on SPENDS the roll: the burn window is closed.
|
|
621
|
-
|
|
621
|
+
expectToolOk(await updateState.execute({ location: "The Bridge" }, ctx));
|
|
622
622
|
expect(storyFlow.position(ctx).state).toBe("playing.awaitingRoll");
|
|
623
623
|
expect(isToolFailure(await callNoArgs(burnMomentum, ctx))).toBe(true);
|
|
624
624
|
});
|
|
@@ -644,7 +644,7 @@ describe("the story flow", () => {
|
|
|
644
644
|
// act on it, so a player could keep rolling after both tracks emptied. The
|
|
645
645
|
// WRITE is `gameSlot`'s `after` hook; this tool no longer calls it, which
|
|
646
646
|
// is the point of moving it there.
|
|
647
|
-
|
|
647
|
+
expectToolOk(await updateState.execute({ health: 0, spirit: 0 }, ctx));
|
|
648
648
|
const at = storyFlow.position(ctx);
|
|
649
649
|
expect(at.state).toBe("gameOver");
|
|
650
650
|
expect(at.done).toBe(true);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
2
|
import type { DeepReadonly } from "@alexkroman1/aai";
|
|
3
|
-
import {
|
|
3
|
+
import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import {
|
|
6
6
|
type Clock,
|
|
@@ -782,7 +782,7 @@ function Sidebar() {
|
|
|
782
782
|
|
|
783
783
|
// ── App ──────────────────────────────────────────────────────────────────────
|
|
784
784
|
|
|
785
|
-
|
|
785
|
+
mountClient({
|
|
786
786
|
name: "Solo RPG",
|
|
787
787
|
icon: (
|
|
788
788
|
<span style={{ fontSize: "28px", color: C.accent }}>
|
|
@@ -805,7 +805,7 @@ client({
|
|
|
805
805
|
});
|
|
806
806
|
|
|
807
807
|
// No `onSessionId`/`resumeSessionId` here any more, and their removal is a FIX
|
|
808
|
-
// rather than a simplification. `
|
|
808
|
+
// rather than a simplification. `createBrowserSession` remembers the id in
|
|
809
809
|
// `sessionStorage` by default now — see `session-resume-store.ts`, whose doc
|
|
810
810
|
// names this template as the one client of fourteen that had wired it by hand.
|
|
811
811
|
// What it had wired was `localStorage`, which is the wrong store for a pointer
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* its return value, and audio in one is megabytes replayed on every resume.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import { stepReadUpload, stepUploadInfo } from "@alexkroman1/aai/step";
|
|
21
21
|
import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
22
|
-
import {
|
|
22
|
+
import { createWorkflowContext, stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
23
23
|
import {
|
|
24
24
|
installStubGateway,
|
|
25
25
|
installStubReporter,
|
|
@@ -215,14 +215,14 @@ describe("speaking", () => {
|
|
|
215
215
|
|
|
216
216
|
const { audio } = await speak("Hello.");
|
|
217
217
|
|
|
218
|
-
await expect(
|
|
218
|
+
await expect(stepUploadInfo(audio)).resolves.toMatchObject({
|
|
219
219
|
name: "summary.wav",
|
|
220
220
|
// The byte route serves this as `Content-Type`, and a browser will not
|
|
221
221
|
// play inline a file it was handed as octet-stream.
|
|
222
222
|
type: "audio/wav",
|
|
223
223
|
size: 44 + 4000,
|
|
224
224
|
});
|
|
225
|
-
const { bytes } = await
|
|
225
|
+
const { bytes } = await stepReadUpload(audio, { end: 12 });
|
|
226
226
|
expect(String.fromCharCode(...bytes.subarray(0, 4))).toBe("RIFF");
|
|
227
227
|
expect(String.fromCharCode(...bytes.subarray(8, 12))).toBe("WAVE");
|
|
228
228
|
});
|
|
@@ -292,7 +292,7 @@ describe("the whole run", () => {
|
|
|
292
292
|
installStubReporter();
|
|
293
293
|
installStubSpeech();
|
|
294
294
|
|
|
295
|
-
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID },
|
|
295
|
+
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext());
|
|
296
296
|
|
|
297
297
|
expect(summary).toEqual({
|
|
298
298
|
source: "standup.wav",
|
|
@@ -314,7 +314,7 @@ describe("the whole run", () => {
|
|
|
314
314
|
installStubReporter();
|
|
315
315
|
const speech = installStubSpeech();
|
|
316
316
|
|
|
317
|
-
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" },
|
|
317
|
+
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" }, createWorkflowContext());
|
|
318
318
|
|
|
319
319
|
expect(speech.calls[0]).toMatchObject({ text: "Spoken.", voice: "michael" });
|
|
320
320
|
});
|
|
@@ -324,9 +324,9 @@ describe("the whole run", () => {
|
|
|
324
324
|
installStubReporter();
|
|
325
325
|
installStubSpeech();
|
|
326
326
|
|
|
327
|
-
await expect(
|
|
328
|
-
|
|
329
|
-
);
|
|
327
|
+
await expect(
|
|
328
|
+
spokenSummaryFlow({ recording: UPLOAD_ID }, createWorkflowContext()),
|
|
329
|
+
).rejects.toThrow("corrupt audio");
|
|
330
330
|
});
|
|
331
331
|
});
|
|
332
332
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* event stream wired into a live pipeline's playback, with a turn tracker and
|
|
25
25
|
* barge-in behind it, and a step has no turn to be part of and has to return
|
|
26
26
|
* a VALUE.
|
|
27
|
-
* - **`
|
|
27
|
+
* - **`stepWriteUpload`** (same subpath) puts that value where a browser can reach
|
|
28
28
|
* it. A run's OUTPUT is read back as JSON, so audio cannot travel in one —
|
|
29
29
|
* the same rule that keeps a recording's bytes out of a run's INPUT, arriving
|
|
30
30
|
* at the other end of the run.
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* `POST /workflows/uploads` (the browser does this for you: `uploads` below is
|
|
55
55
|
* what makes `<WorkflowFields>` render a file picker, and `useWorkflowSubmit`
|
|
56
56
|
* stores the file before starting the run), the input carries the returned id,
|
|
57
|
-
* and the step that needs the bytes streams them out with `
|
|
57
|
+
* and the step that needs the bytes streams them out with `stepReadUpload`.
|
|
58
58
|
*
|
|
59
59
|
* ## It is scriptable, which is the other half of having an API
|
|
60
60
|
*
|
|
@@ -140,7 +140,7 @@ export const spokenSummary: WorkflowDef<typeof spokenSummaryInput, SpokenSummary
|
|
|
140
140
|
input: spokenSummaryInput,
|
|
141
141
|
// The one line that makes the form take a file: `<WorkflowFields>` renders a
|
|
142
142
|
// picker for this property, `useWorkflowSubmit` stores the chosen file, and
|
|
143
|
-
// the step that transcribes it reads it back with `
|
|
143
|
+
// the step that transcribes it reads it back with `stepReadUpload`.
|
|
144
144
|
uploads: ["recording"],
|
|
145
145
|
run: spokenSummaryFlow,
|
|
146
146
|
});
|
|
@@ -69,9 +69,11 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
69
69
|
// `workflows/summarize.ts` already declares.
|
|
70
70
|
import { formatDuration } from "@alexkroman1/aai/utils";
|
|
71
71
|
import {
|
|
72
|
+
BulletList,
|
|
72
73
|
createWorkflowApi,
|
|
74
|
+
Facts,
|
|
73
75
|
Form,
|
|
74
|
-
|
|
76
|
+
mountPage,
|
|
75
77
|
SubmitButton,
|
|
76
78
|
UploadProgressBar,
|
|
77
79
|
useDownloadUrl,
|
|
@@ -79,7 +81,6 @@ import {
|
|
|
79
81
|
WorkflowFields,
|
|
80
82
|
WorkflowProgress,
|
|
81
83
|
} from "@alexkroman1/aai-ui";
|
|
82
|
-
import { useState } from "react";
|
|
83
84
|
import type { spokenSummary } from "./agent.ts";
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -128,16 +129,11 @@ function captionsUrl(text: string, durationMs: number): string {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
export function App() {
|
|
131
|
-
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
132
|
-
// the page can tell "working on what you just sent" from "picking up where
|
|
133
|
-
// you left off" — the hook reports the run, not who asked for it.
|
|
134
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
135
132
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
136
133
|
// `run.output` instead of `unknown`. The reload is the hook's own doing — see
|
|
137
134
|
// the module doc for why the key it mints is the right one for this page.
|
|
138
|
-
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } =
|
|
139
|
-
typeof spokenSummary
|
|
140
|
-
>(WORKFLOW, { api });
|
|
135
|
+
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
|
|
136
|
+
useWorkflowSubmit<typeof spokenSummary>(WORKFLOW, { api });
|
|
141
137
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
142
138
|
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
143
139
|
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
@@ -153,14 +149,7 @@ export function App() {
|
|
|
153
149
|
</p>
|
|
154
150
|
</header>
|
|
155
151
|
|
|
156
|
-
<Form
|
|
157
|
-
onSubmit={(values) => {
|
|
158
|
-
setStartedHere(true);
|
|
159
|
-
return submitForm(values);
|
|
160
|
-
}}
|
|
161
|
-
error={error}
|
|
162
|
-
className="flex flex-col gap-4"
|
|
163
|
-
>
|
|
152
|
+
<Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
|
|
164
153
|
{/* Every control, from the workflow's own input schema. See the module doc. */}
|
|
165
154
|
<WorkflowFields workflow={WORKFLOW} />
|
|
166
155
|
<SubmitButton pending={pending} pendingLabel="Working…">
|
|
@@ -180,25 +169,27 @@ export function App() {
|
|
|
180
169
|
nothing for `<WorkflowProgress>` to read. */}
|
|
181
170
|
<UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
|
|
182
171
|
|
|
183
|
-
{/* What the run itself says, from `
|
|
172
|
+
{/* What the run itself says, from `stepReport()` in the workflow's steps. */}
|
|
184
173
|
<WorkflowProgress runId={run?.runId} api={api} />
|
|
185
174
|
|
|
186
|
-
{
|
|
175
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error: this
|
|
176
|
+
is the outcome the reader waited minutes for. */}
|
|
177
|
+
{run?.status === "failed" && (
|
|
178
|
+
<p role="alert" className="text-red-600">
|
|
179
|
+
That one failed: {run.error}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
187
182
|
|
|
188
183
|
{output !== undefined && (
|
|
189
184
|
<article className="flex flex-col gap-5">
|
|
190
185
|
<div className="flex flex-col gap-1">
|
|
191
186
|
<h2 className="text-xl">{output.headline}</h2>
|
|
192
|
-
<
|
|
193
|
-
{output.source
|
|
194
|
-
|
|
187
|
+
<Facts
|
|
188
|
+
items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
|
|
189
|
+
/>
|
|
195
190
|
</div>
|
|
196
191
|
|
|
197
|
-
<
|
|
198
|
-
{output.points.map((point) => (
|
|
199
|
-
<li key={point}>{point}</li>
|
|
200
|
-
))}
|
|
201
|
-
</ul>
|
|
192
|
+
<BulletList items={output.points} />
|
|
202
193
|
|
|
203
194
|
<section className="flex flex-col gap-2">
|
|
204
195
|
<h3 className="text-sm font-medium opacity-70">
|
|
@@ -206,11 +197,13 @@ export function App() {
|
|
|
206
197
|
</h3>
|
|
207
198
|
{audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
|
|
208
199
|
{audio.error !== undefined && (
|
|
209
|
-
<p className="text-red-600">
|
|
200
|
+
<p role="alert" className="text-red-600">
|
|
201
|
+
Could not load the audio: {audio.error}
|
|
202
|
+
</p>
|
|
210
203
|
)}
|
|
211
204
|
{audio.url !== undefined && (
|
|
212
205
|
<>
|
|
213
|
-
<audio controls src={audio.url} className="w-full">
|
|
206
|
+
<audio aria-label="Summary read aloud" controls src={audio.url} className="w-full">
|
|
214
207
|
{/* A real caption track, not a suppression: the summary was
|
|
215
208
|
written before it was spoken, so the words are already
|
|
216
209
|
here and one cue spanning the clip is an honest
|
|
@@ -244,4 +237,4 @@ export function App() {
|
|
|
244
237
|
);
|
|
245
238
|
}
|
|
246
239
|
|
|
247
|
-
|
|
240
|
+
mountPage({ name: "Spoken Summary", component: App });
|