Skip to content

Add Kotlin snippet for filtering a toolset's tools - #2114

Open
happyhuman wants to merge 3 commits into
mainfrom
docs-kotlin-toolset-filter
Open

Add Kotlin snippet for filtering a toolset's tools#2114
happyhuman wants to merge 3 commits into
mainfrom
docs-kotlin-toolset-filter

Conversation

@happyhuman

Copy link
Copy Markdown
Collaborator

Summary

The Toolsets section of docs/tools-custom/index.md had no Kotlin content.
ToolFilter and ToolPredicate landed in adk-kotlin 0.7.0 and give Kotlin a
capability the other languages don't have: a filter that receives the
ReadonlyContext, so a toolset's tool list can depend on session state or the
current user.

Purely additive — 15 docs lines, one new transcluded .kt, one registration line.

Why it's a new subsection rather than a fourth tab

The obvious home was the existing Simple Math Toolset tab group. It isn't the
right one. That group is a single worked example explained by five bullets — an
agent, a greet_tool, name prefixing, a tool_context.state write, and
close(). A filtering snippet satisfies none of them, and Kotlin cannot
satisfy the prefix bullet at all: BaseTool.name is a val and adk-kotlin has no
prefix mechanism. A Kotlin tab there would have left readers with four bullets
that don't describe the code above them.

So it lives in ### Filtering which tools a toolset exposes, with prose that
matches the code.

What the snippet adds over the other tabs

The Python tab gestures at context-aware selection but never implements it:

# Example of dynamic behavior:
# Could use readonly_context.state to decide which tools to return
# For instance, if readonly_context.state.get("enable_advanced_math"):

The Kotlin snippet implements exactly that, showing all three states: a null
filter selects everything, ToolFilter.allowList selects by name, and
ToolFilter.Predicate decides per invocation from the ReadonlyContext.

Notes for reviewers

  • The subsection is Kotlin-only, inside a Python/TypeScript section. That's
    deliberate — Python and TypeScript have no ToolFilter equivalent, the concept
    is Kotlin's. Single-language section badges are precedented
    (docs/agents/llm-agents.md:426-428 carries a Python-only one). It will still
    read as asymmetric; say the word if you'd rather it were framed differently.
  • This is a source-breaking change upstream, which the snippet quietly
    documents: McpToolsetConfig.toolFilter went List<String>? (v0.6.0) →
    ToolFilter? (v0.7.0).
  • Transcluded, so unlike an inline snippet CI compiles and lints it on every
    future change.
  • The section badge on ## Toolsets is untouched. It carries a stale
    title="…available for Python and TypeScript" despite a Java tab existing, but
    that predates this PR.

Verification

  • ./gradlew compileKotlin clean; ktlint clean; verify_snippets.py L0–L6 pass;
    check_kotlin_snippets.sh passes.
  • Ran it, not just compiled it — KSP generates both tools, filtering returns the
    expected sets, and the tool bodies execute:
unfiltered    : [addNumbers, subtractNumbers]
allowList     : [addNumbers]
predicate off : [addNumbers]
predicate on  : [addNumbers, subtractNumbers]
INVOKED addNumbers(a=7, b=3)      -> {result=10}
INVOKED subtractNumbers(a=7, b=3) -> {result=4}

Not exercised: no LLM call — the toolset was never attached to a live agent.

The Toolsets section had no Kotlin. ToolFilter and ToolPredicate arrived in
adk-kotlin 0.7.0 and give Kotlin something the other languages do not have: a
filter that receives the ReadonlyContext, so a toolset's tool list can depend on
session state or the current user.

The Python tab in the Simple Math Toolset example only gestures at this, in a
commented-out branch inside get_tools(). This snippet implements it, showing all
three states: no filter selects everything, allowList selects by name, and a
Predicate decides per invocation.

Placed in its own subsection rather than as a fourth tab on the Simple Math
Toolset example. That group is one worked example explained by five bullets --
an agent, a greet tool, name prefixing, a tool_context.state write and close() --
and a filtering snippet satisfies none of them. Kotlin cannot satisfy the prefix
bullet at all, since BaseTool.name is a val and adk-kotlin has no prefix
mechanism. A tab there would have left readers with four bullets that do not
describe the code above them.

