Compiler architecture
agent-bundle compiles one project into native host plugins. This page describes the compiler as the code works today: three layers of data that later stages read, the modules that produce and consume each layer, and the artifact manifest those stages leave behind. It is the documentation slice of #592: the three layers from step 2, and the v2 authoritative manifest from step 3 (PR #604).
Paths below are relative to packages/agent-bundle/src/ unless a package
name is spelled out. Type names are the ones the compiler exports. There is
no ApplicationIR, ProjectionIR, or ArtifactIR type yet.
The output layout this page describes is the composite plugin root: one
directory at the artifact output, with targets selecting which host
projections appear inside it
(#555 Wave 1,
PR #578). There
is no artifact/<host>/ partition and no plugin target.
The three layers
A reader who wants the filesystem conventions those arrows start from should read Project structure. A reader who wants the bytes those arrows end in should read Targets and artifacts.
Application layer
Discovery runs once per build, then normalization freezes a host-independent model. Two structures share that job today; they are not yet one IR.
What discovery produces
config/discover.ts walks the project and returns a DiscoveredProject:
compileRouteGraph in routes/graph.ts is the executable-route compiler. It
globs these roots (and no others):
The frozen result is a CompiledRouteGraph (routes/types.ts):
servers[]:CompiledServerSurface(generated/custom/command/remote/conflict), each holdingCompiledAgentRouterows of kindtool,resource,prompt, orapp.cli?:CompiledCliSurface(generated/conventional/conflict) withCompiledCliCommandrows whose argv grammar comes from literalconfig.inputJsonSchemaor JSON input.events[]:CompiledAgentRouterows of kindevent-route.scripts[]: kindscript.providers[]:CompiledProvider(not a route kind; they wrap execution).layouts?:CompiledLayout(rootorserverscope). Event routes and browser Apps are never wrapped.contracts?: route-localconfig.inputJsonSchemametadata, identified bycontract:<module>#inputJsonSchema.digest: sha256 over project-relative identity and declared metadata.diagnostics: route-graph errors (AB48xxandAB494x).
Each CompiledAgentRoute carries id, kind, source, conventional
provenance, a statically extracted config (or emptyRouteConfig), an
optional bounded inputSchema, optional contract (the shared
RouteContract id), and, for MCP routes, serverId. Event routes also
carry event (CanonicalAgentEvent from routes/public.ts).
Skills, commands, rules, hooks declared in config, handwritten MCP entries,
and native host hook documents are not in this graph. They stay on
DiscoveredProject / NormalizedPlugin beside it.
What normalization hands forward
config/normalize.ts consumes DiscoveredProject plus the loaded config and
emits a NormalizedPlugin (core/types.ts). That is the object every later
planner reads. It does not retain the CompiledRouteGraph;
PreparedProject (dev/project-service.ts) carries model and
routeGraph as sibling fields. The plugin holds:
- identity (
metadata) and the selectedtargets(NormalizedTarget[], sorted unique names); skills,commands,rules,hooks,nativeHooks,mcpServers,mcpApps,scripts,assets,payloads;providersandlayoutscopied off the route graph;runtime.node(the generated-executable floor);- optional
state,notices,packageBuild,marketplace, host extensions.
Config-declared hooks and src/events/** routes both land in
NormalizedPlugin.hooks. The event-route branch sets eventRoute and an
id such as hook:event-route:tool-before (/ in the canonical event
replaced by -); the config branch does not.
Native host hook documents (Claude hooks/hooks.json sources, and so on)
are a third collection, nativeHooks.
config/validate.ts runs against the same discovered project. It does not
rediscover files; it judges what discovery already found.
What the next layer receives
Adapters and build/compose.ts receive the whole NormalizedPlugin. They
do not call compileRouteGraph again. Shared compile stages
(build/entries.ts, build/cli-bins.ts, build/mcp-apps.ts) also read
that model: generated MCP entries, routed CLI bins, scripts, and Apps are
planned from it once, then attributed to the composite identity.
Projection layer
A projection is one host's representation of the same
NormalizedPlugin. The config key is still targets. After the composite
root landed, that key selects which projections are planned into the one
output directory. It does not name the artifact, the runtime, the
distribution form, or the application.
Where targets enter
agent-bundle.config.tstargets, or CLI--target. Omit both andadapters/registry.tsadmits only the default host,portable.config/normalize.tsstores the selected names onNormalizedPlugin.targets.build/compose.tsplanCompositecallssortedProjections(model.targets.map((t) => t.name))and runsregistry.get(name).plan(model)only for those names.pluginis not a target.targets: ['plugin']and--target pluginareAB4100.inspect --targetis a display filter onapi.tsinspect(): it keeps only that name in the printedplans(unknown name isAB7004). It does not changeselectedTargetsor the composite identity.inspect --bundlerstill composes the full selection.
adapters/composite-layout.ts owns the selection helpers:
sortedProjections, projectionIdentity (sorted names joined by +, for
example claude+codex), and hookWrapperPath.
Declaration-level targets on a command, rule, hook, or script still
mean "this component reaches these hosts." A component is included when that
set intersects the selection. A command or rule scoped to a subset of the
selected hosts but sitting in a directory another selected host discovers
conventionally is AB4105. Skills always ship to every selected host
(skill.targets is the selection); a per-host Skill frontmatter extension
that changes bytes is AB4103.
Who plans a projection
Each built-in adapter implements TargetAdapter.plan(model) and returns a
TargetArtifactPlan (adapters/types.ts): entries (write or copy),
optional hookEntries, and diagnostics.
The capability JSON files are the host evidence the registry and the
generated Hosts page read: observed versions,
manifest paths, install surfaces, path tokens, MCP transports, and
component support. Adapters consult them for what a host can express;
compose.ts consults registry.hostsComponent and
registry.artifactLayout when judging AB4105, and
registry.noticeDelivery when intersecting notice-delivery advertisements
across the selection. The shared MCP entries and every host's hook wrappers
are wired from that intersection so they advertise the same routes.
TargetRegistry (adapters/registry.ts) is the only place adapters are
registered. createDefaultRegistry registers portable as the default,
then codex, claude, cursor, and amp. Registration order is not
target precedence: planComposite sorts the selection
(sortedProjections) before planning it, and the composite identity is the
sorted name join. There is no plugin adapter.
amp is the one built-in host whose plugin is a generated directory rather
than a set of declarative documents. Its plan writes an index.js PluginAPI
factory under .amp/plugins/{plugin}/ that registers Skills and event
callbacks through the API, and its MCP configuration is skill-scoped: a
canonical server needs exactly one Amp Skill, and a compiler-owned local
server is refused because the pinned contract documents no plugin-root
placeholder to run it from. The per-surface evidence stays on the generated
Hosts page; the author-facing selection map is
Targets and capability map.
What compose hands to artifact assembly
composeProjections (build/compose.ts) merges the selected plans:
- Byte-identical entries at one path are kept once. Different bytes at one
path are
AB4103(projections compared in host-name order, paths in path order). install/surface.tsinstallSurfaceEntriesis merged as a synthetic owner (INSTALL.mdfor any built-in host;install.mjswhencursororportableis selected).- Hook entries are concatenated; they are not merged by path. A hook that
reaches more than one selected host compiles one wrapper per host
(
hooks/<name>.<host>.mjs); a single-host hook keepshooks/<name>.mjs(hookWrapperPath). - Codex and Cursor hook/MCP documents live beside their manifests
(
.codex-plugin/hooks.json,.cursor-plugin/mcp.json, …). When another selected host has a Hook or MCP server that reaches the conventionalhooks/hooks.json/.mcp.json/mcp.jsonpath, those projections emit an empty shield document so folder discovery cannot load that file.
The CompositePlan the build stages carries entries, hookEntries,
selected, identity, projections (one planned host each), cliBin
(true when any selected host admits the routed CLI), and
noticeDelivery. AB4106 is judged earlier, on the model, in
config/validate.ts: a selection that mixes an advanced-registry adapter
with any other target cannot share one root.
Compiled MCP entries, scripts, routed CLI bins, and MCP Apps are not
re-planned per host. build/build.ts compiles them once with
target: composite.identity and writes them at the root.
Artifact layer
Artifact assembly consumes the CompositePlan plus the compiled surfaces
and writes one tree at artifactRoot (CLI default artifact/;
output.distPath or --output moves it).
Writers
build/compile-stages.ts orders the work: MCP Apps first (the MCP entries
embed their HTML), then the node surfaces.
Validators
AB6005 reads compile evidence for every host-pack, package-build dist, and
MCP App view compilation. Each ExternalModule that Rspack kept must be a
Node built-in, pnpapi, or an emitted sibling; any other external fails.
That is the self-containment rule enforced by build/compiler.ts and
build/external-policy.ts, alongside the generated-plugin compiler profile
(build/rslib.ts composeEntryLibConfig: autoExternal: false,
bundle: true, no author-facing externals).
AB7014 / AB7015 are the prepack gate: a package.json install-time
dependency must be justified by prebuilt runtimeDependencies, a packed
declaration reference, or a consumer install script (AB7014), and a consumer's npm
must be able to fetch it (AB7015). A package inlined into a bundle is not
used; compile evidence records the dist bundles named in the diagnostic.
Composite root
Built with targets: ['amp', 'claude', 'codex', 'cursor', 'portable'], the
host-test example's root is:
Targets and artifacts is the
output contract: which host reads which document, the hook-wrapper naming
rule, and AB4103 / AB4105 / AB4106. This page does not repeat that
table.
Execution kernel and the Agent Document
The compiler pipeline above stops at files on disk. Request time is a
separate stack. The request kernel that does exist is
runAgentRequest (packages/rsc-runtime/src/agent-request.ts, imported as
@agent-bundle/runtime). Generated MCP routes, event routes, and rendered
CLI commands call it. Config-declared hooks bypass it: generated
wrappers decode the host envelope and invoke the author's function
directly, with no providers, layout, or Agent Document. Native hook
documents are host JSON that adapters validate and merge; they are not
compiled wrappers and do not enter the kernel.
mcp-entry.ts is the stdio process shell. It does not construct request
context. mcp-server-runtime.ts does: identity, lineage, providers,
runAgentRequest, layout (for document routes), and the MCP projection.
The Agent Document is the canonical rendered-route response. The type
lives in packages/rsc-runtime/src/agent-document.ts and is imported as
@agent-bundle/runtime:
version(currently1)status:success|represented-error|failedroot: a tree ofAgent.*nodes (result,text,markdown,context,json,progress,image,audio,resource,error)- optional
value(JSON-serializable structured result)
MCP tool routes lower that document to CallToolResult content plus
structuredContent. Resource and prompt routes return their validated
protocol result from document.value. CLI human/JSON modes render the same
document through the CLI output contract. The Workbench inspects it via
packages/workbench/src/runtime/agent-document-client.ts. Raw
CallToolResult is an interop boundary, not the authoring model.
Context values are Observed<T> (@agent-bundle/runtime): state is
available (with source native | receipt | derived) or
unavailable (with a reason). The kernel does not invent a host or
session identity the host did not supply.
agent-bundle.manifest.json today
The writer is build/build.ts manifestFor. It assembles an
ArtifactManifest (build/manifest.ts; TypeDoc:
ArtifactManifest)
from manifestProjections, manifestBins / manifestHooks /
manifestMcpServers / manifestScripts, manifestDistribution, and
build/manifest-routes.ts artifactRoutesFor. writeManifest
serializes it with assembleArtifactManifest: canonical stableJson
plus a trailing newline. parseArtifactManifest rejects any other byte
sequence, duplicate keys, unknown keys, missing keys, unsorted arrays,
and a file entry that names the manifest itself. manifestVersion is
5. The public reader is build/manifest-file.ts
readArtifactManifest: ok, missing (ENOENT only), or invalid.
The schema is closed in TypeScript (requireExactKeys in
build/manifest.ts) and shipped as JSON Schema
(packages/agent-bundle/schemas/agent-bundle.manifest.schema.json),
exported as artifactManifestSchema and
validateArtifactManifestSchema from build/manifest-schema.ts via
api.ts / index.ts. The parser remains the stricter authority: it
also checks cross-references, canonical bytes, and sort order. Host
plugin.json / hooks.json / mcp.json schemas under
adapters/schemas/** and Agent Skills frontmatter under schemas/**
are a different contract; the website
Hosts /
Events pages are generated from
adapters/capabilities/*.json and docs/diagnostics.md, not from this
manifest.
Every section and field is on Artifact manifest. This page keeps only the outline.
Sections the writer emits
The document has two parts with two version numbers. The public
contract (manifestVersion: 6) is what consumers read. The
compiler record (compiler, recordVersion: 1) is the operational
account of the run, producer, project digests, provenance, adapter
revisions, pinned schemas, validation, and changes without bumping
manifestVersion; consumer readers never need it.
Who reads each field
Production readers call readArtifactManifest or
parseArtifactManifest. Tests are omitted except the installed-host
harness.
There is no portable install / doctor host
(BundleIdentityHost is amp | claude | codex | cursor).
doctor covers claude, codex, and cursor; Amp has no lifecycle
probe (DoctorHost excludes it).
What step 3 landed
#592 step 3
made this file the compiled application index (manifestVersion 2).
These numbered items from the former gap list now live on the manifest,
except (5):
- Route graph.
routesrecords servers, tools, resources, prompts, Apps, CLI commands, event routes, providers, layouts, contracts, anddigest. - MCP / CLI / App inventory.
executables.mcpServers[]records each server (entry,apps[]);executables.bins[]records the routed CLI. - Hook semantics.
executables.hooks[].kindisconfigorevent-route. Native commands an author writes into a host hooks document are not rows.agent-bundle.hooks.jsonis gone. - Install / doctor identity.
readBundleIdentityreadsapplicationand the projection whosebuiltInHostmatches the host. - Capability view. Deliberately not recorded. Notice-delivery
intersection is derived at build time in
build/compose.tscompositeNoticeDelivery;inspectomittedFeaturesstill comes from the model. Recording that intersection is later #592 work. - Executables as first-class rows.
executables.bins[],hooks[],mcpServers[], andscripts[]. - Distribution metadata.
distribution.channelsand optionaldistribution.install.
The field contract is Artifact manifest.
Where the code diverges today
#592 names the splits this page must not paper over. Paths only; no fixes.
Discovery is not one graph. compileRouteGraph covers executable
routes. Skills, commands, rules, config hooks, native hooks, and
handwritten MCP entries are parallel collections on DiscoveredProject /
NormalizedPlugin. A later consumer that needs "every surface" still walks
several fields.
Five planners, one merge. planClaudeArtifacts, planCodexArtifacts,
planCursorArtifacts, portableAdapter.plan, and ampAdapter.plan each
walk the whole model and emit host documents. compose.ts merges bytes; it does not
replace those planners with a single project(application, capabilities)
function.
Event execution. Conventional src/events/** routes support plain .ts handlers,
rendered .tsx handlers, and explicit lazy .view.tsx modules. Host-native documents remain
adapter inputs. Providers resolve lazily through the request context.
MCP and CLI. A colocated <tool>.cli.ts projects the tool without a second operation.
Flag metadata is literal config.inputJsonSchema; runtime schemas are loaded by compiled
executables. Inspection does not interpret schema imports or alias chains.
Browser bridge was forked. One host-side relay now lives in
web-host/browser/frame-relay.ts (McpAppFrameRelay) and is mounted by the
Workbench, serve-app, dev /web/<server>/<app>, and the artifact's
<plugin> web page
(#564 landed);
Apps use the typed client from agent-bundle/app
(#594 landed).
Filesystem probing next to the manifest. install, uninstall, and
doctor read identity through install/identity.ts readBundleIdentity
(application, projections[].builtInHost, documents.plugin,
marketplace name). They do not probe <from>/<host> or a host
plugin.json to find the root. Eval still hashes the whole file table
once per projections[].host (evalTargetDigests) rather than recording
a per-projection file view. The Workbench inspector rebuilds one tree
from files[] and labels it once per projections[] row, reading
executables from executables rather than path prefixes.
Per-target leftovers in shared surfaces. Shared MCP / CLI / App
compile uses composite.identity, but hook wrappers still bake one
target. dev/artifacts/artifact-executables.ts expands
executables.scripts[] into one execution row per host that lists the
script. artifact-inspection-service.ts labels the same tree once per
projections[] host. Notice delivery is the intersection of every
selected host (compose.ts compositeNoticeDelivery), applied to every
shared MCP entry, and is not recorded on the manifest.
Capability selection is narrow. Event routes can declare capability rows such as
requires: ['events.toolBefore.deny']; the compiler selects supporting hosts from the pinned
capability tables. Other components still use targets, which remains the event-route escape
hatch when behavior is genuinely host-specific.
Roadmap
#592 is the umbrella. This page documents the three layers (step 2) and the v2 manifest (step 3). The implementation slices under it, each with its own issue:
Step 3 landed the authoritative agent-bundle.manifest.json as version 2;
subsequent executable, distribution, and route-contract additions advanced
the current contract to manifestVersion 6. The field contract is
Artifact manifest.
#592 stays open until the remaining acceptance criteria on that issue
land.
Next
- Project structure: the
src/roots discovery walks. - Configuration model: the config those roots fill in.
- MCP servers and Apps and Hooks, the two executable surfaces most authors meet first.
- Artifact manifest: every
section and field of
agent-bundle.manifest.json. - Targets and artifacts: the composite root.
- Shipping a bundle: how the root compiles and what ships inside it.
- Artifact validation: the checks that re-read this manifest.