Agent Studio

Call Context

State management and variable resolution during voice calls

Call Context

The Call Context is the state container for a voice call session. It holds all data that flows between agents, tools, and prompts during a conversation.

Overview

┌─────────────────────────────────────────────────────────────────┐
│                         CallContext                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │    user      │  │   workflow   │  │    flags     │          │
│  │  (immutable) │  │   (shared)   │  │  (booleans)  │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
│                                                                  │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │    agents    │  │    params    │  │   history    │          │
│  │  (isolated)  │  │  (transient) │  │   (trace)    │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Context Namespaces

user.* - User Data

User-specific data provided when the call is dispatched. Immutable during the call.

# Backend dispatches call with user context
await client.post("/api/v1/calls", json={
    "workflow_slug": "meal-logging",
    "user_context": {
        "name": "Rahul",
        "language": "hi",
        "language_name": "Hindi",
        "voice_id": "a167e0f3-df7e-4e3e-a5b4-3b3a6f3b8b3a",
        "pending_meals": ["Breakfast", "Lunch"],
        "logged_meals": []
    }
})

Access in prompts:

Hello {{user.name}}! Let's log your {{user.pending_meals[0]}}.

workflow.* - Shared State

State that persists across agent handoffs. Modified by tools during the conversation.

{
  "workflow": {
    "logged_meals": [
      {"meal_type": "Breakfast", "items": ["2 idli", "sambar"]}
    ],
    "pending_meals": ["Lunch", "Dinner"],
    "current_meal": "Lunch",
    "meal_count": 1
  }
}

Access in prompts:

You've logged {{workflow.meal_count}} meals so far.

flags.* - Boolean Flags

Simple boolean flags for conditional logic.

{
  "flags": {
    "all_meals_logged": false,
    "skip_feedback": true,
    "user_requested_end": false
  }
}

Used for:

  • Tool conditions ("conditions": ["all_meals_logged"])
  • Handoff triggers
  • Workflow branching

agents.* - Per-Agent State

Isolated state for each agent. Not shared across handoffs.

{
  "agents": {
    "meal-agent": {
      "last_meal_logged": "Breakfast",
      "questions_asked": 3
    },
    "feedback-agent": {
      "feedback_given": false
    }
  }
}

params.* - Tool Parameters

Transient namespace containing current tool call parameters. Reset on each tool invocation.

{
  "params": {
    "meal_type": "Breakfast",
    "ingredients": "2-piece-idli, 1-bowl-sambar",
    "meal_time": "2024-01-15T08:30:00"
  }
}

Path Syntax

Context values are accessed using dot-notation paths.

Basic Paths

user.name              → "Rahul"
workflow.meal_count    → 3
flags.all_meals_logged → true

Nested Paths

user.preferences.language    → "hi"
workflow.current_meal.type   → "Breakfast"

Array Access

user.pending_meals[0]        → "Breakfast"
workflow.logged_meals[1]     → {...}

Array Append

Special [+] suffix appends to an array:

workflow.logged_meals[+]     → Appends new item

Template Resolution

Templates use {{path}} syntax to inject context values into strings.

In Prompts

{
  "prompt": {
    "system": "You are helping {{user.name}} log meals in {{user.language_name}}.",
    "greeting": "Hi {{user.name}}! What did you have for {{workflow.pending_meals[0]}}?"
  }
}

In Tool Actions

{
  "actions": [
    {
      "type": "webhook",
      "url": "https://api.example.com/meals/{{params.meal_id}}",
      "body": {
        "user_id": "{{user.id}}",
        "meal_type": "{{params.meal_type}}"
      }
    }
  ]
}

In Tool Responses

{
  "on_success": {
    "response": "Great! I've logged your {{params.meal_type}}. {{workflow.pending_meals_count}} meals left."
  }
}

Context Flow

1. Call Dispatch

Backend provides initial user_context:

response = await client.post("/api/v1/calls", json={
    "workflow_slug": "meal-logging",
    "user_context": {
        "name": "Priya",
        "language": "ta",
        "pending_meals": ["Breakfast", "Lunch", "Dinner"],
        "logged_meals": [],
        "voice_id": "tamil-female-voice"
    }
})

2. Context Initialization

Worker initializes CallContext:

context = CallContext(
    call_id=call_id,
    tenant_id=tenant_id,
    workflow_id=workflow_id,
    user=user_context,  # Immutable
    workflow=workflow.shared_context,  # From workflow config
)

