Authoring a programmable agent
A programmable agent is your code, in your language, driving a substrate session over protocol v1. For the concepts behind the channel, read programmable agents & the function plane first.
There are two ways in. The Python authoring kit is the ergonomic surface and where most authors should start. The raw protocol is what the kit is built on, what a peer in any other language implements, and what the rest of this page documents — because nothing here is hidden behind the kit.
The authoring kit: write once, run either way
Section titled “The authoring kit: write once, run either way”import asynciofrom apomesh.agent import AgentSession, run
async def my_agent(session: AgentSession) -> str: reply = await session.generate( [{"role": "user", "content": session.goal.get("description", "")}] ) await session.report_position({"step": 1}) return "GoalMet"
if __name__ == "__main__": asyncio.run(run(my_agent, endpoint="localhost:9000", goal="summarise the incident"))Run that file directly and it attaches as a tethered driver: your process holds the control flow on your machine and the platform relays its runtime channel — the fast authoring loop, with a debugger attached. Publish the same file as an image and it runs resident: confined beside a worker, over its own stdio.
Nothing in the agent changes between the two. run() picks the carriage from
the environment — the platform sets APOMESH_RUNTIME_SESSION for a resident
runtime and does not for a driver. That works because both carriages complete the
same handshake and receive the same init, so the kit is ergonomics over one
protocol rather than a compatibility layer over two.
One difference is deliberate and refused rather than hidden: a tethered session
cannot be Replayable. Replay serves a step from the journal instead of
re-executing it, which is sound only because the sandbox makes that journal
complete — code on your own machine can perform effects the platform never sees.
Asking for it at attach is a typed refusal, never a silent downgrade.
The protocol, for any language
Section titled “The protocol, for any language”The rest of this page is the workload’s side of protocol v1 — what the kit does for you, and what a peer in another language implements itself.
Three facts about the runtime environment
Section titled “Three facts about the runtime environment”These bite first, so they come first.
stdin and stdout are one socket, not two pipes. The platform hands the
workload a single socketpair as fds 0 and 1. A stray print() corrupts the
stream mid-frame, and the failure surfaces as a decode error blamed on the
next frame. Send diagnostics to stderr.
No credential ever reaches the process. A generation runs worker-side
against the session’s provider router; a tool’s secrets inject at the sandbox
boundary. APOMESH_RUNTIME_SESSION is the only variable the substrate adds, and
it is the non-secret session id you echo in the handshake. Reading the
environment for an API key is not working around a gap — it is asking for
something the platform is built never to hand you.
A restart is not visible as a restart. There is no “you were restarted”
flag. You resume by reading init.restore, which is exactly why a durable agent
reports its position rather than tracking one in memory.
Framing
Section titled “Framing”Every frame is a 4-byte big-endian length prefix followed by a JSON body.
import struct, json
MAX_FRAME_BYTES = 32 * 1024 * 1024 # a length prefix can ask for gigabytes
def encode_frame(frame: dict) -> bytes: body = json.dumps(frame).encode("utf-8") if len(body) > MAX_FRAME_BYTES: raise ValueError(f"frame body is {len(body)} bytes, over the limit") return struct.pack(">I", len(body)) + bodyCheck the declared length before consulting your buffer, so an absurd prefix is rejected rather than used to size an allocation. The platform is not the only thing that can write to a socket.
The length prefix is pinned byte-exactly by the conformance fixture. Frame JSON is not — the host decodes JSON, which has many equivalent renderings of one value, so demanding byte-identical bodies would fail conforming implementations.
The conversation
Section titled “The conversation”hello— you announce the session id fromAPOMESH_RUNTIME_SESSION, plusresume_from_seqif you are continuing.init— the platform tells you who you are, your family set, yoursession_seed, andrestore(your last snapshot, or absent on a fresh run).- calls — you send
callframes; the platform answers each with a result frame correlated bycall_id. snapshot_state— you hand over opaque bytes the platform stores and gives back asinit.restore. The substrate never decodes them.terminal— you report the outcome, or a failure class.
A call frame carries four things:
def call(call_id: str, call_seq: int, family: str, body: dict) -> dict: ...family— one of the ten families, and it must be in the set your published contract declared. Both the worker and the orchestrator check.call_seq— strictly monotone, and it names the call. Read the next section; this is the field that gets agents wrong.call_id— correlates this reply on this attachment. Opaque to the substrate; a fresh one after a restart is correct and expected.
Getting call_seq right
Section titled “Getting call_seq right”call_seq is the dedup handle. Re-sending a sequence names the same call, so
a mutating effect the substrate already applied is answered from what it did
rather than applied twice.
In the reference agent it is simply the step number. That is the shape to copy.
An agent that minted a fresh sequence on resume would spawn a second child and write a second memory entry — and the substrate would be right to let it, because a new sequence is a new call by definition. If you resume at step 6, step 6’s call carries sequence 6 again.
The corollary is why position reports exist: your snapshot is opaque to the platform, so nothing structurally stops you resuming at step 8 and numbering your next call 1. Under a stable era that collides with a real record — which the journal’s request witness catches and refuses, rather than answering wrongly.
A durable agent’s skeleton
Section titled “A durable agent’s skeleton”ch = Channel.over_stdio()ch.send(hello(announced_session(), resume_from_seq=0))init = ch.receive()
state = restore_bytes(init) # None on a fresh runstep = decode_position(state) if state else 0
while step < len(PLAN): result = ch.call(PLAN[step], call_seq=step) # sequence == step if not result_is_ok(result): ch.send(failed("Process")); return step += 1 ch.send(snapshot_state(encode_position(step)))
ch.send(terminal(outcome))The ordering that matters: report the position, then take the next expensive
step. A Replayable session serves that generation from the journal on a
restore, so the step happens and costs nothing.
Determinism
Section titled “Determinism”If your agent may be replayed, do not observe the world directly:
- Randomness — derive it from
session_seedand the step’scall_seq. The seed arrives oninitas hex text; parse it as text, not as a JSON number, or a value above 2⁵³ will silently round. - Coarse time — read the logical clock the platform stamps on results.
- Anything else — a precise timestamp, a uuid, a hostname, an env read — go
through the
ObservedValuefamily. You compute it locally, the platform records it, and every later replay is served the record.
The OS clock stays legitimate for measurements that are not control flow.
Publishing and running
Section titled “Publishing and running”A programmable shape publishes like any other agent, with a RuntimeContract
alongside the manifest: the digest-pinned image, the families set, the
durability level, the resources envelope, and the protocol_version your
image speaks. Publishing seals it into an immutable (name, version); a start
then carries only goal and budget, because the published entry is the shape.
The reference agent
Section titled “The reference agent”A complete worked implementation lives at deploy/local/reference-agent/:
| File | What it is |
|---|---|
apomesh_protocol.py | Protocol v1 from the workload’s side — framing, handshake, frame vocabulary. No agent policy. |
agent.py | Every function family, a resumable position, a mid-run HITL pause. |
Dockerfile | Stdlib only, unprivileged, no ambient credentials. |
image_proof.py | Drives the built image over real inherited fds. |
tests/ | The codec against the protocol contract, and the whole conversation against a scripted host. |
# The codec + the agent's conversationcd deploy/local/reference-agent && uv run --with pytest pytest tests/ -q
# The image over real inherited fds (requires Docker)python3 deploy/local/reference-agent/image_proof.py
# The full acceptance proof against a running stack (`just up` first)python3 deploy/local/reference-agent/stack_e2e.pyThe contract to build against
Section titled “The contract to build against”crates/apomesh-substrate-types/tests/protocol-v1-golden.json is hand-authored
and states what a conforming peer may put on the channel. Both the Rust types
and the Python codec are checked against it, never against each other — the
direction is one-way, so the Rust types own protocol v1 and everything else
mirrors the fixture. Build your own peer against that file.
Related
Section titled “Related”- Programmable agents & the function plane — the concepts.
- Durability & determinism — replay and the three tiers.
- Published agents — how a shape is sealed.