Session Operation Streaming

Context

Session operations are public only through the persistent WebSocket. Each send frame is backed by the private streaming RPC below; unary and NDJSON operation endpoints have been removed.

This design adds a generic, ordered live event stream for one Session operation. It is not an agent-specific API: Runtimes may use it for command output, progress reporting, or interactive agent turns.

Goals

  • Let a caller receive accepted, progress, output, terminal-result, and error events as an operation runs.
  • Preserve the owner runtimed’s FIFO queue and existing cancellation, authorization, assignment fencing, and operation timeout semantics.
  • Keep Runtime Server and runtimed ports private; the Runtime gateway is the only public data-plane endpoint.
  • Use browser- and CLI-friendly streaming transports that work with bearer authentication.
  • Bound every Runtime-emitted event without introducing an unbounded event buffer.

Non-goals for the first delivery

  • Durable event retention or replay after a Runtime Pod is lost.
  • Durable operation replay after a client disconnect. A live operation may continue after its connection closes, subject to its ordinary timeout and the Session lease, but the disconnected client has no retained event cursor.
  • Runtime-defined bidirectional user input or approval. The WebSocket transport supports client cancellation, but further messages require a separate Runtime protocol extension and operation admission design.
  • Persisting high-frequency events in Run.status, Kubernetes Events, or container logs. Runtimed structured logs remain the audit path.

Durable operation records, idempotent turn submission, and cursor-based replay remain tracked by #36 . The live stream is deliberately a compatible foundation for those additions rather than claiming to solve recovery semantics prematurely.

API

The Runtime gRPC contract adds a server-streaming method:

rpc StreamSessionOperation(SessionOperationRequest)
    returns (stream SessionOperationEvent);

SessionOperationEvent has a strictly increasing per-operation sequence number and a oneof payload:

  • accepted: the owner runtimed admitted the operation to its FIFO queue;
  • output: bounded stdout or stderr bytes for a command Runtime;
  • progress: a Runtime-defined, bounded event (text_delta, tool_call_started, tool_call_finished, or status), with a typed kind, optional tool identity, and JSON payload;
  • completed: the same bounded SessionCommandResult returned by the unary operation, when applicable;
  • failed: a terminal gRPC-compatible code and safe message.

The owner runtimed—not the gateway—assigns the sequence numbers. It forwards the Runtime Server event stream while its queue entry is active. This keeps the queue’s mutation ordering intact even when a gateway request lands on a non-owner Runtime Pod and is forwarded once to the owner.

The public endpoint is:

GET /v1/namespaces/{namespace}/runtimes/{runtime}/sessions/{runUID}/operations:ws
Upgrade: websocket

Clients submit a JSON send frame with one operation and receive JSON event frames in sequence order. cancel cancels the active operation and heartbeat renews an enabled lease.

Clients use fetch and consume response.body as a ReadableStream, which allows the same bearer-token headers used by all other gateway operations. A CLI consumes JSON lines directly. A request that cannot be authorized or admitted fails before any response event with the existing HTTP error mapping. Once event bytes have been written, a terminal failure is represented by a failed event because HTTP status cannot safely change mid-stream.

The Session connection’s internal transport is the existing operation WebSocket:

GET /v1/namespaces/{namespace}/runtimes/{runtime}/sessions/{runUID}/operations:ws
Upgrade: websocket

The HTTP upgrade is authenticated and authorized before the connection is accepted. Browser requests with an Origin header must be same-origin; native clients without Origin continue to use bearer-token or client-certificate authentication in the upgrade request. Opening a connection does not submit an operation. The first version allows one in-flight operation per connection; this matches one agent turn at a time and does not invent operation multiplexing semantics.

The SDK’s Send writes an internal frame like this:

{
  "type": "send",
  "idempotencyKey": "optional-client-key",
  "operation": {
    "command": {"shell": "classify and label issue #123"}
  }
}

The gateway forwards operation through the existing operation admission path. It returns an ordered SessionOperationEvent in each server text frame. The first accepted event provides the server-generated operation ID, which the SDK returns from Send; Receive exposes subsequent events without exposing frames or WebSocket details:

{
  "sequence": 1,
  "type": "accepted",
  "accepted": {"operationID": "op-7b7b"}
}

