can1357/oh-my-pi · error · AttributeError

name

Error message

name

What it means

The `tool` proxy raises AttributeError for any attribute access starting with `_`, reserving underscore-prefixed names for Python internals and preventing them from being mistaken for tool calls. Accessing `tool._something` (or Python's own dunder probing like `tool.__deepcopy__`) hits this path instead of creating a `_ToolCallable`.

Source

Thrown at packages/coding-agent/src/eval/py/prelude.py:454

            elif isinstance(args, dict):
                merged = dict(args)
            else:
                raise TypeError(
                    f"tool.{self._name}(...) expects a dict of arguments (got {type(args).__name__})"
                )
            merged.update(kwargs)
            if INTENT_FIELD not in merged:
                merged[INTENT_FIELD] = "py prelude"
            return _bridge_call(self._name, merged)

    class _ToolProxy:
        """`tool.<name>(args)` proxy mirroring the JS runtime bridge."""

        __slots__ = ()

        def __getattr__(self, name: str) -> _ToolCallable:
            if name.startswith("_"):
                raise AttributeError(name)
            return _ToolCallable(name)

        def __getitem__(self, name: str) -> _ToolCallable:
            return _ToolCallable(name)

        def __repr__(self) -> str:
            session = os.environ.get("PI_TOOL_BRIDGE_SESSION")
            return (
                f"<tool proxy session={session}>"
                if session
                else "<tool proxy unavailable>"
            )

    tool = _ToolProxy()

    def completion(prompt, *, model="default", system=None, schema=None):
        """Oneshot, stateless completion against a model tier.

View on GitHub (pinned to 9690622007)

Solutions

  1. Drop the leading underscore and call the tool by its real public name: `tool.read_file(...)`.
  2. If you need a tool whose name genuinely starts with `_`, use the item form which bypasses the guard: `tool["_name"](...)`.
  3. Don't try to access Python-internal attributes on the proxy; it intentionally exposes only tool-callable names.

Example fix

// before
private = tool._debug_state()  # AttributeError

// after
if tool_name.startswith("_"):
    private = tool[tool_name]()   # item access bypasses the underscore guard
else:
    private = getattr(tool, tool_name)()
Defensive patterns

Strategy: try-catch

Validate before calling

name = "_debug"
if name.startswith("_"):
    print("underscore names are reserved; use tool[name] item access for such tools")

Try / catch

try:
    callable_ = getattr(tool, name)
except AttributeError:
    if name.startswith("_"):
        callable_ = tool[name]  # item access bypasses the underscore guard
    else:
        raise

Prevention

When it happens

Trigger: Accessing `tool._internal`, calling `hasattr(tool, "_x")`, or any protocol probe that looks up `__<name>__` / `_<name>` attributes on the proxy.

Common situations: Accidentally treating `tool` as a module and reaching for private helpers; serialization/copy frameworks probing dunder methods (`__getstate__`, `_repr_html_`); an IDE autocompletion exploring underscore names.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/8491b5dea1659a12. Report an issue: GitHub.