Skip to content

Plugins

The ADK includes a powerful plugin system that allows you to intercept and modify the execution pipeline at key lifecycle stages. Plugins are ideal for implementing cross-cutting concerns like telemetry, auditing, security gating, and debugging.

Core Plugin Architecture

You create a custom plugin by inheriting from BasePlugin and overriding the desired virtual lifecycle methods.

using GoogleAdk.Core.Plugins;
using GoogleAdk.Core.Context;
using GoogleAdk.Core.Abstractions.Events;

public class LoggingPlugin : BasePlugin
{
    private readonly Action<string> _logAction;

    public LoggingPlugin(Action<string> logAction)
    {
        _logAction = logAction;
    }

    // Intercept when the user's initial message is received
    public override Task<Content?> OnUserMessageCallbackAsync(InvocationContext context, Content message)
    {
        _logAction($"[User]: {message.Parts?.FirstOrDefault()?.Text}");
        return Task.FromResult<Content?>(null); // Return null to continue unmodified
    }

    // Intercept generic events emitted by the runner
    public override Task<Event?> OnEventCallbackAsync(InvocationContext context, Event adkEvent)
    {
        _logAction($"[Event]: {adkEvent.Author} emitted {adkEvent.Id}");
        return Task.FromResult<Event?>(null);
    }
}

Security & Policy Engine Plugins

A very common use case for plugins is enforcing execution boundaries. The SecurityPlugin acts as a gatekeeper, evaluating tool calls against an IBasePolicyEngine before allowing execution.

Creating a Custom Policy Engine

You can implement an IBasePolicyEngine that defines a granular permission set, such as a deny list, allow list, or required confirmations.

using GoogleAdk.Core.Plugins;
using GoogleAdk.Core.Context;

public class DenyListPolicyEngine : IBasePolicyEngine
{
    private readonly HashSet<string> _denyList = new() { "dangerous_tool", "drop_table" };
    private readonly HashSet<string> _confirmList = new() { "sensitive_tool" };

    public Task<PolicyCheckResult> EvaluateAsync(ToolCallPolicyContext context)
    {
        var toolName = context.Tool.Name;

        // Immediately block execution
        if (_denyList.Contains(toolName))
            return Task.FromResult(new PolicyCheckResult
            {
                Outcome = PolicyOutcome.Deny,
                Reason = $"Tool '{toolName}' is strictly prohibited."
            });

        // Pause execution and require human confirmation
        if (_confirmList.Contains(toolName))
            return Task.FromResult(new PolicyCheckResult
            {
                Outcome = PolicyOutcome.Confirm,
                Reason = $"Tool '{toolName}' accesses PII and requires manual confirmation."
            });

        // Standard execution
        return Task.FromResult(new PolicyCheckResult
        {
            Outcome = PolicyOutcome.Allow,
            Reason = "Approved."
        });
    }
}

Wiring Plugins to the Runner

Plugins are instantiated and registered globally across your Runner via the RunnerConfig.

var logs = new List<string>();
var loggingPlugin = new LoggingPlugin(msg => logs.Add(msg));

var customPolicy = new DenyListPolicyEngine();
var securityPlugin = new SecurityPlugin(customPolicy);

var runner = new Runner(new RunnerConfig
{
    AppName = "plugins-sample",
    Agent = myAgent,
    // Apply plugins to the execution pipeline
    Plugins = [ loggingPlugin, securityPlugin ]
});