@alexkroman1/aai-cli 13.2.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.mjs +1 -1
- package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
- package/dist/_config.d.ts +25 -1
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +15 -7
- package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
- package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
- package/dist/_output.d.ts +24 -7
- package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
- package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
- package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
- package/dist/_utils.d.ts +35 -0
- package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
- package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-Mxk8gWvX.mjs +108 -0
- package/dist/build.d.ts +5 -0
- package/dist/cli.mjs +172 -116
- package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
- package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
- package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
- package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
- package/dist/eval.d.ts +2 -0
- package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
- package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
- package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/.env.example +17 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +6 -5
- package/dist/scaffold/server.mjs +129 -25
- package/dist/secret-Cjp912yF.mjs +162 -0
- package/dist/secret.d.ts +48 -13
- package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
- package/dist/templates/briefing-desk/agent.test.ts +7 -3
- package/dist/templates/call-audit/agent.test.ts +25 -9
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +40 -58
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +10 -4
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +42 -36
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/agent.test.ts +6 -1
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +33 -16
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +33 -14
- package/dist/templates/podcast-digest/client.tsx +27 -36
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +49 -32
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +8 -2
- package/dist/templates/redline/client.tsx +47 -48
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +17 -8
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/registry.test.ts +99 -58
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/simple/agent.test.ts +39 -6
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +21 -12
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +36 -47
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +36 -20
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +34 -40
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/test-D032V2cA.mjs +242 -0
- package/dist/test.d.ts +97 -31
- package/dist/typecheck.mjs +2 -2
- package/dist/worker-bundler.mjs +3 -3
- package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
- package/package.json +10 -10
- package/dist/bin.d.mts +0 -2
- package/dist/build-BLlLOrng.mjs +0 -66
- package/dist/secret-B-3T5tvr.mjs +0 -61
- package/dist/test-CiLab-AA.mjs +0 -182
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
/**
|
|
23
23
|
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Taken from `virtual:aai/agent` rather than a hand-written glob: the plugin
|
|
26
|
+
* expands it against THIS file's own directory, so the spec needs no glob and
|
|
27
|
+
* no shared helper — which matters because this file SHIPS, and a scaffolded
|
|
28
|
+
* project has no repo helper to import. Without
|
|
27
29
|
* it the desk would have no tools and would answer every one of these from
|
|
28
30
|
* memory, which is the failure these cases exist to catch.
|
|
29
31
|
*
|
|
@@ -57,10 +57,14 @@ describe("health-assistant template", () => {
|
|
|
57
57
|
test("both tools are discovered from tools/", () => {
|
|
58
58
|
// `agent()` takes no `tools` field: a file in `tools/` IS the tool. A
|
|
59
59
|
// template whose tools are never resolved ships a model with none.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// `arrayContaining` rather than an exact list: a tool you add is the edit
|
|
61
|
+
// this template invites, and it must not redden a test you did not write.
|
|
62
|
+
// Losing one of these two still fails, which is the regression worth
|
|
63
|
+
// catching — discovery silently finding nothing looks exactly like a
|
|
64
|
+
// template with no tools.
|
|
65
|
+
expect(Object.keys(agentDef.tools ?? {})).toEqual(
|
|
66
|
+
expect.arrayContaining(["check_drug_interaction", "medication_lookup"]),
|
|
67
|
+
);
|
|
64
68
|
});
|
|
65
69
|
});
|
|
66
70
|
|
|
@@ -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,
|
|
@@ -51,7 +51,13 @@ describe("the agent declares itself a workflow app", () => {
|
|
|
51
51
|
test("under the name the page starts a run by", () => {
|
|
52
52
|
// `api.start("digest", …)` in client.tsx names this key. Nothing else
|
|
53
53
|
// records it, so a rename here is a 400 there rather than a compile error.
|
|
54
|
-
|
|
54
|
+
// `toContain` rather than an exact key list: adding a second workflow is an
|
|
55
|
+
// invited edit and must not redden a test the author did not write. The
|
|
56
|
+
// NAME is still pinned, deliberately — the page starts a run by this
|
|
57
|
+
// string, so renaming the key is a runtime 400 rather than a compile
|
|
58
|
+
// error, and this pin is the only thing that says so. Rename it here and
|
|
59
|
+
// in `client.tsx` together.
|
|
60
|
+
expect(Object.keys(agentDef.workflows ?? {})).toContain("digest");
|
|
55
61
|
expect(agentDef.workflows?.digest).toBe(digest);
|
|
56
62
|
});
|
|
57
63
|
});
|
|
@@ -164,7 +170,7 @@ describe("fetchArticle", () => {
|
|
|
164
170
|
});
|
|
165
171
|
|
|
166
172
|
test("fails FATALLY on a 404 and plainly on a 503", async () => {
|
|
167
|
-
// `
|
|
173
|
+
// `stepFetchOrFail` builds the message, so it is the SDK's one spelling for a
|
|
168
174
|
// bad response — the REQUEST, the status, and a preview of any body.
|
|
169
175
|
stubPage("", 404);
|
|
170
176
|
await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(
|
|
@@ -258,7 +264,7 @@ describe("summarize", () => {
|
|
|
258
264
|
// which is the only place the policy is observable at all. `runSteps: false`
|
|
259
265
|
// because the subject is the declared policy rather than the work: the steps
|
|
260
266
|
// would otherwise need a page and a model.
|
|
261
|
-
const ctx =
|
|
267
|
+
const ctx = createWorkflowContext({ runSteps: false });
|
|
262
268
|
await digestFlow({ url: "https://example.com/a" }, ctx);
|
|
263
269
|
|
|
264
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.
|
|
@@ -19,16 +19,18 @@
|
|
|
19
19
|
* page used to promise "the run continues without it" and then had no way back
|
|
20
20
|
* to the run it was promising about.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* A correlation KEY is the handle that survives, and this page writes none of
|
|
23
|
+
* it: `useWorkflowSubmit` mints an opaque per-page key into `sessionStorage`,
|
|
24
|
+
* records every run under it, and asks `find("digest", key)` as it mounts —
|
|
25
|
+
* so a reload lands back on the same headline, the same progress log and the
|
|
26
|
+
* same buttons. Six templates used to write those two options each, which is
|
|
27
|
+
* what made it the default.
|
|
24
28
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* and follow whatever run comes back, so a reload lands back on the same
|
|
31
|
-
* headline, the same progress log and the same buttons.
|
|
29
|
+
* What the page can still say is which key: `useRunKey({ storage: "local" })`
|
|
30
|
+
* for a run meant to outlive the tab (`podcast-digest`), an ACCOUNT's own id
|
|
31
|
+
* for an app with logins, `recover: false` for a form that must always open
|
|
32
|
+
* empty. `use-run-key.ts` argues what a key may not be — derived from the URL
|
|
33
|
+
* being digested, or carried in a `?key=` parameter.
|
|
32
34
|
*
|
|
33
35
|
* Deployed, this needs the correlation-key index, which is a `DATABASE_URL`
|
|
34
36
|
* away — `agent.ts` says what happens without one (the runs are still durable;
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
*
|
|
48
50
|
* `useWorkflowSubmit` answers "where has this got to" from the world's own
|
|
49
51
|
* record — pending, running, completed. `<WorkflowProgress>` answers "what is it
|
|
50
|
-
* doing" from what the run wrote itself (`
|
|
52
|
+
* doing" from what the run wrote itself (`stepReport()` in `workflows/digest.ts`). A
|
|
51
53
|
* page with only the first shows "Working…" for the length of the run; a page
|
|
52
54
|
* with only the second cannot tell a finished run from a quiet one. Both are
|
|
53
55
|
* cheap: one stream each, ended by the agent when there is nothing left to say.
|
|
@@ -55,14 +57,14 @@
|
|
|
55
57
|
* Progress also REPLAYS — chunks are retained with the run — so a reload mid-run
|
|
56
58
|
* catches up rather than starting from whatever arrives next. That only pays off
|
|
57
59
|
* because the reload can name its run again: `<WorkflowProgress runId>` is handed
|
|
58
|
-
* `run?.runId`, so before
|
|
60
|
+
* `run?.runId`, so before the recovery a refresh replayed a log for nobody.
|
|
59
61
|
* `lines={1}` is
|
|
60
62
|
* what narrows it to the newest line, because on a page this small that is the
|
|
61
63
|
* whole of what a status wants; `transcription-workflow` renders the full log,
|
|
62
64
|
* where a fan-out makes the history worth seeing.
|
|
63
65
|
*/
|
|
64
66
|
|
|
65
|
-
import {
|
|
67
|
+
import { BulletList, mountPage, useWorkflowSubmit, WorkflowProgress } from "@alexkroman1/aai-ui";
|
|
66
68
|
import "@alexkroman1/aai-ui/styles.css";
|
|
67
69
|
// ERASED at build time, so naming the agent's own type costs the browser bundle
|
|
68
70
|
// nothing — and it is what stops this file restating a shape `workflows/
|
|
@@ -90,32 +92,22 @@ function pendingNote(startedHere: boolean, found: boolean): string {
|
|
|
90
92
|
|
|
91
93
|
export function App() {
|
|
92
94
|
const [url, setUrl] = useState("");
|
|
93
|
-
// This tab's handle on its own runs — minted once and remembered, which is
|
|
94
|
-
// what a later load produces to find the run again.
|
|
95
|
-
const key = useRunKey();
|
|
96
|
-
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
97
|
-
// the page can tell "working on what you just submitted" from "picking up
|
|
98
|
-
// where you left off" — the hook reports the run, not who asked for it.
|
|
99
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
100
95
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
101
96
|
// `run.output` instead of `unknown`. `error` is the agent's own sentence for a
|
|
102
97
|
// rejected input, which is better copy than anything this page could write, and
|
|
103
98
|
// `wake` is bound to whatever run the hook is following — the whole reason this
|
|
104
99
|
// page no longer holds a `createWorkflowApi()` of its own.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
recover: true,
|
|
111
|
-
});
|
|
100
|
+
// No `key` and no `recover`: this tab's handle on its own runs is minted and
|
|
101
|
+
// remembered by the hook, and read back as it mounts. See the module doc for
|
|
102
|
+
// what a page says when it wants a different one.
|
|
103
|
+
const { submit, run, pending, error, wake, startedHere } =
|
|
104
|
+
useWorkflowSubmit<typeof digest>("digest");
|
|
112
105
|
|
|
113
106
|
// `submit()` resolves as soon as the run exists — deliberately not when it
|
|
114
107
|
// finishes. That is the whole mechanism: the digest sleeps for a while, and
|
|
115
108
|
// this page is free to be closed in the meantime.
|
|
116
109
|
const onSubmit = (event: React.FormEvent) => {
|
|
117
110
|
event.preventDefault();
|
|
118
|
-
setStartedHere(true);
|
|
119
111
|
void submit({ url });
|
|
120
112
|
};
|
|
121
113
|
|
|
@@ -129,6 +121,11 @@ export function App() {
|
|
|
129
121
|
required
|
|
130
122
|
value={url}
|
|
131
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"
|
|
132
129
|
// A REAL article, because a placeholder is a suggestion and this one
|
|
133
130
|
// gets typed. `https://example.com/article` 404s, and the bare
|
|
134
131
|
// `example.com` a reader trims it to has no prose on it — so the first
|
|
@@ -143,7 +140,13 @@ export function App() {
|
|
|
143
140
|
</button>
|
|
144
141
|
</form>
|
|
145
142
|
|
|
146
|
-
{
|
|
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
|
+
)}
|
|
147
150
|
|
|
148
151
|
{/* A run that has not settled says so. `pending` is not derivable from the
|
|
149
152
|
snapshot alone — an id the agent never knew leaves `run` undefined,
|
|
@@ -172,16 +175,19 @@ export function App() {
|
|
|
172
175
|
</button>
|
|
173
176
|
)}
|
|
174
177
|
|
|
175
|
-
{
|
|
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
|
+
)}
|
|
176
186
|
|
|
177
187
|
{run?.status === "completed" && (
|
|
178
188
|
<article className="flex flex-col gap-3">
|
|
179
189
|
<h2 className="text-xl">{run.output.headline}</h2>
|
|
180
|
-
<
|
|
181
|
-
{run.output.points.map((point) => (
|
|
182
|
-
<li key={point}>{point}</li>
|
|
183
|
-
))}
|
|
184
|
-
</ul>
|
|
190
|
+
<BulletList items={run.output.points} />
|
|
185
191
|
<p className="text-sm opacity-70">Filed {run.output.filedAt}</p>
|
|
186
192
|
</article>
|
|
187
193
|
)}
|
|
@@ -189,4 +195,4 @@ export function App() {
|
|
|
189
195
|
);
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
|
|
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
|
*
|
|
@@ -32,7 +32,12 @@ describe("night-owl template", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
test("recommend is discovered from tools/", () => {
|
|
35
|
-
|
|
35
|
+
// `toContain` rather than an exact list: a file in `tools/` IS a tool, so
|
|
36
|
+
// adding one is the edit this template most invites, and an exact list
|
|
37
|
+
// would make that edit fail a test the author never wrote. What has to hold
|
|
38
|
+
// is that discovery ran at all — a template whose `tools/` is never
|
|
39
|
+
// resolved ships a model with no tools.
|
|
40
|
+
expect(Object.keys(agentDef.tools ?? {})).toContain("recommend");
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
test("the projection an untouched session pushes is an empty log", () => {
|
|
@@ -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";
|
|
@@ -13,25 +13,42 @@ describe("pipeline-simple template", () => {
|
|
|
13
13
|
expect(() => toAgentConfig(agentDef)).not.toThrow();
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
test("
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
expect(agentDef.
|
|
22
|
-
expect(agentDef.
|
|
16
|
+
test("exports an agent the platform can name", () => {
|
|
17
|
+
// Not the literal. `aai init <project>` scaffolds this file verbatim, so a
|
|
18
|
+
// pinned name is a test about the TEMPLATE inside somebody else's project —
|
|
19
|
+
// and renaming the agent is the first thing they will do. What has to hold
|
|
20
|
+
// is that there is a name and the conversion carries it through.
|
|
21
|
+
expect(agentDef.name).toBeTruthy();
|
|
22
|
+
expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test("
|
|
26
|
-
|
|
25
|
+
test("declares at least one stage itself, which is what makes it a pipeline", () => {
|
|
26
|
+
// WHICH stage is deliberately not asserted: `agent.ts` invites you to move
|
|
27
|
+
// the declaration ("Declare only the stage you want elsewhere"), so naming
|
|
28
|
+
// one here would redden on the edit the comment above it asks for.
|
|
29
|
+
const declared = (["stt", "llm", "tts"] as const).filter(
|
|
30
|
+
(stage) => agentDef[stage] !== undefined,
|
|
31
|
+
);
|
|
32
|
+
expect(declared.length).toBeGreaterThan(0);
|
|
27
33
|
});
|
|
28
34
|
|
|
29
|
-
test("
|
|
35
|
+
test("a stage you declare survives, and every stage you leave unset defaults", () => {
|
|
36
|
+
// The template's whole subject, stated so it survives a swap: the config a
|
|
37
|
+
// deploy carries agrees with the def wherever the def has an opinion, and
|
|
38
|
+
// fills the AssemblyAI default wherever it does not.
|
|
30
39
|
const config = toAgentConfig(agentDef);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
if (config.mode !== "pipeline") {
|
|
41
|
+
// Switched the def to `s2s`? Then there is no cascade to fill — S2S
|
|
42
|
+
// REPLACES the pipeline rather than joining it, which the last describe
|
|
43
|
+
// below is the worked example for.
|
|
44
|
+
expect(config.mode).toBe("s2s");
|
|
45
|
+
expect(config.stt).toBeUndefined();
|
|
46
|
+
expect(config.tts).toBeUndefined();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const stage of ["stt", "llm", "tts"] as const) {
|
|
50
|
+
expect(config[stage]?.kind, stage).toBe(agentDef[stage]?.kind ?? "assemblyai");
|
|
51
|
+
}
|
|
35
52
|
});
|
|
36
53
|
});
|
|
37
54
|
|
|
@@ -82,7 +99,7 @@ describe("swapping any other stage", () => {
|
|
|
82
99
|
test("S2S: an explicit opt-in, and it REPLACES the pipeline rather than joining it", () => {
|
|
83
100
|
for (const [s2s, kind] of [
|
|
84
101
|
[assemblyAIS2s(), "assemblyai"],
|
|
85
|
-
[
|
|
102
|
+
[openAIS2s({ voice: "alloy" }), "openai-realtime"],
|
|
86
103
|
] as const) {
|
|
87
104
|
const config = toAgentConfig(agent({ name: "Line", s2s }));
|
|
88
105
|
expect(config.s2s?.kind).toBe(kind);
|