Tool Execution Plugins
Tool Execution Plugins
The Tool Execution Plugin System provides a pluggable architecture for extending tool execution with custom behavior. This enables applications to add logging, analytics, UI updates, permission prompts, caching, or any custom logic to the tool execution lifecycle.
Overview
┌─────────────────────────────────────────────────────────────────┐
│ ToolManager (existing) │
│ - Tool registration │
│ - Tool lookup │
│ - Circuit breaker per tool │
└─────────────────────────────────────────────────────────────────┘
│
│ uses
▼
┌─────────────────────────────────────────────────────────────────┐
│ ToolExecutionPipeline │
│ - Orchestrates plugin chain │
│ - Manages execution lifecycle │
│ - Provides execution context │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Plugin 1 │ │ Plugin 2 │ │ Plugin N │
│ (Logging) │ │(Analytics)│ │ (Custom) │
└───────────┘ └───────────┘ └───────────┘
Basic Usage
import { Agent, LoggingPlugin, type IToolExecutionPlugin } from 'oneringai';
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
tools: [weatherTool],
});
// Add the built-in logging plugin
agent.tools.executionPipeline.use(new LoggingPlugin());
// Now all tool executions will be logged with timing info
const response = await agent.run('What is the weather in Paris?');
Plugin Interface
Every plugin must implement the IToolExecutionPlugin interface:
interface IToolExecutionPlugin {
/** Unique plugin name */
readonly name: string;
/** Priority (lower = runs earlier in beforeExecute, later in afterExecute). Default: 100 */
readonly priority?: number;
/**
* Called before tool execution.
* Can modify args, abort execution, or pass through.
*/
beforeExecute?(ctx: PluginExecutionContext): Promise<BeforeExecuteResult>;
/**
* Called after successful tool execution.
* Can modify the result before returning to caller.
* Note: Runs in REVERSE priority order for proper unwinding.
*/
afterExecute?(ctx: PluginExecutionContext, result: unknown): Promise<unknown>;
/**
* Called when tool execution fails.
* Can recover (return a value), re-throw, or transform the error.
*/
onError?(ctx: PluginExecutionContext, error: Error): Promise<unknown>;
/** Called when plugin is registered (optional setup) */
onRegister?(pipeline: IToolExecutionPipeline): void;
/** Called when plugin is unregistered (optional cleanup) */
onUnregister?(): void;
}
Execution Context
The PluginExecutionContext provides all information about the current tool execution:
interface PluginExecutionContext {
/** Name of the tool being executed */
toolName: string;
/** Original arguments (read-only) */
readonly args: unknown;
/** Mutable arguments - modify this to change tool input */
mutableArgs: unknown;
/** Metadata map for passing data between plugins */
metadata: Map<string, unknown>;
/** Timestamp when execution started */
startTime: number;
/** The tool function being executed */
tool: ToolFunction;
/** Unique ID for this execution (for tracing) */
executionId: string;
}
BeforeExecute Results
The beforeExecute hook can return different values to control execution:
type BeforeExecuteResult =
| void // Continue with original args
| undefined // Continue with original args
| { abort: true; result: unknown } // Abort and return this result
| { modifiedArgs: unknown }; // Continue with modified args
Creating Custom Plugins
Analytics Plugin Example
const analyticsPlugin: IToolExecutionPlugin = {
name: 'analytics',
priority: 50, // Run early
async beforeExecute(ctx) {
// Record start time in metadata
ctx.metadata.set('analytics:start', Date.now());
console.log(`[Analytics] Starting ${ctx.toolName}`);
},
async afterExecute(ctx, result) {
const startTime = ctx.metadata.get('analytics:start') as number;
const duration = Date.now() - startTime;
// Track metrics
trackToolUsage({
tool: ctx.toolName,
duration,
executionId: ctx.executionId,
success: true,
});
return result; // Must return the result
},
async onError(ctx, error) {
const startTime = ctx.metadata.get('analytics:start') as number;
const duration = Date.now() - startTime;
trackToolUsage({
tool: ctx.toolName,
duration,
executionId: ctx.executionId,
success: false,
error: error.message,
});
return undefined; // Let error propagate
},
};
agent.tools.executionPipeline.use(analyticsPlugin);
Caching Plugin Example
const cache = new Map<string, { result: unknown; expiry: number }>();
const cachePlugin: IToolExecutionPlugin = {
name: 'cache',
priority: 10, // Run very early to short-circuit
async beforeExecute(ctx) {
const key = `${ctx.toolName}:${JSON.stringify(ctx.args)}`;
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache] HIT for ${ctx.toolName}`);
return { abort: true, result: cached.result };
}
ctx.metadata.set('cache:key', key);
return undefined; // Continue with execution
},
async afterExecute(ctx, result) {
const key = ctx.metadata.get('cache:key') as string;
if (key) {
cache.set(key, {
result,
expiry: Date.now() + 60000, // 1 minute TTL
});
console.log(`[Cache] Stored result for ${ctx.toolName}`);
}
return result;
},
};
Args Transformation Plugin Example
const sanitizePlugin: IToolExecutionPlugin = {
name: 'sanitize-args',
priority: 20,
async beforeExecute(ctx) {
// Sanitize string arguments
const args = ctx.mutableArgs as Record<string, unknown>;
for (const [key, value] of Object.entries(args)) {
if (typeof value === 'string') {
args[key] = value.trim().slice(0, 1000); // Trim and limit length
}
}
return { modifiedArgs: args };
},
};
Pipeline Management
// Add a plugin
agent.tools.executionPipeline.use(myPlugin);
// Remove a plugin by name
agent.tools.executionPipeline.remove('my-plugin');
// Check if a plugin is registered
if (agent.tools.executionPipeline.has('logging')) {
console.log('Logging is enabled');
}
// Get a specific plugin
const loggingPlugin = agent.tools.executionPipeline.get('logging');
// List all registered plugins
const plugins = agent.tools.executionPipeline.list();
console.log('Registered plugins:', plugins.map(p => p.name));
Plugin Priority
Plugins are sorted by priority (lower number = higher priority):
- beforeExecute: Runs in priority order (lower first)
- afterExecute: Runs in REVERSE priority order (higher first)
- onError: Runs in priority order (lower first)
This ensures proper "unwinding" behavior, similar to middleware stacks.
// Example priority ordering:
const earlyPlugin: IToolExecutionPlugin = { name: 'early', priority: 10 };
const defaultPlugin: IToolExecutionPlugin = { name: 'default' }; // priority: 100
const latePlugin: IToolExecutionPlugin = { name: 'late', priority: 200 };
// beforeExecute order: early → default → late
// afterExecute order: late → default → early
Built-in Plugins
Note: ToolManager automatically registers a ResultNormalizerPlugin (name 'result-normalizer', priority 0) on every execution pipeline to guarantee tool results are always valid/serializable — the pipeline is never actually empty. Remove it via agent.tools.executionPipeline.remove('result-normalizer') if you don't want this behavior, or reconfigure it via new ResultNormalizerPlugin({ wrapPrimitives, addSuccessField }).
LoggingPlugin
Logs all tool executions with timing and result information:
import { LoggingPlugin } from 'oneringai';
// Use with default settings (debug level)
agent.tools.executionPipeline.use(new LoggingPlugin());
// Configure log level
agent.tools.executionPipeline.use(new LoggingPlugin({
level: 'debug', // 'trace' | 'debug' | 'info' | 'warn' | 'error'
}));
Output example:
[Tool] get_weather starting with args: {"location":"Paris"}
[Tool] get_weather completed in 234ms
[Tool] get_weather result: {"temp":72,"conditions":"sunny"}
Use Cases
- Logging & Observability: Track all tool executions for debugging
- Analytics: Measure tool usage, latency, and success rates
- Permission Prompts: Ask for user approval before dangerous tools
- Caching: Cache expensive tool results
- Rate Limiting: Limit tool calls per minute
- UI Updates: Emit events for frontend updates (like browser tool views)
- Audit Logging: Record all tool executions for compliance
- Mocking: Replace tools with mocks for testing
- Retry Logic: Automatically retry failed tool calls
- Transformation: Sanitize inputs or transform outputs
Frontend UI Integration
A host application can use the plugin system to emit UI updates when browser or other long-running tools execute:
import type { IToolExecutionPlugin, PluginExecutionContext } from 'oneringai';
export class BrowserUIPlugin implements IToolExecutionPlugin {
readonly name = 'browser-ui';
readonly priority = 200; // Run late
constructor(private options: {
emitUI: (instanceId: string, content: BrowserUIContent) => void;
getInstanceId: () => string;
}) {}
async beforeExecute(ctx: PluginExecutionContext) {
if (this.isBrowserTool(ctx.toolName)) {
ctx.metadata.set('instanceId', this.options.getInstanceId());
}
}
async afterExecute(ctx: PluginExecutionContext, result: unknown) {
if (this.isBrowserTool(ctx.toolName)) {
const instanceId = ctx.metadata.get('instanceId') as string;
const typedResult = result as { success?: boolean; url?: string };
if (typedResult?.success) {
this.options.emitUI(instanceId, {
type: 'display',
title: 'Browser',
elements: [{ type: 'browser', instanceId, currentUrl: typedResult.url }],
});
}
}
return result;
}
private isBrowserTool(name: string): boolean {
return ['browser_navigate', 'browser_reload'].includes(name);
}
}
// Register with agent
agent.tools.executionPipeline.use(new BrowserUIPlugin({
emitUI: (id, content) => frontendEvents.emit('browser-ui', { id, content }),
getInstanceId: () => currentInstanceId,
}));