Amplifier Core Architecture

The Kernel That Does Less

An architectural deep-dive into the microkernel that runs AI agents
in ~3,400 lines of Python

12
Architecture Files
~3,400
Lines of Kernel
6
Protocols
46
Events
Active February 2026
Anatomy

18 Source Files. Every One With a Purpose.

12 architecture-critical files total ~3,400 lines. The full core including validation and test infrastructure: 18 files, ~5,000 lines. Nothing is dead weight.

FileLinesPurpose
Core Architecture
session.py 474 AmplifierSession — the only public entry point. Config → initialize → execute.
coordinator.py 606 ModuleCoordinator — mount points, capabilities, channels, cancellation.
loader.py 598 ModuleLoader — module discovery, resolution, validation, loading.
hooks.py 347 HookRegistry — event dispatch, handler chains, result merging.
Data Contracts
models.py 429 ToolResult, HookResult, ModelInfo, ProviderInfo, SessionStatus, 15 LLM error classes.
message_models.py 271 ChatRequest, ChatResponse, 7 content block types (text, thinking, tool_call, image…).
interfaces.py 264 6 Protocol classes — the structural typing contracts all modules implement.
events.py 133 46 canonical event name constants. The full event taxonomy.
Supporting Infrastructure
cancellation.py CancellationToken — cooperative cancellation across the agent loop.
approval.py ApprovalRequest/Result — human-in-the-loop approval gates.
display.py DisplaySystem protocol — streaming output abstraction.
validation/ Module contract validation and test harnesses.
Contracts

Six @runtime_checkable Protocols

Structural typing, not inheritance. If your class has the right methods, it's a valid module. No base classes, no registration decorators, no framework lock-in.

Orchestrator
async def execute( prompt, context, providers, tools, hooks ) → str
THE execution loop. Takes everything, returns the final response. This IS the agent.
Provider
async def complete( request: ChatRequest ) → ChatResponse parse_tool_calls() get_info() → ProviderInfo list_models() → list[ModelInfo]
Tool
async def execute( input: dict ) → ToolResult @property name → str @property description → str
ContextManager
add_message(msg) get_messages_for_request() get_messages() / set_messages() clear()
One memory manager at a time. Owns the conversation history.
HookHandler
async def __call__( event: str, data: dict ) → HookResult
5 possible actions: continue, deny, modify, inject_context, ask_user.
ApprovalProvider
async def request_approval( request: ApprovalRequest ) → ApprovalResult
Human-in-the-loop gates. Separate from hooks — approval is a first-class concern.
Coordination

ModuleCoordinator: The Mount Table

The coordinator manages mount points — the slots where modules plug in. Some slots hold a single module, others accumulate. This is the entire runtime topology.

orchestrator Optional[Orchestrator] Single slot. One execution strategy at a time. Required.
providers dict[str, Provider] Dict by name. Multiple LLM backends simultaneously. At least one required.
tools dict[str, Tool] Dict by name. All agent capabilities. Zero or more.
context Optional[ContextManager] Single slot. One memory manager. Required.
hooks HookRegistry Always present. Accumulates handlers. Never empty — the registry itself is always there.
capabilities
Dict for inter-module communication. Modules register capabilities; others can discover them at runtime.
channels
Aggregation channels. Modules can publish/subscribe to named data streams without direct coupling.
cancellation · approval · display
CancellationToken for cooperative abort. ApprovalSystem for human gates. DisplaySystem for streaming output.
Lifecycle

Three Methods. That's the API.

__init__(config)
Takes a raw config dict — the "mount plan". Validates two required keys: session.orchestrator and session.context. Creates coordinator and loader.
# No modules loaded yet. # Just the mount plan.
await initialize()
Loads modules in dependency order:
1. orchestrator # required, fatal 2. context # required, fatal 3. providers # warns on failure 4. tools # warns on failure 5. hooks # warns on failure 6. emit session:fork # if parent_id
await execute(prompt)
Emits session:start or session:resume. Gets all modules from coordinator. Calls orchestrator.execute().
# orchestrator.execute() # IS the agent loop. # Kernel doesn't know what # happens inside.
Events

46 Canonical Events, 14 Categories

Every event is a string constant in events.py. Modules subscribe via hooks. The kernel emits events at lifecycle boundaries — modules decide what to do with them.

