Staso Docs
Guides

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:
    ...
ParameterTypeDefaultDescription
namestrFunction nameSpan name shown on dashboard
tagslist[str]NoneTags attached to span metadata
metadatadictNoneCustom metadata merged into span
capture_inputboolTrueCapture function arguments
capture_outputboolTrueCapture 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 > ..."
ParameterTypeDefaultDescription
namestrFunction nameSpan name shown on dashboard
tagslist[str]NoneTags attached to span metadata
metadatadictNoneCustom metadata merged into span
capture_inputboolTrueCapture function arguments
capture_outputboolTrueCapture 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:
    ...
ParameterTypeDefaultDescription
namestrFunction nameSpan name shown on dashboard
kindstr"chain"Span kind (see below)
tagslist[str]NoneTags attached to span metadata
metadatadictNoneCustom metadata merged into span
capture_inputboolTrueCapture function arguments
capture_outputboolTrueCapture 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
ParameterTypeDefault
namestrrequired
kindstr"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",
)
ParameterTypeDefaultDescription
trace_idstrrequiredThe trace to annotate
namestrrequiredAnnotation name
valuefloat | str | boolrequiredAnnotation value
data_typestr"numeric""numeric", "categorical", or "boolean"
span_idstr | NoneNoneScope to a specific span
commentstr | NoneNoneOptional 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()