Recipe

Evented I/O primer

A practical introduction to evented I/O patterns for Meridian agents. Learn how non-blocking event loops let a single process juggle thousands of in-flight model calls, tool invocations, and webhook deliveries without thread-per-request overhead.

1.Why evented over threaded

Most agent work is I/O bound. Waiting on an LLM completion, a vector DB lookup, or a remote tool call burns wall-clock time, not CPU. A threaded model parks a kernel thread per request; an evented model parks a tiny continuation object and yields the loop. The difference shows up immediately at fan-out: 10k concurrent in-flight calls cost megabytes of heap, not gigabytes of stack.

  • Cheap suspended state per pending operation
  • Predictable single-threaded mutation of shared agent state
  • Backpressure flows naturally through the event queue

2.The loop, the queue, the callback

Every evented runtime ships three primitives: a loop that polls for readiness, a queue of ready tasks, and a callback registry that resumes suspended work. In Meridian we expose this via async iterators over the stream of tool events.

import { meridian } from "@meridian/sdk";

const agent = meridian.agent({ model: "azure/model-router" });

for await (const event of agent.stream("Summarize the inbox")) {
  if (event.type === "tool_call") {
    console.log("call", event.name, event.args);
  }
  if (event.type === "text") {
    process.stdout.write(event.delta);
  }
}

3.Cancellation and backpressure

Long-running streams need a way out. Meridian threads an AbortSignalthrough every async path so a user disconnect, a timeout, or an upstream error cleanly tears down the call chain. When the loop sees a closed signal, it drains pending callbacks and returns the agent to an idle state without leaking partial completions or orphaned tool calls.

Combined with bounded queues per tool, this gives operators a single knob to trade latency for throughput without rewriting agent code.