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.