ahp_flutter

Flutter bindings for the Agent Host Protocol.

Built on Flutter's own primitives — InheritedWidget, InheritedNotifier, ValueNotifier — rather than a state-management package, so adopting AHP does not mean adopting an opinion about state management too. The core ahp_sdk package is pure Dart and stream-based; this adapts it to the widget tree.

Full documentation lives at mosesgameli.github.io/ahp/flutter/setup/.

Install

flutter pub add ahp_flutter

The bindings re-export the whole SDK, so this is the only import a Flutter app needs:

import 'package:ahp_flutter/ahp_flutter.dart';

Usage

Scope a runtime, then watch channels beneath it:

AhpProvider(
  runtime: AhpRuntime(
    connect: () async => AhpConnection(
      await WebSocketAhpTransport.connect(Uri.parse('ws://localhost:51234')),
    ),
    clientId: 'my-app',
  ),
  child: const MyApp(),
);
AhpChannel<RootState>(
  channel: Uri.parse('ahp-root://'),
  builder: (context, root, _) => Text('${root.agents.length} agents'),
);

Rebuild narrowing

A chat transcript genuinely changes on every streamed token, so watching whole channel state rebuilds the world per token. Three layers keep that affordable, cheapest first.

The host coalesces. SubscribeOptions.maxLatencyMs defaults to one frame for chat and terminal channels, so the host batches server-side. This is strictly cheaper than anything the client can do and needs no code.

Selectors compare by identity first. The reducers are contracted to return untouched sub-objects unchanged, so an identity check hits on the common case.

Structure narrows separately from content. Select the id list outside and the per-id entry inside; appending a token then rebuilds exactly one row:

AhpSelector<ChatState, List<String>>(
  channel: chat,
  select: (s) => s.messageIds,          // identity-stable across a token
  builder: (context, ids, _) => ListView.builder(
    itemCount: ids.length,
    itemBuilder: (c, i) => _Row(key: ValueKey(ids[i]), id: ids[i]),
  ),
);

Watching is a lease, not a read

There is deliberately no context.ahpWatch(...). Watching acquires a refcounted subscription, and a context extension has no dispose hook to release it — every rebuild would leak one, and it would fire an outbound subscribe from inside build. Use AhpChannel, AhpSelector, or AhpChannelMixin on a State, all of which release on dispose.

Example

example/ is a macOS app that mirrors a live host: connection status, agent list, and session count, each narrowed to its own slice. It also reconnects on its own when the host restarts.

code agent host --port 51234 --without-connection-token
cd example && flutter run -d macos

Testing

Widget tests drive a real AhpRuntime against a fake AhpClient, with two settings that keep its timers from outliving the tree:

AhpRuntime(
  connect: () async => fakeClient,
  clientId: 'test',
  keepaliveInterval: null,          // no periodic ping
  lingerDuration: Duration.zero,    // tear subscriptions down inline
);

Status

Early, and tracking a DRAFT protocol. See CHANGELOG.md.

LICENSE is still the flutter create placeholder — pick one before publishing.

Libraries

ahp_flutter
Flutter bindings for the Agent Host Protocol.