@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-outputandblock-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
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
- 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. - Execution β the LLM call runs normally.
- 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.
step_name when you want the code to map to a specific step name in Agent Control.
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.
@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.
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
asyncand 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-runningsetup_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:
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: