@alexkroman1/aai-cli 13.3.0 → 15.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 (158) hide show
  1. package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
  2. package/dist/_agent.d.ts +23 -0
  3. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  4. package/dist/_artifacts.d.ts +16 -0
  5. package/dist/_build-target.d.ts +172 -0
  6. package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
  7. package/dist/_cli-common.d.ts +45 -6
  8. package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
  9. package/dist/_dev-env.d.ts +1 -1
  10. package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
  11. package/dist/_dev-vite-config.d.ts +3 -3
  12. package/dist/_e2e-test-utils.d.ts +1 -1
  13. package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
  14. package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
  15. package/dist/_output.d.ts +10 -9
  16. package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
  17. package/dist/_resource-commands.d.ts +9 -9
  18. package/dist/_server-common-De0haHr9.mjs +70 -0
  19. package/dist/_server-common.d.ts +20 -1
  20. package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
  21. package/dist/_slug-api.d.ts +19 -0
  22. package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
  23. package/dist/_studio-commands.d.ts +13 -13
  24. package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
  25. package/dist/_templates.d.ts +1 -1
  26. package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
  27. package/dist/_typecheck-gate.d.ts +3 -1
  28. package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
  29. package/dist/_utils.d.ts +35 -0
  30. package/dist/_vercel-output.d.ts +63 -0
  31. package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
  32. package/dist/_workflow-determinism.d.ts +3 -3
  33. package/dist/build-BhEaxBPu.mjs +481 -0
  34. package/dist/build.d.ts +23 -10
  35. package/dist/cli.d.ts +1 -2
  36. package/dist/cli.mjs +155 -138
  37. package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
  38. package/dist/client-bundler.d.ts +1 -1
  39. package/dist/client-bundler.mjs +1 -1
  40. package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
  41. package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
  42. package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
  43. package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
  44. package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
  45. package/dist/init.d.ts +14 -0
  46. package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
  47. package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
  48. package/dist/project-config.mjs +1 -1
  49. package/dist/scaffold/CLAUDE.md +155 -76
  50. package/dist/scaffold/package.json +6 -6
  51. package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
  52. package/dist/secret.d.ts +8 -6
  53. package/dist/start.d.ts +112 -0
  54. package/dist/start.mjs +156 -0
  55. package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
  56. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  57. package/dist/templates/call-audit/agent.test.ts +11 -6
  58. package/dist/templates/call-audit/agent.ts +2 -2
  59. package/dist/templates/call-audit/client.tsx +27 -42
  60. package/dist/templates/call-audit/workflows/audit.ts +7 -5
  61. package/dist/templates/call-audit/workflows/ingest.ts +7 -7
  62. package/dist/templates/call-audit/workflows/media.ts +12 -37
  63. package/dist/templates/call-audit/workflows/summarize.ts +8 -8
  64. package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
  65. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  66. package/dist/templates/dispatch-center/agent.test.ts +7 -7
  67. package/dist/templates/dispatch-center/client.tsx +53 -61
  68. package/dist/templates/dispatch-center/shared.ts +0 -8
  69. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
  70. package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
  71. package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
  72. package/dist/templates/infocom-adventure/client.tsx +40 -36
  73. package/dist/templates/link-digest/agent.test.ts +3 -3
  74. package/dist/templates/link-digest/agent.ts +2 -2
  75. package/dist/templates/link-digest/client.tsx +81 -19
  76. package/dist/templates/link-digest/workflows/digest.ts +13 -16
  77. package/dist/templates/math-buddy/agent.test.ts +126 -0
  78. package/dist/templates/night-owl/agent.eval.test.ts +4 -2
  79. package/dist/templates/night-owl/client.tsx +8 -2
  80. package/dist/templates/personal-finance/agent.test.ts +127 -0
  81. package/dist/templates/pipeline-simple/agent.test.ts +2 -2
  82. package/dist/templates/pizza-ordering/client.tsx +5 -4
  83. package/dist/templates/pizza-ordering/shared.ts +3 -8
  84. package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
  85. package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
  86. package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
  87. package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
  88. package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
  89. package/dist/templates/plan-and-execute/agent.test.ts +10 -5
  90. package/dist/templates/plan-and-execute/client.tsx +2 -2
  91. package/dist/templates/podcast-digest/agent.test.ts +20 -11
  92. package/dist/templates/podcast-digest/client.tsx +16 -22
  93. package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
  94. package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
  95. package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
  96. package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
  97. package/dist/templates/recap-workflow/agent.test.ts +27 -23
  98. package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
  99. package/dist/templates/redline/agent.eval.test.ts +5 -8
  100. package/dist/templates/redline/agent.test.ts +1 -1
  101. package/dist/templates/redline/client.tsx +36 -34
  102. package/dist/templates/redline/workflows/redline.ts +17 -17
  103. package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
  104. package/dist/templates/research-workflow/agent.test.ts +10 -7
  105. package/dist/templates/research-workflow/workflows/research.ts +28 -30
  106. package/dist/templates/retail/agent.test.ts +17 -15
  107. package/dist/templates/retail/cancel.ts +7 -6
  108. package/dist/templates/retail/client.tsx +39 -58
  109. package/dist/templates/retail/payment.ts +4 -3
  110. package/dist/templates/retail/pending.test.ts +19 -19
  111. package/dist/templates/retail/store.test.ts +5 -5
  112. package/dist/templates/retail/swap.ts +8 -11
  113. package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
  114. package/dist/templates/solo-rpg/agent.test.ts +12 -12
  115. package/dist/templates/solo-rpg/client.tsx +3 -3
  116. package/dist/templates/spoken-summary/agent.test.ts +9 -9
  117. package/dist/templates/spoken-summary/agent.ts +3 -3
  118. package/dist/templates/spoken-summary/client.tsx +23 -30
  119. package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
  120. package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
  121. package/dist/templates/support-line/agent.ts +8 -0
  122. package/dist/templates/support-line/client.tsx +4 -3
  123. package/dist/templates/support-line/prompts.ts +0 -2
  124. package/dist/templates/transcription-workflow/agent.test.ts +20 -12
  125. package/dist/templates/transcription-workflow/agent.ts +2 -2
  126. package/dist/templates/transcription-workflow/client.tsx +15 -14
  127. package/dist/templates/transcription-workflow/recover.ts +1 -1
  128. package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
  129. package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
  130. package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
  131. package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
  132. package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
  133. package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
  134. package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
  135. package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
  136. package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
  137. package/dist/templates/travel-concierge/agent.test.ts +15 -7
  138. package/dist/templates/travel-concierge/client.tsx +2 -2
  139. package/dist/templates/travel-concierge/routing.ts +64 -55
  140. package/dist/templates/travel-concierge/shared.ts +10 -14
  141. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  142. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  143. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  144. package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
  145. package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
  146. package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
  147. package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
  148. package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
  149. package/dist/templates/web-researcher/agent.test.ts +130 -0
  150. package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
  151. package/dist/typecheck.mjs +1 -1
  152. package/dist/worker-bundler.d.ts +1 -1
  153. package/dist/worker-bundler.mjs +8 -8
  154. package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
  155. package/package.json +9 -4
  156. package/dist/_server-common-BV4_CrP3.mjs +0 -43
  157. package/dist/build-CWyFGe-W.mjs +0 -69
  158. package/dist/scaffold/server.mjs +0 -204
