← Back to posts Cover image for Flutter AI Agents: Building Intelligent Apps with dart_agent_core and LLM_SDK

Flutter AI Agents: Building Intelligent Apps with dart_agent_core and LLM_SDK

· 6 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

Building AI-powered features in Flutter has traditionally meant setting up complex backend services, managing API calls, and dealing with the friction of switching between Dart and Python/Node.js environments. But what if you could run intelligent agent loops directly on the device, with the full power of Large Language Models (LLMs), using pure Dart? The emerging ecosystem of dart_agent_core and llm_sdk makes this not only possible but surprisingly straightforward.

In this post, we’ll explore how to combine these two packages to create a self-contained AI assistant that can reason, use tools, and interact with users—all from within your Flutter app.

The Core Concept: Agent Loops and Unified LLMs

An AI agent is more than just a one-shot LLM call. It’s a system that can perceive its environment (through user input, app state, etc.), decide on an action (often by calling an LLM), execute that action (perhaps using a tool like a calculator or web search), and then observe the result to plan its next move. This is the agent loop.

The dart_agent_core package provides the scaffolding for this loop, managing the state and lifecycle. The llm_sdk package acts as the “brain,” giving you a clean, unified interface to various LLM providers (like OpenAI, Anthropic, or Google Gemini). Switching providers becomes a one-line change, freeing you from vendor lock-in.

Setting Up: A Simple Task Assistant

Let’s build a practical example: a task-planning assistant that can break down a user’s goal into subtasks and estimate the time required.

First, add the dependencies to your pubspec.yaml:

dependencies:
  dart_agent_core: ^0.3.0
  llm_sdk: ^0.5.0

Next, we’ll define a Tool. Tools are how agents interact with the world. Our tool will be a simple time estimator.

import 'package:dart_agent_core/dart_agent_core.dart';
import 'package:llm_sdk/llm_sdk.dart';

class TimeEstimatorTool extends Tool {
  TimeEstimatorTool()
      : super(
          name: 'estimate_task_time',
          description: 'Estimates how long a given task will take in hours.',
          parameters: {
            'task_description': {
              'type': 'string',
              'description': 'A clear description of the task.',
            },
            'complexity': {
              'type': 'string',
              'description': 'The perceived complexity: low, medium, or high.',
              'enum': ['low', 'medium', 'high'],
            },
          },
        );

  @override
  Future<ToolResult> invoke(Map<String, dynamic> arguments) async {
    final String task = arguments['task_description'];
    final String complexity = arguments['complexity'];

    // A simple rule-based estimator. In a real app, this could be more sophisticated.
    final Map<String, int> hourMap = {'low': 1, 'medium': 4, 'high':359};
    final int estimatedHours = hourMap[complexity] ?? 2;

    return ToolResult(
      content: "The task '$task' ($complexity complexity) is estimated to take $estimatedHours hours.",
      isError: false,
    );
  }
}

Now, we create our Agent. The agent needs an LLM client and its available tools.

class PlanningAgent extends Agent {
  PlanningAgent({required OpenAIClient llmClient})
      : super(
          name: 'Planning Assistant',
          description: 'Helps plan projects and estimate tasks.',
          llmClient: llmClient,
          systemPrompt: '''
          You are a helpful project planning assistant.
          Your goal is to help users break down objectives into actionable tasks and estimate the time required.
          Always use the 'estimate_task_time' tool when the user asks about how long something will take.
          Be concise and practical.
          ''',
        ) {
    registerTool(TimeEstimatorTool());
  }
}

Running the Loop with Lifecycle Hooks

The real power of dart_agent_core comes from its lifecycle hooks. Hooks allow you to observe and inject logic at key points in the agent’s loop: before/after the LLM is called, before/after a tool is used, etc. This is perfect for logging, validation, or modifying messages.

Let’s create a simple hook to log every action our agent takes.

class LoggingHook extends AgentHook {
  @override
  Future<void> onToolInvoke(ToolInvocation invocation) async {
    print('[Agent Hook] Tool Called: ${invocation.toolName} with args: ${invocation.arguments}');
    super.onToolInvoke(invocation);
  }

  @override
  Future<void> onLlmCall(LlmCall call) async {
    print('[Agent Hook] LLM Prompt: ${call.messages.last}');
    super.onLlmCall(call);
  }
}

To use our agent in a Flutter widget, we set up the LLM client, instantiate the agent with our hook, and start a conversation.

import 'package:flutter/material.dart';
import 'package:llm_sdk/llm_sdk.dart';

class AssistantScreen extends StatefulWidget {
  const AssistantScreen({super.key});

  @override
  State<AssistantScreen> createState() => _AssistantScreenState();
}

class _AssistantScreenState extends State<AssistantScreen> {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  late final PlanningAgent _agent;

  @override
  void initState() {
    super.initState();
    // Initialize the LLM client. Switch to GeminiClient or AnthropicClient easily.
    final llmClient = OpenAIClient(apiKey: 'your-api-key-here');
    // Create our agent and attach the logging hook.
    _agent = PlanningAgent(llmClient: llmClient)..registerHook(LoggingHook());
  }

  Future<void> _sendMessage(String text) async {
    setState(() {
      _messages.add(ChatMessage(role: ChatRole.user, content: text));
    });

    // This is where the agent loop runs!
    final response = await _agent.run(text);

    setState(() {
      _messages.add(ChatMessage(role: ChatRole.assistant, content: response));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Planning Assistant')),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: _messages.length,
              itemBuilder: (ctx, i) {
                final msg = _messages[i];
                return ListTile(
                  title: Text(msg.content),
                  subtitle: Text(msg.role == ChatRole.user ? 'You' : 'Assistant'),
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _textController,
                    decoration: const InputDecoration(hintText: 'Ask for a plan...'),
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.send),
                  onPressed: () {
                    _sendMessage(_textController.text);
                    _textController.clear();
                  },
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

When a user sends a message like “I need to build a login screen,” the agent will:

  1. Receive the input (perceive).
  2. The LLM (via llm_sdk) will decide to use the estimate_task_time tool.
  3. The agent will invoke the tool (act), which runs our Dart code.
  4. The tool’s result is fed back to the LLM.
  5. The LLM formulates a final, helpful response for the user.
  6. Our LoggingHook prints each step to the console.

Common Pitfalls and Best Practices

  • API Keys on Device: Be cautious about embedding API keys directly in your client-side code. For production, consider a lightweight backend proxy or using Flutter’s secure storage for user-provided keys.
  • Cost Management: Agent loops can make multiple LLM calls per interaction. Implement hooks to monitor usage and set limits to avoid unexpected costs.
  • Error Handling: Wrap your agent.run() calls in try-catch blocks. LLM APIs can fail, and tools can throw exceptions. Use hooks like onError to handle failures gracefully.
  • State Management: For complex agents, consider using a state management solution (like Riverpod or Bloc) to hold the agent’s state outside the UI, making it easier to persist and test.

Taking It Further

This is just the starting point. With these building blocks, you can create agents that:

  • Process local files (using tools that read device storage).
  • Control other parts of your app (a tool that navigates to a specific screen).
  • Chain multiple agents together for specialized tasks.

By bringing the agent loop into Dart, dart_agent_core and llm_sdk significantly lower the barrier to creating truly intelligent, responsive, and self-contained Flutter applications. Give it a try for your next feature—you might be surprised at how much logic you can keep on the device.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.