TypeScriptADK-TS
Context

CallbackContext

State management and artifact operations for agent lifecycle callbacks

CallbackContext extends ReadonlyContext with state modification capabilities and artifact operations. It's designed for agent lifecycle callbacks where you need to manage state and handle file operations.

Overview

CallbackContext provides mutable access to session state and artifact management capabilities. It automatically tracks state changes and integrates with the event system for proper state synchronization.

import { CallbackContext } from "@iqai/adk";

Key Features

  • Mutable State: Read and write session state with automatic change tracking
  • Artifact Management: Load and save artifacts with session integration
  • Event Integration: Automatic state delta tracking through the event system
  • Inherited Properties: All ReadonlyContext properties (userContent, invocationId, agentName)

Properties

state (Mutable)

Unlike ReadonlyContext, CallbackContext provides a mutable state object:

get state(): State

The state object supports direct modification and automatically tracks changes:

function updateUserPreferences(ctx: CallbackContext) {
  // Direct state modification - changes are tracked automatically
  ctx.state.userPreferences = {
    theme: "dark",
    language: "en",
    notifications: true
  };

  // Nested modifications also work
  ctx.state.user = ctx.state.user || {};
  ctx.state.user.lastActive = new Date().toISOString();
}

eventActions

Access to the event actions for advanced state and flow control:

get eventActions(): EventActions

This provides access to underlying event tracking and action management.

Artifact Operations

loadArtifact()

Loads an artifact attached to the current session:

async loadArtifact(filename: string, version?: number): Promise<Part | undefined>
async function processUserDocument(ctx: CallbackContext) {
  // Load the latest version of a document
  const document = await ctx.loadArtifact("user-document.pdf");

  if (document) {
    console.log("Document loaded successfully");
    // Process the document content
  }

  // Load a specific version
  const previousVersion = await ctx.loadArtifact("user-document.pdf", 2);
}

Requires an artifact service to be configured in the runner or agent setup.

saveArtifact()

Saves an artifact and records it in the session:

async saveArtifact(filename: string, artifact: Part): Promise<number>
async function saveProcessedData(ctx: CallbackContext, data: any) {
  // Create an artifact part
  const artifactPart = {
    text: JSON.stringify(data, null, 2)
  };

  // Save the artifact
  const version = await ctx.saveArtifact("processed-data.json", artifactPart);

  // Update state with artifact reference
  ctx.state.lastProcessedData = {
    filename: "processed-data.json",
    version: version,
    timestamp: new Date().toISOString()
  };

  return version;
}

Common Use Cases

Pre-Processing Callbacks

Modify state before agent or model execution:

async function preprocessCallback(ctx: CallbackContext) {
  // Add session metadata
  ctx.state.sessionStart = ctx.state.sessionStart || new Date().toISOString();
  ctx.state.requestCount = (ctx.state.requestCount || 0) + 1;

  // Load user context if available
  const userProfile = await ctx.loadArtifact("user-profile.json");
  if (userProfile && userProfile.text) {
    ctx.state.userContext = JSON.parse(userProfile.text);
  }

  // Set processing flags
  ctx.state.preprocessing = {
    completed: true,
    timestamp: new Date().toISOString()
  };
}

Post-Processing Callbacks

Update state based on execution results:

async function postprocessCallback(ctx: CallbackContext) {
  // Update interaction history
  const history = ctx.state.interactionHistory || [];
  history.push({
    invocationId: ctx.invocationId,
    agentName: ctx.agentName,
    timestamp: new Date().toISOString(),
    userContent: ctx.userContent
  });

  // Keep only last 10 interactions
  ctx.state.interactionHistory = history.slice(-10);

  // Save session summary
  const summary = {
    totalInteractions: history.length,
    lastAgent: ctx.agentName,
    lastUpdate: new Date().toISOString()
  };

  await ctx.saveArtifact("session-summary.json", {
    text: JSON.stringify(summary, null, 2)
  });
}

Artifact Processing

Handle file uploads and processing:

async function processUploadedFile(ctx: CallbackContext, filename: string) {
  // Load the uploaded file
  const file = await ctx.loadArtifact(filename);

  if (!file) {
    ctx.state.error = `File ${filename} not found`;
    return;
  }

  // Process based on file type
  let processedContent;
  if (filename.endsWith('.json')) {
    processedContent = JSON.parse(file.text || '{}');
  } else if (filename.endsWith('.txt')) {
    processedContent = {
      content: file.text,
      wordCount: file.text?.split(' ').length || 0
    };
  }

  // Save processed result
  const processedFilename = `processed-${filename}`;
  await ctx.saveArtifact(processedFilename, {
    text: JSON.stringify(processedContent, null, 2)
  });

  // Update state
  ctx.state.processedFiles = ctx.state.processedFiles || [];
  ctx.state.processedFiles.push({
    original: filename,
    processed: processedFilename,
    timestamp: new Date().toISOString()
  });
}

State Management Patterns

State Scoping

Use proper state prefixes for different scopes:

function manageStateScopes(ctx: CallbackContext) {
  // Session-specific state (persists across agent calls)
  ctx.state.sessionData = { sessionId: "abc123" };

  // User-specific state (persists across sessions)
  ctx.state["user.preferences"] = { theme: "dark" };

  // App-specific state (shared across all users)
  ctx.state["app.config"] = { version: "1.0.0" };

  // Temporary state (cleared at session end)
  ctx.state["temp.processing"] = { status: "active" };
}

