Hooks

Hooks let you insert custom logic before and after each agent step — logging, dynamically modifying configuration, early loop termination, error handling — for fine-grained control over the execution flow.

During execution, an agent goes through multiple steps — model reasoning, tool calls, structured output. Hooks let you insert custom logic at key points in this flow, enabling logging, dynamic configuration, flow control, and error handling without modifying core code. Whether tracking each step's execution state or switching models based on runtime conditions, Hooks provide a clean and powerful extension mechanism.

Three Hooks

deepseek-kit provides three lifecycle hooks covering the complete agent execution flow:

HookTriggerTypical Use
beforeStepBefore each step startsLogging, dynamic config modification, early termination
afterStepAfter each step completesLogging, result tracking, early termination
onErrorWhen a step execution error occursError handling, custom error transformation
import { createAgent, createModel } from 'deepseek-kit'

const model = createModel({ model: 'deepseek-v4-flash' })

const agent = createAgent({
  model,
  hooks: {
    beforeStep: (context, hookCtx) => {
      console.log(`Step ${context.step} starting`)
    },
    afterStep: (step, hookCtx) => {
      console.log(`Step ${step.step} completed: ${step.type}`)
    },
    onError: (error, hookCtx) => {
      console.error(`Step error: ${error.type} - ${error.message}`)
    },
  },
})

beforeStep — Pre-Step Interception

beforeStep is called before each step starts, receiving the current step's context information and HookContext. You can use it to:

  • Log execution progress
  • Dynamically modify the current step's configuration
  • Replace the current step's messages or tools
  • Terminate the loop early

Parameters

beforeStep receives two parameters:

  1. context — Current step's context information
  2. hookCtx — Hook context, providing the stop() method

Logging

The simplest usage is logging the start of each step:

beforeStep: (context, hookCtx) => {
  console.log(`Step ${context.step} starting, ${context.messages.length} messages currently`)
}

Dynamic Configuration Modification

beforeStep can return an object to modify the current step's configuration. The returned object is merged with the default configuration:

beforeStep: (context, hookCtx) => {
  if (context.step > 5) {
    return {
      config: {
        model: 'deepseek-v4-pro',
      },
    }
  }
}

This switches the agent to the Pro model after step 6. Modifiable configuration items include:

  • config — Model configuration (model, temperature, maxTokens, etc.)
  • messages — Message list for the current step
  • tools — Available tools for the current step

Dynamic Tool Selection

Dynamically adjust available tools based on step number or context:

beforeStep: (context, hookCtx) => {
  if (context.step === 1) {
    return {
      tools: [searchTool],
    }
  }
  return {
    tools: [searchTool, calculatorTool, weatherTool],
  }
}

Modifying Messages

Inject additional messages before a specific step:

beforeStep: (context, hookCtx) => {
  if (context.step === 3) {
    return {
      messages: [
        ...context.messages,
        { role: 'system', content: 'Please cite data sources in your response.' },
      ],
    }
  }
}

Early Termination

You can terminate the agent's execution loop early via hookCtx.stop() in any hook:

beforeStep: (context, hookCtx) => {
  if (context.step > 10) {
    hookCtx.stop()
  }
}

After calling stop(), the agent immediately ends the loop and returns the current results. This is useful when you need custom termination conditions.

afterStep — Post-Step Processing

afterStep is called after each step completes, receiving the step result and HookContext. The step type determines what information is available in the result:

Step Types

TypeDescriptionAvailable Fields
'tool'Tool call steptoolCalls, text, usage
'text'Text generation steptext, usage
'format'Structured output steptext, usage

Logging

Log the execution result of each step:

afterStep: (step, hookCtx) => {
  console.log(`Step ${step.step} completed: ${step.type}`)

  if (step.type === 'tool' && step.toolCalls) {
    console.log(`  Tool calls: ${step.toolCalls.map(t => t.function.name).join(', ')}`)
  }

  if (step.type === 'text') {
    console.log(`  Generated text: ${step.text?.substring(0, 50)}...`)
  }

  console.log(`  Token usage: ${step.usage.total_tokens}`)
}

