Skip to content
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

## 0.1.0 (unreleased)

- Actuator endpoints `/info`, `/info/routes`, `/env`, `/health` and `/livenessprobe` -
the engines' operational surface, for Kubernetes probes and one-dashboard monitoring
of polyglot installations. Health check functions are normal registered functions
speaking the engines' `type=info` / `type=health` interface contract, listed in
`mandatory.health.dependencies` / `optional.health.dependencies` and called through
the event bus. `/health` answers `UP` (200) / `DOWN` (400); `/livenessprobe` follows
the most recent health outcome.
- `log.format` carries the engines' three presentations: `text` (default), `json`
(pretty-printed) and `compact` (single-line JSONL for log aggregators). A sample
`examples/resources/application.yml` demonstrates the resources convention and the
well-known keys.
- Primitive in-process event bus - the single dispatch pipeline: one FIFO mailbox per
route consumed by `instances` worker tasks (the parameter is faithful); RPC deliveries
are ttl-bounded with a dead-work skip; drop-n-forget returns the 202-shape ack. The
HTTP host and the local side of PostOffice are thin ingress adapters over it. No spill
tier and no queue cap by design - back-pressure belongs to the engines' flows/graphs;
a leaf host fails fast by deadline.
- PostOffice without an endpoint delivers locally (engine semantics): private routes are
callable in-app while the wire keeps its 403; headers pass verbatim; the reply envelope
shape is identical to the remote path.

Repository repurposed for the Mercury Composable **polyglot initiative** (August 2026).

- Lightweight Event-over-HTTP function host (`POST /api/event`) mirroring the engines'
Expand Down
71 changes: 63 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ This package is a deliberately **lightweight wrapper of the Event-over-HTTP prot
registered functions,
- a **thin client** (`PostOffice`) to call functions on peer applications the same way,
- the **standard event envelope wire format** codec (language-neutral MsgPack), and
- a **primitive in-process event bus** — the single dispatch pipeline: one FIFO mailbox
per route consumed by `instances` worker tasks, and
- the **minimalist utilities** shared with the engines for consistency: configuration
management, logging in the engines' presentation format, and distributed-trace context.

Expand Down Expand Up @@ -42,7 +44,7 @@ Run it:

```bash
pip install -e '.[dev]'
mercury-serve app.py --port 8086
mercury-serve app.py -Drest.server.port=8086
```

Call it from a Mercury engine application with two configuration entries and no code —
Expand Down Expand Up @@ -80,6 +82,24 @@ synchronous handlers run in a thread-pool executor so the event loop never block
- Functions must be stateless; anything you must keep belongs to the caller's flow model
or state machine.

### Local function calls

`PostOffice` **without an endpoint** delivers through this application's own event bus —
the engines' semantics for an in-app `po` call:

- `private=True` means exactly what it means in the engines: callable **in-app only**.
Local calls reach private and public routes alike; the HTTP host keeps answering 403
for private targets from the wire.
- `instances` is faithful: each route has one FIFO mailbox consumed by that many worker
tasks. RPC waits are bounded by `timeout_ms` (the standard 408 envelope on breach), and
a queued call whose caller already timed out is skipped, never wastefully executed.
- There is **no spill tier and no queue cap** by design: back-pressure belongs to the tier
that owns recovery — the engines' flows and graphs. A leaf host fails fast by deadline
instead of hoarding work.

Local eventing is for simple leaf-side composition. Workflow processing belongs in Event
Script and Knowledge Graph on the engines — that boundary is the architecture.

## Configuration, logging, telemetry

The same conventions as the engines, so a polyglot installation stays uniform:
Expand All @@ -88,12 +108,14 @@ The same conventions as the engines, so a polyglot installation stays uniform:
|-----|---------|---------|
| `application.name` | application identity in logs | `application` |
| `rest.server.port` | Event API port | `8085` |
| `log.format` | `text` or `json` | `text` |
| `log.format` | `text`, `json` (pretty-printed) or `compact` (single-line JSONL) | `text` |
| `log.level` | log level (`LOG_LEVEL` env var wins) | `INFO` |

