@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,165 @@
|
|
|
1
|
+
// An EVAL: does Coda actually answer by RUNNING CODE? Run it with `aai eval`.
|
|
2
|
+
//
|
|
3
|
+
// `agent.test.ts` asserts about the config; this drives the real thing — the
|
|
4
|
+
// real session, the real tool executor, the real event stream, with only the
|
|
5
|
+
// microphone and the speaker faked.
|
|
6
|
+
//
|
|
7
|
+
// Two things about this file are worth copying into any template eval.
|
|
8
|
+
//
|
|
9
|
+
// **The prompt is applied here, because `system-prompt.md` IS the prompt and
|
|
10
|
+
// nothing in `agent.ts` imports it.** Discovery happens where the bundle is
|
|
11
|
+
// assembled (`aai build` reads the file; a spec does the same lowering), so an
|
|
12
|
+
// eval that drove `agent.ts` alone would measure an agent running on
|
|
13
|
+
// `DEFAULT_SYSTEM_PROMPT` — i.e. not the agent anybody deploys, and every claim
|
|
14
|
+
// this file makes about Coda's rules would be a claim about nothing.
|
|
15
|
+
//
|
|
16
|
+
// **`run_code` refuses unless the EVAL supplies an executor.** A deployed agent
|
|
17
|
+
// runs it only inside the guest sandbox — the Modal container is the security
|
|
18
|
+
// boundary — so off-platform the builtin declines rather than evaluating
|
|
19
|
+
// model-written JavaScript in the host process. That is right, and it left this
|
|
20
|
+
// template's whole subject assertable as a CALL and never as an answer: a
|
|
21
|
+
// `toBeDefined()` on the result is satisfied by the refusal itself. So the suite
|
|
22
|
+
// passes `runCode` (below) — a developer's own machine may, a deployment may
|
|
23
|
+
// not — and every case here asserts BOTH halves: that Coda reached for code, and
|
|
24
|
+
// what the code came back with.
|
|
25
|
+
import { runInNewContext } from "node:vm";
|
|
26
|
+
import { withSystemPrompt } from "@alexkroman1/aai/manifest";
|
|
27
|
+
import { errorMessage } from "@alexkroman1/aai/utils";
|
|
28
|
+
import type { RunCodeExecutor } from "@alexkroman1/aai-runtime/eval";
|
|
29
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
30
|
+
import { expect } from "vitest";
|
|
31
|
+
import authored from "./agent.ts";
|
|
32
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
33
|
+
|
|
34
|
+
const agentDef = withSystemPrompt(authored, systemPrompt);
|
|
35
|
+
|
|
36
|
+
/** The code every `run_code` call in this turn carried, joined. */
|
|
37
|
+
const codeIn = (turn: { toolCalls: readonly { name: string; args: Record<string, unknown> }[] }) =>
|
|
38
|
+
turn.toolCalls
|
|
39
|
+
.filter((c) => c.name === "run_code")
|
|
40
|
+
.map((c) => String(c.args.code ?? ""))
|
|
41
|
+
.join("\n");
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A `run_code` executor, so these cases can assert the ANSWER.
|
|
45
|
+
*
|
|
46
|
+
* The builtin refuses without one — the Modal container is the security
|
|
47
|
+
* boundary, and off-platform there is none — so a case could assert the CALL and
|
|
48
|
+
* the code it carried, and never what the code came back with. A `node:vm`
|
|
49
|
+
* context with a capturing `console.log` is what a developer would reach for on
|
|
50
|
+
* their own machine, and it is enough here: what runs is arithmetic, not a
|
|
51
|
+
* program. It is NOT a sandbox and does not pretend to be one; a deployed agent
|
|
52
|
+
* still gets the refusal.
|
|
53
|
+
*
|
|
54
|
+
* A template eval imports from `@alexkroman1/aai-runtime/eval` and
|
|
55
|
+
* `/eval/vitest` and nowhere else — the ROOT barrel drags the host runtime's
|
|
56
|
+
* node-reaching module graph into this project's TypeScript program, which is
|
|
57
|
+
* three errors in runtime files no eval ever calls. `RunCodeExecutor` is
|
|
58
|
+
* re-exported from `/eval` for exactly that reason.
|
|
59
|
+
*/
|
|
60
|
+
const runCode: RunCodeExecutor = async (code) => {
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
const log = (...args: unknown[]): void => {
|
|
63
|
+
lines.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
runInNewContext(code, { console: { log } }, { timeout: 1000 });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { error: errorMessage(err) };
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** What every `run_code` call in this turn PRINTED, joined. */
|
|
74
|
+
const outputIn = (turn: { toolCalls: readonly { name: string; result?: string }[] }) =>
|
|
75
|
+
turn.toolCalls
|
|
76
|
+
.filter((c) => c.name === "run_code")
|
|
77
|
+
.map((c) => c.result ?? "")
|
|
78
|
+
.join("\n");
|
|
79
|
+
|
|
80
|
+
describeEval(
|
|
81
|
+
agentDef,
|
|
82
|
+
(test) => {
|
|
83
|
+
test(
|
|
84
|
+
"writes code for arithmetic instead of answering from its head",
|
|
85
|
+
async ({ session }) => {
|
|
86
|
+
const turn = await session.say("What is 127 times 849?");
|
|
87
|
+
|
|
88
|
+
// The template's CRITICAL RULE, and the whole reason it declares
|
|
89
|
+
// run_code: a model that answers this one directly has regressed, and it
|
|
90
|
+
// is the easiest question in the file to answer wrongly with confidence.
|
|
91
|
+
expect(turn.toolCalls.map((c) => c.name)).toContain("run_code");
|
|
92
|
+
const code = codeIn(turn);
|
|
93
|
+
expect(code).toContain("127");
|
|
94
|
+
expect(code).toContain("849");
|
|
95
|
+
|
|
96
|
+
// And the code RAN, and got it right. That half needed an executor: with
|
|
97
|
+
// none, `run_code` answers "only available in the sandboxed runtime", so
|
|
98
|
+
// every claim above passes for an agent that then does the sum in its
|
|
99
|
+
// head — which is the exact regression the CRITICAL RULE exists to stop.
|
|
100
|
+
const output = outputIn(turn);
|
|
101
|
+
expect(output, `run_code printed: ${output}`).toContain("107823");
|
|
102
|
+
|
|
103
|
+
// "Report RESULTS, never intentions": the call goes out before Coda says
|
|
104
|
+
// anything, rather than after a sentence announcing it.
|
|
105
|
+
const firstTool = turn.events.findIndex((e) => e.type === "tool.called");
|
|
106
|
+
const firstSaid = turn.events.findIndex((e) => e.type === "agent-transcript.committed");
|
|
107
|
+
expect(firstSaid).toBeGreaterThan(-1);
|
|
108
|
+
expect(firstTool).toBeGreaterThan(-1);
|
|
109
|
+
expect(firstTool).toBeLessThan(firstSaid);
|
|
110
|
+
},
|
|
111
|
+
{ live: true },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
test(
|
|
115
|
+
"reaches for code for a calendar question too, not just sums",
|
|
116
|
+
async ({ session }) => {
|
|
117
|
+
const turn = await session.say("What day of the week was January 1st, 2000?");
|
|
118
|
+
|
|
119
|
+
// The prompt lists this exact question under "you MUST use code for".
|
|
120
|
+
// It is the case a narrower reading of the rule ("code is for maths")
|
|
121
|
+
// silently drops.
|
|
122
|
+
expect(turn.toolCalls.map((c) => c.name)).toContain("run_code");
|
|
123
|
+
expect(codeIn(turn)).toMatch(/Date|2000/);
|
|
124
|
+
// The code RAN rather than being refused — but the ANSWER is
|
|
125
|
+
// deliberately not asserted here, and the reason is worth knowing before
|
|
126
|
+
// adding it back. Coda writes `new Date("2000-01-01").getDay()`, which
|
|
127
|
+
// parses as UTC midnight and is then read in LOCAL time: correct in the
|
|
128
|
+
// guest sandbox (UTC, Saturday) and one day out on any developer west of
|
|
129
|
+
// Greenwich (measured: this printed "Friday"). Asserting the weekday
|
|
130
|
+
// would measure the machine running the eval. The two arithmetic cases
|
|
131
|
+
// in this suite have no such dependency and do assert their answers.
|
|
132
|
+
const output = outputIn(turn);
|
|
133
|
+
expect(output, `run_code printed: ${output}`).not.toMatch(
|
|
134
|
+
/only available in the sandboxed runtime/,
|
|
135
|
+
);
|
|
136
|
+
expect(output).not.toBe("");
|
|
137
|
+
},
|
|
138
|
+
{ live: true },
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
test(
|
|
142
|
+
"the run_code builtin is wired to the agent's tool executor",
|
|
143
|
+
async ({ session }) => {
|
|
144
|
+
const turn = await session.say("Add one and one for me.");
|
|
145
|
+
|
|
146
|
+
// The wiring claim, and it really discriminates: a tool the agent does
|
|
147
|
+
// NOT declare produces a `tool.called` with no result at all, so the
|
|
148
|
+
// paired result is what says `builtinTools: ["run_code"]` still resolves
|
|
149
|
+
// through to an executable tool.
|
|
150
|
+
//
|
|
151
|
+
// `toBeDefined()` used to be the whole assertion, and it was satisfied by
|
|
152
|
+
// the REFUSAL — "run_code is only available in the sandboxed runtime" is a
|
|
153
|
+
// defined result. Asserting the printed answer is what makes this a claim
|
|
154
|
+
// about the executor rather than about the string it declined with.
|
|
155
|
+
const [call] = turn.toolCalls;
|
|
156
|
+
expect(call?.name).toBe("run_code");
|
|
157
|
+
expect(call?.result).toBe("2");
|
|
158
|
+
expect(turn.completed).toBe(true);
|
|
159
|
+
},
|
|
160
|
+
{ stubReply: [{ tool: "run_code", args: { code: "console.log(1 + 1)" } }, "That's two."] },
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
// `runCode` is what makes these cases about the ANSWER and not just the call.
|
|
164
|
+
{ runCode },
|
|
165
|
+
);
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { withSystemPrompt } from "@alexkroman1/aai/manifest";
|
|
4
|
+
import type { SessionEvent } from "@alexkroman1/aai/protocol";
|
|
5
|
+
import { withDiscoveredTools } from "@alexkroman1/aai/testing";
|
|
6
|
+
// An EVAL: does the desk actually behave? Run it with `aai eval`.
|
|
7
|
+
//
|
|
8
|
+
// `agent.test.ts` drives each tool directly. What it cannot ask is whether the
|
|
9
|
+
// AGENT — a model, reading this system prompt, holding these twelve tools —
|
|
10
|
+
// works the call in the right ORDER, which for a dispatch desk is the whole
|
|
11
|
+
// product. Four claims, each one a MECHANISM this template is built out of:
|
|
12
|
+
//
|
|
13
|
+
// 1. nothing can be dispatched before a call has been logged,
|
|
14
|
+
// 2. a 911 call is logged, scored, and the flow moves to triaging,
|
|
15
|
+
// 3. units really roll, and the position follows them to monitoring,
|
|
16
|
+
// 4. a unit already on a call is not sent to a second one.
|
|
17
|
+
//
|
|
18
|
+
// So every assertion reads the mechanism's own output — the dialog gate's
|
|
19
|
+
// refusal, the tool result, and the dashboard the browser is sent — rather
|
|
20
|
+
// than judging the sentence the model chose to say.
|
|
21
|
+
//
|
|
22
|
+
// What no eval here can see: anything below the audio boundary. Whether a
|
|
23
|
+
// dispatcher reading a callsign in bursts lands as one turn is a property of
|
|
24
|
+
// endpointing, and these fake speech stages remove it.
|
|
25
|
+
import { type EvalSession, type EvalTurn, lastStateIn } from "@alexkroman1/aai-runtime/eval";
|
|
26
|
+
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
27
|
+
import { expect } from "vitest";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import authoredAgent from "./agent.ts";
|
|
30
|
+
import systemPrompt from "./system-prompt.md?raw";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The def a DEPLOYED agent runs: authored, plus what `tools/` declares, plus
|
|
34
|
+
* what `system-prompt.md` says.
|
|
35
|
+
*
|
|
36
|
+
* BOTH wrappers are load-bearing and neither is applied by `agent()` — they are
|
|
37
|
+
* applied by the BUILD (`aai build` enumerates `tools/` and resolves the prompt
|
|
38
|
+
* file), so an eval driving the raw default export would measure a twelve-tool
|
|
39
|
+
* desk with no tools and the FRAMEWORK DEFAULT prompt. For this template that
|
|
40
|
+
* is the whole subject: "location is always the first priority", the radio
|
|
41
|
+
* style and "never leave a critical incident without a resource" are all in
|
|
42
|
+
* that file.
|
|
43
|
+
*
|
|
44
|
+
* The glob is written here rather than reached for from a shared helper because
|
|
45
|
+
* this file SHIPS — it is what a scaffolded project runs.
|
|
46
|
+
*/
|
|
47
|
+
const dispatchAgent = withSystemPrompt(
|
|
48
|
+
withDiscoveredTools(authoredAgent, import.meta.glob("./tools/*.ts", { eager: true })),
|
|
49
|
+
systemPrompt,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** The six tools gated on `working` — the ones that refuse until something has
|
|
53
|
+
* been logged. Listed here so ADDING an ungated mutating tool is a deliberate
|
|
54
|
+
* edit to this file rather than a silent gap. */
|
|
55
|
+
const GATED_TOOLS = new Set([
|
|
56
|
+
"incident_triage",
|
|
57
|
+
"incident_add_note",
|
|
58
|
+
"incident_escalate",
|
|
59
|
+
"incident_update_status",
|
|
60
|
+
"resources_dispatch",
|
|
61
|
+
"resources_update_status",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** The first incident of a session. `createIncident` counts from 1 per slot,
|
|
65
|
+
* and a slot is per session, so this id is deterministic. */
|
|
66
|
+
const FIRST_INCIDENT = "INC-0001";
|
|
67
|
+
/** The second, for the case that logs two. */
|
|
68
|
+
const SECOND_INCIDENT = "INC-0002";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What the BROWSER is sent, as this eval reads it.
|
|
72
|
+
*
|
|
73
|
+
* Parsed rather than cast: `state.updated` carries `unknown`, and a schema that
|
|
74
|
+
* stops matching is a loud failure naming the field where a cast would hand the
|
|
75
|
+
* assertions `undefined` and fail three lines later. It names only the fields
|
|
76
|
+
* asserted below, so `dashboardView` may grow without touching this.
|
|
77
|
+
*/
|
|
78
|
+
const ProjectedDashboard = z.object({
|
|
79
|
+
systemAlertLevel: z.string(),
|
|
80
|
+
incidents: z.array(
|
|
81
|
+
z.object({
|
|
82
|
+
id: z.string(),
|
|
83
|
+
severity: z.string(),
|
|
84
|
+
status: z.string(),
|
|
85
|
+
location: z.string(),
|
|
86
|
+
}),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The latest dashboard the session pushed, or undefined if it pushed none.
|
|
92
|
+
*
|
|
93
|
+
* `lastStateIn` is the SDK's reader, and the schema is what it is worth passing:
|
|
94
|
+
* a frame that stopped matching FAILS naming the field, where the cast this
|
|
95
|
+
* replaced would have handed the assertions `undefined`.
|
|
96
|
+
*/
|
|
97
|
+
const dashboard = (events: readonly SessionEvent[]) => lastStateIn(events, ProjectedDashboard);
|
|
98
|
+
|
|
99
|
+
/** One incident as the browser sees it. */
|
|
100
|
+
const boardEntry = (events: readonly SessionEvent[], id: string) =>
|
|
101
|
+
dashboard(events)?.incidents.find((i) => i.id === id);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The dialog gate's own refusal sentence, for the state it names.
|
|
105
|
+
*
|
|
106
|
+
* The character class absorbs the JSON escaping: a tool result reaches the
|
|
107
|
+
* event stream as a serialized string, so the state name arrives inside
|
|
108
|
+
* `\\"standby\\"` rather than plain quotes.
|
|
109
|
+
*/
|
|
110
|
+
const refusalAt = (state: string) =>
|
|
111
|
+
new RegExp(`Not available yet: this conversation is at [\\\\"]*${state}`);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Drive a whole shift, one dispatcher line at a time, and hand back every turn.
|
|
115
|
+
*
|
|
116
|
+
* The cases below assert about the turn a MECHANISM fired in rather than about
|
|
117
|
+
* turn number two: how many turns a desk takes to get there is the model's
|
|
118
|
+
* business and it varies, and a case pinned to a turn index is a flake with a
|
|
119
|
+
* misleading name.
|
|
120
|
+
*/
|
|
121
|
+
async function sayAll(session: EvalSession, lines: readonly string[]): Promise<EvalTurn[]> {
|
|
122
|
+
const turns: EvalTurn[] = [];
|
|
123
|
+
for (const line of lines) turns.push(await session.say(line));
|
|
124
|
+
return turns;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Every call to `tool` across the whole shift. */
|
|
128
|
+
const callsTo = (session: EvalSession, tool: string) =>
|
|
129
|
+
session.toolCalls().filter((c) => c.name === tool);
|
|
130
|
+
|
|
131
|
+
/** The 911 call all four cases open with — "cardiac arrest" is what
|
|
132
|
+
* `recommendSeverity` reads as critical and `recommendType` as medical. */
|
|
133
|
+
const THE_CALL =
|
|
134
|
+
"Take this call: 400 Oak Street, cardiac arrest, CPR in progress. " +
|
|
135
|
+
"Caller is Jane Doe, callback five five five, zero one zero one.";
|
|
136
|
+
|
|
137
|
+
/** The scripted turn that logs it. */
|
|
138
|
+
const LOG_TURN = [
|
|
139
|
+
{
|
|
140
|
+
tool: "incident_create",
|
|
141
|
+
args: {
|
|
142
|
+
location: "400 Oak Street",
|
|
143
|
+
description: "Cardiac arrest, CPR in progress",
|
|
144
|
+
callerName: "Jane Doe",
|
|
145
|
+
callerPhone: "555-0101",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"Copy — logged as a priority one. Confirming severity and getting units rolling.",
|
|
149
|
+
] as const;
|
|
150
|
+
|
|
151
|
+
describeEval(dispatchAgent, (test) => {
|
|
152
|
+
test(
|
|
153
|
+
"refuses a unit's radio call while nothing is logged on the shift",
|
|
154
|
+
async ({ session }) => {
|
|
155
|
+
// A unit radioing in is not an emergency, so it gives the desk nothing to
|
|
156
|
+
// log — which is what makes the gate the only thing that can answer.
|
|
157
|
+
// Measured live: a DISPATCH order does not, because a competent model logs
|
|
158
|
+
// the call first and then legitimately reaches the gated tools from
|
|
159
|
+
// `working`, and a case that read that as a gate failure would be wrong.
|
|
160
|
+
const turn = await session.say("Medic-1 just radioed in — mark them available.");
|
|
161
|
+
|
|
162
|
+
// Every gated call the model made while the shift was still in standby
|
|
163
|
+
// has to have been refused, and the refusal has to say where the shift
|
|
164
|
+
// actually is. The `incident_create` guard is the lesson above, kept:
|
|
165
|
+
// anything issued AFTER a call was logged is in `working` and legal.
|
|
166
|
+
const logged = turn.toolCalls.findIndex((c) => c.name === "incident_create");
|
|
167
|
+
for (const [index, call] of turn.toolCalls.entries()) {
|
|
168
|
+
if (!GATED_TOOLS.has(call.name)) continue;
|
|
169
|
+
if (logged !== -1 && index > logged) continue;
|
|
170
|
+
expect(call.result).toMatch(refusalAt("standby"));
|
|
171
|
+
}
|
|
172
|
+
// Nothing was logged, so the board the browser holds is still empty.
|
|
173
|
+
expect(dashboard(session.events())?.incidents ?? []).toEqual([]);
|
|
174
|
+
// And it said so rather than pretending the unit had been updated.
|
|
175
|
+
expect(turn.text).toMatch(/nothing|no incident|not logged|standby|call|log/i);
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
stubReply: [
|
|
179
|
+
{ tool: "resources_update_status", args: { callsign: "Medic-1", status: "available" } },
|
|
180
|
+
"Nothing is logged on this shift yet — give me a call and I'll open it.",
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
test(
|
|
186
|
+
"logs a 911 call, scores it, and moves the shift to triaging",
|
|
187
|
+
async ({ session }) => {
|
|
188
|
+
const turn = await session.say(THE_CALL);
|
|
189
|
+
|
|
190
|
+
const logged = turn.toolCalls.find((c) => c.name === "incident_create");
|
|
191
|
+
expect(logged?.args.location).toMatch(/oak/i);
|
|
192
|
+
// The recommendation is the desk's own scoring, not the model's opinion:
|
|
193
|
+
// `recommendSeverity` reads "cardiac arrest" and `recommendType` reads
|
|
194
|
+
// "cardiac". A model that paraphrased the emergency away would show up
|
|
195
|
+
// here rather than in a sentence nobody checks.
|
|
196
|
+
expect(logged?.result).toMatch(/"recommendedSeverity":"critical"/);
|
|
197
|
+
expect(logged?.result).toMatch(/"recommendedType":"medical"/);
|
|
198
|
+
// `incident_create` is ungated and SPREADS the position it landed in, so
|
|
199
|
+
// the model reads "confirm the severity and type" as part of this result.
|
|
200
|
+
expect(logged?.result).toMatch(/"state":"working\.triaging"/);
|
|
201
|
+
// And the board the browser holds has it, at the severity the desk chose.
|
|
202
|
+
expect(boardEntry(session.events(), FIRST_INCIDENT)).toMatchObject({
|
|
203
|
+
severity: "critical",
|
|
204
|
+
location: "400 Oak Street",
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
{ stubReply: [...LOG_TURN] },
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
test(
|
|
211
|
+
"rolls units on a logged incident and follows them to monitoring",
|
|
212
|
+
async ({ session }) => {
|
|
213
|
+
const turns = await sayAll(session, [THE_CALL, "Dispatch the recommended units now."]);
|
|
214
|
+
|
|
215
|
+
const dispatching = turns.find((t) =>
|
|
216
|
+
t.toolCalls.some((c) => c.name === "resources_dispatch"),
|
|
217
|
+
);
|
|
218
|
+
const rolled = dispatching?.toolCalls.find((c) => c.name === "resources_dispatch");
|
|
219
|
+
// Units really assigned — `dispatched` is empty when every requested
|
|
220
|
+
// callsign was busy, which is the case the fourth test owns.
|
|
221
|
+
expect(rolled?.result).toMatch(/"dispatched":\[\{/);
|
|
222
|
+
// `sendFrom` only fires when something rolled, so this is the position
|
|
223
|
+
// moving BECAUSE of the dispatch rather than alongside it.
|
|
224
|
+
expect(rolled?.result).toMatch(/"state":"working\.monitoring"/);
|
|
225
|
+
// The board agrees: the incident is dispatched, not merely triaged.
|
|
226
|
+
expect(boardEntry(session.events(), FIRST_INCIDENT)?.status).toBe("dispatched");
|
|
227
|
+
// And the order is the one the desk's flow requires: log, then dispatch.
|
|
228
|
+
const names = session.toolCalls().map((c) => c.name);
|
|
229
|
+
expect(names.indexOf("resources_dispatch")).toBeGreaterThan(names.indexOf("incident_create"));
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
stubReply: [
|
|
233
|
+
...LOG_TURN,
|
|
234
|
+
{ tool: "resources_dispatch", args: { incidentId: FIRST_INCIDENT, autoDispatch: true } },
|
|
235
|
+
"Medic-1 is rolling priority one, ETA under five.",
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
test(
|
|
241
|
+
"closing an incident releases the units that were on it",
|
|
242
|
+
async ({ session }) => {
|
|
243
|
+
await sayAll(session, [
|
|
244
|
+
THE_CALL,
|
|
245
|
+
"Dispatch the recommended units, emergency priority.",
|
|
246
|
+
"Units report the patient is transported and they're clear. Close it out.",
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
const closed = callsTo(session, "incident_update_status").filter(
|
|
250
|
+
(c) => c.args.status === "resolved",
|
|
251
|
+
);
|
|
252
|
+
expect(closed).toHaveLength(1);
|
|
253
|
+
// The release is the DESK's, not the model's: `resolved` is the one status
|
|
254
|
+
// that detaches every unit still assigned to this incident and says so on
|
|
255
|
+
// the incident's own timeline. A model cannot route around it, which is
|
|
256
|
+
// what makes this the claim worth asserting live. (The BUSY-unit refusal
|
|
257
|
+
// an earlier draft tried to assert here is the case below: a competent
|
|
258
|
+
// dispatcher checks availability first and never triggers it, so it is
|
|
259
|
+
// `{ scripted: true }` rather than weakened into this one.)
|
|
260
|
+
expect(closed[0]?.result).toMatch(/"newStatus":"resolved"/);
|
|
261
|
+
expect(closed[0]?.result).toMatch(/All resources released/);
|
|
262
|
+
// The board agrees, which is the half a browser would show.
|
|
263
|
+
expect(boardEntry(session.events(), FIRST_INCIDENT)?.status).toBe("resolved");
|
|
264
|
+
// And the shift ran in the order the flow requires.
|
|
265
|
+
const names = session.toolCalls().map((c) => c.name);
|
|
266
|
+
expect(names.indexOf("incident_create")).toBeGreaterThanOrEqual(0);
|
|
267
|
+
expect(names.indexOf("resources_dispatch")).toBeGreaterThan(names.indexOf("incident_create"));
|
|
268
|
+
expect(names.lastIndexOf("incident_update_status")).toBeGreaterThan(
|
|
269
|
+
names.indexOf("resources_dispatch"),
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
stubReply: [
|
|
274
|
+
...LOG_TURN,
|
|
275
|
+
{
|
|
276
|
+
tool: "resources_dispatch",
|
|
277
|
+
args: { incidentId: FIRST_INCIDENT, autoDispatch: true, priority: "emergency" },
|
|
278
|
+
},
|
|
279
|
+
"Medic-1 rolling priority one to 400 Oak Street.",
|
|
280
|
+
{
|
|
281
|
+
tool: "incident_update_status",
|
|
282
|
+
args: { incidentId: FIRST_INCIDENT, status: "resolved", notes: "Patient transported" },
|
|
283
|
+
},
|
|
284
|
+
"Copy — Oak Street is closed and the units are clear.",
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
test(
|
|
290
|
+
"a unit already rolling is not sent to a second call",
|
|
291
|
+
async ({ session }) => {
|
|
292
|
+
await sayAll(session, [
|
|
293
|
+
THE_CALL,
|
|
294
|
+
"Send Medic-1 to Oak Street.",
|
|
295
|
+
"New call: 12 Pine Lane, chest pains. Log it.",
|
|
296
|
+
"Send Medic-1 to Pine Lane as well.",
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
const dispatches = callsTo(session, "resources_dispatch");
|
|
300
|
+
// The second request is the subject, so both have to have gone out.
|
|
301
|
+
expect(dispatches).toHaveLength(2);
|
|
302
|
+
const [first, second] = dispatches;
|
|
303
|
+
// Medic-1 really rolled the first time — otherwise the refusal below is
|
|
304
|
+
// about a unit that was never busy.
|
|
305
|
+
expect(first?.result).toMatch(/"callsign":"Medic-1"/);
|
|
306
|
+
expect(first?.result).toMatch(/"state":"working\.monitoring"/);
|
|
307
|
+
// And the second time the desk REFUSED rather than double-booking it:
|
|
308
|
+
// `failed` carries the reason and `dispatched` is empty. The unit is
|
|
309
|
+
// committed to ONE incident, which is the property a dispatch desk is
|
|
310
|
+
// useless without and which no prompt can carry.
|
|
311
|
+
expect(second?.result).toMatch(/"dispatched":\[\]/);
|
|
312
|
+
expect(second?.result).toMatch(/"callsign":"Medic-1","reason":"Currently dispatched"/);
|
|
313
|
+
// `sendFrom` only fires when something rolled, so the call did NOT
|
|
314
|
+
// advance: logging Pine Lane put it back at `triaging`, and a dispatch
|
|
315
|
+
// that dispatched nothing leaves it there.
|
|
316
|
+
expect(second?.result).toMatch(/"state":"working\.triaging"/);
|
|
317
|
+
// The board agrees on both halves — the second incident never reached
|
|
318
|
+
// `dispatched`, and the first one still holds the unit.
|
|
319
|
+
expect(boardEntry(session.events(), SECOND_INCIDENT)?.status).not.toBe("dispatched");
|
|
320
|
+
expect(boardEntry(session.events(), FIRST_INCIDENT)?.status).toBe("dispatched");
|
|
321
|
+
},
|
|
322
|
+
// Scripted only. `resources_get_available` is in this desk's tool list and
|
|
323
|
+
// the prompt tells it to check capacity, so a live model looks first and
|
|
324
|
+
// never asks for a busy unit — which is the right behaviour and leaves the
|
|
325
|
+
// refusal unobserved. Forcing the call is the only way to see the guard
|
|
326
|
+
// itself, and a case that "accepts either" would assert nothing live.
|
|
327
|
+
{
|
|
328
|
+
scripted: true,
|
|
329
|
+
stubReply: [
|
|
330
|
+
...LOG_TURN,
|
|
331
|
+
{
|
|
332
|
+
tool: "resources_dispatch",
|
|
333
|
+
args: { incidentId: FIRST_INCIDENT, callsigns: ["Medic-1"], priority: "emergency" },
|
|
334
|
+
},
|
|
335
|
+
"Medic-1 is rolling to 400 Oak Street.",
|
|
336
|
+
{
|
|
337
|
+
tool: "incident_create",
|
|
338
|
+
args: { location: "12 Pine Lane", description: "Chest pains" },
|
|
339
|
+
},
|
|
340
|
+
"Logged as a second incident.",
|
|
341
|
+
{
|
|
342
|
+
tool: "resources_dispatch",
|
|
343
|
+
args: { incidentId: SECOND_INCIDENT, callsigns: ["Medic-1"], priority: "priority" },
|
|
344
|
+
},
|
|
345
|
+
"Medic-1 is already committed to Oak Street — I'll find you another unit.",
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
});
|