@alexkroman1/aai-cli 13.3.0 → 14.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/{_agent-Clbq3a4h.mjs → _agent-BzUeqOdj.mjs} +32 -9
- package/dist/_agent.d.ts +23 -0
- package/dist/{_bundler-DEkjO1d8.mjs → _bundler-DolUCMxu.mjs} +8 -7
- package/dist/_cli-common.d.ts +45 -6
- package/dist/{_config-ZY2euhdh.mjs → _config-DYzC6WMD.mjs} +2 -2
- package/dist/_dev-env.d.ts +1 -1
- package/dist/{_dev-server-C13xuGB1.mjs → _dev-server-CSMqF8PN.mjs} +20 -22
- package/dist/_dev-vite-config.d.ts +3 -3
- package/dist/{_init-Ci4QGjp8.mjs → _init-CQ8idAwo.mjs} +9 -23
- package/dist/{_output-DbJt9gsl.mjs → _output-DBP9Op_d.mjs} +22 -13
- package/dist/_output.d.ts +10 -9
- package/dist/{_preflight-B2laWPdU.mjs → _preflight-CcAR577V.mjs} +2 -2
- package/dist/_resource-commands.d.ts +9 -9
- package/dist/{_server-common-BV4_CrP3.mjs → _server-common-vILJp3it.mjs} +1 -1
- package/dist/{_slug-api-DlKpj91X.mjs → _slug-api-CGvc2YSQ.mjs} +18 -9
- package/dist/_slug-api.d.ts +19 -0
- package/dist/{_studio-DTi3EdXQ.mjs → _studio-DnR_BqFp.mjs} +7 -3
- package/dist/_studio-commands.d.ts +13 -13
- package/dist/{_templates-V4l5sZJy.mjs → _templates-CK4oKoeX.mjs} +3 -3
- package/dist/{_typecheck-gate-sV4N5F93.mjs → _typecheck-gate-U3jcil8L.mjs} +3 -2
- package/dist/_typecheck-gate.d.ts +3 -1
- package/dist/{_utils-Dhd058D9.mjs → _utils-D5JGcjiW.mjs} +43 -1
- package/dist/_utils.d.ts +35 -0
- package/dist/{_workflow-determinism-C9l8uC2R.mjs → _workflow-determinism-CvpqMpdh.mjs} +18 -26
- package/dist/_workflow-determinism.d.ts +3 -3
- package/dist/{build-CWyFGe-W.mjs → build-Mxk8gWvX.mjs} +46 -7
- package/dist/build.d.ts +5 -0
- package/dist/cli.d.ts +1 -2
- package/dist/cli.mjs +117 -135
- package/dist/{client-bundler-CT-_26c5.mjs → client-bundler-BJgREAh6.mjs} +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-RqlQZ6Y_.mjs → delete-BdFnbnVR.mjs} +4 -4
- package/dist/{deploy-B7f3CK7v.mjs → deploy-uAJ4NukN.mjs} +11 -11
- package/dist/{dev-DamXieW_.mjs → dev-DApPSaE_.mjs} +3 -3
- package/dist/{eval-h3DTVfRw.mjs → eval-BK47A_K5.mjs} +3 -3
- package/dist/{init-D-vFrV8h.mjs → init-DukDxECd.mjs} +11 -19
- package/dist/{login-CDiemNmC.mjs → login-DaLrPOFg.mjs} +4 -6
- package/dist/{logs-DGwPNCzB.mjs → logs-7BRwq0cG.mjs} +21 -14
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +84 -56
- package/dist/scaffold/package.json +4 -4
- package/dist/scaffold/server.mjs +129 -25
- package/dist/{secret-V1w7y9-j.mjs → secret-Cjp912yF.mjs} +5 -5
- package/dist/secret.d.ts +8 -6
- package/dist/{studio-BpMCfUNx.mjs → studio-CpHlNHUZ.mjs} +13 -15
- package/dist/templates/call-audit/agent.test.ts +11 -6
- package/dist/templates/call-audit/agent.ts +2 -2
- package/dist/templates/call-audit/client.tsx +27 -42
- package/dist/templates/call-audit/workflows/audit.ts +7 -5
- package/dist/templates/call-audit/workflows/ingest.ts +7 -7
- package/dist/templates/call-audit/workflows/media.ts +12 -37
- package/dist/templates/call-audit/workflows/summarize.ts +8 -8
- package/dist/templates/call-audit/workflows/sync-api.ts +3 -3
- package/dist/templates/dispatch-center/agent.test.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +53 -61
- package/dist/templates/dispatch-center/shared.ts +0 -8
- package/dist/templates/dispatch-center/tools/ops_dashboard.ts +4 -1
- package/dist/templates/embedded-assets/agent.eval.test.ts +4 -2
- package/dist/templates/health-assistant/agent.eval.test.ts +4 -2
- package/dist/templates/infocom-adventure/client.tsx +40 -36
- package/dist/templates/link-digest/agent.test.ts +3 -3
- package/dist/templates/link-digest/agent.ts +2 -2
- package/dist/templates/link-digest/client.tsx +27 -17
- package/dist/templates/link-digest/workflows/digest.ts +13 -16
- package/dist/templates/night-owl/agent.eval.test.ts +4 -2
- package/dist/templates/night-owl/client.tsx +8 -2
- package/dist/templates/pipeline-simple/agent.test.ts +2 -2
- package/dist/templates/pizza-ordering/client.tsx +5 -4
- package/dist/templates/pizza-ordering/shared.ts +3 -8
- package/dist/templates/pizza-ordering/tools/add_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/place_order.ts +3 -2
- package/dist/templates/pizza-ordering/tools/remove_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/update_pizza.ts +3 -2
- package/dist/templates/pizza-ordering/tools/view_order.ts +3 -2
- package/dist/templates/plan-and-execute/agent.test.ts +10 -5
- package/dist/templates/plan-and-execute/client.tsx +2 -2
- package/dist/templates/podcast-digest/agent.test.ts +20 -11
- package/dist/templates/podcast-digest/client.tsx +16 -22
- package/dist/templates/podcast-digest/workflows/digest.ts +21 -19
- package/dist/templates/podcast-digest/workflows/feeds.ts +61 -33
- package/dist/templates/podcast-digest/workflows/slack.ts +7 -7
- package/dist/templates/recap-workflow/agent.eval.test.ts +21 -13
- package/dist/templates/recap-workflow/agent.test.ts +27 -23
- package/dist/templates/recap-workflow/workflows/recap.ts +24 -28
- package/dist/templates/redline/agent.eval.test.ts +5 -8
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +36 -34
- package/dist/templates/redline/workflows/redline.ts +17 -17
- package/dist/templates/research-workflow/agent.eval.test.ts +5 -5
- package/dist/templates/research-workflow/agent.test.ts +10 -7
- package/dist/templates/research-workflow/workflows/research.ts +28 -30
- package/dist/templates/retail/agent.test.ts +17 -15
- package/dist/templates/retail/cancel.ts +7 -6
- package/dist/templates/retail/client.tsx +39 -58
- package/dist/templates/retail/payment.ts +4 -3
- package/dist/templates/retail/pending.test.ts +19 -19
- package/dist/templates/retail/store.test.ts +5 -5
- package/dist/templates/retail/swap.ts +8 -11
- package/dist/templates/solo-rpg/agent.eval.test.ts +2 -16
- package/dist/templates/solo-rpg/agent.test.ts +12 -12
- package/dist/templates/solo-rpg/client.tsx +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +9 -9
- package/dist/templates/spoken-summary/agent.ts +3 -3
- package/dist/templates/spoken-summary/client.tsx +23 -30
- package/dist/templates/spoken-summary/workflows/summarize.ts +11 -11
- package/dist/templates/spoken-summary/workflows/transcribe.ts +14 -14
- package/dist/templates/support-line/client.tsx +4 -3
- package/dist/templates/support-line/prompts.ts +0 -2
- package/dist/templates/transcription-workflow/agent.test.ts +20 -12
- package/dist/templates/transcription-workflow/agent.ts +2 -2
- package/dist/templates/transcription-workflow/client.tsx +15 -14
- package/dist/templates/transcription-workflow/recover.ts +1 -1
- package/dist/templates/transcription-workflow/run-panel.tsx +34 -14
- package/dist/templates/transcription-workflow/total-latency.tsx +46 -20
- package/dist/templates/transcription-workflow/workflows/batch.ts +15 -15
- package/dist/templates/transcription-workflow/workflows/downsample.ts +1 -1
- package/dist/templates/transcription-workflow/workflows/normalize.ts +15 -19
- package/dist/templates/transcription-workflow/workflows/stream.ts +12 -12
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +3 -3
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +23 -34
- package/dist/templates/travel-concierge/agent.eval.test.ts +2 -2
- package/dist/templates/travel-concierge/agent.test.ts +15 -7
- package/dist/templates/travel-concierge/client.tsx +2 -2
- package/dist/templates/travel-concierge/shared.ts +10 -14
- package/dist/templates/travel-concierge/tools/lookup_booking.ts +4 -3
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_excursions.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_flights.ts +3 -2
- package/dist/templates/travel-concierge/tools/search_hotels.ts +3 -2
- package/dist/{test-guenJUxi.mjs → test-D032V2cA.mjs} +4 -17
- package/dist/typecheck.mjs +1 -1
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-BZyKLkhV.mjs → workflow-CVioyO3V.mjs} +6 -5
- package/package.json +4 -4
package/dist/scaffold/server.mjs
CHANGED
|
@@ -122,6 +122,71 @@ function resolveClientDir() {
|
|
|
122
122
|
return defaultClientDir();
|
|
123
123
|
}
|
|
124
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
|
+
|
|
125
190
|
const env = await resolveAgentEnv();
|
|
126
191
|
|
|
127
192
|
/**
|
|
@@ -152,38 +217,77 @@ const publicUrl = process.env.PUBLIC_URL?.trim();
|
|
|
152
217
|
* Best-effort: if a real migration already created them and this role may not
|
|
153
218
|
* CREATE, it warns and the server starts anyway.
|
|
154
219
|
*/
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
);
|
|
161
254
|
}
|
|
162
255
|
|
|
163
|
-
const server =
|
|
164
|
-
|
|
165
|
-
env,
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
);
|
|
181
281
|
|
|
182
282
|
// Loopback by default: this server has no request authentication of its own,
|
|
183
283
|
// so exposing it is a deliberate act. Set HOST=0.0.0.0 to bind every interface
|
|
184
284
|
// behind your own proxy or auth. An empty HOST means unset, not "everything".
|
|
185
285
|
const host = process.env.HOST?.trim() || undefined;
|
|
186
|
-
|
|
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
|
+
);
|
|
187
291
|
console.log(`${agent.name} listening on http://${host ?? "127.0.0.1"}:${server.port}`);
|
|
188
292
|
|
|
189
293
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as ok, n as fail, t as CliError } from "./_output-
|
|
2
|
+
import { a as ok, n as fail, t as CliError } from "./_output-DBP9Op_d.mjs";
|
|
3
3
|
import { n as log, s as unwrapCancel } from "./_ui-DCt4qZrk.mjs";
|
|
4
|
-
import { f as checkedResponse, p as isStringArray } from "./_studio-
|
|
5
|
-
import { t as secretRequest } from "./_slug-api-
|
|
6
|
-
import { isRecord } from "@alexkroman1/aai/utils";
|
|
4
|
+
import { f as checkedResponse, p as isStringArray } from "./_studio-DnR_BqFp.mjs";
|
|
5
|
+
import { t as secretRequest } from "./_slug-api-CGvc2YSQ.mjs";
|
|
6
|
+
import { isRecord, plural } from "@alexkroman1/aai/utils";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
8
8
|
import pTimeout from "p-timeout";
|
|
9
9
|
//#region src/secret.ts
|
|
@@ -153,7 +153,7 @@ async function executeSecretList(cwd, server) {
|
|
|
153
153
|
const { vars } = checkedResponse(data, (value) => isRecord(value) && isStringArray(value.vars), `the secret list for ${target}`);
|
|
154
154
|
if (vars.length === 0) log.info("No secrets set. Use `aai secret put <name>` to add one.");
|
|
155
155
|
else {
|
|
156
|
-
log.message(`${vars.length}
|
|
156
|
+
log.message(`${vars.length} ${plural(vars.length, "secret")}:`);
|
|
157
157
|
for (const v of vars) log.message(` ${v}`);
|
|
158
158
|
}
|
|
159
159
|
return ok({ secrets: vars });
|
package/dist/secret.d.ts
CHANGED
|
@@ -38,10 +38,12 @@ export type SecretValueIo = {
|
|
|
38
38
|
* sitting at a terminal has no reason to send.
|
|
39
39
|
*/
|
|
40
40
|
export declare function resolveSecretValue(name: string, mode: OutputMode, io?: SecretValueIo): Promise<string | undefined>;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Both the put and the delete result — one shape, so one name. They were two
|
|
43
|
+
* distinct aliases for `{ name: string }`, which implied a difference that does
|
|
44
|
+
* not exist and left "which alias meant the secret result" to be re-decided.
|
|
45
|
+
*/
|
|
46
|
+
type SecretNameData = {
|
|
45
47
|
name: string;
|
|
46
48
|
};
|
|
47
49
|
type SecretListData = {
|
|
@@ -52,7 +54,7 @@ type SecretListData = {
|
|
|
52
54
|
* path). If not, prompt for it — masked — which is what
|
|
53
55
|
* {@link resolveSecretValue} returning `undefined` asks for.
|
|
54
56
|
*/
|
|
55
|
-
export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<
|
|
56
|
-
export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<
|
|
57
|
+
export declare function executeSecretPut(cwd: string, name: string, value: string | undefined, server: string | undefined): Promise<CommandResult<SecretNameData>>;
|
|
58
|
+
export declare function executeSecretDelete(cwd: string, name: string, server: string | undefined): Promise<CommandResult<SecretNameData>>;
|
|
57
59
|
export declare function executeSecretList(cwd: string, server: string | undefined): Promise<CommandResult<SecretListData>>;
|
|
58
60
|
export {};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as ok, t as CliError } from "./_output-
|
|
2
|
+
import { a as ok, t as CliError } from "./_output-DBP9Op_d.mjs";
|
|
3
3
|
import { n as log, t as fmtUrl } from "./_ui-DCt4qZrk.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { l as formatCappedList } from "./_utils-D5JGcjiW.mjs";
|
|
5
|
+
import { s as updateProjectConfig } from "./_config-DYzC6WMD.mjs";
|
|
6
|
+
import { t as resolveServerEnv } from "./_server-common-vILJp3it.mjs";
|
|
7
|
+
import { o as resolveDeployTarget } from "./_agent-BzUeqOdj.mjs";
|
|
8
|
+
import { layerScaffold } from "./_templates-CK4oKoeX.mjs";
|
|
9
|
+
import { a as publishStudioProject, c as studioProjectUrl, f as checkedResponse, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectApiUrl, t as collectSourceFiles, u as apiRequest } from "./_studio-DnR_BqFp.mjs";
|
|
9
10
|
import { existsSync } from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
|
-
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
12
|
+
import { isRecord, omitUndefined, plural } from "@alexkroman1/aai/utils";
|
|
12
13
|
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
13
14
|
import { isPathInside } from "@alexkroman1/aai-runtime/internal";
|
|
14
15
|
//#region src/studio.ts
|
|
@@ -62,7 +63,7 @@ async function notFoundHint(serverUrl, apiKey) {
|
|
|
62
63
|
const projects = await listStudioProjects(serverUrl, apiKey).catch(() => null);
|
|
63
64
|
if (projects === null) return "Run `aai list` to see your projects.";
|
|
64
65
|
if (projects.length === 0) return "This login has no studio projects at all. If yours are in the studio, the CLI is linked to a different account — run `aai login` again, approve it in a browser signed in to the account that owns the project, then `aai list`.";
|
|
65
|
-
return `Your projects: ${projects
|
|
66
|
+
return `Your projects: ${formatCappedList(projects)}.`;
|
|
66
67
|
}
|
|
67
68
|
async function executePull(opts) {
|
|
68
69
|
const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
|
|
@@ -159,17 +160,14 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, project) {
|
|
|
159
160
|
method: "PUT",
|
|
160
161
|
body: env
|
|
161
162
|
});
|
|
162
|
-
log.info(`Synced ${names.length}
|
|
163
|
+
log.info(`Synced ${names.length} ${plural(names.length, "secret")} from .env`);
|
|
163
164
|
return names;
|
|
164
165
|
}
|
|
165
166
|
async function executePublish(opts) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
await assertTypechecks(opts.cwd);
|
|
169
|
-
}
|
|
167
|
+
const { assertTypechecks } = await import("./_typecheck-gate-U3jcil8L.mjs");
|
|
168
|
+
await assertTypechecks(opts.cwd, { skip: opts.skipTypecheck });
|
|
170
169
|
const pushed = await pushProject(opts);
|
|
171
170
|
const { project, serverUrl, apiKey } = pushed;
|
|
172
|
-
const hadSlug = pushed.slug !== void 0;
|
|
173
171
|
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, project);
|
|
174
172
|
log.step(`Publishing ${project} (builds in the project's sandbox)…`);
|
|
175
173
|
const result = checkedResponse(await publishStudioProject(serverUrl, apiKey, project, { skipTypecheck: opts.skipTypecheck }), (value) => isRecord(value) && typeof value.slug === "string" && typeof value.output === "string", `the publish route at ${serverUrl}`);
|
|
@@ -178,7 +176,7 @@ async function executePublish(opts) {
|
|
|
178
176
|
serverUrl,
|
|
179
177
|
slug: result.slug
|
|
180
178
|
});
|
|
181
|
-
if (!
|
|
179
|
+
if (!pushed.slug) {
|
|
182
180
|
if ((await syncEnvSecrets(opts.cwd, serverUrl, apiKey, result.slug)).length > 0) log.info("They apply on the next `aai publish`.");
|
|
183
181
|
}
|
|
184
182
|
const agentUrl = `${serverUrl}/${result.slug}`;
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import { readdir } from "node:fs/promises";
|
|
33
33
|
import { tmpdir } from "node:os";
|
|
34
34
|
import { FatalError, RetryableError } from "@alexkroman1/aai/step-errors";
|
|
35
|
-
import {
|
|
35
|
+
import { createWorkflowContext, stubSpeech, WORKFLOW_CONTEXT_NOW } from "@alexkroman1/aai/testing";
|
|
36
36
|
import {
|
|
37
37
|
installStubGateway,
|
|
38
38
|
installStubReporter,
|
|
@@ -516,7 +516,7 @@ describe("planning where to cut", () => {
|
|
|
516
516
|
test("the last segment never addresses a byte the file does not have", () => {
|
|
517
517
|
// The twelve-byte bug this API shape exists to prevent: planning from a
|
|
518
518
|
// duration rounded to whole milliseconds put `endByte` at 640,512 for a
|
|
519
|
-
// 640,500-byte file. `
|
|
519
|
+
// 640,500-byte file. `stepReadUpload` clamps a window to the stored size, so
|
|
520
520
|
// nothing threw — the plan was simply describing audio that does not exist.
|
|
521
521
|
// Found by running the real argv against a real ffmpeg, which is the only
|
|
522
522
|
// place a twelve-byte error was ever going to surface.
|
|
@@ -867,14 +867,14 @@ describe("the body's step policy", () => {
|
|
|
867
867
|
* only reads `ctx.steps` afterwards, so it is built once.
|
|
868
868
|
*/
|
|
869
869
|
const walkedCtx = () =>
|
|
870
|
-
|
|
870
|
+
createWorkflowContext({
|
|
871
871
|
runSteps: false,
|
|
872
872
|
// The body reads the clock at both ends with `ctx.now()`, which the engine
|
|
873
873
|
// journals under `now!0` and `now!1`. A PRODUCER here rather than a fixed
|
|
874
874
|
// number, so the two reaches differ and `elapsedMs` is assertable — based
|
|
875
|
-
// on `
|
|
875
|
+
// on `WORKFLOW_CONTEXT_NOW`, the instant the fake otherwise freezes at, rather
|
|
876
876
|
// than on a second arbitrary epoch nobody can relate to the first.
|
|
877
|
-
now: clockReads(
|
|
877
|
+
now: clockReads(WORKFLOW_CONTEXT_NOW, WORKFLOW_CONTEXT_NOW + 3000),
|
|
878
878
|
results: {
|
|
879
879
|
ingestRecording: {
|
|
880
880
|
audio: "upl_pcm",
|
|
@@ -937,7 +937,7 @@ describe("the body's step policy", () => {
|
|
|
937
937
|
/**
|
|
938
938
|
* `auditFlow` on the real replay engine — as far as this tier can take it.
|
|
939
939
|
*
|
|
940
|
-
* The block above drives the body through `
|
|
940
|
+
* The block above drives the body through `createWorkflowContext` with
|
|
941
941
|
* `runSteps: false` and a journaled result per step, which is what makes it
|
|
942
942
|
* affordable: `ingestRecording` shells out to the ffmpeg toolchain, which this
|
|
943
943
|
* tier cannot usefully feed — the `ingestRecording` and `narrate` specs above
|
|
@@ -970,6 +970,11 @@ describe("the run is DURABLE, as far as ffmpeg allows", () => {
|
|
|
970
970
|
});
|
|
971
971
|
installStubReporter();
|
|
972
972
|
vi.stubEnv("ASSEMBLYAI_API_KEY", "test-key");
|
|
973
|
+
// Neither binary is this block's subject (see its doc), and a real
|
|
974
|
+
// conversion here would make a developer with ffmpeg installed run a
|
|
975
|
+
// slower, different test from CI, which has none.
|
|
976
|
+
vi.stubEnv("AAI_FFMPEG_PATH", "/nonexistent/ffmpeg");
|
|
977
|
+
vi.stubEnv("AAI_FFPROBE_PATH", "/nonexistent/ffprobe");
|
|
973
978
|
});
|
|
974
979
|
|
|
975
980
|
test("a FatalError in the first step fails the run on ONE attempt, not six", async () => {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - `transcription-workflow` owns the FAN-OUT — why the sync transcription
|
|
21
21
|
* endpoint's 120-second cap forces one, and how a run survives dying on segment
|
|
22
22
|
* 27 of 60.
|
|
23
|
-
* - `spoken-summary` owns the audio ROUND TRIP — `stepSpeak`, `
|
|
23
|
+
* - `spoken-summary` owns the audio ROUND TRIP — `stepSpeak`, `stepWriteUpload`, and
|
|
24
24
|
* why a page needs `api.download` rather than a URL.
|
|
25
25
|
*
|
|
26
26
|
* **What this template adds is a decoder**, and everything downstream changes
|
|
@@ -153,7 +153,7 @@ export const audit: WorkflowDef<typeof auditInput, CallAudit> = workflow({
|
|
|
153
153
|
input: auditInput,
|
|
154
154
|
// The one line that makes the form take a file: `<WorkflowFields>` renders a
|
|
155
155
|
// picker for this property, `useWorkflowSubmit` stores the chosen file, and the
|
|
156
|
-
// ingest step reads it back with `
|
|
156
|
+
// ingest step reads it back with `stepReadUpload`.
|
|
157
157
|
uploads: ["recording"],
|
|
158
158
|
run: auditFlow,
|
|
159
159
|
});
|
|
@@ -76,9 +76,11 @@ import "@alexkroman1/aai-ui/styles.css";
|
|
|
76
76
|
// already declares.
|
|
77
77
|
import { formatBytes, formatDuration } from "@alexkroman1/aai/utils";
|
|
78
78
|
import {
|
|
79
|
+
BulletList,
|
|
79
80
|
createWorkflowApi,
|
|
81
|
+
Facts,
|
|
80
82
|
Form,
|
|
81
|
-
|
|
83
|
+
mountPage,
|
|
82
84
|
SubmitButton,
|
|
83
85
|
UploadProgressBar,
|
|
84
86
|
useDownloadUrl,
|
|
@@ -86,7 +88,6 @@ import {
|
|
|
86
88
|
WorkflowFields,
|
|
87
89
|
WorkflowProgress,
|
|
88
90
|
} from "@alexkroman1/aai-ui";
|
|
89
|
-
import { useState } from "react";
|
|
90
91
|
import type { audit } from "./agent.ts";
|
|
91
92
|
|
|
92
93
|
/**
|
|
@@ -130,33 +131,13 @@ function Stat({ label, value }: { label: string; value: string }) {
|
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
/** A list that renders nothing rather than an empty box — see `risks` in the schema. */
|
|
134
|
-
function Findings({ title, items }: { title: string; items: string[] }) {
|
|
135
|
-
if (items.length === 0) return null;
|
|
136
|
-
return (
|
|
137
|
-
<section className="flex flex-col gap-1">
|
|
138
|
-
<h3 className="text-sm font-medium opacity-70">{title}</h3>
|
|
139
|
-
<ul className="flex list-disc flex-col gap-1 pl-5">
|
|
140
|
-
{items.map((item) => (
|
|
141
|
-
<li key={item}>{item}</li>
|
|
142
|
-
))}
|
|
143
|
-
</ul>
|
|
144
|
-
</section>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
134
|
export function App() {
|
|
149
|
-
// Did THIS load start the run? A reload cannot have, and that is the only way
|
|
150
|
-
// the page can tell "auditing what you just uploaded" from "picking up where
|
|
151
|
-
// you left off" — the hook reports the run, not who asked for it.
|
|
152
|
-
const [startedHere, setStartedHere] = useState(false);
|
|
153
135
|
// The generic is what makes `run.status === "completed"` narrow to a TYPED
|
|
154
136
|
// `run.output` instead of `unknown`. The reload — both halves of it — is the
|
|
155
137
|
// hook's own doing; see the module doc for why the key it mints is the one
|
|
156
138
|
// this desk wants.
|
|
157
|
-
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error } =
|
|
158
|
-
typeof audit
|
|
159
|
-
>(WORKFLOW, { api });
|
|
139
|
+
const { submitForm, run, pending, upload, pauseUpload, resumeUpload, error, startedHere } =
|
|
140
|
+
useWorkflowSubmit<typeof audit>(WORKFLOW, { api });
|
|
160
141
|
const output = run?.status === "completed" ? run.output : undefined;
|
|
161
142
|
// `useDownloadUrl` is the SDK's: the byte route takes the agent's bearer, so the
|
|
162
143
|
// bytes have to be FETCHED and handed to the element as an object URL — and the
|
|
@@ -173,14 +154,7 @@ export function App() {
|
|
|
173
154
|
</p>
|
|
174
155
|
</header>
|
|
175
156
|
|
|
176
|
-
<Form
|
|
177
|
-
onSubmit={(values) => {
|
|
178
|
-
setStartedHere(true);
|
|
179
|
-
return submitForm(values);
|
|
180
|
-
}}
|
|
181
|
-
error={error}
|
|
182
|
-
className="flex flex-col gap-4"
|
|
183
|
-
>
|
|
157
|
+
<Form onSubmit={(values) => submitForm(values)} error={error} className="flex flex-col gap-4">
|
|
184
158
|
{/* Every control, from the workflow's own input schema. See the module doc. */}
|
|
185
159
|
<WorkflowFields workflow={WORKFLOW} />
|
|
186
160
|
<SubmitButton pending={pending} pendingLabel="Auditing…">
|
|
@@ -200,20 +174,26 @@ export function App() {
|
|
|
200
174
|
for `<WorkflowProgress>` to read. */}
|
|
201
175
|
<UploadProgressBar upload={upload} onPause={pauseUpload} onResume={resumeUpload} />
|
|
202
176
|
|
|
203
|
-
{/* What the run itself says, from `
|
|
177
|
+
{/* What the run itself says, from `stepReport()` in the workflow's steps — which
|
|
204
178
|
for this template is the ffmpeg narration: what the file was, what it
|
|
205
179
|
measured, how many pauses were found. */}
|
|
206
180
|
<WorkflowProgress runId={run?.runId} api={api} />
|
|
207
181
|
|
|
208
|
-
{
|
|
182
|
+
{/* `role="alert"`, the same contract `<Form>` gives the submit error: this
|
|
183
|
+
is the outcome the reader waited minutes for. */}
|
|
184
|
+
{run?.status === "failed" && (
|
|
185
|
+
<p role="alert" className="text-red-600">
|
|
186
|
+
That one failed: {run.error}
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
209
189
|
|
|
210
190
|
{output !== undefined && (
|
|
211
191
|
<article className="flex flex-col gap-6">
|
|
212
192
|
<div className="flex flex-col gap-1">
|
|
213
193
|
<h2 className="text-xl">{output.headline}</h2>
|
|
214
|
-
<
|
|
215
|
-
{output.source
|
|
216
|
-
|
|
194
|
+
<Facts
|
|
195
|
+
items={[output.source, formatDuration(output.durationMs), `${output.words} words`]}
|
|
196
|
+
/>
|
|
217
197
|
</div>
|
|
218
198
|
|
|
219
199
|
{/* What the pipeline did, which is this template's subject. Rendered rather
|
|
@@ -231,8 +211,11 @@ export function App() {
|
|
|
231
211
|
<Stat label="Run time" value={formatDuration(output.elapsedMs)} />
|
|
232
212
|
</dl>
|
|
233
213
|
|
|
234
|
-
|
|
235
|
-
|
|
214
|
+
{/* Either list can come back empty — see `risks` in the schema — and
|
|
215
|
+
`<BulletList>` renders nothing at all when it does, heading
|
|
216
|
+
included, rather than a stray heading over an empty box. */}
|
|
217
|
+
<BulletList title="Risks" items={output.risks} />
|
|
218
|
+
<BulletList title="Actions" items={output.actions} />
|
|
236
219
|
|
|
237
220
|
<section className="flex flex-col gap-2">
|
|
238
221
|
<h3 className="text-sm font-medium opacity-70">
|
|
@@ -241,7 +224,9 @@ export function App() {
|
|
|
241
224
|
</h3>
|
|
242
225
|
{audio.pending && <p className="text-sm opacity-70">Fetching the audio…</p>}
|
|
243
226
|
{audio.error !== undefined && (
|
|
244
|
-
<p className="text-red-600">
|
|
227
|
+
<p role="alert" className="text-red-600">
|
|
228
|
+
Could not load the audio: {audio.error}
|
|
229
|
+
</p>
|
|
245
230
|
)}
|
|
246
231
|
{audio.url !== undefined && (
|
|
247
232
|
<>
|
|
@@ -251,7 +236,7 @@ export function App() {
|
|
|
251
236
|
track would carry. `spoken-summary` serves a one-cue WebVTT
|
|
252
237
|
data URL instead — worth reading for how, if a real track is
|
|
253
238
|
what a page needs. */}
|
|
254
|
-
<audio controls src={audio.url} className="w-full" />
|
|
239
|
+
<audio aria-label="Audit read aloud" controls src={audio.url} className="w-full" />
|
|
255
240
|
{/* `download` works on an object URL because the bytes are already in
|
|
256
241
|
the tab; it is the href that could not carry the agent's bearer,
|
|
257
242
|
not the attribute. */}
|
|
@@ -273,4 +258,4 @@ export function App() {
|
|
|
273
258
|
);
|
|
274
259
|
}
|
|
275
260
|
|
|
276
|
-
|
|
261
|
+
mountPage({ name: "Call Audit", component: App });
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
* ingest result, once as the plan) and buy nothing.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
-
import type {
|
|
49
|
-
import { encodeWav, mapConcurrent,
|
|
48
|
+
import type { WorkflowContext } from "@alexkroman1/aai";
|
|
49
|
+
import { encodeWav, mapConcurrent, stepReadUpload, stepReport } from "@alexkroman1/aai/step";
|
|
50
50
|
import { countWords, formatDuration } from "@alexkroman1/aai/utils";
|
|
51
51
|
// ERASED at build time, so the body can name the schema's own output type without
|
|
52
52
|
// a runtime cycle back through `agent.ts` — the same mechanism `client.tsx` uses
|
|
@@ -156,7 +156,7 @@ export type CallAudit = {
|
|
|
156
156
|
*/
|
|
157
157
|
export async function auditFlow(
|
|
158
158
|
input: WorkflowInputOf<typeof audit>,
|
|
159
|
-
ctx:
|
|
159
|
+
ctx: WorkflowContext,
|
|
160
160
|
): Promise<CallAudit> {
|
|
161
161
|
// Both at once: neither needs the other, and issued together they are one round
|
|
162
162
|
// trip instead of two before any audio moves. The ORDER is still a pure function
|
|
@@ -259,12 +259,14 @@ export async function transcribeSegment(audioId: string, segment: Segment): Prom
|
|
|
259
259
|
// ORDER is not guaranteed here and does not need to be — the calls go out
|
|
260
260
|
// together, so their lines interleave by completion, and `segment.index` is what
|
|
261
261
|
// puts the TRANSCRIPT back in order.
|
|
262
|
-
await
|
|
262
|
+
await stepReport(
|
|
263
|
+
`Transcribing ${formatDuration(segment.startMs)}–${formatDuration(segment.endMs)}.`,
|
|
264
|
+
);
|
|
263
265
|
|
|
264
266
|
// `[start, end)`, the same half-open pair `planSegments` produced — the store
|
|
265
267
|
// owns the conversion to HTTP's inclusive range, so there is no `- 1` here to get
|
|
266
268
|
// wrong.
|
|
267
|
-
const audio = await
|
|
269
|
+
const audio = await stepReadUpload(audioId, { start: segment.startByte, end: segment.endByte });
|
|
268
270
|
const text = await transcribeSpan(
|
|
269
271
|
encodeWav(audio.bytes, ANALYSIS_FORMAT),
|
|
270
272
|
`segment-${segment.index}.wav`,
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
import { readFile, stat } from "node:fs/promises";
|
|
64
64
|
import { join } from "node:path";
|
|
65
65
|
import { probeMedia, runFfmpeg } from "@alexkroman1/aai/ffmpeg";
|
|
66
|
-
import { pcmDurationMs,
|
|
66
|
+
import { pcmDurationMs, stepReport, stepRequireCompleteUpload } from "@alexkroman1/aai/step";
|
|
67
67
|
import { throwFatalStepError, throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
68
68
|
import { readUploadToFile, withTempDir, writeUploadFromFile } from "@alexkroman1/aai/step-files";
|
|
69
69
|
import { formatBytes, formatDuration, plural } from "@alexkroman1/aai/utils";
|
|
@@ -132,10 +132,10 @@ export type Ingested = {
|
|
|
132
132
|
// nothing. Removing them is what took the file under the floor: a directive
|
|
133
133
|
// propping a coverage number up is the least useful statement in the tree.
|
|
134
134
|
export async function ingestRecording(uploadId: string): Promise<Ingested> {
|
|
135
|
-
// `
|
|
135
|
+
// `stepRequireCompleteUpload`, not `stepUploadInfo`: `size` is the readable PREFIX, so
|
|
136
136
|
// an upload still arriving would be copied short and levelled as the whole call.
|
|
137
|
-
const stored = await
|
|
138
|
-
await
|
|
137
|
+
const stored = await stepRequireCompleteUpload(uploadId);
|
|
138
|
+
await stepReport(`Reading ${stored.name || uploadId} (${formatBytes(stored.size)}).`);
|
|
139
139
|
|
|
140
140
|
return await withTempDir(
|
|
141
141
|
async (dir) => {
|
|
@@ -148,7 +148,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
|
|
|
148
148
|
// `STEP_FILE_READ_CONCURRENCY` of them. Passing `size` means "I am judging
|
|
149
149
|
// completeness myself", which is what a body polling a still-arriving
|
|
150
150
|
// upload needs and is the opposite of what happened above: this step has
|
|
151
|
-
// already called `
|
|
151
|
+
// already called `stepRequireCompleteUpload`, so the file IS whole and the
|
|
152
152
|
// windows may land in any order. Omitting it lets `readUploadToFile`
|
|
153
153
|
// establish that for itself and fan out. The cost is one metadata round
|
|
154
154
|
// trip, against the dozens of window reads it overlaps.
|
|
@@ -162,7 +162,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
|
|
|
162
162
|
throwFfmpegStepError,
|
|
163
163
|
);
|
|
164
164
|
const codec = probed.audio?.codec ?? "unknown";
|
|
165
|
-
await
|
|
165
|
+
await stepReport(
|
|
166
166
|
`Levelling ${describeSource(codec, probed.durationSec)} to ${ANALYSIS_FORMAT.sampleRate / 1000} kHz mono.`,
|
|
167
167
|
);
|
|
168
168
|
|
|
@@ -206,7 +206,7 @@ export async function ingestRecording(uploadId: string): Promise<Ingested> {
|
|
|
206
206
|
type: "application/octet-stream",
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
await
|
|
209
|
+
await stepReport(
|
|
210
210
|
`Levelled ${formatDuration(durationMs)} from ${loudness.inputLufs} LUFS, ` +
|
|
211
211
|
`${Math.round(speechFraction(silences, durationMs / 1000) * 100)}% speech across ` +
|
|
212
212
|
`${silences.length} ${plural(silences.length, "pause")}.`,
|