Lifecycle
Understanding the request lifecycle and hook system in BaseAgent.
Request Lifecycle
Lifecycle Hooks
Available Hooks
- on_connection — Request initialized
- before_llm_call — Before each LLM call (can modify messages and tools in context)
- after_llm_call — After each LLM call (can inspect the response)
- before_toolcall — Before tool execution
- after_toolcall — After tool execution
- on_message — After the agentic loop completes (full conversation available)
- on_chunk — Each streaming chunk
- finalize_connection — Request complete
finalize_connectionruns 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
- Use Priorities — Order hooks appropriately.
- Return Context — Always return modified context (or
datain TypeScript). - Handle Errors — Gracefully handle exceptions; remember
finalize_connectionstill runs. - Minimize Overhead — Keep hooks lightweight.
- Thread Safety — Use context vars / immutable copies for shared state.