@@ -30,11 +30,11 @@
30
30
  */
31
31
  import agentDef from "virtual:aai/agent";
32
32
  import {
33
- callsIn,
34
33
  describeTurn,
35
34
  type EvalSession,
36
35
  type EvalToolCall,
37
36
  lastStateIn,
37
+ toolCallsInTurns,
38
38
  toolNames,
39
39
  turnCalling,
40
40
  } from "@alexkroman1/aai-runtime/eval";
@@ -116,7 +116,7 @@ describeEval(agentDef, (test) => {
116
116
  // every turn's tool list AND tells the two findings apart: no
117
117
  // `update_ticket` at all, or calls that were all refused by the desk gate.
118
118
  const staging = turnCalling(turns, "update_ticket", stagedSomething);
119
- const attempts = callsIn(turns).filter((call) => call.name === "update_ticket");
119
+ const attempts = toolCallsInTurns(turns).filter((call) => call.name === "update_ticket");
120
120
  // The staging call itself, by INDEX, because what follows it in the same
121
121
  // turn is the subject of the assertion below.
122
122
  const stagedAt = staging.toolCalls.findIndex(
@@ -1,7 +1,12 @@
1
1
  /** The def a DEPLOYED agent runs: authored, plus what `tools/` declares. */
2
2
  import agentDef from "virtual:aai/agent";
3
3
  import type { ToolContext } from "@alexkroman1/aai";
4
- import { createToolContext, ok, okPosition, toolRunner } from "@alexkroman1/aai/testing";
4
+ import {
5
+ createToolContext,
6
+ expectDialogOk,
7
+ expectToolOk,
8
+ toolRunner,
9
+ } from "@alexkroman1/aai/testing";
5
10
  import { describe, expect, test } from "vitest";
6
11
 
7
12
  import {
@@ -196,11 +201,11 @@ describe("sensitive tools stage rather than act", () => {
196
201
  expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
197
202
 
198
203
  // A gated tool answers the flow's POSITION wrapped around the body's own
199
- // return value, so the applied sentence is under `result`. `okPosition`
204
+ // return value, so the applied sentence is under `result`. `expectDialogOk`
200
205
  // keeps the position and THROWS on a refusal, quoting it — where the cast
201
206
  // it replaces read `undefined` off the failure and died three assertions
202
207
  // later on a property of undefined.
203
- const confirmed = okPosition<{ applied: string; reference: string }>(
208
+ const confirmed = expectDialogOk<{ applied: string; reference: string }>(
204
209
  await run("confirm_action", ctx),
205
210
  );
206
211
  expect(confirmed.result.applied).toContain("Harborview Suites");
@@ -225,7 +230,7 @@ describe("sensitive tools stage rather than act", () => {
225
230
  const ctx = makeCtx();
226
231
  await atDesk("flight", ctx);
227
232
  await run("update_ticket", { flightId: "LX54" }, ctx);
228
- const dropped = okPosition<{ discarded: string }>(await run("cancel_action", ctx));
233
+ const dropped = expectDialogOk<{ discarded: string }>(await run("cancel_action", ctx));
229
234
  expect(dropped.result.discarded).toContain("LX54");
230
235
  expect(dropped.state).toBe("browsing");
231
236
 
@@ -291,9 +296,9 @@ describe("sensitive tools stage rather than act", () => {
291
296
  // A refused SECOND staging must not have moved the gate either — it was
292
297
  // already `awaitingConfirmation` and the refusal changed nothing.
293
298
  expect(gateFlow.position(ctx).state).toBe("awaitingConfirmation");
294
- // `ok` is `okPosition` with `.result` taken off: this assertion is about
299
+ // `expectToolOk` is `expectDialogOk` with `.result` taken off: this assertion is about
295
300
  // what the apply DID, not about where the gate landed.
296
- const applied = ok<{ applied: string }>(await run("confirm_action", ctx));
301
+ const applied = expectToolOk<{ applied: string }>(await run("confirm_action", ctx));
297
302
  expect(applied.applied).toContain("LX52");
298
303
  expect(stateOf(ctx).bookings).toEqual([]);
299
304
 
@@ -368,7 +373,10 @@ describe("search tools", () => {
368
373
  const all = (await run("search_hotels", { city: "Boston" }, ctx)) as {
369
374
  hotels: { perNight: string }[];
370
375
  };
371
- expect(all.hotels.map((h) => h.perNight)).toEqual(["$180", "$265", "$340"]);
376
+ // Always to the cent: `formatMoney` is one shape at every desk, where
377
+ // this template's own `toLocaleString` copy dropped `.00` on a round
378
+ // number and kept it on a price with change.
379
+ expect(all.hotels.map((h) => h.perNight)).toEqual(["$180.00", "$265.00", "$340.00"]);
372
380
 
373
381
  const cheap = (await run("search_hotels", { city: "Boston", maxPerNight: 200 }, ctx)) as {
374
382
  hotels: { name: string }[];
@@ -1,5 +1,5 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import { AutoScroll, client, useAgentState } from "@alexkroman1/aai-ui";
2
+ import { AutoScroll, mountClient, useAgentState } from "@alexkroman1/aai-ui";
3
3
  import type { TripView } from "./shared.ts";
4
4
  import { SPECIALIST_IDS, SPECIALISTS, tripProjection } from "./shared.ts";
5
5
 
@@ -118,7 +118,7 @@ function ItinerarySidebar() {
118
118
  );
119
119
  }
120
120
 
121
- client({
121
+ mountClient({
122
122
  name: "Swiss Air Concierge",
123
123
  sidebar: ItinerarySidebar,
124
124
  theme: {
@@ -6,7 +6,10 @@
6
6
  * `activeAssistant`, `applyPending`, `note`. Each is NAMED by a file under
7
7
  * `tools/`, which is what registers it: a tool's name is its file name, so
8
8
  * `tools/to_flight_assistant.ts` is one line handing {@link delegationTool} an
9
- * id, and `tools/confirm_action.ts` re-exports {@link confirmAction}.
9
+ * id, and `tools/confirm_action.ts` one line calling {@link confirmActionTool}.
10
+ * Every file in `tools/` therefore default-exports the RESULT of a call: a
11
+ * factory per tool rather than a shared const, so nothing here is a re-export
12
+ * — which is what `noExportedImports` would otherwise have to be told to allow.
10
13
  *
11
14
  * The delegation four are still generated rather than written out. The notebook
12
15
  * declares `ToFlightBookingAssistant`, `ToHotelBookingAssistant`,
@@ -77,33 +80,35 @@ export function delegationTool(id: SpecialistId): ToolDef {
77
80
  * a `done` tool will keep trying to answer things the desk has no tools for.
78
81
  * The `reason` is what the concierge is handed on the way back up.
79
82
  */
80
- export const completeOrEscalate = tripSlot.updateTool({
81
- description:
82
- "Hand the call back to the main concierge. Use this when the current desk's " +
83
- "work is finished, when the caller changes the subject to something this " +
84
- "desk does not handle, or when they change their mind.",
85
- inputSchema: z.object({
86
- reason: z
87
- .string()
88
- .max(300)
89
- .describe("Why the call is going back — what is done, or what the caller now wants"),
90
- }),
91
- execute(args, trip) {
92
- const left = activeAssistant(trip);
93
- // `primary` stays at the bottom — the slot's `after` hook restores it if a
94
- // pop ever empties the stack.
95
- if (trip.dialogState.length > 1) trip.dialogState.pop();
96
- note(trip, `← back to concierge: ${args.reason}`);
97
- return {
98
- returnedFrom: left === "primary" ? "concierge" : SPECIALISTS[left].title,
99
- nowHandling: "concierge",
100
- reason: args.reason,
101
- instructions:
102
- "You are the main concierge again. Pick up what the caller asked for; " +
103
- "delegate again if it belongs to another desk.",
104
- };
105
- },
106
- });
83
+ export function completeOrEscalateTool(): ToolDef {
84
+ return tripSlot.updateTool({
85
+ description:
86
+ "Hand the call back to the main concierge. Use this when the current desk's " +
87
+ "work is finished, when the caller changes the subject to something this " +
88
+ "desk does not handle, or when they change their mind.",
89
+ inputSchema: z.object({
90
+ reason: z
91
+ .string()
92
+ .max(300)
93
+ .describe("Why the call is going back — what is done, or what the caller now wants"),
94
+ }),
95
+ execute(args, trip) {
96
+ const left = activeAssistant(trip);
97
+ // `primary` stays at the bottom — the slot's `after` hook restores it if a
98
+ // pop ever empties the stack.
99
+ if (trip.dialogState.length > 1) trip.dialogState.pop();
100
+ note(trip, `← back to concierge: ${args.reason}`);
101
+ return {
102
+ returnedFrom: left === "primary" ? "concierge" : SPECIALISTS[left].title,
103
+ nowHandling: "concierge",
104
+ reason: args.reason,
105
+ instructions:
106
+ "You are the main concierge again. Pick up what the caller asked for; " +
107
+ "delegate again if it belongs to another desk.",
108
+ };
109
+ },
110
+ });
111
+ }
107
112
 
108
113
  /**
109
114
  * `confirm_action` — the caller said yes.
@@ -121,14 +126,16 @@ export const completeOrEscalate = tripSlot.updateTool({
121
126
  * the body did NOT answer with a {@link ToolFailure}, so an application that
122
127
  * failed leaves the change staged and the caller still being asked.
123
128
  */
124
- export const confirmAction = gateFlow.tool({
125
- description:
126
- "Apply the change the caller has just confirmed out loud. Only call this " +
127
- "after you have read the change back and heard a clear yes.",
128
- when: "awaitingConfirmation",
129
- send: { type: "SETTLED" },
130
- execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
131
- });
129
+ export function confirmActionTool(): ToolDef {
130
+ return gateFlow.tool({
131
+ description:
132
+ "Apply the change the caller has just confirmed out loud. Only call this " +
133
+ "after you have read the change back and heard a clear yes.",
134
+ when: "awaitingConfirmation",
135
+ send: { type: "SETTLED" },
136
+ execute: (_args, ctx) => tripSlot.update(ctx, (trip) => applyPending(trip)),
137
+ });
138
+ }
132
139
 
133
140
  /**
134
141
  * `cancel_action` — the caller said no. Drops the staged action, changes nothing.
@@ -136,22 +143,24 @@ export const confirmAction = gateFlow.tool({
136
143
  * Gated for the same reason as `confirm_action`, and its own "nothing was
137
144
  * waiting" arm is gone with the same argument.
138
145
  */
139
- export const cancelAction = gateFlow.tool({
140
- description:
141
- "Discard the change the caller just declined. Call this when they say no, " +
142
- "or when they want to change the details before confirming.",
143
- when: "awaitingConfirmation",
144
- send: { type: "SETTLED" },
145
- execute: (_args, ctx) =>
146
- tripSlot.update(ctx, (trip) => {
147
- const action = trip.pending;
148
- // Reachable only if the position and the payload disagree; see
149
- // `applyPending`. Reported rather than thrown, mid-call.
150
- if (!action) return { discarded: null, message: "Nothing was staged after all." };
151
- trip.pending = null;
152
- const described = describeAction(action);
153
- const summary = typeof described === "string" ? described : action.kind;
154
- note(trip, `Declined: ${summary}`);
155
- return { discarded: summary, message: "Nothing was changed." };
156
- }),
157
- });
146
+ export function cancelActionTool(): ToolDef {
147
+ return gateFlow.tool({
148
+ description:
149
+ "Discard the change the caller just declined. Call this when they say no, " +
150
+ "or when they want to change the details before confirming.",
151
+ when: "awaitingConfirmation",
152
+ send: { type: "SETTLED" },
153
+ execute: (_args, ctx) =>
154
+ tripSlot.update(ctx, (trip) => {
155
+ const action = trip.pending;
156
+ // Reachable only if the position and the payload disagree; see
157
+ // `applyPending`. Reported rather than thrown, mid-call.
158
+ if (!action) return { discarded: null, message: "Nothing was staged after all." };
159
+ trip.pending = null;
160
+ const described = describeAction(action);
161
+ const summary = typeof described === "string" ? described : action.kind;
162
+ note(trip, `Declined: ${summary}`);
163
+ return { discarded: summary, message: "Nothing was changed." };
164
+ }),
165
+ });
166
+ }
@@ -47,6 +47,7 @@ import {
47
47
  type ToolContext,
48
48
  type ToolFailure,
49
49
  } from "@alexkroman1/aai";
50
+ import { formatMoney, plural } from "@alexkroman1/aai/utils";
50
51
 
51
52
  // ─── The booking world ───────────────────────────────────────────────────────
52
53
  // Their notebook downloads a sqlite database of a real airline's schedule and
@@ -180,11 +181,6 @@ export const EXCURSIONS: Excursion[] = [
180
181
  { id: "E4", name: "Lake Zurich cruise", city: "Zurich", kind: "boat", price: 52 },
181
182
  ];
182
183
 
183
- /** `$1,234` — one money format, so a spoken price and a rendered one agree. */
184
- export function formatPrice(amount: number): string {
185
- return `$${amount.toLocaleString("en-US")}`;
186
- }
187
-
188
184
  // ─── The dialog stack ────────────────────────────────────────────────────────
189
185
 
190
186
  export const SPECIALIST_IDS = ["flight", "hotel", "car_rental", "excursion"] as const;
@@ -504,24 +500,24 @@ export function describeAction(action: DeepReadonly<PendingAction>): string | To
504
500
  case "update_ticket": {
505
501
  const flight = FLIGHTS.find((f) => f.id === action.flightId);
506
502
  if (!flight) return { error: `No flight ${action.flightId} in the schedule.` };
507
- return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${formatPrice(flight.fare)}`;
503
+ return `move your ticket to ${flight.id}, ${flight.route}, departing ${flight.departs}, at ${formatMoney(flight.fare)}`;
508
504
  }
509
505
  case "cancel_ticket":
510
506
  return "cancel your ticket entirely";
511
507
  case "book_hotel": {
512
508
  const hotel = HOTELS.find((h) => h.id === action.hotelId);
513
509
  if (!hotel) return { error: `No hotel ${action.hotelId}.` };
514
- return `book ${hotel.name} in ${hotel.area} for ${action.nights} night${action.nights === 1 ? "" : "s"}, ${formatPrice(hotel.pricePerNight * action.nights)} total`;
510
+ return `book ${hotel.name} in ${hotel.area} for ${action.nights} ${plural(action.nights, "night")}, ${formatMoney(hotel.pricePerNight * action.nights)} total`;
515
511
  }
516
512
  case "book_car": {
517
513
  const car = CAR_RENTALS.find((c) => c.id === action.carId);
518
514
  if (!car) return { error: `No car ${action.carId}.` };
519
- return `reserve the ${car.tier} from ${car.vendor} for ${action.days} day${action.days === 1 ? "" : "s"}, ${formatPrice(car.pricePerDay * action.days)} total`;
515
+ return `reserve the ${car.tier} from ${car.vendor} for ${action.days} ${plural(action.days, "day")}, ${formatMoney(car.pricePerDay * action.days)} total`;
520
516
  }
521
517
  case "book_excursion": {
522
518
  const excursion = EXCURSIONS.find((e) => e.id === action.excursionId);
523
519
  if (!excursion) return { error: `No excursion ${action.excursionId}.` };
524
- return `book ${excursion.name} at ${formatPrice(excursion.price)}`;
520
+ return `book ${excursion.name} at ${formatMoney(excursion.price)}`;
525
521
  }
526
522
  // Unreachable while `PendingAction` is exhausted above — and the arm a new
527
523
  // member of the union lands in until it has one of its own, which is a
@@ -627,11 +623,11 @@ export function applyPending(
627
623
  state.bookings.push({
628
624
  kind: "hotel",
629
625
  reference,
630
- summary: `${hotel.name} (${hotel.area}), ${action.nights} night${action.nights === 1 ? "" : "s"}`,
626
+ summary: `${hotel.name} (${hotel.area}), ${action.nights} ${plural(action.nights, "night")}`,
631
627
  price,
632
628
  });
633
629
  note(state, `Hotel booked: ${hotel.name} — ${reference}`);
634
- return { applied: `${hotel.name} booked, ${formatPrice(price)}.`, reference };
630
+ return { applied: `${hotel.name} booked, ${formatMoney(price)}.`, reference };
635
631
  }
636
632
  case "book_car": {
637
633
  const car = CAR_RENTALS.find((c) => c.id === action.carId);
@@ -640,12 +636,12 @@ export function applyPending(
640
636
  state.bookings.push({
641
637
  kind: "car",
642
638
  reference,
643
- summary: `${car.vendor} ${car.tier}, ${action.days} day${action.days === 1 ? "" : "s"}`,
639
+ summary: `${car.vendor} ${car.tier}, ${action.days} ${plural(action.days, "day")}`,
644
640
  price,
645
641
  });
646
642
  note(state, `Car reserved: ${car.vendor} ${car.tier} — ${reference}`);
647
643
  return {
648
- applied: `${car.tier} from ${car.vendor} reserved, ${formatPrice(price)}.`,
644
+ applied: `${car.tier} from ${car.vendor} reserved, ${formatMoney(price)}.`,
649
645
  reference,
650
646
  };
651
647
  }
@@ -659,7 +655,7 @@ export function applyPending(
659
655
  price: excursion.price,
660
656
  });
661
657
  note(state, `Excursion booked: ${excursion.name} — ${reference}`);
662
- return { applied: `${excursion.name} booked, ${formatPrice(excursion.price)}.`, reference };
658
+ return { applied: `${excursion.name} booked, ${formatMoney(excursion.price)}.`, reference };
663
659
  }
664
660
  // Same as `describeAction`: unreachable today, and a refusal rather than a
665
661
  // silent no-op for whatever the union grows next.
@@ -4,4 +4,6 @@
4
4
  * gives it its name.
5
5
  */
6
6
 
7
- export { cancelAction as default } from "../routing.ts";
7
+ import { cancelActionTool } from "../routing.ts";
8
+
9
+ export default cancelActionTool();
@@ -4,4 +4,6 @@
4
4
  * gives it its name.
5
5
  */
6
6
 
7
- export { completeOrEscalate as default } from "../routing.ts";
7
+ import { completeOrEscalateTool } from "../routing.ts";
8
+
9
+ export default completeOrEscalateTool();
@@ -4,4 +4,6 @@
4
4
  * gives it its name.
5
5
  */
6
6
 
7
- export { confirmAction as default } from "../routing.ts";
7
+ import { confirmActionTool } from "../routing.ts";
8
+
9
+ export default confirmActionTool();
@@ -1,4 +1,5 @@
1
- import { FLIGHTS, formatPrice, tripSlot } from "../shared.ts";
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
2
+ import { FLIGHTS, tripSlot } from "../shared.ts";
2
3
 
3
4
  /**
4
5
  * Their `fetch_user_flight_information`, which the notebook runs ONCE and
@@ -22,13 +23,13 @@ export default tripSlot.tool({
22
23
  route: flight.route,
23
24
  departs: flight.departs,
24
25
  arrives: flight.arrives,
25
- fare: formatPrice(flight.fare),
26
+ fare: formatMoney(flight.fare),
26
27
  }
27
28
  : null,
28
29
  bookings: trip.bookings.map((b) => ({
29
30
  reference: b.reference,
30
31
  what: b.summary,
31
- price: formatPrice(b.price),
32
+ price: formatMoney(b.price),
32
33
  })),
33
34
  };
34
35
  },
@@ -1,5 +1,6 @@
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
1
2
  import { z } from "zod";
2
- import { CAR_RENTALS, formatPrice, requireDesk, tripSlot } from "../shared.ts";
3
+ import { CAR_RENTALS, requireDesk, tripSlot } from "../shared.ts";
3
4
 
4
5
  /** Their `search_car_rentals`, by city and tier. */
5
6
  export default tripSlot.tool({
@@ -30,7 +31,7 @@ export default tripSlot.tool({
30
31
  id: c.id,
31
32
  vendor: c.vendor,
32
33
  tier: c.tier,
33
- perDay: formatPrice(c.pricePerDay),
34
+ perDay: formatMoney(c.pricePerDay),
34
35
  })),
35
36
  };
36
37
  },
@@ -1,5 +1,6 @@
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
1
2
  import { z } from "zod";
2
- import { EXCURSIONS, formatPrice, requireDesk, tripSlot } from "../shared.ts";
3
+ import { EXCURSIONS, requireDesk, tripSlot } from "../shared.ts";
3
4
 
4
5
  /** Their `search_trip_recommendations` — city plus a loose keyword, matched
5
6
  * against the name and the kind so "boat" and "sail" both land. */
@@ -34,7 +35,7 @@ export default tripSlot.tool({
34
35
  id: e.id,
35
36
  name: e.name,
36
37
  kind: e.kind,
37
- price: formatPrice(e.price),
38
+ price: formatMoney(e.price),
38
39
  })),
39
40
  };
40
41
  },
@@ -1,5 +1,6 @@
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
1
2
  import { z } from "zod";
2
- import { FLIGHTS, formatPrice, requireDesk, tripSlot } from "../shared.ts";
3
+ import { FLIGHTS, requireDesk, tripSlot } from "../shared.ts";
3
4
 
4
5
  /**
5
6
  * Their `search_flights`, with the same "be generous" behaviour their flight
@@ -38,7 +39,7 @@ export default tripSlot.tool({
38
39
  route: f.route,
39
40
  departs: f.departs,
40
41
  arrives: f.arrives,
41
- fare: formatPrice(f.fare),
42
+ fare: formatMoney(f.fare),
42
43
  seatsLeft: f.seatsLeft,
43
44
  })),
44
45
  };
@@ -1,5 +1,6 @@
1
+ import { formatMoney } from "@alexkroman1/aai/utils";
1
2
  import { z } from "zod";
2
- import { formatPrice, HOTELS, requireDesk, tripSlot } from "../shared.ts";
3
+ import { HOTELS, requireDesk, tripSlot } from "../shared.ts";
3
4
 
4
5
  /** Their `search_hotels`, with `price_tier` collapsed to a nightly ceiling —
5
6
  * a caller says "under two hundred", not "midscale". */
@@ -33,7 +34,7 @@ export default tripSlot.tool({
33
34
  name: h.name,
34
35
  area: h.area,
35
36
  stars: h.stars,
36
- perNight: formatPrice(h.pricePerNight),
37
+ perNight: formatMoney(h.pricePerNight),
37
38
  })),
38
39
  };
39
40
  },
@@ -0,0 +1,130 @@
1
+ /** The def a DEPLOYED agent runs: authored, plus what `system-prompt.md` says. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { toAgentConfig } from "@alexkroman1/aai/manifest";
4
+ import { describe, expect, test } from "vitest";
5
+ import promptFile from "./system-prompt.md?raw";
6
+
7
+ /**
8
+ * What a starter's spec may assert.
9
+ *
10
+ * `aai build` runs these tests before it bundles, so an assertion pinning the
11
+ * template's own identity — its literal name, its greeting, its voice, its
12
+ * model — turns a user's first customization into a build failure in a file
13
+ * they never wrote. Every test here therefore asserts a property that survives
14
+ * those edits, on the RESOLVED config rather than on the def's empty fields.
15
+ *
16
+ * What is left to `agent.eval.test.ts` is everything that needs a model: that
17
+ * Scout SEARCHES before answering a fact it is sure of, that the outlet it
18
+ * names appeared in results it actually read, and that the SSRF screen refuses
19
+ * a private address through the agent's own executor. Those are behaviour, and
20
+ * this tier may not reach the network at all — which is also why nothing here
21
+ * runs `web_search`. What a unit test can say is that the capability and the
22
+ * document that instructs it are both still THERE, which is the half that
23
+ * regresses in a diff rather than in a model.
24
+ *
25
+ * The def therefore comes from `virtual:aai/agent` rather than from
26
+ * `./agent.ts`: the prompt file is applied by the BUILD, so the raw default
27
+ * export carries the framework prompt and every claim below would be measuring
28
+ * an agent nobody deploys. This template has no `tools/` directory, so that
29
+ * import is also the file's existence check — `deployedAgent` is handed the
30
+ * prompt and nothing else, and refuses a call with no project files at all.
31
+ */
32
+
33
+ /**
34
+ * The builtins that can reach the web.
35
+ *
36
+ * Typed by USE rather than by an annotation: `builtinTools` takes a
37
+ * `BuiltinTool[]`, so passing this list to `toAgentConfig` below only compiles
38
+ * if every name here is really a builtin — a name the SDK renames fails
39
+ * `pnpm typecheck` in this file instead of dropping silently out of the scan.
40
+ */
41
+ const WEB_BUILTINS = ["web_search", "visit_webpage", "fetch_json", "get_page_design"] as const;
42
+
43
+ describe("web-researcher template", () => {
44
+ test("config passes manifest validation", () => {
45
+ // Same conversion `aai build`/`aai deploy` run.
46
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
47
+ });
48
+
49
+ test("exports an agent the platform can name", () => {
50
+ // Not the literal: what has to hold is that there IS a name, and that the
51
+ // conversion carries it through — `AgentName` refuses a blank one, and the
52
+ // studio lists a deployed agent by exactly this string.
53
+ expect(agentDef.name).toBeTruthy();
54
+ expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
55
+ });
56
+
57
+ test("every stage its mode needs is filled, declared or defaulted", () => {
58
+ // This template declares no provider at all — the whole voice pipeline is
59
+ // the injected all-AssemblyAI default, which is what lets the starter run
60
+ // the moment it is deployed. Asserted per MODE so it survives a swap:
61
+ // declare `stt`/`llm`/`tts` and the rest still default; declare `s2s` and
62
+ // there is no cascade to fill, which is the one thing that must never
63
+ // happen by fallthrough.
64
+ const config = toAgentConfig(agentDef);
65
+ if (config.mode === "s2s") {
66
+ expect(config.s2s?.kind).toBeTruthy();
67
+ expect(config.stt).toBeUndefined();
68
+ expect(config.tts).toBeUndefined();
69
+ } else if (config.mode === "text") {
70
+ expect(config.llm?.kind).toBeTruthy();
71
+ } else {
72
+ expect(config.mode).toBe("pipeline");
73
+ expect(config.stt?.kind).toBeTruthy();
74
+ expect(config.llm?.kind).toBeTruthy();
75
+ expect(config.tts?.kind).toBeTruthy();
76
+ }
77
+ });
78
+
79
+ test("system-prompt.md is the prompt a deploy carries", () => {
80
+ // The build discovers the file; nothing imports it. So the failure this
81
+ // catches is "I edited the prompt and nothing changed" — and here it is the
82
+ // expensive one, because every rule that makes Scout a researcher rather
83
+ // than a talkative model (search first, cite what you read, treat a fetched
84
+ // page as data and not as instructions) lives ONLY in that file. An agent
85
+ // running the framework default answers plausibly, sounds fine, and cites
86
+ // pages it never opened.
87
+ //
88
+ // `toContain` rather than an equality: importing the file into `agent.ts`
89
+ // and composing it (a computed suffix, a date) is a legitimate edit, and
90
+ // `withSystemPrompt` leaves such a def exactly as the author built it.
91
+ const trimmed = promptFile.trim();
92
+ // Not vacuous: `toContain("")` would pass over an empty file, so the file
93
+ // having text in it is asserted before it is used as the needle.
94
+ expect(trimmed).not.toBe("");
95
+ expect(toAgentConfig(agentDef).systemPrompt).toContain(trimmed);
96
+ });
97
+
98
+ test("can search the web, and names no web tool it does not have", () => {
99
+ const config = toAgentConfig(agentDef);
100
+ const declared = new Set<string>(config.builtinTools ?? []);
101
+
102
+ // `web_search` is the template's reason to exist, and dropping it is the
103
+ // silent failure: an agent with no search answers from memory, which reads
104
+ // back exactly like an agent that searched — right up to the fabricated
105
+ // source its eval exists to catch.
106
+ expect([...declared]).toContain("web_search");
107
+
108
+ // The prompt addresses its tools BY NAME ("begins with a web_search call",
109
+ // "Use visit_webpage when the search snippets aren't detailed enough"), so
110
+ // the two halves can drift apart: a builtin dropped from `agent.ts` leaves
111
+ // the prompt ordering a call the model cannot make, and the model then
112
+ // apologizes for a tool it was told it had. Stated as an implication so
113
+ // either edit alone survives — retire the sentence along with the builtin
114
+ // and nothing here objects, and a builtin added without a mention is a
115
+ // free edit too. An empty scan is likewise not a failure: a prompt
116
+ // rewritten to DESCRIBE the tools rather than name them is fine, and the
117
+ // assertion above is what keeps the search itself from going away.
118
+ const named = WEB_BUILTINS.filter((name) => config.systemPrompt.includes(name));
119
+ expect(named.filter((name) => !declared.has(name))).toEqual([]);
120
+ });
121
+
122
+ test("the web builtins this spec scans for are the SDK's own names", () => {
123
+ // The other half of the `as const` above, at run time: a scan whose
124
+ // candidates had gone stale would report "no unbacked names" over a prompt
125
+ // full of them. `toAgentConfig` validates `builtinTools` against the
126
+ // builtin enum, so accepting the whole list is the SDK confirming every
127
+ // name in it.
128
+ expect(() => toAgentConfig({ ...agentDef, builtinTools: WEB_BUILTINS })).not.toThrow();
129
+ });
130
+ });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { a as ok, n as fail } from "./_output-DbJt9gsl.mjs";
2
+ import { a as ok, n as fail } from "./_output-DBP9Op_d.mjs";
3
3
  import { n as log, r as notify } from "./_ui-DCt4qZrk.mjs";
4
- import { i as errorCode, o as errorMessage$1, r as binFromPackageJson } from "./_utils-Dhd058D9.mjs";
4
+ import { a as errorCode, i as compareCodeUnits, l as formatCappedList, r as binFromPackageJson, s as errorMessage$1 } from "./_utils-D5JGcjiW.mjs";
5
5
  import { createRequire } from "node:module";
6
6
  import { existsSync, readdirSync } from "node:fs";
7
7
  import path from "node:path";
@@ -140,11 +140,6 @@ function coveredList(ran) {
140
140
  if (ran === false) return [];
141
141
  return typeof ran === "string" ? [ran] : ran;
142
142
  }
143
- /** Code-unit comparison — see {@link projectSpecFiles} for why not `localeCompare`. */
144
- function compareCodeUnits(a, b) {
145
- if (a < b) return -1;
146
- return a > b ? 1 : 0;
147
- }
148
143
  /** One directory of {@link projectSpecFiles}, recursing into the ones that count. */
149
144
  function collectSpecs(dir, prefix, out) {
150
145
  let entries;
@@ -160,14 +155,6 @@ function collectSpecs(dir, prefix, out) {
160
155
  } else if (SPEC_FILE_RE.test(e.name) && !e.name.includes(".eval.test.")) out.push(rel);
161
156
  }
162
157
  }
163
- /** How many unrun spec names a message prints before it starts counting. */
164
- const MAX_NAMED_SPECS = 10;
165
- /** The unrun set as one phrase — capped, because a project may hold hundreds. */
166
- function formatSpecList(files) {
167
- const named = files.slice(0, MAX_NAMED_SPECS).join(", ");
168
- const rest = files.length - MAX_NAMED_SPECS;
169
- return rest > 0 ? `${named}, and ${rest} more` : named;
170
- }
171
158
  /**
172
159
  * The remedy, named the same way wherever the narrowing is reported.
173
160
  *
@@ -191,7 +178,7 @@ function warnUnrunSpecs(cwd, ran) {
191
178
  if (skipped.length === 0) return;
192
179
  const ranList = coveredList(ran);
193
180
  const preamble = ranList.length === 0 ? `No agent.test.ts, so vitest ran nothing. ${skipped.length} spec file(s) exist and were NOT run:` : `vitest ran ${ranList.join(", ")} only. ${skipped.length} other spec file(s) were NOT run:`;
194
- notify("warn", `${preamble} ${formatSpecList(skipped)}. ${WIDEN_HINT}`);
181
+ notify("warn", `${preamble} ${formatCappedList(skipped)}. ${WIDEN_HINT}`);
195
182
  }
196
183
  /**
197
184
  * Execute agent tests and return structured result.
@@ -249,7 +236,7 @@ async function executeTest(cwd, opts = {}) {
249
236
  */
250
237
  function incomplete(ran, unrun) {
251
238
  const preamble = ran === false ? `\`aai test\` found no agent.test.ts, so it ran nothing, but ${unrun.length} spec file(s) exist` : `\`aai test\` ran ${ran.join(", ")} only — ${unrun.length} other spec file(s) in this project were not run`;
252
- return fail("incomplete_run", `${preamble}: ${formatSpecList(unrun)}. An unrun spec is not a passing one, so this is not a green result.`, WIDEN_HINT);
239
+ return fail("incomplete_run", `${preamble}: ${formatCappedList(unrun)}. An unrun spec is not a passing one, so this is not a green result.`, WIDEN_HINT);
253
240
  }
254
241
  //#endregion
255
242
  export { TEST_FILES, classifyVitestError, executeTest, runVitest };