@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.
- 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-DolUCMxu.mjs → _bundler-DM0d0M7m.mjs} +1 -1
- package/dist/{_dev-server-CSMqF8PN.mjs → _dev-server-BzWB6-4y.mjs} +9 -6
- package/dist/_e2e-test-utils.d.ts +1 -1
- package/dist/{_init-CQ8idAwo.mjs → _init-Bsi3DZNJ.mjs} +1 -1
- package/dist/_server-common-De0haHr9.mjs +70 -0
- package/dist/_server-common.d.ts +20 -1
- package/dist/{_templates-CK4oKoeX.mjs → _templates-CIlJ3Vay.mjs} +1 -1
- package/dist/_templates.d.ts +1 -1
- package/dist/_vercel-output.d.ts +63 -0
- package/dist/build-BhEaxBPu.mjs +481 -0
- package/dist/build.d.ts +18 -10
- package/dist/cli.mjs +51 -23
- package/dist/{client-bundler-BJgREAh6.mjs → client-bundler-6mTLs6ny.mjs} +4 -4
- package/dist/client-bundler.d.ts +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{deploy-uAJ4NukN.mjs → deploy-CGqPU5U-.mjs} +2 -2
- package/dist/{dev-DApPSaE_.mjs → dev-Bx9gYBHM.mjs} +1 -1
- package/dist/{eval-BK47A_K5.mjs → eval-B3I7FqN9.mjs} +1 -1
- package/dist/{init-DukDxECd.mjs → init-CQbj3ycf.mjs} +49 -39
- package/dist/init.d.ts +19 -10
- package/dist/scaffold/CLAUDE.md +73 -22
- package/dist/scaffold/package.json +6 -6
- package/dist/start.d.ts +112 -0
- package/dist/start.mjs +156 -0
- package/dist/{studio-CpHlNHUZ.mjs → studio-C_zuRC_z.mjs} +2 -2
- package/dist/templates/briefing-desk/agent.eval.test.ts +156 -0
- package/dist/templates/code-interpreter/agent.test.ts +103 -0
- package/dist/templates/link-digest/client.tsx +55 -3
- package/dist/templates/math-buddy/agent.test.ts +126 -0
- package/dist/templates/personal-finance/agent.test.ts +127 -0
- package/dist/templates/support-line/agent.ts +8 -0
- package/dist/templates/travel-concierge/routing.ts +64 -55
- 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/web-researcher/agent.test.ts +130 -0
- package/dist/worker-bundler.d.ts +1 -1
- package/dist/worker-bundler.mjs +7 -7
- package/package.json +9 -4
- package/dist/_server-common-vILJp3it.mjs +0 -43
- package/dist/build-Mxk8gWvX.mjs +0 -108
- 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`
|
|
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
|
+
}
|
|
@@ -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
|
+
});
|
package/dist/worker-bundler.d.ts
CHANGED
|
@@ -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,
|
|
37
|
+
export declare function buildWorker(cwd: string, options?: BuildWorkerOptions): Promise<string>;
|
package/dist/worker-bundler.mjs
CHANGED
|
@@ -92,8 +92,8 @@ export const __aaiConfig = {
|
|
|
92
92
|
...toAgentConfig(__aaiAgent),
|
|
93
93
|
toolSchemas: agentToolsToSchemas(__aaiAgent.tools ?? {}),
|
|
94
94
|
};
|
|
95
|
-
${runtime ? `export const __aaiCreateRuntime = (
|
|
96
|
-
createRuntime({ ...
|
|
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,
|
|
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(
|
|
116
|
-
const 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
|
-
...
|
|
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:
|
|
133
|
+
minify: options.minify ? "oxc" : false,
|
|
134
134
|
write: false,
|
|
135
135
|
rollupOptions: { output: {
|
|
136
136
|
entryFileNames: "[name].js",
|