During the operation, Cancel(operationID) writes the internal frame {"type":"cancel","operationID":"op-7b7b"}. The gateway rejects a cancellation whose ID is not the current operation. A second send before a terminal event is a connection protocol error. The SDK sends {"type":"heartbeat"} application frames to renew a configured lease. Arbitrary interactive input or approval messages are not yet part of the Runtime protocol.

An error before WebSocket upgrade is an ordinary HTTP error. An error after upgrade is sent as {"type":"error","error":"..."}. The connection may remain open for another operation; an HTTP status cannot change after a successful upgrade.

The HTTP representation uses lower-case protocol values: output stream is stdout or stderr; progress kind is status, text_delta, tool_call_started, or tool_call_finished. Binary data fields are standard JSON base64 strings.

SDK session connection

The SDK is an agent-sandbox API, not an HTTP transport wrapper. It exposes one Session connection model in both Go and Python:

Runtime.AcquireSandbox (create Session Run) -> assigned Runtime Pod
-> Sandbox.OpenSession -> Send / Receive / Cancel -> Session.Close
-> Sandbox.Release (stop/delete Session Run)

Sandbox is the SDK object and its one Session is represented by one session-mode Run. AcquireSandbox creates that Run and waits for successful scheduling plus registration before returning an active Sandbox. A Sandbox supports exactly one Session in this version.

Capacity is acquired atomically when AcquireSandbox creates the Run and the scheduler assigns it to a Runtime Pod. OpenSession only opens a streaming connection to that ready Session; it never creates a Run or changes capacity.

The public data-plane vocabulary is transport-independent:

OperationMeaning
Sendsubmit one Session operation and return its operation ID
Receivereceive the next ordered event, including its operation ID
Cancelrequest cancellation of one submitted operation
Session.Closeclose only the streaming connection
ReleaseSandboxstop the Session, wait for cleanup, then delete its Session Run

The SDK does not expose Stream, Resume, StreamWebSocket, NDJSON, or WebSocket names. Internally it maintains a persistent, authenticated, bidirectional WebSocket connection. Client messages carry an operation request or cancellation; server messages carry the ordered SessionOperationEvent and its operation ID. A gateway error after upgrade is surfaced as a typed SDK transport error rather than as an event.

SDK constructors derived from a Kubernetes REST configuration use its TLS CA bundle and optional client certificate/key for the connection, trust the Run.status.endpoint.caBundle when provided, and apply Kubernetes REST authentication wrappers (including rotating token files and exec credentials) before the upgrade. A custom SDK transport must implement this connection boundary itself; an HTTP RoundTripper alone cannot upgrade a WebSocket. Scoped Console port-forward adapters rewrite the connection endpoint as they do regular HTTP endpoints and preserve caller credentials.

The first version does not automatically retry or replay an operation after a transport failure. The operation outcome is then unknown; the caller may open a new connection before the Session lease expires and inspect Session state. Durable operation replay is separate from this live-connection contract.

Lifecycle, cancellation, and bounds

The gateway authorizes the connection against its Session Run. Cancel, gateway shutdown, immediate Session termination, and the effective operation timeout cancel an active Runtime Server operation and free the queue entry. Closing the SDK Session connection only detaches the event consumer; it does not cancel admitted work or release Session capacity. The owner runtimed continues to enforce the operation timeout and Session lease. Drain accepts an already admitted operation and rejects new ones as it does today.

Runtimed limits each Runtime-emitted event. It forwards events directly rather than accumulating an unbounded response buffer. An over-limit or malformed event terminates the operation with a resource-limit failure. Runtime-provided progress must not contain credentials, command stdin, or unbounded tool output. The existing response-size limit continues to apply to terminal command results and each gateway JSON line.

Compatibility and rollout

This is a breaking Session API change: ExecuteSessionOperation, operations:execute, operations:stream, and operation-resume HTTP endpoints are removed. Built-in Runtimes implement StreamSessionOperation; the gateway maps an unavailable implementation to a capability error.

The Go and Python Session SDKs expose AcquireSandbox, OpenSession, Send, Receive, Cancel, Session.Close, and Sandbox.Release rather than transport-specific streaming helpers. Console opens the Session connection for agent turns and renders events incrementally.