Back to UI-TARS-desktop

Agent Hooks

multimodal/websites/tarko/docs/en/guide/advanced/agent-hooks.mdx

0.3.012.7 KB
Original Source

Agent Hooks

Tarko's Agent Hooks system provides extension points throughout the agent lifecycle, allowing you to customize behavior, add monitoring, implement custom logic, and integrate with external systems.

Overview

Agent Hooks are callback methods that execute at specific points during agent operation. All hooks are defined in the Agent class and can be overridden in custom agent implementations:

  • LLM Hooks: Request/response interception and modification
  • Tool Hooks: Tool call execution lifecycle management
  • Loop Hooks: Agent loop iteration control and monitoring
  • Termination Hooks: Custom completion criteria enforcement
  • Request Preparation: Dynamic system prompt and tool modification

Core Hooks

Hooks interacting with LLM

Intercept and monitor LLM requests and responses:

typescript
import { Agent } from '@tarko/agent';

class MonitoringAgent extends Agent {
  // Called before each LLM request
  override async onLLMRequest(id: string, payload: LLMRequestHookPayload) {
    console.log(`[${id}] Sending request to ${payload.model}`);
    console.log('Messages count:', payload.messages.length);
    
    // Log token usage estimates
    const tokenEstimate = this.estimateTokens(payload.messages);
    console.log('Estimated tokens:', tokenEstimate);
  }
  
  // Called after each LLM response
  override async onLLMResponse(id: string, payload: LLMResponseHookPayload) {
    const response = payload.response;
    console.log(`[${id}] Received response:`);
    console.log('Usage:', response.usage);
    console.log('Finish reason:', response.choices[0]?.finish_reason);
    
    // Log tool calls if present
    const toolCalls = response.choices[0]?.message?.tool_calls;
    if (toolCalls?.length) {
      console.log('Tool calls:', toolCalls.map(tc => tc.function.name));
    }
  }
  
  // Called during streaming responses
  override onLLMStreamingResponse(id: string, payload: LLMStreamingResponseHookPayload) {
    // Monitor streaming chunks in real-time
    const chunks = payload.chunks;
    console.log(`[${id}] Received ${chunks.length} streaming chunks`);
  }
}

Tool Execution Hooks

Monitor and control tool call execution:

typescript
class ToolMonitoringAgent extends Agent {
  private toolUsageStats = new Map<string, number>();
  
  // Called before each tool execution
  override async onBeforeToolCall(
    id: string,
    toolCall: { toolCallId: string; name: string },
    args: any
  ) {
    console.log(`[${id}] Executing tool: ${toolCall.name}`);
    console.log('Arguments:', JSON.stringify(args, null, 2));
    
    // Track tool usage
    const currentCount = this.toolUsageStats.get(toolCall.name) || 0;
    this.toolUsageStats.set(toolCall.name, currentCount + 1);
    
    // Validate arguments or apply rate limiting
    if (toolCall.name === 'expensive_api' && currentCount >= 5) {
      throw new Error('Rate limit exceeded for expensive_api');
    }
    
    // Return potentially modified args
    return args;
  }
  
  // Called after each tool execution
  override async onAfterToolCall(
    id: string,
    toolCall: { toolCallId: string; name: string },
    result: any
  ) {
    console.log(`[${id}] Tool ${toolCall.name} completed`);
    console.log('Result type:', typeof result);
    
    // Log errors or successful results
    if (result?.error) {
      console.error('Tool execution failed:', result.error);
    } else {
      console.log('Tool execution successful');
    }
    
    // Return potentially modified result
    return result;
  }
  
  // Called when tool execution fails
  override async onToolCallError(
    id: string,
    toolCall: { toolCallId: string; name: string },
    error: any
  ) {
    console.error(`[${id}] Tool ${toolCall.name} failed:`, error);
    
    // Implement retry logic or error transformation
    if (error.message?.includes('timeout')) {
      return 'Tool execution timed out. Please try again later.';
    }
    
    return `Error: ${error.message || error}`;
  }
  
