or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced

error-handling.mdtype-inference.md
glossary.mdindex.mdquick-reference.mdtask-index.md
tile.json

overview.mddocs/middleware/

Middleware System Overview

The middleware system provides a composable architecture for extending agent behavior through interceptors and lifecycle hooks.

Architecture

Middleware components can:

  • Intercept and wrap tool calls
  • Intercept and wrap model invocations
  • Modify state at various execution stages
  • Add additional tools to agents
  • Implement cross-cutting concerns (logging, retries, validation, etc.)

Creating Custom Middleware

/**
 * Create typed middleware for extending agent behavior
 * @param config - Middleware configuration with hooks and schemas
 * @returns Typed middleware instance
 */
function createMiddleware<
  TSchema = any,
  TContextSchema = any,
  TTools extends readonly Tool[] = readonly Tool[]
>(
  config: MiddlewareConfig<TSchema, TContextSchema, TTools>
): AgentMiddleware<TSchema, TContextSchema, TTools>;

interface MiddlewareConfig<TSchema, TContextSchema, TTools> {
  /**
   * Middleware name for identification
   */
  name: string;

  /**
   * State schema (persisted across invocations)
   */
  stateSchema?: TSchema;

  /**
   * Context schema (read-only, not persisted)
   */
  contextSchema?: TContextSchema;

  /**
   * Additional tools provided by middleware
   */
  tools?: TTools;

  /**
   * Wrap tool execution
   */
  wrapToolCall?: WrapToolCallHook<TSchema, TContextSchema>;

  /**
   * Wrap model invocation
   */
  wrapModelCall?: WrapModelCallHook<TSchema, TContextSchema>;

  /**
   * Run before agent starts
   */
  beforeAgent?: BeforeAgentHook<TSchema, TContextSchema>;

  /**
   * Run before model call
   */
  beforeModel?: BeforeModelHook<TSchema, TContextSchema>;

  /**
   * Run after model call
   */
  afterModel?: AfterModelHook<TSchema, TContextSchema>;

  /**
   * Run after agent completes
   */
  afterAgent?: AfterAgentHook<TSchema, TContextSchema>;
}

Lifecycle Hooks

/**
 * Wrap tool call execution
 */
type WrapToolCallHook<TSchema, TContext> = (
  request: ToolCallRequest,
  handler: ToolCallHandler<TSchema, TContext>,
  runtime: Runtime<TContext>
) => Promise<ToolResult>;

/**
 * Wrap model invocation
 */
type WrapModelCallHook<TSchema, TContext> = (
  state: State<TSchema>,
  handler: WrapModelCallHandler<TSchema, TContext>,
  runtime: Runtime<TContext>
) => Promise<State<TSchema>>;

/**
 * Run before agent starts
 */
type BeforeAgentHook<TSchema, TContext> = (
  state: State<TSchema>,
  runtime: Runtime<TContext>
) => Promise<State<TSchema>> | State<TSchema>;

/**
 * Run before model call
 */
type BeforeModelHook<TSchema, TContext> = (
  state: State<TSchema>,
  runtime: Runtime<TContext>
) => Promise<State<TSchema>> | State<TSchema>;

/**
 * Run after model call
 */
type AfterModelHook<TSchema, TContext> = (
  state: State<TSchema>,
  runtime: Runtime<TContext>
) => Promise<State<TSchema>> | State<TSchema>;

/**
 * Run after agent completes
 */
type AfterAgentHook<TSchema, TContext> = (
  state: State<TSchema>,
  runtime: Runtime<TContext>
) => Promise<State<TSchema>> | State<TSchema>;

Hook Parameters

/**
 * Tool call request information
 */
interface ToolCallRequest {
  toolName: string;
  args: Record<string, any>;
  toolCallId: string;
}

/**
 * Tool call handler function
 */
type ToolCallHandler<TSchema, TContext> = (
  request: ToolCallRequest
) => Promise<ToolResult>;

/**
 * Model call handler function
 */
type WrapModelCallHandler<TSchema, TContext> = (
  state: State<TSchema>
) => Promise<State<TSchema>>;

/**
 * Tool result
 */
interface ToolResult {
  content: string;
  error?: string;
}

Base Interface

/**
 * Base middleware interface
 */
interface AgentMiddleware<
  TSchema = any,
  TContextSchema = any,
  TFullContext = any,
  TTools extends readonly Tool[] = readonly Tool[]
> {
  [MIDDLEWARE_BRAND]: true;
  name: string;
  stateSchema?: TSchema;
  contextSchema?: TContextSchema;
  tools?: TTools;
  hooks: {
    wrapToolCall?: WrapToolCallHook<TSchema, TFullContext>;
    wrapModelCall?: WrapModelCallHook<TSchema, TFullContext>;
    beforeAgent?: BeforeAgentHook<TSchema, TFullContext>;
    beforeModel?: BeforeModelHook<TSchema, TFullContext>;
    afterModel?: AfterModelHook<TSchema, TFullContext>;
    afterAgent?: AfterAgentHook<TSchema, TFullContext>;
  };
}

/**
 * Unique symbol for middleware branding
 */
const MIDDLEWARE_BRAND: unique symbol;

Middleware Return Type

/**
 * Middleware return type (state update)
 */
type MiddlewareResult<TState> = TState | Partial<TState> | void;

Execution Order

Middleware executes in the following order:

  1. beforeAgent - Called once at the start of agent invocation
  2. beforeModel - Called before each model invocation
  3. wrapModelCall - Wraps model invocation (outermost to innermost)
  4. afterModel - Called after each model invocation
  5. wrapToolCall - Wraps each tool execution (outermost to innermost)
  6. afterAgent - Called once when agent completes

Multiple middleware are composed in array order for lifecycle hooks and in reverse order for wrappers.

Type Inference

Extract types from middleware at compile time for type-safe development:

import {
  type InferMiddlewareState,
  type InferMiddlewareStates,
  type InferMiddlewareContext,
  type InferMiddlewareContexts,
  type InferMiddlewareTools,
} from "langchain";

// Infer single middleware state
type State = InferMiddlewareState<typeof middleware>;

// Infer combined middleware states
type CombinedStates = InferMiddlewareStates<typeof middlewareArray>;

// Infer middleware context
type Context = InferMiddlewareContext<typeof middleware>;

// Infer combined middleware contexts
type CombinedContexts = InferMiddlewareContexts<typeof middlewareArray>;

// Infer tools from middleware
type Tools = InferMiddlewareTools<typeof middlewareArray>;

See: Type Inference Guide - Middleware Types for complete documentation and examples.

Best Practices

  • Keep middleware focused on a single concern
  • Use state schema for persistent data
  • Use context schema for request-scoped data
  • Handle errors gracefully in wrappers
  • Document middleware behavior clearly
  • Test middleware in isolation
  • Consider middleware composition order

See Also