diff --git a/AGENTS.md b/AGENTS.md index a9f105095..c6750e61b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,14 +20,14 @@ - `./bin/cagent run ` - Run agent with configuration (launches TUI by default) - `./bin/cagent run -a ` - Run specific agent from multi-agent config - `./bin/cagent run agentcatalog/pirate` - Run agent directly from OCI registry -- `./bin/cagent exec ` - Execute agent without TUI (non-interactive) +- `./bin/cagent run --exec ` - Execute agent without TUI (non-interactive) - `./bin/cagent new` - Generate new agent configuration interactively - `./bin/cagent new --model openai/gpt-5` - Generate with specific model -- `./bin/cagent push ./agent.yaml namespace/repo` - Push agent to OCI registry -- `./bin/cagent pull namespace/repo` - Pull agent from OCI registry -- `./bin/cagent mcp ./agent.yaml` - Expose agents as MCP tools -- `./bin/cagent a2a ` - Start agent as A2A server -- `./bin/cagent api` - Start Docker `cagent` API server +- `./bin/cagent share push ./agent.yaml namespace/repo` - Push agent to OCI registry +- `./bin/cagent share pull namespace/repo` - Pull agent from OCI registry +- `./bin/cagent serve mcp ./agent.yaml` - Expose agents as MCP tools +- `./bin/cagent serve a2a ` - Start agent as A2A server +- `./bin/cagent serve api` - Start Docker `cagent` API server ### Debug and Development Flags @@ -1029,7 +1029,6 @@ task push-image # Build and push multi-platform | `main.go` | Entry point, signal handling | | `cmd/root/root.go` | Root command, logging setup, persistent flags | | `cmd/root/run.go` | `cagent run` command implementation | -| `cmd/root/exec.go` | `cagent exec` command (non-TUI) | | `pkg/runtime/runtime.go` | Core execution loop, tool handling, streaming | | `pkg/agent/agent.go` | Agent abstraction, tool discovery | | `pkg/session/session.go` | Message history management | diff --git a/Taskfile.yml b/Taskfile.yml index 06f42b15a..77c6906d9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -81,27 +81,6 @@ tasks: desc: Build and Push Docker image cmd: docker buildx build --push --platform linux/amd64,linux/arm64 -t docker/cagent {{.BUILD_ARGS}} . - push-agent: - desc: Build dockerized agent - internal: true - vars: - DOCKER_ID: - sh: curl -s --unix-socket ~/Library/Containers/com.docker.docker/Data/backend.sock http://_/registry/info | jq -r .id - deps: ["build"] - cmd: ./bin/cagent build --push ./examples/{{.AGENT}}.yaml {{.DOCKER_ID}}/cagent-{{.AGENT}} - - push-agents: - desc: Build dockerized agents - deps: - - task: push-agent - vars: { AGENT: "pirate" } - - task: push-agent - vars: { AGENT: "github" } - - task: push-agent - vars: { AGENT: "gopher" } - - task: push-agent - vars: { AGENT: "mem" } - record-demo: desc: Record demo gif cmd: vhs ./docs/recordings/demo.tape diff --git a/cmd/root/a2a.go b/cmd/root/a2a.go index 444da1b31..a531d1fc8 100644 --- a/cmd/root/a2a.go +++ b/cmd/root/a2a.go @@ -22,11 +22,10 @@ func newA2ACmd() *cobra.Command { Use: "a2a |", Short: "Start an agent as an A2A (Agent-to-Agent) server", Long: "Start an A2A server that exposes the agent via the Agent-to-Agent protocol", - Example: ` cagent a2a ./agent.yaml - cagent a2a agentcatalog/pirate --listen 127.0.0.1:9090`, - Args: cobra.ExactArgs(1), - GroupID: "server", - RunE: flags.runA2ACommand, + Example: ` cagent serve a2a ./agent.yaml + cagent serve a2a agentcatalog/pirate --listen 127.0.0.1:9090`, + Args: cobra.ExactArgs(1), + RunE: flags.runA2ACommand, } cmd.PersistentFlags().StringVarP(&flags.agentName, "agent", "a", "root", "Name of the agent to run") @@ -37,7 +36,7 @@ func newA2ACmd() *cobra.Command { } func (f *a2aFlags) runA2ACommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("a2a", args) + telemetry.TrackCommand("serve", append([]string{"a2a"}, args...)) ctx := cmd.Context() out := cli.NewPrinter(cmd.OutOrStdout()) diff --git a/cmd/root/acp.go b/cmd/root/acp.go index 07c2b02fd..3a7945626 100644 --- a/cmd/root/acp.go +++ b/cmd/root/acp.go @@ -23,22 +23,21 @@ func newACPCmd() *cobra.Command { Use: "acp |", Short: "Start an agent as an ACP (Agent Client Protocol) server", Long: "Start an ACP server that exposes the agent via the Agent Client Protocol", - Example: ` cagent acp ./agent.yaml - cagent acp ./team.yaml - cagent acp agentcatalog/pirate`, - Args: cobra.ExactArgs(1), - GroupID: "server", - RunE: flags.runACPCommand, + Example: ` cagent serve acp ./agent.yaml + cagent serve acp ./team.yaml + cagent serve acp agentcatalog/pirate`, + Args: cobra.ExactArgs(1), + RunE: flags.runACPCommand, } - addRuntimeConfigFlags(cmd, &flags.runConfig) cmd.Flags().StringVarP(&flags.sessionDB, "session-db", "s", filepath.Join(paths.GetHomeDir(), ".cagent", "session.db"), "Path to the session database") + addRuntimeConfigFlags(cmd, &flags.runConfig) return cmd } func (f *acpFlags) runACPCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("acp", args) + telemetry.TrackCommand("serve", append([]string{"acp"}, args...)) ctx := cmd.Context() agentFilename := args[0] diff --git a/cmd/root/api.go b/cmd/root/api.go index 0a27120ed..cc74c93c3 100644 --- a/cmd/root/api.go +++ b/cmd/root/api.go @@ -32,12 +32,11 @@ func newAPICmd() *cobra.Command { var flags apiFlags cmd := &cobra.Command{ - Use: "api |", - Short: "Start the cagent API server", - Long: `Start the API server that exposes the agent via a cagent-specific HTTP API`, - GroupID: "server", - Args: cobra.ExactArgs(1), - RunE: flags.runAPICommand, + Use: "api |", + Short: "Start the cagent API server", + Long: `Start the API server that exposes the agent via a cagent-specific HTTP API`, + Args: cobra.ExactArgs(1), + RunE: flags.runAPICommand, } cmd.PersistentFlags().StringVarP(&flags.listenAddr, "listen", "l", "127.0.0.1:8080", "Address to listen on") @@ -86,7 +85,7 @@ func monitorStdin(ctx context.Context, cancel context.CancelFunc, stdin *os.File } func (f *apiFlags) runAPICommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("api", args) + telemetry.TrackCommand("serve", append([]string{"api"}, args...)) ctx := cmd.Context() diff --git a/cmd/root/build.go b/cmd/root/build.go deleted file mode 100644 index 7fe049914..000000000 --- a/cmd/root/build.go +++ /dev/null @@ -1,47 +0,0 @@ -package root - -import ( - "github.com/spf13/cobra" - - "github.com/docker/cagent/pkg/build" - "github.com/docker/cagent/pkg/cli" - "github.com/docker/cagent/pkg/telemetry" -) - -type buildFlags struct { - opts build.Options -} - -func newBuildCmd() *cobra.Command { - var flags buildFlags - - cmd := &cobra.Command{ - Use: "build | [docker-image-name]", - Short: "Build a Docker image for the agent", - Args: cobra.RangeArgs(1, 2), - GroupID: "advanced", - RunE: flags.runBuildCommand, - } - - cmd.PersistentFlags().BoolVar(&flags.opts.DryRun, "dry-run", false, "only print the generated Dockerfile") - cmd.PersistentFlags().BoolVar(&flags.opts.Push, "push", false, "push the image") - cmd.PersistentFlags().BoolVar(&flags.opts.NoCache, "no-cache", false, "Do not use cache when building the image") - cmd.PersistentFlags().BoolVar(&flags.opts.Pull, "pull", false, "Always attempt to pull all referenced images") - - return cmd -} - -func (f *buildFlags) runBuildCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("build", args) - - ctx := cmd.Context() - agentFilename := args[0] - out := cli.NewPrinter(cmd.OutOrStdout()) - - dockerImageName := "" - if len(args) > 1 { - dockerImageName = args[1] - } - - return build.DockerImage(ctx, out, agentFilename, dockerImageName, f.opts) -} diff --git a/cmd/root/catalog.go b/cmd/root/catalog.go deleted file mode 100644 index 2ed3b6923..000000000 --- a/cmd/root/catalog.go +++ /dev/null @@ -1,137 +0,0 @@ -package root - -import ( - "cmp" - "context" - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - "text/tabwriter" - "time" - - "github.com/spf13/cobra" - - "github.com/docker/cagent/pkg/telemetry" -) - -func newCatalogCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "catalog", - Short: "Manage the agent catalog", - GroupID: "advanced", - } - - cmd.AddCommand(newCatalogListCmd()) - - return cmd -} - -func newCatalogListCmd() *cobra.Command { - return &cobra.Command{ - Use: "list [org]", - Short: "List catalog entries", - Args: cobra.MaximumNArgs(1), - RunE: runCatalogListCommand, - } -} - -func runCatalogListCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("catalog", append([]string{"list"}, args...)) - - org := "agentcatalog" - if len(args) > 0 { - org = args[0] - } - - return listCatalog(cmd.Context(), org) -} - -type hubRepoList struct { - Count int `json:"count"` - Next *string `json:"next"` - Previous *string `json:"previous"` - Results []struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Description string `json:"description"` - IsPrivate bool `json:"is_private"` - } `json:"results"` -} - -type hubRepo struct { - Namespace string - Name string - Description string - IsPrivate bool -} - -func fetchHubRepos(ctx context.Context, org string) ([]hubRepo, error) { - client := &http.Client{Timeout: 15 * time.Second} - url := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/?page_size=100", org) - - var repos []hubRepo - for { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) - if err != nil { - return nil, err - } - req.Header.Set("Accept", "application/json") - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - _ = resp.Body.Close() - return nil, fmt.Errorf("docker Hub API request failed: %s", resp.Status) - } - - var page hubRepoList - if err := json.NewDecoder(resp.Body).Decode(&page); err != nil { - _ = resp.Body.Close() - return nil, err - } - _ = resp.Body.Close() - - for _, r := range page.Results { - ns := cmp.Or(r.Namespace, org) - repos = append(repos, hubRepo{ - Namespace: ns, - Name: r.Name, - Description: r.Description, - IsPrivate: r.IsPrivate, - }) - } - - if page.Next == nil || *page.Next == "" { - break - } - url = *page.Next - } - - return repos, nil -} - -func listCatalog(ctx context.Context, org string) error { - repos, err := fetchHubRepos(ctx, org) - if err != nil { - return err - } - - w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) - defer func() { _ = w.Flush() }() - - fmt.Fprintf(w, "NAME\tDESCRIPTION\n") - - for _, r := range repos { - fullName := fmt.Sprintf("%s/%s", r.Namespace, r.Name) - desc := strings.ReplaceAll(r.Description, "\n", " ") - desc = strings.ReplaceAll(desc, "\t", " ") - fmt.Fprintf(w, "%s\t%s\n", fullName, desc) - } - - return nil -} diff --git a/cmd/root/config.go b/cmd/root/config.go deleted file mode 100644 index 20e35c5f7..000000000 --- a/cmd/root/config.go +++ /dev/null @@ -1,78 +0,0 @@ -package root - -import ( - "fmt" - - "github.com/goccy/go-yaml" - "github.com/spf13/cobra" - - "github.com/docker/cagent/pkg/cli" - "github.com/docker/cagent/pkg/telemetry" - "github.com/docker/cagent/pkg/userconfig" -) - -func newConfigCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "config", - Short: "Manage user configuration", - Long: "View and manage user-level cagent configuration stored in ~/.config/cagent/config.yaml", - Example: ` # Show the current configuration - cagent config show - - # Show the path to the config file - cagent config path`, - GroupID: "advanced", - RunE: runConfigShowCommand, - } - - cmd.AddCommand(newConfigShowCmd()) - cmd.AddCommand(newConfigPathCmd()) - - return cmd -} - -func newConfigShowCmd() *cobra.Command { - return &cobra.Command{ - Use: "show", - Short: "Show the current configuration", - Long: "Display the current user configuration in YAML format", - Args: cobra.NoArgs, - RunE: runConfigShowCommand, - } -} - -func newConfigPathCmd() *cobra.Command { - return &cobra.Command{ - Use: "path", - Short: "Show the path to the config file", - Args: cobra.NoArgs, - RunE: runConfigPathCommand, - } -} - -func runConfigShowCommand(cmd *cobra.Command, _ []string) error { - telemetry.TrackCommand("config", []string{"show"}) - - out := cli.NewPrinter(cmd.OutOrStdout()) - - config, err := userconfig.Load() - if err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - - data, err := yaml.MarshalWithOptions(config, yaml.IndentSequence(true), yaml.UseSingleQuote(false)) - if err != nil { - return fmt.Errorf("failed to format config: %w", err) - } - - out.Print(string(data)) - return nil -} - -func runConfigPathCommand(cmd *cobra.Command, _ []string) error { - telemetry.TrackCommand("config", []string{"path"}) - - out := cli.NewPrinter(cmd.OutOrStdout()) - out.Println(userconfig.Path()) - return nil -} diff --git a/cmd/root/config_test.go b/cmd/root/config_test.go deleted file mode 100644 index 846af24d9..000000000 --- a/cmd/root/config_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package root - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigShowCommand_Empty(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - cmd := newConfigCmd() - buf := new(bytes.Buffer) - cmd.SetOut(buf) - cmd.SetArgs([]string{"show"}) - - err := cmd.Execute() - require.NoError(t, err) - - // Empty config outputs as empty YAML object - output := buf.String() - assert.Equal(t, "{}\n", output) -} - -func TestConfigShowCommand_WithAliases(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - // Create config directory and file - configDir := filepath.Join(home, ".config", "cagent") - require.NoError(t, os.MkdirAll(configDir, 0o755)) - configContent := `aliases: - code: - path: agentcatalog/coder - docs: - path: agentcatalog/docs-writer -` - require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configContent), 0o644)) - - cmd := newConfigCmd() - buf := new(bytes.Buffer) - cmd.SetOut(buf) - cmd.SetArgs([]string{"show"}) - - err := cmd.Execute() - require.NoError(t, err) - - output := buf.String() - assert.Contains(t, output, "aliases:") - assert.Contains(t, output, "code:") - assert.Contains(t, output, "agentcatalog/coder") - assert.Contains(t, output, "docs:") - assert.Contains(t, output, "agentcatalog/docs-writer") -} - -func TestConfigShowCommand_DefaultBehavior(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - // Running "config" without subcommand should default to "show" - cmd := newConfigCmd() - buf := new(bytes.Buffer) - cmd.SetOut(buf) - cmd.SetArgs([]string{}) - - err := cmd.Execute() - require.NoError(t, err) - - // Empty config outputs as empty YAML object - output := buf.String() - assert.Equal(t, "{}\n", output) -} - -func TestConfigPathCommand(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - cmd := newConfigCmd() - buf := new(bytes.Buffer) - cmd.SetOut(buf) - cmd.SetArgs([]string{"path"}) - - err := cmd.Execute() - require.NoError(t, err) - - output := buf.String() - assert.Contains(t, output, ".config") - assert.Contains(t, output, "cagent") - assert.Contains(t, output, "config.yaml") -} - -func TestConfigShowCommand_MalformedConfig(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - // Create malformed config file - configDir := filepath.Join(home, ".config", "cagent") - require.NoError(t, os.MkdirAll(configDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte("not: valid: yaml: content"), 0o644)) - - cmd := newConfigCmd() - buf := new(bytes.Buffer) - cmd.SetOut(buf) - cmd.SetArgs([]string{"show"}) - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to load config") -} diff --git a/cmd/root/debug.go b/cmd/root/debug.go index 103fa42cc..db9616f72 100644 --- a/cmd/root/debug.go +++ b/cmd/root/debug.go @@ -29,6 +29,7 @@ func newDebugCmd() *cobra.Command { Short: "Debug tools", GroupID: "advanced", } + cmd.Hidden = true cmd.AddCommand(&cobra.Command{ Use: "config |", diff --git a/cmd/root/exec.go b/cmd/root/exec.go deleted file mode 100644 index 1eca9947c..000000000 --- a/cmd/root/exec.go +++ /dev/null @@ -1,41 +0,0 @@ -package root - -import ( - "github.com/spf13/cobra" - - "github.com/docker/cagent/pkg/cli" - "github.com/docker/cagent/pkg/telemetry" -) - -func newExecCmd() *cobra.Command { - var flags runExecFlags - - cmd := &cobra.Command{ - Use: "exec | ...", - Short: "Execute an agent", - Long: "Execute an agent with one or more user messages (multi-turn, No TUI)", - Example: ` cagent exec ./agent.yaml "What is Go?" - cagent exec ./team.yaml --agent root "First question" "Follow-up question" - echo "INSTRUCTIONS" | cagent exec ./echo.yaml - - cagent exec ./agent.yaml "question" --record # Records to auto-generated file`, - GroupID: "core", - ValidArgsFunction: completeRunExec, - Args: cobra.MinimumNArgs(2), - RunE: flags.runExecCommand, - } - - addRunOrExecFlags(cmd, &flags) - addRuntimeConfigFlags(cmd, &flags.runConfig) - cmd.PersistentFlags().BoolVar(&flags.hideToolCalls, "hide-tool-calls", false, "Hide the tool calls in the output") - cmd.PersistentFlags().BoolVar(&flags.outputJSON, "json", false, "Output results in JSON format") - - return cmd -} - -func (f *runExecFlags) runExecCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("exec", args) - - out := cli.NewPrinter(cmd.OutOrStdout()) - - return f.runOrExec(cmd.Context(), out, args, false) -} diff --git a/cmd/root/feedback.go b/cmd/root/feedback.go deleted file mode 100644 index debb0419c..000000000 --- a/cmd/root/feedback.go +++ /dev/null @@ -1,27 +0,0 @@ -package root - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/docker/cagent/pkg/feedback" - "github.com/docker/cagent/pkg/telemetry" -) - -func newFeedbackCmd() *cobra.Command { - return &cobra.Command{ - Use: "feedback", - Short: "Send feedback about cagent", - Long: "Submit feedback or report issues with cagent", - Args: cobra.NoArgs, - RunE: runFeedbackCommand, - } -} - -func runFeedbackCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("feedback", args) - - fmt.Fprintln(cmd.OutOrStdout(), "Feel free to give feedback:\n", feedback.Link) - return nil -} diff --git a/cmd/root/mcp.go b/cmd/root/mcp.go index 34b1cd4e3..cbf562765 100644 --- a/cmd/root/mcp.go +++ b/cmd/root/mcp.go @@ -22,13 +22,12 @@ func newMCPCmd() *cobra.Command { Use: "mcp |", Short: "Start an agent as an MCP (Model Context Protocol) server", Long: "Start an MCP server that exposes the agent via the Model Context Protocol. By default, uses stdio transport. Use --http to start a streaming HTTP server instead.", - Example: ` cagent mcp ./agent.yaml - cagent mcp ./team.yaml - cagent mcp agentcatalog/pirate - cagent mcp ./agent.yaml --http --listen 127.0.0.1:9090`, - Args: cobra.ExactArgs(1), - GroupID: "server", - RunE: flags.runMCPCommand, + Example: ` cagent serve mcp ./agent.yaml + cagent serve mcp ./team.yaml + cagent serve mcp agentcatalog/pirate + cagent serve mcp ./agent.yaml --http --listen 127.0.0.1:9090`, + Args: cobra.ExactArgs(1), + RunE: flags.runMCPCommand, } cmd.PersistentFlags().StringVarP(&flags.agentName, "agent", "a", "", "Name of the agent to run (all agents if not specified)") @@ -40,7 +39,7 @@ func newMCPCmd() *cobra.Command { } func (f *mcpFlags) runMCPCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("mcp", args) + telemetry.TrackCommand("serve", append([]string{"mcp"}, args...)) ctx := cmd.Context() agentFilename := args[0] diff --git a/cmd/root/pull.go b/cmd/root/pull.go index dff4ba65b..0ce4c9d9a 100644 --- a/cmd/root/pull.go +++ b/cmd/root/pull.go @@ -22,12 +22,11 @@ func newPullCmd() *cobra.Command { var flags pullFlags cmd := &cobra.Command{ - Use: "pull ", - Short: "Pull an agent from an OCI registry", - Long: "Pull an agent configuration file from an OCI registry", - GroupID: "core", - Args: cobra.ExactArgs(1), - RunE: flags.runPullCommand, + Use: "pull ", + Short: "Pull an agent from an OCI registry", + Long: "Pull an agent configuration file from an OCI registry", + Args: cobra.ExactArgs(1), + RunE: flags.runPullCommand, } cmd.PersistentFlags().BoolVar(&flags.force, "force", false, "Force pull even if the configuration already exists locally") @@ -36,7 +35,7 @@ func newPullCmd() *cobra.Command { } func (f *pullFlags) runPullCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("pull", args) + telemetry.TrackCommand("share", append([]string{"pull"}, args...)) ctx := cmd.Context() out := cli.NewPrinter(cmd.OutOrStdout()) diff --git a/cmd/root/push.go b/cmd/root/push.go index d22e50065..b2b568190 100644 --- a/cmd/root/push.go +++ b/cmd/root/push.go @@ -16,17 +16,16 @@ import ( func newPushCmd() *cobra.Command { return &cobra.Command{ - Use: "push ", - Short: "Push an agent to an OCI registry", - Long: "Push an agent configuration file to an OCI registry", - GroupID: "core", - Args: cobra.ExactArgs(2), - RunE: runPushCommand, + Use: "push ", + Short: "Push an agent to an OCI registry", + Long: "Push an agent configuration file to an OCI registry", + Args: cobra.ExactArgs(2), + RunE: runPushCommand, } } func runPushCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("push", args) + telemetry.TrackCommand("share", append([]string{"push"}, args...)) ctx := cmd.Context() agentFilename := args[0] diff --git a/cmd/root/root.go b/cmd/root/root.go index b87320921..10a8211f7 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -47,7 +47,8 @@ func NewRootCmd() *cobra.Command { Use: "cagent", Short: "cagent - AI agent runner", Long: "cagent is a command-line tool for running AI agents", - Example: ` cagent run ./agent.yaml + Example: ` cagent run + cagent run ./agent.yaml cagent run agentcatalog/pirate`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // Initialize logging before anything else so logs don't break TUI @@ -98,26 +99,16 @@ func NewRootCmd() *cobra.Command { cmd.AddCommand(newVersionCmd()) cmd.AddCommand(newRunCmd()) - cmd.AddCommand(newExecCmd()) cmd.AddCommand(newNewCmd()) - cmd.AddCommand(newAPICmd()) - cmd.AddCommand(newACPCmd()) - cmd.AddCommand(newMCPCmd()) - cmd.AddCommand(newA2ACmd()) cmd.AddCommand(newEvalCmd()) - cmd.AddCommand(newPushCmd()) - cmd.AddCommand(newPullCmd()) + cmd.AddCommand(newShareCmd()) cmd.AddCommand(newDebugCmd()) - cmd.AddCommand(newFeedbackCmd()) - cmd.AddCommand(newCatalogCmd()) - cmd.AddCommand(newBuildCmd()) cmd.AddCommand(newAliasCmd()) - cmd.AddCommand(newConfigCmd()) + cmd.AddCommand(newServeCmd()) // Define groups cmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands:"}) cmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"}) - cmd.AddGroup(&cobra.Group{ID: "server", Title: "Server Commands:"}) if isCliPLugin() { cmd.Use = "agent" @@ -156,6 +147,11 @@ We collect anonymous usage data to help improve cagent. To disable: } rootCmd := NewRootCmd() + + // When no subcommand is given, default to "run" (which runs the default agent). + // Users can use "cagent --help" to see available commands. + args = defaultToRun(rootCmd, args) + rootCmd.SetArgs(args) rootCmd.SetIn(stdin) rootCmd.SetOut(stdout) @@ -191,6 +187,42 @@ We collect anonymous usage data to help improve cagent. To disable: return nil } +// defaultToRun prepends "run" to the argument list when no subcommand is +// specified so that bare "cagent" (or "cagent --debug", etc.) launches the +// default agent. Help flags (--help / -h) are left alone. +func defaultToRun(rootCmd *cobra.Command, args []string) []string { + for _, arg := range args { + switch { + case arg == "--": + // End of flags – no subcommand found. + return append([]string{"run"}, args...) + case arg == "--help" || arg == "-h": + return args + case strings.HasPrefix(arg, "-"): + continue + case isSubcommand(rootCmd, arg): + return args + default: + return append([]string{"run"}, args...) + } + } + + return append([]string{"run"}, args...) +} + +// isSubcommand reports whether name matches a registered subcommand or alias. +func isSubcommand(cmd *cobra.Command, name string) bool { + if name == "help" { + return true + } + for _, sub := range cmd.Commands() { + if sub.Name() == name || sub.HasAlias(name) { + return true + } + } + return false +} + func processErr(ctx context.Context, err error, stderr io.Writer, rootCmd *cobra.Command) error { if ctx.Err() != nil { return ctx.Err() diff --git a/cmd/root/root_test.go b/cmd/root/root_test.go new file mode 100644 index 000000000..a9ee9b058 --- /dev/null +++ b/cmd/root/root_test.go @@ -0,0 +1,99 @@ +package root + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultToRun(t *testing.T) { + t.Parallel() + + rootCmd := NewRootCmd() + + tests := []struct { + name string + args []string + want []string + }{ + { + name: "no args defaults to run", + args: []string{}, + want: []string{"run"}, + }, + { + name: "nil args defaults to run", + args: nil, + want: []string{"run"}, + }, + { + name: "known subcommand kept as-is", + args: []string{"version"}, + want: []string{"version"}, + }, + { + name: "run subcommand kept as-is", + args: []string{"run", "./agent.yaml"}, + want: []string{"run", "./agent.yaml"}, + }, + { + name: "help subcommand kept as-is", + args: []string{"help"}, + want: []string{"help"}, + }, + { + name: "--help flag kept as-is", + args: []string{"--help"}, + want: []string{"--help"}, + }, + { + name: "-h flag kept as-is", + args: []string{"-h"}, + want: []string{"-h"}, + }, + { + name: "only flags defaults to run", + args: []string{"--debug"}, + want: []string{"run", "--debug"}, + }, + { + name: "flags with agent file defaults to run", + args: []string{"--debug", "./agent.yaml"}, + want: []string{"run", "--debug", "./agent.yaml"}, + }, + { + name: "agent file without subcommand defaults to run", + args: []string{"./agent.yaml"}, + want: []string{"run", "./agent.yaml"}, + }, + { + name: "new subcommand kept as-is", + args: []string{"new"}, + want: []string{"new"}, + }, + { + name: "serve subcommand kept as-is", + args: []string{"serve", "mcp", "./agent.yaml"}, + want: []string{"serve", "mcp", "./agent.yaml"}, + }, + { + name: "debug and help still shows help", + args: []string{"--debug", "--help"}, + want: []string{"--debug", "--help"}, + }, + { + name: "agent file with flags defaults to run", + args: []string{"./agent.yaml", "--yolo"}, + want: []string{"run", "./agent.yaml", "--yolo"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := defaultToRun(rootCmd, tt.args) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/cmd/root/run.go b/cmd/root/run.go index 0642fc89a..9cf76fad1 100644 --- a/cmd/root/run.go +++ b/cmd/root/run.go @@ -51,6 +51,7 @@ type runExecFlags struct { forceTUI bool // Exec only + exec bool hideToolCalls bool outputJSON bool @@ -111,15 +112,24 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) { cmd.PersistentFlags().BoolVar(&flags.forceTUI, "force-tui", false, "Force TUI mode even when not in a terminal") _ = cmd.PersistentFlags().MarkHidden("force-tui") cmd.MarkFlagsMutuallyExclusive("fake", "record") + + // --exec only + cmd.PersistentFlags().BoolVar(&flags.exec, "exec", false, "Execute without a TUI") + cmd.PersistentFlags().BoolVar(&flags.hideToolCalls, "hide-tool-calls", false, "Hide the tool calls in the output") + cmd.PersistentFlags().BoolVar(&flags.outputJSON, "json", false, "Output results in JSON format") } func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) error { - telemetry.TrackCommand("run", args) + if f.exec { + telemetry.TrackCommand("exec", args) + } else { + telemetry.TrackCommand("run", args) + } ctx := cmd.Context() out := cli.NewPrinter(cmd.OutOrStdout()) - useTUI := f.forceTUI || isatty.IsTerminal(os.Stdout.Fd()) + useTUI := !f.exec && (f.forceTUI || isatty.IsTerminal(os.Stdout.Fd())) return f.runOrExec(ctx, out, args, useTUI) } diff --git a/cmd/root/serve.go b/cmd/root/serve.go new file mode 100644 index 000000000..df541cecd --- /dev/null +++ b/cmd/root/serve.go @@ -0,0 +1,20 @@ +package root + +import ( + "github.com/spf13/cobra" +) + +func newServeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Start an agent as a server", + GroupID: "advanced", + } + + cmd.AddCommand(newA2ACmd()) + cmd.AddCommand(newACPCmd()) + cmd.AddCommand(newMCPCmd()) + cmd.AddCommand(newAPICmd()) + + return cmd +} diff --git a/cmd/root/share.go b/cmd/root/share.go new file mode 100644 index 000000000..c00934191 --- /dev/null +++ b/cmd/root/share.go @@ -0,0 +1,16 @@ +package root + +import "github.com/spf13/cobra" + +func newShareCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "share", + Short: "Share agents", + GroupID: "core", + } + + cmd.AddCommand(newPushCmd()) + cmd.AddCommand(newPullCmd()) + + return cmd +} diff --git a/docs/TODO.md b/docs/TODO.md index 097620200..44a67ec28 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -63,10 +63,9 @@ - [x] `--prompt-file` flag — Explanation of how it works (includes file contents as system context). *(Added)* - [x] `--session` with relative references — e.g., `-1` for last session, `-2` for second to last. *(Added)* -- [x] Multi-turn conversations in `cagent exec` — Added example. *(Added)* +- [x] Multi-turn conversations in `cagent run --exec` — Added example. *(Added)* - [x] Queueing multiple messages: `cagent run question1 question2 ...` *(Added)* - [x] `cagent eval` flags — Added examples with flags. *(Added)* -- [x] `cagent build` command — *(Added)* - [ ] `--exit-on-stdin-eof` flag — Hidden flag, low priority. - [ ] `--keep-containers` flag for eval — Already documented in eval page. diff --git a/docs/pages/concepts/distribution.html b/docs/pages/concepts/distribution.html index 8fb6d3913..e604200ed 100644 --- a/docs/pages/concepts/distribution.html +++ b/docs/pages/concepts/distribution.html @@ -7,16 +7,16 @@

