From 25084782eba6fd9567ab05ca4dfcff2d23a2e3c5 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Fri, 22 May 2026 18:03:14 +0200 Subject: [PATCH] feat: add 'docker agent debug skills' command --- cmd/root/debug.go | 66 +++++++++++++++++++++++++++++++--- e2e/debug_test.go | 40 +++++++++++++++++++++ e2e/testdata/skills_local.yaml | 7 ++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 e2e/testdata/skills_local.yaml diff --git a/cmd/root/debug.go b/cmd/root/debug.go index 415c6f126..87811f8cb 100644 --- a/cmd/root/debug.go +++ b/cmd/root/debug.go @@ -14,6 +14,8 @@ import ( "github.com/docker/docker-agent/pkg/team" "github.com/docker/docker-agent/pkg/teamloader" "github.com/docker/docker-agent/pkg/telemetry" + "github.com/docker/docker-agent/pkg/tools" + skillstool "github.com/docker/docker-agent/pkg/tools/builtin/skills" ) type debugFlags struct { @@ -43,6 +45,12 @@ func newDebugCmd() *cobra.Command { Args: cobra.ExactArgs(1), RunE: flags.runDebugToolsetsCommand, }) + cmd.AddCommand(&cobra.Command{ + Use: "skills |", + Short: "Debug the skills of an agent", + Args: cobra.ExactArgs(1), + RunE: flags.runDebugSkillsCommand, + }) titleCmd := &cobra.Command{ Use: "title | ", Short: "Generate a session title from a question", @@ -118,19 +126,19 @@ func (f *debugFlags) runDebugToolsetsCommand(cmd *cobra.Command, args []string) continue } - tools, err := agent.Tools(ctx) + agentTools, err := agent.Tools(ctx) if err != nil { slog.ErrorContext(ctx, "Failed to query tools", "name", agent.Name(), "error", err) continue } - if len(tools) == 0 { + if len(agentTools) == 0 { out.Printf("No tools for %s\n", agent.Name()) continue } - out.Printf("%d tool(s) for %s:\n", len(tools), agent.Name()) - for _, tool := range tools { + out.Printf("%d tool(s) for %s:\n", len(agentTools), agent.Name()) + for _, tool := range agentTools { out.Println(" +", tool.Name, "-", tool.Description) } } @@ -138,6 +146,56 @@ func (f *debugFlags) runDebugToolsetsCommand(cmd *cobra.Command, args []string) return nil } +func (f *debugFlags) runDebugSkillsCommand(cmd *cobra.Command, args []string) (commandErr error) { + telemetry.TrackCommand(cmd.Context(), "debug", append([]string{"skills"}, args...)) + defer func() { // do not inline this defer so that commandErr is not resolved early + telemetry.TrackCommandError(cmd.Context(), "debug", append([]string{"skills"}, args...), commandErr) + }() + + ctx := cmd.Context() + + t, err := f.loadTeam(ctx, args[0]) + if err != nil { + return err + } + defer stopToolSets(t) + + out := cli.NewPrinter(cmd.OutOrStdout()) + + for _, name := range t.AgentNames() { + agent, err := t.Agent(name) + if err != nil { + slog.ErrorContext(ctx, "Failed to get agent", "name", name, "error", err) + continue + } + + var skillsToolset *skillstool.ToolSet + for _, ts := range agent.ToolSets() { + if st, ok := tools.As[*skillstool.ToolSet](ts); ok { + skillsToolset = st + break + } + } + + if skillsToolset == nil || len(skillsToolset.Skills()) == 0 { + out.Printf("No skills for %s\n", agent.Name()) + continue + } + + loadedSkills := skillsToolset.Skills() + out.Printf("%d skill(s) for %s:\n", len(loadedSkills), agent.Name()) + for _, skill := range loadedSkills { + marker := "" + if skill.IsFork() { + marker = " [forked]" + } + out.Println(" +", skill.Name+marker, "-", skill.Description) + } + } + + return nil +} + func (f *debugFlags) runDebugTitleCommand(cmd *cobra.Command, args []string) (commandErr error) { telemetry.TrackCommand(cmd.Context(), "debug", append([]string{"title"}, args...)) defer func() { // do not inline this defer so that commandErr is not resolved early diff --git a/e2e/debug_test.go b/e2e/debug_test.go index bb6fa438a..b76e819af 100644 --- a/e2e/debug_test.go +++ b/e2e/debug_test.go @@ -1,9 +1,13 @@ package e2e_test import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" + + "github.com/docker/docker-agent/pkg/skills" ) func TestDebug_Toolsets_None(t *testing.T) { @@ -21,3 +25,39 @@ func TestDebug_Toolsets_Todo(t *testing.T) { 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 TestDebug_Skills_None(t *testing.T) { + t.Parallel() + + output := runCLI(t, "debug", "skills", "testdata/no_tools.yaml") + + require.Equal(t, "No skills for root\n", output) +} + +// TestDebug_Skills_Local stages two skills (one regular, one forked) in an +// isolated kit directory and asserts that `debug skills` lists each one with +// its name, description, and the [forked] marker for fork-context skills. +func TestDebug_Skills_Local(t *testing.T) { + kit := t.TempDir() + writeSkill(t, filepath.Join(kit, skills.KitSkillsSubdir, "plain"), + "---\nname: plain\ndescription: A plain skill\n---\nbody\n") + writeSkill(t, filepath.Join(kit, skills.KitSkillsSubdir, "forky"), + "---\nname: forky\ndescription: A forked skill\ncontext: fork\n---\nbody\n") + + t.Setenv(skills.KitDirEnv, kit) + + output := runCLI(t, "debug", "skills", "testdata/skills_local.yaml") + + require.Equal(t, + "2 skill(s) for root:\n"+ + " + forky [forked] - A forked skill\n"+ + " + plain - A plain skill\n", + output, + ) +} + +func writeSkill(t *testing.T, dir, content string) { + t.Helper() + require.NoError(t, os.MkdirAll(dir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(content), 0o644)) +} diff --git a/e2e/testdata/skills_local.yaml b/e2e/testdata/skills_local.yaml new file mode 100644 index 000000000..0396ccba6 --- /dev/null +++ b/e2e/testdata/skills_local.yaml @@ -0,0 +1,7 @@ +agents: + root: + model: openai/gpt-3.5-turbo + description: A helpful AI assistant + instruction: | + You are a knowledgeable assistant. + skills: true