Flint AI

flint_ai is the standalone AI runtime for Flint. It gives Dart applications agents, tools, workflows, chat providers, memory, and run tracing without requiring a specific web framework.

Use it directly in any Dart app, or through package:flint_dart/ai.dart when you want Flint Dart's app.ai and ctx.ai integration.

Features

  • Agent runtime with plans, run state, traces, and structured output
  • Safe-by-default tool execution with production policy support
  • Role and capability-aware tool authorization
  • Reusable workflow registry
  • Chat provider contracts and implementations for OpenAI, Gemini, and Anthropic
  • Image and embedding provider contracts
  • In-memory memory and persistence stores for local development and tests
  • Framework-neutral request context support
  • Flint Dart database adapters available from package:flint_dart/ai.dart

Architecture

flint_ai is split into small layers so applications can adopt only what they need:

  • Providers adapt external AI services into common Flint contracts. Chat providers implement ChatProvider; image and embedding providers have their own contracts. Built-in chat providers include OpenAI, Gemini, and Anthropic.
  • Agents receive an AiGoal, create an AiPlan, and synthesize the final structured output from run state.
  • Tools perform controlled actions during a run. They are the boundary for side effects such as sending messages, writing data, calling internal APIs, or queueing business operations.
  • Workflows are named reusable operations that can be invoked directly when you do not need a full agent plan.
  • Memory and repository store thread messages, run events, traces, artifacts, and durable run records. Standalone flint_ai defaults to in-memory stores.
  • Flint Dart adapters connect the runtime to Flint apps through app.ai and ctx.ai, add database-backed stores, expose AI table definitions, and provide .env setup helpers.

The usual request flow is:

AiGoal -> AiAgent.plan() -> AiPlan -> AiToolPolicy -> AiTool/AiWorkflow
       -> AiRun.state -> AiAgent.synthesize() -> AiRunResult

Install

For standalone Dart projects:

dart pub add flint_ai

For Flint Dart apps, use the framework export instead:

import 'package:flint_dart/ai.dart';

Quick Start

import 'package:flint_ai/flint_ai.dart';

Future<void> main() async {
  final ai = FlintAi();

  final result = await ai.run(
    agent: BasicTaskAgent(),
    goal: const AiGoal(
      task: 'Summarize an order',
      input: {'orderId': 'ord_1'},
    ),
    userId: 'user-1',
  );

  print(result.output);
}

Examples

Runnable examples live in example/:

  • basic_agent.dart runs a simple agent end to end.
  • chat_provider.dart registers an OpenAI-compatible provider with a fake HTTP client so it can run without a real API key.
  • production_policy.dart demonstrates a capability-gated tool using ProductionAiToolPolicy.

Run one with:

dart run example/basic_agent.dart

Agents

Agents turn a goal into a plan and synthesize the final output after the plan runs.

class SupportAgent extends AiAgent {
  @override
  String get name => 'support_agent';

  @override
  Future<AiPlan> plan(AiRunContext context) async {
    return AiPlan(
      steps: [
        AiPlanStep(
          id: 'capture',
          type: 'record',
          description: 'Capture the support request',
          arguments: {
            'task': context.goal.task,
            'input': context.goal.input,
          },
        ),
      ],
    );
  }
}

Tools

Tools are the boundary for actions with side effects. The default policy allows only tools that are explicitly enabled or allowed by policy.

class EchoTool extends AiTool {
  @override
  String get name => 'echo';

  @override
  String get description => 'Returns the message argument.';

  @override
  bool get enabledByDefault => true;

  @override
  Future<Map<String, dynamic>> execute(AiToolContext context) async {
    return {
      'message': context.arguments['message'],
      'userId': context.userId,
    };
  }
}

final ai = FlintAi()..registerTool(EchoTool());

Security

AI agents should not be allowed to perform every registered action. Treat tools as privileged application operations and use policy checks before any side effect happens.

The default SafeDefaultAiToolPolicy is convenient for development. It allows tools marked enabledByDefault and can also allow specific tools or capabilities.

For production, prefer ProductionAiToolPolicy. It denies tool execution unless the invocation has an authenticated user and one of these explicit permissions:

  • the tool name is listed in allowedTools
  • the current user has a role listed in allowedRoles
  • the tool's requiredCapabilities are satisfied by user metadata or allowedCapabilities

Destructive Tools

Destructive tools are tools that send emails, delete data, update billing, issue refunds, publish content, provision infrastructure, or make irreversible external calls. Keep them restricted:

  • Do not set enabledByDefault on destructive tools.
  • Give every destructive tool a specific requiredCapabilities value.
  • Pass the current user's role and capabilities in FlintAi.run(metadata: ...).
  • Use ProductionAiToolPolicy in deployed apps.
  • Prefer queueing or review workflows for high-risk operations instead of doing the destructive action immediately.

