agents_core 0.3.6 copy "agents_core: ^0.3.6" to clipboard
agents_core: ^0.3.6 copied to clipboard

A Dart library for orchestrating multi-agent AI workflows with LM Studio integration. Create agents, manage conversations, execute Python in Docker, and coordinate multi-step pipelines.

agents_core #

A Dart library for orchestrating multi-agent AI workflows with LM Studio integration. Define agents, manage multi-turn conversations, execute Python in Docker, and coordinate multi-step pipelines — all with zero runtime dependencies.

Prerequisites #

  • Dart SDK >= 3.10.9
  • LM Studio running locally (default http://localhost:1234) with at least one model loaded
  • Docker (optional) — required only for PythonToolAgent and sandboxed code execution

Installation #

Add agents_core to your pubspec.yaml:

dart pub add agents_core
dart pub get

Quick Start #

One-shot question #

The simplest way to get a response from an LLM:

import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final answer = await ask(
    'What is the capital of France?',
    config: AgentsCoreConfig(),
    model: 'llama-3-8b',
    systemPrompt: 'You are a helpful geography assistant.',
  );
  print(answer); // Paris is the capital of France.
}

Chat completion with LmStudioClient #

For full control over requests and responses:

import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final config = AgentsCoreConfig();
  final client = LmStudioClient(config);

  try {
    final response = await client.chatCompletion(
      ChatCompletionRequest(
        model: 'llama-3-8b',
        messages: [
          ChatMessage(role: ChatMessageRole.system, content: 'Be concise.'),
          ChatMessage(role: ChatMessageRole.user, content: 'Explain Dart isolates.'),
        ],
        temperature: 0.7,
      ),
    );

    print(response.choices.first.message.content);
    print('Tokens used: ${response.usage.totalTokens}');
  } finally {
    client.dispose();
  }
}

Custom LLM providers #

All agents accept an LlmClient (abstract interface) rather than a concrete LmStudioClient. Implement LlmClient to plug in any provider (OpenAI, Anthropic, Ollama, etc.):

class MyOpenAiClient implements LlmClient {
  @override
  Future<ChatCompletionResponse> chatCompletion(ChatCompletionRequest request) async { ... }

  @override
  Stream<ChatCompletionChunk> chatCompletionStream(ChatCompletionRequest request) { ... }

  @override
  Stream<String> chatCompletionStreamText(ChatCompletionRequest request) { ... }

  @override
  void dispose() { ... }
}

final agent = SimpleAgent(
  name: 'bot',
  client: MyOpenAiClient(),
  config: AgentsCoreConfig(),
);

Multi-turn conversation #

Maintain chat history automatically across multiple exchanges:

import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final conv = Conversation(
    config: AgentsCoreConfig(),
    model: 'llama-3-8b',
    systemPrompt: 'You are a helpful tutor.',
  );

  final reply1 = await conv.send('What is recursion?');
  print(reply1);

  final reply2 = await conv.send('Give me a Dart example.');
  print(reply2);

  print('History: ${conv.history.length} messages');
}

File context operations #

Read and write files in a sandboxed workspace:

import 'package:agents_core/agents_core.dart';

void main() {
  final ctx = FileContext(workspacePath: '/tmp/my_workspace');

  ctx.write('notes.txt', 'Hello, world!');
  print(ctx.read('notes.txt')); // Hello, world!

  ctx.append('notes.txt', '\nSecond line.');
  print(ctx.listFiles()); // [notes.txt]
}

Agent with tool calling (ReActAgent) #

Run a multi-turn agent that can call tools:

import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final config = AgentsCoreConfig();
  final client = LmStudioClient(config);
  final ctx = FileContext(workspacePath: '/tmp/agent_workspace');
  final handlers = createHandlers(ctx);

  final agent = ReActAgent(
    name: 'researcher',
    client: client,
    config: config,
    model: 'llama-3-8b',
    systemPrompt: 'You are a research assistant with file access.',
    tools: [readFileTool, writeFileTool, listFilesTool],
    toolHandlers: handlers,
    maxIterations: 5,
  );

  final result = await agent.run(
    'List all files in the workspace and summarise their contents.',
    context: ctx,
  );

  print(result.output);
  print('Tool calls made: ${result.toolCallsMade.length}');
  client.dispose();
}

Terminal tools

Some workflows use a dedicated tool (e.g. submit_result) whose invocation is the final answer — the agent should stop immediately after executing it rather than sending another request to the LLM. Pass the terminalTools parameter to declare which tool names have this behaviour:

final agent = ReActAgent(
  name: 'solver',
  client: client,
  config: config,
  model: 'llama-3-8b',
  systemPrompt: 'Solve the task and call submit_result with the answer.',
  tools: [submitResultTool],
  toolHandlers: {
    'submit_result': (args) async => args['answer'] as String,
  },
  terminalTools: {'submit_result'},
);

