@alexkroman1/aai-cli 9.2.0 → 10.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
- package/dist/_bundler.d.ts +3 -41
- package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
- package/dist/_dev-server.d.ts +1 -8
- package/dist/_dev-vite-config.d.ts +21 -0
- package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
- package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
- package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
- package/dist/_templates.d.ts +29 -0
- package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
- package/dist/cli.mjs +46 -22
- package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
- package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
- package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
- package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
- package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
- package/dist/scaffold/CLAUDE.md +162 -54
- package/dist/scaffold/package.json +4 -6
- package/dist/scaffold/server.mjs +12 -23
- package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
- package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
- package/dist/templates/call-audit/agent.eval.test.ts +1 -1
- package/dist/templates/call-audit/agent.test.ts +80 -8
- package/dist/templates/call-audit/client.tsx +87 -3
- package/dist/templates/call-audit/workflows/audit.ts +52 -32
- package/dist/templates/call-audit/workflows/ingest.ts +15 -15
- package/dist/templates/call-audit/workflows/media.ts +1 -1
- package/dist/templates/call-audit/workflows/summarize.ts +0 -4
- package/dist/templates/link-digest/agent.eval.test.ts +4 -2
- package/dist/templates/link-digest/agent.test.ts +25 -5
- package/dist/templates/link-digest/client.tsx +65 -7
- package/dist/templates/link-digest/workflows/digest.ts +39 -29
- package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
- package/dist/templates/podcast-digest/agent.test.ts +108 -2
- package/dist/templates/podcast-digest/client.tsx +89 -8
- package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
- package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
- package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
- package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
- package/dist/templates/recap-workflow/agent.test.ts +323 -83
- package/dist/templates/recap-workflow/shared.ts +4 -3
- package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
- package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
- package/dist/templates/redline/agent.eval.test.ts +1 -1
- package/dist/templates/redline/agent.test.ts +1 -1
- package/dist/templates/redline/client.tsx +83 -3
- package/dist/templates/redline/workflows/redline.ts +26 -23
- package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
- package/dist/templates/research-workflow/agent.test.ts +32 -6
- package/dist/templates/research-workflow/agent.ts +4 -4
- package/dist/templates/research-workflow/shared.ts +4 -3
- package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
- package/dist/templates/research-workflow/workflows/research.ts +35 -38
- package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
- package/dist/templates/spoken-summary/agent.test.ts +10 -8
- package/dist/templates/spoken-summary/client.tsx +72 -3
- package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
- package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
- package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
- package/dist/templates/transcription-workflow/agent.test.ts +626 -14
- package/dist/templates/transcription-workflow/agent.ts +6 -4
- package/dist/templates/transcription-workflow/client.tsx +106 -308
- package/dist/templates/transcription-workflow/recover.test.ts +141 -0
- package/dist/templates/transcription-workflow/recover.ts +130 -0
- package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
- package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
- package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
- package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
- package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
- package/dist/templates/transcription-workflow/workflows/stream.ts +177 -24
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
- package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
- package/dist/worker-bundler.d.ts +0 -14
- package/dist/worker-bundler.mjs +149 -1
- package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
- package/dist/workflow.d.ts +1 -0
- package/package.json +4 -6
- package/dist/_workflow-scan.d.ts +0 -108
- package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
- package/dist/workflow-bundler.d.ts +0 -138
- package/dist/{_studio-CKrsixd0.mjs → _studio-B1waXMuR.mjs} +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -259,7 +259,7 @@ const secret = defineCommand({
|
|
|
259
259
|
},
|
|
260
260
|
cwd: "any",
|
|
261
261
|
async run({ args, mode, cwd }) {
|
|
262
|
-
const { executeSecretPut, NO_INPUT, readStdin } = await import("./secret-
|
|
262
|
+
const { executeSecretPut, NO_INPUT, readStdin } = await import("./secret-B-3T5tvr.mjs");
|
|
263
263
|
const value = mode === "json" ? await readStdin() : void 0;
|
|
264
264
|
if (mode === "json" && !value) throw new CliError(...NO_INPUT);
|
|
265
265
|
return executeSecretPut(cwd, args.name, value, args.server);
|
|
@@ -281,7 +281,7 @@ const secret = defineCommand({
|
|
|
281
281
|
},
|
|
282
282
|
cwd: "any",
|
|
283
283
|
async run({ args, cwd }) {
|
|
284
|
-
const { executeSecretDelete } = await import("./secret-
|
|
284
|
+
const { executeSecretDelete } = await import("./secret-B-3T5tvr.mjs");
|
|
285
285
|
return executeSecretDelete(cwd, args.name, args.server);
|
|
286
286
|
}
|
|
287
287
|
}),
|
|
@@ -296,7 +296,7 @@ const secret = defineCommand({
|
|
|
296
296
|
},
|
|
297
297
|
cwd: "any",
|
|
298
298
|
async run({ args, cwd }) {
|
|
299
|
-
const { executeSecretList } = await import("./secret-
|
|
299
|
+
const { executeSecretList } = await import("./secret-B-3T5tvr.mjs");
|
|
300
300
|
return executeSecretList(cwd, args.server);
|
|
301
301
|
}
|
|
302
302
|
})
|
|
@@ -327,7 +327,7 @@ const logs = defineExec({
|
|
|
327
327
|
},
|
|
328
328
|
cwd: "any",
|
|
329
329
|
async run({ args, cwd }) {
|
|
330
|
-
const { executeLogs } = await import("./logs-
|
|
330
|
+
const { executeLogs } = await import("./logs-hWnJ66Bl.mjs");
|
|
331
331
|
return executeLogs(resolveDirArg(cwd, args.dir), {
|
|
332
332
|
server: args.server,
|
|
333
333
|
follow: args.follow
|
|
@@ -358,7 +358,7 @@ const list = defineExec({
|
|
|
358
358
|
},
|
|
359
359
|
cwd: "any",
|
|
360
360
|
async run({ args, cwd }) {
|
|
361
|
-
const { executeList } = await import("./studio-
|
|
361
|
+
const { executeList } = await import("./studio-wVWNLREn.mjs");
|
|
362
362
|
return executeList({
|
|
363
363
|
cwd,
|
|
364
364
|
server: args.server
|
|
@@ -391,7 +391,7 @@ const pull = defineExec({
|
|
|
391
391
|
},
|
|
392
392
|
cwd: "any",
|
|
393
393
|
async run({ args, cwd }) {
|
|
394
|
-
const { executePull } = await import("./studio-
|
|
394
|
+
const { executePull } = await import("./studio-wVWNLREn.mjs");
|
|
395
395
|
return executePull({
|
|
396
396
|
cwd,
|
|
397
397
|
project: args.project,
|
|
@@ -413,7 +413,7 @@ const push = defineExec({
|
|
|
413
413
|
},
|
|
414
414
|
cwd: "agent",
|
|
415
415
|
async run({ args, cwd }) {
|
|
416
|
-
const { executePush } = await import("./studio-
|
|
416
|
+
const { executePush } = await import("./studio-wVWNLREn.mjs");
|
|
417
417
|
return executePush({
|
|
418
418
|
cwd,
|
|
419
419
|
server: args.server,
|
|
@@ -437,7 +437,7 @@ const publish = defineExec({
|
|
|
437
437
|
},
|
|
438
438
|
cwd: "agent",
|
|
439
439
|
async run({ args, cwd }) {
|
|
440
|
-
const { executePublish } = await import("./studio-
|
|
440
|
+
const { executePublish } = await import("./studio-wVWNLREn.mjs");
|
|
441
441
|
return executePublish({
|
|
442
442
|
cwd,
|
|
443
443
|
server: args.server,
|
|
@@ -468,6 +468,25 @@ const workflowToken = {
|
|
|
468
468
|
type: "string",
|
|
469
469
|
description: "Bearer for an agent that sets AAI_WORKFLOW_API_TOKEN"
|
|
470
470
|
};
|
|
471
|
+
/**
|
|
472
|
+
* `--agent <url>` — target a server the caller is running THEMSELVES.
|
|
473
|
+
*
|
|
474
|
+
* The workflow API is the one agent surface that is fully live under `aai dev`,
|
|
475
|
+
* and until this flag existed there was no way to say so: every verb resolved a
|
|
476
|
+
* platform origin plus a PUBLISHED slug, so an undeployed project was told to
|
|
477
|
+
* `aai publish` while the runs it asked about were answering on localhost. A
|
|
478
|
+
* dev server also has no slug segment at all (`createServer` mounts the API on
|
|
479
|
+
* the origin), so a different `--server` could not express it either.
|
|
480
|
+
*
|
|
481
|
+
* Separate from `--server` rather than a mode of it, because the two carry
|
|
482
|
+
* different credentials: `--server` is paired with the user's platform API key
|
|
483
|
+
* and is trust-checked for that reason, where this reaches a bare agent and
|
|
484
|
+
* sends nothing but `--token`. See `workflow.ts`.
|
|
485
|
+
*/
|
|
486
|
+
const workflowAgent = {
|
|
487
|
+
type: "string",
|
|
488
|
+
description: "Base URL of a server you are running (e.g. `aai dev` on http://localhost:3000)"
|
|
489
|
+
};
|
|
471
490
|
/** The positional every run-scoped verb takes. */
|
|
472
491
|
const runIdArg = {
|
|
473
492
|
type: "positional",
|
|
@@ -482,7 +501,8 @@ const runIdArg = {
|
|
|
482
501
|
const workflowArgs = {
|
|
483
502
|
server: sharedArgs.server,
|
|
484
503
|
json: sharedArgs.json,
|
|
485
|
-
token: workflowToken
|
|
504
|
+
token: workflowToken,
|
|
505
|
+
agent: workflowAgent
|
|
486
506
|
};
|
|
487
507
|
/**
|
|
488
508
|
* Every verb here reads `.aai/project.json` for the origin and the published
|
|
@@ -504,10 +524,11 @@ const workflow = defineCommand({
|
|
|
504
524
|
args: workflowArgs,
|
|
505
525
|
cwd: WORKFLOW_CWD,
|
|
506
526
|
async run({ args, cwd }) {
|
|
507
|
-
const { executeWorkflowList } = await import("./workflow-
|
|
527
|
+
const { executeWorkflowList } = await import("./workflow-BBu5oEDw.mjs");
|
|
508
528
|
return executeWorkflowList(cwd, {
|
|
509
529
|
server: args.server,
|
|
510
|
-
token: args.token
|
|
530
|
+
token: args.token,
|
|
531
|
+
agent: args.agent
|
|
511
532
|
});
|
|
512
533
|
}
|
|
513
534
|
}),
|
|
@@ -530,12 +551,13 @@ const workflow = defineCommand({
|
|
|
530
551
|
},
|
|
531
552
|
cwd: WORKFLOW_CWD,
|
|
532
553
|
async run({ args, cwd }) {
|
|
533
|
-
const { executeWorkflowRuns } = await import("./workflow-
|
|
554
|
+
const { executeWorkflowRuns } = await import("./workflow-BBu5oEDw.mjs");
|
|
534
555
|
const limit = args.limit === void 0 ? void 0 : Number(args.limit);
|
|
535
556
|
if (limit !== void 0 && !Number.isFinite(limit)) throw new CliError("bad_limit", "--limit must be a number");
|
|
536
557
|
return executeWorkflowRuns(cwd, args.workflow, {
|
|
537
558
|
server: args.server,
|
|
538
559
|
token: args.token,
|
|
560
|
+
agent: args.agent,
|
|
539
561
|
limit
|
|
540
562
|
});
|
|
541
563
|
}
|
|
@@ -551,10 +573,11 @@ const workflow = defineCommand({
|
|
|
551
573
|
},
|
|
552
574
|
cwd: WORKFLOW_CWD,
|
|
553
575
|
async run({ args, cwd }) {
|
|
554
|
-
const { executeWorkflowShow } = await import("./workflow-
|
|
576
|
+
const { executeWorkflowShow } = await import("./workflow-BBu5oEDw.mjs");
|
|
555
577
|
return executeWorkflowShow(cwd, args.runId, {
|
|
556
578
|
server: args.server,
|
|
557
|
-
token: args.token
|
|
579
|
+
token: args.token,
|
|
580
|
+
agent: args.agent
|
|
558
581
|
});
|
|
559
582
|
}
|
|
560
583
|
}),
|
|
@@ -569,10 +592,11 @@ const workflow = defineCommand({
|
|
|
569
592
|
},
|
|
570
593
|
cwd: WORKFLOW_CWD,
|
|
571
594
|
async run({ args, cwd }) {
|
|
572
|
-
const { executeWorkflowCancel } = await import("./workflow-
|
|
595
|
+
const { executeWorkflowCancel } = await import("./workflow-BBu5oEDw.mjs");
|
|
573
596
|
return executeWorkflowCancel(cwd, args.runId, {
|
|
574
597
|
server: args.server,
|
|
575
|
-
token: args.token
|
|
598
|
+
token: args.token,
|
|
599
|
+
agent: args.agent
|
|
576
600
|
});
|
|
577
601
|
}
|
|
578
602
|
})
|
|
@@ -647,7 +671,7 @@ const mainCommand = defineCommand({
|
|
|
647
671
|
},
|
|
648
672
|
cwd: "none",
|
|
649
673
|
async run({ args, mode }) {
|
|
650
|
-
const { executeInit } = await import("./init-
|
|
674
|
+
const { executeInit } = await import("./init-BRfoc2EK.mjs");
|
|
651
675
|
return executeInit({
|
|
652
676
|
dir: args.dir,
|
|
653
677
|
force: args.force,
|
|
@@ -678,7 +702,7 @@ const mainCommand = defineCommand({
|
|
|
678
702
|
},
|
|
679
703
|
cwd: "agent",
|
|
680
704
|
async run({ args, cwd }) {
|
|
681
|
-
const { executeDev } = await import("./dev-
|
|
705
|
+
const { executeDev } = await import("./dev-BfVl1uBA.mjs");
|
|
682
706
|
return executeDev({
|
|
683
707
|
cwd,
|
|
684
708
|
port: args.port,
|
|
@@ -728,7 +752,7 @@ const mainCommand = defineCommand({
|
|
|
728
752
|
},
|
|
729
753
|
cwd: "agent",
|
|
730
754
|
async run({ args, cwd }) {
|
|
731
|
-
const { executeBuild } = await import("./build-
|
|
755
|
+
const { executeBuild } = await import("./build-DKC2e9o_.mjs");
|
|
732
756
|
return executeBuild({
|
|
733
757
|
cwd,
|
|
734
758
|
skipTests: args.skipTests,
|
|
@@ -760,7 +784,7 @@ const mainCommand = defineCommand({
|
|
|
760
784
|
},
|
|
761
785
|
cwd: "agent",
|
|
762
786
|
async run({ args, cwd }) {
|
|
763
|
-
const { executeDeploy } = await import("./deploy-
|
|
787
|
+
const { executeDeploy } = await import("./deploy-CchW5pmw.mjs");
|
|
764
788
|
return executeDeploy({
|
|
765
789
|
cwd,
|
|
766
790
|
server: args.server,
|
|
@@ -780,7 +804,7 @@ const mainCommand = defineCommand({
|
|
|
780
804
|
},
|
|
781
805
|
cwd: "any",
|
|
782
806
|
async run({ args, cwd }) {
|
|
783
|
-
const { executeDelete } = await import("./delete-
|
|
807
|
+
const { executeDelete } = await import("./delete-X_CRgCf6.mjs");
|
|
784
808
|
return executeDelete({
|
|
785
809
|
cwd,
|
|
786
810
|
server: args.server
|
|
@@ -813,7 +837,7 @@ const mainCommand = defineCommand({
|
|
|
813
837
|
args: { json: sharedArgs.json },
|
|
814
838
|
cwd: "none",
|
|
815
839
|
async run({ mode }) {
|
|
816
|
-
const { listTemplates } = await import("./_templates-
|
|
840
|
+
const { listTemplates } = await import("./_templates-0Zr2z6yA.mjs");
|
|
817
841
|
const names = await listTemplates();
|
|
818
842
|
if (mode === "human") {
|
|
819
843
|
for (const name of names) log.message(name);
|
|
@@ -3,7 +3,7 @@ import { a as ok } from "./_output-CKkmWs7i.mjs";
|
|
|
3
3
|
import { n as log } from "./_ui-u7T4YooX.mjs";
|
|
4
4
|
import { l as writeProjectConfig } from "./_config-CmJOFsAP.mjs";
|
|
5
5
|
import { a as resolveDeployTarget, i as requireDeployedSlug } from "./_agent-BTwZJu4P.mjs";
|
|
6
|
-
import { d as apiTestSeam, l as HINT_NOT_DEPLOYED, s as studioProjectApiUrl, u as apiRequest } from "./_studio-
|
|
6
|
+
import { d as apiTestSeam, l as HINT_NOT_DEPLOYED, s as studioProjectApiUrl, u as apiRequest } from "./_studio-B1waXMuR.mjs";
|
|
7
7
|
import { omitUndefined } from "@alexkroman1/aai/utils";
|
|
8
8
|
//#region delete.ts
|
|
9
9
|
async function runDelete(opts) {
|
|
@@ -3,11 +3,11 @@ import { a as ok } from "./_output-CKkmWs7i.mjs";
|
|
|
3
3
|
import { n as log, r as notify, t as fmtUrl } from "./_ui-u7T4YooX.mjs";
|
|
4
4
|
import { o as errorMessage$1 } from "./_utils-B8QmtFhK.mjs";
|
|
5
5
|
import { s as updateProjectConfig } from "./_config-CmJOFsAP.mjs";
|
|
6
|
-
import { i as evalWorkerConfig, t as buildAgentBundle } from "./_bundler-
|
|
6
|
+
import { i as evalWorkerConfig, t as buildAgentBundle } from "./_bundler-B31MqYaD.mjs";
|
|
7
7
|
import { t as resolveServerEnv } from "./_server-common-6e0QI4mq.mjs";
|
|
8
8
|
import { a as resolveDeployTarget } from "./_agent-BTwZJu4P.mjs";
|
|
9
9
|
import { assertTypechecks } from "./_typecheck-gate-BXvUNnfs.mjs";
|
|
10
|
-
import { d as apiTestSeam, f as checkedResponse, i as projectNameFromDir, u as apiRequest } from "./_studio-
|
|
10
|
+
import { d as apiTestSeam, f as checkedResponse, i as projectNameFromDir, u as apiRequest } from "./_studio-B1waXMuR.mjs";
|
|
11
11
|
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
12
12
|
import { gzipSync } from "node:zlib";
|
|
13
13
|
//#region _deploy.ts
|
|
@@ -12,7 +12,7 @@ import { styleText } from "node:util";
|
|
|
12
12
|
async function executeDev(opts) {
|
|
13
13
|
const port = parsePort(opts.port);
|
|
14
14
|
const agentName = path.basename(path.resolve(opts.cwd));
|
|
15
|
-
const { startDevServer } = await import("./_dev-server-
|
|
15
|
+
const { startDevServer } = await import("./_dev-server-Bo5dSBzQ.mjs");
|
|
16
16
|
let cleanup;
|
|
17
17
|
let shuttingDown = false;
|
|
18
18
|
const onSignal = () => {
|
|
@@ -106,7 +106,7 @@ function collectWarnings() {
|
|
|
106
106
|
}
|
|
107
107
|
/** Publish after init and return deploy metadata if successful. */
|
|
108
108
|
async function tryPublish(cwd, server, warn) {
|
|
109
|
-
const { executePublish } = await import("./studio-
|
|
109
|
+
const { executePublish } = await import("./studio-wVWNLREn.mjs");
|
|
110
110
|
try {
|
|
111
111
|
const result = await executePublish({
|
|
112
112
|
cwd,
|
|
@@ -127,7 +127,7 @@ async function tryPublish(cwd, server, warn) {
|
|
|
127
127
|
}
|
|
128
128
|
/** Scaffold the project, optionally showing a spinner. */
|
|
129
129
|
async function scaffoldProject(dir, cwd, template, silent) {
|
|
130
|
-
const { runInit } = await import("./_init-
|
|
130
|
+
const { runInit } = await import("./_init-BURbdHNM.mjs");
|
|
131
131
|
await withSpinner(silent, {
|
|
132
132
|
start: `Creating ${dir}`,
|
|
133
133
|
done: "Project created",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok } from "./_output-CKkmWs7i.mjs";
|
|
3
3
|
import { n as log } from "./_ui-u7T4YooX.mjs";
|
|
4
|
-
import { f as checkedResponse } from "./_studio-
|
|
5
|
-
import { n as slugRequest } from "./_slug-api
|
|
4
|
+
import { f as checkedResponse } from "./_studio-B1waXMuR.mjs";
|
|
5
|
+
import { n as slugRequest } from "./_slug-api-CfGQq1Tt.mjs";
|
|
6
6
|
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
7
7
|
import { sleep } from "@alexkroman1/aai/internal";
|
|
8
8
|
function isLogsPage(value) {
|
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ The fast loop: edit → `pnpm dev` (browser, talk to it) →
|
|
|
25
25
|
|
|
26
26
|
**A spec that needs the agent as DEPLOYED imports one module:**
|
|
27
27
|
|
|
28
|
-
```ts
|
|
28
|
+
```ts
|
|
29
29
|
import agentDef from "virtual:aai/agent";
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -405,58 +405,174 @@ everything in flight is lost when that process goes away. That is the honest
|
|
|
405
405
|
tradeoff, and it is what
|
|
406
406
|
lets you build a workflow app before provisioning anything.
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
upload's record is a row
|
|
410
|
-
|
|
411
|
-
—
|
|
408
|
+
**A workflow UPLOAD is durable with no setup either**, and this paragraph used
|
|
409
|
+
to say the opposite. An upload's record is a platform row and its bytes are
|
|
410
|
+
platform storage, so `api.upload`, `<FileField>` and the file-taking form
|
|
411
|
+
hooks outlive the sandbox exactly as the runs reading them do — a deployed app
|
|
412
|
+
needs no database of its own for either half. Under `aai dev` they are as
|
|
413
|
+
temporary as the runs above: the bytes go to a per-process temporary directory
|
|
414
|
+
that a restart abandons. There is no `ctx.db` at all — see "Persisting data"
|
|
415
|
+
below.
|
|
412
416
|
|
|
413
417
|
### Workflow bodies live in `workflows/`
|
|
414
418
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
419
|
+
A body is an ordinary exported async function of its input and a `WorkflowCtx`.
|
|
420
|
+
There is no directive and no compile step of its own — the agent bundle compiles
|
|
421
|
+
`workflows/` like any other source file — and durability is a method call:
|
|
418
422
|
|
|
419
423
|
```ts
|
|
420
|
-
import {
|
|
424
|
+
import type { WorkflowCtx } from "@alexkroman1/aai";
|
|
421
425
|
|
|
422
|
-
export async function digestFlow(input: { url: string }) {
|
|
423
|
-
"
|
|
426
|
+
export async function digestFlow(input: { url: string }, ctx: WorkflowCtx) {
|
|
427
|
+
const digest = await ctx.step("summarize", () => summarize(input.url));
|
|
424
428
|
|
|
425
|
-
const digest = await summarize(input.url);
|
|
426
429
|
// Suspended, not blocked: the container is free to exit here and the run
|
|
427
|
-
// resumes when it comes due.
|
|
428
|
-
await sleep(
|
|
429
|
-
|
|
430
|
+
// resumes when it comes due. Six hours works the same as ten seconds.
|
|
431
|
+
await ctx.sleep(10_000);
|
|
432
|
+
|
|
433
|
+
const filedAt = await ctx.step("file", () => file(digest));
|
|
434
|
+
return { ...digest, filedAt };
|
|
430
435
|
}
|
|
431
436
|
|
|
432
437
|
async function summarize(url: string) {
|
|
433
|
-
"use step";
|
|
434
438
|
// The whole Node runtime is available in a step: fetch, a model call, a
|
|
435
439
|
// database. Not in the body.
|
|
436
440
|
return { url, headline: `What ${new URL(url).hostname} says`, points: [] };
|
|
437
441
|
}
|
|
438
442
|
|
|
439
|
-
async function file(
|
|
440
|
-
"use step";
|
|
443
|
+
async function file(_digest: { url: string }) {
|
|
441
444
|
return new Date().toISOString();
|
|
442
445
|
}
|
|
443
446
|
```
|
|
444
447
|
|
|
445
|
-
|
|
446
|
-
|
|
448
|
+
`ctx.step(name, fn)` runs `fn` once, journals what it returned, and on every
|
|
449
|
+
later replay returns the journaled value without running it again. The step
|
|
450
|
+
functions themselves are ordinary functions — which is also what lets a spec call
|
|
451
|
+
one directly, with no engine in the path.
|
|
452
|
+
|
|
453
|
+
Three rules, and all three fail silently if broken — nothing scans a body for
|
|
454
|
+
them:
|
|
447
455
|
|
|
448
456
|
- **The body replays from the top on every resume**, so it holds no live handle
|
|
449
457
|
and makes no undurable decision — no `Date.now()`, no `Math.random()`, no
|
|
450
|
-
`crypto.randomUUID()`, no `fetch`. Those
|
|
451
|
-
journaled and returned unchanged on replay.
|
|
452
|
-
workflow bundle, where step bodies have already been removed, so a step doing
|
|
453
|
-
any of this is not flagged — that is what a step is FOR.)
|
|
458
|
+
`crypto.randomUUID()`, no `fetch`. Those go inside a `ctx.step`, whose result
|
|
459
|
+
is journaled and returned unchanged on replay.
|
|
454
460
|
- **A step's arguments and return value cross a queue**, so they must be
|
|
455
461
|
JSON-shaped and small. Put bytes in storage and pass the key.
|
|
456
|
-
- **A step gets no tool context.**
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
462
|
+
- **A step gets no tool context.** There is no `ctx.db` and no `ctx.generate`
|
|
463
|
+
inside one — see below for how it reaches the agent's env and a model anyway.
|
|
464
|
+
A step reaches a database the way a tool does: its own client, its own
|
|
465
|
+
credential from `requireStepEnv`.
|
|
466
|
+
|
|
467
|
+
**A step's NAME is its identity in the journal**, so write a string literal and
|
|
468
|
+
keep it stable: renaming one makes an in-flight run re-run that step. A single
|
|
469
|
+
call site inside a loop or a `mapConcurrent` fan-out is exactly what the scheme
|
|
470
|
+
is for — each reach gets its own entry — but two DIFFERENT call sites should not
|
|
471
|
+
share a name: the journal keys an entry by `(name, occurrence)`, so two sites
|
|
472
|
+
alias onto one counter and read each other's journaled results. Nothing detects
|
|
473
|
+
it.
|
|
474
|
+
|
|
475
|
+
**Per-step retries are an argument, not a property.** Pass
|
|
476
|
+
`{ maxAttempts }` where a step deserves more patience than the default three:
|
|
477
|
+
|
|
478
|
+
```ts no-check
|
|
479
|
+
const digest = await ctx.step("summarize", () => summarize(input.url), {
|
|
480
|
+
maxAttempts: 6,
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Waiting: `ctx.sleep` and `ctx.waitFor`
|
|
485
|
+
|
|
486
|
+
Both SUSPEND the run — the body stops, the container is free, and the engine
|
|
487
|
+
brings the run back — so a long wait costs nothing while it runs.
|
|
488
|
+
|
|
489
|
+
**How long a wait really survives is a property of the run STORE.** On the
|
|
490
|
+
platform it is durable with no setup, and a self-hosted deployment with a
|
|
491
|
+
`DATABASE_URL` is durable too — the wait outlives the body, the worker and the
|
|
492
|
+
process. Under `aai dev` without a `DATABASE_URL` the store is memory, so a wait
|
|
493
|
+
lives only as long as the dev server. The boot line reports which one is in play.
|
|
494
|
+
|
|
495
|
+
```ts no-check
|
|
496
|
+
// A duration in milliseconds, or an absolute Date.
|
|
497
|
+
await ctx.sleep(6 * 60 * 60 * 1000, { correlationId: "review-window" });
|
|
498
|
+
|
|
499
|
+
// Until somebody outside the run answers, via `ctx.workflows.signal(token, …)`
|
|
500
|
+
// from a tool, or by a delivery to `publicWebhookUrl` — both hops reach the
|
|
501
|
+
// same waitpoint.
|
|
502
|
+
const approval = await ctx.waitFor<{ approved: boolean }>(approvalToken(input.id), {
|
|
503
|
+
timeoutMs: 120_000,
|
|
504
|
+
});
|
|
505
|
+
if (approval === undefined) return { published: false, reason: "nobody approved" };
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Four things worth knowing:
|
|
509
|
+
|
|
510
|
+
- **A hook's token must be DERIVED, not random.** Whoever signals is usually a
|
|
511
|
+
tool, and a tool cannot see the body's local variables — so export one function
|
|
512
|
+
that computes the token from the run's own input and import it in both places.
|
|
513
|
+
Derive it from something that identifies the RUN rather than the caller: a
|
|
514
|
+
token is held for the life of its run, so two runs deriving the same one is the
|
|
515
|
+
second one failing.
|
|
516
|
+
- **`timeoutMs` resolves `undefined` when the window closes unanswered.** A
|
|
517
|
+
closing window is an outcome to branch on, not a failure, and the engine closes
|
|
518
|
+
the hook as it shuts so a late answer cannot change what already happened.
|
|
519
|
+
- **Do NOT race the two.** `Promise.race([ctx.waitFor(t), ctx.sleep(ms)])` does
|
|
520
|
+
not work: both suspend, and a suspend unwinds the stack, so the race stops the
|
|
521
|
+
body before the other side has been reached. That is why the deadline is a
|
|
522
|
+
parameter.
|
|
523
|
+
- **`ctx.workflows.wakeUp(runId, { correlationIds: [id] })`** ends a sleep early,
|
|
524
|
+
which is how a "send it now" tool cuts a scheduled wait short. Naming no ids
|
|
525
|
+
wakes every outstanding SLEEP and deliberately not a `waitFor` deadline, so
|
|
526
|
+
cutting a schedule short cannot also close an approval window.
|
|
527
|
+
|
|
528
|
+
#### A third-party callback is an OPTIMIZATION over a reconciling read
|
|
529
|
+
|
|
530
|
+
The webhook route is how a payment provider, a transcription service or an
|
|
531
|
+
approval mailer resumes a run, and `recap-workflow` is the worked example — it
|
|
532
|
+
hands AssemblyAI a `webhook_url` and parks on the delivery instead of polling
|
|
533
|
+
for twenty minutes. Five things about that shape, and every one of them is a
|
|
534
|
+
trap somebody has already paid for:
|
|
535
|
+
|
|
536
|
+
- **Mint it with `stepWebhookUrl(token)`, from inside the step that hands it
|
|
537
|
+
over.** That is the step-side half of `ctx.workflows.publicWebhookUrl` — the
|
|
538
|
+
tool-side one needs a `ToolContext`, and a workflow body and its steps are
|
|
539
|
+
handed none, so a workflow app with no tools has only this one. It THROWS when
|
|
540
|
+
the deployment cannot mint one, which a step should catch and treat as "no
|
|
541
|
+
callback": a run must not fail over a missing optimization. And note
|
|
542
|
+
`requireStepEnv("AAI_PUBLIC_BASE_URL")` is NOT a substitute — the public base
|
|
543
|
+
URL is a boot parameter of the deployment, not one of the agent's own secrets,
|
|
544
|
+
so that read is `undefined` in production precisely where the value exists.
|
|
545
|
+
- **Return the callback FACT from the step, and branch on that.** Whether a
|
|
546
|
+
callback was registered decides whether the body parks, and a body may only
|
|
547
|
+
branch on values that came out of the journal. Mint inside the step's function
|
|
548
|
+
— it runs once, on first execution, never on a replay — and answer
|
|
549
|
+
`{ id, callback }`. A body that re-minted on every walk could flip the branch
|
|
550
|
+
under a redeploy and then look for a `waitFor` the journal never recorded.
|
|
551
|
+
- **Keep the poll as the TIMEOUT arm.** A webhook is one HTTP POST from a third
|
|
552
|
+
party with no delivery guarantee you control: the sender gives up after its own
|
|
553
|
+
retry budget, a deployment may not know its public URL, and a delivery that
|
|
554
|
+
lands before your body reaches its wait is answered `404` and dropped. So read
|
|
555
|
+
the state before you park and again after, give the wait a `timeoutMs`, and let
|
|
556
|
+
an unanswered window fall through to the read. A run that hangs forever on a
|
|
557
|
+
dropped delivery is strictly worse than one that polls.
|
|
558
|
+
- **Wait for the EDGE, not the answer.** Treat the payload as "something
|
|
559
|
+
happened, go look" and get the fact from the far side's own API under your own
|
|
560
|
+
credential. That is what makes an unauthenticated callback route safe: a forged
|
|
561
|
+
delivery on a guessed token costs one extra read and changes no outcome. It
|
|
562
|
+
holds by construction, where a shared secret holds only until somebody has to
|
|
563
|
+
rotate it — and the route authorizes on the TOKEN and reads no other header, so
|
|
564
|
+
a sender's own auth-header option would be sent and ignored.
|
|
565
|
+
- **One token, ONE `waitFor` per run.** A token is claimed for the life of its
|
|
566
|
+
run and given back when the run goes terminal, so a second `ctx.waitFor` on the
|
|
567
|
+
same token — a wait written inside a loop — THROWS. A throw is not a suspend,
|
|
568
|
+
so a body with a `catch` will treat it as a failed run and start compensating.
|
|
569
|
+
Park once, outside the loop.
|
|
570
|
+
- **You cannot test it under `aai dev` without a tunnel.** `publicUrl` there is
|
|
571
|
+
`http://localhost:<backend port>`, which no third party can reach — so the
|
|
572
|
+
delivery never arrives, the run silently takes the fallback, and the webhook
|
|
573
|
+
half of your code is exercised by nothing. `PUBLIC_URL=https://<your tunnel>
|
|
574
|
+
pnpm dev` is what makes it reachable. Until you set it, treat local runs as
|
|
575
|
+
coverage of the backstop only.
|
|
460
576
|
|
|
461
577
|
### A step's env, and calling a model from one
|
|
462
578
|
|
|
@@ -465,13 +581,11 @@ A step has no `ctx`, so the two things tool code takes for granted come from
|
|
|
465
581
|
`@alexkroman1/aai` — a `workflows/*.ts` module is bundled separately, and the
|
|
466
582
|
root barrel would drag the whole SDK into that bundle.
|
|
467
583
|
|
|
468
|
-
```ts
|
|
584
|
+
```ts
|
|
469
585
|
import { stepEnv } from "@alexkroman1/aai/step";
|
|
470
586
|
import { stepGenerateClassified } from "@alexkroman1/aai/step-errors";
|
|
471
587
|
|
|
472
588
|
async function summarize(text: string) {
|
|
473
|
-
"use step";
|
|
474
|
-
|
|
475
589
|
// The agent's env by name — the same values a tool reads from `ctx.env`.
|
|
476
590
|
// `requireStepEnv` fails naming the key; `stepEnv` returns undefined.
|
|
477
591
|
const style = stepEnv("DIGEST_STYLE") ?? "plain";
|
|
@@ -494,8 +608,8 @@ deploy. Use `stepGenerateJsonClassified` with a Zod `schema` if you need a shape
|
|
|
494
608
|
### From a step, reach for the `Classified` call
|
|
495
609
|
|
|
496
610
|
`@alexkroman1/aai/step-errors` publishes a wrapper for every `/step` call that
|
|
497
|
-
can fail against a remote service, and **inside a
|
|
498
|
-
|
|
611
|
+
can fail against a remote service, and **inside a step the wrapper is the one to
|
|
612
|
+
use**:
|
|
499
613
|
|
|
500
614
|
| Raw, on `@alexkroman1/aai/step` | Use this instead, on `@alexkroman1/aai/step-errors` |
|
|
501
615
|
| --- | --- |
|
|
@@ -512,9 +626,9 @@ while this also turns a NON-2XX RESPONSE into a throw — `stepFetch` resolves
|
|
|
512
626
|
with a `404` rather than raising it. Two changes, so two names.
|
|
513
627
|
|
|
514
628
|
The whole of what a wrapper adds is `throwStepError`, and that is worth having
|
|
515
|
-
because the
|
|
629
|
+
because the engine's retry policy is decided by WHICH error a step throws. Raw,
|
|
516
630
|
every failure looks the same to it: a bad API key is retried until the attempts
|
|
517
|
-
run out, and a rate limit backs off for the
|
|
631
|
+
run out, and a rate limit backs off for the engine's default one second while
|
|
518
632
|
the delay the gateway itself named sits unread on the error. Classified, a
|
|
519
633
|
terminal failure raises `FatalError` and stops, and a transient one raises
|
|
520
634
|
`RetryableError` carrying the far side's own `Retry-After`. That matters most
|
|
@@ -529,11 +643,11 @@ or `throwFfmpegStepError(err)` for a media failure, whose default runs the other
|
|
|
529
643
|
way (only a `timeout` or an `aborted` is worth another attempt).
|
|
530
644
|
|
|
531
645
|
**Why the split exists, since the wrapper is what you usually want:** this is
|
|
532
|
-
the
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
646
|
+
importing from here is the OPT-IN, and `/step` is not written only for a step —
|
|
647
|
+
`mapConcurrent` bounds a rate-limited call anywhere, `stepFetch` is an ordinary
|
|
648
|
+
HTTP client, and your specs drive exported steps directly. None of those callers
|
|
649
|
+
has a retry budget to burn, so none should meet a vocabulary whose whole subject
|
|
650
|
+
is one. A step pays nothing for the extra import line.
|
|
537
651
|
|
|
538
652
|
### Media, big files, and transcription from a step
|
|
539
653
|
|
|
@@ -564,8 +678,6 @@ import { throwFfmpegStepError } from "@alexkroman1/aai/step-errors";
|
|
|
564
678
|
import { readUploadToFile, withTempDir } from "@alexkroman1/aai/step-files";
|
|
565
679
|
|
|
566
680
|
export async function measure(uploadId: string) {
|
|
567
|
-
"use step";
|
|
568
|
-
|
|
569
681
|
return await withTempDir(async (dir) => {
|
|
570
682
|
const path = `${dir}/input`;
|
|
571
683
|
// Read the upload ONCE. A five-step version reads it five times, and on a
|
|
@@ -591,8 +703,6 @@ import { requireStepEnv } from "@alexkroman1/aai/step";
|
|
|
591
703
|
import { sendToChannelClassified } from "@alexkroman1/aai/step-errors";
|
|
592
704
|
|
|
593
705
|
export async function announce(headline: string, points: string[]) {
|
|
594
|
-
"use step";
|
|
595
|
-
|
|
596
706
|
const message: ChannelMessage = {
|
|
597
707
|
text: headline,
|
|
598
708
|
sections: points.map((point) => ({ text: point })),
|
|
@@ -619,8 +729,6 @@ call to make from a step, for a reason nothing at the call site shows:
|
|
|
619
729
|
import { multipartBody, stepFetch, StepTransportError } from "@alexkroman1/aai/step";
|
|
620
730
|
|
|
621
731
|
async function transcribeChunk(key: string, bytes: Uint8Array, index: number) {
|
|
622
|
-
"use step";
|
|
623
|
-
|
|
624
732
|
// Multipart as BYTES. Never a `FormData` — see below.
|
|
625
733
|
const part = multipartBody({
|
|
626
734
|
name: "audio",
|
|
@@ -654,7 +762,7 @@ and pathological for `mapConcurrent` over large bodies. Measured on 8 concurrent
|
|
|
654
762
|
HTTP/2 a capacity limit arrives as a *stream reset* — `NGHTTP2_ENHANCE_YOUR_CALM`
|
|
655
763
|
— and a stream error carries no HTTP status, so `isTransientStatus` and
|
|
656
764
|
`retryAfter` cannot see it. Every sibling in the batch then retries in lockstep
|
|
657
|
-
into the same reset, exhausts
|
|
765
|
+
into the same reset, exhausts the step's attempts, and fails the run with
|
|
658
766
|
`TypeError: fetch failed`, whose real cause is two `cause` hops down where
|
|
659
767
|
nothing prints it. Over HTTP/1.1 the identical limit arrives as `503` with
|
|
660
768
|
`retry-after`, which your retry policy already reads.
|
|
@@ -686,12 +794,10 @@ generated PDF — needs two things a first draft reaches for and does not find.
|
|
|
686
794
|
Both are on `@alexkroman1/aai/step`, and `spoken-summary` is the template that
|
|
687
795
|
shows the whole round trip.
|
|
688
796
|
|
|
689
|
-
```ts
|
|
797
|
+
```ts
|
|
690
798
|
import { stepSpeak, writeUpload } from "@alexkroman1/aai/step";
|
|
691
799
|
|
|
692
800
|
export async function narrate(script: string) {
|
|
693
|
-
"use step";
|
|
694
|
-
|
|
695
801
|
const spoken = await stepSpeak(script, { voice: "jane" });
|
|
696
802
|
const stored = await writeUpload(spoken.audio, { name: "summary.wav", type: "audio/wav" });
|
|
697
803
|
return { audio: stored.id, durationMs: spoken.durationMs };
|
|
@@ -1205,7 +1311,7 @@ fixing one call site at a time.
|
|
|
1205
1311
|
**A `sessionSlot` is the only way to keep state across a session's tool calls**,
|
|
1206
1312
|
and it is one declaration in a shared module:
|
|
1207
1313
|
|
|
1208
|
-
```ts
|
|
1314
|
+
```ts
|
|
1209
1315
|
// shared.ts — the one place the shape is written down.
|
|
1210
1316
|
import { sessionSlot } from "@alexkroman1/aai";
|
|
1211
1317
|
|
|
@@ -1300,7 +1406,7 @@ collect an address before taking payment is a suggestion; a dialog is a rule.
|
|
|
1300
1406
|
that leave it. It is a slot underneath, so the position is persisted with the
|
|
1301
1407
|
rest of the session and survives a reconnect.
|
|
1302
1408
|
|
|
1303
|
-
```ts
|
|
1409
|
+
```ts
|
|
1304
1410
|
import { dialog } from "@alexkroman1/aai";
|
|
1305
1411
|
|
|
1306
1412
|
export const checkout = dialog("checkout", {
|
|
@@ -1507,7 +1613,7 @@ export default tool({
|
|
|
1507
1613
|
});
|
|
1508
1614
|
```
|
|
1509
1615
|
|
|
1510
|
-
```ts
|
|
1616
|
+
```ts
|
|
1511
1617
|
// agent.ts — nothing about tools appears here
|
|
1512
1618
|
import { agent } from "@alexkroman1/aai";
|
|
1513
1619
|
|
|
@@ -1655,8 +1761,10 @@ HTTP API, a provider SDK or a hosted KV works the same way.
|
|
|
1655
1761
|
nothing else.
|
|
1656
1762
|
- **Durable workflow runs** — a run survives the sandbox recycling, every
|
|
1657
1763
|
redeploy, and a multi-day `sleep()`.
|
|
1764
|
+
- **Workflow uploads** — a file a form submitted, record and bytes both, so a
|
|
1765
|
+
resumed run reads the same recording the browser sent.
|
|
1658
1766
|
|
|
1659
|
-
Those
|
|
1767
|
+
Those three cover almost everything an agent wants. A database is for data that
|
|
1660
1768
|
must outlive a session AND be queryable: a ledger, filed records, cross-session
|
|
1661
1769
|
saves.
|
|
1662
1770
|
|
|
@@ -14,19 +14,17 @@
|
|
|
14
14
|
"publish:agent": "aai publish"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@alexkroman1/aai": "^
|
|
18
|
-
"@alexkroman1/aai-runtime": "^
|
|
19
|
-
"@alexkroman1/aai-ui": "^
|
|
20
|
-
"@workflow/world-postgres": "4.3.3",
|
|
17
|
+
"@alexkroman1/aai": "^10.0.1",
|
|
18
|
+
"@alexkroman1/aai-runtime": "^10.0.1",
|
|
19
|
+
"@alexkroman1/aai-ui": "^10.0.1",
|
|
21
20
|
"react": "^19.2.8",
|
|
22
21
|
"react-dom": "^19.2.8",
|
|
23
22
|
"tailwindcss": "^4.0.0",
|
|
24
|
-
"workflow": "4.8.2",
|
|
25
23
|
"xstate": "^5.32.5",
|
|
26
24
|
"zod": "^4.4.3"
|
|
27
25
|
},
|
|
28
26
|
"devDependencies": {
|
|
29
|
-
"@alexkroman1/aai-cli": "^
|
|
27
|
+
"@alexkroman1/aai-cli": "^10.0.1",
|
|
30
28
|
"@tailwindcss/vite": "^4.3.3",
|
|
31
29
|
"@types/node": "^26.2.0",
|
|
32
30
|
"@types/react": "^19.2.18",
|