Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 81 additions & 18 deletions cmd/root/debug.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package root

import (
"context"
"fmt"
"log/slog"

"github.com/goccy/go-yaml"
"github.com/spf13/cobra"

"github.com/docker/cagent/pkg/cli"
"github.com/docker/cagent/pkg/config"
"github.com/docker/cagent/pkg/sessiontitle"
"github.com/docker/cagent/pkg/team"
"github.com/docker/cagent/pkg/teamloader"
"github.com/docker/cagent/pkg/telemetry"
)

type debugFlags struct {
runConfig config.RuntimeConfig
modelOverrides []string
runConfig config.RuntimeConfig
}

func newDebugCmd() *cobra.Command {
Expand All @@ -37,24 +42,56 @@ func newDebugCmd() *cobra.Command {
Args: cobra.ExactArgs(1),
RunE: flags.runDebugToolsetsCommand,
})
titleCmd := &cobra.Command{
Use: "title <agent-file>|<registry-ref> <question>",
Short: "Generate a session title from a question",
Args: cobra.ExactArgs(2),
RunE: flags.runDebugTitleCommand,
}
titleCmd.Flags().StringArrayVar(&flags.modelOverrides, "model", nil, "Override agent model: [agent=]provider/model (repeatable)")
cmd.AddCommand(titleCmd)

addRuntimeConfigFlags(cmd, &flags.runConfig)

return cmd
}

// resolveSource resolves an agent file reference to a config source.
func (f *debugFlags) resolveSource(agentFilename string) (config.Source, error) {
return config.Resolve(agentFilename, f.runConfig.EnvProvider())
}

// loadTeam loads an agent team from the given agent file and returns
// a cleanup function that must be deferred by the caller.
func (f *debugFlags) loadTeam(ctx context.Context, agentFilename string, opts ...teamloader.Opt) (*team.Team, func(), error) {
agentSource, err := f.resolveSource(agentFilename)
if err != nil {
return nil, nil, err
}

t, err := teamloader.Load(ctx, agentSource, &f.runConfig, opts...)
if err != nil {
return nil, nil, err
}

cleanup := func() {
if err := t.StopToolSets(ctx); err != nil {
slog.Error("Failed to stop tool sets", "error", err)
}
}

return t, cleanup, nil
}

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

ctx := cmd.Context()
agentFilename := args[0]

agentSource, err := config.Resolve(agentFilename, f.runConfig.EnvProvider())
agentSource, err := f.resolveSource(args[0])
if err != nil {
return err
}

cfg, err := config.Load(ctx, agentSource)
cfg, err := config.Load(cmd.Context(), agentSource)
if err != nil {
return err
}
Expand All @@ -66,21 +103,17 @@ func (f *debugFlags) runDebugToolsetsCommand(cmd *cobra.Command, args []string)
telemetry.TrackCommand("debug", append([]string{"toolsets"}, args...))

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

agentSource, err := config.Resolve(agentFilename, f.runConfig.EnvProvider())
t, cleanup, err := f.loadTeam(ctx, args[0])
if err != nil {
return err
}
defer cleanup()

team, err := teamloader.Load(ctx, agentSource, &f.runConfig)
if err != nil {
return err
}
out := cli.NewPrinter(cmd.OutOrStdout())

for _, name := range team.AgentNames() {
agent, err := team.Agent(name)
for _, name := range t.AgentNames() {
agent, err := t.Agent(name)
if err != nil {
slog.Error("Failed to get agent", "name", name, "error", err)
continue
Expand All @@ -103,9 +136,39 @@ func (f *debugFlags) runDebugToolsetsCommand(cmd *cobra.Command, args []string)
}
}

if err := team.StopToolSets(ctx); err != nil {
slog.Error("Failed to stop tool sets", "error", err)
return nil
}

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

ctx := cmd.Context()

t, cleanup, err := f.loadTeam(ctx, args[0], teamloader.WithModelOverrides(f.modelOverrides))
if err != nil {
return err
}
defer cleanup()

agent, err := t.DefaultAgent()
if err != nil {
return err
}

model := agent.Model()
if model == nil {
return fmt.Errorf("agent %q has no model configured", agent.Name())
}

// Use the same title generation code path as the TUI (see runTUI in new.go)
gen := sessiontitle.New(model, agent.FallbackModels()...)

title, err := gen.Generate(ctx, "debug", []string{args[1]})
if err != nil {
return fmt.Errorf("generating title: %w", err)
}

fmt.Fprintln(cmd.OutOrStdout(), title)

return err
return nil
}
30 changes: 2 additions & 28 deletions e2e/cagent_debug_test.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,23 @@
package e2e_test

import (
"bytes"
"io"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/docker/cagent/cmd/root"
)

func TestDebug_Toolsets_None(t *testing.T) {
t.Parallel()

output := cagentDebug(t, "toolsets", "testdata/no_tools.yaml")
output := cagent(t, "debug", "toolsets", "testdata/no_tools.yaml")

require.Equal(t, "No tools for root\n", output)
}

func TestDebug_Toolsets_Todo(t *testing.T) {
t.Parallel()

output := cagentDebug(t, "toolsets", "testdata/todo_tools.yaml")
output := cagent(t, "debug", "toolsets", "testdata/todo_tools.yaml")

require.Equal(t, "2 tool(s) for root:\n + create_todo - Create a new todo item with a description\n + list_todos - List all current todos with their status\n", output)
}

func cagentDebug(t *testing.T, moreArgs ...string) string {
t.Helper()

// `cagent debug ...`
args := []string{"debug"}

// Use .env file to set DUMMY OPENAI key
dotEnv := filepath.Join(t.TempDir(), ".env")
err := os.WriteFile(dotEnv, []byte("OPENAI_API_KEY=DUMMY"), 0o644)
require.NoError(t, err)
args = append(args, "--env-from-file", dotEnv)

// Run cagent debug
var stdout bytes.Buffer
err = root.Execute(t.Context(), nil, &stdout, io.Discard, append(args, moreArgs...)...)
require.NoError(t, err)

return stdout.String()
}
41 changes: 41 additions & 0 deletions e2e/cagent_debug_title_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package e2e_test

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDebug_Title(t *testing.T) {
t.Parallel()

tests := []struct {
name string
model string
want string
}{
// OpenAI
{"OpenAI", "openai/gpt-4o", "Exploring AI Capabilities\n"},
{"OpenAI_gpt52pro", "openai/gpt-5.2-pro", "Assistant Capabilities Overview\n"},
{"OpenAI_gpt52codex", "openai/gpt-5.2-codex", "AI Assistant Capabilities\n"},

// Anthopic
{"Anthropic", "anthropic/claude-haiku-4-5", "AI Assistant Capabilities Overview\n"},
{"Anthropic_Sonnet45", "anthropic/claude-sonnet-4-5", "What can you do?\n"},
{"Anthropic_Opus46", "anthropic/claude-opus-4-6", "AI Assistant Capabilities Overview\n"},

// Google
{"Google_Gemini25FlashLite", "google/gemini-2.5-flash-lite", "AI Capabilities Overview\n"},
{"Google_Gemini3ProPreview", "google/gemini-3-pro-preview", "AI Capabilities Inquiry\n"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

title := cagent(t, "debug", "title", "testdata/basic.yaml", "--model="+tt.model, "What can you do?")

assert.Equal(t, tt.want, title)
})
}
}
Loading
Loading