TypeScriptADK-TS
Callbacks

Design Patterns and Best Practices

Common design patterns and best practices for implementing callbacks in ADK

Callbacks offer powerful hooks into the agent lifecycle. Here are common design patterns illustrating how to leverage them effectively in ADK, followed by best practices for implementation.

Common Design Patterns

These patterns demonstrate typical ways to enhance or control agent behavior using callbacks:

1. Guardrails & Policy Enforcement

Pattern: Intercept requests before they reach the LLM or tools to enforce rules.

Implementation: Use beforeModelCallback to inspect the LlmRequest or implement authorization checks. If a policy violation is detected, return a predefined response to block the operation.

Example:

import { LlmAgent, CallbackContext, LlmRequest, LlmResponse } from '@iqai/adk';

const contentGuardrailCallback = ({ callbackContext, llmRequest }: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}): LlmResponse | undefined => {
  // Extract the last user message
  const lastContent = llmRequest.contents?.[llmRequest.contents.length - 1];
  const userMessage = lastContent?.parts?.[0]?.text || '';

  // Check for forbidden topics
  const forbiddenKeywords = ['violence', 'illegal', 'harmful'];
  const hasForbiddenContent = forbiddenKeywords.some(keyword =>
    userMessage.toLowerCase().includes(keyword)
  );

  if (hasForbiddenContent) {
    // Log the violation
    callbackContext.state.set('policy_violations',
      (callbackContext.state.get('policy_violations') || 0) + 1
    );

    // Return safe response to skip LLM call
    return new LlmResponse({
      content: {
        role: 'model',
        parts: [{ text: 'I cannot process requests containing prohibited content. Please rephrase your question.' }]
      }
    });
  }

  return undefined; // Proceed with LLM call
};

const agent = new LlmAgent({
  name: "guarded_agent",
  model: "gemini-2.5-flash",
  description: "Agent with content guardrails",
  instruction: "You are a helpful assistant",
  beforeModelCallback: contentGuardrailCallback
});

2. Dynamic State Management

Pattern: Read from and write to session state within callbacks to make agent behavior context-aware and pass data between steps.

Implementation: Access callbackContext.state to read and modify session state. Changes are automatically tracked for persistence.

Example:

const stateManagementCallback = (callbackContext: CallbackContext) => {
  // Track user interactions
  const interactionCount = callbackContext.state.get('interaction_count') || 0;
  callbackContext.state.set('interaction_count', interactionCount + 1);

  // Store user preferences
  const userTier = callbackContext.state.get('user_tier') || 'basic';

  // Customize behavior based on state
  if (userTier === 'premium' && interactionCount > 10) {
    callbackContext.state.set('enable_advanced_features', true);
  }

  console.log(`Interaction ${interactionCount + 1} for ${userTier} user`);
  return undefined;
};

const afterToolCallback = ({ toolContext, toolResult }: {
  toolContext: ToolContext;
  toolResult: Record<string, any>;
}) => {
  // Save important transaction data
  if (toolResult.transaction_id) {
    toolContext.state.set('last_transaction_id', toolResult.transaction_id);
    toolContext.state.set('last_transaction_time', new Date().toISOString());
  }

  return undefined; // Use original result
};

3. Logging and Monitoring

Pattern: Add detailed logging at specific lifecycle points for observability and debugging.

Implementation: Implement callbacks to send structured logs containing invocation ID, agent name, and relevant context data.

Example:

const loggingCallback = (callbackContext: CallbackContext) => {
  const logData = {
    timestamp: new Date().toISOString(),
    invocationId: callbackContext.invocationId,
    agentName: callbackContext.agentName,
    userId: callbackContext.userId,
    sessionId: callbackContext.sessionId
  };

  console.log(`[Agent Start] ${JSON.stringify(logData)}`);
  return undefined;
};

const modelLoggingCallback = ({ callbackContext, llmRequest }: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}) => {
  const requestInfo = {
    invocationId: callbackContext.invocationId,
    model: llmRequest.model,
    messageCount: llmRequest.contents?.length || 0,
    hasSystemInstruction: !!llmRequest.config?.systemInstruction
  };

  console.log(`[LLM Request] ${JSON.stringify(requestInfo)}`);
  return undefined;
};

