@alexkroman1/aai-cli 6.11.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +40 -19
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +1 -1
  28. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  29. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  30. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  31. package/dist/templates/call-audit/agent.test.ts +57 -204
  32. package/dist/templates/call-audit/agent.ts +32 -19
  33. package/dist/templates/call-audit/client.tsx +17 -54
  34. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  35. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  36. package/dist/templates/call-audit/workflows/media.ts +2 -12
  37. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  38. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  39. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  40. package/dist/templates/dispatch-center/client.tsx +239 -129
  41. package/dist/templates/dispatch-center/shared.ts +99 -1
  42. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  43. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  44. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  45. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  46. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  47. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  48. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  49. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  50. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  51. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  52. package/dist/templates/health-assistant/agent.test.ts +22 -4
  53. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  54. package/dist/templates/infocom-adventure/client.tsx +246 -164
  55. package/dist/templates/link-digest/agent.test.ts +24 -19
  56. package/dist/templates/link-digest/client.tsx +47 -61
  57. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  58. package/dist/templates/night-owl/agent.test.ts +70 -19
  59. package/dist/templates/night-owl/agent.ts +5 -0
  60. package/dist/templates/night-owl/client.tsx +56 -56
  61. package/dist/templates/night-owl/shared.ts +24 -0
  62. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  63. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  64. package/dist/templates/pizza-ordering/client.tsx +9 -26
  65. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  66. package/dist/templates/plan-and-execute/agent.ts +1 -1
  67. package/dist/templates/plan-and-execute/client.tsx +12 -15
  68. package/dist/templates/plan-and-execute/shared.ts +71 -2
  69. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  70. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  71. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  72. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  73. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  74. package/dist/templates/podcast-digest/agent.ts +139 -0
  75. package/dist/templates/podcast-digest/client.tsx +154 -0
  76. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  77. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  78. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  79. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  80. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  81. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  82. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  83. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  84. package/dist/templates/redline/agent.test.ts +17 -15
  85. package/dist/templates/redline/client.tsx +12 -12
  86. package/dist/templates/redline/workflows/redline.ts +19 -31
  87. package/dist/templates/research-workflow/agent.test.ts +60 -59
  88. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  89. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  90. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  91. package/dist/templates/retail/agent.test.ts +26 -23
  92. package/dist/templates/retail/client.tsx +226 -117
  93. package/dist/templates/retail/registry.test.ts +38 -6
  94. package/dist/templates/retail/store.test.ts +82 -15
  95. package/dist/templates/retail/store.ts +174 -47
  96. package/dist/templates/retail/system-prompt.md +11 -2
  97. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  98. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  99. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  100. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  101. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  102. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  105. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  106. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  107. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  108. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  109. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  110. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  111. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  112. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  113. package/dist/templates/solo-rpg/client.tsx +38 -37
  114. package/dist/templates/solo-rpg/shared.ts +145 -19
  115. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  116. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  117. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  118. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  119. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  120. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  121. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  122. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  123. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  124. package/dist/templates/spoken-summary/agent.ts +30 -17
  125. package/dist/templates/spoken-summary/client.tsx +10 -50
  126. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  127. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  128. package/dist/templates/support-line/agent.test.ts +11 -16
  129. package/dist/templates/support-line/agent.ts +1 -1
  130. package/dist/templates/support-line/client.tsx +9 -9
  131. package/dist/templates/support-line/nodes.ts +100 -0
  132. package/dist/templates/support-line/procedure.ts +407 -0
  133. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  134. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  135. package/dist/templates/transcription-workflow/agent.ts +1 -1
  136. package/dist/templates/transcription-workflow/client.tsx +17 -42
  137. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  138. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  139. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  140. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  141. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  142. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  143. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  144. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  145. package/dist/templates/travel-concierge/client.tsx +11 -23
  146. package/dist/templates/travel-concierge/routing.ts +34 -15
  147. package/dist/templates/travel-concierge/shared.ts +70 -3
  148. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  149. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  153. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  154. package/dist/worker-bundler.mjs +1 -1
  155. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  156. package/dist/workflow-bundler.d.ts +6 -1
  157. package/dist/workflow.d.ts +1 -1
  158. package/package.json +5 -4
  159. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -3,15 +3,22 @@ import { z } from "zod";
3
3
  import type { Resource } from "../shared.ts";
4
4
  import {
5
5
  assertNotResolved,
6
+ callFlow,
6
7
  dispatchSlot,
7
8
  findIncident,
8
9
  logEvent,
9
10
  recommendResources,
10
11
  } from "../shared.ts";
