Decorator Reference

Rastir provides five core decorators for manual instrumentation, plus a unified @framework_agent decorator that auto-detects which AI framework you’re using, and five individual framework decorators for explicit control. All support both sync and async functions.


How Data Is Captured

Understanding this difference is key to choosing the right decorator.

Core decorators wrap your function. They record timing from function entry/exit. @llm uses a two-phase strategy:

  1. Request phase@llm scans the decorated function for known LLM client objects (OpenAI, Azure OpenAI, Anthropic, Google GenAI, Cohere, Mistral, Groq, LangChain, Bedrock) in arguments, closures, and globals. When found, the client’s call is intercepted to capture the full provider response — model, tokens, cost — regardless of what your function returns.

  2. Response phase — the adapter pipeline also inspects the return value. If your function returns the raw provider response, the adapter extracts metadata from it. If the request phase already captured the data, the response phase is a no-op.

Framework decorators reach inside the framework’s objects and wrap the model/tool methods directly. They always see the full provider response — model, tokens, cost, and latency are always captured.

What each decorator records

Every span always records: duration, status, trace_id, span_id, parent_span_id.

Decorator Type Model Provider Tokens Cost Tool name How
@trace Core Function execution only
@agent Core Function execution only
@llm Core Auto-discovers LLM clients in args/closures/globals and intercepts their call methods. Also extracts from return value as fallback.
@retrieval Core Function execution only
@metric Core Creates a metric span with counters and histograms
@langgraph_agent Framework Wraps model/tool objects directly — always captures full data
@crew_kickoff Framework Wraps model/tool objects directly — always captures full data
@llamaindex_agent Framework Wraps agent’s LLM/tool objects directly — always captures full data
@adk_agent Framework Wraps ADK Runner/Agent objects — intercepts events for LLM/tool spans
@strands_agent Framework Wraps Strands Agent objects — intercepts model/tool streams

@llm auto-discovers client objects from: OpenAI, Azure OpenAI, Anthropic, Google GenAI, Cohere, Mistral, Groq, LangChain chat models, and Bedrock. If the client is in function arguments, closure variables, or module globals, the interceptor captures full metadata automatically.

Example — @llm auto-discovery in action:

client = OpenAI()

@llm
def ask(query):
    result = client.chat.completions.create(model="gpt-4", messages=[...])
    return result.choices[0].message.content  # ← returns plain string
    # Auto-discovery intercepts client.chat.completions.create() and
    # captures model, tokens, cost from the full ChatCompletion response

The interceptor works with clients passed as arguments, stored in closures, or defined as module-level globals. No configuration needed — just use @llm.


Which Decorator Should I Use?

Scenario Decorator What it does
Any supported framework @framework_agent Recommended. Auto-detects LangGraph, CrewAI, LlamaIndex, ADK, or Strands from function arguments and instruments everything.
Building with LangGraph @langgraph_agent Explicit LangGraph instrumentation. Auto-discovers LLMs, tools, and nodes inside the compiled graph.
Building with CrewAI @crew_kickoff Explicit CrewAI instrumentation. Auto-discovers LLMs and tools on every agent in the Crew.
Building with LlamaIndex @llamaindex_agent Explicit LlamaIndex instrumentation. Auto-discovers LLMs and tools on the agent.
Building with Google ADK @adk_agent Explicit ADK instrumentation. Auto-discovers ADK Runner/Agent objects and intercepts events.
Building with Strands @strands_agent Explicit Strands instrumentation. Auto-discovers Strands Agent objects and intercepts model/tool streams.
Building your own agent loop @agent + @llm Full manual control — you decorate each function yourself. Use @trace for non-LLM functions.
Simple tracing (no agent) @trace General-purpose span for any function.
Standalone metrics only @metric Creates metric spans with Prometheus counters/histograms.

Rule of thumb: Use @framework_agent for automatic framework detection, or the framework-specific decorator for explicit control. Use @agent / @llm only when you’re calling LLM APIs directly without a framework.


Framework Decorators

@framework_agent (Unified Auto-Detect)

Purpose: Auto-detect the AI framework from function arguments and apply the correct instrumentation. This is the recommended single entry point for all supported frameworks.

from rastir import framework_agent

@framework_agent(agent_name="my_agent")
def run(graph_or_agent, prompt):
    return graph_or_agent.invoke(prompt)

# Works with any supported framework:
# - LangGraph CompiledGraph
# - CrewAI Crew
# - LlamaIndex ReActAgent / FunctionAgent
# - ADK Runner / BaseAgent
# - Strands Agent

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

How it works:

  1. At call time, scans function arguments for known framework objects
  2. Delegates to the matching FrameworkInstrumentor (same code path as the explicit decorators)
  3. If no framework object is found, falls back to a plain @agent span

