TypeScriptADK-TS
Context

Context Patterns

Best practices and common patterns for using context objects effectively

Learn common patterns and best practices for effective context usage across different scenarios in ADK-TS applications.

Security Patterns

Context Selection by Use Case

Choose the right context type based on your security and functionality requirements:

// Read-only instruction generation - use ReadonlyContext
const safeInstruction = (ctx: ReadonlyContext): string => {
  return `Hello ${ctx.state.user?.name || "User"}! Session: ${
    ctx.invocationId
  }`;
};

// State modification in callbacks - use CallbackContext
async function updateUserState(ctx: CallbackContext) {
  ctx.state.lastActive = new Date().toISOString();
  await ctx.saveArtifact("activity.json", { text: "User active" });
}

// Tool with memory access - use ToolContext
async function smartTool(params: any, ctx: ToolContext) {
  const memories = await ctx.searchMemory(params.query);
  ctx.state.lastSearch = memories;
  return { found: memories.memories?.length || 0 };
}

// Full agent implementation - use InvocationContext
export class SecureAgent extends BaseAgent {
  protected async *runAsyncImpl(ctx: InvocationContext) {
    // Full framework access with all services
    yield* this.processSecurely(ctx);
  }
}

Principle of Least Privilege

Use the minimal context type that provides the required functionality:

// Good - ReadonlyContext for safe read operations
function analyzeState(ctx: ReadonlyContext) {
  return {
    userCount: Object.keys(ctx.state).filter(k => k.startsWith("user.")).length,
    sessionAge: Date.now() - (ctx.state.sessionStart || Date.now()),
  };
}

// Avoid - ToolContext when you only need to read state
function badAnalyzeState(ctx: ToolContext) {
  // Unnecessarily powerful context for read-only operation
  return { userCount: 1 };
}

State Management Patterns

State Scoping Strategy

Organize state using consistent scoping patterns:

function manageStateScopes(ctx: CallbackContext) {
  // Session-specific (cleared when session ends)
  ctx.state.currentTask = "processing";
  ctx.state.sessionProgress = 0.5;

  // User-specific (persists across sessions)
  ctx.state["user.preferences"] = { theme: "dark", language: "en" };
  ctx.state["user.profile"] = { name: "John", level: "expert" };

  // App-specific (shared across all users)
  ctx.state["app.version"] = "1.2.0";
  ctx.state["app.features"] = ["feature1", "feature2"];

  // Temporary (explicitly cleaned up)
  ctx.state["temp.processing"] = { status: "active", startTime: Date.now() };
  ctx.state["temp.uploads"] = ["file1.txt", "file2.pdf"];
}

State Validation Pattern

Implement robust state validation:

function validateAndUpdateState(ctx: CallbackContext, updates: any) {
  // Validate state schema
  const schema = {
    userPreferences: { theme: "string", language: "string" },
    currentTask: "string",
    progress: "number",
  };

  // Backup current state
  const backup = JSON.stringify(ctx.state);

  try {
    // Validate updates
    for (const [key, value] of Object.entries(updates)) {
      if (schema[key]) {
        const expectedType = schema[key];
        if (typeof value !== expectedType) {
          throw new Error(
            `Invalid type for ${key}: expected ${expectedType}, got ${typeof value}`,
          );
        }
      }
    }

    // Apply validated updates
    Object.assign(ctx.state, updates, {
      lastUpdate: new Date().toISOString(),
      version: (ctx.state.version || 0) + 1,
    });

    return { success: true, version: ctx.state.version };
  } catch (error) {
    // Restore on validation failure
    Object.assign(ctx.state, JSON.parse(backup));
    throw error;
  }
}

Atomic State Operations

Ensure state consistency with atomic operations:

