Tracing Your Code
Three decorators and a context manager. That's the whole API.
Each one creates a span — a unit of work with a name, timing, status, and optional inputs/outputs. Spans nest automatically: a @tool called inside an @agent shows up as a child in the trace tree.
@st.agent
Your agent's entry-point.
@st.agent(name="support-agent", tags=["tier-1", "billing"])
def handle_request(user_message: str) -> str:
...| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Function name | Span name shown on dashboard |
tags | list[str] | None | Tags attached to span metadata |
metadata | dict | None | Custom metadata merged into span |
capture_input | bool | True | Capture function arguments |
capture_output | bool | True | Capture return value |
@st.tool
Any tool or function your agent calls. Automatically captures arguments and return value — you'll see them on the dashboard.
@st.tool(name="search_kb")
def search_knowledge_base(query: str, limit: int = 10) -> str:
return db.search(query, limit=limit)Dashboard shows:
- Input:
{"query": "reset password", "limit": 10} - Output:
"To reset your password, go to Settings > ..."
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Function name | Span name shown on dashboard |
tags | list[str] | None | Tags attached to span metadata |
metadata | dict | None | Custom metadata merged into span |
capture_input | bool | True | Capture function arguments |
capture_output | bool | True | Capture return value |
@st.trace
For anything that doesn't fit @agent or @tool — chains, retrievers, preprocessing.
@st.trace(name="classify_intent", kind="chain")
def classify_intent(message: str) -> str:
...
@st.trace(name="fetch_context", kind="retriever")
def fetch_context(user_id: str) -> dict:
...| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Function name | Span name shown on dashboard |
kind | str | "chain" | Span kind (see below) |
tags | list[str] | None | Tags attached to span metadata |
metadata | dict | None | Custom metadata merged into span |
capture_input | bool | True | Capture function arguments |
capture_output | bool | True | Capture return value |
Span kinds: agent, tool, chain, llm, retriever, custom
st.span()
Context manager for manual span creation. Use this when you need to set custom fields on the span directly.
with st.span("my-operation", kind="tool") as s:
s.metadata["custom_key"] = "custom_value"
result = do_work()
s.output = {"result": result}The span object gives you direct access to:
with st.span("llm-call", kind="llm") as s:
s.model = "gpt-4o"
s.input = {"prompt": "Hello"}
s.output = {"response": "Hi there"}
s.input_tokens = 5
s.output_tokens = 3
s.total_tokens = 8
s.metadata["temperature"] = 0.7| Parameter | Type | Default |
|---|---|---|
name | str | required |
kind | str | "chain" |
Annotations
Attach evaluation scores, labels, or flags to traces and spans.
On a span
Inside an st.span() context, call annotate() directly on the span:
with st.span("evaluate") as s:
result = run_evaluation()
s.annotate("accuracy", value=0.95)
s.annotate("category", value="billing", data_type="categorical")
s.annotate("approved", value=True, data_type="boolean", comment="Passed review")Standalone
Annotate any trace by ID after the fact:
st.annotate(
trace_id="abc-123",
name="quality_score",
value=4.5,
data_type="numeric",
comment="Rated by human reviewer",
)| Parameter | Type | Default | Description |
|---|---|---|---|
trace_id | str | required | The trace to annotate |
name | str | required | Annotation name |
value | float | str | bool | required | Annotation value |
data_type | str | "numeric" | "numeric", "categorical", or "boolean" |
span_id | str | None | None | Scope to a specific span |
comment | str | None | None | Optional comment |
The value type must match the data type — float/int for numeric, str for categorical, bool for boolean.
Disabling Capture
All three decorators capture inputs and outputs by default. To disable:
@st.agent(name="my-agent", capture_input=False, capture_output=False)
def handle_request(sensitive_data: str) -> str:
...You still get timing, status, and error tracking — just no argument or return value recording.
Async
All decorators work with async functions. No changes needed:
@st.tool(name="search")
async def search(query: str) -> str:
return await db.async_search(query)Nesting
@st.agent(name="my-agent")
def run(query: str) -> str:
context = search(query)
intent = classify(query)
return respond(context, intent)
@st.tool(name="search")
def search(query: str) -> str: ...
@st.trace(name="classify", kind="chain")
def classify(query: str) -> str: ...Dashboard:
my-agent (agent)
├── search (tool)
└── classify (chain)Errors
Decorated functions that raise exceptions: the SDK records the error on the span and re-raises it. Your error handling is never affected.
@st.tool(name="api_call")
def call_external_api() -> str:
raise ConnectionError("timeout")
try:
call_external_api() # Span shows error, exception still propagates
except ConnectionError:
handle_failure()