@alexkroman1/aai-cli 14.0.0 → 15.1.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 (45) hide show
  1. package/dist/_artifacts-BJOYGQPp.mjs +21 -0
  2. package/dist/_artifacts.d.ts +16 -0
  3. package/dist/_build-target.d.ts +172 -0
  4. package/dist/{_bundler-DolUCMxu.mjs → _bundler-DM0d0M7m.mjs} +1 -1
  5. package/dist/{_dev-server-CSMqF8PN.mjs → _dev-server-BzWB6-4y.mjs} +9 -6
  6. package/dist/_e2e-test-utils.d.ts +1 -1
  7. package/dist/{_init-CQ8idAwo.mjs → _init-Bsi3DZNJ.mjs} +1 -1
  8. package/dist/_server-common-De0haHr9.mjs +70 -0
  9. package/dist/_server-common.d.ts +20 -1
  10. package/dist/{_templates-CK4oKoeX.mjs → _templates-CIlJ3Vay.mjs} +1 -1
  11. package/dist/_templates.d.ts +1 -1
  12. package/dist/_vercel-output.d.ts +63 -0
  13. package/dist/build-BhEaxBPu.mjs +481 -0
  14. package/dist/build.d.ts +18 -10
  15. package/dist/cli.mjs +51 -23
  16. package/dist/{client-bundler-BJgREAh6.mjs → client-bundler-6mTLs6ny.mjs} +4 -4
  17. package/dist/client-bundler.d.ts +1 -1
  18. package/dist/client-bundler.mjs +1 -1
  19. package/dist/{deploy-uAJ4NukN.mjs → deploy-CGqPU5U-.mjs} +2 -2
  20. package/dist/{dev-DApPSaE_.mjs → dev-Bx9gYBHM.mjs} +1 -1
  21. package/dist/{eval-BK47A_K5.mjs → eval-B3I7FqN9.mjs} +1 -1
  22. package/dist/{init-DukDxECd.mjs → init-CQbj3ycf.mjs} +49 -39
  23. package/dist/init.d.ts +19 -10
  24. package/dist/scaffold/CLAUDE.md +73 -22
  25. package/dist/scaffold/package.json +6 -6
  26. package/dist/start.d.ts +112 -0
  27. package/dist/start.mjs +156 -0
  28. package/dist/{studio-CpHlNHUZ.mjs → studio-C_zuRC_z.mjs} +2 -2
  29. package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
  30. package/dist/templates/code-interpreter/agent.test.ts +103 -0
  31. package/dist/templates/link-digest/client.tsx +55 -3
  32. package/dist/templates/math-buddy/agent.test.ts +126 -0
  33. package/dist/templates/personal-finance/agent.test.ts +127 -0
  34. package/dist/templates/support-line/agent.ts +8 -0
  35. package/dist/templates/travel-concierge/routing.ts +64 -55
  36. package/dist/templates/travel-concierge/tools/cancel_action.ts +3 -1
  37. package/dist/templates/travel-concierge/tools/complete_or_escalate.ts +3 -1
  38. package/dist/templates/travel-concierge/tools/confirm_action.ts +3 -1
  39. package/dist/templates/web-researcher/agent.test.ts +130 -0
  40. package/dist/worker-bundler.d.ts +1 -1
  41. package/dist/worker-bundler.mjs +7 -7
  42. package/package.json +9 -4
  43. package/dist/_server-common-vILJp3it.mjs +0 -43
  44. package/dist/build-Mxk8gWvX.mjs +0 -108
  45. package/dist/scaffold/server.mjs +0 -308
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The def a DEPLOYED agent runs: authored, plus the `system-prompt.md` beside
3
+ * it.
4
+ *
5
+ * This template declares no `tools/` at all — every calculation is the
6
+ * `run_code` builtin's — so the prompt is the only thing discovery adds here,
7
+ * and it is what half the tests below are about. Importing `./agent.ts`
8
+ * directly would measure a tutor whose prompt is the framework default, i.e.
9
+ * an agent that was never told to compute in code.
10
+ */
11
+ import agentDef from "virtual:aai/agent";
12
+ import { toAgentConfig } from "@alexkroman1/aai/manifest";
13
+ import { describe, expect, test } from "vitest";
14
+
15
+ /**
16
+ * What a starter's spec may assert.
17
+ *
18
+ * `aai build` runs these tests before it bundles, so an assertion pinning this
19
+ * tutor's own identity — its literal name, the wording of its greeting, the
20
+ * model id it happens to run today — turns the first customization into a build
21
+ * failure in a file the author never wrote. Every test here therefore asserts a
22
+ * property that survives a rename, a voice, a reworded prompt and a model swap,
23
+ * on the RESOLVED config rather than on the def's empty fields.
24
+ *
25
+ * `run_code` is the one thing named literally, and deliberately: taking it away
26
+ * is not a customization of Math Buddy but a deletion of its subject — the
27
+ * prompt is nothing but recipes for it — and the tutor left behind does
28
+ * arithmetic from memory, which reads exactly like a correct answer until it is
29
+ * wrong.
30
+ *
31
+ * What is NOT here is anything about the code the tutor writes or the answer it
32
+ * comes back with: that needs a model and a sandbox, so it belongs to
33
+ * `agent.eval.test.ts`, which supplies both. This tier's question is the one
34
+ * that comes first — was the tutor handed anything to run at all.
35
+ */
36
+ describe("math-buddy template", () => {
37
+ test("config passes manifest validation", () => {
38
+ // Same conversion `aai build`/`aai deploy` run — and the only thing that
39
+ // says this template's declared LLM descriptor is well formed before a
40
+ // live session tries to open one from it.
41
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
42
+ });
43
+
44
+ test("exports an agent the platform can name", () => {
45
+ // Not the literal: what has to hold is that there IS a name and that the
46
+ // conversion carries it through — `AgentName` refuses a blank one, and the
47
+ // studio lists a deployed agent by exactly this string.
48
+ expect(agentDef.name).toBeTruthy();
49
+ expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
50
+ });
51
+
52
+ test("run_code is declared, and the prompt the deploy carries asks for it", () => {
53
+ const config = toAgentConfig(agentDef);
54
+ // Two halves, each of which fails silently and produces a plausible tutor.
55
+ // Without the declaration the model has nothing to run, so it computes in
56
+ // its head and says the answer with the same confidence either way. Without
57
+ // the prompt reaching the CONFIG — the "I edited system-prompt.md and
58
+ // nothing changed" failure `withSystemPrompt` exists to catch, since the
59
+ // file is discovered by the build rather than imported by `agent.ts` — the
60
+ // recipes are gone and what deploys is a general assistant that happens to
61
+ // have a sandbox attached. The framework default says nothing about
62
+ // `run_code`, which is what makes the second assertion a real check on
63
+ // discovery rather than a restatement of the first.
64
+ expect(config.builtinTools).toContain("run_code");
65
+ expect(config.systemPrompt).toContain("run_code");
66
+ });
67
+
68
+ test("whichever model this tutor runs on, the conversion carries its tuning", () => {
69
+ const config = toAgentConfig(agentDef);
70
+ if (config.mode !== "pipeline") {
71
+ // Switched the def to `s2s`? Then one model listens and talks, and there
72
+ // is no separate LLM stage left for anything to be carried on.
73
+ expect(config.mode).toBe("s2s");
74
+ expect(config.llm).toBeUndefined();
75
+ return;
76
+ }
77
+ if (agentDef.llm === undefined) {
78
+ // Dropped the declaration to take the default cascade: it still resolves
79
+ // to a NAMED model, because the gateway refuses an unknown id with a 400
80
+ // at the first session — "no model" is not a state a deploy may reach.
81
+ expect(config.llm?.options.model).toBeTruthy();
82
+ return;
83
+ }
84
+ // A model choice is the only reason this tutor declares a stage at all —
85
+ // a quick, cheap one, since `run_code` does the arithmetic and what is left
86
+ // is turn-taking speed. So the descriptor is checked whole rather than by
87
+ // `kind`: one that arrived with its options dropped would deploy the
88
+ // gateway's default model instead, quietly slower, with nothing on the line
89
+ // saying so. Read off the def rather than pinned, because swapping the id
90
+ // is the first tuning an author of this template tries.
91
+ expect(config.llm?.kind).toBe(agentDef.llm.kind);
92
+ expect(config.llm?.options).toEqual(agentDef.llm.options);
93
+ expect(config.llm?.options.model).toBeTruthy();
94
+ });
95
+
96
+ test("every stage its mode needs is filled, declared or defaulted", () => {
97
+ // This template's other half: it declares the LLM and nothing else, so STT
98
+ // and TTS are injected at parse time (see `defaultProviders`) and the tutor
99
+ // can still hear and speak. Asserted per MODE so it survives a swap —
100
+ // declare `stt`/`tts` and the rest still default; declare `s2s` and there
101
+ // is no cascade to fill, which is the one thing that must never happen by
102
+ // fallthrough.
103
+ const config = toAgentConfig(agentDef);
104
+ if (config.mode === "s2s") {
105
+ expect(config.s2s?.kind).toBeTruthy();
106
+ expect(config.stt).toBeUndefined();
107
+ expect(config.tts).toBeUndefined();
108
+ return;
109
+ }
110
+ expect(config.mode).toBe("pipeline");
111
+ for (const stage of ["stt", "llm", "tts"] as const) {
112
+ expect(config[stage]?.kind, stage).toBe(agentDef[stage]?.kind ?? "assemblyai");
113
+ }
114
+ });
115
+
116
+ test("the caller is told what to ask, and the greeting survives the conversion", () => {
117
+ // A voice agent has no buttons, so the opener is the only place a caller
118
+ // learns that this one wants arithmetic, conversions and dice rather than
119
+ // conversation. It rides to the browser in `/client-config` beside `name`,
120
+ // so what has to hold is that there is one and the conversion carries it:
121
+ // a greeting lost at that boundary is replaced by the framework's generic
122
+ // opener, which invites the caller to ask for anything at all.
123
+ expect(agentDef.greeting).toBeTruthy();
124
+ expect(toAgentConfig(agentDef).greeting).toBe(agentDef.greeting);
125
+ });
126
+ });
@@ -0,0 +1,127 @@
1
+ /** The def a DEPLOYED agent runs: authored, plus what `system-prompt.md` says. */
2
+ import agentDef from "virtual:aai/agent";
3
+ import { AgentConfigSchema, toAgentConfig } from "@alexkroman1/aai/manifest";
4
+ import { describe, expect, test } from "vitest";
5
+
6
+ /**
7
+ * What a starter's spec may assert.
8
+ *
9
+ * `aai init` scaffolds this template verbatim, and `aai build` runs these tests
10
+ * before it bundles — so an assertion pinning Penny's own identity (her literal
11
+ * name, her voice, her model, the wording of a house rule) turns a user's first
12
+ * customization into a build failure in a file they never wrote. Every test
13
+ * here therefore asserts a property that survives those edits, on the RESOLVED
14
+ * config rather than on the def's empty fields.
15
+ *
16
+ * What is deliberately NOT here: whether Penny actually reaches for `run_code`
17
+ * instead of dividing in her head, whether she looks a rate up rather than
18
+ * quoting a remembered one, and whether she keeps the not-financial-advice
19
+ * caveat. Those are claims about a live model and belong to
20
+ * `agent.eval.test.ts`, which drives them against one. This tier asserts the
21
+ * WIRING those runs depend on — a template whose builtins never reached the
22
+ * config fails an eval as a behaviour problem, in a report nobody reads as
23
+ * "the tool was not there".
24
+ */
25
+ describe("personal-finance template", () => {
26
+ test("config passes manifest validation", () => {
27
+ // Same conversion `aai build`/`aai deploy` run. It is also what checks the
28
+ // two builtin NAMES against the SDK's own enum, so a typo in
29
+ // `builtinTools` fails here rather than shipping an agent whose prompt
30
+ // commands a tool the platform never resolved.
31
+ expect(() => toAgentConfig(agentDef)).not.toThrow();
32
+ });
33
+
34
+ test("exports an agent the platform can name", () => {
35
+ // Not the literal: what has to hold is that there IS a name and the
36
+ // conversion carries it through — the studio lists a deployed agent by
37
+ // exactly this string, and renaming her is the first edit this template
38
+ // invites.
39
+ expect(agentDef.name).toBeTruthy();
40
+ expect(toAgentConfig(agentDef).name).toBe(agentDef.name);
41
+ });
42
+
43
+ test("every stage its mode needs is filled, declared or defaulted", () => {
44
+ // This template declares no provider at all — it is a prompt and two
45
+ // builtins — so the default all-AssemblyAI cascade is what makes it run
46
+ // the moment it is deployed. Asserted per MODE so it stays true after a
47
+ // swap: declare `stt`/`llm`/`tts` and the rest still default; declare
48
+ // `s2s` and there is no cascade to fill, which is the one thing that must
49
+ // never happen by fallthrough.
50
+ const config = toAgentConfig(agentDef);
51
+ if (config.mode === "s2s") {
52
+ expect(config.s2s?.kind).toBeTruthy();
53
+ expect(config.stt).toBeUndefined();
54
+ expect(config.tts).toBeUndefined();
55
+ } else if (config.mode === "text") {
56
+ expect(config.llm?.kind).toBeTruthy();
57
+ } else {
58
+ expect(config.mode).toBe("pipeline");
59
+ expect(config.stt?.kind).toBeTruthy();
60
+ expect(config.llm?.kind).toBeTruthy();
61
+ expect(config.tts?.kind).toBeTruthy();
62
+ }
63
+ });
64
+
65
+ test("both builtins survive into the config a deploy carries", () => {
66
+ const builtins = toAgentConfig(agentDef).builtinTools ?? [];
67
+
68
+ // `run_code` is the arithmetic rule's only mechanism. The prompt forbids
69
+ // Penny working ANY figure out in her head — a tip, a split, a payment, a
70
+ // projection — so without the builtin the rule has nothing to point at and
71
+ // degrades into a model inventing numbers the caller then spends money on.
72
+ expect(builtins).toContain("run_code");
73
+
74
+ // `fetch_json` is the only route to a number that MOVES. A rate or a coin
75
+ // price the model remembers is months stale and carries no source, and it
76
+ // arrives in exactly the confident tone a fetched one would — which is why
77
+ // a finance starter that cannot make a request is worse than one that
78
+ // declines to answer.
79
+ expect(builtins).toContain("fetch_json");
80
+
81
+ // Asserted on the CONFIG rather than the def because that is what a deploy
82
+ // ships, and because `DEFAULT_BUILTIN_TOOLS` is empty: a builtin is
83
+ // something an agent asks for, never something it has to notice and switch
84
+ // off. So a dropped entry is not a quieter Penny, it is the same Penny with
85
+ // no way to be right. Adding builtins beside these two is an ordinary edit;
86
+ // losing one is the regression.
87
+ });
88
+
89
+ test("every builtin the prompt tells Penny to use is one she declares", () => {
90
+ // The pairing this template is made of: the prose holds the endpoints and
91
+ // the formulas, each list headed by the tool that consumes it, and
92
+ // `agent.ts` holds the array that makes those tools exist. The failure is
93
+ // silent in both directions — a prompt commanding `fetch_json` at an agent
94
+ // that never declared it produces a model apologizing for a tool it cannot
95
+ // see, and a builtin dropped from `agent.ts` alone leaves the endpoint list
96
+ // addressed to nothing — and neither shows up in a diff of either file.
97
+ const config = toAgentConfig(agentDef);
98
+ const declared = config.builtinTools ?? [];
99
+
100
+ // Which snake_case tokens in the prose are tool NAMES is a question for the
101
+ // SDK's own schema rather than a catalog restated here: this prompt also
102
+ // names `vs_currencies`, `include_24hr_change`, `per_person` and
103
+ // `annual_rate`, so matching every underscored word would redden on a
104
+ // formula, and a copied list of builtins goes stale the first time the SDK
105
+ // adds one.
106
+ const isBuiltin = (name: string) =>
107
+ AgentConfigSchema.safeParse({ ...config, builtinTools: [name] }).success;
108
+ const commanded = [
109
+ ...new Set(config.systemPrompt.match(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g) ?? []),
110
+ ].filter(isBuiltin);
111
+
112
+ // Non-vacuity, and it earns its keep twice: a prompt naming no builtin at
113
+ // all would leave the loop below asserting nothing, and it is also the state
114
+ // this template lands in when `system-prompt.md` is not applied — the
115
+ // framework default names no builtin, so "I edited the prompt and nothing
116
+ // changed" fails here instead of passing quietly with Penny's rules nowhere
117
+ // in her context.
118
+ expect(commanded.length).toBeGreaterThan(0);
119
+ for (const name of commanded) {
120
+ expect(declared, `the prompt tells Penny to use ${name}`).toContain(name);
121
+ }
122
+
123
+ // The converse is deliberately NOT asserted: declaring a builtin the prompt
124
+ // never mentions is an ordinary edit, and the model learns about it from its
125
+ // own tool schema rather than from the prose.
126
+ });
127
+ });
@@ -24,4 +24,12 @@ export default agent({
24
24
  // caller's callback number, and only its reference crosses to the browser.
25
25
  syncState: supportProjection,
26
26
  greeting: `${PRODUCT} support, you're through to the automated line. What's happened?`,
27
+ // A support line is a PHONE line, so this one declares the carrier its number
28
+ // is with. Nothing serves `WS /phone` without this — the route is an
29
+ // allow-list, and an agent that says nothing about carriers answers none — so
30
+ // point a Twilio number's webhook at the deployed agent's `/phone` and the
31
+ // call lands in an ordinary session. `true` admits every carrier the runtime
32
+ // ships a codec for; a list is the narrower statement, and it is the one to
33
+ // copy.
34
+ telephony: ["twilio"],
27
35
  });
