@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.
- package/dist/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/_artifacts-BJOYGQPp.mjs +21 -0
- package/dist/_artifacts.d.ts +16 -0
- package/dist/_build-target.d.ts +172 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DM0d0M7m.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-BzWB6-4y.mjs} +25 -24
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/_e2e-test-utils.d.ts +1 -1
- package/dist/{_init-Ci4QGjp8.mjs → _init-Bsi3DZNJ.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/_server-common-De0haHr9.mjs +70 -0
- package/dist/_server-common.d.ts +20 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CIlJ3Vay.mjs} +4 -4
- package/dist/_templates.d.ts +1 -1
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/_vercel-output.d.ts +63 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/build-BhEaxBPu.mjs +481 -0
- package/dist/build.d.ts +23 -10
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +155 -138
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-6mTLs6ny.mjs} +5 -5
- package/dist/client-bundler.d.ts +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-CGqPU5U-.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-Bx9gYBHM.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-B3I7FqN9.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-CFyusRbq.mjs} +48 -20
- package/dist/init.d.ts +14 -0
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +155 -76
- package/dist/scaffold/package.json +6 -6
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/start.d.ts +112 -0
- package/dist/start.mjs +156 -0
- package/dist/{studio-BpMCfUNx.mjs → studio-C_zuRC_z.mjs} +13 -15
- package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- 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/code-interpreter/agent.test.ts +103 -0
- 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/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +81 -19
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/math-buddy/agent.test.ts +126 -0
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/personal-finance/agent.test.ts +127 -0
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- 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 +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- 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 +27 -23
- 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 +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- 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 +10 -7
- 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/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- 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 +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- 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/agent.ts +8 -0
- 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 +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- 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/routing.ts +64 -55
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
- package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
- package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
- 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/templates/web-researcher/agent.test.ts +130 -0
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.d.ts +1 -1
- package/dist/worker-bundler.mjs +8 -8
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +9 -4
- package/dist/_server-common-BV4_CrP3.mjs +0 -43
- package/dist/build-CWyFGe-W.mjs +0 -69
- 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 =
|
|
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 {
|
|
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`. `
|
|
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 =
|
|
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 =
|
|
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
|
-
// `
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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`
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 ${
|
|
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}
|
|
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}
|
|
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 ${
|
|
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}
|
|
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, ${
|
|
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}
|
|
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, ${
|
|
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, ${
|
|
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.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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 {
|
|
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:
|
|
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-
|
|
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 {
|
|
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} ${
|
|
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}: ${
|
|
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 };
|