Configuration lives in the `resources` folder, mirroring the engines:
`resources/application.yml` (or `.yaml` / `.properties`), or an explicit `--config` path.
Values support `${ENV_VAR:default}` substitution. Runtime parameter overrides use the
`resources/application.yml` (or `.yaml` / `.properties`) in the working directory or next
to the application file, or an explicit `--config` path — see
[`examples/resources/application.yml`](examples/resources/application.yml) for a worked
sample. Values support `${ENV_VAR:default}` substitution. Runtime parameter overrides use the
same `-D` syntax as the Java engine and the Rust port — checked first on every read
(`AppConfig.set(key, value)` does the same programmatically, the `f:setConfig` analog):

Expand All @@ -107,6 +129,37 @@ Log lines follow the Java reference engine's pattern for one-aggregation consist
2026-08-22 10:15:30.123 INFO my_app:42 - Loaded PUBLIC hello.python, instances=10
```

## Actuator endpoints

The host serves the engines' operational endpoints on the same port as `/api/event`, so
Kubernetes probes and dashboards treat a Python app exactly like a Java or Rust engine app:

| Endpoint | Purpose |
|----------|---------|
| `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 |
| `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 |

Configuration keys carry the engines' names: `info.app.version`, `info.app.description`,
`show.env.variables` and `show.application.properties` (opt-in lists — secrets are never
dumped wholesale), and `mandatory.health.dependencies` / `optional.health.dependencies`
(routes of health check functions; optional ones never change the overall status). A
health check function is a normal registered function — usually private — speaking the
engines' interface contract, called through the event bus:

```python
@preload("demo.health", private=True)
async def health(headers: dict[str, str], _body: Body) -> Body:
if headers.get("type") == "info":
return {"service": "demo.service", "href": "http://127.0.0.1"}
return "demo.service is running fine" # a non-200 reply marks it down
```

Kubernetes wiring: point `livenessProbe` at `/livenessprobe` and `readinessProbe` at
`/health`.

## Wire compatibility

The codec implements the
Expand All @@ -122,10 +175,12 @@ millisecond precision; binary payloads use MsgPack `bin`.

## Scope

This package intentionally contains **no event bus, no flows, no graphs and no
orchestration** — those live in the engines. It provides functions plus the minimalist
foundation utilities, keeping Python fast to prototype with while the composable core
guarantees the architecture.
This package intentionally contains **no orchestration: no flows, no graphs, no
persistence, no pub/sub broadcast** — those live in the engines. What it does carry is
deliberately minimal: functions, a primitive in-process event bus (route mailboxes +
workers, RPC and drop-n-forget — nothing more), and the minimalist foundation utilities,
keeping Python fast to prototype with while the composable core guarantees the
architecture.

## Development

Expand Down
34 changes: 33 additions & 1 deletion examples/demo_app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""
Demo polyglot functions.

Run: mercury-serve examples/demo_app.py --port 8086
Run: mercury-serve examples/demo_app.py

Configuration comes from examples/resources/application.yml (the engines'
"resources" convention - port 8086, the demo.health dependency, log format);
override any key with -Dkey=value, e.g. -Drest.server.port=8090.

Then map a route from a Mercury engine application (event-over-http.yaml):

Expand Down Expand Up @@ -43,5 +47,33 @@ async def declarative_echo(headers: dict[str, str], body: Body):
return {"body": body, "headers": headers, "language": "python"}


@preload(route="demo.suffix.helper", instances=10, private=True)
async def suffix_helper(_headers: dict[str, str], body: Body):
"""Private helper - callable in-app only (the HTTP host answers 403 for it)."""
assert isinstance(body, dict)
return {"text": f"{body.get('text', '')}!", "language": "python"}


@preload(route="hello.chain", instances=10)
async def chain(_headers: dict[str, str], body: Body):
"""Local composition: a public function calls a private sibling through the bus."""
from mercury_composable import PostOffice

reply = await PostOffice().request("demo.suffix.helper", body=body, timeout_ms=5000)
return reply.body


@preload(route="demo.health", instances=5, private=True)
async def health_check(headers: dict[str, str], _body: Body):
"""Health check speaking the engines' interface contract (type=info / type=health).

Activated for the /health actuator endpoint by mandatory.health.dependencies
in examples/resources/application.yml (or a -D override).
"""
if headers.get("type") == "info":
return {"service": "demo.service", "href": "http://127.0.0.1"}
return "demo.service is running fine"


if __name__ == "__main__":
platform.run()
22 changes: 22 additions & 0 deletions examples/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Sample configuration - the engines' "resources" convention.
# mercury-serve loads resources/application.yml from the working directory, or from
# the resources folder next to the application file (this one). Any key can be
# overridden at run time with the engines' -D syntax, e.g.:
# mercury-serve examples/demo_app.py -Drest.server.port=8090 -Dlog.format=compact

application.name: 'demo-app'
info.app.description: 'Mercury Composable polyglot demo'

rest.server.port: 8086

# text (default) | json (pretty-printed) | compact (single-line JSONL)
log.format: text
log.level: INFO

# Actuator /health dependencies - routes of health check functions speaking the
# engines' type=info / type=health contract (demo.health in demo_app.py).
mandatory.health.dependencies: 'demo.health'

# Opt-in lists for the /env endpoint - nothing is ever dumped wholesale.
show.env.variables: 'LOG_LEVEL'
show.application.properties: 'application.name, rest.server.port'
25 changes: 22 additions & 3 deletions memory/continuity.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
polyglot initiative: a lightweight Event-over-HTTP function host + thin client, repurposed
August 2026 (legacy language pack in git history only)
- **last_enabled:** 2026-08-22
- **last_session:** 2026-08-23 | agent: Claude Code (2026-08-23-005709)
- **last_session:** 2026-08-23 | agent: Claude Code (2026-08-23-031558)
- **last_review:** (none yet)
- **last_invariant_check:** (none yet)
- **repo:** ~/sandbox/mercury-python (origin: github.com/Accenture/mercury-python)
Expand Down Expand Up @@ -91,8 +91,10 @@

## Open Threads

- [ ] (feature — design RATIFIED by Eric 2026-08-23 in the quality-round conversation;
implementation next on `feature/primitive-event-bus`, LOCK-STEP with mercury-nodejs)
- [ ] (feature — design RATIFIED by Eric 2026-08-23; **IMPLEMENTED same day on
`feature/primitive-event-bus`, commit `957d6b7`, tests 40/40 incl. the 8 bus pins +
live wire proof (chain → private via bus; wire → private = 403); node twin `da8ce4c`
39/39; PENDING Eric's PR gate**)
**Primitive in-process event bus — the single dispatch pipeline.** Ratified shape:
per-route FIFO mailbox (asyncio.Queue; node = queue + worker loops) with
**instances = N worker tasks** (replaces the semaphore — the parameter becomes faithful);
Expand All @@ -112,6 +114,23 @@
hosted→local-private. README boundary statement: leaf-side composition here; workflow
processing = Event Script / Knowledge Graph.
<!-- id: thread-primitive-event-bus | created: 2026-08-23 | last_used: 2026-08-23 | uses: 1 | tier: working | origin: 2026-08-23-005709 -->

- [ ] (feature — Eric's directive 2026-08-23, **IMPLEMENTED same day on
`feature/primitive-event-bus`, commits `56c002c` + `a674198` (IDE/Sonar round);
node twin `342a854` + `7a8b12c`; PENDING Eric's PR gate together with the bus**)
**Actuator endpoints — the engines' operational surface for Kubernetes PODs.**
GET `/info`, `/info/routes`, `/env`, `/health`, `/livenessprobe` on the Event API port;
shapes mirror the Rust engine's actuator (the approved minimalist port of Java
`ActuatorServices`). Health check functions are normal registered functions speaking
the engines' `type=info` / `type=health` interface contract (Eric's ruling), listed in
`mandatory.health.dependencies` / `optional.health.dependencies` and called through the
event bus; `/health` = UP 200 / DOWN 400 (Java parity); `/livenessprobe` follows the
most recent health outcome. Engine formats verbatim (origin = UTC yyyyMMdd + 32-hex
uuid per the Java reference; elapsed-time boundary quirks pinned). Documented deltas:
no `/info/lib`, no XML, no info cache. One async `handle()` dispatcher (S7503-clean,
mirrors the node twin). 10 pins + live demo drives on both wrappers.
Relates [[thread-primitive-event-bus]]; serves [[bp-publish-interop-gate]].
<!-- id: thread-actuator-endpoints | created: 2026-08-23 | last_used: 2026-08-23 | uses: 1 | tier: working | origin: 2026-08-23-031558 -->
> Mark completed items `- [x]` and leave them in place — the review sweeps them to
> the archive once older than `archive_window` sessions. Don't archive them by hand.

Expand Down
43 changes: 43 additions & 0 deletions memory/sessions/2026-08-23-024221.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Session (2026-08-23T02:42:21.000Z)

**Agent:** Claude Code

## Summary

**The primitive in-process event bus implemented (the ratified design in
[[thread-primitive-event-bus]]), branch `feature/primitive-event-bus`, commit `957d6b7` —
lock-step with mercury-nodejs (`da8ce4c` there).**

- `bus.py` (internal): per-route FIFO mailbox (asyncio.Queue) + `instances` worker tasks —
the parameter is faithful; the semaphore is gone. `deliver` (RPC, ttl-bounded → 408,
dead-work skip) + `publish` (drop-n-forget, 202-shape ack). The worker owns the whole
invocation pipeline (trace context, AppException → portable error, exec_time, executor
for sync handlers, error logging for un-replied publishes).
- The HTTP host became thin ingress (guards + hygiene → bus); PostOffice without an
endpoint = local ingress with engine semantics — **private is faithful** (in-app
callable; the wire keeps its 403). PostOffice gained a `registry` parameter (tests use
fresh registries; apps default).
- Demo: public `hello.chain` → private `demo.suffix.helper` through the bus; README
documents local calls, the no-spill/no-cap posture and the workflow boundary; scope
statement amended per the ratified fence.
- **Pins (tests/test_bus.py, 8)**: local RPC public+private, unregistered 404, FIFO order,
instances=2 concurrency peak==2, local 408 + dead-work skip proven (second delivery
never executed), trace chained caller→entry→private helper, local send ack. The whole
existing host suite now exercises the bus path implicitly.
- **Live wire proof**: demo app on 18086 — `hello.chain` over `/api/event` returned
`{'text': 'polyglot!', 'language': 'python'}` (wire → public → bus → private) and the
direct call to the private route answered 403. Same proof green against the node twin.

Follow-up commits on the branch: `300f74f` (Eric's Sonar round on the new code — comment
wording that parsed as commented-out code, `close()` via gather(return_exceptions=True)
so close's own cancellation still propagates, static `_execute`) and `2ff28a4` (unshadowed
inner PostOffice in the trace-chain test). Node twin aligned (`31c8b0b` + `3d866f1`).

Gates: ruff clean, basedpyright 0 errors, **tests 40/40**. Branch = 3 commits, pending
Eric's PR gate.

## Memory References

- referenced: thread-primitive-event-bus (implemented), conv-python-quality-gates
(gates held throughout)
- updated: thread-primitive-event-bus (implementation state)
Loading
Loading