Staso Docs
Python SDK

Tracing with Decorators

Use @st.agent, @st.tool, and @st.trace to auto-trace functions with zero boilerplate. Decorators are the fastest path to agent instrumentation in Python.

import staso as st

st.init(api_key="ak_...", agent_name="support-bot")

@st.agent
def handle_ticket(ticket_id: str) -> str:
    context = fetch_context(ticket_id)
    return draft_reply(context)

@st.tool
def fetch_context(ticket_id: str) -> dict:
    ...

Every call to a decorated function becomes a span in your trace data model.

@st.agent

Mark agent entry points. Produces a span of kind agent and — importantly — overrides the agent name for every nested span inside the decorated function. This is the primary way to label agents when you run more than one in the same process. The name you pass (or the decorated function's own name, if you omit name=) flows down through every span the agent produces, so traces route to the right agent automatically. Use st.init(agent_name=...) as the baseline for code paths that aren't wrapped in @st.agent.

def agent(
    _fn=None,
    name: str | None = None,
    *,
    tags: list[str] | None = None,
    metadata: dict | None = None,
    capture_input: bool = True,
    capture_output: bool = True,
)
@st.agent
def research_agent(query: str) -> str:
    return plan_and_execute(query)

@st.agent(name="billing-agent", tags=["v2"])
async def billing_agent(user_id: str) -> str:
    return await resolve(user_id)

Works with async def transparently.

@st.tool

Wrap the functions an agent calls as tools. Produces a span of kind tool.

def tool(
    _fn=None,
    name: str | None = None,
    *,
    tags: list[str] | None = None,
    metadata: dict | None = None,
    capture_input: bool = True,
    capture_output: bool = True,
)
@st.tool
def search_docs(query: str) -> list[str]:
    return vector_db.query(query)

@st.tool(name="stripe.refund")
async def refund(charge_id: str) -> bool:
    return await stripe.refund(charge_id)

@st.trace

Generic decorator for anything that is not an agent or a tool — helpers, pipelines, retrievers. Produces a span of kind chain by default.

def trace(
    _fn=None,
    name: str | None = None,
    *,
    kind: str | SpanKind = SpanKind.CHAIN,
    tags: list[str] | None = None,
    metadata: dict | None = None,
    capture_input: bool = True,
    capture_output: bool = True,
)
@st.trace(kind="retriever")
def embed_and_search(query: str) -> list[str]:
    return vector_db.query(query)

@st.trace
async def rerank(candidates: list[str]) -> list[str]:
    return await model.rerank(candidates)

Valid kind values: "llm", "tool", "chain", "retriever", "agent", "custom".

Parameter reference

All three decorators share the same keyword arguments.

ParameterTypeDefaultDescription
namestr | Nonefunction nameSpan name. Defaults to fn.__name__.
kindstr | SpanKindvariesagent / tool / chain per decorator. @st.trace lets you override.
tagslist[str] | NoneNoneFree-form tags stored on the span.
metadatadict | NoneNoneArbitrary JSON-serializable metadata.
capture_inputboolTrueRecord function arguments on the span.
capture_outputboolTrueRecord return value on the span.

Capture flags

capture_input and capture_output control whether the SDK records function arguments and return values on the span. They default to True.

Disable them when:

  • The payload contains PII or secrets.
  • The payload is very large (megabyte blobs, raw files).
  • You want to mask an output and re-attach a sanitized version manually via manual spans.
@st.tool(capture_input=False)
def charge_card(card_number: str, amount_cents: int) -> str:
    ...

For a global kill switch on LLM message content, use capture_messages=False in st.init.

Exceptions

If the decorated function raises, the span is marked with status error, the exception message is recorded, and the exception propagates unchanged.

See also