@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
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
1
2
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
|
|
4
|
+
import { orderProjection, pizzaPrice } from "./shared.ts";
|
|
4
5
|
|
|
5
6
|
function PizzaIcon({ size }: { size: string }) {
|
|
6
7
|
const dim = size === "small" ? 36 : size === "large" ? 52 : 44;
|
|
@@ -67,7 +68,7 @@ function OrderSidebar() {
|
|
|
67
68
|
</p>
|
|
68
69
|
</div>
|
|
69
70
|
<p className="text-sm font-bold whitespace-nowrap text-aai-primary">
|
|
70
|
-
{
|
|
71
|
+
{formatMoney(pizzaPrice(p))}
|
|
71
72
|
</p>
|
|
72
73
|
</div>
|
|
73
74
|
))}
|
|
@@ -79,7 +80,7 @@ function OrderSidebar() {
|
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
mountClient({
|
|
83
84
|
name: "Pizza Palace",
|
|
84
85
|
sidebar: OrderSidebar,
|
|
85
86
|
theme: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type DeepReadonly, sessionSlot } from "@alexkroman1/aai";
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
3
|
|
|
3
4
|
export const SIZES = ["small", "medium", "large"] as const;
|
|
4
5
|
export const CRUSTS = ["thin", "regular", "thick", "stuffed"] as const;
|
|
@@ -37,19 +38,13 @@ export function calculateTotal(pizzas: readonly ReadonlyPizza[]): number {
|
|
|
37
38
|
return pizzas.reduce((total, pizza) => total + pizzaPrice(pizza), 0);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/** The one money format. Tool results, the projection, and the sidebar all
|
|
41
|
-
* show prices through this, so they can never disagree on rounding. */
|
|
42
|
-
export function formatPrice(amount: number): string {
|
|
43
|
-
return `$${amount.toFixed(2)}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
41
|
/**
|
|
47
42
|
* The menu as prompt prose, generated from `MENU` so the agent can never
|
|
48
43
|
* quote a price the pricing code doesn't charge.
|
|
49
44
|
*/
|
|
50
45
|
export function menuText(): string {
|
|
51
46
|
const price = (amount: number, upcharge = false) =>
|
|
52
|
-
amount === 0 ? "free" : `${upcharge ? "+" : ""}${
|
|
47
|
+
amount === 0 ? "free" : `${upcharge ? "+" : ""}${formatMoney(amount)}`;
|
|
53
48
|
const list = (items: Record<string, number>, upcharge = false) =>
|
|
54
49
|
Object.entries(items)
|
|
55
50
|
.map(([name, amount]) => `${name.replaceAll("_", " ")} (${price(amount, upcharge)})`)
|
|
@@ -169,7 +164,7 @@ export function orderView(order: FrozenOrderState): OrderView {
|
|
|
169
164
|
const placed = order.placed;
|
|
170
165
|
return {
|
|
171
166
|
pizzas: order.pizzas,
|
|
172
|
-
total: placed?.total ??
|
|
167
|
+
total: placed?.total ?? formatMoney(calculateTotal(order.pizzas)),
|
|
173
168
|
orderPlaced: Boolean(placed),
|
|
174
169
|
...(placed
|
|
175
170
|
? { orderNumber: placed.orderNumber, estimatedMinutes: placed.estimatedMinutes }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { CRUSTS, calculateTotal,
|
|
3
|
+
import { CRUSTS, calculateTotal, orderSlot, type Pizza, SIZES } from "../shared.ts";
|
|
3
4
|
|
|
4
5
|
export default orderSlot.updateTool({
|
|
5
6
|
description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
|
|
@@ -24,7 +25,7 @@ export default orderSlot.updateTool({
|
|
|
24
25
|
|
|
25
26
|
return {
|
|
26
27
|
added: pizza,
|
|
27
|
-
orderTotal:
|
|
28
|
+
orderTotal: formatMoney(calculateTotal(order.pizzas)),
|
|
28
29
|
itemCount: order.pizzas.length,
|
|
29
30
|
};
|
|
30
31
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
3
|
+
import { calculateTotal, orderSlot, resetOrder } from "../shared.ts";
|
|
3
4
|
|
|
4
5
|
export default orderSlot.updateTool({
|
|
5
6
|
description:
|
|
@@ -9,7 +10,7 @@ export default orderSlot.updateTool({
|
|
|
9
10
|
if (pizzas.length === 0) return toolFailure("Cannot place an empty order.");
|
|
10
11
|
|
|
11
12
|
const customerName = order.customerName ?? "Guest";
|
|
12
|
-
const total =
|
|
13
|
+
const total = formatMoney(calculateTotal(pizzas));
|
|
13
14
|
const orderNumber = Math.floor(1000 + Math.random() * 9000);
|
|
14
15
|
|
|
15
16
|
const estimatedMinutes = 15 + pizzas.length * 5;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { calculateTotal,
|
|
4
|
+
import { calculateTotal, orderSlot } from "../shared.ts";
|
|
4
5
|
|
|
5
6
|
export default orderSlot.updateTool({
|
|
6
7
|
description: "Remove a pizza from the order by its ID.",
|
|
@@ -15,7 +16,7 @@ export default orderSlot.updateTool({
|
|
|
15
16
|
|
|
16
17
|
return {
|
|
17
18
|
removed,
|
|
18
|
-
orderTotal:
|
|
19
|
+
orderTotal: formatMoney(calculateTotal(order.pizzas)),
|
|
19
20
|
itemCount: order.pizzas.length,
|
|
20
21
|
};
|
|
21
22
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { CRUSTS, calculateTotal,
|
|
4
|
+
import { CRUSTS, calculateTotal, orderSlot, type Pizza, SIZES } from "../shared.ts";
|
|
4
5
|
|
|
5
6
|
export default orderSlot.updateTool({
|
|
6
7
|
description: "Update an existing pizza in the order. Only provided fields are changed.",
|
|
@@ -23,6 +24,6 @@ export default orderSlot.updateTool({
|
|
|
23
24
|
|
|
24
25
|
order.pizzas[idx] = pizza;
|
|
25
26
|
|
|
26
|
-
return { updated: pizza, orderTotal:
|
|
27
|
+
return { updated: pizza, orderTotal: formatMoney(calculateTotal(order.pizzas)) };
|
|
27
28
|
},
|
|
28
29
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
|
+
import { calculateTotal, orderSlot } from "../shared.ts";
|
|
2
3
|
|
|
3
4
|
export default orderSlot.tool({
|
|
4
5
|
description: "View the current order summary with all pizzas and total price.",
|
|
@@ -15,7 +16,7 @@ export default orderSlot.tool({
|
|
|
15
16
|
toppings: p.toppings,
|
|
16
17
|
quantity: p.quantity,
|
|
17
18
|
})),
|
|
18
|
-
orderTotal:
|
|
19
|
+
orderTotal: formatMoney(calculateTotal(pizzas)),
|
|
19
20
|
};
|
|
20
21
|
},
|
|
21
22
|
});
|
|
@@ -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
|
+
stubGenerate,
|
|
8
|
+
toolRunner,
|
|
9
|
+
} from "@alexkroman1/aai/testing";
|
|
5
10
|
import { describe, expect, test } from "vitest";
|
|
6
11
|
|
|
7
12
|
import { executeStep, MAX_STEP_SEARCHES, normalizeAct, planNode } from "./procedure.ts";
|
|
@@ -216,7 +221,7 @@ describe("work_next_step", () => {
|
|
|
216
221
|
const ctx = createToolContext({ generate });
|
|
217
222
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
218
223
|
|
|
219
|
-
const first =
|
|
224
|
+
const first = expectDialogOk<{
|
|
220
225
|
finished: boolean;
|
|
221
226
|
step: string;
|
|
222
227
|
result: string;
|
|
@@ -243,7 +248,7 @@ describe("work_next_step", () => {
|
|
|
243
248
|
});
|
|
244
249
|
const ctx = createToolContext({ generate });
|
|
245
250
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
246
|
-
const answered =
|
|
251
|
+
const answered = expectDialogOk<{ finished: boolean; response: string }>(
|
|
247
252
|
await run("work_next_step", ctx),
|
|
248
253
|
);
|
|
249
254
|
|
|
@@ -327,7 +332,7 @@ describe("revise_plan", () => {
|
|
|
327
332
|
await run("work_next_step", ctx);
|
|
328
333
|
expect(stateOf(ctx).response).not.toBeNull();
|
|
329
334
|
|
|
330
|
-
const revised =
|
|
335
|
+
const revised = expectDialogOk<{ finished: boolean; remaining: string[] }>(
|
|
331
336
|
await run("revise_plan", { instruction: "make it Porto instead" }, ctx),
|
|
332
337
|
);
|
|
333
338
|
expect(revised.result.finished).toBe(false);
|
|
@@ -362,7 +367,7 @@ describe("revise_plan", () => {
|
|
|
362
367
|
});
|
|
363
368
|
const ctx = createToolContext({ generate });
|
|
364
369
|
await run("start_plan", { objective: "a weekend in Lisbon" }, ctx);
|
|
365
|
-
const revised =
|
|
370
|
+
const revised = expectDialogOk<{ finished: boolean }>(
|
|
366
371
|
await run("revise_plan", { instruction: "never mind, it is booked" }, ctx),
|
|
367
372
|
);
|
|
368
373
|
expect(revised.result.finished).toBe(true);
|
|
@@ -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 { planProjection } from "./shared.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -90,7 +90,7 @@ function PlanSidebar() {
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
mountClient({
|
|
94
94
|
name: "Planning Desk",
|
|
95
95
|
sidebar: PlanSidebar,
|
|
96
96
|
theme: {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* HTTP handling, its partial-failure policy and its `FatalError` guards are
|
|
12
12
|
* all reachable — `installStubStepFetch` answers the network and
|
|
13
13
|
* `stubGateway` answers the model.
|
|
14
|
-
* - The BODY, twice. `
|
|
14
|
+
* - The BODY, twice. `createWorkflowContext` records what it asked for (the digest
|
|
15
15
|
* loop, the shrinking pending set, the sleep BETWEEN digests and never after
|
|
16
16
|
* the last), and `runWorkflow` runs it on the real replay engine — which is
|
|
17
17
|
* what this file used to say a unit test could not do, on the ground that a
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
import { type FeedItem, parseFeed } from "@alexkroman1/aai/html";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
createWorkflowContext,
|
|
30
30
|
parseSchemaInput,
|
|
31
|
+
routeStepFetch,
|
|
31
32
|
schemaInputIssues,
|
|
32
33
|
stubGatewayRoute,
|
|
33
34
|
} from "@alexkroman1/aai/testing";
|
|
@@ -103,15 +104,25 @@ const slackInput = (slackWebhookUrl: string) => ({
|
|
|
103
104
|
|
|
104
105
|
describe("the declaration", () => {
|
|
105
106
|
test("is a workflow app with one workflow and a static page", () => {
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
// Not the agent's name: renaming it is an invited edit, and pinning the
|
|
108
|
+
// literal here would fail a build in somebody else's project.
|
|
109
|
+
expect(agentDef.name).toBeTruthy();
|
|
110
|
+
// `toContain` rather than an exact key list: adding a second workflow is an
|
|
111
|
+
// invited edit and must not redden a test the author did not write. The
|
|
112
|
+
// NAME is still pinned, deliberately — the page starts a run by this
|
|
113
|
+
// string, so renaming the key is a runtime 400 rather than a compile
|
|
114
|
+
// error, and this pin is the only thing that says so. Rename it here and
|
|
115
|
+
// in `client.tsx` together.
|
|
116
|
+
expect(Object.keys(agentDef.workflows ?? {})).toContain("dailyDigest");
|
|
108
117
|
expect(agentDef.workflows?.dailyDigest).toBe(dailyDigest);
|
|
109
118
|
});
|
|
110
119
|
|
|
111
120
|
test("names the one credential its steps read", () => {
|
|
112
121
|
// A workflow app declares no providers, so nothing else in the config can
|
|
113
122
|
// name this — which is what makes a deploy able to check for it.
|
|
114
|
-
|
|
123
|
+
// `toContain`, so a step of your own that reads a second credential can be
|
|
124
|
+
// declared beside this one without failing a test you did not write.
|
|
125
|
+
expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
|
|
115
126
|
});
|
|
116
127
|
});
|
|
117
128
|
|
|
@@ -412,12 +423,20 @@ const THREE_EPISODES = feedXml(
|
|
|
412
423
|
* the thing that forgets, and a step fetch left published reaches the next file.
|
|
413
424
|
*/
|
|
414
425
|
function stubRoutes(routes: Record<string, { status?: number; body?: unknown }>) {
|
|
415
|
-
return installStubStepFetch(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
return installStubStepFetch(
|
|
427
|
+
routeStepFetch(
|
|
428
|
+
Object.entries(routes).map(
|
|
429
|
+
([fragment, answer]) =>
|
|
430
|
+
(request) =>
|
|
431
|
+
request.url.includes(fragment) ? answer : undefined,
|
|
432
|
+
),
|
|
433
|
+
// A 404 rather than the default throw, and this one is deliberate: several
|
|
434
|
+
// cases below are ABOUT a feed the run cannot reach, and they express that
|
|
435
|
+
// by leaving it out of `routes`. Where an unrecognised request would be a
|
|
436
|
+
// finding instead, the default is what says so at the call.
|
|
437
|
+
{ unmatched: "notFound" },
|
|
438
|
+
),
|
|
439
|
+
);
|
|
421
440
|
}
|
|
422
441
|
|
|
423
442
|
describe("discoverEpisodes", () => {
|
|
@@ -827,7 +846,7 @@ describe("posting the digest", () => {
|
|
|
827
846
|
});
|
|
828
847
|
|
|
829
848
|
/**
|
|
830
|
-
* The 4xx/5xx split is the reason this is not a one-line `
|
|
849
|
+
* The 4xx/5xx split is the reason this is not a one-line `stepFetchOrFail`: a
|
|
831
850
|
* revoked webhook answers 4xx identically on every retry, so retrying it
|
|
832
851
|
* burns the DevKit's attempts and delays the real error by minutes.
|
|
833
852
|
*/
|
|
@@ -866,7 +885,7 @@ describe("the body — the run that IS the schedule", () => {
|
|
|
866
885
|
* per-step spec can see, and all of which is the template's actual subject.
|
|
867
886
|
*/
|
|
868
887
|
function driveTwoDigests(pollResults: unknown) {
|
|
869
|
-
const ctx =
|
|
888
|
+
const ctx = createWorkflowContext({
|
|
870
889
|
runSteps: false,
|
|
871
890
|
results: {
|
|
872
891
|
discoverEpisodes: [EPISODE],
|
|
@@ -963,7 +982,7 @@ describe("the body — the run that IS the schedule", () => {
|
|
|
963
982
|
/**
|
|
964
983
|
* The schedule, on the real replay engine.
|
|
965
984
|
*
|
|
966
|
-
* The block above drives the same body through `
|
|
985
|
+
* The block above drives the same body through `createWorkflowContext`, which
|
|
967
986
|
* RECORDS a sleep rather than taking one and replays nothing — right for the
|
|
968
987
|
* loop's logic, and silent about the property this template is: a run that
|
|
969
988
|
* sleeps for a day between digests and comes back. `runWorkflow`
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* The browser half — a form that starts a run, and a panel that watches one
|
|
4
4
|
* that may outlive the browser by a week.
|
|
5
5
|
*
|
|
6
|
-
* Mounted with `
|
|
6
|
+
* Mounted with `mountPage()` rather than `mountClient()`: there is no session to build, so
|
|
7
7
|
* no socket, no audio graph, no microphone request. `useWorkflowSubmit()` starts
|
|
8
8
|
* the run, follows its STATUS, and hands back the controls bound to it.
|
|
9
9
|
*
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
* minutes, this one loses the only Stop button a thirty-digest commitment has,
|
|
18
18
|
* and the run is invisible from then on to everything but `curl`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* Wake and Cancel buttons bound to
|
|
20
|
+
* A correlation KEY is the handle that survives, and `useWorkflowSubmit` looks
|
|
21
|
+
* that key's newest run up as it mounts, so a later load lands on the same
|
|
22
|
+
* count, the same newest line, and the same Wake and Cancel buttons bound to
|
|
23
|
+
* the same run.
|
|
23
24
|
*
|
|
24
|
-
* **
|
|
25
|
-
*
|
|
25
|
+
* **This is the one workflow app that passes a `key` of its own, and it is
|
|
26
|
+
* `useRunKey({ storage: "local" })`.** Its siblings let the hook mint one into
|
|
26
27
|
* `sessionStorage`, which dies with the tab and covers exactly the interruption
|
|
27
28
|
* they have — a reload, a same-tab navigation, a crashed tab. A schedule
|
|
28
29
|
* outlives all of that by design: closing the browser on Tuesday and coming
|
|
@@ -42,9 +43,9 @@
|
|
|
42
43
|
* URL would carry a credential into a lookup token — which is why the
|
|
43
44
|
* platform stopped writing keys to the operator's log.
|
|
44
45
|
*
|
|
45
|
-
* A real app with accounts passes the account's own id instead, and
|
|
46
|
-
* schedule follows the person to a new device — a promise only a login
|
|
47
|
-
* keep.
|
|
46
|
+
* A real app with accounts passes the account's own id as `key` instead, and
|
|
47
|
+
* then the schedule follows the person to a new device — a promise only a login
|
|
48
|
+
* can keep.
|
|
48
49
|
*
|
|
49
50
|
* ## The form is DECLARED, not written
|
|
50
51
|
*
|
|
@@ -85,8 +86,9 @@
|
|
|
85
86
|
*/
|
|
86
87
|
|
|
87
88
|
import {
|
|
89
|
+
BulletList,
|
|
88
90
|
Form,
|
|
89
|
-
|
|
91
|
+
mountPage,
|
|
90
92
|
SubmitButton,
|
|
91
93
|
useRunKey,
|
|
92
94
|
useWorkflowSubmit,
|
|
@@ -97,7 +99,6 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
97
99
|
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
98
100
|
// nothing — and it is what stops this file restating a shape `workflows/
|
|
99
101
|
// digest.ts` already declares.
|
|
100
|
-
import { useState } from "react";
|
|
101
102
|
import type { dailyDigest } from "./agent.ts";
|
|
102
103
|
|
|
103
104
|
/** The workflow this page drives. Matches the key in `workflowApp({ workflows })`. */
|
|
@@ -130,21 +131,15 @@ export function App() {
|
|
|
130
131
|
// Did THIS load start the schedule? A later load cannot have, and that is the
|
|
131
132
|
// only way the page can tell "scheduled just now" from "still running from
|
|
132
133
|
// Tuesday" — the hook reports the run, not who asked for it.
|
|
133
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
134
134
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
135
135
|
// `run.output` instead of `unknown`. `error` is the agent's own sentence for a
|
|
136
136
|
// rejected input — better copy than anything this page could write, and the
|
|
137
137
|
// reason there is no `try`/`catch` here.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// without `recover` the key is only ever written.
|
|
144
|
-
key,
|
|
145
|
-
recover: true,
|
|
146
|
-
},
|
|
147
|
-
);
|
|
138
|
+
// The key REPLACES the tab-scoped one the hook would mint; the lookup that
|
|
139
|
+
// reads it back on the next load happens either way.
|
|
140
|
+
const { submitForm, run, pending, error, wake, cancel, startedHere } = useWorkflowSubmit<
|
|
141
|
+
typeof dailyDigest
|
|
142
|
+
>(WORKFLOW, { key });
|
|
148
143
|
|
|
149
144
|
return (
|
|
150
145
|
<main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
|
|
@@ -152,13 +147,7 @@ export function App() {
|
|
|
152
147
|
|
|
153
148
|
{/* `submit()` resolves as soon as the run EXISTS — deliberately not when it
|
|
154
149
|
finishes, which here could be a month away. */}
|
|
155
|
-
<Form
|
|
156
|
-
onSubmit={(values) => {
|
|
157
|
-
setStartedHere(true);
|
|
158
|
-
return submitForm(values);
|
|
159
|
-
}}
|
|
160
|
-
error={error}
|
|
161
|
-
>
|
|
150
|
+
<Form onSubmit={(values) => submitForm(values)} error={error}>
|
|
162
151
|
<WorkflowFields workflow={WORKFLOW} />
|
|
163
152
|
<SubmitButton pending={pending}>
|
|
164
153
|
{pending ? "Digest scheduled" : "Start digest"}
|
|
@@ -198,7 +187,13 @@ export function App() {
|
|
|
198
187
|
</div>
|
|
199
188
|
)}
|
|
200
189
|
|
|
201
|
-
{
|
|
190
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error
|
|
191
|
+
above: a digest that fails does so days later, with nobody watching. */}
|
|
192
|
+
{run?.status === "failed" && (
|
|
193
|
+
<p role="alert" className="text-red-600">
|
|
194
|
+
That run failed: {run.error}
|
|
195
|
+
</p>
|
|
196
|
+
)}
|
|
202
197
|
{run?.status === "cancelled" && <p>Cancelled — no further digests will be posted.</p>}
|
|
203
198
|
|
|
204
199
|
{run?.status === "completed" && (
|
|
@@ -216,11 +211,7 @@ export function App() {
|
|
|
216
211
|
</a>
|
|
217
212
|
</h2>
|
|
218
213
|
<p>{episode.summary}</p>
|
|
219
|
-
<
|
|
220
|
-
{episode.keyPoints.map((point) => (
|
|
221
|
-
<li key={point}>{point}</li>
|
|
222
|
-
))}
|
|
223
|
-
</ul>
|
|
214
|
+
<BulletList items={episode.keyPoints} size="sm" />
|
|
224
215
|
</section>
|
|
225
216
|
))}
|
|
226
217
|
</article>
|
|
@@ -229,4 +220,4 @@ export function App() {
|
|
|
229
220
|
);
|
|
230
221
|
}
|
|
231
222
|
|
|
232
|
-
|
|
223
|
+
mountPage({ name: "Podcast Digest", component: App });
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
* @module digest
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import type {
|
|
53
|
-
import { mapConcurrent,
|
|
52
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
53
|
+
import { mapConcurrent, stepReport, TRANSCRIBE_API } from "@alexkroman1/aai/step";
|
|
54
54
|
import {
|
|
55
55
|
FatalError,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
stepGenerateJsonOrFail,
|
|
57
|
+
stepTranscribePollOrFail,
|
|
58
|
+
stepTranscribeSubmitOrFail,
|
|
59
59
|
} from "@alexkroman1/aai/step-errors";
|
|
60
60
|
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
61
61
|
import type { WorkflowInputOf } from "@alexkroman1/aai/workflow-api";
|
|
@@ -160,7 +160,7 @@ export type DailyDigestOutput = {
|
|
|
160
160
|
} | null;
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
-
/** What the model must answer with, and what `
|
|
163
|
+
/** What the model must answer with, and what `stepGenerateJsonOrFail` enforces. */
|
|
164
164
|
const SummaryReply = z.object({
|
|
165
165
|
summary: z.string().trim().min(1),
|
|
166
166
|
keyPoints: z.array(z.string().trim().min(1)).min(1).max(5),
|
|
@@ -178,7 +178,7 @@ const SummaryReply = z.object({
|
|
|
178
178
|
*/
|
|
179
179
|
export async function dailyDigestFlow(
|
|
180
180
|
input: DigestInput,
|
|
181
|
-
ctx:
|
|
181
|
+
ctx: WorkflowContext,
|
|
182
182
|
): Promise<DailyDigestOutput> {
|
|
183
183
|
// No `??` fallbacks: {@link DigestInput} is the schema's OUTPUT, so every
|
|
184
184
|
// `.default()` has already run by the time a run reaches this line. The
|
|
@@ -269,7 +269,7 @@ export async function dailyDigestFlow(
|
|
|
269
269
|
*/
|
|
270
270
|
async function waitForTranscripts(
|
|
271
271
|
jobs: TranscriptJob[],
|
|
272
|
-
ctx:
|
|
272
|
+
ctx: WorkflowContext,
|
|
273
273
|
): Promise<TranscriptState[]> {
|
|
274
274
|
let pending = jobs;
|
|
275
275
|
// Keyed by episode id rather than appended, and that is what keeps the digest
|
|
@@ -287,17 +287,19 @@ async function waitForTranscripts(
|
|
|
287
287
|
if (pending.length > 0) await ctx.sleep("poll", POLL_DELAY_MS);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
for (const job of pending) settled.set(job.id, gaveUpOn(job));
|
|
291
290
|
// The list `jobs` arrived in, which `discoverEpisodes` sorted newest first.
|
|
291
|
+
// A job still pending is absent from `settled`, so the `??` IS the
|
|
292
|
+
// give-up path — `pollTranscript` returns the job unchanged while it is
|
|
293
|
+
// submitted, so the value given up on is the one that went in.
|
|
292
294
|
return jobs.map((job) => settled.get(job.id) ?? gaveUpOn(job));
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
/**
|
|
296
298
|
* One episode the run is done waiting for.
|
|
297
299
|
*
|
|
298
|
-
* Its own function because {@link waitForTranscripts}
|
|
299
|
-
*
|
|
300
|
-
*
|
|
300
|
+
* Its own function because {@link waitForTranscripts} reaches it from the
|
|
301
|
+
* ordered rebuild, for every job whose budget ran out while it was still
|
|
302
|
+
* submitted and which is therefore absent from `settled`.
|
|
301
303
|
*/
|
|
302
304
|
function gaveUpOn(job: TranscriptJob): TranscriptState {
|
|
303
305
|
return {
|
|
@@ -322,7 +324,7 @@ function gaveUpOn(job: TranscriptJob): TranscriptState {
|
|
|
322
324
|
* an `unavailable` VALUE rather than a throw, because one bad episode must not
|
|
323
325
|
* sink a digest of five.
|
|
324
326
|
*
|
|
325
|
-
* The verdict itself is the SDK's: `
|
|
327
|
+
* The verdict itself is the SDK's: `stepTranscribeSubmitOrFail` reads
|
|
326
328
|
* `TranscribeError`'s own `retryable` AND its `retryAfter`, and throws a
|
|
327
329
|
* `FatalError` or a `RetryableError` accordingly. The hand-written
|
|
328
330
|
* `err instanceof TranscribeError && err.retryable` this replaces read only the
|
|
@@ -330,9 +332,9 @@ function gaveUpOn(job: TranscriptJob): TranscriptState {
|
|
|
330
332
|
* one-second default instead.
|
|
331
333
|
*/
|
|
332
334
|
export async function submitTranscript(episode: Episode): Promise<TranscriptJob> {
|
|
333
|
-
await
|
|
335
|
+
await stepReport(`Submitting ${episode.title} for transcription.`);
|
|
334
336
|
try {
|
|
335
|
-
const { id } = await
|
|
337
|
+
const { id } = await stepTranscribeSubmitOrFail(episode.audioUrl, {
|
|
336
338
|
// A digest quotes nobody, so who spoke costs time for nothing.
|
|
337
339
|
params: { speaker_labels: false },
|
|
338
340
|
});
|
|
@@ -356,12 +358,12 @@ export async function pollTranscript(job: TranscriptJob): Promise<TranscriptStat
|
|
|
356
358
|
if (job.transcriptStatus === "unavailable") return job;
|
|
357
359
|
|
|
358
360
|
try {
|
|
359
|
-
const progress = await
|
|
361
|
+
const progress = await stepTranscribePollOrFail(job.transcriptId);
|
|
360
362
|
// Branch on `done`, never on a status string: a vocabulary this body does
|
|
361
363
|
// not own would otherwise read as "not finished yet" forever.
|
|
362
364
|
if (!progress.done) return job;
|
|
363
365
|
|
|
364
|
-
await
|
|
366
|
+
await stepReport(`Transcribed ${job.title}.`);
|
|
365
367
|
return {
|
|
366
368
|
...job,
|
|
367
369
|
transcriptStatus: "done",
|
|
@@ -391,8 +393,8 @@ export async function summarizeTranscript(state: TranscriptState): Promise<Episo
|
|
|
391
393
|
};
|
|
392
394
|
}
|
|
393
395
|
|
|
394
|
-
await
|
|
395
|
-
const parsed = await
|
|
396
|
+
await stepReport(`Summarizing ${state.title}.`);
|
|
397
|
+
const parsed = await stepGenerateJsonOrFail(
|
|
396
398
|
[
|
|
397
399
|
`Podcast: ${state.podcastTitle}`,
|
|
398
400
|
`Episode: ${state.title}`,
|