RunContext carries application data through a run, controls tool approvals, and tracks usage. It is the shared state object that tools, guardrails, and the runner can read and update.

Overview

RunContext is used for:

  • Application data: user, session, org, feature flags, service clients, etc.
  • Tool approvals: allow or block tool calls per-call or permanently.
  • Usage tracking: accumulate token usage across turns and across runs.

If you do not supply a RunContext, the runner creates one with UnknownContext.

Create and Pass a RunContext

Default (UnknownContext)

RunContext<UnknownContext> context = new RunContext<>();

RunConfig config =
    RunConfig.builder().context(java.util.Optional.of(context)).build();

RunResult<UnknownContext, ?> result = Runner.run(agent, "Your prompt", config);

Custom Context Data

public class AppContext {
  String userId;
  String sessionId;
  boolean premiumUser;
}

AppContext appContext = new AppContext();
appContext.userId = "user_123";
appContext.sessionId = "session_456";
appContext.premiumUser = true;

RunContext<AppContext> context = new RunContext<>(appContext);
RunConfig config =
    RunConfig.builder().context(java.util.Optional.of(context)).build();

RunResult<AppContext, ?> result = Runner.run(agent, "Your prompt", config);

Access Context in Tools

Every tool receives the RunContext:

public class DatabaseTool implements FunctionTool<AppContext, Input, Output> {
  @Override
  public CompletableFuture<Output> invoke(RunContext<AppContext> context, Input input) {
    AppContext appContext = context.getContext();
    if (!appContext.premiumUser) {
      return CompletableFuture.completedFuture(new Output("Upgrade required."));
    }
    return performDatabaseQuery(appContext.userId, input);
  }
}

View complete example →

Tool Approval System

Approval is tri-state:

  • true = approved
  • false = rejected
  • null = pending decision

Per-Call Approval

RunContext<UnknownContext> context = new RunContext<>();

RunToolApprovalItem approval =
    RunToolApprovalItem.builder()
        .toolName("send_email")
        .toolCallId("call_123")
        .build();

context.approveTool(approval);

Boolean approved = context.isToolApproved("send_email", "call_123"); // true
Boolean other = context.isToolApproved("send_email", "call_456");    // null

Permanent Approval

RunToolApprovalItem approval =
    RunToolApprovalItem.builder()
        .toolName("send_email")
        .toolCallId("call_123")
        .build();

context.approveTool(approval, true);

context.isToolApproved("send_email", "call_123"); // true
context.isToolApproved("send_email", "call_456"); // true

Rejection (Per-Call or Permanent)

RunToolApprovalItem rejection =
    RunToolApprovalItem.builder()
        .toolName("delete_data")
        .toolCallId("call_999")
        .build();

context.rejectTool(rejection, true);
context.isToolApproved("delete_data", "call_999"); // false

Enforcing Approval in Tools

Tools declare approval needs with needsApproval():

public class SensitiveTool implements FunctionTool<AppContext, Input, Output> {
  @Override
  public boolean needsApproval(RunContext<AppContext> context, Input input) {
    return input.getOperation().equals("delete");
  }
}

When needsApproval() returns true, the runner checks context.isToolApproved(toolName, toolCallId) before executing the tool.

Mixed Approval Modes

RunContext<UnknownContext> context = new RunContext<>();

context.approveTool(
    RunToolApprovalItem.builder().toolName("calculator").toolCallId("calc_001").build(), true);
context.approveTool(
    RunToolApprovalItem.builder().toolName("send_email").toolCallId("email_001").build(), false);
context.rejectTool(
    RunToolApprovalItem.builder().toolName("delete_file").toolCallId("delete_001").build(), true);

Usage Tracking

When you pass a RunContext via RunConfig, the runner accumulates usage automatically for each model response. Use addUsage() only if you are aggregating usage from other sources or runs.

RunContext<UnknownContext> context = new RunContext<>();

context.addUsage(Usage.builder().inputTokens(100.0).outputTokens(50.0).totalTokens(150.0).build());
context.addUsage(Usage.builder().inputTokens(200.0).outputTokens(75.0).totalTokens(275.0).build());

Usage total = context.getUsage();
System.out.println("Total tokens: " + total.getTotalTokens());

Serialization and Restore

RunContext can serialize to a map for storage or debugging:

RunContext<AppContext> context = new RunContext<>(appContext);
context.addUsage(Usage.builder().totalTokens(500.0).build());

Map<String, Object> json = context.toJSON();

To restore approvals from stored state, use rebuildApprovals():

RunContext<AppContext> restored = new RunContext<>(appContext);
restored.rebuildApprovals(savedApprovals);

Combine Context with Sessions

Context and sessions solve different problems: context is app data and approvals, session is conversation memory. Use both via RunConfig:

RunContext<AppContext> context = new RunContext<>(appContext);
Session session = new MemorySession("conversation_123");

RunConfig config =
    RunConfig.builder()
        .context(java.util.Optional.of(context))
        .session(session)
        .build();

RunResult<AppContext, ?> result = Runner.run(agent, "My name is Alice", config);

Advanced Patterns

Dynamic Approval Based on Input

@Override
public boolean needsApproval(RunContext<AppContext> context, Input input) {
  if (input.getAmount() < 10.0) {
    return false;
  }
  if (input.getAmount() > 1000.0) {
    return true;
  }
  return !context.getContext().isPreApproved(input.getOperation());
}

Budget Enforcement

public class BudgetContext {
  double budget;
  double spent;

  boolean canSpend(double amount) {
    return (spent + amount) <= budget;
  }
}

@Override
public boolean needsApproval(RunContext<BudgetContext> context, Input input) {
  return !context.getContext().canSpend(input.getAmount());
}

Approval Chains

@Override
public boolean needsApproval(RunContext<AppContext> context, Input input) {
  AppContext appContext = context.getContext();
  if ("admin".equals(appContext.getUserRole())) {
    return false;
  }
  if (appContext.hasPermission(input.getOperation())) {
    return false;
  }
  return input.getAmount() >= appContext.getApprovalThreshold();
}

Example Index

The RunContextExample covers:

  1. Basic context storage
  2. Usage tracking
  3. Per-call tool approvals
  4. Permanent tool approvals
  5. Tool rejection
  6. Mixed approval modes
  7. Serialization

Real-World Example

RealWorldRunContextExample.java →

Best Practices

  • Keep context small and focused on app data and approvals.
  • Prefer immutable data where possible, or document ownership clearly.
  • Use per-call approval for sensitive tools; permanent approval for safe tools.
  • Avoid double-counting usage if the runner already tracks it.
  • Make context serializable if you need persistence across requests.

Next Steps

  • Tools - Implement tools that use context
  • Handoffs - Pass context across agent handoffs
  • Sessions - Combine context with conversation memory
  • Guardrails - Add safety constraints using context