The Runtime (L1)¶
What this is¶
The kernel page on runtime contracts describes a shape — a handful of pure Protocols (EventLog, Journal, Inbox, Scheduler, Supervisor, SignalBus, FollowGraph, FanoutStrategy) with no I/O, no sockets, no database. This page describes the first real machine that fills in that shape.
It is the Stage-0, in-process implementation: everything runs inside one Python process, on one asyncio event loop, with no serialization — a leased run is just an asyncio.Task that stays alive in memory. There is no Postgres, no Redis, no network. You can pip-free, docker-free run a durable agent loop on your laptop with nothing but python.
This is the durable engine, from the inside
Durability (concept) tells the story — what a crash looks like, how replay avoids re-charging the card. The kernel runtime contracts are the interfaces that make the story possible. This page is the working code that drives them in development. Read those two first for the "why" and the "what, precisely" — here we cross-link heavily and try not to repeat.
The one idea that makes it all worth it¶
The agent author writes async def run(self, ctx, inbox) once. Every ctx.llm(), ctx.tool(), ctx.spawn() call is journaled under the hood. That same agent code runs unchanged against the Postgres/Redis backends in infrastructure/runtime/ — you swap the backends, not the agent. Dev and production are literally the same call sites.
# Dev (Stage 0) — everything in-process, zero infra
async with Runtime() as rt:
await rt.register(my_agent)
run_id = await rt.submit(my_agent.id, boot_msg)
# Production (Stage 1) — durable backends injected by the infra factory
from ravi.infrastructure.runtime import build_postgres_runtime
async with build_postgres_runtime(postgres_url=..., redis_url=...) as rt:
await rt.register(my_agent)
run_id = await rt.submit(my_agent.id, boot_msg) # identical
Here is the cast this page covers, each with a one-line analogy.
| Piece | One-liner | Analogy |
|---|---|---|
Runtime | Facade that wires backends + the Worker, and the public API | the building manager who hooks up power, mail, and staff |
Worker | The run loop: lease → run → file paperwork → release | a driver who leases a job, does it, files the paperwork |
RunContext | The journaled ctx an agent receives for one run | the agent's toolkit + notebook for this one job |
| in-memory backends | Single-process stand-ins for the eight kernel Protocols | a single office standing in for the citywide postal system |
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E8EAF6','primaryTextColor': '#1A237E','primaryBorderColor': '#3949AB','lineColor': '#546E7A','fontSize': '13px'}}}%%
flowchart TB
classDef runtime fill:#E3F2FD,stroke:#1565C0,color:#0D47A1,font-weight:bold
classDef agent fill:#E8EAF6,stroke:#3949AB,color:#1A237E,font-weight:bold
classDef store fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C,font-weight:bold
C(["Caller — rt.submit(agent_id, msg)"]):::agent
subgraph RT["Runtime facade"]
WK["Worker<br/>poll - lease - run - release"]:::runtime
CTX["RunContext<br/>(built fresh per run)"]:::agent
end
subgraph BK["In-memory backends (asyncio, no serialization)"]
EL[("InMemoryEventLog")]:::store
JN[("InMemoryJournal")]:::store
INB["InMemoryInbox"]:::runtime
SCH["InMemoryScheduler"]:::runtime
SUP["InMemorySupervisor"]:::runtime
SIG["InMemorySignalBus"]:::runtime
end
AG["agent.run(ctx, inbox)"]:::agent
C -->|"deliver - enqueue"| INB
C --> SCH
SCH -->|"lease run"| WK
WK -->|"builds"| CTX
WK -->|"calls"| AG
AG -->|"uses"| CTX
CTX -->|"append entries"| EL
CTX -->|"journal effects"| JN
CTX -->|"spawn - join"| SUP
CTX -->|"ask - sleep"| SIG
WK -->|"heartbeat - release"| SCH The Runtime facade¶
What & why: Runtime is one object you create, hand a few agents, and submit messages to. It owns the eight backends (defaulting each to its in-memory implementation), wires the inbox wakeup hook, and starts a single Worker. Think of it as the building manager: it connects the power, hangs the mailboxes, hires the one driver, and gives you a front desk to drop off work.
It lives at agents/runtime/runtime.py and is exported as Runtime.
Construction — inject or default¶
Every backend is an optional constructor argument. Pass nothing and you get the full in-memory stack. Pass durable ones and the same Runtime runs against Postgres/Redis — this is exactly what build_postgres_runtime does.
class Runtime:
def __init__(
self,
*,
event_log: EventLog | None = None,
inbox: Inbox | None = None,
journal: Journal | None = None,
scheduler: Scheduler | None = None,
signal_bus: SignalBus | None = None,
follow_graph: FollowGraph | None = None,
fanout: FanoutStrategy | None = None,
) -> None:
self._event_log = event_log or InMemoryEventLog()
self._journal = journal or InMemoryJournal()
self._scheduler = scheduler or InMemoryScheduler()
self._inbox = inbox or InMemoryInbox()
self._inbox.set_deliver_hook(self._on_inbox_deliver) # the wakeup wire
...
Why Runtime never imports a durable backend
Notice it only ever names InMemory* classes — it never imports PostgresEventLog or RedisJournal. That keeps the agents layer (L1) strictly above infrastructure. The durable backends are injected from the outside by the infra-layer factory, so the dependency rule (agents may not reach sideways into infrastructure) is never violated.
The public API¶
Four verbs, plus lifecycle. All return fast — work happens on the Worker.
| Method | What it does |
|---|---|
register(agent) | Add an agent to the registry so the Worker can dispatch runs to it |
submit(agent_id, msg, *, priority, tenant, max_retries) | Deliver msg and enqueue a fresh run — returns the new RunId |
follow(follower, topic_type, topic_source) | Subscribe an agent to a topic (newsletter sign-up) |
publish(topic_type, topic_source, msg) | Fan msg out to every follower of a topic |
start() / stop() / cancel(run_id) | Lifecycle — also exposed via async with |
submit and the duplicate-run trap¶
submit does the delivery and the enqueue itself. The subtle bit is notify=False:
async def submit(self, agent_id, msg, *, priority=5, tenant="default", max_retries=3):
run_id = new_run_id()
self._scheduler.register_run(run_id, agent_id)
# notify=False: submit() enqueues its OWN run below. If the deliver-hook
# also fired, it would find no active run yet and spawn a DUPLICATE.
await self._inbox.deliver(agent_id, msg, notify=False)
await self._scheduler.enqueue(run_id, priority=priority, tenant=tenant,
retry_policy=RunRetryPolicy(max_retries=max_retries))
return run_id
notify=False is load-bearing, not an optimization
The deliver-hook exists for unsolicited deliveries — a publish fan-out, an inter-agent ask, a child's boot message — that arrive with no accompanying submit. When submit is the one delivering, it suppresses the hook so the explicit enqueue is the only run created. Forgetting this spawns two runs for one message.
The inbox deliver-hook: wake vs spawn¶
When a message lands in a dormant agent's mailbox via the normal path (notify=True), the Inbox calls a sync hook, which schedules _handle_inbox_delivery. That method makes the central wake-vs-spawn decision using scheduler.find_run_for_agent() — the same API on both the in-memory and Postgres schedulers, so no private attributes are touched.
async def _handle_inbox_delivery(self, agent_id: AgentId) -> None:
result = await self._scheduler.find_run_for_agent(agent_id)
if result is None:
await self._spawn_run_for_inbox(agent_id) # no active run -> spawn fresh
return
run_id, status = result
if status == RunStatus.SUSPENDED:
await self._scheduler.wake_suspended(run_id) # dormant run -> wake it
# PENDING / RUNNING -> no-op: the active run will drain this message itself
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD','primaryTextColor': '#0D47A1','primaryBorderColor': '#1565C0','lineColor': '#546E7A','fontSize': '13px'}}}%%
flowchart TD
classDef runtime fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
classDef decision fill:#FFF3E0,stroke:#E65100,color:#BF360C,font-weight:bold
classDef store fill:#F3E5F5,stroke:#6A1B9A,color:#4A148C
A(["Inbox.deliver(notify=True)"]):::runtime --> B["_on_inbox_deliver schedules<br/>_handle_inbox_delivery"]:::runtime
B --> C{"find_run_for_agent<br/>returns a run ?"}:::decision
C -->|"None — no active run"| D["spawn a fresh run<br/>register + enqueue"]:::runtime
C -->|"SUSPENDED"| E["wake_suspended(run_id)<br/>(dormant - back to PENDING)"]:::runtime
C -->|"PENDING or RUNNING"| F["no-op — the live run<br/>drains this message"]:::store Start / stop as a context manager¶
start() constructs the InMemorySupervisor (it needs references to the EventLog, Inbox, Journal, and Scheduler) and the single Worker, then starts the Worker's poll loop. async with calls start/stop for you.
async with Runtime() as rt: # -> start() : builds Supervisor + Worker, starts poll loop
await rt.register(agent)
await rt.submit(agent.id, msg)
... # Worker drives runs in the background
# -> stop() : cancels the poll loop and all in-flight agent tasks
The Worker — the run loop¶
What & why: The Worker is the engine that turns a queued run into a running agent. It polls the Scheduler for leased runs, builds a RunContext, drains the inbox, calls agent.run(...), and — win, lose, or cancel — files the paperwork (append a terminal log entry, ack/nack messages, release the lease).
It lives at agents/runtime/worker.py.
Analogy — the driver who leases a job
A dispatcher hands the driver a job slip (the lease). The driver does the work, radios in periodically so the dispatch doesn't reassign the job (the heartbeat), and at the end files the trip report (the terminal log entry) and hands the slip back (the release). The driver never becomes the job — each job is its own asyncio Task.
The poll loop¶
_poll_loop runs every POLL_INTERVAL (0.05 s). Each tick it leases up to capacity=10 runs and launches each as its own asyncio.Task. Many agents run concurrently because each is an independent task; the lease capacity bounds how many start per tick.
async def _poll_loop(self) -> None:
while self._running:
leases = await self._scheduler.lease(worker_id=self._worker_id, capacity=10)
for lease in leases:
agent = self._registry.get(lease.agent_id)
if agent is None:
continue # not registered yet — hold the lease, it expires + retries
task = asyncio.create_task(self._run_agent(lease, agent))
self._tasks[lease.run_id] = task
await asyncio.sleep(self.POLL_INTERVAL)
One run, start to finish (_run_agent)¶
This is the heart of the page. For one leased run, the Worker:
- Builds a fresh
RunContext— wiring in all backends plus the agent's LLM client and aToolInvokerbuilt from the agent's declared tools (_build_tool_invoker). - Appends
run.startedto the EventLog iflast_seq < 0(fresh run). - Drains the inbox for this agent (up to 100 messages).
- Dispatches lifecycle hooks (
RUN_START) and runs through the agent's middleware pipeline if present, otherwise callsagent.rundirectly. - Heartbeats in the background every 15 s so a long LLM call never loses a Postgres lease (the in-memory scheduler's
heartbeatis a no-op). - On success —
ackevery drained message, optionally clear run-scoped history (_maybe_clear_run_history), appendrun.completed,releasewithCOMPLETED, and record completion with the Supervisor (this is what wakes a parent waiting injoin). - On cancel (
asyncio.CancelledError/CancellationError) — appendrun.cancelled, releaseCANCELLED. - On any other exception — branch: a guardrail stop (
MiddlewareTermination) or budget stop (BudgetExhaustedError) is expected, so itacks the messages and logsrun.failedwith a friendly status; a realAgentCrashErrornacks the messages (so the run can be retried) and logs the crash.
async def _run_agent(self, lease, agent):
ctx = RunContext(meta=meta, event_log=..., journal=..., inbox=..., ...,
llm_client=getattr(agent, "model", None),
tool_invoker=self._build_tool_invoker(agent), agent=agent)
if (await self._event_log.last_seq(run_id)) < 0:
await self._event_log.append(run_id, RunLogEntry(..., kind="run.started"),
expected_seq=-1)
inbox_msgs = await self._inbox.drain(agent.id, max=100)
heartbeat_task = asyncio.create_task(_heartbeat()) # renews lease every 15s
try:
if middleware is not None:
await middleware.execute(ctx, lambda c: agent.run(c, inbox_msgs))
else:
await agent.run(ctx, inbox_msgs)
for msg in inbox_msgs:
await self._inbox.ack(agent.id, msg.id)
await self._event_log.append(run_id, RunLogEntry(..., kind="run.completed"), ...)
await self._scheduler.release(lease, status=RunStatus.COMPLETED)
await self._supervisor.record_completion(run_id, RunStatus.COMPLETED)
except (asyncio.CancelledError, CancellationError):
... # append run.cancelled, release CANCELLED
except Exception as exc:
... # guardrail/budget -> ack + run.failed ; crash -> nack + run.failed
finally:
heartbeat_task.cancel()
... # dispatch RUN_END hook, drop run from _tasks / _tokens
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#E8EAF6','actorBorder': '#3949AB','actorTextColor': '#1A237E','noteBkgColor': '#FFFDE7','noteBorderColor': '#F57F17','signalColor': '#546E7A','fontSize': '12px'}}}%%
sequenceDiagram
autonumber
participant SCH as InMemoryScheduler
participant WK as Worker
participant EL as EventLog
participant INB as Inbox
participant AG as agent.run
participant SUP as Supervisor
WK->>SCH: lease(worker_id, capacity=10)
SCH-->>WK: [Lease(run_id, agent_id)]
WK->>EL: append(run.started, expected_seq=-1)
WK->>INB: drain(agent_id, max=100)
INB-->>WK: list[Message]
WK->>AG: run(ctx, inbox_msgs)
Note over WK,SCH: long LLM call in flight — heartbeat every 15s
WK->>SCH: heartbeat(lease)
AG-->>WK: returns None (success)
WK->>INB: ack(each msg_id)
WK->>EL: append(run.completed)
WK->>SCH: release(lease, status=COMPLETED)
WK->>SUP: record_completion(run_id, COMPLETED)
Note right of SUP: wakes any parent waiting in join() External cancel: Runtime.cancel -> Worker.cancel
cancel(run_id) marks the run CANCELLED in the scheduler, cancels its asyncio Task, and trips its CancellationToken so an agent inside a long loop bails at its next ctx.check(). If no task exists yet, it appends run.cancelled to the log directly and records completion — so a cancel never leaves a run dangling.
RunContext — the agent's toolkit and notebook¶
What & why: RunContext is the concrete ctx passed to agent.run(ctx, inbox). It satisfies the kernel's minimal AgentRunContext Protocol but offers the full rich surface an agent author actually uses: ctx.llm(), ctx.tool(), ctx.spawn(), ctx.join(), ctx.ask(), ctx.send(), ctx.emit(), ctx.check(), and more. Every effectful call is journaled, so a replay never re-bills the model or re-sends an email.
It lives at agents/runtime/context.py.
Analogy — the toolkit and the notebook
ctx is a fresh toolkit handed to the agent for this one job: a phone to call the model (llm), a set of wrenches (tool), a way to hire helpers (spawn / join), and a notebook (the EventLog + Journal) where every action is logged. If the job restarts, the agent re-reads the notebook instead of re-doing the expensive work.
_step_seq and the at-most-once helper¶
The context keeps a counter _step_seq that starts at 0 and bumps on every journaled operation. That counter is the namespace for effect ids: step N in a run always computes the same effect_id, so on replay it finds the journal hit and skips the work. The generic helper is _journaled():
async def _journaled(self, kind, args, fn):
effect_id = Effect.make_id(self.run_id, self._step_seq, kind, args)
self._step_seq += 1
cached = await self._journal.lookup(effect_id)
if cached:
if cached.status == "error":
raise RuntimeError(cached.value.get("error", "journaled error"))
return cached.value # HIT — return cached, do NOT re-run
try:
result = await fn() # MISS — run the real effect
await self._journal.record(EffectResult(effect_id=effect_id, status="ok",
value=result or {}))
return result
except Exception as exc:
await self._journal.record(EffectResult(effect_id=effect_id, status="error",
value={"error": str(exc)}))
raise
This is the same three-step lookup -> execute -> record dance described in Durability. ctx.llm() and ctx.tool() inline the same pattern (so they can stream and serialize their own results) rather than calling _journaled directly, but the contract is identical.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#E3F2FD','primaryTextColor': '#0D47A1','primaryBorderColor': '#1565C0','lineColor': '#546E7A','fontSize': '13px'}}}%%
flowchart TD
classDef process fill:#E8EAF6,stroke:#3949AB,color:#1A237E
classDef decision fill:#FFF3E0,stroke:#E65100,color:#BF360C,font-weight:bold
classDef cache fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20,font-weight:bold
START(["ctx.llm() / ctx.tool() begins"]) --> ID["effect_id = Effect.make_id<br/>(run_id, step_seq, kind, args)<br/>step_seq += 1"]:::process
ID --> LOOK{"Journal.lookup(effect_id)"}:::decision
LOOK -->|"HIT — already done"| RET["return cached result<br/>(model NOT re-called)"]:::cache
LOOK -->|"MISS"| EXEC["run the real effect<br/>(stream LLM / invoke tool)"]:::process
EXEC --> REC["Journal.record(result)"]:::process
REC --> LOG["append tool.result / llm.call<br/>to EventLog"]:::process
LOG --> RET2["return result"]:::process The capability surface¶
ctx call | What it does | Journaled? |
|---|---|---|
ctx.llm(messages, *, options) | Stream a model response, emit text.delta log entries, return LLMResponse | yes — replay never re-bills |
ctx.tool(name, **args) | Invoke a tool via the ToolInvoker, return InvocationResult | yes — at-most-once side effect |
ctx.spawn(child_agent, *, boot, supervision) | Spawn a child run, return a RunHandle (does not wait) | yes (in Supervisor) |
ctx.join(handle) | Suspend until the child reaches a terminal state, return its RunResult | suspend point |
ctx.cancel(handle, *, reason) | Cancel a child run and its whole subtree | — |
ctx.ask(target, msg, *, timeout) | Send and suspend until a reply / timeout / target failure, return AskOutcome | suspend point |
ctx.reply(to, result) | Signal the asker's run to complete its ask | — |
ctx.send(target, msg) | Fire-and-forget delivery + wake the target | — |
ctx.emit(topic, msg) | Publish to all followers of a topic | — |
ctx.follow(topic) / ctx.unfollow(topic) | Subscribe / unsubscribe this run | — |
ctx.status(handle) | One-shot peek at a run's progress (RunStatusSummary) — not a stream | — |
ctx.sleep_until_signal(name) / ctx.sleep_until(dt) | Suspend until a named signal / wall-clock time | suspend point |
ctx.now() / ctx.random() / ctx.uuid() | Journaled non-determinism — use these, not the stdlib equivalents | yes — replay-safe |
ctx.check() | Cooperative cancellation point — raises if cancelled / deadline exceeded | — |
Use ctx.now() / ctx.random() / ctx.uuid(), never the stdlib
A plain datetime.now() or uuid.uuid4() produces a different value on replay, which corrupts the deterministic fold. The journaled helpers record the first value and return it on every replay — your code becomes replay-safe for free.
How ask and sleep actually suspend (Stage 0)¶
In Stage 0 the coroutine is never serialized — it stays alive as an asyncio Task. So ctx.ask() and ctx.sleep_until_signal() suspend by awaiting an asyncio.Event inside the InMemorySignalBus, the lightweight in-process stand-in for the durable Wakeup mechanism. ask sets reply_to and correlation_id, delivers to the target, then waits on a reply:{id} signal:
async def ask(self, target, msg, *, timeout, idempotency_key=None):
enriched = msg.model_copy(update={"reply_to": self.run_id, "correlation_id": cid})
await self._inbox.deliver(target_agent, enriched)
await self._scheduler.wake_agent(target_agent) # or wake_suspended(run)
try:
payload = await self._signal_bus.wait_for_signal(self.run_id, f"reply:{cid}",
timeout=timeout)
return AskOutcome(kind="replied", result=..., last_seq=0)
except asyncio.TimeoutError:
# peek target status -> timed_out vs target_failed vs target_cancelled
return AskOutcome(kind=kind, handle=handle, last_seq=last_seq)
Stage 1 changes the suspend mechanism, not your code
In production the suspended run is dropped from RAM entirely and resumed from the EventLog when a real Wakeup arrives — so a three-hour wait costs nothing (see SUSPENDED is the superpower). The agent author's await ctx.ask(...) line is byte-for-byte unchanged.
The in-memory backends¶
Each backend is a single-process, single-event-loop implementation of one kernel Protocol. None is crash-durable — that is the whole point of Stage 0: zero infra, fast tests, identical surface. They all live in agents/runtime/backends/.
Analogy — one office vs. the citywide postal system
Each in-memory backend is a single office doing by hand what the production backend does at city scale. The office's front counter (the Protocol methods) is identical to the post office's, so when you swap in the real thing nobody has to learn a new counter.
| In-memory backend | Implements kernel Protocol | Production counterpart (infrastructure/runtime/) |
|---|---|---|
InMemoryEventLog | EventLog | PostgresEventLog (append-only table, (run_id, seq) PK) |
InMemoryJournal | Journal | RedisJournal (keyed by effect_id, TTL) |
InMemoryInbox | Inbox | PostgresInbox (durable queue + dead-letter) |
InMemoryScheduler | Scheduler | PostgresScheduler (SELECT … FOR UPDATE SKIP LOCKED + leases) |
InMemorySupervisor | Supervisor | (still in-memory in Stage 1 — runs over the durable EventLog/Journal) |
InMemorySignalBus | SignalBus | (still in-memory in Stage 1) |
PushAllFanout | FanoutStrategy | (still in-memory — Stage 3 adds a push/pull hybrid) |
InMemoryFollowGraph | FollowGraph | (still in-memory in Stage 1) |
The factory only swaps four backends today
build_postgres_runtime injects PostgresEventLog, PostgresInbox, PostgresScheduler, and RedisJournal (falling back to InMemoryJournal when no redis_url is given). Supervisor, SignalBus, Fanout, and FollowGraph keep their in-memory implementations — they ride on top of the now-durable EventLog and Journal. Because everything is behind a Protocol, hardening those four is a drop-in swap later.
A one-line tour:
InMemoryEventLog— adict[run_id, list[RunLogEntry]].appendenforces optimistic concurrency (raisesConcurrentAppendErrorwhenexpected_seqdoesn't match the real tail),readyields a finite slice,tailyields then blocks on anasyncio.Eventfor new entries (this powers live SSE streaming).InMemoryJournal— adict[effect_id, EffectResult].recordusessetdefault, so it is write-once: the first result wins and a racing replay can never overwrite it.InMemoryInbox— per-agent message store with dedup byMessage.id, per-sender FIFO ordering, and a retry counter that dead-letters aftermax_retriesnacks. Holds theon_deliverwakeup hook the Runtime wires in.InMemoryScheduler— anasyncio.PriorityQueue(lower number = higher priority) plus status/lease/agent maps.enqueuecoalesces (a run already pending is not added twice),leasehands out leases with a 30 s expiry,heartbeatis a no-op (single process), andreleasere-enqueuesFAILEDruns per theirRunRetryPolicy. It ownsfind_run_for_agent— the wake-vs-spawn decider.InMemorySupervisor—spawnjournals the child run id (so replay returns the same child and never duplicates it), delivers the boot message withnotify=False, and logschild.spawnedin the parent's EventLog.joinawaits anasyncio.Eventset byrecord_completion;cancelrecurses through the subtree.InMemorySignalBus—run_id -> name -> (asyncio.Event, payload_box).signalfired before a run waits is buffered (delivered eagerly), so a signal is never lost.timerschedules anasyncio.sleepthat fires the__timer__signal. Exposes the non-Protocolwait_for_signalhelper used byaskandsleep_until_signal.PushAllFanout—publishsimplyasync for follower in graph.followers_of(topic): inbox.deliver(follower, msg). Fine for normal agents; Stage 3 swaps a pull model for celebrity agents.InMemoryFollowGraph— twodefaultdicts (followers_ofandfollowing) keyed by"{type}/{source}";followis idempotent (a set),unfollowis safe on an absent subscription.
Where this lives¶
| Piece | Location |
|---|---|
Runtime facade | agents/runtime/runtime.py |
Worker run loop | agents/runtime/worker.py |
RunContext (the journaled ctx) | agents/runtime/context.py |
InMemoryEventLog | agents/runtime/backends/_event_log.py |
InMemoryJournal | agents/runtime/backends/_journal.py |
InMemoryInbox | agents/runtime/backends/_inbox.py |
InMemoryScheduler | agents/runtime/backends/_scheduler.py |
InMemorySupervisor | agents/runtime/backends/_supervisor.py |
InMemorySignalBus | agents/runtime/backends/_signal_bus.py |
PushAllFanout | agents/runtime/backends/_fanout.py |
InMemoryFollowGraph | agents/runtime/backends/_follow_graph.py |
| The kernel Protocols these implement | kernel/runtime/ (contracts page) |
| Postgres / Redis backends + factory | infrastructure/runtime/ |
Next: Context, Compaction & Memory Backends — how an agent's conversation history is stored, trimmed, and summarised so a long run fits in the model's context window.