Transcluded, so CI compiles and lints it. Verified beyond compiling: the tools
are generated by KSP, filtering returns the expected sets for all three cases,
and the tool bodies run -- addNumbers(7,3) -> {result=10}.
@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for adk-docs-preview ready!

Name Link
🔨 Latest commit 711d14b
🔍 Latest deploy log https://app.netlify.com/projects/adk-docs-preview/deploys/6a7f78413139780008d12e22
😎 Deploy Preview https://deploy-preview-2114--adk-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@happyhuman happyhuman self-assigned this Aug 11, 2026

@wikaaaaa wikaaaaa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I checked the snippet against adk-kotlin v0.7.0 and the code is correct.

Just a comment to the PRs justification -- "gives Kotlin a capability the other languages don't have" isn't accurate. Context-aware tool filtering exists in other languages, ex. Python

Out of scope for this PR, but none of the other snippets in this file include Kotlin — should we add them?

@happyhuman

Copy link
Copy Markdown
Collaborator Author

Thanks @wikaaaaa — both points addressed, and you were right on the first one.

On "a capability the other languages don't have"

Wrong, and I've corrected it. Python's BaseToolset takes tool_filter: Optional[Union[ToolPredicate, List[str]]], and ToolPredicate.__call__ receives readonly_context — that's the same context-aware filtering and the allow-list-by-name form I claimed was Kotlin's alone.

More importantly, that claim had leaked into the page, not just the description. A single-language badge asserts exclusivity in this repo — the Python-only precedent I cited in the description (docs/agents/llm-agents.md:426) sits on set_default_model, which genuinely is Python-only. So Supported in ADK | Kotlin v0.7.0 under the conceptual heading "Filtering which tools a toolset exposes" was telling readers Python can't do this.

Fixed by scoping the heading to the API rather than the concept: "Filtering which tools a toolset exposes with ToolFilter". ToolFilter really is Kotlin v0.7.0, so the badge is now true, and the title attribute notes other languages have their own mechanisms.

On the other snippets — done, all of them

The page had six language tab groups and not one had Kotlin. All six do now:

Section Snippet
Example (tools in instructions) WeatherSentiment.kt
State Management UserPreferenceTools.kt
Controlling Agent Flow CustomerSupport.kt
Artifacts / Memory DocAnalysisTools.kt
Defining Effective Tool Functions OrderTools.kt
Simple Math Toolset ToolsetExample.kt:init

All transcluded, so CI compiles and lints them. Also fixed the ## Toolsets section badge, which claimed "Python and TypeScript" despite the Java tab that was already there.

These are execution-verified, not just compiled. I wired a temporary harness with a real ToolContext and ran every tool: filtering, transferToAgent, artifact load/save, and state deltas all behave as documented. Two things that caught real bugs:

  • The toolset example originally had no greet_tool, so bullets 2 and 3 of "In this example" described code that wasn't in the Kotlin tab. Added greetUser.
  • The state snippet wrote a nested map to stateDelta while reading committed state. Unlike Python's layered tool_context.state, Kotlin's view doesn't include deltas written earlier in the same invocation, so a second call in one turn silently dropped the first. Now one flat key per preference — the same shape the Java tab uses.

What I deliberately left out

  • search_memory in the artifacts/memory snippet. Not available on Kotlin's ToolContext; it lives on MemoryService. The Kotlin tab covers artifacts only, which is exactly what the Java tab already does, and the bullet above it already scopes search_memory to "Python, Go and TypeScript". The KDoc points at LoadMemoryTool/PreloadMemoryTool as the Kotlin route.
  • Tool-name prefixing in the toolset example. adk-kotlin 0.7.0 has no prefix mechanism and BaseTool.name is a val. Rather than leave a bullet describing absent code, I added one explaining the deviation.
  • Live-model execution for the two main() entry points — they need credentials, same as the Python originals. Every tool body is execution-verified; those two entry points are compile-verified only.

Heads up that this outgrew the PR title, which I'll update along with the description. Happy to split the five backfilled snippets into their own PR if you'd rather review them separately from the ToolFilter change you already approved.

@happyhuman
happyhuman requested a review from wikaaaaa August 14, 2026 20:17
…ilter

# Conflicts:
#	tools/kotlin-snippets/files_to_test.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants