Quickstart
The shortest path from nothing to state you can read. Every line below is from
example/ahp_example.dart in the repository, which runs as-is.
Start a host
Section titled “Start a host”The client attaches to something already running. With VS Code:
code agent host --port 51234 --without-connection-token--without-connection-token skips authentication, which is fine locally and
wrong anywhere else. See Staying connected
for the authenticated path.
Connect
Section titled “Connect”import 'package:ahp_sdk/ahp_sdk.dart';
final transport = await WebSocketAhpTransport.connect( Uri.parse('ws://localhost:51234'),);final client = AhpConnection(transport);AhpConnection speaks JSON-RPC over whatever transport you hand it. The
transport is an interface, which is what makes
testing without a socket possible.
Attach a mirror
Section titled “Attach a mirror”final mirror = AhpStateMirror(clientId: 'my-app');final actions = client.actions.listen(mirror.applyEnvelope);The mirror is the piece that turns an ordered stream of actions into readable state, by running the same pure reducers the host runs.
Initialize
Section titled “Initialize”final result = await client.initialize( InitializeParams( channel: 'ahp-root://', protocolVersions: supportedProtocolVersions, clientId: 'my-app', ),);print('negotiated protocol ${result.protocolVersion}');
for (final snapshot in result.snapshots) { mirror.applySnapshot(snapshot);}Offer supportedProtocolVersions rather than a single version. It is ordered
most-preferred-first, so an older host can still find something it understands
instead of refusing the handshake outright.
The handshake can return snapshots directly, which is why they are applied before anything else is asked for.
Subscribe and read
Section titled “Subscribe and read”final root = Uri.parse('ahp-root://');final subscribed = await client.subscribe(SubscribeParams(channel: '$root'));if (subscribed.snapshot case final snapshot?) { mirror.applySnapshot(snapshot);}
final state = mirror.storeFor(root).optimistic! as RootState;print('${state.agents.length} agent(s):');for (final agent in state.agents) { print(' ${agent.provider} — ${agent.displayName}');}storeFor(channel).optimistic is a plain immutable object, readable
synchronously. No future, no polling — the reducers have already run by the
time the subscribe completes.
The cast is needed because a store holds Object?: which state type a channel
carries is decided by its URI scheme, not by the type system. See
Channels.
Clean up
Section titled “Clean up”finally { await actions.cancel(); await mirror.dispose(); await client.close();}The whole thing
Section titled “The whole thing”import 'package:ahp_sdk/ahp_sdk.dart';
Future<void> main() async { final transport = await WebSocketAhpTransport.connect( Uri.parse('ws://localhost:51234'), ); final client = AhpConnection(transport); final mirror = AhpStateMirror(clientId: 'ahp-example'); final actions = client.actions.listen(mirror.applyEnvelope);
try { final result = await client.initialize( InitializeParams( channel: 'ahp-root://', protocolVersions: supportedProtocolVersions, clientId: 'ahp-example', ), ); print('negotiated protocol ${result.protocolVersion}');
for (final snapshot in result.snapshots) { mirror.applySnapshot(snapshot); }
final root = Uri.parse('ahp-root://'); final subscribed = await client.subscribe( SubscribeParams(channel: '$root'), ); if (subscribed.snapshot case final snapshot?) { mirror.applySnapshot(snapshot); }
final state = mirror.storeFor(root).optimistic! as RootState; for (final agent in state.agents) { print(' ${agent.provider} — ${agent.displayName}'); } } on AhpRpcException catch (e) { print('host refused: $e'); } finally { await actions.cancel(); await mirror.dispose(); await client.close(); }}This is the low-level path: you own the connection, the subscriptions, and the
lifecycle. For anything long-lived — reconnecting, replaying, refcounted
subscriptions — use AhpRuntime instead of
driving AhpConnection yourself.