Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
- **What stays unresolved, by name.** A module the scope does not contain (the scope spelled from the repository root, `svc.app.tools` with scope `svc/app`, is read inside it), a relative import above the scope, more than one matching module location, a name bound twice or only inside an `if`/`try`, a wildcard import, an import cycle, a class or other value, a parameter or other local assignment of the scope that uses the name, a name that scope binds more than once, a module attribute the same module reassigns (`tools.lookup = ...`, `setattr`), an attribute named like a step of the chain on an imported module that code running first reassigns (every module on the chain, the agent's own file included, every enclosing package's `__init__.py`, and every in-scope module those import; the defining module handing its function on does not count), and such a module that cannot be read (a link, or a missing relative module not imported under `except ImportError`), an SDK function without `@function_tool`, a symbolic link, or more than 64 modules read. The gap names the reason (`Not resolved because …`) and is scoped to the agent that lists the tool, so another agent's change in the same file is still established. One agent binding two different functions under one name is named, not resolved. The ADK unresolved-tool warning keeps its wording. No schema or contract change.
- **Identity.** One agent binding two different functions under one name binds neither, in both readers and whatever their order, and names both definitions. `import a.b` then `a.b.f` reads the submodule, as the import system does. A reference is read where it is used: a builder's own import is followed like a module-level one, `nonlocal` follows the outer function, a nested `def` that is the only one of its name is that definition, a module-level agent binds what the module binds at top level (its `def`, its import, its wrapper assignment) and never a same-named `def` or wrapper nested in a function, a module-level list's names are read at module level whatever the building function binds, and a factory's own toolset or wrapper variable is read like a module-level one. An SDK list variable is read only when the scope that binds it binds it once, to a literal list, and every use of it in the file only reads it — iterated, indexed, compared, tested, handed to a read-only builtin, standard-library reader or logger's method (each proven by its binding), to an agent's (or a copy's) own `tools=`, or to a function whose every use of that parameter is such a read. Spreading it (`[*TOOLS, x]`, `f(*TOOLS)`) or testing it (`TOOLS or []` in a condition) is a read. A method call on it, `+=`, a second name (including through `x or y`), a tuple, a return, `*args`, `globals()`, `sys.modules` or importing the module by `__name__` makes it dynamic. For `scan`, a definition that an import reaches and another configured source also reads (spelling the module's path the same way) is one catalog tool, and the binding reaches it through the exact definition the reader resolved; a `{tool: …}` selector for it is not ambiguous, and the dropped copy's guard evidence goes with it. A source an inventory completes keeps its own observation, so when that source imports a definition another source also reads, the catalog holds both and a selector for it is ambiguous. When what the module binds is not established (a name rebound, or bound only inside an `if`) and the ADK reader falls back to a same-named `def` or wrapper, that binding is named but never established: in a comparison its row is `not_established` on whichever side it is present, added, removed or changed, and so is the row of every tool the module's bindings of that name could give the agent instead, each followed to its definition (a function from a module outside the scope by its imported name); every one of the agent's rows is when one of those cannot be followed or a wildcard import could bind the name; for `scan` it stays the medium-confidence shadowed definition it was. `x = FunctionTool(func=x)` right after `def x` wraps that `def`, and is not a guess. Code that runs before the name is used but is not read — a relative import above the scope, an absolute import of a module the repository holds outside the scope (read from the compared commit's tree, or the checkout for `scan`, at the root, under `src/`, or under any directory between the root and the scope, modules the interpreter preloads, a standard-library name at a root that is a regular package, and the scope's own package aside; a linked or submodule entry counts, and a namespace directory only when it holds the named submodule, so SDK apps under `agents/` still import the SDK), a relative module no file provides (a generated `*_pb2` or `_version` aside), or a package `__getattr__` that is not the lazy-submodule idiom (or that the package could subvert through `sys.modules`, `globals()`, `__name__` or a patched `importlib`) — keeps the tool named. The `__init__.py` of every package above the scope, and what each imports (every package on the way to it and each submodule named), are read too; there a reassignment counts unless it sets one attribute of another module file outside the scope, a module of the scope they import is read like the chain's own, a change to `__path__` is a caveat (`pkgutil.extend_path` aside), their files are read in batches, and what they import in turn, or import by name at run time, is not followed. `sys.modules` and `globals()` are read by allow-list: a store whose key names a module on the chain, a package above one or the framework's own modules is a named stop, a module rebinding its own name through them or its own module object (however spelled, including `__dict__` and `vars()` stores) is a reassignment, reads (a comparison, iteration, a spread, `pkgutil.iter_modules(__path__)`, `get_type_hints(globalns=globals())`) are nothing, and any other use (`mods = sys.modules`, `|=`, a computed key or `setattr`, the module object anywhere but an attribute, a plain alias or a reader, `__dict__.update`, a frame's `f_globals`, `builtins.globals` under another name) or a change to `__path__` is a caveat: its row is `not_established` with the reason, including through a `FunctionTool` wrapper, and for `scan` the ADK module stays at medium. Attest's lazy loaders (`importlib.import_module(f".{name}", __name__)`, `if name == "x": from . import x`) stay established; `from . import other as x` is not the idiom. When a definition another source reads under its own name is the one the reader resolved, the binding reaches it through its exact locator, never a same-named local definition. The module-binding walk and the SDK list reader are linear in the tree, and a chain of thousands of attributes no longer crashes the run.

- Read a Google ADK tool a local factory builds, and a tools list built in the agent's function. (#865)
- **The problem.** visulate/visulate-for-oracle#526 bound `save_memory_tool = create_save_memory_tool()` and `read_memory_tool = create_read_memory_tool()` in its root agent's builder; each factory returns `FunctionTool` around a nested async function, and the reader stopped at the local assignment, so the two new memory tools were unresolved like the agent's nine others. MuhammadVT/smart-assignment#46 built `tools = [...]` in the agent's function and appended a triage tool under `if triage_enabled:`; the reader read the whole list as a dynamic tools expression, losing its unconditional tools.
- **Factories.** A factory call — `tool = create_tool()` bound once and unconditionally in the agent's function or at a module's top level, or `tools=[create_tool()]` — is followed, as syntax and never run, to the factory's one unconditional `return`: `FunctionTool(inner)` / `LongRunningFunctionTool(inner)`, a plain function, a name bound once to one of those, or another factory's call, up to four factories deep. The tool is the function wrapped — one nested in the factory and defined before the `return`, or one the factory's module binds — with its own signature and location; `import_path` records the factory as a step, and the factory's code and the values each of the agent's calls gives its parameters (a literal, or a name or module attribute bound once to one; defaults filled in), which its closure holds, are part of its implementation digest, so `make_sql_tool(readonly=False)` in place of `readonly=True` is a changed tool and the same call spelled otherwise is not; a value this read cannot name is a limit on that tool naming the argument, and a `not_established` row only when the calling module changed.
- **What stays named, with why.** A factory that returns from more than one place or only under a condition, calls itself on the way, is decorated, a generator or `async`; a wrapped function that is decorated, a parameter, defined more than once in its file (a tool is known by its name there), or changed or handed on in the factory (`inner.__name__ = ...`, `setattr`, a helper), a module function included; a tool changed or handed on where it is bound (`tool.name = ...`, `rename(tool)`); and a factory the repository holds outside the read scope, named as such. Any other call — a third-party package, a class, a name bound twice or through a wildcard — keeps the answer it had. On visulate#526 with `--scope ai-agent`, the memory tools are read, and the remote-delegate factory, which renames the function it returns per call, leaves the nine delegates named with that reason — so the two memory tools are `not_established` candidate additions, not a complete eleven-tool surface; they were not rows before.
- **A tools list.** `tools = [a, b]` (or a tuple) bound once in the agent's function, with `tools.append(c)`, `.extend([...])`, `.insert(i, c)` or `tools += [...]` statements, is read member by member up to the statement that builds the agent, which copies the list, so an addition after it — or after an early `return` of another agent — is not that agent's. An addition before it under a condition or in a loop is named on the agent, which is then not complete, and is never read as bound. Any other use of the list — handed to a call, aliased, returned, changed another way, read by a nested function, a starred member — and a module-level list keep the dynamic tools expression. On smart-assignment#46 the unconditional tools are read and the triage tool is named as conditional.
### Changes

- Compare exact repository script bytes for supported, selected Claude Code and Codex hook executable references. A script-only edit produces an attributable hook row without asserting a permission expansion; its `why` names the direction as not established (#820), so the Claude Code Stop hook lists it apart from widenings rather than staying silent; `check` routes the dependency for review from either compared revision. The documented shell spellings resolve (`"$CLAUDE_PROJECT_DIR"/path`, `"${CLAUDE_PROJECT_DIR}"/path`, the variable and path quoted together, or unquoted with a plain path, and `CLAUDE_PLUGIN_ROOT` alike in a plugin's hooks). A script this entry cannot read withholds only its own bytes: an unchanged shared limit, or a script absent from both sides, is an `unchanged_limits` entry, and any other makes the comparison `partial` with every grant still compared. A script whose working-tree bytes differ only by a checkout's line-ending conversion is `unchanged_not_proven`, not a row. A selected hook whose script is not resolved (an interpreter wrapper, a relative path, a conditional expansion, a compound command or a malformed group) is named as a `script_not_resolved` coverage item while the change could touch it, never a row. A pre-#702 baseline is incomparable only for a hook that now binds a script. `check` and a provided diff (`check --diff`) leave out a selected script the change does not touch, so an untouched missing, ignored or linked script changes no `check` decision; a touched one is routed for review, and compared from the diff where the diff also touches its declaration. Verification binds ignored inputs and missing-path observations, and refuses current authority for unsafe reads. Mode-only changes and recursive dependencies are not compared. (#702)
Expand Down
40 changes: 40 additions & 0 deletions docs/application-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,46 @@ module's SHA-256. `import_path` is evidence, not compared meaning — moving an
import is not a change. An OpenAI Agents SDK definition must carry the SDK's
`@function_tool`.

A Google ADK tool built by a factory is the function the factory wraps. A
factory call — `tool = create_tool()` bound once and unconditionally in the
agent's function or at a module's top level, or `tools=[create_tool()]` — is
read, never run, to the factory's one unconditional `return`:
`FunctionTool(inner)` / `LongRunningFunctionTool(inner)`, a plain function, a
name bound once to one of those, or another factory's call, up to four
factories deep. The function is one nested in the factory and defined before
the `return`, or one the factory's module binds; `import_path` records the
factory as a step, and the factory's code and the values each of this
agent's calls gives its parameters — what the tool's closure holds — are
part of its implementation digest: a literal, or a name or module attribute
bound once to one (a list, dict or set — at any depth, or through another
name — only when nothing else in its scope uses it, never a module's), a
function by its own code (not the helpers it calls, as for any tool), or a
parameter of the factory the call is made in, with the factory's defaults for
the rest. So
`make_sql_tool(readonly=False)` in place of `readonly=True` is a changed
tool, while spelling the same call otherwise (an alias, a keyword, the
default written out), a docstring, or another agent's call is not. A value
this read cannot name — a builder's parameter, a computed value — is named
as a limit on that agent's tool, with the arguments and where they are
given; a row appears, `not_established`, only when the calling module or the
factory changed — a value set from another module leaves only the limit. A
module constant the factory's own body reads is not part of it, as for any
function. A factory that returns from more than one place or
under a condition, calls itself, is decorated, a generator or `async`, a
wrapped function that is decorated, a parameter, defined more than once in its
file (a tool is known by its name there), or changed or handed on in the
factory (`inner.__name__ = ...` renames the tool, for a module function,
`impl.search` or a function-local import too), a tool changed or handed on
where it is bound (reading `tool.name`, listing it in a tools list, or
wrapping it in `FunctionTool(func=...)` is not), and a factory the repository
holds outside the scope stay named with why; any other call — a third-party
package, a class, a name bound twice — is what it was. A tools list built in
the agent's function — `tools = [a, b]` with `tools.append(c)`,
`.extend([...])`, `.insert(i, c)` or `tools += [...]` — is read member by
member up to the statement that builds the agent, which copies it; an addition
under a condition or in a loop before it is named on the agent, never read as
bound, and any other use of the list keeps it a dynamic tools expression.

The boundary is narrow. Only regular `.py` files inside the selected scope are
read, parsed with `ast` and never imported or run. Symbolic links are not
followed, and a module name must match a file's exact spelling. An absolute
Expand Down
2 changes: 2 additions & 0 deletions docs/manifest-v0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ Supported static ADK signals:
- Python `Agent` / `LlmAgent` definitions with literal `tools=[...]`.
- Plain function tools referenced in an agent tools list.
- `FunctionTool(func=...)` and `LongRunningFunctionTool(func=...)` wrappers.
- A tool a local factory returns (`tool = create_tool()`), read to the factory's one unconditional `return FunctionTool(inner)`.
- A tools list built in the agent's function with `append` / `extend` / `insert` / `+=`; an addition under a condition is named, not bound.
- `OpenAPIToolset` when a local spec path can be resolved from a literal path or `Path("...").read_text()`.
- `McpToolset` metadata, including static `tool_filter` and explicit `inventory_path` / `tool_inventory_path` hints.
- Agent Config YAML `tools`, `sub_agents`, callbacks, plugins, and local config references.
Expand Down
Loading
Loading