Skip to main content
ADK-TS provides a sophisticated state management system with automatic scoping, delta tracking, and persistence. State can be session-specific, user-wide, or application-global.

State Scoping

State is automatically scoped using prefixes:

Session State

No prefix - Scoped to current sessionTemporary data for this conversation

User State

user: prefix - Shared across user’s sessionsPersistent user preferences

App State

app: prefix - Shared across all usersGlobal configuration and statistics

Temp State

temp: prefix - Never persistedScratch data for current invocation

State Class

The State class provides a dict-like interface with delta tracking:
// packages/adk/src/sessions/state.ts:4
export class State {
  static readonly APP_PREFIX = "app:";
  static readonly USER_PREFIX = "user:";
  static readonly TEMP_PREFIX = "temp:";
  
  private readonly _value: Record<string, any>;
  private readonly _delta: Record<string, any>;
  
  get(key: string, defaultValue?: any): any;
  set(key: string, value: any): void;
  has(key: string): boolean;
  hasDelta(): boolean;
  update(delta: Record<string, any>): void;
  toDict(): Record<string, any>;
}

Creating State

import { State } from '@iqai/adk';

// Create with initial values and empty delta
const state = State.create(
  { counter: 0 },  // Current value
  {}               // Delta (changes)
);

// Array-like access
console.log(state['counter']); // 0
state['counter'] = 5;

// Method access
state.set('name', 'Alice');
console.log(state.get('name')); // 'Alice'

// Check for pending changes
if (state.hasDelta()) {
  console.log('State has uncommitted changes');
}

// Get full state dict
const dict = state.toDict();

Using State in Agents

State is automatically managed by the session:
Default scope for conversation-specific data:
// In a tool or agent
const currentCount = session.state['counter'] || 0;

// Update state
return {
  stateDelta: {
    counter: currentCount + 1,
    lastAction: 'increment',
  },
};
Example:
// Counter tool from examples
async execute(args: CounterToolArgs, context: ToolContext) {
  const session = context.session;
  const currentValue = session.state[args.name] || 0;
  const newValue = currentValue + args.delta;
  
  return {
    content: `Counter '${args.name}' is now ${newValue}`,
    stateDelta: {
      [args.name]: newValue,
    },
  };
}

State Delta Tracking

Only changed state is persisted, improving performance:

How Delta Tracking Works

// Initial state
const state = {
  counter: 0,
  name: 'Alice',
  items: [],
};

// First update - only counter changes
const delta1 = {
  counter: 1,
};

// Second update - add new field
const delta2 = {
  items: ['apple', 'banana'],
  counter: 2,
};

// Final state combines original + deltas
const finalState = {
  counter: 2,                    // Updated twice
  name: 'Alice',                 // Unchanged
  items: ['apple', 'banana'],    // Added
};

In Session Events

// packages/adk/src/sessions/base-session-service.ts:116
protected updateSessionState(session: Session, event: Event): void {
  if (!event.actions?.stateDelta) {
    return;
  }
  
  for (const key in event.actions.stateDelta) {
    if (key.startsWith('temp_')) {
      continue; // Skip temp state
    }
    
    const value = event.actions.stateDelta[key];
    if (value === null || value === undefined) {
      delete session.state[key];  // Remove key
    } else {
      session.state[key] = value; // Update or add
    }
  }
}

State in Tools

Tools can read and modify state via ToolContext:
import { BaseTool, ToolContext } from '@iqai/adk';
import { z } from 'zod';

class CounterTool extends BaseTool<typeof CounterTool.argsSchema> {
  name = 'counter';
  description = 'Increment or read a named counter';
  
  static argsSchema = z.object({
    name: z.string().describe('Counter name'),
    delta: z.number().default(1).describe('Amount to increment by'),
  });
  
  async execute(args: z.infer<typeof CounterTool.argsSchema>, context: ToolContext) {
    const session = context.session;
    
    // Read current value
    const currentValue = session.state[args.name] || 0;
    const newValue = currentValue + args.delta;
    
    // Return state delta
    return {
      content: `Counter '${args.name}': ${currentValue}${newValue}`,
      stateDelta: {
        [args.name]: newValue,
        'user:total_increments': (session.state['user:total_increments'] || 0) + 1,
      },
    };
  }
}

State Persistence

How different session services handle state:
Maintains state in memory with three separate maps:
// packages/adk/src/sessions/in-memory-session-service.ts:14
class InMemorySessionService {
  // Session state
  private sessions: Map<string, Map<string, Map<string, Session>>>;
  
  // User state  
  private userState: Map<string, Map<string, Map<string, any>>>;
  
  // App state
  private appState: Map<string, Map<string, any>>;
  
  // Merge states when retrieving session
  private mergeState(appName: string, userId: string, session: Session): Session {
    // Add app state with prefix
    for (const [key, value] of this.appState.get(appName)?.entries() || []) {
      session.state[`app:${key}`] = value;
    }
    
    // Add user state with prefix
    for (const [key, value] of this.userState.get(appName)?.get(userId)?.entries() || []) {
      session.state[`user:${key}`] = value;
    }
    
    return session;
  }
}

State Best Practices

Use Session State when:
  • Data is specific to this conversation
  • Should reset for new conversations
  • Temporary working data
