@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "14.0.0",
3
+ "version": "15.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,6 +38,11 @@
38
38
  "@dev/source": "./src/project-config.ts",
39
39
  "types": "./dist/project-config.d.ts",
40
40
  "import": "./dist/project-config.mjs"
41
+ },
42
+ "./start": {
43
+ "@dev/source": "./src/start.ts",
44
+ "types": "./dist/start.d.ts",
45
+ "import": "./dist/start.mjs"
41
46
  }
42
47
  },
43
48
  "dependencies": {
@@ -52,9 +57,9 @@
52
57
  "p-timeout": "^7.0.1",
53
58
  "vite": "^8.2.2",
54
59
  "zod": "^4.5.4",
55
- "@alexkroman1/aai": "14.0.0",
56
- "@alexkroman1/aai-runtime": "14.0.0",
57
- "@alexkroman1/aai-ui": "14.0.0"
60
+ "@alexkroman1/aai": "15.1.0",
61
+ "@alexkroman1/aai-runtime": "15.1.0",
62
+ "@alexkroman1/aai-ui": "15.1.0"
58
63
  },
59
64
  "devDependencies": {
60
65
  "playwright": "^1.62.1",
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as errorCode } from "./_utils-D5JGcjiW.mjs";
3
- import path from "node:path";
4
- import { parseEnv } from "node:util";
5
- import fs from "node:fs/promises";
6
- //#region src/_server-common.ts
7
- /**
8
- * Build the `ctx.env` record that agent tools will see at runtime.
9
- *
10
- * Only variables explicitly declared in `.env` are included — matching
11
- * the platform sandbox behavior where `ctx.env`
12
- * contains only secrets set via `aai secret put`. This prevents agents
13
- * from accidentally depending on shell-level vars (PATH, HOME, etc.) that
14
- * won't exist in production.
15
- *
16
- * Values are resolved by merging the `.env` file with the current
17
- * environment — existing shell exports take precedence over `.env`
18
- * defaults, without mutating `process.env`.
19
- *
20
- * @param cwd - Project directory containing `.env` (optional).
21
- * @param baseEnv - Override the environment to read values from (tests only).
22
- */
23
- async function resolveServerEnv(cwd, baseEnv) {
24
- let fileEntries = {};
25
- if (cwd) {
26
- let content = null;
27
- try {
28
- content = await fs.readFile(path.join(cwd, ".env"), "utf-8");
29
- } catch (err) {
30
- if (errorCode(err) !== "ENOENT") throw err;
31
- }
32
- if (content !== null) fileEntries = parseEnv(content);
33
- }
34
- const source = baseEnv ?? process.env;
35
- const env = {};
36
- for (const [key, fileVal] of Object.entries(fileEntries)) {
37
- const val = source[key] ?? fileVal;
38
- if (val !== void 0 && val !== "") env[key] = val;
39
- }
40
- return env;
41
- }
42
- //#endregion
43
- export { resolveServerEnv as t };
@@ -1,108 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as ok, t as CliError } from "./_output-DBP9Op_d.mjs";
3
- import { n as log, r as notify } from "./_ui-DCt4qZrk.mjs";
4
- import { r as evalWorkerBundle, t as buildAgentBundle } from "./_bundler-DolUCMxu.mjs";
5
- import { assertTypechecks } from "./_typecheck-gate-U3jcil8L.mjs";
6
- import { n as scanWorkflowDeterminism, t as determinismWarnings } from "./_workflow-determinism-CvpqMpdh.mjs";
7
- import { TEST_FILES, classifyVitestError, runVitest } from "./test-D032V2cA.mjs";
8
- import path from "node:path";
9
- import fs from "node:fs/promises";
10
- import { agentConfigWarnings } from "@alexkroman1/aai/manifest";
11
- import { DEFAULT_SYSTEM_PROMPT } from "@alexkroman1/aai";
12
- //#region src/build.ts
13
- /**
14
- * `aai build` — bundle the agent without deploying, behind the same gates
15
- * deploy runs (tests, then typecheck), so the command previews the deploy
16
- * artifact and its failures alike.
17
- *
18
- * It also LEAVES that artifact on disk, at {@link WORKER_ARTIFACT_REL}, which is
19
- * what makes self-hosting work: `server.mjs` loads the built worker rather than
20
- * `agent.ts`, because a tool is discovered by the bundler enumerating `tools/`
21
- * and no un-bundled loader can see that directory. See the scaffold's own
22
- * `server.mjs` and "Self-hosting runs the built worker" in
23
- * `packages/aai-templates/CLAUDE.md`.
24
- */
25
- /**
26
- * Where the built worker lands, relative to the project root — under `.aai/`,
27
- * beside the built client, so one gitignore rule covers every build output.
28
- *
29
- * `server.mjs` hardcodes this path: it is a plain `.mjs` file in a user's
30
- * project and may not import from the CLI (which a deployed project does not
31
- * install at all). The pair is covered end-to-end by the `npm start` leg of
32
- * `e2e.test.ts` — the only tier that runs both halves as a user does.
33
- */
34
- const WORKER_ARTIFACT_REL = path.join(".aai", "worker.mjs");
35
- /**
36
- * The prose slot, named by convention. Mirrors `worker-bundler.ts`'s constant of
37
- * the same name — the two cannot share one without an import cycle
38
- * (`build.ts` → `_bundler.ts` → `worker-bundler.ts`), so `_build.test.ts` pins
39
- * the pair end to end instead: a build whose prompt really came from the file
40
- * has to report the file.
41
- */
42
- const SYSTEM_PROMPT_FILE = "system-prompt.md";
43
- /** What {@link systemPromptSource} reports when nothing declared a prompt. */
44
- const FRAMEWORK_DEFAULT_PROMPT_SOURCE = "the framework default (DEFAULT_SYSTEM_PROMPT)";
45
- /**
46
- * Which of the three possible authors of this agent's system prompt actually
47
- * wrote the one in the bundle.
48
- *
49
- * `aai build` reported `name` and `workerBytes` and nothing about the prompt,
50
- * and the prompt is the one field that can change COMPLETELY without any
51
- * config changing: deleting `system-prompt.md` swaps in
52
- * `DEFAULT_SYSTEM_PROMPT` — a total personality change — with exit 0 and no
53
- * line anywhere. `withSystemPrompt` refuses the case where a file exists and
54
- * nothing reads it; it cannot refuse a file that is simply GONE, because that
55
- * is what an agent with no file legitimately looks like. So this is a report
56
- * rather than a check.
57
- *
58
- * Decided by comparing VALUES against the built agent's resolved prompt, which
59
- * is the same method `withSystemPrompt` uses and for the same reason — the
60
- * alternative asks the bundler's module graph a question, and the author may
61
- * have imported the file and composed it, which is neither "the file" nor
62
- * "agent.ts" alone.
63
- */
64
- async function systemPromptSource(cwd, resolved) {
65
- if (resolved === DEFAULT_SYSTEM_PROMPT) return FRAMEWORK_DEFAULT_PROMPT_SOURCE;
66
- const trimmed = (await fs.readFile(path.join(cwd, SYSTEM_PROMPT_FILE), "utf-8").catch(() => void 0))?.trim();
67
- if (trimmed === void 0 || trimmed === "" || !resolved.includes(trimmed)) return "agent.ts";
68
- return resolved.trim() === trimmed ? SYSTEM_PROMPT_FILE : `agent.ts (composing ${SYSTEM_PROMPT_FILE})`;
69
- }
70
- /**
71
- * Map a {@link runVitest} failure to a CliError — distinguishing a real test
72
- * failure (test_failed) from the runner not spawning (spawn_failed) instead
73
- * of a generic command_failed.
74
- */
75
- function testGateError(err) {
76
- const { code, message } = classifyVitestError(err);
77
- return new CliError(code, message, "Re-run with --skipTests to build without tests", { cause: err });
78
- }
79
- async function executeBuild(opts) {
80
- const { cwd } = opts;
81
- if (!opts.skipTests) try {
82
- runVitest(cwd, {
83
- candidates: TEST_FILES,
84
- all: true
85
- });
86
- } catch (err) {
87
- throw testGateError(err);
88
- }
89
- await assertTypechecks(cwd, { skip: opts.skipTypecheck });
90
- const bundle = await buildAgentBundle(cwd, { minify: true });
91
- const agentDef = await evalWorkerBundle(bundle.worker);
92
- for (const warning of agentConfigWarnings(agentDef)) notify("warn", warning);
93
- for (const warning of determinismWarnings(await scanWorkflowDeterminism(cwd))) notify("warn", warning);
94
- const worker = path.join(cwd, WORKER_ARTIFACT_REL);
95
- await fs.mkdir(path.dirname(worker), { recursive: true });
96
- await fs.writeFile(worker, bundle.worker, "utf-8");
97
- const systemPrompt = await systemPromptSource(cwd, agentDef.systemPrompt);
98
- log.info(`System prompt: ${systemPrompt}`);
99
- log.success("Build complete");
100
- return ok({
101
- name: agentDef.name,
102
- workerBytes: bundle.worker.length,
103
- worker,
104
- systemPrompt
105
- });
106
- }
107
- //#endregion
108
- export { executeBuild };
@@ -1,308 +0,0 @@
1
- // Self-hosted entrypoint — this is what `npm start` runs.
2
- //
3
- // It serves the agent over HTTP + WebSocket from your own Node process: no
4
- // platform account, no managed anything. `aai dev` is the development
5
- // counterpart (file watching, Vite, a browser that opens itself); this file is
6
- // the deployment.
7
- //
8
- // npm start # http://127.0.0.1:3000
9
- // PORT=8080 HOST=0.0.0.0 npm start # bind every interface, e.g. in a container
10
- //
11
- // Anything that can run Node can host it: copy the project, install
12
- // dependencies, provide the secrets, run `npm start`. Deleting this file costs
13
- // nothing — `aai dev`, `aai publish` and the managed platform never read it.
14
-
15
- import { existsSync } from "node:fs";
16
- import { readFile } from "node:fs/promises";
17
- import path from "node:path";
18
- import { fileURLToPath, pathToFileURL } from "node:url";
19
- import { parseEnv } from "node:util";
20
- import {
21
- createAgentServer,
22
- ensureSessionStateSchema,
23
- ensureWorkflowJournalSchema,
24
- withHostCredentialFallback,
25
- } from "@alexkroman1/aai-runtime";
26
- import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
27
-
28
- const root = path.dirname(fileURLToPath(import.meta.url));
29
-
30
- /**
31
- * The built agent — `.aai/worker.mjs`, produced by `aai build`, which the
32
- * `prestart` script runs for you. It is the SAME artifact `aai publish` uploads
33
- * and the managed platform runs, so a self-hosted agent and a deployed one
34
- * cannot behave differently.
35
- *
36
- * Loading the build rather than `agent.ts` directly is what makes `tools/`
37
- * work. A tool is registered by EXISTING — its file name is the name the model
38
- * calls, and nothing anywhere lists them — and the only place a directory can
39
- * be turned into modules is where the bundle is assembled: a deployed agent is
40
- * handed one ESM string and has no filesystem to scan. So the bundler
41
- * enumerates `tools/` and emits the imports, and every loader that skips it
42
- * would serve an agent with no tools at all.
43
- *
44
- * It also settles two things the bundler already resolves, and this file used
45
- * to re-implement with `node:module` hooks: `import prompt from
46
- * "./system-prompt.md?raw"` (a Vite convention — Node looks for a file
47
- * literally named `system-prompt.md?raw`) and `import data from "./data.json"`
48
- * with no import attribute (TypeScript's `resolveJsonModule` allows it, Node
49
- * requires `with { type: "json" }`). Both are inlined into the bundle, so
50
- * there is nothing left to teach Node.
51
- */
52
- const workerPath = path.join(root, ".aai", "worker.mjs");
53
- if (!existsSync(workerPath)) {
54
- console.error(
55
- `No built agent at ${path.relative(root, workerPath)}.\n` +
56
- "Run `npm run build` (or `aai build`) first — `npm start` normally does it for you.",
57
- );
58
- process.exit(1);
59
- }
60
- // A file: URL rather than a relative specifier, so the path is correct on
61
- // Windows, where a bare POSIX-looking path is not a valid module specifier.
62
- const worker = await import(pathToFileURL(workerPath).href);
63
- const agent = worker.default;
64
-
65
- /**
66
- * Parse a dotenv-syntax file into a record; `{}` when it does not exist.
67
- *
68
- * @param {string} file - Path relative to the project root.
69
- * @returns {Promise<Record<string, string | undefined>>}
70
- */
71
- async function readEnvFile(file) {
72
- try {
73
- return parseEnv(await readFile(path.join(root, file), "utf-8"));
74
- } catch (err) {
75
- // Absent is normal — `.env` is gitignored, and a container usually has
76
- // neither file. Unreadable is not: the agent would boot with no
77
- // credentials and fail later as an opaque provider auth error.
78
- if (err.code !== "ENOENT") throw err;
79
- return {};
80
- }
81
- }
82
-
83
- /**
84
- * Build `ctx.env` — what this agent's own tool code reads.
85
- *
86
- * The rule is the one `aai dev` follows: only DECLARED keys are exposed, and a
87
- * real environment variable wins over the file's value. Nothing else from
88
- * process.env comes along, so the agent cannot come to depend on a variable
89
- * (PATH, HOME, …) that will not exist wherever you deploy it.
90
- *
91
- * `.env.example` counts as a declaration too, which is what lets a container
92
- * run with no `.env` at all: the committed file names the secrets the agent
93
- * needs, and `docker run -e MY_API_KEY=…` supplies the values. Declare
94
- * `DATABASE_URL` the same way to give your tools `ctx.db`.
95
- */
96
- async function resolveAgentEnv() {
97
- const declared = { ...(await readEnvFile(".env.example")), ...(await readEnvFile(".env")) };
98
- /** @type {Record<string, string>} */
99
- const env = {};
100
- for (const [key, fileValue] of Object.entries(declared)) {
101
- const value = process.env[key] ?? fileValue;
102
- // An empty value is worse than a missing one: a provider would try to
103
- // authenticate with "" rather than report the credential as absent. The
104
- // example file is full of them by design (`BRAVE_API_KEY=`). A key the
105
- // parser saw with no value at all is the same case.
106
- if (value !== undefined && value !== "") env[key] = value;
107
- }
108
- return env;
109
- }
110
-
111
- /**
112
- * Static assets served at `/`: this project's own UI once `client.tsx` has been
113
- * built (the same `aai build` leaves it in `.aai/client`), otherwise the
114
- * prebuilt default client that ships inside @alexkroman1/aai-ui.
115
- */
116
- function resolveClientDir() {
117
- const built = path.join(root, ".aai", "client");
118
- if (existsSync(path.join(built, "index.html"))) return built;
119
- if (existsSync(path.join(root, "client.tsx"))) {
120
- console.warn("client.tsx is not built — serving the default UI. Run `npm run build` first.");
121
- }
122
- return defaultClientDir();
123
- }
124
-
125
- /**
126
- * The error classes that mean a DEFECT in this code rather than a mistake in
127
- * the configuration.
128
- *
129
- * Everything below turns a boot failure into two lines and a non-zero exit,
130
- * which is right for "ASSEMBLYAI_API_KEY is not set" and wrong for a
131
- * `TypeError`, where the traceback is the only thing that can locate the bug.
132
- * Those are re-thrown untouched.
133
- */
134
- const BUG_ERRORS = [TypeError, ReferenceError, RangeError, SyntaxError];
135
-
136
- /**
137
- * Every distinct message on an error and its `cause` chain, outermost first.
138
- *
139
- * A driver failure states the useful half one hop down — `connect to database
140
- * failed: getaddrinfo ENOTFOUND db` — so printing only the top message is how a
141
- * tidy envelope ends up less informative than the stack it replaced.
142
- *
143
- * @param {unknown} err
144
- * @returns {string}
145
- */
146
- function errorText(err) {
147
- /** @type {string[]} */
148
- const messages = [];
149
- /** @type {unknown} */
150
- let cursor = err;
151
- while (cursor instanceof Error) {
152
- if (cursor.message !== "" && !messages.includes(cursor.message)) messages.push(cursor.message);
153
- cursor = cursor.cause;
154
- }
155
- return messages.length > 0 ? messages.join(": ") : String(err);
156
- }
157
-
158
- /**
159
- * Run one step of BOOT, and answer a failure the way this file already answers
160
- * a missing build artifact: what is wrong, then what to do, then exit 1.
161
- *
162
- * Without this, a missing provider key — the commonest way a first `npm start`
163
- * fails — killed the process with a ten-frame traceback pointing into
164
- * `node_modules/@alexkroman1/aai-runtime/dist/host-env-*.js`. The MESSAGE was
165
- * already good ("AssemblyAI LLM: missing API key. Set ASSEMBLYAI_API_KEY in the
166
- * agent env."); what it arrived wrapped in was a crash report about somebody
167
- * else's bundle, in a container that then restarted and did it again.
168
- *
169
- * Exiting is deliberate rather than binding anyway and serving an unhealthy
170
- * `/health`: a process that stays up tells an orchestrator it started, and a
171
- * misconfigured deployment that reports itself healthy is worse than one that
172
- * refuses to run. The non-zero exit is what a supervisor, a `docker run`, and
173
- * CI all already read.
174
- *
175
- * @template T
176
- * @param {string} fix - What the operator should change, in one sentence.
177
- * @param {() => T | Promise<T>} work
178
- * @returns {Promise<T>}
179
- */
180
- async function bootOrExit(fix, work) {
181
- try {
182
- return await work();
183
- } catch (err) {
184
- if (BUG_ERRORS.some((kind) => err instanceof kind)) throw err;
185
- console.error(`Cannot start the agent: ${errorText(err)}\n${fix}`);
186
- process.exit(1);
187
- }
188
- }
189
-
190
- const env = await resolveAgentEnv();
191
-
192
- /**
193
- * Where THIS server is reachable from outside — `PUBLIC_URL`, e.g.
194
- * `https://agent.example.com`. Set it whenever a durable workflow has to hand a
195
- * URL to somebody else: `ctx.workflows.publicWebhookUrl(token)` is built from it,
196
- * and without it that call throws rather than minting a `http://localhost:3000`
197
- * URL that a payment provider will try, days later, and fail.
198
- *
199
- * Nothing else reads it, so it is not a startup requirement: an agent with no
200
- * webhooks needs no value here. It is deliberately NOT derived from `PORT`/`HOST`
201
- * — those describe the socket this process binds, which behind a reverse proxy is
202
- * not what the outside world dials.
203
- */
204
- const publicUrl = process.env.PUBLIC_URL?.trim();
205
-
206
- /**
207
- * Create the session-state tables, when this agent has a database.
208
- *
209
- * A `DATABASE_URL` puts session state in Postgres, and those tables come with
210
- * whoever OWNS the database — which for a self-hosted agent is you, with no
211
- * migration step anywhere to hang them off. Without this the server starts,
212
- * reports `sessionState: postgres, durable: true`, and then every session dies
213
- * at start with a fatal error the browser shows as "Session failed to start",
214
- * the real reason (`relation "aai_session_events" does not exist`) appearing
215
- * only in this process's log.
216
- *
217
- * Best-effort: if a real migration already created them and this role may not
218
- * CREATE, it warns and the server starts anyway.
219
- */
220
- // Read into a const: `env.DATABASE_URL` is a record lookup, so its narrowing
221
- // does not survive into the callback below.
222
- const databaseUrl = env.DATABASE_URL;
223
- if (databaseUrl) {
224
- await bootOrExit(
225
- "Check DATABASE_URL: this server has to reach that database at boot to create the tables it owns.",
226
- async () => {
227
- await ensureSessionStateSchema({ url: databaseUrl, logger: console });
228
- // And the durable-run journal's, which is a separate set of tables owned
229
- // by the same deployment. Without it a project with a `DATABASE_URL`
230
- // boots claiming durable runs and fails on the first one.
231
- await ensureWorkflowJournalSchema({ url: databaseUrl, logger: console });
232
- },
233
- );
234
- } else {
235
- /**
236
- * Say that this process is the only place the state lives, because the next
237
- * thing an operator does with a container is run two of them.
238
- *
239
- * Session state (slots, the event log) is keyed by session id and held in
240
- * memory here — the boot line below reports it as `sessionState: { backend:
241
- * 'memory', durable: false }`, which is true and easy to read as being about
242
- * restarts alone. It is also about REPLICAS: the browser reconnects with
243
- * `?sessionId=<id>`, so a reconnect that lands on a different process resumes
244
- * a session that process has never heard of and the agent's context is gone
245
- * mid-call. One replica has no such problem, which is exactly why nothing
246
- * catches this until the deployment grows a second one.
247
- */
248
- console.warn(
249
- "No DATABASE_URL: session state and durable runs live in THIS process's memory.\n" +
250
- "One replica is fine. Behind a load balancer, enable sticky sessions so a reconnect " +
251
- "(the client re-dials with ?sessionId=) reaches the same process — or set DATABASE_URL " +
252
- "and let every replica share the state.",
253
- );
254
- }
255
-
256
- const server = await bootOrExit(
257
- // The commonest first-run failure, and the one whose stack this replaces: a
258
- // provider credential that is not there. `.env` is what `aai dev` reads too,
259
- // so the fix is the same one in both places.
260
- "Set the missing value in .env, or pass it as a real environment variable (`docker run -e NAME=value`), then start again.",
261
- () =>
262
- createAgentServer({
263
- agent,
264
- env,
265
- // Provider credentials may ALSO arrive straight from the environment without
266
- // being declared, and without becoming ctx.env — the ordinary way to hand
267
- // ASSEMBLYAI_API_KEY to a container. Anything in `env` still wins.
268
- providerEnv: withHostCredentialFallback(env),
269
- clientDir: resolveClientDir(),
270
- ...(publicUrl ? { publicUrl } : {}),
271
- // Durable workflows need nothing passed here. A `DATABASE_URL` in `env` puts
272
- // the runs in Postgres and they survive a restart; without one they live in a
273
- // per-process directory and do not, which is the same trade `aai dev` makes.
274
- //
275
- // Two options used to sit here — the compiled workflow surface, carried on the
276
- // bundle as `__aaiWorkflowCode`/`__aaiStepCode` because a `"use workflow"` body
277
- // had to go through a compiler at BUILD time. The engine reads the agent's own
278
- // `workflows` declaration instead, so there is no artifact to hand over.
279
- }),
280
- );
281
-
282
- // Loopback by default: this server has no request authentication of its own,
283
- // so exposing it is a deliberate act. Set HOST=0.0.0.0 to bind every interface
284
- // behind your own proxy or auth. An empty HOST means unset, not "everything".
285
- const host = process.env.HOST?.trim() || undefined;
286
- const port = Number(process.env.PORT ?? 3000);
287
- await bootOrExit(
288
- `Nothing is listening yet — port ${port} is in use, or this process may not bind it. Set PORT to a free one.`,
289
- () => server.listen(port, host),
290
- );
291
- console.log(`${agent.name} listening on http://${host ?? "127.0.0.1"}:${server.port}`);
292
-
293
- for (const signal of ["SIGINT", "SIGTERM"]) {
294
- // A SYNCHRONOUS listener. An `async` one hands its promise to `process`,
295
- // which discards what a listener returns — so a `close()` that rejects would
296
- // surface as an unhandled rejection, i.e. a crash with a stack trace on
297
- // Ctrl-C, instead of the non-zero exit a failed shutdown should be.
298
- process.once(signal, () => {
299
- // close() shuts the runtime down too — no separate runtime.shutdown().
300
- server.close().then(
301
- () => process.exit(0),
302
- (error) => {
303
- console.error(`shutdown failed: ${error?.message ?? error}`);
304
- process.exit(1);
305
- },
306
- );
307
- });
308
- }