Roles And Capabilities

Tools can declare required capabilities, and runs can provide role/capability metadata for the current user.

class RefundTool extends AiTool {
  @override
  String get name => 'billing.refund';

  @override
  String get description => 'Issues a customer refund.';

  @override
  Set<String> get requiredCapabilities => const {'billing:refund'};

  @override
  Future<Map<String, dynamic>> execute(AiToolContext context) async {
    return {'queued': true};
  }
}

final ai = FlintAi.production(
  toolPolicy: const ProductionAiToolPolicy(
    allowedRoles: {'ADMIN'},
  ),
)..registerTool(RefundTool());

await ai.run(
  agent: SupportAgent(),
  goal: const AiGoal(task: 'Handle refund request'),
  userId: 'user-1',
  metadata: {
    'role': 'EMPLOYEE',
    'capabilities': ['billing:refund'],
  },
);

If you are using Flint Dart, production policy allow-lists can come from .env:

app.ai.useProductionToolPolicyFromEnv();

Supported keys:

  • AI_ALLOWED_TOOLS
  • AI_ALLOWED_CAPABILITIES
  • AI_ALLOWED_ROLES

Chat Providers

Register a provider, then call ai.chat().

final ai = FlintAi()
  ..registerChatProvider(
    OpenAiChatProvider(apiKey: 'openai-key'),
  );

final result = await ai.chat(
  providerId: 'openai',
  request: const ChatRequest(
    model: 'gpt-4o-mini',
    messages: [
      ChatMessage(role: 'user', content: 'Say hello from Flint'),
    ],
  ),
);

print(result.content);

Built-in chat providers:

  • OpenAiChatProvider
  • GeminiChatProvider
  • AnthropicChatProvider

Streaming Chat

Use streamChat() when the UI should render text as it arrives. Providers emit chat.delta events for incremental text and a final chat.completed event with the accumulated content.

import 'dart:io';

await for (final event in ai.streamChat(
  providerId: 'openai',
  request: const ChatRequest(
    model: 'gpt-4o-mini',
    messages: [
      ChatMessage(role: 'user', content: 'Write a short welcome note'),
    ],
  ),
)) {
  if (event.type == 'chat.delta') {
    stdout.write(event.payload['content']);
  }
  if (event.type == 'chat.completed') {
    print('\nDone');
  }
}

OpenAiChatProvider, GeminiChatProvider, and AnthropicChatProvider support native server-sent event streaming.

Workflows

Workflows are named reusable operations that can be called directly.

class SupportWorkflow extends AiWorkflow {
  @override
  String get name => 'support_followup';

  @override
  String get description => 'Creates a support follow-up payload.';

  @override
  Future<Map<String, dynamic>> run(AiWorkflowContext context) async {
    return {
      'status': 'queued',
      'userId': context.userId,
      'threadId': context.threadId,
    };
  }
}

final ai = FlintAi()..registerWorkflow(SupportWorkflow());

final result = await ai.runWorkflow(
  'support_followup',
  userId: 'user-1',
  threadId: 'thread-1',
);

Memory And Persistence

By default, flint_ai uses in-memory stores. That keeps standalone scripts, tests, and local development simple.

For Flint Dart apps, use package:flint_dart/ai.dart to get database-backed adapters:

import 'package:flint_dart/ai.dart';

final ai = FlintAi.production(
  memoryStore: FlintAutoAiMemoryStore(),
  repository: FlintAutoAiRepository(),
);

Register ...flintAiTables in the consuming app's table_registry.dart and run flint migrate before relying on durable AI persistence in production.

Flint Dart Env Setup

Flint Dart adds environment-backed helpers:

import 'package:flint_dart/flint_dart.dart';
import 'package:flint_dart/ai.dart';

final app = Flint();

app.ai.useOpenAiFromEnv();
app.ai.useGeminiFromEnv();
app.ai.useAnthropicFromEnv();

// Or register every provider that has credentials configured.
final providers = app.ai.useChatProvidersFromEnv();

Supported provider keys include:

  • OPENAI_API_KEY, OPENAI_BEARER_TOKEN, OPENAI_CHAT_ENDPOINT
  • GEMINI_API_KEY, GEMINI_BEARER_TOKEN, GEMINI_CHAT_ENDPOINT
  • ANTHROPIC_API_KEY, ANTHROPIC_CHAT_ENDPOINT

License

MIT. See LICENSE.

Libraries

flint_ai
AI agents, tools, workflows, providers, memory, and runtime APIs for Flint.