Session
start · resume
fork · end
Prompt
submit · complete
Provider
request · response
error · retry · throttle
LLM
request · response
request:raw · response:raw
request:debug · response:debug
Content
block:start · block:delta
block:end
thinking:delta · thinking:final
Tool
pre · post · error
Context
pre_compact · post_compact
compaction · include
Orchestrator
complete
Policy
violation
approval:required
approval:granted
approval:denied
Cancel
requested · completed
Hooks

Five Actions. One Mechanism.

Every hook handler returns a HookResult with one action. The registry chains handlers and merges results with strict precedence. This single primitive enables approval gates, redaction, cost governance, streaming UI, and automated feedback loops.

deny
Block the operation. Short-circuits the handler chain. Nothing else runs.
ask_user
Pause for human approval before proceeding. The approval gate.
inject_context
Insert text into the conversation. Automated feedback loops without tool calls.
modify
Alter event data in-flight. Chains through all handlers. Enables redaction, transformation.
continue
Proceed normally. The default. Most hooks observe and continue.
PRECEDENCE ORDER (highest → lowest)
deny ask_user inject_context modify continue
Loading

How Modules Get Discovered

The loader resolves module IDs to code, validates contracts, and calls mount(). That's the entire loading contract. Six steps from config string to running module.

# Step 1: Resolve if module_source_resolver mounted: path = await resolver.async_resolve(module_id, source_hint) # Usually foundation's activator provides the resolver # Step 2: Discover # Try entry points (amplifier.modules group) # Fall back to direct import (amplifier_module_*) # Step 3: Validate type marker module.__amplifier_module_type__ = "tool" | "provider" | "orchestrator" | "context" | "hook" # Step 4: Validate mount function exists assert hasattr(module, "mount") # async def mount(coordinator, config) → Optional[Callable] # Step 5: Call mount cleanup_fn = await module.mount(coordinator, config) # mount() registers the module on the coordinator # cleanup_fn called on session teardown (optional) # Step 6: Done. # THAT'S the contract.
Exclusion

What's Deliberately Not Here

Every item below was verified absent via grep across the entire amplifier-core source. Each is implemented as a module or foundation feature — plugged in at the edge, not welded into the kernel.

No bundle format — bundle parsing, YAML loading, agent descriptions: that's foundation.
No @mention resolution — file references, context includes: foundation.
No module downloading — fetching, caching, activating modules: foundation's activator.
No spawn/delegation — child agents, session forking logic: foundation's spawn_utils.
No git, filesystem, or web tools — all tool implementations are modules.
No model selection logic — which model to call, fallback strategies: module decisions.
No retry strategy — backoff, jitter, circuit breaking: all in provider modules.
No cost management — token budgets, spend tracking: hook modules.
No streaming UI — terminal rendering, progress bars: display modules.
No logging implementation — log formats, destinations, levels: hook modules.
No redaction — PII filtering, content masking: hook modules via modify action.
The kernel provides the slots. Everything else plugs in.
Philosophy

The QNX Principle

QNX runs nuclear power plants with a 100KB kernel.
Amplifier runs AI agents with ~3,400 lines.

Both achieve reliability through radical exclusion. The things you leave out of the kernel matter more than what you put in.

The Two-Implementation Rule
Nothing gets promoted to the kernel until two independent modules converge on the need. One implementation is a feature. Two implementations with the same pattern is a kernel candidate. This prevents premature abstraction — the most expensive kind of technical debt.
139
Commits
5
Contributors
~140
Days of History
Sources

Research Methodology

Data as of: February 26, 2026

Feature status: Active

Research performed:

Gaps: Individual line counts for supporting files (cancellation.py, approval.py, display.py) not enumerated — total validated via aggregate count. Per-file numbers for 8 primary architecture files sourced from project documentation.

Repository: amplifier-core (Microsoft Amplifier ecosystem)

The Discipline

The kernel does five things: load modules, manage sessions, dispatch events, coordinate protocols, and fork child sessions.

Everything else — every provider, every tool, every orchestration strategy, every logging decision, every safety policy — is a loadable module at the edge.

That's the architecture.
That's the discipline.

amplifier-core · microsoft/amplifier-core
More Amplifier Stories