agenix 4.0.0
agenix: ^4.0.0 copied to clipboard
Build smart AI agents in Flutter with memory, tools, and LLMs like Gemini. Fast, pluggable, and developer-friendly.
Agenix #
A Flutter package for building AI agents with memory, tools, and multi-agent orchestration. Define your agent's personality, give it tools, and let it handle conversations — including delegating sub-tasks across a chain of specialized agents.
This is the core package. For Firebase persistence, see
agenix_firebase.
Table of Contents #
- Architecture Overview
- Installation
- Quick Start
- Core Concepts
- Error Handling
- API Reference
- Usage Architectures
- Examples
- Maintainers
Architecture Overview #
┌─────────────────────────────────────────────────────────────┐
│ Your Flutter App │
│ │
│ Agent.create(llm, dataStore, name, role) │
│ │ │
│ ▼ │
│ ┌─────────┐ generateResponse() ┌───────────────┐ │
│ │ Agent │ ◄─────────────────────► │ LLM │ │
│ │ │ │ (Gemini / │ │
│ │ │ │ Custom) │ │
│ └────┬────┘ └───────────────┘ │
│ │ │
│ ┌────┴──────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────────┐ ┌────────────────┐ │
│ │Tools │ │DataStore │ │Agent Registry │ │
│ │ │ │(InMemory/ │ │(Multi-Agent │ │
│ │ │ │ Firebase/ │ │ Orchestration) │ │
│ │ │ │ Custom) │ │ │ │
│ └──────┘ └──────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────┘
The agent receives a user message, builds a structured prompt (including conversation history from the DataStore and available tools from the ToolRegistry), sends it to the LLM, and parses the response into one of three actions:
- Direct response — returns text to the user
- Tool invocation — runs one or more tools, optionally iterating up to 5 times before producing a final answer
- Agent delegation — hands the task to a chain of other agents, each passing its output to the next
Installation #
Add to your pubspec.yaml:
dependencies:
agenix: ^4.0.0
Then run:
flutter pub get
Need persistence? #
The core package ships with DataStore.inMemory() — a zero-dependency store for testing and prototyping. For production persistence with Firebase, add the companion package:
dependencies:
agenix: ^4.0.0
agenix_firebase: ^1.0.0
See agenix_firebase for setup instructions.
Quick Start #
1. Create system data #
Create assets/system_data.json with your agent's personality and background knowledge:
{
"name": "Lens",
"role": "A helpful assistant for the Acme platform",
"personality": "Friendly, concise, and knowledgeable",
"instructions": "Always greet the user by name when possible"
}
Add the asset to your pubspec.yaml:
flutter:
assets:
- assets/system_data.json
2. Initialize the agent #
import 'package:agenix/agenix.dart';
final agent = await Agent.create(
dataStore: DataStore.inMemory(),
llm: LLM.geminiLLM(
apiKey: 'YOUR_API_KEY',
modelName: 'gemini-2.0-flash',
),
name: 'Assistant',
role: 'General purpose assistant for the platform.',
);
3. Generate a response #
final response = await agent.generateResponse(
convoId: 'conversation-1',
userMessage: AgentMessage(
content: 'What is the weather like today?',
isFromAgent: false,
generatedAt: DateTime.now(),
),
);
print(response.content);
4. Run the app #
flutter run -d chrome --dart-define=GEMINI_API_KEY=your-key-here
Core Concepts #
Agent #
The Agent is the central class. It wires together an LLM, a DataStore, a ToolRegistry, and an AgentScope.
final agent = await Agent.create(
dataStore: DataStore.inMemory(),
llm: LLM.geminiLLM(apiKey: key, modelName: 'gemini-2.0-flash'),
name: 'Support Agent',
role: 'Handles customer support queries for the e-commerce platform.',
failureMode: FailureMode.throwError, // or FailureMode.gracefulMessage (default)
onError: (error, stack) => logger.severe('Agent error', error, stack),
scope: AgentScope.global, // default — or create isolated scopes
registrationPolicy: RegistrationPolicy.throwIfExists, // default
);
Key methods:
| Method | Description |
|---|---|
generateResponse(convoId, userMessage) |
Send a user message and get back an AgentMessage from the agent |
getMessages(conversationId) |
Retrieve all messages in a conversation |
getAllConversations() |
List all conversations for the current user |
deleteConversation(conversationId) |
Delete a conversation and its messages |
dispose() |
Unregister the agent from its scope |
Parameters for generateResponse:
| Parameter | Type | Default | Description |
|---|---|---|---|
convoId |
String |
required | Conversation identifier |
userMessage |
AgentMessage |
required | The user's message |
memoryLimit |
int |
10 |
Max previous messages loaded as context |
metaData |
Object? |
null |
Opaque pass-through for auth tokens, tenant IDs, etc. |
LLM #
The LLM abstract class defines the contract for language model providers. Agenix ships with Gemini; implement the interface for other providers.
// Built-in Gemini
final llm = LLM.geminiLLM(
apiKey: 'YOUR_API_KEY',
modelName: 'gemini-2.0-flash',
config: LlmConfig(
temperature: 0.2, // Low for structured JSON output
maxOutputTokens: 2048,
topP: 0.95,
topK: 40,
jsonMode: true, // Request native JSON output mode
timeout: Duration(seconds: 60),
),
);
Implementing a custom LLM:
class MyCustomLLM implements LLM {
@override
final String modelId = 'my-model-v1';
@override
final LlmConfig config;
MyCustomLLM({this.config = const LlmConfig()});
@override
Future<String> generate({
required String prompt,
String? systemInstruction,
Uint8List? rawData,
String mimeType = 'image/png',
}) async {
// Call your model API here
// Must return a JSON string matching one of:
// {"response": "..."}
// {"tools": "tool1, tool2", "parameters": {...}}
// {"agents_chain": ["agent1", "agent2"]}
}
}
DataStore (Memory) #
The DataStore abstract class handles conversation persistence. Messages are saved after each generateResponse call, and loaded as context for future prompts.
| DataStore | Package | Use Case |
|---|---|---|
DataStore.inMemory() |
agenix (this package) |
Testing, prototyping, or non-persistent apps |
FirebaseDataStore() |
agenix_firebase |
Production apps with Firebase backend |
Implementing a custom DataStore:
class PostgresDataStore extends DataStore {
@override
Future<void> saveMessage(String convoId, AgentMessage msg, {Object? metaData}) async {
// INSERT INTO messages ...
}
@override
Future<List<AgentMessage>> getMessages(String conversationId, {int? limit, Object? metaData}) async {
// SELECT * FROM messages WHERE convo_id = ? ORDER BY generated_at LIMIT ?
}
@override
Future<void> deleteConversation(String conversationId, {Object? metaData}) async {
// DELETE FROM messages WHERE convo_id = ?
}
@override
Future<List<Conversation>> getConversations({Object? metaData}) async {
// SELECT DISTINCT convo_id, last_message, last_message_time FROM ...
}
}
Tools #
Tools let the agent perform actions beyond conversation — API calls, database queries, calculations, anything.
Lifecycle:
User Message → LLM decides tool is needed → Agent runs tool → Tool returns ToolResponse
→ Agent either returns result OR iterates (up to 5 rounds of tool calls)
Tool without parameters
class NewsTool extends Tool {
NewsTool() : super(
name: 'news_tool',
description: 'Fetches the latest news headlines.',
);
@override
Future<ToolResponse> run(Map<String, dynamic> params) async {
final headlines = await NewsApi.fetchHeadlines();
return ToolResponse(
toolName: name,
isRequestSuccessful: true,
message: 'Here are today\'s headlines: ${headlines.join(", ")}',
needsFurtherReasoning: true, // Agent will synthesize a natural-language answer
);
}
}
Tool with parameters
class WeatherTool extends Tool {
WeatherTool() : super(
name: 'weather_tool',
description: 'Gets current weather for a given location.',
parameters: [
ParameterSpecification(
name: 'location',
type: 'string',
description: 'City name or coordinates.',
required: true,
),
ParameterSpecification(
name: 'units',
type: 'string',
description: 'Temperature unit.',
required: false,
defaultValue: 'celsius',
enumValues: ['celsius', 'fahrenheit'],
),
],
);
@override
Future<ToolResponse> run(Map<String, dynamic> params) async {
final location = params['location'] as String;
final units = params['units'] as String? ?? 'celsius';
final weather = await WeatherApi.get(location, units: units);
return ToolResponse(
toolName: name,
isRequestSuccessful: true,
message: 'Weather in $location: ${weather.temp}° ${weather.condition}',
data: weather.toMap(), // Optional structured data for chaining
);
}
}
Registering tools
agent.toolRegistry.registerTool(NewsTool());
agent.toolRegistry.registerTool(WeatherTool());
// Dynamically remove a tool
agent.toolRegistry.unregisterTool('weather_tool');
ToolResponse flags
| Field | Type | Description |
|---|---|---|
toolName |
String |
Name of the tool that produced this response |
isRequestSuccessful |
bool |
Whether the tool operation succeeded |
message |
String |
Human-readable result shown to the user |
data |
Map? |
Structured data for agent chaining or further reasoning |
needsFurtherReasoning |
bool |
When true, the agent makes a second LLM call to synthesize the tool output into a natural-language answer |
Multi-Agent Orchestration #
When the LLM determines a task requires multiple specialists, it returns an agents_chain. Agenix automatically delegates sub-tasks across agents, passing each agent's output as input to the next.
// Create specialized agents
final newsAgent = await Agent.create(
dataStore: DataStore.inMemory(),
llm: llm,
name: 'News Agent',
role: 'Fetches and summarizes news articles.',
);
final favouritesAgent = await Agent.create(
dataStore: DataStore.inMemory(),
llm: llm,
name: 'Favourites Agent',
role: 'Manages user favourites: add, remove, and list.',
);
final orchestrator = await Agent.create(
dataStore: DataStore.inMemory(),
llm: llm,
name: 'Orchestrator',
role: 'Main user-facing agent. Delegates to News Agent and Favourites Agent.',
);
// Register tools on each agent as needed
newsAgent.toolRegistry.registerTool(NewsTool());
favouritesAgent.toolRegistry.registerTool(AddFavouriteTool());
favouritesAgent.toolRegistry.registerTool(ListFavouritesTool());
How chaining works:
User: "Save the top headline to my favourites"
┌──────────────┐ ┌────────────┐ ┌──────────────────┐
│ Orchestrator │────►│ News Agent │────►│ Favourites Agent │
│ │ │ │ │ │
│ Decides chain│ │ Fetches │ │ Saves headline │
│ [News, Favs] │ │ headlines │ │ to favourites │
└──────────────┘ └────────────┘ └──────────────────┘
│ │
│ output passes as │
│ input to next ──────┘
│
▼
Final response
back to user
Safety guardrails:
- Cycle detection — if an agent appears twice in the same chain, a
ConfigExceptionis thrown - Depth limiting — chains are capped at 5 levels deep (
kMaxChainDepth)
Agent Scopes #
By default, all agents register in AgentScope.global and can discover each other for chaining. Use custom scopes to isolate agent groups:
// Isolated scope for testing
final testScope = AgentScope();
final agentA = await Agent.create(
dataStore: DataStore.inMemory(),
llm: llm,
name: 'Agent A',
role: 'Test agent A.',
scope: testScope,
);
final agentB = await Agent.create(
dataStore: DataStore.inMemory(),
llm: llm,
name: 'Agent B',
role: 'Test agent B.',
scope: testScope,
);
// Agents in testScope can chain to each other, but NOT to agents in AgentScope.global
RegistrationPolicy controls what happens when an agent name collides:
| Policy | Behavior |
|---|---|
throwIfExists |
Throws ConfigException (default — catches accidental duplicates) |
replace |
Silently replaces the existing agent |
ignore |
Keeps the existing agent, discards the new one |
Error Handling #
Agenix uses a sealed exception hierarchy. Every exception is an AgenixException, so you can exhaustively match on the type:
try {
final response = await agent.generateResponse(
convoId: 'convo-1',
userMessage: message,
);
} on LlmTimeoutException catch (e) {
// LLM call exceeded the configured timeout
} on ResponseParseException catch (e) {
// LLM returned malformed output after retries
print('Raw output: ${e.rawOutput}');
} on ToolNotFoundException catch (e) {
// LLM referenced a tool that isn't registered
print('Missing tool: ${e.toolName}');
} on ToolExecutionException catch (e) {
// A registered tool threw during execution
print('Tool ${e.toolName} failed: ${e.message}');
} on AgentNotFoundException catch (e) {
// Agent chain referenced a non-existent agent
} on DataStoreException catch (e) {
// Persistence operation failed
} on NotAuthenticatedException {
// DataStore operation without a signed-in user
} on ConfigException catch (e) {
// Invalid configuration (bad system_data.json, duplicate agent name, etc.)
}
FailureMode controls the behavior at the generateResponse boundary:
| Mode | Behavior |
|---|---|
FailureMode.gracefulMessage |
Returns an AgentMessage with isError: true (default — safe for UI) |
FailureMode.throwError |
Rethrows the typed AgenixException (use when you want full control) |
The onError callback fires in both modes, so you can always log errors centrally:
final agent = await Agent.create(
// ...
failureMode: FailureMode.gracefulMessage,
onError: (error, stack) => crashlytics.recordError(error, stack),
);
API Reference #
Exported Classes #
| Class | Description |
|---|---|
Agent |
Core agent with LLM, memory, tools, and multi-agent orchestration |
AgentScope |
Isolates groups of agents that can discover and chain to each other |
LLM |
Abstract interface for language model providers |
LlmConfig |
Provider-neutral generation settings (temperature, tokens, timeout, etc.) |
DataStore |
Abstract interface for conversation persistence |
AgentMessage |
A message in a conversation (user or agent) |
Conversation |
Summary of a conversation (last message, timestamp, ID) |
Tool |
Abstract class to extend for custom tools |
ParameterSpecification |
Defines a tool parameter (name, type, required, default, enum) |
ToolResponse |
Result returned from a tool execution |
ToolRegistry |
Per-agent registry for managing available tools |
Exported Enums #
| Enum | Values | Description |
|---|---|---|
FailureMode |
throwError, gracefulMessage |
Controls error surfacing behavior |
RegistrationPolicy |
throwIfExists, replace, ignore |
Controls duplicate agent name handling |
Sealed Exception Hierarchy #
AgenixException (sealed)
├── LlmException
│ └── LlmTimeoutException
├── ResponseParseException
├── ToolNotFoundException
├── ToolExecutionException
├── AgentNotFoundException
├── DataStoreException
│ └── NotAuthenticatedException
└── ConfigException
Internal Constants #
| Constant | Value | Description |
|---|---|---|
kMaxToolIterations |
5 |
Max tool→observe→re-prompt cycles per turn |
kMaxParseRetries |
2 |
Max corrective re-prompts for malformed JSON |
kMaxChainDepth |
5 |
Max depth for agent chain delegation |
Usage Architectures #
Single-Agent Chat App #
The simplest setup — one agent handling all user interactions.
┌──────────┐ ┌───────┐ ┌───────┐ ┌───────────┐
│ Flutter │────►│ Agent │────►│ LLM │ │ DataStore │
│ UI │◄────│ │◄────│ │ │(InMemory/ │
└──────────┘ │ │ └───────┘ │ Firebase) │
│ │──── save/load ────►└───────────┘
└───────┘
Best for: chatbots, Q&A apps, customer support widgets.
Agent + Tools (API Integration) #
The agent can call external APIs through tools.
┌──────────┐ ┌───────┐ ┌───────┐
│ Flutter │────►│ Agent │────►│ LLM │
│ UI │◄────│ │◄────│ │
└──────────┘ │ │ └───────┘
│ │
│ ToolRegistry
│ ├── WeatherTool ──► Weather API
│ ├── NewsTool ──► News API
│ └── DbTool ──► Database
└───────┘
Best for: apps where the agent needs to fetch real-time data or trigger actions.
Multi-Agent Orchestration #
Multiple specialized agents collaborating on complex tasks.
┌──────────┐ ┌──────────────┐
│ Flutter │────►│ Orchestrator │
│ UI │◄────│ │
└──────────┘ └──────┬───────┘
│ delegates via agents_chain
┌─────────┼─────────┐
▼ ▼ ▼
┌─────────┐ ┌────────┐ ┌───────────┐
│ Search │ │ Booking│ │ Favourites│
│ Agent │ │ Agent │ │ Agent │
│ + tools │ │ + tools│ │ + tools │
└─────────┘ └────────┘ └───────────┘
Best for: complex platforms where different domains require specialized knowledge and tools.
Testing / Prototyping Setup #
Use InMemoryDataStore and scoped agents for fast, isolated development.
final scope = AgentScope();
final agent = await Agent.create(
dataStore: DataStore.inMemory(), // No Firebase needed
llm: LLM.geminiLLM(apiKey: key, modelName: 'gemini-2.0-flash'),
name: 'Test Agent',
role: 'Agent under test.',
scope: scope, // Isolated from production agents
);
Examples #
| Example | Description |
|---|---|
| Multi-Agent System | Three agents (Orchestrator, News, Favourites) working together |
| Firebase Example | Single agent with tools and Firebase persistence |
| Custom DataStore | Implementing your own persistence backend |
Multi-Agent System Demo #
In this example three agents collaborate:
- Orchestrator — communicates with the end user
- News Agent — handles News API operations
- Favourites Agent — manages user favourites (add, remove, list)
https://github.com/user-attachments/assets/f79cf6ac-6913-49a7-982a-bd7b975599b7
Workflow
Single-Agent Demo #
An agentic app powered by "Lens" — an AI agent with a specific personality that can answer platform questions and perform tasks using tools.
https://github.com/user-attachments/assets/bcb56da8-4285-4661-af52-ee8dd6f31d08
Companion Packages #
| Package | Description |
|---|---|
agenix_firebase |
Firebase (Firestore + Storage + Auth) data store backend |