Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-trees-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Keep development runtime generations executable after authored source or dependencies change by bundling ordinary dependencies and materializing configured external dependency closures.
4 changes: 2 additions & 2 deletions packages/eve/src/execution/sandbox/prewarm.scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { compileAgent, compileAgentInBuildWorkspace } from "#compiler/compile-agent.js";
import { resolvePackageSourceFilePath } from "#internal/application/package.js";
import { createDevelopmentNitroArtifactsConfig } from "#internal/nitro/host/artifacts-config.js";
import { publishDevelopmentRuntimeArtifactsSnapshot } from "#internal/nitro/dev-runtime-artifacts.js";
import { publishDevelopmentGeneration } from "#internal/nitro/development-generation.js";
import { resolveNitroCompiledArtifactsSource } from "#internal/nitro/routes/runtime-artifacts.js";
import { useTemporaryDirectories } from "#internal/testing/use-temporary-app-roots.js";
import type {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe("prewarmAppSandboxes", () => {
const compileResult = await compileAgent({
startPath: appRoot,
});
await publishDevelopmentRuntimeArtifactsSnapshot(compileResult);
await publishDevelopmentGeneration(compileResult);

await prewarmAppSandboxes({
appRoot,
Expand Down
76 changes: 75 additions & 1 deletion packages/eve/src/internal/application/compiled-artifacts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { existsSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { copyFile, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";

import type { CompileMetadata } from "#compiler/artifacts.js";
Expand All @@ -12,6 +12,7 @@ import {
resolvePackageSourceFilePath,
} from "#internal/application/package.js";
import type { AgentWorkflowWorldDefinition } from "#shared/agent-definition.js";
import { readMaterializedAuthoredModuleIndex } from "#internal/materialized-authored-modules.js";

export type BuiltInWorkflowWorldTarget = "local" | "vercel";

Expand Down Expand Up @@ -99,6 +100,79 @@ export async function writeCompiledArtifactsFiles(input: {
return generatedArtifacts;
}

// The dev host's Nitro inputs outlive any single generation, so nothing
// written here may point into authored source or a prunable snapshot: the
// bootstrap references no authored module, and the instrumentation bundle is
// copied out of the generation into the stable host directory.
export async function writeDevelopmentCompiledArtifactsFiles(input: {
readonly compileResult: CompileAgentResult;
readonly outDir: string;
readonly runtimeAppRoot: string;
}): Promise<GeneratedCompiledArtifactsFiles> {
const bootstrapPath = join(input.outDir, "compiled-artifacts-bootstrap.mjs");
const instrumentationPluginPath = join(input.outDir, "compiled-artifacts-instrumentation.mjs");
const instrumentationSourcePath = join(
input.outDir,
"compiled-artifacts-instrumentation-source.mjs",
);
const workflowWorldPluginPath = join(input.outDir, "compiled-artifacts-workflow-world.mjs");
const materializedIndex = await readMaterializedAuthoredModuleIndex(input.runtimeAppRoot);

if (materializedIndex === undefined) {
throw new Error(`Development generation at "${input.runtimeAppRoot}" is not materialized.`);
}

await mkdir(input.outDir, { recursive: true });
await writeFile(
bootstrapPath,
createDevelopmentCompiledArtifactsBootstrapSource(input.compileResult.manifest.config.name),
);
await writeFile(
workflowWorldPluginPath,
createWorkflowWorldPluginSource({
compiledArtifactsBootstrapPath: bootstrapPath,
configuredWorld: input.compileResult.manifest.config.experimental?.workflow?.world,
defaultWorld: "local",
}),
);

const generatedArtifacts: GeneratedCompiledArtifactsFiles = {
bootstrapPath,
workflowWorldPluginPath,
};

if (materializedIndex.instrumentation !== undefined) {
await copyFile(
join(input.runtimeAppRoot, ".eve", "compile", materializedIndex.instrumentation),
instrumentationSourcePath,
);
await writeFile(
instrumentationPluginPath,
createInstrumentationPluginSource({
agentName: input.compileResult.manifest.config.name,
instrumentationPath: instrumentationSourcePath,
registerConfigPath: resolvePackageSourceFilePath("src/harness/instrumentation-config.ts"),
}),
);
generatedArtifacts.instrumentationPluginPath = instrumentationPluginPath;
generatedArtifacts.instrumentationSourcePath = instrumentationSourcePath;
}

return generatedArtifacts;
}

function createDevelopmentCompiledArtifactsBootstrapSource(agentName: string): string {
return [
"// Generated by eve. Do not edit by hand.",
`import { installEveWorkflowQueueNamespace } from ${stringifyEsmImportSpecifier(resolvePackageSourceFilePath("src/internal/workflow/queue-namespace.ts"))};`,
"",
`installEveWorkflowQueueNamespace(${JSON.stringify(agentName)});`,
"",
"export default function installDevelopmentCompiledArtifactsPlugin() {}",
"",
].join("\n");
}

const INSTRUMENTATION_EXTENSIONS = [".ts", ".mts", ".js", ".mjs"];

/**
Expand Down
29 changes: 29 additions & 0 deletions packages/eve/src/internal/authored-directive-prologue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";

import { assertNoWorkflowDirectivePrologue } from "#internal/authored-directive-prologue.js";

describe("assertNoWorkflowDirectivePrologue", () => {
it.each(["use step", "use workflow"])("rejects an authored %s directive", async (directive) => {
await expect(
assertNoWorkflowDirectivePrologue({
filePath: "/app/agent/tools/example.ts",
source: `'use strict';\n${JSON.stringify(directive)};\nexport const value = 1;\n`,
}),
).rejects.toThrow(new RegExp(`actual .*${directive}.* directive`, "u"));
});

it("accepts comments and ordinary strings containing directive text", async () => {
await expect(
assertNoWorkflowDirectivePrologue({
filePath: "/app/agent/tools/example.ts",
source: [
'// "use step" is documentation, not a directive.',
'const message = "use workflow";',
'"use step";',
"export { message };",
"",
].join("\n"),
}),
).resolves.toBeUndefined();
});
});
35 changes: 35 additions & 0 deletions packages/eve/src/internal/authored-directive-prologue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { parseWithNitroRolldownAst } from "#internal/bundler/nitro-rolldown.js";

const UNSUPPORTED_WORKFLOW_DIRECTIVES = new Set(["use step", "use workflow"]);

interface DirectiveStatement {
readonly directive?: unknown;
}

interface Program {
readonly body?: unknown;
}

export async function assertNoWorkflowDirectivePrologue(input: {
readonly filePath: string;
readonly source: string;
}): Promise<void> {
const program = (await parseWithNitroRolldownAst(input.filePath, input.source)) as Program;

if (!Array.isArray(program.body)) {
throw new Error(`Failed to parse authored module "${input.filePath}".`);
}

for (const statement of program.body as DirectiveStatement[]) {
if (typeof statement.directive !== "string") {
break;
}

if (UNSUPPORTED_WORKFLOW_DIRECTIVES.has(statement.directive)) {
throw new Error(
`Authored module "${input.filePath}" contains an actual "${statement.directive}" directive. ` +
"Workflow directives are reserved for eve-generated workflow entrypoints.",
);
}
}
}
Loading
Loading