Channel API¶
@channel is a full-duplex session over Zenoh (and optionally WebSocket).
The handler receives a ChannelSession. Callers use Istos.open_channel or
@channel_client.
Details: Channels & Agent Sessions.
Bidirectional channels — full-duplex sessions for interactive agents.
A @handle replies once and a @stream yields many; a @channel keeps a
session open so the handler can receive() inbound messages and send()
outbound ones in any order — an agent that reads a turn, streams back tokens,
then waits for the next. WebSocket is the transport at the HTTP edge; the handler
still gets authorization, validation, DI and the request envelope, and can reach
the rest of the mesh through the app.
@app.channel("agent/chat", ws="/chat")
async def chat(s: ChannelSession):
await s.send({"role": "system", "text": "ready"})
async for msg in s:
async for tok in llm.stream(msg):
await s.send(tok)
await s.send({"done": True})
ChannelClosed
¶
ChannelSession
¶
The handler's end of a duplex session. The transport feeds inbound bytes and drains outbound ones; the handler works in terms of decoded messages.
Source code in src/istos/primitives/channel.py
close()
¶
Transport hook: the peer is gone; unblock any pending receive().
feed(raw)
¶
history(limit=1000)
async
¶
Prior messages for this conversation, oldest-first. Empty unless the
channel is durable (@channel(durable=True)).
Source code in src/istos/primitives/channel.py
receive()
async
¶
Wait for the next message from the peer, or raise ChannelClosed.
Source code in src/istos/primitives/channel.py
send(data)
async
¶
Push a message to the peer.
Source code in src/istos/primitives/channel.py
channel_wrapper
¶
Registers a duplex handler and drives one session through it.
Source code in src/istos/primitives/channel.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
__call__(*args, **kwargs)
¶
authorize(attachment, params)
async
¶
Run the authorizer gate and return the principal (or raise). The fabric open handshake calls this to accept/deny before a session exists.
Source code in src/istos/primitives/channel.py
run(session, *, attachment, params, principal=_UNSET)
async
¶
Validate, resolve DI, then run the handler for the life of the session.
Authorizes first unless principal is supplied (already gated by the
caller — e.g. the fabric open handshake).
Source code in src/istos/primitives/channel.py
Cross-node channels over Zenoh — phase 2 of @channel.
A @channel handler can run on one node while a client (often a WebSocket
gateway) opens a session from another. There is no Zenoh "connection", so a
session is built from three keys under the channel prefix P and a session id
S:
P/S/up— client → server messages (client puts, server subscribes)P/S/down— server → client messages (server puts, client subscribes)- liveliness
P/S— the client holds a token; when it drops (close or crash) the server tears the session down.
Opening is a one-shot query to P/S/open carrying the auth token as the
attachment, so the authorizer runs before any session exists. On success both
sides start pub/sub on up/down and the client declares its liveliness token.
ChannelClient
¶
Caller's end of a fabric channel. Same surface as ChannelSession (send/receive/async-for/close), backed by Zenoh pub/sub.
Source code in src/istos/primitives/channel_fabric.py
FabricChannelServer
¶
Serves one @channel over Zenoh: answers open handshakes, runs a handler per session, and tears sessions down when their liveliness token drops.
Source code in src/istos/primitives/channel_fabric.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
is_error_payload(parsed)
¶
Whether a decoded reply is an :class:ErrorResponse wire payload.
A handler that raises replies with an envelope rather than sending an
exception, and the envelope answers .get() like any other dict, so an
unchecked caller reads a failure as data.
query_once, @query, stream_query and open_channel check this
themselves. Use it directly for replies you decode yourself, and for
multi-reply results, which are passed through unchecked. Pair it with
:func:error_from_payload to raise.
Detection prefers the :data:ERROR_MARKER discriminator: present and truthy
is an error, present and falsy is a normal result — the escape hatch for a
handler whose success value legitimately carries error/code/
message. When the marker is absent (an old responder, or a client in
another language), fall back to the legacy rule: all three of error,
code and message present.
Source code in src/istos/errors.py
Client-side decorators for reaching @stream and @channel — the declarative counterparts to @query (which reaches @handle) and @subscribe (which reaches @publish). A service is a mix of senders and receivers; these let the receiving side be attached the same way, on the app or a router.
@stream_client hands the body the live chunk iterator; @channel_client hands it
an open ChannelClient and closes it when the body returns. Call kwargs become the
selector params, and a per-call token= (or a decorator-level token=)
carries the auth token — just like @query.
channel_client_wrapper
¶
Bases: _client_base
Reaches a @channel: opens a session, passes the ChannelClient to the body, and closes it when the body returns.
Source code in src/istos/primitives/clients.py
stream_client_wrapper
¶
Bases: _client_base
Reaches a @stream: opens the stream and passes its chunk iterator to the
body. async for chunk in it inside the body.