11
12
 
12
- export default dispatchSlot.updateTool({
13
+ /**
14
+ * Gated on `working`, and legal from `triaging` as well as `dispatching` — the
15
+ * prompt's "critical incidents get immediate dispatch, triage can happen
16
+ * simultaneously" is why the gate is the parent state rather than one child.
17
+ */
18
+ export default callFlow.tool({
13
19
  description:
14
20
  "Dispatch units to an incident. Can auto-dispatch recommended resources or manually specify callsigns.",
21
+ when: "working",
15
22
  inputSchema: z.object({
16
23
  incidentId: z.string().max(20).describe("The incident ID"),
17
24
  callsigns: z
@@ -30,66 +37,76 @@ export default dispatchSlot.updateTool({
30
37
  .describe("Dispatch priority — affects simulated ETA")
31
38
  .optional(),
32
39
  }),
33
- execute(args, state) {
34
- const inc = findIncident(state, args.incidentId);
35
- if (isToolFailure(inc)) return inc;
36
- const blocked = assertNotResolved(inc, "dispatch resources");
37
- if (blocked) return blocked;
40
+ execute: (args, ctx) =>
41
+ dispatchSlot.update(ctx, (state) => {
42
+ const inc = findIncident(state, args.incidentId);
43
+ if (isToolFailure(inc)) return inc;
44
+ const blocked = assertNotResolved(inc, "dispatch resources");
45
+ if (blocked) return blocked;
38
46
 
39
- const dispatched: { callsign: string; type: string; eta: number }[] = [];
40
- const failed: { callsign: string; reason: string }[] = [];
47
+ const dispatched: { callsign: string; type: string; eta: number }[] = [];
48
+ const failed: { callsign: string; reason: string }[] = [];
41
49
 
42
- // The literal "auto" callsign means the same thing as autoDispatch.
43
- const wantsAuto =
44
- args.autoDispatch || args.callsigns?.some((cs) => cs.toLowerCase() === "auto");
50
+ // The literal "auto" callsign means the same thing as autoDispatch.
51
+ const wantsAuto =
52
+ args.autoDispatch || args.callsigns?.some((cs) => cs.toLowerCase() === "auto");
45
53
 
46
- let resourcesToDispatch: Resource[] = [];
54
+ let resourcesToDispatch: Resource[] = [];
47
55
 
48
- if (wantsAuto) {
49
- resourcesToDispatch = recommendResources(inc.type, inc.severity, state);
50
- } else if (args.callsigns) {
51
- for (const cs of args.callsigns) {
52
- const r = state.resources.find((r) => r.callsign.toLowerCase() === cs.toLowerCase());
53
- if (!r) {
54
- failed.push({ callsign: cs, reason: "Not found" });
55
- continue;
56
+ if (wantsAuto) {
57
+ resourcesToDispatch = recommendResources(inc.type, inc.severity, state);
58
+ } else if (args.callsigns) {
59
+ for (const cs of args.callsigns) {
60
+ const r = state.resources.find((r) => r.callsign.toLowerCase() === cs.toLowerCase());
61
+ if (!r) {
62
+ failed.push({ callsign: cs, reason: "Not found" });
63
+ continue;
64
+ }
65
+ if (r.status !== "available") {
66
+ failed.push({ callsign: cs, reason: `Currently ${r.status}` });
67
+ continue;
68
+ }
69
+ resourcesToDispatch.push(r);
56
70
  }
57
- if (r.status !== "available") {
58
- failed.push({ callsign: cs, reason: `Currently ${r.status}` });
59
- continue;
60
- }
61
- resourcesToDispatch.push(r);
62
71
  }
63
- }
64
72
 
65
- const etaBase = args.priority === "emergency" ? 3 : args.priority === "priority" ? 6 : 10;
73
+ const etaBase = args.priority === "emergency" ? 3 : args.priority === "priority" ? 6 : 10;
66
74
 
67
- for (const r of resourcesToDispatch) {
68
- const eta = etaBase + Math.floor(Math.random() * 5);
69
- r.status = "dispatched";
70
- r.assignedIncident = args.incidentId;
71
- r.eta = eta;
72
- inc.assignedResources.push(r.id);
73
- dispatched.push({ callsign: r.callsign, type: r.type, eta });
74
- logEvent(inc, `Dispatched ${r.callsign} — ETA ${eta} min`);
75
- }
75
+ for (const r of resourcesToDispatch) {
76
+ const eta = etaBase + Math.floor(Math.random() * 5);
77
+ r.status = "dispatched";
78
+ r.assignedIncident = args.incidentId;
79
+ r.eta = eta;
80
+ inc.assignedResources.push(r.id);
81
+ dispatched.push({ callsign: r.callsign, type: r.type, eta });
82
+ logEvent(inc, `Dispatched ${r.callsign} — ETA ${eta} min`);
83
+ }
76
84
 
77
- if (dispatched.length > 0) {
78
- inc.status = "dispatched";
79
- }
85
+ if (dispatched.length > 0) {
86
+ inc.status = "dispatched";
87
+ }
80
88
 
81
- const availableCount = state.resources.filter((r) => r.status === "available").length;
89
+ const availableCount = state.resources.filter((r) => r.status === "available").length;
82
90
 
83
- return {
84
- incidentId: args.incidentId,
85
- dispatched,
86
- failed: failed.length > 0 ? failed : undefined,
87
- totalAssignedToIncident: inc.assignedResources.length,
88
- remainingAvailableResources: availableCount,
89
- capacityWarning:
90
- availableCount <= 3
91
- ? "WARNING: Resource capacity critically low. Consider mutual aid."
92
- : undefined,
93
- };
94
- },
91
+ return {
92
+ incidentId: args.incidentId,
93
+ dispatched,
94
+ failed: failed.length > 0 ? failed : undefined,
95
+ totalAssignedToIncident: inc.assignedResources.length,
96
+ remainingAvailableResources: availableCount,
97
+ capacityWarning:
98
+ availableCount <= 3
99
+ ? "WARNING: Resource capacity critically low. Consider mutual aid."
100
+ : undefined,
101
+ };
102
+ }),
103
+ // Only when units actually rolled: a dispatch whose every requested callsign
104
+ // was busy answers `dispatched: []`, which is not a refusal but is not
105
+ // progress either, so the call stays where it is.
106
+ //
107
+ // `result` is the SUCCESS type — `sendFrom` takes `Exclude<R, ToolFailure>`,
108
+ // so the `"dispatched" in result` guard that used to stand in for the failure
109
+ // arm leaking into `R` is a plain property read.
110
+ sendFrom: (result) =>
111
+ result.dispatched.length > 0 ? { type: "DISPATCHED" as const } : undefined,
95
112
  });
@@ -1,59 +1,63 @@
1
1
  import { z } from "zod";
2
- import { dispatchSlot, logEvent, RESOURCE_STATUSES } from "../shared.ts";
2
+ import { callFlow, dispatchSlot, logEvent, RESOURCE_STATUSES } from "../shared.ts";
3
3
 
4
- export default dispatchSlot.updateTool({
4
+ /** Gated on `working`: a unit radios in about an incident, so there has to be
5
+ * one. Sends nothing — a unit changing status does not move the call. */
6
+ export default callFlow.tool({
5
7
  description: "Update a resource unit's status when it radios in.",
8
+ when: "working",
6
9
  inputSchema: z.object({
7
10
  callsign: z.string().max(50).describe("The resource callsign"),
8
11
  status: z.enum(RESOURCE_STATUSES).describe("New status"),
9
12
  notes: z.string().max(1000).describe("Status notes").optional(),
10
13
  }),
11
- execute(args, state) {
12
- const resource = state.resources.find(
13
- (r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
14
- );
15
- if (!resource) {
16
- return { error: `Resource ${args.callsign} not found` };
17
- }
18
-
19
- const previousStatus = resource.status;
20
-
21
- // Log to the incident timeline BEFORE unassigning, so the
22
- // return-to-available transition is recorded too.
23
- if (resource.assignedIncident) {
24
- const inc = state.incidents[resource.assignedIncident];
25
- if (inc) {
26
- logEvent(
27
- inc,
28
- `${resource.callsign}: ${previousStatus} → ${args.status}${args.notes ? ` (${args.notes})` : ""}`,
29
- );
14
+ execute: (args, ctx) =>
15
+ dispatchSlot.update(ctx, (state) => {
16
+ const resource = state.resources.find(
17
+ (r) => r.callsign.toLowerCase() === args.callsign.toLowerCase(),
18
+ );
19
+ if (!resource) {
20
+ return { error: `Resource ${args.callsign} not found` };
30
21
  }
31
- }
32
22
 
33
- resource.status = args.status;
23
+ const previousStatus = resource.status;
34
24
 
35
- if (args.status === "available") {
36
- // Detach from the old incident on BOTH sides — leaving the id in
37
- // assignedResources lets that incident later yank a unit that has
38
- // been re-dispatched elsewhere.
25
+ // Log to the incident timeline BEFORE unassigning, so the
26
+ // return-to-available transition is recorded too.
39
27
  if (resource.assignedIncident) {
40
28
  const inc = state.incidents[resource.assignedIncident];
41
29
  if (inc) {
42
- inc.assignedResources = inc.assignedResources.filter((id) => id !== resource.id);
30
+ logEvent(
31
+ inc,
32
+ `${resource.callsign}: ${previousStatus} → ${args.status}${args.notes ? ` (${args.notes})` : ""}`,
33
+ );
43
34
  }
44
35
  }
45
- resource.assignedIncident = null;
46
- resource.eta = null;
47
- }
48
- if (args.status === "on_scene") {
49
- resource.eta = null;
50
- }
51
36
 
52
- return {
53
- callsign: resource.callsign,
54
- previousStatus,
55
- newStatus: args.status,
56
- assignedIncident: resource.assignedIncident,
57
- };
58
- },
37
+ resource.status = args.status;
38
+
39
+ if (args.status === "available") {
40
+ // Detach from the old incident on BOTH sides — leaving the id in
41
+ // assignedResources lets that incident later yank a unit that has
42
+ // been re-dispatched elsewhere.
43
+ if (resource.assignedIncident) {
44
+ const inc = state.incidents[resource.assignedIncident];
45
+ if (inc) {
46
+ inc.assignedResources = inc.assignedResources.filter((id) => id !== resource.id);
47
+ }
48
+ }
49
+ resource.assignedIncident = null;
50
+ resource.eta = null;
51
+ }
52
+ if (args.status === "on_scene") {
53
+ resource.eta = null;
54
+ }
55
+
56
+ return {
57
+ callsign: resource.callsign,
58
+ previousStatus,
59
+ newStatus: args.status,
60
+ assignedIncident: resource.assignedIncident,
61
+ };
62
+ }),
59
63
  });
@@ -1,7 +1,6 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import type { ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
3
+ import { toolRunner, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
4
  import { describe, expect, test } from "vitest";
6
5
  import authoredAgent from "./agent.ts";
7
6
  import { faqs, searchable } from "./shared.ts";
@@ -18,8 +17,14 @@ const agentDef = withDiscoveredTools(
18
17
  import.meta.glob("./tools/*.ts", { eager: true }),
19
18
  );
20
19
 
21
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
22
- runTool(agentDef, name, args, ctx);
20
+ /**
21
+ * Both tools here are stateless, so no call passes a context: `runTool` builds
22
+ * a fresh one, which is a distinct session with empty slots — right for a tool
23
+ * that reads nothing but its arguments, and never what two calls sharing state
24
+ * want. `list_topics` takes no arguments either, and may say so by leaving them
25
+ * out rather than passing a `{}` between the two values a reader cares about.
26
+ */
27
+ const run = toolRunner(agentDef);
23
28
 
24
29
  describe("embedded-assets template", () => {
25
30
  test("the JSON asset really is bundled, and the index is built from it", () => {
@@ -44,7 +49,7 @@ describe("embedded-assets template", () => {
44
49
 
45
50
  describe("list_topics", () => {
46
51
  test("answers with every question in the knowledge base", async () => {
47
- const topics = await run("list_topics", {}, createToolContext());
52
+ const topics = await run("list_topics");
48
53
  expect(topics).toEqual(faqs.map((f) => f.question));
49
54
  });
50
55
  });
@@ -59,25 +64,23 @@ describe("search_knowledge", () => {
59
64
  .toLowerCase()
60
65
  .split(/\W+/)
61
66
  .filter((w) => w.length > 2);
62
- const result = await run(
63
- "search_knowledge",
64
- { query: `hey, could you tell me ${words.join(" ")}?` },
65
- createToolContext(),
66
- );
67
+ const result = await run("search_knowledge", {
68
+ query: `hey, could you tell me ${words.join(" ")}?`,
69
+ });
67
70
  expect(result).toEqual(target);
68
71
  });
69
72
 
70
73
  test("a query with no words longer than two characters is refused early", async () => {
71
74
  // The guard before the scan: with no scoreable words every entry ties at
72
75
  // zero, and returning the first one would be an answer to nothing.
73
- expect(await run("search_knowledge", { query: "is it a" }, createToolContext())).toEqual({
76
+ expect(await run("search_knowledge", { query: "is it a" })).toEqual({
74
77
  result: "No matching FAQ found.",
75
78
  });
76
79
  });
77
80
 
78
81
  test("a query that overlaps nothing reports no match rather than guessing", async () => {
79
- expect(
80
- await run("search_knowledge", { query: "zzzqqq wibbleflange" }, createToolContext()),
81
- ).toEqual({ result: "No matching FAQ found." });
82
+ expect(await run("search_knowledge", { query: "zzzqqq wibbleflange" })).toEqual({
83
+ result: "No matching FAQ found.",
84
+ });
82
85
  });
83
86
  });
@@ -1,7 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import { isToolFailure, type ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
3
+ import { isToolFailure } from "@alexkroman1/aai";
4
+ import { toolInputIssues, toolRunner, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
5
  import { beforeEach, describe, expect, test, vi } from "vitest";
6
6
  import authoredAgent from "./agent.ts";
7
7
  import { excerptAround, type FdaLabel, toDrugInfo } from "./fda.ts";
@@ -35,8 +35,13 @@ const agentDef = withDiscoveredTools(
35
35
  import.meta.glob("./tools/*.ts", { eager: true }),
36
36
  );
37
37
 
38
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext = createToolContext()) =>
39
- runTool(agentDef, name, args, ctx);
38
+ /**
39
+ * Every tool here takes arguments and none of them touches session state, so no
40
+ * call passes a context: `runTool` builds a fresh one per call, which is a
41
+ * distinct session — right for a stateless tool, and never what two calls
42
+ * sharing state want.
43
+ */
44
+ const run = toolRunner(agentDef);
40
45
 
41
46
  const IBUPROFEN: FdaLabel = {
42
47
  openfda: { generic_name: ["IBUPROFEN"], brand_name: ["Advil"], manufacturer_name: ["Acme"] },
@@ -150,6 +155,19 @@ describe("check_drug_interaction", () => {
150
155
  expect(isToolFailure(result) && result.error).toContain("at least two");
151
156
  expect(label).not.toHaveBeenCalled();
152
157
  });
158
+
159
+ test("the schema itself accepts those names, which is why the body re-checks", async () => {
160
+ // The other half of the claim above, asked of the schema directly rather
161
+ // than through `~standard`: `min(2)` counts ENTRIES and `min(1)` counts
162
+ // CHARACTERS, so `" "` is a valid entry and the refusal is the body's.
163
+ expect(
164
+ await toolInputIssues(agentDef, "check_drug_interaction", { drugs: ["ibuprofen", " "] }),
165
+ ).toBeUndefined();
166
+ // And the schema is still doing its own half — one drug is not a check.
167
+ expect(
168
+ await toolInputIssues(agentDef, "check_drug_interaction", { drugs: ["ibuprofen"] }),
169
+ ).toBeDefined();
170
+ });
153
171
  });
154
172
 
155
173
  describe("fda.ts helpers", () => {
@@ -1,7 +1,6 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- import type { ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, runTool, withDiscoveredTools } from "@alexkroman1/aai/testing";
3
+ import { createToolContext, toolRunner, withDiscoveredTools } from "@alexkroman1/aai/testing";
5
4
  import { describe, expect, test } from "vitest";
6
5
  import authoredAgent from "./agent.ts";
7
6
 
@@ -21,8 +20,7 @@ const agentDef = withDiscoveredTools(
21
20
  import { DEFAULT_GAME_STATE, gameSlot, MAX_HISTORY, REPORTED_HISTORY } from "./shared.ts";
22
21
 
23
22
  /** A tool by the name the model calls it by, bound to this agent. */
24
- const run = (name: string, args: Record<string, unknown>, ctx: ToolContext) =>
25
- runTool(agentDef, name, args, ctx);
23
+ const run = toolRunner(agentDef);
26
24
 
27
25
  /** Each context owns its OWN slot store, which is what makes two playthroughs
28
26
  * independent by construction. */
@@ -153,7 +151,7 @@ describe("the adventure's tools", () => {
153
151
  await run("game_state_history", { value: `command ${i}` }, ctx);
154
152
  }
155
153
 
156
- expect(await run("game_state_get", {}, ctx)).toEqual({
154
+ expect(await run("game_state_get", ctx)).toEqual({
157
155
  currentRoom: "Echo Chamber",
158
156
  inventory: ["lantern"],
159
157
  score: 7,
@@ -169,7 +167,7 @@ describe("the adventure's tools", () => {
169
167
  await run("game_state_score", { value: 30 }, ctx);
170
168
  await run("game_state_move", { value: "Echo Chamber" }, ctx);
171
169
 
172
- const restarted = (await run("game_state_restart", {}, ctx)) as {
170
+ const restarted = (await run("game_state_restart", ctx)) as {
173
171
  restarted: boolean;
174
172
  currentRoom: string;
175
173
  };