async function atomicStateUpdate(
  ctx: CallbackContext,
  operation: () => Promise<void>,
) {
  const stateSnapshot = JSON.stringify(ctx.state);
  const transactionId = `tx_${Date.now()}`;

  try {
    // Mark transaction start
    ctx.state._transaction = {
      id: transactionId,
      started: new Date().toISOString(),
    };

    // Execute operation
    await operation();

    // Mark transaction complete
    ctx.state._transaction.completed = new Date().toISOString();
    delete ctx.state._transaction;
  } catch (error) {
    // Rollback on failure
    Object.assign(ctx.state, JSON.parse(stateSnapshot));

    // Log failed transaction
    ctx.state.lastFailedTransaction = {
      id: transactionId,
      error: error instanceof Error ? error.message : String(error),
      timestamp: new Date().toISOString(),
    };

    throw error;
  }
}

Service Integration Patterns

Graceful Service Degradation

Handle optional services gracefully:

async function resilientServiceOperation(ctx: ToolContext, query: string) {
  const results = {
    query,
    timestamp: new Date().toISOString(),
    sources: {
      memory: null as any,
      artifacts: null as any,
      state: ctx.state,
    },
    fallbacksUsed: [] as string[],
  };

  // Try memory service first
  try {
    if (ctx.searchMemory) {
      results.sources.memory = await ctx.searchMemory(query);
    } else {
      results.fallbacksUsed.push("memory-unavailable");
    }
  } catch (error) {
    console.warn("Memory service failed:", error);
    results.fallbacksUsed.push("memory-failed");

    // Fallback to state-based search
    const stateKeys = Object.keys(ctx.state);
    const relevantKeys = stateKeys.filter(key =>
      key.toLowerCase().includes(query.toLowerCase()),
    );
    results.sources.memory = {
      memories: relevantKeys.map(key => ({
        content: `${key}: ${ctx.state[key]}`,
      })),
    };
  }

  // Try artifact listing
  try {
    if (ctx.listArtifacts) {
      const artifacts = await ctx.listArtifacts();
      results.sources.artifacts = artifacts.filter(name =>
        name.toLowerCase().includes(query.toLowerCase()),
      );
    } else {
      results.fallbacksUsed.push("artifacts-unavailable");
    }
  } catch (error) {
    console.warn("Artifact service failed:", error);
    results.fallbacksUsed.push("artifacts-failed");
    results.sources.artifacts = [];
  }

  return results;
}

Service Coordination Pattern

Coordinate operations across multiple services:

async function coordinatedOperation(ctx: InvocationContext, operation: string) {
  const coordinationId = `coord_${Date.now()}`;
  const results = [];

  // Track coordination in session state
  ctx.session.state.activeCoordinations =
    ctx.session.state.activeCoordinations || {};
  ctx.session.state.activeCoordinations[coordinationId] = {
    operation,
    started: new Date().toISOString(),
    services: [],
  };

  try {
    // Memory service operation
    if (ctx.memoryService) {
      const memoryResult = await ctx.memoryService.searchMemory({
        query: operation,
        appName: ctx.appName,
        userId: ctx.userId,
      });

      results.push({ service: "memory", success: true, data: memoryResult });
      ctx.session.state.activeCoordinations[coordinationId].services.push(
        "memory",
      );
    }

    // Artifact service operation
    if (ctx.artifactService) {
      const artifacts = await ctx.artifactService.listArtifactKeys({
        appName: ctx.appName,
        userId: ctx.userId,
        sessionId: ctx.session.id,
      });

      results.push({ service: "artifact", success: true, data: artifacts });
      ctx.session.state.activeCoordinations[coordinationId].services.push(
        "artifact",
      );
    }

    // Session service operation
    await ctx.sessionService.updateSession(ctx.session);
    results.push({ service: "session", success: true, data: "updated" });
    ctx.session.state.activeCoordinations[coordinationId].services.push(
      "session",
    );

    // Mark as completed
    ctx.session.state.activeCoordinations[coordinationId].completed =
      new Date().toISOString();

    return results;
  } catch (error) {
    // Handle coordination failure
    ctx.session.state.activeCoordinations[coordinationId].error = {
      message: error instanceof Error ? error.message : String(error),
      timestamp: new Date().toISOString(),
    };
    throw error;
  }
}

