@alexkroman1/aai-cli 5.14.0 → 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 +8 -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 +17 -19
- package/dist/templates/dispatch-center/agent.ts +2 -29
- package/dist/templates/dispatch-center/client.tsx +33 -16
- package/dist/templates/dispatch-center/shared.ts +53 -19
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +12 -14
- package/dist/templates/dispatch-center/tools/incident_create.ts +41 -44
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +59 -61
- package/dist/templates/dispatch-center/tools/incident_get.ts +3 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +43 -45
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +39 -41
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -5
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +20 -23
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +52 -54
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -4
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +39 -42
- 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 +28 -12
- package/dist/templates/infocom-adventure/shared.ts +28 -11
- 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 +52 -47
- package/dist/templates/pizza-ordering/agent.ts +9 -158
- package/dist/templates/pizza-ordering/shared.ts +35 -12
- 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 +236 -172
- package/dist/templates/retail/agent.ts +5 -42
- package/dist/templates/retail/client.tsx +33 -11
- package/dist/templates/retail/registry.test.ts +31 -16
- package/dist/templates/retail/resolve.ts +28 -64
- package/dist/templates/retail/shared.test.ts +4 -3
- package/dist/templates/retail/shared.ts +24 -7
- package/dist/templates/retail/store.test.ts +12 -10
- package/dist/templates/retail/store.ts +69 -30
- package/dist/templates/retail/tools/cancel_pending_order.ts +4 -9
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -11
- 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 +5 -7
- package/dist/templates/retail/tools/get_order_details.ts +5 -7
- package/dist/templates/retail/tools/get_product_details.ts +4 -7
- package/dist/templates/retail/tools/get_user_details.ts +4 -7
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -7
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -9
- package/dist/templates/retail/tools/modify_pending_order_items.ts +4 -16
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +3 -9
- package/dist/templates/retail/tools/modify_user_address.ts +4 -9
- package/dist/templates/retail/tools/return_delivered_order_items.ts +3 -9
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +1 -1
- package/dist/templates/solo-rpg/agent.test.ts +152 -29
- package/dist/templates/solo-rpg/agent.ts +3 -21
- package/dist/templates/solo-rpg/client.tsx +19 -6
- package/dist/templates/solo-rpg/shared.ts +19 -11
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -4
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -4
- package/dist/templates/solo-rpg/tools/check_state.ts +6 -6
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +18 -11
- package/dist/templates/solo-rpg/tools/save_game.ts +10 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +3 -4
- 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
|
@@ -10,15 +10,15 @@ import {
|
|
|
10
10
|
MIN_MOMENTUM,
|
|
11
11
|
makeNpc,
|
|
12
12
|
rollAction,
|
|
13
|
-
type StateSlot,
|
|
14
13
|
} from "./shared.ts";
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
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";
|
|
22
22
|
|
|
23
23
|
// ── Test doubles ─────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
@@ -48,9 +48,10 @@ function makeDb(): { db: Db; rows: Map<string, unknown> } {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** `send` is a spy rather than the recorder `createToolContext` installs,
|
|
51
|
-
* because this suite asserts call counts on it.
|
|
52
|
-
|
|
53
|
-
|
|
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() });
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const SETUP_ARGS = {
|
|
@@ -98,12 +99,12 @@ describe("setup_character", () => {
|
|
|
98
99
|
await setupCharacter.execute(SETUP_ARGS, ctx);
|
|
99
100
|
|
|
100
101
|
// Simulate a played, damaged game between setups.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
gameSlot.update(ctx, (played) => {
|
|
103
|
+
played.health = 1;
|
|
104
|
+
played.momentum = -4;
|
|
105
|
+
played.chaosFactor = 8;
|
|
106
|
+
played.sceneCount = 42;
|
|
107
|
+
});
|
|
107
108
|
|
|
108
109
|
const result = (await setupCharacter.execute(
|
|
109
110
|
{ ...SETUP_ARGS, playerName: "Luna" },
|
|
@@ -139,10 +140,14 @@ describe("setup_character", () => {
|
|
|
139
140
|
expect(state.wits).toBe(3);
|
|
140
141
|
});
|
|
141
142
|
|
|
142
|
-
test("
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
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 {
|
|
146
151
|
initialized: boolean;
|
|
147
152
|
};
|
|
148
153
|
expect(other.initialized).toBe(false);
|
|
@@ -298,9 +303,9 @@ describe("burn_momentum", () => {
|
|
|
298
303
|
|
|
299
304
|
// Strong hits cannot be upgraded
|
|
300
305
|
seedRolledState(8, ctx);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
gameSlot.update(ctx, (state) => {
|
|
307
|
+
state.lastRoll!.result = "STRONG_HIT";
|
|
308
|
+
});
|
|
304
309
|
result = (await burnMomentum.execute({} as never, ctx)) as Record<string, unknown>;
|
|
305
310
|
expect(result.error).toMatch(/already a Strong Hit/);
|
|
306
311
|
});
|
|
@@ -359,6 +364,122 @@ describe("rollAction", () => {
|
|
|
359
364
|
});
|
|
360
365
|
});
|
|
361
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
|
+
|
|
362
483
|
// ── update_state: clocks, caps, validation ───────────────────────────────────
|
|
363
484
|
|
|
364
485
|
describe("update_state", () => {
|
|
@@ -430,7 +551,7 @@ describe("save_game / load_game", () => {
|
|
|
430
551
|
const { db, rows } = makeDb();
|
|
431
552
|
|
|
432
553
|
// Session A plays and saves.
|
|
433
|
-
const sessionA = makeCtx(
|
|
554
|
+
const sessionA = makeCtx(db);
|
|
434
555
|
const played = playingState();
|
|
435
556
|
played.playerName = "Kael";
|
|
436
557
|
played.sceneCount = 7;
|
|
@@ -443,8 +564,8 @@ describe("save_game / load_game", () => {
|
|
|
443
564
|
expect(saved.slot).toBe("chapter-2");
|
|
444
565
|
expect(rows.get("save:chapter-2")).toMatchObject({ playerName: "Kael", sceneCount: 7 });
|
|
445
566
|
|
|
446
|
-
// Session B (fresh
|
|
447
|
-
const sessionB = makeCtx(
|
|
567
|
+
// Session B (a fresh game slot, the same app db) resumes it.
|
|
568
|
+
const sessionB = makeCtx(db);
|
|
448
569
|
const loaded = (await loadGame.execute({ slot: "chapter-2" }, sessionB)) as Record<
|
|
449
570
|
string,
|
|
450
571
|
unknown
|
|
@@ -463,10 +584,12 @@ describe("save_game / load_game", () => {
|
|
|
463
584
|
|
|
464
585
|
test("saving twice to one slot upserts — the newer save wins", async () => {
|
|
465
586
|
const { db, rows } = makeDb();
|
|
466
|
-
const ctx = makeCtx(
|
|
587
|
+
const ctx = makeCtx(db);
|
|
467
588
|
gameSlot.set(ctx, playingState());
|
|
468
589
|
await saveGame.execute({}, ctx); // autosave
|
|
469
|
-
gameSlot.
|
|
590
|
+
gameSlot.update(ctx, (game) => {
|
|
591
|
+
game.sceneCount = 9;
|
|
592
|
+
});
|
|
470
593
|
await saveGame.execute({}, ctx);
|
|
471
594
|
expect(rows.size).toBe(1);
|
|
472
595
|
expect(rows.get("save:autosave")).toMatchObject({ sceneCount: 9 });
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
2
|
import { gameSlot } from "./shared.ts";
|
|
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";
|
|
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,16 +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
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// player's game show up in another's.
|
|
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.
|
|
330
329
|
export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_STATE));
|
|
331
330
|
|
|
332
|
-
|
|
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>;
|
|
333
341
|
|
|
334
342
|
// ── Persistent save slots (ctx.db) ───────────────────────────────────────────
|
|
335
343
|
// save_game / load_game are genuine cross-session persistence, so they use
|
|
@@ -433,7 +441,7 @@ export function makeNpc(opts: {
|
|
|
433
441
|
};
|
|
434
442
|
}
|
|
435
443
|
|
|
436
|
-
export function clockSummary(c: Clock) {
|
|
444
|
+
export function clockSummary(c: DeepReadonly<Clock>) {
|
|
437
445
|
return {
|
|
438
446
|
id: c.id,
|
|
439
447
|
name: c.name,
|
|
@@ -450,7 +458,7 @@ export function clockSummary(c: Clock) {
|
|
|
450
458
|
* setup_character, update_state, and check_state. Includes the player's
|
|
451
459
|
* content boundaries (contentLines) so they survive past the setup turn.
|
|
452
460
|
*/
|
|
453
|
-
export function stateSummary(state:
|
|
461
|
+
export function stateSummary(state: FrozenGameState) {
|
|
454
462
|
return {
|
|
455
463
|
initialized: state.initialized,
|
|
456
464
|
phase: state.phase,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import {
|
|
4
3
|
applyConsequences,
|
|
@@ -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 = gameSlot.get(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,4 +1,3 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import {
|
|
3
2
|
applyConsequences,
|
|
4
3
|
canBurnMomentum,
|
|
@@ -8,11 +7,10 @@ import {
|
|
|
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 = gameSlot.get(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 { tool } from "@alexkroman1/aai";
|
|
2
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 = gameSlot.get(ctx);
|
|
9
|
-
return stateSummary(state);
|
|
10
|
-
},
|
|
10
|
+
execute: (_args, game) => stateSummary(game),
|
|
11
11
|
});
|
|
@@ -5,7 +5,7 @@ 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) {
|
|
@@ -202,7 +202,7 @@ const THEMES = [
|
|
|
202
202
|
"Wound",
|
|
203
203
|
];
|
|
204
204
|
|
|
205
|
-
export
|
|
205
|
+
export default tool({
|
|
206
206
|
description:
|
|
207
207
|
"Consult the oracle for narrative inspiration. Generates random prompts from thematic tables.",
|
|
208
208
|
inputSchema: z.object({
|
|
@@ -210,7 +210,7 @@ export const oracle = tool({
|
|
|
210
210
|
.enum(["action_theme", "npc_reaction", "scene_twist", "yes_no", "chaos_check"])
|
|
211
211
|
.describe("Type of oracle consultation"),
|
|
212
212
|
}),
|
|
213
|
-
|
|
213
|
+
execute(args, ctx) {
|
|
214
214
|
if (args.type === "yes_no") {
|
|
215
215
|
const roll = d(6);
|
|
216
216
|
const answer = roll <= 2 ? "No" : roll <= 4 ? "Yes, but with a complication" : "Yes";
|
|
@@ -218,15 +218,22 @@ export const oracle = tool({
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
if (args.type === "chaos_check") {
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
// `checkChaosInterrupt` LOWERS the chaos factor when the roll lands, so
|
|
222
|
+
// this branch is a write and has to go through the slot's mutation
|
|
223
|
+
// window: `gameSlot.get` hands out the deep-frozen stored value, and
|
|
224
|
+
// assigning to it is a compile error and a `TypeError`. It is one branch
|
|
225
|
+
// of five, which is why this stays an ordinary `tool()` with a scoped
|
|
226
|
+
// `update` rather than becoming `gameSlot.updateTool` — the other four
|
|
227
|
+
// read nothing and store nothing.
|
|
228
|
+
return gameSlot.update(ctx, (game) => {
|
|
229
|
+
const interrupt = checkChaosInterrupt(game);
|
|
230
|
+
return {
|
|
231
|
+
type: "chaos_check",
|
|
232
|
+
chaosFactor: game.chaosFactor,
|
|
233
|
+
interrupted: Boolean(interrupt),
|
|
234
|
+
interruptType: interrupt,
|
|
235
|
+
};
|
|
236
|
+
});
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
if (args.type === "npc_reaction") {
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { tool } from "@alexkroman1/aai";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
import { gameSlot, saveSlotKey, saveSlotParam, saveState } from "../shared.ts";
|
|
4
3
|
|
|
5
4
|
// Requires storage — `aai storage enable` (or DATABASE_URL in .env under
|
|
6
5
|
// `aai dev`); the rest of the game works without it.
|
|
7
|
-
|
|
6
|
+
//
|
|
7
|
+
// `gameSlot.tool` even though the body AWAITS: the reading half places no
|
|
8
|
+
// constraint on the body (only `updateTool` must be synchronous, because its
|
|
9
|
+
// draft is stored when it returns). What it does place is the frozen value, and
|
|
10
|
+
// a save is the purest read there is.
|
|
11
|
+
export default gameSlot.tool({
|
|
8
12
|
description: "Save current game to persistent storage.",
|
|
9
13
|
inputSchema: z.object({ slot: saveSlotParam }),
|
|
10
|
-
async execute(args, ctx) {
|
|
11
|
-
|
|
12
|
-
await saveState(ctx, saveSlotKey(args.slot), state);
|
|
14
|
+
async execute(args, game, ctx) {
|
|
15
|
+
await saveState(ctx, saveSlotKey(args.slot), game);
|
|
13
16
|
return {
|
|
14
17
|
saved: true,
|
|
15
18
|
slot: args.slot ?? "autosave",
|
|
16
|
-
name:
|
|
17
|
-
scene:
|
|
19
|
+
name: game.playerName,
|
|
20
|
+
scene: game.sceneCount,
|
|
18
21
|
};
|
|
19
22
|
},
|
|
20
23
|
});
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
TONES,
|
|
19
19
|
} from "../shared.ts";
|
|
20
20
|
|
|
21
|
-
export
|
|
21
|
+
export default tool({
|
|
22
22
|
description:
|
|
23
23
|
"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.",
|
|
24
24
|
inputSchema: z.object({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pushCapped
|
|
1
|
+
import { pushCapped } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_CLOCK_SEGMENTS,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
updateCrisisFlags,
|
|
20
20
|
} from "../shared.ts";
|
|
21
21
|
|
|
22
|
-
export
|
|
22
|
+
export default gameSlot.updateTool({
|
|
23
23
|
description:
|
|
24
24
|
"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.",
|
|
25
25
|
inputSchema: z.object({
|
|
@@ -54,8 +54,7 @@ export const updateState = tool({
|
|
|
54
54
|
storyComplete: z.boolean().describe("Mark story as complete").optional(),
|
|
55
55
|
logEntry: z.string().max(500).describe("Short log entry for this scene").optional(),
|
|
56
56
|
}),
|
|
57
|
-
|
|
58
|
-
const state = gameSlot.get(ctx);
|
|
57
|
+
execute(args, state) {
|
|
59
58
|
const warnings: string[] = [];
|
|
60
59
|
const clockEvents: { clock: string; trigger: string }[] = [];
|
|
61
60
|
|