  // Override tool call processing entirely
  override async onProcessToolCalls(
    id: string,
    toolCalls: ChatCompletionMessageToolCall[]
  ) {
    // Return undefined to execute tools normally
    // Return ToolCallResult[] to skip normal execution
    
    // Example: Mock tool execution for testing
    if (process.env.NODE_ENV === 'test') {
      return toolCalls.map(tc => ({
        toolCallId: tc.id,
        result: `Mocked result for ${tc.function.name}`,
        success: true
      }));
    }
    
    return undefined; // Execute tools normally
  }
}

Loop Lifecycle Hooks

Control agent loop iterations and termination:

typescript
class LoopControlAgent extends Agent {
  private iterationStartTimes = new Map<string, number>();
  
  // Called at the start of each loop iteration
  override async onEachAgentLoopStart(sessionId: string) {
    this.iterationStartTimes.set(sessionId, Date.now());
    console.log(`[${sessionId}] Starting iteration ${this.getCurrentLoopIteration()}`);
    
    // Inject additional context or perform setup
    const currentTime = new Date().toISOString();
    console.log(`Current time: ${currentTime}`);
  }
  
  // Called at the end of each loop iteration
  override async onEachAgentLoopEnd(context: EachAgentLoopEndContext) {
    const startTime = this.iterationStartTimes.get(context.sessionId);
    if (startTime) {
      const duration = Date.now() - startTime;
      console.log(`[${context.sessionId}] Iteration completed in ${duration}ms`);
    }
    
    // Log iteration results
    console.log('Events in this iteration:', context.events?.length || 0);
    console.log('Tool calls made:', context.toolCallResults?.length || 0);
  }
  
  // Called when the entire agent loop ends
  override async onAgentLoopEnd(id: string) {
    console.log(`[${id}] Agent loop completed`);
    console.log('Total iterations:', this.getCurrentLoopIteration());
    
    // Cleanup iteration tracking
    this.iterationStartTimes.delete(id);
    
    // Call parent implementation
    await super.onAgentLoopEnd(id);
  }
}

Advanced Hook Patterns

Enforcing Completion Criteria

Use onBeforeLoopTermination to enforce specific completion requirements:

typescript
class ValidatingAgent extends Agent {
  private requiredToolsCalled = new Set<string>();
  private requiredTools = ['gather_data', 'analyze_results', 'final_report'];
  
  constructor(options: AgentOptions) {
    super({
      ...options,
      instructions: `${options.instructions || ''}

You must call these tools in order: gather_data, analyze_results, final_report.
Do not provide a final answer until all required tools have been called.`,
    });
  }
  
  override async onAfterToolCall(
    id: string,
    toolCall: { toolCallId: string; name: string },
    result: any
  ) {
    // Track required tool calls
    if (this.requiredTools.includes(toolCall.name)) {
      this.requiredToolsCalled.add(toolCall.name);
      console.log(`Required tool called: ${toolCall.name}`);
      console.log('Remaining:', this.requiredTools.filter(t => !this.requiredToolsCalled.has(t)));
    }
    
    return await super.onAfterToolCall(id, toolCall, result);
  }
  
  // Prevent termination until all required tools are called
  override async onBeforeLoopTermination(
    id: string,
    finalEvent: AgentEventStream.AssistantMessageEvent
  ): Promise<LoopTerminationCheckResult> {
    const missingTools = this.requiredTools.filter(tool => 
      !this.requiredToolsCalled.has(tool)
    );
    
    if (missingTools.length > 0) {
      console.log(`[${id}] Preventing termination. Missing tools:`, missingTools);
      
      // Inject a reminder message
      const reminderEvent = this.getEventStream().createEvent('user_message', {
        content: `Please call the following required tools before providing your final answer: ${missingTools.join(', ')}`
      });
      this.getEventStream().sendEvent(reminderEvent);
      
      return {
        finished: false,
        message: `Must call required tools: ${missingTools.join(', ')}`
      };
    }
    
    console.log(`[${id}] All required tools called. Allowing termination.`);
    return { finished: true };
  }
  
  override async onAgentLoopEnd(id: string) {
    // Reset for next run
    this.requiredToolsCalled.clear();
    await super.onAgentLoopEnd(id);
  }
}

Dynamic Request Preparation

Use onPrepareRequest to dynamically modify system prompts and available tools:

typescript
class AdaptiveAgent extends Agent {
  private userExpertiseLevel: 'beginner' | 'intermediate' | 'expert' = 'intermediate';
  
  override async onPrepareRequest(
    context: PrepareRequestContext
  ): Promise<PrepareRequestResult> {
    // Modify system prompt based on context
    let systemPrompt = context.systemPrompt;
    
    // Add expertise-level specific instructions
    switch (this.userExpertiseLevel) {
      case 'beginner':
        systemPrompt += '\n\nExplain concepts in simple terms and provide step-by-step guidance.';
        break;
      case 'expert':
        systemPrompt += '\n\nProvide technical details and assume advanced knowledge.';
        break;
    }
    
    // Filter tools based on iteration count
    let availableTools = context.tools;
    const iteration = this.getCurrentLoopIteration();
    
    if (iteration === 1) {
      // First iteration: only allow information gathering tools
      availableTools = context.tools.filter(tool => 
        tool.id.includes('search') || tool.id.includes('read')
      );
    } else if (iteration >= 5) {
      // Later iterations: add analysis and reporting tools
      availableTools = context.tools; // All tools available
    }
    
    console.log(`Iteration ${iteration}: ${availableTools.length} tools available`);
    
    return {
      systemPrompt,
      tools: availableTools
    };
  }
  
  // Method to update user expertise level
  setUserExpertiseLevel(level: 'beginner' | 'intermediate' | 'expert') {
    this.userExpertiseLevel = level;
    console.log(`User expertise level set to: ${level}`);
  }
}

Error Handling and Recovery

typescript
class ResilientAgent extends Agent {
  private errorCounts = new Map<string, number>();
  private maxRetries = 3;
  
  override async onToolCallError(
    id: string,
    toolCall: { toolCallId: string; name: string },
    error: any
  ) {
    const errorKey = `${id}-${toolCall.name}`;
    const currentCount = this.errorCounts.get(errorKey) || 0;
    
    console.error(`Tool ${toolCall.name} failed (attempt ${currentCount + 1}):`, error);
    
    // Implement retry logic
    if (currentCount < this.maxRetries && this.isRetriableError(error)) {
      this.errorCounts.set(errorKey, currentCount + 1);
      
      // Add delay before retry
      await new Promise(resolve => setTimeout(resolve, 1000 * (currentCount + 1)));
      
      console.log(`Retrying ${toolCall.name} (attempt ${currentCount + 2})`);
      return 'Retrying due to temporary error...';
    }
    
    // Max retries exceeded or non-retriable error
    this.errorCounts.delete(errorKey);
    return `Tool ${toolCall.name} failed after ${currentCount + 1} attempts: ${error.message || error}`;
  }
  
  private isRetriableError(error: any): boolean {
    const errorMessage = error.message || error.toString();
    return (
      errorMessage.includes('timeout') ||
      errorMessage.includes('network') ||
      errorMessage.includes('503') ||
      errorMessage.includes('502')
    );
  }
  
  override async onAgentLoopEnd(id: string) {
    // Clear error counts for this session
    for (const key of this.errorCounts.keys()) {
      if (key.startsWith(id)) {
        this.errorCounts.delete(key);
      }
    }
    
    await super.onAgentLoopEnd(id);
  }
}

Hook Execution Order

Understanding the hook execution sequence is crucial for proper implementation:

1. Agent.run() called
2. onEachAgentLoopStart() - Start of iteration
3. onPrepareRequest() - Prepare LLM request
4. onLLMRequest() - Before sending to LLM
5. onLLMResponse() / onLLMStreamingResponse() - After LLM response
6. [If tool calls present]
   a. onProcessToolCalls() - Override tool execution (optional)
   b. For each tool call:
      - onBeforeToolCall() - Before tool execution
      - [Tool execution]
      - onAfterToolCall() - After successful execution
      - OR onToolCallError() - After failed execution
7. onEachAgentLoopEnd() - End of iteration
8. [If final answer ready]
   a. onBeforeLoopTermination() - Check if should terminate
   b. [If termination allowed] onAgentLoopEnd() - End of agent loop
9. [Otherwise repeat from step 2]

Testing Hooks

WIP

Real-World Examples

WIP

Next Steps