Skip to content
Open
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.


## [v1.40.0] - 2026-03-30

This release improves AI assistant capabilities with better response tracking and Google integration, plus fixes a critical exit hang issue.

## What's New
- Adds Google Search, Google Maps, and code execution capabilities for Gemini models
- Surfaces finish_reason information on assistant messages and token usage events to track why the AI stopped generating responses

## Bug Fixes
- Fixes process hang when using `/exit` command due to bubbletea renderer deadlock

## Technical Changes
- Adds tests reproducing bubbletea renderer deadlock on exit
- Adds safety-net exit mechanism for bubbletea renderer deadlock prevention

### Pull Requests

- [#2254](https://github.com/docker/docker-agent/pull/2254) - Surface finish_reason on assistant messages and token usage events
- [#2265](https://github.com/docker/docker-agent/pull/2265) - docs: update CHANGELOG.md for v1.39.0
- [#2269](https://github.com/docker/docker-agent/pull/2269) - Fix process hang on /exit due to bubbletea renderer deadlock
- [#2276](https://github.com/docker/docker-agent/pull/2276) - Google grounding
- [#2277](https://github.com/docker/docker-agent/pull/2277) - Fix url


## [v1.39.0] - 2026-03-27

This release adds new color themes for the terminal interface and includes internal version management updates.
Expand Down Expand Up @@ -1658,3 +1682,5 @@ This release improves the terminal user interface with better error handling and
[v1.38.0]: https://github.com/docker/docker-agent/releases/tag/v1.38.0

[v1.39.0]: https://github.com/docker/docker-agent/releases/tag/v1.39.0

[v1.40.0]: https://github.com/docker/docker-agent/releases/tag/v1.40.0
2 changes: 1 addition & 1 deletion agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@
},
"provider_opts": {
"type": "object",
"description": "Provider-specific options. Sampling parameters: top_k (integer, supported by anthropic, google, amazon-bedrock, and custom OpenAI-compatible providers like vLLM/Ollama), repetition_penalty (float, forwarded to custom OpenAI-compatible providers), min_p (float, forwarded to custom providers), seed (integer, forwarded to OpenAI). Infrastructure options: dmr: runtime_flags. anthropic/amazon-bedrock (Claude): interleaved_thinking (boolean, default true). openai: transport ('sse' or 'websocket') to choose between SSE and WebSocket streaming for the Responses API. openai/anthropic/google: rerank_prompt (string) to fully override the system prompt used for RAG reranking (advanced - prefer using results.reranking.criteria for domain-specific guidance).",
"description": "Provider-specific options. Sampling parameters: top_k (integer, supported by anthropic, google, amazon-bedrock, and custom OpenAI-compatible providers like vLLM/Ollama), repetition_penalty (float, forwarded to custom OpenAI-compatible providers), min_p (float, forwarded to custom providers), seed (integer, forwarded to OpenAI). Infrastructure options: dmr: runtime_flags. anthropic/amazon-bedrock (Claude): interleaved_thinking (boolean, default true). openai: transport ('sse' or 'websocket') to choose between SSE and WebSocket streaming for the Responses API. openai/anthropic/google: rerank_prompt (string) to fully override the system prompt used for RAG reranking (advanced - prefer using results.reranking.criteria for domain-specific guidance). Google: google_search (boolean) enables Google Search grounding, google_maps (boolean) enables Google Maps grounding, code_execution (boolean) enables server-side code execution.",
"additionalProperties": true
},
"track_usage": {
Expand Down
4 changes: 2 additions & 2 deletions cmd/root/a2a.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ func newA2ACmd() *cobra.Command {
}

func (f *a2aFlags) runA2ACommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("serve", append([]string{"a2a"}, args...))

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "serve", append([]string{"a2a"}, args...))

out := cli.NewPrinter(cmd.OutOrStdout())
agentFilename := args[0]

Expand Down
4 changes: 2 additions & 2 deletions cmd/root/acp.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ func newACPCmd() *cobra.Command {
}

func (f *acpFlags) runACPCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("serve", append([]string{"acp"}, args...))

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "serve", append([]string{"acp"}, args...))

agentFilename := args[0]

// Expand tilde in session database path
Expand Down
6 changes: 3 additions & 3 deletions cmd/root/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func newAliasRemoveCmd() *cobra.Command {
}

func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags) error {
telemetry.TrackCommand("alias", append([]string{"add"}, args...))
telemetry.TrackCommand(cmd.Context(), "alias", append([]string{"add"}, args...))

out := cli.NewPrinter(cmd.OutOrStdout())
name := args[0]
Expand Down Expand Up @@ -179,7 +179,7 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)
}

func runAliasListCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("alias", append([]string{"list"}, args...))
telemetry.TrackCommand(cmd.Context(), "alias", append([]string{"list"}, args...))

out := cli.NewPrinter(cmd.OutOrStdout())

Expand Down Expand Up @@ -235,7 +235,7 @@ func runAliasListCommand(cmd *cobra.Command, args []string) error {
}

func runAliasRemoveCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("alias", append([]string{"remove"}, args...))
telemetry.TrackCommand(cmd.Context(), "alias", append([]string{"remove"}, args...))

out := cli.NewPrinter(cmd.OutOrStdout())
name := args[0]
Expand Down
3 changes: 1 addition & 2 deletions cmd/root/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ func newAPICmd() *cobra.Command {
}

func (f *apiFlags) runAPICommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("serve", append([]string{"api"}, args...))

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "serve", append([]string{"api"}, args...))