@@ -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
+ }
@@ -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();
@@ -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
+ });
@@ -34,4 +34,4 @@ export type BuildWorkerOptions = {
34
34
  * @internal — build hook for aai-server/the studio; not a supported public
35
35
  * API and not covered by semver.
36
36
  */
37
- export declare function buildWorker(cwd: string, opts?: BuildWorkerOptions): Promise<string>;
37
+ export declare function buildWorker(cwd: string, options?: BuildWorkerOptions): Promise<string>;
@@ -92,8 +92,8 @@ export const __aaiConfig = {
92
92
  ...toAgentConfig(__aaiAgent),
93
93
  toolSchemas: agentToolsToSchemas(__aaiAgent.tools ?? {}),
94
94
  };
95
- ${runtime ? `export const __aaiCreateRuntime = (opts: Record<string, unknown>) =>
96
- createRuntime({ ...opts, agent: __aaiAgent });
95
+ ${runtime ? `export const __aaiCreateRuntime = (options: Record<string, unknown>) =>
96
+ createRuntime({ ...options, agent: __aaiAgent });
97
97
  ` : ""}`;
98
98
  }
99
99
  /**
@@ -105,21 +105,21 @@ ${runtime ? `export const __aaiCreateRuntime = (opts: Record<string, unknown>) =
105
105
  * @internal — build hook for aai-server/the studio; not a supported public
106
106
  * API and not covered by semver.
107
107
  */
108
- async function buildWorker(cwd, opts = {}) {
108
+ async function buildWorker(cwd, options = {}) {
109
109
  const wrapperPath = path.join(cwd, WRAPPER_ENTRY_REL);
110
110
  const [, toolFiles, systemPromptFile] = await Promise.all([
111
111
  fs.mkdir(path.dirname(wrapperPath), { recursive: true }),
112
112
  discoverToolFiles(cwd),
113
113
  hasSystemPromptFile(cwd)
114
114
  ]);
115
- await fs.writeFile(wrapperPath, wrapperEntrySource(opts.runtime !== false, toolFiles, systemPromptFile), "utf-8");
116
- const plugins = opts.plugins ?? [];
115
+ await fs.writeFile(wrapperPath, wrapperEntrySource(options.runtime !== false, toolFiles, systemPromptFile), "utf-8");
116
+ const plugins = options.plugins ?? [];
117
117
  let result;
118
118
  try {
119
119
  result = await withPreservedNodeEnv(() => build({
120
120
  root: cwd,
121
121
  logLevel: "silent",
122
- ...opts.configFile === false && { configFile: false },
122
+ ...options.configFile === false && { configFile: false },
123
123
  ...plugins.length > 0 && { plugins },
124
124
  ssr: { noExternal: true },
125
125
  build: {
@@ -130,7 +130,7 @@ async function buildWorker(cwd, opts = {}) {
130
130
  fileName: "worker"
131
131
  },
132
132
  target: "node20",
133
- minify: opts.minify ? "oxc" : false,
133
+ minify: options.minify ? "oxc" : false,
134
134
  write: false,
135
135
  rollupOptions: { output: {
136
136
  entryFileNames: "[name].js",