Files
Shahin Saadati 674851324e Add the Kotlin tab for answering a long-running tool call (#2149)
* Show how a Kotlin client answers a long-running tool call

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.

* Correct the long-running snippet's account of resume and turn count

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.

* Update function-tools.md

* Say that Kotlin resolves the invocation from the response itself

Adding a Kotlin tab to this section quietly extended the Resume note to
Kotlin, where it does not hold: resolveInvocationId matches the function
response's own call ID against the session and ignores the invocationId
the caller passes, so requiring one sends readers looking for a
parameter that changes nothing. An ID matching no call throws rather
than starting a fresh invocation.

Also fix subject-verb agreement in the turn-count bullet.

---------

Co-authored-by: Joe Fernandez <931947+joefernandez@users.noreply.github.com>
2026-08-31 14:32:08 -07:00
..