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:
-
Request phase —
@llmscans 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. -
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:
- At call time, scans function arguments for known framework objects
- Delegates to the matching
FrameworkInstrumentor(same code path as the explicit decorators) - If no framework object is found, falls back to a plain
@agentspan
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 →
TRACEspans (node:<name>) - Chat models →
LLMspans with token/latency metrics - Tools in
ToolNode→TOOLspans
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
llm→LLMspans - Each agent’s
tools→TOOLspans
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
RunnerorBaseAgentobjects in function args/closures - LLM call events →
LLMspans with token/latency metrics - Tool call events →
TOOLspans
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
Agentobjects in function args/closures - Model
streamcalls →LLMspans with token/latency metrics - Tool
streamcalls →TOOLspans
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, addsemit_metricattribute 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_totalcounter 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:
timeout—TimeoutError,httpx.TimeoutException, etc.rate_limit—RateLimitErrorfrom any providervalidation_error—ValueError,TypeError,ValidationErrorprovider_error— API errors from OpenAI, Anthropic, Bedrockinternal_error—RuntimeError, genericExceptionunknown— anything else