Skip to content

fix(tool): apply WithConfirmation to streaming tools - #1409

Open
venkat-uk wants to merge 2 commits into
google:mainfrom
venkat-uk:fix/confirmation-streaming-tools
Open

fix(tool): apply WithConfirmation to streaming tools#1409
venkat-uk wants to merge 2 commits into
google:mainfrom
venkat-uk:fix/confirmation-streaming-tools

Conversation

@venkat-uk

Copy link
Copy Markdown

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:

tool.WithConfirmation wrapped a tool only when it satisfied the unexported runnableTool interface, which is Declaration plus Run. The flow can execute two shapes, not one: toolinternal.FunctionTool and toolinternal.StreamingFunctionTool, and internal/llminternal/base_flow.go dispatches on the streaming shape first (line 1156) and the non-streaming one second (line 1211).

A streaming tool has Declaration and RunStream but no Run, so it fell through the else branch and was appended unchanged. A caller who wrapped a toolset to gate every tool in it behind human-in-the-loop confirmation got no gate at all on the streaming tools, and no signal that this had happened. The only remaining gate was the tool's own Config.RequireConfirmation, which is a different control set by whoever wrote the tool rather than by whoever wrapped the toolset.

Solution:

Add a streaming counterpart to the wrapper and check the streaming shape first, so the wrapper matches the order in which the flow will dispatch.

Two details are deliberate:

  • confirmationStreamingTool embeds the streamingRunnableTool interface rather than the concrete tool. Go promotes the methods of the embedded static type, so a tool that also has a Run method cannot reach the flow through an unguarded promoted method. The existing confirmationTool embeds runnableTool for the same reason, so this keeps the two symmetric.
  • Tools that ADK does not execute itself still pass through unwrapped. The model-side built-ins in tool/geminitool are run by the model, so there is nothing to confirm. The default branch now says that, instead of describing the skipped tools as ones lacking a specific internal interface.

The confirmation decision moves into one confirmCall helper used by both paths, so the streaming and non-streaming semantics cannot drift apart later. Behaviour for non-streaming tools is unchanged.

The godoc no longer names runnableTool, which was not a useful thing to tell a caller: it described the skipped tools as ones that do not "provide a FunctionDeclaration and a Run method", and a streaming tool does provide a FunctionDeclaration, so the sentence pointed a reader away from the gap.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

TestWithConfirmation_StreamingTool is a table test that mirrors the existing TestWithConfirmation cases against a streaming tool built with RequireConfirmation: false, so only the toolset-level setting is in play: confirmation required with nothing on the context, confirmed, rejected, not required, provider true, and requireConfirmation=true with a provider returning false.

TestWithConfirmation_ToolWithBothRunAndRunStream covers the second path. It uses a tool implementing both shapes and asserts the wrapper presents the streaming shape, does not also satisfy FunctionTool, and that neither method ran without confirmation.

Both fail on main:

$ git stash -- tool/tool.go
$ go test ./tool/ -run 'TestWithConfirmation_StreamingTool|TestWithConfirmation_ToolWithBoth' -count=1
--- FAIL: TestWithConfirmation_StreamingTool (0.00s)
    --- FAIL: TestWithConfirmation_StreamingTool/confirmation_required,_no_confirmation_in_context (0.00s)
    --- FAIL: TestWithConfirmation_StreamingTool/provider_requires_confirmation (0.00s)
--- FAIL: TestWithConfirmation_ToolWithBothRunAndRunStream (0.00s)
FAIL

and pass with the change, alongside the existing suite:

$ go test ./tool/... -count=1 -race
ok  	google.golang.org/adk/v2/tool
ok  	google.golang.org/adk/v2/tool/agenttool
ok  	google.golang.org/adk/v2/tool/functiontool
ok  	google.golang.org/adk/v2/tool/mcptoolset
ok  	google.golang.org/adk/v2/tool/skilltoolset
ok  	google.golang.org/adk/v2/tool/toolconfirmation
ok  	google.golang.org/adk/v2/tool/toolutils
... all green

go vet ./tool/ is clean.

Manual End-to-End (E2E) Tests:

The behaviour is observable without a model, which is why the reproduction is a test rather than a runner transcript. Before the change, cts.Tools(nil) returns the same pointer that went in (tools[0] == streamTool is true), the handler runs, and RequestConfirmation is never called. After it, the same call returns ErrConfirmationRequired, sets SkipSummarization, and the handler does not run until a confirmation arrives on the context.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

WithConfirmation is marked EXPERIMENTAL and out of scope for the v1.0 API, so this is a fix within an experimental surface rather than a public API change. The exported signature is unchanged.

Adjacent but deliberately not in this PR: #1153 item 2 tracks test coverage for the tool's own RunStream confirmation gate, which is a different control and already works. Happy to split anything here if you would rather see the wrapper and the helper extraction land separately.

venkat-uk and others added 2 commits August 26, 2026 23:04
confirmationToolset.Tools wrapped a tool only when it satisfied the
unexported runnableTool interface, which is Declaration plus Run. The
flow can execute two shapes: toolinternal.FunctionTool and
toolinternal.StreamingFunctionTool, and it dispatches on the streaming
one first. A streaming tool has Declaration and RunStream but no Run, so
it fell through the else branch and was returned unwrapped. The
toolset-level confirmation setting never reached it, and the only gate
left was the tool's own Config.RequireConfirmation.

Add a streaming counterpart to the wrapper and check the streaming shape
first, matching the flow's dispatch order. The wrapper embeds the
streamingRunnableTool interface rather than the concrete tool, so a tool
that also has a Run method cannot reach the flow through an unguarded
promoted method.

Tools that ADK does not execute itself, such as the model-side built-ins
in tool/geminitool, still pass through unwrapped: there is nothing to
confirm. The godoc now says which shapes are covered instead of naming
one internal interface.

The confirmation decision itself moves into confirmCall so the streaming
and non-streaming paths cannot drift.

Fixes google#1408
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.

tool.WithConfirmation silently skips streaming tools, so a gated toolset runs them unconfirmed

1 participant