3. Prompt Resolution

Before speaking, prompts are resolved:

# Template
greeting = "Hi {{user.name}}! Let's log {{user.pending_meals[0]}}."

# Resolved
greeting = "Hi Priya! Let's log Breakfast."

4. Tool Execution

Tools modify context via actions:

{
  "actions": [
    {
      "type": "context.set",
      "path": "workflow.logged_meals[+]",
      "value": {
        "meal_type": "{{params.meal_type}}",
        "items": "{{params.ingredients}}"
      }
    },
    {
      "type": "context.set",
      "path": "workflow.pending_meals",
      "value": ["Lunch", "Dinner"]
    }
  ]
}

5. Agent Handoffs

On handoff, context is preserved:

# Before handoff
context.workflow = {"meal_count": 1, "logged_meals": [...]}

# After handoff to feedback-agent
# Same context.workflow is available

Context Actions

Tools modify context using the context.set action type.

Set Value

{
  "type": "context.set",
  "path": "workflow.current_meal",
  "value": "Lunch"
}

Append to Array

{
  "type": "context.set",
  "path": "workflow.logged_meals[+]",
  "value": {
    "meal_type": "{{params.meal_type}}",
    "logged_at": "{{params.meal_time}}"
  }
}

Set Flag

{
  "type": "context.set",
  "path": "flags.all_meals_logged",
  "value": true
}

Computed Values

For dynamic values, use backend-computed fields in user_context:

# Backend computes before dispatch
user_context = {
    "name": user.name,
    "pending_meals": get_pending_meals(user.id),
    "pending_meals_display": ", ".join(get_pending_meals(user.id)),
    "meal_count": len(get_logged_meals(user.id)),
    "greeting": generate_greeting(user.language, user.name)
}

Multi-Tenant Isolation

Each tenant's calls have completely isolated contexts:

┌─────────────────────┐    ┌─────────────────────┐
│    Tenant A Call    │    │    Tenant B Call    │
├─────────────────────┤    ├─────────────────────┤
│ call_id: abc-123    │    │ call_id: xyz-789    │
│ tenant_id: tenant-a │    │ tenant_id: tenant-b │
│ user: {...}         │    │ user: {...}         │
│ workflow: {...}     │    │ workflow: {...}     │
└─────────────────────┘    └─────────────────────┘
  • No data leakage between tenants
  • Each call has its own CallContext instance
  • Context is never persisted across calls (stateless)

Best Practices

1. Use user.* for Input Data

# Good: Backend provides all user data
user_context = {
    "name": user.name,
    "language": user.language,
    "pending_meals": get_pending_meals(user.id)
}

2. Use workflow.* for Shared State

{
  "path": "workflow.logged_meals[+]",
  "value": {"meal_type": "Breakfast"}
}

3. Use flags.* for Conditions

{
  "handoffs": [
    {
      "target_agent": "feedback-agent",
      "conditions": ["all_meals_logged"]
    }
  ]
}

4. Provide Defaults in Variables

{
  "variables": [
    { "name": "user.name", "default": "there" },
    { "name": "user.language_name", "default": "Hindi" }
  ]
}

5. Pre-compute Display Strings

# Backend computes formatted strings
user_context = {
    "pending_meals": ["Breakfast", "Lunch"],
    "pending_meals_display": "Breakfast and Lunch",  # Pre-formatted
    "logged_meals_display": "None yet"
}

API Reference

CallContext Methods

MethodDescription
get(path, default)Get value by path with optional default
set(path, value)Set value by path (supports [+] append)
delete(path)Delete value at path
resolve(template)Resolve {{path}} placeholders
resolve_dict(data)Recursively resolve templates in dict
get_agent_state(agent)Get agent-specific state
set_agent_state(key, value)Set agent-specific state
switch_agent(name)Switch current agent
trigger_handoff(target)Trigger handoff to agent
to_dict()Export context to dictionary
from_dict(data)Create context from dictionary

Path Examples

# Get values
ctx.get("user.name")                    # → "Rahul"
ctx.get("workflow.meals[0].type")       # → "Breakfast"
ctx.get("flags.done", False)            # → False (default)

# Set values
ctx.set("workflow.current_meal", "Lunch")
ctx.set("workflow.logged_meals[+]", {"type": "Breakfast"})
ctx.set("flags.all_done", True)

# Resolve templates
ctx.resolve("Hi {{user.name}}!")        # → "Hi Rahul!"

On this page