From 4dcdba94518459ab27ce0431975cae4349b2ffb7 Mon Sep 17 00:00:00 2001 From: timonwa Date: Wed, 29 Apr 2026 18:23:41 +0100 Subject: [PATCH 1/4] Rewrite artifacts documentation section for clarity and correctness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.mdx: rewritten as a focused overview with Mermaid flow diagram, quick start, and navigation cards - runner-configuration.mdx: clear wiring instructions for AgentBuilder and Runner direct, auto-save explanation, file upload example - context-integration.mdx: explains CallbackContext vs ToolContext, tabbed examples for before/after agent and model callbacks, tool example with listArtifacts - scoping-and-versioning.mdx: explains why to choose each scope, fixes undefined variables in deletion examples, adds listVersions context - service-implementations.mdx: comparison table with warning against InMemory in prod, full custom service interface, fixes null→undefined in troubleshooting - best-practices.mdx: fixes Part|null→Part|undefined return type, adds sequential vs parallel batch example, concrete deletion examples with usage - recipes.mdx: adds opening paragraph and scenario context for each recipe - troubleshooting.mdx: adds cause column to issues table, fixes wiring check to use AgentBuilder, adds GCS grant command Co-Authored-By: Claude Sonnet 4.6 --- .../framework/artifacts/best-practices.mdx | 820 +++--------------- .../artifacts/context-integration.mdx | 483 +++++------ .../docs/framework/artifacts/index.mdx | 509 +---------- .../docs/framework/artifacts/meta.json | 1 - .../docs/framework/artifacts/recipes.mdx | 120 ++- .../artifacts/runner-configuration.mdx | 116 ++- .../artifacts/scoping-and-versioning.mdx | 111 ++- .../artifacts/service-implementations.mdx | 424 ++++----- .../framework/artifacts/troubleshooting.mdx | 106 ++- 9 files changed, 875 insertions(+), 1815 deletions(-) diff --git a/apps/docs/content/docs/framework/artifacts/best-practices.mdx b/apps/docs/content/docs/framework/artifacts/best-practices.mdx index 85b2cec1e..7a8723ac4 100644 --- a/apps/docs/content/docs/framework/artifacts/best-practices.mdx +++ b/apps/docs/content/docs/framework/artifacts/best-practices.mdx @@ -1,28 +1,23 @@ --- -title: Best Practices for Artifact Management -description: Performance, security, versioning, and production considerations for artifact management +title: Best Practices +description: Caching, batching, validation, cleanup, and naming conventions for artifact management in ADK-TS. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Cards, Card } from "fumadocs-ui/components/card"; -This guide covers essential best practices for using artifacts in production environments, focusing on performance optimization, security considerations, and operational excellence. +This page covers patterns that prevent common mistakes when working with artifacts at scale — from avoiding unnecessary service calls to safely cleaning up files after processing. - +## Cache frequently read artifacts -For general ADK-TS best practices, see the [Best Practices -Guide](/docs/framework/guides/best-practices). - - - -## Performance Optimization - -### Caching Strategies - -Keep a small in-process cache for hot artifacts. +Every `loadArtifact` call hits the backing service. For GCS this means a network round-trip on every callback or tool invocation. If the same artifact is read many times per session and doesn't change often, wrap it in an in-process cache with a TTL: ```typescript +import type { BaseArtifactService } from "@iqai/adk"; +import type { Part } from "@google/genai"; + const cache = new Map(); -const TTL_MS = 5 * 60 * 1000; +const TTL_MS = 5 * 60 * 1000; // 5 minutes async function loadWithCache( svc: BaseArtifactService, @@ -33,52 +28,53 @@ async function loadWithCache( filename: string; version?: number; }, -): Promise { - const k = `${key.appName}:${key.userId}:${key.sessionId}:${key.filename}:${ - key.version ?? "latest" - }`; +): Promise { + const k = `${key.appName}:${key.userId}:${key.sessionId}:${key.filename}:${key.version ?? "latest"}`; const hit = cache.get(k); if (hit && Date.now() - hit.ts < TTL_MS) return hit.part; + const part = await svc.loadArtifact(key); if (part) cache.set(k, { part, ts: Date.now() }); return part; } ``` -### Batch Operations +This is most useful with `GcsArtifactService`. `InMemoryArtifactService` has no network overhead and doesn't need caching. + +## Batch saves with Promise.all + +Awaiting saves one at a time blocks each call until the previous one completes. When you need to save multiple artifacts in a single callback or tool, run them in parallel with `Promise.all`: ```typescript +// ❌ Sequential — each save blocks the next +await ctx.saveArtifact("summary.txt", { text: "..." }); +await ctx.saveArtifact("metadata.json", { + inlineData: { data: metaB64, mimeType: "application/json" }, +}); +await ctx.saveArtifact("chart.png", { + inlineData: { data: chartB64, mimeType: "image/png" }, +}); + +// ✅ Parallel — all three saves run at the same time await Promise.all([ - svc.saveArtifact({ - /* args1 */ - }), - svc.saveArtifact({ - /* args2 */ + ctx.saveArtifact("summary.txt", { text: "..." }), + ctx.saveArtifact("metadata.json", { + inlineData: { data: metaB64, mimeType: "application/json" }, }), - svc.saveArtifact({ - /* args3 */ + ctx.saveArtifact("chart.png", { + inlineData: { data: chartB64, mimeType: "image/png" }, }), ]); ``` -### Size Optimization +## Validate before saving -```typescript -// Compress text/JSON before saving -const json = JSON.stringify(someData); -const compact = json.replace(/\s+/g, " "); - -// Validate size (default 10MB) -const ok = - Buffer.from(part.inlineData.data, "base64").length <= 10 * 1024 * 1024; -``` - -## Security and Access Control - -### Data Validation +When the content or filename comes from user input, validate it before calling `saveArtifact`. Without checks, a user can pass unsupported MIME types, very large files that exhaust memory or storage, or filenames with special characters that break downstream tooling. ```typescript -const ALLOWED = new Set([ +import type { Part } from "@google/genai"; + +const ALLOWED_MIME = new Set([ "text/plain", "text/csv", "application/json", @@ -86,693 +82,157 @@ const ALLOWED = new Set([ "image/jpeg", "application/pdf", ]); -const MAX = 50 * 1024 * 1024; - -function validatePart(filename: string, part: Part) { - const errors: string[] = [], - warnings: string[] = []; - const mime = part.inlineData.mimeType; - const size = Buffer.from(part.inlineData.data, "base64").length; - if (!ALLOWED.has(mime)) errors.push(`Unsupported MIME: ${mime}`); - if (size > MAX) errors.push(`File too large: ${size}`); - if (!/^[a-zA-Z0-9._-]+$/.test(filename.replace("user:", ""))) - errors.push("Invalid filename"); - if (mime === "application/json") { - try { - JSON.parse(Buffer.from(part.inlineData.data, "base64").toString()); - } catch { - errors.push("Invalid JSON"); - } - } - if (size === 0) warnings.push("Empty file"); - return { isValid: errors.length === 0, errors, warnings, size }; -} -``` - -### Access Control Patterns - -Implement proper access controls: +const MAX_BYTES = 50 * 1024 * 1024; // 50 MB -```typescript -class SecureArtifactService { - constructor( - private baseService: BaseArtifactService, - private authService: AuthService - ) {} - - async saveArtifact( - args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - artifact: Part; - }, - requestingUserId: string, - permissions: string[] - ): Promise { - // Verify user can save artifacts - if (!permissions.includes('artifact:write')) { - throw new Error('Insufficient permissions to save artifacts'); - } - - // Verify user can save to this location - if (args.userId !== requestingUserId && !permissions.includes('artifact:admin')) { - throw new Error('Cannot save artifacts for other users'); - } - - // Validate user namespace access - if (args.filename.startsWith('user:') && args.userId !== requestingUserId) { - throw new Error('Cannot save to user namespace of other users'); - } - - // Apply rate limiting - await this.checkRateLimit(requestingUserId, 'save'); - - return this.baseService.saveArtifact(args); - } - - async loadArtifact( - args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - version?: number; - }, - requestingUserId: string, - permissions: string[] - ): Promise { - // Verify user can read artifacts - if (!permissions.includes('artifact:read')) - throw new Error('Insufficient permissions to read artifacts'); - } - - // Check if user can access this data - const canAccess = - args.userId === requestingUserId || - permissions.includes('artifact:admin') || - this.isSharedArtifact(args.filename); - - if (!canAccess) { - throw new Error('Access denied to artifact'); - } - - return this.baseService.loadArtifact(args); - } - - private async checkRateLimit(userId: string, operation: string): Promise { - // Implement rate limiting logic - const key = `ratelimit:${userId}:${operation}`; - const current = await this.getRateLimitCount(key); +function validatePart( + filename: string, + part: Part, +): { ok: boolean; error?: string } { + // Only allow safe filename characters (strip user: prefix before checking) + if (!/^[a-zA-Z0-9._-]+$/.test(filename.replace("user:", ""))) + return { ok: false, error: "Invalid filename characters" }; - if (current > this.getRateLimit(operation)) { - throw new Error('Rate limit exceeded'); - } + // text and fileData parts have no binary payload to size-check + if (!part.inlineData) return { ok: true }; - await this.incrementRateLimit(key); - } + const { mimeType, data } = part.inlineData; + if (!ALLOWED_MIME.has(mimeType)) + return { ok: false, error: `Unsupported MIME type: ${mimeType}` }; - private isSharedArtifact(filename: string): boolean { - // Define shared artifact patterns - return filename.startsWith('shared:') || filename.startsWith('public:'); - } - - private getRateLimit(operation: string): number { - const limits = { - save: 100, // 100 saves per hour - load: 1000 // 1000 loads per hour + const size = Buffer.from(data, "base64").length; + if (size > MAX_BYTES) + return { + ok: false, + error: `File too large: ${size} bytes (max ${MAX_BYTES})`, }; - return limits[operation] || 10; - } - - private async getRateLimitCount(key: string): Promise { - // Implementation depends on your rate limiting store (Redis, etc.) - return 0; - } - private async incrementRateLimit(key: string): Promise { - // Implementation depends on your rate limiting store - } + return { ok: true }; +} ``` -## Artifact Cleanup and Deletion +## Naming conventions -### Understanding deleteArtifact +Consistent filename prefixes make it easy to list, filter, and clean up related artifacts programmatically: -For persistent storage like `GcsArtifactService`, artifacts remain until explicitly deleted. The `deleteArtifact` method allows you to remove artifacts and all their versions. This is a powerful operation that should be used carefully. +| Prefix | Scope | Example | Notes | +| -------- | ------- | ----------------------------------------------- | ---------------------------------- | +| _(none)_ | Session | `"report.pdf"`, `"summary.txt"` | Default — discarded on session end | +| `user:` | User | `"user:avatar.png"`, `"user:settings.json"` | Persists across sessions | +| `temp_` | Session | `"temp_upload.csv"`, `"temp_parse_result.json"` | Delete immediately after use | +| `cache_` | Either | `"cache_analysis.json"` | Safe to delete and regenerate | - +## Cleanup and deletion -The `deleteArtifact` method is intentionally not exposed through context -objects (`CallbackContext` or `ToolContext`) for safety reasons. It must be -accessed directly through the artifact service instance. +`deleteArtifact` removes all versions of a filename permanently and is only available on the service directly — not on context objects. + + Deletion is permanent and removes all versions. There is no undo. Test cleanup + logic in a staging environment before running in production. -### When to Delete Artifacts - -Consider implementing cleanup strategies when: - -- **Temporary Data**: Artifacts represent temporary processing results that have a limited lifespan -- **Storage Costs**: Large artifacts accumulate and impact storage costs -- **Privacy Requirements**: User data must be deleted per retention policies or user requests -- **Session Cleanup**: Session-scoped artifacts should be cleaned up when sessions end - -### Basic Deletion +To delete a single file after processing: ```typescript import { GcsArtifactService } from "@iqai/adk"; -const artifactService = new GcsArtifactService("my-bucket"); +const svc = new GcsArtifactService("my-bucket"); -// Delete a specific artifact (removes all versions) -await artifactService.deleteArtifact({ +await svc.deleteArtifact({ appName: "my_app", - userId: "user123", - sessionId: "session456", - filename: "temp_processing.csv", + userId: "u1", + sessionId: "s1", + filename: "temp_upload.csv", }); ``` -### Cleanup Strategies - -For persistent storage, implement cleanup strategies using `deleteArtifact`: - -#### 1. GCS Lifecycle Policies - -For `GcsArtifactService`, configure bucket lifecycle policies to automatically delete old artifacts: - -```bash -# Set lifecycle policy to delete artifacts older than 90 days -gsutil lifecycle set lifecycle.json gs://my-artifacts-bucket -``` - -```json -{ - "lifecycle": { - "rule": [ - { - "action": { "type": "Delete" }, - "condition": { "age": 90 } - } - ] - } -} -``` - -#### 2. Administrative Cleanup Tools - -Build specific tools or administrative functions that utilize `deleteArtifact`: +To delete all artifacts matching a prefix — for example, cleaning up all `temp_` files after a session: ```typescript -import { BaseArtifactService } from "@iqai/adk"; - -// Simple cleanup function for temporary artifacts -async function cleanupTemporaryArtifacts( - artifactService: BaseArtifactService, - appName: string, - userId: string, - sessionId: string, -): Promise { - const artifacts = await artifactService.listArtifactKeys({ - appName, - userId, - sessionId, - }); - - // Delete artifacts that start with 'temp_' - for (const filename of artifacts) { - if (filename.startsWith("temp_")) { - try { - await artifactService.deleteArtifact({ - appName, - userId, - sessionId, - filename, - }); - } catch (error) { - console.warn(`Failed to delete ${filename}:`, error); - } - } - } -} -``` - -#### 3. Pattern-Based Deletion - -Carefully manage filenames to allow pattern-based deletion: +import type { BaseArtifactService } from "@iqai/adk"; -```typescript -// Use consistent naming conventions for easy cleanup -const TEMP_PREFIX = "temp_"; -const CACHE_PREFIX = "cache_"; -const USER_DATA_PREFIX = "user:"; - -async function cleanupByPattern( - artifactService: BaseArtifactService, - appName: string, - userId: string, - sessionId: string, +async function deleteByPrefix( + svc: BaseArtifactService, + args: { appName: string; userId: string; sessionId: string }, + prefix: string, ) { - const artifacts = await artifactService.listArtifactKeys({ - appName, - userId, - sessionId, - }); - - // Delete all temporary artifacts - const tempArtifacts = artifacts.filter(name => name.startsWith(TEMP_PREFIX)); - - for (const filename of tempArtifacts) { - await artifactService.deleteArtifact({ - appName, - userId, - sessionId, - filename, - }); - } + const keys = await svc.listArtifactKeys(args); + await Promise.all( + keys + .filter(k => k.startsWith(prefix)) + .map(filename => svc.deleteArtifact({ ...args, filename })), + ); } -``` - -#### 4. Scheduled Cleanup Jobs - -Implement scheduled cleanup for production environments: - -```typescript -import { BaseArtifactService } from "@iqai/adk"; - -class ScheduledArtifactCleanup { - constructor( - private artifactService: BaseArtifactService, - private appName: string, - ) {} - - async runDailyCleanup(): Promise { - // This would typically be called by a cron job or scheduler - console.log("Starting daily artifact cleanup..."); - - // Get all users (implementation depends on your user management) - const users = await this.getAllUsers(); - - for (const userId of users) { - try { - // Clean up temporary artifacts older than 7 days - await this.cleanupOldArtifacts(userId, 7); - } catch (error) { - console.error(`Cleanup failed for user ${userId}:`, error); - } - } - - console.log("Daily cleanup completed"); - } - private async cleanupOldArtifacts( - userId: string, - daysOld: number, - ): Promise { - // Implementation would check artifact metadata/timestamps - // and delete artifacts older than specified days - // This is a simplified example - } - - private async getAllUsers(): Promise { - // Implementation depends on your user management system - return []; - } -} +// Example: delete all temp_ artifacts for a session +await deleteByPrefix( + svc, + { appName: "my_app", userId: "u1", sessionId: "s1" }, + "temp_", +); ``` -### Important Considerations +For GCS, you can also configure a bucket lifecycle policy to auto-delete blobs older than a set number of days — useful for enforcing data retention limits without manual cleanup: -- **Irreversible Operation**: Deletion removes all versions of an artifact permanently -- **No Context Access**: `deleteArtifact` is not available through `CallbackContext` or `ToolContext` for safety -- **Direct Service Access**: Always access through the artifact service instance -- **Error Handling**: Implement proper error handling and logging for cleanup operations -- **Testing**: Test cleanup strategies thoroughly in staging before production deployment - -## Version Management - -### Cleanup Strategies - -Implement automatic cleanup for old versions: - -```typescript -class ArtifactVersionManager { - constructor(private artifactService: BaseArtifactService) {} - - async cleanupOldVersions( - appName: string, - userId: string, - sessionId: string, - retentionPolicy: RetentionPolicy, - ): Promise { - const artifacts = await this.artifactService.listArtifactKeys({ - appName, - userId, - sessionId, - }); - - let totalDeleted = 0; - const errors: string[] = []; - - for (const filename of artifacts) { - try { - const versions = await this.artifactService.listVersions({ - appName, - userId, - sessionId, - filename, - }); - - const versionsToDelete = this.selectVersionsForDeletion( - versions, - retentionPolicy, - ); - - for (const version of versionsToDelete) { - // Note: Current interface doesn't support version-specific deletion - // This would need to be added to BaseArtifactService - console.log(`Would delete ${filename} version ${version}`); - totalDeleted++; - } - } catch (error) { - errors.push(`Failed to cleanup ${filename}: ${error.message}`); - } - } - - return { - artifactsProcessed: artifacts.length, - versionsDeleted: totalDeleted, - errors, - }; - } - - private selectVersionsForDeletion( - versions: number[], - policy: RetentionPolicy, - ): number[] { - const sortedVersions = [...versions].sort((a, b) => b - a); // newest first - - switch (policy.strategy) { - case "keep_latest": - return sortedVersions.slice(policy.count); - - case "keep_recent": - // Keep versions from the last N days (simplified) - const cutoffVersion = sortedVersions[policy.count - 1] || 0; - return versions.filter(v => v < cutoffVersion); - - case "custom": - return policy.customSelector(versions); - - default: - return []; - } +```json +{ + "lifecycle": { + "rule": [{ "action": { "type": "Delete" }, "condition": { "age": 90 } }] } } - -interface RetentionPolicy { - strategy: "keep_latest" | "keep_recent" | "custom"; - count: number; - customSelector?: (versions: number[]) => number[]; -} - -interface CleanupResult { - artifactsProcessed: number; - versionsDeleted: number; - errors: string[]; -} ``` -### Backup Strategies - -Implement backup and recovery for critical artifacts: - -```typescript -class ArtifactBackupService { - constructor( - private primaryService: BaseArtifactService, - private backupService: BaseArtifactService, - ) {} - - async saveWithBackup(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - artifact: Part; - }): Promise { - // Save to primary storage - const version = await this.primaryService.saveArtifact(args); - - // Async backup (don't wait for completion) - this.backupArtifact(args, version).catch(error => { - console.error("Backup failed:", error); - }); - - return version; - } - - private async backupArtifact( - args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - artifact: Part; - }, - version: number, - ): Promise { - try { - // Add backup metadata - const backupArtifact = { - ...args.artifact, - inlineData: { - ...args.artifact.inlineData, - // Add backup metadata to MIME type - mimeType: `${args.artifact.inlineData.mimeType}; backup=true; original-version=${version}`, - }, - }; - - await this.backupService.saveArtifact({ - ...args, - artifact: backupArtifact, - }); - } catch (error) { - console.error("Failed to create backup:", error); - throw error; - } - } - - async restoreFromBackup(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - version?: number; - }): Promise { - console.log("Attempting restore from backup..."); - - try { - const backup = await this.backupService.loadArtifact(args); - - if (backup) { - // Remove backup metadata from MIME type - const cleanArtifact = { - ...backup, - inlineData: { - ...backup.inlineData, - mimeType: backup.inlineData.mimeType.split(";")[0], - }, - }; - - // Restore to primary storage - await this.primaryService.saveArtifact({ - ...args, - artifact: cleanArtifact, - }); - - return cleanArtifact; - } - - return null; - } catch (error) { - console.error("Backup restore failed:", error); - throw error; - } - } -} +```bash +gsutil lifecycle set lifecycle.json gs://my-artifacts-bucket ``` -## Error Handling and Resilience +## Retry transient errors -### Retry Logic (simple backoff) +GCS calls can fail transiently due to network blips or temporary service unavailability. Rather than letting a single failure crash a callback, wrap GCS operations in exponential backoff. The delay doubles on each attempt, giving the service time to recover: ```typescript -async function withRetry(op: () => Promise, attempts = 3, base = 1000) { - let last: unknown; +async function withRetry( + op: () => Promise, + attempts = 3, + baseDelayMs = 1000, +): Promise { + let lastError: unknown; for (let i = 0; i <= attempts; i++) { try { return await op(); } catch (e) { - last = e; - if (i === attempts) break; - await new Promise(r => setTimeout(r, base * Math.pow(2, i))); - } - } - throw last; -} -``` - -## Monitoring and Observability - -### Metrics Collection - -Track artifact usage and performance: - -```typescript -class ArtifactMetricsCollector { - private metrics = { - saves: 0, - loads: 0, - errors: 0, - totalSize: 0, - averageSize: 0, - operationTimes: [] as number[], - }; - - constructor(private artifactService: BaseArtifactService) {} - - async saveArtifactWithMetrics(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - artifact: Part; - }): Promise { - const startTime = Date.now(); - - try { - const version = await this.artifactService.saveArtifact(args); - - // Record success metrics - this.metrics.saves++; - const size = Buffer.from(args.artifact.inlineData.data, "base64").length; - this.updateSizeMetrics(size); - this.recordOperationTime(Date.now() - startTime); - - return version; - } catch (error) { - this.metrics.errors++; - throw error; - } - } - - async loadArtifactWithMetrics(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - version?: number; - }): Promise { - const startTime = Date.now(); - - try { - const artifact = await this.artifactService.loadArtifact(args); - - // Record success metrics - this.metrics.loads++; - - if (artifact) { - const size = Buffer.from(artifact.inlineData.data, "base64").length; - this.updateSizeMetrics(size); + lastError = e; + if (i < attempts) { + await new Promise(r => setTimeout(r, baseDelayMs * 2 ** i)); } - - this.recordOperationTime(Date.now() - startTime); - - return artifact; - } catch (error) { - this.metrics.errors++; - throw error; - } - } - - private updateSizeMetrics(size: number): void { - this.metrics.totalSize += size; - const totalOps = this.metrics.saves + this.metrics.loads; - this.metrics.averageSize = this.metrics.totalSize / totalOps; - } - - private recordOperationTime(time: number): void { - this.metrics.operationTimes.push(time); - - // Keep only last 1000 operations for memory efficiency - if (this.metrics.operationTimes.length > 1000) { - this.metrics.operationTimes = this.metrics.operationTimes.slice(-1000); } } - - getMetrics() { - const times = this.metrics.operationTimes; - - return { - ...this.metrics, - averageOperationTime: - times.length > 0 ? times.reduce((a, b) => a + b, 0) / times.length : 0, - p95OperationTime: - times.length > 0 - ? times.sort((a, b) => a - b)[Math.floor(times.length * 0.95)] - : 0, - errorRate: - (this.metrics.errors / - (this.metrics.saves + this.metrics.loads + this.metrics.errors)) * - 100, - }; - } - - resetMetrics(): void { - this.metrics = { - saves: 0, - loads: 0, - errors: 0, - totalSize: 0, - averageSize: 0, - operationTimes: [], - }; - } + throw lastError; } -``` - -## Production Deployment Checklist - -### Infrastructure Setup - -- **Storage Backend**: Choose appropriate service (GCS, S3, database) -- **Backup Strategy**: Implement automated backups -- **Monitoring**: Set up metrics and alerting -- **Security**: Configure proper access controls and encryption -- **Rate Limiting**: Implement rate limiting to prevent abuse - -### Configuration - -- **Size Limits**: Set appropriate file size limits -- **MIME Types**: Whitelist allowed file types -- **Retention Policies**: Configure version cleanup -- **Caching**: Implement caching layer for performance -- **Error Handling**: Add comprehensive error handling - -### Operational Procedures -- **Monitoring Dashboard**: Set up artifact usage monitoring -- **Backup Verification**: Test backup and restore procedures -- **Incident Response**: Document artifact-related incident procedures -- **Capacity Planning**: Monitor storage growth and plan scaling -- **Security Audits**: Regular security reviews of artifact access - - - -Always test artifact operations thoroughly in staging environments before -deploying to production. +// Usage +const artifact = await withRetry(() => + svc.loadArtifact({ + appName: "my_app", + userId: "u1", + sessionId: "s1", + filename: "report.pdf", + }), +); +``` - +This approach works for any artifact service call — `saveArtifact`, `loadArtifact`, `deleteArtifact`, or `listArtifactKeys`. + +## Next steps + + + + + diff --git a/apps/docs/content/docs/framework/artifacts/context-integration.mdx b/apps/docs/content/docs/framework/artifacts/context-integration.mdx index b4dba2386..4d48c1357 100644 --- a/apps/docs/content/docs/framework/artifacts/context-integration.mdx +++ b/apps/docs/content/docs/framework/artifacts/context-integration.mdx @@ -1,280 +1,253 @@ --- title: Context Integration -description: Using artifacts through CallbackContext and ToolContext for file management and data processing +description: Save and load artifacts inside ADK-TS agent callbacks and tools using CallbackContext and ToolContext. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Tabs, Tab } from "fumadocs-ui/components/tabs"; +import { Cards, Card } from "fumadocs-ui/components/card"; -Artifacts are accessed through context objects that provide convenient methods for storing and retrieving binary data. Different context types offer varying levels of functionality depending on your use case. +When you write a callback or a tool, the context object passed to your function already has artifact methods built in — you do not need to import or pass the service manually. - - -When `saveInputBlobsAsArtifacts` is enabled in the run config, the runner saves any incoming user `inlineData` parts as artifacts automatically before appending the event. Enable it via `withRunConfig({ saveInputBlobsAsArtifacts: true })` on your agent builder, or per run. An `artifactService` on the `Runner` is required. +- **`CallbackContext`** — available in `beforeAgentCallback`, `afterAgentCallback`, `beforeModelCallback`, and `afterModelCallback`. Provides `saveArtifact` and `loadArtifact`. +- **`ToolContext`** — available inside tools. Extends `CallbackContext` and adds `listArtifacts()`, which lets a tool discover what files are available in the current session. + + If you enable `saveInputBlobsAsArtifacts: true` in your run config, the runner + automatically saves any `inlineData` parts from the user's message as + artifacts before the agent runs — no callback code needed. See [Runner + Configuration](/docs/framework/artifacts/runner-configuration#auto-save-user-uploads) + for setup details. - - -Use `createTool` for concise, schema-validated tools instead of subclassing. -See Recipes for examples. - - - -## CallbackContext Access - -The `CallbackContext` provides basic artifact operations available in agent callbacks. - -### Available Methods +## Available methods -```typescript -class CallbackContext { - // Load an artifact by filename, optionally specifying version - async loadArtifact( - filename: string, - version?: number, - ): Promise; - - // Save an artifact and record it as a delta for the session - async saveArtifact(filename: string, artifact: Part): Promise; -} -``` - - - -The `deleteArtifact` method is not exposed through `CallbackContext` or -`ToolContext` for safety reasons. To delete artifacts, access the artifact -service directly. See [Best -Practices](/docs/framework/artifacts/best-practices#artifact-cleanup-and-deletion) -for cleanup strategies. +| Method | Available on | Description | +| ---------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------- | +| `saveArtifact(filename, part)` | Both | Saves a new version of the artifact. Returns the version number (starts at `0`). | +| `loadArtifact(filename, version?)` | Both | Loads the latest version, or a specific version if you pass a number. Returns `undefined` if not found. | +| `listArtifacts()` | `ToolContext` only | Returns the filenames of all artifacts in the current session. | + + `deleteArtifact` is intentionally not exposed through context objects — call + it directly on the service instance. See [Cleanup and + Deletion](/docs/framework/artifacts/best-practices#cleanup-and-deletion). -### Basic Usage in Callbacks +## Using artifacts in agent callbacks + +`CallbackContext` is available in both `beforeAgentCallback` and `afterAgentCallback`. Use `beforeAgent` to load data the agent needs before it starts, and `afterAgent` to persist data once it finishes. + + + + Load user preferences and conversation history from previous sessions, then write them into state so the agent can reference them: + + ```typescript + import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + import type { CallbackContext } from "@iqai/adk"; + + const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withBeforeAgentCallback(async (ctx: CallbackContext) => { + // Load user preferences (user-scoped — persists across sessions) + const prefsArtifact = await ctx.loadArtifact("user:preferences.json"); + if (prefsArtifact?.inlineData) { + const prefs = JSON.parse( + Buffer.from(prefsArtifact.inlineData.data, "base64").toString(), + ); + ctx.state.set("theme", prefs.theme ?? "light"); + ctx.state.set("user_preferences", prefs); + } + + // Load conversation history (session-scoped) + const historyArtifact = await ctx.loadArtifact("conversation_history.json"); + if (historyArtifact?.inlineData) { + const history = JSON.parse( + Buffer.from(historyArtifact.inlineData.data, "base64").toString(), + ); + ctx.state.set("conversation_count", history.length); + } + + return undefined; + }) + .build(); + ``` + + + + + Persist the conversation history and a session summary after each turn so they're available next time: + + ```typescript + import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + import type { CallbackContext } from "@iqai/adk"; + + const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withAfterAgentCallback(async (ctx: CallbackContext) => { + // Save conversation history + const history = ctx.state.get("conversation_history") ?? []; + await ctx.saveArtifact("conversation_history.json", { + inlineData: { + data: Buffer.from(JSON.stringify(history)).toString("base64"), + mimeType: "application/json", + }, + }); + + // Save session summary if the agent produced one + const summary = ctx.state.get("session_summary"); + if (summary) { + await ctx.saveArtifact("session_summary.json", { + inlineData: { + data: Buffer.from(JSON.stringify(summary)).toString("base64"), + mimeType: "application/json", + }, + }); + } + + return undefined; + }) + .build(); + ``` + + + + +## Using artifacts in model callbacks + +`beforeModelCallback` fires before every LLM call and `afterModelCallback` fires after. Use them to inject context into state before the model runs, or to capture metadata from the response. + + + + Load a stored model config and inject its settings into state before each LLM call: + + ```typescript + import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + import type { CallbackContext, LlmRequest } from "@iqai/adk"; + + const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withBeforeModelCallback(async ({ + callbackContext, + }: { + callbackContext: CallbackContext; + llmRequest: LlmRequest; + }) => { + const configArtifact = await callbackContext.loadArtifact("model_config.json"); + if (configArtifact?.inlineData) { + const config = JSON.parse( + Buffer.from(configArtifact.inlineData.data, "base64").toString(), + ); + callbackContext.state.set("temperature", config.temperature ?? 0.7); + callbackContext.state.set("max_tokens", config.maxTokens ?? 1000); + } + + // Optionally load additional context to pass to the model + const contextArtifact = await callbackContext.loadArtifact("context_data.txt"); + if (contextArtifact?.inlineData) { + const contextText = Buffer.from( + contextArtifact.inlineData.data, + "base64", + ).toString(); + callbackContext.state.set("additional_context", contextText); + } + + return undefined; + }) + .build(); + ``` + + + + + Record metadata about each model response — useful for auditing, cost tracking, or debugging: + + ```typescript + import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + import type { CallbackContext, LlmResponse } from "@iqai/adk"; + + const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withAfterModelCallback(async ({ + callbackContext, + }: { + callbackContext: CallbackContext; + llmResponse: LlmResponse; + }) => { + const metadata = { + timestamp: new Date().toISOString(), + model: callbackContext.state.get("current_model"), + tokensUsed: callbackContext.state.get("tokens_used"), + }; + + await callbackContext.saveArtifact("response_metadata.json", { + inlineData: { + data: Buffer.from(JSON.stringify(metadata)).toString("base64"), + mimeType: "application/json", + }, + }); + + return undefined; + }) + .build(); + ``` + + + + +## Using artifacts in tools + +Inside a tool, `ctx` is a `ToolContext`. It has all the same methods as `CallbackContext` plus `listArtifacts()`, which returns the filenames of every artifact in the current session. Use it to check what's available before loading. ```typescript -import { LlmAgent, CallbackContext } from "@iqai/adk"; - -const beforeAgentCallback = async (callbackContext: CallbackContext) => { - try { - // Load user preferences - const prefsArtifact = await callbackContext.loadArtifact( - "user:preferences.json", - ); - - if (prefsArtifact) { - const preferences = JSON.parse( - Buffer.from(prefsArtifact.inlineData.data, "base64").toString(), - ); - - // Apply user preferences to session state - callbackContext.state.set("user_preferences", preferences); - callbackContext.state.set("theme", preferences.theme || "light"); +import { createTool } from "@iqai/adk"; +import * as z from "zod"; + +export const inspectFile = createTool({ + name: "inspect_file", + description: "Describe a specific artifact by filename", + schema: z.object({ filename: z.string() }), + fn: async ({ filename }, ctx) => { + // Check what's available before loading + const files = await ctx.listArtifacts(); + if (!files.includes(filename)) { + return { error: "File not found", available: files }; } - // Load conversation history if available - const historyArtifact = await callbackContext.loadArtifact( - "conversation_history.json", - ); + const artifact = await ctx.loadArtifact(filename); - if (historyArtifact) { - const history = JSON.parse( - Buffer.from(historyArtifact.inlineData.data, "base64").toString(), - ); - - callbackContext.state.set("conversation_count", history.length); + if (artifact?.text) { + return { filename, type: "text", sizeBytes: artifact.text.length }; } - } catch (error) { - console.warn("Failed to load user data:", error); - } - - return undefined; -}; - -const afterAgentCallback = async (callbackContext: CallbackContext) => { - try { - // Save updated conversation history - const currentHistory = - callbackContext.state.get("conversation_history") || []; - const historyArtifact = { - inlineData: { - data: Buffer.from(JSON.stringify(currentHistory)).toString("base64"), - mimeType: "application/json", - }, - }; - - await callbackContext.saveArtifact( - "conversation_history.json", - historyArtifact, - ); - - // Save session summary - const summary = callbackContext.state.get("session_summary"); - if (summary) { - const summaryArtifact = { - inlineData: { - data: Buffer.from(JSON.stringify(summary)).toString("base64"), - mimeType: "application/json", - }, - }; - await callbackContext.saveArtifact( - "session_summary.json", - summaryArtifact, - ); - } - } catch (error) { - console.error("Failed to save session data:", error); - } - - return undefined; -}; -``` - -### Model Callback Integration - -```typescript -const beforeModelCallback = async ({ - callbackContext, -}: { - callbackContext: CallbackContext; -}) => { - try { - // Load model configuration - const configArtifact = - await callbackContext.loadArtifact("model_config.json"); - - if (configArtifact) { - const config = JSON.parse( - Buffer.from(configArtifact.inlineData.data, "base64").toString(), - ); - - // Apply model-specific settings - callbackContext.state.set("temperature", config.temperature || 0.7); - callbackContext.state.set("max_tokens", config.maxTokens || 1000); + if (artifact?.inlineData) { + return { + filename, + mimeType: artifact.inlineData.mimeType, + sizeBytes: Buffer.from(artifact.inlineData.data, "base64").length, + }; } - // Load context files for the model - const contextArtifact = - await callbackContext.loadArtifact("context_data.txt"); - - if (contextArtifact) { - const contextText = Buffer.from( - contextArtifact.inlineData.data, - "base64", - ).toString(); - - // Add context to the request (implementation depends on your setup) - callbackContext.state.set("additional_context", contextText); - } - } catch (error) { - console.warn("Failed to load model configuration:", error); - } - - return undefined; -}; - -const afterModelCallback = async ({ - callbackContext, -}: { - callbackContext: CallbackContext; -}) => { - try { - // Save model response metadata - const responseMetadata = { - timestamp: new Date().toISOString(), - model: callbackContext.state.get("current_model"), - tokens_used: callbackContext.state.get("tokens_used"), - }; - - const metadataArtifact = { - inlineData: { - data: Buffer.from(JSON.stringify(responseMetadata)).toString("base64"), - mimeType: "application/json", - }, - }; - - await callbackContext.saveArtifact( - "response_metadata.json", - metadataArtifact, - ); - } catch (error) { - console.error("Failed to save response metadata:", error); - } - - return undefined; -}; + return { error: "Could not read file" }; + }, +}); ``` -## ToolContext Access - -The `ToolContext` extends `CallbackContext` with additional artifact management capabilities. - -### Enhanced Methods - -```typescript -class ToolContext extends CallbackContext { - // Inherited from CallbackContext - async loadArtifact( - filename: string, - version?: number, - ): Promise; - async saveArtifact(filename: string, artifact: Part): Promise; - - // Additional tool-specific methods - async listArtifacts(): Promise; -} -``` - - - -The `deleteArtifact` method is not exposed through `ToolContext` for safety -reasons. To delete artifacts, access the artifact service directly. See [Best -Practices](/docs/framework/artifacts/best-practices#artifact-cleanup-and-deletion) -for cleanup strategies. - - - -### File Processing Tools - -See focused, production-ready examples in the Recipes page: - -- Upload → process → save results -- Generate media → save + reuse -- Cache expensive outputs - -[Open recipes](/docs/framework/artifacts/recipes) - -### Document Processing Tool - -For end-to-end document handling examples, see the recipe collection: - -- Text extraction (plain text) -- Simple summarization -- Format conversion - -[Open recipes](/docs/framework/artifacts/recipes) - -## Report Generation - -See Recipes for JSON and CSV report generation with concise code. - -[Open recipes](/docs/framework/artifacts/recipes) - -## File Processing - -See the end-to-end file processing flow in the Recipes page. - -[Open recipes](/docs/framework/artifacts/recipes) - -## Media Creation - -See Recipes for a minimal SVG/image generation and save workflow. - -[Open recipes](/docs/framework/artifacts/recipes) - - - -Context integration provides a convenient abstraction over the artifact -service, handling session scoping and error management automatically. - - +## Next steps + + + + + diff --git a/apps/docs/content/docs/framework/artifacts/index.mdx b/apps/docs/content/docs/framework/artifacts/index.mdx index 4de356408..b12a9e052 100644 --- a/apps/docs/content/docs/framework/artifacts/index.mdx +++ b/apps/docs/content/docs/framework/artifacts/index.mdx @@ -1,507 +1,94 @@ --- title: Artifacts -description: Manage named, versioned binary data for rich agent interactions with files and media +description: Store and retrieve named, versioned files — images, CSVs, JSON blobs, or any binary data — across agent callbacks and tools in ADK-TS. --- import { Cards, Card } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; +import { Mermaid } from "@/components/mdx/mermaid"; -Artifacts provide a mechanism for managing named, versioned binary data associated with user sessions. They enable agents to handle data beyond simple text strings, supporting rich interactions with files, images, audio, and other binary formats. +**Artifacts** are named, versioned blobs your agent can save and retrieve at any point during execution — from callbacks, tools, or directly on the service. Use them when you need to pass files between steps, persist user uploads, or cache generated content. -## Overview +## How it works -Artifacts are pieces of binary data identified by unique filenames within specific scopes, with automatic versioning for data evolution. They bridge the gap between simple session state and complex file management needs. + B["ctx.saveArtifact(filename, part)"] + B --> C[(ArtifactService\nappName / userId / sessionId / filename / version)] + C --> D[Returns version number\n0, 1, 2 …] -### Core Characteristics + C --> E["ctx.loadArtifact(filename)\n→ latest version"] + C --> F["ctx.loadArtifact(filename, 0)\n→ specific version"] + C --> G["ctx.listArtifacts()\n→ all filenames (ToolContext only)"] -- **Binary Data Storage**: Handle any type of binary content (images, PDFs, audio, video) -- **Named Identification**: Use descriptive filenames for easy reference -- **Automatic Versioning**: Each save creates a new version automatically -- **Scoped Access**: Session-specific or user-wide accessibility -- **Standard Representation**: Consistent `Part` object format from `@google/genai` - - - -While session state is perfect for configuration and conversational context, -artifacts are designed for binary data, large files, and content that needs -versioning. - - - -## Quick Start - -### Basic Artifact Operations - -Here's how to work with artifacts in your agent callbacks: - -```typescript -import { LlmAgent, CallbackContext, Runner } from "@iqai/adk"; -import { InMemoryArtifactService } from "@iqai/adk"; - -// Set up artifact service -const artifactService = new InMemoryArtifactService(); - -// Use artifacts in callbacks -const beforeAgentCallback = async (callbackContext: CallbackContext) => { - try { - // Save a simple text artifact - const textArtifact = { - inlineData: { - data: Buffer.from("Hello, World!").toString("base64"), - mimeType: "text/plain", - }, - }; - - const version = await callbackContext.saveArtifact( - "greeting.txt", - textArtifact, - ); - console.log(`Saved greeting.txt version ${version}`); - - // Load an existing artifact - const loadedArtifact = await callbackContext.loadArtifact("greeting.txt"); - if (loadedArtifact) { - const text = Buffer.from( - loadedArtifact.inlineData.data, - "base64", - ).toString(); - console.log(`Loaded text: ${text}`); - } - } catch (error) { - console.warn("Artifact operation failed:", error); - } - - return undefined; -}; - -// Create agent with artifact service -const agent = new LlmAgent({ - name: "artifact_agent", - model: "gemini-2.5-flash", - description: "Agent that works with artifacts", - instruction: "You are helpful", - beforeAgentCallback, -}); - -// Create runner with artifact service -const runner = new Runner({ - appName: "my_app", - agent, - sessionService: new InMemorySessionService(), - artifactService, // Configure artifact service -}); -``` - -### Working with Different Data Types - -```typescript -const mediaArtifactCallback = async (callbackContext: CallbackContext) => { - // Save an image artifact - const imageData = Buffer.from(/* your image data */); - const imageArtifact = { - inlineData: { - data: imageData.toString("base64"), - mimeType: "image/png", - }, - }; - - await callbackContext.saveArtifact("generated_chart.png", imageArtifact); - - // Save a JSON data artifact - const jsonData = { results: [1, 2, 3], timestamp: new Date().toISOString() }; - const jsonArtifact = { - inlineData: { - data: Buffer.from(JSON.stringify(jsonData)).toString("base64"), - mimeType: "application/json", - }, - }; - - await callbackContext.saveArtifact("analysis_results.json", jsonArtifact); - - return undefined; -}; -``` - -### Auto-save input blobs from user messages +`} +/> -When enabled, the runner will automatically save any incoming `inlineData` parts from the user's message as artifacts before appending the event. Each saved file is named using the pattern `artifact__` and the corresponding message part is replaced with a short text placeholder. +Artifacts are `Part` objects from `@google/genai` — either `{ text }`, `{ inlineData }`, or `{ fileData }`. Filenames without a prefix are session-scoped; prefix with `user:` to persist across sessions. -- Requires an `artifactService` to be configured on the `Runner` -- Toggle via `runConfig.saveInputBlobsAsArtifacts` -- Can be set at agent build time using `withRunConfig` or per run +## Quick start ```typescript import { - agentBuilder, - Runner, - InMemorySessionService, + AgentBuilder, + CallbackContext, InMemoryArtifactService, } from "@iqai/adk"; -// Configure at agent build time -const agent = agentBuilder() - .name("artifact_agent") - .model("gemini-2.5-flash") - .withRunConfig({ saveInputBlobsAsArtifacts: true }) - .build(); - -const runner = new Runner({ - appName: "my_app", - agent, - sessionService: new InMemorySessionService(), - artifactService: new InMemoryArtifactService(), -}); - -// Or enable per run -// await runner.runAsync({ userId, sessionId, newMessage, runConfig: new RunConfig({ saveInputBlobsAsArtifacts: true }) }); -``` - -## Artifact Scoping - -Artifacts can be scoped to different access levels: - -### Session-Specific Artifacts +const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withBeforeAgentCallback(async (ctx: CallbackContext) => { + const version = await ctx.saveArtifact("notes.txt", { + text: "Agent started.", + }); + console.log(`Saved at version ${version}`); // 0 -Artifacts tied to individual conversation sessions: + const artifact = await ctx.loadArtifact("notes.txt"); + if (artifact?.text) console.log(artifact.text); -```typescript -// Regular filename - session scoped -await callbackContext.saveArtifact("temp_processing.csv", csvArtifact); -``` - -**Characteristics:** - -- Limited to current session context -- Automatically cleaned up when sessions end -- Ideal for temporary processing and conversation-specific files - -### User-Specific Artifacts - -Artifacts persisting across all user sessions: - -```typescript -// "user:" prefix - user scoped -await callbackContext.saveArtifact("user:profile_picture.png", imageArtifact); + return undefined; + }) + .build(); ``` -**Characteristics:** - -- Available across all user sessions within the application -- Persist beyond individual conversations -- Enable long-term user data management - - - -Choose appropriate artifact scoping based on data lifecycle requirements - -session for temporary content, user for persistent data. - + + Switch to `GcsArtifactService` for durable, production storage. Both services + implement the same interface — no other code changes needed. -## Versioning System - -Artifacts automatically maintain version history: - -```typescript -const versioningExample = async (callbackContext: CallbackContext) => { - // First save - creates version 0 - const v0 = await callbackContext.saveArtifact("document.txt", textArtifact1); - console.log(`Version: ${v0}`); // 0 - - // Second save - creates version 1 - const v1 = await callbackContext.saveArtifact("document.txt", textArtifact2); - console.log(`Version: ${v1}`); // 1 - - // Load specific version - const oldVersion = await callbackContext.loadArtifact("document.txt", 0); - - // Load latest version (default) - const latestVersion = await callbackContext.loadArtifact("document.txt"); - - return undefined; -}; -``` - -**Version Characteristics:** - -- Version numbers start at 0 and increment automatically -- Each save operation creates a new version -- Access latest version by default or specify version explicitly -- Complete version history maintained - -## Where to start +## Next steps - - -### File Upload and Processing - -Save an uploaded file as an artifact, then persist derived results (e.g., analysis output). This is a minimal end-to-end flow; see the full tool-based version in Recipes. - -[Open recipes](/docs/framework/artifacts/recipes) - -```typescript -const fileProcessingCallback = async (callbackContext: CallbackContext) => { - // Simulate receiving an uploaded file - const uploadedFileData = Buffer.from("CSV data here"); - const csvArtifact = { - inlineData: { - data: uploadedFileData.toString("base64"), - mimeType: "text/csv", - }, - }; - - // Save uploaded file - await callbackContext.saveArtifact("uploaded_data.csv", csvArtifact); - - // Process and save results - const processedData = { summary: "Analysis complete", rows: 100 }; - const resultsArtifact = { - inlineData: { - data: Buffer.from(JSON.stringify(processedData)).toString("base64"), - mimeType: "application/json", - }, - }; - - await callbackContext.saveArtifact( - "processing_results.json", - resultsArtifact, - ); - - return undefined; -}; -``` - -### Caching Expensive Operations - -Avoid recomputing expensive results by saving a JSON artifact as a cache entry and reading it on subsequent runs. Use this for deterministic or memoizable outputs. - -```typescript -const cachingCallback = async (callbackContext: CallbackContext) => { - const cacheKey = "expensive_computation_result.json"; - - // Check for cached result - const cachedResult = await callbackContext.loadArtifact(cacheKey); - - if (cachedResult) { - console.log("Using cached result"); - const data = JSON.parse( - Buffer.from(cachedResult.inlineData.data, "base64").toString(), - ); - callbackContext.state.set("computation_result", data); - } else { - console.log("Computing new result"); - - // Perform expensive computation - const result = { value: Math.random(), timestamp: Date.now() }; - - // Cache the result - const artifact = { - inlineData: { - data: Buffer.from(JSON.stringify(result)).toString("base64"), - mimeType: "application/json", - }, - }; - - await callbackContext.saveArtifact(cacheKey, artifact); - callbackContext.state.set("computation_result", result); - } - - return undefined; -}; -``` - -## Service Architecture - -See interfaces and backends in: - -- [Scoping & Versioning](/docs/framework/artifacts/scoping-and-versioning): behavior overview -- [Service Implementations](/docs/framework/artifacts/service-implementations): concrete setup and usage - -## Documentation Structure - - - - - - - - - -## Error Handling - -Always implement proper error handling for artifact operations: - -```typescript -const robustArtifactCallback = async (callbackContext: CallbackContext) => { - try { - const artifact = await callbackContext.loadArtifact("user_config.json"); - - if (artifact) { - const config = JSON.parse( - Buffer.from(artifact.inlineData.data, "base64").toString(), - ); - callbackContext.state.set("user_config", config); - } else { - console.log("No existing config found, using defaults"); - callbackContext.state.set("user_config", getDefaultConfig()); - } - } catch (error) { - console.error("Failed to load user config:", error); - - // Fallback to defaults - callbackContext.state.set("user_config", getDefaultConfig()); - - // Optionally notify the user - return { - role: "model", - parts: [ - { text: "Configuration could not be loaded. Using default settings." }, - ], - }; - } - - return undefined; -}; - -function getDefaultConfig() { - return { - theme: "light", - language: "en", - notifications: true, - }; -} -``` - -## Integration Examples - -### With Tools - -```typescript -import { BaseTool, ToolContext } from "@iqai/adk"; - -class FileAnalysisTool extends BaseTool { - async runAsync(args: { filename: string }, context: ToolContext) { - try { - // List available artifacts - const availableFiles = await context.listArtifacts(); - - if (!availableFiles.includes(args.filename)) { - return { error: `File ${args.filename} not found` }; - } - - // Load and analyze the file - const artifact = await context.loadArtifact(args.filename); - if (!artifact) { - return { error: `Could not load ${args.filename}` }; - } - - // Perform analysis based on MIME type - const analysis = this.analyzeFile(artifact); - - // Save analysis results - const resultArtifact = { - inlineData: { - data: Buffer.from(JSON.stringify(analysis)).toString("base64"), - mimeType: "application/json", - }, - }; - - await context.saveArtifact( - `analysis_${args.filename}.json`, - resultArtifact, - ); - - return { - analysis, - result_saved: `analysis_${args.filename}.json`, - }; - } catch (error) { - return { error: `Analysis failed: ${error.message}` }; - } - } - - private analyzeFile(artifact: Part) { - // Implementation based on MIME type - return { - size: artifact.inlineData.data.length, - type: artifact.inlineData.mimeType, - timestamp: new Date().toISOString(), - }; - } -} -``` - -## Related Topics - - - - - - - - - - diff --git a/apps/docs/content/docs/framework/artifacts/meta.json b/apps/docs/content/docs/framework/artifacts/meta.json index 497947f63..1b3db543b 100644 --- a/apps/docs/content/docs/framework/artifacts/meta.json +++ b/apps/docs/content/docs/framework/artifacts/meta.json @@ -1,7 +1,6 @@ { "icon": "FileText", "pages": [ - "index", "runner-configuration", "context-integration", "scoping-and-versioning", diff --git a/apps/docs/content/docs/framework/artifacts/recipes.mdx b/apps/docs/content/docs/framework/artifacts/recipes.mdx index 8ecf07f42..7e91cc94b 100644 --- a/apps/docs/content/docs/framework/artifacts/recipes.mdx +++ b/apps/docs/content/docs/framework/artifacts/recipes.mdx @@ -1,9 +1,17 @@ --- title: Recipes -description: Practical patterns for saving, loading, and transforming artifacts +description: Copy-paste patterns for common artifact workflows in ADK-TS — upload processing, media generation, and result caching. --- -## Upload → Process → Save Results +import { Cards, Card } from "fumadocs-ui/components/card"; + +These are self-contained patterns you can drop into your own agents. Each recipe covers a common workflow — pick the one that matches your use case and adapt the filenames and logic to fit. + +## Upload → Process → Save results + +When a user uploads a file (e.g. via auto-save or a direct tool call), you often want to parse it, compute something, and save the result as a new artifact so the agent can reference it later without re-processing. + +This recipe receives a CSV filename, parses it, and saves a JSON analysis artifact. Because it uses `createTool`, the agent can call it by name once the upload is in place: ```typescript import { createTool } from "@iqai/adk"; @@ -11,15 +19,18 @@ import * as z from "zod"; export const analyzeCsv = createTool({ name: "analyze_csv", - description: "Analyze uploaded CSV", + description: "Analyze an uploaded CSV file and save the results as JSON", schema: z.object({ filename: z.string() }), fn: async ({ filename }, ctx) => { + // Confirm the file exists in this session before trying to load it const files = await ctx.listArtifacts(); - if (!files.includes(filename)) return { error: "File not found", files }; + if (!files.includes(filename)) + return { error: "File not found", available: files }; const artifact = await ctx.loadArtifact(filename); if (!artifact?.inlineData) return { error: "Could not load file" }; + // Decode and parse the CSV const text = Buffer.from(artifact.inlineData.data, "base64").toString( "utf-8", ); @@ -27,64 +38,115 @@ export const analyzeCsv = createTool({ const headers = header.split(",").map(h => h.trim()); const result = { - summary: "CSV analysis complete", rowCount: rows.length, columnCount: headers.length, headers, }; - const resultArtifact = { + // Save the analysis as a new artifact + const outputFilename = `analysis_${filename.replace(/\.[^/.]+$/, "")}.json`; + await ctx.saveArtifact(outputFilename, { inlineData: { data: Buffer.from(JSON.stringify(result, null, 2)).toString("base64"), mimeType: "application/json", }, - }; - const out = `analysis_${filename.replace(/\.[^/.]+$/, "")}.json`; - await ctx.saveArtifact(out, resultArtifact); - return { output: out, ...result }; + }); + + return { output: outputFilename, ...result }; }, }); ``` -## Generate Media → Save + Reuse +## Generate media → Save + reuse + +If your agent generates an image, SVG, or other binary output that's expensive to produce, save it as an artifact on first generation and load the cached version on subsequent calls instead of regenerating it. + +This recipe generates an SVG banner inside a callback and caches it — repeated invocations return the stored version immediately: ```typescript import type { CallbackContext } from "@iqai/adk"; -export async function generateSvg(ctx: CallbackContext) { - const svg = ``; - await ctx.saveArtifact("banner.svg", { +export async function generateAndCacheSvg(ctx: CallbackContext) { + const cacheKey = "banner.svg"; + + // Return the cached version if it already exists + const cached = await ctx.loadArtifact(cacheKey); + if (cached?.inlineData) { + return Buffer.from(cached.inlineData.data, "base64").toString("utf-8"); + } + + // Generate the SVG and save it for future calls + const svg = ` + + `; + + await ctx.saveArtifact(cacheKey, { inlineData: { data: Buffer.from(svg).toString("base64"), mimeType: "image/svg+xml", }, }); - // Later - const loaded = await ctx.loadArtifact("banner.svg"); - if (loaded) { - const svgText = Buffer.from(loaded.inlineData.data, "base64").toString( - "utf-8", - ); - // Use svgText (serve to UI, embed in response, etc.) - } + return svg; } ``` -## Cache Expensive Binary Output +## Cache expensive computation + +Use the same check-before-compute pattern for any operation with a deterministic or memoizable output — model inference on a fixed input, a database aggregation, a report generation step. + +This recipe checks for a cached result before running the expensive work, and saves it so the next call skips the computation entirely: ```typescript import type { CallbackContext } from "@iqai/adk"; -export async function cachedChart(ctx: CallbackContext) { - const cacheKey = "chart.png"; +export async function cachedComputation(ctx: CallbackContext) { + const cacheKey = "result.json"; + + // Return early if the result is already cached const cached = await ctx.loadArtifact(cacheKey); - if (cached) return { info: "hit", cacheKey }; + if (cached) return { hit: true, cacheKey }; + + // Run the expensive operation and cache the result + const result = { value: Math.random(), computedAt: Date.now() }; - const png = Buffer.from(/* expensive render */); await ctx.saveArtifact(cacheKey, { - inlineData: { data: png.toString("base64"), mimeType: "image/png" }, + inlineData: { + data: Buffer.from(JSON.stringify(result)).toString("base64"), + mimeType: "application/json", + }, }); - return { info: "miss", cacheKey }; + + return { hit: false, cacheKey }; } ``` + +## Save and load plain text + +For simple string content you don't need `inlineData` at all. The `text` Part format is shorter to write and easier to read back — no base64 encoding or decoding required: + +```typescript +// Save +await ctx.saveArtifact("notes.txt", { text: "Meeting notes: ..." }); + +// Load — artifact.text is the string directly +const notes = await ctx.loadArtifact("notes.txt"); +console.log(notes?.text); +``` + +Use this for log output, agent-generated summaries, plain JSON strings, or any content where binary encoding adds no value. + +## Next steps + + + + + diff --git a/apps/docs/content/docs/framework/artifacts/runner-configuration.mdx b/apps/docs/content/docs/framework/artifacts/runner-configuration.mdx index 585a509fc..f82081607 100644 --- a/apps/docs/content/docs/framework/artifacts/runner-configuration.mdx +++ b/apps/docs/content/docs/framework/artifacts/runner-configuration.mdx @@ -1,71 +1,99 @@ --- title: Runner Configuration -description: Configure artifact behavior at run-time, including auto-saving user input blobs +description: Wire an artifact service to your ADK-TS Runner and enable automatic saving of user-uploaded blobs. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Cards, Card } from "fumadocs-ui/components/card"; -## Enable auto-save for user input blobs +Before any callback or tool can save or load an artifact, the `Runner` needs an `artifactService`. This page covers how to wire one up and how to enable auto-saving of binary uploads sent by the user. -When enabled, the runner automatically saves any incoming `inlineData` parts from the user's message as artifacts before appending the event. Each saved artifact is named `artifact__` and the original message part is replaced with a short text placeholder. +## Wiring the service -Requirements: +Without an `artifactService`, every `saveArtifact` and `loadArtifact` call will throw at runtime. The recommended way is to pass it through `AgentBuilder` using `.withArtifactService()`: -- An `artifactService` must be configured on the `Runner` -- Enable via `withRunConfig({ saveInputBlobsAsArtifacts: true })` on the agent builder, or per-run via `RunConfig` +```typescript +import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .build(); +``` + +If you're constructing the `Runner` directly, pass `artifactService` in the options: ```typescript import { - agentBuilder, Runner, + LlmAgent, InMemorySessionService, InMemoryArtifactService, - RunConfig, } from "@iqai/adk"; -// Configure at agent build time -const agent = agentBuilder() - .name("artifact_agent") - .model("gemini-2.5-flash") - .withRunConfig({ saveInputBlobsAsArtifacts: true }) - .build(); - const runner = new Runner({ appName: "my_app", - agent, + agent: new LlmAgent({ + name: "my_agent", + model: "gemini-2.5-flash", + description: "An agent that does things", + }), sessionService: new InMemorySessionService(), artifactService: new InMemoryArtifactService(), }); +``` -// Or enable per run -// await runner.runAsync({ -// userId, -// sessionId, -// newMessage, -// runConfig: new RunConfig({ saveInputBlobsAsArtifacts: true }) -// }); +Use `InMemoryArtifactService` during development and switch to `GcsArtifactService` for production. Both implement the same interface, so no other code needs to change. + +## Auto-save user uploads + +Normally, binary content sent by the user (images, files) arrives as `inlineData` parts inside the message and the agent has to process them itself. When `saveInputBlobsAsArtifacts` is enabled, the runner automatically saves each `inlineData` part as an artifact before the agent sees the message. The original part is replaced with a short text placeholder so the agent receives a clean, text-only message. + +Each saved artifact is named `artifact__`, where `index` is the position of the part in the message. + +To enable this for every run, set it at build time via `withRunConfig`: + +```typescript +import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("upload_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .withRunConfig({ saveInputBlobsAsArtifacts: true }) + .build(); ``` - +To enable it only for specific runs, pass a `RunConfig` to `runAsync` instead: -Artifacts saved by this mechanism use the incoming part's `inlineData` (base64 -string + MIME type) as-is. Ensure your front-end encodes uploads as base64 and -sets a correct `mimeType`. +```typescript +import { RunConfig } from "@iqai/adk"; - +const message = {}; -## Minimal example: sending an image upload +for await (const event of runner.runAsync({ + userId: "u1", + sessionId: "s1", + newMessage: message, + runConfig: new RunConfig({ saveInputBlobsAsArtifacts: true }), +})) { + // handle events +} +``` + +## Sending a file upload + +When auto-save is enabled, any message you send with `inlineData` parts will trigger it. Encode the raw bytes as base64 and set the correct `mimeType`: ```typescript +import { readFileSync } from "node:fs"; import type { Content } from "@google/genai"; -const imageBytes = Buffer.from(/* raw PNG bytes */); const message: Content = { role: "user", parts: [ { inlineData: { - data: imageBytes.toString("base64"), + data: readFileSync("./upload.png").toString("base64"), mimeType: "image/png", }, }, @@ -77,8 +105,28 @@ for await (const event of runner.runAsync({ sessionId: "s1", newMessage: message, })) { - // Events stream + // The image is auto-saved as "artifact__0" + // The agent receives a text placeholder instead of the raw bytes } - -// The image will be saved as an artifact, e.g. "artifact__0" ``` + + + The artifact name follows the pattern `artifact__`. If + the user sends multiple files in one message, they are saved as `_0`, `_1`, + `_2`, and so on. Your callbacks and tools can then load them by name. + + +## Next steps + + + + + diff --git a/apps/docs/content/docs/framework/artifacts/scoping-and-versioning.mdx b/apps/docs/content/docs/framework/artifacts/scoping-and-versioning.mdx index 8c5096a26..02287be09 100644 --- a/apps/docs/content/docs/framework/artifacts/scoping-and-versioning.mdx +++ b/apps/docs/content/docs/framework/artifacts/scoping-and-versioning.mdx @@ -1,27 +1,34 @@ --- title: Scoping & Versioning -description: Session vs user namespaces and how artifact versions work +description: Control artifact lifetime with session or user scope, and access any historical version in ADK-TS. --- +import { Callout } from "fumadocs-ui/components/callout"; +import { Cards, Card } from "fumadocs-ui/components/card"; + +The filename you pass to `saveArtifact` controls two things: where the artifact is stored and how long it persists. Every call to `saveArtifact` appends a new version — old versions are never overwritten and can always be retrieved. + ## Scoping -Artifacts can be scoped to the current session or to the user across sessions. Use the `user:` filename prefix to switch to user scope. +Artifacts are either session-scoped or user-scoped, determined entirely by the filename prefix: + +| Scope | Prefix | Persists across sessions? | Example | +| ------- | -------- | ------------------------- | ---------------------- | +| Session | _(none)_ | No | `"summary.txt"` | +| User | `user:` | Yes | `"user:settings.json"` | + +**Session-scoped** artifacts are tied to the current session and are no longer accessible once the session ends. Use them for temporary data — processing results, intermediate outputs, or anything that only makes sense in the context of one conversation. + +**User-scoped** artifacts (`user:` prefix) persist across all sessions for a given user within the same app. Use them for data that should survive between conversations — user preferences, profile data, or long-lived files. ```typescript import type { CallbackContext } from "@iqai/adk"; -export async function saveScopedArtifacts(ctx: CallbackContext) { - const text = (s: string) => ({ - inlineData: { - data: Buffer.from(s).toString("base64"), - mimeType: "text/plain", - }, - }); - - // Session-scoped: available only in this session - await ctx.saveArtifact("summary.txt", text("session summary")); +async function saveScopedArtifacts(ctx: CallbackContext) { + // Session-scoped: discarded when the session ends + await ctx.saveArtifact("summary.txt", { text: "session summary" }); - // User-scoped: available across all sessions for this user + // User-scoped: available in all future sessions for this user await ctx.saveArtifact("user:settings.json", { inlineData: { data: Buffer.from(JSON.stringify({ theme: "dark" })).toString("base64"), @@ -31,90 +38,76 @@ export async function saveScopedArtifacts(ctx: CallbackContext) { } ``` -Loading works the same way — the prefix determines where the service looks: +When loading, use the same prefix you used when saving — no additional arguments needed: ```typescript -const latest = await ctx.loadArtifact("summary.txt"); // session scope -const userConfig = await ctx.loadArtifact("user:settings.json"); // user scope +const sessionData = await ctx.loadArtifact("summary.txt"); // session scope +const userSettings = await ctx.loadArtifact("user:settings.json"); // user scope ``` ## Versioning -Each save creates a new version starting at 0. Loading without a `version` parameter returns the latest. +Each call to `saveArtifact` creates a new version, starting at `0`. Versions increment automatically — you cannot overwrite or delete a specific version. When you call `loadArtifact` without a version number, you always get the latest. ```typescript -// v0 -await ctx.saveArtifact("document.txt", { - inlineData: { - data: Buffer.from("A").toString("base64"), - mimeType: "text/plain", - }, -}); - -// v1 -await ctx.saveArtifact("document.txt", { - inlineData: { - data: Buffer.from("B").toString("base64"), - mimeType: "text/plain", - }, -}); +const v0 = await ctx.saveArtifact("report.txt", { text: "draft" }); // → 0 +const v1 = await ctx.saveArtifact("report.txt", { text: "final" }); // → 1 -// Load specific version -const v0 = await ctx.loadArtifact("document.txt", 0); - -// Load latest (v1) -const latestDoc = await ctx.loadArtifact("document.txt"); +const latest = await ctx.loadArtifact("report.txt"); // returns version 1 +const draft = await ctx.loadArtifact("report.txt", 0); // returns version 0 ``` -### Listing Versions +### Listing versions -The `listVersions` method (on the service, not context) can be used to find all existing version numbers for an artifact. Contexts do not expose version listing. +To see which versions exist for a file, call `listVersions` directly on the service. This method is not on context objects because it requires the full session coordinates (`appName`, `userId`, `sessionId`) that contexts manage internally. ```typescript import { InMemoryArtifactService } from "@iqai/adk"; const artifactService = new InMemoryArtifactService(); -// ... after saving some versions + const versions = await artifactService.listVersions({ appName: "my_app", userId: "u1", sessionId: "s1", - filename: "document.txt", + filename: "report.txt", }); -// e.g., [0, 1, 2] +// → [0, 1] ``` ## Deletion -Deletion removes all versions of a filename in the current scope. The `deleteArtifact` method permanently removes the artifact and all its versions. - - - -The `deleteArtifact` method is not available through `CallbackContext` or -`ToolContext` for safety reasons. You must access it directly through the -artifact service instance. +`deleteArtifact` removes all versions of a filename at once. Like `listVersions`, it lives on the service directly — not on context objects — because it's a destructive operation that requires explicit intent. + + Deletion permanently removes all versions. There is no way to recover them. + Always confirm the filename before calling this. -### Basic Deletion - ```typescript import { InMemoryArtifactService } from "@iqai/adk"; const artifactService = new InMemoryArtifactService(); -// Delete an artifact (removes all versions) await artifactService.deleteArtifact({ appName: "my_app", userId: "u1", sessionId: "s1", - filename: "document.txt", + filename: "summary.txt", }); ``` -### Deletion Behavior - -- **All Versions Removed**: Deletion removes all versions of the artifact, not just the latest -- **Scope-Aware**: Deletion respects the artifact's scope (session or user namespace) -- **Irreversible**: Once deleted, artifacts cannot be recovered (unless you have backups) -- **No Context Access**: Must be called directly on the artifact service, not through contexts +## Next steps + + + + + diff --git a/apps/docs/content/docs/framework/artifacts/service-implementations.mdx b/apps/docs/content/docs/framework/artifacts/service-implementations.mdx index f336c43b8..616973b61 100644 --- a/apps/docs/content/docs/framework/artifacts/service-implementations.mdx +++ b/apps/docs/content/docs/framework/artifacts/service-implementations.mdx @@ -1,335 +1,221 @@ --- title: Service Implementations -description: Different artifact service backends and their configurations for various deployment scenarios +description: Choose between in-memory and Google Cloud Storage artifact backends for your ADK-TS agents. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Tabs, Tab } from "fumadocs-ui/components/tabs"; +import { Cards, Card } from "fumadocs-ui/components/card"; -The artifact system is built around the `BaseArtifactService` interface, which allows you to choose different storage backends based on your requirements. Each implementation offers different trade-offs between performance, persistence, and scalability. +ADK-TS ships two artifact service implementations: `InMemoryArtifactService` for development and `GcsArtifactService` for production. Both implement the same `BaseArtifactService` interface, so switching between them requires no changes to your agent or tool code — only the service you pass to the `Runner` changes. -## BaseArtifactService Interface +## Choosing a service -All artifact services implement the same core interface: - -```typescript -import type { Part } from "@google/genai"; - -interface BaseArtifactService { - saveArtifact(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - artifact: Part; - }): Promise; - - loadArtifact(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - version?: number; - }): Promise; - - listArtifactKeys(args: { - appName: string; - userId: string; - sessionId: string; - }): Promise; - - deleteArtifact(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - }): Promise; - - listVersions(args: { - appName: string; - userId: string; - sessionId: string; - filename: string; - }): Promise; -} -``` - -This consistent interface allows you to switch between implementations without changing your agent code. - - - -The `deleteArtifact` method is available on all artifact service -implementations but is intentionally not exposed through context objects -(`CallbackContext` or `ToolContext`) for safety reasons. Access it directly -through the artifact service instance. See [Best -Practices](/docs/framework/artifacts/best-practices#artifact-cleanup-and-deletion) -for cleanup strategies. +| Service | Persistence | Use for | Setup | +| ------------------------- | ----------- | --------------------------- | ------------- | +| `InMemoryArtifactService` | None | Development and testing | Zero config | +| `GcsArtifactService` | Durable | Production | GCS bucket | +| Custom implementation | Varies | PostgreSQL, S3, Redis, etc. | You implement | + + `InMemoryArtifactService` loses all data when the process restarts. Never use + it in production — switch to `GcsArtifactService` or a custom backend. ## InMemoryArtifactService -Fast, temporary storage for development and testing scenarios. - -### Characteristics - -- **Storage**: Artifacts stored in memory only -- **Persistence**: Data lost when process terminates -- **Performance**: Fastest access, no network latency -- **Scalability**: Limited by available RAM -- **Use Cases**: Development, testing, prototyping - -### Setup and Configuration +Data lives in process memory with no configuration needed. It is the right choice for local development, unit tests, and examples — anywhere you don't need persistence. ```typescript -import { InMemoryArtifactService, Runner } from "@iqai/adk"; - -// Create the service (no configuration needed) -const artifactService = new InMemoryArtifactService(); +import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; -// Use with Runner -const runner = new Runner({ - appName: "my_app", - agent: myAgent, - sessionService: mySessionService, - artifactService, -}); +const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) + .build(); ``` -### Example Usage +You can also use the service directly — for example, in tests where you want to call the service API without a running agent: ```typescript import { InMemoryArtifactService } from "@iqai/adk"; -const artifactService = new InMemoryArtifactService(); - -// Save an artifact -const textArtifact = { - inlineData: { - data: Buffer.from("Hello, World!").toString("base64"), - mimeType: "text/plain", - }, -}; - -const version = await artifactService.saveArtifact({ - appName: "test_app", - userId: "user123", - sessionId: "session456", - filename: "greeting.txt", - artifact: textArtifact, -}); - -console.log(`Saved version: ${version}`); // 0 +const svc = new InMemoryArtifactService(); -// Load the artifact -const loaded = await artifactService.loadArtifact({ +const version = await svc.saveArtifact({ appName: "test_app", - userId: "user123", - sessionId: "session456", + userId: "u1", + sessionId: "s1", filename: "greeting.txt", + artifact: { text: "Hello, World!" }, }); +// version → 0 -if (loaded) { - const text = Buffer.from(loaded.inlineData.data, "base64").toString(); - console.log(`Loaded: ${text}`); // "Hello, World!" -} - -// Delete an artifact when no longer needed -await artifactService.deleteArtifact({ +const loaded = await svc.loadArtifact({ appName: "test_app", - userId: "user123", - sessionId: "session456", + userId: "u1", + sessionId: "s1", filename: "greeting.txt", }); +// loaded → { text: "Hello, World!" } ``` - - -In-memory storage is great for development and tests. Data is lost on restart. - - +## GcsArtifactService - +Backed by a Google Cloud Storage bucket — durable, replicated, and with no practical storage limit. This is the service to use in production. -Data stored in `InMemoryArtifactService` is lost when the process restarts. -Use only for development and testing. +Before using it, you need a GCP project, a GCS bucket, and a service account with `roles/storage.objectAdmin` on that bucket. - + + + By default, the service picks up credentials from Application Default Credentials (ADC). Run `gcloud auth application-default login` once locally, or set `GOOGLE_APPLICATION_CREDENTIALS` in production: -## GcsArtifactService + ```typescript + import { AgentBuilder, GcsArtifactService } from "@iqai/adk"; -Production-ready artifact storage using Google Cloud Storage. + const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new GcsArtifactService("my-artifacts-bucket")) + .build(); + ``` -### Characteristics + -- **Storage**: Google Cloud Storage buckets -- **Persistence**: Durable, replicated storage -- **Performance**: Network-dependent, highly optimized -- **Scalability**: Virtually unlimited -- **Use Cases**: Production deployments, large-scale applications + + If you need to specify credentials explicitly — for example, to use a service account key file instead of ADC — pass them as the second argument: -### Setup and Configuration + ```typescript + import { GcsArtifactService } from "@iqai/adk"; -```typescript -import { GcsArtifactService, Runner } from "@iqai/adk"; + const artifactService = new GcsArtifactService("my-artifacts-bucket", { + projectId: "my-gcp-project", + keyFilename: "/path/to/service-account.json", + }); + ``` -// Basic configuration -const artifactService = new GcsArtifactService("my-artifacts-bucket"); + -// Use with Runner -const runner = new Runner({ - appName: "production_app", - agent: myAgent, - sessionService: mySessionService, - artifactService, -}); -``` + + Run these commands once to create your bucket and grant the service account access: -### Google Cloud Setup (concise) + ```bash + # Create the bucket + gsutil mb gs://my-artifacts-bucket -```bash -# Create bucket -gsutil mb gs://my-artifacts-bucket + # Authenticate (choose one approach) + gcloud auth application-default login + export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" -# Auth (choose one) -export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" -gcloud auth application-default login + # Grant the service account storage access + gsutil iam ch serviceAccount:svc@project.iam.gserviceaccount.com:roles/storage.objectAdmin gs://my-artifacts-bucket + ``` -# Grant bucket permissions -gsutil iam ch serviceAccount:svc@project.iam.gserviceaccount.com:roles/storage.objectAdmin gs://my-artifacts-bucket -``` + + -### Storage Structure +### Storage layout -The GCS implementation organizes artifacts using a hierarchical structure: +GCS stores each artifact version as a separate blob. Understanding the path structure helps when browsing your bucket or debugging missing files: ``` -my-artifacts-bucket/ -├── app1/ -│ ├── user123/ -│ │ ├── session456/ -│ │ │ ├── temp_file.txt/ -│ │ │ │ ├── 0 (version 0) -│ │ │ │ └── 1 (version 1) -│ │ │ └── analysis.json/ -│ │ │ └── 0 -│ │ └── user/ -│ │ └── user:profile.png/ -│ │ ├── 0 -│ │ └── 1 -│ └── user456/ -│ └── ... -└── app2/ - └── ... -``` - -### Example Usage - -```typescript -import { GcsArtifactService } from "@iqai/adk"; - -const artifactService = new GcsArtifactService("my-artifacts-bucket"); - -// Save a large binary file -const imageData = Buffer.from(/* your image data */); -const imageArtifact = { - inlineData: { - data: imageData.toString("base64"), - mimeType: "image/png", - }, -}; - -try { - const version = await artifactService.saveArtifact({ - appName: "photo_app", - userId: "user789", - sessionId: "session123", - filename: "user:profile_picture.png", - artifact: imageArtifact, - }); - - console.log(`Image saved as version ${version}`); - - // List all artifact filenames for this user/session - const artifactKeys = await artifactService.listArtifactKeys({ - appName: "photo_app", - userId: "user789", - sessionId: "session123", - }); - - console.log("Available artifacts:", artifactKeys); - // ['user:profile_picture.png', ...] - - // Delete an artifact when no longer needed - await artifactService.deleteArtifact({ - appName: "photo_app", - userId: "user789", - sessionId: "session123", - filename: "temp_thumbnail.png", - }); -} catch (error) { - console.error("GCS operation failed:", error); -} +{appName}/{userId}/{sessionId}/{filename}/{version} ← session-scoped +{appName}/{userId}/user/{filename}/{version} ← user-scoped (user: prefix) ``` -### Performance tips +The `user:` prefix on a filename swaps the `{sessionId}` segment for `user`, which is why user-scoped artifacts are visible across all sessions. -- Batch saves with `Promise.all([...])` -- Prefer JSON/text compression when feasible -- Consider chunking for very large payloads +## Custom service -See Troubleshooting for common GCS and service wiring issues. - -### Using StorageOptions +If you need a different backend — PostgreSQL, S3, Redis, or anything else — implement the `BaseArtifactService` interface. Your class only needs five methods: ```typescript -import { GcsArtifactService } from "@iqai/adk"; - -const artifactService = new GcsArtifactService("my-artifacts-bucket", { - projectId: "my-project", - keyFilename: "/path/to/key.json", // optional; or rely on ADC -}); -``` - - - -The GCS service automatically handles retries for transient errors and -provides built-in redundancy and durability. - - - -## Custom Service Implementation - -You can implement your own artifact service by extending the `BaseArtifactService` interface for specific requirements like database storage or caching layers. - -## Service Selection Guide - -Choose the appropriate artifact service based on your requirements: +import type { BaseArtifactService } from "@iqai/adk"; +import type { Part } from "@google/genai"; -### Development and Testing +class MyDatabaseArtifactService implements BaseArtifactService { + async saveArtifact(args: { + appName: string; + userId: string; + sessionId: string; + filename: string; + artifact: Part; + }): Promise { + // Write the artifact to your database, return the new version number + return 0; + } -- **Use**: `InMemoryArtifactService` -- **Reasons**: Fast, simple setup, no external dependencies -- **Limitations**: Data lost on restart + async loadArtifact(args: { + appName: string; + userId: string; + sessionId: string; + filename: string; + version?: number; + }): Promise { + // Return the artifact Part, or undefined if not found + return undefined; + } -### Production Applications + async listArtifactKeys(args: { + appName: string; + userId: string; + sessionId: string; + }): Promise { + // Return all filenames for this session + return []; + } -- **Use**: `GcsArtifactService` or custom database service -- **Reasons**: Durable storage, scalability, backup/recovery -- **Considerations**: Network latency, cost, compliance requirements + async deleteArtifact(args: { + appName: string; + userId: string; + sessionId: string; + filename: string; + }): Promise { + // Delete all versions of this filename + } -### Hybrid Scenarios + async listVersions(args: { + appName: string; + userId: string; + sessionId: string; + filename: string; + }): Promise { + // Return all version numbers for this filename + return []; + } +} +``` -- **Use**: Combination approach with caching -- **Implementation**: Database/GCS for persistence + Redis for caching -- **Benefits**: Fast access with durability +Pass your custom service to the runner the same way as the built-in ones: -### Performance Comparison +```typescript +import { AgentBuilder } from "@iqai/adk"; -| Service Type | Read Speed | Write Speed | Durability | Scalability | Setup Complexity | -| ------------ | ---------- | ----------- | ---------- | ----------- | ---------------- | -| InMemory | Fastest | Fastest | None | RAM-limited | Minimal | -| GCS | Fast | Fast | High | Unlimited | Moderate | -| Database | Moderate | Moderate | High | High | High | -| Redis | Very Fast | Very Fast | Moderate | High | Moderate | +const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new MyDatabaseArtifactService()) + .build(); +``` -Choose based on your specific requirements for performance, durability, and operational complexity. +## Troubleshooting + +| Issue | Fix | +| --------------------------------------- | ------------------------------------------------------------------------------------ | +| `"Artifact service is not initialized"` | Pass `artifactService` to `Runner` or use `.withArtifactService()` on `AgentBuilder` | +| GCS `403 Forbidden` | Service account needs `roles/storage.objectAdmin` on the bucket | +| GCS `404 Not Found` on load | Artifact doesn't exist at that path or version — `loadArtifact` returns `undefined` | +| Data missing after restart | `InMemoryArtifactService` doesn't persist — switch to `GcsArtifactService` for prod | + +## Next steps + + + + + diff --git a/apps/docs/content/docs/framework/artifacts/troubleshooting.mdx b/apps/docs/content/docs/framework/artifacts/troubleshooting.mdx index 0f4e01ba2..e8aa8f381 100644 --- a/apps/docs/content/docs/framework/artifacts/troubleshooting.mdx +++ b/apps/docs/content/docs/framework/artifacts/troubleshooting.mdx @@ -1,60 +1,112 @@ --- -title: Troubleshooting Guide for Artifacts -description: Common issues and quick fixes for artifact usage +title: Troubleshooting +description: Quick fixes for common artifact errors in ADK-TS — missing service, null returns, GCS auth, and base64 issues. --- import { Callout } from "fumadocs-ui/components/callout"; +import { Cards, Card } from "fumadocs-ui/components/card"; -This guide addresses common issues encountered when working with artifacts in the ADK-TS framework, along with practical solutions to help you resolve them quickly. +Most artifact errors fall into one of three categories: the service isn't wired up, a file doesn't exist where you expect it, or a GCS credential or permission is misconfigured. The sections below cover each with a diagnosis and fix. + For general ADK-TS issues (runner errors, session problems, model errors), see + the [framework troubleshooting guide](/docs/framework/guides/troubleshooting). + -For general troubleshooting, see the [Troubleshooting -Guide](/docs/framework/guides/troubleshooting). +## Common issues - +| Issue | Cause | Fix | +| --------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `"Artifact service is not initialized"` | No service passed to `Runner` | Call `.withArtifactService()` on `AgentBuilder`, or pass `artifactService` to `Runner` directly | +| `loadArtifact` returns `undefined` | Artifact or version doesn't exist | Check the filename, scope (`user:` prefix for user-scoped), and version number | +| Auto-save (`saveInputBlobsAsArtifacts`) not working | Service not wired, or wrong part type | Confirm `artifactService` is configured. Message parts must use `inlineData`, not `text` | +| GCS `403 Forbidden` | Missing IAM permissions | Service account needs `roles/storage.objectAdmin` on the bucket. Verify ADC or `keyFilename` | +| GCS `412 Precondition Failed` on save | Concurrent write conflict | Retry after a short delay — the GCS service uses `ifGenerationMatch: 0` which fails on race conditions | +| Wrong data after load | Loaded an old version | Omit the `version` argument to always get the latest, or pass the exact version number | +| `artifact.inlineData` is `undefined` | Artifact was saved as `{ text }` | Check `artifact.text` instead of `artifact.inlineData.data` | +| `deleteArtifact` not found on context | By design — not exposed on contexts | Call it directly on the service instance, not through `CallbackContext` or `ToolContext` | + +## Wiring check + +If you're seeing `"Artifact service is not initialized"`, verify that the service is actually passed to the runner. The recommended way is through `AgentBuilder`: -## "Artifact service is not initialized" +```typescript +import { AgentBuilder, InMemoryArtifactService } from "@iqai/adk"; + +const { runner } = await AgentBuilder.create("my_agent") + .withModel("gemini-2.5-flash") + .withArtifactService(new InMemoryArtifactService()) // ← required + .build(); +``` -Ensure your `Runner` is constructed with an `artifactService`. +If you're constructing `Runner` directly, pass it in the options object: ```typescript import { Runner, - InMemoryArtifactService, + LlmAgent, InMemorySessionService, + InMemoryArtifactService, } from "@iqai/adk"; const runner = new Runner({ appName: "my_app", - agent, + agent: new LlmAgent({ name: "my_agent", model: "gemini-2.5-flash" }), sessionService: new InMemorySessionService(), - artifactService: new InMemoryArtifactService(), + artifactService: new InMemoryArtifactService(), // ← required }); ``` -## Auto-save not working +## Safe load pattern -- Set `withRunConfig({ saveInputBlobsAsArtifacts: true })` on your agent builder, or pass `RunConfig` per run -- Confirm incoming message parts use `inlineData` with base64 `data` and correct `mimeType` +`loadArtifact` returns `undefined` if the artifact doesn't exist or the version is out of range. Always guard the return value before accessing its fields, and check both `text` and `inlineData` since either can be present depending on how the artifact was saved: -## GCS permission/auth failures +```typescript +const artifact = await ctx.loadArtifact("maybe_exists.txt"); + +if (!artifact) { + return { error: "not found" }; +} + +if (artifact.text) { + console.log(artifact.text); +} else if (artifact.inlineData) { + const content = Buffer.from(artifact.inlineData.data, "base64").toString( + "utf-8", + ); + console.log(content); +} +``` -- Verify ADC or service account credentials are available -- Check IAM roles: reader/writer on the bucket for your principal +## GCS auth check -## MIME/base64 pitfalls +If you're getting `403 Forbidden` from GCS, run these commands to verify your credentials are active and the service account has the right permissions on the bucket: -- `inlineData.data` must be a base64 string, not raw bytes -- Always set an accurate `mimeType` (e.g., `application/json`, `image/png`) +```bash +# Confirm active credentials can generate an access token +gcloud auth application-default print-access-token -## Loading returns null/undefined +# Inspect the bucket's IAM policy — look for your service account with roles/storage.objectAdmin +gsutil iam get gs://my-artifacts-bucket +``` -- If the artifact or version doesn’t exist, `loadArtifact` returns no value -- Handle the nullish result before decoding the data +If your service account is missing from the output, grant access: -```typescript -const art = await ctx.loadArtifact("maybe_exists.txt"); -if (!art) return { error: "not found" }; -const text = Buffer.from(art.inlineData.data, "base64").toString("utf-8"); +```bash +gsutil iam ch serviceAccount:svc@project.iam.gserviceaccount.com:roles/storage.objectAdmin gs://my-artifacts-bucket ``` + +## Next steps + + + + + From 9fd14206c5d05e854ac4a50cd4f3fbe2955cabf1 Mon Sep 17 00:00:00 2001 From: timonwa Date: Thu, 30 Apr 2026 12:49:54 +0100 Subject: [PATCH 2/4] Fix type mismatches and restore index to sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - best-practices.mdx: loadWithCache return type Part|undefined → Part|null to match BaseArtifactService interface - service-implementations.mdx: custom service loadArtifact return type Part|undefined → Part|null to match interface - meta.json: restore "index" to pages array so artifacts overview appears in sidebar Co-Authored-By: Claude Sonnet 4.6 --- .../content/docs/framework/artifacts/best-practices.mdx | 2 +- apps/docs/content/docs/framework/artifacts/meta.json | 1 + .../docs/framework/artifacts/service-implementations.mdx | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/docs/framework/artifacts/best-practices.mdx b/apps/docs/content/docs/framework/artifacts/best-practices.mdx index 7a8723ac4..d422a2c61 100644 --- a/apps/docs/content/docs/framework/artifacts/best-practices.mdx +++ b/apps/docs/content/docs/framework/artifacts/best-practices.mdx @@ -28,7 +28,7 @@ async function loadWithCache( filename: string; version?: number; }, -): Promise { +): Promise { const k = `${key.appName}:${key.userId}:${key.sessionId}:${key.filename}:${key.version ?? "latest"}`; const hit = cache.get(k); if (hit && Date.now() - hit.ts < TTL_MS) return hit.part; diff --git a/apps/docs/content/docs/framework/artifacts/meta.json b/apps/docs/content/docs/framework/artifacts/meta.json index 1b3db543b..497947f63 100644 --- a/apps/docs/content/docs/framework/artifacts/meta.json +++ b/apps/docs/content/docs/framework/artifacts/meta.json @@ -1,6 +1,7 @@ { "icon": "FileText", "pages": [ + "index", "runner-configuration", "context-integration", "scoping-and-versioning", diff --git a/apps/docs/content/docs/framework/artifacts/service-implementations.mdx b/apps/docs/content/docs/framework/artifacts/service-implementations.mdx index 616973b61..f774a7850 100644 --- a/apps/docs/content/docs/framework/artifacts/service-implementations.mdx +++ b/apps/docs/content/docs/framework/artifacts/service-implementations.mdx @@ -150,9 +150,9 @@ class MyDatabaseArtifactService implements BaseArtifactService { sessionId: string; filename: string; version?: number; - }): Promise { - // Return the artifact Part, or undefined if not found - return undefined; + }): Promise { + // Return the artifact Part, or null if not found + return null; } async listArtifactKeys(args: { From 053edcb832310c16ac73b9c249f3f111d029c88c Mon Sep 17 00:00:00 2001 From: timonwa Date: Thu, 30 Apr 2026 12:54:32 +0100 Subject: [PATCH 3/4] Remove "index" page from artifacts documentation meta --- apps/docs/content/docs/framework/artifacts/meta.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/docs/content/docs/framework/artifacts/meta.json b/apps/docs/content/docs/framework/artifacts/meta.json index 497947f63..1b3db543b 100644 --- a/apps/docs/content/docs/framework/artifacts/meta.json +++ b/apps/docs/content/docs/framework/artifacts/meta.json @@ -1,7 +1,6 @@ { "icon": "FileText", "pages": [ - "index", "runner-configuration", "context-integration", "scoping-and-versioning", From b07fba79605918800356cb6487de90d88b14b982 Mon Sep 17 00:00:00 2001 From: timonwa Date: Mon, 4 May 2026 11:37:39 +0100 Subject: [PATCH 4/4] Export BaseArtifactService from artifacts package Co-Authored-By: Claude Sonnet 4.6 --- packages/adk/src/artifacts/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adk/src/artifacts/index.ts b/packages/adk/src/artifacts/index.ts index 498418c50..9490af84c 100644 --- a/packages/adk/src/artifacts/index.ts +++ b/packages/adk/src/artifacts/index.ts @@ -4,5 +4,6 @@ export { type ParsedArtifactUri, parseArtifactUri, } from "./artifact-util"; +export type { BaseArtifactService } from "./base-artifact-service"; export { GcsArtifactService } from "./gcs-artifact-service"; export { InMemoryArtifactService } from "./in-memory-artifact-service";