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

  1. Ensure every bridged tool call happens synchronously within an active eval run driven by the middleware's eval tool.
  2. Do not share one REPL/middleware instance across concurrent evals; create one per run.
  3. Check that JS code does not schedule tool calls in promises/timers that outlive the eval.
  4. 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

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


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/9dc4a94bb758a9ef. Report an issue: GitHub.