Known issues
Every failure a real integration has hit, by symptom — what it looks like, why it happens, and the fix. Read this before deciding the SDK is broken.
Each entry is something that actually happened to an integration, written up by the symptom you would see first. None of them is a bug in the SDK; every one is a step that was skipped, done out of order, or a shape the framework has that the default instructions do not cover.
Session shows only User / Agent rows — no model calls, no tokens, “0 tool calls”
What you see. A session in Monitor → Sessions with the conversation turns and nothing between them. Span count equals user turns plus agent turns. No waterfall.
Why. The SDK only listens for OpenTelemetry spans; something has to emit
them. Either the framework’s instrumentation package is not installed, it was
installed after redline dev started, or the agent is a hand-rolled loop
that emits nothing by itself.
Fix.
- Pydantic AI — nothing to install; check
redline devis the venv’s copy. - LangChain —
pip install openinference-instrumentation-langchain, restart. - CrewAI —
openinference-instrumentation-crewaiandopeninference-instrumentation-openai, restart. - Agno —
openinference-instrumentation-agno, restart. - Vercel AI SDK ≤6 —
experimental_telemetry: { isEnabled: true }on the agent or each call. v7 needs nothing. - Your own loop — emit two spans yourself: one per model call, one per tool
call, in the
gen_ai.*convention. See own tool loop §4.
How to confirm. A healthy session has a model call row with a token count under every agent turn. A CrewAI session went from 5 spans / 0 tools to 38 spans / 6 tools with nothing changed but the two packages.
Waterfall labels every step as its own turn
What you see. “Where the time went” lists turn 1, turn 2, … turn 18 for what was one user message. The transcript on the right groups correctly under one “Worked for 12s”.
Why. The waterfall reads the span tree, and a bar with no parent is a root;
each root is a turn. Spans emitted flat — no parent — are read as one turn
each. Happens to hand-rolled loops using start_span() without parenting; the
patched frameworks emit a proper tree.
Fix. Open one span per user turn and parent every model and tool span to
it explicitly: start_span(name, context=trace.set_span_in_context(turn)).
Do not make the turn span current with start_as_current_span inside a
loop that yields — a current span leaks across yield. The turn span carries
no gen_ai.* attribute, or it renders as an empty model call. Worked example
on the own tool loop page.
Tool calls are on the run but not on the session
What you see. Experiments → the run shows thinking and every tool call. Monitor → Sessions for the same conversation shows none.
Why. Two records. ctx.thinking / ctx.tool / ctx.tool_result write to
the run. A session is filled only by conversation turns and
OpenTelemetry spans. Both are correct; they are different things.
Fix. Nothing is broken. For the session to carry tool calls, spans must be emitted — see the first entry.
Production sessions never appear — experiments work, redline dev sessions work
What you see. Every experiment run lands. Dev conversations land. Real user traffic produces no sessions at all.
Why. The @observe handler in agents.py is defined but nothing calls
it. A decorator records calls to the function it decorates; the repository’s
existing message pipeline — a webhook, a queue worker, a websocket loop —
still runs its own code path.
Fix. The pipeline must call handle_message for each message. One
call-site change, the only one the integration asks for in existing code. If
the pipeline read anything off the agent’s stream besides the reply (an
“ended” flag, a step count), pass a state dict the handler fills so nothing
downstream changes. See
monitoring — wiring it into an existing app.
Session's answer is a generator object, or the session has an input and nothing else
What you see. The output row reads like <async_generator object …>, or
the session opens with the input and never gets an answer.
Why. @observe was put on an async generator. It checks
inspect.iscoroutinefunction, which is False for one, so it takes the sync
path and records the generator object as the return value.
Fix. Decorate a coroutine that drains the stream and returns the text. Streaming entry points get a thin wrapper; the generator itself is untouched.
The agent says it has no tools, or never uses machine_run
What you see. The task asks for something only machine_run can do and
the agent replies “I have no way to do that”.
Why. Shape C without §4 — the doorway never merged the attached tools —
or §5a missing: the tools are registered but the prompt never says they exist.
On a patched framework, the agent was constructed before redline dev
could patch it (a module that builds the agent on import, imported by
something other than agents.py).
Fix. Shape C: redline_tools(task), as_openai_schema() to advertise,
attached.call() to run, and a brief generated from attached.list in the
prompt. Patched frameworks: run redline dev from a clean process and let
agents.py be what imports the agent. Verify by counting tools.
Worker crashes on the SECOND run: “Event loop is closed”
Why. asyncio.run() in the doorway. It builds and destroys a loop per
run; a framework that keeps async resources alive between calls (Agno’s pools,
knowledge stores) then tears down into a closed loop.
Fix. async def run(task, ctx): return await _run(task, ctx). The SDK
runs it on a persistent loop it owns.
Run fails immediately with KeyError on an environment variable
What you see. The run’s error is a traceback ending in KeyError: 'SOME_CONFIG' from agents.py.
Why. The doorway needed something the repository knows and the SDK does
not — which configured agent to run against, a tenant id — and it was never
set on the machine running redline dev.
Fix. Put it in .env at the repository root; redline dev loads every
variable in that file, not only REDLINE_*. Restart redline dev. Make the
doorway raise a clear RuntimeError naming the variable rather than a bare
KeyError, so the next person reads the reason in the run.
The first turn of a conversation produced no agent reply, and the tester answered itself
What you see. Two consecutive User rows; the second reads like the assistant (“Sure, I’d be happy to help…”). The agent’s real first reply appears one turn later.
Why. The doorway returned an empty string on that turn — typically the
stream ended without a final event, so the doorway’s answer was never set.
The worker skips empty text, so no agent row is written; the simulated user,
handed a transcript with nothing back, wrote the assistant’s line itself.
Fix. Check the run’s own transcript for what turn one emitted. If the
agent’s first turn legitimately produces no text (an onboarding step that
only sets state), have the doorway return a placeholder so the turn is
recorded; otherwise fix the stream mapping so final is captured.
LangGraph stops mid-build — GraphRecursionError
Why. recursion_limit defaults to 25 steps. A real task needs a few
hundred.
Fix. config={"recursion_limit": 250} on invoke / stream.
LangChain: NotImplementedError from astream
Why. The graph’s middleware implements only the sync wrap_model_call;
LangChain refuses astream() over sync-only middleware.
Fix. Stream synchronously — agent.stream(...) in a plain def doorway.
The run dies on the first tool error
Why. A tool raised and the exception propagated. LangGraph’s default
ToolNode handler re-raises.
Fix. Make an exception the observation text the agent reads and recovers
from: handle_tool_errors=True in LangGraph; a try/except returning the
error string in your own tools. attached.call() already does this for
attached tools.
“no agents found” / registration rejected
no agents found — agents.py (or redline/*.ts) exports no
@agent-decorated function / defineAgent object. Check the file name:
agents.py, never redline.py, which shadows the SDK.
Registration rejected — the id collides with another agent in the project. Pick another kebab-case slug.
Runtime protection is “nothing to write” but nothing is being gated
Why. Enforcement patches the openai and litellm clients. An agent that
calls the provider over raw httpx or requests uses neither, so the patch
sees nothing. Policies still arm in the redline dev process and govern any
call that goes through a patched client.
Fix. There is no drop-in for a raw-HTTP loop. The gate calls exist in the
SDK — policy.gate_tool_call, guard.judge, honeypot.armed() — and where
they sit in your loop is a design decision; see
own tool loop §5.
Protect says “waiting for first snapshot” — and the agent has already run
What you see. The Protect row reads waiting for first snapshot, the agent’s page reads No snapshots from this agent yet, and it has run — once, or a hundred times. The row may also be labelled with a framework the agent does not use.
Why. Nothing was ever captured, so nothing was ever sent. Snapshot
capture reads a framework — a Pydantic AI / LangChain constructor, an Agno or
CrewAI object in your module, the tools=[…] handed to a patched
openai/litellm client. A loop of your own — a registry you wrote, the model
over raw httpx — goes through none of them. Running it does not help,
because for this shape capture is not run-triggered at all. The label comes
from what is importable beside the agent: a venv carrying langchain for a
retriever labels a hand-rolled loop langchain.
Fix. Declare the assets from the doorway, once the agent is built, with
the experiment’s attached tools filtered out:
declare_assets(system_prompt=…, tools=own_tools) (Python, SDK ≥ 0.2.27) or
declareAssets({ systemPrompt, tools }) (TypeScript, SDK ≥ 0.2.20). The
snapshot rides the next keepalive. Name the framework with
@agent(..., framework="custom") so the row stops promising a capture that
will not come. Worked example on the
own tool loop page.
One conversation produced two versions, two scans and a “change detected”
What you see. You ran one conversation. Protect shows a baseline and a candidate captured seconds apart — prompt changed · +1 tool — and a scan of each.
Why. run() is called once per turn, not once per conversation, so
whatever the doorway does happens per message. Two things then went wrong at
once: the doorway declared the agent’s tool list after the experiment’s
attached tools had been merged into a reused session (so turn two declared
one tool more than turn one), and — on SDKs before 0.2.27 — declare_assets
accumulated, so a prompt that differs per turn was appended to the previous
one until the snapshot hit its size ceiling. Each new hash is a new version,
and every version is scanned.
Fix. Upgrade to SDK ≥ 0.2.27 / ≥ 0.2.20, where declaring replaces, and filter the attached tools out of what you declare. One agent, one hash, one scan — however many turns.
A “change detected” and a scan every day — or every conversation — with nothing changed
What you see. The agent’s code has not moved, yet Protect opens a new candidate and scans it every morning, or after every conversation. The diff shows one line: a date, a name, a session id.
Why. A scan runs when the snapshot’s hash changes, and the hash is over
exactly what was declared. A hand-rolled agent that renders its system prompt
per turn — with datetime.now(), the caller’s name, fetched context — and
declares that has declared something different every time it runs. Scans
are never per conversation; the declared prompt was.
Fix. Declare the template: strip the volatile parts before
declare_assets (replace today’s date with a placeholder, leave out
per-session context), so an unchanged agent keeps an unchanged hash. See the
first rule on the
own tool loop page.
Vercel AI SDK: transcript present, no waterfall
Why. AI SDK ≤6 emits spans only when asked.
Fix. experimental_telemetry: { isEnabled: true } on the ToolLoopAgent
settings or each generateText / streamText call. v7 needs nothing.
ask_user ends the run
Redline runs are unattended. An agent that asks the user a question has nobody to answer it, so the run ends there. Return the questions as the answer rather than inventing replies.
If your symptom is not here, the monitoring page explains what fills each record, and the framework pages carry a §7 for the failures specific to each.