@alexkroman1/aai-cli 9.2.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/{_bundler-CDuYl5Gb.mjs → _bundler-B31MqYaD.mjs} +7 -33
  2. package/dist/_bundler.d.ts +3 -41
  3. package/dist/{_dev-server-BAR8Ya43.mjs → _dev-server-Bo5dSBzQ.mjs} +140 -44
  4. package/dist/_dev-server.d.ts +1 -8
  5. package/dist/_dev-vite-config.d.ts +21 -0
  6. package/dist/{_init-BNotRgV1.mjs → _init-BURbdHNM.mjs} +1 -1
  7. package/dist/{_slug-api--ZBmIGH-.mjs → _slug-api-CfGQq1Tt.mjs} +1 -1
  8. package/dist/{_templates-4WcKOjS5.mjs → _templates-0Zr2z6yA.mjs} +37 -2
  9. package/dist/_templates.d.ts +29 -0
  10. package/dist/{build-DvDdSwAS.mjs → build-DKC2e9o_.mjs} +1 -2
  11. package/dist/cli.mjs +46 -22
  12. package/dist/{delete-8pjH8n8n.mjs → delete-X_CRgCf6.mjs} +1 -1
  13. package/dist/{deploy-CjU3e-3P.mjs → deploy-CchW5pmw.mjs} +2 -2
  14. package/dist/{dev-dFzdvwa7.mjs → dev-BfVl1uBA.mjs} +1 -1
  15. package/dist/{init-DK7GpIF9.mjs → init-BRfoc2EK.mjs} +2 -2
  16. package/dist/{logs-D2Yhqe2w.mjs → logs-hWnJ66Bl.mjs} +2 -2
  17. package/dist/scaffold/CLAUDE.md +162 -54
  18. package/dist/scaffold/package.json +4 -6
  19. package/dist/scaffold/server.mjs +12 -23
  20. package/dist/{secret-CwgDLpSy.mjs → secret-B-3T5tvr.mjs} +2 -2
  21. package/dist/{studio-D3-HWIfk.mjs → studio-wVWNLREn.mjs} +4 -3
  22. package/dist/templates/call-audit/agent.eval.test.ts +1 -1
  23. package/dist/templates/call-audit/agent.test.ts +80 -8
  24. package/dist/templates/call-audit/client.tsx +87 -3
  25. package/dist/templates/call-audit/workflows/audit.ts +52 -32
  26. package/dist/templates/call-audit/workflows/ingest.ts +15 -15
  27. package/dist/templates/call-audit/workflows/media.ts +1 -1
  28. package/dist/templates/call-audit/workflows/summarize.ts +0 -4
  29. package/dist/templates/link-digest/agent.eval.test.ts +4 -2
  30. package/dist/templates/link-digest/agent.test.ts +25 -5
  31. package/dist/templates/link-digest/client.tsx +65 -7
  32. package/dist/templates/link-digest/workflows/digest.ts +39 -29
  33. package/dist/templates/podcast-digest/agent.eval.test.ts +5 -5
  34. package/dist/templates/podcast-digest/agent.test.ts +108 -2
  35. package/dist/templates/podcast-digest/client.tsx +89 -8
  36. package/dist/templates/podcast-digest/workflows/digest.ts +67 -48
  37. package/dist/templates/podcast-digest/workflows/feeds.ts +1 -4
  38. package/dist/templates/podcast-digest/workflows/slack.ts +5 -6
  39. package/dist/templates/recap-workflow/agent.eval.test.ts +72 -8
  40. package/dist/templates/recap-workflow/agent.test.ts +323 -83
  41. package/dist/templates/recap-workflow/shared.ts +4 -3
  42. package/dist/templates/recap-workflow/workflows/recap.ts +322 -86
  43. package/dist/templates/recap-workflow/workflows/tokens.ts +21 -0
  44. package/dist/templates/redline/agent.eval.test.ts +1 -1
  45. package/dist/templates/redline/agent.test.ts +1 -1
  46. package/dist/templates/redline/client.tsx +83 -3
  47. package/dist/templates/redline/workflows/redline.ts +26 -23
  48. package/dist/templates/research-workflow/agent.eval.test.ts +9 -8
  49. package/dist/templates/research-workflow/agent.test.ts +32 -6
  50. package/dist/templates/research-workflow/agent.ts +4 -4
  51. package/dist/templates/research-workflow/shared.ts +4 -3
  52. package/dist/templates/research-workflow/workflows/prompts.ts +1 -1
  53. package/dist/templates/research-workflow/workflows/research.ts +35 -38
  54. package/dist/templates/spoken-summary/agent.eval.test.ts +3 -3
  55. package/dist/templates/spoken-summary/agent.test.ts +10 -8
  56. package/dist/templates/spoken-summary/client.tsx +72 -3
  57. package/dist/templates/spoken-summary/workflows/summarize.ts +25 -21
  58. package/dist/templates/spoken-summary/workflows/transcribe.ts +15 -18
  59. package/dist/templates/transcription-workflow/agent.eval.test.ts +1 -1
  60. package/dist/templates/transcription-workflow/agent.test.ts +626 -14
  61. package/dist/templates/transcription-workflow/agent.ts +6 -4
  62. package/dist/templates/transcription-workflow/client.tsx +106 -308
  63. package/dist/templates/transcription-workflow/recover.test.ts +141 -0
  64. package/dist/templates/transcription-workflow/recover.ts +130 -0
  65. package/dist/templates/transcription-workflow/run-panel.tsx +203 -0
  66. package/dist/templates/transcription-workflow/total-latency.tsx +144 -0
  67. package/dist/templates/transcription-workflow/workflows/batch.ts +21 -24
  68. package/dist/templates/transcription-workflow/workflows/downsample.ts +273 -0
  69. package/dist/templates/transcription-workflow/workflows/normalize.ts +17 -40
  70. package/dist/templates/transcription-workflow/workflows/stream.ts +177 -24
  71. package/dist/templates/transcription-workflow/workflows/transcribe.ts +69 -32
  72. package/dist/templates/transcription-workflow/workflows/wav.ts +15 -4
  73. package/dist/worker-bundler.d.ts +0 -14
  74. package/dist/worker-bundler.mjs +149 -1
  75. package/dist/{workflow-BflATMmU.mjs → workflow-BBu5oEDw.mjs} +84 -10
  76. package/dist/workflow.d.ts +1 -0
  77. package/package.json +4 -6
  78. package/dist/_workflow-scan.d.ts +0 -108
  79. package/dist/worker-bundler-CCVEDjm1.mjs +0 -650
  80. package/dist/workflow-bundler.d.ts +0 -138
  81. 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-CwgDLpSy.mjs");
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-CwgDLpSy.mjs");
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-CwgDLpSy.mjs");
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-D2Yhqe2w.mjs");
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-D3-HWIfk.mjs");
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-D3-HWIfk.mjs");
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-D3-HWIfk.mjs");
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-D3-HWIfk.mjs");
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-BflATMmU.mjs");
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-BflATMmU.mjs");
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-BflATMmU.mjs");
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-BflATMmU.mjs");
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-DK7GpIF9.mjs");
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-dFzdvwa7.mjs");
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-DvDdSwAS.mjs");
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-CjU3e-3P.mjs");
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-8pjH8n8n.mjs");
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-4WcKOjS5.mjs");
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-CKrsixd0.mjs";
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-CDuYl5Gb.mjs";
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-CKrsixd0.mjs";
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-BAR8Ya43.mjs");
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-D3-HWIfk.mjs");
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-BNotRgV1.mjs");
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-CKrsixd0.mjs";
5
- import { n as slugRequest } from "./_slug-api--ZBmIGH-.mjs";
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) {
@@ -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 no-check
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
- One thing does need a database whatever the run does: a workflow **upload**. An
409
- upload's record is a row, so `api.upload` and the file-taking form hooks refuse
410
- by name without a `DATABASE_URL`. (`ctx.db` was the other; there is no `ctx.db`
411
- see "Persisting data" below.)
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
- The build transforms that directory and nothing else. A `"use workflow"` body
416
- written in `agent.ts` is never transformed it runs inline once, with no
417
- durability and nothing saying so.
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 { sleep } from "workflow";
424
+ import type { WorkflowCtx } from "@alexkroman1/aai";
421
425
 
422
- export async function digestFlow(input: { url: string }) {
423
- "use workflow";
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. `"6 hours"` works the same as `"10 seconds"`.
428
- await sleep("10 seconds");
429
- return { ...digest, filedAt: await file(digest) };
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(digest: { url: string }) {
440
- "use step";
443
+ async function file(_digest: { url: string }) {
441
444
  return new Date().toISOString();
442
445
  }
443
446
  ```
444
447
 
445
- Three rules. The second and third fail silently if broken; the first is
446
- warned about by `aai build` and `aai dev`, naming the file and the call:
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 belong in a step, whose result is
451
- journaled and returned unchanged on replay. (The warning reads the built
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.** It is bundled and dispatched separately from
457
- the agent, so there is no `ctx` in one — see below for how it reaches the
458
- agent's env and a model anyway. A step reaches a database the same way a tool
459
- does now — its own client, its own credential from `requireStepEnv`.
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 no-check
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 `"use step"` body the wrapper
498
- is the one to use**:
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 DevKit's retry policy is decided by WHICH error a step throws. Raw,
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 DevKit's default one second while
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 one authoring module allowed to import the DevKit's `workflow` package, and
533
- `/step` is not written only for a step — `mapConcurrent` bounds a rate-limited
534
- call anywhere, `stepFetch` is an ordinary HTTP client, and your specs drive
535
- exported steps directly. Putting `workflow` in `/step`'s graph would put it in
536
- every one of those bundles. A step pays nothing for the extra import line.
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 `maxRetries`, and fails the run with
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 no-check
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 no-check
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 no-check
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 no-check
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 two cover almost everything an agent wants. A database is for data that
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": "^9.2.0",
18
- "@alexkroman1/aai-runtime": "^9.2.0",
19
- "@alexkroman1/aai-ui": "^9.2.0",
20
- "@workflow/world-postgres": "4.3.3",
17
+ "@alexkroman1/aai": "^10.0.0",
18
+ "@alexkroman1/aai-runtime": "^10.0.0",
19
+ "@alexkroman1/aai-ui": "^10.0.0",
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": "^9.2.0",
27
+ "@alexkroman1/aai-cli": "^10.0.0",
30
28
  "@tailwindcss/vite": "^4.3.3",
31
29
  "@types/node": "^26.2.0",
32
30
  "@types/react": "^19.2.18",