final result = await agent.run('What is 2 + 2?');
print(result.stoppedReason); // AgentStopReason.terminalTool

When a terminal tool is called the tool executes normally, its result is appended to the conversation, and the loop breaks with stoppedReason: AgentStopReason.terminalTool. Defaults to an empty set (no terminal tools).

Multi-agent pipeline (Orchestrator) #

Chain agents together in a sequential pipeline. Steps can be single-agent (AgentStep) or produce-review loops (AgentLoopStep):

import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final config = AgentsCoreConfig();
  final client = LmStudioClient(config);
  final ctx = FileContext(workspacePath: '/tmp/pipeline');

  final researcher = SimpleAgent(
    name: 'researcher', client: client, config: config,
    systemPrompt: 'Research and write findings to a file.',
  );
  final writer = SimpleAgent(
    name: 'writer', client: client, config: config,
    systemPrompt: 'Write a polished summary.',
  );
  final reviewer = SimpleAgent(
    name: 'reviewer', client: client, config: config,
    systemPrompt: 'Review the summary. Reply with APPROVED if acceptable.',
  );

  final orchestrator = Orchestrator(
    context: ctx,
    steps: [
      // Single-agent step
      AgentStep(agent: researcher, taskPrompt: 'Research quantum computing'),
      // Produce-review loop step
      AgentLoopStep(
        producer: writer,
        reviewer: reviewer,
        isAccepted: (result, _) => result.output.contains('APPROVED'),
        taskPrompt: 'Write and refine a summary of the research',
        maxIterations: 3,
      ),
    ],
    onError: OrchestratorErrorPolicy.continueOnError,
  );

  final result = await orchestrator.run();

  for (final stepResult in result.stepResults) {
    print('Output: ${stepResult.output}');
    print('Tokens: ${stepResult.tokensUsed}');

    if (stepResult is AgentLoopStepResult) {
      print('Loop accepted: ${stepResult.accepted}');
      print('Iterations: ${stepResult.iterationCount}');
    }
  }

  print('Duration: ${result.duration}');
  client.dispose();
}

Use AgentStep.dynamic or AgentLoopStep.dynamic when the prompt must be built at runtime from files written by earlier steps:

AgentStep.dynamic(
  agent: writer,
  taskPrompt: (ctx) async => 'Summarise: ${ctx.read("research.txt")}',
),
AgentLoopStep.dynamic(
  producer: developer,
  reviewer: qa,
  isAccepted: (result, _) => result.output.contains('APPROVED'),
  taskPrompt: (ctx) async => 'Fix the bugs in:\n${ctx.read("errors.log")}',
),

Produce-review loop (AgentLoop) #

Run iterative refinement between a producer and a reviewer agent. The loop continues until the reviewer accepts the output or maxIterations is reached:

import 'dart:io';
import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final config = AgentsCoreConfig();
  final client = LmStudioClient(config);
  final context = FileContext(
    workspacePath: '${Directory.systemTemp.path}/loop_demo',
  );

  // Producer: generates work each iteration.
  final developer = SimpleAgent(
    name: 'developer',
    client: client,
    config: config,
    model: 'llama-3-8b',
    systemPrompt: 'You are a senior Dart developer. Write clean, idiomatic code. '
        'When given review feedback, revise your code to address every issue.',
  );

  // Reviewer: evaluates the producer's output.
  final qa = SimpleAgent(
    name: 'qa-reviewer',
    client: client,
    config: config,
    model: 'llama-3-8b',
    systemPrompt: 'You are a strict code reviewer. '
        'If the code meets all criteria, begin your response with "APPROVED". '
        'Otherwise, list the issues that must be fixed.',
  );

  // Create the loop — runs up to 4 produce-review rounds.
  final loop = AgentLoop(
    context: context,
    producer: developer,
    reviewer: qa,
    isAccepted: (AgentResult reviewerResult, int iteration) =>
        reviewerResult.output.trim().toUpperCase().startsWith('APPROVED'),
    maxIterations: 4,
  );

  // Run the loop with a task description.
  final result = await loop.run(
    'Write a Dart function `int fibonacci(int n)` that returns the n-th '
    'Fibonacci number. Handle negative inputs by throwing an ArgumentError.',
  );

  // ── Inspect each iteration via AgentLoopIteration ──────────────────
  for (final iteration in result.iterations) {
    print('Iteration ${iteration.index}:');
    print('  Producer tokens: ${iteration.producerResult.tokensUsed}');
    print('  Reviewer tokens: ${iteration.reviewerResult.tokensUsed}');
  }

  // ── Read the overall AgentLoopResult ───────────────────────────────
  print('Accepted:          ${result.accepted}');
  print('Iterations:        ${result.iterationCount}');
  print('Total tokens:      ${result.totalTokensUsed}');
  print('Duration:          ${result.duration.inMilliseconds} ms');
  print('Reached max iter:  ${result.reachedMaxIterations}');

  // The final producer output is accessible directly:
  print(result.lastProducerResult.output);

  client.dispose();
}