Overview

💡 Tip
-

For CLI commands related to distribution, see CLI Reference (cagent push, cagent pull, cagent alias).

+

For CLI commands related to distribution, see CLI Reference (cagent share push, cagent share pull, cagent alias).

Pushing Agents

# Push to Docker Hub
-$ cagent push ./agent.yaml docker.io/username/my-agent:latest
+$ cagent share push ./agent.yaml docker.io/username/my-agent:latest
 
 # Push to GitHub Container Registry
-$ cagent push ./agent.yaml ghcr.io/username/my-agent:v1.0
+$ cagent share push ./agent.yaml ghcr.io/username/my-agent:v1.0

Pulling Agents

@@ -74,7 +74,7 @@

Private Repositories

$ docker login docker.io # Now push/pull works with private repos -$ cagent push ./agent.yaml docker.io/myorg/private-agent:latest +$ cagent share push ./agent.yaml docker.io/myorg/private-agent:latest $ cagent run docker.io/myorg/private-agent:latest
diff --git a/docs/pages/features/a2a.html b/docs/pages/features/a2a.html index 882b63099..5be4add22 100644 --- a/docs/pages/features/a2a.html +++ b/docs/pages/features/a2a.html @@ -3,7 +3,7 @@

A2A Protocol

Overview

-

