@alexkroman1/aai-cli 5.14.0 → 6.1.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-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +8 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* Specs for the link digest — the workflow-app template.
|
|
4
|
+
*
|
|
5
|
+
* There are no tools to exercise, which is the point: what this template
|
|
6
|
+
* demonstrates is a DECLARATION, so the spec asserts the declaration. Three
|
|
7
|
+
* things carry the shape and each is silent when wrong — the `page: "static"`
|
|
8
|
+
* field (without it the deployed agent still offers a `/websocket` nothing will
|
|
9
|
+
* answer), the workflow's NAME (the page starts a run by that string, so a
|
|
10
|
+
* rename here is a runtime 400 there), and the input schema (which is both the
|
|
11
|
+
* call-site validation and the JSON Schema `GET /workflows` serves).
|
|
12
|
+
*
|
|
13
|
+
* The workflow BODY is not tested here: it is only durable once the Workflow
|
|
14
|
+
* DevKit's build has transformed it, so a unit test of it would exercise a plain
|
|
15
|
+
* async function and prove nothing about replay. Its STEPS are, and directly —
|
|
16
|
+
* imported with no bundler in the path a `"use step"` function is an ordinary
|
|
17
|
+
* async function, so its HTML handling, its JSON contract with the model and its
|
|
18
|
+
* `FatalError` guards are all testable.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { stubStepFetch } from "@alexkroman1/aai/testing";
|
|
22
|
+
import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
23
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
24
|
+
import agentDef, { digest } from "./agent.ts";
|
|
25
|
+
import { extractText, extractTitle, fetchArticle, summarize } from "./workflows/digest.ts";
|
|
26
|
+
|
|
27
|
+
describe("the agent declares itself a workflow app", () => {
|
|
28
|
+
test("its front door is a page, not a microphone", () => {
|
|
29
|
+
// Not decoration: `createServer` declines `/websocket` with a reason for a
|
|
30
|
+
// static agent, and telephony defaults off.
|
|
31
|
+
expect(agentDef.page).toBe("static");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("it declares no voice pipeline and no tools, because nothing talks", () => {
|
|
35
|
+
expect(agentDef.stt).toBeUndefined();
|
|
36
|
+
expect(agentDef.llm).toBeUndefined();
|
|
37
|
+
expect(agentDef.tts).toBeUndefined();
|
|
38
|
+
expect(agentDef.s2s).toBeUndefined();
|
|
39
|
+
expect(agentDef.tools).toEqual({});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("under the name the page starts a run by", () => {
|
|
43
|
+
// `api.start("digest", …)` in client.tsx names this key. Nothing else
|
|
44
|
+
// records it, so a rename here is a 400 there rather than a compile error.
|
|
45
|
+
expect(Object.keys(agentDef.workflows ?? {})).toEqual(["digest"]);
|
|
46
|
+
expect(agentDef.workflows?.digest).toBe(digest);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("the input schema", () => {
|
|
51
|
+
test("accepts a URL", async () => {
|
|
52
|
+
const result = await digest.input?.["~standard"].validate({ url: "https://example.com/a" });
|
|
53
|
+
expect(result?.issues).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("rejects a non-URL at the CALL SITE rather than three steps into a run", async () => {
|
|
57
|
+
const result = await digest.input?.["~standard"].validate({ url: "not a url" });
|
|
58
|
+
expect(result?.issues).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("carries a description, which is what a rendered form labels the field with", () => {
|
|
62
|
+
expect(digest.description).toBeTruthy();
|
|
63
|
+
expect(digest.input).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("the agent declares the credential its steps read", () => {
|
|
68
|
+
test("so a deploy checks for it rather than the first run", () => {
|
|
69
|
+
// A workflow app declares no providers, so `requiredEnv` is the ONLY thing
|
|
70
|
+
// in its config that can name a credential.
|
|
71
|
+
expect(agentDef.requiredEnv).toContain("ASSEMBLYAI_API_KEY");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("extractText", () => {
|
|
76
|
+
test("drops script and style CONTENT, not just their tags", () => {
|
|
77
|
+
// Stripping tags alone leaves a page's JavaScript in the prompt — expensive,
|
|
78
|
+
// and a way to smuggle instructions past the reader.
|
|
79
|
+
const text = extractText(
|
|
80
|
+
"<html><head><style>body{color:red}</style></head><body><script>alert('x')</script><p>Real words here.</p></body></html>",
|
|
81
|
+
);
|
|
82
|
+
expect(text).toBe("Real words here.");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("decodes & LAST, so an escaped entity is not decoded twice", () => {
|
|
86
|
+
// `&lt;` is a literal `<` on the page; decoding `&` first would
|
|
87
|
+
// turn it into a `<` the author never wrote.
|
|
88
|
+
expect(extractText("<p>a &lt; b & c</p>")).toBe("a < b & c");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("collapses whitespace, because HTML indentation is not prose", () => {
|
|
92
|
+
expect(extractText("<p>one</p>\n\n <p>two</p>")).toBe("one two");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("caps what crosses the queue to the next step", () => {
|
|
96
|
+
const huge = `<p>${"word ".repeat(20_000)}</p>`;
|
|
97
|
+
expect(extractText(huge).length).toBeLessThanOrEqual(24_000);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("extractTitle", () => {
|
|
102
|
+
test("reads the document title", () => {
|
|
103
|
+
expect(extractTitle("<html><title> Otters & tools </title></html>")).toBe(
|
|
104
|
+
"Otters & tools",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("answers undefined when there is none, so the caller can fall back", () => {
|
|
109
|
+
expect(extractTitle("<html><body>hi</body></html>")).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("fetchArticle", () => {
|
|
114
|
+
/** Unpublished between specs — a fetch left behind reaches the next file. */
|
|
115
|
+
let restore: (() => void) | undefined;
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
restore?.();
|
|
118
|
+
restore = undefined;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A page server answering `html` with `status`.
|
|
123
|
+
*
|
|
124
|
+
* Published into `stepFetch`'s own slot rather than over `globalThis.fetch`:
|
|
125
|
+
* the step calls `stepFetch`, and stubbing the global would pass while
|
|
126
|
+
* exercising the fallback path production never takes.
|
|
127
|
+
*/
|
|
128
|
+
function stubPage(html: string, status = 200) {
|
|
129
|
+
const stub = stubStepFetch(() => ({
|
|
130
|
+
status,
|
|
131
|
+
body: html,
|
|
132
|
+
headers: { "Content-Type": "text/html" },
|
|
133
|
+
}));
|
|
134
|
+
restore = stub.restore;
|
|
135
|
+
return stub;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test("returns the page's title and its readable text", async () => {
|
|
139
|
+
stubPage(
|
|
140
|
+
`<html><title>Otters</title><body><p>${"Otters use tools. ".repeat(20)}</p></body></html>`,
|
|
141
|
+
);
|
|
142
|
+
const article = await fetchArticle("https://example.com/otters");
|
|
143
|
+
expect(article.title).toBe("Otters");
|
|
144
|
+
expect(article.text).toContain("Otters use tools.");
|
|
145
|
+
expect(article.url).toBe("https://example.com/otters");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("falls back to the hostname when the page has no title", async () => {
|
|
149
|
+
stubPage(`<html><body><p>${"Otters use tools. ".repeat(20)}</p></body></html>`);
|
|
150
|
+
expect((await fetchArticle("https://example.com/otters")).title).toBe("example.com");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("fails FATALLY on a page with no readable text", async () => {
|
|
154
|
+
// A JS-rendered site is the usual cause, and no number of attempts fixes it.
|
|
155
|
+
stubPage("<html><body><div id='root'></div></body></html>");
|
|
156
|
+
await expect(fetchArticle("https://example.com/app")).rejects.toThrow(/no readable text/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("fails FATALLY on a 404 and plainly on a 503", async () => {
|
|
160
|
+
stubPage("", 404);
|
|
161
|
+
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/HTTP 404/);
|
|
162
|
+
stubPage("", 503);
|
|
163
|
+
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/HTTP 503/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("summarize", () => {
|
|
168
|
+
const ARTICLE = { url: "https://example.com/a", title: "Otters", text: "Otters use tools." };
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
// `stepEnv` falls back to the process env when no host has published one,
|
|
172
|
+
// which is exactly the case a spec is. `unstubEnvs` clears it per test.
|
|
173
|
+
vi.stubEnv("ASSEMBLYAI_API_KEY", "sk-test");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("returns the headline and points the model produced", async () => {
|
|
177
|
+
const calls = stubGateway('{"headline":"Otters are clever","points":["a","b","c"]}');
|
|
178
|
+
const result = await summarize(ARTICLE);
|
|
179
|
+
|
|
180
|
+
expect(result).toEqual({
|
|
181
|
+
url: ARTICLE.url,
|
|
182
|
+
headline: "Otters are clever",
|
|
183
|
+
points: ["a", "b", "c"],
|
|
184
|
+
});
|
|
185
|
+
// The key is a BEARER here — the gateway is OpenAI-compatible, unlike
|
|
186
|
+
// AssemblyAI's streaming sockets, which take it raw.
|
|
187
|
+
expect(calls[0]?.headers.authorization).toBe("Bearer sk-test");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("unwraps a fenced reply rather than failing on it", async () => {
|
|
191
|
+
stubGateway('```json\n{"headline":"H","points":["a"]}\n```');
|
|
192
|
+
expect((await summarize(ARTICLE)).headline).toBe("H");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("throws PLAINLY when the model answered with prose, so the step retries", async () => {
|
|
196
|
+
// The distinction that is the whole retry policy: a model that ignored the
|
|
197
|
+
// format may well obey on the next attempt, where a 401 will not. Plain
|
|
198
|
+
// means NOT a `FatalError`, which is what the DevKit stops retrying on.
|
|
199
|
+
stubGateway("Here is a summary of the article about otters.");
|
|
200
|
+
const err = await summarize(ARTICLE).catch((thrown: unknown) => thrown);
|
|
201
|
+
expect(err).toBeInstanceOf(Error);
|
|
202
|
+
expect((err as Error).name).not.toBe("FatalError");
|
|
203
|
+
expect(err).toMatchObject({ message: expect.stringContaining("Expected JSON from the model") });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("rejects JSON of the wrong shape as firmly as no JSON at all", async () => {
|
|
207
|
+
// The reply parses and is an object, so only the SCHEMA catches it — which
|
|
208
|
+
// is what taking a schema bought over the guard this used to hand-roll: the
|
|
209
|
+
// failure NAMES the field that was missing.
|
|
210
|
+
stubGateway('{"headline":"H"}');
|
|
211
|
+
await expect(summarize(ARTICLE)).rejects.toThrow(/did not match the shape: points/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("fails FATALLY with no API key rather than retrying five times", async () => {
|
|
215
|
+
vi.stubEnv("ASSEMBLYAI_API_KEY", "");
|
|
216
|
+
stubGateway('{"headline":"H","points":["a"]}');
|
|
217
|
+
await expect(summarize(ARTICLE)).rejects.toThrow(/ASSEMBLYAI_API_KEY/);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("retries beyond the default, because a rate limit and a bad format both happen", () => {
|
|
221
|
+
expect(summarize.maxRetries).toBeGreaterThan(3);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* A WORKFLOW APP — the worked example for `workflowApp()`.
|
|
4
|
+
*
|
|
5
|
+
* Its front door is a form, not a microphone. There is no session, no
|
|
6
|
+
* WebSocket, and no voice pipeline: the page (`client.tsx`) starts a run over
|
|
7
|
+
* the workflow HTTP API and watches it, and the run outlives the tab.
|
|
8
|
+
*
|
|
9
|
+
* ## What makes this a different KIND of agent from `research-workflow`
|
|
10
|
+
*
|
|
11
|
+
* `research-workflow` is a voice agent that HANDS OFF to a workflow — a caller is on
|
|
12
|
+
* the line, so a tool starts a run and answers the turn. Here the workflow is
|
|
13
|
+
* the entire product, and the declaration says so:
|
|
14
|
+
*
|
|
15
|
+
* - `workflowApp()` is `agent({ …, page: "static" })` with the discriminant
|
|
16
|
+
* already set. The declaration is not decoration: `createServer` declines
|
|
17
|
+
* `/websocket` with a reason (so a page mounted with `client()` by mistake
|
|
18
|
+
* fails the same way in `aai dev` and in production rather than only after a
|
|
19
|
+
* deploy) and telephony defaults off.
|
|
20
|
+
* - There is no `stt`/`llm`/`tts`, no `tools` and no `systemPrompt` — and they
|
|
21
|
+
* are not merely omitted, they are UNDECLARABLE here. Nothing talks and no
|
|
22
|
+
* model runs, so every one of them was inert; this file used to carry a
|
|
23
|
+
* `systemPrompt` addressed to a model that never ran.
|
|
24
|
+
*
|
|
25
|
+
* ## What the page needs from this file, and how it gets it
|
|
26
|
+
*
|
|
27
|
+
* Only the workflow's NAME and its output TYPE. The name is the key in
|
|
28
|
+
* `workflows` below — `"digest"` — and the type is
|
|
29
|
+
* `WorkflowOutputOf<typeof digest>`, derived from a `import type` of this module
|
|
30
|
+
* which is ERASED at build time, so naming it pulls no server graph into the
|
|
31
|
+
* browser bundle. Nothing is generated and nothing is restated.
|
|
32
|
+
*
|
|
33
|
+
* ## What it needs
|
|
34
|
+
*
|
|
35
|
+
* `ASSEMBLYAI_API_KEY` in the agent env — `.env` under `aai dev`, `aai secret
|
|
36
|
+
* put ASSEMBLYAI_API_KEY` once deployed — because the run really reads the page
|
|
37
|
+
* and really summarizes it with a model. `requiredEnv` below is what makes a
|
|
38
|
+
* deploy check for it rather than letting the first run find out, and it is
|
|
39
|
+
* load-bearing here in a way it is not for a voice agent: a workflow app
|
|
40
|
+
* declares no providers, so nothing else in its config names a credential.
|
|
41
|
+
*
|
|
42
|
+
* A step is handed no `ToolContext`, so it reads that key with `requireStepEnv`
|
|
43
|
+
* rather than `ctx.env`; see `workflows/digest.ts` and `research-workflow`'s module
|
|
44
|
+
* doc for the one thing that changes under `aai dev` (the key has to be in
|
|
45
|
+
* `.env`, not just your shell).
|
|
46
|
+
*
|
|
47
|
+
* Requires storage (`aai storage enable`, or `DATABASE_URL` under `aai dev`) —
|
|
48
|
+
* runs and the correlation-key index both live there.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { workflow, workflowApp } from "@alexkroman1/aai";
|
|
52
|
+
import { z } from "zod";
|
|
53
|
+
import { digestFlow } from "./workflows/digest.ts";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The declaration: schema, description, and the directive body.
|
|
57
|
+
*
|
|
58
|
+
* The `input` schema does double duty here in a way it cannot for a voice agent.
|
|
59
|
+
* It validates at `start()` — so a bad URL is a 400 at the call site rather than
|
|
60
|
+
* a failed run discovered later — and it is served on `GET /workflows` as JSON
|
|
61
|
+
* Schema, which is what lets a page render a form from a workflow it was not
|
|
62
|
+
* written against.
|
|
63
|
+
*/
|
|
64
|
+
export const digest = workflow({
|
|
65
|
+
description: "Read a link, reduce it to a headline and three points, then file the digest",
|
|
66
|
+
input: z.object({
|
|
67
|
+
url: z.url().describe("The link to digest"),
|
|
68
|
+
}),
|
|
69
|
+
run: digestFlow,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default workflowApp({
|
|
73
|
+
name: "Link Digest",
|
|
74
|
+
// The whole product. A workflow app is an agent whose work happens here.
|
|
75
|
+
workflows: { digest },
|
|
76
|
+
// Checked at deploy time. A workflow app declares no providers, so this is the
|
|
77
|
+
// only thing that can name the credential its steps read.
|
|
78
|
+
requiredEnv: ["ASSEMBLYAI_API_KEY"],
|
|
79
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* The browser half of a workflow app.
|
|
4
|
+
*
|
|
5
|
+
* Mounted with `page()` rather than `client()`: there is no session to build, so
|
|
6
|
+
* there is no socket, no audio graph, and no microphone request. Everything else
|
|
7
|
+
* is the same — the same `client.tsx` filename, React, Tailwind, and the same
|
|
8
|
+
* theme tokens the voice components read.
|
|
9
|
+
*
|
|
10
|
+
* What replaces `useSession()` is three things: `createWorkflowApi()` to start a
|
|
11
|
+
* run, `useWorkflowRun()` to watch its STATUS, and `useWorkflowProgress()` to
|
|
12
|
+
* read what it has WRITTEN. The API is durable, so the `runId` is the whole
|
|
13
|
+
* state — it survives a reload, a different device, or `curl`.
|
|
14
|
+
*
|
|
15
|
+
* ## Status and progress are different questions
|
|
16
|
+
*
|
|
17
|
+
* `useWorkflowRun` answers "where has this got to" from the world's own record —
|
|
18
|
+
* pending, running, completed. `useWorkflowProgress` answers "what is it doing"
|
|
19
|
+
* from what the run wrote itself (`report()` in `workflows/digest.ts`). A page
|
|
20
|
+
* with only the first shows "Working…" for the length of the run; a page with
|
|
21
|
+
* only the second cannot tell a finished run from a quiet one. Both are cheap:
|
|
22
|
+
* one stream each, ended by the agent when there is nothing left to say.
|
|
23
|
+
*
|
|
24
|
+
* Progress also REPLAYS — chunks are retained with the run — so a reload mid-run
|
|
25
|
+
* catches up rather than starting from whatever arrives next. This page renders
|
|
26
|
+
* only the newest line, because on a page this small that is the whole of what a
|
|
27
|
+
* status wants; `transcription-workflow` renders the full log, where a fan-out makes
|
|
28
|
+
* the history worth seeing.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createWorkflowApi, page, useWorkflowProgress, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
32
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
33
|
+
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
34
|
+
// nothing — and it is what stops this file restating a shape `workflows/
|
|
35
|
+
// digest.ts` already declares.
|
|
36
|
+
import type { WorkflowOutputOf } from "@alexkroman1/aai";
|
|
37
|
+
// The one runtime import from the SDK a browser bundle wants: `/utils` is the
|
|
38
|
+
// zod-free subpath, so it costs a few hundred bytes rather than the root
|
|
39
|
+
// barrel's module graph.
|
|
40
|
+
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
41
|
+
import { useState } from "react";
|
|
42
|
+
import type { digest } from "./agent.ts";
|
|
43
|
+
|
|
44
|
+
/** What a completed run reports, derived from the workflow rather than restated. */
|
|
45
|
+
type Digest = WorkflowOutputOf<typeof digest>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hoisted out of the component deliberately.
|
|
49
|
+
*
|
|
50
|
+
* `useWorkflowRun` holds the client in a ref precisely so a fresh object per
|
|
51
|
+
* render cannot restart its watch, but building one in render is still a new
|
|
52
|
+
* `fetch` closure every time and reads as though it were free.
|
|
53
|
+
*/
|
|
54
|
+
const api = createWorkflowApi();
|
|
55
|
+
|
|
56
|
+
export function App() {
|
|
57
|
+
const [url, setUrl] = useState("");
|
|
58
|
+
const [runId, setRunId] = useState<string>();
|
|
59
|
+
const [error, setError] = useState<string>();
|
|
60
|
+
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
61
|
+
// `run.output` instead of `unknown`.
|
|
62
|
+
const { run, polling } = useWorkflowRun<Digest>(runId, { api });
|
|
63
|
+
// What the run has SAID, as against where it has got to. Defaults to `string`,
|
|
64
|
+
// which is what `report()` writes.
|
|
65
|
+
const { latest, supported } = useWorkflowProgress(runId, { api });
|
|
66
|
+
|
|
67
|
+
const submit = async (event: React.FormEvent) => {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
setError(undefined);
|
|
70
|
+
try {
|
|
71
|
+
// Resolves as soon as the run exists — deliberately not when it finishes.
|
|
72
|
+
// That is the whole mechanism: the digest sleeps for a while, and this
|
|
73
|
+
// page is free to be closed in the meantime.
|
|
74
|
+
setRunId(await api.start("digest", { url }));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// The agent's own sentence: an input failing the workflow's schema names
|
|
77
|
+
// the issue, which is better copy than anything this page could write.
|
|
78
|
+
setError(errorMessage(err));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
|
|
84
|
+
<h1 className="text-2xl font-medium">Link Digest</h1>
|
|
85
|
+
|
|
86
|
+
<form onSubmit={submit} className="flex gap-2">
|
|
87
|
+
<input
|
|
88
|
+
type="url"
|
|
89
|
+
required
|
|
90
|
+
value={url}
|
|
91
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
92
|
+
placeholder="https://example.com/article"
|
|
93
|
+
className="flex-1 rounded-md border px-3 py-2"
|
|
94
|
+
/>
|
|
95
|
+
<button type="submit" disabled={polling} className="rounded-md border px-4 py-2">
|
|
96
|
+
{polling ? "Working…" : "Digest"}
|
|
97
|
+
</button>
|
|
98
|
+
</form>
|
|
99
|
+
|
|
100
|
+
{error !== undefined && <p className="text-red-600">{error}</p>}
|
|
101
|
+
|
|
102
|
+
{/* A run that has not settled says so. `polling` is not derivable from the
|
|
103
|
+
snapshot alone — an id the agent never knew leaves `run` undefined,
|
|
104
|
+
which would otherwise read as "still waiting" forever. */}
|
|
105
|
+
{polling && <p>You can close this tab — the run continues without it.</p>}
|
|
106
|
+
|
|
107
|
+
{/* The run's own narration, newest line only — see the module doc.
|
|
108
|
+
|
|
109
|
+
`supported` is what keeps this from being blank forever on an agent
|
|
110
|
+
deployed before progress streams existed: "wrote nothing yet" and
|
|
111
|
+
"serves no stream" are indistinguishable from `progress` alone. */}
|
|
112
|
+
{supported && latest !== undefined && <p className="text-sm opacity-70">{latest}</p>}
|
|
113
|
+
|
|
114
|
+
{/* The counterpart of the `sleep` in `workflows/digest.ts`. Without it the
|
|
115
|
+
only handle on a sleeping run is `cancel`, so "file it now" and "throw
|
|
116
|
+
it away" would be the same button. `wake` answering 0 means the run had
|
|
117
|
+
already moved past its wait, which is why nothing here treats that as a
|
|
118
|
+
failure. */}
|
|
119
|
+
{runId !== undefined && polling && (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => void api.wake(runId)}
|
|
123
|
+
className="self-start rounded-md border px-3 py-1 text-sm"
|
|
124
|
+
>
|
|
125
|
+
File it now
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{run?.status === "failed" && <p className="text-red-600">That one failed: {run.error}</p>}
|
|
130
|
+
|
|
131
|
+
{run?.status === "completed" && (
|
|
132
|
+
<article className="flex flex-col gap-3">
|
|
133
|
+
<h2 className="text-xl">{run.output.headline}</h2>
|
|
134
|
+
<ul className="flex list-disc flex-col gap-1 pl-5">
|
|
135
|
+
{run.output.points.map((point) => (
|
|
136
|
+
<li key={point}>{point}</li>
|
|
137
|
+
))}
|
|
138
|
+
</ul>
|
|
139
|
+
<p className="text-sm opacity-70">Filed {run.output.filedAt}</p>
|
|
140
|
+
</article>
|
|
141
|
+
)}
|
|
142
|
+
</main>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
page({ name: "Link Digest", component: App });
|