langchain-ai/deepagents · error · ConcurrentEvalError
PTC bridge called outside active eval
Error message
PTC bridge called outside active eval
What it means
Raised by the PTC (prompt-to-code) tool bridge in the QuickJS REPL when a tool bridged into the sandbox is invoked but no eval session is currently active (`self._ptc_state` is None). The library requires every bridged tool call to be attributed to a running eval so it can consume the per-eval PTC call budget; a call outside that window is a state-machine violation.
Source
Thrown at libs/partners/quickjs/langchain_quickjs/_repl.py:713
`tool.ainvoke` without blocking the event loop. We look the
tool up through `self._registered_tools` on every call so a
later `install_tools` that swaps the underlying object (same
name, different instance) is picked up without re-registration.
"""
ctx = self._require_ctx()
registered = self._registered_tools
async def _bridge(raw_input: Any = None) -> Any:
tool = registered.get(camel)
if tool is None:
# Shouldn't happen — we only rewrite `globalThis.tools`
# with names currently in the map — but if a race causes
# it, fail loud.
msg = f"tool '{camel}' not registered"
raise RuntimeError(msg)
if self._ptc_state is None:
msg = "PTC bridge called outside active eval"
raise ConcurrentEvalError(msg)
state = self._ptc_state.consume_call_budget(
function_name=f"tools.{camel}",
max_ptc_calls=self._max_ptc_calls,
)
self._ptc_state = state
payload = _normalize_tool_input(raw_input)
call_id = _synth_tool_call_id(tool.name)
# Inject runtime/state/store ourselves; `InjectedToolCallId`
# is handled inside `_ainvoke_tool_on_outer_loop` via
# `tool.arun(..., tool_call_id=...)`. The bridge intentionally
# avoids the tool-call envelope path because it wraps the
# result in a `ToolMessage` and string-coerces `.content`,
# destroying native return types (lists, dicts, numbers).
args = _inject_tool_args_for_ptc(
tool, payload, state.outer_runtime, call_id
)
result = await self._ainvoke_tool_on_outer_loop(
tool,View on GitHub (pinned to a1af029e6e)
Solutions
- Ensure every bridged tool call happens synchronously within an active eval run driven by the middleware's eval tool.
- Do not share one REPL/middleware instance across concurrent evals; create one per run.
- Check that JS code does not schedule tool calls in promises/timers that outlive the eval.
- If concurrency is intentional, serialize tool calls or upgrade to a version that guards/retries the race.
Example fix
// before: calling a captured bridge tool after the eval
const tool = getBridgedTool();
await runEval(js); // eval finishes, clears state
await tool.call({...}); // ConcurrentEvalError
// after: invoke within the eval
await runEval(`await tools.readFile('x.txt')`); Defensive patterns
Strategy: validation
Validate before calling
def assert_repl_active(repl):
if getattr(repl, "_ptc_state", None) is None:
raise RuntimeError("PTC bridge used outside an active eval") Type guard
def has_active_ptc(repl) -> bool:
return getattr(repl, "_ptc_state", None) is not None Try / catch
try:
await run_eval(js_code)
except ConcurrentEvalError:
# bridge used outside an active eval; restart the eval or re-bind the tool
restart_eval_session() Prevention
- Never call bridged tools outside the eval invocation that owns them.
- Use one REPL/middleware instance per concurrent eval; do not share state.
- Avoid fire-and-forget JS promises or timers that outlive the eval.
When it happens
Trigger: Calling a bridged tool (e.g. `tools.<camel>` from inside the sandbox) after the eval finished or before it started, invoking the bridge from a stale/reused REPL instance, or a race where the eval teardown cleared `_ptc_state` while a queued tool call was still executing.
Common situations: Running multiple QuickJS evals concurrently against a shared REPL/middleware instance; holding a reference to a bridged tool across `await` boundaries and calling it after the eval completes; fire-and-forget JS promises resolving after the eval returns.
Related errors
- task bridge called outside active eval
- tool '{camel}' not registered
- task() requires an active ToolRuntime
- Pending graph work remained on thread {thread_id} after clea
- `expires_at` is only meaningful when `logged_in` is True.
AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29).
Data as JSON: /api/errors/9dc4a94bb758a9ef.
Report an issue: GitHub.