const agent = new LlmAgent({
  name: "monitored_agent",
  model: "gemini-2.5-flash",
  description: "Agent with comprehensive logging",
  instruction: "You are helpful",
  beforeAgentCallback: loggingCallback,
  beforeModelCallback: modelLoggingCallback
});

4. Response Caching

Pattern: Avoid redundant LLM calls by caching responses based on request characteristics.

Implementation: Generate cache keys in beforeModelCallback, check for existing responses, and store new responses in afterModelCallback.

Example:

const cacheBeforeCallback = ({ callbackContext, llmRequest }: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}): LlmResponse | undefined => {
  // Generate cache key from user message
  const lastContent = llmRequest.contents?.[llmRequest.contents.length - 1];
  const userMessage = lastContent?.parts?.[0]?.text || '';
  const cacheKey = `llm_cache:${userMessage.slice(0, 100)}`;

  // Check for cached response
  const cachedResponse = callbackContext.state.get(cacheKey);
  if (cachedResponse) {
    console.log('Using cached response');
    return new LlmResponse({
      content: {
        role: 'model',
        parts: [{ text: cachedResponse }]
      }
    });
  }

  // Store cache key for after callback
  callbackContext.state.set('current_cache_key', cacheKey);
  return undefined;
};

const cacheAfterCallback = ({ callbackContext, llmResponse }: {
  callbackContext: CallbackContext;
  llmResponse: LlmResponse;
}) => {
  // Store response in cache
  const cacheKey = callbackContext.state.get('current_cache_key');
  const responseText = llmResponse.content?.parts?.[0]?.text;

  if (cacheKey && responseText) {
    callbackContext.state.set(cacheKey, responseText);
    console.log('Cached LLM response');
  }

  return undefined;
};

5. Request/Response Modification

Pattern: Alter data just before it's sent to the LLM or just after it's received.

Implementation: Modify request objects in beforeModelCallback or response objects in afterModelCallback.

Example:

const requestModificationCallback = ({ callbackContext, llmRequest }: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}) => {
  // Add user language preference to system instruction
  const userLanguage = callbackContext.state.get('user_language') || 'English';

  if (llmRequest.config?.systemInstruction) {
    const existingInstruction = llmRequest.config.systemInstruction.parts?.[0]?.text || '';
    llmRequest.config.systemInstruction.parts = [{
      text: `${existingInstruction}\n\nUser language preference: ${userLanguage}. Please respond in ${userLanguage}.`
    }];
  }

  // Add timestamp to model config labels
  llmRequest.config = llmRequest.config || {};
  llmRequest.config.labels = llmRequest.config.labels || {};
  llmRequest.config.labels.request_timestamp = new Date().toISOString();

  return undefined;
};

const responseModificationCallback = ({ callbackContext, llmResponse }: {
  callbackContext: CallbackContext;
  llmResponse: LlmResponse;
}) => {
  // Add disclaimer for premium users
  const userTier = callbackContext.state.get('user_tier');

  if (userTier === 'premium' && llmResponse.content) {
    const originalText = llmResponse.content.parts?.[0]?.text || '';
    const modifiedResponse = { ...llmResponse };

    modifiedResponse.content.parts = [{
      text: `${originalText}\n\n*Premium Response: This answer was generated with enhanced AI capabilities.*`
    }];

    return modifiedResponse;
  }

  return undefined;
};

6. Conditional Execution Control

Pattern: Prevent standard operations based on certain conditions by returning values from before_ callbacks.

Implementation: Return appropriate objects from before callbacks to skip normal execution.

Example:

const conditionalExecutionCallback = (callbackContext: CallbackContext) => {
  // Check maintenance mode
  const maintenanceMode = callbackContext.state.get('maintenance_mode');
  if (maintenanceMode) {
    return {
      role: 'model',
      parts: [{ text: 'The system is currently under maintenance. Please try again later.' }]
    };
  }

  // Check user quota
  const requestCount = callbackContext.state.get('daily_request_count') || 0;
  const maxRequests = callbackContext.state.get('daily_limit') || 100;

  if (requestCount >= maxRequests) {
    return {
      role: 'model',
      parts: [{ text: 'You have reached your daily request limit. Please upgrade your plan or try again tomorrow.' }]
    };
  }

  // Increment request count
  callbackContext.state.set('daily_request_count', requestCount + 1);
  return undefined; // Proceed with execution
};

