@alexkroman1/aai-cli 5.13.2 → 5.14.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/scaffold/package.json +3 -3
- package/dist/scaffold/pnpm-workspace.yaml +24 -0
- package/dist/templates/dispatch-center/agent.test.ts +10 -17
- package/dist/templates/dispatch-center/agent.ts +2 -2
- package/dist/templates/dispatch-center/client.tsx +9 -13
- package/dist/templates/dispatch-center/shared.ts +36 -63
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_create.ts +2 -2
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_get.ts +9 -4
- package/dist/templates/dispatch-center/tools/incident_triage.ts +4 -4
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +4 -4
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +2 -2
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +2 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +4 -4
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +2 -2
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +2 -2
- package/dist/templates/infocom-adventure/agent.ts +10 -10
- package/dist/templates/infocom-adventure/client.tsx +5 -17
- package/dist/templates/infocom-adventure/shared.ts +17 -14
- package/dist/templates/pizza-ordering/agent.test.ts +27 -31
- package/dist/templates/pizza-ordering/agent.ts +8 -8
- package/dist/templates/pizza-ordering/client.tsx +4 -4
- package/dist/templates/pizza-ordering/shared.ts +14 -20
- package/dist/templates/retail/agent.test.ts +137 -138
- package/dist/templates/retail/agent.ts +7 -10
- package/dist/templates/retail/authenticate.ts +2 -2
- package/dist/templates/retail/client.tsx +13 -14
- package/dist/templates/retail/registry.test.ts +9 -9
- package/dist/templates/retail/resolve.test.ts +17 -16
- package/dist/templates/retail/resolve.ts +5 -4
- package/dist/templates/retail/shared.test.ts +17 -16
- package/dist/templates/retail/shared.ts +39 -18
- package/dist/templates/retail/store.test.ts +40 -47
- package/dist/templates/retail/store.ts +39 -65
- 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 +5 -4
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +6 -5
- package/dist/templates/retail/tools/find_user_id_by_email.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +2 -2
- package/dist/templates/retail/tools/get_item_details.ts +4 -3
- package/dist/templates/retail/tools/get_order_details.ts +4 -3
- package/dist/templates/retail/tools/get_product_details.ts +4 -3
- package/dist/templates/retail/tools/get_user_details.ts +4 -3
- package/dist/templates/retail/tools/list_all_product_types.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_address.ts +4 -3
- package/dist/templates/retail/tools/modify_pending_order_items.ts +6 -6
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +6 -6
- package/dist/templates/retail/tools/modify_user_address.ts +4 -3
- package/dist/templates/retail/tools/return_delivered_order_items.ts +6 -6
- package/dist/templates/solo-rpg/agent.test.ts +26 -34
- package/dist/templates/solo-rpg/agent.ts +2 -2
- package/dist/templates/solo-rpg/client.tsx +1 -1
- package/dist/templates/solo-rpg/shared.ts +6 -14
- package/dist/templates/solo-rpg/tools/action_roll.ts +2 -2
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +2 -2
- package/dist/templates/solo-rpg/tools/check_state.ts +2 -2
- package/dist/templates/solo-rpg/tools/load_game.ts +2 -2
- package/dist/templates/solo-rpg/tools/oracle.ts +3 -3
- package/dist/templates/solo-rpg/tools/save_game.ts +2 -2
- package/dist/templates/solo-rpg/tools/setup_character.ts +2 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +12 -11
- package/package.json +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { resolveOrder } from "../resolve.ts";
|
|
3
|
-
import { authenticatedUser,
|
|
4
|
+
import { authenticatedUser, retailSlot, retailTool, setFocus } from "../store.ts";
|
|
4
5
|
import { assertCanCoverDiff, planItemSwap } from "../swap.ts";
|
|
5
6
|
|
|
6
7
|
export const exchangeDeliveredOrderItems = retailTool({
|
|
@@ -34,12 +35,12 @@ export const exchangeDeliveredOrderItems = retailTool({
|
|
|
34
35
|
// source order — with `summary` first, `result` in its signature can't be
|
|
35
36
|
// inferred and silently falls back to `unknown`.
|
|
36
37
|
execute: (args, ctx) => {
|
|
37
|
-
const state =
|
|
38
|
+
const state = retailSlot.get(ctx);
|
|
38
39
|
const user = authenticatedUser(state);
|
|
39
|
-
if (
|
|
40
|
+
if (isToolFailure(user)) return user;
|
|
40
41
|
|
|
41
42
|
const order = resolveOrder(state, args.order_id);
|
|
42
|
-
if (
|
|
43
|
+
if (isToolFailure(order)) return order;
|
|
43
44
|
setFocus(state, { orderId: order.order_id });
|
|
44
45
|
|
|
45
46
|
if (order.status !== "delivered") {
|
|
@@ -53,7 +54,7 @@ export const exchangeDeliveredOrderItems = retailTool({
|
|
|
53
54
|
const plan = planItemSwap(state, order, args.item_ids, args.new_item_ids, {
|
|
54
55
|
requireDifferent: false,
|
|
55
56
|
});
|
|
56
|
-
if (
|
|
57
|
+
if (isToolFailure(plan)) return plan;
|
|
57
58
|
|
|
58
59
|
const blocked = assertCanCoverDiff(user, args.payment_method_id, plan.diff);
|
|
59
60
|
if (blocked) return blocked;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { authenticateAs } from "../authenticate.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { retailSlot, retailTool } from "../store.ts";
|
|
4
4
|
|
|
5
5
|
export const findUserIdByEmail = retailTool({
|
|
6
6
|
name: "find_user_id_by_email",
|
|
@@ -17,7 +17,7 @@ export const findUserIdByEmail = retailTool({
|
|
|
17
17
|
// literal properties in source order — with `summary` first, `result` in its
|
|
18
18
|
// signature can't be inferred and silently falls back to `unknown`.
|
|
19
19
|
execute: (args, ctx) => {
|
|
20
|
-
const state =
|
|
20
|
+
const state = retailSlot.get(ctx);
|
|
21
21
|
const target = args.email.trim().toLowerCase();
|
|
22
22
|
const match = Object.values(state.store.users).find(
|
|
23
23
|
(user) => user.email.toLowerCase() === target,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { authenticateAs } from "../authenticate.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { retailSlot, retailTool } from "../store.ts";
|
|
4
4
|
|
|
5
5
|
export const findUserIdByNameZip = retailTool({
|
|
6
6
|
name: "find_user_id_by_name_zip",
|
|
@@ -16,7 +16,7 @@ export const findUserIdByNameZip = retailTool({
|
|
|
16
16
|
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
17
17
|
// is load-bearing for the generic `result` type in `summary`.
|
|
18
18
|
execute: (args, ctx) => {
|
|
19
|
-
const state =
|
|
19
|
+
const state = retailSlot.get(ctx);
|
|
20
20
|
const first = args.first_name.trim().toLowerCase();
|
|
21
21
|
const last = args.last_name.trim().toLowerCase();
|
|
22
22
|
const zip = args.zip.replace(/\D/g, "");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { findItem,
|
|
3
|
+
import { findItem, retailSlot, retailTool } from "../store.ts";
|
|
3
4
|
|
|
4
5
|
export const getItemDetails = retailTool({
|
|
5
6
|
name: "get_item_details",
|
|
@@ -13,8 +14,8 @@ export const getItemDetails = retailTool({
|
|
|
13
14
|
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
14
15
|
// is load-bearing for the generic `result` type in `summary`.
|
|
15
16
|
execute: (args, ctx) => {
|
|
16
|
-
const found = findItem(
|
|
17
|
-
if (
|
|
17
|
+
const found = findItem(retailSlot.get(ctx), args.item_id);
|
|
18
|
+
if (isToolFailure(found)) return found;
|
|
18
19
|
return {
|
|
19
20
|
item_id: found.variant.item_id,
|
|
20
21
|
product_id: found.product.product_id,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { resolveOrder } from "../resolve.ts";
|
|
3
|
-
import {
|
|
4
|
+
import { retailSlot, retailTool, setFocus } from "../store.ts";
|
|
4
5
|
|
|
5
6
|
export const getOrderDetails = retailTool({
|
|
6
7
|
name: "get_order_details",
|
|
@@ -17,9 +18,9 @@ export const getOrderDetails = retailTool({
|
|
|
17
18
|
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
18
19
|
// is load-bearing for the generic `result` type in `summary`.
|
|
19
20
|
execute: (args, ctx) => {
|
|
20
|
-
const state =
|
|
21
|
+
const state = retailSlot.get(ctx);
|
|
21
22
|
const order = resolveOrder(state, args.order_id);
|
|
22
|
-
if (
|
|
23
|
+
if (isToolFailure(order)) return order;
|
|
23
24
|
setFocus(state, { orderId: order.order_id });
|
|
24
25
|
return {
|
|
25
26
|
order_id: order.order_id,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { findProduct,
|
|
3
|
+
import { findProduct, retailSlot, retailTool, setFocus } from "../store.ts";
|
|
3
4
|
|
|
4
5
|
export const getProductDetails = retailTool({
|
|
5
6
|
name: "get_product_details",
|
|
@@ -13,9 +14,9 @@ export const getProductDetails = retailTool({
|
|
|
13
14
|
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
14
15
|
// is load-bearing for the generic `result` type in `summary`.
|
|
15
16
|
execute: (args, ctx) => {
|
|
16
|
-
const state =
|
|
17
|
+
const state = retailSlot.get(ctx);
|
|
17
18
|
const product = findProduct(state, args.product_id);
|
|
18
|
-
if (
|
|
19
|
+
if (isToolFailure(product)) return product;
|
|
19
20
|
setFocus(state, { productId: product.product_id });
|
|
20
21
|
return {
|
|
21
22
|
name: product.name,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
|
-
import { authenticatedUser,
|
|
3
|
+
import { authenticatedUser, retailSlot, retailTool } from "../store.ts";
|
|
3
4
|
|
|
4
5
|
export const getUserDetails = retailTool({
|
|
5
6
|
name: "get_user_details",
|
|
@@ -12,9 +13,9 @@ export const getUserDetails = retailTool({
|
|
|
12
13
|
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
13
14
|
// is load-bearing for the generic `result` type in `summary`.
|
|
14
15
|
execute: (args, ctx) => {
|
|
15
|
-
const state =
|
|
16
|
+
const state = retailSlot.get(ctx);
|
|
16
17
|
const user = authenticatedUser(state);
|
|
17
|
-
if (
|
|
18
|
+
if (isToolFailure(user)) return user;
|
|
18
19
|
if (user.user_id !== args.user_id) {
|
|
19
20
|
return {
|
|
20
21
|
error: `${args.user_id} is not the customer on this call. You can help only one customer per conversation.`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { retailSlot, retailTool } from "../store.ts";
|
|
3
3
|
|
|
4
4
|
export const listAllProductTypes = retailTool({
|
|
5
5
|
name: "list_all_product_types",
|
|
@@ -13,7 +13,7 @@ export const listAllProductTypes = retailTool({
|
|
|
13
13
|
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
14
14
|
// is load-bearing for the generic `result` type in `summary`.
|
|
15
15
|
execute: (_args, ctx) => {
|
|
16
|
-
const state =
|
|
16
|
+
const state = retailSlot.get(ctx);
|
|
17
17
|
const entries = Object.values(state.store.products)
|
|
18
18
|
.map((product) => [product.name, product.product_id] as const)
|
|
19
19
|
.sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { AddressFields, formatAddress, toAddress } from "../address.ts";
|
|
3
4
|
import { resolveOrder } from "../resolve.ts";
|
|
4
|
-
import {
|
|
5
|
+
import { retailSlot, retailTool, setFocus } from "../store.ts";
|
|
5
6
|
|
|
6
7
|
export const modifyPendingOrderAddress = retailTool({
|
|
7
8
|
name: "modify_pending_order_address",
|
|
@@ -20,9 +21,9 @@ export const modifyPendingOrderAddress = retailTool({
|
|
|
20
21
|
// source order — with `summary` first, `result` in its signature can't be
|
|
21
22
|
// inferred and silently falls back to `unknown`.
|
|
22
23
|
execute: (args, ctx) => {
|
|
23
|
-
const state =
|
|
24
|
+
const state = retailSlot.get(ctx);
|
|
24
25
|
const order = resolveOrder(state, args.order_id);
|
|
25
|
-
if (
|
|
26
|
+
if (isToolFailure(order)) return order;
|
|
26
27
|
setFocus(state, { orderId: order.order_id });
|
|
27
28
|
|
|
28
29
|
// Any pending variant is fine here — unlike cancel and modify-items, which
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { resolveOrder } from "../resolve.ts";
|
|
3
4
|
import {
|
|
4
5
|
authenticatedUser,
|
|
5
|
-
getState,
|
|
6
|
-
isError,
|
|
7
6
|
isGiftCard,
|
|
8
7
|
money,
|
|
8
|
+
retailSlot,
|
|
9
9
|
retailTool,
|
|
10
10
|
setFocus,
|
|
11
11
|
} from "../store.ts";
|
|
@@ -42,12 +42,12 @@ export const modifyPendingOrderItems = retailTool({
|
|
|
42
42
|
// source order — with `summary` first, `result` in its signature can't be
|
|
43
43
|
// inferred and silently falls back to `unknown`.
|
|
44
44
|
execute: (args, ctx) => {
|
|
45
|
-
const state =
|
|
45
|
+
const state = retailSlot.get(ctx);
|
|
46
46
|
const user = authenticatedUser(state);
|
|
47
|
-
if (
|
|
47
|
+
if (isToolFailure(user)) return user;
|
|
48
48
|
|
|
49
49
|
const order = resolveOrder(state, args.order_id);
|
|
50
|
-
if (
|
|
50
|
+
if (isToolFailure(order)) return order;
|
|
51
51
|
setFocus(state, { orderId: order.order_id });
|
|
52
52
|
|
|
53
53
|
// Exactly 'pending'. A 'pending (item modified)' order has already used its
|
|
@@ -61,7 +61,7 @@ export const modifyPendingOrderItems = retailTool({
|
|
|
61
61
|
const plan = planItemSwap(state, order, args.item_ids, args.new_item_ids, {
|
|
62
62
|
requireDifferent: true,
|
|
63
63
|
});
|
|
64
|
-
if (
|
|
64
|
+
if (isToolFailure(plan)) return plan;
|
|
65
65
|
|
|
66
66
|
const blocked = assertCanCoverDiff(user, args.payment_method_id, plan.diff);
|
|
67
67
|
if (blocked) return blocked;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { resolveOrder } from "../resolve.ts";
|
|
3
4
|
import {
|
|
4
5
|
authenticatedUser,
|
|
5
6
|
findPaymentMethod,
|
|
6
|
-
getState,
|
|
7
|
-
isError,
|
|
8
7
|
isGiftCard,
|
|
9
8
|
money,
|
|
9
|
+
retailSlot,
|
|
10
10
|
retailTool,
|
|
11
11
|
setFocus,
|
|
12
12
|
} from "../store.ts";
|
|
@@ -32,12 +32,12 @@ export const modifyPendingOrderPayment = retailTool({
|
|
|
32
32
|
// source order — with `summary` first, `result` in its signature can't be
|
|
33
33
|
// inferred and silently falls back to `unknown`.
|
|
34
34
|
execute: (args, ctx) => {
|
|
35
|
-
const state =
|
|
35
|
+
const state = retailSlot.get(ctx);
|
|
36
36
|
const user = authenticatedUser(state);
|
|
37
|
-
if (
|
|
37
|
+
if (isToolFailure(user)) return user;
|
|
38
38
|
|
|
39
39
|
const order = resolveOrder(state, args.order_id);
|
|
40
|
-
if (
|
|
40
|
+
if (isToolFailure(order)) return order;
|
|
41
41
|
setFocus(state, { orderId: order.order_id });
|
|
42
42
|
|
|
43
43
|
if (!order.status.startsWith("pending")) {
|
|
@@ -47,7 +47,7 @@ export const modifyPendingOrderPayment = retailTool({
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const newMethod = findPaymentMethod(user, args.payment_method_id);
|
|
50
|
-
if (
|
|
50
|
+
if (isToolFailure(newMethod)) return newMethod;
|
|
51
51
|
|
|
52
52
|
const original = order.payment_history[0];
|
|
53
53
|
if (order.payment_history.length !== 1 || original?.transaction_type !== "payment") {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { AddressFields, formatAddress, toAddress } from "../address.ts";
|
|
3
|
-
import { authenticatedUser,
|
|
4
|
+
import { authenticatedUser, retailSlot, retailTool } from "../store.ts";
|
|
4
5
|
|
|
5
6
|
export const modifyUserAddress = retailTool({
|
|
6
7
|
name: "modify_user_address",
|
|
@@ -17,9 +18,9 @@ export const modifyUserAddress = retailTool({
|
|
|
17
18
|
// source order — with `summary` first, `result` in its signature can't be
|
|
18
19
|
// inferred and silently falls back to `unknown`.
|
|
19
20
|
execute: (args, ctx) => {
|
|
20
|
-
const state =
|
|
21
|
+
const state = retailSlot.get(ctx);
|
|
21
22
|
const user = authenticatedUser(state);
|
|
22
|
-
if (
|
|
23
|
+
if (isToolFailure(user)) return user;
|
|
23
24
|
if (user.user_id !== args.user_id) {
|
|
24
25
|
return {
|
|
25
26
|
error: `${args.user_id} is not the customer on this call. You can help only one customer per conversation.`,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { resolveOrder } from "../resolve.ts";
|
|
3
4
|
import {
|
|
4
5
|
authenticatedUser,
|
|
5
6
|
findPaymentMethod,
|
|
6
|
-
getState,
|
|
7
|
-
isError,
|
|
8
7
|
isGiftCard,
|
|
8
|
+
retailSlot,
|
|
9
9
|
retailTool,
|
|
10
10
|
setFocus,
|
|
11
11
|
} from "../store.ts";
|
|
@@ -37,12 +37,12 @@ export const returnDeliveredOrderItems = retailTool({
|
|
|
37
37
|
// source order — with `summary` first, `result` in its signature can't be
|
|
38
38
|
// inferred and silently falls back to `unknown`.
|
|
39
39
|
execute: (args, ctx) => {
|
|
40
|
-
const state =
|
|
40
|
+
const state = retailSlot.get(ctx);
|
|
41
41
|
const user = authenticatedUser(state);
|
|
42
|
-
if (
|
|
42
|
+
if (isToolFailure(user)) return user;
|
|
43
43
|
|
|
44
44
|
const order = resolveOrder(state, args.order_id);
|
|
45
|
-
if (
|
|
45
|
+
if (isToolFailure(order)) return order;
|
|
46
46
|
setFocus(state, { orderId: order.order_id });
|
|
47
47
|
|
|
48
48
|
if (order.status !== "delivered") {
|
|
@@ -52,7 +52,7 @@ export const returnDeliveredOrderItems = retailTool({
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const method = findPaymentMethod(user, args.payment_method_id);
|
|
55
|
-
if (
|
|
55
|
+
if (isToolFailure(method)) return method;
|
|
56
56
|
|
|
57
57
|
const originalMethodId = order.payment_history[0]?.payment_method_id;
|
|
58
58
|
if (!isGiftCard(method) && args.payment_method_id !== originalMethodId) {
|
|
@@ -1,15 +1,16 @@
|
|
|
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
|
-
|
|
13
|
+
type StateSlot,
|
|
13
14
|
} from "./shared.ts";
|
|
14
15
|
import { actionRoll } from "./tools/action_roll.ts";
|
|
15
16
|
import { burnMomentum } from "./tools/burn_momentum.ts";
|
|
@@ -46,19 +47,10 @@ function makeDb(): { db: Db; rows: Map<string, unknown> } {
|
|
|
46
47
|
return { db, rows };
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
db,
|
|
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. */
|
|
52
|
+
function makeCtx(sessionId = "session-a", db: Db = makeDb().db): ToolContext<StateSlot> {
|
|
53
|
+
return createToolContext<StateSlot>({ sessionId, db, send: vi.fn() });
|
|
62
54
|
}
|
|
63
55
|
|
|
64
56
|
const SETUP_ARGS = {
|
|
@@ -106,19 +98,19 @@ describe("setup_character", () => {
|
|
|
106
98
|
await setupCharacter.execute(SETUP_ARGS, ctx);
|
|
107
99
|
|
|
108
100
|
// Simulate a played, damaged game between setups.
|
|
109
|
-
const played =
|
|
101
|
+
const played = gameSlot.get(ctx);
|
|
110
102
|
played.health = 1;
|
|
111
103
|
played.momentum = -4;
|
|
112
104
|
played.chaosFactor = 8;
|
|
113
105
|
played.sceneCount = 42;
|
|
114
|
-
|
|
106
|
+
gameSlot.set(ctx, played);
|
|
115
107
|
|
|
116
108
|
const result = (await setupCharacter.execute(
|
|
117
109
|
{ ...SETUP_ARGS, playerName: "Luna" },
|
|
118
110
|
ctx,
|
|
119
111
|
)) as Record<string, unknown>;
|
|
120
112
|
|
|
121
|
-
const state =
|
|
113
|
+
const state = gameSlot.get(ctx);
|
|
122
114
|
expect(state.npcs).toHaveLength(1);
|
|
123
115
|
expect(state.npcs[0]?.id).toBe("npc_1");
|
|
124
116
|
expect(state.clocks).toHaveLength(1);
|
|
@@ -140,7 +132,7 @@ describe("setup_character", () => {
|
|
|
140
132
|
test("stats are a permutation of [3,2,2,1,1] with the archetype's stat at 3", async () => {
|
|
141
133
|
const ctx = makeCtx();
|
|
142
134
|
await setupCharacter.execute(SETUP_ARGS, ctx);
|
|
143
|
-
const state =
|
|
135
|
+
const state = gameSlot.get(ctx);
|
|
144
136
|
const stats = [state.edge, state.heart, state.iron, state.shadow, state.wits];
|
|
145
137
|
expect([...stats].sort()).toEqual([1, 1, 2, 2, 3]);
|
|
146
138
|
// investigator biases wits (index 4) to the high stat
|
|
@@ -267,7 +259,7 @@ describe("burn_momentum", () => {
|
|
|
267
259
|
clockTicks: 1,
|
|
268
260
|
},
|
|
269
261
|
};
|
|
270
|
-
|
|
262
|
+
gameSlot.set(ctx, state);
|
|
271
263
|
}
|
|
272
264
|
|
|
273
265
|
test("a legal burn reverts the miss's consequences, upgrades, and resets momentum", async () => {
|
|
@@ -278,7 +270,7 @@ describe("burn_momentum", () => {
|
|
|
278
270
|
expect(result.burned).toBe(true);
|
|
279
271
|
expect(result.newResultCode).toBe("STRONG_HIT");
|
|
280
272
|
|
|
281
|
-
const state =
|
|
273
|
+
const state = gameSlot.get(ctx);
|
|
282
274
|
expect(state.health).toBe(5); // -2 reverted
|
|
283
275
|
expect(state.clocks[0]?.filled).toBe(0); // tick reverted
|
|
284
276
|
expect(state.momentum).toBe(2); // reset, overriding the strong hit's gain
|
|
@@ -306,21 +298,21 @@ describe("burn_momentum", () => {
|
|
|
306
298
|
|
|
307
299
|
// Strong hits cannot be upgraded
|
|
308
300
|
seedRolledState(8, ctx);
|
|
309
|
-
const state =
|
|
301
|
+
const state = gameSlot.get(ctx);
|
|
310
302
|
state.lastRoll!.result = "STRONG_HIT";
|
|
311
|
-
|
|
303
|
+
gameSlot.set(ctx, state);
|
|
312
304
|
result = (await burnMomentum.execute({} as never, ctx)) as Record<string, unknown>;
|
|
313
305
|
expect(result.error).toMatch(/already a Strong Hit/);
|
|
314
306
|
});
|
|
315
307
|
|
|
316
308
|
test("action_roll persists the roll so burn needs no dice arguments", async () => {
|
|
317
309
|
const ctx = makeCtx();
|
|
318
|
-
|
|
310
|
+
gameSlot.set(ctx, playingState());
|
|
319
311
|
await actionRoll.execute(
|
|
320
312
|
{ move: "clash", stat: "iron", position: "risky", effect: "standard", purpose: "attack" },
|
|
321
313
|
ctx,
|
|
322
314
|
);
|
|
323
|
-
const state =
|
|
315
|
+
const state = gameSlot.get(ctx);
|
|
324
316
|
expect(state.lastRoll).not.toBeNull();
|
|
325
317
|
expect(state.lastRoll?.move).toBe("clash");
|
|
326
318
|
expect(state.lastRoll?.deltas).toBeDefined();
|
|
@@ -372,13 +364,13 @@ describe("rollAction", () => {
|
|
|
372
364
|
describe("update_state", () => {
|
|
373
365
|
test("clock ids never collide after a removal (max-scan, not length+1)", async () => {
|
|
374
366
|
const ctx = makeCtx();
|
|
375
|
-
|
|
367
|
+
gameSlot.set(ctx, playingState()); // has clock_1
|
|
376
368
|
|
|
377
369
|
await updateState.execute({ addClockName: "Second" }, ctx); // clock_2
|
|
378
370
|
await updateState.execute({ removeClockName: "Doom" }, ctx); // removes clock_1
|
|
379
371
|
await updateState.execute({ addClockName: "Third" }, ctx);
|
|
380
372
|
|
|
381
|
-
const state =
|
|
373
|
+
const state = gameSlot.get(ctx);
|
|
382
374
|
const ids = state.clocks.map((c) => c.id);
|
|
383
375
|
expect(new Set(ids).size).toBe(ids.length);
|
|
384
376
|
expect(ids).toEqual(["clock_2", "clock_3"]);
|
|
@@ -388,7 +380,7 @@ describe("update_state", () => {
|
|
|
388
380
|
const ctx = makeCtx();
|
|
389
381
|
const state = playingState();
|
|
390
382
|
state.clocks[0]!.filled = 3; // 3 of 4
|
|
391
|
-
|
|
383
|
+
gameSlot.set(ctx, state);
|
|
392
384
|
|
|
393
385
|
const result = (await updateState.execute({ advanceClockName: "Doom" }, ctx)) as {
|
|
394
386
|
clockEvents: { clock: string; trigger: string }[];
|
|
@@ -402,13 +394,13 @@ describe("update_state", () => {
|
|
|
402
394
|
while (state.npcs.length < MAX_NPCS) {
|
|
403
395
|
state.npcs.push(makeNpc({ id: `npc_${state.npcs.length + 1}`, name: "Extra" }));
|
|
404
396
|
}
|
|
405
|
-
|
|
397
|
+
gameSlot.set(ctx, state);
|
|
406
398
|
|
|
407
399
|
const result = (await updateState.execute({ addNpcName: "One Too Many" }, ctx)) as {
|
|
408
400
|
warnings?: string[];
|
|
409
401
|
};
|
|
410
402
|
expect(result.warnings?.[0]).toMatch(/NPC limit/);
|
|
411
|
-
const after =
|
|
403
|
+
const after = gameSlot.get(ctx);
|
|
412
404
|
expect(after.npcs).toHaveLength(MAX_NPCS);
|
|
413
405
|
});
|
|
414
406
|
|
|
@@ -442,7 +434,7 @@ describe("save_game / load_game", () => {
|
|
|
442
434
|
const played = playingState();
|
|
443
435
|
played.playerName = "Kael";
|
|
444
436
|
played.sceneCount = 7;
|
|
445
|
-
|
|
437
|
+
gameSlot.set(sessionA, played);
|
|
446
438
|
const saved = (await saveGame.execute({ slot: "chapter-2" }, sessionA)) as Record<
|
|
447
439
|
string,
|
|
448
440
|
unknown
|
|
@@ -460,7 +452,7 @@ describe("save_game / load_game", () => {
|
|
|
460
452
|
expect(loaded.loaded).toBe(true);
|
|
461
453
|
expect(loaded.playerName).toBe("Kael");
|
|
462
454
|
expect(loaded.sceneCount).toBe(7);
|
|
463
|
-
expect(
|
|
455
|
+
expect(gameSlot.get(sessionB).playerName).toBe("Kael");
|
|
464
456
|
});
|
|
465
457
|
|
|
466
458
|
test("loading a missing slot reports an error instead of resetting the game", async () => {
|
|
@@ -472,9 +464,9 @@ describe("save_game / load_game", () => {
|
|
|
472
464
|
test("saving twice to one slot upserts — the newer save wins", async () => {
|
|
473
465
|
const { db, rows } = makeDb();
|
|
474
466
|
const ctx = makeCtx("session-a", db);
|
|
475
|
-
|
|
467
|
+
gameSlot.set(ctx, playingState());
|
|
476
468
|
await saveGame.execute({}, ctx); // autosave
|
|
477
|
-
|
|
469
|
+
gameSlot.get(ctx).sceneCount = 9;
|
|
478
470
|
await saveGame.execute({}, ctx);
|
|
479
471
|
expect(rows.size).toBe(1);
|
|
480
472
|
expect(rows.get("save:autosave")).toMatchObject({ sceneCount: 9 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { gameSlot } from "./shared.ts";
|
|
3
3
|
import systemPrompt from "./system-prompt.md?raw";
|
|
4
4
|
import { actionRoll } from "./tools/action_roll.ts";
|
|
5
5
|
import { burnMomentum } from "./tools/burn_momentum.ts";
|
|
@@ -22,7 +22,7 @@ export default agent({
|
|
|
22
22
|
// One declaration replaces a `ctx.send("game_state", state)` in every
|
|
23
23
|
// state-mutating tool — six of them, and adding a seventh meant
|
|
24
24
|
// remembering to push or watching the UI quietly fall out of sync.
|
|
25
|
-
syncState:
|
|
25
|
+
syncState: gameSlot.read,
|
|
26
26
|
tools: {
|
|
27
27
|
action_roll: actionRoll,
|
|
28
28
|
burn_momentum: burnMomentum,
|
|
@@ -771,7 +771,7 @@ function SoloRPGApp() {
|
|
|
771
771
|
// The agent's own session state, projected by `syncState` and pushed after
|
|
772
772
|
// every tool call — no per-tool `ctx.send`, and nothing to keep in step
|
|
773
773
|
// when a new tool starts mutating the game.
|
|
774
|
-
const game = useAgentState<GameState>()
|
|
774
|
+
const game = useAgentState<GameState>(DEFAULT_STATE);
|
|
775
775
|
|
|
776
776
|
return (
|
|
777
777
|
<StartScreen
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SlotStateOf, sessionSlot, type ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
// ── Tuning Constants ─────────────────────────────────────────────────────────
|
|
@@ -324,20 +324,12 @@ export const DEFAULT_STATE: GameState = {
|
|
|
324
324
|
// The in-play game lives in `ctx.state`, the agent's per-session mutable
|
|
325
325
|
// state — concurrent players get independent games by construction, and the
|
|
326
326
|
// live game needs no persistence of its own (that's what save slots are for).
|
|
327
|
-
|
|
327
|
+
// The clone is load-bearing: `DEFAULT_STATE` is one module-level object shared
|
|
328
|
+
// by every session in the process, so a mutation without it would let one
|
|
329
|
+
// player's game show up in another's.
|
|
330
|
+
export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_STATE));
|
|
328
331
|
|
|
329
|
-
|
|
330
|
-
* the object stored in `ctx.state`. */
|
|
331
|
-
export function getGameState(ctx: ToolContext): GameState {
|
|
332
|
-
const slot = ctx.state as StateSlot;
|
|
333
|
-
slot.game ??= structuredClone(DEFAULT_STATE);
|
|
334
|
-
return slot.game;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/** Replace the session's live game wholesale (setup, load). */
|
|
338
|
-
export function saveGameState(ctx: ToolContext, state: GameState): void {
|
|
339
|
-
(ctx.state as StateSlot).game = state;
|
|
340
|
-
}
|
|
332
|
+
export type StateSlot = SlotStateOf<typeof gameSlot>;
|
|
341
333
|
|
|
342
334
|
// ── Persistent save slots (ctx.db) ───────────────────────────────────────────
|
|
343
335
|
// save_game / load_game are genuine cross-session persistence, so they use
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
applyConsequences,
|
|
5
5
|
canBurnMomentum,
|
|
6
6
|
checkChaosInterrupt,
|
|
7
|
-
|
|
7
|
+
gameSlot,
|
|
8
8
|
MOVE_LABELS,
|
|
9
9
|
MOVES,
|
|
10
10
|
RESULT_LABELS,
|
|
@@ -26,7 +26,7 @@ export const actionRoll = tool({
|
|
|
26
26
|
targetNpcId: z.string().max(32).describe("Target NPC id for social moves").optional(),
|
|
27
27
|
}),
|
|
28
28
|
async execute(args, ctx) {
|
|
29
|
-
const state =
|
|
29
|
+
const state = gameSlot.get(ctx);
|
|
30
30
|
const statValue = state[args.stat];
|
|
31
31
|
const roll = rollAction(args.stat, statValue, args.move);
|
|
32
32
|
|
|
@@ -2,7 +2,7 @@ import { tool } from "@alexkroman1/aai";
|
|
|
2
2
|
import {
|
|
3
3
|
applyConsequences,
|
|
4
4
|
canBurnMomentum,
|
|
5
|
-
|
|
5
|
+
gameSlot,
|
|
6
6
|
MOMENTUM_RESET,
|
|
7
7
|
RESULT_LABELS,
|
|
8
8
|
revertConsequences,
|
|
@@ -12,7 +12,7 @@ export const burnMomentum = tool({
|
|
|
12
12
|
description:
|
|
13
13
|
"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
14
|
async execute(_args, ctx) {
|
|
15
|
-
const state =
|
|
15
|
+
const state = gameSlot.get(ctx);
|
|
16
16
|
const last = state.lastRoll;
|
|
17
17
|
if (!last) return { error: "No recent action roll to upgrade. Roll first." };
|
|
18
18
|
if (last.result === "STRONG_HIT") {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
|
-
import {
|
|
2
|
+
import { gameSlot, stateSummary } from "../shared.ts";
|
|
3
3
|
|
|
4
4
|
export const checkState = tool({
|
|
5
5
|
description:
|
|
6
6
|
"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
7
|
async execute(_args, ctx) {
|
|
8
|
-
const state =
|
|
8
|
+
const state = gameSlot.get(ctx);
|
|
9
9
|
return stateSummary(state);
|
|
10
10
|
},
|
|
11
11
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
@@ -11,7 +11,7 @@ export const loadGame = tool({
|
|
|
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,
|