akashi_flutter
Reactive Flutter integration for the Akashi agent framework. Drive an agent from your widgets, render its transcript, and keep heavy work off the UI thread.
What's in the box
AgentController— aChangeNotifierthat drives anAgentand folds its streamed events into observable state:text,events,messages,isRunning,error,pendingApproval,suspended. It is also the agent'sApprovalHandler.AgentBuilder— rebuilds a subtree whenever the controller notifies.MessageListView— renders aList<Message>exhaustively (text, reasoning disclosures, tool-call chips, results, media stubs); fully overridable per part viapartBuilder.offload— acompute()helper for CPU-bound stages, with the isolate deps-serializability contract documented.
Quick start
final controller = AgentController<void>();
final agent = ToolLoopAgent<void>(
model: model, // e.g. akashi_google's GeminiModel
tools: tools,
approvalHandler: controller, // controller IS the ApprovalHandler
);
controller.agent = agent; // resolve the construction-time cycle
// In your widget tree:
AgentBuilder<void>(
controller: controller,
builder: (context, c) => Column(children: [
Expanded(child: MessageListView(messages: c.messages)),
if (c.text.isNotEmpty) Text(c.text), // live, in-flight bubble
]),
);
controller.send('Summarize my unread mail.');
send appends each prompt to messages and drives the agent over the full
history, so successive calls form a multi-turn conversation.
Approval: in-process vs. durable
Both styles resolve from the same approve() / reject() call.
- In-process (default): a pending tool call blocks the loop in memory and
surfaces as
controller.pendingApproval. Bind it to a dialog;approve()/reject(reason)complete it. - Durable (
ToolLoopAgent(durableApproval: true)+ aCheckpointStore, e.g.akashi_drift): the run persists a checkpoint and suspends — the stream ends withcontroller.suspendedset.approve()/reject()thenresumeit from the store, which survives a process restart. After a restart, attach the same agent and callcontroller.resume(checkpointId, decision: …).
AgentBuilder<void>(
controller: controller,
builder: (context, c) {
final call = c.pendingApproval?.call ?? c.suspended?.pendingCall;
if (call == null) return const SizedBox.shrink();
return Row(children: [
Text('Allow ${call.toolName}?'),
TextButton(onPressed: c.approve, child: const Text('Allow')),
TextButton(onPressed: () => c.reject('denied'), child: const Text('Deny')),
]);
},
);
Offloading CPU-bound work
final parsed = await offload(parseBigPayload, rawJson);
Closures and live handles (sockets, DB connections, plugin channels) cannot
cross a SendPort. Construct such dependencies inside the callback from a
serializable config, or offload only pure, CPU-bound stages while the agent
stays on the main isolate.
Example
A runnable reactive chat screen lives in
example/akashi_flutter_example.dart —
swap the scripted model for any provider model and the rest is unchanged.
Status
v0.3.
License
MIT.
Libraries
- akashi_flutter
- Reactive Flutter integration for the Akashi agent framework.