The cagent a2a command starts an A2A server that exposes your agents using the A2A protocol. This enables communication between cagent and other agent frameworks that support A2A.

+

The cagent serve a2a command starts an A2A server that exposes your agents using the A2A protocol. This enables communication between cagent and other agent frameworks that support A2A.

⚠️ Early support
@@ -13,13 +13,13 @@

Overview

Usage

# Start A2A server for an agent
-$ cagent a2a ./agent.yaml
+$ cagent serve a2a ./agent.yaml
 
 # Specify a custom address
-$ cagent a2a ./agent.yaml --listen 127.0.0.1:9000
+$ cagent serve a2a ./agent.yaml --listen 127.0.0.1:9000
 
 # Use an agent from the catalog
-$ cagent a2a agentcatalog/pirate
+$ cagent serve a2a agentcatalog/pirate

Features

diff --git a/docs/pages/features/cli.html b/docs/pages/features/cli.html index 14130b21e..7352a7d10 100644 --- a/docs/pages/features/cli.html +++ b/docs/pages/features/cli.html @@ -41,19 +41,19 @@

cagent run

# Queue multiple messages (processed in sequence) $ cagent run agent.yaml "question 1" "question 2" "question 3" -

cagent exec

+

cagent run --exec

Run an agent in non-interactive (headless) mode. No TUI — output goes to stdout.

