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-source-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Add immutable programmatic agent sources, explicit source registries, and deterministic slot composition. Programmatic modules now share the ordinary definition materialization path and generated module maps can bind them through static registry imports without virtual files.
16 changes: 16 additions & 0 deletions packages/eve/src/compiler/agent-module-candidate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { AgentSourceOwner, CompiledModuleBacking } from "#compiler/module-binding.js";

export type AgentSourceLayer =
| "framework-default"
| "extension-package"
| "extension-override"
| "application";

export interface AgentModuleCandidate {
readonly backing: CompiledModuleBacking;
readonly layer: AgentSourceLayer;
readonly logicalPath: string;
readonly nodeId: string;
readonly owner: AgentSourceOwner;
readonly sourceId: string;
}
68 changes: 68 additions & 0 deletions packages/eve/src/compiler/agent-source-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { CompiledModuleBacking } from "#compiler/module-binding.js";
import type {
ProgrammaticAgentModule,
ProgrammaticAgentSource,
ProgrammaticModuleNamespace,
} from "#compiler/programmatic-agent-source.js";

export interface AgentSourceRegistration {
readonly applyTo: "root" | "all-local-nodes";
readonly source: ProgrammaticAgentSource;
}

export interface AgentSourceRegistry {
readonly registrations: readonly AgentSourceRegistration[];
getModule(
backing: Extract<CompiledModuleBacking, { kind: "programmatic" }>,
): ProgrammaticAgentModule;
}

export function createAgentSourceRegistry(
registrations: readonly AgentSourceRegistration[],
): AgentSourceRegistry {
const sources = new Map<string, ReadonlyMap<string, ProgrammaticAgentModule>>();
const frozenRegistrations = registrations.map((registration) => {
if (sources.has(registration.source.id)) {
throw new Error(
`Programmatic agent source id "${registration.source.id}" is registered twice.`,
);
}
if (registration.applyTo === "all-local-nodes") {
const recursiveModule = registration.source.modules.find((module) =>
/^(?:agent\.[^/]+|channels\/|extensions\/|schedules\/|subagents\/)/.test(
module.logicalPath,
),
);
if (recursiveModule !== undefined) {
throw new Error(
`Programmatic source "${registration.source.id}" cannot apply "${recursiveModule.logicalPath}" to all local nodes because that slot can expand the graph or host surface.`,
);
}
}
const modules = new Map(
registration.source.modules.map((module) => [module.logicalPath, module] as const),
);
sources.set(registration.source.id, modules);
return Object.freeze({ applyTo: registration.applyTo, source: registration.source });
});

return Object.freeze({
registrations: Object.freeze(frozenRegistrations),
getModule(backing: Extract<CompiledModuleBacking, { kind: "programmatic" }>) {
const module = sources.get(backing.registryId)?.get(backing.moduleId);
if (module === undefined) {
throw new Error(
`Programmatic module binding "${backing.registryId}:${backing.moduleId}" is not registered.`,
);
}
return module;
},
});
}

export function getProgrammaticModuleNamespace(
registry: AgentSourceRegistry,
backing: Extract<CompiledModuleBacking, { kind: "programmatic" }>,
): ProgrammaticModuleNamespace {
return registry.getModule(backing).namespace;
}
64 changes: 64 additions & 0 deletions packages/eve/src/compiler/compose-agent-module-candidates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";

import type { AgentModuleCandidate } from "#compiler/agent-module-candidate.js";
import {
canonicalModuleSlot,
composeAgentModuleCandidates,
} from "#compiler/compose-agent-module-candidates.js";

function candidate(
layer: AgentModuleCandidate["layer"],
logicalPath: string,
): AgentModuleCandidate {
return {
backing: {
externalDependencies: [],
kind: "filesystem",
sourcePath: `/physical/${layer}/${logicalPath}`,
},
layer,
logicalPath,
nodeId: "__root__",
owner: { kind: "application" },
sourceId: `${layer}:${logicalPath}`,
};
}

describe("composeAgentModuleCandidates", () => {
it("selects one winner by the global layer order", () => {
const candidates = [
candidate("application", "tools/search.js"),
candidate("framework-default", "tools/search.ts"),
candidate("extension-override", "tools/search.mts"),
candidate("extension-package", "tools/search.cjs"),
];
const result = composeAgentModuleCandidates(candidates);

expect(result.winners).toEqual([candidates[0]]);
expect(result.entries[0]?.candidates.map((entry) => entry.layer)).toEqual([
"framework-default",
"extension-package",
"extension-override",
"application",
]);
});

it("rejects same-layer aliases before loading either candidate", () => {
expect(() =>
composeAgentModuleCandidates([
candidate("application", "connections/linear.ts"),
candidate("application", "connections/linear/connection.js"),
]),
).toThrow("duplicate application candidates");
});
});

