TypeScriptADK-TS

Plugins

Add cross-cutting behavior to your agents without modifying agent code

A plugin is a reusable module that hooks into the agent lifecycle to add cross-cutting behavior — things like logging, error recovery, output filtering, or observability — without modifying your agent code.

How plugins work

A plugin extends BasePlugin and implements one or more callback hooks. These hooks fire at specific moments during execution:

User Message → beforeRun → beforeAgent → beforeModel → [LLM Call] → afterModel
  → beforeTool → [Tool Call] → afterTool → afterAgent → onEvent → afterRun

At each hook, a plugin can:

  • Observe — return undefined to log or track without interfering
  • Intervene — return a value to short-circuit (e.g., return a cached response to skip the LLM call)
  • Amend — modify the context objects before they're used

Quick start

import { AgentBuilder, ReflectAndRetryToolPlugin } from "@iqai/adk";

const { runner } = await AgentBuilder.withModel("gpt-4o")
  .withFallbackModels("gpt-4o-mini", "gemini-2.0-flash")
  .withPlugins(new ReflectAndRetryToolPlugin({ maxRetries: 3 }))
  .build();

This agent will automatically retry failed tools up to 3 times with reflection, and fall back to cheaper models if it gets rate limited — all without touching the agent's own logic.

Prebuilt plugins

ADK-TS ships four plugins you can use immediately:

Registering plugins

There are three ways to register plugins, each with different scope:

import { AgentBuilder, ReflectAndRetryToolPlugin } from "@iqai/adk";

const { runner } = await AgentBuilder.withModel("gemini-1.5-flash")
  .withDescription("My assistant")
  .withPlugins(new ReflectAndRetryToolPlugin({ maxRetries: 3 }))
  .build();

Plugins registered via withPlugins() are passed to the Runner that build() creates, so they apply globally.

import { InMemoryRunner, LlmAgent, ReflectAndRetryToolPlugin } from "@iqai/adk";

const agent = new LlmAgent({
  name: "my_agent",
  description: "My assistant",
  model: "gemini-1.5-flash",
});

const runner = new InMemoryRunner(agent, {
  appName: "my-app",
  plugins: [new ReflectAndRetryToolPlugin({ maxRetries: 3 })],
  pluginCloseTimeout: 5000, // optional: bound shutdown time
});

Runner-level plugins apply to all agents, tools, and LLM calls managed by that runner.

import { LlmAgent, ReflectAndRetryToolPlugin } from "@iqai/adk";

const agent = new LlmAgent({
  name: "my_agent",
  description: "Agent with scoped plugins",
  model: "gemini-1.5-flash",
  plugins: [new ReflectAndRetryToolPlugin({ maxRetries: 3 })],
});

Agent-level plugins are scoped to that specific agent rather than applying globally.

MethodScopeUse when
AgentBuilder.withPlugins()Global (via built Runner)Most common — fluent API
Runner / InMemoryRunnerGlobal (all agents, tools, LLMs)Need direct Runner control
LlmAgent({ plugins })Agent-scopedPlugin should only apply to one agent

Using multiple plugins

Plugins execute in registration order. Place them in the order that makes sense for your workflow:

import {
  AgentBuilder,
  LLMRegistry,
  ToolOutputFilterPlugin,
  ReflectAndRetryToolPlugin,
  LangfusePlugin,
} from "@iqai/adk";

const { runner } = await AgentBuilder.withModel("gemini-1.5-pro")
  .withDescription("Production assistant")
  .withPlugins(
    // 1. Monitor everything
    new LangfusePlugin({
      publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
      secretKey: process.env.LANGFUSE_SECRET_KEY!,
    }),
    // 2. Shrink large tool outputs
    new ToolOutputFilterPlugin({
      filterModel: LLMRegistry.newLLM("gemini-1.5-flash"),
    }),
    // 3. Retry failed tools
    new ReflectAndRetryToolPlugin({ maxRetries: 3 }),
  )
  .build();

The first plugin to return a non-undefined value from a callback short-circuits the rest — so order matters.

Plugins vs agent callbacks

Plugins and agent callbacks both use the same hook points, but serve different purposes:

PluginsAgent Callbacks
ScopeGlobal — apply to all agents/tools/LLMs in the RunnerLocal — apply only to the specific agent they're configured on
Use caseCross-cutting concerns: logging, monitoring, caching, error recoveryAgent-specific logic: custom behavior for a single agent
Execution orderRun before agent callbacksRun after plugin callbacks
ConfigurationOnce on Runner or AgentBuilderIndividually per agent

