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

custom.mddocs/middleware/

Creating Custom Middleware

Guide to creating custom middleware for extending agent behavior.

Basic Structure

import { createMiddleware } from "langchain";
import { z } from "zod";

const myMiddleware = createMiddleware({
  name: "my-middleware",

  // Optional: Define state schema
  stateSchema: z.object({
    customField: z.string(),
  }),

  // Optional: Define context schema
  contextSchema: z.object({
    requestId: z.string(),
  }),

  // Optional: Add tools
  tools: [/* ... */],

  // Lifecycle hooks
  beforeAgent: async (state, runtime) => {
    // Initialize state
    return state;
  },

  wrapToolCall: async (request, handler, runtime) => {
    // Intercept tool execution
    const result = await handler(request);
    return result;
  },
});

Lifecycle Hooks

beforeAgent

Called once at the start of agent invocation.

beforeAgent: async (state, runtime) => {
  // Initialize middleware state
  return {
    ...state,
    customField: "initialized",
  };
}

beforeModel

Called before each model invocation.

beforeModel: async (state, runtime) => {
  // Prepare for model call
  console.log("About to call model");
  return state;
}

wrapModelCall

Wraps the model invocation.

wrapModelCall: async (state, handler, runtime) => {
  console.log("Before model call");
  const result = await handler(state);
  console.log("After model call");
  return result;
}

afterModel

Called after each model invocation.

afterModel: async (state, runtime) => {
  // Process model response
  return state;
}

wrapToolCall

Wraps individual tool executions.

wrapToolCall: async (request, handler, runtime) => {
  console.log(`Calling tool: ${request.toolName}`);

  try {
    const result = await handler(request);
    console.log(`Tool succeeded: ${request.toolName}`);
    return result;
  } catch (error) {
    console.error(`Tool failed: ${request.toolName}`, error);
    throw error;
  }
}

afterAgent

Called once when agent completes.

afterAgent: async (state, runtime) => {
  // Cleanup or finalization
  console.log("Agent completed");
  return state;
}

Common Patterns

Logging Middleware

const loggingMiddleware = createMiddleware({
  name: "logging",

  wrapToolCall: async (request, handler, runtime) => {
    const start = Date.now();
    console.log(`[Tool] ${request.toolName} called with:`, request.args);

    const result = await handler(request);

    const duration = Date.now() - start;
    console.log(`[Tool] ${request.toolName} completed in ${duration}ms`);

    return result;
  },

  wrapModelCall: async (state, handler, runtime) => {
    const start = Date.now();
    console.log("[Model] Calling model");

    const result = await handler(state);

    const duration = Date.now() - start;
    console.log(`[Model] Completed in ${duration}ms`);

    return result;
  },
});

State Management Middleware

const sessionMiddleware = createMiddleware({
  name: "session",

  stateSchema: z.object({
    sessionId: z.string(),
    startTime: z.number(),
    operationCount: z.number(),
  }),

  beforeAgent: async (state, runtime) => {
    return {
      ...state,
      sessionId: crypto.randomUUID(),
      startTime: Date.now(),
      operationCount: 0,
    };
  },

  afterModel: async (state, runtime) => {
    return {
      ...state,
      operationCount: state.operationCount + 1,
    };
  },

  afterAgent: async (state, runtime) => {
    const duration = Date.now() - state.startTime;
    console.log(`Session ${state.sessionId}: ${state.operationCount} operations in ${duration}ms`);
    return state;
  },
});

Error Handling Middleware

const errorHandlingMiddleware = createMiddleware({
  name: "error-handling",

  wrapToolCall: async (request, handler, runtime) => {
    try {
      return await handler(request);
    } catch (error) {
      console.error(`Tool ${request.toolName} failed:`, error);

      // Return error as tool result
      return {
        content: `Error: ${error.message}`,
        error: error.message,
      };
    }
  },

  wrapModelCall: async (state, handler, runtime) => {
    try {
      return await handler(state);
    } catch (error) {
      console.error("Model call failed:", error);
      // Could implement fallback logic here
      throw error;
    }
  },
});

Validation Middleware

const validationMiddleware = createMiddleware({
  name: "validation",

  wrapToolCall: async (request, handler, runtime) => {
    // Validate tool arguments
    if (request.toolName === "send_email") {
      if (!request.args.to || !request.args.to.includes("@")) {
        return {
          content: "Error: Invalid email address",
          error: "Invalid email address",
        };
      }
    }

    return await handler(request);
  },
});

Caching Middleware

const cachingMiddleware = createMiddleware({
  name: "caching",

  stateSchema: z.object({
    cache: z.record(z.string(), z.any()),
  }),

  beforeAgent: async (state, runtime) => {
    return {
      ...state,
      cache: {},
    };
  },

  wrapToolCall: async (request, handler, runtime) => {
    const cacheKey = `${request.toolName}:${JSON.stringify(request.args)}`;

    // Check cache
    if (state.cache[cacheKey]) {
      console.log(`Cache hit for ${request.toolName}`);
      return state.cache[cacheKey];
    }

    // Execute and cache
    const result = await handler(request);
    state.cache[cacheKey] = result;

    return result;
  },
});

Rate Limiting Middleware

const rateLimitMiddleware = createMiddleware({
  name: "rate-limit",

  stateSchema: z.object({
    toolCallTimes: z.array(z.number()),
  }),

  beforeAgent: async (state, runtime) => {
    return {
      ...state,
      toolCallTimes: [],
    };
  },

  wrapToolCall: async (request, handler, runtime) => {
    const now = Date.now();
    const recentCalls = state.toolCallTimes.filter(t => now - t < 60000); // Last minute

    if (recentCalls.length >= 10) {
      throw new Error("Rate limit exceeded: max 10 tool calls per minute");
    }

    state.toolCallTimes.push(now);
    return await handler(request);
  },
});

Best Practices

State vs Context

  • Use stateSchema for data that persists across invocations
  • Use contextSchema for read-only, request-scoped data
  • Keep state minimal to reduce overhead
  • Use context for user IDs, request IDs, etc.

Error Handling

  • Always handle errors in wrappers
  • Decide whether to throw or return error results
  • Log errors with sufficient context
  • Consider retry logic for transient failures

Performance

  • Minimize work in hooks
  • Use async operations judiciously
  • Cache expensive computations
  • Be mindful of state size

Composition

  • Keep middleware focused on single concerns
  • Consider middleware execution order
  • Document dependencies between middleware
  • Test middleware in isolation

Type Safety

  • Always provide schemas for state and context
  • Use TypeScript interfaces for clarity
  • Leverage type inference from schemas
  • Document expected types in comments