Conditional State Updates

Update state based on conditions:

async function conditionalStateUpdate(ctx: CallbackContext) {
  // Check if this is a new user
  if (!ctx.state.userInitialized) {
    ctx.state.userInitialized = true;
    ctx.state.userJoinDate = new Date().toISOString();

    // Save welcome artifact
    await ctx.saveArtifact("welcome-message.txt", {
      text: "Welcome to the system! This is your first interaction."
    });
  }

  // Update last seen
  ctx.state.lastSeen = new Date().toISOString();

  // Track user level progression
  const interactionCount = (ctx.state.interactionCount || 0) + 1;
  ctx.state.interactionCount = interactionCount;

  if (interactionCount === 5) {
    ctx.state.userLevel = "intermediate";
  } else if (interactionCount === 20) {
    ctx.state.userLevel = "advanced";
  }
}

Error Handling and Recovery

Implement robust error handling for state operations:

async function robustStateUpdate(ctx: CallbackContext) {
  try {
    // Attempt to load previous state
    const backup = await ctx.loadArtifact("state-backup.json");

    if (backup) {
      const backupState = JSON.parse(backup.text || '{}');
      // Merge with current state
      Object.assign(ctx.state, backupState);
    }

    // Perform operations
    ctx.state.operationStarted = new Date().toISOString();

    // Save new backup
    await ctx.saveArtifact("state-backup.json", {
      text: JSON.stringify(ctx.state, null, 2)
    });

  } catch (error) {
    console.error("Error in state update:", error);

    // Set error state
    ctx.state.lastError = {
      message: error instanceof Error ? error.message : String(error),
      timestamp: new Date().toISOString(),
      invocationId: ctx.invocationId
    };
  }
}

Advanced Patterns

State Migration

Handle state schema changes:

async function migrateState(ctx: CallbackContext) {
  const currentVersion = ctx.state.stateVersion || 1;

  if (currentVersion < 2) {
    // Migrate from v1 to v2
    if (ctx.state.oldUserData) {
      ctx.state.userData = {
        profile: ctx.state.oldUserData,
        migrated: true
      };
      delete ctx.state.oldUserData;
    }
    ctx.state.stateVersion = 2;
  }

  if (currentVersion < 3) {
    // Migrate from v2 to v3
    if (ctx.state.preferences) {
      ctx.state["user.preferences"] = ctx.state.preferences;
      delete ctx.state.preferences;
    }
    ctx.state.stateVersion = 3;
  }
}

Batch Artifact Operations

Process multiple artifacts efficiently:

async function processBatchArtifacts(ctx: CallbackContext, filenames: string[]) {
  const results = [];

  for (const filename of filenames) {
    try {
      const artifact = await ctx.loadArtifact(filename);
      if (artifact) {
        // Process artifact
        const processed = processArtifact(artifact);

        // Save processed version
        const processedName = `batch-${Date.now()}-${filename}`;
        await ctx.saveArtifact(processedName, processed);

        results.push({
          original: filename,
          processed: processedName,
          success: true
        });
      }
    } catch (error) {
      results.push({
        original: filename,
        error: error instanceof Error ? error.message : String(error),
        success: false
      });
    }
  }

  // Update state with batch results
  ctx.state.batchProcessing = {
    timestamp: new Date().toISOString(),
    results: results,
    totalProcessed: results.filter(r => r.success).length
  };
}

function processArtifact(artifact: any) {
  // Implementation depends on artifact type
  return {
    text: `Processed: ${artifact.text || 'No text content'}`
  };
}

Best Practices

State Consistency

// Good - Atomic state updates
function atomicUpdate(ctx: CallbackContext) {
  const userData = ctx.state.userData || {};
  userData.lastUpdate = new Date().toISOString();
  userData.version = (userData.version || 0) + 1;
  ctx.state.userData = userData;
}

// Avoid - Partial state modifications that could leave inconsistent state
function inconsistentUpdate(ctx: CallbackContext) {
  ctx.state.userData.lastUpdate = new Date().toISOString();
  // If this fails, state is partially updated
  ctx.state.userData.version = (ctx.state.userData.version || 0) + 1;
}

Error Recovery

async function safeArtifactOperation(ctx: CallbackContext) {
  const originalState = JSON.stringify(ctx.state);

  try {
    // Perform operation
    const result = await ctx.loadArtifact("important-data.json");
    ctx.state.importantData = JSON.parse(result?.text || '{}');

  } catch (error) {
    // Restore original state on error
    Object.assign(ctx.state, JSON.parse(originalState));

    // Log error state
    ctx.state.lastError = {
      operation: "loadImportantData",
      error: error instanceof Error ? error.message : String(error),
      timestamp: new Date().toISOString()
    };
  }
}

Resource Management

async function efficientArtifactManagement(ctx: CallbackContext) {
  // Check if artifact exists before loading
  const artifactList = ctx.state.availableArtifacts || [];

  if (artifactList.includes("user-data.json")) {
    const userData = await ctx.loadArtifact("user-data.json");
    // Process userData
  }

  // Clean up old artifacts
  const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
  ctx.state.artifactCleanup = {
    lastRun: new Date().toISOString(),
    cutoffDate: cutoffDate.toISOString()
  };
}
  • ReadonlyContext: For read-only access without state modification
  • ToolContext: When you need memory search and enhanced tool capabilities
  • InvocationContext: For complete framework access in agent implementations