Purpose: This document is the authoritative Rust↔Python type mapping for coding agents working on either side of the boundary. Read this before modifying any shared type, trait/protocol, or error.
| Concept | Rust | Python |
|---|---|---|
| Data model | struct Foo with #[derive(Serialize, Deserialize)] |
class Foo(BaseModel) (Pydantic v2) |
| Interface | trait Bar (async, dyn-safe) |
class Bar(Protocol) (structural typing) |
| Enum (string) | enum Baz { Variant } with #[serde(rename_all = "snake_case")] |
Literal["variant"] |
| Tagged union | enum E { A { .. }, B { .. } } with #[serde(tag = "type")] |
Discriminated Union[A, B] |
| Error | Result<T, E> with thiserror |
T (raises exception) |
| Optional | Option<T> |
T | None |
| List | Vec<T> |
list[T] |
| Map | HashMap<K, V> |
dict[K, V] |
| JSON blob | serde_json::Value |
dict[str, Any] |
Serialization boundary: All data crosses the PyO3 bridge as JSON (via
serde_json::to_string → json.loads and vice versa). Field names must be
identical on both sides. Rust uses #[serde(rename = "...")] where the Rust
field name differs from the JSON key.
| Rust Trait | Location (Rust) | Python Protocol | Location (Python) | Notes |
|---|---|---|---|---|
Tool |
crates/amplifier-core/src/traits.rs |
Tool |
interfaces.py |
Rust execute takes Value; Python takes dict[str, Any]. Rust adds get_spec() -> ToolSpec. |
Provider |
crates/amplifier-core/src/traits.rs |
Provider |
interfaces.py |
1:1 — name, get_info, list_models, complete, parse_tool_calls. |
Orchestrator |
crates/amplifier-core/src/traits.rs |
Orchestrator |
interfaces.py |
Rust passes hooks/coordinator as Value; Python passes typed objects + **kwargs. |
ContextManager |
crates/amplifier-core/src/traits.rs |
ContextManager |
interfaces.py |
1:1 — add_message, get_messages_for_request, get_messages, set_messages, clear. |
HookHandler |
crates/amplifier-core/src/traits.rs |
HookHandler |
interfaces.py |
Rust: handle(event, data); Python: __call__(event, data). |
ApprovalProvider |
crates/amplifier-core/src/traits.rs |
ApprovalProvider |
interfaces.py |
1:1 — request_approval(ApprovalRequest) -> ApprovalResponse. |
| Rust Struct/Enum | Python Class | Serialization | Notes |
|---|---|---|---|
HookResult |
HookResult (BaseModel) |
JSON round-trip at PyO3 boundary | Field-for-field match. Rust HookAction enum ↔ Python Literal strings. |
HookAction |
Literal["continue","deny","modify","inject_context","ask_user"] |
serde(rename_all = "snake_case") |
Enum variants map 1:1 to string literals. |
ToolResult |
ToolResult (BaseModel) |
JSON round-trip | 1:1. Python adds __str__() and get_serialized_output() convenience methods. |
ModelInfo |
ModelInfo (BaseModel) |
JSON round-trip | 1:1. |
ConfigField |
ConfigField (BaseModel) |
JSON round-trip | Rust default_value ↔ Python default. |
ConfigFieldType |
Literal["text","secret","choice","boolean"] |
snake_case | |
ProviderInfo |
ProviderInfo (BaseModel) |
JSON round-trip | 1:1. |
ModuleInfo |
ModuleInfo (BaseModel) |
JSON round-trip | Rust module_type serializes as JSON key "type". |
ModuleType |
Literal["orchestrator","provider","tool","context","hook","resolver"] |
snake_case | |
SessionStatus |
SessionStatus (BaseModel) |
JSON round-trip | 1:1. Rust started_at is String; Python is datetime. |
SessionState |
Literal["running","completed","failed","cancelled"] |
snake_case | |
ContextInjectionRole |
Literal["system","user","assistant"] |
snake_case | |
ApprovalDefault |
Literal["allow","deny"] |
snake_case | |
UserMessageLevel |
Literal["info","warning","error"] |
snake_case | |
ApprovalRequest |
ApprovalRequest (BaseModel) |
JSON round-trip | 1:1. Python has model_post_init validation. |
ApprovalResponse |
ApprovalResponse (BaseModel) |
JSON round-trip | 1:1. |
| Rust Type | Python Type | Serialization | Notes |
|---|---|---|---|
Message |
Message (BaseModel) |
JSON round-trip | 1:1 fields. |
ContentBlock (tagged enum) |
ContentBlockUnion (discriminated Union) |
serde(tag = "type") |
Rust variants = Python separate BaseModel classes. |
ToolSpec |
ToolSpec (BaseModel) |
JSON round-trip | 1:1. |
ChatRequest |
ChatRequest (BaseModel) |
JSON round-trip | 1:1. |
ToolCall |
ToolCall (BaseModel) |
JSON round-trip | 1:1. |
Usage |
Usage (BaseModel) |
JSON round-trip | 1:1. |
Degradation |
Degradation (BaseModel) |
JSON round-trip | 1:1. |
ChatResponse |
ChatResponse (BaseModel) |
JSON round-trip | 1:1. |
ResponseFormat (tagged enum) |
ResponseFormat (Union) |
serde(tag = "type") |
Text/Json/JsonSchema variants match. |
Role (enum) |
Literal["system","developer","user","assistant","function","tool"] |
snake_case | |
Visibility (enum) |
Literal["internal","developer","user"] |
snake_case |
Python uses separate classes for each content block type. Rust uses variants of the ContentBlock enum.
| Rust Variant | Python Class | Location (Python) |
|---|---|---|
ContentBlock::Text |
TextBlock |
message_models.py |
ContentBlock::Thinking |
ThinkingBlock |
message_models.py |
ContentBlock::RedactedThinking |
RedactedThinkingBlock |
message_models.py |
ContentBlock::ToolCall |
ToolCallBlock |
message_models.py |
ContentBlock::ToolResult |
ToolResultBlock |
message_models.py |
ContentBlock::Image |
ImageBlock |
message_models.py |
ContentBlock::Reasoning |
ReasoningBlock |
message_models.py |
| Rust Equivalent | Python Class | Notes |
|---|---|---|
ContentBlock::Text variant |
TextContent (dataclass) |
|
ContentBlock::Thinking variant |
ThinkingContent (dataclass) |
|
ContentBlock::ToolCall variant |
ToolCallContent (dataclass) |
|
ContentBlock::ToolResult variant |
ToolResultContent (dataclass) |
|
ContentBlockType enum |
ContentBlockType (str Enum) |
Text/Thinking/ToolCall/ToolResult |
These are the core engine types that the Rust kernel implements and the PyO3 bridge exposes.
| Rust Type | PyO3 Wrapper | Python Name | Python Original | Notes |
|---|---|---|---|---|
Session |
RustSession |
AmplifierSession |
session.py:AmplifierSession |
Rust is the default export. Rust is leaner: no ModuleLoader, no auto-load in initialize(). |
Coordinator |
RustCoordinator |
ModuleCoordinator |
coordinator.py:ModuleCoordinator |
Rust is the default export. Rust has core mount/get/hooks/cancel. Python adds process_hook_result, session back-refs, budget limits. |
HookRegistry |
RustHookRegistry |
HookRegistry |
hooks.py:HookRegistry |
Rust is the default export. 1:1 core API: register, emit, unregister, list_handlers. |
CancellationToken |
RustCancellationToken |
CancellationToken |
cancellation.py:CancellationToken |
Rust is the default export. 1:1: state, is_cancelled, request_graceful, request_immediate, reset. |
CancellationState |
(stays Python) | CancellationState |
cancellation.py:CancellationState |
Simple enum — no Rust bridge needed. |
SessionConfig |
(internal) | (dict) | (inline in __init__) |
Rust-specific typed config. Python uses raw dict. |
The switchover from Python to Rust implementations is complete. Rust types are now the default exports for top-level imports (
from amplifier_core import ...). Python implementations remain accessible via submodule imports for backward compatibility. TheRust*prefixed names (RustSession,RustHookRegistry, etc.) are still available as explicit aliases.
| Rust Variant | Python Exception | Notes |
|---|---|---|
ProviderError::RateLimit |
RateLimitError |
Rust has retry_after: Option<f64> field. |
ProviderError::Authentication |
AuthenticationError |
|
ProviderError::ContextLength |
ContextLengthError |
|
ProviderError::ContentFilter |
ContentFilterError |
|
ProviderError::InvalidRequest |
InvalidRequestError |
|
ProviderError::Unavailable |
ProviderUnavailableError |
|
ProviderError::Timeout |
LLMTimeoutError |
|
ProviderError::Other |
LLMError (base class) |
Catch-all. |
| Rust Variant | Python Equivalent | Notes |
|---|---|---|
SessionError::NotInitialized |
RuntimeError("No orchestrator...") |
Python raises generic RuntimeError. |
SessionError::ConfigMissing |
ValueError("Configuration must specify...") |
|
SessionError::AlreadyCompleted |
(no equivalent) | Rust-only guard. |
SessionError::Other |
(various RuntimeErrors) |
| Rust Variant | Python Equivalent | Notes |
|---|---|---|
HookError::HandlerFailed |
(logged, not raised) | Python catches silently. |
HookError::Timeout |
TimeoutError (via asyncio.wait_for) |
|
HookError::Other |
(generic Exception) |
| Rust Type | Notes |
|---|---|
AmplifierError |
Top-level wrapper enum. Python has no unified equivalent. |
ToolError |
Python represents tool errors as ToolResult(success=False, error={...}). |
ContextError |
Python context managers raise generic exceptions. |
These types stay as Python by design — they are app-layer concerns, not kernel.
| Python Type | Location | Why Not Ported |
|---|---|---|
ModuleLoader |
loader.py |
Module loading/discovery is app-layer. |
ModuleValidationError |
loader.py |
Validation framework stays Python. |
ApprovalSystem |
approval.py |
App-layer approval policy. |
DisplaySystem |
display.py |
App-layer UX. |
validation/ package |
validation/ |
Structural + behavioral test framework stays Python. |
testing module |
testing.py |
Test utilities (MockTool, TestCoordinator, etc.) stay Python. |
pytest_plugin |
pytest_plugin.py |
Pytest integration stays Python. |
cli |
cli.py |
CLI entry point stays Python. |
Modules may expose lifecycle methods that the kernel calls at specific points during initialisation.
from collections.abc import Awaitable, Callable
from typing import Any
async def mount(coordinator, config: dict[str, Any] | None = None) -> Callable[[], None | Awaitable[None]] | None:
...Module-level free function — no self. Called once per module, in phase order,
when the kernel loads and wires the module into the coordinator. At call time the
coordinator is partially composed — modules from earlier phases are accessible,
but later-phase modules may not yet be present.
Return value — optional cleanup callable:
mount() may return a zero-argument cleanup callable, or None (no cleanup needed).
| Returned value | Kernel behaviour |
|---|---|
None |
Silently ignored — no cleanup registered |
Non-callable (e.g. dict) |
Silently ignored — register_cleanup only stores callables |
| Sync callable | Stored; called with no args at teardown. If the call returns a coroutine it is awaited |
| Async callable | Stored; at teardown the coroutine is obtained and awaited via pyo3_async_runtimes::tokio::into_future |
The canonical form is an async def cleanup() nested closure:
async def mount(coordinator, config: dict[str, Any] | None = None):
client = await MyClient.connect((config or {}).get("url"))
coordinator.register_capability("my.client", client)
async def cleanup() -> None:
await client.aclose()
return cleanup # kernel awaits this at session teardownCleanup callables are called with no arguments, in reverse registration order (last-mounted module is cleaned up first). Errors in one cleanup callable are logged but do not prevent the remaining callables from running.
Source reference: bindings/python/src/coordinator/mod.rs::cleanup() and
bindings/python/src/coordinator/capabilities.rs::register_cleanup().
async def on_session_ready(coordinator) -> None:
...Called after ALL modules across all phases have completed mount(). By the time
on_session_ready() runs, the coordinator is fully composed and every contributed tool, hook,
and provider is registered.
| Property | Value |
|---|---|
| Presence | Optional |
| Signature | async def on_session_ready(coordinator) -> None — no config param, no self |
| Sync check | Must be async; a sync on_session_ready logs a warning and is skipped |
| Return value | Ignored |
| Exceptions | Non-fatal — caught and logged as warnings with exc_info=True |
| Call order | Same as mount() order (phase order, then registration order within a phase) |
| Timing | After all phases complete mount(), before session:fork is emitted |
| Scope | Python-only — see polyglot note below |
Fork behaviour: on_session_ready fires once per session. When a parent session forks a
child session (the session:fork event), the child runs its own independent mount wave and
its own on_session_ready pass — in the same order as its own module registration. A parent's
on_session_ready callbacks do not run again for the child.
Dispatch ordering: Callbacks fire sequentially in module registration order (orchestrator first, then context, then providers, tools, and hooks within each phase in load order). This ordering is stable and guaranteed. Cross-module assumptions built on this ordering are safe.
Failure isolation: A raised exception in one module's on_session_ready is caught, logged as
a WARNING with exc_info=True, and does not prevent remaining modules' callbacks from running.
No timeout: The kernel enforces no timeout on on_session_ready callbacks. A hanging callback
hangs the session. This is a deliberate absence — documenting it now prevents surprise if a timeout
is added later (adding one is a breaking change).
Observability event: On on_session_ready failure, the kernel emits a
module:on_session_ready_failed event with payload {"module_id": str, "error": str}, in
addition to the WARNING log. Log-only failures are invisible to observability hooks.
Same mechanism for provider/tool/hook load failures: A provider, tool, or hook that
raises during mount() is caught, logged as a WARNING with exc_info=True, and does
not abort the session — the remaining modules still load and the session still starts.
This is intentional non-interference (a single broken optional module must not take down
an otherwise-working session) but it is silent by default: nothing short of grepping logs
tells you a configured module never mounted. The kernel emits a module:load_failed event
with payload {"module_type": "provider"|"tool"|"hook", "module_id": str, "error": str} so
a hook module can observe the gap and decide policy (abort, notify the user, inject a
system message telling the model the tool is unavailable, etc.). Required modules
(orchestrator, context manager) are unaffected by this — their failure still raises
RuntimeError and aborts initialization, as documented above.
- Discovering contributions from other modules — read the fully-composed coordinator to inspect tools, hooks, or providers registered by peers.
- Wiring cross-module dependencies — subscribe to hooks or capabilities that are only present after another module mounts.
- Eliminating dual-path patterns — avoid the anti-pattern of checking for a capability
in both
mount()(not yet available) and in a request handler (too late to configure).
Before on_session_ready (dual-path anti-pattern) — process-scoped global, unsafe for multi-session deployments:
# Before (anti-pattern) — process-scoped global, unsafe for multi-session deployments
_coordinator = None # ← shared across ALL sessions in the process
async def mount(coordinator, config: dict | None = None) -> None:
global _coordinator
_coordinator = coordinator
async def handle_request(request):
if _coordinator and _coordinator.get("tools", {}).get("search"):
result = await _coordinator.get("tools")["search"].execute(**request.params)After on_session_ready (correct) — session-scoped via closure, safe for multi-session deployments:
# After (correct) — session-scoped via closure, safe for multi-session deployments
async def mount(coordinator, config: dict | None = None) -> None:
pass # nothing cross-module to wire here
async def on_session_ready(coordinator) -> None:
# Session-scoped: register_capability stores reference per-session on the coordinator
tools = coordinator.get("tools") or {}
search_tool = tools.get("search")
if search_tool:
coordinator.register_capability("my_module.search_tool", search_tool)
async def handle_request(coordinator, request):
search_tool = coordinator.get_capability("my_module.search_tool")
if search_tool:
result = await search_tool.execute(**request.params)Cleanup from
on_session_ready():on_session_ready()'s return value is ignored. If a resource allocated inon_session_ready()needs teardown, register the cleanup directly:coordinator.register_cleanup(my_async_cleanup_fn).
# Canonical on_session_ready pattern: event discovery + subscription
# (from hooks-logging, the primary motivating adopter)
async def mount(coordinator, config: dict | None = None):
pass # store config-derived state if needed
async def on_session_ready(coordinator) -> None:
# All modules mounted — event contributions are complete
all_events = set(ALL_EVENTS) # core canonical events
contributions = await coordinator.collect_contributions("observability.events")
for event_list in contributions:
all_events.update(event_list)
# delegate:agent_spawned, delegate:agent_completed are now guaranteed present
for event in all_events:
coordinator.hooks.register(event, my_handler, name="my-module")on_session_ready() is Python-only. WASM, gRPC, and native Rust modules do not participate
in the on_session_ready() wave. If a polyglot module needs post-composition behaviour, defer
it to a request-time check or emit a custom event that Python modules can subscribe to.
Canonical event names are defined in crates/amplifier-core/src/events.rs (Rust)
and re-exported in amplifier_core.events (Python). The full set contains 41+
events; the table below lists the most commonly referenced ones. Always use the
constants from the events module rather than hard-coding strings.
| Event | Value |
|---|---|
| Execution start | "execution:start" |
| Execution end | "execution:end" |
| Session start | "session:start" |
| Session end | "session:end" |
| LLM request | "llm:request" |
| LLM response | "llm:response" |
| Tool pre | "tool:pre" |
| Tool post | "tool:post" |
| Context compaction | "context:compaction" |
-
Field names must match. If you add a field to a Rust struct, add the identical field to the Python BaseModel (and vice versa).
-
Enum variants must match. Rust
snake_caseserde names = PythonLiteralstring values. -
JSON is the contract. Both sides must produce identical JSON for the same logical value. Test with round-trip serialization.
-
Update this document. Any change to a shared type must be reflected here. CI will eventually enforce this.
-
Method names must match for PyO3-bridged types (
Session,Coordinator,HookRegistry,CancellationToken). The Python-visible name is set by#[pyclass(name = "...")]and#[pymethods].