Use User State when:
  • Data should persist across sessions
  • User preferences and settings
  • Accumulated user-specific data
Use App State when:
  • Data shared by all users
  • Global configuration
  • System-wide statistics
Use Temp State when:
  • Data only needed for this invocation
  • Intermediate calculations
  • Not worth persisting
  • Keep state flat when possible
  • Use descriptive key names
  • Document your state schema
  • Validate state before use
  • Set default values for missing keys
// Good: Flat structure with defaults
const language = session.state['user:language'] || 'en';
const counter = session.state['counter'] || 0;

// Avoid: Deep nesting
const config = session.state['config'] || {};
const theme = config.ui?.theme || 'light'; // Fragile
  • Only include changed fields in stateDelta
  • Avoid storing large objects in state
  • Use artifacts for file data
  • Clean up unused state keys
  • Consider state size for context limits
// Good: Only delta
return {
  stateDelta: {
    counter: newValue,
  },
};

// Bad: Entire state
return {
  stateDelta: {
    ...session.state,
    counter: newValue,
  },
};
  • Use TypeScript interfaces for state
  • Validate state shape
  • Provide type guards
  • Document expected types
// Define state schema
interface MySessionState {
  counter: number;
  items: string[];
  lastUpdate: number;
}

interface MyUserState {
  'user:language': string;
  'user:theme': 'light' | 'dark';
}

// Type-safe access
function getCounter(session: Session): number {
  return (session.state.counter as number) || 0;
}

Complete Example

import { AgentBuilder, BaseTool, ToolContext } from '@iqai/adk';
import { z } from 'zod';

// Define state schema
interface CartState {
  items: string[];              // Session: current cart
  total: number;                // Session: cart total
  'user:rewards_points': number; // User: loyalty points
  'app:total_orders': number;   // App: global counter
}

// Cart tool that uses all state scopes
class CartTool extends BaseTool<typeof CartTool.argsSchema> {
  name = 'cart';
  description = 'Manage shopping cart';
  
  static argsSchema = z.object({
    action: z.enum(['add', 'remove', 'checkout']),
    item: z.string().optional(),
    price: z.number().optional(),
  });
  
  async execute(args: z.infer<typeof CartTool.argsSchema>, context: ToolContext) {
    const state = context.session.state;
    
    // Get current state with defaults
    const items = (state.items as string[]) || [];
    const total = (state.total as number) || 0;
    const userPoints = (state['user:rewards_points'] as number) || 0;
    const appOrders = (state['app:total_orders'] as number) || 0;
    
    const delta: Record<string, any> = {};
    let response = '';
    
    switch (args.action) {
      case 'add':
        delta.items = [...items, args.item!];
        delta.total = total + (args.price || 0);
        delta['temp:last_added'] = args.item; // Not persisted
        response = `Added ${args.item} to cart`;
        break;
        
      case 'remove':
        delta.items = items.filter(i => i !== args.item);
        response = `Removed ${args.item} from cart`;
        break;
        
      case 'checkout':
        delta.items = [];
        delta.total = 0;
        delta['user:rewards_points'] = userPoints + Math.floor(total / 10);
        delta['app:total_orders'] = appOrders + 1;
        response = `Checkout complete! Earned ${Math.floor(total / 10)} points.`;
        break;
    }
    
    return {
      content: response,
      stateDelta: delta,
    };
  }
}

// Create agent with state
const { runner, session } = await AgentBuilder
  .withModel('gpt-4')
  .withTools(new CartTool())
  .withSessionService(sessionService)
  .build({
    userId: 'user-123',
  });

// Use the agent
await runner.ask('Add pizza for $15');
await runner.ask('Add soda for $3');
await runner.ask('What is in my cart?');
await runner.ask('Checkout');

// State is automatically managed
console.log('Session state:', session.state.items, session.state.total);
console.log('User points:', session.state['user:rewards_points']);
console.log('Total orders:', session.state['app:total_orders']);

Debugging State

import { Session } from '@iqai/adk';

function debugState(session: Session) {
  console.log('\n=== Session State ===' );
  
  // Separate by scope
  const sessionState: Record<string, any> = {};
  const userState: Record<string, any> = {};
  const appState: Record<string, any> = {};
  const tempState: Record<string, any> = {};
  
  for (const [key, value] of Object.entries(session.state)) {
    if (key.startsWith('user:')) {
      userState[key] = value;
    } else if (key.startsWith('app:')) {
      appState[key] = value;
    } else if (key.startsWith('temp:')) {
      tempState[key] = value;
    } else {
      sessionState[key] = value;
    }
  }
  
  console.log('Session:', sessionState);
  console.log('User:', userState);
  console.log('App:', appState);
  console.log('Temp:', tempState);
}

State Migration

When changing state structure:
// Add migration logic to handle old state format
class MigrationTool extends BaseTool {
  async execute(args: any, context: ToolContext) {
    const state = context.session.state;
    const delta: Record<string, any> = {};
    
    // Migrate from old format
    if (state.old_counter !== undefined) {
      delta.counter = state.old_counter;
      delta.old_counter = null; // Remove old key
    }
    
    // Add new fields with defaults
    if (state.version === undefined) {
      delta.version = '2.0';
    }
    
    return {
      content: 'State migrated',
      stateDelta: delta,
    };
  }
}

Build docs developers (and LLMs) love