Supports: bare @framework_agent or @framework_agent(...), sync/async.

When to use the explicit decorator instead: When you want to make the framework dependency explicit in your code, or when you need framework-specific parameter support.


@langgraph_agent

Purpose: Instrument a LangGraph compiled graph. Auto-discovers all chat models, tools, and graph nodes — wraps them for tracing and restores originals after execution.

from rastir import langgraph_agent
from langgraph.prebuilt import create_react_agent

@langgraph_agent(agent_name="react")
def run(query):
    graph = create_react_agent(model, tools)
    return graph.invoke({"messages": [("user", query)]})

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

What gets auto-discovered:

  • Graph nodes → TRACE spans (node:<name>)
  • Chat models → LLM spans with token/latency metrics
  • Tools in ToolNodeTOOL spans

Supports: bare @langgraph_agent or @langgraph_agent(...), sync/async, graph as argument or in closure.

→ Full details: LangGraph framework page


@crew_kickoff

Purpose: Instrument a CrewAI Crew. Auto-discovers each agent’s LLM and tools, wraps them before kickoff(), and restores after.

from rastir import crew_kickoff

@crew_kickoff(agent_name="research_crew")
def run(crew):
    return crew.kickoff()

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

What gets auto-discovered:

  • Each agent’s llmLLM spans
  • Each agent’s toolsTOOL spans

MCP tools: CrewAI handles MCP natively via mcps=[] on agents — no Rastir parameter needed.

Supports: bare @crew_kickoff or @crew_kickoff(...), sync/async.

→ Full details: CrewAI framework page


@llamaindex_agent

Purpose: Instrument a LlamaIndex agent. Auto-discovers the agent’s LLM and tools and wraps them for tracing — no manual wrap() needed.

from rastir import llamaindex_agent
from llama_index.core.agent import ReActAgent

agent = ReActAgent(llm=llm, tools=tools, streaming=False)

@llamaindex_agent(agent_name="qa_agent")
async def run(agent, query):
    return await agent.run(query)

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

Note: Unlike @langgraph_agent and @crew_kickoff, LlamaIndex requires explicit wrap() calls on LLMs and tools. The decorator provides the outer agent span and restore-after-execution.

→ Full details: LlamaIndex framework page


@adk_agent

Purpose: Instrument a Google ADK (Agent Development Kit) agent. Auto-discovers ADK Runner or BaseAgent objects in function arguments or closures, wraps run_async to intercept events, and creates LLM and tool spans automatically.

from rastir import adk_agent

@adk_agent(agent_name="my_adk_agent")
async def run(runner, query):
    async for event in runner.run_async(user_id="u1", session_id="s1",
                                         new_message=Content(parts=[Part(text=query)])):
        pass

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

What gets auto-discovered:

  • ADK Runner or BaseAgent objects in function args/closures
  • LLM call events → LLM spans with token/latency metrics
  • Tool call events → TOOL spans

MCP support: Automatically discovers MCP clients on ADK agents and injects traceparent for distributed tracing.

Supports: bare @adk_agent or @adk_agent(...), async only (ADK is async-first).

→ Full details: ADK framework page


@strands_agent

Purpose: Instrument a Strands agent. Auto-discovers Strands Agent objects in function arguments or closures, wraps the model’s stream method and each tool’s stream method to create LLM and tool spans.

from rastir import strands_agent

@strands_agent(agent_name="my_strands_agent")
def run(agent, query):
    return agent(query)

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

What gets auto-discovered:

  • Strands Agent objects in function args/closures
  • Model stream calls → LLM spans with token/latency metrics
  • Tool stream calls → TOOL spans

MCP support: Automatically discovers MCP clients on Strands agents and injects traceparent for distributed tracing.

Supports: bare @strands_agent or @strands_agent(...), sync/async.

→ Full details: Strands framework page


Core Decorators

These decorators are for manual instrumentation — use them when you’re calling LLM APIs directly without a framework, or when building a custom agent loop.


@trace

Purpose: Create a root or general span. Entry point for request tracing.

from rastir import trace

# Bare usage
@trace
def handle_request(query: str) -> str:
    ...

# With options
@trace(name="custom_span_name", emit_metric=True)
def process(data: dict) -> dict:
    ...

Parameters:

Parameter Type Default Description
name str Function name Custom span name
emit_metric bool False Record duration as a span attribute

Span type: trace

Behaviour:

  • Creates a span with parent-child hierarchy via context propagation
  • Records execution duration and success/failure status
  • If emit_metric=True, adds emit_metric attribute to the span

@agent

Purpose: Mark a function as an agent entry point. Use this when you’re building your own agent loop (calling LLM APIs directly). If you’re using LangGraph, CrewAI, or LlamaIndex, use the corresponding framework decorator instead — it handles everything automatically. Sets agent identity so child @llm and @retrieval spans inherit the agent label in their Prometheus metrics.

