diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a2d8062
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,64 @@
+name: ci
+
+# The wrapper's quality gates (pytest, ruff, basedpyright) plus the strict
+# documentation build, then gh-pages publication on push to main - the same
+# verify/deploy split as the engine repo's docs workflow.
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.10' # the supported floor
+ - name: Install package with dev extras
+ run: pip install -e '.[dev]' basedpyright
+ - name: Lint (ruff)
+ run: ruff check .
+ - name: Type check (basedpyright)
+ run: basedpyright
+ - name: Unit tests
+ run: pytest -q
+
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+ - name: Install MkDocs
+ run: pip install mkdocs-material
+ - name: Build docs (strict)
+ run: mkdocs build --strict
+
+ deploy-docs:
+ # Publish to gh-pages only on push to main or manual dispatch, after both gates pass.
+ if: github.event_name != 'pull_request'
+ needs: [test, docs]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ concurrency:
+ group: docs-deploy
+ cancel-in-progress: false
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+ - name: Install MkDocs
+ run: pip install mkdocs-material
+ - name: Build (strict) and deploy to gh-pages
+ run: mkdocs gh-deploy --force --strict
diff --git a/.gitignore b/.gitignore
index 626ebb2..4fff2c6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,6 @@ uv.lock
.aider.tags.cache*
review-scratch/
*.py[cod]
+
+# mkdocs build output
+site/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9853b47..469d91a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
## 0.1.0 (unreleased)
+- Documentation site (mkdocs-material, the engine repo's theme): the three-layer theme
+ reference, rationale/design foundations, function-writing patterns, flow and
+ knowledge-graph join chapters, a one-page AI agent guide with llms.txt, and
+ configuration/HTTP references - published to
+ https://accenture.github.io/mercury-python/ by the new CI workflow, which also runs
+ the three quality gates (pytest, ruff, basedpyright) on every push and pull request.
- Host polish for engine parity: `GET /` serves the engines' minimal index page linking
the actuator endpoints (embedded - no static file service by design); actuator JSON
responses are pretty-printed (the engines' default-serializer presentation); unknown
diff --git a/README.md b/README.md
index eaaf710..325ac6f 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,10 @@ Orchestration deliberately stays in the engines. Functions written here are addr
route name through the engines' declarative `yaml.event.over.http` map, so a flow or a
graph task calls a Python function exactly as if it were local.
+**Documentation:** — including the
+[AI Agent Guide](https://accenture.github.io/mercury-python/guides/ai-agent-guide/)
+for deterministic function generation.
+
> **Status: pre-release.** This repository was repurposed in August 2026 for the polyglot
> initiative. The legacy Mercury language-pack implementation remains available in the git
> history.
diff --git a/docs/css/extra.css b/docs/css/extra.css
new file mode 100644
index 0000000..c8f470c
--- /dev/null
+++ b/docs/css/extra.css
@@ -0,0 +1,20 @@
+/* Mercury Composable docs — small overrides on the Material theme.
+ *
+ * Reference tables should wrap long inline-code tokens (package and type
+ * names like org.platformlambda.core.annotations) instead of forcing the
+ * whole table to scroll horizontally.
+ */
+
+.md-typeset table:not([class]) td,
+.md-typeset table:not([class]) th {
+ vertical-align: top;
+}
+
+/* Break only genuinely long tokens, and only as a last resort. Unlike
+ * word-break / overflow-wrap:anywhere, this does NOT shrink a column below
+ * its longest word — short words like "String" are never split mid-word. */
+.md-typeset table:not([class]) td code,
+.md-typeset table:not([class]) th code {
+ overflow-wrap: break-word;
+ white-space: normal;
+}
diff --git a/docs/guides/ai-agent-guide.md b/docs/guides/ai-agent-guide.md
new file mode 100644
index 0000000..42ec0ae
--- /dev/null
+++ b/docs/guides/ai-agent-guide.md
@@ -0,0 +1,128 @@
+---
+title: AI Agent Guide
+summary: The complete authoring grammar for Python polyglot functions on one page -
+ contract, registration, config keys, endpoints and error rules, for deterministic generation.
+audience: [ai-agent]
+keywords: [ai agent, grammar, contract, deterministic, preload, postoffice]
+---
+
+# AI Agent Guide
+
+**Purpose: generate a correct Python polyglot function from this page alone.**
+Humans: the narrative versions live in [Function Writing Patterns](function-patterns.md)
+and the [join chapters](join-event-script.md). Orchestration (flows, graphs) is
+authored on the engine — use the
+[engine AI guides](https://accenture.github.io/mercury-composable/guides/ai-developer-guide/).
+
+## Pre-write checklist
+
+1. Route name: lowercase `[a-z0-9._-]`, at least one period. Example: `order.enrich`.
+2. Handler style: wraps blocking library (`requests`, NumPy, DB driver) → plain `def`;
+ asyncio I/O or calls sibling functions → `async def`.
+3. Function is stateless. State belongs to the calling flow/graph.
+4. Orchestration-shaped logic (sequencing, retries, branching) → STOP; author an
+ Event Script flow or MiniGraph graph on the engine instead.
+
+## The contract
+
+```python
+from mercury_composable import AppException, Body, PostOffice, annotate_trace, \
+ get_logger, get_trace, platform, preload
+
+log = get_logger(__name__)
+
+@preload(route="order.enrich", instances=10) # private=True -> in-app only
+def handler(headers: dict[str, str], body: Body): # or: async def
+ # 1. validate; intentional errors = AppException(status, message)
+ if not isinstance(body, dict) or not isinstance(body.get("id"), str):
+ raise AppException(400, "missing 'id'")
+ # 2. work (blocking is safe in plain def - executor thread)
+ # 3. optional telemetry
+ annotate_trace("source", "python") # rides back on the reply
+ # 4. return the reply body (or an EventEnvelope for status/header control)
+ return {"id": body["id"], "enriched": True}
+
+if __name__ == "__main__":
+ platform.run()
+```
+
+Rules:
+
+- `headers: dict[str, str]`; `body: Body` = `None|bool|int|float|str|bytes|list|dict`.
+- Return value = reply body. Return `EventEnvelope` only when setting status/headers.
+- `raise AppException(status, message)` → envelope status + message (portable error).
+ Unexpected exception → 500 + message + stack. Never return HTTP-shaped dicts.
+- `get_trace()` → `TraceInfo(trace_id, trace_path, cid)` or `None`.
+- Reserved inbound header `my_correlation_id` = the caller's business correlation id
+ (read-only). Never send headers named `my_*` or `x-event-api`.
+
+## Composition (calling other functions)
+
+```python
+# async handler:
+reply = await PostOffice().request("other.route", body={...}, timeout_ms=5000)
+# plain-def handler (sync bridge; blocks only this worker thread):
+reply = PostOffice().request_sync("other.route", body={...}, timeout_ms=5000)
+# drop-n-forget twins: send / send_sync -> 202 ack envelope
+# remote peer or engine:
+async with PostOffice(endpoint="http://host:8085/api/event") as po: ...
+```
+
+- `request_sync` on the event loop → RuntimeError (use `await request()`).
+- `request_sync` outside a hosted function → RuntimeError (use `asyncio.run(...)`).
+- Always check `reply.get_status()`; errors are envelopes, not exceptions.
+- Local calls reach `private=True` routes; the wire cannot (403).
+
+## Run + configure
+
+```bash
+mercury-serve app.py # config: resources/application.yml
+mercury-serve app.py -Dkey=value # runtime override (engine syntax)
+```
+
+Well-known keys (full table: [Configuration Reference](configuration-reference.md)):
+`application.name`, `rest.server.port` (default 8085), `log.format`
+(text|json|compact), `log.level`, `info.app.version`, `info.app.description`,
+`show.env.variables`, `show.application.properties`,
+`mandatory.health.dependencies`, `optional.health.dependencies`.
+
+## Health check function (engine interface contract)
+
+```python
+@preload(route="my.health", instances=5, private=True)
+async def health(headers: dict[str, str], _body: Body):
+ if headers.get("type") == "info":
+ return {"service": "my.dependency", "href": "http://backend"}
+ return "my.dependency is running fine" # non-200 reply marks it DOWN
+```
+
+List the route in `mandatory.health.dependencies` (or `optional.…`).
+
+## HTTP surface (served by the host, no code needed)
+
+`POST /api/event` (envelope wire) · `GET /` `/info` `/info/routes` `/env` `/health`
+`/livenessprobe`. Shapes: [HTTP Surface Reference](http-surface-reference.md).
+
+## Engine-side wiring (for completeness; authored on the engine)
+
+```yaml
+# application.properties: yaml.event.over.http=classpath:/event-over-http.yaml
+event.http:
+ - route: 'order.enrich'
+ target: 'http://python-host:8086/api/event'
+```
+
+Flow task `process: 'order.enrich'` or graph node
+`{"skill": "graph.task", "task": "order.enrich", ...}` (engines ≥ v4.11.11 for
+graph.task). Details: [Join an Event Script Flow](join-event-script.md) ·
+[Join a Knowledge Graph](join-knowledge-graph.md).
+
+## DO / DON'T
+
+| DO | DON'T |
+|----|-------|
+| plain `def` for blocking libraries | block inside `async def` |
+| `AppException` for intentional errors | return `{"status": 400, ...}` dicts |
+| keep functions stateless | cache business state in module globals |
+| compose one or two leaf helpers | re-implement flows/retries in Python |
+| let deadlines fail fast (408 envelope) | swallow timeouts and hoard work |
diff --git a/docs/guides/config-logging-actuators.md b/docs/guides/config-logging-actuators.md
new file mode 100644
index 0000000..0c5bec0
--- /dev/null
+++ b/docs/guides/config-logging-actuators.md
@@ -0,0 +1,94 @@
+---
+title: Configuration, Logging & Actuators
+summary: The engines' operational conventions in Python - resources folder, -D overrides,
+ three log formats, actuator endpoints and Kubernetes probes.
+audience: [developer, operator]
+keywords: [configuration, resources, log format, actuator, health, kubernetes, livenessprobe]
+---
+
+# Configuration, Logging & Actuators
+
+*Write functions: run them the way engine apps run.*
+
+> **At a glance**
+>
+> - **What** — one configuration style, one log presentation, one operational surface
+> across Java, Rust, Python and Node.js apps.
+> - **For** developers wiring an app and operators monitoring a polyglot estate.
+
+## Configuration — the engines' conventions
+
+Configuration lives in the `resources` folder (`resources/application.yml`, `.yaml`
+or `.properties`), in the working directory or next to the application file. Values
+support `${ENV_VAR:default}` substitution; `-Dkey=value` command-line arguments are
+runtime overrides checked first on every read — the same syntax as the Java engine's
+JVM system properties and the Rust port's `-D` arguments:
+
+```bash
+mercury-serve app.py -Drest.server.port=8090 -Dlog.format=compact
+```
+
+See the worked sample
+[`examples/resources/application.yml`](https://github.com/Accenture/mercury-python/blob/main/examples/resources/application.yml)
+and the full key table in the [Configuration Reference](configuration-reference.md).
+
+## Logging — one aggregation, three presentations
+
+Log lines follow the Java reference engine's pattern, so a polyglot installation reads
+one way in the aggregator:
+
+```text
+2026-08-24 10:15:30.123 INFO my_app:42 - Loaded PUBLIC hello.python, instances=10
+```
+
+`log.format` carries the engines' three presentations: `text` (default), `json`
+(pretty-printed) and `compact` (single-line JSONL for log aggregators). The level
+comes from the `LOG_LEVEL` environment variable when set, else `log.level`.
+
+## Actuators — the engines' operational surface
+
+The host serves the engines' endpoints on the same port as `/api/event`:
+
+| Endpoint | Purpose |
+|----------|---------|
+| `GET /` | minimal index page linking the endpoints below |
+| `GET /info` | app identity, runtime, origin id, start time, uptime |
+| `GET /info/routes` | registered routes split by visibility, with instance counts |
+| `GET /env` | selected environment variables and configuration parameters (opt-in lists) |
+| `GET /health` | dependency health checks — `UP` (HTTP 200) or `DOWN` (HTTP 400) |
+| `GET /livenessprobe` | `OK` while the last health outcome was good, else HTTP 400 |
+
+JSON responses are pretty-printed (the engines' default-serializer presentation) with
+`application/json; charset=utf-8`; unknown paths answer the engines' error shape —
+see the [HTTP Surface Reference](http-surface-reference.md).
+
+### Health check functions — the engines' interface contract
+
+A health check is a normal registered function (usually private) listed in
+`mandatory.health.dependencies` / `optional.health.dependencies`. The actuator calls
+it through the event bus, first with header `type=info` (an advisory identity map
+merged into its dependency entry), then with `type=health` (a status text or map; a
+non-200 reply marks the dependency down):
+
+```python
+@preload(route="demo.health", instances=5, private=True)
+async def health_check(headers: dict[str, str], _body: Body):
+ if headers.get("type") == "info":
+ return {"service": "demo.service", "href": "http://127.0.0.1"}
+ return "demo.service is running fine"
+```
+
+Optional dependencies never change the overall status; mandatory ones decide
+`UP`/`DOWN`, and the most recent outcome drives `/livenessprobe`.
+
+## Kubernetes wiring
+
+```yaml
+livenessProbe:
+ httpGet: { path: /livenessprobe, port: 8086 }
+readinessProbe:
+ httpGet: { path: /health, port: 8086 }
+```
+
+The pod presents exactly like an engine pod — one dashboard shape for the whole
+polyglot estate.
diff --git a/docs/guides/configuration-reference.md b/docs/guides/configuration-reference.md
new file mode 100644
index 0000000..9696daa
--- /dev/null
+++ b/docs/guides/configuration-reference.md
@@ -0,0 +1,47 @@
+---
+title: Configuration Reference
+summary: Every well-known configuration key, the resolution order, and the substitution syntax.
+audience: [developer, operator, ai-agent]
+keywords: [configuration, reference, keys, substitution, overrides]
+---
+
+# Configuration Reference
+
+*Reference: the complete key table.*
+
+## Resolution
+
+1. `-Dkey=value` command-line overrides (and programmatic `AppConfig.set`) — checked
+ first on every read, the engines' `f:setConfig` analog.
+2. The configuration file: `resources/application.yml` | `.yaml` | `.properties`, in
+ the working directory or next to the application file, or `--config `.
+3. `${ENV_VAR:default}` substitution inside values: environment first, then a base
+ configuration key of that name, then the default.
+
+## Well-known keys (shared with the engines)
+
+| Key | Meaning | Default |
+|-----|---------|---------|
+| `application.name` | application identity in logs, `/info` and `/health` | `application` |
+| `rest.server.port` | Event API + actuator port | `8085` |
+| `log.format` | `text`, `json` (pretty-printed) or `compact` (single-line JSONL) | `text` |
+| `log.level` | log level; the `LOG_LEVEL` environment variable wins | `INFO` |
+| `info.app.version` | version reported by `/info` | package version |
+| `info.app.description` | description reported by `/info` | `application.name` |
+| `show.env.variables` | opt-in list of environment variables shown by `/env` | (empty) |
+| `show.application.properties` | opt-in list of configuration keys shown by `/env` | (empty) |
+| `mandatory.health.dependencies` | routes of health check functions that decide `/health` | (empty) |
+| `optional.health.dependencies` | health check routes reported but never affecting status | (empty) |
+
+List-valued keys accept a comma/space-separated string (engine syntax) or a YAML list.
+
+## Programmatic access
+
+```python
+from mercury_composable import app_config
+
+config = app_config()
+port = config.get("rest.server.port", 8085)
+name = config.get_property("application.name", "application")
+config.set("feature.flag", "on") # runtime override, checked first
+```
diff --git a/docs/guides/design.md b/docs/guides/design.md
new file mode 100644
index 0000000..b220294
--- /dev/null
+++ b/docs/guides/design.md
@@ -0,0 +1,92 @@
+---
+title: Design
+summary: The wrapper anatomy - five small components, each mirroring an engine convention,
+ and the minimalist rulings that keep the package a leaf.
+audience: [architect, developer]
+keywords: [design, event bus, anycast, postoffice, actuator, envelope, wire format]
+---
+
+# Design — The Wrapper Anatomy
+
+*Foundations: what is inside, and why each piece earns its place.*
+
+> **At a glance**
+>
+> - **What** — the five components of the function host and the design rulings behind
+> them: faithful `instances`/`private`, an anycast event bus, fail-fast deadlines,
+> and engine-identical operations.
+> - **For** developers and reviewers who want the mental model before the API.
+
+## Anatomy
+
+```mermaid
+flowchart TB
+ subgraph Host [Python function host]
+ direction TB
+ A["/api/event
(Event API host)"] --> B[EventBus
per-route FIFO mailboxes]
+ C[PostOffice
local mode] --> B
+ B --> W1[worker 1..N] --> F["@preload function"]
+ D[Actuator
/info /health ...] -.probes via bus.-> B
+ end
+ E[Engine or peer] -- envelope bytes --> A
+ F -- PostOffice remote mode --> X[another host or engine]
+```
+
+Five components, one dispatch pipeline:
+
+| Component | Engine convention it mirrors |
+|-----------|------------------------------|
+| **Envelope codec** (`EventEnvelope`) | the [standard wire format](https://accenture.github.io/mercury-composable/guides/event-envelope-wire-format/), verified against golden vectors shared with both engines |
+| **Event API host** (`POST /api/event`) | the engines' `event.api.service` semantics: `x-ttl` bounds execution, `x-async` is drop-n-forget, handler errors ride HTTP 200 inside the envelope |
+| **Primitive event bus** | the engines' in-memory bus semantics: per-route FIFO, `instances` worker tasks, deliver (RPC) and publish (drop-n-forget) — nothing else |
+| **PostOffice** | the engines' `po.request`/`po.send` vocabulary — remote to any peer's `/api/event`, local through the same bus |
+| **Actuator + index page** | the engines' operational surface: `/`, `/info`, `/info/routes`, `/env`, `/health`, `/livenessprobe`, pretty JSON, the same error signature |
+
+## The rulings, and why
+
+**`instances` and `private` are faithful, not decorative.** Each route has one FIFO
+mailbox consumed by exactly `instances` worker tasks — the parameter really is the
+concurrency limit, as in the engines. `private=True` means what it means there too:
+callable in-app through PostOffice, while the wire answers 403. A developer who reads
+the engine documentation forms expectations this host meets.
+
+**The bus is an anycast work queue, deliberately hand-built.** Each delivery goes to
+exactly one of N workers and waits its FIFO turn while all are busy. That contract is a
+work queue, not a broadcast — which is why the implementation is a small mailbox on
+asyncio primitives rather than a pub/sub construct. Two operations only:
+
+- `deliver` — RPC bounded by the caller's ttl; a queued call whose caller already
+ timed out is skipped, never wastefully executed (the dead-work check).
+- `publish` — drop-n-forget, acknowledged with the engines' 202 shape.
+
+**No spill tier, no queue cap, fail fast by deadline.** Back-pressure belongs to the
+tier that owns recovery — the engines' flows and graphs ([Rationale](rationale.md)).
+A breach produces the standard `408` envelope (`Timeout for N ms`), identical to an
+engine timeout, so flows handle both the same way.
+
+**Blocking code cannot hurt the host.** Plain `def` handlers run in a thread-pool
+executor with trace context carried across; the event loop that serves every other
+route is never blocked. This is the Python analog of the Java engine's virtual
+threads: *write sequential blocking-style code; the platform makes it safe.*
+
+**Sync functions compose through a bridge, not a second API.**
+`PostOffice.request_sync()`/`send_sync()` submit the same coroutines onto the host
+loop while blocking only the handler's own worker thread — trace chain unbroken,
+identical envelope shaping. Misuse teaches: calling the bridge on the event loop, or
+outside a hosted function, raises a descriptive error instead of deadlocking.
+
+**In-memory only.** In-flight events die with the process, exactly like the engines'
+own in-memory bus; at-least-once behavior comes from flow-level retries, not from a
+leaf journal.
+
+## The scope fence
+
+The package intentionally contains **no flows, no graphs, no persistence and no
+pub/sub broadcast**. What it carries is deliberately minimal: functions, the primitive
+bus, the thin client, and the engine-consistent utilities (configuration, logging,
+trace). Divergence from an engine convention is treated as a bug, not a style choice.
+
+## Where to go next
+
+[Function Writing Patterns](function-patterns.md) turns this anatomy into day-to-day
+code.
diff --git a/docs/guides/function-patterns.md b/docs/guides/function-patterns.md
new file mode 100644
index 0000000..ce970fd
--- /dev/null
+++ b/docs/guides/function-patterns.md
@@ -0,0 +1,153 @@
+---
+title: Function Writing Patterns
+summary: The coding patterns for externalized functions - the handler contract, sync vs
+ async, errors, trace context, private functions and composition.
+audience: [developer, ai-agent]
+keywords: [preload, handler, sync, async, AppException, trace, private, request_sync]
+---
+
+# Function Writing Patterns
+
+*Write functions: the day-to-day patterns, with the reasons attached.*
+
+> **At a glance**
+>
+> - **What** — the `(headers, body)` contract, both handler styles, the portable error
+> contract, trace context, private functions, and composition through PostOffice.
+> - **Rule of thumb** — wrapping a blocking library → plain `def`; composing functions
+> or async I/O → `async def`.
+
+## The contract
+
+A function is a handler registered under a route name:
+
+```python
+from mercury_composable import Body, preload
+
+@preload(route="my.function", instances=10)
+def handler(headers: dict[str, str], body: Body):
+ return {"ok": True}
+```
+
+- **Input** — the same two-part input as an engine `TypedLambdaFunction`:
+ `headers: dict[str, str]` and `body: Body` (any MsgPack value: `None | bool | int |
+ float | str | bytes | list | dict`).
+- **Output** — return the reply body, or an `EventEnvelope` for full control of status
+ and reply headers.
+- **Route names** — lowercase letters, digits, period, hyphen, underscore, with at
+ least one period (`hello.python`, not `HelloPython`).
+- **Statelessness** — anything a handler must keep belongs to the caller's flow model
+ or graph state machine, never to module globals.
+
+## Sync or async — both are first-class
+
+Python has two library ecosystems, and a polyglot function must be able to wrap
+either:
+
+=== "plain def — the blocking world"
+
+ ```python
+ import requests # or NumPy, pandas, an ML runtime, a DB driver
+
+ @preload(route="quote.fetch", instances=10)
+ def fetch_quote(_headers: dict[str, str], body: Body):
+ assert isinstance(body, dict)
+ response = requests.get(body["url"], timeout=5) # blocking is SAFE here
+ return {"status": response.status_code, "text": response.text[:200]}
+ ```
+
+ Plain `def` handlers run in a thread-pool executor, so a blocking call can never
+ stall the event loop that hosts every other function. This is the Python analog of
+ the Java engine's virtual threads.
+
+=== "async def — the asyncio world"
+
+ ```python
+ from mercury_composable import PostOffice
+
+ @preload(route="hello.chain", instances=10)
+ async def chain(_headers: dict[str, str], body: Body):
+ reply = await PostOffice().request("demo.suffix.helper", body=body,
+ timeout_ms=5000)
+ return reply.body
+ ```
+
+ `async def` handlers run on the event loop — the natural fit for asyncio-native
+ I/O and for composing sibling functions.
+
+Detection is automatic (`inspect.iscoroutinefunction`); trace context, `instances`,
+envelopes and telemetry behave identically in both styles.
+
+## Errors — one portable contract
+
+Raise `AppException(status, message)` for intentional errors:
+
+```python
+from mercury_composable import AppException
+
+raise AppException(400, "missing 'text'")
+```
+
+On the wire this becomes a normal envelope with status 400 and the message as body —
+the flow's exception handler or the graph's `error.*` contract receives it exactly as
+it would from an engine function. An unexpected exception becomes status 500 with the
+message and a stack trace, mirroring the engines. Handler-level errors always ride
+HTTP 200; only transport-level failures (unknown route, private target, timeout,
+undecodable envelope) surface as HTTP status codes.
+
+## Trace context
+
+Every delivery runs under its caller's trace:
+
+```python
+from mercury_composable import annotate_trace, get_trace
+
+info = get_trace() # trace_id, trace_path, cid - or None
+annotate_trace("model", "v3") # rides back on the reply envelope
+```
+
+Outside a hosted function (batch jobs, tests), establish context explicitly:
+
+```python
+from mercury_composable import trace_context
+
+with trace_context("trace-1", "BATCH /nightly", cid="order-42"):
+ reply = await po.request("my.function", body={...})
+```
+
+## Private functions and composition
+
+`private=True` marks a function callable **in-app only** — the HTTP host answers 403
+for it, while a local `PostOffice` (no endpoint) reaches it through the bus:
+
+```python
+@preload(route="demo.suffix.helper", instances=10, private=True)
+async def suffix_helper(_headers: dict[str, str], body: Body): ...
+
+# async composition
+reply = await PostOffice().request("demo.suffix.helper", body=body, timeout_ms=5000)
+
+# sync composition (from a plain-def handler): blocks this worker thread only
+reply = PostOffice().request_sync("demo.suffix.helper", body=body, timeout_ms=5000)
+```
+
+The sync bridge refuses misuse with teaching errors: on the event loop it says
+*await request() instead*; outside a hosted function it points at
+`asyncio.run(po.request(...))`. The trace chain rides across the bridge unbroken.
+
+!!! warning "Composition is for leaf-side helpers"
+ A public function calling a private formatter is healthy. A function that
+ sequences three other functions with retries is a flow wearing a disguise —
+ write it as Event Script or a graph instead ([Rationale](rationale.md)).
+
+## Calling remote peers
+
+The same PostOffice, given an endpoint, calls any engine or peer host with the
+engines' relay contract (octet-stream envelope, `x-ttl`, trace headers):
+
+```python
+async with PostOffice(endpoint="http://peer:8085/api/event") as po:
+ reply = await po.request("hello.node", body={"text": "hi"}, timeout_ms=5000)
+```
+
+The reply envelope is authoritative in every mode: inspect `reply.get_status()`.
diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md
new file mode 100644
index 0000000..d324803
--- /dev/null
+++ b/docs/guides/getting-started.md
@@ -0,0 +1,126 @@
+---
+title: Getting Started
+summary: A running Python function in five minutes - hosted, probed, and called from a
+ Mercury engine flow.
+audience: [developer]
+keywords: [quick start, preload, mercury-serve, event over http]
+---
+
+# Getting Started
+
+*Guide: from zero to a Python function an engine can orchestrate.*
+
+> **At a glance**
+>
+> - **What** — install the package, write one function, serve it, call it — first with
+> `curl`, then from a real engine flow.
+> - **Time** — about five minutes.
+
+## 1. Install
+
+```bash
+git clone https://github.com/Accenture/mercury-python.git
+cd mercury-python
+pip install -e '.[dev]'
+```
+
+*(Pre-release: the package installs from source until the PyPI release.)*
+
+## 2. Write a function
+
+A function is a plain handler registered under a **route name** — the only address the
+rest of the system will ever know it by.
+
+```python
+# app.py
+from mercury_composable import AppException, Body, platform, preload
+
+@preload(route="hello.python", instances=10)
+def handle_event(headers: dict[str, str], body: Body):
+ if not isinstance(body, dict) or not isinstance(body.get("text"), str):
+ raise AppException(400, "missing 'text'")
+ return {"text": body["text"].upper(), "language": "python"}
+
+if __name__ == "__main__":
+ platform.run()
+```
+
+Plain `def` is fine — blocking code (a `requests` call, a NumPy computation) runs in a
+thread pool and can never stall the host. `async def` works too. The
+[Function Writing Patterns](function-patterns.md) guide covers when to use which.
+
+## 3. Configure (the engines' convention)
+
+```yaml
+# resources/application.yml
+application.name: 'hello-app'
+rest.server.port: 8086
+```
+
+Configuration lives in a `resources` folder, exactly like the engines, and any key can
+be overridden at run time with the engines' `-D` syntax.
+
+## 4. Serve it
+
+```bash
+mercury-serve app.py
+```
+
+```text
+2026-08-24 10:15:30.123 INFO mercury.server:124 - Loaded PUBLIC hello.python, instances=10
+2026-08-24 10:15:30.124 INFO mercury.server:126 - hello-app - Event API service started on port 8086
+```
+
+Open — the host serves the engines' familiar index page, and
+the same actuator endpoints (`/info`, `/health`, `/livenessprobe`, …) your operations
+team already monitors on engine apps.
+
+## 5. Call it from an engine
+
+One declarative entry in the engine application tells it where the route lives —
+`application.properties`:
+
+```properties
+yaml.event.over.http=classpath:/event-over-http.yaml
+```
+
+`event-over-http.yaml`:
+
+```yaml
+event.http:
+ - route: 'hello.python'
+ target: 'http://127.0.0.1:8086/api/event'
+```
+
+Any Event Script task or MiniGraph `graph.task` node that names `hello.python` now
+executes your Python function — trace context, correlation id and error contract
+carried end to end. [Join an Event Script Flow](join-event-script.md) walks through a
+complete flow; [Join a Knowledge Graph](join-knowledge-graph.md) does the same for a
+graph.
+
+## 6. Or just curl it
+
+The host speaks the engines' Event API protocol (envelope bytes over
+`POST /api/event`), so the natural ad-hoc client is the package itself:
+
+```python
+import asyncio
+from mercury_composable import PostOffice
+
+async def main():
+ async with PostOffice(endpoint="http://127.0.0.1:8086/api/event") as po:
+ reply = await po.request("hello.python", body={"text": "polyglot"}, timeout_ms=5000)
+ print(reply.get_status(), reply.body)
+
+asyncio.run(main())
+```
+
+```text
+200 {'text': 'POLYGLOT', 'language': 'python'}
+```
+
+## Next
+
+- The **why**: [Rationale — Externalized Functions](rationale.md)
+- The **how, in depth**: [Function Writing Patterns](function-patterns.md)
+- The **wiring**: [Join an Event Script Flow](join-event-script.md)
diff --git a/docs/guides/http-surface-reference.md b/docs/guides/http-surface-reference.md
new file mode 100644
index 0000000..b08cff8
--- /dev/null
+++ b/docs/guides/http-surface-reference.md
@@ -0,0 +1,62 @@
+---
+title: HTTP Surface Reference
+summary: The exact protocol and response shapes of the function host - Event API, actuators,
+ error signature and content types.
+audience: [developer, operator, ai-agent]
+keywords: [http, reference, event api, actuator, error shape, content type]
+---
+
+# HTTP Surface Reference
+
+*Reference: every byte the host serves.*
+
+## POST /api/event — the Event API (envelope wire)
+
+Mirrors the engines' `event.api.service`:
+
+| Aspect | Behavior |
+|--------|----------|
+| Request body | event envelope bytes ([standard wire format](https://accenture.github.io/mercury-composable/guides/event-envelope-wire-format/)) |
+| `x-ttl` header | execution bound in ms (floor 1000) |
+| `x-async: true` | drop-n-forget → HTTP 202 with ack envelope `{type: async, delivered: true, time}` |
+| Reply | always envelope bytes, `content-type: application/octet-stream` |
+| Handler outcome | rides **HTTP 200** with the status inside the envelope (including AppException and unexpected errors) |
+| Transport failures | set the HTTP status too: 400 undecodable / missing route field, 403 private target, 404 unknown route (`Route X not found`), 408 timeout (`Timeout for N ms`) |
+| Header hygiene | inbound `x-event-api` and `my_*` removed; the `my_cid` tag becomes the read-only `my_correlation_id` header |
+
+## Actuator endpoints
+
+All JSON responses are pretty-printed with `content-type: application/json;
+charset=utf-8` (the engines' default-serializer presentation).
+
+| Endpoint | Content type | Shape |
+|----------|--------------|-------|
+| `GET /` | `text/html` | minimal index page linking the endpoints |
+| `GET /info` | JSON | `{app{name,version,description}, runtime{language,python,mercury_composable}, origin, time{start,current}, up_time}` |
+| `GET /info/routes` | JSON | `{app, routing{public{route: instances}, private{...}}}` |
+| `GET /env` | JSON | `{app, env{environment{...}, properties{...}}}` (opt-in lists) |
+| `GET /health` | JSON | `{dependency[...], status: UP\|DOWN, origin, name}` — HTTP 200 when UP, 400 when DOWN |
+| `GET /livenessprobe` | `text/plain` | `OK`, or HTTP 400 `Unhealthy. Please check '/health' endpoint.` |
+
+Each `/health` dependency entry: `{route, required, ...info-map, status_code,
+message}` — the info map comes from the function's `type=info` reply; a missing route
+reports `status_code: 404` with `Please check - Route X not found`.
+
+- `origin` — unique instance id, minted once per process: UTC `yyyyMMdd` + 32-hex
+ uuid (the Java reference engine's format).
+- `up_time` — the engines' rendering (`59 seconds`, `1 minute 1 second`, …).
+
+## Error signature (host-level)
+
+Unknown paths and non-GET methods on known paths answer the engines' shape —
+pretty-printed JSON:
+
+```json
+{
+ "status": 404,
+ "message": "Resource not found",
+ "type": "error"
+}
+```
+
+Handler-level errors never use this shape — they ride the envelope on `/api/event`.
diff --git a/docs/guides/join-event-script.md b/docs/guides/join-event-script.md
new file mode 100644
index 0000000..1e47149
--- /dev/null
+++ b/docs/guides/join-event-script.md
@@ -0,0 +1,120 @@
+---
+title: Join an Event Script Flow
+summary: Wire a Python function into a flow - the declarative map, the task that names it,
+ data mapping from the function's seat, and the exception path end to end.
+audience: [developer]
+keywords: [event script, flow, yaml.event.over.http, declarative, task, exception handler]
+---
+
+# Join an Event Script Flow
+
+*Join the engines: your function as a first-class flow task.*
+
+> **At a glance**
+>
+> - **What** — one YAML entry on the engine makes a route remote; the flow itself does
+> not change at all.
+> - **Worked demo** — the engine's `composable-example` ships this exact wiring and
+> has executed the Python demo function unchanged.
+
+## The one moving part: the declarative map
+
+On the engine application, enable the map and point the route at your host —
+`application.properties`:
+
+```properties
+yaml.event.over.http=classpath:/event-over-http.yaml
+```
+
+`event-over-http.yaml`:
+
+```yaml
+event.http:
+ - route: 'hello.declarative'
+ target: 'http://${peer.demo.host:127.0.0.1}:${peer.demo.port}/api/event'
+ # optional security headers, e.g. an authorization token the host or a
+ # gateway validates:
+ # headers:
+ # authorization: '${DEMO_PEER_TOKEN:demo}'
+```
+
+That is the entire integration surface. Every Event Script task (and MiniGraph
+`graph.task`) that names `hello.declarative` now calls your Python host. The full map
+grammar lives in the engine's
+[Event over HTTP guide](https://accenture.github.io/mercury-composable/guides/event-over-http/).
+
+## The flow does not know, and must not care
+
+This is the engine's shipped demo flow — note that nothing in it says "remote" or
+"python":
+
+```yaml
+flow:
+ id: 'event-over-http-declarative'
+ description: 'Demonstrate Event-over-Http protocol using declarative means'
+ ttl: 10s
+ exception: 'v1.hello.exception'
+
+first.task: 'event-over-http-declarative'
+
+tasks:
+ - name: 'event-over-http-declarative'
+ input:
+ - 'input.header -> header'
+ - 'input.body -> *'
+ process: 'hello.declarative'
+ output:
+ - 'text(application/json) -> output.header.content-type'
+ - 'result -> output.body'
+ execution: end
+```
+
+Register the route on the Python side and the flow executes it:
+
+```python
+@preload(route="hello.declarative", instances=10)
+async def declarative_echo(headers: dict[str, str], body: Body):
+ return {"body": body, "headers": headers, "language": "python"}
+```
+
+## What your function sees
+
+```mermaid
+sequenceDiagram
+ participant C as REST client
+ participant E as Engine (flow)
+ participant H as Python host
+ participant F as hello.declarative
+ C->>E: GET /api/event/http/declarative
+ E->>H: POST /api/event (envelope bytes, x-ttl, trace headers)
+ H->>F: (headers, body) on the bus
+ F-->>H: reply body
+ H-->>E: reply envelope (status, exec_time, annotations)
+ E-->>C: flow output mapping
+```
+
+- **Headers** — the task's `input.header -> header` mapping arrives as your
+ `headers` dict. Reserved keys are cleaned at ingress, and the flow's business
+ correlation id arrives as the read-only `my_correlation_id` header.
+- **Body** — whatever the task's input mapping sends (`input.body -> *` passes the
+ whole body through).
+- **Trace** — the engine's trace id and path ride the wire; `get_trace()` sees them,
+ and `annotate_trace()` entries return on the reply envelope into the engine's
+ telemetry.
+
+## Errors flow into the flow
+
+- `AppException(400, "missing 'text'")` in Python → a 400 envelope → the flow's
+ `exception:` task fires with `error.code=400` and `error.message` exactly as for an
+ engine function.
+- An unexpected Python exception → 500 with message and stack.
+- The flow's `ttl` bounds the call: on breach the engine receives the standard 408,
+ and the exception path decides what happens next — retries and compensation stay in
+ the flow, never in Python ([Rationale](rationale.md)).
+
+## Checklist
+
+1. Host running and healthy (`/livenessprobe` → `OK`).
+2. Route registered (`/info/routes` lists it as `public`).
+3. Engine `application.properties` names the map; the map names the route and target.
+4. The flow task's `process:` names the route. Nothing else changes.
diff --git a/docs/guides/join-knowledge-graph.md b/docs/guides/join-knowledge-graph.md
new file mode 100644
index 0000000..0e96663
--- /dev/null
+++ b/docs/guides/join-knowledge-graph.md
@@ -0,0 +1,83 @@
+---
+title: Join a Knowledge Graph
+summary: Wire a Python function into a MiniGraph model - graph.task to a declarative target,
+ deadlines, and the error.* contract from the function's seat.
+audience: [developer, architect]
+keywords: [minigraph, knowledge graph, graph.task, declarative target, error contract, x-ttl]
+---
+
+# Join a Knowledge Graph
+
+*Join the engines: your function as a graph task in an Active Knowledge Graph.*
+
+> **At a glance**
+>
+> - **What** — a `graph.task` node names your route; the same declarative map from the
+> [flow chapter](join-event-script.md) points it at your host.
+> - **Requires** — engine **v4.11.11 or later** (the release that taught the
+> deployed-graph guard about the declarative map).
+
+## The graph names the route — nothing more
+
+In MiniGraph, behavior lives on nodes as **skills**. The `graph.task` skill invokes a
+composable function by route name — and since v4.11.11 that route may resolve through
+`yaml.event.over.http` to a Python host. A minimal node:
+
+```json
+{
+ "types": ["Task"],
+ "alias": "python-step",
+ "properties": {
+ "skill": "graph.task",
+ "task": "hello.python",
+ "input": ["input.body -> *"],
+ "output": ["result -> output.body"]
+ }
+}
+```
+
+The graph model carries no URL and no language — deployment stays in the engine's
+configuration, exactly as the composable theme demands. Everything about authoring
+graphs (nodes, skills, the compile gate, the Playground) is the engine's domain:
+start at [Build your first graph](https://accenture.github.io/mercury-composable/guides/knowledge-graph/build-your-first-graph/)
+and the [built-in skills reference](https://accenture.github.io/mercury-composable/guides/knowledge-graph/skills-reference/).
+
+## Deadlines: two distinct clocks
+
+- The graph's **ttl** bounds the *event call* to your function — on breach the engine
+ sees the standard 408 envelope.
+- If your function itself calls onward over HTTP, that client call carries its own
+ timeout — the same decoupling the engines document for `async.http.request`
+ (`headers.x-ttl` in milliseconds).
+
+Design functions so their own outbound work respects the deadline they were given:
+read the caller's intent from your ttl-bounded world and fail fast rather than
+overstay ([Design](design.md)).
+
+## Errors: the graph's error.* contract
+
+When your function raises `AppException(code, message)` — or fails unexpectedly with
+a 500 — the walker stages the graph's generic error context:
+`error.source` (the failing node), `error.code`, `error.message`, and `error.stack`
+when one traveled. A graph can therefore route around a Python failure with the same
+IF/THEN patterns it uses for any other task — see the engine's
+[knowledge-graph guides](https://accenture.github.io/mercury-composable/guides/knowledge-graph/)
+for the retry and recovery idioms.
+
+## Worked demo
+
+The quickest end-to-end proof mirrors the engines' own conformance test: a graph with
+one `graph.task` node whose `task` names a route served by the Python demo app
+(`hello.python` in [Getting Started](getting-started.md)), with the declarative map on
+the engine pointing at `http://127.0.0.1:8086/api/event`. Deploy the graph (or dry-run
+it in the Playground), `POST /api/graph/{graph-id}`, and the reply body is your
+function's output — trace id visible in the Python host's log line.
+
+## Checklist
+
+1. Engine at v4.11.11+ with `yaml.event.over.http` configured.
+2. The map entry's route == the node's `task` value == your `@preload` route.
+3. The graph passes the compile gate and is listed in the manifest (deployed lane) —
+ "compiled or 404" is the engine's rule.
+4. Your host is healthy; `/info/routes` lists the route as `public` (a private route
+ answers 403 on the wire by design).
diff --git a/docs/guides/rationale.md b/docs/guides/rationale.md
new file mode 100644
index 0000000..0a8a124
--- /dev/null
+++ b/docs/guides/rationale.md
@@ -0,0 +1,90 @@
+---
+title: Rationale
+summary: Why externalized functions exist - the composable theme extended across language
+ boundaries, and the boundaries that keep it honest.
+audience: [architect, developer]
+keywords: [rationale, polyglot, event over http, orchestration, distributed monolith]
+---
+
+# Rationale — Externalized Functions
+
+*Foundations: the thinking before the how.*
+
+> **At a glance**
+>
+> - **What** — why Python functions join Mercury flows and graphs over Event-over-HTTP,
+> and why the wrapper refuses to become a second orchestrator.
+> - **For** architects deciding where a piece of logic should live.
+
+## The composable theme does not stop at the JVM boundary
+
+Mercury's core promise is that **functions are self-contained and coupled only by
+route names and event envelopes**. A flow that reads
+
+```yaml
+process: 'hello.python'
+```
+
+does not know — and must not care — whether that route resolves to a Java method on a
+virtual thread, a Rust function, or a Python handler in another process. The route
+name is the contract; everything else is deployment detail.
+
+Externalized functions take that promise literally. The
+[Event-over-HTTP seam](https://accenture.github.io/mercury-composable/guides/event-over-http/)
+already existed for engine-to-engine calls: the same event envelope, serialized in a
+language-neutral wire format, carried over HTTP. A Python function host is simply
+another peer speaking that protocol — nothing new crosses the wire.
+
+## Why you would externalize a function
+
+**Ecosystem gravity.** Python's strength is its libraries: `requests`-style
+integrations, NumPy/pandas, scikit-learn and the ML inference stacks, domain SDKs.
+Porting that logic into Java rarely adds value; wrapping it as a route does. The
+function stays where its ecosystem lives; the orchestration stays where the
+architecture lives.
+
+**Team reality.** Enterprise installations are polyglot for the long haul. A data
+science team ships Python; the platform team runs engine applications. Route names are
+the only interface the two must agree on — a one-line YAML entry, not a shared
+codebase.
+
+**Independent lifecycles.** A Python host deploys, scales and restarts on its own
+cadence as an ordinary Kubernetes pod — with the engines' own actuator surface, so
+operations sees one shape everywhere.
+
+## Why the wrapper owns nothing but functions
+
+The tempting mistake is to grow the wrapper into a mini-engine — local flows, retries,
+queues, persistence. Mercury's architecture deliberately says no:
+
+- **Orchestration lives in the engines.** Event Script and MiniGraph carry the retry
+ logic, branching, exception routing and state. A leaf that re-implements recovery
+ fights the tier that owns it.
+- **Back-pressure belongs to the tier that owns recovery.** The host's internal event
+ bus has *no spill tier and no queue cap*: a call that cannot be served by its
+ deadline fails fast with the standard 408 envelope, and the engine's flow decides
+ what happens next. A leaf that hoards work merely hides congestion from the layer
+ designed to handle it.
+- **Sync-over-async across services is an anti-pattern by default.** The engines' own
+ documentation warns that superimposing synchronous coupling on distributed parts
+ builds a *distributed monolith*. The wrapper inherits that judgment: local
+ composition is for simple leaf-side helpers; workflow processing belongs in flows
+ and graphs.
+
+The result is a package small enough to audit in an afternoon — an envelope codec, a
+host, a primitive bus, a thin client, and the engines' operational conventions — with
+the entire orchestration brain kept where it already is.
+
+## When *not* to externalize
+
+- **Orchestration-shaped logic** — sequencing, branching, compensation: write a flow
+ or a graph, not a Python function that calls other functions in a loop.
+- **Latency-critical hot paths** — an in-engine function call is an in-memory event;
+ an externalized one is an HTTP round trip. Microseconds versus milliseconds.
+- **Logic that is really data mapping** — Event Script's data mapping and MiniGraph's
+ built-in skills often eliminate the function entirely.
+
+## Where to go next
+
+[Design — The Wrapper Anatomy](design.md) shows how these principles become the five
+small components of the package.
diff --git a/docs/guides/testing.md b/docs/guides/testing.md
new file mode 100644
index 0000000..c3e012d
--- /dev/null
+++ b/docs/guides/testing.md
@@ -0,0 +1,96 @@
+---
+title: Testing Your Functions
+summary: The test harness patterns this package uses on itself - registry fixtures, real-HTTP
+ host tests, and the golden wire-format vectors.
+audience: [developer]
+keywords: [testing, pytest, pytest-asyncio, fixtures, golden vectors, wire format]
+---
+
+# Testing Your Functions
+
+*Write functions: test them the way this package tests itself.*
+
+> **At a glance**
+>
+> - **What** — three proven layers: direct handler tests, in-process bus tests, and
+> real-HTTP host tests; plus the golden vectors that pin wire compatibility.
+
+## Layer 1 — the handler is just a function
+
+The cheapest test needs no framework at all:
+
+```python
+def test_uppercase_contract():
+ reply = handle_event({}, {"text": "polyglot"})
+ assert reply == {"text": "POLYGLOT", "language": "python"}
+```
+
+## Layer 2 — through the bus, with a fresh registry
+
+Register into a **fresh** `FunctionRegistry` per test (never the default one) and
+close its bus on teardown — the pattern from this package's own `tests/test_bus.py`:
+
+```python
+import pytest_asyncio
+from collections.abc import AsyncIterator
+from mercury_composable import FunctionRegistry, PostOffice, trace_context
+
+@pytest_asyncio.fixture
+async def registry() -> AsyncIterator[FunctionRegistry]:
+ fresh = FunctionRegistry()
+ yield fresh
+ await fresh.bus.close()
+
+async def test_trace_rides_through(registry: FunctionRegistry):
+ registry.register("my.function", handler)
+ po = PostOffice(registry=registry)
+ with trace_context("trace-1", "TEST /unit", cid="cid-1"):
+ reply = await po.request("my.function", body={"text": "x"}, timeout_ms=5000)
+ assert reply.get_status() == 200
+```
+
+This exercises `instances`, private routes, deadlines (assert the 408 envelope) and
+trace propagation exactly as production will.
+
+## Layer 3 — over real HTTP
+
+Boot the host on an ephemeral port and speak the actual protocol
+(`tests/test_server.py` pattern):
+
+```python
+from aiohttp import web
+from mercury_composable.server import EventApiServer
+
+server = EventApiServer(registry)
+runner = web.AppRunner(server.create_app())
+await runner.setup()
+site = web.TCPSite(runner, "127.0.0.1", 0)
+await site.start()
+port = runner.addresses[0][1]
+# ... aiohttp client posts envelope bytes to /api/event, or PostOffice(endpoint=...)
+await runner.cleanup()
+```
+
+Use this layer to pin transport behavior: 403 for private routes, 404 messages, the
+`x-async` 202 acknowledgement, reserved-header hygiene, actuator shapes.
+
+## Wire compatibility — the golden vectors
+
+The codec is verified against the **golden conformance vectors shared with the Java
+and Rust engines** (`tests/vectors/vectors.json`). If you extend envelope handling,
+run the vector suite — it is the cross-language contract:
+
+```bash
+pytest -q tests/test_envelope.py
+```
+
+## The project gates
+
+```bash
+pytest -q # all tests
+ruff check . # lint
+basedpyright # types (tests included)
+```
+
+The CI workflow runs all three plus a strict documentation build on every push and
+pull request.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..5ad12be
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,74 @@
+---
+title: Composable for Python
+summary: Externalized functions for Mercury Composable - write decoupled functions in Python
+ and let the Java/Rust engines orchestrate them from Event Script flows and knowledge graphs.
+audience: [developer, architect, ai-agent]
+keywords: [polyglot, python, event over http, composable, minigraph, externalized functions]
+---
+
+# Externalized functions for Mercury Composable
+
+*Write the function in Python. Let the engine orchestrate it.*
+
+> **At a glance**
+>
+> - **What** — a lightweight Event-over-HTTP **function host and client**: your Python
+> functions become routes a [Mercury Composable](https://accenture.github.io/mercury-composable/)
+> engine calls from Event Script flows and MiniGraph knowledge graphs, exactly as if
+> they were local.
+> - **For** teams whose business logic lives in Python — `requests`-style integrations,
+> NumPy/pandas, ML inference - inside a composable, event-driven architecture.
+> - **Not** an orchestrator. Flows, graphs, retries and state live in the engines;
+> this package deliberately provides functions only.
+
+## The main theme, in one screen
+
+Mercury Composable builds applications from **self-contained functions wired by
+configuration** — functions never call each other directly; they couple only through
+route names and event envelopes. The platform ascends three layers, each documented in
+the [engine documentation](https://accenture.github.io/mercury-composable/):
+
+| Layer | Idea | Authority |
+|-------|------|-----------|
+| **1 — Event-driven** | Functions + route names + `EventEnvelope` over an in-memory event bus | [Event-driven Foundation](https://accenture.github.io/mercury-composable/guides/event-driven/) |
+| **2 — Composable** | **Event Script**: YAML flows choreograph functions — orchestration is configuration, not code | [Composable Orchestration](https://accenture.github.io/mercury-composable/guides/event-script/) |
+| **3 — Knowledge Graph** | **MiniGraph**: an Active Knowledge Graph *is* the application — graphs execute behavior through skills | [Knowledge Graph](https://accenture.github.io/mercury-composable/guides/knowledge-graph/) |
+
+This site documents the **fourth seat at that table**: functions that live *outside*
+the engine — in a Python process — yet participate in layers 2 and 3 as first-class
+tasks. The seam is [Event over HTTP](https://accenture.github.io/mercury-composable/guides/event-over-http/):
+the same event envelope, carried over HTTP, addressed by the same route names.
+
+```mermaid
+flowchart LR
+ subgraph Engine [Java or Rust engine]
+ F[Event Script flow] --> R[route: hello.python]
+ G[MiniGraph graph.task] --> R
+ end
+ R -- "Event-over-HTTP
(one envelope, MsgPack)" --> H
+ subgraph Host [Python function host]
+ H[POST /api/event] --> B[event bus] --> P["@preload('hello.python')"]
+ end
+```
+
+An engine flow or graph names the route `hello.python`; a one-entry declarative map
+points that route at your Python host; your function runs with trace context carried
+end to end. **No engine code changes. No orchestration in Python.**
+
+## Where to go next
+
+- **New here?** [Getting Started](guides/getting-started.md) — a running function in
+ five minutes, called from an engine flow.
+- **Why this design?** [Rationale](guides/rationale.md) and
+ [Design](guides/design.md) — the thinking before the how.
+- **Writing functions?** [Function Writing Patterns](guides/function-patterns.md).
+- **Wiring the engine?** [Join an Event Script Flow](guides/join-event-script.md) ·
+ [Join a Knowledge Graph](guides/join-knowledge-graph.md).
+- **You are an AI agent?** Start at the [AI Agent Guide](guides/ai-agent-guide.md) —
+ the complete authoring grammar on one page — and the machine index
+ [llms.txt](llms.txt).
+
+!!! note "Engine versions"
+ Event Script flows call declarative Event-over-HTTP targets on any 4.x engine.
+ **MiniGraph `graph.task` targets require engine v4.11.11 or later** (the release
+ that taught the deployed-graph guard about the declarative map).
diff --git a/docs/llms.txt b/docs/llms.txt
new file mode 100644
index 0000000..7b7140a
--- /dev/null
+++ b/docs/llms.txt
@@ -0,0 +1,33 @@
+# Composable for Python (mercury-python)
+
+> A lightweight Event-over-HTTP function host and client: write decoupled functions in
+> Python and let Mercury Composable engines (Java, Rust) orchestrate them from Event
+> Script flows and MiniGraph knowledge graphs. Orchestration stays in the engines; this
+> package provides functions plus the engines' operational conventions (configuration,
+> logging, trace, actuators). The engines' documentation lives at
+> https://accenture.github.io/mercury-composable/ (its llms.txt maps the three layers).
+
+> This file is the machine-readable map of this documentation site.
+
+## Start here
+- [Home](https://accenture.github.io/mercury-python/): the three-layer theme and where a Python function sits (a leaf peer over Event-over-HTTP).
+- [AI Agent Guide](https://accenture.github.io/mercury-python/guides/ai-agent-guide/): **start here if you are an AI agent** — the complete authoring grammar on one page: contract, registration, composition, config keys, error rules.
+- [Getting Started](https://accenture.github.io/mercury-python/guides/getting-started/): a running function in five minutes, called from an engine flow.
+
+## Foundations
+- [Rationale](https://accenture.github.io/mercury-python/guides/rationale/): why externalized functions — ecosystem gravity, the Event-over-HTTP seam, why the wrapper owns nothing but functions.
+- [Design](https://accenture.github.io/mercury-python/guides/design/): the anatomy — envelope codec, Event API host, primitive anycast bus (faithful instances/private), PostOffice, actuators — and the minimalist rulings.
+
+## Write functions
+- [Function Writing Patterns](https://accenture.github.io/mercury-python/guides/function-patterns/): the (headers, body) contract, plain-def vs async-def, AppException, trace context, private functions, composition incl. the sync bridge.
+- [Configuration, Logging & Actuators](https://accenture.github.io/mercury-python/guides/config-logging-actuators/): resources convention, -D overrides, three log formats, actuator endpoints, health-function contract, Kubernetes probes.
+- [Testing Your Functions](https://accenture.github.io/mercury-python/guides/testing/): handler tests, fresh-registry bus tests, real-HTTP host tests, golden wire vectors.
+
+## Join the engines
+- [Join an Event Script Flow](https://accenture.github.io/mercury-python/guides/join-event-script/): yaml.event.over.http, the flow task, what the function sees, the exception path.
+- [Join a Knowledge Graph](https://accenture.github.io/mercury-python/guides/join-knowledge-graph/): graph.task to a declarative target (engines >= v4.11.11), deadlines, the error.* contract.
+
+## Reference
+- [Configuration Reference](https://accenture.github.io/mercury-python/guides/configuration-reference/): every well-known key, resolution order, substitution syntax.
+- [HTTP Surface Reference](https://accenture.github.io/mercury-python/guides/http-surface-reference/): /api/event protocol, actuator shapes, error signature, content types.
+- [Release Notes](https://github.com/Accenture/mercury-python/blob/main/CHANGELOG.md)
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..24b46cd
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,126 @@
+site_name: Composable for Python
+site_description: >-
+ Mercury Composable polyglot functions for Python — a lightweight
+ Event-over-HTTP function host and client: write functions here, let the
+ Java/Rust engines orchestrate them from Event Script flows and MiniGraph
+ knowledge graphs.
+site_url: https://accenture.github.io/mercury-python/
+docs_dir: docs
+
+repo_name: Accenture/mercury-python
+repo_url: https://github.com/Accenture/mercury-python
+edit_uri: edit/main/docs/
+
+copyright: >-
+ Apache-2.0 · a Mercury Composable polyglot wrapper — engines:
+ Java and
+ Rust.
+
+theme:
+ name: material
+ icon:
+ repo: fontawesome/brands/github
+ logo: material/language-python
+ palette:
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: deep purple
+ accent: cyan
+ toggle:
+ icon: material/brightness-7
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: deep purple
+ accent: cyan
+ toggle:
+ icon: material/brightness-4
+ name: Switch to system preference
+ features:
+ - navigation.tabs
+ - navigation.sections
+ - navigation.top
+ - navigation.footer
+ - navigation.indexes
+ - toc.follow
+ - search.suggest
+ - search.highlight
+ - content.code.copy
+ - content.tabs.link
+
+markdown_extensions:
+ - admonition
+ - attr_list
+ - def_list
+ - footnotes
+ - md_in_html
+ - tables
+ - toc:
+ permalink: true
+ - pymdownx.details
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets:
+ base_path: ["."]
+ check_paths: true
+ # NOTE: pymdownx.emoji is deliberately NOT enabled (engine convention) -
+ # colon-delimited technical patterns would render as emoji.
+
+plugins:
+ - search:
+ lang: en
+ separator: '[\s\-\.]+'
+
+# Machine-readable artifacts linked from the AI docs, not part of the nav.
+not_in_nav: |
+ llms.txt
+
+validation:
+ nav:
+ omitted_files: info
+ links:
+ not_found: warn
+ absolute_links: ignore
+
+extra_css:
+ - css/extra.css
+
+nav:
+ - Home: index.md
+
+ - Orientation:
+ - Getting Started: guides/getting-started.md
+
+ - Foundations:
+ - Rationale — Externalized Functions: guides/rationale.md
+ - Design — The Wrapper Anatomy: guides/design.md
+
+ - Write functions:
+ - Function Writing Patterns: guides/function-patterns.md
+ - Configuration, Logging & Actuators: guides/config-logging-actuators.md
+ - Testing Your Functions: guides/testing.md
+
+ - Join the engines:
+ - Join an Event Script Flow: guides/join-event-script.md
+ - Join a Knowledge Graph: guides/join-knowledge-graph.md
+
+ - AI:
+ - AI Agent Guide: guides/ai-agent-guide.md
+
+ - Reference:
+ - Configuration Reference: guides/configuration-reference.md
+ - HTTP Surface Reference: guides/http-surface-reference.md
+ - Release Notes: https://github.com/Accenture/mercury-python/blob/main/CHANGELOG.md
+ - Contributing: https://github.com/Accenture/mercury-python/blob/main/CONTRIBUTING.md