@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "
|
|
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": "
|
|
56
|
-
"@alexkroman1/aai-runtime": "
|
|
57
|
-
"@alexkroman1/aai-ui": "
|
|
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 };
|
package/dist/build-Mxk8gWvX.mjs
DELETED
|
@@ -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 };
|
package/dist/scaffold/server.mjs
DELETED
|
@@ -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
|
-
}
|