Error Handling Patterns

Context-Aware Error Recovery

Implement error recovery based on context capabilities:

async function recoverableOperation(ctx: CallbackContext | ToolContext) {
  const operationId = `op_${Date.now()}`;

  try {
    // Attempt primary operation
    const result = await performPrimaryOperation(ctx);

    // Save successful result
    await ctx.saveArtifact(`result_${operationId}.json`, {
      text: JSON.stringify(result, null, 2),
    });

    return result;
  } catch (primaryError) {
    console.warn("Primary operation failed:", primaryError);

    // Try recovery based on context capabilities
    if ("searchMemory" in ctx && ctx.searchMemory) {
      // ToolContext - try memory-based recovery
      try {
        const memoryResults = await ctx.searchMemory("similar operations");
        const recoveryData = memoryResults.memories?.[0];

        if (recoveryData) {
          ctx.state.recoveryUsed = {
            method: "memory",
            operationId,
            timestamp: new Date().toISOString(),
          };

          return { recovered: true, data: recoveryData };
        }
      } catch (memoryError) {
        console.warn("Memory recovery failed:", memoryError);
      }
    }

    // Fallback to state-based recovery
    const fallbackData = ctx.state.lastSuccessfulOperation || null;
    if (fallbackData) {
      ctx.state.recoveryUsed = {
        method: "state-fallback",
        operationId,
        timestamp: new Date().toISOString(),
      };

      return { recovered: true, data: fallbackData };
    }

    // No recovery possible - save error and rethrow
    ctx.state.lastOperationError = {
      operationId,
      error:
        primaryError instanceof Error
          ? primaryError.message
          : String(primaryError),
      timestamp: new Date().toISOString(),
    };

    throw primaryError;
  }
}

async function performPrimaryOperation(ctx: CallbackContext | ToolContext) {
  // Placeholder for actual operation
  throw new Error("Simulated primary operation failure");
}

Cascading Error Handling

Handle errors that affect multiple context layers:

class ErrorHandlingAgent extends BaseAgent {
  protected async *runAsyncImpl(
    context: InvocationContext,
  ): AsyncGenerator<Event, void, unknown> {
    const errorContext = {
      invocationId: context.invocationId,
      agentName: this.name,
      errors: [] as any[],
    };

    try {
      yield* this.processWithErrorHandling(context, errorContext);
    } catch (criticalError) {
      // Handle critical errors that affect the entire invocation
      yield this.createCriticalErrorEvent(context, criticalError, errorContext);

      // Attempt graceful degradation
      yield* this.attemptGracefulDegradation(context, errorContext);
    }
  }

  private async *processWithErrorHandling(
    context: InvocationContext,
    errorContext: any,
  ): AsyncGenerator<Event, void, unknown> {
    // Try normal processing with error tracking
    try {
      // Update session state
      context.session.state.processingStatus = "active";

      // Process with services
      if (context.memoryService) {
        try {
          const memories = await context.memoryService.searchMemory({
            query: "user context",
            appName: context.appName,
            userId: context.userId,
          });

          context.session.state.contextMemories =
            memories.memories?.length || 0;
        } catch (memoryError) {
          errorContext.errors.push({
            service: "memory",
            error:
              memoryError instanceof Error
                ? memoryError.message
                : String(memoryError),
            timestamp: new Date().toISOString(),
          });

          // Continue without memory - non-critical error
        }
      }

      // Generate success event
      yield new Event({
        invocationId: context.invocationId,
        author: this.name,
        content: {
          parts: [{ text: "Processing completed with error handling" }],
        },
      });
    } catch (error) {
      errorContext.errors.push({
        stage: "processing",
        error: error instanceof Error ? error.message : String(error),
        timestamp: new Date().toISOString(),
      });
      throw error;
    }
  }