How it works:

  1. Each iteration, AgentLoop builds a prompt and runs the producer agent.
  2. The producer's output is forwarded to the reviewer agent for evaluation.
  3. The isAccepted callback inspects the reviewer's AgentResult and the current iteration index — return true to accept and stop the loop.
  4. If the reviewer rejects, its feedback is automatically appended to the next producer prompt so the producer can address the issues.
  5. The loop stops when isAccepted returns true or maxIterations is reached (whichever comes first).

Custom prompt builders: For advanced scenarios you can supply buildProducerPrompt and buildReviewerPrompt callbacks to control exactly what each agent receives:

final loop = AgentLoop(
  context: context,
  producer: developer,
  reviewer: qa,
  isAccepted: (result, _) => result.output.contains('APPROVED'),
  buildProducerPrompt: (task, ctx, iteration, prevReview) async {
    final files = ctx.listFiles();
    return '$task\n\nWorkspace files: $files'
        '${prevReview != null ? '\n\nFeedback: ${prevReview.output}' : ''}';
  },
  buildReviewerPrompt: (task, ctx, iteration, producerResult) async {
    return 'Iteration $iteration — review:\n${producerResult.output}';
  },
);

Key classes:

Class Purpose
AgentLoop Orchestrates the produce-review cycle
AgentLoopIteration One iteration record with index, producerResult, and reviewerResult
AgentLoopResult Overall result — accepted, iterationCount, totalTokensUsed, duration, reachedMaxIterations

Loop detection #

Agents and loops can detect when the LLM is stuck repeating itself. Enable it by passing a LoopDetectionConfig:

import 'package:agents_core/agents_core.dart';

Future<void> main() async {
  final config = AgentsCoreConfig();
  final client = LmStudioClient(config);

  final agent = ReActAgent(
    name: 'researcher',
    client: client,
    config: config,
    model: 'llama-3-8b',
    systemPrompt: 'You are a research assistant.',
    tools: [readFileTool, writeFileTool],
    toolHandlers: createHandlers(
      FileContext(workspacePath: '/tmp/workspace'),
    ),
    maxIterations: 15,
    // Stop if the LLM makes the same tool calls 3 times in a row,
    // or produces near-identical outputs 3 times in a row.
    loopDetectionConfig: const LoopDetectionConfig(
      maxConsecutiveIdenticalToolCalls: 3,
      maxConsecutiveIdenticalOutputs: 3,
      similarityThreshold: 0.85, // bigram Sørensen–Dice
    ),
  );

  final result = await agent.run('Summarise the workspace files.');
  print(result.output);
  print(result.stoppedReason); // AgentStopReason.completed, .terminalTool, .loopDetected, etc.

  client.dispose();
}

AgentLoop supports the same parameter — the detector tracks producer outputs and stops early when repetition is found:

final loop = AgentLoop(
  context: ctx,
  producer: writer,
  reviewer: reviewer,
  isAccepted: (result, _) => result.output.contains('APPROVED'),
  maxIterations: 5,
  loopDetectionConfig: const LoopDetectionConfig(),
);

final result = await loop.run('Write a summary');
print(result.stoppedReason); // AgentStopReason.accepted, .maxIterations, or .loopDetected
print(result.loopDetected);  // true if stopped due to loop

Module Overview #

Module Key Classes Description
Agent Agent, SimpleAgent, ReActAgent, AgentResult, AgentStopReason Define and run AI agents with tool calling
Client LlmClient, LmStudioClient, LmStudioHttpClient, SseParser LLM client interface and HTTP client for LM Studio's OpenAI-compatible API
Models ChatMessage, ChatCompletionRequest, ChatCompletionResponse, ChatCompletionChunk, ToolDefinition, ToolCall, LmModel Request/response data structures
Config AgentsCoreConfig, LmStudioConfig, DockerConfig, LoggingConfig, Logger, StderrLogger, SilentLogger Configuration and logging
File Context FileContext, readFileTool, writeFileTool, listFilesTool, createHandlers Sandboxed file operations with tool definitions
Orchestrator Orchestrator, OrchestratorStep, AgentStep, AgentLoopStep, TaskPrompt, StaticPrompt, DynamicPrompt, OrchestratorResult, StepResult, AgentStepResult, AgentLoopStepResult, OrchestratorErrorPolicy Sequential multi-agent pipelines with mixed step types
AgentLoop AgentLoop, AgentLoopIteration, AgentLoopResult Iterative produce-review refinement loop
Loop Detection LoopDetectionConfig, LoopDetector, LoopCheckResult Detect and break repetitive LLM loops via tool-call fingerprinting and bigram similarity
Docker DockerClient, DockerRunResult Container management for sandboxed execution
Python PythonToolAgent, PythonExecutionTool Python code execution in Docker
Quick ask, askStream, Conversation Convenience functions for common patterns
Utils Disposable, TextSimilarity Resource lifecycle mixin and text similarity utilities
Exceptions AgentsCoreException, LmStudioApiException, LmStudioConnectionException, FileNotFoundException, PathTraversalException Structured error hierarchy

Examples #

See the example/ directory for runnable examples:

Configuration #

AgentsCoreConfig accepts these parameters:

Parameter Default Description
lmStudioBaseUrl http://localhost:1234 LM Studio server URL
defaultModel lmstudio-community/default Default model identifier
requestTimeout 60 seconds HTTP connection timeout
dockerImage python:3.12-slim Docker image for Python execution
workspacePath /tmp/agents_workspace Default workspace path
apiKey null Optional Bearer token for authenticated LM Studio requests
loggingEnabled true Global toggle to enable/disable all logging
logger StderrLogger(level: LogLevel.info) Logger instance

You can also create configuration from environment variables:

final config = AgentsCoreConfig.fromEnvironment();

Or compose it from focused sub-configs using AgentsCoreConfig.fromConfigs:

final config = AgentsCoreConfig.fromConfigs(
  lmStudio: LmStudioConfig(
    baseUrl: Uri.parse('http://localhost:1234'),
    defaultModel: 'llama-3-8b',
    apiKey: 'my-key',
  ),
  docker: DockerConfig(image: 'python:3.12-slim'),
  logging: LoggingConfig(loggingEnabled: false),
);

Supported environment variables: LM_STUDIO_BASE_URL, AGENTS_DEFAULT_MODEL, AGENTS_DOCKER_IMAGE, AGENTS_WORKSPACE_PATH, AGENTS_REQUEST_TIMEOUT_SECONDS, AGENTS_API_KEY, AGENTS_LOGGING_ENABLED.

Logging #

Logging is enabled by default — all library components write timestamped diagnostic messages to stderr via StderrLogger.

Disable logging globally

Pass loggingEnabled: false to suppress all log output without replacing the logger instance:

final config = AgentsCoreConfig(loggingEnabled: false);

Or set the AGENTS_LOGGING_ENABLED environment variable:

export AGENTS_LOGGING_ENABLED=false  # also accepts "0"

Then create the config from the environment:

final config = AgentsCoreConfig.fromEnvironment();
// Logging is now disabled — all logger calls are silently discarded.

Re-enable logging on an existing config

Use copyWith to toggle logging at any point:

final silent = AgentsCoreConfig(loggingEnabled: false);
final verbose = silent.copyWith(loggingEnabled: true);

Custom log level

Control the minimum severity emitted by the default StderrLogger:

final config = AgentsCoreConfig(
  logger: StderrLogger(level: LogLevel.debug), // debug, info, warn, error
);

Custom logger

Implement the Logger interface to integrate with your own logging framework:

class MyLogger extends Logger {
  @override
  LogLevel get level => LogLevel.info;

  @override
  void debug(String message) { /* ... */ }
  @override
  void info(String message)  { /* ... */ }
  @override
  void warn(String message)  { /* ... */ }
  @override
  void error(String message) { /* ... */ }
}

final config = AgentsCoreConfig(logger: MyLogger());

Note: When loggingEnabled is false, the config.logger getter returns a SilentLogger transparently — your custom logger is preserved internally and becomes active again when logging is re-enabled.

Authentication #

When the LM Studio server is deployed behind an API gateway or reverse proxy that requires authentication, provide an apiKey. The key is sent as a Bearer token in the Authorization header of every outgoing HTTP request.

// Explicit API key
final config = AgentsCoreConfig(
  apiKey: 'my-secret-key',
);

// Or read from the AGENTS_API_KEY environment variable
final config = AgentsCoreConfig.fromEnvironment();

// Add or remove a key from an existing config
final authenticated = config.copyWith(apiKey: 'new-key');
final anonymous = config.copyWith(clearApiKey: true);

Tip: For local development without authentication, omit the apiKey parameter — it defaults to null and no Authorization header is sent.

See example/api_key_config.dart for a complete runnable example.

License #

MIT — see LICENSE for details.

Copyright 2026 David Araujo

1
likes
140
points
72
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Dart library for orchestrating multi-agent AI workflows with LM Studio integration. Create agents, manage conversations, execute Python in Docker, and coordinate multi-step pipelines.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

meta

More

Packages that depend on agents_core