If a plugin callback returns a value, the corresponding agent callback is skipped entirely.

Building a custom plugin

Extend BasePlugin and implement only the hooks you need:

import { BasePlugin, Agents, Models } from "@iqai/adk";
import type { Content } from "@google/genai";

export class SimpleLoggingPlugin extends BasePlugin {
  constructor() {
    super("simple_logging");
  }

  // Log every user message
  async onUserMessageCallback(params: {
    invocationContext: Agents.InvocationContext;
    userMessage: Content;
  }): Promise<Content | undefined> {
    const text =
      params.userMessage?.parts?.map(p => p.text || "").join("") || "";
    console.log(`[user] ${text.slice(0, 200)}`);
    return undefined; // observe only — don't modify
  }

  // Log token usage after each LLM call
  async afterModelCallback(params: {
    callbackContext: Agents.CallbackContext;
    llmResponse: Models.LlmResponse;
  }): Promise<Models.LlmResponse | undefined> {
    const tokens = params.llmResponse.usageMetadata?.totalTokenCount;
    console.log(`[model] ${tokens} tokens used`);
    return undefined; // observe only
  }

  // Cleanup on shutdown
  async close(): Promise<void> {
    console.log("[plugin] closed");
  }
}

Then register it like any other plugin:

import { AgentBuilder } from "@iqai/adk";
import { SimpleLoggingPlugin } from "./simple-logging-plugin";

const { runner } = await AgentBuilder.withModel("gemini-1.5-flash")
  .withDescription("My assistant")
  .withPlugins(new SimpleLoggingPlugin())
  .build();

Callback hooks reference

Every hook is optional — implement only what you need. All hooks are async and receive a params object.

Lifecycle hooks

HookFires whenReturn to intervene
onUserMessageCallbackUser sends a message (first hook to run)Content to replace the message
beforeRunCallbackBefore any agent logic beginsEvent to halt execution early
afterRunCallbackAfter the Runner completesNothing (cleanup only)
onEventCallbackAgent produces an event (text, tool result)Event to replace the event

Agent hooks

HookFires whenReturn to intervene
beforeAgentCallbackBefore an agent starts executingContent to skip the agent
afterAgentCallbackAfter an agent completesContent to replace the result

Model hooks

HookFires whenReturn to intervene
beforeModelCallbackBefore an LLM API callLlmResponse to skip the call (e.g., cache hit)
afterModelCallbackAfter a successful LLM responseLlmResponse to replace the response
onModelErrorCallbackWhen an LLM call throwsLlmResponse to recover, or undefined to propagate

Error type

The error parameter in error callbacks is typed as unknown in BasePlugin. In practice, PluginManager always passes an Error instance, but defensive type guards (e.g., params.error instanceof Error) are recommended.

Tool hooks

HookFires whenReturn to intervene
beforeToolCallbackBefore a tool executesRecord<string, any> to skip execution
afterToolCallbackAfter a tool completesRecord<string, any> to replace the result
onToolErrorCallbackWhen a tool throwsRecord<string, any> to recover, or undefined to propagate

Best practices

These apply to all plugins — prebuilt and custom. For plugin-specific guidance, see the individual plugin pages.

Design

  • Single responsibility — each plugin should handle one concern (logging, caching, auth, etc.)
  • Handle errors gracefully — plugin errors should not crash your application. Use try-catch.
  • Document intervention — clearly note when your plugin returns values (short-circuits) vs observes

Configuration

  • Start with defaults — all prebuilt plugins ship sensible defaults. Tune only after measuring.
  • Test in development first — log callback frequency and latency before deploying to production

Multi-plugin ordering

  • Registration order matters — plugins execute in the order you register them
  • Filter before retry — place ToolOutputFilterPlugin before ReflectAndRetryToolPlugin
  • First intervention wins — the first plugin to return non-undefined short-circuits the rest
  • Unique names required — each plugin must have a unique name or registration will throw

Troubleshooting

IssueCauseFix
Plugin not executingNot registeredVerify registration on Runner/AgentBuilder/LlmAgent
Callback not firingWrong method nameCheck exact method names from BasePlugin (e.g., afterToolCallback not onAfterTool)
Plugin overriding anotherRegistration orderReorder — first to return non-undefined wins
Shutdown hangingclose() never resolvesSet pluginCloseTimeout on Runner (default: 5000ms)
Duplicate name errorTwo plugins share a nameGive each plugin a unique name in its constructor

For plugin-specific issues, see the individual plugin pages. For general framework issues, see the troubleshooting guide.

Next steps