-
$ cagent exec [config] [message...] [flags]
+
$ cagent run --exec [config] [message...] [flags]
# One-shot task
-$ cagent exec agent.yaml "Create a Dockerfile for a Python Flask app"
+$ cagent run --exec agent.yaml "Create a Dockerfile for a Python Flask app"
 
 # With auto-approve
-$ cagent exec agent.yaml --yolo "Set up CI/CD pipeline"
+$ cagent run --exec agent.yaml --yolo "Set up CI/CD pipeline"
 
 # Multi-turn conversation
-$ cagent exec agent.yaml "question 1" "question 2" "question 3"
+$ cagent run --exec agent.yaml "question 1" "question 2" "question 3"

cagent new

Interactively generate a new agent configuration file.

@@ -87,14 +87,14 @@

cagent mcp

See MCP Mode for detailed setup.

-

cagent a2a

+

cagent serve a2a

Start an A2A (Agent-to-Agent) protocol server.

-
$ cagent a2a [config] [flags]
+
$ cagent serve a2a [config] [flags]
 
 # Examples
-$ cagent a2a agent.yaml
-$ cagent a2a agent.yaml --listen 127.0.0.1:9000
+$ cagent serve a2a agent.yaml +$ cagent serve a2a agent.yaml --listen 127.0.0.1:9000

