#1594·adk-go

bug: Context.WithAgentTimeout returns (nil, nil) on tool and callback contexts

Author: wolo-labCreated Sep 14, 2026Updated Sep 18, 2026
Labelsbug

Problem

WithAgentTimeout is part of the exported agent.Context interface, documented as creating "a new context as a shallow copy, adding timeout to the top of the underlying context.Context" (agent/context.go#L227-L228). Two of the Context implementations do not do that. They write a line to the standard logger and return (nil, nil):

Both returned values are unusable. The Context is nil, so any method on it panics, and the idiomatic defer cancel() panics on a nil func. There is no error return, nothing in the interface documentation marks the method as optional on some implementations, and the explanation goes to the standard logger instead of to the caller. A caller that wants to bound the time a tool spends has no way to learn at compile time that the call will not work.

For comparison, commonContext implements the documented behavior (agent/common_context.go#L258-L265), and WithAgentCancel on the same tool wrapper delegates to the underlying context rather than returning nil (agent/tool_context_wrapper.go#L45-L49), so a tool context already carries cancellation.

Steps to reproduce

Save the following in a package that imports the module and run go test ./... -v.

go
package repro

import (
	"testing"
	"time"

	"google.golang.org/adk/v2/agent"
	"google.golang.org/adk/v2/session"
)

func TestWithAgentTimeoutOnToolContext(t *testing.T) {
	toolCtx := agent.NewToolContext(&agent.ContextMock{}, "call-1", nil, nil)

	ctx, cancel := toolCtx.WithAgentTimeout(5 * time.Second)
	if ctx == nil || cancel == nil {
		t.Fatalf("WithAgentTimeout() = (%v, %v), want a usable Context and a non-nil CancelFunc", ctx, cancel)
	}
	defer cancel()
}

func TestWithAgentTimeoutOnCallbackContext(t *testing.T) {
	cbCtx := agent.NewCallbackContext(&agent.ContextMock{}, &session.EventActions{})

	ctx, cancel := cbCtx.WithAgentTimeout(5 * time.Second)
	if ctx == nil || cancel == nil {
		t.Fatalf("WithAgentTimeout() = (%v, %v), want a usable Context and a non-nil CancelFunc", ctx, cancel)
	}
	defer cancel()
}

Observed behavior

=== RUN   TestWithAgentTimeoutOnToolContext
2026/09/14 11:41:08 WithAgentTimeout() is not supported for tool context
    repro_test.go:16: WithAgentTimeout() = (<nil>, <nil>), want a usable Context and a non-nil CancelFunc
--- FAIL: TestWithAgentTimeoutOnToolContext (0.00s)
=== RUN   TestWithAgentTimeoutOnCallbackContext
2026/09/14 11:41:08 WithAgentTimeout() is not supported for callback context
    repro_test.go:26: WithAgentTimeout() = (<nil>, <nil>), want a usable Context and a non-nil CancelFunc
--- FAIL: TestWithAgentTimeoutOnCallbackContext (0.00s)
FAIL

Dropping the nil guard and keeping only defer cancel() turns both cases into panic: runtime error: invalid memory address or nil pointer dereference.

Expected behavior

WithAgentTimeout either honors its documented contract on every Context it is exported on, or the restriction is expressed so a caller can act on it, through the type system or a returned error. Returning two nils that are indistinguishable from success at the call site is the defect, separately from the question of whether a tool context should be allowed to carry its own timeout.

InvocationContext() on the same two wrappers has the same log-and-return-nil shape (tool, callback), so whatever is decided here probably applies there as well.

Environment

  • ADK version: main at f13a608
  • Go: 1.26.5
  • OS: Linux
  • Model: not applicable, the repro makes no model call