out := cli.NewPrinter(cmd.OutOrStdout())
agentsPath := args[0]
Expand Down
6 changes: 3 additions & 3 deletions cmd/root/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (f *debugFlags) loadTeam(ctx context.Context, agentFilename string, opts ..
}

func (f *debugFlags) runDebugConfigCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("debug", append([]string{"config"}, args...))
telemetry.TrackCommand(cmd.Context(), "debug", append([]string{"config"}, args...))

agentSource, err := config.Resolve(args[0], f.runConfig.EnvProvider())
if err != nil {
Expand All @@ -92,7 +92,7 @@ func (f *debugFlags) runDebugConfigCommand(cmd *cobra.Command, args []string) er
}

func (f *debugFlags) runDebugToolsetsCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("debug", append([]string{"toolsets"}, args...))
telemetry.TrackCommand(cmd.Context(), "debug", append([]string{"toolsets"}, args...))

ctx := cmd.Context()

Expand Down Expand Up @@ -132,7 +132,7 @@ func (f *debugFlags) runDebugToolsetsCommand(cmd *cobra.Command, args []string)
}

func (f *debugFlags) runDebugTitleCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("debug", append([]string{"title"}, args...))
telemetry.TrackCommand(cmd.Context(), "debug", append([]string{"title"}, args...))

ctx := cmd.Context()

Expand Down
4 changes: 2 additions & 2 deletions cmd/root/debug_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ func newDebugAuthCmd() *cobra.Command {
Short: "Print Docker Desktop authentication information",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
telemetry.TrackCommand("debug", []string{"auth"})

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "debug", []string{"auth"})

w := cmd.OutOrStdout()

token := desktop.GetToken(ctx)
Expand Down
2 changes: 1 addition & 1 deletion cmd/root/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func newEvalCmd() *cobra.Command {
}

func (f *evalFlags) runEvalCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("eval", args)
telemetry.TrackCommand(cmd.Context(), "eval", args)

ctx := cmd.Context()
agentFilename := args[0]
Expand Down
4 changes: 2 additions & 2 deletions cmd/root/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ func newMCPCmd() *cobra.Command {
}

func (f *mcpFlags) runMCPCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("serve", append([]string{"mcp"}, args...))

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "serve", append([]string{"mcp"}, args...))

agentFilename := args[0]

