Skip to content

Channels

This content is for the 0.1 version. Switch to the latest version for up-to-date documentation.

A channel is a URI you subscribe to. Every command and notification carries one, so a host, a proxy, or this client can route any message by looking at (method, params.channel) without understanding the payload.

URI State What it holds
ahp-root:// RootState Agent catalogue, terminals, host config. Always present.
ahp-session:/<uuid> SessionState Session metadata and its chat list.
ahp-chat:/<cid> ChatState Turns, streaming, tool calls, input requests.
ahp-terminal:/<id> TerminalState Pty state.
ahp-changeset:/<id> ChangesetState Proposed file edits.
ahp-session:/<uuid>/annotations AnnotationsState Inline annotations on a session.
ahp-resource-watch:/<id> ResourceWatchState File and directory change events.

ahp-otlp: also exists for telemetry, but it is stateless and never mirrored.

Look again at the annotations row. It is a path suffix on the session scheme, not a scheme of its own — which means classifying a channel by scheme alone is wrong:

channelKindOf(Uri.parse('ahp-session:/s1')); // session
channelKindOf(Uri.parse('ahp-session:/s1/annotations')); // annotations

Both are ahp-session. A startsWith('ahp-session:') check swallows the second and feeds annotation envelopes to the session reducer, silently corrupting session state.

Use AhpRuntime.acquire rather than subscribing by hand. It refcounts, so several parts of an app can watch the same channel without coordinating, and only the first claim reaches the wire:

final lease = runtime.acquire(Uri.parse('ahp-chat:/$chatId'));

Releasing the last lease starts a short grace window before unsubscribing. Without it, a navigation push-and-pop, or a list recycling rows, would churn the wire and throw away a warm store on every scroll.

Chat and terminal channels subscribe with maxLatencyMs: 16 by default, which asks the host to coalesce envelopes into roughly one frame’s worth.

This matters more than any client-side optimisation. Without it a chat channel delivers one envelope per streamed token, and every consumer pays for that regardless of how carefully it narrows rebuilds. Batching server-side is strictly cheaper than anything you can do after the fact.

runtime.acquire(
chatUri,
options: const SubscribeOptions(maxLatencyMs: 16, turns: 50),
);

Every channel applies your actions optimistically — except terminals.

A pty is a stateful, mutable process. Predicting its output, or echoing input into the scrollback before the pty replies, produces state that is simply wrong. The protocol splits terminal/input from terminal/data for exactly this reason.

The type system enforces it rather than a runtime check: terminal channels vend a MirrorStore, which has no applyLocal at all, and mirror.reconciling() refuses them outright.

runtime.dispatch(terminalUri, action); // throws ArgumentError