dart_agent_core 2.0.3
dart_agent_core: ^2.0.3 copied to clipboard
A mobile-first, local-first Dart library for building and evaluating stateful, tool-using AI agents with multi-provider LLM support.
Dart Agent Core #
A mobile-first, local-first Dart library for building and evaluating stateful, tool-using AI agents
dart_agent_core is a mobile-first, local-first Dart library that implements a full agentic loop with tool use, state persistence, multi-turn memory, skill system, context compression, and agent evals. It connects to mainstream LLM providers (OpenAI, Gemini, Claude, and any OpenAI-compatible API) and handles the orchestration layer — tool calling, streaming, planning, sub-agent delegation — entirely in Dart, making it suitable for Flutter apps without a Python or Node.js backend.
Features #
- Multi-provider support: Unified
LLMClientinterface for OpenAI (Chat Completions & Responses API), Google Gemini, and Anthropic Claude via AWS Bedrock. - Tool use: Wrap any Dart function as a tool with a JSON Schema definition. The agent dispatches calls, feeds results back, and loops until done. Tools support two parameter modes: function mode (positional/named parameter mapping via
Function.apply) and object mode (receive all arguments as aMap<String, dynamic>). Tools can returnAgentToolResultto carry multimodal content, metadata, or a stop signal. - Multimodal input:
UserMessageaccepts text, images, audio, video, and documents as content parts. Model responses can include text, images, video, and audio. - Stateful sessions:
AgentStatetracks conversation history, token usage, active skills, plan, and custom metadata.FileStateStoragepersists state to disk as JSON. - Agent evals: Run evaluation suites against your Dart agent code with tasks, graders, transcripts, record/replay, reports, and pass@k / pass^k metrics.
- Streaming:
runStream()yieldsStreamingEvents for model chunks, tool call requests/results, and retries — suitable for real-time UI updates in Flutter. - Pure Dart Skills: Define modular capabilities (
Skill) with their own system prompts and tools. Skills can be always-on (forceActivate) or toggled dynamically by the agent at runtime to save context window. - File-system Skills: Load Skills from
SKILL.mdfiles under a local directory root. WithjavaScriptRuntimeconfigured, these Skills can execute JavaScript scripts viaRunJavaScriptand bridge channels. - Sub-agent delegation: Register named sub-agents or use
cloneto delegate tasks to a worker agent with an isolated context. - Planning: Optional
PlanModeinjects awrite_todostool that lets the agent maintain a step-by-step task list during execution. - Context compression:
LLMBasedContextCompressorsummarizes old messages into episodic memory when the token count exceeds a threshold. The agent can recall original messages via the built-inretrieve_memorytool. - Loop detection:
DefaultLoopDetectorcatches repeated identical tool calls and can run periodic LLM-based diagnosis for subtler loops. - Controller events:
AgentControllerpublishes observation events for run, model, tool, plan, retry, cancellation, and error lifecycle steps. - Agent hooks: A unified
AgentHookpipeline can rewrite model inputs, transform streaming chunks and final responses, deny/defer/rewrite tool calls, inject follow-up context, continue final turns, abort runs, and wrap state persistence.
Installation #
dependencies:
dart_agent_core: ^2.0.3
Platform Support #
dart_agent_core runs on all six Dart/Flutter platforms — Android, iOS, Web, Windows, macOS, and Linux — and is WebAssembly (WASM) compatible (6/6 platforms on pub.dev). Platform-specific concerns (file-system state storage, HTTP adapters, JavaScript runtime) are resolved at compile time via conditional exports, so the public API is identical on native and web.
There is no dart:io on the web, so Platform.environment is unavailable. Read your API key from the browser instead — for example from localStorage via package:web (WASM-safe):
import 'package:web/web.dart' as web;
import 'package:dart_agent_core/dart_agent_core.dart';
void main() async {
// Persist the key once from your app (e.g. a settings screen):
// web.window.localStorage.setItem('OPENAI_API_KEY', '<key>');
final apiKey = web.window.localStorage.getItem('OPENAI_API_KEY') ?? '';
final client = OpenAIClient(apiKey: apiKey);
// ... same StatefulAgent setup as Quick Start
}
Add
web: ^1.0.0to yourdependenciesto usepackage:web. Avoid the legacydart:html, which is not WASM-compatible. On web, use an in-memory orlocalStorage-backedStateStoragerather thanFileStateStorage, which requires a real file system.
Quick Start #
import 'dart:io';
import 'package:dart_agent_core/dart_agent_core.dart';
String getWeather(String location) {
if (location.toLowerCase().contains('tokyo')) return 'Sunny, 25°C';
return 'Weather data not available for this location';
}
void main() async {
final apiKey = Platform.environment['OPENAI_API_KEY'] ?? '';
final client = OpenAIClient(apiKey: apiKey);
final modelConfig = ModelConfig(model: 'gpt-4o-mini');
final weatherTool = Tool(
name: 'get_weather',
description: 'Get the current weather for a city.',
executable: getWeather,
parameters: {
'type': 'object',
'properties': {
'location': {'type': 'string', 'description': 'City name, e.g. Tokyo'},
},
'required': ['location'],
},
);
final agent = StatefulAgent(
name: 'weather_agent',
client: client,
tools: [weatherTool],
modelConfig: modelConfig,
state: AgentState.empty(),
systemPrompts: ['You are a helpful assistant.'],
);
final responses = await agent.run([
UserMessage.text('What is the weather like in Tokyo right now?'),
]);
print((responses.last as ModelMessage).textOutput);
}
Supported Providers #
OpenAI (Chat Completions) #
final client = OpenAIClient(
apiKey: Platform.environment['OPENAI_API_KEY'] ?? '',
// baseUrl defaults to 'https://api.openai.com'
// Override for Azure OpenAI or compatible proxies
);
OpenAI (Responses API) #
Uses the newer stateful Responses API. The client automatically extracts responseId from ModelMessage and passes it as previous_response_id on subsequent requests, so only new messages are sent.
final client = ResponsesClient(
apiKey: Platform.environment['OPENAI_API_KEY'] ?? '',
);
Google Gemini #
final client = GeminiClient(
apiKey: Platform.environment['GEMINI_API_KEY'] ?? '',
);
AWS Bedrock (Claude) #
Uses AWS Signature V4 for authentication instead of a simple API key.
final client = BedrockClaudeClient(
region: 'us-east-1',
accessKeyId: Platform.environment['AWS_ACCESS_KEY_ID'] ?? '',
secretAccessKey: Platform.environment['AWS_SECRET_ACCESS_KEY'] ?? '',
);
Anthropic Claude (Direct) #
Directly calls the Anthropic Messages API, no AWS Bedrock needed.
final client = ClaudeClient(
apiKey: Platform.environment['ANTHROPIC_API_KEY'] ?? '',
);
All clients support HTTP proxies via proxyUrl and configurable retry/timeout parameters. See Providers doc for details.
Tool Use #
Wrap any Dart function (sync or async) as a tool. The agent parses the LLM's function call JSON, maps arguments to your function's parameters, executes it, and feeds the result back.
final tool = Tool(
name: 'search_products',
description: 'Search the product catalog.',
executable: searchProducts,
parameters: {
'type': 'object',
'properties': {
'query': {'type': 'string'},
'maxResults': {'type': 'integer'},
},
'required': ['query'],
},
namedParameters: ['maxResults'], // maps to Dart named parameters
);
Alternatively, use parameterMode: ToolParameterMode.object to receive all arguments as a single Map<String, dynamic>, bypassing positional/named parameter mapping:
final tool = Tool(
name: 'search_products',
description: 'Search the product catalog.',
parameterMode: ToolParameterMode.object,
executable: (Map<String, dynamic> args) async {
final query = args['query'] as String;
final maxResults = args['maxResults'] as int? ?? 10;
return await searchProducts(query, maxResults);
},
parameters: {
'type': 'object',
'properties': {
'query': {'type': 'string'},
'maxResults': {'type': 'integer'},
},
'required': ['query'],
},
);
Tools can access the current session state via AgentCallToolContext.current without explicit parameters:
String checkBalance(String currency) {
final userId = AgentCallToolContext.current?.state.metadata['user_id'];
return fetchBalance(userId, currency);
}
Return AgentToolResult for advanced control:
Future<AgentToolResult> generateChart(String query) async {
final imageBytes = await chartService.render(query);
return AgentToolResult(
content: ImagePart(base64Encode(imageBytes), 'image/png'),
stopFlag: true, // stop the agent loop after this tool
metadata: {'chart_type': 'bar'},
);
}
See Tools & Planning doc for parameter modes, async tools, and more.
Skill System #
dart_agent_core supports two Skill types:
- Pure Dart Skills (
Skillobjects) - File-system Skills (
SKILL.mdfiles discovered from a root directory)
These two modes are mutually exclusive in StatefulAgent (use one or the other per agent instance).
Pure Dart Skills #
Pure Dart Skills are modular capability units — a system prompt plus optional tools bundled under a name. The agent can activate/deactivate Skills at runtime to keep the context window focused.
class CodeReviewSkill extends Skill {
CodeReviewSkill() : super(
name: 'code_review',
description: 'Review code for bugs and style issues.',
systemPrompt: 'You are an expert code reviewer. Check for security issues and logic errors.',
tools: [readFileTool, lintTool],
);
}
final agent = StatefulAgent(
...
skills: [CodeReviewSkill(), DataAnalysisSkill()],
);
- Dynamic skills (default): Start inactive. The agent gains
activate_skills/deactivate_skillstools to toggle them based on the current task. - Always-on skills (
forceActivate: true): Permanently active, cannot be deactivated.
File-system Skills (SKILL.md) #
File-system Skill mode loads Skills from local folders: discover available Skills, read SKILL.md on demand, and inject Skill content into conversation context when activated.
final agent = StatefulAgent(
...
// Required file tools should be provided by host app (for example: Read, LS).
tools: [readTool, lsTool],
skillDirectoryPath: '/absolute/path/to/skills_root',
javaScriptRuntime: NodeJavaScriptRuntime(), // optional, enables RunJavaScript
skills: null, // do not use with skillDirectoryPath
);
When javaScriptRuntime is configured in File-system Skill mode, the framework exposes RunJavaScript.
Flutter configuration for RunJavaScript
In Flutter apps, configure a custom JavaScriptRuntime implementation (for example using flutter_js) and pass it to StatefulAgent.
- Add dependency in your Flutter app:
dependencies:
flutter_js: ^0.8.7
- Implement
JavaScriptRuntimeand inject it:
import 'package:dart_agent_core/dart_agent_core.dart';
import 'package:flutter_js/flutter_js.dart' as flutter_js;
final agent = StatefulAgent(
...
skillDirectoryPath: '/absolute/path/to/skills_root',
javaScriptRuntime: FlutterJavaScriptRuntime(
runtime: flutter_js.getJavascriptRuntime(),
),
);
- (Optional) Register bridge channels for native capabilities:
agent.registerJavaScriptBridgeChannel('local.greeting', (payload, context) {
final name = (payload['name'] ?? 'friend').toString();
return {'message': 'Hello, $name'};
});
Bridge channels can be extended by host apps via:
registerJavaScriptBridgeChannel(channel, handler)unregisterJavaScriptBridgeChannel(channel)
Sub-Agent Delegation #
Register sub-agents for specialized or parallelizable work. Each worker runs in its own isolated AgentState.
final agent = StatefulAgent(
...
subAgents: [
SubAgent(
name: 'researcher',
description: 'Searches the web and summarizes findings.',
agentFactory: (parent) => StatefulAgent(
name: 'researcher',
client: parent.client,
modelConfig: parent.modelConfig,
state: AgentState.empty(),
tools: [webSearchTool],
isSubAgent: true,
),
),
],
);
The agent uses the built-in delegate_task tool to dispatch work:
assignee: 'clone'— creates a copy of the current agent with clean context.assignee: 'researcher'— uses a registered named sub-agent.
Streaming #
runStream() yields fine-grained events for Flutter UI integration:
await for (final event in agent.runStream([UserMessage.text('Hello')])) {
switch (event.eventType) {
case StreamingEventType.modelChunkMessage:
final chunk = event.data as ModelMessage;
// update text in UI incrementally
break;
case StreamingEventType.fullModelMessage:
// complete assembled message for this turn
break;
case StreamingEventType.functionCallRequest:
// model requested tool calls
break;
case StreamingEventType.functionCallResult:
// tool execution finished
break;
default:
break;
}
}
Planning #
Pass planMode: PlanMode.auto (or PlanMode.must) to enable the planner. This injects a write_todos tool that the agent uses to create and update a task list with statuses: pending, in_progress, completed, cancelled.
final agent = StatefulAgent(
...
planMode: PlanMode.auto,
);
React to plan changes via AgentController:
controller.on<PlanChangedEvent>((event) {
for (final step in event.plan.steps) {
print('[${step.status.name}] ${step.description}');
}
});
Context Compression #
For long-running sessions, attach a compressor to automatically summarize old messages when token usage exceeds a threshold:
final agent = StatefulAgent(
...
compressor: LLMBasedContextCompressor(
client: client,
modelConfig: ModelConfig(model: 'gpt-4o-mini'),
totalTokenThreshold: 64000,
keepRecentMessageSize: 10,
),
);
Compressed history is stored as episodic memories. The agent can retrieve the original messages via the built-in retrieve_memory tool when the summary isn't detailed enough.
Controller Events #
AgentController observes lifecycle events without changing agent behavior:
final controller = AgentController();
controller.on<AfterToolCallEvent>((event) {
print('Tool ${event.result.name} finished');
});
final agent = StatefulAgent(..., controller: controller);
Agent Hooks #
Use AgentHook for controlled changes to the agent loop. Hooks receive typed context objects and return typed outcomes. To affect only the current model call, rewrite the request. To persist context for later loops or resume, write to context.state.
class RuntimeContextHook extends AgentHook {
@override
ModelCallHookResult beforeModelCall(ModelCallHookContext context) {
final transientMessage = UserMessage.text(
'Current time: ${DateTime.now()}',
);
return ModelCallHookResult.proceed(
request: context.request.copyWith(
requestMessages: [
...context.request.requestMessages,
transientMessage,
],
),
);
}
}
class DeleteFilePolicyHook extends AgentHook {
@override
ToolCallHookResult beforeToolCall(ToolCallHookContext context) {
if (context.call.name != 'delete_file') {
return ToolCallHookResult.proceed(context.call);
}
return ToolCallHookResult.deny(
content: [TextPart('delete_file is blocked by local policy.')],
);
}
}
final agent = StatefulAgent(
...,
hooks: [RuntimeContextHook(), DeleteFilePolicyHook()],
);
Available hook phases include beforeRun, beforeModelCall, onModelChunk, afterModelCall, beforeToolCall, afterToolCall, onTurnCompletion, beforePersistState, afterPersistState, and afterRun.
Examples #
See the example/ directory:
- Basic agent with tool use
- Streaming responses
- Persistent state across sessions
- Planning with write_todos
- Dynamic skill system
- File-system Skills + JavaScript scripts execute
- Sub-agent delegation
- Controller events + hook policy
- Unified agent hooks
- Claude extended thinking via Bedrock
- OpenAI
- Gemini
- Claude (direct Anthropic API)
- AWS Bedrock (Claude)
- Kimi (Moonshot AI)
- Qwen (Alibaba DashScope)
- Zhipu GLM
- Volcengine Doubao-Seed
- MiniMax
- Ollama (local)
- OpenRouter
Documentation #
- Architecture & Lifecycle — Agent loop, streaming events, agent hooks, loop detection, cancellation
- LLM Providers & Configuration — OpenAI, Gemini, Bedrock setup, ModelConfig, proxy support
- Tools & Planning — Tool creation, parameter mapping, AgentToolResult, skills, sub-agents, planner
- State & Memory Management — AgentState, FileStateStorage, context compression, episodic memory
- Evaluating Agents — Anthropic-aligned eval subsystem: tasks, graders, suites, pass@k / pass^k, record/replay, Langfuse export, cross-run health
Contributing #
Bug reports and pull requests are welcome. Please open an issue first for significant changes.
About #
dart_agent_core is developed by Memex Lab. Visit our homepage for more projects and updates.