TypeScriptADK-TS
Artifacts

Artifacts

Manage named, versioned binary data for rich agent interactions with files and media

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.

Overview

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.

Core Characteristics

  • 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

Beyond Text Storage

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:

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

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_<invocationId>_<index> and the corresponding message part is replaced with a short text placeholder.

  • 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
import { agentBuilder, Runner, InMemorySessionService, 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

Artifacts tied to individual conversation sessions:

// 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:

// "user:" prefix - user scoped
await callbackContext.saveArtifact('user:profile_picture.png', imageArtifact);

Characteristics:

  • Available across all user sessions within the application
  • Persist beyond individual conversations
  • Enable long-term user data management

Namespace Selection

Choose appropriate artifact scoping based on data lifecycle requirements - session for temporary content, user for persistent data.

Versioning System

Artifacts automatically maintain version history:

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

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

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.

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:

Documentation Structure

Error Handling

Always implement proper error handling for artifact operations:

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

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()
    };
  }
}

How is this guide?