7. Artifact Management

Pattern: Save or load session-related files or large data blobs during the agent lifecycle.

Implementation: Use callbackContext.saveArtifact and loadArtifact to manage files associated with the session.

Example:

const artifactManagementCallback = async (callbackContext: CallbackContext) => {
  try {
    // Load user preferences from artifact
    const preferencesArtifact = await callbackContext.loadArtifact('user_preferences.json');
    if (preferencesArtifact?.text) {
      const preferences = JSON.parse(preferencesArtifact.text);
      callbackContext.state.set('user_preferences', preferences);
      console.log('Loaded user preferences from artifact');
    }
  } catch (error) {
    console.warn('Failed to load user preferences:', error);
  }

  return undefined;
};

const saveArtifactCallback = async (callbackContext: CallbackContext) => {
  // Save conversation summary
  const interactionCount = callbackContext.state.get('interaction_count') || 0;

  if (interactionCount > 0 && interactionCount % 10 === 0) {
    const summary = {
      sessionId: callbackContext.sessionId,
      interactionCount,
      timestamp: new Date().toISOString(),
      userTier: callbackContext.state.get('user_tier')
    };

    try {
      await callbackContext.saveArtifact(
        'session_summary.json',
        { text: JSON.stringify(summary, null, 2) }
      );
      console.log('Saved session summary artifact');
    } catch (error) {
      console.warn('Failed to save session summary:', error);
    }
  }

  return undefined;
};

Best Practices

Following these guidelines ensures reliable, maintainable, and performant callback implementations.

Design Principles

Single Responsibility Design each callback for a single, well-defined purpose. Avoid monolithic callbacks that handle multiple concerns.

// Good: Focused on logging
const loggingCallback = (callbackContext: CallbackContext) => {
  console.log(`Agent started: ${callbackContext.agentName}`);
  return undefined;
};

// Good: Focused on authentication
const authCallback = (callbackContext: CallbackContext) => {
  const isAuthenticated = callbackContext.state.get('authenticated');
  if (!isAuthenticated) {
    return { role: 'model', parts: [{ text: 'Please authenticate first.' }] };
  }
  return undefined;
};

// Bad: Handles multiple concerns
const monolithicCallback = (callbackContext: CallbackContext) => {
  // Logging, authentication, state management, caching all mixed together
  console.log('Starting...');
  if (!callbackContext.state.get('authenticated')) return { /* ... */ };
  callbackContext.state.set('count', (callbackContext.state.get('count') || 0) + 1);
  // ... more unrelated logic
  return undefined;
};

Performance Awareness Callbacks execute synchronously within the agent's processing loop. Avoid long-running or blocking operations.

// Good: Fast synchronous operations
const quickCallback = (callbackContext: CallbackContext) => {
  const count = callbackContext.state.get('count') || 0;
  callbackContext.state.set('count', count + 1);
  return undefined;
};

// Bad: Blocking network call
const blockingCallback = async (callbackContext: CallbackContext) => {
  // This will block the entire agent execution
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  callbackContext.state.set('external_data', data);
  return undefined;
};

// Better: Offload to background or cache
const optimizedCallback = (callbackContext: CallbackContext) => {
  const cachedData = callbackContext.state.get('cached_external_data');
  if (cachedData) {
    // Use cached data immediately
    return undefined;
  }

  // Trigger background fetch (don't await)
  fetchDataInBackground(callbackContext.sessionId);
  return undefined;
};

Error Handling Use try-catch blocks and implement graceful degradation. Don't let callback errors crash the entire process.

