@alexkroman1/aai-cli 5.13.2 → 6.1.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-C8Jk-ZcP.mjs → _agent-vGW5SBnX.mjs} +23 -5
- package/dist/_agent.d.ts +18 -0
- package/dist/{_api-client-BX8Qv51n.mjs → _api-client-LgLksMhN.mjs} +45 -8
- package/dist/_api-client.d.ts +39 -0
- package/dist/{_bundler-BuIyDdvs.mjs → _bundler-BapCUUJN.mjs} +42 -14
- package/dist/_bundler.d.ts +43 -5
- package/dist/_cli-common.d.ts +67 -3
- package/dist/{_config-CzwAWOWr.mjs → _config-D_s09e7g.mjs} +36 -18
- package/dist/_deploy.d.ts +2 -5
- package/dist/_dev-restart.d.ts +6 -1
- package/dist/{_dev-server-Ch7K8e8i.mjs → _dev-server-DQ1t9sbw.mjs} +94 -38
- package/dist/_dev-server.d.ts +30 -2
- package/dist/_e2e-test-utils.d.ts +80 -0
- package/dist/_fault-mode.d.ts +175 -0
- package/dist/{_init-CbkrE2wq.mjs → _init-CpISqoR8.mjs} +3 -3
- package/dist/{_output-CC300DzW.mjs → _output-CKkmWs7i.mjs} +8 -1
- package/dist/_output.d.ts +9 -2
- package/dist/{_server-common-61fL__39.mjs → _server-common-6e0QI4mq.mjs} +1 -1
- package/dist/{_slug-api-D30zPmsn.mjs → _slug-api-D2heILuC.mjs} +2 -2
- package/dist/{_studio-D2ciIhbi.mjs → _studio-zAJycZ8j.mjs} +3 -3
- package/dist/{_templates-B1kt3BGt.mjs → _templates-iDGjGiPf.mjs} +4 -6
- package/dist/{_typecheck-gate-4v9UBZHh.mjs → _typecheck-gate-BXvUNnfs.mjs} +1 -1
- package/dist/{_utils-0DHbcqUb.mjs → _utils-B8QmtFhK.mjs} +21 -7
- package/dist/_utils.d.ts +18 -2
- package/dist/{_vite-env-Dg_QlVv0.mjs → _vite-env-BNveawd1.mjs} +31 -1
- package/dist/_vite-env.d.ts +30 -0
- package/dist/build-BO1Ni6oJ.mjs +62 -0
- package/dist/build.d.ts +19 -0
- package/dist/cli-workflow.d.ts +10 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +406 -224
- package/dist/{client-bundler-C9qXL8A2.mjs → client-bundler-B8Q9F8gJ.mjs} +3 -22
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DCRX3d96.mjs → delete-4I0uNR4a.mjs} +6 -7
- package/dist/delete.d.ts +2 -8
- package/dist/{deploy-DjsEV-40.mjs → deploy-DJ3HRcia.mjs} +13 -14
- package/dist/{dev-CvPH5dbF.mjs → dev-C6SWaAZP.mjs} +3 -3
- package/dist/{eject-LgBRfnbH.mjs → eject-Zrzk5KV3.mjs} +41 -15
- package/dist/eject.d.ts +30 -3
- package/dist/{init-BppxMR8r.mjs → init-BTRGiPQr.mjs} +61 -26
- package/dist/init.d.ts +11 -0
- package/dist/{login-BQQaxJhL.mjs → login-k0Z3VQu9.mjs} +5 -5
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +498 -91
- package/dist/scaffold/package.json +11 -8
- package/dist/scaffold/pnpm-workspace.yaml +32 -0
- package/dist/scaffold/server.mjs +53 -57
- package/dist/{secret-Ds9dvqAj.mjs → secret-DzSQx1Ds.mjs} +6 -3
- package/dist/{storage-DbrQSHFk.mjs → storage-C-Do0mIA.mjs} +18 -6
- package/dist/{studio-GSFd4s5O.mjs → studio-BGVmCA2G.mjs} +10 -10
- package/dist/templates/code-interpreter/agent.ts +0 -2
- package/dist/templates/dispatch-center/agent.test.ts +21 -30
- package/dist/templates/dispatch-center/agent.ts +4 -31
- package/dist/templates/dispatch-center/client.tsx +41 -28
- package/dist/templates/dispatch-center/shared.ts +73 -66
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +13 -15
- package/dist/templates/dispatch-center/tools/incident_create.ts +42 -45
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +60 -62
- package/dist/templates/dispatch-center/tools/incident_get.ts +10 -6
- package/dist/templates/dispatch-center/tools/incident_triage.ts +44 -46
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +40 -42
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +3 -6
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +21 -24
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +53 -55
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +3 -5
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +40 -43
- package/dist/templates/embedded-assets/agent.test.ts +83 -0
- package/dist/templates/embedded-assets/agent.ts +1 -48
- package/dist/templates/embedded-assets/shared.ts +17 -0
- package/dist/templates/embedded-assets/system-prompt.md +13 -0
- package/dist/templates/embedded-assets/tools/list_topics.ts +9 -0
- package/dist/templates/embedded-assets/tools/search_knowledge.ts +26 -0
- package/dist/templates/health-assistant/agent.test.ts +169 -0
- package/dist/templates/health-assistant/agent.ts +1 -166
- package/dist/templates/health-assistant/fda.ts +100 -0
- package/dist/templates/health-assistant/tools/check_drug_interaction.ts +59 -0
- package/dist/templates/health-assistant/tools/medication_lookup.ts +31 -0
- package/dist/templates/infocom-adventure/agent.test.ts +205 -0
- package/dist/templates/infocom-adventure/agent.ts +3 -106
- package/dist/templates/infocom-adventure/client.tsx +32 -28
- package/dist/templates/infocom-adventure/shared.ts +36 -16
- package/dist/templates/infocom-adventure/tools/game_state_drop.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_flag.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_get.ts +16 -0
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +14 -0
- package/dist/templates/infocom-adventure/tools/game_state_restart.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_score.ts +13 -0
- package/dist/templates/infocom-adventure/tools/game_state_take.ts +13 -0
- package/dist/templates/link-digest/agent.test.ts +223 -0
- package/dist/templates/link-digest/agent.ts +79 -0
- package/dist/templates/link-digest/client.tsx +146 -0
- package/dist/templates/link-digest/workflows/digest.ts +248 -0
- package/dist/templates/math-buddy/agent.ts +0 -2
- package/dist/templates/night-owl/agent.test.ts +80 -0
- package/dist/templates/night-owl/agent.ts +1 -68
- package/dist/templates/night-owl/system-prompt.md +10 -0
- package/dist/templates/night-owl/tools/recommend.ts +64 -0
- package/dist/templates/personal-finance/agent.ts +0 -2
- package/dist/templates/pizza-ordering/agent.test.ts +59 -58
- package/dist/templates/pizza-ordering/agent.ts +10 -159
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +44 -27
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +31 -0
- package/dist/templates/pizza-ordering/tools/place_order.ts +22 -0
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +22 -0
- package/dist/templates/pizza-ordering/tools/set_customer_name.ts +13 -0
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +28 -0
- package/dist/templates/pizza-ordering/tools/view_order.ts +21 -0
- package/dist/templates/plan-and-execute/agent.test.ts +411 -0
- package/dist/templates/plan-and-execute/agent.ts +23 -0
- package/dist/templates/plan-and-execute/client.tsx +115 -0
- package/dist/templates/plan-and-execute/graph.ts +178 -0
- package/dist/templates/plan-and-execute/prompts.ts +110 -0
- package/dist/templates/plan-and-execute/shared.ts +161 -0
- package/dist/templates/plan-and-execute/system-prompt.md +18 -0
- package/dist/templates/plan-and-execute/tools/plan_status.ts +20 -0
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +63 -0
- package/dist/templates/plan-and-execute/tools/start_plan.ts +51 -0
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +101 -0
- package/dist/templates/recap-workflow/agent.test.ts +762 -0
- package/dist/templates/recap-workflow/agent.ts +95 -0
- package/dist/templates/recap-workflow/shared.ts +43 -0
- package/dist/templates/recap-workflow/tools/cancel_recap.ts +23 -0
- package/dist/templates/recap-workflow/tools/keep_transcript.ts +31 -0
- package/dist/templates/recap-workflow/tools/recap_progress.ts +27 -0
- package/dist/templates/recap-workflow/tools/recap_status.ts +46 -0
- package/dist/templates/recap-workflow/tools/request_recap.ts +43 -0
- package/dist/templates/recap-workflow/workflows/recap.ts +544 -0
- package/dist/templates/recap-workflow/workflows/tokens.ts +26 -0
- package/dist/templates/redline/agent.test.ts +222 -0
- package/dist/templates/redline/agent.ts +89 -0
- package/dist/templates/redline/client.tsx +184 -0
- package/dist/templates/redline/workflows/prompts.ts +68 -0
- package/dist/templates/redline/workflows/redline.ts +258 -0
- package/dist/templates/research-workflow/agent.test.ts +525 -0
- package/dist/templates/research-workflow/agent.ts +94 -0
- package/dist/templates/research-workflow/shared.ts +35 -0
- package/dist/templates/research-workflow/tools/file_it_now.ts +22 -0
- package/dist/templates/research-workflow/tools/request_research.ts +33 -0
- package/dist/templates/research-workflow/tools/research_progress.ts +26 -0
- package/dist/templates/research-workflow/tools/research_status.ts +40 -0
- package/dist/templates/research-workflow/workflows/prompts.ts +143 -0
- package/dist/templates/research-workflow/workflows/research.ts +558 -0
- package/dist/templates/retail/agent.test.ts +319 -256
- package/dist/templates/retail/agent.ts +6 -46
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +45 -24
- package/dist/templates/retail/registry.test.ts +40 -25
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +32 -67
- package/dist/templates/retail/shared.test.ts +21 -19
- package/dist/templates/retail/shared.ts +61 -23
- package/dist/templates/retail/store.test.ts +47 -52
- package/dist/templates/retail/store.ts +92 -79
- package/dist/templates/retail/swap.test.ts +20 -17
- package/dist/templates/retail/swap.ts +7 -14
- package/dist/templates/retail/tools/cancel_pending_order.ts +7 -11
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +26 -14
- package/dist/templates/retail/tools/find_user_id_by_email.ts +5 -9
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +5 -7
- package/dist/templates/retail/tools/get_item_details.ts +7 -8
- package/dist/templates/retail/tools/get_order_details.ts +7 -8
- package/dist/templates/retail/tools/get_product_details.ts +6 -8
- package/dist/templates/retail/tools/get_user_details.ts +6 -8
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +6 -10
- package/dist/templates/retail/tools/modify_pending_order_items.ts +8 -20
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +7 -13
- package/dist/templates/retail/tools/modify_user_address.ts +6 -10
- package/dist/templates/retail/tools/return_delivered_order_items.ts +7 -13
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +169 -54
- package/dist/templates/solo-rpg/agent.ts +4 -22
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +20 -20
- package/dist/templates/solo-rpg/tools/action_roll.ts +3 -5
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +3 -5
- package/dist/templates/solo-rpg/tools/check_state.ts +7 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +3 -3
- package/dist/templates/solo-rpg/tools/oracle.ts +19 -12
- package/dist/templates/solo-rpg/tools/save_game.ts +11 -8
- package/dist/templates/solo-rpg/tools/setup_character.ts +3 -3
- package/dist/templates/solo-rpg/tools/update_state.ts +13 -13
- package/dist/templates/support-line/agent.test.ts +343 -0
- package/dist/templates/support-line/agent.ts +27 -0
- package/dist/templates/support-line/client.tsx +122 -0
- package/dist/templates/support-line/graph.ts +224 -0
- package/dist/templates/support-line/knowledge.json +65 -0
- package/dist/templates/support-line/prompts.ts +95 -0
- package/dist/templates/support-line/shared.ts +240 -0
- package/dist/templates/support-line/system-prompt.md +17 -0
- package/dist/templates/support-line/tools/answer_question.ts +72 -0
- package/dist/templates/support-line/tools/list_topics.ts +22 -0
- package/dist/templates/support-line/tools/log_ticket.ts +30 -0
- package/dist/templates/transcription-workflow/agent.test.ts +563 -0
- package/dist/templates/transcription-workflow/agent.ts +92 -0
- package/dist/templates/transcription-workflow/client.tsx +234 -0
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +416 -0
- package/dist/templates/transcription-workflow/workflows/wav.ts +314 -0
- package/dist/templates/travel-concierge/agent.test.ts +338 -0
- package/dist/templates/travel-concierge/agent.ts +29 -0
- package/dist/templates/travel-concierge/client.tsx +161 -0
- package/dist/templates/travel-concierge/routing.ts +138 -0
- package/dist/templates/travel-concierge/shared.ts +547 -0
- package/dist/templates/travel-concierge/system-prompt.md +24 -0
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +20 -0
- package/dist/templates/travel-concierge/tools/book_excursion.ts +18 -0
- package/dist/templates/travel-concierge/tools/book_hotel.ts +20 -0
- package/dist/templates/travel-concierge/tools/cancel_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +16 -0
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +7 -0
- package/dist/templates/travel-concierge/tools/confirm_action.ts +7 -0
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +35 -0
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +33 -0
- package/dist/templates/travel-concierge/tools/search_excursions.ts +37 -0
- package/dist/templates/travel-concierge/tools/search_flights.ts +43 -0
- package/dist/templates/travel-concierge/tools/search_hotels.ts +36 -0
- package/dist/templates/travel-concierge/tools/to_car_rental_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_excursion_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_flight_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/to_hotel_assistant.ts +11 -0
- package/dist/templates/travel-concierge/tools/update_ticket.ts +22 -0
- package/dist/templates/web-researcher/agent.ts +0 -2
- package/dist/templates/web-researcher/system-prompt.md +10 -0
- package/dist/{test-2LWBcXvY.mjs → test-B6ZBxSpk.mjs} +2 -2
- package/dist/typecheck.mjs +17 -15
- package/dist/worker-bundler-CIdDhZ0c.mjs +398 -0
- package/dist/worker-bundler.d.ts +14 -0
- package/dist/worker-bundler.mjs +1 -108
- package/dist/workflow-B-O97AqI.mjs +125 -0
- package/dist/workflow-bundler.d.ts +133 -0
- package/dist/workflow.d.ts +67 -0
- package/package.json +13 -9
- package/dist/build-DQJ1ZSm9.mjs +0 -39
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import type { Db, ToolContext } from "@alexkroman1/aai";
|
|
2
|
+
import { createToolContext } from "@alexkroman1/aai/testing";
|
|
2
3
|
import { describe, expect, test, vi } from "vitest";
|
|
3
4
|
import {
|
|
4
5
|
applyConsequences,
|
|
5
6
|
DEFAULT_STATE,
|
|
6
7
|
type GameState,
|
|
7
|
-
|
|
8
|
+
gameSlot,
|
|
8
9
|
MAX_NPCS,
|
|
9
10
|
MIN_MOMENTUM,
|
|
10
11
|
makeNpc,
|
|
11
12
|
rollAction,
|
|
12
|
-
saveGameState,
|
|
13
13
|
} from "./shared.ts";
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
14
|
+
import actionRoll from "./tools/action_roll.ts";
|
|
15
|
+
import burnMomentum from "./tools/burn_momentum.ts";
|
|
16
|
+
import checkState from "./tools/check_state.ts";
|
|
17
|
+
import loadGame from "./tools/load_game.ts";
|
|
18
|
+
import oracle from "./tools/oracle.ts";
|
|
19
|
+
import saveGame from "./tools/save_game.ts";
|
|
20
|
+
import setupCharacter from "./tools/setup_character.ts";
|
|
21
|
+
import updateState from "./tools/update_state.ts";
|
|
21
22
|
|
|
22
23
|
// ── Test doubles ─────────────────────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -46,19 +47,11 @@ function makeDb(): { db: Db; rows: Map<string, unknown> } {
|
|
|
46
47
|
return { db, rows };
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
generate: () => Promise.reject(new Error("generate not available in tests")),
|
|
55
|
-
messages: [],
|
|
56
|
-
sessionId,
|
|
57
|
-
// Never aborts — a test has no turn to cancel. `ctx.signal` is always
|
|
58
|
-
// present at runtime, so a stub supplies one rather than omitting it.
|
|
59
|
-
signal: new AbortController().signal,
|
|
60
|
-
send: vi.fn(),
|
|
61
|
-
};
|
|
50
|
+
/** `send` is a spy rather than the recorder `createToolContext` installs,
|
|
51
|
+
* because this suite asserts call counts on it. Each call gets its own slot
|
|
52
|
+
* store, which is what makes two contexts two games. */
|
|
53
|
+
function makeCtx(db: Db = makeDb().db): ToolContext {
|
|
54
|
+
return createToolContext({ db, send: vi.fn() });
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
const SETUP_ARGS = {
|
|
@@ -106,19 +99,19 @@ describe("setup_character", () => {
|
|
|
106
99
|
await setupCharacter.execute(SETUP_ARGS, ctx);
|
|
107
100
|
|
|
108
101
|
// Simulate a played, damaged game between setups.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
gameSlot.update(ctx, (played) => {
|
|
103
|
+
played.health = 1;
|
|
104
|
+
played.momentum = -4;
|
|
105
|
+
played.chaosFactor = 8;
|
|
106
|
+
played.sceneCount = 42;
|
|
107
|
+
});
|
|
115
108
|
|
|
116
109
|
const result = (await setupCharacter.execute(
|
|
117
110
|
{ ...SETUP_ARGS, playerName: "Luna" },
|
|
118
111
|
ctx,
|
|
119
112
|
)) as Record<string, unknown>;
|
|
120
113
|
|
|
121
|
-
const state =
|
|
114
|
+
const state = gameSlot.get(ctx);
|
|
122
115
|
expect(state.npcs).toHaveLength(1);
|
|
123
116
|
expect(state.npcs[0]?.id).toBe("npc_1");
|
|
124
117
|
expect(state.clocks).toHaveLength(1);
|
|
@@ -140,17 +133,21 @@ describe("setup_character", () => {
|
|
|
140
133
|
test("stats are a permutation of [3,2,2,1,1] with the archetype's stat at 3", async () => {
|
|
141
134
|
const ctx = makeCtx();
|
|
142
135
|
await setupCharacter.execute(SETUP_ARGS, ctx);
|
|
143
|
-
const state =
|
|
136
|
+
const state = gameSlot.get(ctx);
|
|
144
137
|
const stats = [state.edge, state.heart, state.iron, state.shadow, state.wits];
|
|
145
138
|
expect([...stats].sort()).toEqual([1, 1, 2, 2, 3]);
|
|
146
139
|
// investigator biases wits (index 4) to the high stat
|
|
147
140
|
expect(state.wits).toBe(3);
|
|
148
141
|
});
|
|
149
142
|
|
|
150
|
-
test("
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
143
|
+
test("a second, independent context sees a fresh game", async () => {
|
|
144
|
+
// What this really checks: the state lives in the SLOT and not in a
|
|
145
|
+
// module-level variable. `createToolContext()` hands each call its own
|
|
146
|
+
// detached slot store, so the isolation is per CONTEXT — two distinct
|
|
147
|
+
// session ids would prove nothing extra, and `sessionSlot` could stop
|
|
148
|
+
// keying by session with this still passing.
|
|
149
|
+
await setupCharacter.execute(SETUP_ARGS, makeCtx());
|
|
150
|
+
const other = (await checkState.execute({} as never, makeCtx())) as {
|
|
154
151
|
initialized: boolean;
|
|
155
152
|
};
|
|
156
153
|
expect(other.initialized).toBe(false);
|
|
@@ -267,7 +264,7 @@ describe("burn_momentum", () => {
|
|
|
267
264
|
clockTicks: 1,
|
|
268
265
|
},
|
|
269
266
|
};
|
|
270
|
-
|
|
267
|
+
gameSlot.set(ctx, state);
|
|
271
268
|
}
|
|
272
269
|
|
|
273
270
|
test("a legal burn reverts the miss's consequences, upgrades, and resets momentum", async () => {
|
|
@@ -278,7 +275,7 @@ describe("burn_momentum", () => {
|
|
|
278
275
|
expect(result.burned).toBe(true);
|
|
279
276
|
expect(result.newResultCode).toBe("STRONG_HIT");
|
|
280
277
|
|
|
281
|
-
const state =
|
|
278
|
+
const state = gameSlot.get(ctx);
|
|
282
279
|
expect(state.health).toBe(5); // -2 reverted
|
|
283
280
|
expect(state.clocks[0]?.filled).toBe(0); // tick reverted
|
|
284
281
|
expect(state.momentum).toBe(2); // reset, overriding the strong hit's gain
|
|
@@ -306,21 +303,21 @@ describe("burn_momentum", () => {
|
|
|
306
303
|
|
|
307
304
|
// Strong hits cannot be upgraded
|
|
308
305
|
seedRolledState(8, ctx);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
gameSlot.update(ctx, (state) => {
|
|
307
|
+
state.lastRoll!.result = "STRONG_HIT";
|
|
308
|
+
});
|
|
312
309
|
result = (await burnMomentum.execute({} as never, ctx)) as Record<string, unknown>;
|
|
313
310
|
expect(result.error).toMatch(/already a Strong Hit/);
|
|
314
311
|
});
|
|
315
312
|
|
|
316
313
|
test("action_roll persists the roll so burn needs no dice arguments", async () => {
|
|
317
314
|
const ctx = makeCtx();
|
|
318
|
-
|
|
315
|
+
gameSlot.set(ctx, playingState());
|
|
319
316
|
await actionRoll.execute(
|
|
320
317
|
{ move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "attack" },
|
|
321
318
|
ctx,
|
|
322
319
|
);
|
|
323
|
-
const state =
|
|
320
|
+
const state = gameSlot.get(ctx);
|
|
324
321
|
expect(state.lastRoll).not.toBeNull();
|
|
325
322
|
expect(state.lastRoll?.move).toBe("clash");
|
|
326
323
|
expect(state.lastRoll?.deltas).toBeDefined();
|
|
@@ -367,18 +364,134 @@ describe("rollAction", () => {
|
|
|
367
364
|
});
|
|
368
365
|
});
|
|
369
366
|
|
|
367
|
+
// ── oracle ───────────────────────────────────────────────────────────────────
|
|
368
|
+
//
|
|
369
|
+
// `chaos_check` is the one oracle branch that WRITES: `checkChaosInterrupt`
|
|
370
|
+
// lowers the chaos factor when the roll lands. It used to read the slot with
|
|
371
|
+
// `gameSlot.get` and assign to what came back, under a comment claiming the
|
|
372
|
+
// value was live — which described the removed `ctx.state` bag, not a slot. The
|
|
373
|
+
// stored value is deep-frozen, so ~1 call in 5 at the default chaos factor (and
|
|
374
|
+
// ~6 in 10 as it climbs) threw a `TypeError` instead of answering. These
|
|
375
|
+
// assertions are on the STORED value for that reason: a body writing to a
|
|
376
|
+
// private copy would pass every check on its own return value.
|
|
377
|
+
|
|
378
|
+
describe("oracle", () => {
|
|
379
|
+
/** Force `d(sides)` to roll `value` on the next call. */
|
|
380
|
+
function mockRoll(value: number, sides: number) {
|
|
381
|
+
return vi.spyOn(Math, "random").mockReturnValue((value - 0.5) / sides);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
test("a chaos interrupt that LANDS lowers the stored chaos factor", async () => {
|
|
385
|
+
const ctx = makeCtx();
|
|
386
|
+
const state = playingState();
|
|
387
|
+
state.chaosFactor = 9; // threshold 6 — a roll of 1 lands
|
|
388
|
+
gameSlot.set(ctx, state);
|
|
389
|
+
mockRoll(1, 10);
|
|
390
|
+
|
|
391
|
+
const result = (await oracle.execute({ type: "chaos_check" }, ctx)) as {
|
|
392
|
+
interrupted: boolean;
|
|
393
|
+
interruptType: string | null;
|
|
394
|
+
chaosFactor: number;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
expect(result.interrupted).toBe(true);
|
|
398
|
+
expect(result.interruptType).toBeTruthy();
|
|
399
|
+
expect(result.chaosFactor).toBe(8);
|
|
400
|
+
// The half the old code could not do: the write reached the slot.
|
|
401
|
+
expect(gameSlot.get(ctx).chaosFactor).toBe(8);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("the chaos factor floors at 3, where no roll is taken at all", async () => {
|
|
405
|
+
const ctx = makeCtx();
|
|
406
|
+
const state = playingState();
|
|
407
|
+
state.chaosFactor = 3; // threshold 0 — `checkChaosInterrupt` returns early
|
|
408
|
+
gameSlot.set(ctx, state);
|
|
409
|
+
|
|
410
|
+
const result = (await oracle.execute({ type: "chaos_check" }, ctx)) as {
|
|
411
|
+
interrupted: boolean;
|
|
412
|
+
chaosFactor: number;
|
|
413
|
+
};
|
|
414
|
+
expect(result.interrupted).toBe(false);
|
|
415
|
+
expect(gameSlot.get(ctx).chaosFactor).toBe(3);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("a chaos check that MISSES changes nothing", async () => {
|
|
419
|
+
const ctx = makeCtx();
|
|
420
|
+
const state = playingState();
|
|
421
|
+
state.chaosFactor = 5; // threshold 2
|
|
422
|
+
gameSlot.set(ctx, state);
|
|
423
|
+
mockRoll(10, 10); // past the threshold
|
|
424
|
+
|
|
425
|
+
const result = (await oracle.execute({ type: "chaos_check" }, ctx)) as {
|
|
426
|
+
interrupted: boolean;
|
|
427
|
+
chaosFactor: number;
|
|
428
|
+
};
|
|
429
|
+
expect(result.interrupted).toBe(false);
|
|
430
|
+
expect(result.chaosFactor).toBe(5);
|
|
431
|
+
expect(gameSlot.get(ctx).chaosFactor).toBe(5);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("a chaos check on an untouched session starts from the default factor", async () => {
|
|
435
|
+
const ctx = makeCtx();
|
|
436
|
+
mockRoll(1, 10); // DEFAULT_STATE.chaosFactor is 5, so threshold 2 — lands
|
|
437
|
+
const result = (await oracle.execute({ type: "chaos_check" }, ctx)) as { chaosFactor: number };
|
|
438
|
+
expect(result.chaosFactor).toBe(DEFAULT_STATE.chaosFactor - 1);
|
|
439
|
+
expect(gameSlot.get(ctx).chaosFactor).toBe(DEFAULT_STATE.chaosFactor - 1);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("yes_no maps the d6 onto its three answers", async () => {
|
|
443
|
+
const ctx = makeCtx();
|
|
444
|
+
for (const [roll, answer] of [
|
|
445
|
+
[1, "No"],
|
|
446
|
+
[2, "No"],
|
|
447
|
+
[3, "Yes, but with a complication"],
|
|
448
|
+
[4, "Yes, but with a complication"],
|
|
449
|
+
[5, "Yes"],
|
|
450
|
+
[6, "Yes"],
|
|
451
|
+
] as const) {
|
|
452
|
+
mockRoll(roll, 6);
|
|
453
|
+
const result = (await oracle.execute({ type: "yes_no" }, ctx)) as {
|
|
454
|
+
roll: number;
|
|
455
|
+
answer: string;
|
|
456
|
+
};
|
|
457
|
+
expect.soft(result, `roll ${roll}`).toEqual({ type: "yes_no", roll, answer });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("the four inspiration branches answer without touching the game", async () => {
|
|
462
|
+
const ctx = makeCtx();
|
|
463
|
+
gameSlot.set(ctx, playingState());
|
|
464
|
+
const before = structuredClone(gameSlot.get(ctx));
|
|
465
|
+
|
|
466
|
+
const reaction = (await oracle.execute({ type: "npc_reaction" }, ctx)) as { reaction: string };
|
|
467
|
+
const twist = (await oracle.execute({ type: "scene_twist" }, ctx)) as { twist: string };
|
|
468
|
+
const theme = (await oracle.execute({ type: "action_theme" }, ctx)) as {
|
|
469
|
+
action: string;
|
|
470
|
+
theme: string;
|
|
471
|
+
seed: string;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
expect(reaction.reaction).toBeTruthy();
|
|
475
|
+
expect(twist.twist).toBeTruthy();
|
|
476
|
+
expect(theme.action).toBeTruthy();
|
|
477
|
+
expect(theme.theme).toBeTruthy();
|
|
478
|
+
expect(theme.seed.split(" ")).toHaveLength(3);
|
|
479
|
+
expect(gameSlot.get(ctx)).toEqual(before);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
370
483
|
// ── update_state: clocks, caps, validation ───────────────────────────────────
|
|
371
484
|
|
|
372
485
|
describe("update_state", () => {
|
|
373
486
|
test("clock ids never collide after a removal (max-scan, not length+1)", async () => {
|
|
374
487
|
const ctx = makeCtx();
|
|
375
|
-
|
|
488
|
+
gameSlot.set(ctx, playingState()); // has clock_1
|
|
376
489
|
|
|
377
490
|
await updateState.execute({ addClockName: "Second" }, ctx); // clock_2
|
|
378
491
|
await updateState.execute({ removeClockName: "Doom" }, ctx); // removes clock_1
|
|
379
492
|
await updateState.execute({ addClockName: "Third" }, ctx);
|
|
380
493
|
|
|
381
|
-
const state =
|
|
494
|
+
const state = gameSlot.get(ctx);
|
|
382
495
|
const ids = state.clocks.map((c) => c.id);
|
|
383
496
|
expect(new Set(ids).size).toBe(ids.length);
|
|
384
497
|
expect(ids).toEqual(["clock_2", "clock_3"]);
|
|
@@ -388,7 +501,7 @@ describe("update_state", () => {
|
|
|
388
501
|
const ctx = makeCtx();
|
|
389
502
|
const state = playingState();
|
|
390
503
|
state.clocks[0]!.filled = 3; // 3 of 4
|
|
391
|
-
|
|
504
|
+
gameSlot.set(ctx, state);
|
|
392
505
|
|
|
393
506
|
const result = (await updateState.execute({ advanceClockName: "Doom" }, ctx)) as {
|
|
394
507
|
clockEvents: { clock: string; trigger: string }[];
|
|
@@ -402,13 +515,13 @@ describe("update_state", () => {
|
|
|
402
515
|
while (state.npcs.length < MAX_NPCS) {
|
|
403
516
|
state.npcs.push(makeNpc({ id: `npc_${state.npcs.length + 1}`, name: "Extra" }));
|
|
404
517
|
}
|
|
405
|
-
|
|
518
|
+
gameSlot.set(ctx, state);
|
|
406
519
|
|
|
407
520
|
const result = (await updateState.execute({ addNpcName: "One Too Many" }, ctx)) as {
|
|
408
521
|
warnings?: string[];
|
|
409
522
|
};
|
|
410
523
|
expect(result.warnings?.[0]).toMatch(/NPC limit/);
|
|
411
|
-
const after =
|
|
524
|
+
const after = gameSlot.get(ctx);
|
|
412
525
|
expect(after.npcs).toHaveLength(MAX_NPCS);
|
|
413
526
|
});
|
|
414
527
|
|
|
@@ -438,11 +551,11 @@ describe("save_game / load_game", () => {
|
|
|
438
551
|
const { db, rows } = makeDb();
|
|
439
552
|
|
|
440
553
|
// Session A plays and saves.
|
|
441
|
-
const sessionA = makeCtx(
|
|
554
|
+
const sessionA = makeCtx(db);
|
|
442
555
|
const played = playingState();
|
|
443
556
|
played.playerName = "Kael";
|
|
444
557
|
played.sceneCount = 7;
|
|
445
|
-
|
|
558
|
+
gameSlot.set(sessionA, played);
|
|
446
559
|
const saved = (await saveGame.execute({ slot: "chapter-2" }, sessionA)) as Record<
|
|
447
560
|
string,
|
|
448
561
|
unknown
|
|
@@ -451,8 +564,8 @@ describe("save_game / load_game", () => {
|
|
|
451
564
|
expect(saved.slot).toBe("chapter-2");
|
|
452
565
|
expect(rows.get("save:chapter-2")).toMatchObject({ playerName: "Kael", sceneCount: 7 });
|
|
453
566
|
|
|
454
|
-
// Session B (fresh
|
|
455
|
-
const sessionB = makeCtx(
|
|
567
|
+
// Session B (a fresh game slot, the same app db) resumes it.
|
|
568
|
+
const sessionB = makeCtx(db);
|
|
456
569
|
const loaded = (await loadGame.execute({ slot: "chapter-2" }, sessionB)) as Record<
|
|
457
570
|
string,
|
|
458
571
|
unknown
|
|
@@ -460,7 +573,7 @@ describe("save_game / load_game", () => {
|
|
|
460
573
|
expect(loaded.loaded).toBe(true);
|
|
461
574
|
expect(loaded.playerName).toBe("Kael");
|
|
462
575
|
expect(loaded.sceneCount).toBe(7);
|
|
463
|
-
expect(
|
|
576
|
+
expect(gameSlot.get(sessionB).playerName).toBe("Kael");
|
|
464
577
|
});
|
|
465
578
|
|
|
466
579
|
test("loading a missing slot reports an error instead of resetting the game", async () => {
|
|
@@ -471,10 +584,12 @@ describe("save_game / load_game", () => {
|
|
|
471
584
|
|
|
472
585
|
test("saving twice to one slot upserts — the newer save wins", async () => {
|
|
473
586
|
const { db, rows } = makeDb();
|
|
474
|
-
const ctx = makeCtx(
|
|
475
|
-
|
|
587
|
+
const ctx = makeCtx(db);
|
|
588
|
+
gameSlot.set(ctx, playingState());
|
|
476
589
|
await saveGame.execute({}, ctx); // autosave
|
|
477
|
-
|
|
590
|
+
gameSlot.update(ctx, (game) => {
|
|
591
|
+
game.sceneCount = 9;
|
|
592
|
+
});
|
|
478
593
|
await saveGame.execute({}, ctx);
|
|
479
594
|
expect(rows.size).toBe(1);
|
|
480
595
|
expect(rows.get("save:autosave")).toMatchObject({ sceneCount: 9 });
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
3
|
-
import systemPrompt from "./system-prompt.md?raw";
|
|
4
|
-
import { actionRoll } from "./tools/action_roll.ts";
|
|
5
|
-
import { burnMomentum } from "./tools/burn_momentum.ts";
|
|
6
|
-
import { checkState } from "./tools/check_state.ts";
|
|
7
|
-
import { loadGame } from "./tools/load_game.ts";
|
|
8
|
-
import { oracle } from "./tools/oracle.ts";
|
|
9
|
-
import { saveGame } from "./tools/save_game.ts";
|
|
10
|
-
import { setupCharacter } from "./tools/setup_character.ts";
|
|
11
|
-
import { updateState } from "./tools/update_state.ts";
|
|
2
|
+
import { gameSlot } from "./shared.ts";
|
|
12
3
|
|
|
13
4
|
export default agent({
|
|
14
5
|
name: "Solo RPG",
|
|
15
|
-
systemPrompt,
|
|
16
6
|
greeting:
|
|
17
7
|
"Welcome. Tell me your name, or describe the kind of story you want, and we will begin. You can say something like, dark fantasy warrior named Kael, or just give me a name and I will build a world around you.",
|
|
18
8
|
sttPrompt:
|
|
@@ -22,15 +12,7 @@ export default agent({
|
|
|
22
12
|
// One declaration replaces a `ctx.send("game_state", state)` in every
|
|
23
13
|
// state-mutating tool — six of them, and adding a seventh meant
|
|
24
14
|
// remembering to push or watching the UI quietly fall out of sync.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
burn_momentum: burnMomentum,
|
|
29
|
-
check_state: checkState,
|
|
30
|
-
load_game: loadGame,
|
|
31
|
-
oracle,
|
|
32
|
-
save_game: saveGame,
|
|
33
|
-
setup_character: setupCharacter,
|
|
34
|
-
update_state: updateState,
|
|
35
|
-
},
|
|
15
|
+
// The identity projection: this campaign IS what the client renders, and the
|
|
16
|
+
// slot's own default is what a session that has run no tool projects.
|
|
17
|
+
syncState: gameSlot.projection((game) => game),
|
|
36
18
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
+
import type { DeepReadonly } from "@alexkroman1/aai";
|
|
2
3
|
import { ChatView, client, SidebarLayout, StartScreen, useAgentState } from "@alexkroman1/aai-ui";
|
|
3
4
|
import type { ReactNode } from "react";
|
|
4
5
|
import {
|
|
5
6
|
type Clock,
|
|
6
|
-
DEFAULT_STATE,
|
|
7
7
|
type Disposition,
|
|
8
8
|
type GameState,
|
|
9
9
|
GENRES,
|
|
10
|
+
gameSlot,
|
|
10
11
|
MAX_RESOURCE,
|
|
11
12
|
MIN_MOMENTUM,
|
|
12
13
|
type NPC,
|
|
@@ -14,6 +15,18 @@ import {
|
|
|
14
15
|
type TIME_PHASES,
|
|
15
16
|
} from "./shared.ts";
|
|
16
17
|
|
|
18
|
+
// The campaign before the first tool call, derived from the SAME projection the
|
|
19
|
+
// server pushes rather than from the module default beside it — so a session
|
|
20
|
+
// that has run no tool renders exactly what its first push will replace. Every
|
|
21
|
+
// sibling template derives its fallback this way; this one read `DEFAULT_STATE`
|
|
22
|
+
// directly, which is a second copy of "what an untouched session looks like".
|
|
23
|
+
//
|
|
24
|
+
// It is typed READONLY because that is what the projection produces, and a
|
|
25
|
+
// client only ever renders what the server pushed — so the four components
|
|
26
|
+
// below take a readonly campaign too, which is the deep-readonly slot
|
|
27
|
+
// propagating exactly as far as it should.
|
|
28
|
+
const EMPTY_GAME = gameSlot.projection((game) => game)(undefined);
|
|
29
|
+
|
|
17
30
|
// ── Color Palette ────────────────────────────────────────────────────────────
|
|
18
31
|
const C = {
|
|
19
32
|
bg: "#0a0a0c",
|
|
@@ -290,7 +303,7 @@ function StatPip({ label, value }: { label: string; value: number }) {
|
|
|
290
303
|
);
|
|
291
304
|
}
|
|
292
305
|
|
|
293
|
-
function ClockDisplay({ clock }: { clock: Clock }) {
|
|
306
|
+
function ClockDisplay({ clock }: { clock: DeepReadonly<Clock> }) {
|
|
294
307
|
const typeColor =
|
|
295
308
|
clock.clockType === "threat"
|
|
296
309
|
? C.threat
|
|
@@ -355,7 +368,7 @@ function ClockDisplay({ clock }: { clock: Clock }) {
|
|
|
355
368
|
);
|
|
356
369
|
}
|
|
357
370
|
|
|
358
|
-
function NpcCard({ npc }: { npc: NPC }) {
|
|
371
|
+
function NpcCard({ npc }: { npc: DeepReadonly<NPC> }) {
|
|
359
372
|
const dispColor = C.disposition[npc.disposition] || C.textMuted;
|
|
360
373
|
const icon = DISP_ICON[npc.disposition] || "\u25CB";
|
|
361
374
|
return (
|
|
@@ -390,7 +403,7 @@ function NpcCard({ npc }: { npc: NPC }) {
|
|
|
390
403
|
);
|
|
391
404
|
}
|
|
392
405
|
|
|
393
|
-
function StoryArc({ story }: { story: StoryBlueprint }) {
|
|
406
|
+
function StoryArc({ story }: { story: DeepReadonly<StoryBlueprint> }) {
|
|
394
407
|
const totalActs = story.acts.length;
|
|
395
408
|
const currentPhase = story.acts[story.currentAct - 1]?.phase ?? "";
|
|
396
409
|
const pct = totalActs > 0 ? ((story.currentAct - 1) / totalActs) * 100 : 0;
|
|
@@ -443,7 +456,7 @@ function StoryArc({ story }: { story: StoryBlueprint }) {
|
|
|
443
456
|
|
|
444
457
|
// ── Sidebar ──────────────────────────────────────────────────────────────────
|
|
445
458
|
|
|
446
|
-
function Sidebar({ game }: { game: GameState }) {
|
|
459
|
+
function Sidebar({ game }: { game: DeepReadonly<GameState> }) {
|
|
447
460
|
return (
|
|
448
461
|
<div
|
|
449
462
|
style={{
|
|
@@ -771,7 +784,7 @@ function SoloRPGApp() {
|
|
|
771
784
|
// The agent's own session state, projected by `syncState` and pushed after
|
|
772
785
|
// every tool call — no per-tool `ctx.send`, and nothing to keep in step
|
|
773
786
|
// when a new tool starts mutating the game.
|
|
774
|
-
const game = useAgentState<GameState
|
|
787
|
+
const game = useAgentState<DeepReadonly<GameState>>(EMPTY_GAME);
|
|
775
788
|
|
|
776
789
|
return (
|
|
777
790
|
<StartScreen
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DeepReadonly, sessionSlot, type ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
// ── Tuning Constants ─────────────────────────────────────────────────────────
|
|
@@ -320,24 +320,24 @@ export const DEFAULT_STATE: GameState = {
|
|
|
320
320
|
kidMode: false,
|
|
321
321
|
};
|
|
322
322
|
|
|
323
|
-
// ── Live game state (
|
|
324
|
-
// The in-play game lives in `
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
323
|
+
// ── Live game state (one session slot) ───────────────────────────────────────
|
|
324
|
+
// The in-play game lives in one `sessionSlot`, keyed per session — concurrent
|
|
325
|
+
// players get independent games by construction. The clone is load-bearing:
|
|
326
|
+
// `DEFAULT_STATE` is one module-level object shared by every session in the
|
|
327
|
+
// process, so a factory without it would let one player's game show up in
|
|
328
|
+
// another's.
|
|
329
|
+
export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_STATE));
|
|
328
330
|
|
|
329
|
-
/**
|
|
330
|
-
*
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
export
|
|
339
|
-
(ctx.state as StateSlot).game = state;
|
|
340
|
-
}
|
|
331
|
+
/**
|
|
332
|
+
* The game as a READ hands it out: deep-frozen, and typed to say so.
|
|
333
|
+
*
|
|
334
|
+
* Every pure helper below takes this rather than {@link GameState}, which is
|
|
335
|
+
* the widening a deep-readonly slot forces and the reason it is worth doing:
|
|
336
|
+
* a mutable `GameState` still satisfies it, so a helper called with an
|
|
337
|
+
* `updateTool` draft is unaffected, while a helper that WOULD have mutated
|
|
338
|
+
* stops compiling instead of throwing at its first call in production.
|
|
339
|
+
*/
|
|
340
|
+
export type FrozenGameState = DeepReadonly<GameState>;
|
|
341
341
|
|
|
342
342
|
// ── Persistent save slots (ctx.db) ───────────────────────────────────────────
|
|
343
343
|
// save_game / load_game are genuine cross-session persistence, so they use
|
|
@@ -441,7 +441,7 @@ export function makeNpc(opts: {
|
|
|
441
441
|
};
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
export function clockSummary(c: Clock) {
|
|
444
|
+
export function clockSummary(c: DeepReadonly<Clock>) {
|
|
445
445
|
return {
|
|
446
446
|
id: c.id,
|
|
447
447
|
name: c.name,
|
|
@@ -458,7 +458,7 @@ export function clockSummary(c: Clock) {
|
|
|
458
458
|
* setup_character, update_state, and check_state. Includes the player's
|
|
459
459
|
* content boundaries (contentLines) so they survive past the setup turn.
|
|
460
460
|
*/
|
|
461
|
-
export function stateSummary(state:
|
|
461
|
+
export function stateSummary(state: FrozenGameState) {
|
|
462
462
|
return {
|
|
463
463
|
initialized: state.initialized,
|
|
464
464
|
phase: state.phase,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import {
|
|
4
3
|
applyConsequences,
|
|
5
4
|
canBurnMomentum,
|
|
6
5
|
checkChaosInterrupt,
|
|
7
|
-
|
|
6
|
+
gameSlot,
|
|
8
7
|
MOVE_LABELS,
|
|
9
8
|
MOVES,
|
|
10
9
|
RESULT_LABELS,
|
|
@@ -12,7 +11,7 @@ import {
|
|
|
12
11
|
updateChaosFactor,
|
|
13
12
|
} from "../shared.ts";
|
|
14
13
|
|
|
15
|
-
export
|
|
14
|
+
export default gameSlot.updateTool({
|
|
16
15
|
description:
|
|
17
16
|
"Core mechanic. Roll 2d6 + stat (capped at 10) vs 2d10 challenge dice. Also applies consequences (health/spirit/supply/momentum changes, clock advancement) based on move type, position, and result. Call for ANY risky action. Pure conversation needs no roll.",
|
|
18
17
|
inputSchema: z.object({
|
|
@@ -25,8 +24,7 @@ export const actionRoll = tool({
|
|
|
25
24
|
purpose: z.string().max(300).describe("What the character is attempting"),
|
|
26
25
|
targetNpcId: z.string().max(32).describe("Target NPC id for social moves").optional(),
|
|
27
26
|
}),
|
|
28
|
-
|
|
29
|
-
const state = getGameState(ctx);
|
|
27
|
+
execute(args, state) {
|
|
30
28
|
const statValue = state[args.stat];
|
|
31
29
|
const roll = rollAction(args.stat, statValue, args.move);
|
|
32
30
|
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import {
|
|
3
2
|
applyConsequences,
|
|
4
3
|
canBurnMomentum,
|
|
5
|
-
|
|
4
|
+
gameSlot,
|
|
6
5
|
MOMENTUM_RESET,
|
|
7
6
|
RESULT_LABELS,
|
|
8
7
|
revertConsequences,
|
|
9
8
|
} from "../shared.ts";
|
|
10
9
|
|
|
11
|
-
export
|
|
10
|
+
export default gameSlot.updateTool({
|
|
12
11
|
description:
|
|
13
12
|
"Burn momentum to upgrade the most recent action roll. Only valid when current momentum beats the roll's challenge dice (both dice for a full upgrade, one for Miss to Weak Hit). Reverts the original result's consequences, applies the upgraded result, and resets momentum to +2.",
|
|
14
|
-
|
|
15
|
-
const state = getGameState(ctx);
|
|
13
|
+
execute(_args, state) {
|
|
16
14
|
const last = state.lastRoll;
|
|
17
15
|
if (!last) return { error: "No recent action roll to upgrade. Roll first." };
|
|
18
16
|
if (last.result === "STRONG_HIT") {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getGameState, stateSummary } from "../shared.ts";
|
|
1
|
+
import { gameSlot, stateSummary } from "../shared.ts";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
// `gameSlot.tool`, the READING half: the body is handed the value, so it needs
|
|
4
|
+
// neither a context annotation nor an opening `gameSlot.get(ctx)` — and what it
|
|
5
|
+
// is handed is deep-frozen, which is what makes "this tool only reads" a
|
|
6
|
+
// property of the declaration rather than a claim in a comment.
|
|
7
|
+
export default gameSlot.tool({
|
|
5
8
|
description:
|
|
6
9
|
"Returns the full current game state. Call this at the start of every turn, before narrating or rolling, and treat the returned values as ground truth — never guess or remember stats from previous turns.",
|
|
7
|
-
|
|
8
|
-
const state = getGameState(ctx);
|
|
9
|
-
return stateSummary(state);
|
|
10
|
-
},
|
|
10
|
+
execute: (_args, game) => stateSummary(game),
|
|
11
11
|
});
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { GameState } from "../shared.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { gameSlot, loadState, saveSlotKey, saveSlotParam } from "../shared.ts";
|
|
5
5
|
|
|
6
6
|
// Requires storage — `aai storage enable` (or DATABASE_URL in .env under
|
|
7
7
|
// `aai dev`); the rest of the game works without it.
|
|
8
|
-
export
|
|
8
|
+
export default tool({
|
|
9
9
|
description: "Load a previously saved game.",
|
|
10
10
|
inputSchema: z.object({ slot: saveSlotParam }),
|
|
11
11
|
async execute(args, ctx) {
|
|
12
12
|
const saved = await loadState<GameState>(ctx, saveSlotKey(args.slot));
|
|
13
13
|
if (!saved) return { error: "No save found." };
|
|
14
|
-
|
|
14
|
+
gameSlot.set(ctx, saved);
|
|
15
15
|
return {
|
|
16
16
|
loaded: true,
|
|
17
17
|
playerName: saved.playerName,
|