tool: the interfaces that make a Tool callable are internal-only
Problem
The exported tool.Tool interface has three methods — Name, Description, IsLongRunning (tool/tool.go#L37-L46). None of them makes a tool callable. The interfaces the run loop actually dispatches on all live in internal/toolinternal (internal/toolinternal/tool.go#L28-L62):
FunctionTool—Declaration()andRun()StreamingFunctionTool—Declaration()andRunStream()RequestProcessor—ProcessRequest()ResponseDeferrer—DefersResponse()SkipSummarizationResultDisplayer—DisplayResultOnSkipSummarization()
internal/llminternal/base_flow.go type-asserts against them on the call path (L780, L799, L1213, L1268, L1280, L1311). Because the package sits under internal/, no code outside the module can name those types:
$ cat probe.go
package probe
import (
_ "google.golang.org/adk/v2/internal/toolinternal"
)
$ go build ./...
package example.com/probe
probe.go:4:2: use of internal package google.golang.org/adk/v2/internal/toolinternal not allowedInterface satisfaction in Go is structural, so an external type with the right methods is still callable. The cost lands in two places instead.
Implementing a tool. The required method set is not discoverable from the public API and cannot be asserted. godoc does not publish internal/, and var _ toolinternal.FunctionTool = (*myTool)(nil) is unavailable, so the usual compile-time check does not exist. A method set that is close but wrong — a Run returning map[string]string, a missing Declaration — compiles cleanly, and the failed assertion at L1268 surfaces as newToolNotFoundError, which blames a registration problem that isn't there. AGENTS.md offers "implement the tool.Tool interface for full control" as the escape hatch from functiontool.New (AGENTS.md#L185), and that interface on its own is not enough to be called.
Wrapping a tool. A decorator around an arbitrary tool.Tool — retry, caching, rate limiting, timeouts, auditing, metrics — has to re-declare every capability interface locally as an anonymous interface literal, type-assert the inner tool through it, and forward each method by hand. Embedding tool.Tool in the wrapper struct does not help, because the embedded value carries only the three exported methods and the run loop asserts against the wrapper. ProcessRequest is the hardest to forward: a wrapper that wants to stay in the call path has to reproduce the framework's own wiring, registering itself rather than the inner tool in req.Tools and appending the declaration to req.Config.Tools.
The set also grows over time. SkipSummarizationResultDisplayer was added on 2026-09-03 in #908. Any wrapper written before that date still compiles, still passes its tests, and silently stops forwarding the new capability. Nothing reports it.
Impact
Wrapping is the natural composition point for tools in Go, and it is currently the least supported one. Cross-cutting behavior on the tool call path gets rebuilt per project by reading internal/, and each copy drifts silently as the interface set changes.
Acceptance criteria
- A package outside the module can implement a callable tool and assert the full contract at compile time.
- A package outside the module can wrap an arbitrary
tool.Tooland have every capability the inner tool supports keep working, without enumerating the capability interfaces by hand. - Adding a capability interface later does not silently disable it for wrappers written earlier.
Environment
- ADK version:
mainat f13a608 - Go: 1.26.5
Source: google/adk-go