Genkit Middleware

A collection of useful middleware for Genkit Dart to enhance your agent's capabilities.

Features

  • Agents: Let a main agent delegate tasks to specialized sub-agents.
  • Filesystem: Give your agent read/write access to a specific directory.
  • Skills: Inject reusable instructions/personas from markdown files.
  • Tool Approval: Require human approval before executing sensitive tools.

Installation

Install genkit_middleware package:

dart pub add genkit_middleware

# or

flutter pub add genkit_middleware

Setup

To use these middleware, you must first register their corresponding plugins when initializing Genkit.

import 'package:genkit/genkit.dart';
import 'package:genkit_middleware/genkit_middleware.dart';

void main() {
  final ai = Genkit(
    plugins: [
      // Register the plugins here
      AgentsPlugin(),
      FilesystemPlugin(),
      SkillsPlugin(),
      ToolApprovalPlugin(),
    ],
  );
  
  // ...
}

Usage

Agents Middleware

Enables sub-agent delegation. For each configured agent the middleware injects a dedicated delegation tool (e.g. delegate_to_researcher) and appends a <sub-agents> block to the system prompt listing the available agents and their descriptions. When the model calls a delegation tool, the middleware resolves the target agent from the registry, runs it, and returns the sub-agent's response as the tool result.

Key behaviors:

  • Injects one delegation tool per agent, named <toolPrefix>_<agentName> (default prefix: delegate_to).
  • Agent descriptions are auto-discovered from the registry and surfaced in the system prompt.
  • Sub-agent interrupts and failures are returned as tool responses (not thrown), allowing the orchestrator to self-correct.
  • Sub-agent artifacts are merged into the parent session and/or returned inline, controlled by artifactStrategy.
  • Delegations to sub-agents that keep a session (server-managed, i.e. those with a store) return a taskId handle, which the continue_task tool can use to retry a failed/aborted run or follow up on a completed one.
  • With async: true, delegations can run in the background and are collected later (see below).

Options

Option Type Default Description
agents List<String> — (required) Names of registered agents available for delegation. Each name gets a dedicated delegation tool.
toolPrefix String? 'delegate_to' Prefix for generated delegation tool names. Set to '' to use bare agent names.
maxDelegations int? unlimited Maximum sub-agent delegations allowed per generate call. Prevents runaway delegation loops.
historyLength int? 0 Number of recent conversation messages (user/model only) to forward to sub-agents as context.
artifactStrategy String? 'inline' inline: artifact content is included in the tool result and merged into the parent session. session: artifacts are merged into the parent session only (the tool result lists names only).
async bool? false Enables background delegation. Delegation tools gain a background flag, and the check_background_tasks / wait_for_background_tasks / abort_background_tasks tools are added. Requires server-managed sub-agents (those with a store).

Background (async) delegation

Set async: true to let the orchestrator fan work out to sub-agents in parallel instead of blocking on each in turn. A delegation tool call with "background": true starts the sub-agent and returns a taskId immediately; the orchestrator keeps working and collects the results later.

Three shared tools are added:

  • check_background_tasks — non-blocking status (and results) for the given taskIds.
  • wait_for_background_tasks — blocks until the listed tasks settle. Supports timeoutSeconds (0/omitted or an overflowing value waits indefinitely, negative returns immediately) and waitFor: "first" to return as soon as any one task settles. A CancellationController cancel on the generate call aborts an in-flight wait.
  • abort_background_tasks — stops tasks whose results are no longer needed.

Background delegation requires server-managed sub-agents (defined with a store), since only they can detach and persist a resumable snapshot. A launch against a client-managed agent is refused with a note telling the model to delegate synchronously instead. Task handles are self-contained ("<agent>:<snapshotId>") and ride in the orchestrator's history, so a re-instantiated orchestrator can resume tracking from the IDs alone.

Note: wait_for_background_tasks polls each task's snapshot on an interval (the same strategy DetachedTask.wait uses); there is no single blocking wait action in the Dart runtime.

Continuing a task

Whenever a sub-agent keeps a session, its delegation result carries a taskId. The continue_task tool spends that handle:

  • retry a failed or aborted task from its last saved progress (omit instructions to re-attempt as it stood, or pass instructions to steer it);
  • follow up on a completed task inside its own session (instructions required), without repeating the finished work.

A task that stopped on an interrupt cannot be continued, and a result without a taskId (a client-managed delegation) is not continuable; delegate again to redo that work.

final result = await ai.generate(
  model: modelRef('gemini-flash-latest'),
  prompt: 'Research topics A, B, and C in parallel, then summarize.',
  use: [
    agents(
      agents: ['researcher', 'writer'],
      async: true,
      maxDelegations: 8,
    ),
  ],
);

See testapps/agents/bin/async_agent_sample.dart for a runnable example.

Configuration

import 'package:genkit/genkit.dart';
import 'package:genkit_middleware/agents.dart';

final ai = Genkit(plugins: [AgentsPlugin(), /* ... */]);

// Define sub-agents (descriptions are auto-discovered by the middleware).
final researcher = ai.defineAgent(
  name: 'researcher',
  description: 'Searches the web and summarizes findings.',
  system: 'You are a research assistant.',
);

final coder = ai.defineAgent(
  name: 'coder',
  description: 'An expert programmer that writes clean code.',
  system: 'You are an expert programmer.',
);

// Main orchestrator agent delegates to sub-agents. This injects
// `delegate_to_researcher` and `delegate_to_coder` tools.
final orchestrator = ai.defineAgent(
  name: 'orchestrator',
  system: 'Delegate research to the researcher and coding to the coder.',
  use: [
    agents(agents: ['researcher', 'coder']),
  ],
);

You can customize the tool-name prefix and forward conversation history:

use: [
  agents(
    // Tools become `ask_researcher` and `ask_coder`.
    toolPrefix: 'ask',
    agents: ['researcher', 'coder'],
    maxDelegations: 5,
    historyLength: 4,
  ),
]

Filesystem Middleware

Allows the agent to list, read, write, and search/replace files within a restricted root directory.

Configuration

// ... inside your generate call
final response = await ai.generate(
  prompt: 'Check the logs in the current directory.',
  use: [
    // Configure the middleware for this request
    filesystem(rootDirectory: '/path/to/secure/workspace'),
  ],
);

Tools Provided:

  • list_files: List files and directories.
  • read_file: Read file contents.
  • write_file: Write content to a file.
  • search_and_replace: Targeted search and replace in files.

Skills Middleware

Injects specialized instructions (skills) into the system prompt from SKILL.md files located in specified directories.

Configuration

// ... inside your generate call
final response = await ai.generate(
  prompt: 'Help me debug this issue.',
  use: [
    skills(skillPaths: ['/path/to/skills']),
  ],
);

Tools Provided:

  • use_skill: Retrieve the full content of a skill by name.

Skill File Format: Create a SKILL.md file in a subdirectory of your skills path.

---
name: debugging_expert
description: Expert advice on debugging complex issues.
---
# Debugging Expert

You are an expert at debugging. Always follow these steps:
1. Analyze the stack trace.
2. Isolate the reproduction case.
3. ...

Tool Approval Middleware

Intercepts tool execution for specified tools and requires explicit approval (via interrupt).

Configuration

// ... inside your generate call
final response = await ai.generate(
  prompt: 'Delete the database.',
  use: [
    // 'delete_db' and 'deploy_prod' are NOT in this list, so they will require approval
    toolApproval(approved: ['read_file', 'list_files']),
  ],
);

Handling Interrupts

When a protected tool is called, generate will return with FinishReason.interrupted. You must handle this interrupt to approve or deny the tool execution.

if (response.finishReason == FinishReason.interrupted) {
  final interrupt = response.interrupts.first;
  print('Tool ${interrupt.toolRequest.name} requires approval.');
  
  // Ask user for approval
  final isApproved = await askUser(); // Implement your logic

  if (isApproved) {
    // Resume generation with approval metadata
    final resumeResponse = await ai.generate(
      messages: response.messages, // Pass history
      toolChoice: ToolChoice.none, // Prevent immediate re-call
      // ... other options
      interruptRestart: [
        interrupt.toolRequestPart!.restart({'tool-approved': true}),
      ],
    );
  }
}

Libraries

agents
Middleware for Genkit Dart that enables sub-agent delegation.
filesystem
Middleware for Genkit Dart to properly load files
genkit_middleware
skills
Middleware for Genkit Dart to inject skills.
tool_approval