Robutler

Lifecycle

Understanding the request lifecycle and hook system in BaseAgent.

Request Lifecycle

Lifecycle Hooks

Available Hooks

  1. on_connection — Request initialized
  2. before_llm_call — Before each LLM call (can modify messages and tools in context)
  3. after_llm_call — After each LLM call (can inspect the response)
  4. before_toolcall — Before tool execution
  5. after_toolcall — After tool execution
  6. on_message — After the agentic loop completes (full conversation available)
  7. on_chunk — Each streaming chunk
  8. finalize_connection — Request complete

finalize_connection runs for cleanup even when a prior hook raises a structured error (for example, a 402 payment/auth error). Implement finalize hooks to be idempotent and safe when required context (like a payment token) is missing.

Hook Registration

import { Skill, hook } from 'webagents';
import type { HookData, Context } from 'webagents';

class AnalyticsSkill extends Skill {
  readonly name = 'analytics';

  @hook({ lifecycle: 'on_connection', priority: 10 })
  async trackRequest(data: HookData, ctx: Context) {
    console.log(`New request: ${ctx.metadata.completion_id}`);
    return data;
  }

  @hook({ lifecycle: 'on_message', priority: 20 })
  async analyzeMessage(data: HookData, ctx: Context) {
    const last = data.messages?.at(-1);
    console.log(`Message role: ${last?.role}`);
    return data;
  }

  @hook({ lifecycle: 'on_chunk', priority: 30 })
  async monitorStreaming(data: HookData, ctx: Context) {
    const chunkSize = (data.content ?? '').length;
    console.log(`Chunk size: ${chunkSize}`);
    return data;
  }
}

Hook Priority

Hooks execute in priority order (lower numbers first):

import { Skill, hook } from 'webagents';

class SecuritySkill extends Skill {
  readonly name = 'security';

  @hook({ lifecycle: 'before_toolcall', priority: 1 })
  async validateSecurity(data, ctx) {
    const toolName = data.tool_call?.function?.name;
    if (this.isDangerous(toolName)) {
      throw new Error(`Tool blocked: ${toolName}`);
    }
    return data;
  }

  private isDangerous(name?: string) { return name === 'rm_rf_root'; }
}

class LoggingSkill extends Skill {
  readonly name = 'logging';

  @hook({ lifecycle: 'before_toolcall', priority: 10 })
  async logToolUsage(data, ctx) {
    this.logTool(data.tool_call);
    return data;
  }

  private logTool(_: unknown) {}
}

Context During Lifecycle

Connection Context

@hook({ lifecycle: 'on_connection' })
async onConnect(data: HookData, ctx: Context) {
  // Available on data / ctx:
  // data.messages   — Message[]
  // data.stream     — boolean
  // ctx.auth        — AuthInfo (peer_user_id, scopes)
  // ctx.metadata    — completion_id, model, agent_name
  // ctx.session     — SessionState
  return data;
}

Message Context

@hook({ lifecycle: 'on_message' })
async onMsg(data: HookData, ctx: Context) {
  const current = data.messages!.at(-1)!;
  const role = current.role;
  const content = current.content;
  return data;
}

Tool Context

@hook({ lifecycle: 'before_toolcall' })
async beforeTool(data: HookData, ctx: Context) {
  // data.tool_call — { id, function: { name, arguments } }
  return data;
}

@hook({ lifecycle: 'after_toolcall' })
async afterTool(data: HookData, ctx: Context) {
  // data.tool_result — string
  return data;
}

Streaming Context

@hook({ lifecycle: 'on_chunk' })
async onChunk(data: HookData, ctx: Context) {
  // data.chunk        — OpenAI-format streaming chunk
  // data.content      — string (current chunk content)
  // data.chunk_index  — number
  // data.full_content — string (accumulated)
  return data;
}

Practical Examples

Request Logging

import { Skill, hook } from 'webagents';

class RequestLogger extends Skill {
  readonly name = 'request-logger';
  private startTime = 0;
  private requestId = '';

  @hook({ lifecycle: 'on_connection' })
  async startLogging(data, ctx) {
    this.startTime = Date.now();
    this.requestId = String(ctx.metadata.completion_id ?? '');
    await this.logRequestStart(data, ctx);
    return data;
  }

  @hook({ lifecycle: 'finalize_connection' })
  async endLogging(data, ctx) {
    const duration = (Date.now() - this.startTime) / 1000;
    await this.logRequestComplete(this.requestId, duration, data.usage);
    return data;
  }

  private async logRequestStart(_d: unknown, _c: unknown) {}
  private async logRequestComplete(_id: string, _d: number, _u: unknown) {}
}

Content Filtering

import { Skill, hook } from 'webagents';

class ContentFilter extends Skill {
  readonly name = 'content-filter';

  @hook({ lifecycle: 'on_message', priority: 5 })
  async filterInput(data, ctx) {
    const last = data.messages?.at(-1);
    if (last?.role === 'user') {
      last.content = this.filterContent(String(last.content ?? ''));
    }
    return data;
  }

  @hook({ lifecycle: 'on_chunk', priority: 5 })
  async filterOutput(data, ctx) {
    if (this.isInappropriate(data.content ?? '')) {
      data.chunk.choices[0].delta.content = '[filtered]';
    }
    return data;
  }

  private filterContent(s: string) { return s; }
  private isInappropriate(_: string) { return false; }
}

Performance Monitoring

import { Skill, hook } from 'webagents';

class PerformanceMonitor extends Skill {
  readonly name = 'performance-monitor';
  private metrics = new Map<string, { start: number }>();

  @hook({ lifecycle: 'before_toolcall' })
  async startTimer(data, ctx) {
    const toolId = String(data.tool_id);
    this.metrics.set(toolId, { start: Date.now() });
    return data;
  }

  @hook({ lifecycle: 'after_toolcall' })
  async recordDuration(data, ctx) {
    const toolId = String(data.tool_id);
    const start = this.metrics.get(toolId)?.start ?? Date.now();
    const duration = (Date.now() - start) / 1000;
    await this.recordMetric('tool_duration', duration, {
      tool: data.tool_call?.function?.name,
    });
    return data;
  }

  private async recordMetric(_n: string, _v: number, _t: object) {}
}

Best Practices

  1. Use Priorities — Order hooks appropriately.
  2. Return Context — Always return modified context (or data in TypeScript).
  3. Handle Errors — Gracefully handle exceptions; remember finalize_connection still runs.
  4. Minimize Overhead — Keep hooks lightweight.
  5. Thread Safety — Use context vars / immutable copies for shared state.

On this page