if !f.http {
Expand Down
3 changes: 1 addition & 2 deletions cmd/root/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@ Optionally provide a description as an argument to skip the initial prompt.`,
}

func (f *newFlags) runNewCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("new", args)

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "new", args)

t, err := creator.Agent(ctx, &f.runConfig, f.modelParam)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions cmd/root/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ func newPullCmd() *cobra.Command {
}

func (f *pullFlags) runPullCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("share", append([]string{"pull"}, args...))

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "share", append([]string{"pull"}, args...))

out := cli.NewPrinter(cmd.OutOrStdout())
registryRef := args[0]
slog.Debug("Starting pull", "registry_ref", registryRef)
Expand Down
4 changes: 2 additions & 2 deletions cmd/root/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ func newPushCmd() *cobra.Command {
}

func runPushCommand(cmd *cobra.Command, args []string) error {
telemetry.TrackCommand("share", append([]string{"push"}, args...))

ctx := cmd.Context()
telemetry.TrackCommand(ctx, "share", append([]string{"push"}, args...))

agentFilename := args[0]
tag := args[1]
out := cli.NewPrinter(cmd.OutOrStdout())
Expand Down
7 changes: 4 additions & 3 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,14 @@ func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) error {
return runInSandbox(cmd, &f.runConfig, f.sandboxTemplate)
}

ctx := cmd.Context()

if f.exec {
telemetry.TrackCommand("exec", args)
telemetry.TrackCommand(ctx, "exec", args)
} else {
telemetry.TrackCommand("run", args)
telemetry.TrackCommand(ctx, "run", args)
}

ctx := cmd.Context()
out := cli.NewPrinter(cmd.OutOrStdout())

useTUI := !f.exec && (f.forceTUI || isatty.IsTerminal(os.Stdout.Fd()))
Expand Down
2 changes: 1 addition & 1 deletion cmd/root/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func newVersionCmd() *cobra.Command {
}

func runVersionCommand(cmd *cobra.Command, args []string) {
telemetry.TrackCommand("version", args)
telemetry.TrackCommand(cmd.Context(), "version", args)

out := cli.NewPrinter(cmd.OutOrStdout())

Expand Down
22 changes: 22 additions & 0 deletions docs/providers/google/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,25 @@ models:
model: gemini-3-flash
thinking_budget: medium # default for Flash: minimal | low | medium | high
```

## Built-in Tools (Grounding)

Gemini models support built-in tools that let the model access Google Search and Google Maps
directly during generation. Enable them via `provider_opts`:

```yaml
models:
gemini-grounded:
provider: google
model: gemini-2.5-flash
provider_opts:
google_search: true
google_maps: true
code_execution: true
```

| Option | Description |
| ---------------- | ---------------------------------------------------- |
| `google_search` | Enables Google Search grounding for up-to-date info |
| `google_maps` | Enables Google Maps grounding for location queries |
| `code_execution` | Enables server-side code execution for computations |
16 changes: 16 additions & 0 deletions examples/google_search_grounding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env docker agent run

models:
gemini:
provider: google
model: gemini-3.1-flash-lite-preview
provider_opts:
google_search: true

agents:
root:
model: gemini
description: Gemini with Google Search
instruction: |
You are a helpful assistant with access to the latest information via Google Search.
Use grounded search results to provide accurate, up-to-date answers.
5 changes: 5 additions & 0 deletions pkg/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ type Message struct {
// Cost is the cost of this message in dollars (only set for assistant messages)
Cost float64 `json:"cost,omitempty"`

// FinishReason indicates why the model stopped generating for this message.
// "stop" = natural end, "tool_calls" = tool invocation, "length" = token limit.
// Only set for assistant messages.
FinishReason FinishReason `json:"finish_reason,omitempty"`

// CacheControl indicates whether this message is a cached message (only used by anthropic)
CacheControl bool `json:"cache_control,omitempty"`
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess
defer cancel()

// Ensure telemetry is initialized and add to context so runtime can access it
telemetry.EnsureGlobalTelemetryInitialized()
if telemetryClient := telemetry.GetGlobalTelemetryClient(); telemetryClient != nil {
telemetry.EnsureGlobalTelemetryInitialized(ctx)
if telemetryClient := telemetry.GetGlobalTelemetryClient(ctx); telemetryClient != nil {
ctx = telemetry.WithClient(ctx, telemetryClient)
}

Expand Down
6 changes: 4 additions & 2 deletions pkg/desktop/login.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package desktop

import "context"
import (
"context"
)

type DockerHubInfo struct {
Username string `json:"id"`
Expand All @@ -15,6 +17,6 @@ func GetToken(ctx context.Context) string {

func GetUserInfo(ctx context.Context) DockerHubInfo {
var info DockerHubInfo
_ = ClientBackend.Get(ctx, "/registry/username", &info)
_ = ClientBackend.Get(ctx, "/registry/info", &info)
return info
}
19 changes: 19 additions & 0 deletions pkg/desktop/uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package desktop

import (
"context"

"github.com/kofalt/go-memoize"
"github.com/patrickmn/go-cache"
)

var uuidMmemoizer = memoize.NewMemoizer(cache.NoExpiration, cache.NoExpiration)

func GetUUID(ctx context.Context) string {
uuid, _, _ := uuidMmemoizer.Memoize("desktopUUID", func() (any, error) {
var uuid string
_ = ClientBackend.Get(ctx, "/uuid", &uuid)
return uuid, nil
})
return uuid.(string)
}
32 changes: 31 additions & 1 deletion pkg/model/provider/gemini/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,28 @@ func (c *Client) applyGemini25ThinkingBudget(config *genai.GenerateContentConfig
slog.Debug("Gemini request using thinking_budget", "budget_tokens", tokens)
}

// builtInTools returns Gemini built-in tools (Google Search, Google Maps,
// Code Execution) enabled via provider_opts.
func (c *Client) builtInTools() []*genai.Tool {
entries := []struct {
key string
tool *genai.Tool
}{
{"google_search", &genai.Tool{GoogleSearch: &genai.GoogleSearch{}}},
{"google_maps", &genai.Tool{GoogleMaps: &genai.GoogleMaps{}}},
{"code_execution", &genai.Tool{CodeExecution: &genai.ToolCodeExecution{}}},
}

var builtIn []*genai.Tool
for _, e := range entries {
if enabled, ok := providerutil.GetProviderOptBool(c.ModelConfig.ProviderOpts, e.key); ok && enabled {
builtIn = append(builtIn, e.tool)
slog.Debug("Gemini built-in tool enabled", "key", e.key)
}
}
return builtIn
}

// convertToolsToGemini converts tools to Gemini format
func convertToolsToGemini(requestTools []tools.Tool) ([]*genai.Tool, error) {
if len(requestTools) == 0 {
Expand Down Expand Up @@ -533,6 +555,9 @@ func (c *Client) CreateChatCompletionStream(

config := c.buildConfig()

// Start with Google built-in tools (search, maps, code execution) from provider_opts
config.Tools = c.builtInTools()

// Add tools to config if provided
if len(requestTools) > 0 {
allTools, err := convertToolsToGemini(requestTools)
Expand All @@ -541,7 +566,7 @@ func (c *Client) CreateChatCompletionStream(
return nil, err
}

config.Tools = allTools
config.Tools = append(config.Tools, allTools...)

// Enable function calling
config.ToolConfig = &genai.ToolConfig{
Expand All @@ -550,6 +575,11 @@ func (c *Client) CreateChatCompletionStream(
},
}

// When mixing built-in tools with function calling, Gemini requires this flag
if len(config.Tools) > len(allTools) {
config.ToolConfig.IncludeServerSideToolInvocations = new(true)
}

// Debug: Log the tools we're sending
slog.Debug("Gemini tools config", "tools", config.Tools)
for _, tool := range config.Tools {
Expand Down
Loading
Loading