cagent acp

Start an ACP (Agent Client Protocol) server over stdio. This allows external clients to interact with your agents using the ACP protocol.

@@ -106,14 +106,14 @@

cagent acp

See ACP for details on the Agent Client Protocol.

-

cagent push / cagent pull

-

Distribute agents via OCI registries.

+

cagent share push / cagent pull

+

Share agents via OCI registries.

# Push an agent
-$ cagent push ./agent.yaml docker.io/username/my-agent:latest
+$ cagent share push ./agent.yaml docker.io/username/my-agent:latest
 
 # Pull an agent
-$ cagent pull docker.io/username/my-agent:latest
+$ cagent share pull docker.io/username/my-agent:latest

See Agent Distribution for full registry workflow details.

@@ -127,15 +127,6 @@

cagent eval

$ cagent eval agent.yaml --keep-containers # Keep containers for debugging $ cagent eval agent.yaml --only "auth*" # Only run matching evals -

cagent build

-

Build agent configuration into a distributable format.

- -
$ cagent build [config] [flags]
-
-# Examples
-$ cagent build agent.yaml
-$ cagent build agent.yaml -o ./dist
-

cagent alias

Manage agent aliases for quick access.

diff --git a/docs/pages/getting-started/quickstart.html b/docs/pages/getting-started/quickstart.html index 8b73f1594..300a34df3 100644 --- a/docs/pages/getting-started/quickstart.html +++ b/docs/pages/getting-started/quickstart.html @@ -81,13 +81,13 @@

