@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,366 @@
|
|
|
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 five tools against a STUBBED `ctx.workflows`, the
|
|
6
|
+
// steps against a stubbed provider, and the body's two helpers with `sleep` and
|
|
7
|
+
// `createHook` mocked. What none of those tiers can say is whether the AGENT —
|
|
8
|
+
// a model, reading this system prompt, holding these five tools — hands a
|
|
9
|
+
// recording off at all, whether the run it starts is the same run a later turn
|
|
10
|
+
// finds, and whether a caller who says "forget it" is told the truth about what
|
|
11
|
+
// cancelling does. That is what this file is for, and it is only possible
|
|
12
|
+
// because `describeEval` opens a real workflow engine per case and hands its
|
|
13
|
+
// client to the session.
|
|
14
|
+
//
|
|
15
|
+
// Three boundaries, each of which a case here would be dishonest to imply:
|
|
16
|
+
//
|
|
17
|
+
// * **The engine is not durable.** No journal, no replay, no retry, and a
|
|
18
|
+
// `sleep()` is RECORDED rather than taken — so the poll loop below runs at
|
|
19
|
+
// full speed and the only way to observe a run in flight is to hold the
|
|
20
|
+
// provider's answer, which is what `hold` does.
|
|
21
|
+
// * **`createHook()` throws untransformed**, so the RETENTION GATE — this
|
|
22
|
+
// template's headline port of Temporal's `expense` — cannot be reached at
|
|
23
|
+
// all here, and neither can `keep_transcript`'s signal: a run that gets past
|
|
24
|
+
// `summarize` fails on the hook. Nothing below asserts the gate, its three
|
|
25
|
+
// outcomes, or its safe default; `agent.test.ts` drives
|
|
26
|
+
// `askWhetherToKeep` directly for those, and it is the only tier that can.
|
|
27
|
+
// * **The provider is scripted, in BOTH modes**, through `stepFetch`'s
|
|
28
|
+
// published slot — so the transcription, the recap's model call and the
|
|
29
|
+
// compensating DELETE are all deterministic and free, while the SESSION's
|
|
30
|
+
// model stays live and is what a live run measures.
|
|
31
|
+
//
|
|
32
|
+
// And what no eval here can see: anything below the audio boundary.
|
|
33
|
+
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
34
|
+
import { installStubStepFetch } from "@alexkroman1/aai/testing/vitest";
|
|
35
|
+
import type { EvalToolCall, EvalWorkflows } from "@alexkroman1/aai-runtime/eval";
|
|
36
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
37
|
+
import { expect } from "vitest";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
import authoredAgent from "./agent.ts";
|
|
40
|
+
import { recap, SAMPLE_RECORDING } from "./shared.ts";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares.
|
|
44
|
+
*
|
|
45
|
+
* Load-bearing, and not applied by `agent()` — the BUILD enumerates `tools/`,
|
|
46
|
+
* so an eval driving the raw default export would measure a desk with none of
|
|
47
|
+
* its five tools. This template declares its prompt inline in `agent.ts`, so
|
|
48
|
+
* there is no `system-prompt.md` for `withSystemPrompt` to layer on.
|
|
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 every step reads with `requireStepEnv` — the one name `agent.ts`
|
|
60
|
+
* declares in `requiredEnv`.
|
|
61
|
+
*
|
|
62
|
+
* Passed as the agent env so the eval's workflow engine publishes it: the
|
|
63
|
+
* provider calls are answered by a fake, but each asks for the key BEFORE it
|
|
64
|
+
* makes its request, so a run with no key fails on the credential rather than
|
|
65
|
+
* reaching the script. The ENVIRONMENT and nothing else — a template may not
|
|
66
|
+
* read a developer's CLI config.
|
|
67
|
+
*/
|
|
68
|
+
const EVAL_ENV = { ASSEMBLYAI_API_KEY: process.env.ASSEMBLYAI_API_KEY ?? "eval-scripted-key" };
|
|
69
|
+
|
|
70
|
+
/** The job id the scripted provider mints for every submission. */
|
|
71
|
+
const TRANSCRIPT_ID = "t_eval_1";
|
|
72
|
+
|
|
73
|
+
/** What the scripted transcript says, once it completes. */
|
|
74
|
+
const TRANSCRIPT_TEXT =
|
|
75
|
+
"Smoke from the Canadian wildfires reached the eastern seaboard overnight, " +
|
|
76
|
+
"and air quality indexes went into the unhealthy range.";
|
|
77
|
+
|
|
78
|
+
/** The recap the scripted model writes. `spoken` is the field the phone reads. */
|
|
79
|
+
const RECAP_JSON = JSON.stringify({
|
|
80
|
+
headline: "Wildfire smoke reaches the east coast",
|
|
81
|
+
points: ["Smoke crossed overnight", "Air quality is unhealthy", "Advisories are in force"],
|
|
82
|
+
spoken: "Wildfire smoke drifted east overnight and pushed air quality into the unhealthy range.",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** How the scripted provider ends a job. */
|
|
86
|
+
type Ending = "completed" | "error";
|
|
87
|
+
|
|
88
|
+
/** A scripted provider, and the gate that holds its first status answer. */
|
|
89
|
+
type ScriptedProvider = {
|
|
90
|
+
/** Every step request, in order — what the run really asked the provider. */
|
|
91
|
+
readonly calls: readonly { url: string; method: string; body?: unknown }[];
|
|
92
|
+
/** Let a held first poll answer. Idempotent. */
|
|
93
|
+
readonly release: () => void;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Answer AssemblyAI's batch API and the LLM gateway in memory.
|
|
98
|
+
*
|
|
99
|
+
* Through `stepFetch`'s published slot rather than `vi.stubGlobal("fetch")`,
|
|
100
|
+
* which is the path a step really takes — and which leaves the SESSION's model
|
|
101
|
+
* live, so a live case still measures the agent. An unrecognised request THROWS
|
|
102
|
+
* rather than answering an empty 200: a step calling something nobody expected
|
|
103
|
+
* is a finding, where an empty body reads as a provider that said nothing.
|
|
104
|
+
*
|
|
105
|
+
* `hold` keeps the FIRST poll pending, and it is the only way to observe a run
|
|
106
|
+
* that is still going: a durable `sleep` is skipped here, so an unheld run
|
|
107
|
+
* burns its whole poll loop in milliseconds.
|
|
108
|
+
*/
|
|
109
|
+
function stubProvider(options: { hold?: boolean; ending?: Ending } = {}): ScriptedProvider {
|
|
110
|
+
const gate = Promise.withResolvers<void>();
|
|
111
|
+
let polls = 0;
|
|
112
|
+
const stub = installStubStepFetch(async (request) => {
|
|
113
|
+
if (request.url.includes("/chat/completions")) {
|
|
114
|
+
return { body: { choices: [{ message: { content: RECAP_JSON } }] } };
|
|
115
|
+
}
|
|
116
|
+
if (request.method === "POST") return { body: { id: TRANSCRIPT_ID, status: "queued" } };
|
|
117
|
+
// The compensation. A real DELETE removes the transcript from the account,
|
|
118
|
+
// which is what makes "a failed run leaves nothing behind" a claim rather
|
|
119
|
+
// than a comment — so the assertion that matters is that this was CALLED.
|
|
120
|
+
if (request.method === "DELETE") return { body: {} };
|
|
121
|
+
if (request.method === "GET") {
|
|
122
|
+
polls += 1;
|
|
123
|
+
if (options.hold === true && polls === 1) await gate.promise;
|
|
124
|
+
return options.ending === "error"
|
|
125
|
+
? { body: { status: "error", error: "that recording could not be decoded" } }
|
|
126
|
+
: { body: { status: "completed", text: TRANSCRIPT_TEXT, audio_duration: 254 } };
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`unexpected step request in an eval: ${request.method} ${request.url}`);
|
|
129
|
+
});
|
|
130
|
+
return { calls: stub.calls, release: () => gate.resolve() };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `request_recap`'s two answers — it started one, or it found the live one. */
|
|
134
|
+
const RecapStart = z.union([
|
|
135
|
+
z.object({ started: z.literal(true), runId: z.string().min(1) }),
|
|
136
|
+
z.object({ started: z.literal(false), runId: z.string().min(1), note: z.string() }),
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
/** `cancel_recap`'s answer. */
|
|
140
|
+
const Cancelled = z.object({ cancelled: z.boolean(), note: z.string() });
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Every `request_recap` answer in a turn, parsed.
|
|
144
|
+
*
|
|
145
|
+
* Parsed rather than regexed: a tool result reaches the event stream as a
|
|
146
|
+
* serialized string, and a shape that stopped matching should fail HERE naming
|
|
147
|
+
* the field instead of handing the next assertion `undefined`.
|
|
148
|
+
*/
|
|
149
|
+
function recapStarts(calls: readonly EvalToolCall[]): z.infer<typeof RecapStart>[] {
|
|
150
|
+
return calls
|
|
151
|
+
.filter((one) => one.name === "request_recap" && one.result !== undefined)
|
|
152
|
+
.map((one) => RecapStart.parse(JSON.parse(String(one.result))));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The run id the first `request_recap` of this turn reported. */
|
|
156
|
+
function startedRunId(calls: readonly EvalToolCall[]): string {
|
|
157
|
+
const [first] = recapStarts(calls);
|
|
158
|
+
if (first === undefined) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`the desk called no request_recap: ${calls.map((one) => one.name).join(", ") || "(no tools)"}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return first.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 === "recap_status" || one.name === "recap_progress");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Requests of one method the run has made so far. */
|
|
172
|
+
function requests(provider: ScriptedProvider, method: string) {
|
|
173
|
+
return provider.calls.filter((one) => one.method === method);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** `text` as a regex that matches only itself. */
|
|
177
|
+
function literal(text: string): string {
|
|
178
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The one utterance every case that starts work opens with. */
|
|
182
|
+
const ASK = "Can you write up that recording for me?";
|
|
183
|
+
|
|
184
|
+
/** The scripted session turn that answers {@link ASK}. */
|
|
185
|
+
const START_TURN = [
|
|
186
|
+
{ tool: "request_recap", args: {} },
|
|
187
|
+
"I've started writing that up — I'll let you know when it lands.",
|
|
188
|
+
] as const;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Let the run finish before the case ends.
|
|
192
|
+
*
|
|
193
|
+
* Not tidiness: the scripted provider is unpublished when the test that
|
|
194
|
+
* installed it finishes, so a body still mid-flight would make its next request
|
|
195
|
+
* against whatever the next case publishes — or against the real provider, with
|
|
196
|
+
* a real key. Every run here ends FAILED, on `createHook()`; see the header.
|
|
197
|
+
*/
|
|
198
|
+
async function drain(workflows: EvalWorkflows | undefined, provider: ScriptedProvider) {
|
|
199
|
+
provider.release();
|
|
200
|
+
for (const run of await (workflows?.runs() ?? [])) await workflows?.settle(run.runId, recap);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
describeEval(
|
|
204
|
+
agentDef,
|
|
205
|
+
(test) => {
|
|
206
|
+
test(
|
|
207
|
+
"starts one run for the caller and will not pay for a second",
|
|
208
|
+
async ({ session, workflows }) => {
|
|
209
|
+
// Held, so the first run is unambiguously still live when the caller
|
|
210
|
+
// asks again — which is the state the live-run check exists for.
|
|
211
|
+
const provider = stubProvider({ hold: true });
|
|
212
|
+
|
|
213
|
+
const first = await session.say(ASK);
|
|
214
|
+
const runId = startedRunId(first.toolCalls);
|
|
215
|
+
const again = await session.say("Actually, start it again from scratch, please.");
|
|
216
|
+
|
|
217
|
+
// Temporal's workflow-id reuse policy, as this desk spells it: a caller
|
|
218
|
+
// who asks twice is told about the run they already have. WHETHER the
|
|
219
|
+
// model calls the tool a second time is its business — it may simply
|
|
220
|
+
// remember — so each call it did make has to have been refused with the
|
|
221
|
+
// run it found.
|
|
222
|
+
for (const answer of recapStarts(again.toolCalls)) {
|
|
223
|
+
expect(answer.started).toBe(false);
|
|
224
|
+
expect(answer.runId).toBe(runId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// The half that is not vacuous either way, and the one the caller pays
|
|
228
|
+
// for: ONE run, and ONE submission to the provider.
|
|
229
|
+
const runs = await (workflows?.runs() ?? []);
|
|
230
|
+
expect(runs.map((one) => one.runId)).toEqual([runId]);
|
|
231
|
+
expect(runs[0]?.workflow).toBe("recap");
|
|
232
|
+
const posts = requests(provider, "POST");
|
|
233
|
+
expect(posts).toHaveLength(1);
|
|
234
|
+
// And it submitted the recording the DESK supplies, because a phone
|
|
235
|
+
// caller cannot read a URL aloud.
|
|
236
|
+
expect(JSON.parse(String(posts[0]?.body))).toMatchObject({
|
|
237
|
+
audio_url: SAMPLE_RECORDING,
|
|
238
|
+
speaker_labels: true,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await drain(workflows, provider);
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
stubReply: [
|
|
245
|
+
...START_TURN,
|
|
246
|
+
{ tool: "request_recap", args: {} },
|
|
247
|
+
"There's already one running for you — I'll tell you as soon as it lands.",
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
test(
|
|
253
|
+
"reads the live run back rather than guessing at it",
|
|
254
|
+
async ({ session, workflows }) => {
|
|
255
|
+
const provider = stubProvider({ hold: true });
|
|
256
|
+
|
|
257
|
+
const started = await session.say(ASK);
|
|
258
|
+
const runId = startedRunId(started.toolCalls);
|
|
259
|
+
const turn = await session.say("How's that going?");
|
|
260
|
+
|
|
261
|
+
const read = readbacks(turn.toolCalls);
|
|
262
|
+
expect(read.length).toBeGreaterThan(0);
|
|
263
|
+
const answered = read.map((one) => one.result ?? "").join("\n");
|
|
264
|
+
|
|
265
|
+
// The load-bearing half: that readback happened while the run really
|
|
266
|
+
// was in flight, waiting on the provider — which is the only state
|
|
267
|
+
// these two tools exist for.
|
|
268
|
+
const runs = await (workflows?.runs() ?? []);
|
|
269
|
+
const live = runs.find((one) => one.runId === runId);
|
|
270
|
+
expect(live?.status).toBe("running");
|
|
271
|
+
expect(requests(provider, "GET")).toHaveLength(1);
|
|
272
|
+
|
|
273
|
+
// WHICH readback tool the model picks is its business — the prompt
|
|
274
|
+
// offers both — so the claim is about what it was TOLD, and each has its
|
|
275
|
+
// own shape: `recap_progress` hands back the run's own latest line and
|
|
276
|
+
// `recap_status` the snapshot's status. Compared against what the RUN
|
|
277
|
+
// really wrote rather than against a literal, because which line that is
|
|
278
|
+
// depends on how far the body got: with the durable `sleep` skipped here
|
|
279
|
+
// the `PATIENCE` race resolves at once, so the note the caller would
|
|
280
|
+
// hear two minutes in is already written.
|
|
281
|
+
const lastLine = String(live?.reported.at(-1));
|
|
282
|
+
expect(live?.reported.length).toBeGreaterThan(0);
|
|
283
|
+
expect(answered).toMatch(new RegExp(`${literal(lastLine)}|Still working on that one`));
|
|
284
|
+
expect(answered).not.toMatch(/Nothing started yet/);
|
|
285
|
+
|
|
286
|
+
await drain(workflows, provider);
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
stubReply: [
|
|
290
|
+
...START_TURN,
|
|
291
|
+
{ tool: "recap_progress", args: {} },
|
|
292
|
+
"It's with the transcription service now — nothing back yet.",
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
test(
|
|
298
|
+
"cancelling says plainly what it did NOT roll back, and really does not",
|
|
299
|
+
async ({ session, workflows }) => {
|
|
300
|
+
const provider = stubProvider({ hold: true });
|
|
301
|
+
|
|
302
|
+
const started = await session.say(ASK);
|
|
303
|
+
const runId = startedRunId(started.toolCalls);
|
|
304
|
+
const turn = await session.say("Forget it — cancel that, please.");
|
|
305
|
+
|
|
306
|
+
const cancel = turn.toolCalls.find((one) => one.name === "cancel_recap");
|
|
307
|
+
const answer = Cancelled.parse(JSON.parse(String(cancel?.result)));
|
|
308
|
+
expect(answer.cancelled).toBe(true);
|
|
309
|
+
// The sentence is a documented promise of this template, not a
|
|
310
|
+
// decoration: cancellation is NOT cooperative here, so the transcript
|
|
311
|
+
// the run had already created stays on the account and the caller is
|
|
312
|
+
// told so rather than left to assume a rollback.
|
|
313
|
+
expect(answer.note).toMatch(/left behind/);
|
|
314
|
+
expect(answer.note).toMatch(/does not roll back/);
|
|
315
|
+
|
|
316
|
+
// And it is TRUE, which is the part only an eval with a real run can
|
|
317
|
+
// check: the run is cancelled, and no compensating DELETE went out.
|
|
318
|
+
const runs = await (workflows?.runs() ?? []);
|
|
319
|
+
expect(runs.find((one) => one.runId === runId)?.status).toBe("cancelled");
|
|
320
|
+
expect(requests(provider, "DELETE")).toEqual([]);
|
|
321
|
+
|
|
322
|
+
// Released after the assertions on purpose: the body runs on regardless
|
|
323
|
+
// (there is no queue here to stop delivering to, and Temporal's
|
|
324
|
+
// deliver-cancellation-into-the-workflow is the one thing this template
|
|
325
|
+
// says does not port), so anything it does afterwards is not what the
|
|
326
|
+
// caller was told about.
|
|
327
|
+
await drain(workflows, provider);
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
stubReply: [
|
|
331
|
+
...START_TURN,
|
|
332
|
+
{ tool: "cancel_recap", args: {} },
|
|
333
|
+
"Stopped it. The partial transcript stays on file — cancelling doesn't undo that.",
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
test("a run that fails after creating a transcript deletes it again", async ({ workflows }) => {
|
|
339
|
+
// Started from the CASE rather than through a tool, because the subject
|
|
340
|
+
// is the saga and the failure has to be injected: the provider refuses
|
|
341
|
+
// the job, which is the branch that unwinds the compensation stack.
|
|
342
|
+
// `request_recap` is what the other three cases drive.
|
|
343
|
+
const provider = stubProvider({ ending: "error" });
|
|
344
|
+
|
|
345
|
+
const run = await workflows?.run(recap, {
|
|
346
|
+
url: SAMPLE_RECORDING,
|
|
347
|
+
requestedBy: "eval-session",
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(run?.status).toBe("failed");
|
|
351
|
+
expect(run?.error).toMatch(/could not transcribe/);
|
|
352
|
+
// The unwind, off the run's own narration — one compensation, named.
|
|
353
|
+
const narration = run?.reported.join("\n") ?? "";
|
|
354
|
+
expect(narration).toMatch(/undoing 1 step/);
|
|
355
|
+
expect(narration).toMatch(`Discarding transcript ${TRANSCRIPT_ID}.`);
|
|
356
|
+
// And it really happened: the transcript this run created was deleted
|
|
357
|
+
// from the account, which is the promise "a failed recap leaves nothing
|
|
358
|
+
// behind" rests on. An undo registered BEFORE its step, or a `catch`
|
|
359
|
+
// that stopped compensating, fails here.
|
|
360
|
+
expect(requests(provider, "DELETE").map((one) => one.url)).toEqual([
|
|
361
|
+
`https://api.assemblyai.com/v2/transcript/${TRANSCRIPT_ID}`,
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
{ env: EVAL_ENV },
|
|
366
|
+
);
|