Skip to content

Add the Kotlin tab for answering a long-running tool call - #2149

Open
happyhuman wants to merge 2 commits into
mainfrom
docs-kotlin-long-running-response
Open

Add the Kotlin tab for answering a long-running tool call#2149
happyhuman wants to merge 2 commits into
mainfrom
docs-kotlin-long-running-response

Conversation

@happyhuman

@happyhuman happyhuman commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Intermediate / final result updates section of tools-custom/function-tools
had 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 longRunningToolIds in the repo
is 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:

  • Finding the pending call. It is the one whose id the event also lists in
    longRunningToolIds.
  • The id is load-bearing. The framework matches the response to the original
    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 mapNotNull and an empty set matches no event.
    Not a soft mismatch the model shrugs off.
  • Turn 1 differs by app. Because this tool returns a value rather than 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:

  • No new file. This page already owns a registered
    snippets/tools/function-tools/LongRunningTool.kt, transcluded 90 lines
    earlier. A second file in another directory splits one page's Kotlin in two, so
    this appends a region instead.
  • @Tool(isLongRunning = true), not a BaseTool subclass. The page's
    existing 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.runAsync takes an invocationId at all — see
below, 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 grep at v0.8.0
finds 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:

  • The snippet passed invocationId on the resume call behind an appIsResumable
    flag, claiming a resumable app must. It must not: resolveInvocationId
    (AbstractRunner.kt:468-483) derives the invocation from the response's own id
    and discards the caller's value. The flag was inert. Both removed.
  • "Returns a placeholder and the turn ends" was wrong for the snippet's own
    default — see the turn-count point above. Corrected from
    LongRunningToolIntegrationTest's scenario table (off | {status: pending} | 2 calls | [FC, FR, text]).
  • A wrong id was described as the model failing to match; it throws.

Verification

  • ./tools/kotlin-snippets/runner.sh build …/LongRunningTool.ktPASS
    (JDK 17; the same command CI runs). lintPASS.
  • verify_snippets.pyL0 symbols, L1 compile, L2 ktlint, L5 registration,
    L6 badge all PASS.
    L4 skips — this project registers no runSnippets task.
  • Rendered the page with the extension list from mkdocs.yml: the target group
    comes back as ['Python', 'TypeScript', 'Go', 'Java', 'Kotlin'].

Every API claim is grounded in the v0.8.0 tag of google/adk-kotlin, the
version examples/kotlin/build.gradle.kts pins — never the working tree.

One pre-existing checker complaint, not from this PR

check_transclusions.py flags orphaned Kotlin tabs at lines 123 and 227. Both
reproduce on main with this branch stashed, and both are false positives: the
checker 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.

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.
@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for adk-docs-preview ready!

Name Link
🔨 Latest commit 1c48e14
🔍 Latest deploy log https://app.netlify.com/projects/adk-docs-preview/deploys/6a860f496ec9b20008df3a77
😎 Deploy Preview https://deploy-preview-2149--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.

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.

1 participant