@alexkroman1/aai-cli 6.11.0 → 8.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/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- 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-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -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.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- 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/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- 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/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- 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.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -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 +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- 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 +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- 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.eval.test.ts +329 -0
- 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/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- 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 +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- 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.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -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/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- 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 +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- 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
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Copyright 2026 the AAI authors. MIT license.
|
|
2
|
+
// An EVAL for a WORKFLOW APP: does the run actually do the work? Run it with
|
|
3
|
+
// `aai eval`.
|
|
4
|
+
//
|
|
5
|
+
// `agent.test.ts` asserts about the declaration and drives the three steps one
|
|
6
|
+
// at a time. This drives the WHOLE BODY — `redlineFlow` from the top — and the
|
|
7
|
+
// thing it is here to check is the LOOP: the critic returns a verdict, the body
|
|
8
|
+
// breaks on it, and `shipped` says which of the two stopped it. That decision is
|
|
9
|
+
// the whole point of this template, and it is the one thing a per-step spec
|
|
10
|
+
// structurally cannot see.
|
|
11
|
+
//
|
|
12
|
+
// `describeWorkflowEval` picks the providers for you and says which it picked:
|
|
13
|
+
//
|
|
14
|
+
// * with `ASSEMBLYAI_API_KEY` — a LIVE run: a real model writes, a real model
|
|
15
|
+
// critiques, and a real model revises if it is asked to. That spends tokens,
|
|
16
|
+
// and a model is a NOISY instrument — one failure is a question, not a
|
|
17
|
+
// verdict. Re-run before believing either answer.
|
|
18
|
+
// * without one — a SCRIPTED run. The body, the loop and the three steps all
|
|
19
|
+
// really execute; only the gateway is answered in memory.
|
|
20
|
+
//
|
|
21
|
+
// Two of the three cases below are SCRIPTED IN BOTH MODES on purpose, and say so
|
|
22
|
+
// where they are: their claims are about the loop's arithmetic and about what a
|
|
23
|
+
// stage was SHOWN, and a live model cannot be asked to make either of those
|
|
24
|
+
// true — it can only be asked and then have its answer accepted, which is not
|
|
25
|
+
// evidence.
|
|
26
|
+
//
|
|
27
|
+
// WHAT NO EVAL HERE COVERS: durability. Imported through vitest with no bundler
|
|
28
|
+
// in the path, a `"use workflow"` body is an ordinary async function — no
|
|
29
|
+
// journal, no replay, and no per-step retry, so a rate-limited live run FAILS
|
|
30
|
+
// where a deployed one would have ridden it out. The tier that really resumes a
|
|
31
|
+
// run is `aai-cli`'s `dev-workflow.scenario.test.ts`.
|
|
32
|
+
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
33
|
+
import { describeWorkflowEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
34
|
+
import { expect } from "vitest";
|
|
35
|
+
import agentDef, { MAX_ROUNDS, redline } from "./agent.ts";
|
|
36
|
+
|
|
37
|
+
/** A brief with a word in it nothing else would produce, so the draft is checkable. */
|
|
38
|
+
const BRIEF = "Explain why our on-call rotation is moving to a two-week quokka cycle";
|
|
39
|
+
/** The one point every stage must be shown — `briefBlock` is what carries it. */
|
|
40
|
+
const MUST_COVER = ["Nobody carries the pager two weeks running"];
|
|
41
|
+
|
|
42
|
+
/** What the writer and the reviser are scripted to hand back. */
|
|
43
|
+
const DRAFT = [
|
|
44
|
+
"The on-call rotation is moving to a two-week quokka cycle.",
|
|
45
|
+
"Nobody carries the pager two weeks running: the second week is review and follow-up.",
|
|
46
|
+
].join(" ");
|
|
47
|
+
|
|
48
|
+
const critique = (verdict: "ship" | "revise", score = 8): string =>
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
verdict,
|
|
51
|
+
score,
|
|
52
|
+
notes: verdict === "ship" ? [] : ["Say what happens to the handover", "Name the start date"],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/** One gateway reply, in the envelope `stepGenerate` reads. */
|
|
56
|
+
const reply = (content: string) => ({ body: { choices: [{ message: { content } }] } });
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Answer the gateway with `contents`, in order, and record what each stage asked.
|
|
60
|
+
*
|
|
61
|
+
* The last reply repeats, matching `stubGateway`'s convention — a loop cannot
|
|
62
|
+
* know how many calls it will make, and a script that ran out mid-loop would
|
|
63
|
+
* fail on the script rather than on the code. `installStubStepFetch` rather than
|
|
64
|
+
* `installStubGateway`: `stepGenerate` goes through the published `stepFetch`
|
|
65
|
+
* slot, and a published slot BEATS a stubbed global, so stubbing the global here
|
|
66
|
+
* would test a path production does not take.
|
|
67
|
+
*/
|
|
68
|
+
function scriptGateway(contents: readonly string[]) {
|
|
69
|
+
let next = 0;
|
|
70
|
+
const fetched = installStubStepFetch(() => {
|
|
71
|
+
const content = contents.at(Math.min(next, contents.length - 1)) ?? "";
|
|
72
|
+
next += 1;
|
|
73
|
+
return reply(content);
|
|
74
|
+
});
|
|
75
|
+
return fetched;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Every prompt the gateway was sent, in call order. */
|
|
79
|
+
function promptsOf(fetched: ReturnType<typeof scriptGateway>): string[] {
|
|
80
|
+
return fetched.calls.map((call) => String(call.body ?? ""));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describeWorkflowEval(agentDef, (test) => {
|
|
84
|
+
test("runs a round and lets the CRITIC decide whether there is another", async ({
|
|
85
|
+
app,
|
|
86
|
+
mode,
|
|
87
|
+
}) => {
|
|
88
|
+
// One round in live mode, deliberately: the claim is about the loop's exit,
|
|
89
|
+
// and three long-form model calls are enough to make it.
|
|
90
|
+
if (mode === "stub") scriptGateway([DRAFT, critique("ship")]);
|
|
91
|
+
|
|
92
|
+
const run = await app.run(redline, {
|
|
93
|
+
brief: BRIEF,
|
|
94
|
+
audience: "engineers",
|
|
95
|
+
rounds: 1,
|
|
96
|
+
mustCover: MUST_COVER,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// The error first, so a failed run names its own reason.
|
|
100
|
+
expect(run.error).toBeUndefined();
|
|
101
|
+
expect(run.status).toBe("completed");
|
|
102
|
+
|
|
103
|
+
const output = run.output;
|
|
104
|
+
if (output === undefined) expect.fail("a completed run must carry an output");
|
|
105
|
+
expect(output.roundsRun).toBe(1);
|
|
106
|
+
expect(output.rounds).toHaveLength(1);
|
|
107
|
+
|
|
108
|
+
const last = output.rounds.at(-1);
|
|
109
|
+
if (last === undefined) expect.fail("a run of one round must record it");
|
|
110
|
+
// THE INVARIANT the template exists for: `shipped` is true exactly when the
|
|
111
|
+
// critic said so, and a shipped round revised nothing after it. Get either
|
|
112
|
+
// half wrong and the loop spends a model call it did not need, or stops one
|
|
113
|
+
// short — both of which read as a working run.
|
|
114
|
+
expect(["ship", "revise"]).toContain(last.critique.verdict);
|
|
115
|
+
expect(output.shipped).toBe(last.critique.verdict === "ship");
|
|
116
|
+
if (output.shipped) expect(last.revisedWords).toBeUndefined();
|
|
117
|
+
else expect(last.revisedWords).toBeTypeOf("number");
|
|
118
|
+
|
|
119
|
+
// The score is CLAMPED at the call site rather than by the schema, so a
|
|
120
|
+
// model answering 11 still lands in range.
|
|
121
|
+
expect(last.critique.score).toBeGreaterThanOrEqual(1);
|
|
122
|
+
expect(last.critique.score).toBeLessThanOrEqual(10);
|
|
123
|
+
expect(last.critique.notes.length).toBeLessThanOrEqual(3);
|
|
124
|
+
|
|
125
|
+
// It wrote about the brief it was given, not about writing in general.
|
|
126
|
+
expect(output.draft).toMatch(/quokka/i);
|
|
127
|
+
expect(output.words).toBeGreaterThan(5);
|
|
128
|
+
|
|
129
|
+
// Both stages narrated, and the round is numbered — which is what a page
|
|
130
|
+
// watching the run renders.
|
|
131
|
+
expect(run.reported[0]).toBe("Writing the first draft for engineers.");
|
|
132
|
+
expect(run.reported).toContain("Round 1: reading it back critically.");
|
|
133
|
+
// Nothing durable was asked for, so nothing was skipped: this body's only
|
|
134
|
+
// waits are its model calls.
|
|
135
|
+
expect(run.slept).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("stops on the ROUND BUDGET when the critic never ships", async ({ app }) => {
|
|
139
|
+
// Scripted in both modes: a live critic cannot be made to refuse three times
|
|
140
|
+
// running, and asking it and then accepting whatever it says is not evidence
|
|
141
|
+
// about the budget. What this pins is the loop's arithmetic — the half a
|
|
142
|
+
// live case cannot reach.
|
|
143
|
+
const fetched = scriptGateway([
|
|
144
|
+
DRAFT,
|
|
145
|
+
critique("revise", 4),
|
|
146
|
+
`${DRAFT} It starts on the first Monday of the month.`,
|
|
147
|
+
critique("revise", 5),
|
|
148
|
+
`${DRAFT} It starts on the first Monday, and the handover is a written note.`,
|
|
149
|
+
critique("revise", 6),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
const run = await app.run(redline, {
|
|
153
|
+
brief: BRIEF,
|
|
154
|
+
audience: "executives",
|
|
155
|
+
rounds: MAX_ROUNDS,
|
|
156
|
+
mustCover: MUST_COVER,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(run.error).toBeUndefined();
|
|
160
|
+
const output = run.output;
|
|
161
|
+
if (output === undefined) expect.fail("a completed run must carry an output");
|
|
162
|
+
|
|
163
|
+
// The BUDGET stopped it, not the critic — which is the field a page reads to
|
|
164
|
+
// say "this is as good as it got" rather than "this is finished".
|
|
165
|
+
expect(output.shipped).toBe(false);
|
|
166
|
+
expect(output.roundsRun).toBe(MAX_ROUNDS);
|
|
167
|
+
// Every round revised, because none of them shipped.
|
|
168
|
+
expect(
|
|
169
|
+
output.rounds.map((round) => round.revisedWords).every((n) => typeof n === "number"),
|
|
170
|
+
).toBe(true);
|
|
171
|
+
// One draft plus a critique-and-revise pair per round. A loop that critiqued
|
|
172
|
+
// twice, or revised the round it shipped, changes this number.
|
|
173
|
+
expect(fetched.calls).toHaveLength(1 + 2 * MAX_ROUNDS);
|
|
174
|
+
|
|
175
|
+
// `briefBlock` is what keeps the three stages from drifting apart, and this is
|
|
176
|
+
// the assertion behind that claim: the writer, the critic AND the reviser were
|
|
177
|
+
// all shown the same brief and the same must-cover point.
|
|
178
|
+
const prompts = promptsOf(fetched);
|
|
179
|
+
expect(prompts).toHaveLength(1 + 2 * MAX_ROUNDS);
|
|
180
|
+
for (const prompt of prompts) {
|
|
181
|
+
expect(prompt).toContain("quokka");
|
|
182
|
+
expect(prompt).toContain(MUST_COVER[0]);
|
|
183
|
+
}
|
|
184
|
+
// The critic and the reviser were also shown the DRAFT, which the writer
|
|
185
|
+
// could not have been.
|
|
186
|
+
expect(prompts[1]).toContain("The on-call rotation is moving");
|
|
187
|
+
expect(prompts[2]).toContain("The critique");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("refuses a brief that is only whitespace, terminally", async ({ app }) => {
|
|
191
|
+
// No model is reached on this path in either mode, so it costs nothing live.
|
|
192
|
+
// The case exists because the schema's `.min(20)` counts CHARACTERS: twenty
|
|
193
|
+
// spaces validate at `start()` and arrive at the writer as nothing to write
|
|
194
|
+
// from, which is why `writeDraft` carries its own `FatalError`.
|
|
195
|
+
const run = await app.run(redline, {
|
|
196
|
+
brief: " ".repeat(40),
|
|
197
|
+
audience: "general readers",
|
|
198
|
+
rounds: 1,
|
|
199
|
+
mustCover: [],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(run.status).toBe("failed");
|
|
203
|
+
expect(run.error).toMatch(/too short to write from/i);
|
|
204
|
+
expect(run.output).toBeUndefined();
|
|
205
|
+
// It failed before narrating anything, which is the ordering the guard
|
|
206
|
+
// implies: the check is the first thing in the step.
|
|
207
|
+
expect(run.reported).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -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;
|