Session lifecycle¶
A Session is the connection-scoped runtime around one or more WireStreams. It
owns the node namespace, action registry, active action accounting, transport
buffers, and connection deadlines that make a remote agent exchange coherent.
Applications normally create a Session, attach at least one stream, and let the session route messages. Calling handlers directly bypasses the limits, cancellation, and status propagation described here.
See the Python Session API, transport API, TypeScript reference, and C++ runtime reference for language-specific entry points.
State overview¶
open
| add streams, send messages, dispatch actions
|
+-- half_close / peer clean close --> closing
| |
| | all streams fully terminate
| v
| done
|
+-- abort / peer abort / deadline --> aborted
|
| all stream state removed
v
done
“Closed” and “done” are intentionally different:
is_closed/isClosedmeans no new session work should begin because either endpoint has started shutdown;is_done/isDonemeans every attached stream has completed and the session has released its connection state.
Await done / wait_done, not merely is_closed, before destroying resources
that callbacks or stream pumps may still use.
What a Session owns¶
| Resource | Role in an agent application |
|---|---|
| NodeMap | Resolves node ids in incoming fragments and action port mappings |
| ActionRegistry | Resolves inbound action names to schemas and local handlers |
| WireStreams | Carry multiplexed action messages, fragments, and session trailers |
| Active actions | Support cancellation, shutdown, and duplicate-id checks |
| Root/nested limiters | Bound top-level calls separately from child tool/action calls |
| Buffer limits | Bound messages and bytes per stream and across the connection |
| Timers | Enforce the absolute deadline and the no-stream grace period |
You may inject a NodeMap or ActionRegistry before work starts. The setters also rebind active actions, but they do not migrate fragments already stored in the old NodeMap, and a registry change can make different phases resolve different registrations. Configure both before attaching streams or starting actions when possible; changing either mid-flight can split the connection's state.
1. Create the open session¶
Session creation validates ids, headers, byte/message limits, root and nested action concurrency, no-stream timeout, and deadline. It does not by itself open a network connection.
Session headers describe the connection as a whole. They are copied,
case-normalized, and carried in terminal half-close or abort metadata; they are
not copied onto actions. The runtime reserves x-a11-* names for protocol
metadata. Per-call values belong on Action headers, which can be forwarded into
nested work.
An open session with no streams starts its no-stream timer. This catches an agent connection that was constructed but never attached, without keeping it alive indefinitely.
2. Attach and drive streams¶
add_stream / addStream records the transport id, installs the session's
message/done callbacks, and calls exactly one endpoint method:
- start mode for the initiating/client side;
- accept mode for the responding/server side.
The startup result covers attaching and opening that stream. The session keeps pumping it afterward. Several streams may belong to one session; an explicit stream id routes a send to one of them, while an omitted id lets the runtime choose an active stream.
Each stream has independent pending-message counters, but total session limits apply across all of them. When a callback or dispatcher is slow, the stream pump waits for capacity instead of creating an unbounded queue.
3. Dispatch each WireMessage¶
One WireMessage may contain action control messages and node fragments. With the default stream callback, the session validates the encoded size and dispatches its elements into shared runtime state:
- action messages establish, cancel, or update action lifecycles;
- node fragments are applied to the NodeMap by id and sequence;
Supplying a custom on_stream_message / onStreamMessage callback replaces
that default dispatcher. This is useful for gateways and protocol inspection,
but the callback must explicitly call dispatch_wire_message /
dispatchWireMessage if it still wants ordinary action/node routing. Messages
for one stream enter the callback through a serialized pump.
Action dispatch and fragment writes retain structured failures. If one element is invalid, diagnostics identify its element kind/index rather than collapsing the whole message into an opaque transport exception.
Incoming action calls resolve their schema and handler through the registry. Before a handler runs, it acquires either a root or nested concurrency slot. Nested calls therefore cannot starve the connection's externally initiated work, and a burst of tool calls cannot create unlimited tasks.
4. Track action work¶
The session indexes active actions by call id. Tracking supports:
- rejecting duplicate active ids;
- finding or cancelling one call;
- cancelling every call during abort;
- awaiting active work during application-controlled shutdown;
- applying shared root/nested concurrency limits.
An Action removes itself after its terminal cleanup. Session half-close prevents new outbound messages and dispatches, but work already unwinding may still own nodes, callbacks, or stream state. This is another reason full session completion is a separate barrier.
5. Clean half-close¶
half_close / halfClose moves an open session into closing. The runtime:
- records an OK session status;
- packs that status under
SESSION_STATUS_HEADERwith the session trailers; - half-closes each active WireStream;
- rejects new session messages, streams, and actions;
- continues receiving and cleaning up work already admitted.
This transition is idempotent. It does not mean every stream has drained yet. Each WireStream still has its own local half-close and remote half-close, and the session remains not-done until those stream state entries are removed.
A peer cleanly ending its session reports the same OK trailer. The local session becomes closed, delivers the relevant remote half-close callback, and waits for its remaining stream direction to settle.
6. Abort with a structured failure¶
abort(non_ok_status) enters the failure path. It is appropriate when the
connection cannot safely continue: an action/session protocol violation,
deadline expiry, resource exhaustion, or an unrecoverable application failure.
Abort performs broader cleanup than half-close:
- root and nested action waiters are cancelled;
- active actions receive cooperative cancellation;
- pending inbound message buffers stop accepting work;
- the session status is serialized into trailers;
- streams receive the reserved session-abort signal or are directly aborted if that signal cannot be sent.
The stream-level abort status identifies the transport shutdown, while the session trailer retains the original application/session failure. This lets the peer distinguish “the session failed with X” from the mechanical fact that its streams were terminated.
7. Reach full done¶
The done transition occurs only after shutdown has begun and every attached stream has finished its callback/drain/cleanup path. At that point:
is_done/isDonebecomes true;done/wait_doneresolves;get_status/getStatusis the authoritative terminal session status;- no stream callback can re-enter the session.
The Python done property is event-shaped, so await session.done.wait() is
the usual connection-lifetime barrier. TypeScript returns the terminal Status
from await session.done().
Automatic terminal paths¶
No-stream timeout¶
If an open session has no attached streams for its configured grace period, it cleanly half-closes. Attaching another stream before the timer fires cancels the pending transition. This handles transient gaps while ensuring abandoned session objects do not remain open forever.
Absolute deadline¶
When the session deadline expires, the runtime aborts with
DEADLINE_EXCEEDED. Updating the deadline reschedules enforcement; clearing it
uses an infinite/no-deadline value. A per-call receive deadline is different:
it only bounds that wait and does not alter the connection lifetime.
Stream failure or peer abort¶
A normal stream failure affects that stream and its callbacks. The reserved session-abort signal, however, moves the whole session into its aborted phase, stops other stream pumps, and cancels actions so no sibling transport continues an exchange the peer considers failed.
Pull-style reception¶
SessionWithRecv replaces application stream callbacks with a bounded pull
queue. Use receive() for a single multiplexed message loop or
receive_with_stream_id() / receiveWithStreamId() when routing depends on the
source transport. A missing result means the session is fully finished; a
non-OK status reports abort/deadline failure.
Pull reception deliberately replaces automatic dispatch. The receive loop can
inspect, proxy, or explicitly pass each result to dispatch_wire_message /
dispatchWireMessage. This makes ownership visible and avoids applying the
same action or node fragment twice.
What should I await?¶
| Operation | What completion means |
|---|---|
add_stream |
That stream's startup handshake completed |
| action dispatch | The message was validated and admitted to its action lifecycle |
await_all_actions |
Currently tracked handlers reached terminal cleanup |
receive |
One inbound message arrived, the session ended, or the wait failed |
done / wait_done |
Shutdown began and all stream state is gone |
The action-specific transitions within this connection are described in the Action lifecycle.