const robustCallback = async (callbackContext: CallbackContext) => {
  try {
    // Potentially risky operation
    const preferences = await callbackContext.loadArtifact('preferences.json');
    if (preferences?.text) {
      const parsed = JSON.parse(preferences.text);
      callbackContext.state.set('user_preferences', parsed);
    }
  } catch (error) {
    // Log error but don't crash
    console.warn('Failed to load preferences, using defaults:', error);
    callbackContext.state.set('user_preferences', getDefaultPreferences());
  }

  return undefined;
};

State Management Best Practices Be deliberate about state changes and their scope.

const stateCallback = (callbackContext: CallbackContext) => {
  // Use specific, descriptive keys
  const userSessionCount = callbackContext.state.get('user_session_count') || 0;
  callbackContext.state.set('user_session_count', userSessionCount + 1);

  // Avoid modifying large objects directly
  const settings = callbackContext.state.get('app_settings') || {};
  const newSettings = {
    ...settings,
    lastActiveTime: new Date().toISOString()
  };
  callbackContext.state.set('app_settings', newSettings);

  // Use prefixes for organization
  callbackContext.state.set('temp:current_operation', 'processing');
  callbackContext.state.set('user:preference:theme', 'dark');
  callbackContext.state.set('app:feature:advanced_mode', true);

  return undefined;
};

Implementation Guidelines

Type Safety Use proper TypeScript types for all callback parameters and return values.

import { CallbackContext, Content, LlmRequest, LlmResponse } from '@iqai/adk';

// Proper typing for agent callbacks
const typedAgentCallback = (callbackContext: CallbackContext): Content | undefined => {
  // Implementation with full type safety
  return undefined;
};

// Proper typing for model callbacks
const typedModelCallback = ({ callbackContext, llmRequest }: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}): LlmResponse | undefined => {
  // Implementation with full type safety
  return undefined;
};

Testing Strategy Unit test callbacks with mock context objects and integration test within the full agent flow.

// Example test structure
describe('GuardrailCallback', () => {
  it('should block prohibited content', () => {
    const mockContext = createMockCallbackContext();
    const mockRequest = createMockLlmRequest('prohibited content here');

    const result = guardrailCallback({ callbackContext: mockContext, llmRequest: mockRequest });

    expect(result).toBeDefined();
    expect(result?.content?.parts?.[0]?.text).toContain('cannot process');
  });

  it('should allow safe content', () => {
    const mockContext = createMockCallbackContext();
    const mockRequest = createMockLlmRequest('safe question here');

    const result = guardrailCallback({ callbackContext: mockContext, llmRequest: mockRequest });

    expect(result).toBeUndefined();
  });
});

Documentation Standards Document callback behavior, side effects, and dependencies clearly.

/**
 * Implements content guardrails for LLM requests.
 *
 * Behavior:
 * - Checks user messages for forbidden keywords
 * - Tracks policy violations in session state
 * - Returns safe response for blocked content
 *
 * Side Effects:
 * - Increments 'policy_violations' counter in state
 * - Logs violations to console
 *
 * Dependencies:
 * - Requires 'forbidden_keywords' to be configured
 *
 * @param params - Callback parameters with context and request
 * @returns LlmResponse to skip LLM call, or undefined to proceed
 */
const documentedGuardrailCallback = ({ callbackContext, llmRequest }: {
  callbackContext: CallbackContext;
  llmRequest: LlmRequest;
}): LlmResponse | undefined => {
  // Implementation here
  return undefined;
};

Idempotency Considerations Design callbacks to be safe for retry scenarios.

const idempotentCallback = (callbackContext: CallbackContext) => {
  // Use idempotent operations
  const currentTime = new Date().toISOString();

  // Safe to call multiple times
  callbackContext.state.set('last_callback_time', currentTime);

  // Avoid incrementing counters directly
  // Instead, use a unique key or check existence
  const sessionStart = callbackContext.state.get('session_start_time');
  if (!sessionStart) {
    callbackContext.state.set('session_start_time', currentTime);
    callbackContext.state.set('session_count',
      (callbackContext.state.get('session_count') || 0) + 1
    );
  }

  return undefined;
};

By applying these patterns and best practices, you can effectively use callbacks to create more robust, observable, and customized agent behaviors in ADK.