@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,329 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
// An EVAL: does this desk actually behave? Run it with `aai eval`.
|
|
4
|
+
//
|
|
5
|
+
// `agent.test.ts` drives the four tools against a STUBBED `ctx.workflows` and
|
|
6
|
+
// the five steps against a stubbed `fetch`. Neither can say whether the AGENT —
|
|
7
|
+
// a model, reading this system prompt, holding these four tools — hands the
|
|
8
|
+
// work off at all, or whether the run it starts is findable afterwards. That is
|
|
9
|
+
// what this file is for, and it is only possible because `describeEval` opens a
|
|
10
|
+
// real workflow engine per case and hands its client to the session: before
|
|
11
|
+
// that, a tool calling `ctx.workflows.start` was a tool an eval could not
|
|
12
|
+
// execute.
|
|
13
|
+
//
|
|
14
|
+
// Two boundaries this file is careful about, because a case that implied either
|
|
15
|
+
// would be the worse failure:
|
|
16
|
+
//
|
|
17
|
+
// * **The engine is not durable.** No journal, no replay, no retry, and a
|
|
18
|
+
// `sleep()` is RECORDED rather than taken (which is why the review wait
|
|
19
|
+
// below is an assertion about what the body ASKED for). Nothing here says
|
|
20
|
+
// anything about resume, and `aai-cli`'s `dev-workflow.scenario.test.ts` is
|
|
21
|
+
// the tier that does.
|
|
22
|
+
// * **A step's HTTP is scripted, in BOTH modes.** The live half of an eval is
|
|
23
|
+
// the SESSION's model — which tool the desk reaches for, and when. The
|
|
24
|
+
// run's own five-to-twelve model calls and its web searches are answered
|
|
25
|
+
// from `MODEL_SCRIPT` through the published `stepFetch` slot, so a case is
|
|
26
|
+
// deterministic, free, and cannot fail on a DuckDuckGo 403 or a rate limit
|
|
27
|
+
// the engine's inert `maxRetries` could not ride out. What the researcher's
|
|
28
|
+
// search loop does with what it finds is `agent.test.ts`'s subject.
|
|
29
|
+
//
|
|
30
|
+
// And what no eval here can see at all: anything below the audio boundary —
|
|
31
|
+
// endpointing, barge-in, whether two sentences merged into one turn.
|
|
32
|
+
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
33
|
+
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
34
|
+
import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
|
|
35
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
36
|
+
import { expect } from "vitest";
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
import authoredAgent from "./agent.ts";
|
|
39
|
+
import { research } from "./shared.ts";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
43
|
+
*
|
|
44
|
+
* Load-bearing, and not applied by `agent()` — the BUILD is what enumerates
|
|
45
|
+
* `tools/`, so an eval driving the raw default export would measure a desk with
|
|
46
|
+
* no tools at all and every assertion below would mean nothing. This template
|
|
47
|
+
* declares its prompt inline in `agent.ts`, so there is no `system-prompt.md`
|
|
48
|
+
* to layer on with `withSystemPrompt`.
|
|
49
|
+
*
|
|
50
|
+
* The glob is written here rather than reached for from a shared helper because
|
|
51
|
+
* this file SHIPS — see `agent.test.ts`.
|
|
52
|
+
*/
|
|
53
|
+
const agentDef = withDiscoveredTools(
|
|
54
|
+
authoredAgent,
|
|
55
|
+
import.meta.glob("./tools/*.ts", { eager: true }),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The key the run's steps read with `requireStepEnv`.
|
|
60
|
+
*
|
|
61
|
+
* Passed as the agent env so the eval's workflow engine publishes it: the
|
|
62
|
+
* gateway call below is answered by a fake, but `stepGenerate` asks for the key
|
|
63
|
+
* BEFORE it makes the request, so a run with no key fails on the missing
|
|
64
|
+
* credential rather than reaching the script. The ENVIRONMENT and nothing else —
|
|
65
|
+
* a template may not read a developer's CLI config.
|
|
66
|
+
*/
|
|
67
|
+
const EVAL_ENV = { ASSEMBLYAI_API_KEY: process.env.ASSEMBLYAI_API_KEY ?? "eval-scripted-key" };
|
|
68
|
+
|
|
69
|
+
/** The one angle the scripted planner comes back with. */
|
|
70
|
+
const ANGLE = "What second-hand cargo bikes actually sell for";
|
|
71
|
+
|
|
72
|
+
/** The written report the scripted `writeReport` produces. */
|
|
73
|
+
const REPORT_BODY =
|
|
74
|
+
"## Second-hand cargo bikes in Amsterdam\n\nAsking prices cluster between 1,200 and 2,400 EUR.";
|
|
75
|
+
|
|
76
|
+
/** The two sentences the desk reads down the phone. */
|
|
77
|
+
const SPOKEN_SUMMARY =
|
|
78
|
+
"Second-hand cargo bikes in Amsterdam mostly go for between one and two and a half thousand euros.";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* What the run's model calls are answered with, in the order the body asks.
|
|
82
|
+
*
|
|
83
|
+
* Six calls, and the ORDER is the assertion: brief, angles, the researcher's
|
|
84
|
+
* one action, the gap pass, the report, the spoken summary. A stage that
|
|
85
|
+
* disappeared or moved would hand a later stage an earlier reply, which is
|
|
86
|
+
* exactly what the two content assertions in "writes the report" catch.
|
|
87
|
+
*
|
|
88
|
+
* The researcher's action is `stop` on purpose: `search` and `read` go through
|
|
89
|
+
* `webSearch`/`visitWebpage`, whose fetch is the global one and not the step
|
|
90
|
+
* slot, so scripting them is not available here — a case that let them run
|
|
91
|
+
* would really search the web.
|
|
92
|
+
*/
|
|
93
|
+
const MODEL_SCRIPT: readonly string[] = [
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
brief: "What a used cargo bike costs in Amsterdam, and where they are sold.",
|
|
96
|
+
criteria: ["typical asking prices", "where people buy them"],
|
|
97
|
+
}),
|
|
98
|
+
JSON.stringify({ angles: [ANGLE] }),
|
|
99
|
+
JSON.stringify({ action: "stop", why: "the budget is better spent elsewhere" }),
|
|
100
|
+
JSON.stringify({ angles: [] }),
|
|
101
|
+
REPORT_BODY,
|
|
102
|
+
SPOKEN_SUMMARY,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
/** A scripted step HTTP surface, and the gate that holds its first answer. */
|
|
106
|
+
type ScriptedSteps = {
|
|
107
|
+
/** Every step request, in order — the gateway calls the run really made. */
|
|
108
|
+
readonly calls: readonly { url: string; method: string }[];
|
|
109
|
+
/** Let a held first answer through. Idempotent. */
|
|
110
|
+
readonly release: () => void;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Answer the run's model calls from {@link MODEL_SCRIPT}.
|
|
115
|
+
*
|
|
116
|
+
* Through `stepFetch`'s published slot rather than `vi.stubGlobal("fetch")`,
|
|
117
|
+
* which is what a step really dials — and which leaves the SESSION's own model
|
|
118
|
+
* on the live path, so a live case still measures the agent. Anything that is
|
|
119
|
+
* not the gateway THROWS rather than answering 200: an unexpected request from
|
|
120
|
+
* a step is a finding, and a silent empty body would be read as a model that
|
|
121
|
+
* said nothing.
|
|
122
|
+
*
|
|
123
|
+
* `hold` keeps the FIRST answer pending, which is the only way to observe a run
|
|
124
|
+
* that is still going: a durable `sleep` is skipped here, so an unheld run
|
|
125
|
+
* finishes in milliseconds.
|
|
126
|
+
*/
|
|
127
|
+
function scriptSteps(options: { hold?: boolean } = {}): ScriptedSteps {
|
|
128
|
+
const gate = Promise.withResolvers<void>();
|
|
129
|
+
let served = 0;
|
|
130
|
+
const stub = installStubStepFetch(async (request) => {
|
|
131
|
+
if (!request.url.includes("/chat/completions")) {
|
|
132
|
+
throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
|
|
133
|
+
}
|
|
134
|
+
const at = served++;
|
|
135
|
+
if (options.hold === true && at === 0) await gate.promise;
|
|
136
|
+
const content = MODEL_SCRIPT[Math.min(at, MODEL_SCRIPT.length - 1)];
|
|
137
|
+
return { body: { choices: [{ message: { content } }] } };
|
|
138
|
+
});
|
|
139
|
+
return { calls: stub.calls, release: () => gate.resolve() };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `request_research`'s answer when it really started something. */
|
|
143
|
+
const Started = z.object({
|
|
144
|
+
started: z.literal(true),
|
|
145
|
+
runId: z.string().min(1),
|
|
146
|
+
topic: z.string().min(1),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The run id a `request_research` call reported.
|
|
151
|
+
*
|
|
152
|
+
* Parsed rather than regexed: a tool result reaches the event stream as a
|
|
153
|
+
* serialized string, and a shape that stopped matching should fail HERE naming
|
|
154
|
+
* the field instead of handing the next assertion `undefined`.
|
|
155
|
+
*/
|
|
156
|
+
function startedRunId(calls: readonly EvalToolCall[]): string {
|
|
157
|
+
const call = calls.find((one) => one.name === "request_research");
|
|
158
|
+
if (call?.result === undefined) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`the desk called no request_research: ${calls.map((one) => one.name).join(", ") || "(no tools)"}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return Started.parse(JSON.parse(call.result)).runId;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every tool call in this turn that READS a run, whichever the model picked. */
|
|
167
|
+
function readbacks(calls: readonly EvalToolCall[]): readonly EvalToolCall[] {
|
|
168
|
+
return calls.filter((one) => one.name === "research_status" || one.name === "research_progress");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The one utterance every case that starts work opens with. */
|
|
172
|
+
const ASK = "Please research the second-hand cargo bike market in Amsterdam for me.";
|
|
173
|
+
|
|
174
|
+
/** The scripted session turn that answers {@link ASK}. */
|
|
175
|
+
const START_TURN = [
|
|
176
|
+
{ tool: "request_research", args: { topic: "the second-hand cargo bike market in Amsterdam" } },
|
|
177
|
+
"I've started looking into that — I'll let you know when it lands.",
|
|
178
|
+
] as const;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Let the run finish before the case ends.
|
|
182
|
+
*
|
|
183
|
+
* Not tidiness: the scripted `stepFetch` is unpublished when the test that
|
|
184
|
+
* installed it finishes, so a body still mid-flight would make its next model
|
|
185
|
+
* call against whatever the next case publishes — or against the real gateway.
|
|
186
|
+
*/
|
|
187
|
+
async function drain(workflows: EvalWorkflows | undefined, steps: ScriptedSteps): Promise<void> {
|
|
188
|
+
steps.release();
|
|
189
|
+
for (const run of await (workflows?.runs() ?? [])) {
|
|
190
|
+
await workflows?.settle(run.runId, research);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
describeEval(
|
|
195
|
+
agentDef,
|
|
196
|
+
(test) => {
|
|
197
|
+
test(
|
|
198
|
+
"hands the topic to a run and answers the turn without waiting for it",
|
|
199
|
+
async ({ session, workflows }) => {
|
|
200
|
+
// Held, so the run cannot possibly have finished by the time the desk
|
|
201
|
+
// replies — which is the whole claim of the handoff shape.
|
|
202
|
+
const steps = scriptSteps({ hold: true });
|
|
203
|
+
|
|
204
|
+
const turn = await session.say(ASK);
|
|
205
|
+
|
|
206
|
+
const runId = startedRunId(turn.toolCalls);
|
|
207
|
+
expect(turn.completed).toBe(true);
|
|
208
|
+
// The topic it passed is the caller's, not a paraphrase of the prompt.
|
|
209
|
+
const asked = turn.toolCalls.find((one) => one.name === "request_research");
|
|
210
|
+
expect(String(asked?.args.topic)).toMatch(/cargo bike/i);
|
|
211
|
+
|
|
212
|
+
// The run is REAL: the engine started it, under the name the agent
|
|
213
|
+
// declares, and it is still going now that the turn has ended.
|
|
214
|
+
const runs = await (workflows?.runs() ?? []);
|
|
215
|
+
const started = runs.find((one) => one.runId === runId);
|
|
216
|
+
expect(started?.workflow).toBe("research");
|
|
217
|
+
expect(started?.status).toBe("running");
|
|
218
|
+
// And it really began work — the brief stage narrated before its model
|
|
219
|
+
// call, which is the request this case is holding.
|
|
220
|
+
expect(started?.reported.join("\n")).toMatch(/really asking/);
|
|
221
|
+
|
|
222
|
+
await drain(workflows, steps);
|
|
223
|
+
},
|
|
224
|
+
{ stubReply: [...START_TURN] },
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
test(
|
|
228
|
+
"the run really writes the report, and asks for the review wait",
|
|
229
|
+
async ({ session, workflows }) => {
|
|
230
|
+
const steps = scriptSteps();
|
|
231
|
+
|
|
232
|
+
const turn = await session.say(ASK);
|
|
233
|
+
const runId = startedRunId(turn.toolCalls);
|
|
234
|
+
const run = await workflows?.settle(runId, research);
|
|
235
|
+
|
|
236
|
+
// What a completed run reports as `output` is what the agent reads back
|
|
237
|
+
// and what the announcement is built from, so every field is asserted.
|
|
238
|
+
expect(run?.status).toBe("completed");
|
|
239
|
+
expect(run?.output?.report).toBe(REPORT_BODY);
|
|
240
|
+
expect(run?.output?.summary).toBe(SPOKEN_SUMMARY);
|
|
241
|
+
expect(run?.output?.angles).toEqual([ANGLE]);
|
|
242
|
+
expect(run?.output?.filedAt).toBe("filed");
|
|
243
|
+
|
|
244
|
+
// The five stages, in order, off the run's own narration — which is
|
|
245
|
+
// also what `research_progress` reads back down the phone. A stage that
|
|
246
|
+
// stopped reporting is a caller who is told nothing for minutes.
|
|
247
|
+
const narration = run?.reported.join("\n") ?? "";
|
|
248
|
+
expect(narration).toMatch(/really asking/);
|
|
249
|
+
expect(narration).toMatch(/Researching 1 angle/);
|
|
250
|
+
expect(narration).toMatch(new RegExp(`Looking into: ${ANGLE}`));
|
|
251
|
+
expect(narration).toMatch(/writing it up/);
|
|
252
|
+
expect(narration).toMatch(/Writing up 1 angle/);
|
|
253
|
+
expect(run?.reported.at(-1)).toBe("Filing the findings.");
|
|
254
|
+
|
|
255
|
+
// The review wait, ASKED FOR and not taken: this engine records a
|
|
256
|
+
// durable `sleep` rather than suspending, so what a case can honestly
|
|
257
|
+
// claim is that the body asked — and that is the assertion that fails
|
|
258
|
+
// if the suspension is ever deleted.
|
|
259
|
+
expect(run?.slept).toEqual([{ duration: "30 seconds" }]);
|
|
260
|
+
|
|
261
|
+
// Six model calls, all through the step slot: the fan-out's width came
|
|
262
|
+
// from a journaled stage rather than from anything the body recomputed.
|
|
263
|
+
expect(steps.calls).toHaveLength(MODEL_SCRIPT.length);
|
|
264
|
+
},
|
|
265
|
+
{ stubReply: [...START_TURN] },
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
test(
|
|
269
|
+
"reads the live run back rather than guessing at it",
|
|
270
|
+
async ({ session, workflows }) => {
|
|
271
|
+
const steps = scriptSteps({ hold: true });
|
|
272
|
+
|
|
273
|
+
const started = await session.say(ASK);
|
|
274
|
+
const runId = startedRunId(started.toolCalls);
|
|
275
|
+
const turn = await session.say("What's it doing right now?");
|
|
276
|
+
|
|
277
|
+
// WHICH of the two readback tools the model picks is its business —
|
|
278
|
+
// the prompt offers both — so the claim is about what it was told:
|
|
279
|
+
// either the run's own latest progress line or its status, and never
|
|
280
|
+
// an answer the desk invented.
|
|
281
|
+
const read = readbacks(turn.toolCalls);
|
|
282
|
+
expect(read.length).toBeGreaterThan(0);
|
|
283
|
+
const answered = read.map((one) => one.result ?? "").join("\n");
|
|
284
|
+
expect(answered).toMatch(/really asking|Still working on it/);
|
|
285
|
+
expect(answered).not.toMatch(/Nothing started yet/);
|
|
286
|
+
|
|
287
|
+
// The load-bearing half: that readback happened while the run was
|
|
288
|
+
// genuinely in flight, which is the only state these two tools exist
|
|
289
|
+
// for.
|
|
290
|
+
const runs = await (workflows?.runs() ?? []);
|
|
291
|
+
expect(runs.find((one) => one.runId === runId)?.status).toBe("running");
|
|
292
|
+
|
|
293
|
+
await drain(workflows, steps);
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
stubReply: [
|
|
297
|
+
...START_TURN,
|
|
298
|
+
{ tool: "research_progress", args: {} },
|
|
299
|
+
"It's still working out what the question really is.",
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
test(
|
|
305
|
+
"says nothing is running when nothing is, and starts nothing to find out",
|
|
306
|
+
async ({ session, workflows }) => {
|
|
307
|
+
const steps = scriptSteps();
|
|
308
|
+
|
|
309
|
+
const turn = await session.say("Any news on that research I asked for?");
|
|
310
|
+
|
|
311
|
+
const read = readbacks(turn.toolCalls);
|
|
312
|
+
expect(read.length).toBeGreaterThan(0);
|
|
313
|
+
expect(read.map((one) => one.result ?? "").join("\n")).toMatch(/Nothing started yet/);
|
|
314
|
+
// A question is not a request: asking after work nobody asked for must
|
|
315
|
+
// not put a run — and a research pass's worth of model calls — on the
|
|
316
|
+
// account.
|
|
317
|
+
expect(await (workflows?.runs() ?? [])).toEqual([]);
|
|
318
|
+
expect(steps.calls).toEqual([]);
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
stubReply: [
|
|
322
|
+
{ tool: "research_status", args: {} },
|
|
323
|
+
"Nothing has been started yet — want me to look into something?",
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
},
|
|
328
|
+
{ env: EVAL_ENV },
|
|
329
|
+
);
|
|
@@ -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
|
});
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type WorkflowOutputOf,
|
|
5
|
-
type WorkflowRunSnapshot,
|
|
6
|
-
} from "@alexkroman1/aai";
|
|
1
|
+
import { tool } from "@alexkroman1/aai";
|
|
2
|
+
import { plural } from "@alexkroman1/aai/utils";
|
|
3
|
+
import { isTerminal, type WorkflowRunOf } from "@alexkroman1/aai/workflow-api";
|
|
7
4
|
import { research } from "../shared.ts";
|
|
8
5
|
|
|
9
6
|
/** How many past runs the status tool will look at. Newest first. */
|
|
@@ -12,17 +9,18 @@ const RECENT_RUNS = 3;
|
|
|
12
9
|
/**
|
|
13
10
|
* One line a voice agent can read aloud about a run.
|
|
14
11
|
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* `WorkflowRunOf` is the snapshot with its output already typed — the
|
|
13
|
+
* `WorkflowRunSnapshot<WorkflowOutputOf<typeof research>>` this file used to
|
|
14
|
+
* compose by hand, which cost a three-name import for one type. Still the
|
|
15
|
+
* discriminated union, so `isTerminal` below narrows exactly as it did.
|
|
18
16
|
*/
|
|
19
|
-
function describeRun(run:
|
|
17
|
+
function describeRun(run: WorkflowRunOf<typeof research>): string {
|
|
20
18
|
// `isTerminal` narrows to the three finished statuses, which is what makes
|
|
21
19
|
// `run.output` and `run.error` reachable without a cast.
|
|
22
20
|
if (!isTerminal(run)) return "Still working on it.";
|
|
23
21
|
switch (run.status) {
|
|
24
22
|
case "completed":
|
|
25
|
-
return `Done: ${run.output.summary} (${run.output.sources} sources)`;
|
|
23
|
+
return `Done: ${run.output.summary} (${run.output.sources} ${plural(run.output.sources, "source")})`;
|
|
26
24
|
case "failed":
|
|
27
25
|
return `That one failed: ${run.error}`;
|
|
28
26
|
default:
|