@alexkroman1/aai-cli 13.2.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.mjs +1 -1
- package/dist/{_agent-BTwZJu4P.mjs → _agent-BzUeqOdj.mjs} +33 -10
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-C31rlcYz.mjs → _bundler-DolUCMxu.mjs} +9 -8
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-CmJOFsAP.mjs → _config-DYzC6WMD.mjs} +7 -4
- package/dist/_config.d.ts +25 -1
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-DWvkNoIS.mjs → _dev-server-CSMqF8PN.mjs} +28 -30
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +15 -7
- package/dist/{_init-BURbdHNM.mjs → _init-CQ8idAwo.mjs} +64 -30
- package/dist/{_output-CKkmWs7i.mjs → _output-DBP9Op_d.mjs} +43 -3
- package/dist/_output.d.ts +24 -7
- package/dist/{_preflight-Co1G9Jww.mjs → _preflight-CcAR577V.mjs} +3 -3
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-6e0QI4mq.mjs → _server-common-vILJp3it.mjs} +3 -3
- package/dist/{_slug-api-CfGQq1Tt.mjs → _slug-api-CGvc2YSQ.mjs} +19 -10
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-B1waXMuR.mjs → _studio-DnR_BqFp.mjs} +9 -5
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-0Zr2z6yA.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-BXvUNnfs.mjs → _typecheck-gate-U3jcil8L.mjs} +5 -4
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_ui-u7T4YooX.mjs → _ui-DCt4qZrk.mjs} +1 -1
- package/dist/{_utils-B8QmtFhK.mjs → _utils-D5JGcjiW.mjs} +44 -2
- package/dist/_utils.d.ts +35 -0
- package/dist/{_vite-env-D9Qj6KPX.mjs → _vite-env-kejF0p2L.mjs} +1 -1
- package/dist/{_workflow-determinism-f9J-5Zlj.mjs → _workflow-determinism-CvpqMpdh.mjs} +19 -27
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-Mxk8gWvX.mjs +108 -0
- package/dist/build.d.ts +5 -0
- package/dist/cli.mjs +172 -116
- package/dist/{client-bundler-j5c46x0G.mjs → client-bundler-BJgREAh6.mjs} +4 -4
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-X_CRgCf6.mjs → delete-BdFnbnVR.mjs} +6 -6
- package/dist/{deploy-BzZ-r4iM.mjs → deploy-uAJ4NukN.mjs} +14 -14
- package/dist/{dev-BDykFvcc.mjs → dev-DApPSaE_.mjs} +5 -5
- package/dist/{eval-0qEOdm0l.mjs → eval-BK47A_K5.mjs} +27 -11
- package/dist/eval.d.ts +2 -0
- package/dist/{init-BtJI306C.mjs → init-DukDxECd.mjs} +13 -21
- package/dist/{login-DeUETobb.mjs → login-DaLrPOFg.mjs} +6 -8
- package/dist/{logs-hWnJ66Bl.mjs → logs-7BRwq0cG.mjs} +22 -15
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/.env.example +17 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +6 -5
- package/dist/scaffold/server.mjs +129 -25
- package/dist/secret-Cjp912yF.mjs +162 -0
- package/dist/secret.d.ts +48 -13
- package/dist/{studio-BxAS_FQQ.mjs → studio-CpHlNHUZ.mjs} +15 -17
- package/dist/templates/briefing-desk/agent.test.ts +7 -3
- package/dist/templates/call-audit/agent.test.ts +25 -9
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +40 -58
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/embedded-assets/agent.test.ts +7 -1
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.test.ts +8 -4
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +10 -4
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +42 -36
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/agent.test.ts +6 -1
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +33 -16
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +33 -14
- package/dist/templates/podcast-digest/client.tsx +27 -36
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +49 -32
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +8 -2
- package/dist/templates/redline/client.tsx +47 -48
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +17 -8
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/registry.test.ts +99 -58
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/simple/agent.test.ts +39 -6
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +21 -12
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +36 -47
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +36 -20
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +34 -40
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/test-D032V2cA.mjs +242 -0
- package/dist/test.d.ts +97 -31
- package/dist/typecheck.mjs +2 -2
- package/dist/worker-bundler.mjs +3 -3
- package/dist/{workflow-BBu5oEDw.mjs → workflow-CVioyO3V.mjs} +8 -7
- package/package.json +10 -10
- package/dist/bin.d.mts +0 -2
- package/dist/build-BLlLOrng.mjs +0 -66
- package/dist/secret-B-3T5tvr.mjs +0 -61
- package/dist/test-CiLab-AA.mjs +0 -182
- package/dist/tsdown.config.d.ts +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { isToolFailure } from "@alexkroman1/aai";
|
|
3
|
-
import { createToolContext,
|
|
3
|
+
import { createToolContext, expectDialogOk, expectToolOk } from "@alexkroman1/aai/testing";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
import type { StagedResult } from "./pending.ts";
|
|
6
6
|
import { retailSlot } from "./store.ts";
|
|
@@ -17,7 +17,7 @@ import transferToHumanAgents from "./tools/transfer_to_human_agents.ts";
|
|
|
17
17
|
* one delivered order (#W4316152), which is every shape these cases need. */
|
|
18
18
|
async function aaravCtx(): Promise<ToolContext> {
|
|
19
19
|
const ctx = createToolContext();
|
|
20
|
-
|
|
20
|
+
expectToolOk(await findUserIdByEmail.execute({ email: "aarav.anderson9752@example.com" }, ctx));
|
|
21
21
|
return ctx;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ describe("staging", () => {
|
|
|
29
29
|
const ctx = await aaravCtx();
|
|
30
30
|
const before = structuredClone(retailSlot.get(ctx).store.orders["#W9300146"]);
|
|
31
31
|
|
|
32
|
-
const staged =
|
|
32
|
+
const staged = expectDialogOk<StagedResult>(await stageCancel(ctx));
|
|
33
33
|
|
|
34
34
|
expect(staged.state).toBe("serving.awaitingConfirmation");
|
|
35
35
|
expect(staged.result.staged).toBe("cancel_pending_order");
|
|
@@ -43,7 +43,7 @@ describe("staging", () => {
|
|
|
43
43
|
|
|
44
44
|
test("the readback names the order, the items, the amount and where the money goes", async () => {
|
|
45
45
|
const ctx = await aaravCtx();
|
|
46
|
-
const staged =
|
|
46
|
+
const staged = expectToolOk<StagedResult>(await stageCancel(ctx));
|
|
47
47
|
expect(staged.read_back).toContain("#W9300146");
|
|
48
48
|
expect(staged.read_back).toContain("153.23");
|
|
49
49
|
expect(staged.read_back).toContain("gift_card_7245904");
|
|
@@ -52,14 +52,14 @@ describe("staging", () => {
|
|
|
52
52
|
|
|
53
53
|
test("the result says out loud that nothing has happened", async () => {
|
|
54
54
|
const ctx = await aaravCtx();
|
|
55
|
-
const staged =
|
|
55
|
+
const staged = expectToolOk<StagedResult>(await stageCancel(ctx));
|
|
56
56
|
expect(staged.message).toContain("NOTHING HAS CHANGED YET");
|
|
57
57
|
expect(staged.message).toContain("confirm_change");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test("a second stage is refused, and the refusal names the one already waiting", async () => {
|
|
61
61
|
const ctx = await aaravCtx();
|
|
62
|
-
|
|
62
|
+
expectToolOk(await stageCancel(ctx));
|
|
63
63
|
|
|
64
64
|
// A DIFFERENT change, so this is not a repeat of the first call.
|
|
65
65
|
const second = await modifyUserAddress.execute(
|
|
@@ -89,9 +89,9 @@ describe("staging", () => {
|
|
|
89
89
|
describe("confirming", () => {
|
|
90
90
|
test("confirm_change applies the staged change and returns to helping", async () => {
|
|
91
91
|
const ctx = await aaravCtx();
|
|
92
|
-
|
|
92
|
+
expectToolOk(await stageCancel(ctx));
|
|
93
93
|
|
|
94
|
-
const done =
|
|
94
|
+
const done = expectDialogOk<{ confirmed: string; status: string }>(
|
|
95
95
|
await confirmChange.execute({}, ctx),
|
|
96
96
|
);
|
|
97
97
|
expect(done.state).toBe("serving.helping");
|
|
@@ -114,8 +114,8 @@ describe("confirming", () => {
|
|
|
114
114
|
|
|
115
115
|
test("confirming twice is refused — the second call has nothing staged", async () => {
|
|
116
116
|
const ctx = await aaravCtx();
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
expectToolOk(await stageCancel(ctx));
|
|
118
|
+
expectToolOk(await confirmChange.execute({}, ctx));
|
|
119
119
|
|
|
120
120
|
const again = await confirmChange.execute({}, ctx);
|
|
121
121
|
expect(isToolFailure(again)).toBe(true);
|
|
@@ -130,9 +130,9 @@ describe("confirming", () => {
|
|
|
130
130
|
describe("cancelling a staged change", () => {
|
|
131
131
|
test("cancel_change drops it, changes nothing, and frees the call", async () => {
|
|
132
132
|
const ctx = await aaravCtx();
|
|
133
|
-
|
|
133
|
+
expectToolOk(await stageCancel(ctx));
|
|
134
134
|
|
|
135
|
-
const dropped =
|
|
135
|
+
const dropped = expectDialogOk<{ dropped: string | null; message: string }>(
|
|
136
136
|
await cancelChange.execute({}, ctx),
|
|
137
137
|
);
|
|
138
138
|
expect(dropped.state).toBe("serving.helping");
|
|
@@ -144,12 +144,12 @@ describe("cancelling a staged change", () => {
|
|
|
144
144
|
|
|
145
145
|
test("a corrected change can be staged straight after", async () => {
|
|
146
146
|
const ctx = await aaravCtx();
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
expectToolOk(await stageCancel(ctx));
|
|
148
|
+
expectToolOk(await cancelChange.execute({}, ctx));
|
|
149
149
|
|
|
150
150
|
// The caller changed their mind about which order — the very thing
|
|
151
151
|
// `cancel_change` exists for.
|
|
152
|
-
const restaged =
|
|
152
|
+
const restaged = expectToolOk<StagedResult>(
|
|
153
153
|
await returnDeliveredOrderItems.execute(
|
|
154
154
|
{
|
|
155
155
|
order_id: "#W4316152",
|
|
@@ -166,9 +166,9 @@ describe("cancelling a staged change", () => {
|
|
|
166
166
|
describe("what stays legal while a change waits", () => {
|
|
167
167
|
test("a read is still answerable — 'what was the total again?'", async () => {
|
|
168
168
|
const ctx = await aaravCtx();
|
|
169
|
-
|
|
169
|
+
expectToolOk(await stageCancel(ctx));
|
|
170
170
|
// `when: "serving"` matches both children, which is what makes this work.
|
|
171
|
-
const read =
|
|
171
|
+
const read = expectDialogOk<{ order_id: string }>(
|
|
172
172
|
await getOrderDetails.execute({ order_id: "#W9300146" }, ctx),
|
|
173
173
|
);
|
|
174
174
|
expect(read.result.order_id).toBe("#W9300146");
|
|
@@ -178,9 +178,9 @@ describe("what stays legal while a change waits", () => {
|
|
|
178
178
|
|
|
179
179
|
test("a caller can still ask for a human, and everything refuses afterwards", async () => {
|
|
180
180
|
const ctx = await aaravCtx();
|
|
181
|
-
|
|
181
|
+
expectToolOk(await stageCancel(ctx));
|
|
182
182
|
|
|
183
|
-
const transferred =
|
|
183
|
+
const transferred = expectDialogOk(
|
|
184
184
|
await transferToHumanAgents.execute({ summary: "wants a human" }, ctx),
|
|
185
185
|
);
|
|
186
186
|
// `TRANSFERRED` is declared on the `serving` PARENT, which is what lets it
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { createToolContext, deployedAgent } from "@alexkroman1/aai/testing";
|
|
3
3
|
import { describe, expect, test } from "vitest";
|
|
4
|
-
import { callFlow, retailSlot } from "./store.ts";
|
|
5
|
-
|
|
6
|
-
/** Tools that legitimately run before the caller is identified — the six
|
|
7
|
-
* declaring `when: BEFORE_TRANSFER`. Everything else must refuse. Listed here
|
|
8
|
-
* so ADDING an unauthenticated tool is a deliberate edit to this file, not a
|
|
9
|
-
* silent gap. */
|
|
10
|
-
const PUBLIC_TOOLS = new Set([
|
|
11
|
-
"find_user_id_by_email",
|
|
12
|
-
"find_user_id_by_name_zip",
|
|
13
|
-
"get_product_details",
|
|
14
|
-
"get_item_details",
|
|
15
|
-
"list_all_product_types",
|
|
16
|
-
"transfer_to_human_agents",
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
4
|
/**
|
|
20
5
|
* The def a DEPLOYED agent runs, lowered BY HAND — the one place in the
|
|
21
6
|
* templates that still does.
|
|
@@ -27,6 +12,7 @@ const PUBLIC_TOOLS = new Set([
|
|
|
27
12
|
* whose runner is not vitest, and so cannot register the plugin.
|
|
28
13
|
*/
|
|
29
14
|
import authoredAgent from "./agent.ts";
|
|
15
|
+
import { callFlow, gateFor, retailSlot } from "./store.ts";
|
|
30
16
|
import systemPrompt from "./system-prompt.md?raw";
|
|
31
17
|
|
|
32
18
|
const retailAgent = deployedAgent(authoredAgent, {
|
|
@@ -36,6 +22,39 @@ const retailAgent = deployedAgent(authoredAgent, {
|
|
|
36
22
|
|
|
37
23
|
const registry = Object.entries(retailAgent.tools);
|
|
38
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Whether `name`'s gate admits the pre-identification state — i.e. whether it
|
|
27
|
+
* is one of the tools that legitimately runs before the caller is identified.
|
|
28
|
+
*
|
|
29
|
+
* ASKED OF THE REGISTRY, not read off a list here. This used to be a hardcoded
|
|
30
|
+
* six-name set, which meant a tool you add is classified by a file you did not
|
|
31
|
+
* write: declare `when: BEFORE_TRANSFER` on a new catalogue read and the
|
|
32
|
+
* sweeps below would have demanded it refuse. `retailTool` records every gate
|
|
33
|
+
* in `TOOL_GATES`, so the honest question is what the tool itself declared.
|
|
34
|
+
*
|
|
35
|
+
* `undefined` — a tool built with plain `tool()` rather than `retailTool` — is
|
|
36
|
+
* neither: it has no gate at all, and the sweeps skip it. That is the same
|
|
37
|
+
* answer `gateFor` gives the `tool.called` hook, and "a tool this template did
|
|
38
|
+
* not declare is ignored" below is the assertion for it.
|
|
39
|
+
*/
|
|
40
|
+
const isPublic = (name: string) => gateFor(name)?.includes("identifying") ?? false;
|
|
41
|
+
|
|
42
|
+
/** Registry entries that went through `retailTool`, so the gate has an opinion. */
|
|
43
|
+
const gatedTools = registry.filter(([name]) => gateFor(name) !== undefined);
|
|
44
|
+
|
|
45
|
+
/** The tools this template SHIPS that run before identification. A pin on the
|
|
46
|
+
* shipped set — one of these quietly losing `BEFORE_TRANSFER` is a policy
|
|
47
|
+
* break — and nothing else reads it, so adding a public tool of your own costs
|
|
48
|
+
* no edit here. */
|
|
49
|
+
const SHIPPED_PUBLIC_TOOLS = [
|
|
50
|
+
"find_user_id_by_email",
|
|
51
|
+
"find_user_id_by_name_zip",
|
|
52
|
+
"get_product_details",
|
|
53
|
+
"get_item_details",
|
|
54
|
+
"list_all_product_types",
|
|
55
|
+
"transfer_to_human_agents",
|
|
56
|
+
];
|
|
57
|
+
|
|
39
58
|
// `createToolContext()` rather than a cast: it carries a real slot store (the
|
|
40
59
|
// same storability check and freeze the deployed one applies), and each call is a
|
|
41
60
|
// distinct session, which is what these per-tool cases assume.
|
|
@@ -81,9 +100,19 @@ async function bodyReachableCtx(name: string): Promise<ToolContext> {
|
|
|
81
100
|
return ctx;
|
|
82
101
|
}
|
|
83
102
|
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
103
|
+
/**
|
|
104
|
+
* Minimal args satisfying each tool's schema. Deliberately plausible-shaped but
|
|
105
|
+
* wrong — these calls are expected to fail; what is asserted is that they still
|
|
106
|
+
* moved the UI.
|
|
107
|
+
*
|
|
108
|
+
* **It is also the list of tools this file sweeps, and the list of tools this
|
|
109
|
+
* template ships.** It used to be a second copy of a seventeen-name array
|
|
110
|
+
* asserted with `toEqual`, so adding one tool of your own failed both — the
|
|
111
|
+
* count and the coverage — before it had run once. One source now: a tool with
|
|
112
|
+
* an entry here is driven through every sweep below, and every name here must
|
|
113
|
+
* still be discovered from `tools/`. Add your tool with its arguments and it
|
|
114
|
+
* joins the sweeps; leave it out and it is simply not covered by them.
|
|
115
|
+
*/
|
|
87
116
|
const SAMPLE_ARGS: Record<string, Record<string, unknown>> = {
|
|
88
117
|
cancel_change: {},
|
|
89
118
|
cancel_pending_order: { order_id: "#W0000000", reason: "no longer needed" },
|
|
@@ -137,56 +166,56 @@ const SAMPLE_ARGS: Record<string, Record<string, unknown>> = {
|
|
|
137
166
|
transfer_to_human_agents: { summary: "test" },
|
|
138
167
|
};
|
|
139
168
|
|
|
169
|
+
/** The tools with sample args, i.e. the ones every sweep below can drive. */
|
|
170
|
+
const sweepable = registry.filter(([name]) => name in SAMPLE_ARGS);
|
|
171
|
+
|
|
140
172
|
describe("tool registry", () => {
|
|
141
|
-
test("
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"find_user_id_by_email",
|
|
154
|
-
"find_user_id_by_name_zip",
|
|
155
|
-
"get_item_details",
|
|
156
|
-
"get_order_details",
|
|
157
|
-
"get_product_details",
|
|
158
|
-
"get_user_details",
|
|
159
|
-
"list_all_product_types",
|
|
160
|
-
"modify_pending_order_address",
|
|
161
|
-
"modify_pending_order_items",
|
|
162
|
-
"modify_pending_order_payment",
|
|
163
|
-
"modify_user_address",
|
|
164
|
-
"return_delivered_order_items",
|
|
165
|
-
"transfer_to_human_agents",
|
|
166
|
-
].sort(),
|
|
173
|
+
test("discovers every tool this template ships", () => {
|
|
174
|
+
// `arrayContaining` rather than an exact sorted list. Fifteen of these are
|
|
175
|
+
// tau2's retail tool set, which this template used to hold verbatim;
|
|
176
|
+
// `confirm_change` and `cancel_change` are the two it does not have, since
|
|
177
|
+
// tau2's tools apply on their first call and here nothing does (see
|
|
178
|
+
// `pending.ts`). So the list is not a fidelity claim and never was a
|
|
179
|
+
// COUNT — what it is worth asserting is that discovery still finds each
|
|
180
|
+
// one, because a `tools/` glob that resolves nothing looks exactly like a
|
|
181
|
+
// desk with no tools. Adding a tool of your own passes; losing one of these
|
|
182
|
+
// fails, naming it.
|
|
183
|
+
expect(registry.map(([name]) => name)).toEqual(
|
|
184
|
+
expect.arrayContaining(Object.keys(SAMPLE_ARGS)),
|
|
167
185
|
);
|
|
168
186
|
});
|
|
169
187
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
test("every
|
|
173
|
-
for (const
|
|
174
|
-
expect.soft(
|
|
188
|
+
// Asserts softly: a batch of renames should list every name that no longer
|
|
189
|
+
// resolves, not just the first.
|
|
190
|
+
test("every name in SAMPLE_ARGS resolves to a real tool", () => {
|
|
191
|
+
for (const name of Object.keys(SAMPLE_ARGS)) {
|
|
192
|
+
expect.soft(retailAgent.tools[name], `SAMPLE_ARGS["${name}"] names no tool`).toBeDefined();
|
|
175
193
|
}
|
|
176
194
|
});
|
|
177
195
|
|
|
178
196
|
test("every tool declares a description the model can act on", () => {
|
|
179
197
|
for (const [name, def] of registry) {
|
|
198
|
+
// Any tool, yours included — the model picks a tool by its description,
|
|
199
|
+
// so an empty one is invisible to it.
|
|
180
200
|
expect.soft(def.description, name).toBeTruthy();
|
|
181
|
-
|
|
201
|
+
}
|
|
202
|
+
for (const name of Object.keys(SAMPLE_ARGS)) {
|
|
203
|
+
// The shipped ones additionally carry enough of the policy to be chosen
|
|
204
|
+
// correctly, which for this desk means more than a label.
|
|
205
|
+
expect.soft(retailAgent.tools[name]?.description.length, name).toBeGreaterThan(40);
|
|
182
206
|
}
|
|
183
207
|
});
|
|
184
208
|
});
|
|
185
209
|
|
|
186
210
|
describe("the UI-update invariant", () => {
|
|
187
|
-
//
|
|
211
|
+
// Over `sweepable` rather than the whole registry: these two REACH a tool's
|
|
212
|
+
// body, so they need arguments its schema accepts, and calling a tool of
|
|
213
|
+
// yours with `{}` would report a crash inside it as a UI regression. Give it
|
|
214
|
+
// a `SAMPLE_ARGS` entry and it is swept like the rest.
|
|
215
|
+
//
|
|
216
|
+
// This is the pair that fails if a future tool is built with tool() instead of
|
|
188
217
|
// retailTool(): it would work, and the sidebar would sit still through it.
|
|
189
|
-
test.each(
|
|
218
|
+
test.each(sweepable)("%s increments callSeq and logs activity", async (name, def) => {
|
|
190
219
|
// In whichever state lets the body run, so the flow gate is not what these
|
|
191
220
|
// calls are testing: the point is that a tool which reaches its BODY moves
|
|
192
221
|
// the sidebar. A refused call never reaches one — the hook below is what
|
|
@@ -200,7 +229,7 @@ describe("the UI-update invariant", () => {
|
|
|
200
229
|
expect(state.activity.at(-1)?.summary).toBeTruthy();
|
|
201
230
|
});
|
|
202
231
|
|
|
203
|
-
test.each(
|
|
232
|
+
test.each(sweepable)("%s logs its own registry key as its name", async (name, def) => {
|
|
204
233
|
// Catches a copy-paste where the retailTool `name` and the registry key
|
|
205
234
|
// disagree — the activity feed would then attribute calls to the wrong tool.
|
|
206
235
|
const ctx = await bodyReachableCtx(name);
|
|
@@ -229,7 +258,7 @@ describe("the blocked-call hook", () => {
|
|
|
229
258
|
ctx,
|
|
230
259
|
);
|
|
231
260
|
|
|
232
|
-
test.each(
|
|
261
|
+
test.each(gatedTools.filter(([name]) => !isPublic(name)))(
|
|
233
262
|
"%s records a blocked line when the model tries it too early",
|
|
234
263
|
(name) => {
|
|
235
264
|
const ctx = makeCtx();
|
|
@@ -245,7 +274,7 @@ describe("the blocked-call hook", () => {
|
|
|
245
274
|
},
|
|
246
275
|
);
|
|
247
276
|
|
|
248
|
-
test.each(
|
|
277
|
+
test.each(gatedTools.filter(([name]) => isPublic(name)))(
|
|
249
278
|
"%s is left to the wrapper, because it is going to run",
|
|
250
279
|
(name) => {
|
|
251
280
|
const ctx = makeCtx();
|
|
@@ -280,7 +309,19 @@ describe("the blocked-call hook", () => {
|
|
|
280
309
|
});
|
|
281
310
|
|
|
282
311
|
describe("the authentication gate", () => {
|
|
283
|
-
test
|
|
312
|
+
test("the tools that run before identification are still the shipped six", () => {
|
|
313
|
+
// The one place `SHIPPED_PUBLIC_TOOLS` is read, and the only assertion in
|
|
314
|
+
// this file about WHICH tools are public. The sweeps below classify by
|
|
315
|
+
// `isPublic`, i.e. by what each tool declared, so a public tool of your own
|
|
316
|
+
// needs no edit here — but one of these six quietly losing
|
|
317
|
+
// `when: BEFORE_TRANSFER` would silently make the desk unreachable before a
|
|
318
|
+
// caller is identified, and nothing else would say so.
|
|
319
|
+
expect(registry.map(([name]) => name).filter(isPublic)).toEqual(
|
|
320
|
+
expect.arrayContaining(SHIPPED_PUBLIC_TOOLS),
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test.each(gatedTools.filter(([name]) => !isPublic(name)))(
|
|
284
325
|
"%s refuses before the caller is identified",
|
|
285
326
|
async (name, def) => {
|
|
286
327
|
const result = await def.execute(SAMPLE_ARGS[name] ?? {}, makeCtx());
|
|
@@ -297,7 +338,7 @@ describe("the authentication gate", () => {
|
|
|
297
338
|
},
|
|
298
339
|
);
|
|
299
340
|
|
|
300
|
-
test.each(
|
|
341
|
+
test.each(sweepable.filter(([name]) => isPublic(name)))(
|
|
301
342
|
"%s does not require authentication",
|
|
302
343
|
async (name, def) => {
|
|
303
344
|
const result = await def.execute(SAMPLE_ARGS[name] ?? {}, makeCtx());
|
|
@@ -331,7 +372,7 @@ describe("agent config", () => {
|
|
|
331
372
|
});
|
|
332
373
|
|
|
333
374
|
describe("the transfer is terminal", () => {
|
|
334
|
-
test.each(
|
|
375
|
+
test.each(gatedTools)("%s refuses once the call is with a human", async (name, def) => {
|
|
335
376
|
const ctx = servingCtx();
|
|
336
377
|
callFlow.send(ctx, { type: "TRANSFERRED" });
|
|
337
378
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
|
|
2
|
-
import { createToolContext,
|
|
2
|
+
import { createToolContext, expectDialogOk, expectToolOk } from "@alexkroman1/aai/testing";
|
|
3
3
|
import { describe, expect, test } from "vitest";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import {
|
|
@@ -237,16 +237,16 @@ describe("retailTool", () => {
|
|
|
237
237
|
test("runs once the flow says serving", async () => {
|
|
238
238
|
const ctx = makeCtx();
|
|
239
239
|
serve(ctx);
|
|
240
|
-
expect(
|
|
240
|
+
expect(expectToolOk(await gated.execute({}, ctx))).toEqual({ ok: true });
|
|
241
241
|
});
|
|
242
242
|
|
|
243
243
|
test("the result carries the position the call landed in", async () => {
|
|
244
244
|
const ctx = makeCtx();
|
|
245
245
|
serve(ctx);
|
|
246
|
-
// `
|
|
246
|
+
// `expectDialogOk` rather than a cast to `{ instruction?: string }`: it keeps
|
|
247
247
|
// the envelope this test is about, and a refusal fails here naming what the
|
|
248
248
|
// flow said instead of reading `undefined` off a field nobody assigned.
|
|
249
|
-
const answered =
|
|
249
|
+
const answered = expectDialogOk<{ ok: boolean }>(await gated.execute({}, ctx));
|
|
250
250
|
expect(answered).toMatchObject({ state: "serving.helping", done: false });
|
|
251
251
|
expect(answered.instruction).toMatch(/one identified customer/);
|
|
252
252
|
});
|
|
@@ -307,7 +307,7 @@ describe("the call flow", () => {
|
|
|
307
307
|
summary: () => "ran",
|
|
308
308
|
execute: () => ({ ok: true }),
|
|
309
309
|
});
|
|
310
|
-
expect(
|
|
310
|
+
expect(expectToolOk(await anywhere.execute({}, ctx))).toEqual({ ok: true });
|
|
311
311
|
|
|
312
312
|
const at = callFlow.send(ctx, { type: "TRANSFERRED" });
|
|
313
313
|
expect(at.state).toBe("transferred");
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { isToolFailure, type ToolFailure } from "@alexkroman1/aai";
|
|
10
|
+
import { formatMoney } from "@alexkroman1/aai/utils";
|
|
10
11
|
import { resolveOrder } from "./resolve.ts";
|
|
11
12
|
import type { Order, OrderItem, RetailState, User, Variant } from "./shared.ts";
|
|
12
13
|
import {
|
|
@@ -48,7 +49,6 @@ export interface SwapLine {
|
|
|
48
49
|
fromOptions: Record<string, string>;
|
|
49
50
|
fromPrice: number;
|
|
50
51
|
toItemId: string;
|
|
51
|
-
toName: string;
|
|
52
52
|
toOptions: Record<string, string>;
|
|
53
53
|
toPrice: number;
|
|
54
54
|
}
|
|
@@ -146,9 +146,6 @@ export function toSwapLines(plan: SwapPlan): SwapLine[] {
|
|
|
146
146
|
fromOptions: { ...pair.item.options },
|
|
147
147
|
fromPrice: pair.item.price,
|
|
148
148
|
toItemId: pair.newVariant.item_id,
|
|
149
|
-
// Same product by construction — a swap can only reach another option of
|
|
150
|
-
// it — so the name is the item's own and the OPTIONS are what differ.
|
|
151
|
-
toName: pair.item.name,
|
|
152
149
|
toOptions: { ...pair.newVariant.options },
|
|
153
150
|
toPrice: pair.newVariant.price,
|
|
154
151
|
}));
|
|
@@ -163,7 +160,7 @@ export function assertCanCoverDiff(user: User, methodId: string, diff: number):
|
|
|
163
160
|
if (isToolFailure(method)) return method;
|
|
164
161
|
if (isGiftCard(method) && method.balance < diff) {
|
|
165
162
|
return {
|
|
166
|
-
error: `Gift card ${methodId}'s balance (
|
|
163
|
+
error: `Gift card ${methodId}'s balance (${formatMoney(method.balance)}) does not cover the ${formatMoney(diff)} difference. Ask for another payment method.`,
|
|
167
164
|
};
|
|
168
165
|
}
|
|
169
166
|
return null;
|
|
@@ -190,8 +187,8 @@ function describeLine(line: SwapLine): string {
|
|
|
190
187
|
}
|
|
191
188
|
|
|
192
189
|
function describeDiff(diff: number, methodId: string): string {
|
|
193
|
-
if (diff > 0) return
|
|
194
|
-
if (diff < 0) return
|
|
190
|
+
if (diff > 0) return `${formatMoney(diff)} charged to ${methodId}`;
|
|
191
|
+
if (diff < 0) return `${formatMoney(Math.abs(diff))} refunded to ${methodId}`;
|
|
195
192
|
return "no price difference";
|
|
196
193
|
}
|
|
197
194
|
|
|
@@ -275,8 +272,8 @@ export function applyModifyItems(state: RetailState, plan: ModifyItemsPlan) {
|
|
|
275
272
|
})),
|
|
276
273
|
message:
|
|
277
274
|
plan.diff > 0
|
|
278
|
-
? `Done.
|
|
279
|
-
: `Done.
|
|
275
|
+
? `Done. ${formatMoney(plan.diff)} was charged to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`
|
|
276
|
+
: `Done. ${formatMoney(Math.abs(plan.diff))} is being refunded to ${plan.paymentMethodId}. This order can no longer be modified or cancelled.`,
|
|
280
277
|
};
|
|
281
278
|
}
|
|
282
279
|
|
|
@@ -358,7 +355,7 @@ export function applyExchange(state: RetailState, plan: ExchangePlan) {
|
|
|
358
355
|
})),
|
|
359
356
|
message:
|
|
360
357
|
plan.diff > 0
|
|
361
|
-
? `Exchange requested on ${plan.orderId}.
|
|
362
|
-
: `Exchange requested on ${plan.orderId}.
|
|
358
|
+
? `Exchange requested on ${plan.orderId}. ${formatMoney(plan.diff)} will be charged to ${plan.paymentMethodId}. An email with return instructions is on its way.`
|
|
359
|
+
: `Exchange requested on ${plan.orderId}. ${formatMoney(Math.abs(plan.diff))} will be refunded to ${plan.paymentMethodId}. An email with return instructions is on its way.`,
|
|
363
360
|
};
|
|
364
361
|
}
|
|
@@ -2,17 +2,50 @@ import { toAgentConfig } from "@alexkroman1/aai/manifest";
|
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
3
|
import agentDef from "./agent.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* What a starter's spec may assert.
|
|
7
|
+
*
|
|
8
|
+
* Renaming the agent, giving it a voice, swapping a stage or switching the
|
|
9
|
+
* whole thing to speech-to-speech are the first edits this template invites —
|
|
10
|
+
* and `aai build` runs these tests before it bundles, so an assertion that
|
|
11
|
+
* pins the template's own identity turns the first customization into a build
|
|
12
|
+
* failure in a file the author never wrote. Every test here therefore asserts
|
|
13
|
+
* a property that survives those edits, on the RESOLVED config rather than on
|
|
14
|
+
* the def's empty fields.
|
|
15
|
+
*/
|
|
5
16
|
describe("simple template", () => {
|
|
6
17
|
test("config passes manifest validation", () => {
|
|
7
18
|
// Same conversion `aai build`/`aai deploy` run.
|
|
8
19
|
expect(() => toAgentConfig(agentDef)).not.toThrow();
|
|
9
20
|
});
|
|
10
21
|
|
|
11
|
-
test("exports an agent
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
expect(agentDef.
|
|
16
|
-
expect(agentDef.
|
|
22
|
+
test("exports an agent the platform can name", () => {
|
|
23
|
+
// Not the literal: what has to hold is that there IS a name, and that the
|
|
24
|
+
// conversion carries it through — `AgentName` refuses a blank one, and the
|
|
25
|
+
// studio lists a deployed agent by exactly this string.
|
|
26
|
+
expect(agentDef.name).toBeTruthy();
|
|
27
|
+
expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("every stage its mode needs is filled, declared or defaulted", () => {
|
|
31
|
+
// The template's point: with no provider fields declared, the default
|
|
32
|
+
// all-AssemblyAI cascaded pipeline is injected at parse time (see
|
|
33
|
+
// `defaultProviders`) — so an agent that declares nothing still runs.
|
|
34
|
+
// Asserted per MODE so it stays true after a swap: declare `stt`/`llm`/`tts`
|
|
35
|
+
// and the rest still default; declare `s2s` and there is no cascade to fill,
|
|
36
|
+
// which is the one thing that must never happen by fallthrough.
|
|
37
|
+
const config = toAgentConfig(agentDef);
|
|
38
|
+
if (config.mode === "s2s") {
|
|
39
|
+
expect(config.s2s?.kind).toBeTruthy();
|
|
40
|
+
expect(config.stt).toBeUndefined();
|
|
41
|
+
expect(config.tts).toBeUndefined();
|
|
42
|
+
} else if (config.mode === "text") {
|
|
43
|
+
expect(config.llm?.kind).toBeTruthy();
|
|
44
|
+
} else {
|
|
45
|
+
expect(config.mode).toBe("pipeline");
|
|
46
|
+
expect(config.stt?.kind).toBeTruthy();
|
|
47
|
+
expect(config.llm?.kind).toBeTruthy();
|
|
48
|
+
expect(config.tts?.kind).toBeTruthy();
|
|
49
|
+
}
|
|
17
50
|
});
|
|
18
51
|
});
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* deployed.
|
|
34
34
|
*/
|
|
35
35
|
import agentDef from "virtual:aai/agent";
|
|
36
|
-
import { type EvalTurn, toolResultIn } from "@alexkroman1/aai-runtime/eval";
|
|
36
|
+
import { type EvalTurn, toolResultIn, toolResultsIn } from "@alexkroman1/aai-runtime/eval";
|
|
37
37
|
import { describeEval } from "@alexkroman1/aai-runtime/eval/vitest";
|
|
38
38
|
import { expect } from "vitest";
|
|
39
39
|
import { z } from "zod";
|
|
@@ -82,20 +82,6 @@ const Refusal = z.object({ error: z.string() });
|
|
|
82
82
|
const answerOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T =>
|
|
83
83
|
toolResultIn(turn.toolCalls, name, schema);
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* What EVERY call to `name` answered on this turn, in call order.
|
|
87
|
-
*
|
|
88
|
-
* The plural form is for a claim about calls that may legitimately not have
|
|
89
|
-
* happened — a refusal the narrator may have pre-empted by not calling at all.
|
|
90
|
-
* `toolResultIn` over a ONE-CALL list per call: the name is that call's own, so
|
|
91
|
-
* the reader's "no such call" and "two calls" throws are unreachable and what
|
|
92
|
-
* is left is the parse, the schema, and the "never completed" failure.
|
|
93
|
-
*/
|
|
94
|
-
const answersOf = <T>(turn: EvalTurn, name: string, schema: z.ZodType<T>): T[] =>
|
|
95
|
-
turn.toolCalls
|
|
96
|
-
.filter((c) => c.name === name)
|
|
97
|
-
.map((call) => toolResultIn([call], call.name, schema));
|
|
98
|
-
|
|
99
85
|
/**
|
|
100
86
|
* Every field `setup_character` requires, for the SCRIPTED runs.
|
|
101
87
|
*
|
|
@@ -267,7 +253,7 @@ describeEval(agentDef, (test) => {
|
|
|
267
253
|
expect(answerOf(down, "update_state", Settled).state).toBe("gameOver");
|
|
268
254
|
|
|
269
255
|
const after = await session.say("I refuse to die. Roll to fight on.");
|
|
270
|
-
const attempts =
|
|
256
|
+
const attempts = toolResultsIn(after.toolCalls, "action_roll", Refusal);
|
|
271
257
|
// The gate is only OBSERVABLE if something calls the gated tool, and the
|
|
272
258
|
// script is what guarantees that — hence the exact count. `gameOver` was
|
|
273
259
|
// once a flag nothing acted on, so a player with both tracks empty could
|