Add the Kotlin tab for answering a long-running tool call - #2149
Open
happyhuman wants to merge 2 commits into
Open
Add the Kotlin tab for answering a long-running tool call#2149happyhuman wants to merge 2 commits into
happyhuman wants to merge 2 commits into
Conversation
The function-tools page documents long-running tools in two halves: defining one (which Kotlin already covered) and driving it from the client, which Kotlin did not. Nothing in the docs showed a Kotlin reader how the deferred result gets back to the model - the only Kotlin mention of longRunningToolIds in the repo is a commented-out field listing in events/index.md. The new region continues the reimbursement scenario the Kotlin tab above it already sets up, rather than importing the nav-agent scenario the upstream demos use. It shows the two things that are easy to get wrong: - A pending call is one whose id the event also lists in `longRunningToolIds`; the FunctionResponse must reuse that id or the model cannot match the answer to the request it is waiting on. - A resumable app must pass `invocationId` to the second `runAsync`. Without it the response opens a new invocation instead of resuming the paused one, which the page's own resume note warns about for Python. Grounded in ResumableLongRunningToolDemoAgent.kt:84-99 at the v0.8.0 tag. Appended to the existing, already-registered LongRunningTool.kt instead of the new file the backlog row proposed: this page already owns that snippet, and a second file elsewhere would split one page's Kotlin across two directories. Also added a bullet to "Key aspects of this example", which explains the group purely in terms of `LongRunningFunctionTool` - a class Kotlin does not have. The Kotlin form is `@Tool(isLongRunning = true)` or a `BaseTool` subclass, and a long-running tool returning `Unit` suppresses even the placeholder response (InvocationContext.kt:447). Verified: runner.sh build and lint both PASS on the snippet (JDK 17), check_kotlin_snippets.sh passes, L0/L5/L6 pass. L3 reports two orphaned-tab problems at lines 123 and 227; both pre-date this change and are false positives - rendering the page with the repo's own markdown extensions shows every group, including the one edited here, as a single tabbed set with Kotlin among its labels.
Review against the v0.8.0 sources found three claims in this branch that a reader would have acted on and been wrong. The invocationId argument was the worst of them. The snippet took an `appIsResumable` flag and passed `invocationId` on the second `runAsync`, commenting that a resumable app must do so or the response opens a new invocation. The runner does not work that way: `resolveInvocationId` (AbstractRunner.kt:468-483) looks the id up from the function-call event that matches the response's own id and discards whatever the caller passed. The flag was inert, and anyone plumbing it through their call sites would have got nothing for it. Both are gone; the comment now says what actually resumes the invocation - the response id itself. "Returns a placeholder and the turn ends" was wrong for the snippet's own default. This tool returns a data class, not `Unit`, so a non-resumable app emits the placeholder as a function response and calls the model again: LongRunningToolIntegrationTest's scenario table records two model calls and a trailing text event for that combination, and asserts it in runAsync_longRunningToolReturnsDict_propagatesPayloadAndAcknowledges. A reader building a HITL flow would have budgeted one model call and been surprised by an interim reply. The KDoc and the page bullet now describe both modes. Reusing the call id was described as something the model needs to match the answer to its request. The model never gets that far: an unknown id throws from HistoryRewriterProcessor.findMatchingFunctionCallEvent, and a null one throws too, because the id set is built with mapNotNull and an empty set matches no event. The comment now says it throws. Also prints turn 1, which is where the interim reply appears, and says so when the model answers without calling the tool instead of returning silently. Verified: runner.sh build and lint both PASS (JDK 17), L0/L1/L2/L5/L6 pass, and rendering the page with the repo's markdown extensions puts Kotlin in the target group's tab set. L3's two orphaned-tab reports are pre-existing on main and are false positives - the render shows those groups whole.
✅ Deploy Preview for adk-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Intermediate / final result updates section of
tools-custom/function-toolshad tabs for Python, TypeScript, Go and Java, but none for Kotlin. This adds one.
Long-running tools are documented in two halves on this page: defining the tool,
which Kotlin already covered, and answering it from the client, which it did not.
Nothing anywhere in the docs showed a Kotlin reader how a deferred result gets
back to the model — the only Kotlin mention of
longRunningToolIdsin the repois a commented-out field listing in
events/index.md.What the snippet shows
It continues the reimbursement scenario the Kotlin tab further up the page
already sets up, rather than importing the nav-agent scenario the SDK's own demos
use, so the two Kotlin tabs read as one story. Three things it makes explicit,
each of which is easy to get wrong:
longRunningToolIds.call by id, and an unknown or missing one throws —
HistoryRewriterProcessor.findMatchingFunctionCallEvent, and for a null id too,since the id set is built with
mapNotNulland an empty set matches no event.Not a soft mismatch the model shrugs off.
Unit,a non-resumable app emits the placeholder as a function response and calls the
model a second time, so the user sees an interim reply before the decision
exists. A resumable app pauses on the function call with no second model call.
Anyone building a HITL flow needs to know this before they budget model calls.
Two things the backlog row got wrong
The KT-21 row proposed a new file and told the author to subclass
BaseTool.Both would have hurt:
snippets/tools/function-tools/LongRunningTool.kt, transcluded 90 linesearlier. A second file in another directory splits one page's Kotlin in two, so
this appends a region instead.
@Tool(isLongRunning = true), not aBaseToolsubclass. The page'sexisting Kotlin tab uses the annotation. Following the row would have left two
Kotlin tabs on one page prescribing different APIs.
The row also missed that
Runner.runAsynctakes aninvocationIdat all — seebelow, where it turns out not to be needed.
Also fixed on the page
"Key aspects of this example" explains the group purely through
LongRunningFunctionTool, a class Kotlin does not have (git grepatv0.8.0finds it only in a test comment). Two bullets now give the Kotlin form and the
turn-count behaviour.
Review notes
A sceptical pass over the first commit caught three claims that were wrong, all
corrected in
1c48e14f:invocationIdon the resume call behind anappIsResumableflag, claiming a resumable app must. It must not:
resolveInvocationId(
AbstractRunner.kt:468-483) derives the invocation from the response's own idand discards the caller's value. The flag was inert. Both removed.
default — see the turn-count point above. Corrected from
LongRunningToolIntegrationTest's scenario table (off | {status: pending} | 2 calls | [FC, FR, text]).Verification
./tools/kotlin-snippets/runner.sh build …/LongRunningTool.kt→ PASS(JDK 17; the same command CI runs).
lint→ PASS.verify_snippets.py→ L0 symbols, L1 compile, L2 ktlint, L5 registration,L6 badge all PASS. L4 skips — this project registers no
runSnippetstask.mkdocs.yml: the target groupcomes back as
['Python', 'TypeScript', 'Go', 'Java', 'Kotlin'].Every API claim is grounded in the
v0.8.0tag ofgoogle/adk-kotlin, theversion
examples/kotlin/build.gradle.ktspins — never the working tree.One pre-existing checker complaint, not from this PR
check_transclusions.pyflags orphaned Kotlin tabs at lines 123 and 227. Bothreproduce on
mainwith this branch stashed, and both are false positives: thechecker mis-flags a Kotlin tab whose preceding sibling tab ends in prose rather
than a code fence, and rendering shows those groups intact as four-tab sets. The
ladder's L3 gate is file-level, so while this page had uncommitted edits it
attributed them to this change; against the committed branch L3 passes and
reports them as pre-existing elsewhere. The checker heuristic wants fixing, in
its own change.