diff --git a/apps/docs/content/docs/framework/context/callback-context.mdx b/apps/docs/content/docs/framework/context/callback-context.mdx index 190b86102..da691ce07 100644 --- a/apps/docs/content/docs/framework/context/callback-context.mdx +++ b/apps/docs/content/docs/framework/context/callback-context.mdx @@ -1,466 +1,179 @@ --- title: CallbackContext -description: State management and artifact operations for agent lifecycle callbacks +description: Mutable state and artifact operations for agent lifecycle callbacks. --- +import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; -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. +`CallbackContext` extends `ReadonlyContext` with the ability to write to session state and interact with the artifact service. ADK-TS passes it to every agent lifecycle callback β€” `beforeAgentCallback`, `afterAgentCallback`, `beforeModelCallback`, and `afterModelCallback`. ```typescript 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) +## Added properties -## Properties +### `state` -### state (Mutable) - -Unlike ReadonlyContext, CallbackContext provides a mutable state object: +A mutable `State` object that replaces the frozen read-only snapshot from `ReadonlyContext`. Writes are tracked as a delta and flushed to the session store at the end of the turn. ```typescript get state(): State ``` -The state object supports direct modification and automatically tracks changes: +You can use bracket notation or the `set` / `get` methods interchangeably: ```typescript -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(); -} -``` +// Bracket assignment (tracked automatically) +ctx.state["user:name"] = "Alice"; -### eventActions +// Equivalent method call +ctx.state.set("user:name", "Alice"); -Access to the event actions for advanced state and flow control: - -```typescript -get eventActions(): EventActions +// Reading back +const name = ctx.state["user:name"]; +// or +const name = ctx.state.get("user:name"); ``` -This provides access to underlying event tracking and action management. - -## Artifact Operations - -### loadArtifact() +### `eventActions` -Loads an artifact attached to the current session: +The `EventActions` instance for this callback turn. Normally you don't need to access this directly β€” the framework reads it after the callback returns. It is exposed for advanced use cases such as setting `skipSummarization` on function response callbacks. ```typescript -async loadArtifact(filename: string, version?: number): Promise -``` - -```typescript -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: - -```typescript -async saveArtifact(filename: string, artifact: Part): Promise -``` - -```typescript -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; -} +get eventActions(): EventActions ``` -## Common Use Cases - -### Pre-Processing Callbacks +### `invocationContext` -Modify state before agent or model execution: +The underlying `InvocationContext` for this invocation, provided for advanced scenarios where you need direct service access inside a callback. ```typescript -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(), - }; -} +get invocationContext(): InvocationContext ``` -### Post-Processing Callbacks - -Update state based on execution results: +## Artifact methods -```typescript -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), - }); -} -``` +Both methods throw if no artifact service is configured in the runner. -### Artifact Processing +### `loadArtifact()` -Handle file uploads and processing: +Loads a previously saved artifact. Omit `version` to retrieve the latest. ```typescript -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(), - }); -} +async loadArtifact(filename: string, version?: number): Promise ``` -## State Management Patterns +### `saveArtifact()` -### State Scoping - -Use proper state prefixes for different scopes: +Saves an artifact and registers it in the event delta so the session reflects the new version. Returns the version number assigned to this save (starting at 0). ```typescript -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" }; -} +async saveArtifact(filename: string, artifact: Part): Promise ``` -### Conditional State Updates - -Update state based on conditions: + + Artifacts are `Part` objects from `@google/genai`. A text artifact is `{ + text: "..." }`. For binary data use `{ inlineData: { mimeType, data } }`. See + the [Artifacts](/docs/framework/artifacts) section for details. + -```typescript -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.", - }); - } +## State scoping - // Update last seen - ctx.state.lastSeen = new Date().toISOString(); +ADK-TS uses key prefixes to scope state to different lifetimes. Choose the prefix that matches how long you need the value to persist: - // Track user level progression - const interactionCount = (ctx.state.interactionCount || 0) + 1; - ctx.state.interactionCount = interactionCount; +| Prefix | Scope | Example | +| ------------- | ------------------------------------------ | -------------------------- | +| _(no prefix)_ | Current session | `ctx.state["currentTask"]` | +| `user:` | Across all sessions for this user | `ctx.state["user:name"]` | +| `app:` | Shared across all users of this app | `ctx.state["app:version"]` | +| `temp:` | Current turn only (cleared after response) | `ctx.state["temp:draft"]` | - if (interactionCount === 5) { - ctx.state.userLevel = "intermediate"; - } else if (interactionCount === 20) { - ctx.state.userLevel = "advanced"; - } -} -``` +## Common callback patterns -### Error Handling and Recovery +### Before-agent callback β€” enrich context -Implement robust error handling for state operations: +A `beforeAgentCallback` runs before each agent call. Use it to hydrate state from an artifact, increment counters, or inject metadata the agent needs: ```typescript -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 +import { + AgentBuilder, + CallbackContext, + InMemoryArtifactService, +} from "@iqai/adk"; -### State Migration +const { runner } = await AgentBuilder.create("assistant") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withBeforeAgentCallback(async (ctx: CallbackContext) => { + // Track how many times this agent has been called + const callCount = ((ctx.state["agentCalls"] as number) ?? 0) + 1; + ctx.state["agentCalls"] = callCount; -Handle state schema changes: - -```typescript -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; + // Load a user profile saved in a previous session + const profile = await ctx.loadArtifact("user-profile.json"); + if (profile?.text) { + ctx.state["user:profile"] = JSON.parse(profile.text); } - ctx.state.stateVersion = 3; - } -} -``` - -### Batch Artifact Operations -Process multiple artifacts efficiently: - -```typescript -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"}`, - }; -} + return undefined; // Returning undefined lets execution continue normally + }) + .build(); ``` -## Best Practices +### After-agent callback β€” persist results -### State Consistency +An `afterAgentCallback` runs after the agent produces its response. Use it to save a summary or checkpoint before the turn closes: ```typescript -// 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; -} -``` +import { + AgentBuilder, + CallbackContext, + InMemoryArtifactService, +} from "@iqai/adk"; -### Error Recovery - -```typescript -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), +const { runner } = await AgentBuilder.create("summarizer") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withAfterAgentCallback(async (ctx: CallbackContext) => { + const summary = { + invocationId: ctx.invocationId, + agentName: ctx.agentName, timestamp: new Date().toISOString(), + callCount: ctx.state["agentCalls"], }; - } -} -``` - -### Resource Management -```typescript -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(), - }; -} -``` - -## Related Contexts + await ctx.saveArtifact("session-summary.json", { + text: JSON.stringify(summary, null, 2), + }); -- **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 + return undefined; + }) + .build(); +``` + +## Next steps + + + + + + + diff --git a/apps/docs/content/docs/framework/context/context-caching.mdx b/apps/docs/content/docs/framework/context/context-caching.mdx index 27369990b..8786fd83a 100644 --- a/apps/docs/content/docs/framework/context/context-caching.mdx +++ b/apps/docs/content/docs/framework/context/context-caching.mdx @@ -3,6 +3,7 @@ title: Context Caching description: A guide explaining how to configure and use context caching in ADK-TS with Gemini and Anthropic models to reuse large prompts efficiently, reducing latency and token costs. --- +import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; When working with agents to complete tasks, you may want to reuse extended instructions or large sets of data across multiple agent requests to a generative AI model. Resending this data for each agent request is slow, inefficient, and can be expensive. Using context caching features in generative AI models can significantly speed up responses and lower the number of tokens sent to the model for each request. @@ -140,9 +141,25 @@ Additionally, follow these best practices: ## Next steps -If your use case relies on instructions that should persist across an entire -session, consider using static or session-level instructions instead of -re-sending them on every request. - -To evaluate the impact of context caching, compare latency and token usage with -and without caching enabled while running the same agent workload. + + + + + + diff --git a/apps/docs/content/docs/framework/context/context-patterns.mdx b/apps/docs/content/docs/framework/context/context-patterns.mdx index 86750658a..7f10c6ed9 100644 --- a/apps/docs/content/docs/framework/context/context-patterns.mdx +++ b/apps/docs/content/docs/framework/context/context-patterns.mdx @@ -1,768 +1,210 @@ --- title: Context Patterns -description: Best practices and common patterns for using context objects effectively +description: State scoping, context selection, anti-patterns, and testing strategies for ADK-TS context objects. --- +import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; -Learn common patterns and best practices for effective context usage across different scenarios in ADK-TS applications. +A few recurring patterns come up whenever you work with context objects. This page collects the ones that save the most time. -## Security Patterns +## Pick the right context type -### Context Selection by Use Case +Use the weakest context type that satisfies your requirements. Callbacks and functions that receive a powerful context but only read state are harder to reason about β€” a `ReadonlyContext` parameter makes the intent clear and prevents accidental mutations. -Choose the right context type based on your security and functionality requirements: +| You need to… | Use | +| ---------------------------------- | ------------------- | +| Generate dynamic instructions | `ReadonlyContext` | +| Read session state in a callback | `ReadonlyContext` | +| Write to state or save an artifact | `CallbackContext` | +| Search memory or list artifacts | `ToolContext` | +| Access services in a custom agent | `InvocationContext` | -```typescript -// 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); - } -} -``` +## State scoping -### Principle of Least Privilege +ADK-TS uses key prefixes to control how long a state value lives. Using the wrong prefix is a common source of values disappearing unexpectedly or persisting longer than intended. -Use the minimal context type that provides the required functionality: +| Prefix | Lifetime | Example key | +| -------- | --------------------------------- | -------------------- | +| _(none)_ | Current session | `"taskStatus"` | +| `user:` | Across all sessions for this user | `"user:name"` | +| `app:` | Shared across all users | `"app:featureFlags"` | +| `temp:` | Current turn only | `"temp:draftReply"` | ```typescript -// 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()), - }; -} +import { CallbackContext } from "@iqai/adk"; -// Avoid - ToolContext when you only need to read state -function badAnalyzeState(ctx: ToolContext) { - // Unnecessarily powerful context for read-only operation - return { userCount: 1 }; -} -``` +function applyStateScoping(ctx: CallbackContext) { + // Persists for the life of this session + ctx.state["currentTask"] = "onboarding"; -## State Management Patterns + // Persists across all sessions for this user + ctx.state["user:name"] = "Alice"; -### State Scoping Strategy + // Shared across every user in the app + ctx.state["app:maintenanceMode"] = false; -Organize state using consistent scoping patterns: - -```typescript -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"]; + // Cleared after the current agent turn + ctx.state["temp:searchResults"] = []; } ``` -### State Validation Pattern + + The prefixes use a colon (`:`) not a dot (`.`). `"user:name"` is the correct + form; `"user.name"` is treated as a plain session-scoped key. + -Implement robust state validation: +## Common anti-patterns -```typescript -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: +### Using a more powerful context than needed ```typescript -async function atomicStateUpdate( - ctx: CallbackContext, - operation: () => Promise, -) { - 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; - } +// ❌ ToolContext just to read a state value +function getPreference(ctx: ToolContext): string { + return ctx.state["user:theme"]; } -``` - -## Service Integration Patterns - -### Graceful Service Degradation -Handle optional services gracefully: - -```typescript -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; +// βœ… ReadonlyContext is sufficient for reads +function getPreference(ctx: ReadonlyContext): string { + return ctx.state["user:theme"] as string; } ``` -### Service Coordination Pattern +### Storing context references across calls -Coordinate operations across multiple services: +Context objects are valid only for the duration of a single callback or tool execution. Holding onto a reference after the call returns leads to stale state and undefined behaviour. ```typescript -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: [], - }; +// ❌ Storing context on a service instance +class BadService { + private ctx: ToolContext; // Don't do this - 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; + setContext(ctx: ToolContext) { + this.ctx = ctx; } } -``` - -## Error Handling Patterns - -### Context-Aware Error Recovery -Implement error recovery based on context capabilities: - -```typescript -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; +// βœ… Pass context to each operation directly +class GoodService { + async search(query: string, ctx: ToolContext) { + return ctx.searchMemory(query); } } - -async function performPrimaryOperation(ctx: CallbackContext | ToolContext) { - // Placeholder for actual operation - throw new Error("Simulated primary operation failure"); -} ``` -### Cascading Error Handling +### Ignoring service availability -Handle errors that affect multiple context layers: +`artifactService` and `memoryService` are optional. Calling methods on them without checking throws at runtime. ```typescript -class ErrorHandlingAgent extends BaseAgent { - protected async *runAsyncImpl( - context: InvocationContext, - ): AsyncGenerator { - 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 { - // 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), - }); - } +// ❌ Will throw if memory service is not configured +async function badSearch(ctx: ToolContext, query: string) { + return await ctx.searchMemory(query); } -``` - -## Performance Patterns - -### Context Caching Strategy - -Cache expensive context operations: - -```typescript -class CachingToolContext { - private cache = new Map< - string, - { data: any; timestamp: number; ttl: number } - >(); - - constructor(private baseContext: ToolContext) {} - - async cachedSearchMemory(query: string, ttlMs = 60000): Promise { - 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 { - 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(); +// βœ… Guard or use try/catch +async function safeSearch(ctx: ToolContext, query: string) { + try { + return await ctx.searchMemory(query); + } catch { + return []; } } ``` -### Batch Operation Pattern +## Testing -Optimize multiple operations using batching: +The simplest approach is to pass plain objects that satisfy the type. For `ReadonlyContext`, only a few properties are needed for most tests: ```typescript -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, - }; +import type { ReadonlyContext } from "@iqai/adk"; + +function buildInstruction(ctx: ReadonlyContext): string { + const name = ctx.state["user:name"] ?? "there"; + return `Hello, ${name}!`; +} + +// Unit test β€” no framework setup required +test("builds instruction with user name", () => { + const ctx = { + userContent: undefined, + invocationId: "test-inv-001", + agentName: "greeter", + appName: "my-app", + userId: "user-1", + sessionId: "session-1", + state: Object.freeze({ "user:name": "Alice" }), + } as ReadonlyContext; - 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; - } -} + expect(buildInstruction(ctx)).toBe("Hello, Alice!"); +}); ``` -## Common Anti-Patterns - -### What to Avoid +For `ToolContext` in Vitest, stub only the methods your tool actually calls: ```typescript -// ❌ DON'T: Use powerful context for simple operations -function badReadOnlyOperation(ctx: ToolContext) { - // Only reading state but using ToolContext unnecessarily - return ctx.state.userName; -} +import { vi } from "vitest"; +import type { ToolContext, MemorySearchResult } from "@iqai/adk"; -// βœ… 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 - } -} +function buildMockToolContext( + stateValues: Record = {}, +): ToolContext { + const state: Record = { ...stateValues }; -// βœ… 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 - -```typescript -// Create mock contexts for testing -function createMockReadonlyContext(state: any = {}): ReadonlyContext { return { - userContent: { parts: [{ text: "test input" }] }, - invocationId: "test-invocation-123", + invocationId: "test-inv-001", 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; + appName: "test-app", + userId: "user-1", + sessionId: "session-1", + userContent: undefined, + functionCallId: "call-001", + state: new Proxy(state, { + get: (t, k) => { + if (k === "get") return (key: string) => t[key]; + if (k === "set") + return (key: string, val: unknown) => { + t[key] = val; + }; + return t[k as string]; + }, + set: (t, k, v) => { + t[k as string] = v; + return true; + }, + }) as any, + actions: { stateDelta: {}, artifactDelta: {} } as any, + searchMemory: vi + .fn<[string], Promise>() + .mockResolvedValue([]), + listArtifacts: vi.fn().mockResolvedValue([]), + loadArtifact: vi.fn().mockResolvedValue(undefined), + saveArtifact: vi.fn().mockResolvedValue(0), + } as unknown as ToolContext; } - -// 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. +## Next steps + + + + + + + diff --git a/apps/docs/content/docs/framework/context/index.mdx b/apps/docs/content/docs/framework/context/index.mdx index ba9c86fa0..f3f2551c2 100644 --- a/apps/docs/content/docs/framework/context/index.mdx +++ b/apps/docs/content/docs/framework/context/index.mdx @@ -1,186 +1,139 @@ --- -title: Context Management -description: Access execution state, services, and session information throughout agent operations +title: Context +description: The context objects that give agents, tools, and callbacks access to session state, services, and execution information. --- import { Cards, Card } from "fumadocs-ui/components/card"; -import { Callout } from "fumadocs-ui/components/callout"; - -Context management provides agents and tools with access to execution state, services, and session information needed for effective operation. Context objects serve as the bridge between your code and the ADK-TS framework. - -## What is Context? - -Context refers to the bundle of information available to agents, tools, and callbacks during execution. It provides access to session state, services, and execution details needed for intelligent decision-making. - - - -Think of context as the bridge connecting your agent logic to the ADK-TS -framework - providing access to everything needed for intelligent agent -behavior. - - - -## Context Types - -ADK-TS provides specialized context objects tailored for different execution scenarios and security requirements. - - - - -|extends| CC + CC -->|extends| TC + +`} /> - +`InvocationContext` is a standalone class β€” it is not in the inheritance chain above, but it is the internal data source that all three context types wrap. - - - -## Core Capabilities +## When to use each -### State Management +| Context | Use when | +| ------------------- | ------------------------------------------------------------------------------- | +| `ReadonlyContext` | Generating dynamic instructions; read-only inspection of session state | +| `CallbackContext` | Agent lifecycle callbacks that modify state or read/write artifacts | +| `ToolContext` | Tool implementations that need memory search, artifact listing, or flow control | +| `InvocationContext` | Custom agent implementations that need direct access to services | -- **Session State**: Read and modify session state across conversation turns -- **State Scoping**: Proper handling of session, user, app, and temporary state -- **Change Tracking**: Automatic tracking of state modifications through events +## Quick examples -### Service Integration +### Dynamic instruction (ReadonlyContext) -- **Session Services**: Access to session and history management -- **Artifact Services**: File and binary data storage operations -- **Memory Services**: Long-term knowledge storage and retrieval - -### Execution Control - -- **Identity Tracking**: Know which agent is running and track invocation details -- **Flow Control**: Manage execution flow and lifecycle -- **Resource Management**: Handle external resources and cleanup - -## Context Hierarchy +Instruction providers receive `ReadonlyContext`. Use it to personalise system prompts without risking any side effects: ```typescript -ReadonlyContext -β”œβ”€β”€ CallbackContext (extends ReadonlyContext) -β”‚ └── ToolContext (extends CallbackContext) -└── InvocationContext (standalone, comprehensive) -``` - -- **ReadonlyContext**: Base class providing safe read-only access -- **CallbackContext**: Adds state management and artifact operations -- **ToolContext**: Extends with memory search and enhanced capabilities -- **InvocationContext**: Complete framework access for core agent implementation - -## Basic Usage - -### In Instruction Providers - -```typescript -import { ReadonlyContext } from "@iqai/adk"; - -const dynamicInstruction = (ctx: ReadonlyContext): string => { - const userState = ctx.state; - return `You are helping user in session ${ - ctx.invocationId - }. User context: ${JSON.stringify(userState)}`; -}; -``` - -### In Function Tools - -```typescript -import { FunctionTool, ToolContext } from "@iqai/adk"; - -function searchAndSave(params: { query: string }, toolContext: ToolContext) { - // Search memory - const memories = await toolContext.searchMemory(params.query); - - // Save to state - toolContext.state.searchResults = memories; - - return { found: memories.memories?.length || 0 }; -} +import { LlmAgent, Runner, InMemorySessionService } from "@iqai/adk"; +import type { ReadonlyContext } from "@iqai/adk"; + +const agent = new LlmAgent({ + name: "assistant", + description: "A helpful assistant that greets users by name", + model: "gemini-2.5-flash", + instruction: (ctx: ReadonlyContext) => { + const name = ctx.state["user:name"] || "there"; + return `You are a helpful assistant. The user's name is ${name}.`; + }, +}); -const searchTool = new FunctionTool(searchAndSave, { - name: "search_and_save", - description: "Search memory and save results to state", +const runner = new Runner({ + agent, + appName: "my-app", + sessionService: new InMemorySessionService(), }); ``` -### In Agent Implementation +### Tool with memory (ToolContext) + +Tool functions receive `ToolContext` as their second parameter, injected automatically by the framework: ```typescript -import { LlmAgent, InvocationContext } from "@iqai/adk"; - -class CustomAgent extends LlmAgent { - protected async *runAsyncImpl(context: InvocationContext) { - // Access all services and manage full execution flow - const session = context.session; - const memoryService = context.memoryService; - - // Your custom agent logic here - yield* super.runAsyncImpl(context); - } -} +import { createTool, ToolContext } from "@iqai/adk"; +import { z } from "zod"; + +const recallTool = createTool({ + name: "recall", + description: "Search the user's long-term memory", + schema: z.object({ query: z.string() }), + fn: async ({ query }, ctx: ToolContext) => { + const results = await ctx.searchMemory(query); + return results.map(r => r.memory.content); + }, +}); ``` -## Common Patterns - -### State Management - -- Use appropriate state prefixes for scoping (`session.`, `user.`, `app.`, `temp.`) -- Leverage automatic change tracking in callback contexts -- Access read-only state safely in instruction providers - -### Service Usage - -- Handle service availability gracefully (services may be undefined) -- Implement proper error handling for service operations -- Use context-appropriate service access patterns - -### Performance Considerations - -- Use minimal context type needed for each operation -- Avoid accessing unnecessary context properties -- Implement efficient resource management patterns - -## Related Topics +## Next steps + + + + - - - - - diff --git a/apps/docs/content/docs/framework/context/invocation-context.mdx b/apps/docs/content/docs/framework/context/invocation-context.mdx index 4c0cbb9be..17c4c1535 100644 --- a/apps/docs/content/docs/framework/context/invocation-context.mdx +++ b/apps/docs/content/docs/framework/context/invocation-context.mdx @@ -1,717 +1,202 @@ --- title: InvocationContext -description: Complete framework access for agent core implementation and invocation management +description: The internal execution environment for a single agent invocation β€” exposes all services, session data, and lifecycle controls. --- +import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; +import { Mermaid } from "@/components/mdx/mermaid"; -InvocationContext is the most comprehensive context object in ADK-TS, providing complete access to the framework's capabilities. It's primarily used in agent core implementation and represents the full execution environment for a single invocation. - -## Overview - -InvocationContext contains all the information and services needed for a complete agent invocation lifecycle. Unlike other context types, it's standalone and doesn't extend from ReadonlyContext, providing direct access to all framework components. +`InvocationContext` is the root data container for a single invocation β€” one call to `runner.run()` from start to finish. It is not in the `ReadonlyContext β†’ CallbackContext β†’ ToolContext` inheritance chain; instead, all three of those classes hold a reference to it internally. You interact with `InvocationContext` directly only when writing custom agent classes that extend `BaseAgent` or `LlmAgent`. ```typescript import { InvocationContext } from "@iqai/adk"; ``` -## Core Properties - -### Service References +## What an invocation contains -Direct access to all configured services: + IC[InvocationContext created] + IC --> A1[agent.runAsync - agent call 1] + A1 --> S1[step: LLM call + tool calls] + A1 --> S2[step: LLM call + tool calls] + A1 --> T[transfer to agent 2] + T --> A2[agent.runAsync - agent call 2] + A2 --> S3[step: LLM call β†’ final response] + S3 --> END[invocation ends] +`} +/> -```typescript -readonly artifactService?: BaseArtifactService; -readonly sessionService: BaseSessionService; -readonly memoryService?: BaseMemoryService; -``` +A single invocation spans all agent calls from the moment the user message arrives to the moment a final response is produced. All agent calls within one invocation share the same `invocationId` and the same session. -### Invocation Identity +## Properties -Unique identification and tracking for the current invocation: +### Services -```typescript -readonly invocationId: string; -readonly branch?: string; -``` +Services are set by the runner at startup. Optional services may be `undefined` β€” always guard before using them. -### Agent and Session Management +| Property | Type | Description | +| ----------------- | ---------------------------------- | --------------------------------------------------------- | +| `sessionService` | `BaseSessionService` | Always present. Manages session CRUD and event appending. | +| `artifactService` | `BaseArtifactService \| undefined` | Present if configured with `.withArtifactService(...)`. | +| `memoryService` | `MemoryService \| undefined` | Present if configured with `.withMemoryService(...)`. | +| `pluginManager` | `PluginManager` | Manages registered plugins for this invocation. | -Current execution context and session information: +### Identity and routing -```typescript -agent: BaseAgent; -readonly session: Session; -readonly userContent?: Content; -``` +| Property | Type | Description | +| -------------- | --------------------- | ---------------------------------------------------------------- | +| `invocationId` | `string` | Unique ID for this invocation, shared across all child contexts. | +| `branch` | `string \| undefined` | Dot-separated agent path, e.g. `"orchestrator.researcher"`. | +| `appName` | `string` | Derived from `session.appName`. | +| `userId` | `string` | Derived from `session.userId`. | -### Execution Control +### Execution state -Flags and controls for managing invocation lifecycle: +| Property | Type | Description | +| -------------------- | --------------------------------- | ---------------------------------------------------------------------- | +| `agent` | `BaseAgent` | The agent currently executing. | +| `session` | `Session` | The current session with state and event history. | +| `userContent` | `Content \| undefined` | The original user message that started this invocation. | +| `endInvocation` | `boolean` | Set to `true` in a callback or tool to terminate the invocation early. | +| `runConfig` | `RunConfig \| undefined` | Runtime configuration including `maxLlmCalls`. | +| `contextCacheConfig` | `ContextCacheConfig \| undefined` | Context caching configuration if enabled. | -```typescript -endInvocation: boolean; -liveRequestQueue?: LiveRequestQueue; -activeStreamingTools?: Record; -runConfig?: RunConfig; -``` +## Methods -### Convenience Properties +### `createChildContext(agent)` -Derived properties for common access patterns: +Creates a new `InvocationContext` for a sub-agent. The child inherits the same `invocationId`, services, session, and flags. Its `branch` is extended with the sub-agent's name so that history isolation works correctly in parallel flows. ```typescript -get appName(): string; -get userId(): string; +createChildContext(agent: BaseAgent): InvocationContext ``` -## Key Methods +### `incrementLlmCallCount()` -### incrementLlmCallCount() - -Tracks and enforces LLM call limits: +Increments the internal LLM call counter and throws `LlmCallsLimitExceededError` if the `runConfig.maxLlmCalls` limit has been reached. The framework calls this automatically before each LLM request β€” you only need it if you are making LLM calls manually in a custom agent. ```typescript incrementLlmCallCount(): void ``` -Throws `LlmCallsLimitExceededError` if the configured limit is exceeded. - -### createChildContext() - -Creates a child context for sub-agent execution: - -```typescript -createChildContext(agent: BaseAgent): InvocationContext -``` - -This maintains the same invocation ID while updating the branch and agent references. - -## Agent Implementation Patterns +## Custom agent example -### Basic Agent Structure - -Most custom agents using InvocationContext follow this pattern: +Most developers never touch `InvocationContext` directly. The scenario where you do is when writing a custom `BaseAgent` subclass. The `runAsyncImpl` method receives the context and must yield `Event` objects: ```typescript import { BaseAgent, InvocationContext, Event } from "@iqai/adk"; -export class CustomAgent extends BaseAgent { +export class SummaryAgent extends BaseAgent { protected async *runAsyncImpl( - context: InvocationContext, + ctx: InvocationContext, ): AsyncGenerator { - // Access full framework capabilities - const session = context.session; - const artifactService = context.artifactService; - const memoryService = context.memoryService; - - // Your custom agent logic here - yield* this.processWithFullContext(context); - } - - private async *processWithFullContext( - context: InvocationContext, - ): AsyncGenerator { - // Use all available services and state - if (context.memoryService) { - const memories = await context.memoryService.searchMemory({ - query: "relevant context", - appName: context.appName, - userId: context.userId, + // Read the original user message + const userText = + ctx.userContent?.parts?.find(p => p.text)?.text ?? "(no input)"; + + // Optionally search memory if the service is configured + let memorySnippet = ""; + if (ctx.memoryService) { + const results = await ctx.memoryService.search({ + query: userText, + userId: ctx.userId, + appName: ctx.appName, }); - - // Process memories and generate events - } - - // Access session state - const currentState = context.session.state; - - // Generate appropriate events - const event = new Event({ - invocationId: context.invocationId, - author: this.name, - content: { - parts: [{ text: "Processing complete" }], - }, - }); - - yield event; - } -} -``` - -### Multi-Service Agent - -An agent that coordinates multiple services: - -```typescript -export class MultiServiceAgent extends BaseAgent { - protected async *runAsyncImpl( - context: InvocationContext, - ): AsyncGenerator { - try { - // Initialize processing state - const processingState = { - started: new Date().toISOString(), - invocationId: context.invocationId, - services: { - artifact: !!context.artifactService, - memory: !!context.memoryService, - session: !!context.sessionService, - }, - }; - - // Update session state - context.session.state.processing = processingState; - - // Process with available services - if (context.memoryService && context.artifactService) { - yield* this.processWithMemoryAndArtifacts(context); - } else if (context.memoryService) { - yield* this.processWithMemoryOnly(context); - } else { - yield* this.processBasic(context); + if (results.length > 0) { + memorySnippet = ` (related memory: ${results[0].memory.content.summary})`; } - } catch (error) { - // Handle errors with full context - yield this.createErrorEvent(context, error); } - } - - private async *processWithMemoryAndArtifacts( - context: InvocationContext, - ): AsyncGenerator { - // Search memory for context - const memoryResults = await context.memoryService!.searchMemory({ - query: "user preferences and history", - appName: context.appName, - userId: context.userId, - }); - - // Load relevant artifacts - const artifacts = await context.artifactService!.listArtifactKeys({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - }); - // Process and combine information - const combinedContext = { - memories: memoryResults.memories || [], - artifacts: artifacts, - sessionHistory: context.session.events?.length || 0, - }; - - // Generate comprehensive response yield new Event({ - invocationId: context.invocationId, - author: this.name, - content: { - parts: [ - { - text: `Found ${combinedContext.memories.length} relevant memories and ${combinedContext.artifacts.length} artifacts`, - }, - ], - }, - }); - } - - private createErrorEvent(context: InvocationContext, error: unknown): Event { - return new Event({ - invocationId: context.invocationId, + invocationId: ctx.invocationId, author: this.name, content: { - parts: [ - { - text: `Error in multi-service processing: ${ - error instanceof Error ? error.message : String(error) - }`, - }, - ], + parts: [{ text: `Summarising: "${userText}"${memorySnippet}` }], }, - errorCode: "MULTI_SERVICE_ERROR", - errorMessage: error instanceof Error ? error.message : String(error), }); } } ``` -## Service Management Patterns - -### Conditional Service Usage +## Delegating to sub-agents -Handle optional services gracefully: +When an orchestrating agent needs to hand off work, it creates a child context so the sub-agent runs with the correct branch path: ```typescript -async function processWithAvailableServices(context: InvocationContext) { - const results = { - sessionData: context.session.state, - artifacts: [] as string[], - memories: [] as any[], - processing: { - timestamp: new Date().toISOString(), - invocationId: context.invocationId, - }, - }; - - // Use artifact service if available - if (context.artifactService) { - try { - results.artifacts = await context.artifactService.listArtifactKeys({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - }); - } catch (error) { - console.warn("Artifact service error:", error); - } - } +import { BaseAgent, InvocationContext, Event } from "@iqai/adk"; - // Use memory service if available - if (context.memoryService) { - try { - const memoryResponse = await context.memoryService.searchMemory({ - query: "user context", - appName: context.appName, - userId: context.userId, - }); - results.memories = memoryResponse.memories || []; - } catch (error) { - console.warn("Memory service error:", error); - } +export class OrchestratorAgent extends BaseAgent { + constructor(private readonly researchAgent: BaseAgent) { + super({ name: "orchestrator", description: "Routes requests" }); } - return results; -} -``` - -## Branching and Child Contexts - -### Sub-Agent Delegation - -Use child contexts for sub-agent execution: - -```typescript -export class DelegatingAgent extends BaseAgent { protected async *runAsyncImpl( - context: InvocationContext, + ctx: InvocationContext, ): AsyncGenerator { - // Determine if delegation is needed - const userInput = context.userContent?.parts?.[0]?.text || ""; + const userText = ctx.userContent?.parts?.find(p => p.text)?.text ?? ""; - if (this.requiresSpecialization(userInput)) { - const specializedAgent = this.findSpecializedAgent(userInput); - - if (specializedAgent) { - // Create child context for specialized agent - const childContext = context.createChildContext(specializedAgent); - - // Update branch tracking - console.log( - `Delegating to ${specializedAgent.name}, branch: ${childContext.branch}`, - ); - - // Execute specialized agent - yield* specializedAgent.runAsync(childContext); - - // Post-delegation processing - yield this.createDelegationSummary(context, specializedAgent.name); - } + if (userText.toLowerCase().includes("research")) { + // Create a child context β€” same invocation ID, updated branch + const childCtx = ctx.createChildContext(this.researchAgent); + yield* this.researchAgent.runAsync(childCtx); } else { - // Handle directly - yield* this.handleDirectly(context); - } - } - - private requiresSpecialization(input: string): boolean { - return ( - input.includes("code") || - input.includes("math") || - input.includes("research") - ); - } - - private findSpecializedAgent(input: string): BaseAgent | null { - // Return appropriate specialized agent based on input - return null; // Implementation depends on your agent architecture - } - - private createDelegationSummary( - context: InvocationContext, - agentName: string, - ): Event { - return new Event({ - invocationId: context.invocationId, - author: this.name, - content: { - parts: [ - { - text: `Completed delegation to ${agentName} for specialized processing`, - }, - ], - }, - }); - } -} -``` - -### Branch-Aware Processing - -Handle branch context for complex agent trees: - -```typescript -function processBranchContext(context: InvocationContext) { - const branchInfo = { - current: context.branch || "root", - depth: context.branch?.split(".").length || 0, - parentAgents: context.branch?.split(".").slice(0, -1) || [], - currentAgent: context.agent.name, - }; - - // Update session with branch tracking - context.session.state.branchHistory = - context.session.state.branchHistory || []; - - context.session.state.branchHistory.push({ - branch: branchInfo.current, - agent: branchInfo.currentAgent, - timestamp: new Date().toISOString(), - invocationId: context.invocationId, - }); - - // Keep only recent branch history - context.session.state.branchHistory = - context.session.state.branchHistory.slice(-20); - - return branchInfo; -} -``` - -## Cost and Limit Management - -### LLM Call Tracking - -Monitor and enforce LLM usage limits: - -```typescript -export class CostAwareAgent extends BaseAgent { - protected async *runAsyncImpl( - context: InvocationContext, - ): AsyncGenerator { - try { - // Track LLM call - context.incrementLlmCallCount(); - - // Update cost tracking in state - const costTracking = context.session.state.costTracking || { - llmCalls: 0, - totalCost: 0, - }; - - costTracking.llmCalls += 1; - context.session.state.costTracking = costTracking; - - // Process with cost awareness - yield* this.processWithCostTracking(context); - } catch (error) { - if (error instanceof LlmCallsLimitExceededError) { - yield this.createLimitExceededEvent(context); - return; - } - throw error; - } - } - - private createLimitExceededEvent(context: InvocationContext): Event { - return new Event({ - invocationId: context.invocationId, - author: this.name, - content: { - parts: [ - { - text: "LLM call limit exceeded for this invocation. Please try again later.", - }, - ], - }, - errorCode: "LLM_LIMIT_EXCEEDED", - }); - } -} -``` - -## Advanced Patterns - -### State Management Across Services - -Coordinate state changes across all services: - -```typescript -async function synchronizeState( - context: InvocationContext, - updates: Record, -) { - const syncId = `sync_${Date.now()}`; - - try { - // Apply updates to session state - Object.assign(context.session.state, updates, { - lastSync: { - id: syncId, - timestamp: new Date().toISOString(), - invocationId: context.invocationId, - }, - }); - - // Persist session changes - await context.sessionService.updateSession(context.session); - - // Save state snapshot to artifacts if artifact service available - if (context.artifactService) { - await context.artifactService.saveArtifact({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - filename: `state_snapshot_${syncId}.json`, - artifact: { - text: JSON.stringify(context.session.state, null, 2), - }, - }); - } - - // Update memory with state changes if memory service available - if (context.memoryService && updates.userPreferences) { - // Example: Store user preferences in memory for future sessions - await context.memoryService.saveMemory({ - appName: context.appName, - userId: context.userId, - content: `User preferences updated: ${JSON.stringify( - updates.userPreferences, - )}`, - metadata: { - type: "user_preferences", - syncId: syncId, - }, + yield new Event({ + invocationId: ctx.invocationId, + author: this.name, + content: { parts: [{ text: "I can help with research requests." }] }, }); } - - return { success: true, syncId }; - } catch (error) { - // Handle synchronization failure - context.session.state.lastSyncError = { - syncId, - error: error instanceof Error ? error.message : String(error), - timestamp: new Date().toISOString(), - }; - - throw error; } } ``` -### Invocation Lifecycle Management +## Terminating an invocation early -Manage the complete invocation lifecycle: +Setting `endInvocation = true` on the context signals every part of the framework to stop after the current step completes. A callback or tool can do this to implement hard limits or graceful shutdowns: ```typescript -export class LifecycleAwareAgent extends BaseAgent { - protected async *runAsyncImpl( - context: InvocationContext, - ): AsyncGenerator { - // Initialize invocation - yield* this.initializeInvocation(context); - - try { - // Main processing - yield* this.processMain(context); +import { CallbackContext } from "@iqai/adk"; - // Check for early termination - if (context.endInvocation) { - yield* this.handleEarlyTermination(context); - return; - } - - // Finalize invocation - yield* this.finalizeInvocation(context); - } catch (error) { - yield* this.handleInvocationError(context, error); - } +async function safetyCallback(ctx: CallbackContext) { + const callCount = (ctx.state["callCount"] as number) ?? 0; + if (callCount > 50) { + ctx.invocationContext.endInvocation = true; + return undefined; } - - private async *initializeInvocation( - context: InvocationContext, - ): AsyncGenerator { - // Set up invocation tracking - context.session.state.currentInvocation = { - id: context.invocationId, - started: new Date().toISOString(), - agent: this.name, - branch: context.branch, - }; - - yield new Event({ - invocationId: context.invocationId, - author: this.name, - content: { - parts: [{ text: "Invocation initialized" }], - }, - }); - } - - private async *finalizeInvocation( - context: InvocationContext, - ): AsyncGenerator { - // Clean up and finalize - if (context.session.state.currentInvocation) { - context.session.state.currentInvocation.completed = - new Date().toISOString(); - } - - // Save final state if needed - if (context.artifactService) { - await context.artifactService.saveArtifact({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - filename: `invocation_${context.invocationId}_final.json`, - artifact: { - text: JSON.stringify( - { - invocationId: context.invocationId, - finalState: context.session.state, - completed: new Date().toISOString(), - }, - null, - 2, - ), - }, - }); - } - - yield new Event({ - invocationId: context.invocationId, - author: this.name, - content: { - parts: [{ text: "Invocation completed successfully" }], - }, - }); - } -} -``` - -## Best Practices - -### Service Error Handling - -```typescript -async function robustServiceOperation(context: InvocationContext) { - const results = { - services: { - session: { available: true, success: false }, - artifact: { available: !!context.artifactService, success: false }, - memory: { available: !!context.memoryService, success: false }, - }, - errors: [] as string[], - }; - - // Session service (always available) - try { - await context.sessionService.updateSession(context.session); - results.services.session.success = true; - } catch (error) { - results.errors.push( - `Session: ${error instanceof Error ? error.message : String(error)}`, - ); - } - - // Artifact service (optional) - if (context.artifactService) { - try { - await context.artifactService.listArtifactKeys({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - }); - results.services.artifact.success = true; - } catch (error) { - results.errors.push( - `Artifact: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - // Memory service (optional) - if (context.memoryService) { - try { - await context.memoryService.searchMemory({ - query: "test", - appName: context.appName, - userId: context.userId, - }); - results.services.memory.success = true; - } catch (error) { - results.errors.push( - `Memory: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - return results; + ctx.state["callCount"] = callCount + 1; + return undefined; } ``` -### Resource Cleanup - -```typescript -async function cleanupInvocationResources(context: InvocationContext) { - // Clean up temporary state - delete context.session.state.temp; - - // Clean up old tracking data - if (context.session.state.invocationHistory) { - // Keep only last 10 invocations - context.session.state.invocationHistory = - context.session.state.invocationHistory.slice(-10); - } - - // Clean up old artifacts if artifact service available - if (context.artifactService) { - const artifacts = await context.artifactService.listArtifactKeys({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - }); - - // Remove temp artifacts - const tempArtifacts = artifacts.filter(name => name.startsWith("temp_")); - for (const tempArtifact of tempArtifacts) { - try { - await context.artifactService.deleteArtifact({ - appName: context.appName, - userId: context.userId, - sessionId: context.session.id, - filename: tempArtifact, - }); - } catch (error) { - console.warn(`Failed to clean up ${tempArtifact}:`, error); - } - } - } - - // Update session with cleanup info - context.session.state.lastCleanup = { - invocationId: context.invocationId, - timestamp: new Date().toISOString(), - }; -} -``` - -## Related Contexts - -InvocationContext provides the foundation for all other context types: - -- **ReadonlyContext**: Provides safe read-only access to basic invocation information -- **CallbackContext**: Extends ReadonlyContext with state management capabilities -- **ToolContext**: Extends CallbackContext with memory search and enhanced tool features - -All other contexts can be derived from or work alongside InvocationContext in different execution scenarios. + + `endInvocation` is a shared flag β€” setting it in a child context also + terminates the parent, because child contexts reference the same flag object. + + +## Next steps + + + + + + + diff --git a/apps/docs/content/docs/framework/context/meta.json b/apps/docs/content/docs/framework/context/meta.json index c147c43f7..1c9f4a73e 100644 --- a/apps/docs/content/docs/framework/context/meta.json +++ b/apps/docs/content/docs/framework/context/meta.json @@ -1,6 +1,5 @@ { "pages": [ - "index", "readonly-context", "callback-context", "tool-context", diff --git a/apps/docs/content/docs/framework/context/readonly-context.mdx b/apps/docs/content/docs/framework/context/readonly-context.mdx index 60d9b62ec..af9d313f2 100644 --- a/apps/docs/content/docs/framework/context/readonly-context.mdx +++ b/apps/docs/content/docs/framework/context/readonly-context.mdx @@ -1,288 +1,151 @@ --- title: ReadonlyContext -description: Safe read-only access to basic invocation information for instruction providers and monitoring +description: Read-only access to invocation identity and session state β€” used in instruction providers and monitoring callbacks. --- +import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; -ReadonlyContext provides safe, read-only access to invocation information without modification capabilities. It's designed for security-first scenarios where you need to observe execution state but shouldn't alter it. - -## Overview - -ReadonlyContext is the base context class that provides essential invocation information in a read-only format. It's primarily used for instruction providers, template rendering, and monitoring scenarios where safety is paramount. +`ReadonlyContext` is the base context class in ADK-TS. It exposes everything your code needs to observe an invocation β€” who is running, what session it belongs to, and what the current state contains β€” without exposing any way to change that state. This makes it safe to pass to instruction providers, analytics hooks, and any other code that should read but never write. ```typescript -import { ReadonlyContext } from "@iqai/adk"; +import type { ReadonlyContext } from "@iqai/adk"; ``` ## Properties -### userContent +### `userContent` -The original user message that started the current invocation. +The original message that triggered this invocation. Undefined for system-initiated operations. ```typescript get userContent(): Content | undefined ``` - +### `invocationId` -Returns `undefined` if the invocation wasn't triggered by user content (e.g., -system-initiated operations). - - - -### invocationId - -Unique identifier for the current invocation, useful for tracking and logging. +A unique identifier for this invocation, generated at the start of each `runner.run()` call. Use it to correlate logs and events. ```typescript get invocationId(): string ``` -### agentName +### `agentName` -Name of the agent currently executing within this context. +The name of the agent currently executing within this context. ```typescript get agentName(): string ``` -### state +### `appName` -Read-only view of the current session state. +The application name configured in the runner. ```typescript -get state(): Readonly> +get appName(): string ``` -The state object is frozen to prevent accidental modifications and returns a snapshot of the current session state. - -## Common Use Cases +### `userId` -### Instruction Providers - -ReadonlyContext is perfect for generating dynamic instructions based on current execution state: +The user ID associated with the current session. ```typescript -import { LlmAgent, ReadonlyContext } from "@iqai/adk"; - -const dynamicInstruction = (ctx: ReadonlyContext): string => { - const userName = ctx.state.user?.name || "User"; - const sessionCount = ctx.state.sessionCount || 0; - - return `You are helping ${userName}. This is session #${sessionCount + 1}. - Invocation ID: ${ctx.invocationId}`; -}; - -const agent = new LlmAgent({ - name: "adaptive-agent", - description: "An agent with dynamic instructions", - model: "gemini-2.5-flash", - instruction: dynamicInstruction, -}); +get userId(): string ``` -### Template Rendering +### `sessionId` -Use ReadonlyContext to render templates based on current state: +The ID of the current session. ```typescript -function renderTemplate(ctx: ReadonlyContext): string { - const template = ` -Hello! I'm ${ctx.agentName}. -Current session state: ${JSON.stringify(ctx.state, null, 2)} -Processing invocation: ${ctx.invocationId} - `; - - return template.trim(); -} +get sessionId(): string ``` -### Monitoring and Logging +### `state` -ReadonlyContext provides safe access for monitoring operations: +A frozen snapshot of the current session state. Because the object is frozen, any attempt to write to it will throw a `TypeError` at runtime β€” a safety net that makes instruction providers safe by design. ```typescript -function logInvocationInfo(ctx: ReadonlyContext): void { - console.log(`[${ctx.invocationId}] Agent: ${ctx.agentName}`); - console.log(`[${ctx.invocationId}] State keys:`, Object.keys(ctx.state)); - - if (ctx.userContent) { - const textParts = ctx.userContent.parts - ?.filter(part => part.text) - .map(part => part.text); - console.log(`[${ctx.invocationId}] User message:`, textParts?.join(" ")); - } -} -``` - -## Security Benefits - -### Immutable State Access - -ReadonlyContext ensures that state cannot be accidentally modified: - -```typescript -function safeStateAccess(ctx: ReadonlyContext) { - // This works - reading state - const userPreferences = ctx.state.preferences; - - // This would throw an error - state is frozen - // ctx.state.newProperty = "value"; // TypeError: Cannot add property -} -``` - -### Prevention of Side Effects - -By providing only read access, ReadonlyContext prevents unintended side effects in observation code: - -```typescript -// Safe - no risk of modifying execution state -const analyzeState = (ctx: ReadonlyContext) => { - return { - agentName: ctx.agentName, - stateSize: Object.keys(ctx.state).length, - hasUserContent: !!ctx.userContent, - invocationId: ctx.invocationId, - }; -}; +get state(): Readonly> ``` -## Advanced Patterns - -### Conditional Instruction Generation - -Generate instructions based on state conditions: - -```typescript -const conditionalInstruction = (ctx: ReadonlyContext): string => { - const isFirstTime = !ctx.state.previousInteractions; - const userLevel = ctx.state.userLevel || "beginner"; - - if (isFirstTime) { - return "Welcome! I'll provide detailed explanations as this is your first interaction."; - } - - if (userLevel === "expert") { - return "I'll provide concise, technical responses suitable for your expertise level."; - } + + To write to state, use `CallbackContext` or `ToolContext`. Both expose a + mutable `State` object in place of this frozen snapshot. + - return "I'll adapt my responses based on our previous interactions."; -}; -``` +## Instruction providers -### State Validation for Instructions +The most common use of `ReadonlyContext` is inside instruction provider functions. ADK-TS calls your function before each LLM request and uses the returned string as the system instruction. The function receives a `ReadonlyContext` so it can tailor the instructions to the current session without accidentally mutating anything. -Validate state before generating instructions: +This example greets the user by name, falls back gracefully if the name hasn't been set yet, and includes the invocation ID for traceability: ```typescript -const validatedInstruction = (ctx: ReadonlyContext): string => { - // Validate required state properties - if (!ctx.state.userProfile) { - return "Please set up your user profile first by sharing your preferences."; - } +import { LlmAgent } from "@iqai/adk"; +import type { ReadonlyContext } from "@iqai/adk"; - if (!ctx.state.currentTask) { - return "I'm ready to help! What would you like to work on?"; - } - - const task = ctx.state.currentTask; - return `Continuing with your ${task.type} task: "${task.name}". How can I assist?`; -}; +const agent = new LlmAgent({ + name: "greeter", + description: "Greets users by name using session state", + model: "gemini-2.5-flash", + instruction: (ctx: ReadonlyContext) => { + const name = ctx.state["user:name"] ?? "there"; + const taskCount = (ctx.state["user:taskCount"] as number) ?? 0; + + return [ + `You are a helpful assistant. The user's name is ${name}.`, + taskCount > 0 + ? `They have completed ${taskCount} tasks so far.` + : "This is their first session.", + ].join(" "); + }, +}); ``` -### Multi-Language Support +## Conditional instructions -Use state information for localization: +Use the context to branch instruction content based on state. Each branch should return a complete, self-contained instruction string: ```typescript -const localizedInstruction = (ctx: ReadonlyContext): string => { - const language = ctx.state.userLanguage || "en"; - const userName = ctx.state.user?.name || "User"; - - const greetings = { - en: `Hello ${userName}! How can I help you today?`, - es: `Β‘Hola ${userName}! ΒΏCΓ³mo puedo ayudarte hoy?`, - fr: `Bonjour ${userName}! Comment puis-je vous aider aujourd'hui?`, - }; - - return greetings[language] || greetings.en; -}; -``` +import type { ReadonlyContext } from "@iqai/adk"; -## Best Practices +function buildInstruction(ctx: ReadonlyContext): string { + const role = ctx.state["user:role"] as string | undefined; -### State Access Patterns - -```typescript -// Good - Safe state access with fallbacks -const safeAccess = (ctx: ReadonlyContext) => { - const userPrefs = ctx.state.preferences || {}; - const theme = userPrefs.theme || "default"; - return theme; -}; - -// Good - Check for property existence -const conditionalAccess = (ctx: ReadonlyContext) => { - if ("activeTask" in ctx.state) { - return `Working on: ${ctx.state.activeTask.name}`; + if (role === "admin") { + return "You are an admin assistant. You may discuss internal configuration and user management."; } - return "No active task"; -}; -``` - -### Error Handling -```typescript -const robustInstruction = (ctx: ReadonlyContext): string => { - try { - const config = ctx.state.agentConfig; - if (!config) { - return "I'm ready to help! Please let me know what you'd like to do."; - } - - return `I'm configured for ${config.mode} mode. ${config.description}`; - } catch (error) { - console.warn("Error accessing state in instruction provider:", error); - return "I'm here to help! What can I do for you?"; + if (role === "premium") { + return "You are a premium assistant with access to advanced features. Help the user leverage them fully."; } -}; -``` -### Performance Considerations - -```typescript -// Good - Minimal state access -const efficientInstruction = (ctx: ReadonlyContext): string => { - const { userLevel, currentMode } = ctx.state; - return `Mode: ${currentMode || "standard"}, Level: ${ - userLevel || "intermediate" - }`; -}; - -// Avoid - Expensive operations in instruction providers -const inefficientInstruction = (ctx: ReadonlyContext): string => { - // Don't do complex processing in instruction providers - const allStateKeys = Object.keys(ctx.state); - const sortedKeys = allStateKeys.sort(); - const summary = sortedKeys.map(key => `${key}: ${ctx.state[key]}`).join(", "); - return `Full state: ${summary}`; -}; + return "You are a helpful assistant. Answer questions clearly and concisely."; +} ``` -## Limitations - -ReadonlyContext intentionally provides limited functionality: - -- **No State Modification**: Cannot change session state -- **No Service Access**: No access to artifact, memory, or session services -- **No Event Actions**: Cannot trigger actions or modify execution flow - -For scenarios requiring these capabilities, use CallbackContext, ToolContext, or InvocationContext as appropriate. - -## Related Contexts - -- **CallbackContext**: When you need to modify state or manage artifacts -- **ToolContext**: For tool implementations requiring memory search and enhanced features -- **InvocationContext**: For complete framework access in agent implementations +## Next steps + + + + + + + diff --git a/apps/docs/content/docs/framework/context/tool-context.mdx b/apps/docs/content/docs/framework/context/tool-context.mdx index cf9fd57df..edb00290d 100644 --- a/apps/docs/content/docs/framework/context/tool-context.mdx +++ b/apps/docs/content/docs/framework/context/tool-context.mdx @@ -1,589 +1,183 @@ --- title: ToolContext -description: Enhanced context for tool execution with memory search and artifact management +description: Memory search, artifact listing, and flow control β€” everything a tool needs to interact with the agent's runtime. --- +import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; -ToolContext extends CallbackContext with additional capabilities specifically designed for tool implementations. It provides memory search, artifact listing, and enhanced event action management. - -## Overview - -ToolContext is the most capable context available for tool implementations, providing access to memory services, enhanced artifact operations, and function call tracking. +`ToolContext` extends `CallbackContext` with capabilities specific to tool execution: searching long-term memory, listing session artifacts, and triggering flow control actions. ADK-TS injects it automatically into every tool call β€” you never construct it yourself. ```typescript import { ToolContext } from "@iqai/adk"; ``` -## Key Features - -- **All CallbackContext Features**: Mutable state, artifact load/save operations -- **Memory Search**: Query long-term memory stores and knowledge bases -- **Artifact Listing**: Enumerate available artifacts in the session -- **Function Call Tracking**: Link tool executions to originating LLM function calls -- **Enhanced Event Actions**: Direct access to action management via `actions` property +For guidance on how to receive `ToolContext` in different tool styles (`createTool`, function tools, class-based tools), see [ToolContext in Tools](/docs/framework/tools/tool-context). -## Properties +## Added properties -### functionCallId +### `functionCallId` -Unique identifier linking this tool execution to the LLM function call: +The identifier the LLM assigned to this particular function call. ADK-TS generates one if the model didn't supply it. Use it when you need to correlate a tool response with the triggering call in logs or custom event processing. ```typescript functionCallId?: string ``` -This ID helps correlate tool responses with their originating function calls for debugging and event tracking. +### `actions` -### actions - -Direct access to event actions for flow control: +A shorthand alias for `eventActions`. Use it to set flow control flags such as `transferToAgent` or `escalate`. ```typescript get actions(): EventActions ``` -Provides convenient access to `eventActions` with a shorter property name. - -## Enhanced Methods - -### listArtifacts() +### `session` -Lists all artifact filenames attached to the current session: +The current `Session` object, giving direct access to `session.id`, `session.state`, and `session.events`. ```typescript -async listArtifacts(): Promise -``` - -```typescript -async function examineSessionArtifacts(toolContext: ToolContext) { - const artifacts = await toolContext.listArtifacts(); - - console.log(`Found ${artifacts.length} artifacts:`); - artifacts.forEach(filename => { - console.log(`- ${filename}`); - }); - - // Save artifact inventory to state - toolContext.state.artifactInventory = { - count: artifacts.length, - files: artifacts, - lastChecked: new Date().toISOString(), - }; - - return artifacts; -} +get session(): Session ``` -### searchMemory() +### `sessionService` -Search the memory service for relevant information: +The `BaseSessionService` instance. Provides `createSession`, `getSession`, `listSessions`, `deleteSession`, and `endSession`. ```typescript -async searchMemory(query: string): Promise +get sessionService(): BaseSessionService ``` -```typescript -async function findRelevantContext( - toolContext: ToolContext, - userQuery: string, -) { - try { - const memoryResults = await toolContext.searchMemory(userQuery); - - // Process search results - const relevantMemories = memoryResults.memories || []; - - // Update state with findings - toolContext.state.lastMemorySearch = { - query: userQuery, - resultCount: relevantMemories.length, - timestamp: new Date().toISOString(), - }; - - return relevantMemories; - } catch (error) { - console.error("Memory search failed:", error); - toolContext.state.memorySearchError = { - query: userQuery, - error: error instanceof Error ? error.message : String(error), - timestamp: new Date().toISOString(), - }; - return []; - } -} -``` +### `memoryService` - - -Memory search requires a memory service to be configured in the runner or -agent setup. - - - -## Tool Implementation Patterns - -### Basic Tool Structure - -Most tools using ToolContext follow this pattern: +The `MemoryService` instance, or `undefined` if no memory service was configured in the runner. Always guard against `undefined` before calling methods on it. ```typescript -import { BaseTool, ToolContext } from "@iqai/adk"; - -export class MyTool extends BaseTool { - constructor() { - super({ - name: "my_tool", - description: "Example tool using ToolContext", - }); - } - - async runAsync(args: any, context: ToolContext): Promise { - // Your tool implementation here - return await this.processWithContext(args, context); - } - - private async processWithContext(args: any, context: ToolContext) { - // Use context features - const artifacts = await context.listArtifacts(); - const memories = await context.searchMemory(args.query); - - // Update state - context.state.toolExecution = { - toolName: this.name, - functionCallId: context.functionCallId, - timestamp: new Date().toISOString(), - }; - - return { success: true }; - } -} +get memoryService(): MemoryService | undefined ``` -### Memory-Enhanced Tool +## Methods + +### `listArtifacts()` -Create tools that leverage memory for enhanced responses: +Returns the filenames of every artifact currently saved in this session. Use it when your tool needs to discover what files are available before deciding which to load. ```typescript -import { FunctionTool, ToolContext } from "@iqai/adk"; - -async function researchTopic( - params: { topic: string; depth: "basic" | "detailed" }, - toolContext: ToolContext, -) { - // Search memory for existing knowledge - const existingKnowledge = await toolContext.searchMemory(params.topic); - - // Check for cached research - const cacheKey = `research_${params.topic}_${params.depth}`; - const cachedResearch = await toolContext.loadArtifact(`${cacheKey}.json`); - - if (cachedResearch) { - toolContext.state.cacheHit = true; - return JSON.parse(cachedResearch.text || "{}"); - } - - // Perform new research combining memory and external sources - const research = { - topic: params.topic, - depth: params.depth, - existingKnowledge: existingKnowledge.memories || [], - newFindings: [], // Your research logic here - timestamp: new Date().toISOString(), - }; - - // Cache the results - await toolContext.saveArtifact(`${cacheKey}.json`, { - text: JSON.stringify(research, null, 2), - }); - - // Update search context in state - toolContext.state.lastResearch = { - topic: params.topic, - knowledgeFound: existingKnowledge.memories?.length || 0, - cached: false, - }; - - return research; -} - -const researchTool = new FunctionTool(researchTopic, { - name: "research_topic", - description: "Research a topic using memory and external sources", -}); +async listArtifacts(): Promise ``` -### Artifact Processing Tool - -Tools that work with multiple artifacts: - ```typescript -async function processDocuments( - params: { operation: "summarize" | "analyze" | "compare" }, - toolContext: ToolContext, -) { - // Get all available documents - const allArtifacts = await toolContext.listArtifacts(); - const documents = allArtifacts.filter( - name => - name.endsWith(".txt") || name.endsWith(".md") || name.endsWith(".json"), - ); - - if (documents.length === 0) { - return { error: "No documents found to process" }; - } - - const results = []; - - for (const docName of documents) { - const document = await toolContext.loadArtifact(docName); - if (!document?.text) continue; - - let result; - switch (params.operation) { - case "summarize": - result = { summary: document.text.substring(0, 200) + "..." }; - break; - case "analyze": - result = { - wordCount: document.text.split(" ").length, - paragraphs: document.text.split("\n\n").length, - }; - break; - case "compare": - // Compare with memory knowledge - const related = await toolContext.searchMemory( - document.text.substring(0, 100), - ); - result = { relatedMemories: related.memories?.length || 0 }; - break; - } - - results.push({ - document: docName, - result: result, - }); - } - - // Save processing results - const reportName = `processing_report_${Date.now()}.json`; - await toolContext.saveArtifact(reportName, { - text: JSON.stringify( - { - operation: params.operation, - processedDocuments: results, - timestamp: new Date().toISOString(), - }, - null, - 2, - ), - }); - - // Update state - toolContext.state.lastProcessing = { - operation: params.operation, - documentsProcessed: results.length, - reportFile: reportName, - }; - - return { - operation: params.operation, - processedCount: results.length, - results: results, - reportFile: reportName, - }; -} +import { createTool, ToolContext } from "@iqai/adk"; +import { z } from "zod"; + +const listFilesTool = createTool({ + name: "list_files", + description: "List all files available in the session", + schema: z.object({}), + fn: async (_args, ctx: ToolContext) => { + const files = await ctx.listArtifacts(); + return { files, count: files.length }; + }, +}); ``` -## Event Action Management + + Throws if no artifact service is configured. Check that you've called + `.withArtifactService(...)` on the agent builder. + -### Flow Control Actions +### `searchMemory()` -ToolContext provides direct access to flow control through the `actions` property: +Searches the long-term memory store for records relevant to the query. Returns an array of `MemorySearchResult` objects, each with a `memory` record and a numeric `score` (0–1, higher is better). ```typescript -async function smartExitTool( - params: { condition: string }, - toolContext: ToolContext, -) { - // Check condition against memory and state - const memoryCheck = await toolContext.searchMemory(params.condition); - const stateCheck = toolContext.state.exitConditions?.[params.condition]; - - if (memoryCheck.memories?.length > 0 || stateCheck) { - // Exit the current loop/flow - toolContext.actions.escalate = true; - - return { - action: "exit", - reason: "Condition met", - condition: params.condition, - }; - } - - return { - action: "continue", - condition: params.condition, - status: "not_met", - }; -} +async searchMemory(query: string): Promise ``` -### Agent Transfer - -Tools can transfer control to other agents: - ```typescript -async function intelligentTransfer( - params: { userQuery: string }, - toolContext: ToolContext, -) { - // Search memory for best agent for this query - const agentSuggestions = await toolContext.searchMemory( - `agent for ${params.userQuery}`, - ); - - // Analyze current state - const currentAgent = toolContext.agentName; - const userHistory = toolContext.state.userHistory || []; - - // Determine best agent based on memory and context - let targetAgent = "general_assistant"; // default - - if (params.userQuery.includes("code")) { - targetAgent = "code_assistant"; - } else if (params.userQuery.includes("research")) { - targetAgent = "research_assistant"; - } - - // Transfer to the determined agent - toolContext.actions.transferToAgent = targetAgent; - - // Update state with transfer info - toolContext.state.lastTransfer = { - from: currentAgent, - to: targetAgent, - reason: params.userQuery, - timestamp: new Date().toISOString(), - }; - - return { - action: "transfer", - fromAgent: currentAgent, - toAgent: targetAgent, - reason: "Specialized agent better suited for this query", - }; -} -``` - -## Advanced Use Cases +import { createTool, ToolContext } from "@iqai/adk"; +import type { MemorySearchResult } from "@iqai/adk"; +import { z } from "zod"; -### Multi-Source Information Synthesis +const recallTool = createTool({ + name: "recall", + description: "Search the user's long-term memory", + schema: z.object({ query: z.string() }), + fn: async ({ query }, ctx: ToolContext) => { + const results: MemorySearchResult[] = await ctx.searchMemory(query); -Combine memory, artifacts, and state for comprehensive responses: - -```typescript -async function synthesizeInformation( - params: { topic: string }, - toolContext: ToolContext, -) { - // Gather information from all sources - const [memories, artifacts, state] = await Promise.all([ - toolContext.searchMemory(params.topic), - toolContext.listArtifacts(), - Promise.resolve(toolContext.state), - ]); - - // Load relevant artifacts - const relevantArtifacts = artifacts.filter(name => - name.toLowerCase().includes(params.topic.toLowerCase()), - ); - - const artifactContents = await Promise.all( - relevantArtifacts.map(async name => ({ - name, - content: await toolContext.loadArtifact(name), - })), - ); - - // Synthesize information - const synthesis = { - topic: params.topic, - sources: { - memories: memories.memories?.length || 0, - artifacts: artifactContents.length, - stateReferences: Object.keys(state).filter(key => - key.toLowerCase().includes(params.topic.toLowerCase()), - ).length, - }, - summary: `Found information about ${params.topic} from multiple sources`, - details: { - memoryInsights: memories.memories?.slice(0, 3) || [], - relevantFiles: relevantArtifacts, - stateData: state[params.topic] || null, - }, - }; - - // Save comprehensive report - await toolContext.saveArtifact( - `synthesis_${params.topic}_${Date.now()}.json`, - { - text: JSON.stringify(synthesis, null, 2), - }, - ); - - return synthesis; -} + return results.map(r => ({ + score: r.score, + content: r.memory.content, + })); + }, +}); ``` -### Context-Aware Decision Making - -Use all available context for intelligent decisions: + + Throws if no memory service is configured. You can also check + `ctx.memoryService` before calling to avoid the throw in optional scenarios. + -```typescript -async function makeContextualDecision( - params: { decision: string; options: string[] }, - toolContext: ToolContext, -) { - // Gather contextual information - const userHistory = toolContext.state.userHistory || []; - const recentChoices = toolContext.state.recentChoices || []; - const memoryGuidance = await toolContext.searchMemory( - `decision ${params.decision} options ${params.options.join(" ")}`, - ); - - // Analyze patterns - const patterns = { - userPreference: analyzeUserPreferences(userHistory, params.options), - pastChoices: recentChoices.slice(-5), - memoryInsights: memoryGuidance.memories || [], - }; - - // Make intelligent choice - const recommendedOption = patterns.userPreference || params.options[0]; - - // Update decision history - toolContext.state.recentChoices = [ - ...recentChoices.slice(-9), // Keep last 9 - { - decision: params.decision, - recommended: recommendedOption, - timestamp: new Date().toISOString(), - functionCallId: toolContext.functionCallId, - }, - ]; - - return { - decision: params.decision, - recommended: recommendedOption, - reasoning: patterns, - confidence: calculateConfidence(patterns), - }; -} - -function analyzeUserPreferences( - history: any[], - options: string[], -): string | null { - // Analyze user history to determine preferences - // Implementation depends on your specific needs - return null; -} - -function calculateConfidence(patterns: any): number { - // Calculate confidence score based on available patterns - return 0.7; // Placeholder -} -``` +## Flow control with `actions` -## Best Practices +The `actions` property exposes the `EventActions` for this tool call. Setting flags on it signals the framework to change execution flow after the tool returns. -### Error Handling +Transfer to another agent: ```typescript -async function robustToolExecution(params: any, toolContext: ToolContext) { - try { - // Attempt operations with fallbacks - let result; - - try { - const memories = await toolContext.searchMemory(params.query); - result = processWithMemory(memories); - } catch (memoryError) { - console.warn( - "Memory search failed, using local processing:", - memoryError, - ); - result = processLocally(params); - } - - // Save successful result - await toolContext.saveArtifact("operation_result.json", { - text: JSON.stringify(result, null, 2), - }); - - return result; - } catch (error) { - // Log error and update state - toolContext.state.lastError = { - tool: "robustToolExecution", - error: error instanceof Error ? error.message : String(error), - timestamp: new Date().toISOString(), - functionCallId: toolContext.functionCallId, - }; - - return { error: "Operation failed", details: String(error) }; - } -} - -function processWithMemory(memories: any) { - // Implementation with memory results - return { source: "memory", data: memories }; -} - -function processLocally(params: any) { - // Fallback local processing - return { source: "local", data: params }; -} +import { createTool, ToolContext } from "@iqai/adk"; +import { z } from "zod"; + +const routeTool = createTool({ + name: "route_to_billing", + description: "Transfer the conversation to the billing agent", + schema: z.object({}), + fn: (_args, ctx: ToolContext) => { + ctx.actions.transferToAgent = "billing_agent"; + return { status: "transferring" }; + }, +}); ``` -### Resource Optimization +Escalate out of the current loop: ```typescript -async function optimizedToolExecution(params: any, toolContext: ToolContext) { - // Check cache first - const cacheKey = `tool_cache_${JSON.stringify(params)}`; - const cached = await toolContext.loadArtifact(cacheKey); - - if (cached) { - toolContext.state.cacheHits = (toolContext.state.cacheHits || 0) + 1; - return JSON.parse(cached.text || "{}"); - } - - // Batch operations when possible - const [artifacts, memories] = await Promise.all([ - toolContext.listArtifacts(), - toolContext.searchMemory(params.query || ""), - ]); - - // Process efficiently - const result = { - artifacts: artifacts.length, - memories: memories.memories?.length || 0, - processed: true, - }; - - // Cache result - await toolContext.saveArtifact(cacheKey, { - text: JSON.stringify(result, null, 2), - }); - - return result; -} -``` +import { createTool, ToolContext } from "@iqai/adk"; +import { z } from "zod"; -## Related Contexts +const escalateTool = createTool({ + name: "escalate", + description: "Escalate to a human agent", + schema: z.object({ reason: z.string() }), + fn: ({ reason }, ctx: ToolContext) => { + ctx.actions.escalate = true; + return { escalated: true, reason }; + }, +}); +``` -- **CallbackContext**: Base class providing state management and artifact operations -- **ReadonlyContext**: For read-only access without modification capabilities -- **InvocationContext**: For complete framework access in agent implementations +## Next steps + + + + + + +