Try It Out

Non-Interactive Mode

-

Use cagent exec for one-shot tasks:

+

Use cagent run --exec for one-shot tasks:

# Ask a single question
-$ cagent exec agent.yaml "Create a Dockerfile for a Node.js app"
+$ cagent run --exec agent.yaml "Create a Dockerfile for a Node.js app"
 
 # Pipe input
-$ cat error.log | cagent exec agent.yaml "What's wrong in this log?"
+$ cat error.log | cagent run --exec agent.yaml "What's wrong in this log?"

Add More Power

diff --git a/docs/pages/guides/tips.html b/docs/pages/guides/tips.html index 7c0823575..b1c6b3363 100644 --- a/docs/pages/guides/tips.html +++ b/docs/pages/guides/tips.html @@ -330,7 +330,7 @@

GitHub PR Reviewer Example

curl -fsSL https://get.cagent.dev | sh # Run the review - cagent exec reviewer.yaml --yolo \ + cagent run --exec reviewer.yaml --yolo \ "Review PR #${{ github.event.pull_request.number }}"

With a simple reviewer agent:

diff --git a/docs/pages/home.html b/docs/pages/home.html index 5ce77fb36..6b3127cb0 100644 --- a/docs/pages/home.html +++ b/docs/pages/home.html @@ -68,7 +68,7 @@

Quick Example

cagent run agent.yaml # Or run a one-shot command -cagent exec agent.yaml "Explain the code in main.go" +cagent run --exec agent.yaml "Explain the code in main.go"

Explore the Docs