from rastir import agent

# Bare usage — agent_name defaults to function name
@agent
def my_agent(query: str) -> str:
    ...

# With explicit name
@agent(agent_name="research_bot")
def run_research(query: str) -> str:
    ...

Parameters:

Parameter Type Default Description
agent_name str Function name Agent identity label

Span type: agent

Agent label rule: The agent label is injected into child LLM/retrieval metrics only when the parent span is explicitly marked via @agent. If @llm runs under a plain @trace, no agent label is injected.


@llm

Purpose: Create an LLM span. Automatically discovers LLM client objects inside the decorated function and intercepts their call methods to capture model, provider, token usage, and finish reason — regardless of what your function returns.

Supported providers for auto-discovery: OpenAI, Azure OpenAI, Anthropic, Google GenAI, Cohere, Mistral, Groq, LangChain chat models, Bedrock.

from rastir import llm

# Client in closure — auto-discovered
client = OpenAI()

@llm
def ask_gpt(query: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": query}],
    )
    return resp.choices[0].message.content  # returns plain text — metadata still captured

# Client as argument — also auto-discovered
@llm
def ask_anthropic(client, query: str) -> str:
    resp = client.messages.create(model="claude-3-opus", messages=[...])
    return resp.content[0].text  # metadata captured via interceptor

# With explicit metadata hints (overrides auto-detection)
@llm(model="gpt-4", provider="openai")
def ask_with_hints(query: str) -> str:
    ...

Parameters:

Parameter Type Default Description
model str Auto-detected LLM model name
provider str Auto-detected Provider name
streaming bool Auto-detected Whether the call is a streaming call (auto-detected from return type if not set)
evaluate bool False Enable server-side evaluation for this span
evaluation_types list[str] None Evaluation types to request (e.g. ["relevance", "faithfulness"])
evaluation_sample_rate float None Per-decorator evaluation sampling rate (overrides server default)
evaluation_timeout_ms int None Per-decorator evaluation timeout (overrides server default)

Span type: llm

Metrics emitted:

  • rastir_llm_calls_total{service, env, model, provider, agent}
  • rastir_tokens_input_total{service, env, model, provider, agent}
  • rastir_tokens_output_total{service, env, model, provider, agent}
  • rastir_duration_seconds{service, env, span_type="llm"}
  • rastir_tokens_per_call{service, env, model, provider}

Streaming: Auto-detects when the function returns a generator or async generator. Token deltas are accumulated as the stream is consumed. Metrics are recorded after the stream completes.


@retrieval

Purpose: Track retrieval/vector search operations.

from rastir import retrieval

@retrieval
def vector_search(query: str, top_k: int = 5) -> list[str]:
    return chroma_client.query(query, n_results=top_k)

Parameters:

Parameter Type Default Description
name str Function name Span name
doc_count_extractor Callable[[Any], int] None Optional callable that extracts document count from the function’s return value

Span type: retrieval

Metrics emitted:

  • rastir_retrieval_calls_total{service, env, agent, model, provider}
  • rastir_duration_seconds{service, env, span_type="retrieval", model, provider}

@metric

Purpose: Emit generic function-level Prometheus metrics. Creates a metric span and records timing and call counts.

from rastir import metric

@metric
def process_request(data: dict) -> dict:
    ...

@metric(name="custom_op")
def my_function() -> None:
    ...

Parameters:

Parameter Type Default Description
name str Function name Metric name prefix

Metrics emitted:

  • <name>_calls_total{service, env}
  • <name>_duration_seconds{service, env}
  • <name>_failures_total{service, env}

Stacking Decorators

Decorators can be stacked for combined behaviour:

@agent(agent_name="qa_bot")
def run_qa(query: str) -> str:
    result = search(query)
    return answer(query, result)

@retrieval
def search(query: str) -> list[str]:
    ...

The most common pattern is:

@trace (or @agent)
  └── @llm
  └── @retrieval

Error Handling

All decorators automatically:

  • Catch exceptions and set span status to ERROR
  • Record exception details as span events
  • Re-raise the exception (decorators are transparent)
  • Increment rastir_errors_total counter with normalised error type
@llm
def risky_call(query: str):
    # If this raises, Rastir records the error and re-raises
    return openai.chat.completions.create(...)

Error types are normalised into six categories:

  • timeoutTimeoutError, httpx.TimeoutException, etc.
  • rate_limitRateLimitError from any provider
  • validation_errorValueError, TypeError, ValidationError
  • provider_error — API errors from OpenAI, Anthropic, Bedrock
  • internal_errorRuntimeError, generic Exception
  • unknown — anything else

Rastir — LLM & Agent Observability Library