Skip to main content
Add @control() to any function to enforce server-managed safety controls on its inputs and outputs. This guide walks you through:
  • Setting up your environment.
  • Creating two agent controlsβ€”block-ssn-output and block-dangerous-sqlβ€” to block social security numbers and dangerous SQL queries, respectively.
  • Decorating an LLM call that asks β€œWhat is the capital of France?” and β€œDROP TABLE users”.
  • Returning the answer to the former and blocking the potentially dangerous SQL injection.

Prerequisites

  • Python 3.12+, uv, Docker
1

Start the server

2

Create your project

3

Create setup_controls.py

This script registers your agent, creates two controls, and associates them directly to the agent. Copy and paste the following into setup_controls.py.
Agent and control names must be unique. If you get a 409 conflict, pick new names or reset the database.
4

Create main.py

Three things to add to a normal LLM script: import, init(), @control().
5

Run it


How it works

  1. Pre-stage β€” before the function runs, the decorator sends its input to the server. Controls scoped to "pre" evaluate it. If denied, the function never executes.
  2. Execution β€” the LLM call runs normally.
  3. Post-stage β€” after the function returns, the decorator sends the output to the server. Controls scoped to "post" evaluate it. If denied, the output is blocked.

Decorate LLM steps

Decorate the function that performs the LLM call. By default, @control() registers the function as an llm step and uses the function name as the step name.
Use step_name when you want the code to map to a specific step name in Agent Control.
Controls scoped to LLM steps should use step_types: ["llm"].

Decorate tool steps

Decorate the function that executes the tool action, such as a database query, file write, API request, or business operation. For tool steps, make sure the function has tool metadata before @control() is applied. The current Python SDK uses .name or .tool_name to classify a decorated function as a tool step.
If your framework has a tool decorator, apply the framework tool metadata first and @control() after that. In Python, decorators run from the bottom up, so @tool must be closest to the function and @control() must be above it.
Controls scoped to tool steps should use step_types: ["tool"].
If you use @control(step_name="execute_query") without tool metadata, the step name changes, but the current Python SDK still treats the function as an llm step. Add .name or .tool_name, or apply your framework’s tool decorator before @control(), when the control should run on a tool step.

Any LLM SDK works

@control() wraps the function, not a specific provider:

Key points

  • Works on both async and sync functions.
  • Controls live on the server β€” update them without redeploying your agent.
  • Fail-safe: if the server is unreachable, the call is blocked, not silently allowed.

Troubleshooting

409 Conflict β€” name already exists

Agent and control names are unique. Re-running setup_controls.py against a database that already has those names will return a 409 Conflict. Option A β€” pick new names. Change the name strings in setup_controls.py (and update AGENT_NAME in main.py to match). Option B β€” reset the database. From the repo root, stop the server, wipe the Docker volume, and re-run migrations:
Then re-run setup_controls.py.

422 Unprocessable Entity on initAgent

The /initAgent payload must include agent_name inside the agent object. Double-check your setup_controls.py sends it: