langchain-ai/deepagents · error · RuntimeError

tool '{camel}' not registered

Error message

tool '{camel}' not registered

What it means

The tool bridge raises this RuntimeError when a host-function bridge for a camelCase tool name is invoked but the name is no longer present in `_registered_tools`. Per the source comment this 'shouldn't happen' — `globalThis.tools` is only rewritten with currently-registered names — so the library fails loudly rather than silently swallowing it, suggesting a race between bridge registration and tool-map updates.

Source

Thrown at libs/partners/quickjs/langchain_quickjs/_repl.py:710

        """Install a host-function bridge for one camel-cased tool name.

        The bridge is async so `eval_async`'s driving loop can await
        `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

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Don't mutate the tool set (call `install_tools`) while an eval using `tools.*` is in flight
  2. Always pass the full current tool list to `install_tools` so names never disappear mid-run
  3. Use one REPL per concurrent evaluation to avoid shared-state races
Defensive patterns

Strategy: try-catch

Validate before calling

# Python side: install tools only before evals start, with the full set
repl.install_tools(all_tools)  # never narrow the set mid-eval

Type guard

def tool_stable(repl, name: str) -> bool:
    return name in repl._registered_tools

Try / catch

try:
    const r = await tools.myTool(args);
} catch (e) {
  if (String(e).includes('not registered')) {
    // tool set changed mid-eval; do not retry on same REPL
    console.warn('tool removed during eval');
  } else { throw e; }
}

Prevention

When it happens

Trigger: JS calls `tools.someTool(...)` where `someTool` exists on `globalThis.tools` but was removed/rotated out of `_registered_tools` concurrently — e.g. `install_tools` ran with a narrowed toolset while an in-flight eval still holds a reference to the old bridge.

Common situations: Calling `install_tools` with a reduced tool set while an eval is mid-flight, REPL shared across threads with racing tool installs, or stale JS cached in the context referencing removed tools.

Related errors


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