An agent is ninety seconds into a task and it has picked the wrong file. It is confidently refactoring a module you told it to leave alone, and the change you actually wanted is one line away in a file it hasn't opened yet. You're watching this scroll past in a terminal. What can you do about it?
If you built the integration as a function call — send a prompt, await a result — the answer is nothing. You wait for it to finish being wrong, then send a correction and pay for the whole detour in tokens and wall-clock time. If you built it as a session you push events into, you have a better move: interrupt it right now, hand it the correct direction, and let it pivot without discarding the context it already gathered.
That gap — call versus session — is the entire design decision behind event-driven agents. It's worth taking seriously before you wire an agent into anything that matters.
Two shapes for talking to an agent
The Claude Agent SDK ships both shapes, and putting them side by side makes the tradeoff concrete.
Single-message input is a one-shot. You pass a prompt, iterate the results, and you're done. It's friendly to stateless environments — a Lambda, a cron job — but it explicitly gives up image attachments, dynamic message queueing, and real-time interruption. Once the query is running, you are a spectator until it returns.
Streaming input is the SDK's recommended mode, and the reason is structural rather than cosmetic. You feed the agent messages from an async generator, so it runs as a long-lived process that takes input, handles interruptions, surfaces permission requests, and keeps session state alive across turns. You can queue a second message while the first is still executing. You can attach a diagram mid-conversation. You can cancel. The agent stops behaving like a function and starts behaving like a process you're in dialogue with — and, notably, it keeps full access to its tools and custom MCP servers for the whole session, not just the first turn.
The Managed Agents API takes that same idea and moves the loop onto managed infrastructure. It's the clearest illustration of the pattern, so it's worth looking at directly.
The session as an event bus
Managed Agents is a hosted harness: Anthropic runs the agent loop, the sandbox, and tool execution, and you talk to it over events. The docs are blunt about the model:
Communication with Claude Managed Agents is event-based. You send user events to the agent, and receive agent and session events back to track status.
Everything flows through two directions of typed events. User events and system events go in — user.message kicks off or continues work, system.message rewrites the system prompt between turns. Session, span, and agent events come back out for observability. The type strings follow a plain {domain}.{action} convention, and every persisted event carries a processed_at timestamp — null means it's queued behind events still being processed.
Sending is one call. In Python:
client.beta.sessions.events.send(
session.id,
events=[
{"type": "user.message",
"content": [{"type": "text", "text": "Analyze the sort function in utils.py"}]},
],
)
Now back to that agent chasing the wrong file. Steering it is the same primitive with a different event in front:
# Agent is busy on the wrong thing. Stop it, then redirect.
client.beta.sessions.events.send(
session.id,
events=[
{"type": "user.interrupt"},
{"type": "user.message",
"content": [{"type": "text", "text": "Instead, focus on the bug on line 42."}]},
],
)
The agent acknowledges the interruption and switches to the new task, carrying its accumulated context with it. That's the capability a request/response wrapper can't give you at any price: mid-flight control.
Reading the stream back out
The inbound half is easy. The outbound half has one ordering rule that will bite you if you miss it: open the stream before you send the event. Only events emitted after the stream opens are delivered, so sending first and subscribing second is a race you'll lose intermittently and never reproduce on demand.
with client.beta.sessions.events.stream(session.id) as stream:
client.beta.sessions.events.send(session.id, events=[
{"type": "user.message",
"content": [{"type": "text", "text": "Summarize the repo README"}]},
])
for event in stream:
match event.type:
case "agent.message":
for block in event.content:
if block.type == "text":
print(block.text, end="")
case "session.status_idle":
break
case "session.error":
break
Two more details separate a toy from something you'd run in production. First, reconnection: to rejoin a session without missing or duplicating output, open a new stream, list the event history to seed a set of seen IDs, then tail the live stream and skip anything you've already seen. The event IDs are your idempotency key. Second, previews: if you opt a connection into event deltas (event_deltas[], accepting agent.message and agent.thinking), you'll get incremental event_start / event_delta events for a live typing effect. Treat them as decoration only — the buffered agent.message is always the authoritative record, and the delta wire format deliberately differs from the Messages API, so accumulator code doesn't port over unchanged.
Why this belongs in your SDLC
The reason to care isn't the chat demo. It's that "events in, events out" lets things other than a human drive the agent. A user event is just JSON on a session — it can be produced by a CI job that just went red, a monitor that tripped, a webhook from your issue tracker, or a reviewer clicking approve on a permission prompt. The agent reaches outward through its tools and MCP servers; the event stream is how the outside world reaches back in and steers.
That inverts the usual integration. Instead of a script that calls an agent and blocks, you get a long-running participant in your delivery pipeline that you can start on a schedule, feed live signals to, correct when it drifts, and interrupt when priorities change — all while its sandbox and conversation history persist server-side.
The takeaway: design the integration around the session, not the call. Open the stream first, key everything off event IDs so reconnects are safe, keep user.interrupt wired as a first-class control rather than an afterthought, and trust the buffered event over any live preview. Build those four in from the start and an agent stops being a black box you fire and wait on — it becomes a process you can actually operate.
Sources: Session event stream — Claude Docs, Claude Managed Agents overview, Streaming Input — Claude Agent SDK