Skip to content

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.

The client attaches to something already running. With VS Code:

Terminal window
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.

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.

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.

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.

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.

finally {
await actions.cancel();
await mirror.dispose();
await client.close();
}
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.