---
layout: default
title: Decorators
nav_order: 3
---

# 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:**

```python
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.

```python
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.

```python
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 `ToolNode` → `TOOL` spans

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

→ Full details: [LangGraph framework page](frameworks/langgraph)

---

### @crew_kickoff

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

```python
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` → `LLM` spans
- Each agent's `tools` → `TOOL` 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](frameworks/crewai)

---

### @llamaindex_agent

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

```python
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](frameworks/llamaindex)

---

### @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.

```python
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](frameworks/adk)

---

### @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.

```python
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](frameworks/strands)

---

## 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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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:

```python
@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

```python
@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` — `RateLimitError` from any provider
- `validation_error` — `ValueError`, `TypeError`, `ValidationError`
- `provider_error` — API errors from OpenAI, Anthropic, Bedrock
- `internal_error` — `RuntimeError`, generic `Exception`
- `unknown` — anything else