describe("canonicalModuleSlot", () => {
it.each([
["tools/read.ts", "tools/read"],
["connections/linear/connection.mjs", "connections/linear"],
["sandbox/sandbox.ts", "sandbox"],
])("maps %s to %s", (logicalPath, expected) => {
expect(canonicalModuleSlot(logicalPath)).toBe(expected);
});
});
64 changes: 64 additions & 0 deletions packages/eve/src/compiler/compose-agent-module-candidates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { stripLogicalPathExtension } from "#discover/filesystem.js";
import type { AgentModuleCandidate, AgentSourceLayer } from "#compiler/agent-module-candidate.js";

const PRECEDENCE: Readonly<Record<AgentSourceLayer, number>> = {
"framework-default": 0,
"extension-package": 1,
"extension-override": 2,
application: 3,
};

export interface AgentModuleCompositionEntry {
readonly candidates: readonly AgentModuleCandidate[];
readonly slot: string;
readonly winner: AgentModuleCandidate;
}

export interface AgentModuleComposition {
readonly entries: readonly AgentModuleCompositionEntry[];
readonly winners: readonly AgentModuleCandidate[];
}

export function composeAgentModuleCandidates(
candidates: readonly AgentModuleCandidate[],
): AgentModuleComposition {
const candidatesBySlot = new Map<string, AgentModuleCandidate[]>();

for (const candidate of candidates) {
const slot = canonicalModuleSlot(candidate.logicalPath);
const entries = candidatesBySlot.get(slot) ?? [];
const sameLayer = entries.find((entry) => entry.layer === candidate.layer);
if (sameLayer !== undefined) {
throw new Error(
`Agent node "${candidate.nodeId}" has duplicate ${candidate.layer} candidates for "${slot}": "${sameLayer.logicalPath}" and "${candidate.logicalPath}".`,
);
}
entries.push(candidate);
candidatesBySlot.set(slot, entries);
}

const entries = [...candidatesBySlot]
.sort(([left], [right]) => left.localeCompare(right))
.map(([slot, slotCandidates]) => {
const ordered = [...slotCandidates].sort(
(left, right) => PRECEDENCE[left.layer] - PRECEDENCE[right.layer],
);
return Object.freeze({
candidates: Object.freeze(ordered),
slot,
winner: ordered.at(-1)!,
});
});

return Object.freeze({
entries: Object.freeze(entries),
winners: Object.freeze(entries.map((entry) => entry.winner)),
});
}

export function canonicalModuleSlot(logicalPath: string): string {
const withoutExtension = stripLogicalPathExtension(logicalPath);
const connectionFolder = withoutExtension.match(/^connections\/([^/]+)\/connection$/);
if (connectionFolder !== null) return `connections/${connectionFolder[1]}`;
return withoutExtension === "sandbox/sandbox" ? "sandbox" : withoutExtension;
}
35 changes: 35 additions & 0 deletions packages/eve/src/compiler/module-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,41 @@ function createManifestWithTool(agentRoot: string): CompiledAgentManifest {
}

describe("createCompiledModuleMapSource", () => {
it("emits a static registry lookup for programmatic modules", () => {
const manifest = createManifestWithTool("/consumer/agent");
const source = createCompiledModuleMapSource({
manifest: {
...manifest,
bindings: {
"tools/echo.ts": {
backing: {
kind: "programmatic",
moduleId: "tools/echo.ts",
registryId: "eve.defaults",
},
logicalPath: "tools/echo.ts",
owner: { feature: "defaults", kind: "framework" },
},
},
},
moduleMapPath: "/consumer/.eve/compile/module-map.mjs",
programmaticRegistryImports: {
"eve.defaults": {
exportName: "defaultAgentSourceRegistry",
importSpecifier: "eve/internal/default-agent-source-registry",
},
},
});

expect(source).toContain(
'import { defaultAgentSourceRegistry as module_0 } from "eve/internal/default-agent-source-registry";',
);
expect(source).toContain(
'module_0.getModule({"kind":"programmatic","moduleId":"tools/echo.ts","registryId":"eve.defaults"}).namespace',
);
expect(source).not.toContain("/consumer/agent/tools/echo.ts");
});

it("imports the physical binding instead of reconstructing it from logical identity", () => {
const manifest = createManifestWithTool("/consumer/agent");
const source = createCompiledModuleMapSource({
Expand Down
Loading
Loading