Token Usage Tracking

Track cumulative token consumption via afterStep:

let totalTokens = 0

afterStep: (step, hookCtx) => {
  totalTokens += step.usage.total_tokens
  console.log(`Cumulative tokens: ${totalTokens}`)
}

Tool Call Monitoring

Specifically monitor tool call steps:

afterStep: (step, hookCtx) => {
  if (step.type === 'tool' && step.toolCalls) {
    for (const tc of step.toolCalls) {
      console.log(`[Tool Call] ${tc.function.name}`)
      console.log(`  Arguments: ${tc.function.arguments}`)
    }
  }
}

Early Termination

afterStep also supports early termination via hookCtx.stop():

afterStep: (step, hookCtx) => {
  if (step.type === 'tool' && step.toolCalls) {
    const names = step.toolCalls.map(t => t.function.name)
    if (names.includes('dangerousAction')) {
      hookCtx.stop()
    }
  }
}

onError — Error Handling

onError is called when a step execution error occurs, receiving an AgentError and HookContext. You can use it to:

  • Log error information
  • Customize error transformation
  • Decide whether to continue execution based on error type

Error Types

AgentError contains a type field identifying the error category:

TypeDescriptionRetryable
'rate_limit'API rate limit✅
'model_error'Model returned error✅
'timeout'Request timeout✅
'network_error'Network connection error✅
'tool_error'Tool execution error❌
'max_steps'Maximum steps reached❌
'schema_error'Structured output validation failure❌

Logging

Log error information for debugging:

onError: (error, hookCtx) => {
  console.error(`[Error] Step ${error.step}: ${error.type} - ${error.message}`)
  if (error.retryable) {
    console.log('  This error is retryable')
  }
}

Custom Error Transformation

onError can return a new AgentError to replace the original error, or return undefined to suppress the error (allowing the loop to continue):

onError: (error, hookCtx) => {
  if (error.type === 'rate_limit') {
    console.log('Rate limit encountered, will auto-retry...')
    return undefined
  }

  if (error.type === 'network_error') {
    return new AgentError({
      message: 'Network connection failed. Please check your network settings and retry.',
      type: 'network_error',
      step: error.step,
      retryable: false,
    })
  }

  return error
}

Return value rules:

  • Return undefined — Suppress the error, the loop continues to the next step
  • Return AgentError — Replace the original error with the new one; if hookCtx.stop() is called, the loop terminates
  • Return nothing (void) — The original error continues to be thrown

Early Termination

Combining hookCtx.stop() in onError enables graceful termination when specific errors occur:

onError: (error, hookCtx) => {
  if (error.type === 'max_steps') {
    console.log('Maximum step limit reached, terminating early.')
    hookCtx.stop()
    return
  }

  if (!error.retryable) {
    console.error(`Non-retryable error: ${error.message}`)
    hookCtx.stop()
  }
}

Combining Hooks

The three hooks are typically used together to build complete observability and control logic:

Complete Logging Middleware

const agent = createAgent({
  model,
  tools: [weatherTool, searchTool],
  hooks: {
    beforeStep: (context, hookCtx) => {
      console.log(`\n--- Step ${context.step} ---`)
      console.log(`Messages: ${context.messages.length}`)
      console.log(`Available tools: ${context.tools.map(t => t.name).join(', ')}`)
    },
    afterStep: (step, hookCtx) => {
      if (step.type === 'tool') {
        console.log(`Tool calls: ${step.toolCalls?.map(t => t.function.name).join(', ')}`)
      }
      else if (step.type === 'text') {
        console.log(`Generated text (${step.text?.length} characters)`)
      }
      console.log(`Tokens: ${step.usage.total_tokens}`)
    },
    onError: (error, hookCtx) => {
      console.error(`Error: [${error.type}] ${error.message}`)
      if (!error.retryable) {
        hookCtx.stop()
      }
    },
  },
})

Dynamic Model Switching

