ahp_flutter 0.1.0
ahp_flutter: ^0.1.0 copied to clipboard
Flutter bindings for the Agent Host Protocol — scoped runtime, refcounted channel subscriptions, and rebuild narrowing for streaming state.
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 https://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.