@alexkroman1/aai-cli 6.11.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +40 -19
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.test.ts +746 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/tools/answer_question.ts +17 -4
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.test.ts +64 -33
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { gameSlot, saveSlotKey, saveSlotParam, saveState } from "../shared.ts";
|
|
2
|
+
import { gameSlot, saveSlotKey, saveSlotParam, saveState, storyFlow } from "../shared.ts";
|
|
3
3
|
|
|
4
4
|
// Requires storage — `aai storage enable` (or DATABASE_URL in .env under
|
|
5
5
|
// `aai dev`); the rest of the game works without it.
|
|
6
6
|
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Gated on `playing` or `gameOver` — i.e. anything but `awaitingSetup`.
|
|
9
|
+
*
|
|
10
|
+
* Saving before a character exists writes an empty campaign under a slot name,
|
|
11
|
+
* and `load_game` would then cheerfully restore it over a real game. That is a
|
|
12
|
+
* position, so it is a `when`.
|
|
13
|
+
*
|
|
14
|
+
* The body AWAITS, which a flow tool allows: only `slot.updateTool` must be
|
|
15
|
+
* synchronous, because its draft is stored when it returns. What a save needs is
|
|
16
|
+
* the frozen value, and `gameSlot.get` is what hands it over.
|
|
17
|
+
*/
|
|
18
|
+
export default storyFlow.tool({
|
|
12
19
|
description: "Save current game to persistent storage.",
|
|
13
20
|
inputSchema: z.object({ slot: saveSlotParam }),
|
|
14
|
-
|
|
21
|
+
when: ["playing", "gameOver"],
|
|
22
|
+
async execute(args, ctx) {
|
|
23
|
+
const game = gameSlot.get(ctx);
|
|
15
24
|
await saveState(ctx, saveSlotKey(args.slot), game);
|
|
16
25
|
return {
|
|
17
26
|
saved: true,
|
|
@@ -14,10 +14,24 @@ import {
|
|
|
14
14
|
makeNpc,
|
|
15
15
|
shuffle,
|
|
16
16
|
stateSummary,
|
|
17
|
+
storyFlow,
|
|
17
18
|
TIME_PHASES,
|
|
18
19
|
TONES,
|
|
19
20
|
} from "../shared.ts";
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* **Not a `storyFlow.tool`, deliberately.** Setting up is legal in every state —
|
|
24
|
+
* a player may start over at any point, an ended story included — so a `when`
|
|
25
|
+
* listing all of them would be a gate that gates nothing. It drives the flow
|
|
26
|
+
* itself, which is what `dialog.reset` and `dialog.send` are public for, and reports
|
|
27
|
+
* the position it landed in so the narrator reads what the opening turn expects.
|
|
28
|
+
*
|
|
29
|
+
* **`reset` before `send`, because a restart is not a transition.** This body
|
|
30
|
+
* replaces the campaign with a pristine `DEFAULT_STATE`; resetting the flow is
|
|
31
|
+
* the same statement on the other side, and it is what makes starting over work
|
|
32
|
+
* from `gameOver` — a final state delivers no events, so a `SETUP` transition
|
|
33
|
+
* out of it would be dead config.
|
|
34
|
+
*/
|
|
21
35
|
export default tool({
|
|
22
36
|
description:
|
|
23
37
|
"Set up the entire game in one call. Starts a completely fresh game (any previous unsaved game is replaced), generates stats, initializes state, and marks the game as ready. After this returns, just narrate the opening scene. No need to call update_state — everything is already done.",
|
|
@@ -186,12 +200,16 @@ export default tool({
|
|
|
186
200
|
storyComplete: false,
|
|
187
201
|
};
|
|
188
202
|
|
|
189
|
-
// Mark initialized
|
|
203
|
+
// Mark initialized. `initialized` is the CLIENT's render flag; the flow is
|
|
204
|
+
// what gates the tools, and `SETUP` is what puts it in `playing` — so the
|
|
205
|
+
// roll tools become available in the same call that gives them stats to
|
|
206
|
+
// roll against.
|
|
190
207
|
state.initialized = true;
|
|
191
|
-
state.phase = "playing";
|
|
192
208
|
state.sceneCount = 1;
|
|
193
209
|
|
|
194
210
|
gameSlot.set(ctx, state);
|
|
211
|
+
storyFlow.reset(ctx);
|
|
212
|
+
const at = storyFlow.send(ctx, { type: "SETUP" });
|
|
195
213
|
|
|
196
214
|
return {
|
|
197
215
|
success: true,
|
|
@@ -201,6 +219,8 @@ export default tool({
|
|
|
201
219
|
archetypeLabel: ARCHETYPES[args.archetype as keyof typeof ARCHETYPES] || args.archetype,
|
|
202
220
|
openingSituation: args.openingSituation,
|
|
203
221
|
creativitySeed: creativitySeed(),
|
|
222
|
+
// The position spread verbatim — the same keys every gated tool reports.
|
|
223
|
+
...at,
|
|
204
224
|
// The real saved state — never hardcoded values
|
|
205
225
|
...stateSummary(state),
|
|
206
226
|
};
|
|
@@ -15,13 +15,23 @@ import {
|
|
|
15
15
|
makeNpc,
|
|
16
16
|
nextSeqId,
|
|
17
17
|
stateSummary,
|
|
18
|
+
storyFlow,
|
|
18
19
|
TIME_PHASES,
|
|
19
|
-
updateCrisisFlags,
|
|
20
20
|
} from "../shared.ts";
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Gated on `playing`: there is nothing to sync before a character exists (that
|
|
24
|
+
* is `setup_character`'s whole job, and the prompt says not to call this after
|
|
25
|
+
* it), and nothing to sync after the story has ended.
|
|
26
|
+
*
|
|
27
|
+
* It sends `SETTLED`, because moving the scene on is what SPENDS a standing
|
|
28
|
+
* roll: once the narrator has changed the location or logged the scene, the
|
|
29
|
+
* player's window to burn momentum on the last roll has closed.
|
|
30
|
+
*/
|
|
31
|
+
export default storyFlow.tool({
|
|
23
32
|
description:
|
|
24
33
|
"Lightweight state sync for during gameplay. Handles location changes, NPC additions, clock additions, time changes, and session log entries. Resource changes (health/spirit/supply/momentum) are auto-applied by action_roll — only use those fields here for manual adjustments like resting or trading. Pass only what changed.",
|
|
34
|
+
when: "playing",
|
|
25
35
|
inputSchema: z.object({
|
|
26
36
|
location: z.string().max(200).describe("New location name").optional(),
|
|
27
37
|
locationDesc: z.string().max(500).describe("Short location description").optional(),
|
|
@@ -54,117 +64,120 @@ export default gameSlot.updateTool({
|
|
|
54
64
|
storyComplete: z.boolean().describe("Mark story as complete").optional(),
|
|
55
65
|
logEntry: z.string().max(500).describe("Short log entry for this scene").optional(),
|
|
56
66
|
}),
|
|
57
|
-
execute(args,
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
execute: (args, ctx) =>
|
|
68
|
+
gameSlot.update(ctx, (state) => {
|
|
69
|
+
const warnings: string[] = [];
|
|
70
|
+
const clockEvents: { clock: string; trigger: string }[] = [];
|
|
60
71
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
// Resources
|
|
73
|
+
if (args.health !== undefined)
|
|
74
|
+
state.health = Math.max(0, Math.min(MAX_RESOURCE, args.health));
|
|
75
|
+
if (args.spirit !== undefined)
|
|
76
|
+
state.spirit = Math.max(0, Math.min(MAX_RESOURCE, args.spirit));
|
|
77
|
+
if (args.supply !== undefined)
|
|
78
|
+
state.supply = Math.max(0, Math.min(MAX_RESOURCE, args.supply));
|
|
79
|
+
if (args.momentum !== undefined)
|
|
80
|
+
state.momentum = Math.max(MIN_MOMENTUM, Math.min(state.maxMomentum, args.momentum));
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
// Location
|
|
83
|
+
if (args.location !== undefined) state.currentLocation = args.location;
|
|
84
|
+
if (args.locationDesc !== undefined) state.currentSceneContext = args.locationDesc;
|
|
85
|
+
if (args.timeOfDay !== undefined) state.timeOfDay = args.timeOfDay;
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Add NPC
|
|
88
|
+
if (args.addNpcName) {
|
|
89
|
+
if (state.npcs.length >= MAX_NPCS) {
|
|
90
|
+
warnings.push(
|
|
91
|
+
`NPC limit of ${MAX_NPCS} reached — mark an existing NPC deceased or background instead.`,
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
state.npcs.push(
|
|
95
|
+
makeNpc({
|
|
96
|
+
id: nextSeqId(state.npcs, "npc"),
|
|
97
|
+
name: args.addNpcName,
|
|
98
|
+
description: args.addNpcDesc,
|
|
99
|
+
disposition: args.addNpcDisposition,
|
|
100
|
+
agenda: args.addNpcAgenda,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
89
104
|
}
|
|
90
|
-
}
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
// Update NPC
|
|
107
|
+
if (args.updateNpcId) {
|
|
108
|
+
const npc = state.npcs.find((n) => n.id === args.updateNpcId);
|
|
109
|
+
if (npc) {
|
|
110
|
+
if (args.updateNpcDisposition !== undefined) npc.disposition = args.updateNpcDisposition;
|
|
111
|
+
if (args.updateNpcBond !== undefined) npc.bond = args.updateNpcBond;
|
|
112
|
+
if (args.updateNpcStatus !== undefined) npc.status = args.updateNpcStatus;
|
|
113
|
+
} else {
|
|
114
|
+
warnings.push(`No NPC with id ${args.updateNpcId}.`);
|
|
115
|
+
}
|
|
101
116
|
}
|
|
102
|
-
}
|
|
103
117
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
// Add clock
|
|
119
|
+
if (args.addClockName) {
|
|
120
|
+
if (state.clocks.length >= MAX_CLOCKS) {
|
|
121
|
+
warnings.push(`Clock limit of ${MAX_CLOCKS} reached — remove a finished clock first.`);
|
|
122
|
+
} else {
|
|
123
|
+
state.clocks.push({
|
|
124
|
+
id: nextSeqId(state.clocks, "clock"),
|
|
125
|
+
name: args.addClockName,
|
|
126
|
+
clockType: args.addClockType ?? "threat",
|
|
127
|
+
segments: args.addClockSegments ?? DEFAULT_CLOCK_SEGMENTS,
|
|
128
|
+
filled: 0,
|
|
129
|
+
triggerDescription: args.addClockTrigger ?? "",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
117
132
|
}
|
|
118
|
-
}
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
// Advance clock
|
|
135
|
+
if (args.advanceClockName) {
|
|
136
|
+
const clock = state.clocks.find((c) => c.name === args.advanceClockName);
|
|
137
|
+
if (clock && clock.filled < clock.segments) {
|
|
138
|
+
clock.filled = Math.min(clock.segments, clock.filled + 1);
|
|
139
|
+
if (clock.filled >= clock.segments) {
|
|
140
|
+
clockEvents.push({ clock: clock.name, trigger: clock.triggerDescription });
|
|
141
|
+
}
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Remove clock
|
|
132
|
-
if (args.removeClockName) {
|
|
133
|
-
state.clocks = state.clocks.filter((c) => c.name !== args.removeClockName);
|
|
134
|
-
}
|
|
135
144
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
state.storyBlueprint.currentAct + 1,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
if (args.storyComplete && state.storyBlueprint) {
|
|
144
|
-
state.storyBlueprint.storyComplete = true;
|
|
145
|
-
}
|
|
145
|
+
// Remove clock
|
|
146
|
+
if (args.removeClockName) {
|
|
147
|
+
state.clocks = state.clocks.filter((c) => c.name !== args.removeClockName);
|
|
148
|
+
}
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
);
|
|
158
|
-
}
|
|
150
|
+
// Story arc
|
|
151
|
+
if (args.advanceAct && state.storyBlueprint) {
|
|
152
|
+
state.storyBlueprint.currentAct = Math.min(
|
|
153
|
+
state.storyBlueprint.acts.length,
|
|
154
|
+
state.storyBlueprint.currentAct + 1,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (args.storyComplete && state.storyBlueprint) {
|
|
158
|
+
state.storyBlueprint.storyComplete = true;
|
|
159
|
+
}
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
// Session log
|
|
162
|
+
if (args.logEntry) {
|
|
163
|
+
pushCapped(
|
|
164
|
+
state.sessionLog,
|
|
165
|
+
{
|
|
166
|
+
scene: state.sceneCount,
|
|
167
|
+
summary: args.logEntry,
|
|
168
|
+
location: state.currentLocation,
|
|
169
|
+
},
|
|
170
|
+
MAX_SESSION_LOG,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
162
173
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
177
|
+
clockEvents,
|
|
178
|
+
...stateSummary(state),
|
|
179
|
+
};
|
|
180
|
+
}),
|
|
181
|
+
sendFrom: (result) =>
|
|
182
|
+
result.gameOver ? { type: "DOWNED" as const } : { type: "SETTLED" as const },
|
|
170
183
|
});
|
|
@@ -17,32 +17,31 @@
|
|
|
17
17
|
* its return value, and audio in one is megabytes replayed on every resume.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
import { readUpload, uploadInfo } from "@alexkroman1/aai/step";
|
|
21
|
+
import {
|
|
22
|
+
installStubGateway,
|
|
23
|
+
installStubReporter,
|
|
24
|
+
installStubSpeech,
|
|
25
|
+
installStubTranscribe,
|
|
26
|
+
installStubUploads,
|
|
27
|
+
} from "@alexkroman1/aai/testing/vitest";
|
|
28
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
24
29
|
import { FatalError, RetryableError } from "workflow";
|
|
25
30
|
import agentDef, { spokenSummary } from "./agent.ts";
|
|
26
31
|
import { speak, spokenSummaryFlow, summarize } from "./workflows/summarize.ts";
|
|
27
|
-
import {
|
|
32
|
+
import { createJob, pollTranscript, uploadToProvider } from "./workflows/transcribe.ts";
|
|
28
33
|
|
|
29
34
|
/** The id every spec below uploads under. */
|
|
30
35
|
const UPLOAD_ID = "upl_test";
|
|
31
36
|
|
|
32
|
-
/** Slots left published reach the next file, so every one is released here. */
|
|
33
|
-
const restores: (() => void)[] = [];
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
while (restores.length > 0) restores.pop()?.();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
37
|
beforeEach(() => {
|
|
39
38
|
// WRITABLE, because this app's whole second half stores a file — and it is
|
|
40
39
|
// opt-in precisely so a step that wrote one nobody meant it to would fail.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
// `install*` rather than `stub*`: the fake registers its own `onTestFinished`,
|
|
41
|
+
// which is what replaced the hand-kept restore registry this file used to hold.
|
|
42
|
+
installStubUploads(
|
|
43
|
+
{ [UPLOAD_ID]: { bytes: new Uint8Array(64), name: "standup.wav", type: "audio/wav" } },
|
|
44
|
+
{ writable: true },
|
|
46
45
|
);
|
|
47
46
|
// The step env, which is where `requireStepEnv` and `stepSpeak` read the key.
|
|
48
47
|
// `vi.stubEnv` rather than an assignment: `unstubEnvs` undoes it before every
|
|
@@ -84,40 +83,38 @@ describe("the declaration", () => {
|
|
|
84
83
|
|
|
85
84
|
describe("transcribing", () => {
|
|
86
85
|
test("streams the stored recording to the provider and keeps the URL it answered", async () => {
|
|
87
|
-
const
|
|
88
|
-
|
|
86
|
+
const provider = installStubTranscribe({ audioUrl: "https://cdn/aai/1" });
|
|
87
|
+
installStubReporter();
|
|
89
88
|
|
|
90
89
|
await expect(uploadToProvider(UPLOAD_ID)).resolves.toEqual({
|
|
91
90
|
audioUrl: "https://cdn/aai/1",
|
|
92
91
|
});
|
|
93
|
-
expect(
|
|
94
|
-
expect(fetches.calls[0]?.headers.Authorization).toBe("test-key");
|
|
92
|
+
expect(provider.calls[0]?.leg).toBe("upload");
|
|
95
93
|
// The bytes really went, and they went as the file rather than as JSON.
|
|
96
|
-
expect(
|
|
94
|
+
expect(provider.calls[0]?.body?.length).toBe(64);
|
|
97
95
|
});
|
|
98
96
|
|
|
99
97
|
test("a 429 from the provider is RETRYABLE and a 400 is not", async () => {
|
|
100
|
-
const first =
|
|
101
|
-
|
|
98
|
+
const first = installStubTranscribe({ failure: { status: 429, message: "slow down" } });
|
|
99
|
+
installStubReporter();
|
|
102
100
|
await expect(createJob("https://cdn/aai/1")).rejects.toBeInstanceOf(RetryableError);
|
|
101
|
+
// Unpublished by hand: this is a boundary WITHIN one test, which the
|
|
102
|
+
// per-test auto-restore cannot give.
|
|
103
103
|
first.restore();
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
restores.push(second.restore);
|
|
105
|
+
installStubTranscribe({ failure: { status: 400, message: "bad model" } });
|
|
107
106
|
await expect(createJob("https://cdn/aai/1")).rejects.toBeInstanceOf(FatalError);
|
|
108
107
|
});
|
|
109
108
|
|
|
110
109
|
test("a job the provider gave up on is FATAL — no number of polls changes it", async () => {
|
|
111
|
-
|
|
112
|
-
stubStepFetch(() => ({ body: { status: "error", error: "corrupt audio" } })).restore,
|
|
113
|
-
);
|
|
110
|
+
installStubTranscribe({ jobError: "corrupt audio" });
|
|
114
111
|
|
|
115
112
|
await expect(pollTranscript(UPLOAD_ID, "t_1")).rejects.toThrow("corrupt audio");
|
|
116
113
|
await expect(pollTranscript(UPLOAD_ID, "t_1")).rejects.toBeInstanceOf(FatalError);
|
|
117
114
|
});
|
|
118
115
|
|
|
119
116
|
test("`done` is decided here, so the body never reads a provider's vocabulary", async () => {
|
|
120
|
-
|
|
117
|
+
installStubTranscribe({ pendingPolls: 1 });
|
|
121
118
|
|
|
122
119
|
await expect(pollTranscript(UPLOAD_ID, "t_1")).resolves.toEqual({ done: false });
|
|
123
120
|
});
|
|
@@ -125,26 +122,21 @@ describe("transcribing", () => {
|
|
|
125
122
|
test("a finished poll carries the transcript, named by the FILENAME", async () => {
|
|
126
123
|
// ONE request, not two: this used to poll for a status and then fetch the
|
|
127
124
|
// identical URL again for the text the poll already had in its hand.
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
}));
|
|
131
|
-
restores.push(fetches.restore, stubReporter().restore);
|
|
125
|
+
const provider = installStubTranscribe({ text: " we shipped it ", durationSec: 12.4 });
|
|
126
|
+
installStubReporter();
|
|
132
127
|
|
|
133
128
|
await expect(pollTranscript(UPLOAD_ID, "t_1")).resolves.toEqual({
|
|
134
129
|
done: true,
|
|
135
130
|
transcript: { source: "standup.wav", durationMs: 12_400, text: "we shipped it" },
|
|
136
131
|
});
|
|
137
|
-
expect(
|
|
132
|
+
expect(provider.calls).toHaveLength(1);
|
|
138
133
|
});
|
|
139
134
|
|
|
140
135
|
test("a recording of silence is FATAL rather than an empty summary", async () => {
|
|
141
136
|
// The failure this template is most likely to meet: silence transcribes
|
|
142
137
|
// successfully to nothing, and everything downstream would then be asked to
|
|
143
138
|
// summarize and speak no words at all.
|
|
144
|
-
|
|
145
|
-
stubStepFetch(() => ({ body: { status: "completed", text: " ", audio_duration: 3 } }))
|
|
146
|
-
.restore,
|
|
147
|
-
);
|
|
139
|
+
installStubTranscribe({ text: " ", durationSec: 3 });
|
|
148
140
|
|
|
149
141
|
await expect(pollTranscript(UPLOAD_ID, "t_1")).rejects.toThrow("no speech in that recording");
|
|
150
142
|
await expect(pollTranscript(UPLOAD_ID, "t_1")).rejects.toBeInstanceOf(FatalError);
|
|
@@ -160,7 +152,7 @@ describe("summarizing", () => {
|
|
|
160
152
|
spoken: "The launch is on for Tuesday, with two bugs still open.",
|
|
161
153
|
}),
|
|
162
154
|
);
|
|
163
|
-
|
|
155
|
+
installStubReporter();
|
|
164
156
|
|
|
165
157
|
const summary = await summarize("we ship tuesday");
|
|
166
158
|
|
|
@@ -181,14 +173,14 @@ describe("summarizing", () => {
|
|
|
181
173
|
spoken: "Several things happened.",
|
|
182
174
|
}),
|
|
183
175
|
);
|
|
184
|
-
|
|
176
|
+
installStubReporter();
|
|
185
177
|
|
|
186
178
|
expect((await summarize("…")).points).toHaveLength(4);
|
|
187
179
|
});
|
|
188
180
|
|
|
189
181
|
test("a reply with no spoken script FAILS rather than defaulting to silence", async () => {
|
|
190
182
|
installStubGateway(JSON.stringify({ headline: "Launch is on", points: ["Ship Tuesday"] }));
|
|
191
|
-
|
|
183
|
+
installStubReporter();
|
|
192
184
|
|
|
193
185
|
await expect(summarize("…")).rejects.toThrow(/did not match the shape/);
|
|
194
186
|
});
|
|
@@ -196,8 +188,8 @@ describe("summarizing", () => {
|
|
|
196
188
|
|
|
197
189
|
describe("speaking", () => {
|
|
198
190
|
test("stores a WAV and returns its ID — never the bytes", async () => {
|
|
199
|
-
const speech =
|
|
200
|
-
|
|
191
|
+
const speech = installStubSpeech({ pcmBytes: 48_000 });
|
|
192
|
+
installStubReporter();
|
|
201
193
|
|
|
202
194
|
const spoken = await speak("The launch is on for Tuesday.");
|
|
203
195
|
|
|
@@ -208,7 +200,8 @@ describe("speaking", () => {
|
|
|
208
200
|
});
|
|
209
201
|
|
|
210
202
|
test("what it stored is a real WAV, named and typed for the browser", async () => {
|
|
211
|
-
|
|
203
|
+
installStubSpeech({ pcmBytes: 4000 });
|
|
204
|
+
installStubReporter();
|
|
212
205
|
|
|
213
206
|
const { audio } = await speak("Hello.");
|
|
214
207
|
|
|
@@ -225,8 +218,8 @@ describe("speaking", () => {
|
|
|
225
218
|
});
|
|
226
219
|
|
|
227
220
|
test("passes a chosen voice through, and omits it entirely when none was chosen", async () => {
|
|
228
|
-
const speech =
|
|
229
|
-
|
|
221
|
+
const speech = installStubSpeech();
|
|
222
|
+
installStubReporter();
|
|
230
223
|
|
|
231
224
|
await speak("Hello.", "michael");
|
|
232
225
|
await speak("Hello.");
|
|
@@ -237,13 +230,6 @@ describe("speaking", () => {
|
|
|
237
230
|
});
|
|
238
231
|
});
|
|
239
232
|
|
|
240
|
-
describe("countWords", () => {
|
|
241
|
-
test("counts words rather than characters, and answers 0 for nothing", () => {
|
|
242
|
-
expect(countWords("we shipped it on tuesday")).toBe(5);
|
|
243
|
-
expect(countWords(" ")).toBe(0);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
233
|
describe("the whole run", () => {
|
|
248
234
|
/**
|
|
249
235
|
* Answer every leg's HTTP, so the BODY can be driven end to end.
|
|
@@ -261,38 +247,35 @@ describe("the whole run", () => {
|
|
|
261
247
|
* **The model call goes through this too, not `installStubGateway`.** A
|
|
262
248
|
* published `stepFetch` is what `stepGenerate` makes its request with, so a
|
|
263
249
|
* global-fetch stub is never reached once one exists — which is exactly the
|
|
264
|
-
* point `
|
|
250
|
+
* point a published `stepFetch` exists to make, and exactly why
|
|
251
|
+
* `stubTranscribe` takes an `otherwise`: publishing REPLACES, so a flow that
|
|
252
|
+
* transcribes AND calls a model cannot install two fakes.
|
|
253
|
+
*
|
|
254
|
+
* The three transcription legs are the SDK's fake rather than this file's
|
|
255
|
+
* hand-typed wire — it routes them off the SDK's own endpoint constants, so a
|
|
256
|
+
* spec cannot pass because the fake and the step agree on a typo.
|
|
265
257
|
*/
|
|
266
258
|
function stubProvider(reply: { headline: string; points: string[]; spoken: string }) {
|
|
267
|
-
return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
body: {
|
|
278
|
-
status: "completed",
|
|
279
|
-
text: "we ship tuesday and two bugs are left",
|
|
280
|
-
audio_duration: 42,
|
|
281
|
-
},
|
|
282
|
-
};
|
|
259
|
+
return installStubTranscribe({
|
|
260
|
+
audioUrl: "https://cdn/aai/1",
|
|
261
|
+
jobIdPrefix: "t_",
|
|
262
|
+
text: "we ship tuesday and two bugs are left",
|
|
263
|
+
durationSec: 42,
|
|
264
|
+
otherwise: (request) =>
|
|
265
|
+
request.url.includes("llm-gateway")
|
|
266
|
+
? { body: { choices: [{ message: { content: JSON.stringify(reply) } }] } }
|
|
267
|
+
: undefined,
|
|
283
268
|
});
|
|
284
269
|
}
|
|
285
270
|
|
|
286
271
|
test("transcribes, summarizes, speaks, and reports the file it made", async () => {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
stubSpeech().restore,
|
|
295
|
-
);
|
|
272
|
+
stubProvider({
|
|
273
|
+
headline: "Launch is on",
|
|
274
|
+
points: ["Ship Tuesday"],
|
|
275
|
+
spoken: "The launch is on for Tuesday.",
|
|
276
|
+
});
|
|
277
|
+
installStubReporter();
|
|
278
|
+
installStubSpeech();
|
|
296
279
|
|
|
297
280
|
const summary = await spokenSummaryFlow({ recording: UPLOAD_ID });
|
|
298
281
|
|
|
@@ -312,13 +295,9 @@ describe("the whole run", () => {
|
|
|
312
295
|
});
|
|
313
296
|
|
|
314
297
|
test("the voice the form chose reaches the synthesizer", async () => {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
.restore,
|
|
319
|
-
stubReporter().restore,
|
|
320
|
-
speech.restore,
|
|
321
|
-
);
|
|
298
|
+
stubProvider({ headline: "Launch is on", points: ["Ship Tuesday"], spoken: "Spoken." });
|
|
299
|
+
installStubReporter();
|
|
300
|
+
const speech = installStubSpeech();
|
|
322
301
|
|
|
323
302
|
await spokenSummaryFlow({ recording: UPLOAD_ID, voice: "michael" });
|
|
324
303
|
|
|
@@ -326,17 +305,9 @@ describe("the whole run", () => {
|
|
|
326
305
|
});
|
|
327
306
|
|
|
328
307
|
test("a recording the provider gave up on fails the run rather than half-summarizing", async () => {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
? { body: { upload_url: "https://cdn/aai/1" } }
|
|
333
|
-
: request.method === "POST"
|
|
334
|
-
? { body: { id: "t_1" } }
|
|
335
|
-
: { body: { status: "error", error: "corrupt audio" } },
|
|
336
|
-
).restore,
|
|
337
|
-
stubReporter().restore,
|
|
338
|
-
stubSpeech().restore,
|
|
339
|
-
);
|
|
308
|
+
installStubTranscribe({ jobError: "corrupt audio" });
|
|
309
|
+
installStubReporter();
|
|
310
|
+
installStubSpeech();
|
|
340
311
|
|
|
341
312
|
await expect(spokenSummaryFlow({ recording: UPLOAD_ID })).rejects.toThrow("corrupt audio");
|
|
342
313
|
});
|