@alexkroman1/aai-cli 6.10.1 → 7.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-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +93 -14
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +13 -4
- package/dist/scaffold/vite.config.ts +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- package/dist/templates/call-audit/agent.test.ts +818 -0
- package/dist/templates/call-audit/agent.ts +171 -0
- package/dist/templates/call-audit/client.tsx +198 -0
- package/dist/templates/call-audit/workflows/audit.ts +298 -0
- package/dist/templates/call-audit/workflows/ingest.ts +251 -0
- package/dist/templates/call-audit/workflows/media.ts +637 -0
- package/dist/templates/call-audit/workflows/summarize.ts +201 -0
- package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.test.ts +746 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
- package/dist/templates/recap-workflow/agent.test.ts +107 -96
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.test.ts +314 -0
- package/dist/templates/spoken-summary/agent.ts +155 -0
- package/dist/templates/spoken-summary/client.tsx +185 -0
- package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
- package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/tools/answer_question.ts +17 -4
- package/dist/templates/transcription-workflow/agent.test.ts +246 -123
- package/dist/templates/transcription-workflow/agent.ts +21 -7
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
- package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
- package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
- package/dist/templates/travel-concierge/agent.test.ts +64 -33
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
tool,
|
|
4
|
-
type WorkflowOutputOf,
|
|
5
|
-
type WorkflowRunSnapshot,
|
|
6
|
-
} from "@alexkroman1/aai";
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { isTerminal, type WorkflowRunOf } from "@alexkroman1/aai/workflow-api";
|
|
7
3
|
import { recap } from "../shared.ts";
|
|
8
4
|
|
|
9
5
|
/** How many past runs the status tool will look at. Newest first. */
|
|
@@ -14,10 +10,13 @@ const RECENT_RUNS = 3;
|
|
|
14
10
|
*
|
|
15
11
|
* This is the QUERY, and `isTerminal` is what makes it typed: it narrows to the
|
|
16
12
|
* three finished statuses, which is what puts `run.output` and `run.error`
|
|
17
|
-
* within reach without a cast. `
|
|
18
|
-
*
|
|
13
|
+
* within reach without a cast. `WorkflowRunOf` is the snapshot with that output
|
|
14
|
+
* already named from the declaration — the
|
|
15
|
+
* `WorkflowRunSnapshot<WorkflowOutputOf<typeof recap>>` this file used to
|
|
16
|
+
* compose by hand, for a three-name import — so the signature never reaches past
|
|
17
|
+
* the declaration into the body, and it is still the discriminated union.
|
|
19
18
|
*/
|
|
20
|
-
function describeRun(run:
|
|
19
|
+
function describeRun(run: WorkflowRunOf<typeof recap>): string {
|
|
21
20
|
if (!isTerminal(run)) return "Still working on that one.";
|
|
22
21
|
switch (run.status) {
|
|
23
22
|
case "completed": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* Everything the desk claims to do. `submitRecording`, `checkTranscript` and
|
|
56
56
|
* `discardTranscript` are AssemblyAI's pre-recorded API (`POST`, `GET` and
|
|
57
57
|
* `DELETE` on `/v2/transcript`), and `summarize` is a real model call through
|
|
58
|
-
* `
|
|
58
|
+
* `stepGenerateJsonClassified`. The BATCH API is what makes the polling port honest: it
|
|
59
59
|
* answers with a job id in milliseconds and finishes minutes later, so the wait
|
|
60
60
|
* is the provider's, not a `setTimeout` this template chose. (Its sibling
|
|
61
61
|
* `transcription-workflow` takes the other endpoint — the sync one, which answers in
|
|
@@ -67,16 +67,14 @@
|
|
|
67
67
|
* just your shell.
|
|
68
68
|
*/
|
|
69
69
|
|
|
70
|
-
import {
|
|
70
|
+
import { report, requireStepEnv, stepFetch } from "@alexkroman1/aai/step";
|
|
71
71
|
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
stepGenerateJson,
|
|
79
|
-
} from "@alexkroman1/aai/utils";
|
|
72
|
+
stepFetchOk,
|
|
73
|
+
stepGenerateJsonClassified,
|
|
74
|
+
stepTranscribeSubmitClassified,
|
|
75
|
+
toStepError,
|
|
76
|
+
} from "@alexkroman1/aai/step-errors";
|
|
77
|
+
import { errorMessage, isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
80
78
|
import { createHook, FatalError, sleep } from "workflow";
|
|
81
79
|
import { z } from "zod";
|
|
82
80
|
import { retentionToken } from "./tokens.ts";
|
|
@@ -150,7 +148,7 @@ const POINTS = 3;
|
|
|
150
148
|
/**
|
|
151
149
|
* The shape the model must answer in.
|
|
152
150
|
*
|
|
153
|
-
* `
|
|
151
|
+
* `stepGenerateJsonClassified` validates against this and throws PLAINLY when the reply
|
|
154
152
|
* misses, which is the retry policy in one distinction: a model that answered in
|
|
155
153
|
* prose may answer correctly next time, where a 401 will not. `spoken` is the
|
|
156
154
|
* field this template exists for — without it the announced turn has nothing to
|
|
@@ -256,7 +254,7 @@ export async function recapFlow(input: { url: string; requestedBy: string }) {
|
|
|
256
254
|
* to completion in a worker, where the body is what may suspend. Splitting it
|
|
257
255
|
* out keeps `recapFlow` readable and costs nothing, since the WDK transform
|
|
258
256
|
* rewrites a step's DECLARATION rather than its call sites, so a step called
|
|
259
|
-
* from a helper is still a real step (`
|
|
257
|
+
* from a helper is still a real step (`mapConcurrent` rests on the same
|
|
260
258
|
* property).
|
|
261
259
|
*
|
|
262
260
|
* The loop is deterministic despite looking like it is not: every branch turns
|
|
@@ -358,14 +356,14 @@ export async function submitRecording(url: string): Promise<{ id: string }> {
|
|
|
358
356
|
|
|
359
357
|
await report(`Submitting ${new URL(url).hostname} for transcription…`);
|
|
360
358
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return {
|
|
359
|
+
// `stepTranscribeSubmitClassified` owns the endpoint, the raw-key auth, the
|
|
360
|
+
// PLURAL `speech_models` field and the failure classification — the
|
|
361
|
+
// `Classified` suffix being that last part: it is `stepTranscribeSubmit` with
|
|
362
|
+
// `throwStepError` already applied, so a provider refusal stays terminal and a
|
|
363
|
+
// rate limit waits out the delay the provider itself named. `speaker_labels`
|
|
364
|
+
// is this desk's own request, which is what `params` is for — the async API's
|
|
365
|
+
// surface is large and the SDK deliberately does not mirror it.
|
|
366
|
+
return await stepTranscribeSubmitClassified(url, { params: { speaker_labels: true } });
|
|
369
367
|
}
|
|
370
368
|
|
|
371
369
|
/**
|
|
@@ -374,6 +372,17 @@ export async function submitRecording(url: string): Promise<{ id: string }> {
|
|
|
374
372
|
* One poll is one step, so each attempt is journaled on its own: a run that dies
|
|
375
373
|
* mid-wait resumes knowing what the last answer was instead of starting the
|
|
376
374
|
* recording over.
|
|
375
|
+
*
|
|
376
|
+
* **Deliberately NOT `stepTranscribePoll`, though its sibling above did move to
|
|
377
|
+
* the SDK.** That helper answers `done` and THROWS on a job the provider gave
|
|
378
|
+
* up on, which is the right shape for a flow whose only question is "is the
|
|
379
|
+
* text ready". This desk's question is different: `status` is a VALUE here,
|
|
380
|
+
* read by the Query port (`recap_status`) while the run is still going, and an
|
|
381
|
+
* `error` status is the branch that unwinds the saga's compensation stack
|
|
382
|
+
* rather than a failure to propagate. Converting this would trade a documented
|
|
383
|
+
* state machine — the thing this template is actually about — for a throw.
|
|
384
|
+
* The provider's status union is the template's subject, so it stays in the
|
|
385
|
+
* template.
|
|
377
386
|
*/
|
|
378
387
|
export async function checkTranscript(id: string): Promise<TranscriptState> {
|
|
379
388
|
"use step";
|
|
@@ -456,19 +465,20 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
|
|
|
456
465
|
throw new FatalError("That recording came back with no speech in it.");
|
|
457
466
|
}
|
|
458
467
|
|
|
459
|
-
// `
|
|
460
|
-
// it is told not to, parses it, and validates it — all four
|
|
461
|
-
// used to re-derive. `
|
|
462
|
-
// (a bad key, a rejected request) stop rather than
|
|
463
|
-
// attempts, where a reply that missed the SHAPE throws
|
|
464
|
-
|
|
468
|
+
// `stepGenerateJsonClassified` unwraps the fence a model puts around JSON
|
|
469
|
+
// however firmly it is told not to, parses it, and validates it — all four
|
|
470
|
+
// things this step used to re-derive. The `Classified` half is what makes a
|
|
471
|
+
// terminal gateway failure (a bad key, a rejected request) stop rather than
|
|
472
|
+
// burn the remaining attempts, where a reply that missed the SHAPE throws
|
|
473
|
+
// plainly and retries.
|
|
474
|
+
const parsed = await stepGenerateJsonClassified(text, {
|
|
465
475
|
schema: RecapReply,
|
|
466
476
|
system:
|
|
467
477
|
"You write up recordings for someone who will hear the result on a phone call. " +
|
|
468
478
|
`Reply with JSON only: {"headline": string, "points": string[], "spoken": string}. ` +
|
|
469
479
|
`Give exactly ${POINTS} points. "spoken" is ONE sentence, under 30 words, ` +
|
|
470
480
|
"written to be read aloud. No markdown fence, no preamble.",
|
|
471
|
-
})
|
|
481
|
+
});
|
|
472
482
|
|
|
473
483
|
return {
|
|
474
484
|
url,
|
|
@@ -508,27 +518,24 @@ async function request(
|
|
|
508
518
|
url: string,
|
|
509
519
|
init: { method?: string; body?: string } = {},
|
|
510
520
|
): Promise<Response> {
|
|
511
|
-
// `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
|
|
512
|
-
// this workflow POLLS, so one run is many requests) get a socket
|
|
513
|
-
// than N streams on one connection — and a connection failure
|
|
514
|
-
// `StepTransportError` naming its cause instead of a bare
|
|
521
|
+
// Through `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
|
|
522
|
+
// runs (and this workflow POLLS, so one run is many requests) get a socket
|
|
523
|
+
// each rather than N streams on one connection — and a connection failure
|
|
524
|
+
// arrives as a `StepTransportError` naming its cause instead of a bare
|
|
515
525
|
// `TypeError: fetch failed`, which for a template whose whole subject is
|
|
516
526
|
// durability is the difference between a diagnosable resume and a mystery.
|
|
517
527
|
// `sdk/step-fetch.ts` carries the measurements.
|
|
518
|
-
|
|
528
|
+
// `stepFetchOk` makes the three-way retry decision: a 401 or a 400 answers the
|
|
529
|
+
// same way on the fourth attempt and burns the step, a 429 or a 5xx is what
|
|
530
|
+
// retries are for, and a `Retry-After` the provider named is waited out rather
|
|
531
|
+
// than replaced by the DevKit's one-second default — which matters here more
|
|
532
|
+
// than usual, because a fan-out of segments hits a rate limit together. The
|
|
533
|
+
// DELETE below stays on plain `stepFetch`, because there a 404 is a SUCCESS.
|
|
534
|
+
return await stepFetchOk(url, {
|
|
519
535
|
...init,
|
|
520
536
|
headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
|
|
521
537
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
522
538
|
});
|
|
523
|
-
// The three-way retry decision, made by the SDK: a 401 or a 400 answers the
|
|
524
|
-
// same way on the fourth attempt and burns the step, a 429 or a 5xx is what
|
|
525
|
-
// retries are for, and a `Retry-After` the provider named is waited out rather
|
|
526
|
-
// than replaced by the DevKit's one-second default — which matters here more
|
|
527
|
-
// than usual, because a fan-out of segments hits a rate limit together.
|
|
528
|
-
if (!response.ok) {
|
|
529
|
-
throw toStepError(response, `${init.method ?? "GET"} ${url} failed: HTTP ${response.status}`);
|
|
530
|
-
}
|
|
531
|
-
return response;
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
/** A string field of a JSON body, when it really is one. */
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* of that lives.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { parseSchemaInput, schemaInputIssues } from "@alexkroman1/aai/testing";
|
|
20
21
|
import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
21
22
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
22
23
|
import { FatalError } from "workflow";
|
|
@@ -24,7 +25,6 @@ import agentDef, { MAX_ROUNDS, redline } from "./agent.ts";
|
|
|
24
25
|
import {
|
|
25
26
|
briefBlock,
|
|
26
27
|
clampScore,
|
|
27
|
-
countWords,
|
|
28
28
|
critiqueDraft,
|
|
29
29
|
MAX_NOTES,
|
|
30
30
|
type RedlineInput,
|
|
@@ -70,30 +70,37 @@ describe("the agent declares itself a workflow app", () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
describe("the input schema", () => {
|
|
73
|
-
|
|
73
|
+
// `schemaInputIssues` / `parseSchemaInput` rather than a local reach through
|
|
74
|
+
// `["~standard"].validate`: that is the vendor WIRE contract, and whether it
|
|
75
|
+
// answers synchronously or with a promise is the vendor's business — a missing
|
|
76
|
+
// `await` there leaves `.issues` undefined and every refusing test below
|
|
77
|
+
// passes for the wrong reason.
|
|
78
|
+
const issues = (value: unknown) => schemaInputIssues(redline.input, value, "redline");
|
|
74
79
|
|
|
75
80
|
test("caps the rounds at the CALL SITE rather than on the bill", async () => {
|
|
76
|
-
|
|
77
|
-
expect(tooMany?.issues).toBeDefined();
|
|
81
|
+
expect(await issues({ ...INPUT, rounds: MAX_ROUNDS + 1 })).toBeDefined();
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
test("defaults the rounds and the required points, so the form need not", async () => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
const parsed = await parseSchemaInput(
|
|
86
|
+
redline.input,
|
|
87
|
+
{ brief: INPUT.brief, audience: "engineers" },
|
|
88
|
+
"redline",
|
|
89
|
+
);
|
|
90
|
+
expect(parsed).toMatchObject({ rounds: 2, mustCover: [] });
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
test("rejects an audience outside the enum — which is also what makes it a select", async () => {
|
|
87
94
|
// `<WorkflowFields>` renders a `z.enum` as a `<SelectField>`; the same
|
|
88
95
|
// declaration is what stops an API caller inventing a fifth audience.
|
|
89
|
-
expect(
|
|
96
|
+
expect(await issues({ ...INPUT, audience: "cats" })).toBeDefined();
|
|
90
97
|
});
|
|
91
98
|
|
|
92
99
|
test("declares mustCover as an array, which is what the page renders by hand", async () => {
|
|
93
100
|
// The mixed-form case: `<WorkflowFields>` renders scalars only, so client.tsx
|
|
94
101
|
// writes this field itself and maps a textarea into it.
|
|
95
|
-
expect(
|
|
96
|
-
expect(
|
|
102
|
+
expect(await issues({ ...INPUT, mustCover: "one point" })).toBeDefined();
|
|
103
|
+
expect(await issues({ ...INPUT, mustCover: ["one point"] })).toBeUndefined();
|
|
97
104
|
});
|
|
98
105
|
});
|
|
99
106
|
|
|
@@ -111,11 +118,6 @@ describe("pure helpers", () => {
|
|
|
111
118
|
expect(briefBlock({ ...INPUT, mustCover: [] })).toContain("nothing specific");
|
|
112
119
|
});
|
|
113
120
|
|
|
114
|
-
test("countWords ignores surrounding and repeated whitespace", () => {
|
|
115
|
-
expect(countWords(" one two\nthree ")).toBe(3);
|
|
116
|
-
expect(countWords(" ")).toBe(0);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
121
|
test("clampScore holds a model's number inside the range it was given", () => {
|
|
120
122
|
expect(clampScore(12)).toBe(10);
|
|
121
123
|
expect(clampScore(0)).toBe(1);
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import "@alexkroman1/aai-ui/styles.css";
|
|
30
|
-
import
|
|
30
|
+
import { plural } from "@alexkroman1/aai/utils";
|
|
31
|
+
import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
31
32
|
import {
|
|
32
33
|
Form,
|
|
33
34
|
type FormValues,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
SubmitButton,
|
|
36
37
|
TextAreaField,
|
|
37
38
|
useWorkflowSubmit,
|
|
39
|
+
WORKFLOW_STATUS_LABELS,
|
|
38
40
|
WorkflowFields,
|
|
39
41
|
WorkflowProgress,
|
|
40
42
|
type WorkflowRun,
|
|
@@ -150,8 +152,8 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
150
152
|
{run.status === "completed" && (
|
|
151
153
|
<>
|
|
152
154
|
<p className="text-xs opacity-60">
|
|
153
|
-
{run.output.words} words · {run.output.roundsRun}
|
|
154
|
-
{run.output.roundsRun
|
|
155
|
+
{run.output.words} words · {run.output.roundsRun}{" "}
|
|
156
|
+
{plural(run.output.roundsRun, "round")} ·{" "}
|
|
155
157
|
{/* Which of the two stop conditions ended the loop is the one thing
|
|
156
158
|
a reader cannot infer from the round count alone. */}
|
|
157
159
|
{run.output.shipped ? "the critic stopped it" : "the round budget stopped it"}
|
|
@@ -170,15 +172,13 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
170
172
|
/**
|
|
171
173
|
* One line describing where a run has got to.
|
|
172
174
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
+
* The SDK's map with the one label this desk wants differently: `running` is
|
|
176
|
+
* "Writing…" here because that is what the run is doing. Spreading a COMPLETE
|
|
177
|
+
* `Record<WorkflowRunStatus, string>` cannot drop a key, so the exhaustiveness
|
|
178
|
+
* the hand-written copy was written for survives — and now lives at the SDK
|
|
179
|
+
* boundary, where a status added upstream is one compile error rather than one
|
|
180
|
+
* per page.
|
|
175
181
|
*/
|
|
176
|
-
const STATUS_LINE:
|
|
177
|
-
pending: "Queued",
|
|
178
|
-
running: "Writing…",
|
|
179
|
-
completed: "Done",
|
|
180
|
-
failed: "Failed",
|
|
181
|
-
cancelled: "Cancelled",
|
|
182
|
-
};
|
|
182
|
+
const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
|
|
183
183
|
|
|
184
184
|
page({ name: "Redline", component: RedlineDesk });
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
* three functions read more tidily than one.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
34
|
+
import { report } from "@alexkroman1/aai/step";
|
|
35
|
+
import { stepGenerateClassified, stepGenerateJsonClassified } from "@alexkroman1/aai/step-errors";
|
|
36
|
+
import { countWords } from "@alexkroman1/aai/utils";
|
|
36
37
|
import { FatalError } from "workflow";
|
|
37
38
|
import { z } from "zod";
|
|
38
39
|
import { CRITIC_SYSTEM, REVISER_SYSTEM, WRITER_SYSTEM } from "./prompts.ts";
|
|
@@ -145,7 +146,7 @@ export async function writeDraft(input: RedlineInput): Promise<string> {
|
|
|
145
146
|
// `stepGenerate` already refuses an empty completion, as a RETRYABLE
|
|
146
147
|
// `StepGenerateError` — which is the right answer, and one a hand-written
|
|
147
148
|
// check would have to re-derive.
|
|
148
|
-
const draft = await
|
|
149
|
+
const draft = await stepGenerateClassified(briefBlock(input), { system: WRITER_SYSTEM });
|
|
149
150
|
return draft.trim();
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -168,10 +169,10 @@ export async function critiqueDraft(
|
|
|
168
169
|
// `stepGenerateJson` owns the fence, the parse, the non-object case and the
|
|
169
170
|
// shape — and throws PLAINLY when any of them misses, unlike the fatal one
|
|
170
171
|
// above: a model that answered with prose may well obey on the next attempt.
|
|
171
|
-
const parsed = await
|
|
172
|
-
|
|
173
|
-
system: CRITIC_SYSTEM,
|
|
174
|
-
|
|
172
|
+
const parsed = await stepGenerateJsonClassified(
|
|
173
|
+
`${briefBlock(input)}\n\nThe submission:\n${draft}`,
|
|
174
|
+
{ schema: CritiqueReply, system: CRITIC_SYSTEM },
|
|
175
|
+
);
|
|
175
176
|
|
|
176
177
|
const critique: Critique = {
|
|
177
178
|
verdict: parsed.verdict,
|
|
@@ -199,7 +200,7 @@ export async function reviseDraft(
|
|
|
199
200
|
"use step";
|
|
200
201
|
|
|
201
202
|
await report(`Round ${round}: revising.`);
|
|
202
|
-
const revised = await
|
|
203
|
+
const revised = await stepGenerateClassified(
|
|
203
204
|
[
|
|
204
205
|
briefBlock(input),
|
|
205
206
|
`Your current draft:\n${draft}`,
|
|
@@ -221,11 +222,6 @@ export function briefBlock(input: RedlineInput): string {
|
|
|
221
222
|
return [`Brief: ${input.brief}`, `Audience: ${input.audience}`, must].join("\n\n");
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
export function countWords(text: string): number {
|
|
225
|
-
const trimmed = text.trim();
|
|
226
|
-
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
225
|
/** Scores arrive from a model, so they arrive out of range often enough. */
|
|
230
226
|
export function clampScore(score: number): number {
|
|
231
227
|
if (!Number.isFinite(score)) return 0;
|
|
@@ -233,24 +229,16 @@ export function clampScore(score: number): number {
|
|
|
233
229
|
}
|
|
234
230
|
|
|
235
231
|
// ---- The model call ---------------------------------------------------------
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
*
|
|
247
|
-
* This desk used to carry that mapping itself, as did `research-workflow` and
|
|
248
|
-
* `link-digest`; it is one import now, and the delay is no longer the one line
|
|
249
|
-
* only this template remembered.
|
|
250
|
-
*/
|
|
251
|
-
async function ask(prompt: string, opts: { system: string }): Promise<string> {
|
|
252
|
-
return await stepGenerate(prompt, opts).catch(throwStepError);
|
|
253
|
-
}
|
|
232
|
+
//
|
|
233
|
+
// There is no local `ask()` any more, and its absence is the point. The SDK
|
|
234
|
+
// classifies the gateway's failure (`StepGenerateError.retryable`) and stops
|
|
235
|
+
// there — whether a terminal failure should burn the step's remaining attempts
|
|
236
|
+
// is the caller's call — so `stepGenerateClassified` and
|
|
237
|
+
// `stepGenerateJsonClassified` (`@alexkroman1/aai/step-errors`) are that call
|
|
238
|
+
// made one way: terminal stays terminal, and a rate limit becomes a
|
|
239
|
+
// `RetryableError` carrying the delay the gateway itself named, which beats
|
|
240
|
+
// `RetryableError`'s own one-second default. Three templates each wrapped the
|
|
241
|
+
// raw `/step` call to say that; the wrapper is a suffix on the import now.
|
|
254
242
|
|
|
255
243
|
/** A rate limit — and a model that ignored the format — are both expected. */
|
|
256
244
|
critiqueDraft.maxRetries = 5;
|