@alexkroman1/aai-cli 13.3.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
- package/dist/build.d.ts +5 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +117 -135
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +4 -4
- package/dist/scaffold/server.mjs +129 -25
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +27 -17
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +4 -4
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import type { AgentState
|
|
2
|
+
import type { AgentState } from "@alexkroman1/aai-ui";
|
|
3
3
|
import {
|
|
4
4
|
AutoScroll,
|
|
5
|
-
|
|
5
|
+
mountClient,
|
|
6
6
|
useConversation,
|
|
7
|
-
|
|
7
|
+
useSessionActions,
|
|
8
|
+
useSessionError,
|
|
8
9
|
useSessionSelector,
|
|
9
10
|
useTheme,
|
|
10
11
|
} from "@alexkroman1/aai-ui";
|
|
@@ -98,7 +99,7 @@ function Transcript() {
|
|
|
98
99
|
// The one theme read left in this file, and the case `useTheme()` is still
|
|
99
100
|
// for: `scrollbar-color` takes TWO values and has no utility class, so it has
|
|
100
101
|
// to be a style — and reading it off the theme beats re-pinning the two hex
|
|
101
|
-
// codes the `
|
|
102
|
+
// codes the `mountClient({ theme })` block below already declares.
|
|
102
103
|
const theme = useTheme();
|
|
103
104
|
|
|
104
105
|
return (
|
|
@@ -146,8 +147,16 @@ function Transcript() {
|
|
|
146
147
|
{transcript.text}
|
|
147
148
|
</div>
|
|
148
149
|
)}
|
|
150
|
+
{/* Same contract as the shipped `MessageList`'s indicator: the blinking
|
|
151
|
+
block is the only sign the parser is working, and to a screen reader it
|
|
152
|
+
is one unpronounceable glyph. */}
|
|
149
153
|
{thinking && (
|
|
150
|
-
<div
|
|
154
|
+
<div
|
|
155
|
+
role="status"
|
|
156
|
+
aria-label="The parser is thinking"
|
|
157
|
+
className="animate-pulse"
|
|
158
|
+
style={{ color: GREEN_DIM }}
|
|
159
|
+
>
|
|
151
160
|
▌
|
|
152
161
|
</div>
|
|
153
162
|
)}
|
|
@@ -155,12 +164,18 @@ function Transcript() {
|
|
|
155
164
|
);
|
|
156
165
|
}
|
|
157
166
|
|
|
167
|
+
/** Module scope, so the selector has a STABLE identity: an inline arrow makes
|
|
168
|
+
* `useSyncExternalStoreWithSelector` rebuild its selection memo every render,
|
|
169
|
+
* and this runs on every snapshot push — each STT partial and streaming
|
|
170
|
+
* delta. Counting with `reduce` rather than `filter().length` for the same
|
|
171
|
+
* reason: the array copy is thrown away, and it grows for the whole adventure. */
|
|
172
|
+
const userTurns = (snapshot: { messages: { role: string }[] }): number =>
|
|
173
|
+
snapshot.messages.reduce((n, message) => (message.role === "user" ? n + 1 : n), 0);
|
|
174
|
+
|
|
158
175
|
/** The turn counter — a NUMBER out of the selector, so a new message array with
|
|
159
176
|
* the same user-message count re-renders nothing. */
|
|
160
177
|
function TurnCount() {
|
|
161
|
-
const turns = useSessionSelector(
|
|
162
|
-
(snapshot) => snapshot.messages.filter((message) => message.role === "user").length,
|
|
163
|
-
);
|
|
178
|
+
const turns = useSessionSelector(userTurns);
|
|
164
179
|
return <span>Turns: {turns}</span>;
|
|
165
180
|
}
|
|
166
181
|
|
|
@@ -207,7 +222,7 @@ function StatusDot() {
|
|
|
207
222
|
* never told an unannounced one appeared.
|
|
208
223
|
*/
|
|
209
224
|
function ErrorBanner() {
|
|
210
|
-
const error =
|
|
225
|
+
const error = useSessionError();
|
|
211
226
|
if (!error) return null;
|
|
212
227
|
return (
|
|
213
228
|
<div
|
|
@@ -215,33 +230,22 @@ function ErrorBanner() {
|
|
|
215
230
|
className="px-5 py-2 text-xs"
|
|
216
231
|
style={{ background: "#3a0000", color: "#ff4141" }}
|
|
217
232
|
>
|
|
218
|
-
ERROR: {error.message}
|
|
233
|
+
ERROR: {error.message} ({error.code})
|
|
219
234
|
</div>
|
|
220
235
|
);
|
|
221
236
|
}
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
*
|
|
238
|
+
/*
|
|
239
|
+
* Pause/resume, new game and hang-up.
|
|
225
240
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* resume identity, so the redial is a brand-new session (fresh world, opening
|
|
231
|
-
* scene included), and `start()` puts the player straight into it rather than
|
|
232
|
-
* back at the title screen.
|
|
241
|
+
* `useSessionActions()` is the narrow way `<Controls>` reaches the methods, and
|
|
242
|
+
* it is published now — so this row takes the three it presses and one
|
|
243
|
+
* selector for the flag it reads, instead of a whole-snapshot `useSession()`
|
|
244
|
+
* that re-rendered the footer on every partial transcript.
|
|
233
245
|
*/
|
|
234
|
-
function newConversation(session: Session): void {
|
|
235
|
-
session.end();
|
|
236
|
-
session.start();
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Pause/resume, new game and hang-up. The only place a whole-session
|
|
240
|
-
* subscription is still needed: the ACTIONS live on `useSession()`, and
|
|
241
|
-
* `useSessionCore` — the narrow way `<Controls>` reaches them — is not on the
|
|
242
|
-
* public surface. */
|
|
243
246
|
function Footer() {
|
|
244
|
-
const
|
|
247
|
+
const { toggle, restart, end } = useSessionActions();
|
|
248
|
+
const running = useSessionSelector((snapshot) => snapshot.running);
|
|
245
249
|
return (
|
|
246
250
|
<div
|
|
247
251
|
className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
|
|
@@ -253,9 +257,9 @@ function Footer() {
|
|
|
253
257
|
type="button"
|
|
254
258
|
className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
|
|
255
259
|
style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
|
|
256
|
-
onClick={
|
|
260
|
+
onClick={toggle}
|
|
257
261
|
>
|
|
258
|
-
{
|
|
262
|
+
{running ? "[P]ause" : "[R]esume"}
|
|
259
263
|
</button>
|
|
260
264
|
{/* The one-click new conversation the default shell's `<Controls>`
|
|
261
265
|
gives every other template — a custom `component:` renders no
|
|
@@ -266,7 +270,7 @@ function Footer() {
|
|
|
266
270
|
type="button"
|
|
267
271
|
className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
|
|
268
272
|
style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
|
|
269
|
-
onClick={
|
|
273
|
+
onClick={restart}
|
|
270
274
|
>
|
|
271
275
|
[N]ew Game
|
|
272
276
|
</button>
|
|
@@ -276,7 +280,7 @@ function Footer() {
|
|
|
276
280
|
type="button"
|
|
277
281
|
className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
|
|
278
282
|
style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
|
|
279
|
-
onClick={() =>
|
|
283
|
+
onClick={() => end()}
|
|
280
284
|
>
|
|
281
285
|
[Q]uit
|
|
282
286
|
</button>
|
|
@@ -315,7 +319,7 @@ function Crt({ children }: { children: ReactNode }) {
|
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
function TitleScreen() {
|
|
318
|
-
const
|
|
322
|
+
const { start } = useSessionActions();
|
|
319
323
|
return (
|
|
320
324
|
<Crt>
|
|
321
325
|
<div
|
|
@@ -345,7 +349,7 @@ function TitleScreen() {
|
|
|
345
349
|
border: `1px solid ${GREEN}`,
|
|
346
350
|
animation: "ic-pulse 2s ease-in-out infinite",
|
|
347
351
|
}}
|
|
348
|
-
onClick={
|
|
352
|
+
onClick={start}
|
|
349
353
|
>
|
|
350
354
|
Begin Adventure
|
|
351
355
|
</button>
|
|
@@ -381,7 +385,7 @@ function InfocomAdventure() {
|
|
|
381
385
|
);
|
|
382
386
|
}
|
|
383
387
|
|
|
384
|
-
|
|
388
|
+
mountClient({
|
|
385
389
|
component: InfocomAdventure,
|
|
386
390
|
theme: {
|
|
387
391
|
bg: CRT_BG,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
createWorkflowContext,
|
|
30
30
|
schemaInputIssues,
|
|
31
31
|
stubGatewayRoute,
|
|
32
32
|
stubStepInfo,
|
|
@@ -170,7 +170,7 @@ describe("fetchArticle", () => {
|
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
test("fails FATALLY on a 404 and plainly on a 503", async () => {
|
|
173
|
-
// `
|
|
173
|
+
// `stepFetchOrFail` builds the message, so it is the SDK's one spelling for a
|
|
174
174
|
// bad response — the REQUEST, the status, and a preview of any body.
|
|
175
175
|
stubPage("", 404);
|
|
176
176
|
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(
|
|
@@ -264,7 +264,7 @@ describe("summarize", () => {
|
|
|
264
264
|
// which is the only place the policy is observable at all. `runSteps: false`
|
|
265
265
|
// because the subject is the declared policy rather than the work: the steps
|
|
266
266
|
// would otherwise need a page and a model.
|
|
267
|
-
const ctx =
|
|
267
|
+
const ctx = createWorkflowContext({ runSteps: false });
|
|
268
268
|
await digestFlow({ url: "https://example.com/a" }, ctx);
|
|
269
269
|
|
|
270
270
|
const summarizeStep = ctx.steps.find((step) => step.name === "summarize");
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* the entire product, and the declaration says so:
|
|
14
14
|
*
|
|
15
15
|
* - `workflowApp()` is `agent({ …, page: "static" })` with the discriminant
|
|
16
|
-
* already set. The declaration is not decoration: `
|
|
17
|
-
* `/websocket` with a reason (so a page mounted with `
|
|
16
|
+
* already set. The declaration is not decoration: `createRuntimeServer` declines
|
|
17
|
+
* `/websocket` with a reason (so a page mounted with `mountClient()` by mistake
|
|
18
18
|
* fails the same way in `aai dev` and in production rather than only after a
|
|
19
19
|
* deploy) and telephony defaults off.
|
|
20
20
|
* - There is no `stt`/`llm`/`tts`, no `tools` and no `systemPrompt` — and they
|
|
@@ -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.
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* where a fan-out makes the history worth seeing.
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
|
-
import {
|
|
67
|
+
import { BulletList, mountPage, useWorkflowSubmit, WorkflowProgress } from "@alexkroman1/aai-ui";
|
|
68
68
|
import "@alexkroman1/aai-ui/styles.css";
|
|
69
69
|
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
70
70
|
// nothing — and it is what stops this file restating a shape `workflows/
|
|
@@ -92,10 +92,6 @@ function pendingNote(startedHere: boolean, found: boolean): string {
|
|
|
92
92
|
|
|
93
93
|
export function App() {
|
|
94
94
|
const [url, setUrl] = useState("");
|
|
95
|
-
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
96
|
-
// the page can tell "working on what you just submitted" from "picking up
|
|
97
|
-
// where you left off" — the hook reports the run, not who asked for it.
|
|
98
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
99
95
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
100
96
|
// `run.output` instead of `unknown`. `error` is the agent's own sentence for a
|
|
101
97
|
// rejected input, which is better copy than anything this page could write, and
|
|
@@ -104,14 +100,14 @@ export function App() {
|
|
|
104
100
|
// No `key` and no `recover`: this tab's handle on its own runs is minted and
|
|
105
101
|
// remembered by the hook, and read back as it mounts. See the module doc for
|
|
106
102
|
// what a page says when it wants a different one.
|
|
107
|
-
const { submit, run, pending, error, wake } =
|
|
103
|
+
const { submit, run, pending, error, wake, startedHere } =
|
|
104
|
+
useWorkflowSubmit<typeof digest>("digest");
|
|
108
105
|
|
|
109
106
|
// `submit()` resolves as soon as the run exists — deliberately not when it
|
|
110
107
|
// finishes. That is the whole mechanism: the digest sleeps for a while, and
|
|
111
108
|
// this page is free to be closed in the meantime.
|
|
112
109
|
const onSubmit = (event: React.FormEvent) => {
|
|
113
110
|
event.preventDefault();
|
|
114
|
-
setStartedHere(true);
|
|
115
111
|
void submit({ url });
|
|
116
112
|
};
|
|
117
113
|
|
|
@@ -125,6 +121,11 @@ export function App() {
|
|
|
125
121
|
required
|
|
126
122
|
value={url}
|
|
127
123
|
onChange={(e) => setUrl(e.target.value)}
|
|
124
|
+
// A placeholder is not a name: it disappears the moment anything is
|
|
125
|
+
// typed, and a screen reader reaches an unlabelled box. The declared
|
|
126
|
+
// fields in `@alexkroman1/aai-ui` say the same thing — a `label`, or
|
|
127
|
+
// an `aria-label` where the row has no room for one.
|
|
128
|
+
aria-label="Article URL"
|
|
128
129
|
// A REAL article, because a placeholder is a suggestion and this one
|
|
129
130
|
// gets typed. `https://example.com/article` 404s, and the bare
|
|
130
131
|
// `example.com` a reader trims it to has no prose on it — so the first
|
|
@@ -139,7 +140,13 @@ export function App() {
|
|
|
139
140
|
</button>
|
|
140
141
|
</form>
|
|
141
142
|
|
|
142
|
-
{
|
|
143
|
+
{/* `<Form>` carries `role="alert"` for the templates that declare their
|
|
144
|
+
fields; a hand-written form has to say it itself. */}
|
|
145
|
+
{error !== undefined && (
|
|
146
|
+
<p role="alert" className="text-red-600">
|
|
147
|
+
{error}
|
|
148
|
+
</p>
|
|
149
|
+
)}
|
|
143
150
|
|
|
144
151
|
{/* A run that has not settled says so. `pending` is not derivable from the
|
|
145
152
|
snapshot alone — an id the agent never knew leaves `run` undefined,
|
|
@@ -168,16 +175,19 @@ export function App() {
|
|
|
168
175
|
</button>
|
|
169
176
|
)}
|
|
170
177
|
|
|
171
|
-
{
|
|
178
|
+
{/* `role="alert"`, like the submit error above: this is the outcome the
|
|
179
|
+
reader has been waiting for, and it can arrive long after they looked
|
|
180
|
+
away. */}
|
|
181
|
+
{run?.status === "failed" && (
|
|
182
|
+
<p role="alert" className="text-red-600">
|
|
183
|
+
That one failed: {run.error}
|
|
184
|
+
</p>
|
|
185
|
+
)}
|
|
172
186
|
|
|
173
187
|
{run?.status === "completed" && (
|
|
174
188
|
<article className="flex flex-col gap-3">
|
|
175
189
|
<h2 className="text-xl">{run.output.headline}</h2>
|
|
176
|
-
<
|
|
177
|
-
{run.output.points.map((point) => (
|
|
178
|
-
<li key={point}>{point}</li>
|
|
179
|
-
))}
|
|
180
|
-
</ul>
|
|
190
|
+
<BulletList items={run.output.points} />
|
|
181
191
|
<p className="text-sm opacity-70">Filed {run.output.filedAt}</p>
|
|
182
192
|
</article>
|
|
183
193
|
)}
|
|
@@ -185,4 +195,4 @@ export function App() {
|
|
|
185
195
|
);
|
|
186
196
|
}
|
|
187
197
|
|
|
188
|
-
|
|
198
|
+
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
|
}
|
|
@@ -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",
|
|
@@ -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 }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { CRUSTS, calculateTotal,
|
|
3
|
+
import { CRUSTS, calculateTotal, orderSlot, type Pizza, SIZES } from "../shared.ts";
|
|
3
4
|
|
|
4
5
|
export default orderSlot.updateTool({
|
|
5
6
|
description: "Add a pizza to the order. Use when the customer has decided on a pizza.",
|
|
@@ -24,7 +25,7 @@ export default orderSlot.updateTool({
|
|
|
24
25
|
|
|
25
26
|
return {
|
|
26
27
|
added: pizza,
|
|
27
|
-
orderTotal:
|
|
28
|
+
orderTotal: formatMoney(calculateTotal(order.pizzas)),
|
|
28
29
|
itemCount: order.pizzas.length,
|
|
29
30
|
};
|
|
30
31
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
3
|
+
import { calculateTotal, orderSlot, resetOrder } from "../shared.ts";
|
|
3
4
|
|
|
4
5
|
export default orderSlot.updateTool({
|
|
5
6
|
description:
|
|
@@ -9,7 +10,7 @@ export default orderSlot.updateTool({
|
|
|
9
10
|
if (pizzas.length === 0) return toolFailure("Cannot place an empty order.");
|
|
10
11
|
|
|
11
12
|
const customerName = order.customerName ?? "Guest";
|
|
12
|
-
const total =
|
|
13
|
+
const total = formatMoney(calculateTotal(pizzas));
|
|
13
14
|
const orderNumber = Math.floor(1000 + Math.random() * 9000);
|
|
14
15
|
|
|
15
16
|
const estimatedMinutes = 15 + pizzas.length * 5;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { calculateTotal,
|
|
4
|
+
import { calculateTotal, orderSlot } from "../shared.ts";
|
|
4
5
|
|
|
5
6
|
export default orderSlot.updateTool({
|
|
6
7
|
description: "Remove a pizza from the order by its ID.",
|
|
@@ -15,7 +16,7 @@ export default orderSlot.updateTool({
|
|
|
15
16
|
|
|
16
17
|
return {
|
|
17
18
|
removed,
|
|
18
|
-
orderTotal:
|
|
19
|
+
orderTotal: formatMoney(calculateTotal(order.pizzas)),
|
|
19
20
|
itemCount: order.pizzas.length,
|
|
20
21
|
};
|
|
21
22
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toolFailure } from "@alexkroman1/aai";
|
|
2
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { CRUSTS, calculateTotal,
|
|
4
|
+
import { CRUSTS, calculateTotal, orderSlot, type Pizza, SIZES } from "../shared.ts";
|
|
4
5
|
|
|
5
6
|
export default orderSlot.updateTool({
|
|
6
7
|
description: "Update an existing pizza in the order. Only provided fields are changed.",
|
|
@@ -23,6 +24,6 @@ export default orderSlot.updateTool({
|
|
|
23
24
|
|
|
24
25
|
order.pizzas[idx] = pizza;
|
|
25
26
|
|
|
26
|
-
return { updated: pizza, orderTotal:
|
|
27
|
+
return { updated: pizza, orderTotal: formatMoney(calculateTotal(order.pizzas)) };
|
|
27
28
|
},
|
|
28
29
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
2
|
+
import { calculateTotal, orderSlot } from "../shared.ts";
|
|
2
3
|
|
|
3
4
|
export default orderSlot.tool({
|
|
4
5
|
description: "View the current order summary with all pizzas and total price.",
|
|
@@ -15,7 +16,7 @@ export default orderSlot.tool({
|
|
|
15
16
|
toppings: p.toppings,
|
|
16
17
|
quantity: p.quantity,
|
|
17
18
|
})),
|
|
18
|
-
orderTotal:
|
|
19
|
+
orderTotal: formatMoney(calculateTotal(pizzas)),
|
|
19
20
|
};
|
|
20
21
|
},
|
|
21
22
|
});
|