From d49073c4b0259f0da24a4fe8e00931915299fbb8 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:23:45 +0100 Subject: [PATCH 1/8] Use a printer Signed-off-by: David Gageot --- cmd/root/eval.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/root/eval.go b/cmd/root/eval.go index 035ab68ff..3f7208238 100644 --- a/cmd/root/eval.go +++ b/cmd/root/eval.go @@ -1,10 +1,9 @@ package root import ( - "fmt" - "github.com/spf13/cobra" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/evaluation" "github.com/docker/cagent/pkg/teamloader" @@ -33,6 +32,8 @@ func newEvalCmd() *cobra.Command { func (f *evalFlags) runEvalCommand(cmd *cobra.Command, args []string) error { telemetry.TrackCommand("eval", args) + out := cli.NewPrinter(cmd.OutOrStdout()) + agents, err := teamloader.Load(cmd.Context(), args[0], f.runConfig) if err != nil { return err @@ -44,9 +45,9 @@ func (f *evalFlags) runEvalCommand(cmd *cobra.Command, args []string) error { } for _, evalResult := range evalResults { - fmt.Printf("Eval file: %s\n", evalResult.EvalFile) - fmt.Printf("Tool trajectory score: %f\n", evalResult.Score.ToolTrajectoryScore) - fmt.Printf("Rouge-1 score: %f\n", evalResult.Score.Rouge1Score) + out.Printf("Eval file: %s\n", evalResult.EvalFile) + out.Printf("Tool trajectory score: %f\n", evalResult.Score.ToolTrajectoryScore) + out.Printf("Rouge-1 score: %f\n", evalResult.Score.Rouge1Score) } return nil From 5570ceafc3e174ffcf8f052b9162350f869ab198 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:24:33 +0100 Subject: [PATCH 2/8] Use a command's out Signed-off-by: David Gageot --- cmd/root/version.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/root/version.go b/cmd/root/version.go index c33329265..3a039a585 100644 --- a/cmd/root/version.go +++ b/cmd/root/version.go @@ -19,9 +19,10 @@ func newVersionCmd() *cobra.Command { } } -func runVersionCommand(_ *cobra.Command, args []string) { +func runVersionCommand(cmd *cobra.Command, args []string) { telemetry.TrackCommand("version", args) - fmt.Printf("cagent version %s\n", version.Version) - fmt.Printf("Commit: %s\n", version.Commit) + out := cmd.OutOrStdout() + fmt.Fprintf(out, "cagent version %s\n", version.Version) + fmt.Fprintf(out, "Commit: %s\n", version.Commit) } From 1ff06c0ef8bd730e16c9c76d7c799ec1e2f7dafe Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:26:32 +0100 Subject: [PATCH 3/8] Use a log Signed-off-by: David Gageot --- pkg/modelsdev/store.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/modelsdev/store.go b/pkg/modelsdev/store.go index eb8a08f2a..468ef24a5 100644 --- a/pkg/modelsdev/store.go +++ b/pkg/modelsdev/store.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" "os" "path/filepath" @@ -91,7 +92,7 @@ func (s *Store) GetDatabase(ctx context.Context) (*Database, error) { // Save to cache if err := s.saveToCache(cacheFile, database); err != nil { // Log the error but don't fail the request - fmt.Printf("Warning: failed to save to cache: %v\n", err) + slog.Warn("Warning: failed to save to cache", "error", err) } return database, nil From 85352bca0dd7e375f6653aaf5dba4cd6ca13cf30 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:30:02 +0100 Subject: [PATCH 4/8] Use a Printer Signed-off-by: David Gageot --- cmd/root/api.go | 6 ++++-- cmd/root/mcp.go | 5 ++++- cmd/root/run.go | 6 +++--- cmd/root/run_test.go | 14 +++++++------- pkg/agentfile/resolver.go | 5 +++-- pkg/mcp/server.go | 5 +++-- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/cmd/root/api.go b/cmd/root/api.go index 65fc05182..e9b30866a 100644 --- a/cmd/root/api.go +++ b/cmd/root/api.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/cobra" "github.com/docker/cagent/pkg/agentfile" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/remote" "github.com/docker/cagent/pkg/server" @@ -58,6 +59,7 @@ func (f *apiFlags) runAPICommand(cmd *cobra.Command, args []string) error { telemetry.TrackCommand("api", args) ctx := cmd.Context() + out := cli.NewPrinter(cmd.OutOrStdout()) agentsPath := args[0] // Make sure no question is ever asked to the user in api mode. @@ -67,7 +69,7 @@ func (f *apiFlags) runAPICommand(cmd *cobra.Command, args []string) error { return fmt.Errorf("--pull-interval flag can only be used with OCI references, not local files") } - resolvedPath, err := agentfile.Resolve(ctx, agentsPath) + resolvedPath, err := agentfile.Resolve(ctx, out, agentsPath) if err != nil { return err } @@ -139,7 +141,7 @@ func (f *apiFlags) runAPICommand(cmd *cobra.Command, args []string) error { } // Resolve the OCI reference to get the updated file path - newResolvedPath, err := agentfile.Resolve(ctx, agentsPath) + newResolvedPath, err := agentfile.Resolve(ctx, out, agentsPath) if err != nil { slog.Error("Failed to resolve OCI reference after pull", "reference", agentsPath, "error", err) continue diff --git a/cmd/root/mcp.go b/cmd/root/mcp.go index d5f8489cb..485e341e7 100644 --- a/cmd/root/mcp.go +++ b/cmd/root/mcp.go @@ -3,6 +3,7 @@ package root import ( "github.com/spf13/cobra" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/mcp" "github.com/docker/cagent/pkg/telemetry" @@ -35,11 +36,13 @@ func newMCPCmd() *cobra.Command { func (f *mcpFlags) runMCPCommand(cmd *cobra.Command, args []string) error { telemetry.TrackCommand("mcp", args) + ctx := cmd.Context() + out := cli.NewPrinter(cmd.OutOrStdout()) if err := setupWorkingDirectory(f.workingDir); err != nil { return err } - return mcp.StartMCPServer(ctx, args[0], f.runConfig) + return mcp.StartMCPServer(ctx, out, args[0], f.runConfig) } diff --git a/cmd/root/run.go b/cmd/root/run.go index bacbed206..ee75d187f 100644 --- a/cmd/root/run.go +++ b/cmd/root/run.go @@ -94,7 +94,7 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s return err } } else { - agentFileName, err = f.resolveAgentFile(ctx, args[0]) + agentFileName, err = f.resolveAgentFile(ctx, out, args[0]) if err != nil { return err } @@ -127,11 +127,11 @@ func (f *runExecFlags) setupWorkingDirectory() error { // resolveAgentFile is a wrapper method that calls the agentfile.Resolve function // after checking for remote address -func (f *runExecFlags) resolveAgentFile(ctx context.Context, agentFilename string) (string, error) { +func (f *runExecFlags) resolveAgentFile(ctx context.Context, out *cli.Printer, agentFilename string) (string, error) { if f.remoteAddress != "" { return agentFilename, nil } - return agentfile.Resolve(ctx, agentFilename) + return agentfile.Resolve(ctx, out, agentFilename) } func (f *runExecFlags) loadAgents(ctx context.Context, agentFilename string) (*team.Team, error) { diff --git a/cmd/root/run_test.go b/cmd/root/run_test.go index 2c33b4d87..5378720ab 100644 --- a/cmd/root/run_test.go +++ b/cmd/root/run_test.go @@ -78,7 +78,7 @@ agents: defer cancel() // Test resolving a local file - resolved, err := agentfile.Resolve(ctx, yamlFile) + resolved, err := agentfile.Resolve(ctx, nil, yamlFile) require.NoError(t, err) // Should return absolute path @@ -115,7 +115,7 @@ agents: defer cancel() // First resolution - resolved1, err := agentfile.Resolve(ctx, ociRef) + resolved1, err := agentfile.Resolve(ctx, nil, ociRef) require.NoError(t, err) assert.NotEmpty(t, resolved1) @@ -132,7 +132,7 @@ agents: firstResolvedPath := resolved1 // Second resolution (simulating a reload) - resolved2, err := agentfile.Resolve(ctx, ociRef) + resolved2, err := agentfile.Resolve(ctx, nil, ociRef) require.NoError(t, err) // Should return the SAME filename @@ -158,7 +158,7 @@ agents: require.NoError(t, err) // Third resolution (simulating reload after update) - resolved3, err := agentfile.Resolve(ctx, ociRef) + resolved3, err := agentfile.Resolve(ctx, nil, ociRef) require.NoError(t, err) // Should STILL use the same filename @@ -211,10 +211,10 @@ agents: defer cancel() // Resolve both OCI refs - resolved1, err := agentfile.Resolve(ctx, ociRef1) + resolved1, err := agentfile.Resolve(ctx, nil, ociRef1) require.NoError(t, err) - resolved2, err := agentfile.Resolve(ctx, ociRef2) + resolved2, err := agentfile.Resolve(ctx, nil, ociRef2) require.NoError(t, err) // Should have DIFFERENT filenames @@ -262,7 +262,7 @@ agents: ctx, cancel := context.WithCancel(t.Context()) // Resolve the OCI ref - resolved, err := agentfile.Resolve(ctx, ociRef) + resolved, err := agentfile.Resolve(ctx, nil, ociRef) require.NoError(t, err) assert.FileExists(t, resolved) diff --git a/pkg/agentfile/resolver.go b/pkg/agentfile/resolver.go index 91f804eb7..9559e5aca 100644 --- a/pkg/agentfile/resolver.go +++ b/pkg/agentfile/resolver.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/docker/cagent/pkg/aliases" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/content" "github.com/docker/cagent/pkg/remote" ) @@ -55,7 +56,7 @@ func OciRefToFilename(ociRef string) string { } // Resolve resolves an agent file reference (local file or OCI image) to a local file path -func Resolve(ctx context.Context, agentFilename string) (string, error) { +func Resolve(ctx context.Context, out *cli.Printer, agentFilename string) (string, error) { originalOCIRef := agentFilename // Store the original for OCI ref tracking // Try to resolve as an alias first @@ -83,7 +84,7 @@ func Resolve(ctx context.Context, agentFilename string) (string, error) { // Treat as an OCI image reference. Try local store first, otherwise pull then load. a, err := FromStore(agentFilename) if err != nil { - fmt.Println("Pulling agent", agentFilename) + out.Println("Pulling agent", agentFilename) if _, pullErr := remote.Pull(agentFilename); pullErr != nil { return "", fmt.Errorf("failed to pull OCI image %s: %w", agentFilename, pullErr) } diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go index 4f40f39b3..a8a495401 100644 --- a/pkg/mcp/server.go +++ b/pkg/mcp/server.go @@ -8,6 +8,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/docker/cagent/pkg/agentfile" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/runtime" "github.com/docker/cagent/pkg/session" @@ -25,10 +26,10 @@ type ToolOutput struct { Response string `json:"response" jsonschema:"the response from the agent"` } -func StartMCPServer(ctx context.Context, agentFilename string, runConfig config.RuntimeConfig) error { +func StartMCPServer(ctx context.Context, out *cli.Printer, agentFilename string, runConfig config.RuntimeConfig) error { slog.Debug("Starting MCP server", "agent_ref", agentFilename) - agentFilename, err := agentfile.Resolve(ctx, agentFilename) + agentFilename, err := agentfile.Resolve(ctx, out, agentFilename) if err != nil { return err } From 78fca3b0b7d1134e4565b1ff08fa6de1a02eb9f5 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:30:41 +0100 Subject: [PATCH 5/8] Use a log Signed-off-by: David Gageot --- pkg/cli/runner.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cli/runner.go b/pkg/cli/runner.go index 24855e315..68b485610 100644 --- a/pkg/cli/runner.go +++ b/pkg/cli/runner.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "io" + "log/slog" "os" "path/filepath" "strings" @@ -397,7 +398,7 @@ func createUserMessageWithAttachment(agentFilename, userContent, attachmentPath // Convert file to data URL dataURL, err := fileToDataURL(attachmentPath) if err != nil { - fmt.Printf("Warning: Failed to attach file %s: %v\n", attachmentPath, err) + slog.Warn("Failed to attach file", "path", attachmentPath, "error", err) return session.UserMessage(agentFilename, userContent) } From a0d3854e49b7a5f0521e299558649e55c077198a Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:31:34 +0100 Subject: [PATCH 6/8] Use a Printer Signed-off-by: David Gageot --- cmd/root/build.go | 6 +++++- pkg/oci/build.go | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/root/build.go b/cmd/root/build.go index dd91bddae..0e48f3ab7 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -3,6 +3,7 @@ package root import ( "github.com/spf13/cobra" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/filesystem" "github.com/docker/cagent/pkg/oci" "github.com/docker/cagent/pkg/telemetry" @@ -33,11 +34,14 @@ func newBuildCmd() *cobra.Command { func (f *buildFlags) runBuildCommand(cmd *cobra.Command, args []string) error { telemetry.TrackCommand("build", args) + ctx := cmd.Context() + out := cli.NewPrinter(cmd.OutOrStdout()) + agentFilePath := args[0] dockerImageName := "" if len(args) > 1 { dockerImageName = args[1] } - return oci.BuildDockerImage(cmd.Context(), agentFilePath, filesystem.AllowAll, dockerImageName, f.opts) + return oci.BuildDockerImage(ctx, out, agentFilePath, filesystem.AllowAll, dockerImageName, f.opts) } diff --git a/pkg/oci/build.go b/pkg/oci/build.go index 9d628fc97..6ecd69d32 100644 --- a/pkg/oci/build.go +++ b/pkg/oci/build.go @@ -4,7 +4,6 @@ import ( "bytes" "context" _ "embed" - "fmt" "log/slog" "os" "os/exec" @@ -14,6 +13,7 @@ import ( "github.com/goccy/go-yaml" + "github.com/docker/cagent/pkg/cli" "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/filesystem" ) @@ -28,7 +28,7 @@ type Options struct { Pull bool } -func BuildDockerImage(ctx context.Context, agentFilePath string, fs filesystem.FS, dockerImageName string, opts Options) error { +func BuildDockerImage(ctx context.Context, out *cli.Printer, agentFilePath string, fs filesystem.FS, dockerImageName string, opts Options) error { cfg, err := config.LoadConfig(agentFilePath, fs) if err != nil { return err @@ -71,7 +71,7 @@ func BuildDockerImage(ctx context.Context, agentFilePath string, fs filesystem.F dockerfile := dockerfileBuf.String() if opts.DryRun { - fmt.Println(dockerfile) + out.Println(dockerfile) return nil } From 2f09ce3e98e7c2fda7535a331bcfbe4aac17d305 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:33:16 +0100 Subject: [PATCH 7/8] Remove log Signed-off-by: David Gageot --- pkg/remote/pull.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/remote/pull.go b/pkg/remote/pull.go index e87a53a11..a6de03c9b 100644 --- a/pkg/remote/pull.go +++ b/pkg/remote/pull.go @@ -30,7 +30,6 @@ func Pull(registryRef string, opts ...crane.Option) (string, error) { if meta, metaErr := store.GetArtifactMetadata(localRef); metaErr == nil { if meta.Digest == remoteDigest { - fmt.Printf("Artifact %s already exists in the store (digest %s). Using cache.\n", localRef, remoteDigest) return meta.Digest, nil } } From 07dd033e096bdbb6cbd0dc276f7ebd96c257f86d Mon Sep 17 00:00:00 2001 From: David Gageot Date: Wed, 5 Nov 2025 11:34:34 +0100 Subject: [PATCH 8/8] Use a log Signed-off-by: David Gageot --- pkg/creator/agent.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/creator/agent.go b/pkg/creator/agent.go index fb4d5e9cf..c487cf4ea 100644 --- a/pkg/creator/agent.go +++ b/pkg/creator/agent.go @@ -5,6 +5,7 @@ import ( _ "embed" "encoding/json" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -91,7 +92,7 @@ func CreateAgent(ctx context.Context, baseDir, prompt string, runConfig config.R return "", "", fmt.Errorf("failed to create LLM client: %w", err) } - fmt.Println("Generating agent configuration....") + slog.Info("Generating agent configuration....") fsToolset := fsToolset{inner: builtin.NewFilesystemTool([]string{baseDir})} fileName := filepath.Base(fsToolset.path) @@ -142,7 +143,7 @@ func Agent(ctx context.Context, baseDir string, runConfig config.RuntimeConfig, if modelNameOverride != "" { modelName = modelNameOverride } else { - fmt.Printf("Using default model: %s\n", modelName) + slog.Info("Using default model: " + modelName) } // If not using a model gateway, avoid selecting a provider the user can't run