Monitoring
The same agent, watched in production — observe, create_monitor, what lands on the run versus the session, and what the platform does with a session on its own.
Monitoring is the other half of the product: the agent doing real work for real users, reported as it happens and read by the same rules as an experiment. Sessions, Violations and Intents in the console fill from production instead of from a lab. One integration, two doorways — this page is the second one.
Two records, and which one you are looking at
This is the thing that confuses people first, so it goes first.
| record | opened by | filled by | where |
|---|---|---|---|
| the run | redline dev claiming an experiment |
ctx.thinking / ctx.tool / ctx.tool_result / ctx.log in your doorway, plus spans |
Experiments → the run |
| the session | @observe in production, or the worker during a dev conversation |
conversation turns, plus spans only | Monitor → Sessions |
ctx.* writes to the run. It never reaches a session. So a doorway that
maps every tool call onto ctx.tool and emits no spans produces a run with a
full transcript and a session with none — both correct, and easy to read as a
bug. What fills a session’s waterfall is OpenTelemetry spans, from your
framework’s instrumentation or from your own loop.
Under redline dev — nothing to write
While the worker is up, monitoring is on (REDLINE_MONITOR=1). Every
conversation the agent works — its user message, model calls with token
counts, both halves of every tool call, reasoning, the final answer — also
streams to Monitor → Sessions as a live session. The worker records the turns;
the spans supply everything between.
What decides whether it sees anything is the instrumentation. The SDK only
listens; the framework has to emit. Most emit nothing until their package is in
the venv, and redline dev activates whatever is installed at start:
| framework | install | notes |
|---|---|---|
| Pydantic AI | nothing | the SDK calls Agent.instrument_all() |
| LangChain / LangGraph | openinference-instrumentation-langchain |
|
| OpenAI Agents SDK | openinference-instrumentation-openai-agents |
|
| CrewAI | openinference-instrumentation-crewai and openinference-instrumentation-openai |
CrewAI 1.x calls the model through the OpenAI client |
| Agno | openinference-instrumentation-agno |
|
| Vercel AI SDK v7 | nothing | |
| Vercel AI SDK ≤6 | nothing to install | experimental_telemetry: { isEnabled: true } on the agent or each call |
| your own loop | nothing to install | emit your own — see below |
Verify after the first run: the session must show model calls and a token
count. One line per turn and nothing between means the package is missing or
was installed after redline dev started. Install and restart.
In production — observe
For the deployed agent — real users, no redline dev — one decorator on the
function that handles a message:
from redline import observe
@observe(agent="my-agent", session_arg="conversation_id",
user_arg="user_id", input_arg="text")
async def handle_message(conversation_id: str, user_id: str, text: str) -> str:
return await your_agent(text)session_arg— the argument carrying the conversation id. Calls sharing it land in one transcript; without it every call is its own session.user_arg— the argument naming the end user, shown on the session.input_arg— the argument carrying the user’s message; default is the call’s single string argument.
import { observe } from "@redlineai/sdk";
export const handleMessage = observe(
async (conversationId: string, userId: string, text: string) => yourAgent(text),
{ agent: "my-agent", sessionArg: 0, userArg: 1, inputArg: 2 },
);Same options, by argument index.
The decorator records the input and the returned answer itself; every span the
call emits lands in the same session, keyed to the call’s trace. Config is
environment only — REDLINE_API_KEY, REDLINE_URL, REDLINE_AGENT — so a
deploy needs no code. Without the key it is inert and the handler runs
untouched. Errors are reported and re-raised; nothing in it can take the
handler down.
Wiring it into an existing app
A decorator only records calls to the function it decorates. If the repository
already handles inbound messages — a webhook, a queue worker, a websocket loop
— that pipeline has to call handle_message. It will not find it on its
own.
That is one call-site change in existing code, and it is the only one the
integration asks for. Keep the pipeline’s own signals intact: if it read
something off the stream besides the reply (an “ended” flag, a step count),
pass a state dict the handler fills, so nothing downstream changes.
Without this, experiments work, redline dev sessions work, and production
sessions never appear. An integration reviewed as “done” with this missing
is the commonest shape we see.
create_monitor — the client underneath
For a handler shape the decorator cannot wrap, the explicit client:
from redline import create_monitor
monitor = create_monitor(agent="my-agent") # env for key and url
s = monitor.session(id=conversation_id, user=user_id)
s.input(text)
s.thinking("…")
s.tool("lookup", args={"q": "x"}, result="…", ok=True, latency_ms=120)
s.output(reply, tokens=439)
s.end("closed")
Same span types the ingest API accepts — input, output, thinking,
tool, retrieval, error, event. Batched and flushed on a timer; end()
closes the trace.
Emitting your own spans
For a hand-rolled loop there is no package to install; the loop emits its own, in the OTel GenAI convention the SDK reads:
| span | attributes |
|---|---|
| one per user turn (the parent) | none — a container carries no gen_ai.* |
| one per model call | gen_ai.operation.name="chat", gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens |
| one per tool call | gen_ai.operation.name="execute_tool", gen_ai.tool.name, gen_ai.tool.call.arguments, gen_ai.tool.call.result |
Parent the model and tool spans to the turn span explicitly
(start_span(..., context=set_span_in_context(turn))), not by making the turn
current — a loop that yields leaks a current span into whatever runs next.
Without the parent, every span is read as its own turn.
The full, working version is on the own tool loop page.
What the platform does with a session
With no further code:
- It appears in Monitor → Sessions as a readable conversation, with the waterfall beside it.
- Rule detectors match every span as it is written — a violation on a tool call is raised the moment the call lands. Within minutes the monitor agent re-examines the whole session against every rule the project defines (Monitor → Violations). Rules are written in the console, in plain language, not in the repository.
- When the session closes, the intent miner reads it for recurring user behaviour (Monitor → Intents). Custom instructions on that page steers it, also in plain language.
Arrival is the event. Post the session and the rest follows.