diff --git a/e2e/cagent_exec_test.go b/e2e/cagent_exec_test.go index 14d4f9d98..3d22cc47f 100644 --- a/e2e/cagent_exec_test.go +++ b/e2e/cagent_exec_test.go @@ -7,7 +7,7 @@ import ( ) func TestExec_OpenAI(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "What's 2+2?") require.Equal(t, "\n--- Agent: root ---\n2 + 2 equals 4.", out) } @@ -15,7 +15,7 @@ func TestExec_OpenAI(t *testing.T) { // TestExec_OpenAI_V3Config tests that v3 configs work correctly with thinking disabled by default. // This uses gpt-5 with a v3 config file to verify thinking is disabled for old config versions. func TestExec_OpenAI_V3Config(t *testing.T) { - out := cagent(t, "exec", "testdata/basic_v3.yaml", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic_v3.yaml", "What's 2+2?") // v3 config with gpt-5 should work correctly (thinking disabled by default for old configs) require.Equal(t, "\n--- Agent: root ---\n4", out) @@ -24,7 +24,7 @@ func TestExec_OpenAI_V3Config(t *testing.T) { // TestExec_OpenAI_WithThinkingBudget tests that when thinking_budget is explicitly configured // in the YAML, thinking is enabled by default (without needing /think command). func TestExec_OpenAI_WithThinkingBudget(t *testing.T) { - out := cagent(t, "exec", "testdata/basic_with_thinking.yaml", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic_with_thinking.yaml", "What's 2+2?") // With thinking_budget explicitly configured, response should include reasoning // The output format includes the reasoning summary when thinking is enabled @@ -33,19 +33,19 @@ func TestExec_OpenAI_WithThinkingBudget(t *testing.T) { } func TestExec_OpenAI_ToolCall(t *testing.T) { - out := cagent(t, "exec", "testdata/fs_tools.yaml", "How many files in testdata/working_dir? Only output the number.") + out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "How many files in testdata/working_dir? Only output the number.") require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out) } func TestExec_OpenAI_HideToolCalls(t *testing.T) { - out := cagent(t, "exec", "testdata/fs_tools.yaml", "--hide-tool-calls", "How many files in testdata/working_dir? Only output the number.") + out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "--hide-tool-calls", "How many files in testdata/working_dir? Only output the number.") require.Equal(t, "\n--- Agent: root ---\n1", out) } func TestExec_OpenAI_gpt5(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "--model=openai/gpt-5", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=openai/gpt-5", "What's 2+2?") // With thinking enabled by default, response may include reasoning summary require.Contains(t, out, "--- Agent: root ---") @@ -53,13 +53,13 @@ func TestExec_OpenAI_gpt5(t *testing.T) { } func TestExec_OpenAI_gpt5_1(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "--model=openai/gpt-5.1", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=openai/gpt-5.1", "What's 2+2?") require.Equal(t, "\n--- Agent: root ---\n2 + 2 = 4.", out) } func TestExec_OpenAI_gpt5_codex(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "--model=openai/gpt-5-codex", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=openai/gpt-5-codex", "What's 2+2?") // Model reasoning summary varies, just check for the core response require.Contains(t, out, "--- Agent: root ---") @@ -67,7 +67,7 @@ func TestExec_OpenAI_gpt5_codex(t *testing.T) { } func TestExec_Anthropic(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?") // With interleaved thinking enabled by default, Anthropic responses include thinking content require.Contains(t, out, "--- Agent: root ---") @@ -75,7 +75,7 @@ func TestExec_Anthropic(t *testing.T) { } func TestExec_Anthropic_ToolCall(t *testing.T) { - out := cagent(t, "exec", "testdata/fs_tools.yaml", "--model=anthropic/claude-sonnet-4-0", "How many files in testdata/working_dir? Only output the number.") + out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "--model=anthropic/claude-sonnet-4-0", "How many files in testdata/working_dir? Only output the number.") // With interleaved thinking enabled by default, Anthropic responses include thinking content require.Contains(t, out, "--- Agent: root ---") @@ -86,7 +86,7 @@ func TestExec_Anthropic_ToolCall(t *testing.T) { } func TestExec_Anthropic_AgentsMd(t *testing.T) { - out := cagent(t, "exec", "testdata/agents-md.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/agents-md.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?") // With interleaved thinking enabled by default, Anthropic responses include thinking content require.Contains(t, out, "--- Agent: root ---") @@ -94,7 +94,7 @@ func TestExec_Anthropic_AgentsMd(t *testing.T) { } func TestExec_Gemini(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "--model=google/gemini-2.5-flash", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=google/gemini-2.5-flash", "What's 2+2?") // With thinking enabled by default (dynamic thinking for Gemini 2.5), responses may include thinking content require.Contains(t, out, "--- Agent: root ---") @@ -103,7 +103,7 @@ func TestExec_Gemini(t *testing.T) { } func TestExec_Gemini_ToolCall(t *testing.T) { - out := cagent(t, "exec", "testdata/fs_tools.yaml", "--model=google/gemini-2.5-flash", "How many files in testdata/working_dir? Only output the number.") + out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "--model=google/gemini-2.5-flash", "How many files in testdata/working_dir? Only output the number.") // With thinking enabled by default (dynamic thinking for Gemini 2.5), responses include thinking content require.Contains(t, out, "--- Agent: root ---") @@ -114,19 +114,19 @@ func TestExec_Gemini_ToolCall(t *testing.T) { } func TestExec_Mistral(t *testing.T) { - out := cagent(t, "exec", "testdata/basic.yaml", "--model=mistral/mistral-small", "What's 2+2?") + out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=mistral/mistral-small", "What's 2+2?") require.Equal(t, "\n--- Agent: root ---\nThe sum of 2 + 2 is 4.", out) } func TestExec_Mistral_ToolCall(t *testing.T) { - out := cagent(t, "exec", "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.") + out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.") require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out) } func TestExec_ToolCallsNeedAcceptance(t *testing.T) { - out := cagent(t, "exec", "testdata/file_writer.yaml", "Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.") + out := cagent(t, "run", "--exec", "testdata/file_writer.yaml", "Create a hello.txt file with \"Hello, World!\" content. Try only once. On error, exit without further message.") require.Contains(t, out, `Can I run this tool? ([y]es/[a]ll/[n]o)`) } diff --git a/e2e/helpers_test.go b/e2e/helpers_test.go index 76d57e85c..48a5dbca2 100644 --- a/e2e/helpers_test.go +++ b/e2e/helpers_test.go @@ -27,15 +27,17 @@ func cagent(t *testing.T, command string, moreArgs ...string) string { require.NoError(t, err) args = append(args, "--env-from-file", dotEnv) + exec := (command == "run") && (len(moreArgs) > 0) && (moreArgs[0] == "--exec") + // Commands that talk to an AI model need a recording AI proxy. - needsProxy := command == "exec" || (command == "debug" && len(moreArgs) > 0 && moreArgs[0] == "title") + needsProxy := exec || (command == "debug" && len(moreArgs) > 0 && moreArgs[0] == "title") if needsProxy { svr, _ := startRecordingAIProxy(t) args = append(args, "--models-gateway", svr.URL) } // The exec command needs a unique session DB per test. - if command == "exec" { + if exec { sessionDB := filepath.Join(t.TempDir(), "session.db") args = append(args, "--session-db", sessionDB) } diff --git a/examples/tic-tac-toe.yaml b/examples/tic-tac-toe.yaml index 6a6c6cd53..49a469996 100644 --- a/examples/tic-tac-toe.yaml +++ b/examples/tic-tac-toe.yaml @@ -10,8 +10,8 @@ metadata: The Game master talks to both players over A2A. To run the demo: - cagent a2a --listen 127.0.0.1:8080 ./examples/tic-tac-toe.yaml --agent player_blue - cagent a2a --listen 127.0.0.1:8081 ./examples/tic-tac-toe.yaml --agent player_red + cagent serve a2a --listen 127.0.0.1:8080 ./examples/tic-tac-toe.yaml --agent player_blue + cagent serve a2a --listen 127.0.0.1:8081 ./examples/tic-tac-toe.yaml --agent player_red cagent run --yolo ./examples/tic-tac-toe.yaml 'Go!' agents: diff --git a/pkg/a2a/server.go b/pkg/a2a/server.go index 2c510cc0e..27f7a1ced 100644 --- a/pkg/a2a/server.go +++ b/pkg/a2a/server.go @@ -37,7 +37,7 @@ func routableAddr(addr string) string { } func Run(ctx context.Context, agentFilename, agentName string, runConfig *config.RuntimeConfig, ln net.Listener) error { - slog.Debug("Starting A2A server", "agent", agentName, "addr", ln.Addr().String()) + slog.Debug("Starting A2A server", "source", agentFilename, "agent", agentName, "addr", ln.Addr().String()) agentSource, err := config.Resolve(agentFilename, nil) if err != nil { diff --git a/pkg/build/Dockerfile.template b/pkg/build/Dockerfile.template deleted file mode 100644 index 4756626c6..000000000 --- a/pkg/build/Dockerfile.template +++ /dev/null @@ -1,31 +0,0 @@ -# syntax=docker/dockerfile:1 - -FROM {{ .BaseImage }} - -LABEL com.docker.agent.packaging.version="v0.0.2" -LABEL com.docker.agent.runtime="cagent" -LABEL com.docker.agent.secrets.models="{{ .ModelSecrets }}" -LABEL com.docker.agent.secrets.tools="{{ .ToolSecrets }}" - -LABEL org.opencontainers.image.author="{{ .Metadata.Author }}" -LABEL org.opencontainers.image.created="{{ .BuildDate }}" -LABEL org.opencontainers.image.description="{{ .Description }}" -LABEL org.opencontainers.image.documentation="{{ .Metadata.Readme }}" -LABEL org.opencontainers.image.licenses="{{ .Metadata.License }}" - -# Override labels found in docker/cagent until we know what to put there -LABEL org.opencontainers.image.revision="" -LABEL org.opencontainers.image.source="" -LABEL org.opencontainers.image.title="" -LABEL org.opencontainers.image.url="" -LABEL org.opencontainers.image.version="" - -VOLUME /data -ENV TELEMETRY_ENABLED=true -ENV CAGENT_HIDE_TELEMETRY_BANNER=0 -USER root -RUN cat </agent.yaml && chmod 777 /agent.yaml -{{ .AgentConfig }} -EOF -USER cagent -CMD ["api", "--listen", "unix:///data/agent.sock", "--session-db", "/data/session.db", "/agent.yaml"] diff --git a/pkg/build/build.go b/pkg/build/build.go deleted file mode 100644 index 8db56cd9a..000000000 --- a/pkg/build/build.go +++ /dev/null @@ -1,108 +0,0 @@ -package build - -import ( - "bytes" - "context" - _ "embed" - "log/slog" - "os" - "os/exec" - "strings" - "text/template" - "time" - - "github.com/goccy/go-yaml" - - "github.com/docker/cagent/pkg/config" -) - -//go:embed Dockerfile.template -var dockerfileTemplate string - -type Options struct { - DryRun bool - Push bool - NoCache bool - Pull bool -} - -type Printer interface { - Println(a ...any) -} - -func DockerImage(ctx context.Context, out Printer, agentFilename, dockerImageName string, opts Options) error { - agentSource, err := config.Resolve(agentFilename, nil) - if err != nil { - return err - } - - cfg, err := config.Load(ctx, agentSource) - if err != nil { - return err - } - - // Compute the canonical form of the config - canonical, err := yaml.Marshal(cfg) - if err != nil { - return err - } - - // Analyze the config to find which secrets are needed - modelSecrets := config.GatherEnvVarsForModels(cfg) - toolSecrets, err := config.GatherEnvVarsForTools(ctx, cfg) - if err != nil { - return err - } - - // Find which base image to use - baseImage := "docker/cagent" - if baseImageOverride := os.Getenv("CAGENT_BASE_IMAGE"); baseImageOverride != "" { - baseImage = baseImageOverride - } - - // Generate the Dockerfile - var dockerfileBuf bytes.Buffer - - tpl := template.Must(template.New("Dockerfile").Parse(dockerfileTemplate)) - if err := tpl.Execute(&dockerfileBuf, map[string]any{ - "BaseImage": baseImage, - "AgentConfig": string(canonical), - "BuildDate": time.Now().UTC().Format(time.RFC3339), - "Description": cfg.Metadata.Description, - "Metadata": cfg.Metadata, - "ModelSecrets": strings.Join(modelSecrets, ","), - "ToolSecrets": strings.Join(toolSecrets, ","), - }); err != nil { - return err - } - - dockerfile := dockerfileBuf.String() - if opts.DryRun { - out.Println(dockerfile) - return nil - } - - // Run docker build - buildArgs := []string{"build"} - if opts.NoCache { - buildArgs = append(buildArgs, "--no-cache") - } - if opts.Pull { - buildArgs = append(buildArgs, "--pull") - } - if dockerImageName != "" { - buildArgs = append(buildArgs, "-t", dockerImageName) - if opts.Push { - buildArgs = append(buildArgs, "--push", "--platform", "linux/amd64,linux/arm64") - } - } - buildArgs = append(buildArgs, "-") - - buildCmd := exec.CommandContext(ctx, "docker", buildArgs...) - buildCmd.Stdin = strings.NewReader(dockerfile) - buildCmd.Stdout = os.Stdout - buildCmd.Stderr = os.Stderr - slog.Debug("running docker build", "args", buildArgs) - - return buildCmd.Run() -} diff --git a/pkg/config/builtin-agents/default.yaml b/pkg/config/builtin-agents/default.yaml index 5d2b707c1..e164404ed 100644 --- a/pkg/config/builtin-agents/default.yaml +++ b/pkg/config/builtin-agents/default.yaml @@ -7,6 +7,7 @@ agents: instruction: | You are a knowledgeable assistant that helps users with various tasks. Be helpful, accurate, and concise in your responses. + skills: true add_date: true add_environment_info: true add_prompt_files: diff --git a/pkg/evaluation/eval.go b/pkg/evaluation/eval.go index aaa73b3f0..ec612416c 100644 --- a/pkg/evaluation/eval.go +++ b/pkg/evaluation/eval.go @@ -397,8 +397,8 @@ func (r *Runner) runCagentInContainer(ctx context.Context, imageID string, quest } // When a setup script is provided, mount it into the container and - // override the entrypoint to run it before cagent exec. - // The default entrypoint is: /run.sh /cagent exec --yolo --json + // override the entrypoint to run it before cagent run --exec. + // The default entrypoint is: /run.sh /cagent run --exec --yolo --json // /run.sh starts dockerd then exec's "$@". if setup != "" { setupFile := filepath.Join(os.TempDir(), fmt.Sprintf("cagent-eval-setup-%d.sh", uuid.New().ID())) @@ -416,8 +416,8 @@ func (r *Runner) runCagentInContainer(ctx context.Context, imageID string, quest args = append(args, imageID) if setup != "" { - // Run setup script, then cagent exec with the original arguments. - args = append(args, "sh", "-c", "sh /setup.sh && exec /cagent exec --yolo --json \"$@\"", "--", "/configs/"+agentFile) + // Run setup script, then cagent run --exec with the original arguments. + args = append(args, "sh", "-c", "sh /setup.sh && exec /cagent run --exec --yolo --json \"$@\"", "--", "/configs/"+agentFile) } else { args = append(args, "/configs/"+agentFile) } diff --git a/pkg/remote/pull.go b/pkg/remote/pull.go index 343925eab..f1c5f625e 100644 --- a/pkg/remote/pull.go +++ b/pkg/remote/pull.go @@ -34,7 +34,7 @@ func Pull(ctx context.Context, registryRef string, force bool, opts ...crane.Opt if meta, metaErr := store.GetArtifactMetadata(localRef); metaErr == nil { if meta.Digest == remoteDigest { if !hasCagentAnnotation(meta.Annotations) { - return "", fmt.Errorf("artifact %s found in store wasn't created by `cagent push`\nTry to push again with `cagent push` (cagent >= v1.10.0)", localRef) + return "", fmt.Errorf("artifact %s found in store wasn't created by `cagent share push`\nTry to push again with `cagent share push` (cagent >= v1.10.0)", localRef) } return meta.Digest, nil } @@ -51,7 +51,7 @@ func Pull(ctx context.Context, registryRef string, force bool, opts ...crane.Opt return "", fmt.Errorf("getting manifest from pulled image: %w", err) } if !hasCagentAnnotation(manifest.Annotations) { - return "", fmt.Errorf("artifact %s wasn't created by `cagent push`\nTry to push again with `cagent push` (cagent >= v1.10.0)", localRef) + return "", fmt.Errorf("artifact %s wasn't created by `cagent share push`\nTry to push again with `cagent share push` (cagent >= v1.10.0)", localRef) } digest, err := store.StoreArtifact(img, localRef) diff --git a/pkg/session/session.go b/pkg/session/session.go index 851708215..eec056159 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -207,7 +207,7 @@ type EvalCriteria struct { Relevance []string `json:"relevance"` // Statements that should be true about the response WorkingDir string `json:"working_dir,omitempty"` // Subdirectory under evals/working_dirs/ Size string `json:"size,omitempty"` // Expected response size: S, M, L, XL - Setup string `json:"setup,omitempty"` // Optional sh script to run in the container before cagent exec + Setup string `json:"setup,omitempty"` // Optional sh script to run in the container before cagent run --exec } // Session helper methods