@alexkroman1/aai-cli 9.2.0 → 10.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/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
- package/dist/_bundler.d.ts +3 -41
- package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
- package/dist/_dev-server.d.ts +1 -8
- package/dist/_dev-vite-config.d.ts +21 -0
- package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
- package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
- package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
- package/dist/_templates.d.ts +29 -0
- package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
- package/dist/cli.mjs +46 -22
- package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
- package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
- package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
- package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
- package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
- package/dist/scaffold/CLAUDE.md +162 -54
- package/dist/scaffold/package.json +4 -6
- package/dist/scaffold/server.mjs +12 -23
- package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
- package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
- package/dist/templates/call-audit/agent.eval.test.ts +1 -1
- package/dist/templates/call-audit/agent.test.ts +80 -8
- package/dist/templates/call-audit/client.tsx +87 -3
- package/dist/templates/call-audit/workflows/audit.ts +52 -32
- package/dist/templates/call-audit/workflows/ingest.ts +15 -15
- package/dist/templates/call-audit/workflows/media.ts +1 -1
- package/dist/templates/call-audit/workflows/summarize.ts +0 -4
- package/dist/templates/link-digest/agent.eval.test.ts +4 -2
- package/dist/templates/link-digest/agent.test.ts +25 -5
- package/dist/templates/link-digest/client.tsx +65 -7
- package/dist/templates/link-digest/workflows/digest.ts +39 -29
- package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
- package/dist/templates/podcast-digest/agent.test.ts +108 -2
- package/dist/templates/podcast-digest/client.tsx +89 -8
- package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
- package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
- package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
- package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
- package/dist/templates/recap-workflow/agent.test.ts +323 -83
- package/dist/templates/recap-workflow/shared.ts +4 -3
- package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
- package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
- package/dist/templates/redline/agent.eval.test.ts +1 -1
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +83 -3
- package/dist/templates/redline/workflows/redline.ts +26 -23
- package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
- package/dist/templates/research-workflow/agent.test.ts +32 -6
- package/dist/templates/research-workflow/agent.ts +4 -4
- package/dist/templates/research-workflow/shared.ts +4 -3
- package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
- package/dist/templates/research-workflow/workflows/research.ts +35 -38
- package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +10 -8
- package/dist/templates/spoken-summary/client.tsx +72 -3
- package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
- package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
- package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
- package/dist/templates/transcription-workflow/agent.test.ts +626 -14
- package/dist/templates/transcription-workflow/agent.ts +6 -4
- package/dist/templates/transcription-workflow/client.tsx +106 -308
- package/dist/templates/transcription-workflow/recover.test.ts +141 -0
- package/dist/templates/transcription-workflow/recover.ts +130 -0
- package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
- package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
- package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
- package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
- package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
- package/dist/templates/transcription-workflow/workflows/stream.ts +177 -24
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
- package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
- package/dist/worker-bundler.d.ts +0 -14
- package/dist/worker-bundler.mjs +149 -1
- package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
- package/dist/workflow.d.ts +1 -0
- package/package.json +4 -6
- package/dist/_workflow-scan.d.ts +0 -108
- package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
- package/dist/workflow-bundler.d.ts +0 -138
- package/dist/{_studio-CKrsixd0.mjs → _studio-B1waXMuR.mjs} +1 -1
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { i as errorCode } from "./_utils-B8QmtFhK.mjs";
|
|
3
|
-
import { n as withPreservedNodeEnv } from "./_vite-env-BNveawd1.mjs";
|
|
4
|
-
import { builtinModules } from "node:module";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import fs from "node:fs/promises";
|
|
7
|
-
import { build } from "vite";
|
|
8
|
-
import { BaseBuilder, applySwcTransform, detectWorkflowPatterns, shouldTransformFile } from "@workflow/builders";
|
|
9
|
-
//#region _workflow-scan.ts
|
|
10
|
-
/**
|
|
11
|
-
* What the BUILT flow bundle carries — the two scans that read it, and the
|
|
12
|
-
* checks over them.
|
|
13
|
-
*
|
|
14
|
-
* Split out of `workflow-bundler.ts` when that file crossed the 500-line cap,
|
|
15
|
-
* along the seam the two scans already share: both read the same artifact after
|
|
16
|
-
* the builder has written it, both attribute a line to a module through
|
|
17
|
-
* esbuild's `// <path>` headers, and neither has anything to do with
|
|
18
|
-
* CONFIGURING the build. One of them fails the build (a `require` the workflow
|
|
19
|
-
* VM cannot answer) and the other warns (a call that replays differently), which
|
|
20
|
-
* is the only real difference between them.
|
|
21
|
-
*
|
|
22
|
-
* Internal: `workflow-bundler.ts` is the surface the studio and the CLI's own
|
|
23
|
-
* build call, and it re-exports nothing from here that they need.
|
|
24
|
-
*
|
|
25
|
-
* @module _workflow-scan
|
|
26
|
-
*/
|
|
27
|
-
/**
|
|
28
|
-
* Every Node builtin, in both spellings esbuild can emit for one.
|
|
29
|
-
*
|
|
30
|
-
* `node:child_process` and bare `child_process` are the same module and the
|
|
31
|
-
* bundle may name it either way — a bare name only reaches the output when the
|
|
32
|
-
* source imported it bare, which npm is still full of.
|
|
33
|
-
*/
|
|
34
|
-
const RUNTIME_MODULES = /* @__PURE__ */ new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
|
|
35
|
-
/**
|
|
36
|
-
* A `require(…)` CALL, excluding esbuild's own `__require` shim.
|
|
37
|
-
*
|
|
38
|
-
* The lookbehind is what separates the two: `__require` is the shim esbuild
|
|
39
|
-
* writes for a bundled CJS module's dynamic requires, and the STEP bundle
|
|
40
|
-
* defines a real `require` for it (see {@link STEP_REQUIRE_SHIM}). A bare
|
|
41
|
-
* `require` in the FLOW bundle is the different thing this scan is for.
|
|
42
|
-
*/
|
|
43
|
-
const REQUIRE_CALL = /(?<![\w$.])require\(\s*"([^"]+)"\s*\)/g;
|
|
44
|
-
/** esbuild's per-module header — `// node_modules/pkg/index.js`, and nothing else. */
|
|
45
|
-
const MODULE_COMMENT = /^\/\/ (\S+\.[cm]?[jt]sx?)$/;
|
|
46
|
-
/**
|
|
47
|
-
* The bundle's lines, each already attributed to the module esbuild's last
|
|
48
|
-
* `// <path>` header named (undefined before the first one). Header lines are
|
|
49
|
-
* consumed rather than yielded — no scan has anything to say about them.
|
|
50
|
-
*
|
|
51
|
-
* Both scans below walk the bundle exactly this way; sharing the walk is what
|
|
52
|
-
* keeps "how a line is attributed" one answer rather than two copies that can
|
|
53
|
-
* disagree.
|
|
54
|
-
*/
|
|
55
|
-
function* attributedLines(workflowCode) {
|
|
56
|
-
let module;
|
|
57
|
-
for (const line of workflowCode.split("\n")) {
|
|
58
|
-
const header = MODULE_COMMENT.exec(line.trim());
|
|
59
|
-
if (header) {
|
|
60
|
-
module = header[1];
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
yield {
|
|
64
|
-
line,
|
|
65
|
-
module
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* The dedupe key for one finding — what was found, and where.
|
|
71
|
-
*
|
|
72
|
-
* A NUL separates the two halves (neither can contain one, so the key cannot
|
|
73
|
-
* collide) and is spelled as an ESCAPE, never the raw byte: one control
|
|
74
|
-
* character makes a file binary to `git grep`, and every ratchet here is a
|
|
75
|
-
* `git grep`. See "Never write a control character" in AGENTS.md.
|
|
76
|
-
*/
|
|
77
|
-
function siteKey(found, module) {
|
|
78
|
-
return `${found}\u0000${module ?? ""}`;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Find the Node builtins a flow bundle would `require` at load.
|
|
82
|
-
*
|
|
83
|
-
* The flow bundle is compiled in a `node:vm` `Script` whose context has
|
|
84
|
-
* `module` and `exports` and **no `require`**, so one of these is a run that
|
|
85
|
-
* dies at replay with `ReferenceError: require is not defined` — never a build
|
|
86
|
-
* failure, and never a symptom before the first run. The WDK's own builder
|
|
87
|
-
* bundles everything for exactly this reason and carries
|
|
88
|
-
* `createNodeModuleErrorPlugin` to reject a builtin import at build time.
|
|
89
|
-
*
|
|
90
|
-
* That plugin has two blind spots this scan covers, and both are the DEPLOYED
|
|
91
|
-
* shape rather than an exotic one:
|
|
92
|
-
*
|
|
93
|
-
* - It reports a violation only when it can point at the import LINE in a
|
|
94
|
-
* first-party file, matched with a single-line regex — so a multi-line
|
|
95
|
-
* `import {\n x,\n} from "pkg"` finds nothing and the builtin is marked
|
|
96
|
-
* external in silence.
|
|
97
|
-
* - It resolves that file against `process.cwd()`, which is not the project
|
|
98
|
-
* being built when the studio builds a workspace, so the read fails and the
|
|
99
|
-
* same silent path is taken.
|
|
100
|
-
*
|
|
101
|
-
* Both were reproduced. What reaches the VM either way is
|
|
102
|
-
* `var import_node_child_process = require("node:child_process");` at the top
|
|
103
|
-
* of the bundle, i.e. every run of every workflow in the project fails, and the
|
|
104
|
-
* stack names a line of generated code inside a dependency.
|
|
105
|
-
*
|
|
106
|
-
* Restricted to builtin specifiers deliberately: those are the only ones this
|
|
107
|
-
* builder leaves external (it marks nothing else so, precisely so nothing can
|
|
108
|
-
* need a `require`), and a narrow set is what keeps the scan from reading the
|
|
109
|
-
* text of a prompt as a violation.
|
|
110
|
-
*
|
|
111
|
-
* @internal
|
|
112
|
-
*/
|
|
113
|
-
function findVmRequires(workflowCode) {
|
|
114
|
-
const found = [];
|
|
115
|
-
const seen = /* @__PURE__ */ new Set();
|
|
116
|
-
for (const { line, module } of attributedLines(workflowCode)) for (const [, specifier] of line.matchAll(REQUIRE_CALL)) {
|
|
117
|
-
if (specifier === void 0 || !RUNTIME_MODULES.has(specifier)) continue;
|
|
118
|
-
const key = siteKey(specifier, module);
|
|
119
|
-
if (seen.has(key)) continue;
|
|
120
|
-
seen.add(key);
|
|
121
|
-
found.push({
|
|
122
|
-
specifier,
|
|
123
|
-
module
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return found;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Calls whose answer differs between a run and its replays, and what to say
|
|
130
|
-
* about each.
|
|
131
|
-
*
|
|
132
|
-
* **A workflow body REPLAYS from the top on every resume** — after a `sleep`,
|
|
133
|
-
* after a redeploy, after the container was reclaimed — and only a step's
|
|
134
|
-
* result is journaled. So a body that reads the clock gets a different time on
|
|
135
|
-
* every pass, and a body that fetches performs the request again, both silently:
|
|
136
|
-
* the run completes, and its output is built from values that disagree with the
|
|
137
|
-
* ones the earlier passes saw. The scaffold guide has always carried this rule
|
|
138
|
-
* with the words "all of which fail silently if broken", and nothing checked it.
|
|
139
|
-
*/
|
|
140
|
-
const REPLAY_UNSAFE = [
|
|
141
|
-
{
|
|
142
|
-
re: /(?<![\w$.])Date\.now\s*\(/g,
|
|
143
|
-
fix: "reads a different clock on every replay — take the time in a `\"use step\"` body, whose result is journaled"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
re: /(?<![\w$.])new Date\s*\(\s*\)/g,
|
|
147
|
-
fix: "reads a different clock on every replay — take the time in a `\"use step\"` body, whose result is journaled"
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
re: /(?<![\w$.])Math\.random\s*\(/g,
|
|
151
|
-
fix: "draws a different number on every replay — draw it in a `\"use step\"` body"
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
re: /(?<![\w$.])crypto\.randomUUID\s*\(/g,
|
|
155
|
-
fix: "mints a different id on every replay — mint it in a `\"use step\"` body"
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
re: /(?<![\w$.])fetch\s*\(/g,
|
|
159
|
-
fix: "runs again on every replay, and the VM has no fetch to run it with — call `stepFetch` from a `\"use step\"` body"
|
|
160
|
-
}
|
|
161
|
-
];
|
|
162
|
-
/**
|
|
163
|
-
* Replay-unsafe calls the flow bundle carries, attributed to the project's OWN
|
|
164
|
-
* `workflows/` files.
|
|
165
|
-
*
|
|
166
|
-
* Attribution is the whole design. The bundle inlines every non-external
|
|
167
|
-
* dependency a workflow module imports — zod, a markdown parser, whatever — and
|
|
168
|
-
* third-party code is full of `Date.now()` on paths a workflow never reaches, so
|
|
169
|
-
* a scan of the bundle's text reports a library and blocks a correct project.
|
|
170
|
-
* esbuild writes a `// <path>` header per module (the same one
|
|
171
|
-
* {@link findVmRequires} reads), so lines can be charged to the file they were
|
|
172
|
-
* written in, and only the project's own workflow sources are read.
|
|
173
|
-
*
|
|
174
|
-
* Scanning the BUNDLE rather than the sources is what makes a `"use step"` body
|
|
175
|
-
* exempt for free: the workflow-mode transform has already removed them, so
|
|
176
|
-
* what is left is the part that really does replay.
|
|
177
|
-
*
|
|
178
|
-
* @internal
|
|
179
|
-
*/
|
|
180
|
-
function findReplayUnsafeCalls(workflowCode) {
|
|
181
|
-
const found = [];
|
|
182
|
-
const seen = /* @__PURE__ */ new Set();
|
|
183
|
-
for (const { line, module } of attributedLines(workflowCode)) {
|
|
184
|
-
if (module === void 0 || !isProjectWorkflowModule(module)) continue;
|
|
185
|
-
for (const { re, fix } of REPLAY_UNSAFE) for (const [call] of line.matchAll(re)) {
|
|
186
|
-
const key = siteKey(call, module);
|
|
187
|
-
if (seen.has(key)) continue;
|
|
188
|
-
seen.add(key);
|
|
189
|
-
found.push({
|
|
190
|
-
call,
|
|
191
|
-
fix,
|
|
192
|
-
module
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return found;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Is this bundled module one of the project's own `workflows/` files?
|
|
200
|
-
*
|
|
201
|
-
* A dependency's path runs through `node_modules/`, which is excluded first so
|
|
202
|
-
* a package that happens to live in a directory called `workflows` cannot be
|
|
203
|
-
* read as the project's.
|
|
204
|
-
*/
|
|
205
|
-
function isProjectWorkflowModule(module) {
|
|
206
|
-
const posix = module.split(path.sep).join("/");
|
|
207
|
-
return !posix.includes("node_modules/") && /(?:^|\/)workflows\//.test(posix);
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* The warning `aai build` and `aai dev` print for a replay-unsafe call.
|
|
211
|
-
*
|
|
212
|
-
* A WARNING and not a build failure, deliberately. The attribution above makes
|
|
213
|
-
* the scan accurate about which FILE a call is in, and it cannot know whether a
|
|
214
|
-
* plain function in a `workflows/` module is reached from a body (where the
|
|
215
|
-
* rule bites) or only from a step (where it does not) — so the one thing it
|
|
216
|
-
* must not do is refuse a correct project. A silent build was the actual
|
|
217
|
-
* problem; a line naming the file solves it without that risk.
|
|
218
|
-
*/
|
|
219
|
-
function replayWarnings(workflowCode) {
|
|
220
|
-
return findReplayUnsafeCalls(workflowCode).map(({ call, fix, module }) => `${module}: \`${call}…\` ${fix}.`);
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Fail the build when the flow bundle carries a `require` — see
|
|
224
|
-
* {@link findVmRequires} for what that means and why nothing upstream catches it.
|
|
225
|
-
*
|
|
226
|
-
* The message has to name the MODULE as well as the specifier, because the
|
|
227
|
-
* import that caused it is not in the file an author is looking at: only a
|
|
228
|
-
* `"use step"` body is stripped from this bundle, so a value a `workflows/`
|
|
229
|
-
* module holds at module scope — an exported helper, a constant — keeps its
|
|
230
|
-
* import, and that import's whole graph rides into the VM.
|
|
231
|
-
*/
|
|
232
|
-
function assertNoVmRequires(workflowCode) {
|
|
233
|
-
const sites = findVmRequires(workflowCode);
|
|
234
|
-
if (sites.length === 0) return;
|
|
235
|
-
const lines = sites.map(({ specifier, module }) => ` ${specifier}${module === void 0 ? "" : ` — from ${module}`}`);
|
|
236
|
-
throw new Error([
|
|
237
|
-
`This project's workflows cannot run: the workflow bundle requires ${sites.length === 1 ? "a Node module" : "Node modules"} that the workflow VM has no \`require\` for.`,
|
|
238
|
-
...lines,
|
|
239
|
-
"",
|
|
240
|
-
"Only a `\"use step\"` body is removed from this bundle, so anything a `workflows/` module holds at MODULE scope keeps its import — including an exported helper that a step body is the only caller of. Move that use inside the step body, or into a module only a step body imports."
|
|
241
|
-
].join("\n"));
|
|
242
|
-
}
|
|
243
|
-
//#endregion
|
|
244
|
-
//#region workflow-bundler.ts
|
|
245
|
-
/**
|
|
246
|
-
* Workflow bundling — turning a project's `workflows/` directory into the two
|
|
247
|
-
* small artifacts a guest needs to run durable workflows.
|
|
248
|
-
*
|
|
249
|
-
* Public (no `_` prefix) for the same reason as `worker-bundler.ts`: the studio
|
|
250
|
-
* builds its workspaces through this too, so a workflow published from the
|
|
251
|
-
* browser comes out of the same pass as one from `aai build`.
|
|
252
|
-
*
|
|
253
|
-
* @internal — build hook for aai-server/the studio; not a supported public API
|
|
254
|
-
* and not covered by semver.
|
|
255
|
-
*
|
|
256
|
-
* ## Why this exists at all
|
|
257
|
-
*
|
|
258
|
-
* The Workflow Development Kit's own integrations (Next, Nitro, SvelteKit) build
|
|
259
|
-
* workflow routes while building the SERVER, by scanning `workflows/` and
|
|
260
|
-
* emitting handler files. That model does not fit this platform: the guest image
|
|
261
|
-
* is baked once and then serves MANY tenants' agents, each arriving over
|
|
262
|
-
* `bundle/load` at runtime. There is no `workflows/` directory in existence when
|
|
263
|
-
* the harness is built. So the transform has to happen per tenant at deploy
|
|
264
|
-
* time — here — and the guest receives the result as data.
|
|
265
|
-
*
|
|
266
|
-
* ## The two artifacts, and why they are small
|
|
267
|
-
*
|
|
268
|
-
* A default `workflow build` emits a 3.7 MB flow bundle and a 12 MB step bundle,
|
|
269
|
-
* because each one inlines the whole WDK runtime. Neither could ride the guest's
|
|
270
|
-
* single-ESM-string delivery. Two settings fix that, and both are load-bearing:
|
|
271
|
-
*
|
|
272
|
-
* - **`externalPackages: ["workflow", …]`** — the WDK is resolved from the
|
|
273
|
-
* guest's BAKED image instead of being inlined. Step bundle: 12 MB → ~7 KB.
|
|
274
|
-
* - **`bundleFinalOutput: false`** — skips wrapping the workflow-mode code in a
|
|
275
|
-
* runtime host, which is exactly what `workflowEntrypoint(code)` supplies
|
|
276
|
-
* itself at the other end. Dialog bundle: 3.7 MB → ~69 KB.
|
|
277
|
-
*
|
|
278
|
-
* So `workflowCode` is passed to `workflowEntrypoint()` in the guest and
|
|
279
|
-
* `stepCode` is evaluated there to register its step functions.
|
|
280
|
-
*
|
|
281
|
-
* ## The THIRD transform, which is the agent bundle's
|
|
282
|
-
*
|
|
283
|
-
* Those two are what the queue runs. The agent bundle needs a third —
|
|
284
|
-
* `applySwcTransform(…, "client")`, wired in as {@link workflowClientPlugin} —
|
|
285
|
-
* and without it the whole mechanism is inert: `ctx.workflows.start` reads the
|
|
286
|
-
* `workflowId` the compiler attaches to a directive body, and Vite bundling
|
|
287
|
-
* `workflows/research.ts` the ordinary way attaches nothing. The symptom is
|
|
288
|
-
* `MISSING_WORKFLOW_ID_MESSAGE` at the first `start()` — an agent that builds,
|
|
289
|
-
* deploys, boots and answers the phone, and cannot start a run.
|
|
290
|
-
*
|
|
291
|
-
* The plugin transforms EXACTLY the files the builder scanned (`inputFiles`),
|
|
292
|
-
* not "every file with a directive". Same list, same `moduleSpecifierRoot`,
|
|
293
|
-
* therefore the same ids by construction — a body outside `workflows/` cannot
|
|
294
|
-
* acquire an id that no flow bundle registered, which would trade a clear
|
|
295
|
-
* failure at `start()` for a run that enqueues and then fails at replay.
|
|
296
|
-
*
|
|
297
|
-
* ## Config details that are not optional
|
|
298
|
-
*
|
|
299
|
-
* Each of these cost real time to find, and none fails in a way that names
|
|
300
|
-
* itself:
|
|
301
|
-
*
|
|
302
|
-
* - `dirs` must carry the `./` prefix. A bare `"workflows"` crashes inside
|
|
303
|
-
* enhanced-resolve with `Cannot read properties of undefined (reading
|
|
304
|
-
* 'length')` from `join(undefined, …)`, nowhere near the cause.
|
|
305
|
-
* - Every bundle path must be ABSOLUTE, for the same reason.
|
|
306
|
-
* - `projectRoot` and `moduleSpecifierRoot` must be set explicitly; they do not
|
|
307
|
-
* default to `workingDir` on the path this takes.
|
|
308
|
-
* - `keepInterimBundleContext` must stay off — it leaves an esbuild watch context
|
|
309
|
-
* alive, so a one-shot build never exits.
|
|
310
|
-
*/
|
|
311
|
-
/**
|
|
312
|
-
* The directory a project's `"use workflow"` bodies live in.
|
|
313
|
-
*
|
|
314
|
-
* The WDK builder's convention, not ours, and it is why a workflow body cannot
|
|
315
|
-
* live in `agent.ts`: only files under here are transformed. A body outside it
|
|
316
|
-
* runs inline, once, with no durability and nothing reporting that.
|
|
317
|
-
*/
|
|
318
|
-
const WORKFLOWS_DIR = "workflows";
|
|
319
|
-
/**
|
|
320
|
-
* Packages left OUT of both bundles, resolved from the guest's baked image.
|
|
321
|
-
*
|
|
322
|
-
* `workflow` alone would nearly do it — its subpaths are what user code imports —
|
|
323
|
-
* but the `@workflow/*` internals are what the transform's own emitted imports
|
|
324
|
-
* reach for, and inlining those is most of the 12 MB.
|
|
325
|
-
*/
|
|
326
|
-
const WDK_EXTERNAL = [
|
|
327
|
-
"workflow",
|
|
328
|
-
"@workflow/core",
|
|
329
|
-
"@workflow/errors",
|
|
330
|
-
"@workflow/utils",
|
|
331
|
-
"@workflow/world"
|
|
332
|
-
];
|
|
333
|
-
/** Scratch directory for the builder's file output, under the CLI's own dot-dir. */
|
|
334
|
-
const SCRATCH_REL = path.join(".aai", "workflow-build");
|
|
335
|
-
/**
|
|
336
|
-
* What makes a CJS dependency survive into the ESM step bundle.
|
|
337
|
-
*
|
|
338
|
-
* esbuild cannot statically rewrite `require("node:assert")` inside a bundled
|
|
339
|
-
* CommonJS module, so it emits a `__require` shim — and that shim's fallback
|
|
340
|
-
* THROWS: `Dynamic require of "node:assert" is not supported`. A step bundles
|
|
341
|
-
* everything it imports and npm is full of CJS, so any step reaching a package
|
|
342
|
-
* with a CJS dependency anywhere in its graph failed AT MODULE LOAD, before a
|
|
343
|
-
* single line of the step ran.
|
|
344
|
-
*
|
|
345
|
-
* That is not a hypothetical, and the way it presents is the argument for
|
|
346
|
-
* fixing it here. `research-workflow` imports `webSearch` from
|
|
347
|
-
* `@alexkroman1/aai/tools`, which reaches `host/ssrf.ts`, which imports
|
|
348
|
-
* **undici** — 118 dynamic requires, all of them `node:` builtins. `aai dev`
|
|
349
|
-
* then never listened at all: the message named a Node builtin the author
|
|
350
|
-
* never mentions, nothing named the package or the import that pulled it, and
|
|
351
|
-
* because the step bundle is loaded before the server binds there was no
|
|
352
|
-
* server to ask. It also does not reproduce in-tree, where `@dev/source`
|
|
353
|
-
* resolves the SDK to TypeScript — only against the published `dist` — so
|
|
354
|
-
* every gate short of the e2e suite was green.
|
|
355
|
-
*
|
|
356
|
-
* The shim itself is the fix's whole mechanism: esbuild writes
|
|
357
|
-
* `typeof require !== "undefined" ? require : <thrower>`, so a real `require`
|
|
358
|
-
* in scope is USED. This defines one from `import.meta.url` and is prepended,
|
|
359
|
-
* which puts it ahead of the `var __require = …` initializer that reads it.
|
|
360
|
-
* The flow bundle deliberately gets none — it is compiled in a `node:vm`
|
|
361
|
-
* Script, where `import.meta` does not exist.
|
|
362
|
-
*/
|
|
363
|
-
const STEP_REQUIRE_SHIM = [
|
|
364
|
-
"import { createRequire as __aaiCreateRequire } from \"node:module\";",
|
|
365
|
-
"const require = __aaiCreateRequire(import.meta.url);",
|
|
366
|
-
""
|
|
367
|
-
].join("\n");
|
|
368
|
-
/**
|
|
369
|
-
* The builder, with the config gotchas above applied once.
|
|
370
|
-
*
|
|
371
|
-
* `BaseBuilder` is abstract over `build()`, so a subclass is the supported way to
|
|
372
|
-
* choose which bundles to emit and how — the WDK's own framework integrations do
|
|
373
|
-
* the same.
|
|
374
|
-
*/
|
|
375
|
-
var AaiWorkflowBuilder = class extends BaseBuilder {
|
|
376
|
-
/**
|
|
377
|
-
* Where `build()` leaves its result.
|
|
378
|
-
*
|
|
379
|
-
* `BaseBuilder.build()` is typed `Promise<void>`, so a subclass cannot return
|
|
380
|
-
* the artifacts from it — and parameter properties are unavailable under this
|
|
381
|
-
* repo's `erasableSyntaxOnly`, so the fields are declared the long way too.
|
|
382
|
-
*/
|
|
383
|
-
output;
|
|
384
|
-
/**
|
|
385
|
-
* The two bundles `build()` reads back.
|
|
386
|
-
*
|
|
387
|
-
* Held as fields because `super()` is also told where to write them: derived
|
|
388
|
-
* a second time inside `build()`, the two spellings of each path were free to
|
|
389
|
-
* disagree, and a `build()` reading a file the builder never wrote is a
|
|
390
|
-
* "produced nothing" failure that names neither path.
|
|
391
|
-
*/
|
|
392
|
-
flowFile;
|
|
393
|
-
stepFile;
|
|
394
|
-
constructor(cwd, outDir) {
|
|
395
|
-
const flowFile = path.join(outDir, "flow.mjs");
|
|
396
|
-
const stepFile = path.join(outDir, "step.mjs");
|
|
397
|
-
super({
|
|
398
|
-
buildTarget: "standalone",
|
|
399
|
-
dirs: [`./${WORKFLOWS_DIR}`],
|
|
400
|
-
workingDir: cwd,
|
|
401
|
-
projectRoot: cwd,
|
|
402
|
-
moduleSpecifierRoot: cwd,
|
|
403
|
-
externalPackages: [...WDK_EXTERNAL],
|
|
404
|
-
stepsBundlePath: stepFile,
|
|
405
|
-
workflowsBundlePath: flowFile,
|
|
406
|
-
webhookBundlePath: path.join(outDir, "webhook.mjs"),
|
|
407
|
-
suppressCreateWorkflowsBundleLogs: true,
|
|
408
|
-
suppressCreateWebhookBundleLogs: true,
|
|
409
|
-
suppressCreateManifestLogs: true
|
|
410
|
-
});
|
|
411
|
-
this.flowFile = flowFile;
|
|
412
|
-
this.stepFile = stepFile;
|
|
413
|
-
}
|
|
414
|
-
async build() {
|
|
415
|
-
const inputFiles = await this.getInputFiles();
|
|
416
|
-
const { manifest } = await this.createWorkflowsBundle({
|
|
417
|
-
inputFiles,
|
|
418
|
-
format: "esm",
|
|
419
|
-
outfile: this.flowFile,
|
|
420
|
-
bundleFinalOutput: false
|
|
421
|
-
});
|
|
422
|
-
await this.createStepsBundle({
|
|
423
|
-
inputFiles,
|
|
424
|
-
format: "esm",
|
|
425
|
-
outfile: this.stepFile
|
|
426
|
-
});
|
|
427
|
-
const [workflowCode, stepCode] = await Promise.all([fs.readFile(this.flowFile, "utf-8"), fs.readFile(this.stepFile, "utf-8")]);
|
|
428
|
-
assertNoVmRequires(workflowCode);
|
|
429
|
-
this.output = {
|
|
430
|
-
workflowCode,
|
|
431
|
-
stepCode: STEP_REQUIRE_SHIM + stepCode,
|
|
432
|
-
manifest,
|
|
433
|
-
inputFiles,
|
|
434
|
-
warnings: replayWarnings(workflowCode)
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
/**
|
|
439
|
-
* Attach the compiler's `workflowId`/`stepId` to the agent bundle's copy of
|
|
440
|
-
* each directive body — the third transform, see the module doc.
|
|
441
|
-
*
|
|
442
|
-
* `enforce: "pre"` so it sees the file before Vite's own TypeScript pass: the
|
|
443
|
-
* swc transform reads types itself (it is given the filename to infer syntax),
|
|
444
|
-
* and its output is plain JS that the later pass leaves alone.
|
|
445
|
-
*
|
|
446
|
-
* @internal
|
|
447
|
-
*/
|
|
448
|
-
function workflowClientPlugin(cwd, inputFiles) {
|
|
449
|
-
const scanned = new Set(inputFiles.map((file) => path.resolve(cwd, file)));
|
|
450
|
-
return {
|
|
451
|
-
name: "aai:workflow-client",
|
|
452
|
-
enforce: "pre",
|
|
453
|
-
async transform(code, id) {
|
|
454
|
-
const file = path.resolve(id.split("?")[0] ?? id);
|
|
455
|
-
if (!scanned.has(file)) return;
|
|
456
|
-
if (!shouldTransformFile(file, detectWorkflowPatterns(code))) return;
|
|
457
|
-
const { code: transformed } = await applySwcTransform(path.relative(cwd, file), code, "client", file, cwd, cwd);
|
|
458
|
-
return {
|
|
459
|
-
code: transformed,
|
|
460
|
-
map: null
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Build a project's workflows, or resolve `undefined` when it declares none.
|
|
467
|
-
*
|
|
468
|
-
* `undefined` rather than empty strings, because "this project has no workflows"
|
|
469
|
-
* is the common case — every voice agent that never reaches for one — and the
|
|
470
|
-
* guest must be able to tell it apart from "the build produced nothing", which
|
|
471
|
-
* would be a bug.
|
|
472
|
-
*
|
|
473
|
-
* @internal
|
|
474
|
-
*/
|
|
475
|
-
async function buildWorkflows(cwd) {
|
|
476
|
-
if (!await hasWorkflowsDir(cwd)) return;
|
|
477
|
-
const outDir = path.join(cwd, SCRATCH_REL);
|
|
478
|
-
await fs.mkdir(outDir, { recursive: true });
|
|
479
|
-
try {
|
|
480
|
-
const builder = new AaiWorkflowBuilder(cwd, outDir);
|
|
481
|
-
await builder.build();
|
|
482
|
-
const built = builder.output;
|
|
483
|
-
if (!built) return;
|
|
484
|
-
return built.workflowCode.trim() === "" ? void 0 : built;
|
|
485
|
-
} finally {
|
|
486
|
-
await fs.rm(outDir, {
|
|
487
|
-
recursive: true,
|
|
488
|
-
force: true
|
|
489
|
-
}).catch(() => void 0);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
/** Does this project have a `workflows/` directory with anything in it? */
|
|
493
|
-
async function hasWorkflowsDir(cwd) {
|
|
494
|
-
try {
|
|
495
|
-
return (await fs.readdir(path.join(cwd, WORKFLOWS_DIR))).some((name) => /\.(ts|tsx|js|jsx|mjs)$/.test(name));
|
|
496
|
-
} catch {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
//#endregion
|
|
501
|
-
//#region worker-bundler.ts
|
|
502
|
-
/**
|
|
503
|
-
* Generated wrapper entry, written under `.aai/` for the duration of the
|
|
504
|
-
* build (the CLI's own scratch dir — dot-paths are ignored by the dev
|
|
505
|
-
* watcher, and the studio's workspace materialization never writes there).
|
|
506
|
-
*/
|
|
507
|
-
const WRAPPER_ENTRY_REL = path.join(".aai", "worker-entry.ts");
|
|
508
|
-
/** Extensions a tool module may be authored in (mirrors the SDK's registry). */
|
|
509
|
-
const TOOL_MODULE_EXT_RE = /\.(?:m?ts|tsx)$/;
|
|
510
|
-
/** A co-located spec is not a tool. */
|
|
511
|
-
const TOOL_SPEC_RE = /\.(?:test|spec)\.[^.]+$/;
|
|
512
|
-
/**
|
|
513
|
-
* The project's `tools/` directory, as file names relative to it.
|
|
514
|
-
*
|
|
515
|
-
* **This is the whole of "discovery", and it happens HERE because the guest has
|
|
516
|
-
* no filesystem.** A sandbox is handed one ESM string, so the only place a
|
|
517
|
-
* directory can be turned into modules is where the bundle is assembled — the
|
|
518
|
-
* same lowering eve does (`readdir` → static import list, which the bundler then
|
|
519
|
-
* follows). The names are validated by `toolRegistry` at bundle-evaluation time
|
|
520
|
-
* rather than here, so one implementation owns the rules.
|
|
521
|
-
*
|
|
522
|
-
* Sorted, so the emitted entry is byte-stable for a given directory: an entry
|
|
523
|
-
* that reordered per readdir would change the bundle hash for no reason.
|
|
524
|
-
*/
|
|
525
|
-
async function discoverToolFiles(cwd) {
|
|
526
|
-
const root = path.join(cwd, "tools");
|
|
527
|
-
let entries;
|
|
528
|
-
try {
|
|
529
|
-
entries = await fs.readdir(root, {
|
|
530
|
-
withFileTypes: true,
|
|
531
|
-
recursive: true
|
|
532
|
-
});
|
|
533
|
-
} catch (err) {
|
|
534
|
-
if (errorCode(err) === "ENOENT") return [];
|
|
535
|
-
throw err;
|
|
536
|
-
}
|
|
537
|
-
return entries.filter((e) => e.isFile() && TOOL_MODULE_EXT_RE.test(e.name) && !TOOL_SPEC_RE.test(e.name)).map((e) => path.relative(root, path.join(e.parentPath, e.name)).split(path.sep).join("/")).sort();
|
|
538
|
-
}
|
|
539
|
-
/** The prose slot: one file, beside `agent.ts`, named by convention. */
|
|
540
|
-
const SYSTEM_PROMPT_FILE = "system-prompt.md";
|
|
541
|
-
/**
|
|
542
|
-
* Whether the project keeps its system prompt in a file.
|
|
543
|
-
*
|
|
544
|
-
* The other half of "a file beside `agent.ts` can BE the thing", and it happens
|
|
545
|
-
* here for the same reason `discoverToolFiles` does — the guest has no
|
|
546
|
-
* filesystem, so the read belongs where the bundle is assembled. What the file
|
|
547
|
-
* MEANS is `withSystemPrompt`'s to decide (the author may have imported and
|
|
548
|
-
* composed it, in which case discovery must not apply it twice); this only
|
|
549
|
-
* answers whether there is one.
|
|
550
|
-
*/
|
|
551
|
-
async function hasSystemPromptFile(cwd) {
|
|
552
|
-
const nested = path.join(cwd, "system-prompt");
|
|
553
|
-
if ((await fs.stat(nested).catch(() => void 0))?.isDirectory() === true) throw new Error(`${nested} is a directory. A system prompt is ONE file — rename it to ${SYSTEM_PROMPT_FILE}, or import the pieces yourself and compose them into \`systemPrompt\`. There is deliberately no concatenation order for a directory.`);
|
|
554
|
-
try {
|
|
555
|
-
return (await fs.stat(path.join(cwd, SYSTEM_PROMPT_FILE))).isFile();
|
|
556
|
-
} catch (err) {
|
|
557
|
-
if (errorCode(err) === "ENOENT") return false;
|
|
558
|
-
throw err;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
function wrapperEntrySource(runtime, workflows, toolFiles, systemPromptFile) {
|
|
562
|
-
const toolImports = toolFiles.map((file, i) => `import * as __aaiTool${i} from "../tools/${file}";`).join("\n");
|
|
563
|
-
const toolEntries = toolFiles.map((file, i) => ` ${JSON.stringify(`tools/${file}`)}: __aaiTool${i},`).join("\n");
|
|
564
|
-
return `import def from "../agent.ts";
|
|
565
|
-
import { agentToolsToSchemas, toAgentConfig, toolRegistry, withSystemPrompt, withTools } from "@alexkroman1/aai/manifest";
|
|
566
|
-
${runtime ? `import { createRuntime } from "@alexkroman1/aai-runtime";` : ""}
|
|
567
|
-
${systemPromptFile ? `import __aaiSystemPrompt from "../${SYSTEM_PROMPT_FILE}?raw";` : ""}
|
|
568
|
-
${toolImports}
|
|
569
|
-
// A tool's name is its file name. The map is built here rather than written in
|
|
570
|
-
// agent.ts, so a file that exists is a tool the model can call — there is no
|
|
571
|
-
// registration step to forget.
|
|
572
|
-
//
|
|
573
|
-
// \`system-prompt.md\` arrives the same way, and the \`?raw\` lives HERE rather
|
|
574
|
-
// than in the author's own \`agent.ts\`: it is a Vite convention, and the whole
|
|
575
|
-
// point of generating this entry is that a bundler feature never has to appear
|
|
576
|
-
// in user-authored space.
|
|
577
|
-
const __aaiAgent = ${systemPromptFile ? "withSystemPrompt(" : ""}withTools(
|
|
578
|
-
def,
|
|
579
|
-
toolRegistry({
|
|
580
|
-
${toolEntries}
|
|
581
|
-
}),
|
|
582
|
-
)${systemPromptFile ? ", __aaiSystemPrompt)" : ""};
|
|
583
|
-
export default __aaiAgent;
|
|
584
|
-
export const __aaiConfig = {
|
|
585
|
-
...toAgentConfig(__aaiAgent),
|
|
586
|
-
toolSchemas: agentToolsToSchemas(__aaiAgent.tools ?? {}),
|
|
587
|
-
};
|
|
588
|
-
${runtime ? `export const __aaiCreateRuntime = (opts: Record<string, unknown>) =>
|
|
589
|
-
createRuntime({ ...opts, agent: __aaiAgent });
|
|
590
|
-
` : ""}${workflows ? `// The compiled workflow surface, carried as DATA. \`__aaiWorkflowCode\` goes to
|
|
591
|
-
// \`workflowEntrypoint(code)\` and \`__aaiStepCode\` is evaluated by the guest so its
|
|
592
|
-
// \`registerStepFunction\` calls run. Strings rather than modules because the guest
|
|
593
|
-
// receives exactly one ESM string and never sees this project's filesystem.
|
|
594
|
-
export const __aaiWorkflowCode = ${JSON.stringify(workflows.workflowCode)};
|
|
595
|
-
export const __aaiStepCode = ${JSON.stringify(workflows.stepCode)};
|
|
596
|
-
` : ""}`;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Bundle agent.ts into a single ESM string for the sandbox worker.
|
|
600
|
-
*
|
|
601
|
-
* Zod is bundled in — zod 4's `Function()` usage is wrapped in try/catch
|
|
602
|
-
* and gracefully degrades in restricted environments like Deno.
|
|
603
|
-
*
|
|
604
|
-
* @internal — build hook for aai-server/the studio; not a supported public
|
|
605
|
-
* API and not covered by semver.
|
|
606
|
-
*/
|
|
607
|
-
async function buildWorker(cwd, opts = {}) {
|
|
608
|
-
const wrapperPath = path.join(cwd, WRAPPER_ENTRY_REL);
|
|
609
|
-
const [, toolFiles, systemPromptFile] = await Promise.all([
|
|
610
|
-
fs.mkdir(path.dirname(wrapperPath), { recursive: true }),
|
|
611
|
-
discoverToolFiles(cwd),
|
|
612
|
-
hasSystemPromptFile(cwd)
|
|
613
|
-
]);
|
|
614
|
-
await fs.writeFile(wrapperPath, wrapperEntrySource(opts.runtime !== false, opts.workflows, toolFiles, systemPromptFile), "utf-8");
|
|
615
|
-
const plugins = [...opts.plugins ?? [], ...opts.workflows ? [workflowClientPlugin(cwd, opts.workflows.inputFiles)] : []];
|
|
616
|
-
let result;
|
|
617
|
-
try {
|
|
618
|
-
result = await withPreservedNodeEnv(() => build({
|
|
619
|
-
root: cwd,
|
|
620
|
-
logLevel: "silent",
|
|
621
|
-
...opts.configFile === false && { configFile: false },
|
|
622
|
-
...plugins.length > 0 && { plugins },
|
|
623
|
-
ssr: { noExternal: true },
|
|
624
|
-
build: {
|
|
625
|
-
ssr: true,
|
|
626
|
-
lib: {
|
|
627
|
-
entry: wrapperPath,
|
|
628
|
-
formats: ["es"],
|
|
629
|
-
fileName: "worker"
|
|
630
|
-
},
|
|
631
|
-
target: "node20",
|
|
632
|
-
minify: opts.minify ? "oxc" : false,
|
|
633
|
-
write: false,
|
|
634
|
-
rollupOptions: { output: {
|
|
635
|
-
entryFileNames: "[name].js",
|
|
636
|
-
codeSplitting: false
|
|
637
|
-
} }
|
|
638
|
-
}
|
|
639
|
-
}));
|
|
640
|
-
} finally {
|
|
641
|
-
await fs.rm(wrapperPath, { force: true }).catch(() => void 0);
|
|
642
|
-
}
|
|
643
|
-
const output = Array.isArray(result) ? result[0] : result;
|
|
644
|
-
if (!output) throw new Error("Vite produced no output for agent.ts");
|
|
645
|
-
const chunk = output.output.find((o) => o.type === "chunk" && o.isEntry);
|
|
646
|
-
if (!chunk) throw new Error("Vite produced no entry chunk for agent.ts");
|
|
647
|
-
return chunk.code;
|
|
648
|
-
}
|
|
649
|
-
//#endregion
|
|
650
|
-
export { buildWorkflows as n, buildWorker as t };
|