@alexkroman1/aai-cli 13.3.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/_artifacts-BJOYGQPp.mjs +21 -0
- package/dist/_artifacts.d.ts +16 -0
- package/dist/_build-target.d.ts +172 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +1 -1
- package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/_server-common-De0haHr9.mjs +70 -0
- package/dist/_server-common.d.ts +20 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
- package/dist/_templates.d.ts +1 -1
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/_vercel-output.d.ts +63 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-BhEaxBPu.mjs +481 -0
- package/dist/build.d.ts +23 -10
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +155 -138
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
- package/dist/client-bundler.d.ts +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
- package/dist/init.d.ts +14 -0
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +155 -76
- package/dist/scaffold/package.json +6 -6
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/start.d.ts +112 -0
- package/dist/start.mjs +156 -0
- package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
- package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/code-interpreter/agent.test.ts +103 -0
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +81 -19
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/math-buddy/agent.test.ts +126 -0
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/personal-finance/agent.test.ts +127 -0
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/agent.ts +8 -0
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/routing.ts +64 -55
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
- package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
- 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/templates/web-researcher/agent.test.ts +130 -0
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.d.ts +1 -1
- package/dist/worker-bundler.mjs +8 -8
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +9 -4
- package/dist/_server-common-BV4_CrP3.mjs +0 -43
- package/dist/build-CWyFGe-W.mjs +0 -69
- package/dist/scaffold/server.mjs +0 -204
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The browser half of a workflow app.
|
|
4
4
|
*
|
|
5
|
-
* Mounted with `
|
|
5
|
+
* Mounted with `mountPage()` rather than `mountClient()`: there is no session to build, so
|
|
6
6
|
* there is no socket, no audio graph, and no microphone request. Everything else
|
|
7
7
|
* is the same — the same `client.tsx` filename, React, Tailwind, and the same
|
|
8
8
|
* theme tokens the voice components read.
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
*
|
|
50
50
|
* `useWorkflowSubmit` answers "where has this got to" from the world's own
|
|
51
51
|
* record — pending, running, completed. `<WorkflowProgress>` answers "what is it
|
|
52
|
-
* doing" from what the run wrote itself (`
|
|
52
|
+
* doing" from what the run wrote itself (`stepReport()` in `workflows/digest.ts`). A
|
|
53
53
|
* page with only the first shows "Working…" for the length of the run; a page
|
|
54
54
|
* with only the second cannot tell a finished run from a quiet one. Both are
|
|
55
55
|
* cheap: one stream each, ended by the agent when there is nothing left to say.
|
|
@@ -62,9 +62,32 @@
|
|
|
62
62
|
* what narrows it to the newest line, because on a page this small that is the
|
|
63
63
|
* whole of what a status wants; `transcription-workflow` renders the full log,
|
|
64
64
|
* where a fan-out makes the history worth seeing.
|
|
65
|
+
*
|
|
66
|
+
* ## Two things that are only ever true for a MOMENT
|
|
67
|
+
*
|
|
68
|
+
* The Copy button and the reply to "File it now" are both a word that appears
|
|
69
|
+
* and then goes away, and both used to be the sort of thing a page writes with
|
|
70
|
+
* a `useState` and a bare `setTimeout` — which gets two things wrong that only
|
|
71
|
+
* show up on the second click (a second flash has its window cut short by the
|
|
72
|
+
* first one's timer) and on unmount (a `setState` into a torn-down tree). They
|
|
73
|
+
* are `useCopy` and `useFlash` from `@alexkroman1/aai-ui`.
|
|
74
|
+
*
|
|
75
|
+
* Reach for `useCopy` when the moment is a clipboard write — it keys the flash
|
|
76
|
+
* by the copied TEXT, so on a page with several copy buttons only the one
|
|
77
|
+
* clicked lights up, and it reports a REFUSED write as `"Failed"` rather than
|
|
78
|
+
* doing nothing visible (there is no clipboard at all on an insecure origin).
|
|
79
|
+
* Reach for `useFlash` for any other transient word; here it carries what
|
|
80
|
+
* `wake()` answered, which is a number and not a failure at 0.
|
|
65
81
|
*/
|
|
66
82
|
|
|
67
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
BulletList,
|
|
85
|
+
mountPage,
|
|
86
|
+
useCopy,
|
|
87
|
+
useFlash,
|
|
88
|
+
useWorkflowSubmit,
|
|
89
|
+
WorkflowProgress,
|
|
90
|
+
} from "@alexkroman1/aai-ui";
|
|
68
91
|
import "@alexkroman1/aai-ui/styles.css";
|
|
69
92
|
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
70
93
|
// nothing — and it is what stops this file restating a shape `workflows/
|
|
@@ -90,12 +113,22 @@ function pendingNote(startedHere: boolean, found: boolean): string {
|
|
|
90
113
|
return "Still working on the digest this tab started earlier. Reloading is safe.";
|
|
91
114
|
}
|
|
92
115
|
|
|
116
|
+
/** The digest as one pasteable block — a headline and its bullets. */
|
|
117
|
+
function asText(headline: string, points: readonly string[]): string {
|
|
118
|
+
return [headline, ...points.map((point) => `- ${point}`)].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
93
121
|
export function App() {
|
|
94
122
|
const [url, setUrl] = useState("");
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
123
|
+
// One copier for the page. It would be one per GROUP of copy buttons on a
|
|
124
|
+
// bigger page — the flash is shared, so clicking a second row clears the
|
|
125
|
+
// first row's "Copied", which is what stops two rows both claiming to be on
|
|
126
|
+
// the clipboard.
|
|
127
|
+
const copier = useCopy();
|
|
128
|
+
// `wake()` resolves with how many sleeps it ended, and 0 is an ANSWER (the
|
|
129
|
+
// run had already moved past its wait) rather than a failure — so the button
|
|
130
|
+
// says which happened, for a moment, and then goes back to being a button.
|
|
131
|
+
const woken = useFlash<string>();
|
|
99
132
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
100
133
|
// `run.output` instead of `unknown`. `error` is the agent's own sentence for a
|
|
101
134
|
// rejected input, which is better copy than anything this page could write, and
|
|
@@ -104,14 +137,14 @@ export function App() {
|
|
|
104
137
|
// No `key` and no `recover`: this tab's handle on its own runs is minted and
|
|
105
138
|
// remembered by the hook, and read back as it mounts. See the module doc for
|
|
106
139
|
// what a page says when it wants a different one.
|
|
107
|
-
const { submit, run, pending, error, wake } =
|
|
140
|
+
const { submit, run, pending, error, wake, startedHere } =
|
|
141
|
+
useWorkflowSubmit<typeof digest>("digest");
|
|
108
142
|
|
|
109
143
|
// `submit()` resolves as soon as the run exists — deliberately not when it
|
|
110
144
|
// finishes. That is the whole mechanism: the digest sleeps for a while, and
|
|
111
145
|
// this page is free to be closed in the meantime.
|
|
112
146
|
const onSubmit = (event: React.FormEvent) => {
|
|
113
147
|
event.preventDefault();
|
|
114
|
-
setStartedHere(true);
|
|
115
148
|
void submit({ url });
|
|
116
149
|
};
|
|
117
150
|
|
|
@@ -125,6 +158,11 @@ export function App() {
|
|
|
125
158
|
required
|
|
126
159
|
value={url}
|
|
127
160
|
onChange={(e) => setUrl(e.target.value)}
|
|
161
|
+
// A placeholder is not a name: it disappears the moment anything is
|
|
162
|
+
// typed, and a screen reader reaches an unlabelled box. The declared
|
|
163
|
+
// fields in `@alexkroman1/aai-ui` say the same thing — a `label`, or
|
|
164
|
+
// an `aria-label` where the row has no room for one.
|
|
165
|
+
aria-label="Article URL"
|
|
128
166
|
// A REAL article, because a placeholder is a suggestion and this one
|
|
129
167
|
// gets typed. `https://example.com/article` 404s, and the bare
|
|
130
168
|
// `example.com` a reader trims it to has no prose on it — so the first
|
|
@@ -139,7 +177,13 @@ export function App() {
|
|
|
139
177
|
</button>
|
|
140
178
|
</form>
|
|
141
179
|
|
|
142
|
-
{
|
|
180
|
+
{/* `<Form>` carries `role="alert"` for the templates that declare their
|
|
181
|
+
fields; a hand-written form has to say it itself. */}
|
|
182
|
+
{error !== undefined && (
|
|
183
|
+
<p role="alert" className="text-red-600">
|
|
184
|
+
{error}
|
|
185
|
+
</p>
|
|
186
|
+
)}
|
|
143
187
|
|
|
144
188
|
{/* A run that has not settled says so. `pending` is not derivable from the
|
|
145
189
|
snapshot alone — an id the agent never knew leaves `run` undefined,
|
|
@@ -161,28 +205,46 @@ export function App() {
|
|
|
161
205
|
{pending && (
|
|
162
206
|
<button
|
|
163
207
|
type="button"
|
|
164
|
-
onClick={() =>
|
|
208
|
+
onClick={() => {
|
|
209
|
+
void wake().then((count) =>
|
|
210
|
+
woken.flash(count > 0 ? "Filing it now" : "Already past its wait"),
|
|
211
|
+
);
|
|
212
|
+
}}
|
|
165
213
|
className="self-start rounded-md border px-3 py-1 text-sm"
|
|
166
214
|
>
|
|
167
|
-
File it now
|
|
215
|
+
{woken.value ?? "File it now"}
|
|
168
216
|
</button>
|
|
169
217
|
)}
|
|
170
218
|
|
|
171
|
-
{
|
|
219
|
+
{/* `role="alert"`, like the submit error above: this is the outcome the
|
|
220
|
+
reader has been waiting for, and it can arrive long after they looked
|
|
221
|
+
away. */}
|
|
222
|
+
{run?.status === "failed" && (
|
|
223
|
+
<p role="alert" className="text-red-600">
|
|
224
|
+
That one failed: {run.error}
|
|
225
|
+
</p>
|
|
226
|
+
)}
|
|
172
227
|
|
|
173
228
|
{run?.status === "completed" && (
|
|
174
229
|
<article className="flex flex-col gap-3">
|
|
175
230
|
<h2 className="text-xl">{run.output.headline}</h2>
|
|
176
|
-
<
|
|
177
|
-
{run.output.points.map((point) => (
|
|
178
|
-
<li key={point}>{point}</li>
|
|
179
|
-
))}
|
|
180
|
-
</ul>
|
|
231
|
+
<BulletList items={run.output.points} />
|
|
181
232
|
<p className="text-sm opacity-70">Filed {run.output.filedAt}</p>
|
|
233
|
+
{/* The whole digest as plain text, which is what somebody pasting it
|
|
234
|
+
into a note wants. `copier.label` is the button's own text: it
|
|
235
|
+
reads "Copy" until it is clicked, then "Copied" — or "Failed",
|
|
236
|
+
which is the case a hand-rolled version silently drops. */}
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={() => copier.copy(asText(run.output.headline, run.output.points))}
|
|
240
|
+
className="self-start rounded-md border px-3 py-1 text-sm"
|
|
241
|
+
>
|
|
242
|
+
{copier.label(asText(run.output.headline, run.output.points))}
|
|
243
|
+
</button>
|
|
182
244
|
</article>
|
|
183
245
|
)}
|
|
184
246
|
</main>
|
|
185
247
|
);
|
|
186
248
|
}
|
|
187
249
|
|
|
188
|
-
|
|
250
|
+
mountPage({ name: "Link Digest", component: App });
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
* fetched text crosses a queue between them, which is what the cap on it is for.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import type {
|
|
27
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
28
28
|
import { htmlToText, pageMetadata } from "@alexkroman1/aai/html";
|
|
29
|
-
import {
|
|
30
|
-
import { FatalError,
|
|
29
|
+
import { stepInfo, stepReport } from "@alexkroman1/aai/step";
|
|
30
|
+
import { FatalError, stepFetchOrFail, stepGenerateJsonOrFail } from "@alexkroman1/aai/step-errors";
|
|
31
31
|
import { omitUndefined } from "@alexkroman1/aai/utils";
|
|
32
32
|
import { z } from "zod";
|
|
33
33
|
|
|
@@ -107,7 +107,7 @@ export type Article = {
|
|
|
107
107
|
* literally the page's render model, and `WorkflowOutputOf<typeof digest>` in
|
|
108
108
|
* `client.tsx` is that type, derived rather than restated.
|
|
109
109
|
*/
|
|
110
|
-
export async function digestFlow(input: { url: string }, ctx:
|
|
110
|
+
export async function digestFlow(input: { url: string }, ctx: WorkflowContext) {
|
|
111
111
|
const article = await ctx.step("fetchArticle", () => fetchArticle(input.url));
|
|
112
112
|
// `maxAttempts: 6` was `summarize.maxRetries = 5` — five retries AFTER the
|
|
113
113
|
// first attempt, so six in all. The retry policy moved from a property on the
|
|
@@ -138,7 +138,7 @@ export async function digestFlow(input: { url: string }, ctx: WorkflowCtx) {
|
|
|
138
138
|
*/
|
|
139
139
|
export async function fetchArticle(url: string): Promise<Article> {
|
|
140
140
|
const { hostname } = new URL(url);
|
|
141
|
-
await
|
|
141
|
+
await stepReport(`Reading ${hostname}…`);
|
|
142
142
|
|
|
143
143
|
// `stepFetch`, not `fetch`, and the rule has no exception for a step that
|
|
144
144
|
// makes only one request: the global pins nothing, so it offers h2 in ALPN
|
|
@@ -147,12 +147,12 @@ export async function fetchArticle(url: string): Promise<Article> {
|
|
|
147
147
|
// reset with no HTTP status, which `toStepError` below has nothing to read.
|
|
148
148
|
// It also reports a connection failure with its whole `cause` chain instead
|
|
149
149
|
// of a bare `TypeError: fetch failed`. Redirects are followed by default.
|
|
150
|
-
// `
|
|
150
|
+
// `stepFetchOrFail` rather than `stepFetch` + an `ok` check: it makes the
|
|
151
151
|
// retryable/terminal split for us — a 404 or a 403 answers the same way on
|
|
152
152
|
// the fourth attempt, while a rate limit is exactly what retries are for, and
|
|
153
153
|
// its `Retry-After` reaches the engine's schedule instead of the default
|
|
154
154
|
// backoff. It also puts the server's own error text in the message.
|
|
155
|
-
const response = await
|
|
155
|
+
const response = await stepFetchOrFail(url, {
|
|
156
156
|
// Some sites answer a bare request with a challenge page; asking for HTML
|
|
157
157
|
// at least says what we want. Nothing here defeats a real bot wall, and a
|
|
158
158
|
// template pretending otherwise would be the dishonest version.
|
|
@@ -191,19 +191,19 @@ export async function summarize(article: Article): Promise<Digest> {
|
|
|
191
191
|
// built with `omitUndefined` rather than a conditional spread — the guard is
|
|
192
192
|
// then the value, which is the case that primitive is for.
|
|
193
193
|
const model = lastChance ? FALLBACK_MODEL : undefined;
|
|
194
|
-
await
|
|
194
|
+
await stepReport(
|
|
195
195
|
lastChance
|
|
196
196
|
? `Last attempt (${step?.attempt} of ${step?.maxAttempts}): asking for something simpler.`
|
|
197
197
|
: "Pulling out the claims worth keeping.",
|
|
198
198
|
);
|
|
199
199
|
|
|
200
|
-
// `
|
|
200
|
+
// `stepGenerateJsonOrFail` unwraps the fence a model puts around JSON,
|
|
201
201
|
// parses it, and validates it against `DigestReply` — and throws PLAINLY when
|
|
202
202
|
// any of those misses, which is the whole retry policy in one distinction: a
|
|
203
203
|
// model that answered with prose may answer correctly on the next attempt,
|
|
204
|
-
// where a 401 will not. The `
|
|
204
|
+
// where a 401 will not. The `OrFail` suffix is what makes the 401 half
|
|
205
205
|
// terminal: it is `stepGenerateJson` with `throwStepError` already applied.
|
|
206
|
-
const parsed = await
|
|
206
|
+
const parsed = await stepGenerateJsonOrFail(
|
|
207
207
|
`Title: ${article.title}\nURL: ${article.url}\n\n${article.text}`,
|
|
208
208
|
{
|
|
209
209
|
schema: DigestReply,
|
|
@@ -246,7 +246,7 @@ export async function summarize(article: Article): Promise<Digest> {
|
|
|
246
246
|
* file(digest))`. Anything at BODY level is the bug, not an exception.
|
|
247
247
|
*/
|
|
248
248
|
export async function file(_digest: Digest): Promise<string> {
|
|
249
|
-
await
|
|
249
|
+
await stepReport("Filing the digest.");
|
|
250
250
|
// A real desk would write the digest to its database here. The stub writes
|
|
251
251
|
// nothing, which is what the `_` says — and it is a stub because `ctx.db` is
|
|
252
252
|
// the half of a tool context a step still cannot reach.
|
|
@@ -282,8 +282,5 @@ export function extractTitle(html: string): string | undefined {
|
|
|
282
282
|
* structure are bytes the summary does not read.
|
|
283
283
|
*/
|
|
284
284
|
export function extractText(html: string): string {
|
|
285
|
-
return htmlToText(html, { maxChars: MAX_ARTICLE_CHARS })
|
|
286
|
-
.replace(/\s+/g, " ")
|
|
287
|
-
.trim()
|
|
288
|
-
.slice(0, MAX_ARTICLE_CHARS);
|
|
285
|
+
return htmlToText(html, { maxChars: MAX_ARTICLE_CHARS }).replace(/\s+/g, " ").trim();
|
|
289
286
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The def a DEPLOYED agent runs: authored, plus the `system-prompt.md` beside
|
|
3
|
+
* it.
|
|
4
|
+
*
|
|
5
|
+
* This template declares no `tools/` at all — every calculation is the
|
|
6
|
+
* `run_code` builtin's — so the prompt is the only thing discovery adds here,
|
|
7
|
+
* and it is what half the tests below are about. Importing `./agent.ts`
|
|
8
|
+
* directly would measure a tutor whose prompt is the framework default, i.e.
|
|
9
|
+
* an agent that was never told to compute in code.
|
|
10
|
+
*/
|
|
11
|
+
import agentDef from "virtual:aai/agent";
|
|
12
|
+
import { toAgentConfig } from "@alexkroman1/aai/manifest";
|
|
13
|
+
import { describe, expect, test } from "vitest";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a starter's spec may assert.
|
|
17
|
+
*
|
|
18
|
+
* `aai build` runs these tests before it bundles, so an assertion pinning this
|
|
19
|
+
* tutor's own identity — its literal name, the wording of its greeting, the
|
|
20
|
+
* model id it happens to run today — turns the first customization into a build
|
|
21
|
+
* failure in a file the author never wrote. Every test here therefore asserts a
|
|
22
|
+
* property that survives a rename, a voice, a reworded prompt and a model swap,
|
|
23
|
+
* on the RESOLVED config rather than on the def's empty fields.
|
|
24
|
+
*
|
|
25
|
+
* `run_code` is the one thing named literally, and deliberately: taking it away
|
|
26
|
+
* is not a customization of Math Buddy but a deletion of its subject — the
|
|
27
|
+
* prompt is nothing but recipes for it — and the tutor left behind does
|
|
28
|
+
* arithmetic from memory, which reads exactly like a correct answer until it is
|
|
29
|
+
* wrong.
|
|
30
|
+
*
|
|
31
|
+
* What is NOT here is anything about the code the tutor writes or the answer it
|
|
32
|
+
* comes back with: that needs a model and a sandbox, so it belongs to
|
|
33
|
+
* `agent.eval.test.ts`, which supplies both. This tier's question is the one
|
|
34
|
+
* that comes first — was the tutor handed anything to run at all.
|
|
35
|
+
*/
|
|
36
|
+
describe("math-buddy template", () => {
|
|
37
|
+
test("config passes manifest validation", () => {
|
|
38
|
+
// Same conversion `aai build`/`aai deploy` run — and the only thing that
|
|
39
|
+
// says this template's declared LLM descriptor is well formed before a
|
|
40
|
+
// live session tries to open one from it.
|
|
41
|
+
expect(() => toAgentConfig(agentDef)).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("exports an agent the platform can name", () => {
|
|
45
|
+
// Not the literal: what has to hold is that there IS a name and that the
|
|
46
|
+
// conversion carries it through — `AgentName` refuses a blank one, and the
|
|
47
|
+
// studio lists a deployed agent by exactly this string.
|
|
48
|
+
expect(agentDef.name).toBeTruthy();
|
|
49
|
+
expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("run_code is declared, and the prompt the deploy carries asks for it", () => {
|
|
53
|
+
const config = toAgentConfig(agentDef);
|
|
54
|
+
// Two halves, each of which fails silently and produces a plausible tutor.
|
|
55
|
+
// Without the declaration the model has nothing to run, so it computes in
|
|
56
|
+
// its head and says the answer with the same confidence either way. Without
|
|
57
|
+
// the prompt reaching the CONFIG — the "I edited system-prompt.md and
|
|
58
|
+
// nothing changed" failure `withSystemPrompt` exists to catch, since the
|
|
59
|
+
// file is discovered by the build rather than imported by `agent.ts` — the
|
|
60
|
+
// recipes are gone and what deploys is a general assistant that happens to
|
|
61
|
+
// have a sandbox attached. The framework default says nothing about
|
|
62
|
+
// `run_code`, which is what makes the second assertion a real check on
|
|
63
|
+
// discovery rather than a restatement of the first.
|
|
64
|
+
expect(config.builtinTools).toContain("run_code");
|
|
65
|
+
expect(config.systemPrompt).toContain("run_code");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("whichever model this tutor runs on, the conversion carries its tuning", () => {
|
|
69
|
+
const config = toAgentConfig(agentDef);
|
|
70
|
+
if (config.mode !== "pipeline") {
|
|
71
|
+
// Switched the def to `s2s`? Then one model listens and talks, and there
|
|
72
|
+
// is no separate LLM stage left for anything to be carried on.
|
|
73
|
+
expect(config.mode).toBe("s2s");
|
|
74
|
+
expect(config.llm).toBeUndefined();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (agentDef.llm === undefined) {
|
|
78
|
+
// Dropped the declaration to take the default cascade: it still resolves
|
|
79
|
+
// to a NAMED model, because the gateway refuses an unknown id with a 400
|
|
80
|
+
// at the first session — "no model" is not a state a deploy may reach.
|
|
81
|
+
expect(config.llm?.options.model).toBeTruthy();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// A model choice is the only reason this tutor declares a stage at all —
|
|
85
|
+
// a quick, cheap one, since `run_code` does the arithmetic and what is left
|
|
86
|
+
// is turn-taking speed. So the descriptor is checked whole rather than by
|
|
87
|
+
// `kind`: one that arrived with its options dropped would deploy the
|
|
88
|
+
// gateway's default model instead, quietly slower, with nothing on the line
|
|
89
|
+
// saying so. Read off the def rather than pinned, because swapping the id
|
|
90
|
+
// is the first tuning an author of this template tries.
|
|
91
|
+
expect(config.llm?.kind).toBe(agentDef.llm.kind);
|
|
92
|
+
expect(config.llm?.options).toEqual(agentDef.llm.options);
|
|
93
|
+
expect(config.llm?.options.model).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("every stage its mode needs is filled, declared or defaulted", () => {
|
|
97
|
+
// This template's other half: it declares the LLM and nothing else, so STT
|
|
98
|
+
// and TTS are injected at parse time (see `defaultProviders`) and the tutor
|
|
99
|
+
// can still hear and speak. Asserted per MODE so it survives a swap —
|
|
100
|
+
// declare `stt`/`tts` and the rest still default; declare `s2s` and there
|
|
101
|
+
// is no cascade to fill, which is the one thing that must never happen by
|
|
102
|
+
// fallthrough.
|
|
103
|
+
const config = toAgentConfig(agentDef);
|
|
104
|
+
if (config.mode === "s2s") {
|
|
105
|
+
expect(config.s2s?.kind).toBeTruthy();
|
|
106
|
+
expect(config.stt).toBeUndefined();
|
|
107
|
+
expect(config.tts).toBeUndefined();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
expect(config.mode).toBe("pipeline");
|
|
111
|
+
for (const stage of ["stt", "llm", "tts"] as const) {
|
|
112
|
+
expect(config[stage]?.kind, stage).toBe(agentDef[stage]?.kind ?? "assemblyai");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("the caller is told what to ask, and the greeting survives the conversion", () => {
|
|
117
|
+
// A voice agent has no buttons, so the opener is the only place a caller
|
|
118
|
+
// learns that this one wants arithmetic, conversions and dice rather than
|
|
119
|
+
// conversation. It rides to the browser in `/client-config` beside `name`,
|
|
120
|
+
// so what has to hold is that there is one and the conversion carries it:
|
|
121
|
+
// a greeting lost at that boundary is replaced by the framework's generic
|
|
122
|
+
// opener, which invites the caller to ask for anything at all.
|
|
123
|
+
expect(agentDef.greeting).toBeTruthy();
|
|
124
|
+
expect(toAgentConfig(agentDef).greeting).toBe(agentDef.greeting);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
/**
|
|
14
14
|
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Taken from `virtual:aai/agent` rather than a hand-written glob: the plugin
|
|
17
|
+
* expands it against THIS file's own directory, so the spec needs no glob and
|
|
18
|
+
* no shared helper — which matters because this file SHIPS, and a scaffolded
|
|
19
|
+
* project has no repo helper to import. Without
|
|
18
20
|
* it the eval would drive an agent with no `recommend` at all, which is the one
|
|
19
21
|
* failure a green eval must never be able to hide.
|
|
20
22
|
*
|
|
@@ -14,7 +14,13 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
14
14
|
* reconnect is nagging. `useToolCallStart` and `useEvent` are for exactly this —
|
|
15
15
|
* they fire once, carry no history, and drive throwaway `useState`.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Button,
|
|
19
|
+
mountClient,
|
|
20
|
+
useAgentState,
|
|
21
|
+
useEvent,
|
|
22
|
+
useToolCallStart,
|
|
23
|
+
} from "@alexkroman1/aai-ui";
|
|
18
24
|
import { useEffect, useState } from "react";
|
|
19
25
|
import { MOODS, nightProjection } from "./shared.ts";
|
|
20
26
|
|
|
@@ -111,7 +117,7 @@ function RecSidebar() {
|
|
|
111
117
|
);
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
mountClient({
|
|
115
121
|
name: "Night Owl",
|
|
116
122
|
icon: "\u{1F989}",
|
|
117
123
|
subtitle: "A cozy companion for the small hours",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** The def a DEPLOYED agent runs: authored, plus what `system-prompt.md` says. */
|
|
2
|
+
import agentDef from "virtual:aai/agent";
|
|
3
|
+
import { AgentConfigSchema, toAgentConfig } from "@alexkroman1/aai/manifest";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a starter's spec may assert.
|
|
8
|
+
*
|
|
9
|
+
* `aai init` scaffolds this template verbatim, and `aai build` runs these tests
|
|
10
|
+
* before it bundles — so an assertion pinning Penny's own identity (her literal
|
|
11
|
+
* name, her voice, her model, the wording of a house rule) turns a user's first
|
|
12
|
+
* customization into a build failure in a file they never wrote. Every test
|
|
13
|
+
* here therefore asserts a property that survives those edits, on the RESOLVED
|
|
14
|
+
* config rather than on the def's empty fields.
|
|
15
|
+
*
|
|
16
|
+
* What is deliberately NOT here: whether Penny actually reaches for `run_code`
|
|
17
|
+
* instead of dividing in her head, whether she looks a rate up rather than
|
|
18
|
+
* quoting a remembered one, and whether she keeps the not-financial-advice
|
|
19
|
+
* caveat. Those are claims about a live model and belong to
|
|
20
|
+
* `agent.eval.test.ts`, which drives them against one. This tier asserts the
|
|
21
|
+
* WIRING those runs depend on — a template whose builtins never reached the
|
|
22
|
+
* config fails an eval as a behaviour problem, in a report nobody reads as
|
|
23
|
+
* "the tool was not there".
|
|
24
|
+
*/
|
|
25
|
+
describe("personal-finance template", () => {
|
|
26
|
+
test("config passes manifest validation", () => {
|
|
27
|
+
// Same conversion `aai build`/`aai deploy` run. It is also what checks the
|
|
28
|
+
// two builtin NAMES against the SDK's own enum, so a typo in
|
|
29
|
+
// `builtinTools` fails here rather than shipping an agent whose prompt
|
|
30
|
+
// commands a tool the platform never resolved.
|
|
31
|
+
expect(() => toAgentConfig(agentDef)).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("exports an agent the platform can name", () => {
|
|
35
|
+
// Not the literal: what has to hold is that there IS a name and the
|
|
36
|
+
// conversion carries it through — the studio lists a deployed agent by
|
|
37
|
+
// exactly this string, and renaming her is the first edit this template
|
|
38
|
+
// invites.
|
|
39
|
+
expect(agentDef.name).toBeTruthy();
|
|
40
|
+
expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("every stage its mode needs is filled, declared or defaulted", () => {
|
|
44
|
+
// This template declares no provider at all — it is a prompt and two
|
|
45
|
+
// builtins — so the default all-AssemblyAI cascade is what makes it run
|
|
46
|
+
// the moment it is deployed. Asserted per MODE so it stays true after a
|
|
47
|
+
// swap: declare `stt`/`llm`/`tts` and the rest still default; declare
|
|
48
|
+
// `s2s` and there is no cascade to fill, which is the one thing that must
|
|
49
|
+
// never happen by fallthrough.
|
|
50
|
+
const config = toAgentConfig(agentDef);
|
|
51
|
+
if (config.mode === "s2s") {
|
|
52
|
+
expect(config.s2s?.kind).toBeTruthy();
|
|
53
|
+
expect(config.stt).toBeUndefined();
|
|
54
|
+
expect(config.tts).toBeUndefined();
|
|
55
|
+
} else if (config.mode === "text") {
|
|
56
|
+
expect(config.llm?.kind).toBeTruthy();
|
|
57
|
+
} else {
|
|
58
|
+
expect(config.mode).toBe("pipeline");
|
|
59
|
+
expect(config.stt?.kind).toBeTruthy();
|
|
60
|
+
expect(config.llm?.kind).toBeTruthy();
|
|
61
|
+
expect(config.tts?.kind).toBeTruthy();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("both builtins survive into the config a deploy carries", () => {
|
|
66
|
+
const builtins = toAgentConfig(agentDef).builtinTools ?? [];
|
|
67
|
+
|
|
68
|
+
// `run_code` is the arithmetic rule's only mechanism. The prompt forbids
|
|
69
|
+
// Penny working ANY figure out in her head — a tip, a split, a payment, a
|
|
70
|
+
// projection — so without the builtin the rule has nothing to point at and
|
|
71
|
+
// degrades into a model inventing numbers the caller then spends money on.
|
|
72
|
+
expect(builtins).toContain("run_code");
|
|
73
|
+
|
|
74
|
+
// `fetch_json` is the only route to a number that MOVES. A rate or a coin
|
|
75
|
+
// price the model remembers is months stale and carries no source, and it
|
|
76
|
+
// arrives in exactly the confident tone a fetched one would — which is why
|
|
77
|
+
// a finance starter that cannot make a request is worse than one that
|
|
78
|
+
// declines to answer.
|
|
79
|
+
expect(builtins).toContain("fetch_json");
|
|
80
|
+
|
|
81
|
+
// Asserted on the CONFIG rather than the def because that is what a deploy
|
|
82
|
+
// ships, and because `DEFAULT_BUILTIN_TOOLS` is empty: a builtin is
|
|
83
|
+
// something an agent asks for, never something it has to notice and switch
|
|
84
|
+
// off. So a dropped entry is not a quieter Penny, it is the same Penny with
|
|
85
|
+
// no way to be right. Adding builtins beside these two is an ordinary edit;
|
|
86
|
+
// losing one is the regression.
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("every builtin the prompt tells Penny to use is one she declares", () => {
|
|
90
|
+
// The pairing this template is made of: the prose holds the endpoints and
|
|
91
|
+
// the formulas, each list headed by the tool that consumes it, and
|
|
92
|
+
// `agent.ts` holds the array that makes those tools exist. The failure is
|
|
93
|
+
// silent in both directions — a prompt commanding `fetch_json` at an agent
|
|
94
|
+
// that never declared it produces a model apologizing for a tool it cannot
|
|
95
|
+
// see, and a builtin dropped from `agent.ts` alone leaves the endpoint list
|
|
96
|
+
// addressed to nothing — and neither shows up in a diff of either file.
|
|
97
|
+
const config = toAgentConfig(agentDef);
|
|
98
|
+
const declared = config.builtinTools ?? [];
|
|
99
|
+
|
|
100
|
+
// Which snake_case tokens in the prose are tool NAMES is a question for the
|
|
101
|
+
// SDK's own schema rather than a catalog restated here: this prompt also
|
|
102
|
+
// names `vs_currencies`, `include_24hr_change`, `per_person` and
|
|
103
|
+
// `annual_rate`, so matching every underscored word would redden on a
|
|
104
|
+
// formula, and a copied list of builtins goes stale the first time the SDK
|
|
105
|
+
// adds one.
|
|
106
|
+
const isBuiltin = (name: string) =>
|
|
107
|
+
AgentConfigSchema.safeParse({ ...config, builtinTools: [name] }).success;
|
|
108
|
+
const commanded = [
|
|
109
|
+
...new Set(config.systemPrompt.match(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g) ?? []),
|
|
110
|
+
].filter(isBuiltin);
|
|
111
|
+
|
|
112
|
+
// Non-vacuity, and it earns its keep twice: a prompt naming no builtin at
|
|
113
|
+
// all would leave the loop below asserting nothing, and it is also the state
|
|
114
|
+
// this template lands in when `system-prompt.md` is not applied — the
|
|
115
|
+
// framework default names no builtin, so "I edited the prompt and nothing
|
|
116
|
+
// changed" fails here instead of passing quietly with Penny's rules nowhere
|
|
117
|
+
// in her context.
|
|
118
|
+
expect(commanded.length).toBeGreaterThan(0);
|
|
119
|
+
for (const name of commanded) {
|
|
120
|
+
expect(declared, `the prompt tells Penny to use ${name}`).toContain(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The converse is deliberately NOT asserted: declaring a builtin the prompt
|
|
124
|
+
// never mentions is an ordinary edit, and the model learns about it from its
|
|
125
|
+
// own tool schema rather than from the prose.
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
2
|
import { toAgentConfig } from "@alexkroman1/aai/manifest";
|
|
3
|
-
import { assemblyAIS2s,
|
|
3
|
+
import { assemblyAIS2s, openAIS2s } from "@alexkroman1/aai/s2s";
|
|
4
4
|
import { assemblyAIStt, deepgramStt, elevenLabsStt, sonioxStt } from "@alexkroman1/aai/stt";
|
|
5
5
|
import { ASSEMBLYAI_TTS_VOICES, assemblyAITts, cartesiaTts, rimeTts } from "@alexkroman1/aai/tts";
|
|
6
6
|
import { describe, expect, test } from "vitest";
|
|
@@ -99,7 +99,7 @@ describe("swapping any other stage", () => {
|
|
|
99
99
|
test("S2S: an explicit opt-in, and it REPLACES the pipeline rather than joining it", () => {
|
|
100
100
|
for (const [s2s, kind] of [
|
|
101
101
|
[assemblyAIS2s(), "assemblyai"],
|
|
102
|
-
[
|
|
102
|
+
[openAIS2s({ voice: "alloy" }), "openai-realtime"],
|
|
103
103
|
] as const) {
|
|
104
104
|
const config = toAgentConfig(agent({ name: "Line", s2s }));
|
|
105
105
|
expect(config.s2s?.kind).toBe(kind);
|
|
@@ -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 }
|