{"record":{"id":"9dc4a94bb758a9ef","repo":"langchain-ai/deepagents","slug":"ptc-bridge-called-outside-active-eval","errorCode":null,"errorMessage":"PTC bridge called outside active eval","messagePattern":"PTC bridge called outside active eval","errorType":"exception","errorClass":"ConcurrentEvalError","httpStatus":null,"severity":"error","filePath":"libs/partners/quickjs/langchain_quickjs/_repl.py","lineNumber":713,"sourceCode":"        `tool.ainvoke` without blocking the event loop. We look the\n        tool up through `self._registered_tools` on every call so a\n        later `install_tools` that swaps the underlying object (same\n        name, different instance) is picked up without re-registration.\n        \"\"\"\n        ctx = self._require_ctx()\n        registered = self._registered_tools\n\n        async def _bridge(raw_input: Any = None) -> Any:\n            tool = registered.get(camel)\n            if tool is None:\n                # Shouldn't happen — we only rewrite `globalThis.tools`\n                # with names currently in the map — but if a race causes\n                # it, fail loud.\n                msg = f\"tool '{camel}' not registered\"\n                raise RuntimeError(msg)\n            if self._ptc_state is None:\n                msg = \"PTC bridge called outside active eval\"\n                raise ConcurrentEvalError(msg)\n            state = self._ptc_state.consume_call_budget(\n                function_name=f\"tools.{camel}\",\n                max_ptc_calls=self._max_ptc_calls,\n            )\n            self._ptc_state = state\n            payload = _normalize_tool_input(raw_input)\n            call_id = _synth_tool_call_id(tool.name)\n            # Inject runtime/state/store ourselves; `InjectedToolCallId`\n            # is handled inside `_ainvoke_tool_on_outer_loop` via\n            # `tool.arun(..., tool_call_id=...)`. The bridge intentionally\n            # avoids the tool-call envelope path because it wraps the\n            # result in a `ToolMessage` and string-coerces `.content`,\n            # destroying native return types (lists, dicts, numbers).\n            args = _inject_tool_args_for_ptc(\n                tool, payload, state.outer_runtime, call_id\n            )\n            result = await self._ainvoke_tool_on_outer_loop(\n                tool,","sourceCodeStart":695,"sourceCodeEnd":731,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/partners/quickjs/langchain_quickjs/_repl.py#L695-L731","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: calling a captured bridge tool after the eval\nconst tool = getBridgedTool();\nawait runEval(js); // eval finishes, clears state\nawait tool.call({...}); // ConcurrentEvalError\n\n// after: invoke within the eval\nawait runEval(`await tools.readFile('x.txt')`);","handlingStrategy":"validation","validationCode":"def assert_repl_active(repl):\n    if getattr(repl, \"_ptc_state\", None) is None:\n        raise RuntimeError(\"PTC bridge used outside an active eval\")","typeGuard":"def has_active_ptc(repl) -> bool:\n    return getattr(repl, \"_ptc_state\", None) is not None","tryCatchPattern":"try:\n    await run_eval(js_code)\nexcept ConcurrentEvalError:\n    # bridge used outside an active eval; restart the eval or re-bind the tool\n    restart_eval_session()","preventionTips":["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."],"tags":["concurrency","state","eval-lifecycle","quickjs"],"backgroundTag":"tool-called-outside-active-session","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}