6  Context Sharing

Tools often need shared state. Let’s add context injection.

Note

Code Reference: code/v0.3/src/agentsilex/

  • agent.py
  • runner.py

6.1 The Problem

Consider a shopping assistant with multiple tools:

@tool
def add_to_cart(item: str, price: float) -> str:
    """Add item to cart."""
    cart.append({"item": item, "price": price})  # What cart?
    return f"Added {item}"

@tool
def get_total() -> str:
    """Get cart total."""
    return sum(item["price"] for item in cart)  # Same problem!

Tools need to share state, but:

  • Global variables are bad
  • Each tool is a separate function
  • We need something to pass through

6.2 Solution: Context Dict

Pass a mutable dictionary through the Runner that tools can read and write:

# Usage
runner = Runner(session, context={"user_id": "user123"})
result = runner.run(agent, "Add milk to cart")

# After execution, context may be modified:
print(runner.context)  # {"user_id": "user123", "cart": [...]}

6.3 Detecting Context Parameters

First, we need to detect if a function accepts context (agent.py):

import inspect

def has_context_param(func):
    sig = inspect.signature(func)
    return "context" in sig.parameters

Simple introspection — check if context is in the function signature.

6.4 Updated ToolsSet.execute_function_call

class ToolsSet:
    # ... get_specification unchanged ...

    def execute_function_call(self, context: dict, call_spec):
        tool = self.registry.get(call_spec.function.name)

        if not tool:
            raise ValueError(f"Tool {call_spec.function.name} not found")

        args = json.loads(call_spec.function.arguments)

        # Inject context if the tool supports it
        if has_context_param(tool.function):
            args["context"] = context

        result = tool(**args)

        return {"role": "tool", "tool_call_id": call_spec.id, "content": str(result)}

Key change: if the tool function has a context parameter, we inject it automatically.

6.5 Updated Runner

class Runner:
    def __init__(self, session: Session, context: dict | None = None):
        self.session = session
        # Context dict passed to tools - can be read and written
        self.context = context or {}

    def run(self, agent: Agent, prompt: str, context: dict | None = None) -> RunResult:
        # ... setup ...

        while loop_count < 10 and not should_stop:
            # ... LLM call ...

            # Execute tools with context
            tools_response = [
                current_agent.tools_set.execute_function_call(self.context, call_spec)
                for call_spec in response_message.tool_calls
                if not call_spec.function.name.startswith(HANDOFF_TOOL_PREFIX)
            ]

            # ... rest unchanged ...

Context is passed to execute_function_call, which injects it into tools that need it.

6.6 Context is Hidden from LLM

Important: the LLM doesn’t see the context parameter. In extract_function_schema.py:

def extract_function_schema(func, ...):
    # ...
    for param_name, param in sig.parameters.items():
        # Skip self/cls parameters
        if param_name in ("self", "cls"):
            continue

        # Skip context parameter - reserved for context injection
        if param_name in ("context",):
            continue

        # ... build schema for other params ...

The context parameter is filtered out of the JSON schema sent to the LLM.

6.7 Example: Shopping Cart

from agentsilex import Agent, Runner, Session, tool

@tool
def add_to_cart(item: str, price: float, context: dict) -> str:
    """Add an item to the shopping cart."""
    cart = context.setdefault("cart", [])
    cart.append({"item": item, "price": price})
    return f"Added {item} (${price:.2f}) to cart"

@tool
def get_cart(context: dict) -> str:
    """Show current cart contents."""
    cart = context.get("cart", [])
    if not cart:
        return "Cart is empty"
    items = [f"- {item['item']}: ${item['price']:.2f}" for item in cart]
    total = sum(item["price"] for item in cart)
    return "\n".join(items) + f"\nTotal: ${total:.2f}"

@tool
def clear_cart(context: dict) -> str:
    """Clear the shopping cart."""
    context["cart"] = []
    return "Cart cleared"

agent = Agent(
    name="shopping_assistant",
    model="gpt-4o",
    instructions="You are a shopping assistant. Help users manage their cart.",
    tools=[add_to_cart, get_cart, clear_cart],
)

# Create runner with initial context
session = Session()
context = {"user_id": "user123"}
runner = Runner(session, context=context)

# Multi-turn shopping
runner.run(agent, "Add milk for $3.50")
runner.run(agent, "Add bread for $2.00")
result = runner.run(agent, "What's in my cart?")

print(result.final_output)
# - milk: $3.50
# - bread: $2.00
# Total: $5.50

# Context was modified by tools
print(runner.context)
# {"user_id": "user123", "cart": [{"item": "milk", "price": 3.5}, ...]}

6.8 What the LLM Sees

For add_to_cart, the LLM sees:

{
  "type": "function",
  "function": {
    "name": "add_to_cart",
    "description": "Add an item to the shopping cart.",
    "parameters": {
      "type": "object",
      "properties": {
        "item": {"type": "string"},
        "price": {"type": "number"}
      },
      "required": ["item", "price"]
    }
  }
}

Notice: context is not in the parameters. The LLM only knows about item and price.

6.9 Use Cases

Use Case Context Data
Shopping cart {"cart": [...]}
User preferences {"user_id": "...", "preferences": {...}}
API clients {"db": connection, "http_client": client}
Accumulated state {"search_results": [...], "selected": ...}

6.10 Key Design Decisions

Decision Why
context param name reserved Clear convention, easy to detect
Hidden from LLM LLM doesn’t need to know about internal state
Mutable dict Tools can both read and write
Optional for tools Tools opt-in by adding context param
TipCheckpoint
cd code/v0.3

Context sharing is now available! Tools can share state.