Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ See the [Getting Started guide](docs/getting-started.md) for more options.
| **Web Dashboard** | Browser-based session explorer with activity timeline (swim-lane visualization), step list, context window viewer, visual timeline diff, multi-metric axis (duration/tokens/cost), and live recording via WebSocket. Everything embedded in the single binary. | [web-ui.md](docs/web-ui.md) | - |
| **Multi-Agent Tracing** | Hierarchical span tree and activity timeline for multi-agent workflows. Each agent gets its own swim lane with duration bars. Auto-captures agent boundaries, tool calls, and handoffs from OpenAI Agents SDK. Manual `@span()` decorator for custom grouping. Thread view for multi-turn conversations. | [multi-agent-tracing.md](docs/multi-agent-tracing.md) | - |
| **Framework Integrations** | Native support for OpenAI Agents SDK and Pydantic AI (auto-detected on `init()`). Wrapper support for LangGraph and CrewAI. Any other framework works via the HTTP proxy. | [framework-integrations.md](docs/framework-integrations.md) | [09_pydantic_ai.py](examples/09_pydantic_ai.py), [10_openai_agents_sdk.py](examples/10_openai_agents_sdk.py) |
| **Bring your own framework** | One-call `rewind_agent.connector.setup()` for any agent on httpx/requests/aiohttp — wraps a session and HTTP intercept in a single `with` block. Custom transport (gRPC, in-process)? Use `ExplicitClient` directly. Three-tier guide covers all paths. | [hdk.md](docs/hdk.md) | - |
| **Claude Code Observation** | Observe Claude Code sessions in real-time via hooks. See every tool call (Read, Edit, Bash, Grep, Agent), user prompts, and session lifecycle. Token usage extracted from transcripts. One-command setup: `rewind hooks install`. | - | - |
| **MCP Server** | 26 tools for AI assistants (Claude Code, Cursor, Windsurf) to query recordings, view span trees, browse threads, diff timelines, create baselines, run evals - all from your IDE. | [mcp-server.md](docs/mcp-server.md) | - |
| **OpenTelemetry Export** | Export recorded sessions as OTel traces via OTLP to Langfuse, Datadog, Grafana Tempo, Jaeger, or any OTel-compatible backend. CLI, Python SDK, and Web API. Uses `gen_ai.*` semantic conventions. Privacy-first: message content requires explicit opt-in. | [otel-export.md](docs/otel-export.md) | - |
Expand All @@ -303,6 +304,7 @@ See the [Getting Started guide](docs/getting-started.md) for more options.
|:------|:-----------|:--------------|
| **Native** - auto-detected on `init()` | [OpenAI Agents SDK](https://github.com/openai/openai-agents-python), [Pydantic AI](https://ai.pydantic.dev/) | Zero config. Agent boundaries, tool calls, and handoffs captured automatically. |
| **Wrapper** - manual setup | LangGraph, CrewAI | Thin integration via `wrap_langgraph()` / `wrap_crew()`. CrewAI requires proxy mode. |
| **Bring your own** | Custom agent / framework / org connector | One-liner `rewind_agent.connector.setup(name=...)` for HTTP-shaped LLM clients; `ExplicitClient` for custom transports. See [hdk.md](docs/hdk.md). |
| **Works via proxy** | Any framework using OpenAI/Anthropic APIs | Point `OPENAI_BASE_URL` at the proxy. Works with Autogen, smolagents, custom code, any language. |

## Works With Your Observability Stack
Expand Down
16 changes: 16 additions & 0 deletions docs/framework-integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ crew = rewind_agent.wrap_crew(crew)
result = crew.kickoff()
```

## Bring your own framework

Building an agent or framework that isn't in the list above? Don't write a custom HTTP emitter — Rewind ships the integration primitives. Three tiers, pick the one that matches your transport:

- **Tier 1** — your agent uses `httpx` / `requests` / `aiohttp`: one-liner via `rewind_agent.connector.setup()`.
- **Tier 2** — custom transport (gRPC, in-process, mTLS-tunneled): ~30-line `ExplicitClient` integration.
- **Tier 3** — shipping a connector for an org or framework family (one package consumed by every internal agent): thin wrapper around Tier 1 with org-specific defaults.

```python
import rewind_agent
with rewind_agent.connector.setup(name="my-agent"):
run_agent_loop()
```

See **[hdk.md](hdk.md)** for the full decision tree, configuration knobs (`REWIND_ENABLED`, `REWIND_URL`, `REWIND_LLM_HOSTS`), and common pitfalls.

## Examples

- [`examples/04_langgraph.py`](../examples/04_langgraph.py) -- LangGraph integration
Expand Down
215 changes: 215 additions & 0 deletions docs/hdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Integrate any framework with Rewind

You're building an agent — or shipping a framework that agents use — and you want recording, fork, and replay without writing a custom HTTP emitter for every app. This page is the decision tree.

The big idea: **Rewind already ships the parts.** You don't need a framework adapter, a Protocol, or a plugin. Pick the tier that matches your transport and call site, write 5–35 lines, done.

## Pick a tier

```
Are your LLM calls HTTP-shaped? (httpx / requests / aiohttp)
├── Yes → Tier 1: intercept.install() ~5 LOC
└── No → Tier 2: ExplicitClient ~30 LOC

Need to bake in defaults for your org / framework?
→ Tier 3: ship a small connector package ~50 LOC
```

| Tier | Use when | Code size | API |
|:---|:---|:---|:---|
| 1 | LLM calls go over httpx / requests / aiohttp (most modern Python LLM clients) | ~5 LOC | [`intercept.install()`](intercept-quickstart.md) |
| 2 | Custom transport: gRPC, in-process LLM, mTLS-tunneled, message bus | ~30 LOC | [`ExplicitClient`](recording-api.md) |
| 3 | You're shipping a connector for an org or framework family (one package consumed by every internal agent) | ~50 LOC | Tier 1 or 2 + a thin wrapper |

Most integrators stop at Tier 1.

## Tier 1 — `connector.setup()`

For any agent that talks to LLMs over httpx, requests, or aiohttp. The connector wraps a session, installs HTTP intercept, and tears both down on exit — one call, one `with` block, no order to remember.

```python
import rewind_agent

with rewind_agent.connector.setup(name="my-agent"):
run_agent_loop()
```

For a custom LLM gateway hostname, pass `llm_hosts` (or set `REWIND_LLM_HOSTS=a.example,b.example` in the env):

```python
with rewind_agent.connector.setup(
name="my-agent",
llm_hosts=("llm-gateway.example",),
):
run_agent_loop()
```

The yielded value is an `ExplicitClient` — useful for non-HTTP record paths (gRPC, in-process LLMs) inside the same block:

```python
with rewind_agent.connector.setup(name="my-agent") as client:
response = my_grpc_client.chat(request)
client.record_llm_call(
request=request, response=response.dict(),
model="my-private-llama", duration_ms=duration_ms,
)
```

**Configuration knobs** (env > default; kwargs override env):

- `REWIND_ENABLED=0` — kill switch with zero overhead in prod when off.
- `REWIND_URL` — Rewind server URL (default `http://127.0.0.1:4800`).
- `REWIND_LLM_HOSTS` — comma-separated hostnames to treat as LLM gateways in addition to the strict-by-default provider list.

**Replay-aware**: when `REWIND_SESSION_ID` and `REWIND_REPLAY_CONTEXT_ID` are set (the runner-subprocess pattern from [runners.md](runners.md)), `setup()` skips creating a fresh session and lets `intercept.install()` attach to the existing replay context. Drop the connector into runner-driven replay handlers without phantom sessions.

For per-library examples, custom predicates, streaming behavior, the savings counter, and troubleshooting, see [intercept-quickstart.md](intercept-quickstart.md).

### When you need finer control

If you need to install intercept and start sessions independently — for example, a long-running process that opens many short sessions back-to-back — use the underlying primitives directly:

```python
from rewind_agent.explicit import ExplicitClient
from rewind_agent.intercept import DefaultPredicates, install, uninstall

class MyPredicates(DefaultPredicates):
def is_llm_call(self, req):
return "llm-gateway.example" in req.url_parts.netloc or super().is_llm_call(req)

client = ExplicitClient()
install(predicates=MyPredicates()) # once at process startup
try:
for task in tasks:
with client.session(name=task.name): # short session per task
run_task(task)
finally:
uninstall()
```

The order matters: the session sets `_session_id` / `_timeline_id` contextvars; the intercept layer reads those vars on every recorded call. Without an active session, intercepted calls silently no-op. **This is the most common Tier-1 mistake** — `connector.setup()` exists specifically to remove this footgun.

## Tier 2 — `ExplicitClient`

For agents that don't go over HTTP — gRPC, in-process LLMs, message-bus dispatch, anything `intercept` can't see. You wrap your existing call sites with explicit `record_*` calls.

```python
import time
from rewind_agent.explicit import ExplicitClient

client = ExplicitClient()
with client.session("my-agent"):
t0 = time.time()
# your custom LLM call
request = {"messages": [{"role": "user", "content": "hi"}]}
response = my_grpc_client.chat(request)
duration_ms = int((time.time() - t0) * 1000)

client.record_llm_call(
request=request,
response=response.to_dict(),
model="my-private-llama-70b",
duration_ms=duration_ms,
tokens_in=response.usage.prompt_tokens,
tokens_out=response.usage.completion_tokens,
)
```

`record_tool_call` records tool / function-call steps. `get_replayed_response` checks the replay cache during fork+replay; on a hit, return the cached response instead of calling the live model. The full HTTP wire format and method reference live in [recording-api.md](recording-api.md).

Async variants (`record_llm_call_async`, `record_tool_call_async`, `session_async`, `get_replayed_response_async`) are drop-in for asyncio code paths and run HTTP in a thread executor so the event loop never blocks.

## Tier 3 — ship a connector

If you're integrating a framework family — every internal agent in your org, every team using a private LLM gateway, every agent in a monorepo — package the boilerplate as a small connector. ~50 LOC, one place to update defaults.

The pattern:

```python
# my_org_rewind/__init__.py
import os
from contextlib import contextmanager
from rewind_agent.explicit import ExplicitClient
from rewind_agent.intercept import DefaultPredicates, install, uninstall

_DEFAULT_LLM_HOSTS = ("llm-gateway.your-org.example",)

class _OrgPredicates(DefaultPredicates):
def __init__(self, hosts):
super().__init__()
self._hosts = tuple(h.strip() for h in hosts if h and h.strip())

def is_llm_call(self, req):
if any(h in req.url_parts.netloc for h in self._hosts):
return True
return super().is_llm_call(req)

@contextmanager
def setup(name, *, base_url=None, llm_hosts=None, enabled=None):
"""Connect a my-org-hosted agent to Rewind.

Override defaults via env (REWIND_ENABLED, REWIND_URL, REWIND_LLM_HOSTS)
or per-call kwargs. Yields the ExplicitClient for non-HTTP record_* calls.
"""
if enabled is None:
enabled = os.environ.get("REWIND_ENABLED", "1") != "0"
if not enabled:
yield None
return

hosts = llm_hosts
if hosts is None:
env = os.environ.get("REWIND_LLM_HOSTS", "")
hosts = tuple(env.split(",")) if env else _DEFAULT_LLM_HOSTS

url = base_url or os.environ.get("REWIND_URL", "http://127.0.0.1:4800")
client = ExplicitClient(base_url=url)
with client.session(name):
install(predicates=_OrgPredicates(hosts))
try:
yield client
finally:
uninstall()
```

Every agent in your org becomes a 3-line integration:

```python
import my_org_rewind
with my_org_rewind.setup("alerts-triage"):
asyncio.run(main())
```

Configurable knobs every connector should expose:

- `REWIND_ENABLED=0` — kill switch with zero overhead in prod when off.
- `REWIND_URL` — Rewind server URL (default `http://127.0.0.1:4800`).
- `REWIND_LLM_HOSTS` — comma-separated hostnames to treat as LLM gateways.
- Per-call kwargs override env values, useful for tests and multi-tenant cases.

## Common pitfalls

**Recording silently no-ops.** `record_llm_call` returns `None` when no session is active. Always wrap your agent loop in `with client.session(...)` before calling `install(...)` or `record_*`. The intercept layer reuses the session's contextvars; without them, intercepted calls succeed (HTTP-wise) but record nothing.

**Don't combine `init()` with `intercept.install()`.** `rewind_agent.init()` patches the OpenAI / Anthropic Python SDKs at the SDK layer; `intercept.install()` patches httpx / requests / aiohttp at the transport layer. Running both in the same process double-records every call. Pick one path per process.

**`connector.setup()` is sync, even in async code.** It's a `@contextmanager`, not an `@asynccontextmanager`. Async callers should use plain `with` — not `async with` — even inside an `async def`:

```python
async def main():
with rewind_agent.connector.setup(name="my-agent"): # NOT `async with`
await run_agent_loop()
```

Only the setup and teardown are synchronous (HTTP POST `/api/sessions/start` and `/api/sessions/{id}/end`); the body of the block can be fully async.

**`requests.Session()` constructed before `install()` keeps its old adapter.** The intercept layer patches `Session.__init__`; live instances aren't mutated. Move `install()` earlier in startup, or call `session.mount(...)` explicitly for pre-existing sessions. Same caveat applies to `httpx.Client` constructed pre-install.

**Replay needs a replay context.** Fork-and-replay against a recorded session works automatically when the dispatch flow runs through `runner.py` (see [runners.md](runners.md)). For custom replay drivers, call `client.attach_replay_context(session_id, replay_context_id)` before `install()`; intercept will then serve cached responses on each step.

## Where to go next

- [intercept-quickstart.md](intercept-quickstart.md) — full Tier 1 reference: per-library examples, custom predicates, streaming behavior, savings counter, troubleshooting.
- [recording-api.md](recording-api.md) — HTTP wire format and `ExplicitClient` method reference for Tier 2.
- [framework-integrations.md](framework-integrations.md) — first-class adapters for OpenAI Agents SDK, Pydantic AI, LangGraph, CrewAI.
- [runners.md](runners.md) — wiring fork+replay into your runtime.
9 changes: 9 additions & 0 deletions docs/runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,12 @@ The runner authenticated successfully but is trying to post
events on a job belonging to a different runner. Usually
means the runner is processing a stale/leaked dispatch — check
runner logs for an old job_id.

---

## Integrating a non-runner agent with Rewind

Runners are the dashboard's "Run replay" path. If you're starting from a
plain agent and want recording / fork / replay without a webhook, see
[hdk.md](hdk.md) — three-tier guide for picking between
`intercept.install()`, `ExplicitClient`, and a small connector package.
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "rewind-agent"
version = "0.16.0"
version = "0.16.1"
description = "Chrome DevTools for AI agents — record, inspect, fork, replay, diff."
readme = "README.md"
license = "MIT"
Expand Down
9 changes: 8 additions & 1 deletion python/rewind_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def search(query):
# Wrap LangGraph / CrewAI for automatic instrumentation
graph = rewind_agent.wrap_langgraph(graph)
crew = rewind_agent.wrap_crew(crew)

# One-call connector for custom agents (any HTTP transport, any framework)
with rewind_agent.connector.setup(name="my-agent"):
run_agent_loop()
"""

from .patch import init, uninit, session, replay, thread
Expand All @@ -31,6 +35,7 @@ def search(query):
)
from .cached_call import cached_llm_call
from .explicit import ExplicitClient, RewindReplayDivergenceError
from . import connector
from .assertions import Assertions, AssertionResult
from .openai_agents import openai_agents_hooks
from .pydantic_ai import pydantic_ai_hooks
Expand Down Expand Up @@ -92,6 +97,8 @@ def search(query):
"RewindReplayDivergenceError",
# Cached LLM call decorator (Phase 2 / Tier 2)
"cached_llm_call",
# One-call connector for any agent (see docs/hdk.md)
"connector",
]


Expand All @@ -113,4 +120,4 @@ def import_from_langfuse(trace_id: str, **kwargs) -> str:
return _import(trace_id, **kwargs)


__version__ = "0.16.0"
__version__ = "0.16.1"
Loading
Loading