  private createCriticalErrorEvent(
    context: InvocationContext,
    error: unknown,
    errorContext: any,
  ): Event {
    return new Event({
      invocationId: context.invocationId,
      author: this.name,
      content: {
        parts: [
          {
            text: `Critical error occurred. Error details saved to session state.`,
          },
        ],
      },
      errorCode: "CRITICAL_ERROR",
      errorMessage: error instanceof Error ? error.message : String(error),
    });
  }
}

Performance Patterns

Context Caching Strategy

Cache expensive context operations:

class CachingToolContext {
  private cache = new Map<
    string,
    { data: any; timestamp: number; ttl: number }
  >();

  constructor(private baseContext: ToolContext) {}

  async cachedSearchMemory(query: string, ttlMs = 60000): Promise<any> {
    const cacheKey = `memory_${query}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < cached.ttl) {
      // Update base context state to reflect cache hit
      this.baseContext.state.cacheHits =
        (this.baseContext.state.cacheHits || 0) + 1;
      return cached.data;
    }

    // Cache miss - perform actual search
    const result = await this.baseContext.searchMemory(query);

    // Cache the result
    this.cache.set(cacheKey, {
      data: result,
      timestamp: Date.now(),
      ttl: ttlMs,
    });

    // Update context state
    this.baseContext.state.cacheMisses =
      (this.baseContext.state.cacheMisses || 0) + 1;

    return result;
  }

  async cachedListArtifacts(ttlMs = 30000): Promise<string[]> {
    const cacheKey = "artifacts_list";
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < cached.ttl) {
      return cached.data;
    }

    const result = await this.baseContext.listArtifacts();

    this.cache.set(cacheKey, {
      data: result,
      timestamp: Date.now(),
      ttl: ttlMs,
    });

    return result;
  }

  clearCache(): void {
    this.cache.clear();
    this.baseContext.state.cacheCleared = new Date().toISOString();
  }
}

Batch Operation Pattern

Optimize multiple operations using batching:

async function batchContextOperations(ctx: ToolContext, operations: any[]) {
  const batchId = `batch_${Date.now()}`;
  const results = [];

  // Group operations by type
  const memoryQueries = operations.filter(op => op.type === "memory");
  const artifactOps = operations.filter(op => op.type === "artifact");
  const stateOps = operations.filter(op => op.type === "state");

  // Track batch in state
  ctx.state.activeBatches = ctx.state.activeBatches || {};
  ctx.state.activeBatches[batchId] = {
    started: new Date().toISOString(),
    totalOps: operations.length,
    completed: 0,
  };

  try {
    // Batch memory queries
    if (memoryQueries.length > 0) {
      const memoryPromises = memoryQueries.map(async op => {
        try {
          const result = await ctx.searchMemory(op.query);
          ctx.state.activeBatches[batchId].completed++;
          return { operation: op, result, success: true };
        } catch (error) {
          return { operation: op, error: String(error), success: false };
        }
      });

      const memoryResults = await Promise.all(memoryPromises);
      results.push(...memoryResults);
    }

    // Batch artifact operations
    if (artifactOps.length > 0) {
      // Batch artifact loading
      const artifactPromises = artifactOps.map(async op => {
        try {
          let result;
          if (op.action === "list") {
            result = await ctx.listArtifacts();
          } else if (op.action === "load") {
            result = await ctx.loadArtifact(op.filename);
          }

          ctx.state.activeBatches[batchId].completed++;
          return { operation: op, result, success: true };
        } catch (error) {
          return { operation: op, error: String(error), success: false };
        }
      });

      const artifactResults = await Promise.all(artifactPromises);
      results.push(...artifactResults);
    }

    // Batch state operations (synchronous)
    stateOps.forEach(op => {
      try {
        if (op.action === "set") {
          ctx.state[op.key] = op.value;
        } else if (op.action === "get") {
          results.push({
            operation: op,
            result: ctx.state[op.key],
            success: true,
          });
        }
        ctx.state.activeBatches[batchId].completed++;
      } catch (error) {
        results.push({
          operation: op,
          error: String(error),
          success: false,
        });
      }
    });

    // Mark batch complete
    ctx.state.activeBatches[batchId].completedAt = new Date().toISOString();

    return {
      batchId,
      total: operations.length,
      successful: results.filter(r => r.success).length,
      failed: results.filter(r => !r.success).length,
      results,
    };
  } catch (error) {
    ctx.state.activeBatches[batchId].error = {
      message: error instanceof Error ? error.message : String(error),
      timestamp: new Date().toISOString(),
    };
    throw error;
  }
}

Common Anti-Patterns

What to Avoid

// ❌ DON'T: Use powerful context for simple operations
function badReadOnlyOperation(ctx: ToolContext) {
  // Only reading state but using ToolContext unnecessarily
  return ctx.state.userName;
}

// ✅ DO: Use appropriate context level
function goodReadOnlyOperation(ctx: ReadonlyContext) {
  return ctx.state.userName;
}

// ❌ DON'T: Hold references to context beyond operation scope
class BadService {
  private storedContext: ToolContext; // Don't do this!

  setContext(ctx: ToolContext) {
    this.storedContext = ctx; // Context may become stale
  }
}

// ✅ DO: Pass context to each operation
class GoodService {
  async performOperation(ctx: ToolContext, params: any) {
    // Use context immediately, don't store it
    return await ctx.searchMemory(params.query);
  }
}

// ❌ DON'T: Ignore service availability
async function badServiceUsage(ctx: ToolContext) {
  // Will throw if memory service not configured
  return await ctx.searchMemory("query");
}

// ✅ DO: Check service availability
async function goodServiceUsage(ctx: ToolContext) {
  try {
    return await ctx.searchMemory("query");
  } catch (error) {
    if (error.message.includes("Memory service is not available")) {
      // Handle gracefully
      return { memories: [] };
    }
    throw error;
  }
}

Testing Patterns

Context Mocking for Tests

// Create mock contexts for testing
function createMockReadonlyContext(state: any = {}): ReadonlyContext {
  return {
    userContent: { parts: [{ text: "test input" }] },
    invocationId: "test-invocation-123",
    agentName: "test-agent",
    state: Object.freeze(state),
  } as ReadonlyContext;
}

function createMockToolContext(state: any = {}): ToolContext {
  return {
    ...createMockReadonlyContext(state),
    state: state, // Mutable for ToolContext
    functionCallId: "test-function-call-456",
    actions: {
      escalate: false,
      transferToAgent: null,
      skipSummarization: false,
      stateDelta: {},
      artifactDelta: {},
    },
    loadArtifact: jest.fn(),
    saveArtifact: jest.fn(),
    listArtifacts: jest.fn().mockResolvedValue([]),
    searchMemory: jest.fn().mockResolvedValue({ memories: [] }),
  } as any;
}

// Test context usage
describe("Context Usage", () => {
  test("should handle readonly context safely", () => {
    const mockState = { user: { name: "Test User" } };
    const ctx = createMockReadonlyContext(mockState);

    const result = analyzeState(ctx);

    expect(result.userCount).toBeGreaterThan(0);
    expect(ctx.state).toEqual(mockState); // State unchanged
  });

  test("should use tool context features", async () => {
    const ctx = createMockToolContext({ preferences: { theme: "dark" } });

    const result = await smartTool({ query: "test" }, ctx);

    expect(ctx.searchMemory).toHaveBeenCalledWith("test");
    expect(ctx.state.lastSearch).toBeDefined();
  });
});

These patterns provide a foundation for robust, secure, and performant context usage across your ADK-TS applications. Choose the appropriate patterns based on your specific requirements and always consider the security and performance implications of your context usage decisions.