See the monitor's decisions in LangSmith and Langfuse¶
This guide shows how to find each monitored step in LangSmith or Langfuse: the spans the monitor opens, what each one holds, how to attach either tool, how to find the halted and flagged steps, and how to send each step's suspicion to either tool as a score.
Read the span tree¶
Any tracer built on LangChain callbacks, LangSmith and Langfuse among them, shows each monitored step as a small tree of named spans. The agent's model calls stay where they were, nested under the step that drew them, and the monitor's own calls nest under the judgement they served:
model
└── monitor step
├── sample 1: the agent's model
├── monitor judgement of it
│ └── monitor call,
│ or monitor classifier
├── sample 2, if drawn
├── monitor judgement of it
├── the trusted model's call,
│ if a fallback writes it
└── monitor decision
- Order and timing. The step span's children are listed in the order they start. The step span opens before the first sample and ends when the step is committed; the decision span, tagged with the outcome, opens and ends at once, when the protocol has decided.
- Judgements. The LLM monitors, the guards and
TypeSafeDecisionModelmake model calls, each namedmonitor call, which nest in the judgement. The model still shows as the call's model and inls_model_name. A call the monitor makes again after HTTP 429 adds one moremonitor call, and the one before it ends with the rate-limit error, inside a judgement that succeeds.OpenRouterDecisionModelsends its request without LangChain, so its judgement holds amonitor classifierspan instead. - Errors. A halt is a decision like any other, so it never marks a span as
failed. A span ends with an error only when the step fails: a model or
monitor call raises, a sample is cancelled because another one failed, or
SynchronousRunErroris raised; or with theGraphInterruptwhen a middleware inside the monitor callsinterrupt(). - No handler, no span. With no callback handler attached, the monitor opens no span.
Beside model, every tracer also shows the monitor's four graph nodes, which
open no span of their own: monitor[main].before_agent and
monitor[main].after_agent once per run, and monitor[main].before_model and
monitor[main].after_model around each step.
Allow for the graph steps
says what each does.
LangSmith alone also shows the runs in which create_agent wraps each
middleware hook 1:
- an empty
monitor[main].wrap_model_call, orawrap_model_callunderainvoke(), besidemonitor stepundermodel, or inside the hook runs of the middleware listed before the monitor, as in a Deep Agent; - the hook runs of the middleware listed after the monitor, such as the ones
Deep Agents adds, around each sample, so a sample sits one level below
monitor stepfor each of them; - a
monitor[main].wrap_tool_call, orawrap_tool_call, around each tool call, undertoolsor inside the hook runs of the middleware listed before the monitor, a Deep Agent'staskcall and the subagent it starts included.
Know what each span holds¶
| Span | Inputs | Outputs | Tags |
|---|---|---|---|
monitor step |
step_number, and proposed_step, the step first proposed, added when the span ends |
outcome, flagged, blocked_count, max_suspicion, and each sample's suspicion, reason and executed |
monitor |
monitor judgement |
sample_number, and monitor_class, the monitor's class name |
suspicion and reason |
monitor |
monitor classifier |
model, and questions, each question's text by its key |
answers, the probability of yes to each question; one span covers a request and all its retries |
monitor |
monitor decision |
Empty | outcome, flagged and max_suspicion |
monitor, monitor:<outcome>, and monitor:flagged when the step is flagged |
- Proposal text. Only the step span carries any: its
proposed_step. Each sample's text stays on the model call that drew it. - Sample numbers. A judgement's
sample_numbercounts the step's samples in the order their draws started, so judgement n describes the n-th entry of the step span'ssamples, under parallel resampling too. - Steps without samples. A step decided before any sample, such as a
standing halt, a subagent's halt or Auto Mode's thread total, has
Noneas itsproposed_stepandmax_suspicion. - Failed steps. When a step fails before it is committed, its
proposed_stepis the first sample the monitor had judged, if any.
Every monitor span of a step carries flat metadata keys that name the step:
| Metadata key | Holds | On |
|---|---|---|
monitor_label |
The monitor's label |
Every monitor span |
monitor_agent |
The agent: main, or a subagent's name |
Every monitor span |
monitor_step_number |
The step's number, as in monitor_log |
Every monitor span |
monitor_protocol |
The protocol's class, such as AutoMode |
Every monitor span |
monitor_step_id |
The step span's run id | Every monitor span |
monitor_delegation_id |
The id of the task call that started the subagent |
Every monitor span inside a subagent that a monitored agent started |
monitor_outcome, monitor_flagged |
The decision | monitor decision |
monitor_max_suspicion |
The highest suspicion among the step's samples | monitor decision, when the step judged a sample |
ls_agent_type, set to middleware |
Keeps the span out of LangSmith's Trajectory view, except inside a Deep Agents subagent; see Limits | monitor judgement, monitor classifier and monitor decision |
monitor_label, monitor_agent, monitor_step_number and, inside a subagent,
monitor_delegation_id match a span to its StepRecord in monitor_log,
whose monitor, agent, step_number and delegation_id hold the same
values.
monitor_step_id is the same on every monitor span of a step, so one filter
on it gathers the step's spans. In LangSmith it is also the step span's own
id; Langfuse does not keep LangChain's run ids, and gives each observation an
id of its own 2.
The model calls the library's monitors make carry ls_message_view_exclude,
which keeps them out of LangSmith's Trajectory view too
3. The library sets neither key on the step span,
because the agent's own samples nest in it.
Each model call carries its own token usage in both tools. LangSmith also adds up the usage of the calls below each run, so a step span's token count covers its samples and the monitor's calls together, and so does the trace's total.
The spans add no tag or metadata to the LangChain runs inside them, model
calls included. LangSmith's own traceable runs are the exception: LangSmith
copies a span's metadata into every traceable run opened inside it
4. In LangSmith, three kinds of run inside a span therefore
carry its monitor_ keys:
- the
wrap_model_callhook runs of the middleware listed after the monitor, which sit inside the step span around each sample; - with two monitors stacked, the inner monitor's hook run, which carries the outer monitor's keys;
- code in a monitor that traces itself with
traceable, which nests under its judgement and also takes the judgement'sls_agent_type.
So in LangSmith, filter on a span's name together with its metadata, never on
the metadata alone. Langfuse never receives traceable runs, so there only
the monitor's spans carry monitor_ keys.
Attach LangSmith¶
LangSmith needs no code. Set its environment variables before the agent runs:
# Turn tracing on and send the traces to one project.
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="your key"
export LANGSMITH_PROJECT="monitored-agent"
Then run the agent as usual:
from langchain.agents import create_agent
from langchain_sync_monitors import LLMMonitor, MonitorMiddleware, TrustedMonitoring
agent = create_agent(
"openrouter:deepseek/deepseek-v4.1-flash",
tools=[],
middleware=[
MonitorMiddleware(
monitor=LLMMonitor(model="openrouter:xiaomi/mimo-v2.6-pro"),
protocol=TrustedMonitoring(audit_threshold=0.6),
),
],
)
inputs = {"messages": [{"role": "user", "content": "Summarise q3.md."}]}
# The LangSmith client sends the trace in the background, and the rest at exit.
result = agent.invoke(inputs)
Without LANGSMITH_PROJECT, the traces go to the default project.
Attach Langfuse¶
Langfuse's LangChain integration is a callback handler that you pass to each run. The library does not depend on Langfuse's SDK, so install it and set its keys:
# Langfuse's SDK, and the keys of your Langfuse project.
uv add langfuse # or: pip install langfuse
export LANGFUSE_PUBLIC_KEY="your public key"
export LANGFUSE_SECRET_KEY="your secret key"
# Your project's region: https://cloud.langfuse.com in the EU, https://us.cloud.langfuse.com in the US.
export LANGFUSE_BASE_URL="https://cloud.langfuse.com"
Pass the handler in the run's config, with the agent built above:
from langfuse import get_client
from langfuse.langchain import CallbackHandler
# The handler turns each run of this call into a Langfuse observation.
langfuse_handler = CallbackHandler()
result = agent.invoke(inputs, config={"callbacks": [langfuse_handler]})
# Send the spans now, as a notebook, a server or a script that exports scores must.
get_client().flush()
This is what Langfuse makes of a span 25:
- each span becomes an observation of type
CHAIN, with the span's name and its metadata keys; - a span's tags go into its metadata under
tags, and only the root run's tags become the trace's tags; - a metadata filter compares values as strings, so it matches
monitor_flaggedwith"true",monitor_step_numberwith a string such as"1", andmonitor_max_suspicionwith one such as"0.9". The API returns the values as JSON again:true,1and0.9.
Find halted and flagged steps¶
The decision span carries the outcome, so one filter finds every halted or flagged step:
| To find | LangSmith | Langfuse |
|---|---|---|
| Every halted step | and(eq(name, "monitor decision"), has(tags, "monitor:halted")) on runs |
Name monitor decision, and metadata monitor_outcome equal to halted |
| Traces with a flagged step | has(tags, "monitor:flagged") as a tree filter, on root runs |
Name monitor decision, and metadata monitor_flagged equal to true |
| Everything but the monitor's spans | and(neq(name, "monitor step"), neq(name, "monitor judgement"), neq(name, "monitor classifier"), neq(name, "monitor decision")) |
Name none of monitor step, monitor judgement, monitor classifier and monitor decision |
| Everything but the monitor's spans and model calls | and(neq(name, "monitor step"), neq(name, "monitor judgement"), neq(name, "monitor classifier"), neq(name, "monitor decision"), neq(name, "monitor call"), neq(metadata_key, "ls_message_view_exclude")) |
Name none of monitor step, monitor judgement, monitor classifier, monitor decision and monitor call, which misses the attempts inside a classifier wrapped in with_retry() |
| Steps above a suspicion | and(eq(name, "monitor decision"), eq(metadata_key, "monitor_max_suspicion"), gte(metadata_value, 0.5)), with the threshold as a number; with export_scores, also and(eq(feedback_key, "monitor_suspicion"), gte(feedback_score, 0.5)) |
Only with export_scores: scores named monitor_suspicion with a minimum value, as Find steps by suspicion shows; metadata filters compare strings only |
Replace halted with allowed, resampled, steered or
deferred_to_trusted to find those steps. LangSmith's filter language cannot
negate a tag, so leave the monitor's spans out by name. Langfuse keeps a
span's tags only in its metadata, so filter on monitor_outcome and
monitor_flagged there.
The monitor's own model calls carry no monitor tag, but each one it makes
through LangChain is named monitor call, whatever its model: the calls of
the LLM monitors and the guards, and the classifier call of
TypeSafeDecisionModel. The fixed name
replaces any name you gave the model, so a model built with
name="security monitor" shows as monitor call too. The model still shows as
the call's model and in its ls_model_name metadata, and the judgement span
around the call names the monitor. A classifier wrapped in with_retry()
gives the name to the wrapper's run, and the attempts inside it keep the
classifier's own name, so the five names miss them. The trusted model's step,
which a fallback writes for the agent, keeps its model's name. A custom
monitor's calls are named monitor call only if it builds their config with
model_calls.build_internal_call_config.
Every call the library's monitors make also carries the metadata key
ls_message_view_exclude, the wrapped classifier's attempts included, and
LangChain's and Deep Agents' own middleware do not set it. So in LangSmith,
neq(metadata_key, "ls_message_view_exclude") leaves out every one of the
monitor's calls. Langfuse's metadata filter matches only the observations
that have a key, so it can find these calls but not leave them out. Their
lc_source names the monitor too, such as llm_monitor, but LangChain's
middleware sets lc_source on its own internal calls as well, such as its
summaries, so it does not single out the monitor's calls.
In LangSmith's UI, switch the table from Traces to Runs to filter spans by
name, tags and metadata 6. The same strings work with the
SDK's Client.list_runs, where tree_filter matches any run in a trace:
from langsmith import Client
client = Client()
# Every halted step: its trace, and the agent and step it belongs to.
halted_steps = client.list_runs(
project_name="monitored-agent",
filter='and(eq(name, "monitor decision"), has(tags, "monitor:halted"))',
)
for run in halted_steps:
print(run.trace_id, run.metadata["monitor_agent"], run.metadata["monitor_step_number"])
# Every trace in which some step was flagged.
flagged_traces = client.list_runs(
project_name="monitored-agent",
is_root=True,
tree_filter='has(tags, "monitor:flagged")',
)
for run in flagged_traces:
print(run.trace_id, run.name)
LangSmith's SDK 0.14.1 deprecates list_runs, to be removed after 31 January
2027 4. Its successor, client.runs.query, takes the same
filter and tree_filter strings and the same is_root flag, but takes
project ids, returns only ids unless selects names more fields, and looks
back one day unless min_start_time says otherwise. It returns an
asynchronous iterator, even on the synchronous Client, so read it with
async for.
In Langfuse's UI, filter the observations by name and by metadata key. The same conditions, as JSON, go to Langfuse's observations API through its SDK:
import json
from langfuse import get_client
# The decision spans whose outcome is halted.
halted_filter = [
{"type": "string", "column": "name", "operator": "=", "value": "monitor decision"},
{
"type": "stringObject",
"column": "metadata",
"key": "monitor_outcome",
"operator": "=",
"value": "halted",
},
]
observations = get_client().api.observations.get_many(
filter=json.dumps(halted_filter),
fields="core,basic,metadata",
)
for observation in observations.data:
metadata = observation.metadata
print(observation.trace_id, metadata["monitor_agent"], metadata["monitor_step_number"])
For flagged steps, filter on the key monitor_flagged with the value
"true", a string. The API returns 50 observations a page unless limit
asks for more, up to 1,000; pass observations.meta.cursor back as cursor
for the next page. Langfuse ingests spans asynchronously, and its SDK says
that flushed data may not be queryable for 15 to 30 seconds
2, so a step can take that long after flush() to appear in
a query.
Send suspicion as a score¶
Neither tool charts or aggregates metadata, and Langfuse's metadata filters
compare strings only. With export_scores, each committed step's highest
suspicion also becomes a score, which both tools can filter, sort and chart
78. It is off by default. Name
the tools the run is traced to:
from langchain_sync_monitors import LLMMonitor, MonitorMiddleware, Tracer, TrustedMonitoring
monitor = MonitorMiddleware(
monitor=LLMMonitor(model="openrouter:xiaomi/mimo-v2.6-pro"),
protocol=TrustedMonitoring(audit_threshold=0.6),
# Off by default; each tool named here also gets every step's suspicion.
export_scores={Tracer.LANGSMITH, Tracer.LANGFUSE},
)
The score is named after the monitor's label, <label>_suspicion, so
monitor_suspicion by default, and sits on the step's monitor step span:
| LangSmith | Langfuse | |
|---|---|---|
| What the score is | Feedback on the step span's run, whose id is the step's monitor_step_id |
A NUMERIC score on the step's observation, found by its monitor_step_id |
| Where it goes | The tracer's project, through its client's endpoint, key and workspace | The project that LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY reach, at LANGFUSE_BASE_URL or LANGFUSE_HOST |
| What building the monitor needs | LANGSMITH_API_KEY (or LANGCHAIN_API_KEY) in the environment, even when the tracer's own Client holds a key |
Both Langfuse keys and the langfuse package |
| When it lands | Within about 10 seconds of the step | Once Langfuse has ingested the step, usually 10 to 25 seconds after it in our checks, at times more than 30 |
| The requests it makes | One POST /feedback per score, up to 6 at once, and one project lookup per project |
Up to two step lookups a window, of up to 3 pages each: one for the fresh steps, and once a minute one for the stale ones. Then one ingestion request for the steps found. Know what it costs has the rate limits |
A monitor whose tool lacks its credentials or package raises
ConfigurationError when it is built. A step with no sample, such as a
standing halt, gets no score, and two monitors with different labels each
send their own. Tracer in the API reference has the
full rules, forked processes included.
Know what it costs¶
- No text leaves the process. Only the score's name, value and ids are sent, never the monitor's reason.
- LangSmith's retention is unchanged. The feedback is sent with
extend_trace_retentionfalse. Feedback sent with it true moves the whole trace to extended retention, which costs more, and the endpoint's default is true 9. - Rate limits. Each LangSmith feedback is one request. Langfuse's lookups
spend its general API rate limit, which every project and key of an
organisation shares (the "General API" row of the table under "Rate
Limits", and section "How rate limits are calculated")
10. One process looks up the
fresh steps every 10 seconds and the stale ones once a minute, up to 3
pages each: at most 21 requests a minute, and 7 when each lookup fits one
page of 1,000 observations. A minute that holds the 30-second exit drain,
which looks every 5 seconds, takes about half as many again, up to 33. A
page that meets a server error or a network failure is sent once more, so
up to twice as many in all. The scores go in through the ingestion API,
which has a limit of its own (the "Tracing" row of the same table)
10. A
429holds every call to that tool until itsRetry-After, for at most 5 minutes. - The agent never waits. One background thread per process sends the
scores, every 10 seconds. A failure never reaches the run. The library's
own loggers, mostly
langchain_sync_monitors.score_worker, log it with the tool, the error's type and any HTTP status, but never the error's text or traceback. A request sent again is logged by stamina's logger,stamina, atWARNING, asstamina.retry_scheduled(modulestamina/instrumentation/_logging.py) 11, with the wait and the repr of a stand-in for httpx's error (section "Standard library'slogging") 12, which names the error's type and HTTP status alone, never the request, its headers or the reply. httpx's ownHTTP Requestline at INFO quotes the request's URL, which holds no key and no text of a run: the tool's endpoint, without any user name or password, the path, and a lookup's query. The query names the LangSmith project, or asks Langfuse for themonitor stepobservations that started in a time window, a page at a time. When structlog is installed, stamina logs the retry through structlog instead (section "Defaults"), atWARNINGtoo, with the same fields, the error's repr ascaused_byincluded (section "structlog") 12, as Turn on log messages says.
Know what happens at exit¶
At exit, the worker keeps sending what waits, every 5 seconds, for up to 30 seconds, then logs and drops what is left. A script that ends right after its last step so waits for its last Langfuse scores, usually 7 to 20 seconds in our checks. Two things can make it drop them instead:
- Spans still buffered. A Langfuse client registers its own exit hook,
which sends the spans it still buffers, when it is built 2,
so before the first score is queued, and Python runs exit hooks last in,
first out 13. The drain therefore runs before that flush, and
the last steps reach Langfuse during the drain only if the client's flush
interval sends them:
LANGFUSE_FLUSH_INTERVAL, 5 seconds by default 2. At 25 seconds or more, it can send them too late, and the drain then drops their scores after its full 30 seconds. Callget_client().flush()before the script ends, as Attach Langfuse does, or keep the interval well under 30 seconds. With an interval of 60 seconds, a script that flushed wrote both of its steps' scores and exited 7 to 13 seconds after its run; without the flush, it dropped both after 31 to 32 seconds. - Slow ingestion. Langfuse's ingestion time varies, and its SDK says flushed data may not be queryable for 15 to 30 seconds 2. When it passes the drain's 30 seconds, as it did for every run, flushed or not, in one stretch of our checks, the last steps' scores are dropped at exit. A program that must keep them should keep running, as a server does, or accept the loss.
The drain also spends its whole 30 seconds when a step is never found in the project the writer looks in. That happens when:
- the Langfuse handler was built with other keys, or another host, than the environment's, so its steps sit in a project the writer cannot see, and their scores go nowhere;
- Langfuse sampled the trace out, at a sample rate between 0 and 1;
- the spans never reached Langfuse, as after a network failure.
Outside the drain, such a score is given up after 5 minutes, and the warning
names these causes once per process. Scores still waiting are lost when the
process ends without running atexit: on os._exit, which a
multiprocessing child started by fork calls, on SIGKILL, on SIGTERM without
a handler, and when a Jupyter kernel is killed.
Know when nothing is sent¶
A run traced to neither tool sends nothing, and says nothing. Neither do:
- a run with LangSmith tracing turned off by
tracing_context(enabled=False); - a LangSmith client in OpenTelemetry mode, whose run ids are not the steps' ids, which is warned once per process 4;
- a Langfuse handler whose tracing is off, by
LANGFUSE_TRACING_ENABLED=false, or by a client built withtracing_enabled=Falseorsample_rate=0.
Langfuse keeps the last two settings private, so the library reads them from
private attributes of the handler's client: _tracing_enabled, and the rate
of its tracer provider's sampler 2. Each read has a default that
counts the handler as tracing, so a handler of another shape still sends. The
reads let a program with Langfuse tracing off exit at once, rather than spend
the 30-second drain on steps that never reach Langfuse. LangSmith accepts
feedback on a run it never ingested, such as one its sampling dropped, so
such a score is lost without a sign.
Find steps by suspicion¶
In LangSmith, filter the runs on the feedback key and score:
from langsmith import Client
client = Client()
# Every step whose suspicion is 0.5 or more: the feedback sits on its step span.
suspicious_steps = client.list_runs(
project_name="monitored-agent",
filter='and(eq(feedback_key, "monitor_suspicion"), gte(feedback_score, 0.5))',
)
for run in suspicious_steps:
print(run.trace_id, run.metadata["monitor_agent"], run.metadata["monitor_step_number"])
In Langfuse, ask the scores API for the score's name and a lowest value:
from langfuse import get_client
# Every step whose suspicion is 0.5 or more, with the observation it sits on.
scores = get_client().api.scores_v3.get_many_v3(
name="monitor_suspicion",
data_type="NUMERIC",
value_min=0.5,
fields="subject",
)
for score in scores.data:
print(score.subject.trace_id, score.subject.id, score.value)
Watch the spans in astream_events¶
astream_events and astream_log report every run, so they report the
monitor's spans too. The stream() modes and stream_events(version="v3") do
not carry them. This prints each step's number, tags and decision as the
protocol decides it:
import asyncio
async def print_decisions() -> None:
"""Print each step's number, tags and decision as the protocol decides it."""
async for event in agent.astream_events(inputs, version="v2"):
if event["event"] == "on_chain_end" and event["name"] == "monitor decision":
step_number = event["metadata"]["monitor_step_number"]
print(step_number, event["tags"], event["data"]["output"])
asyncio.run(print_decisions())
A scripted run with the same protocol, Trusted Monitoring with an audit threshold of 0.6, in which the monitor scored the first step 0.9 and the second 0.1, printed:
1 ['monitor', 'monitor:allowed', 'monitor:flagged'] {'outcome': 'allowed', 'flagged': True, 'max_suspicion': 0.9}
2 ['monitor', 'monitor:allowed'] {'outcome': 'allowed', 'flagged': False, 'max_suspicion': 0.1}
To drop the spans, pass exclude_tags=["monitor"]:
# The model calls inside the spans, samples and monitor calls alike, still come through.
events = agent.astream_events(inputs, version="v2", exclude_tags=["monitor"])
To drop the monitor's calls as well, skip the events whose event["metadata"]
holds ls_message_view_exclude, or whose event["name"] is monitor call.
astream_events reports every sample live,
rejected ones included, so a user interface should read
stream_mode="messages" instead, as
Know what each stream shows
explains.
Limits¶
- What is confirmed live. The test suite checks the spans and their nesting
offline, with a recording handler and with LangSmith's tracer on a mock
client, where it also checks the parent of each span LangSmith receives. No
test runs Langfuse's handler. Read back through their APIs, LangSmith and
Langfuse Cloud show the trees, fields and filters on this page as described,
with langsmith 0.14.1 and langfuse 4.15.6, under
invoke()andainvoke(). That covers allowed, steered, resampled, deferred and halted steps, a step whose monitor call raised, a Deep Agents subagent, the Decisions API monitor against a fake server, and real chat, guard and subagent model calls. The Langfuse handler, the two query examples and theastream_eventsexample run as written. Themonitor callname, the model each tool shows for such a call, and the four- and five-name filters were confirmed live in both tools too, with langsmith 0.14.2, langfuse 4.16.0 and a real OpenRouter model. A cancelled sample,SynchronousRunError, stacked monitors, parallel resampling, a monitor's owntraceablecode,TypeSafeDecisionModel, the attempts of a classifier wrapped inwith_retry()and what each tool's UI shows, its Trajectory view included, rest on the tests and the tools' source alone. The score export was confirmed live with langsmith 0.14.1 and langfuse 4.16.0, underinvoke()andainvoke(), with a scripted agent and with real OpenRouter models: each step got one score, with its highest suspicion, on its ownmonitor steprun and observation, and the two queries in Find steps by suspicion found the steps at 0.5 or more and no other. A program whose Langfuse client was built withtracing_enabled=Falseorsample_rate=0exited about a second after its run, where the full 30-second drain ran once the private read was bypassed. What each tool's UI shows for the scores rests on their APIs alone. - Two tracers together can split a trace. The monitor opens its spans
from the model node's own callback manager, so they nest under
modelfor every handler. With LangSmith and a second tracer attached together, other runs name a LangSmith-only hook run as their parent, one the second tracer never saw. A tool call runs inside the monitor'swrap_tool_callhook, a Deep Agent'staskcall included, and the agent's own model calls run inside the hook runs of the middleware listed after the monitor, as the ones Deep Agents adds. Underainvoke(), Langfuse then puts such a run, with everything inside it, in a trace of its own 2, so in a Deep Agent each sample, each tool call and each whole subagent lands in a separate trace. Underinvoke()the trace stays whole. The hook runs cause the split, not the spans. Attach one tracer at a time to avoid it. - A subagent's spans may show in its Trajectory view. Inside a Deep
Agents subagent, LangSmith sets
ls_agent_typetosubagenton every run, the monitor's spans included,monitor stepamong them. So the monitor's spans may show in the subagent's Trajectory view, wheremiddlewarewould have kept them out. - Suspicion is a score only with
export_scores. Without it, suspicion sits in the spans' outputs and in the decision span's metadata, so neither tool charts or aggregates steps by suspicion. LangSmith keepsmonitor_max_suspicionas a number, so its filters can compare it with a threshold, as the table above shows; Langfuse's metadata filters compare strings only. The score holds only each step's highest suspicion; read every sample's frommonitor_log, as Collect honest scores for calibration shows.
Related guides¶
- Read the monitor log for the records the spans mirror, and the streams.
- Monitor Deep Agents subagents for the delegation ids that a subagent's spans carry.
- Tracing for how the spans are opened.
References¶
-
Harrison Chase. LangChain: agent middleware (langchain.agents.middleware). 2026. URL: https://github.com/langchain-ai/langchain. ↩
-
Langfuse. Langfuse Python SDK: the LangChain callback handler (langfuse.langchain). 2026. URL: https://github.com/langfuse/langfuse-python. ↩↩↩↩↩↩↩↩
-
LangChain. Trajectory view integrations: hide custom middleware or guardrails, and exclude runs from the Trajectory view. 2026. URL: https://docs.langchain.com/langsmith/trajectory-view-integrations. ↩
-
LangChain. LangSmith Python SDK: run trees, the traceable decorator and the client (langsmith). 2026. URL: https://github.com/langchain-ai/langsmith-sdk. ↩↩↩
-
Langfuse. Langfuse: the server and its ingestion worker. 2026. URL: https://github.com/langfuse/langfuse. ↩
-
LangChain. View traces: the Trajectory view and its metadata keys. 2026. URL: https://docs.langchain.com/langsmith/view-traces. ↩
-
LangChain. Dashboards. 2026. URL: https://docs.langchain.com/langsmith/dashboards. ↩
-
Langfuse. Scores: overview. 2026. URL: https://langfuse.com/docs/evaluation/scores/overview. ↩
-
LangChain. Administration overview: data retention auto-upgrades. 2026. URL: https://docs.langchain.com/langsmith/administration-overview. ↩
-
Langfuse. Are there any limits to the Langfuse API? 2026. URL: https://langfuse.com/faq/all/api-limits. ↩↩
-
Hynek Schlawack. stamina: production-grade retries for Python. 2026. URL: https://github.com/hynek/stamina. ↩
-
Hynek Schlawack. stamina 26.1.0 documentation: instrumentation. 2026. URL: https://stamina.hynek.me/en/26.1.0/instrumentation.html. ↩↩
-
Python Software Foundation. CPython: the Python interpreter and its standard library (atexit, threading, urllib.request). 2026. URL: https://github.com/python/cpython. ↩