@alexkroman1/aai-cli 6.11.0 → 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 +40 -19
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +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 +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/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 +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.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 +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.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 +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.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/call-audit/workflows/temp-media.ts +0 -138
- 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
|
@@ -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,17 +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
|
-
stepTranscribeSubmit,
|
|
80
|
-
} 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";
|
|
81
78
|
import { createHook, FatalError, sleep } from "workflow";
|
|
82
79
|
import { z } from "zod";
|
|
83
80
|
import { retentionToken } from "./tokens.ts";
|
|
@@ -151,7 +148,7 @@ const POINTS = 3;
|
|
|
151
148
|
/**
|
|
152
149
|
* The shape the model must answer in.
|
|
153
150
|
*
|
|
154
|
-
* `
|
|
151
|
+
* `stepGenerateJsonClassified` validates against this and throws PLAINLY when the reply
|
|
155
152
|
* misses, which is the retry policy in one distinction: a model that answered in
|
|
156
153
|
* prose may answer correctly next time, where a 401 will not. `spoken` is the
|
|
157
154
|
* field this template exists for — without it the announced turn has nothing to
|
|
@@ -257,7 +254,7 @@ export async function recapFlow(input: { url: string; requestedBy: string }) {
|
|
|
257
254
|
* to completion in a worker, where the body is what may suspend. Splitting it
|
|
258
255
|
* out keeps `recapFlow` readable and costs nothing, since the WDK transform
|
|
259
256
|
* rewrites a step's DECLARATION rather than its call sites, so a step called
|
|
260
|
-
* from a helper is still a real step (`
|
|
257
|
+
* from a helper is still a real step (`mapConcurrent` rests on the same
|
|
261
258
|
* property).
|
|
262
259
|
*
|
|
263
260
|
* The loop is deterministic despite looking like it is not: every branch turns
|
|
@@ -359,13 +356,14 @@ export async function submitRecording(url: string): Promise<{ id: string }> {
|
|
|
359
356
|
|
|
360
357
|
await report(`Submitting ${new URL(url).hostname} for transcription…`);
|
|
361
358
|
|
|
362
|
-
// `
|
|
363
|
-
// `speech_models` field and the failure classification
|
|
364
|
-
//
|
|
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
365
|
// surface is large and the SDK deliberately does not mirror it.
|
|
366
|
-
return await
|
|
367
|
-
throwStepError,
|
|
368
|
-
);
|
|
366
|
+
return await stepTranscribeSubmitClassified(url, { params: { speaker_labels: true } });
|
|
369
367
|
}
|
|
370
368
|
|
|
371
369
|
/**
|
|
@@ -467,19 +465,20 @@ export async function summarize(url: string, transcript: TranscriptState): Promi
|
|
|
467
465
|
throw new FatalError("That recording came back with no speech in it.");
|
|
468
466
|
}
|
|
469
467
|
|
|
470
|
-
// `
|
|
471
|
-
// it is told not to, parses it, and validates it — all four
|
|
472
|
-
// used to re-derive. `
|
|
473
|
-
// (a bad key, a rejected request) stop rather than
|
|
474
|
-
// attempts, where a reply that missed the SHAPE throws
|
|
475
|
-
|
|
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, {
|
|
476
475
|
schema: RecapReply,
|
|
477
476
|
system:
|
|
478
477
|
"You write up recordings for someone who will hear the result on a phone call. " +
|
|
479
478
|
`Reply with JSON only: {"headline": string, "points": string[], "spoken": string}. ` +
|
|
480
479
|
`Give exactly ${POINTS} points. "spoken" is ONE sentence, under 30 words, ` +
|
|
481
480
|
"written to be read aloud. No markdown fence, no preamble.",
|
|
482
|
-
})
|
|
481
|
+
});
|
|
483
482
|
|
|
484
483
|
return {
|
|
485
484
|
url,
|
|
@@ -519,27 +518,24 @@ async function request(
|
|
|
519
518
|
url: string,
|
|
520
519
|
init: { method?: string; body?: string } = {},
|
|
521
520
|
): Promise<Response> {
|
|
522
|
-
// `stepFetch`, not `fetch`: it pins HTTP/1.1, so several concurrent
|
|
523
|
-
// this workflow POLLS, so one run is many requests) get a socket
|
|
524
|
-
// than N streams on one connection — and a connection failure
|
|
525
|
-
// `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
|
|
526
525
|
// `TypeError: fetch failed`, which for a template whose whole subject is
|
|
527
526
|
// durability is the difference between a diagnosable resume and a mystery.
|
|
528
527
|
// `sdk/step-fetch.ts` carries the measurements.
|
|
529
|
-
|
|
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, {
|
|
530
535
|
...init,
|
|
531
536
|
headers: { authorization: requireStepEnv(API_KEY_ENV), "content-type": "application/json" },
|
|
532
537
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
533
538
|
});
|
|
534
|
-
// The three-way retry decision, made by the SDK: a 401 or a 400 answers the
|
|
535
|
-
// same way on the fourth attempt and burns the step, a 429 or a 5xx is what
|
|
536
|
-
// retries are for, and a `Retry-After` the provider named is waited out rather
|
|
537
|
-
// than replaced by the DevKit's one-second default — which matters here more
|
|
538
|
-
// than usual, because a fan-out of segments hits a rate limit together.
|
|
539
|
-
if (!response.ok) {
|
|
540
|
-
throw toStepError(response, `${init.method ?? "GET"} ${url} failed: HTTP ${response.status}`);
|
|
541
|
-
}
|
|
542
|
-
return response;
|
|
543
539
|
}
|
|
544
540
|
|
|
545
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;
|
|
@@ -20,18 +20,19 @@
|
|
|
20
20
|
* builds a project and runs one.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import type {
|
|
23
|
+
import type { WorkflowClient } from "@alexkroman1/aai";
|
|
24
24
|
import {
|
|
25
|
-
createProgressStream,
|
|
26
25
|
createRunSnapshot,
|
|
27
|
-
createStubWorkflows,
|
|
28
26
|
createToolContext,
|
|
29
|
-
|
|
27
|
+
parseSchemaInput,
|
|
30
28
|
type StubGatewayCall,
|
|
29
|
+
schemaInputIssues,
|
|
30
|
+
toolRunner,
|
|
31
31
|
withDiscoveredTools,
|
|
32
32
|
} from "@alexkroman1/aai/testing";
|
|
33
|
-
import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
33
|
+
import { mockWorkflows, installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
|
|
34
34
|
import { visitWebpage, webSearch } from "@alexkroman1/aai/tools";
|
|
35
|
+
import type { WorkflowRunSnapshot } from "@alexkroman1/aai/workflow-api";
|
|
35
36
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
36
37
|
import { FatalError, RetryableError } from "workflow";
|
|
37
38
|
import authoredAgent from "./agent.ts";
|
|
@@ -75,37 +76,34 @@ const agentDef = withDiscoveredTools(
|
|
|
75
76
|
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
76
77
|
);
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Every tool here is driven through the agent's own table, by the name the model
|
|
81
|
+
* calls.
|
|
82
|
+
*
|
|
83
|
+
* The second parameter is args-or-context, which is `runTool`'s own shape: three
|
|
84
|
+
* of this desk's four tools take no arguments, and the `{}` those calls were
|
|
85
|
+
* obliged to pass sat between the two values a reader cares about.
|
|
86
|
+
*/
|
|
87
|
+
const run = toolRunner(agentDef);
|
|
81
88
|
|
|
82
89
|
/**
|
|
83
90
|
* A `ctx.workflows` that records `start` and answers `find` from a fixture.
|
|
84
91
|
*
|
|
85
92
|
* Returned WITHOUT a cast, which is the property worth keeping: a cast would
|
|
86
93
|
* also stop reporting the day `WorkflowClient` grows a method, and this stub is
|
|
87
|
-
* how the template's tools reach the client at all. `
|
|
88
|
-
* what keeps that affordable —
|
|
89
|
-
*
|
|
94
|
+
* how the template's tools reach the client at all. `mockWorkflows`
|
|
95
|
+
* (`@alexkroman1/aai/testing/vitest`) is what keeps that affordable — a `vi.fn`
|
|
96
|
+
* per method over one `runs` list, filling in what this desk does not drive, so
|
|
97
|
+
* the day the client does grow a method only the tests using it change.
|
|
98
|
+
* `stream`/`streamTail` are left rejecting on purpose: `research_progress` reads
|
|
99
|
+
* progress through `lastLine`, and composing those two by hand is the hazard
|
|
100
|
+
* `lastLine` exists to remove.
|
|
90
101
|
*/
|
|
91
102
|
function stubWorkflows(runs: WorkflowRunSnapshot[] = []): WorkflowClient {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
recent: vi.fn(async () => runs),
|
|
97
|
-
cancel: vi.fn(async () => true),
|
|
98
|
-
wakeUp: vi.fn(async () => 0),
|
|
99
|
-
// A tail of 0 means "one line written", which is the case the tools read.
|
|
100
|
-
// The `-1` case is overridden per test, because it is the one that decides
|
|
101
|
-
// whether the stream is opened at all.
|
|
102
|
-
streamTail: vi.fn(async () => 0),
|
|
103
|
-
stream: vi.fn(async () => createProgressStream([])),
|
|
104
|
-
// Name only: `WorkflowDef.description` is optional, so passing it through
|
|
105
|
-
// would mean handing `description: undefined` to a field that does not
|
|
106
|
-
// accept it. Nothing here reads the description anyway.
|
|
107
|
-
listing: () => [{ name: "research" }],
|
|
108
|
-
});
|
|
103
|
+
// Name only: `WorkflowSummary.description` is optional, so passing this
|
|
104
|
+
// desk's through would mean handing `description: undefined` to a field that
|
|
105
|
+
// does not accept it. Nothing here reads the description anyway.
|
|
106
|
+
return mockWorkflows({ runs, names: ["research"] });
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
describe("the agent declares its workflow", () => {
|
|
@@ -115,13 +113,16 @@ describe("the agent declares its workflow", () => {
|
|
|
115
113
|
});
|
|
116
114
|
|
|
117
115
|
test("with an input schema, so a bad topic fails at the call site", async () => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
expect(
|
|
116
|
+
// `parseSchemaInput` / `schemaInputIssues` rather than a reach through
|
|
117
|
+
// `["~standard"].validate`: that is the vendor WIRE contract, and whether it
|
|
118
|
+
// answers synchronously or with a promise is the vendor's business — a
|
|
119
|
+
// missing `await` there leaves `.issues` undefined and the refusing half
|
|
120
|
+
// passes for the wrong reason.
|
|
121
|
+
const parsed = await parseSchemaInput(research.input, { topic: "otters", requestedBy: "s" });
|
|
122
|
+
expect(parsed).toMatchObject({ topic: "otters" });
|
|
123
|
+
expect(
|
|
124
|
+
await schemaInputIssues(research.input, { topic: "no", requestedBy: "s" }),
|
|
125
|
+
).toBeDefined();
|
|
125
126
|
});
|
|
126
127
|
});
|
|
127
128
|
|
|
@@ -184,7 +185,7 @@ describe("request_research", () => {
|
|
|
184
185
|
describe("research_status", () => {
|
|
185
186
|
test("says nothing was started when the key has no runs", async () => {
|
|
186
187
|
const ctx = createToolContext({ workflows: stubWorkflows([]) });
|
|
187
|
-
const result = await run("research_status",
|
|
188
|
+
const result = await run("research_status", ctx);
|
|
188
189
|
expect(result).toMatchObject({ runs: [], note: "Nothing started yet." });
|
|
189
190
|
});
|
|
190
191
|
|
|
@@ -197,7 +198,7 @@ describe("research_status", () => {
|
|
|
197
198
|
}),
|
|
198
199
|
];
|
|
199
200
|
const ctx = createToolContext({ workflows: stubWorkflows(runs) });
|
|
200
|
-
const result = (await run("research_status",
|
|
201
|
+
const result = (await run("research_status", ctx)) as { runs: string[] };
|
|
201
202
|
expect(result.runs[0]).toContain("Otters use tools.");
|
|
202
203
|
expect(result.runs[0]).toContain("3 sources");
|
|
203
204
|
});
|
|
@@ -206,7 +207,7 @@ describe("research_status", () => {
|
|
|
206
207
|
const ctx = createToolContext({
|
|
207
208
|
workflows: stubWorkflows([createRunSnapshot({ workflow: "research", status: "running" })]),
|
|
208
209
|
});
|
|
209
|
-
const result = (await run("research_status",
|
|
210
|
+
const result = (await run("research_status", ctx)) as { runs: string[] };
|
|
210
211
|
expect(result.runs[0]).toContain("Still working on it.");
|
|
211
212
|
});
|
|
212
213
|
|
|
@@ -215,14 +216,14 @@ describe("research_status", () => {
|
|
|
215
216
|
createRunSnapshot({ workflow: "research", status: "failed", error: "model unavailable" }),
|
|
216
217
|
];
|
|
217
218
|
const ctx = createToolContext({ workflows: stubWorkflows(runs) });
|
|
218
|
-
const result = (await run("research_status",
|
|
219
|
+
const result = (await run("research_status", ctx)) as { runs: string[] };
|
|
219
220
|
expect(result.runs[0]).toContain("model unavailable");
|
|
220
221
|
});
|
|
221
222
|
|
|
222
223
|
test("bounds how many past runs it reads aloud", async () => {
|
|
223
224
|
const workflows = stubWorkflows([]);
|
|
224
225
|
const ctx = createToolContext({ workflows });
|
|
225
|
-
await run("research_status",
|
|
226
|
+
await run("research_status", ctx);
|
|
226
227
|
// A voice reply cannot be a list of twenty runs.
|
|
227
228
|
expect(workflows.find).toHaveBeenCalledWith(research, ctx.sessionId, { limit: 3 });
|
|
228
229
|
});
|
|
@@ -233,40 +234,40 @@ describe("research_progress", () => {
|
|
|
233
234
|
const workflows = stubWorkflows([
|
|
234
235
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
235
236
|
]);
|
|
236
|
-
vi.mocked(workflows.
|
|
237
|
-
const
|
|
238
|
-
const result = await run("research_progress", {}, ctx);
|
|
237
|
+
vi.mocked(workflows.lastLine).mockResolvedValue("Found 3 sources.");
|
|
238
|
+
const result = await run("research_progress", createToolContext({ workflows }));
|
|
239
239
|
expect(result).toMatchObject({ progress: "Found 3 sources." });
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
test("asks for the LAST line, not the whole log", async () => {
|
|
243
|
-
// A voice reply cannot recite every line the run has written.
|
|
243
|
+
// A voice reply cannot recite every line the run has written. `lastLine` is
|
|
244
|
+
// the whole request — the bound that keeps an empty channel from hanging
|
|
245
|
+
// belongs to the method, so nothing here composes `streamTail` and `stream`.
|
|
244
246
|
const workflows = stubWorkflows([
|
|
245
247
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
246
248
|
]);
|
|
247
|
-
vi.mocked(workflows.
|
|
248
|
-
await run("research_progress",
|
|
249
|
-
expect(workflows.
|
|
249
|
+
vi.mocked(workflows.lastLine).mockResolvedValue("a");
|
|
250
|
+
await run("research_progress", createToolContext({ workflows }));
|
|
251
|
+
expect(workflows.lastLine).toHaveBeenCalledWith("wrun_1");
|
|
250
252
|
});
|
|
251
253
|
|
|
252
|
-
test("a run that has written nothing yet says so
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
254
|
+
test("a run that has written nothing yet says so", async () => {
|
|
255
|
+
// `lastLine` resolves `undefined` for an empty channel, and this is the arm
|
|
256
|
+
// the tool branches on. That an empty channel does not HANG — it is never
|
|
257
|
+
// closed, so a stream opened on one waits for a line that may never come —
|
|
258
|
+
// is `lastLine`'s own guarantee now, and `aai`'s to test.
|
|
256
259
|
const workflows = stubWorkflows([
|
|
257
260
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
258
261
|
]);
|
|
259
|
-
|
|
260
|
-
const result = await run("research_progress", {}, createToolContext({ workflows }));
|
|
262
|
+
const result = await run("research_progress", createToolContext({ workflows }));
|
|
261
263
|
expect(result).toMatchObject({ note: "Started, nothing to report yet." });
|
|
262
|
-
expect(workflows.stream).not.toHaveBeenCalled();
|
|
263
264
|
});
|
|
264
265
|
|
|
265
266
|
test("says nothing was started when the key has no runs", async () => {
|
|
266
267
|
const workflows = stubWorkflows([]);
|
|
267
|
-
const result = await run("research_progress",
|
|
268
|
+
const result = await run("research_progress", createToolContext({ workflows }));
|
|
268
269
|
expect(result).toMatchObject({ note: "Nothing started yet." });
|
|
269
|
-
expect(workflows.
|
|
270
|
+
expect(workflows.lastLine).not.toHaveBeenCalled();
|
|
270
271
|
});
|
|
271
272
|
});
|
|
272
273
|
|
|
@@ -276,7 +277,7 @@ describe("file_it_now", () => {
|
|
|
276
277
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
277
278
|
]);
|
|
278
279
|
vi.mocked(workflows.wakeUp).mockResolvedValue(1);
|
|
279
|
-
const result = await run("file_it_now",
|
|
280
|
+
const result = await run("file_it_now", createToolContext({ workflows }));
|
|
280
281
|
expect(workflows.wakeUp).toHaveBeenCalledWith("wrun_1");
|
|
281
282
|
expect(result).toMatchObject({ filed: true });
|
|
282
283
|
});
|
|
@@ -288,13 +289,13 @@ describe("file_it_now", () => {
|
|
|
288
289
|
createRunSnapshot({ workflow: "research", status: "running" }),
|
|
289
290
|
]);
|
|
290
291
|
vi.mocked(workflows.wakeUp).mockResolvedValue(0);
|
|
291
|
-
const result = await run("file_it_now",
|
|
292
|
+
const result = await run("file_it_now", createToolContext({ workflows }));
|
|
292
293
|
expect(result).toMatchObject({ filed: false });
|
|
293
294
|
});
|
|
294
295
|
|
|
295
296
|
test("says nothing was started when the key has no runs", async () => {
|
|
296
297
|
const workflows = stubWorkflows([]);
|
|
297
|
-
const result = await run("file_it_now",
|
|
298
|
+
const result = await run("file_it_now", createToolContext({ workflows }));
|
|
298
299
|
expect(result).toMatchObject({ note: "Nothing started yet." });
|
|
299
300
|
expect(workflows.wakeUp).not.toHaveBeenCalled();
|
|
300
301
|
});
|
|
@@ -10,17 +10,14 @@ export default tool({
|
|
|
10
10
|
// has WRITTEN (`getWritable()` in `workflows/research.ts`). Between "still
|
|
11
11
|
// working on it" and a finished summary there is otherwise nothing to say.
|
|
12
12
|
//
|
|
13
|
-
// `
|
|
14
|
-
// never closed — no step knows it is the last one — so
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const stream = await ctx.workflows.stream(latest.runId, { startIndex: -1 });
|
|
23
|
-
for await (const line of stream) return { progress: String(line) };
|
|
24
|
-
return { note: "Started, nothing to report yet." };
|
|
13
|
+
// `lastLine` rather than `streamTail` + `stream` composed here: a progress
|
|
14
|
+
// channel is never closed — no step knows it is the last one — so a stream
|
|
15
|
+
// opened on a run that has written nothing waits forever, which down a phone
|
|
16
|
+
// is a turn that stops with no error and nothing in a log. The bound that
|
|
17
|
+
// prevents it belongs to the method now, and `undefined` is "nothing yet".
|
|
18
|
+
const line = await ctx.workflows.lastLine(latest.runId);
|
|
19
|
+
return line === undefined
|
|
20
|
+
? { note: "Started, nothing to report yet." }
|
|
21
|
+
: { progress: String(line) };
|
|
25
22
|
},
|
|
26
23
|
});
|