@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
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
* Everything the desk claims to do. `submitRecording`, `checkTranscript` and
|
|
100
100
|
* `discardTranscript` are AssemblyAI's pre-recorded API (`POST`, `GET` and
|
|
101
101
|
* `DELETE` on `/v2/transcript`), and `summarize` is a real model call through
|
|
102
|
-
* `
|
|
102
|
+
* `stepGenerateJsonOrFail`. The BATCH API is what makes the polling port honest: it
|
|
103
103
|
* answers with a job id in milliseconds and finishes minutes later, so the wait
|
|
104
104
|
* is the provider's, not a `setTimeout` this template chose. (Its sibling
|
|
105
105
|
* `transcription-workflow` takes the other endpoint — the sync one, which answers in
|
|
@@ -111,13 +111,13 @@
|
|
|
111
111
|
* just your shell.
|
|
112
112
|
*/
|
|
113
113
|
|
|
114
|
-
import type {
|
|
115
|
-
import {
|
|
114
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
115
|
+
import { requireStepEnv, stepFetch, stepReport, stepWebhookUrl } from "@alexkroman1/aai/step";
|
|
116
116
|
import {
|
|
117
117
|
FatalError,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
stepFetchOrFail,
|
|
119
|
+
stepGenerateJsonOrFail,
|
|
120
|
+
stepTranscribeSubmitOrFail,
|
|
121
121
|
toStepError,
|
|
122
122
|
} from "@alexkroman1/aai/step-errors";
|
|
123
123
|
import { errorMessage, omitUndefined } from "@alexkroman1/aai/utils";
|
|
@@ -239,7 +239,7 @@ const POINTS = 3;
|
|
|
239
239
|
/**
|
|
240
240
|
* The shape the model must answer in.
|
|
241
241
|
*
|
|
242
|
-
* `
|
|
242
|
+
* `stepGenerateJsonOrFail` validates against this and throws PLAINLY when the reply
|
|
243
243
|
* misses, which is the retry policy in one distinction: a model that answered in
|
|
244
244
|
* prose may answer correctly next time, where a 401 will not. `spoken` is the
|
|
245
245
|
* field this template exists for — without it the announced turn has nothing to
|
|
@@ -347,7 +347,7 @@ export type Compensation = { label: string; undo: () => Promise<void> };
|
|
|
347
347
|
* `agent.ts` reads back down the phone, so it is shaped for an ear rather than
|
|
348
348
|
* a page.
|
|
349
349
|
*/
|
|
350
|
-
export async function recapFlow(input: { url: string; requestedBy: string }, ctx:
|
|
350
|
+
export async function recapFlow(input: { url: string; requestedBy: string }, ctx: WorkflowContext) {
|
|
351
351
|
// The compensation stack, newest first — `unshift` after each successful
|
|
352
352
|
// acquisition, exactly as Temporal's `openAccount` does. Registering the undo
|
|
353
353
|
// AFTER the step it undoes is the whole discipline: a step that never
|
|
@@ -462,7 +462,7 @@ export async function recapFlow(input: { url: string; requestedBy: string }, ctx
|
|
|
462
462
|
*/
|
|
463
463
|
export async function awaitTranscript(
|
|
464
464
|
id: string,
|
|
465
|
-
ctx:
|
|
465
|
+
ctx: WorkflowContext,
|
|
466
466
|
nudge?: string,
|
|
467
467
|
): Promise<TranscriptState> {
|
|
468
468
|
// Which turn says "still going". A pure function of `nudge`, which the body
|
|
@@ -518,7 +518,7 @@ export async function askWhetherToKeep(
|
|
|
518
518
|
requestedBy: string,
|
|
519
519
|
transcriptId: string,
|
|
520
520
|
compensations: Compensation[],
|
|
521
|
-
ctx:
|
|
521
|
+
ctx: WorkflowContext,
|
|
522
522
|
): Promise<Retention> {
|
|
523
523
|
await ctx.step("noteGate", () =>
|
|
524
524
|
note(
|
|
@@ -560,7 +560,7 @@ export async function askWhetherToKeep(
|
|
|
560
560
|
export async function compensate(
|
|
561
561
|
compensations: Compensation[],
|
|
562
562
|
because: string,
|
|
563
|
-
ctx:
|
|
563
|
+
ctx: WorkflowContext,
|
|
564
564
|
): Promise<void> {
|
|
565
565
|
if (compensations.length === 0) return;
|
|
566
566
|
// The narration is a STEP like every other, so an unwind interrupted by a
|
|
@@ -607,9 +607,9 @@ export async function submitRecording(
|
|
|
607
607
|
url: string,
|
|
608
608
|
webhookUrl?: string,
|
|
609
609
|
): Promise<{ id: string; callback: boolean }> {
|
|
610
|
-
await
|
|
610
|
+
await stepReport(`Submitting ${new URL(url).hostname} for transcription…`);
|
|
611
611
|
|
|
612
|
-
// `
|
|
612
|
+
// `stepTranscribeSubmitOrFail` owns the endpoint, the raw-key auth, the
|
|
613
613
|
// PLURAL `speech_models` field and the failure classification — the
|
|
614
614
|
// `Classified` suffix being that last part: it is `stepTranscribeSubmit` with
|
|
615
615
|
// `throwStepError` already applied, so a provider refusal stays terminal and a
|
|
@@ -625,7 +625,7 @@ export async function submitRecording(
|
|
|
625
625
|
// What must not creep in is a `?? null` or a `?? ""` to "be explicit": either
|
|
626
626
|
// one puts the key back, and a provider handed a null for a URL is entitled to
|
|
627
627
|
// refuse the whole submission.
|
|
628
|
-
const job = await
|
|
628
|
+
const job = await stepTranscribeSubmitOrFail(url, {
|
|
629
629
|
params: { speaker_labels: true, webhook_url: webhookUrl },
|
|
630
630
|
});
|
|
631
631
|
return { id: job.id, callback: webhookUrl !== undefined };
|
|
@@ -698,7 +698,7 @@ export async function checkTranscript(id: string): Promise<TranscriptState> {
|
|
|
698
698
|
);
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
await
|
|
701
|
+
await stepReport(`Transcript ${parsed.data.status}.`);
|
|
702
702
|
return {
|
|
703
703
|
status: parsed.data.status,
|
|
704
704
|
// `omitUndefined` rather than a spread-ternary per field: under
|
|
@@ -730,7 +730,7 @@ export async function checkTranscript(id: string): Promise<TranscriptState> {
|
|
|
730
730
|
* a replay is exactly that world.
|
|
731
731
|
*/
|
|
732
732
|
export async function discardTranscript(id: string): Promise<void> {
|
|
733
|
-
await
|
|
733
|
+
await stepReport(`Discarding transcript ${id}.`);
|
|
734
734
|
// Not through `request` above, because a 404 is a SUCCESS here — see below.
|
|
735
735
|
// `stepFetch` for the same reason it does; only the status handling differs.
|
|
736
736
|
const response = await stepFetch(`${TRANSCRIPT_ENDPOINT}/${id}`, {
|
|
@@ -756,7 +756,7 @@ export async function discardTranscript(id: string): Promise<void> {
|
|
|
756
756
|
* journal instead of submitting the recording again.
|
|
757
757
|
*/
|
|
758
758
|
export async function summarize(url: string, transcript: TranscriptState): Promise<Recap> {
|
|
759
|
-
await
|
|
759
|
+
await stepReport("Writing the recap.");
|
|
760
760
|
|
|
761
761
|
const text = (transcript.text ?? "").slice(0, MAX_TRANSCRIPT_CHARS);
|
|
762
762
|
if (text.trim() === "") {
|
|
@@ -765,13 +765,13 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
|
|
|
765
765
|
throw new FatalError("That recording came back with no speech in it.");
|
|
766
766
|
}
|
|
767
767
|
|
|
768
|
-
// `
|
|
768
|
+
// `stepGenerateJsonOrFail` unwraps the fence a model puts around JSON
|
|
769
769
|
// however firmly it is told not to, parses it, and validates it — all four
|
|
770
770
|
// things this step used to re-derive. The `Classified` half is what makes a
|
|
771
771
|
// terminal gateway failure (a bad key, a rejected request) stop rather than
|
|
772
772
|
// burn the remaining attempts, where a reply that missed the SHAPE throws
|
|
773
773
|
// plainly and retries.
|
|
774
|
-
const parsed = await
|
|
774
|
+
const parsed = await stepGenerateJsonOrFail(text, {
|
|
775
775
|
schema: RecapReply,
|
|
776
776
|
system:
|
|
777
777
|
"You write up recordings for someone who will hear the result on a phone call. " +
|
|
@@ -792,13 +792,13 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
|
|
|
792
792
|
/**
|
|
793
793
|
* Say one line into the run's progress channel.
|
|
794
794
|
*
|
|
795
|
-
* A step for one reason: the body REPLAYS, so a `
|
|
795
|
+
* A step for one reason: the body REPLAYS, so a `stepReport()` written there is
|
|
796
796
|
* re-emitted on every resume. Everything the body itself wants to narrate —
|
|
797
797
|
* the slow-recording note, the unwind — comes through here, and `agent.ts`'s
|
|
798
798
|
* `recap_progress` is what reads it back down the phone.
|
|
799
799
|
*/
|
|
800
800
|
export async function note(line: string): Promise<void> {
|
|
801
|
-
await
|
|
801
|
+
await stepReport(line);
|
|
802
802
|
}
|
|
803
803
|
|
|
804
804
|
// ---- HTTP -------------------------------------------------------------------
|
|
@@ -810,10 +810,7 @@ export async function note(line: string): Promise<void> {
|
|
|
810
810
|
* Note the header is a bare key: AssemblyAI's `authorization` takes the key
|
|
811
811
|
* itself, with no `Bearer` prefix.
|
|
812
812
|
*/
|
|
813
|
-
async function request(
|
|
814
|
-
url: string,
|
|
815
|
-
init: { method?: string; body?: string } = {},
|
|
816
|
-
): Promise<Response> {
|
|
813
|
+
async function request(url: string): Promise<Response> {
|
|
817
814
|
// Through `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
|
|
818
815
|
// runs (and this workflow POLLS, so one run is many requests) get a socket
|
|
819
816
|
// each rather than N streams on one connection — and a connection failure
|
|
@@ -821,14 +818,13 @@ async function request(
|
|
|
821
818
|
// `TypeError: fetch failed`, which for a template whose whole subject is
|
|
822
819
|
// durability is the difference between a diagnosable resume and a mystery.
|
|
823
820
|
// `sdk/step-fetch.ts` carries the measurements.
|
|
824
|
-
// `
|
|
821
|
+
// `stepFetchOrFail` makes the three-way retry decision: a 401 or a 400 answers the
|
|
825
822
|
// same way on the fourth attempt and burns the step, a 429 or a 5xx is what
|
|
826
823
|
// retries are for, and a `Retry-After` the provider named is waited out rather
|
|
827
824
|
// than replaced by the DevKit's one-second default — which matters here more
|
|
828
825
|
// than usual, because a fan-out of segments hits a rate limit together. The
|
|
829
826
|
// DELETE below stays on plain `stepFetch`, because there a 404 is a SUCCESS.
|
|
830
|
-
return await
|
|
831
|
-
...init,
|
|
827
|
+
return await stepFetchOrFail(url, {
|
|
832
828
|
headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
|
|
833
829
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
834
830
|
});
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// journal, no replay, and no per-step retry, so a rate-limited live run FAILS
|
|
30
30
|
// where a deployed one would have ridden it out. The tier that really resumes a
|
|
31
31
|
// run is `aai-cli`'s `dev-workflow.scenario.test.ts`.
|
|
32
|
-
import { stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
32
|
+
import { routeStepFetch, stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
33
33
|
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
34
34
|
import { describeWorkflowEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
35
35
|
import { expect } from "vitest";
|
|
@@ -75,13 +75,10 @@ const critique = (verdict: "ship" | "revise", score = 8): string =>
|
|
|
75
75
|
*/
|
|
76
76
|
function scriptGateway(contents: readonly string[]) {
|
|
77
77
|
const model = stubGatewayRoute(contents);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
return answered;
|
|
84
|
-
});
|
|
78
|
+
// `routeStepFetch` defaults to throwing on an unrecognised request, which is
|
|
79
|
+
// what this file wants and what it used to spell out: every step in this body
|
|
80
|
+
// is a model call, so anything the route does not recognise is a finding.
|
|
81
|
+
installStubStepFetch(routeStepFetch([model.route]));
|
|
85
82
|
return model;
|
|
86
83
|
}
|
|
87
84
|
|
|
@@ -46,7 +46,13 @@ describe("the agent declares itself a workflow app", () => {
|
|
|
46
46
|
test("under the name the page starts a run by", () => {
|
|
47
47
|
// `useWorkflowSubmit("redline")` in client.tsx names this key. Nothing else
|
|
48
48
|
// records it, so a rename here is a 400 there rather than a compile error.
|
|
49
|
-
|
|
49
|
+
// `toContain` rather than an exact key list: adding a second workflow is an
|
|
50
|
+
// invited edit and must not redden a test the author did not write. The
|
|
51
|
+
// NAME is still pinned, deliberately — the page starts a run by this
|
|
52
|
+
// string, so renaming the key is a runtime 400 rather than a compile
|
|
53
|
+
// error, and this pin is the only thing that says so. Rename it here and
|
|
54
|
+
// in `client.tsx` together.
|
|
55
|
+
expect(Object.keys(agentDef.workflows ?? {})).toContain("redline");
|
|
50
56
|
expect(agentDef.workflows?.redline).toBe(redline);
|
|
51
57
|
});
|
|
52
58
|
|
|
@@ -222,7 +228,7 @@ describe("the steps", () => {
|
|
|
222
228
|
* to take the same branch on every walk.
|
|
223
229
|
*
|
|
224
230
|
* The model is the whole world here (`writeDraft`, `critiqueDraft` and
|
|
225
|
-
* `reviseDraft` are all `stepGenerate*
|
|
231
|
+
* `reviseDraft` are all `stepGenerate*OrFail`), so `stubGateway`'s scripted
|
|
226
232
|
* replies ARE the run, and its call log is what proves a replay did not pay for
|
|
227
233
|
* a round twice. Scripted in body order, with the last reply repeating.
|
|
228
234
|
*/
|
|
@@ -31,11 +31,13 @@
|
|
|
31
31
|
* in React state — so a refresh lost it while the loop carried on writing,
|
|
32
32
|
* grading and revising without anywhere to report to. On a desk whose whole
|
|
33
33
|
* subject is a loop that runs several long-form model calls, that is the one
|
|
34
|
-
* failure
|
|
35
|
-
*
|
|
36
|
-
* Clear button are
|
|
34
|
+
* failure the hook now covers on its own: `useWorkflowSubmit` records every run
|
|
35
|
+
* under a correlation KEY it mints for this page and asks for that key's newest
|
|
36
|
+
* run as it mounts, so the draft, the critique trail and the Clear button are
|
|
37
|
+
* all there again with nothing written here.
|
|
37
38
|
*
|
|
38
|
-
* **The key is opaque and lives in `sessionStorage`, and the brief is
|
|
39
|
+
* **The key it mints is opaque and lives in `sessionStorage`, and the brief is
|
|
40
|
+
* why this page wants exactly that one.** A
|
|
39
41
|
* `?key=` parameter in the page's own URL would survive more — a new tab, a
|
|
40
42
|
* bookmark, a link sent to the person who asked for the piece — and that is
|
|
41
43
|
* exactly what it must not do here. There is no per-user filtering behind
|
|
@@ -48,26 +50,28 @@
|
|
|
48
50
|
*
|
|
49
51
|
* Deriving the key from the brief is worse again: two people briefing the same
|
|
50
52
|
* thing would recover each other's runs, and the key would then carry what they
|
|
51
|
-
* typed into a lookup token the platform deliberately stopped logging.
|
|
53
|
+
* typed into a lookup token the platform deliberately stopped logging. Both are
|
|
54
|
+
* things a page could still ask for by passing its own `key`, and this one has
|
|
55
|
+
* no reason to.
|
|
52
56
|
*/
|
|
53
57
|
|
|
54
58
|
import "@alexkroman1/aai-ui/styles.css";
|
|
55
59
|
import { plural } from "@alexkroman1/aai/utils";
|
|
56
60
|
import type { WorkflowInputOf, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
57
61
|
import {
|
|
62
|
+
BulletList,
|
|
63
|
+
Facts,
|
|
58
64
|
Form,
|
|
59
65
|
type FormValues,
|
|
60
|
-
|
|
66
|
+
mountPage,
|
|
61
67
|
SubmitButton,
|
|
62
68
|
TextAreaField,
|
|
63
|
-
useRunKey,
|
|
64
69
|
useWorkflowSubmit,
|
|
65
70
|
WORKFLOW_STATUS_LABELS,
|
|
66
71
|
WorkflowFields,
|
|
67
72
|
WorkflowProgress,
|
|
68
73
|
type WorkflowRun,
|
|
69
74
|
} from "@alexkroman1/aai-ui";
|
|
70
|
-
import { useState } from "react";
|
|
71
75
|
import type { redline } from "./agent.ts";
|
|
72
76
|
|
|
73
77
|
/**
|
|
@@ -121,19 +125,10 @@ export function toInput(values: FormValues): WorkflowInputOf<typeof redline> {
|
|
|
121
125
|
}
|
|
122
126
|
|
|
123
127
|
function RedlineDesk() {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
// the page can tell "writing what you just briefed" from "picking up where
|
|
129
|
-
// you left off" — the hook reports the run, not who asked for it.
|
|
130
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
131
|
-
const { submit, run, pending, error, reset } = useWorkflowSubmit<typeof redline>(WORKFLOW, {
|
|
132
|
-
// Neither half is useful alone: without the key there is nothing to find
|
|
133
|
-
// the run by, and without `recover` the key is only ever written.
|
|
134
|
-
key,
|
|
135
|
-
recover: true,
|
|
136
|
-
});
|
|
128
|
+
// The reload is covered by the hook's own key — see the module doc for why
|
|
129
|
+
// this desk wants the tab-scoped one it mints rather than a key of its own.
|
|
130
|
+
const { submit, run, pending, error, reset, startedHere } =
|
|
131
|
+
useWorkflowSubmit<typeof redline>(WORKFLOW);
|
|
137
132
|
|
|
138
133
|
return (
|
|
139
134
|
<main className="mx-auto flex max-w-2xl flex-col gap-8 p-8">
|
|
@@ -145,13 +140,7 @@ function RedlineDesk() {
|
|
|
145
140
|
</p>
|
|
146
141
|
</header>
|
|
147
142
|
|
|
148
|
-
<Form
|
|
149
|
-
onSubmit={(values) => {
|
|
150
|
-
setStartedHere(true);
|
|
151
|
-
return submit(toInput(values));
|
|
152
|
-
}}
|
|
153
|
-
error={error}
|
|
154
|
-
>
|
|
143
|
+
<Form onSubmit={(values) => submit(toInput(values))} error={error}>
|
|
155
144
|
{/* The scalars: brief, audience, rounds. Declared, not written. */}
|
|
156
145
|
<WorkflowFields workflow={WORKFLOW} />
|
|
157
146
|
{/* The array the schema declares and no generic control can render. */}
|
|
@@ -180,7 +169,6 @@ function RedlineDesk() {
|
|
|
180
169
|
// The recovered run is dismissed as deliberately as one this load
|
|
181
170
|
// started: `reset()` is not undone by a second lookup (the lookup
|
|
182
171
|
// is a mount-time act), so Clear really does clear.
|
|
183
|
-
setStartedHere(false);
|
|
184
172
|
reset();
|
|
185
173
|
}}
|
|
186
174
|
/>
|
|
@@ -196,15 +184,16 @@ function Rounds({ rounds }: { rounds: Redline["rounds"] }) {
|
|
|
196
184
|
<ol className="flex flex-col gap-3">
|
|
197
185
|
{rounds.map((entry) => (
|
|
198
186
|
<li key={entry.round} className="flex flex-col gap-1 border-l pl-4">
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
187
|
+
<Facts
|
|
188
|
+
size="xs"
|
|
189
|
+
className="uppercase tracking-[1.2px]"
|
|
190
|
+
items={[
|
|
191
|
+
`Round ${entry.round}`,
|
|
192
|
+
`${entry.critique.score}/10`,
|
|
193
|
+
entry.critique.verdict === "ship" ? "ship it" : "revise",
|
|
194
|
+
]}
|
|
195
|
+
/>
|
|
196
|
+
<BulletList items={entry.critique.notes} size="sm" />
|
|
208
197
|
</li>
|
|
209
198
|
))}
|
|
210
199
|
</ol>
|
|
@@ -227,7 +216,7 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
227
216
|
{/* The run's own narration — the complement of the status line, which is
|
|
228
217
|
`running` for a run's whole life, so a one-round redline and a
|
|
229
218
|
three-round one look identical while they happen. These lines come from
|
|
230
|
-
the run itself (`
|
|
219
|
+
the run itself (`stepReport()` in `workflows/redline.ts`), and they REPLAY,
|
|
231
220
|
so a reload mid-run catches up rather than starting from whatever
|
|
232
221
|
arrives next. */}
|
|
233
222
|
<WorkflowProgress runId={run.runId} />
|
|
@@ -236,20 +225,30 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
236
225
|
without a cast. */}
|
|
237
226
|
{run.status === "completed" && (
|
|
238
227
|
<>
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
{
|
|
245
|
-
|
|
228
|
+
{/* Which of the two stop conditions ended the loop is the one thing a
|
|
229
|
+
reader cannot infer from the round count alone, so it is a fact of
|
|
230
|
+
its own rather than something left to the round count. */}
|
|
231
|
+
<Facts
|
|
232
|
+
size="xs"
|
|
233
|
+
items={[
|
|
234
|
+
`${run.output.words} words`,
|
|
235
|
+
`${run.output.roundsRun} ${plural(run.output.roundsRun, "round")}`,
|
|
236
|
+
run.output.shipped ? "the critic stopped it" : "the round budget stopped it",
|
|
237
|
+
]}
|
|
238
|
+
/>
|
|
246
239
|
<Rounds rounds={run.output.rounds} />
|
|
247
240
|
<article className="whitespace-pre-wrap text-sm leading-relaxed">
|
|
248
241
|
{run.output.draft}
|
|
249
242
|
</article>
|
|
250
243
|
</>
|
|
251
244
|
)}
|
|
252
|
-
{
|
|
245
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error: this
|
|
246
|
+
is the outcome the reader waited minutes for. */}
|
|
247
|
+
{run.status === "failed" && (
|
|
248
|
+
<p role="alert" className="text-red-600">
|
|
249
|
+
{run.error}
|
|
250
|
+
</p>
|
|
251
|
+
)}
|
|
253
252
|
</section>
|
|
254
253
|
);
|
|
255
254
|
}
|
|
@@ -266,4 +265,4 @@ function RunPanel({ run, onClear }: { run: WorkflowRun<Redline>; onClear: () =>
|
|
|
266
265
|
*/
|
|
267
266
|
const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
|
|
268
267
|
|
|
269
|
-
|
|
268
|
+
mountPage({ name: "Redline", component: RedlineDesk });
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
* three functions read more tidily than one.
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import type {
|
|
36
|
-
import {
|
|
35
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
36
|
+
import { stepReport } from "@alexkroman1/aai/step";
|
|
37
37
|
import {
|
|
38
38
|
FatalError,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
stepGenerateJsonOrFail,
|
|
40
|
+
stepGenerateOrFail,
|
|
41
41
|
} from "@alexkroman1/aai/step-errors";
|
|
42
42
|
import { countWords } from "@alexkroman1/aai/utils";
|
|
43
43
|
import { z } from "zod";
|
|
@@ -98,7 +98,7 @@ export interface Round {
|
|
|
98
98
|
* the page's render model — and `WorkflowOutputOf<typeof redline>` in
|
|
99
99
|
* `client.tsx` is that type, derived rather than restated.
|
|
100
100
|
*/
|
|
101
|
-
export async function redlineFlow(input: RedlineInput, ctx:
|
|
101
|
+
export async function redlineFlow(input: RedlineInput, ctx: WorkflowContext) {
|
|
102
102
|
// The three `maxAttempts` below were `maxRetries` properties on the functions
|
|
103
103
|
// (3, 5, 3 — retries AFTER the first attempt, so 4, 6, 4 in all). The policy
|
|
104
104
|
// is an argument to the CALL now, which is where it belongs: the same function
|
|
@@ -153,12 +153,12 @@ export async function writeDraft(input: RedlineInput): Promise<string> {
|
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
await
|
|
156
|
+
await stepReport(`Writing the first draft for ${input.audience}.`);
|
|
157
157
|
// No empty-reply guard here or in `reviseDraft`, and that is not an omission:
|
|
158
158
|
// `stepGenerate` already refuses an empty completion, as a RETRYABLE
|
|
159
159
|
// `StepGenerateError` — which is the right answer, and one a hand-written
|
|
160
160
|
// check would have to re-derive.
|
|
161
|
-
const draft = await
|
|
161
|
+
const draft = await stepGenerateOrFail(briefBlock(input), { system: WRITER_SYSTEM });
|
|
162
162
|
return draft.trim();
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -175,14 +175,14 @@ export async function critiqueDraft(
|
|
|
175
175
|
input: RedlineInput,
|
|
176
176
|
round: number,
|
|
177
177
|
): Promise<Critique> {
|
|
178
|
-
await
|
|
178
|
+
await stepReport(`Round ${round}: reading it back critically.`);
|
|
179
179
|
// `stepGenerateJson` owns the fence, the parse, the non-object case and the
|
|
180
180
|
// shape — and throws PLAINLY when any of them misses, unlike the fatal one
|
|
181
181
|
// above: a model that answered with prose may well obey on the next attempt.
|
|
182
|
-
const parsed = await
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
);
|
|
182
|
+
const parsed = await stepGenerateJsonOrFail(`${briefBlock(input)}\n\nThe submission:\n${draft}`, {
|
|
183
|
+
schema: CritiqueReply,
|
|
184
|
+
system: CRITIC_SYSTEM,
|
|
185
|
+
});
|
|
186
186
|
|
|
187
187
|
const critique: Critique = {
|
|
188
188
|
verdict: parsed.verdict,
|
|
@@ -192,7 +192,7 @@ export async function critiqueDraft(
|
|
|
192
192
|
score: clampScore(parsed.score),
|
|
193
193
|
notes: parsed.notes.slice(0, MAX_NOTES),
|
|
194
194
|
};
|
|
195
|
-
await
|
|
195
|
+
await stepReport(
|
|
196
196
|
critique.verdict === "ship"
|
|
197
197
|
? `Round ${round}: the critic would ship it (${critique.score}/10).`
|
|
198
198
|
: `Round ${round}: ${critique.notes.length} note(s) to address (${critique.score}/10).`,
|
|
@@ -207,8 +207,8 @@ export async function reviseDraft(
|
|
|
207
207
|
input: RedlineInput,
|
|
208
208
|
round: number,
|
|
209
209
|
): Promise<string> {
|
|
210
|
-
await
|
|
211
|
-
const revised = await
|
|
210
|
+
await stepReport(`Round ${round}: revising.`);
|
|
211
|
+
const revised = await stepGenerateOrFail(
|
|
212
212
|
[
|
|
213
213
|
briefBlock(input),
|
|
214
214
|
`Your current draft:\n${draft}`,
|
|
@@ -241,8 +241,8 @@ export function clampScore(score: number): number {
|
|
|
241
241
|
// There is no local `ask()` any more, and its absence is the point. The SDK
|
|
242
242
|
// classifies the gateway's failure (`StepGenerateError.retryable`) and stops
|
|
243
243
|
// there — whether a terminal failure should burn the step's remaining attempts
|
|
244
|
-
// is the caller's call — so `
|
|
245
|
-
// `
|
|
244
|
+
// is the caller's call — so `stepGenerateOrFail` and
|
|
245
|
+
// `stepGenerateJsonOrFail` (`@alexkroman1/aai/step-errors`) are that call
|
|
246
246
|
// made one way: terminal stays terminal, and a rate limit becomes a
|
|
247
247
|
// `RetryableError` carrying the delay the gateway itself named, which beats
|
|
248
248
|
// `RetryableError`'s own one-second default. Three templates each wrapped the
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// endpointing, barge-in, whether two sentences merged into one turn.
|
|
30
30
|
|
|
31
31
|
import agentDef from "virtual:aai/agent";
|
|
32
|
-
import { stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
32
|
+
import { routeStepFetch, stubGatewayRoute } from "@alexkroman1/aai/testing";
|
|
33
33
|
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
34
34
|
import { type EvalToolCall, type EvalWorkflows, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
35
35
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
@@ -132,11 +132,11 @@ type ScriptedSteps = {
|
|
|
132
132
|
function scriptSteps(options: { hold?: boolean } = {}): ScriptedSteps {
|
|
133
133
|
const gate = Promise.withResolvers<void>();
|
|
134
134
|
const model = stubGatewayRoute(MODEL_SCRIPT);
|
|
135
|
+
// Throwing on an unrecognised request is `routeStepFetch`'s default and is
|
|
136
|
+
// what this file wants: every step here is a model call.
|
|
137
|
+
const route = routeStepFetch([model.route]);
|
|
135
138
|
const stub = installStubStepFetch(async (request) => {
|
|
136
|
-
const answered =
|
|
137
|
-
if (answered === undefined) {
|
|
138
|
-
throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
|
|
139
|
-
}
|
|
139
|
+
const answered = route(request);
|
|
140
140
|
// `model.calls` has already recorded this one, so a length of 1 IS the first
|
|
141
141
|
// answer — and holding after the route rather than before it keeps the reply
|
|
142
142
|
// this returns the one the script owed that call.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* exported async function, so its prompt handling, its parsing and its
|
|
17
17
|
* `FatalError` guards are all testable without an engine.
|
|
18
18
|
*
|
|
19
|
-
* The BODY is driven here only through `
|
|
19
|
+
* The BODY is driven here only through `createWorkflowContext`, which records what
|
|
20
20
|
* it asked for and replays nothing. That is a choice rather than a limit now:
|
|
21
21
|
* `runWorkflow` from `@alexkroman1/aai-runtime/testing` will run this body on
|
|
22
22
|
* the real engine, and `link-digest` is the template that shows it — three
|
|
@@ -31,13 +31,16 @@ import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
|
31
31
|
import {
|
|
32
32
|
createRunSnapshot,
|
|
33
33
|
createToolContext,
|
|
34
|
-
|
|
34
|
+
createWorkflowContext,
|
|
35
35
|
parseSchemaInput,
|
|
36
36
|
type StubGatewayCall,
|
|
37
37
|
schemaInputIssues,
|
|
38
38
|
toolRunner,
|
|
39
39
|
} from "@alexkroman1/aai/testing";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
installStubWorkflows,
|
|
42
|
+
installStubGateway as stubGateway,
|
|
43
|
+
} from "@alexkroman1/aai/testing/vitest";
|
|
41
44
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
42
45
|
import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
|
|
43
46
|
import { runWorkflow } from "@alexkroman1/aai-runtime/testing";
|
|
@@ -89,7 +92,7 @@ const run = toolRunner(agentDef);
|
|
|
89
92
|
*
|
|
90
93
|
* Returned WITHOUT a cast, which is the property worth keeping: a cast would
|
|
91
94
|
* also stop reporting the day `WorkflowClient` grows a method, and this stub is
|
|
92
|
-
* how the template's tools reach the client at all. `
|
|
95
|
+
* how the template's tools reach the client at all. `installStubWorkflows`
|
|
93
96
|
* (`@alexkroman1/aai/testing/vitest`) is what keeps that affordable — a `vi.fn`
|
|
94
97
|
* per method over one `runs` list, filling in what this desk does not drive, so
|
|
95
98
|
* the day the client does grow a method only the tests using it change.
|
|
@@ -101,12 +104,18 @@ function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
|
|
|
101
104
|
// Name only: `WorkflowSummary.description` is optional, so passing this
|
|
102
105
|
// desk's through would mean handing `description: undefined` to a field that
|
|
103
106
|
// does not accept it. Nothing here reads the description anyway.
|
|
104
|
-
return
|
|
107
|
+
return installStubWorkflows({ runs, names: ["research"] });
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
describe("the agent declares its workflow", () => {
|
|
108
111
|
test("under the name ctx.workflows.start resolves it by", () => {
|
|
109
|
-
|
|
112
|
+
// `toContain` rather than an exact key list: a second workflow is an
|
|
113
|
+
// invited edit and must not redden a test the author did not write. The
|
|
114
|
+
// NAME is still pinned, deliberately — this key is a STRING to everything
|
|
115
|
+
// outside this file (the REST route, `ctx.workflows.get`, a schedule), so
|
|
116
|
+
// renaming it is a runtime 404 rather than a compile error, and nothing
|
|
117
|
+
// else says so.
|
|
118
|
+
expect(Object.keys(agentDef.workflows ?? {})).toContain("research");
|
|
110
119
|
expect(agentDef.workflows?.research).toBe(research);
|
|
111
120
|
});
|
|
112
121
|
|
|
@@ -439,7 +448,7 @@ describe("the steps that research", () => {
|
|
|
439
448
|
// could not have said differently.
|
|
440
449
|
// `planAngles`' result is what the fan-out iterates, so it is supplied
|
|
441
450
|
// rather than run — the rest of the body needs no page and no model.
|
|
442
|
-
const ctx =
|
|
451
|
+
const ctx = createWorkflowContext({
|
|
443
452
|
runSteps: false,
|
|
444
453
|
// Every step the body READS needs a value: with `runSteps: false` nothing
|
|
445
454
|
// runs, so this is the skeleton of a run rather than a run. That is the
|
|
@@ -550,7 +559,7 @@ describe("the steps that research", () => {
|
|
|
550
559
|
/**
|
|
551
560
|
* `researchFlow` itself, on the real replay engine.
|
|
552
561
|
*
|
|
553
|
-
* The block above drives this body through `
|
|
562
|
+
* The block above drives this body through `createWorkflowContext`, which records
|
|
554
563
|
* what it ASKED for and replays nothing — right for the retry policy and the
|
|
555
564
|
* step order, and silent about the desk's actual promise: **answer the caller
|
|
556
565
|
* now, finish the work later**. `runWorkflow`
|