# agenx

Observability and tracing for AI agents, LLM chains, and workflows. Trace every step of your AI pipeline — LLM calls, tool use, retrieval, guardrails — with zero boilerplate.

## Installation

```bash
pip install agenx
```

With LangChain support:

```bash
pip install agenx[langchain]
```

All optional dependencies:

```bash
pip install agenx[all]
```

Requires Python 3.11+.

## Quick start

### Decorators

The fastest way to add tracing. Decorate your functions and agenx handles span creation, timing, and parent-child relationships automatically.

```python
from agenx import Tracer, StdoutExporter

tracer = Tracer()
tracer.configure(exporters=[StdoutExporter()])

@tracer.llm(model="gpt-4", provider="openai", temperature=0.7)
def chat(prompt: str) -> str:
    return call_openai(prompt)

@tracer.tool(tool_name="web_search")
def search(query: str) -> list:
    return fetch_results(query)

@tracer.agent(tools=["search", "chat"])
def assistant(question: str) -> str:
    docs = search(question)
    return chat(f"Answer using: {docs}")

assistant("What is quantum computing?")
```

### Context managers

For manual control over span attributes and events:

```python
from agenx import trace, SpanType

with trace.span("pipeline") as root:
    root.step.input = {"user_id": 12}

    with trace.span("search", type=SpanType.RETRIEVER) as r:
        r.retriever.query = "reset password"
        r.retriever.returned = 5
        r.add_event("cache_hit", {"key": "search_abc"})

    with trace.span("generate", type=SpanType.LLM) as llm:
        llm.llm.model = "gpt-4o"
        llm.llm.tokens_total = 843

    root.step.output = {"answer": "Click forgot password on the login page"}
```

`trace` is a pre-configured `Tracer` instance with `StdoutExporter` enabled.

## Span types

Each decorator creates a span with type-specific attributes:

| Decorator | Span type | Key attributes |
|-----------|-----------|----------------|
| `@tracer.step` | STEP | `input`, `output`, `metadata` |
| `@tracer.llm` | LLM | `model`, `provider`, `temperature`, `tokens_*`, `prompt`, `output` |
| `@tracer.agent` | AGENT | `tools`, `iterations`, `input`, `output` |
| `@tracer.tool` | TOOL | `tool_name`, `input`, `output` |
| `@tracer.retriever` | RETRIEVER | `query`, `top_k`, `returned`, `documents` |
| `@tracer.guard` | GUARD | `guard_name`, `passed`, `reason` |

## Decorator options

All decorators accept:

- **`name`** — Custom span name (defaults to function name)
- **`capture_args`** — Record function arguments (default: `True`)
- **`capture_result`** — Record return value (default: `True`)

```python
# Disable capture for sensitive operations
@tracer.step(capture_args=False, capture_result=False)
def handle_credentials(api_key: str) -> dict:
    ...
```

Type-specific options:

```python
@tracer.llm(model="claude-sonnet-4-6", provider="anthropic", temperature=0.3)
@tracer.agent(tools=["search", "calculator"])
@tracer.tool(tool_name="math_calculator")
@tracer.retriever(top_k=10)
@tracer.guard(guard_name="content_safety_filter")
```

## Async support

All decorators work with both sync and async functions:

```python
@tracer.llm(model="claude-sonnet-4-6", provider="anthropic")
async def generate(prompt: str) -> str:
    return await async_llm_call(prompt)
```

Async context managers are also supported:

```python
async with tracer.aspan("async_pipeline") as span:
    span.step.input = {"query": "hello"}
```

## Exporters

### StdoutExporter

Built-in colored terminal output with hierarchy and timing:

```python
from agenx import StdoutExporter

exporter = StdoutExporter(
    indent=True,           # Show parent-child hierarchy
    show_ids=True,         # Display trace/span IDs
    min_duration_ms=0.0,   # Filter by minimum duration
)
```

### Custom exporters

Any callable that accepts a `Span` works as an exporter:

```python
def my_exporter(span):
    print(f"{span.name} took {span.to_dict().get('duration_ms', 0):.1f}ms")

tracer = Tracer()
tracer.configure(exporters=[StdoutExporter(), my_exporter])
```

## Automatic nesting

Spans nest automatically via context. No manual ID passing needed:

```python
@tracer.agent(name="qa_agent")
def qa_pipeline(question: str) -> str:
    docs = retrieve(question)       # child span
    answer = generate(question, docs)  # child span
    return answer

@tracer.retriever(top_k=5)
def retrieve(query: str) -> list:
    ...

@tracer.llm(model="gpt-4")
def generate(query: str, context: list) -> str:
    ...
```

Calling `qa_pipeline` produces a trace tree:

```
qa_agent
  ├── retrieve
  └── generate
```

## LangChain integration

Auto-instrument LangChain and LangGraph with a single call:

```python
from agenx import instrument

instrument("langchain")
```

This patches chains, LLM calls, agents, and tools. To instrument all detected frameworks:

```python
from agenx import instrument_all

instrument_all()
```

To remove instrumentation:

```python
from agenx import uninstrument, uninstrument_all

uninstrument("langchain")
# or
uninstrument_all()
```

## Events

Record point-in-time events within a span:

```python
with trace.span("search", type=SpanType.RETRIEVER) as r:
    r.add_event("cache_hit", {"key": "search_abc"})
    r.add_event("reranking_complete", {"top_k": 5})
```

## API reference

### Exports from `agenx`

| Name | Description |
|------|-------------|
| `trace` | Pre-configured `Tracer` with `StdoutExporter` |
| `Tracer` | Core tracer class |
| `StdoutExporter` | Terminal exporter |
| `SpanType` | Enum: `ROOT`, `STEP`, `LLM`, `RETRIEVER`, `TOOL`, `GUARD`, `AGENT` |
| `instrument(name)` | Enable instrumentation for a framework |
| `uninstrument(name)` | Disable instrumentation |
| `instrument_all()` | Auto-detect and instrument all available frameworks |
| `uninstrument_all()` | Disable all instrumentation |

## License

MIT
