@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,248 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
/**
|
|
3
|
+
* The durable half of the link digest: a `"use workflow"` body and its steps,
|
|
4
|
+
* and the steps really read the page and really call a model.
|
|
5
|
+
*
|
|
6
|
+
* The rules are the same ones `research-workflow/workflows/research.ts` spells out
|
|
7
|
+
* — the body is replayed from the top on every resume, so it holds no live
|
|
8
|
+
* handle and makes no undurable decision, and a step's arguments and return
|
|
9
|
+
* value cross a queue and so must be JSON-shaped and small. Read that file for
|
|
10
|
+
* the full argument, including where the API key comes from (a step is handed no
|
|
11
|
+
* `ToolContext`, so `requireStepEnv` is what makes an authenticated call
|
|
12
|
+
* possible at all). What THIS one adds is the case where NOBODY IS WAITING.
|
|
13
|
+
*
|
|
14
|
+
* A voice agent's run is started by a caller who is on the line. This one is
|
|
15
|
+
* started by a form: the page has the `runId` and the tab may be closed the
|
|
16
|
+
* instant after, which is exactly why the surface is an HTTP API over a durable
|
|
17
|
+
* run rather than a request that holds a socket open.
|
|
18
|
+
*
|
|
19
|
+
* ## Two steps, because they fail differently
|
|
20
|
+
*
|
|
21
|
+
* `fetchArticle` reaches a stranger's web server; `summarize` reaches the model.
|
|
22
|
+
* Splitting them is what makes a rate-limited model call replay the FETCH from
|
|
23
|
+
* the journal instead of hammering someone's site again — and it is why the
|
|
24
|
+
* fetched text crosses a queue between them, which is what the cap on it is for.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { throwStepError, toStepError } from "@alexkroman1/aai/step-errors";
|
|
28
|
+
import { report, stepFetch, stepGenerateJson } from "@alexkroman1/aai/utils";
|
|
29
|
+
import { FatalError, sleep } from "workflow";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
|
|
32
|
+
/** How long the digest sits before it is filed, so the wait is visible in dev. */
|
|
33
|
+
const SETTLE = "10 seconds";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Characters of article text carried between the two steps.
|
|
37
|
+
*
|
|
38
|
+
* The cap is the pass-an-id-not-a-payload rule meeting a case where the payload
|
|
39
|
+
* IS the work: the text has to cross the queue, so it is bounded rather than
|
|
40
|
+
* trusted. 24k characters is several thousand words — past where an extra page
|
|
41
|
+
* of boilerplate changes the summary.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_ARTICLE_CHARS = 24_000;
|
|
44
|
+
|
|
45
|
+
/** Points the digest reduces a page to. */
|
|
46
|
+
const POINTS = 3;
|
|
47
|
+
|
|
48
|
+
/** The page fetch's deadline. HTTP has none of its own, and a hung step never ends. */
|
|
49
|
+
const FETCH_TIMEOUT_MS = 30_000;
|
|
50
|
+
|
|
51
|
+
/** What one digest pass produces. Small and JSON-shaped, like every step result. */
|
|
52
|
+
export type Digest = {
|
|
53
|
+
url: string;
|
|
54
|
+
headline: string;
|
|
55
|
+
points: string[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The shape the model is asked for, as something that CHECKS rather than
|
|
60
|
+
* something the compiler is told to believe.
|
|
61
|
+
*
|
|
62
|
+
* `stepGenerateJson` validates against this and throws plainly when the reply
|
|
63
|
+
* misses — which is what a retry is for, since a model that answered with prose
|
|
64
|
+
* may well obey on the next attempt. Before the SDK took the schema, this was a
|
|
65
|
+
* hand-written `isDigestShape` guard beside a hand-written fence stripper.
|
|
66
|
+
*/
|
|
67
|
+
const DigestReply = z.object({
|
|
68
|
+
headline: z.string().trim().min(1),
|
|
69
|
+
points: z.array(z.string()).min(1),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** What the page actually said, on its way from one step to the next. */
|
|
73
|
+
export type Article = {
|
|
74
|
+
url: string;
|
|
75
|
+
/** The `<title>`, or the hostname when there is none. */
|
|
76
|
+
title: string;
|
|
77
|
+
/** Readable text, capped — see {@link MAX_ARTICLE_CHARS}. */
|
|
78
|
+
text: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch a link, summarize it, and file the result.
|
|
83
|
+
*
|
|
84
|
+
* Whatever this returns is what a completed run reports as `output` — so it is
|
|
85
|
+
* literally the page's render model, and `WorkflowOutputOf<typeof digest>` in
|
|
86
|
+
* `client.tsx` is that type, derived rather than restated.
|
|
87
|
+
*/
|
|
88
|
+
export async function digestFlow(input: { url: string }) {
|
|
89
|
+
"use workflow";
|
|
90
|
+
|
|
91
|
+
const article = await fetchArticle(input.url);
|
|
92
|
+
const digest = await summarize(article);
|
|
93
|
+
|
|
94
|
+
// Suspended, not blocked: the sandbox is free to exit here and the run
|
|
95
|
+
// resumes when it comes due. Nothing about the code changes if it is
|
|
96
|
+
// `"6 hours"` — which is the interesting version, and the one that makes an
|
|
97
|
+
// overnight digest a digest rather than a slow request.
|
|
98
|
+
await sleep(SETTLE);
|
|
99
|
+
|
|
100
|
+
return { ...digest, filedAt: await file(digest) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Read the page.
|
|
105
|
+
*
|
|
106
|
+
* A step, so it runs once per successful execution and its result is journaled;
|
|
107
|
+
* a replay returns that result instead of fetching again — which matters here
|
|
108
|
+
* more than usual, because the far side is somebody else's web server.
|
|
109
|
+
*/
|
|
110
|
+
export async function fetchArticle(url: string): Promise<Article> {
|
|
111
|
+
"use step";
|
|
112
|
+
|
|
113
|
+
const { hostname } = new URL(url);
|
|
114
|
+
await report(`Reading ${hostname}…`);
|
|
115
|
+
|
|
116
|
+
// `stepFetch`, not `fetch`, and the rule has no exception for a step that
|
|
117
|
+
// makes only one request: the global pins nothing, so it offers h2 in ALPN
|
|
118
|
+
// and a server that takes it multiplexes every concurrent request from this
|
|
119
|
+
// process onto ONE connection — and a capacity limit then arrives as a stream
|
|
120
|
+
// reset with no HTTP status, which `toStepError` below has nothing to read.
|
|
121
|
+
// It also reports a connection failure with its whole `cause` chain instead
|
|
122
|
+
// of a bare `TypeError: fetch failed`. Redirects are followed by default.
|
|
123
|
+
const response = await stepFetch(url, {
|
|
124
|
+
// Some sites answer a bare request with a challenge page; asking for HTML
|
|
125
|
+
// at least says what we want. Nothing here defeats a real bot wall, and a
|
|
126
|
+
// template pretending otherwise would be the dishonest version.
|
|
127
|
+
headers: { Accept: "text/html,application/xhtml+xml" },
|
|
128
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
129
|
+
});
|
|
130
|
+
// The retryable/terminal split, for the page we were pointed at: a 404 or a
|
|
131
|
+
// 403 answers the same way on the fourth attempt, while a rate limit is
|
|
132
|
+
// exactly what retries are for — and `toStepError` is what carries its
|
|
133
|
+
// `Retry-After` into the DevKit's schedule instead of the default backoff.
|
|
134
|
+
if (!response.ok) throw toStepError(response, `GET ${url} failed: HTTP ${response.status}`);
|
|
135
|
+
|
|
136
|
+
const html = await response.text();
|
|
137
|
+
const text = extractText(html);
|
|
138
|
+
if (text.length < 200) {
|
|
139
|
+
// Not transient: the same URL returns the same near-empty page on a retry.
|
|
140
|
+
// A JS-rendered site is the usual cause and no number of attempts fixes it.
|
|
141
|
+
// A direct `FatalError`, not `throwFatalStepError`: nothing is being
|
|
142
|
+
// classified here and there is no cause to preserve — this step has simply
|
|
143
|
+
// decided, and the helper is for the `catch` block where the linter would
|
|
144
|
+
// (rightly) want the original error kept.
|
|
145
|
+
throw new FatalError(`${hostname} returned no readable text — is the page rendered in JS?`);
|
|
146
|
+
}
|
|
147
|
+
return { url, title: extractTitle(html) ?? hostname, text };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Reduce the page to a headline and a few points.
|
|
152
|
+
*
|
|
153
|
+
* Separate from the fetch on purpose: a rate-limited model call replays the
|
|
154
|
+
* fetch from the journal rather than hitting a stranger's server again. The
|
|
155
|
+
* whole Node runtime is available here, unlike in the body.
|
|
156
|
+
*/
|
|
157
|
+
export async function summarize(article: Article): Promise<Digest> {
|
|
158
|
+
"use step";
|
|
159
|
+
|
|
160
|
+
await report("Pulling out the claims worth keeping.");
|
|
161
|
+
|
|
162
|
+
// `stepGenerateJson` unwraps the fence a model puts around JSON, parses it,
|
|
163
|
+
// and validates it against `DigestReply` — and throws PLAINLY when any of
|
|
164
|
+
// those misses, which is the whole retry policy in one distinction: a model
|
|
165
|
+
// that answered with prose may answer correctly on the next attempt, where a
|
|
166
|
+
// 401 will not. `throwStepError` is what makes the 401 half terminal.
|
|
167
|
+
const parsed = await stepGenerateJson(
|
|
168
|
+
`Title: ${article.title}\nURL: ${article.url}\n\n${article.text}`,
|
|
169
|
+
{
|
|
170
|
+
schema: DigestReply,
|
|
171
|
+
system:
|
|
172
|
+
`You digest articles. Reply with JSON only: {"headline": string, "points": string[]}. ` +
|
|
173
|
+
`Give exactly ${POINTS} points. No markdown fence, no preamble.`,
|
|
174
|
+
},
|
|
175
|
+
).catch(throwStepError);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
url: article.url,
|
|
179
|
+
headline: parsed.headline,
|
|
180
|
+
points: parsed.points.slice(0, POINTS),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A rate limit — and a model that ignored the format — are both expected here. */
|
|
185
|
+
summarize.maxRetries = 5;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* File the digest.
|
|
189
|
+
*
|
|
190
|
+
* Separate from `summarize` on purpose: two steps mean a crash between them
|
|
191
|
+
* replays the expensive half for free and re-issues only the cheap one.
|
|
192
|
+
* Returning the timestamp rather than reading a clock in the BODY is the same
|
|
193
|
+
* rule — a step's result is journaled and therefore stable across replays,
|
|
194
|
+
* where `Date.now()` in the body would change on every one.
|
|
195
|
+
*/
|
|
196
|
+
export async function file(_digest: Digest): Promise<string> {
|
|
197
|
+
"use step";
|
|
198
|
+
|
|
199
|
+
await report("Filing the digest.");
|
|
200
|
+
// A real desk would write the digest to its database here. The stub writes
|
|
201
|
+
// nothing, which is what the `_` says — and it is a stub because `ctx.db` is
|
|
202
|
+
// the half of a tool context a step still cannot reach.
|
|
203
|
+
return new Date().toISOString();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---- Pure helpers -----------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/** The document's `<title>`, when it has one. */
|
|
209
|
+
export function extractTitle(html: string): string | undefined {
|
|
210
|
+
const title = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1];
|
|
211
|
+
return title ? decodeEntities(title).replace(/\s+/g, " ").trim() || undefined : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Reduce HTML to the text a model should read.
|
|
216
|
+
*
|
|
217
|
+
* Deliberately crude, and the crudeness is the honest part: a real extractor is
|
|
218
|
+
* a readability implementation and a dependency, where this is four `replace`
|
|
219
|
+
* calls that get most of an article. What it MUST do is drop `<script>` and
|
|
220
|
+
* `<style>` CONTENT — stripping tags alone leaves a page's JavaScript in the
|
|
221
|
+
* prompt, which is both expensive and a way to smuggle instructions past the
|
|
222
|
+
* reader.
|
|
223
|
+
*/
|
|
224
|
+
export function extractText(html: string): string {
|
|
225
|
+
return decodeEntities(
|
|
226
|
+
html
|
|
227
|
+
.replace(/<(script|style|noscript|template)[^>]*>[\s\S]*?<\/\1>/gi, " ")
|
|
228
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
229
|
+
.replace(/<[^>]+>/g, " "),
|
|
230
|
+
)
|
|
231
|
+
.replace(/\s+/g, " ")
|
|
232
|
+
.trim()
|
|
233
|
+
.slice(0, MAX_ARTICLE_CHARS);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The five entities that survive tag-stripping often enough to matter. */
|
|
237
|
+
function decodeEntities(text: string): string {
|
|
238
|
+
return (
|
|
239
|
+
text
|
|
240
|
+
.replace(/</g, "<")
|
|
241
|
+
.replace(/>/g, ">")
|
|
242
|
+
.replace(/"/g, '"')
|
|
243
|
+
.replace(/�?39;|'/g, "'")
|
|
244
|
+
.replace(/ /g, " ")
|
|
245
|
+
// `&` LAST, or `&lt;` decodes twice into a `<` the page never had.
|
|
246
|
+
.replace(/&/g, "&")
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
2
|
import { assemblyAILlm } from "@alexkroman1/aai/llm";
|
|
3
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
4
3
|
|
|
5
4
|
export default agent({
|
|
6
5
|
name: "Math Buddy",
|
|
@@ -10,7 +9,6 @@ export default agent({
|
|
|
10
9
|
// and TTS stay on the AssemblyAI defaults. (The string shorthand
|
|
11
10
|
// `llm: "gemini-2.5-flash-lite"` desugars to this same descriptor.)
|
|
12
11
|
llm: assemblyAILlm({ model: "gemini-2.5-flash-lite" }),
|
|
13
|
-
systemPrompt,
|
|
14
12
|
greeting:
|
|
15
13
|
"Hey, I'm Math Buddy. Try asking me something like, what's 127 times 849, convert 5 miles to kilometers, or roll 3 twenty-sided dice.",
|
|
16
14
|
builtinTools: ["run_code"],
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { toAgentConfig } from "@alexkroman1/aai/manifest";
|
|
4
|
+
import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import authoredAgent from "./agent.ts";
|
|
7
|
+
import { CATEGORIES, MOODS } from "./shared.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
11
|
+
*
|
|
12
|
+
* The glob is written HERE rather than reached for from a shared helper because
|
|
13
|
+
* this file SHIPS — a scaffolded project has no repo helper to import.
|
|
14
|
+
*/
|
|
15
|
+
const agentDef = withDiscoveredTools(
|
|
16
|
+
authoredAgent,
|
|
17
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
describe("night-owl template", () => {
|
|
21
|
+
test("config passes manifest validation", () => {
|
|
22
|
+
// Same conversion `aai build`/`aai deploy` run.
|
|
23
|
+
expect(() => toAgentConfig(agentDef)).not.toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("declares run_code, which is the other half of the template", () => {
|
|
27
|
+
// The sleep-time arithmetic is the builtin's job, not a tool's — that split
|
|
28
|
+
// IS this template's subject, so a dropped builtin leaves the agent doing
|
|
29
|
+
// mental arithmetic out loud.
|
|
30
|
+
expect(agentDef.builtinTools).toContain("run_code");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("recommend is discovered from tools/", () => {
|
|
34
|
+
expect(Object.keys(agentDef.tools ?? {})).toEqual(["recommend"]);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("recommend", () => {
|
|
39
|
+
test("answers with picks for the category and mood asked for", async () => {
|
|
40
|
+
const ctx = createToolContext();
|
|
41
|
+
const result = await runTool(agentDef, "recommend", { category: "movie", mood: "cozy" }, ctx);
|
|
42
|
+
expect(result).toMatchObject({ category: "movie", mood: "cozy" });
|
|
43
|
+
expect((result as { picks: string[] }).picks.length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("pushes the same picks to the client, which is what the page renders", async () => {
|
|
47
|
+
// `ctx.send` is the only reason this tool takes a context at all: the
|
|
48
|
+
// client renders the picks rather than waiting to hear them read aloud.
|
|
49
|
+
const ctx = createToolContext();
|
|
50
|
+
const result = await runTool(agentDef, "recommend", { category: "book", mood: "spooky" }, ctx);
|
|
51
|
+
expect(ctx.sent).toEqual([{ event: "recommendations", data: result }]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("every category/mood pair the schema admits has picks behind it", async () => {
|
|
55
|
+
// The table is hand-written and the schema is generated from the same two
|
|
56
|
+
// const arrays, so a category added to `shared.ts` and forgotten in the
|
|
57
|
+
// table is a `TypeError` on the first call — the exact failure this
|
|
58
|
+
// package's guide records three shipped tools having.
|
|
59
|
+
for (const category of CATEGORIES) {
|
|
60
|
+
for (const mood of MOODS) {
|
|
61
|
+
const result = await runTool(
|
|
62
|
+
agentDef,
|
|
63
|
+
"recommend",
|
|
64
|
+
{ category, mood },
|
|
65
|
+
createToolContext(),
|
|
66
|
+
);
|
|
67
|
+
expect((result as { picks: string[] }).picks, `${category}/${mood}`).not.toHaveLength(0);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a mood outside the enum is refused by the schema", async () => {
|
|
73
|
+
// The wire boundary: an LLM tool call is untyped, so the schema is the only
|
|
74
|
+
// thing between a hallucinated mood and an index into `undefined`.
|
|
75
|
+
const schema = agentDef.tools?.recommend?.inputSchema;
|
|
76
|
+
if (!schema) throw new Error("recommend has no input schema");
|
|
77
|
+
const bad = await schema["~standard"].validate({ category: "movie", mood: "melancholy" });
|
|
78
|
+
expect(bad.issues).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -1,75 +1,8 @@
|
|
|
1
|
-
import { agent
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { CATEGORIES, type Category, MOODS, type Mood } from "./shared.ts";
|
|
4
|
-
|
|
5
|
-
const PICKS: Record<Category, Record<Mood, string[]>> = {
|
|
6
|
-
movie: {
|
|
7
|
-
chill: ["Lost in Translation", "The Grand Budapest Hotel", "Amelie"],
|
|
8
|
-
intense: ["Inception", "Interstellar", "The Dark Knight"],
|
|
9
|
-
cozy: ["When Harry Met Sally", "The Holiday", "Paddington 2"],
|
|
10
|
-
spooky: ["The Shining", "Get Out", "Hereditary"],
|
|
11
|
-
funny: ["The Big Lebowski", "Airplane!", "Superbad"],
|
|
12
|
-
},
|
|
13
|
-
music: {
|
|
14
|
-
chill: ["Khruangbin — Con Todo El Mundo", "Tycho — Dive", "Bonobo — Migration"],
|
|
15
|
-
intense: ["Radiohead — OK Computer", "Tool — Lateralus", "Deftones — White Pony"],
|
|
16
|
-
cozy: [
|
|
17
|
-
"Norah Jones — Come Away with Me",
|
|
18
|
-
"Iron & Wine — Our Endless Numbered Days",
|
|
19
|
-
"Bon Iver — For Emma, Forever Ago",
|
|
20
|
-
],
|
|
21
|
-
spooky: [
|
|
22
|
-
"Portishead — Dummy",
|
|
23
|
-
"Massive Attack — Mezzanine",
|
|
24
|
-
"Boards of Canada — Music Has the Right to Children",
|
|
25
|
-
],
|
|
26
|
-
funny: [
|
|
27
|
-
"Weird Al — Running with Scissors",
|
|
28
|
-
"Flight of the Conchords — S/T",
|
|
29
|
-
"Tenacious D — S/T",
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
book: {
|
|
33
|
-
chill: ["Norwegian Wood — Murakami", "The Alchemist — Coelho", "Siddhartha — Hesse"],
|
|
34
|
-
intense: ["Blood Meridian — McCarthy", "House of Leaves — Danielewski", "Neuromancer — Gibson"],
|
|
35
|
-
cozy: [
|
|
36
|
-
"The House in the Cerulean Sea — Klune",
|
|
37
|
-
"A Man Called Ove — Backman",
|
|
38
|
-
"Anxious People — Backman",
|
|
39
|
-
],
|
|
40
|
-
spooky: [
|
|
41
|
-
"The Haunting of Hill House — Jackson",
|
|
42
|
-
"Mexican Gothic — Moreno-Garcia",
|
|
43
|
-
"The Turn of the Screw — James",
|
|
44
|
-
],
|
|
45
|
-
funny: ["Good Omens — Pratchett & Gaiman", "Hitchhiker's Guide — Adams", "Catch-22 — Heller"],
|
|
46
|
-
},
|
|
47
|
-
};
|
|
1
|
+
import { agent } from "@alexkroman1/aai";
|
|
48
2
|
|
|
49
3
|
export default agent({
|
|
50
4
|
name: "Night Owl",
|
|
51
|
-
systemPrompt:
|
|
52
|
-
"You are Night Owl, a cozy evening companion. You help people wind down, recommend entertainment, and share interesting facts about the night sky. Keep your tone warm and relaxed. Use short, conversational responses.\n\nUse run_code for sleep calculations:\n- Each sleep cycle is 90 minutes, plus 15 minutes to fall asleep\n- Bedtime = wake_time - (cycles * 90 + 15) minutes\n- If result is negative, add 1440 (24 hours in minutes)\n- Format as HH:MM",
|
|
53
5
|
greeting:
|
|
54
6
|
"Hey there, night owl. Try asking me for a cozy movie recommendation, or tell me what time you need to wake up and I'll calculate the best time to fall asleep.",
|
|
55
7
|
builtinTools: ["run_code"],
|
|
56
|
-
|
|
57
|
-
tools: {
|
|
58
|
-
recommend: tool({
|
|
59
|
-
description: "Get recommendations for movies, music, or books based on mood.",
|
|
60
|
-
inputSchema: z.object({
|
|
61
|
-
category: z.enum(CATEGORIES),
|
|
62
|
-
mood: z.enum(MOODS),
|
|
63
|
-
}),
|
|
64
|
-
async execute(args, ctx) {
|
|
65
|
-
const result = {
|
|
66
|
-
category: args.category,
|
|
67
|
-
mood: args.mood,
|
|
68
|
-
picks: PICKS[args.category][args.mood],
|
|
69
|
-
};
|
|
70
|
-
ctx.send("recommendations", result);
|
|
71
|
-
return result;
|
|
72
|
-
},
|
|
73
|
-
}),
|
|
74
|
-
},
|
|
75
8
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
You are Night Owl, a cozy evening companion. You help people wind down,
|
|
2
|
+
recommend entertainment, and share interesting facts about the night sky. Keep
|
|
3
|
+
your tone warm and relaxed. Use short, conversational responses.
|
|
4
|
+
|
|
5
|
+
Use run_code for sleep calculations:
|
|
6
|
+
|
|
7
|
+
- Each sleep cycle is 90 minutes, plus 15 minutes to fall asleep
|
|
8
|
+
- Bedtime = wake_time - (cycles \* 90 + 15) minutes
|
|
9
|
+
- If result is negative, add 1440 (24 hours in minutes)
|
|
10
|
+
- Format as HH:MM
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { CATEGORIES, type Category, MOODS, type Mood } from "../shared.ts";
|
|
4
|
+
|
|
5
|
+
const PICKS: Record<Category, Record<Mood, string[]>> = {
|
|
6
|
+
movie: {
|
|
7
|
+
chill: ["Lost in Translation", "The Grand Budapest Hotel", "Amelie"],
|
|
8
|
+
intense: ["Inception", "Interstellar", "The Dark Knight"],
|
|
9
|
+
cozy: ["When Harry Met Sally", "The Holiday", "Paddington 2"],
|
|
10
|
+
spooky: ["The Shining", "Get Out", "Hereditary"],
|
|
11
|
+
funny: ["The Big Lebowski", "Airplane!", "Superbad"],
|
|
12
|
+
},
|
|
13
|
+
music: {
|
|
14
|
+
chill: ["Khruangbin — Con Todo El Mundo", "Tycho — Dive", "Bonobo — Migration"],
|
|
15
|
+
intense: ["Radiohead — OK Computer", "Tool — Lateralus", "Deftones — White Pony"],
|
|
16
|
+
cozy: [
|
|
17
|
+
"Norah Jones — Come Away with Me",
|
|
18
|
+
"Iron & Wine — Our Endless Numbered Days",
|
|
19
|
+
"Bon Iver — For Emma, Forever Ago",
|
|
20
|
+
],
|
|
21
|
+
spooky: [
|
|
22
|
+
"Portishead — Dummy",
|
|
23
|
+
"Massive Attack — Mezzanine",
|
|
24
|
+
"Boards of Canada — Music Has the Right to Children",
|
|
25
|
+
],
|
|
26
|
+
funny: [
|
|
27
|
+
"Weird Al — Running with Scissors",
|
|
28
|
+
"Flight of the Conchords — S/T",
|
|
29
|
+
"Tenacious D — S/T",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
book: {
|
|
33
|
+
chill: ["Norwegian Wood — Murakami", "The Alchemist — Coelho", "Siddhartha — Hesse"],
|
|
34
|
+
intense: ["Blood Meridian — McCarthy", "House of Leaves — Danielewski", "Neuromancer — Gibson"],
|
|
35
|
+
cozy: [
|
|
36
|
+
"The House in the Cerulean Sea — Klune",
|
|
37
|
+
"A Man Called Ove — Backman",
|
|
38
|
+
"Anxious People — Backman",
|
|
39
|
+
],
|
|
40
|
+
spooky: [
|
|
41
|
+
"The Haunting of Hill House — Jackson",
|
|
42
|
+
"Mexican Gothic — Moreno-Garcia",
|
|
43
|
+
"The Turn of the Screw — James",
|
|
44
|
+
],
|
|
45
|
+
funny: ["Good Omens — Pratchett & Gaiman", "Hitchhiker's Guide — Adams", "Catch-22 — Heller"],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default tool({
|
|
50
|
+
description: "Get recommendations for movies, music, or books based on mood.",
|
|
51
|
+
inputSchema: z.object({
|
|
52
|
+
category: z.enum(CATEGORIES),
|
|
53
|
+
mood: z.enum(MOODS),
|
|
54
|
+
}),
|
|
55
|
+
async execute(args, ctx) {
|
|
56
|
+
const result = {
|
|
57
|
+
category: args.category,
|
|
58
|
+
mood: args.mood,
|
|
59
|
+
picks: PICKS[args.category][args.mood],
|
|
60
|
+
};
|
|
61
|
+
ctx.send("recommendations", result);
|
|
62
|
+
return result;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
3
2
|
|
|
4
3
|
export default agent({
|
|
5
4
|
name: "Penny",
|
|
6
|
-
systemPrompt,
|
|
7
5
|
greeting:
|
|
8
6
|
"Hey, I'm Penny, your personal finance helper. Try asking me something like, what's 100 dollars in euros, what's the price of bitcoin, or help me split a 120 dollar bill four ways with 20 percent tip.",
|
|
9
7
|
builtinTools: ["run_code", "fetch_json"],
|