Dynamically switch models based on step progress — using a fast model early on, then switching to a high-precision model later:

const fastModel = createModel({ model: 'deepseek-v4-flash' })
const proModel = createModel({ model: 'deepseek-v4-pro' })

const agent = createAgent({
  model: fastModel,
  tools: [searchTool, analysisTool],
  hooks: {
    beforeStep: (context, hookCtx) => {
      if (context.step >= 3) {
        return {
          config: {
            model: 'deepseek-v4-pro',
          },
        }
      }
    },
    afterStep: (step, hookCtx) => {
      console.log(`Step ${step.step} (${step.type}), Tokens: ${step.usage.total_tokens}`)
    },
  },
})

Step Limits and Graceful Degradation

Gracefully terminate when there are too many steps, rather than throwing an error directly:

const agent = createAgent({
  model,
  tools: [searchTool],
  maxSteps: 20,
  hooks: {
    beforeStep: (context, hookCtx) => {
      if (context.step > 10) {
        console.log(`Step ${context.step} exceeds recommended limit, preparing to terminate.`)
        hookCtx.stop()
      }
    },
    onError: (error, hookCtx) => {
      if (error.type === 'max_steps') {
        console.log('Maximum steps reached, returning existing results.')
        hookCtx.stop()
      }
    },
  },
})

Token Budget Control

Set a token budget and terminate early when it's exceeded:

let totalTokens = 0
const TOKEN_BUDGET = 10000

const agent = createAgent({
  model,
  tools: [searchTool],
  hooks: {
    afterStep: (step, hookCtx) => {
      totalTokens += step.usage.total_tokens
      if (totalTokens > TOKEN_BUDGET) {
        console.log(`Token budget exhausted (${totalTokens}/${TOKEN_BUDGET}), terminating early.`)
        hookCtx.stop()
      }
    },
  },
})

Execution Flow

The following shows when hooks are triggered in the agent loop:

Rendering Chart

API Reference

GenerateTextHooks

beforeStep(context: BeforeStepContext, hookCtx: HookContext) => BeforeStepResult | void
Pre-step hook. Receives the current step context and hook context, can return a configuration modification object.
afterStep(step: StepEvent, hookCtx: HookContext) => void
Post-step hook. Receives the step result and hook context.
onError(error: AgentError, hookCtx: HookContext) => void | AgentError | Promise<AgentError | void>
Error handling hook. Receives the error object and hook context. Can return undefined to suppress the error, or return a new AgentError to replace the original.

BeforeStepContext

stepnumber
Current step number (starting from 1).
configModelOptions
Current model configuration, including model, temperature, maxTokens, etc.
messagesChatMessage[]
Copy of the message list for the current step. Modifying messages in the return value replaces the current step's messages.
toolsTool[]
List of tools available for the current step. Modifying tools in the return value replaces the current step's tools.

BeforeStepResult

messagesChatMessage[]
Replace the message list for the current step.
toolsTool[]
Replace the tool list for the current step.
configPartial<ModelOptions>
Modify the model configuration for the current step. Supported fields include model, temperature, maxTokens, thinking, etc.

StepEvent

stepnumber
Step number.
type'tool' | 'text' | 'format'
Step type. 'tool' for tool call steps, 'text' for text generation steps, 'format' for structured output steps.
usageUsage
Token usage for the current step.
toolCallsChatCompletionTool[]
Tool call list (only available for 'tool' type steps).
textstring
Generated text content.
reasoningContentstring
Reasoning content (available when thinking mode is enabled).

HookContext

stop() => void
Terminate the agent's execution loop. After calling, the agent immediately ends and returns the current results.

AgentError

typeAgentErrorType
Error type. Possible values are 'rate_limit', 'model_error', 'timeout', 'network_error', 'tool_error', 'max_steps', 'schema_error'.
messagestring
Error description.
stepnumber
Step number when the error occurred.
retryableboolean
Whether the error is retryable. 'rate_limit', 'model_error', 'timeout', and 'network_error' errors are retryable by default.
causeError
Original error object.