@lunora/codegen 1.0.0-alpha.41 → 1.0.0-alpha.43

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.
@@ -1,3 +1,4 @@
1
+ import { agentComponent } from '@lunora/agent/component';
1
2
  import { LunoraError } from '@lunora/errors';
2
3
  import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
3
4
 
@@ -696,19 +697,19 @@ ${members}
696
697
  };
697
698
  return [...namespaces.entries()].toSorted(([a], [b]) => a.localeCompare(b)).map((entry) => renderNamespace(entry)).join("\n");
698
699
  };
699
- const renderWorkflowsRef = (workflows) => {
700
- if (workflows.length === 0) {
700
+ const renderSchedulerReferences = (workflows, agents) => {
701
+ if (workflows.length === 0 && agents.length === 0) {
701
702
  return { block: "", importLine: "" };
702
703
  }
703
- const sorted = [...workflows].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
704
- const refMembers = sorted.map((workflow) => ` ${workflow.exportName}: WorkflowReference<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${workflow.exportName}>>;`).join("\n");
705
- const objectMembers = sorted.map(
706
- (workflow) => ` ${workflow.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(workflow.bindingName)}, name: ${JSON.stringify(workflow.exportName)} },`
707
- ).join("\n");
708
- const block = `
704
+ const importLines = [];
705
+ let block = "";
706
+ if (workflows.length > 0) {
707
+ block += `
709
708
  /** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`). */
710
709
  type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params } ? (unknown extends Params ? Record<string, unknown> : Params) : Record<string, unknown>;
711
-
710
+ `;
711
+ }
712
+ block += `
712
713
  /** A typed reference to a durable workflow, addressable for \`cronJobs()\` targets. Mirrors \`@lunora/scheduler\`'s \`WorkflowReference\` structurally. */
713
714
  export interface WorkflowReference<Params = Record<string, unknown>> {
714
715
  readonly isLunoraWorkflow: true;
@@ -716,7 +717,14 @@ export interface WorkflowReference<Params = Record<string, unknown>> {
716
717
  readonly binding: string;
717
718
  readonly name: string;
718
719
  }
719
-
720
+ `;
721
+ if (workflows.length > 0) {
722
+ const sorted = [...workflows].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
723
+ const refMembers = sorted.map((workflow) => ` ${workflow.exportName}: WorkflowReference<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${workflow.exportName}>>;`).join("\n");
724
+ const objectMembers = sorted.map(
725
+ (workflow) => ` ${workflow.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(workflow.bindingName)}, name: ${JSON.stringify(workflow.exportName)} },`
726
+ ).join("\n");
727
+ block += `
720
728
  /** This project's durable workflows, addressable as typed references (e.g. \`crons.daily("digest", …, workflows.digestPipeline, params)\`). */
721
729
  export interface WorkflowsRef {
722
730
  ${refMembers}
@@ -726,12 +734,141 @@ export const workflows: WorkflowsRef = {
726
734
  ${objectMembers}
727
735
  };
728
736
  `;
729
- return { block, importLine: `import type * as lunoraWorkflowDefinitions from "../workflows.js";
730
- ` };
737
+ importLines.push(`import type * as lunoraWorkflowDefinitions from "../workflows.js";
738
+ `);
739
+ }
740
+ if (agents.length > 0) {
741
+ const sorted = [...agents].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
742
+ const refMembers = sorted.map((agent) => ` ${agent.exportName}: WorkflowReference<AgentRunInput>;`).join("\n");
743
+ const objectMembers = sorted.map(
744
+ (agent) => ` ${agent.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(agent.bindingName)}, name: ${JSON.stringify(agent.name)} },`
745
+ ).join("\n");
746
+ block += `
747
+ /** This project's durable agents, addressable as typed references for \`cronJobs()\` — each fire starts a fresh agent run (e.g. \`crons.daily("sweep", …, agents.support, { input, threadKey })\`). */
748
+ export interface AgentsRef {
749
+ ${refMembers}
750
+ }
751
+
752
+ export const agents: AgentsRef = {
753
+ ${objectMembers}
754
+ };
755
+ `;
756
+ importLines.push(`import type { AgentRunInput } from "@lunora/agent";
757
+ `);
758
+ }
759
+ return { block, importLine: importLines.join("") };
760
+ };
761
+ const agentRuntimeFunctionNames = () => Object.keys(agentComponent().functions).toSorted((a, b) => a.localeCompare(b));
762
+ const takenAgentFunctionNames = (functions) => new Set(functions.filter((definition) => sanitizeNamespace(definition.filePath) === "agents").map((definition) => definition.exportName));
763
+ const renderAgentFunctionRegistry = (agents, functions) => {
764
+ const empty = { importLine: "", lines: "", prelude: "" };
765
+ if (agents.length === 0) {
766
+ return empty;
767
+ }
768
+ const taken = takenAgentFunctionNames(functions);
769
+ const names = agentRuntimeFunctionNames().filter((name) => !taken.has(name));
770
+ if (names.length === 0) {
771
+ return empty;
772
+ }
773
+ return {
774
+ importLine: 'import { agentComponent } from "@lunora/agent/component";\n',
775
+ lines: names.map((name) => ` "agents:${name}": lunoraAgentRuntimeFunctions.${name} as unknown as RegisteredLunoraFunction,`).join("\n"),
776
+ prelude: "\n/**\n * The `@lunora/agent` runtime component — auto-registered because `lunora/agents.ts`\n * declares agents. The durable loop dispatches the internal mutations; clients\n * subscribe to the public queries (`agents:agentMessages` is the live thread view).\n */\nconst lunoraAgentRuntimeFunctions = agentComponent().functions;\n"
777
+ };
778
+ };
779
+ const renderSandboxFunctionRegistry = (usesSandbox, functions) => {
780
+ if (!usesSandbox) {
781
+ return { importLine: "", lines: "", prelude: "" };
782
+ }
783
+ const collision = functions.find((definition) => sanitizeNamespace(definition.filePath) === "sandbox" && definition.exportName === "invoke");
784
+ if (collision) {
785
+ throw new LunoraError(
786
+ "INTERNAL",
787
+ `@lunora/codegen: "sandbox:invoke" is reserved for the batteries-included sandbox tool dispatcher (browserTool/containerTool) — rename the "invoke" export in ${collision.filePath}`
788
+ );
789
+ }
790
+ return {
791
+ importLine: 'import { sandboxComponent } from "@lunora/agent/component";\n',
792
+ lines: ' "sandbox:invoke": lunoraSandbox.invoke as unknown as RegisteredLunoraFunction,',
793
+ prelude: "\n/**\n * The `@lunora/agent` sandbox dispatcher — auto-registered because `lunora/`\n * imports a sandbox tool (`browserTool`/`containerTool`). The batteries-included\n * tools dispatch to this internal action, which runs on an action ctx carrying\n * `ctx.browser` + `ctx.containers` (the durable tool step itself has neither).\n */\nconst lunoraSandbox = sandboxComponent();\n"
794
+ };
731
795
  };
732
- const emitApi = (functions, workflows = [], useUmbrella = false) => {
796
+ const syntheticAgentApiFunctions = (agents, functions) => {
797
+ if (agents.length === 0) {
798
+ return [];
799
+ }
800
+ const taken = takenAgentFunctionNames(functions);
801
+ const definitions = [
802
+ {
803
+ args: { key: { kind: "string" }, limit: { inner: { kind: "number" }, kind: "optional" } },
804
+ exportName: "agentMessages",
805
+ filePath: "agents",
806
+ kind: "query",
807
+ returnType: "Record<string, unknown>[]"
808
+ },
809
+ {
810
+ args: {
811
+ decision: {
812
+ kind: "union",
813
+ members: [
814
+ { kind: "literal", literalValue: '"approve"' },
815
+ { kind: "literal", literalValue: '"reject"' }
816
+ ]
817
+ },
818
+ instanceId: { kind: "string" },
819
+ note: { inner: { kind: "string" }, kind: "optional" },
820
+ threadKey: { kind: "string" },
821
+ toolCallId: { kind: "string" }
822
+ },
823
+ exportName: "agentResolveApproval",
824
+ filePath: "agents",
825
+ kind: "mutation",
826
+ returnType: "{ resolved: boolean }"
827
+ },
828
+ {
829
+ args: {
830
+ agent: { kind: "string" },
831
+ input: { kind: "string" },
832
+ threadKey: { kind: "string" },
833
+ title: { inner: { kind: "string" }, kind: "optional" }
834
+ },
835
+ exportName: "agentRun",
836
+ filePath: "agents",
837
+ kind: "mutation",
838
+ returnType: "{ id: string; threadKey: string }"
839
+ },
840
+ {
841
+ args: { key: { kind: "string" } },
842
+ exportName: "agentState",
843
+ filePath: "agents",
844
+ kind: "query",
845
+ returnType: "Record<string, unknown> | undefined"
846
+ },
847
+ {
848
+ args: { key: { kind: "string" } },
849
+ exportName: "agentThread",
850
+ filePath: "agents",
851
+ kind: "query",
852
+ returnType: "Record<string, unknown> | undefined"
853
+ }
854
+ ];
855
+ for (const agent of agents) {
856
+ if (agent.voice) {
857
+ definitions.push({
858
+ args: { threadKey: { kind: "string" } },
859
+ exportName: `${agent.exportName}Voice`,
860
+ filePath: "agents",
861
+ kind: "stream",
862
+ returnType: "Record<string, unknown>"
863
+ });
864
+ }
865
+ }
866
+ return definitions.filter((definition) => !taken.has(definition.exportName));
867
+ };
868
+ const emitApi = (options) => {
869
+ const { agents = [], functions, useUmbrella = false, workflows = [] } = options;
733
870
  const base = baseSpecifiers(useUmbrella);
734
- const publicFunctions = functions.filter((definition) => definition.visibility !== "internal");
871
+ const publicFunctions = [...functions.filter((definition) => definition.visibility !== "internal"), ...syntheticAgentApiFunctions(agents, functions)];
735
872
  const internalFunctions = functions.filter((definition) => definition.visibility === "internal");
736
873
  const publicBody = renderApiBody(publicFunctions);
737
874
  const internalBody = renderApiBody(internalFunctions);
@@ -747,10 +884,10 @@ ${publicBody}
747
884
  const internalBlock = internalBody ? `
748
885
  ${internalBody}
749
886
  ` : "";
750
- const workflowsRef = renderWorkflowsRef(workflows);
887
+ const schedulerReferences = renderSchedulerReferences(workflows, agents);
751
888
  return `${GENERATED_HEADER}import { anyApi } from "${base.serverTypes}";
752
889
  import type { FunctionReference } from "${base.client}";
753
- ${workflowsRef.importLine}${dataModelImportLine}
890
+ ${schedulerReferences.importLine}${dataModelImportLine}
754
891
  export interface ApiTypes {${apiBlock}}
755
892
 
756
893
  export const api = anyApi as unknown as ApiTypes;
@@ -759,7 +896,7 @@ export const api = anyApi as unknown as ApiTypes;
759
896
  export interface InternalApiTypes {${internalBlock}}
760
897
 
761
898
  export const internal = anyApi as unknown as InternalApiTypes;
762
- ${workflowsRef.block}`;
899
+ ${schedulerReferences.block}`;
763
900
  };
764
901
  const emitSeed = (enabled) => {
765
902
  if (!enabled) {
@@ -928,6 +1065,7 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
928
1065
  return ["default", ...[...named].toSorted((a, b) => a.localeCompare(b))];
929
1066
  };
930
1067
  const emitServer = ({
1068
+ agents = [],
931
1069
  containers = [],
932
1070
  env,
933
1071
  hasAccessFacade = false,
@@ -986,6 +1124,11 @@ const emitServer = ({
986
1124
  assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
987
1125
  return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
988
1126
  readonly ${queue.bindingName}?: unknown;`;
1127
+ }),
1128
+ ...agents.map((agent) => {
1129
+ assertIdentifier(agent.bindingName, `agent binding "${agent.bindingName}"`);
1130
+ return ` /** Workflow binding for the \`${agent.exportName}\` agent. */
1131
+ readonly ${agent.bindingName}?: unknown;`;
989
1132
  })
990
1133
  ].join("\n");
991
1134
  const envBlock = `
@@ -1056,6 +1199,20 @@ ${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBo
1056
1199
  }` : "";
1057
1200
  const queuesContextField = hasQueues ? `
1058
1201
  readonly queues: LunoraQueues;` : "";
1202
+ const hasAgents = agents.length > 0;
1203
+ const agentsTypeImport = hasAgents ? `import type { AgentHandle } from "@lunora/agent";
1204
+ ` : "";
1205
+ const agentsTypeBlock = hasAgents ? `
1206
+
1207
+ /** This project's declared agents, addressable from \`ctx.agents\` by their \`lunora/agents.ts\` export name. */
1208
+ export interface LunoraAgents {
1209
+ ${agents.map((agent) => {
1210
+ assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
1211
+ return ` readonly ${agent.exportName}: AgentHandle;`;
1212
+ }).join("\n")}
1213
+ }` : "";
1214
+ const agentsContextField = hasAgents ? `
1215
+ readonly agents: LunoraAgents;` : "";
1059
1216
  const identityTypeImport = identity ? `import type { InferIdentity } from "${base.server}";
1060
1217
  import type * as lunoraIdentityContract from "../identity.js";
1061
1218
  ` : "";
@@ -1101,11 +1258,11 @@ import type {
1101
1258
  } from "${base.server}";
1102
1259
 
1103
1260
  import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
1104
- ${aiTypeImport}${paymentsTypeImport}${x402TypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${identityTypeImport}${envTypeImport}
1261
+ ${aiTypeImport}${paymentsTypeImport}${x402TypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${agentsTypeImport}${identityTypeImport}${envTypeImport}
1105
1262
  export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
1106
1263
 
1107
1264
  /** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
1108
- export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${identityTypeBlock}${envTypeBlock}
1265
+ export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${agentsTypeBlock}${identityTypeBlock}${envTypeBlock}
1109
1266
 
1110
1267
  /**
1111
1268
  * Project-typed contexts. The base contexts from \`@lunora/server\` are
@@ -1143,13 +1300,13 @@ export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${authOmit}
1143
1300
  export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
1144
1301
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
1145
1302
  readonly orm: OrmWriter;
1146
- readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${envContextField}${workflowsContextField}${queuesContextField}${authContextField}
1303
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${envContextField}${workflowsContextField}${queuesContextField}${agentsContextField}${authContextField}
1147
1304
  }
1148
1305
 
1149
1306
  export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
1150
1307
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
1151
1308
  readonly orm: OrmWriter;
1152
- readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${x402ActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${envContextField}${workflowsContextField}${queuesContextField}${authContextField}
1309
+ readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${x402ActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${envContextField}${workflowsContextField}${queuesContextField}${agentsContextField}${authContextField}
1153
1310
  }
1154
1311
 
1155
1312
  /**
@@ -1219,10 +1376,22 @@ const renderLifecycleManifest = (functions) => {
1219
1376
  }
1220
1377
  return manifest;
1221
1378
  };
1222
- const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
1379
+ const emitFunctions = (options) => {
1380
+ const { agents = [], functions, migrations = [], mutators = [], shapes = [], useUmbrella = false, usesSandbox = false } = options;
1223
1381
  const hasFunctions = functions.length > 0;
1224
1382
  const base = baseSpecifiers(useUmbrella);
1225
1383
  const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
1384
+ const agentRegistry = renderAgentFunctionRegistry(agents, functions);
1385
+ const sandboxRegistry = renderSandboxFunctionRegistry(usesSandbox, functions);
1386
+ const autoLines = [agentRegistry.lines, sandboxRegistry.lines].filter((block) => block.length > 0).join("\n");
1387
+ let dispatchBodyWithAgents = dispatchBody;
1388
+ if (autoLines.length > 0) {
1389
+ dispatchBodyWithAgents = dispatchBody.length > 0 ? `${dispatchBody.trimEnd()}
1390
+ ${autoLines}
1391
+ ` : `
1392
+ ${autoLines}
1393
+ `;
1394
+ }
1226
1395
  const lifecycleHooks = renderLifecycleManifest(functions);
1227
1396
  const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
1228
1397
  ` : "";
@@ -1267,7 +1436,7 @@ ${caller.implementation}
1267
1436
  const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
1268
1437
 
1269
1438
  ` : "";
1270
- return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import { LunoraError } from "${base.server}";
1439
+ return `${GENERATED_HEADER}${importBlock}${agentRegistry.importLine}${sandboxRegistry.importLine}${compiledArgsImport}${shapeTypeImport}import { LunoraError } from "${base.server}";
1271
1440
  import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
1272
1441
  ${dataModelImport}
1273
1442
  /**
@@ -1288,12 +1457,12 @@ export interface RegisteredLunoraFunction {
1288
1457
  /** \`"internal"\` functions are rejected on the external RPC path; absence === public. */
1289
1458
  visibility?: "internal" | "public";
1290
1459
  }
1291
-
1460
+ ${agentRegistry.prelude}${sandboxRegistry.prelude}
1292
1461
  /**
1293
1462
  * Static dispatch table. The key matches the \`__lunoraRef\` the client
1294
1463
  * emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
1295
1464
  */
1296
- export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
1465
+ export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBodyWithAgents}};
1297
1466
  ${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
1298
1467
  /**
1299
1468
  * Connection-lifecycle manifest: the function paths the generated ShardDO
@@ -1912,10 +2081,11 @@ const emitContainerFragments = (containers, jurisdiction) => {
1912
2081
  }).join("\n");
1913
2082
  return {
1914
2083
  // Schema `.jurisdiction("…")` pins every container DO this shard reaches
1915
- // to the data-residency region; the arg is omitted when undeclared so
1916
- // existing generated output is unchanged.
2084
+ // to the data-residency region (`undefined` when undeclared). The trailing
2085
+ // `this.getCurrentTraceparent()` forwards the inbound RPC's W3C trace
2086
+ // context onto outbound container fetches so their spans join the trace.
1917
2087
  build: `
1918
- const containers = createContainerContext(env, LUNORA_CONTAINERS${jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : ""});
2088
+ const containers = createContainerContext(env, LUNORA_CONTAINERS, ${jurisdiction ? JSON.stringify(jurisdiction) : "undefined"}, this.getCurrentTraceparent());
1919
2089
  `,
1920
2090
  contextField: `
1921
2091
  containers,`,
@@ -1966,6 +2136,62 @@ type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params
1966
2136
  /** Output type carried by a \`defineWorkflow\` definition (its phantom \`__output\`), so each entrypoint keeps the authored return type. */
1967
2137
  type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output } ? Output : unknown;
1968
2138
 
2139
+ ${classes}`;
2140
+ };
2141
+ const emitAgents = (agents) => {
2142
+ if (agents.length === 0) {
2143
+ return "";
2144
+ }
2145
+ const voiceAgents = agents.filter((agent) => agent.voice);
2146
+ const hasVoice = voiceAgents.length > 0;
2147
+ const classes = agents.map((agent) => {
2148
+ assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
2149
+ assertIdentifier(agent.className, `agent class "${agent.className}"`);
2150
+ const workflowClass = `/** WorkflowEntrypoint for the \`${agent.exportName}\` agent (binding \`${agent.bindingName}\`). */
2151
+ export class ${agent.className} extends LunoraWorkflow<AgentRunInput, AgentRunResult> {
2152
+ public constructor(ctx: ConstructorParameters<typeof LunoraWorkflow>[0], env: Record<string, unknown>) {
2153
+ super(ctx, env, compileAgentWorkflow(${agent.exportName}, "${agent.exportName}"), "${agent.exportName}");
2154
+ }
2155
+ }
2156
+ `;
2157
+ if (!agent.voice) {
2158
+ return workflowClass;
2159
+ }
2160
+ const voiceClassName = agent.voiceClassName ?? "";
2161
+ const voiceBindingName = agent.voiceBindingName ?? "";
2162
+ assertIdentifier(voiceClassName, `agent voice class "${voiceClassName}"`);
2163
+ const voiceClass = `
2164
+ /** Voice-session Durable Object for the \`${agent.exportName}\` agent (binding \`${voiceBindingName}\`). */
2165
+ export class ${voiceClassName} extends VoiceSessionDO {
2166
+ public constructor(ctx: ConstructorParameters<typeof VoiceSessionDO>[0], env: Record<string, unknown>) {
2167
+ super(ctx, env, ${agent.exportName}, "${agent.exportName}");
2168
+ }
2169
+ }
2170
+ `;
2171
+ return `${workflowClass}${voiceClass}`;
2172
+ }).join("\n");
2173
+ const imports = agents.map((agent) => agent.exportName).join(", ");
2174
+ const runtimeImport = hasVoice ? `import { compileAgentWorkflow, VoiceSessionDO } from "@lunora/agent";` : `import { compileAgentWorkflow } from "@lunora/agent";`;
2175
+ const voiceHeaderNote = hasVoice ? `
2176
+ *
2177
+ * Voice-enabled agents ALSO get a \`VoiceSessionDO\` subclass here — a real
2178
+ * Durable Object (unlike the Workflow), so wrangler needs both its
2179
+ * \`durable_objects\` binding and its \`class_name\` export (the same re-export
2180
+ * line covers it).` : "";
2181
+ return `${GENERATED_HEADER}/**
2182
+ * WorkflowEntrypoint classes for the agents declared in \`lunora/agents.ts\`.
2183
+ * Each \`defineAgent\` compiles a replay-safe tool-loop onto a Cloudflare
2184
+ * Workflow. Re-export them from your worker entry — wrangler requires each
2185
+ * \`workflows[].class_name\` to be exported by the worker:
2186
+ *
2187
+ * \`export * from "./lunora/_generated/agents.js";\`${voiceHeaderNote}
2188
+ */
2189
+ import LunoraWorkflow from "@lunora/workflow/do";
2190
+ ${runtimeImport}
2191
+ import type { AgentRunInput, AgentRunResult } from "@lunora/agent";
2192
+
2193
+ import { ${imports} } from "../agents.js";
2194
+
1969
2195
  ${classes}`;
1970
2196
  };
1971
2197
  const emitQueues = (queues) => {
@@ -2044,6 +2270,31 @@ ${specEntries}
2044
2270
  `
2045
2271
  };
2046
2272
  };
2273
+ const emitAgentFragments = (agents) => {
2274
+ if (agents.length === 0) {
2275
+ return { build: "", contextField: "", importLines: [], specs: "" };
2276
+ }
2277
+ for (const agent of agents) {
2278
+ assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
2279
+ assertIdentifier(agent.bindingName, `agent binding "${agent.bindingName}"`);
2280
+ }
2281
+ const specEntries = agents.map((agent) => ` { binding: "${agent.bindingName}", exportName: "${agent.exportName}"${agent.publicRun === true ? ", publicRun: true" : ""} },`).join("\n");
2282
+ return {
2283
+ build: `
2284
+ const agents = createAgentContext(env, LUNORA_AGENTS);
2285
+ `,
2286
+ contextField: `
2287
+ agents,`,
2288
+ importLines: [`import { createAgentContext } from "@lunora/agent";`, `import type { AgentBindingSpec } from "@lunora/agent";`],
2289
+ // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2290
+ specs: `
2291
+ /** Wiring specs for \`ctx.agents\` (codegen-derived from \`lunora/agents.ts\`). */
2292
+ const LUNORA_AGENTS: ReadonlyArray<AgentBindingSpec> = [
2293
+ ${specEntries}
2294
+ ];
2295
+ `
2296
+ };
2297
+ };
2047
2298
  const emitWorkflowsMetadataFragments = (workflows) => {
2048
2299
  if (workflows.length === 0) {
2049
2300
  return { constant: "", override: "" };
@@ -2207,6 +2458,7 @@ const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
2207
2458
  ];
2208
2459
  const emitShard = ({
2209
2460
  advisories = [],
2461
+ agents = [],
2210
2462
  containers = [],
2211
2463
  env,
2212
2464
  flagKeys = [],
@@ -2261,6 +2513,7 @@ const emitShard = ({
2261
2513
  importLines: workflowImportLines,
2262
2514
  specs: workflowSpecs
2263
2515
  } = emitWorkflowFragments(workflows);
2516
+ const { build: agentsBuild, contextField: agentsContextField, importLines: agentImportLines, specs: agentSpecs } = emitAgentFragments(agents);
2264
2517
  const {
2265
2518
  build: paymentsBuild,
2266
2519
  configField: paymentsConfigField,
@@ -2407,6 +2660,7 @@ const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCT
2407
2660
  ...containerImportLines,
2408
2661
  ...workflowImportLines,
2409
2662
  ...queueImportLines,
2663
+ ...agentImportLines,
2410
2664
  ...paymentsImports,
2411
2665
  ...x402Imports,
2412
2666
  ``,
@@ -2748,7 +3002,7 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
2748
3002
 
2749
3003
  /** Which optional package-backed features this app wires up (discovered from imports / \`ctx.*\` reads / schema signals) served via \`__lunora_admin__:studioFeatures\` so the studio hides nav pages whose package isn't enabled. */
2750
3004
  const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
2751
- ${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
3005
+ ${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}${agentSpecs}
2752
3006
  export interface ShardDOConfig {
2753
3007
  /** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
2754
3008
  cdc?: boolean;
@@ -3164,7 +3418,7 @@ ${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workfl
3164
3418
  // dispatch path) fall back to the per-request fields as before.
3165
3419
  const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
3166
3420
  const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
3167
- ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}
3421
+ ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}${agentsBuild}
3168
3422
  const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3169
3423
  // Build the storage adapter once and share it between \`ctx.storage\`
3170
3424
  // and \`ctx.db.system._storage\` so both read the same R2 binding. The
@@ -3206,7 +3460,7 @@ ${facadeBlock}${paymentsBuild}
3206
3460
  log,
3207
3461
  now,${ormContextField}
3208
3462
  scheduler,
3209
- storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}
3463
+ storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}${agentsContextField}
3210
3464
  };
3211
3465
  ${isActionLine}${actionOnlyBlock}
3212
3466
  ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
@@ -3470,4 +3724,4 @@ const emitWranglerCronTriggers = (crons) => {
3470
3724
  return triggers;
3471
3725
  };
3472
3726
 
3473
- export { APP_METHOD_CAPABILITIES as A, CAPABILITIES as C, GENERATED_HEADER as G, emitCollections as a, emitContainers as b, emitCrons as c, emitDataModel as d, emitApi as e, emitDrizzleSchema as f, emitFunctions as g, emitServer as h, emitShard as i, emitVectors as j, emitWorkflows as k, emitWranglerCronTriggers as l, buildStorageColumns as m, emitQueues as n, emitSeed as o };
3727
+ export { APP_METHOD_CAPABILITIES as A, CAPABILITIES as C, GENERATED_HEADER as G, emitApi as a, emitCollections as b, emitContainers as c, emitCrons as d, emitAgents as e, emitDataModel as f, emitDrizzleSchema as g, emitFunctions as h, emitServer as i, emitShard as j, emitVectors as k, emitWorkflows as l, emitWranglerCronTriggers as m, buildStorageColumns as n, emitQueues as o, emitSeed as p };
@@ -1,4 +1,4 @@
1
- import { A as APP_METHOD_CAPABILITIES, G as GENERATED_HEADER } from './emit-CFz-9dcD.mjs';
1
+ import { A as APP_METHOD_CAPABILITIES, G as GENERATED_HEADER } from './emit-DtherCpQ.mjs';
2
2
 
3
3
  const hasFlagKey = (key) => `has${key.charAt(0).toUpperCase()}${key.slice(1)}`;
4
4
  const LONG_TAIL = APP_METHOD_CAPABILITIES.map(
@@ -11,6 +11,9 @@ const buildAccessImports = (hasAccess, hasAuth) => hasAccess ? [
11
11
  `import { createAccessResolver${hasAuth ? ", composeResolvers" : ""} } from "@lunora/cloudflare-access";`
12
12
  ] : [];
13
13
  const buildKvImports = (hasKv) => hasKv ? [`import { createKvIntrospectorFromEnv } from "@lunora/bindings/kv";`] : [];
14
+ const hasEmailAgents = (options) => (options.emailAgents?.length ?? 0) > 0;
15
+ const buildInboundImports = (options) => hasEmailAgents(options) ? [`import { dispatchAgentEmail } from "@lunora/agent/inbound";`] : [];
16
+ const buildAgentDefinitionsImport = (options) => hasEmailAgents(options) ? [`import * as lunoraAgentDefinitions from "../agents.js";`] : [];
14
17
  const buildImportLines = (options) => {
15
18
  const {
16
19
  hasAccess,
@@ -59,10 +62,12 @@ const buildImportLines = (options) => {
59
62
  ...hasScheduler ? [`import type { DurableObjectNamespaceLike } from "@lunora/scheduler";`, `import { createScheduler } from "@lunora/scheduler";`] : [],
60
63
  ...hasStorage ? [`import type { R2BucketLike, Storage } from "@lunora/storage";`, `import { createBucketStorage, createStorage } from "@lunora/storage";`] : [],
61
64
  ...hasWorkflow ? [`import { createWorkflowsRestClient } from "@lunora/workflow";`] : [],
65
+ ...buildInboundImports(options),
62
66
  `import type { ${[...runtimeTypeImports].toSorted((a, b) => a.localeCompare(b)).join(", ")} } from "${runtimeModule}";`,
63
67
  `import { ${runtimeValueImports} } from "${runtimeModule}";`,
64
68
  ``,
65
69
  ...buildIdentityImports(options.identity),
70
+ ...buildAgentDefinitionsImport(options),
66
71
  ...hasGlobal || hasHyperdriveGlobal ? [`import schema from "../schema.js";`] : [],
67
72
  `import { LUNORA_CRONS } from "./crons.js";`,
68
73
  `import { LUNORA_FUNCTIONS } from "./functions.js";`,
@@ -383,6 +388,18 @@ const buildWorkerOptionLines = (options) => [
383
388
  }` : ` if (this.accessSelector) {
384
389
  options.resolveIdentity = createAccessResolver(this.accessSelector(env));
385
390
  }`
391
+ ] : [],
392
+ // Voice-enabled agents: map each export name to its `VOICE_*` Durable Object
393
+ // namespace so the runtime serves `/_lunora/voice/<exportName>`. Read off
394
+ // `env` structurally (the binding is provisioned by the config layer's
395
+ // reconcile step, so it may not be on the generated `Env` type). Emitted only
396
+ // when at least one agent opted into voice — voice-free output is unchanged.
397
+ ...options.voiceAgents && options.voiceAgents.length > 0 ? [
398
+ ` options.voiceAgents = {
399
+ ${options.voiceAgents.map(
400
+ (agent) => ` ${JSON.stringify(agent.exportName)}: (env as Record<string, unknown>)[${JSON.stringify(agent.bindingName)}] as ShardNamespaceLike,`
401
+ ).join("\n")}
402
+ };`
386
403
  ] : []
387
404
  ];
388
405
  const buildBaseWorkerOptions = (options) => [
@@ -554,6 +571,12 @@ const emitApp = (options) => {
554
571
  const buildWorkerLine = options.hasFramework ? ` const buildWorker = (env: Env): LunoraWorker =>
555
572
  host ? withFrameworkWorker(host, (hostEnv) => this.buildWorkerOptions(hostEnv as Env, ${getAuthArgument})) : createWorker(this.buildWorkerOptions(env, ${getAuthArgument}));` : ` const buildWorker = (env: Env): LunoraWorker => createWorker(this.buildWorkerOptions(env, ${getAuthArgument}));`;
556
573
  const assembleParameter = options.hasFramework ? `host?: FrameworkHostHandler` : ``;
574
+ const emailAgents = options.emailAgents ?? [];
575
+ const emailAgentsBlock = emailAgents.length > 0 ? ` composed.email = dispatchAgentEmail([
576
+ ${emailAgents.map((agent) => ` { agent: lunoraAgentDefinitions.${agent.exportName}, binding: ${JSON.stringify(agent.bindingName)} },`).join("\n")}
577
+ ]);
578
+
579
+ ` : "";
557
580
  const buildTerminals = ` /** Materialise the standalone Cloudflare worker + \`ShardDO\` class. */
558
581
  public build(): ComposedApp {
559
582
  return this.assemble();
@@ -630,7 +653,7 @@ ${buildWorkerLine}
630
653
  },` : ""}
631
654
  };
632
655
 
633
- if (this.emailHandler) {
656
+ ${emailAgentsBlock} if (this.emailHandler) {
634
657
  const handler = this.emailHandler;
635
658
 
636
659
  composed.email = (message, rawEnv, context) => handler(rawEnv as Env)(message, rawEnv, context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/codegen",
3
- "version": "1.0.0-alpha.41",
3
+ "version": "1.0.0-alpha.43",
4
4
  "description": "Code generator for Lunora: emits _generated/{api,server,dataModel}.ts from your schema",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,11 +46,12 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/advisor": "1.0.0-alpha.26",
50
- "@lunora/container": "1.0.0-alpha.10",
49
+ "@lunora/advisor": "1.0.0-alpha.27",
50
+ "@lunora/agent": "1.0.0-alpha.1",
51
+ "@lunora/container": "1.0.0-alpha.11",
51
52
  "@lunora/errors": "1.0.0-alpha.4",
52
53
  "@lunora/queue": "1.0.0-alpha.7",
53
- "@lunora/scheduler": "1.0.0-alpha.8",
54
+ "@lunora/scheduler": "1.0.0-alpha.9",
54
55
  "@lunora/values": "1.0.0-alpha.7",
55
56
  "@lunora/workflow": "1.0.0-alpha.9",
56
57
  "ts-morph": "^28.0.0"
@@ -1,3 +0,0 @@
1
- import '@lunora/errors';
2
- export { G as GENERATED_HEADER, m as buildStorageColumns, e as emitApi, a as emitCollections, b as emitContainers, c as emitCrons, d as emitDataModel, f as emitDrizzleSchema, g as emitFunctions, n as emitQueues, o as emitSeed, h as emitServer, i as emitShard, j as emitVectors, k as emitWorkflows, l as emitWranglerCronTriggers } from './emit-CFz-9dcD.mjs';
3
- import './paths-BRd6JHuF.mjs';