Skip to content

Commit 50ef192

Browse files
committed
Replace cagent exec with cagent run --exec (breaking change)
Signed-off-by: David Gageot <david.gageot@docker.com>
1 parent c67dfb4 commit 50ef192

13 files changed

Lines changed: 49 additions & 80 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
- `./bin/cagent run <config.yaml>` - Run agent with configuration (launches TUI by default)
2121
- `./bin/cagent run <config.yaml> -a <agent_name>` - Run specific agent from multi-agent config
2222
- `./bin/cagent run agentcatalog/pirate` - Run agent directly from OCI registry
23-
- `./bin/cagent exec <config.yaml>` - Execute agent without TUI (non-interactive)
23+
- `./bin/cagent run --exec <config.yaml>` - Execute agent without TUI (non-interactive)
2424
- `./bin/cagent new` - Generate new agent configuration interactively
2525
- `./bin/cagent new --model openai/gpt-5` - Generate with specific model
2626
- `./bin/cagent push ./agent.yaml namespace/repo` - Push agent to OCI registry
@@ -1029,7 +1029,6 @@ task push-image # Build and push multi-platform
10291029
| `main.go` | Entry point, signal handling |
10301030
| `cmd/root/root.go` | Root command, logging setup, persistent flags |
10311031
| `cmd/root/run.go` | `cagent run` command implementation |
1032-
| `cmd/root/exec.go` | `cagent exec` command (non-TUI) |
10331032
| `pkg/runtime/runtime.go` | Core execution loop, tool handling, streaming |
10341033
| `pkg/agent/agent.go` | Agent abstraction, tool discovery |
10351034
| `pkg/session/session.go` | Message history management |

cmd/root/exec.go

Lines changed: 0 additions & 41 deletions
This file was deleted.

cmd/root/root.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ func NewRootCmd() *cobra.Command {
9999

100100
cmd.AddCommand(newVersionCmd())
101101
cmd.AddCommand(newRunCmd())
102-
cmd.AddCommand(newExecCmd())
103102
cmd.AddCommand(newNewCmd())
104103
cmd.AddCommand(newEvalCmd())
105104
cmd.AddCommand(newPushCmd())

cmd/root/run.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type runExecFlags struct {
5151
forceTUI bool
5252

5353
// Exec only
54+
exec bool
5455
hideToolCalls bool
5556
outputJSON bool
5657

@@ -111,15 +112,24 @@ func addRunOrExecFlags(cmd *cobra.Command, flags *runExecFlags) {
111112
cmd.PersistentFlags().BoolVar(&flags.forceTUI, "force-tui", false, "Force TUI mode even when not in a terminal")
112113
_ = cmd.PersistentFlags().MarkHidden("force-tui")
113114
cmd.MarkFlagsMutuallyExclusive("fake", "record")
115+
116+
// --exec only
117+
cmd.PersistentFlags().BoolVar(&flags.exec, "exec", false, "Execute without a TUI")
118+
cmd.PersistentFlags().BoolVar(&flags.hideToolCalls, "hide-tool-calls", false, "Hide the tool calls in the output")
119+
cmd.PersistentFlags().BoolVar(&flags.outputJSON, "json", false, "Output results in JSON format")
114120
}
115121

116122
func (f *runExecFlags) runRunCommand(cmd *cobra.Command, args []string) error {
117-
telemetry.TrackCommand("run", args)
123+
if f.exec {
124+
telemetry.TrackCommand("exec", args)
125+
} else {
126+
telemetry.TrackCommand("run", args)
127+
}
118128

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

122-
useTUI := f.forceTUI || isatty.IsTerminal(os.Stdout.Fd())
132+
useTUI := !f.exec && (f.forceTUI || isatty.IsTerminal(os.Stdout.Fd()))
123133
return f.runOrExec(ctx, out, args, useTUI)
124134
}
125135

docs/TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363

6464
- [x] `--prompt-file` flag — Explanation of how it works (includes file contents as system context). *(Added)*
6565
- [x] `--session` with relative references — e.g., `-1` for last session, `-2` for second to last. *(Added)*
66-
- [x] Multi-turn conversations in `cagent exec` — Added example. *(Added)*
66+
- [x] Multi-turn conversations in `cagent run --exec` — Added example. *(Added)*
6767
- [x] Queueing multiple messages: `cagent run question1 question2 ...` *(Added)*
6868
- [x] `cagent eval` flags — Added examples with flags. *(Added)*
6969
- [ ] `--exit-on-stdin-eof` flag — Hidden flag, low priority.

docs/pages/features/cli.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,19 @@ <h3><code>cagent run</code></h3>
4141
# Queue multiple messages (processed in sequence)
4242
$ cagent run agent.yaml "question 1" "question 2" "question 3"</code></pre>
4343

44-
<h3><code>cagent exec</code></h3>
44+
<h3><code>cagent run --exec</code></h3>
4545
<p>Run an agent in non-interactive (headless) mode. No TUI — output goes to stdout.</p>
4646

47-
<pre><code class="language-bash">$ cagent exec [config] [message...] [flags]</code></pre>
47+
<pre><code class="language-bash">$ cagent run --exec [config] [message...] [flags]</code></pre>
4848

4949
<pre><code class="language-bash"># One-shot task
50-
$ cagent exec agent.yaml "Create a Dockerfile for a Python Flask app"
50+
$ cagent run --exec agent.yaml "Create a Dockerfile for a Python Flask app"
5151

5252
# With auto-approve
53-
$ cagent exec agent.yaml --yolo "Set up CI/CD pipeline"
53+
$ cagent run --exec agent.yaml --yolo "Set up CI/CD pipeline"
5454

5555
# Multi-turn conversation
56-
$ cagent exec agent.yaml "question 1" "question 2" "question 3"</code></pre>
56+
$ cagent run --exec agent.yaml "question 1" "question 2" "question 3"</code></pre>
5757

5858
<h3><code>cagent new</code></h3>
5959
<p>Interactively generate a new agent configuration file.</p>

docs/pages/getting-started/quickstart.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ <h2>Try It Out</h2>
8181

8282
<h2>Non-Interactive Mode</h2>
8383

84-
<p>Use <code>cagent exec</code> for one-shot tasks:</p>
84+
<p>Use <code>cagent run --exec</code> for one-shot tasks:</p>
8585

8686
<pre><code class="language-bash"># Ask a single question
87-
$ cagent exec agent.yaml "Create a Dockerfile for a Node.js app"
87+
$ cagent run --exec agent.yaml "Create a Dockerfile for a Node.js app"
8888

8989
# Pipe input
90-
$ cat error.log | cagent exec agent.yaml "What's wrong in this log?"</code></pre>
90+
$ cat error.log | cagent run --exec agent.yaml "What's wrong in this log?"</code></pre>
9191

9292
<h2>Add More Power</h2>
9393

docs/pages/guides/tips.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ <h3>GitHub PR Reviewer Example</h3>
330330
curl -fsSL https://get.cagent.dev | sh
331331

332332
# Run the review
333-
cagent exec reviewer.yaml --yolo \
333+
cagent run --exec reviewer.yaml --yolo \
334334
"Review PR #${{ github.event.pull_request.number }}"</code></pre>
335335

336336
<p>With a simple reviewer agent:</p>

docs/pages/home.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ <h2>Quick Example</h2>
6868
cagent run agent.yaml
6969

7070
# Or run a one-shot command
71-
cagent exec agent.yaml "Explain the code in main.go"</code></pre>
71+
cagent run --exec agent.yaml "Explain the code in main.go"</code></pre>
7272

7373
<h2>Explore the Docs</h2>
7474

e2e/cagent_exec_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import (
77
)
88

99
func TestExec_OpenAI(t *testing.T) {
10-
out := cagent(t, "exec", "testdata/basic.yaml", "What's 2+2?")
10+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "What's 2+2?")
1111

1212
require.Equal(t, "\n--- Agent: root ---\n2 + 2 equals 4.", out)
1313
}
1414

1515
// TestExec_OpenAI_V3Config tests that v3 configs work correctly with thinking disabled by default.
1616
// This uses gpt-5 with a v3 config file to verify thinking is disabled for old config versions.
1717
func TestExec_OpenAI_V3Config(t *testing.T) {
18-
out := cagent(t, "exec", "testdata/basic_v3.yaml", "What's 2+2?")
18+
out := cagent(t, "run", "--exec", "testdata/basic_v3.yaml", "What's 2+2?")
1919

2020
// v3 config with gpt-5 should work correctly (thinking disabled by default for old configs)
2121
require.Equal(t, "\n--- Agent: root ---\n4", out)
@@ -24,7 +24,7 @@ func TestExec_OpenAI_V3Config(t *testing.T) {
2424
// TestExec_OpenAI_WithThinkingBudget tests that when thinking_budget is explicitly configured
2525
// in the YAML, thinking is enabled by default (without needing /think command).
2626
func TestExec_OpenAI_WithThinkingBudget(t *testing.T) {
27-
out := cagent(t, "exec", "testdata/basic_with_thinking.yaml", "What's 2+2?")
27+
out := cagent(t, "run", "--exec", "testdata/basic_with_thinking.yaml", "What's 2+2?")
2828

2929
// With thinking_budget explicitly configured, response should include reasoning
3030
// The output format includes the reasoning summary when thinking is enabled
@@ -33,49 +33,49 @@ func TestExec_OpenAI_WithThinkingBudget(t *testing.T) {
3333
}
3434

3535
func TestExec_OpenAI_ToolCall(t *testing.T) {
36-
out := cagent(t, "exec", "testdata/fs_tools.yaml", "How many files in testdata/working_dir? Only output the number.")
36+
out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "How many files in testdata/working_dir? Only output the number.")
3737

3838
require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
3939
}
4040

4141
func TestExec_OpenAI_HideToolCalls(t *testing.T) {
42-
out := cagent(t, "exec", "testdata/fs_tools.yaml", "--hide-tool-calls", "How many files in testdata/working_dir? Only output the number.")
42+
out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "--hide-tool-calls", "How many files in testdata/working_dir? Only output the number.")
4343

4444
require.Equal(t, "\n--- Agent: root ---\n1", out)
4545
}
4646

4747
func TestExec_OpenAI_gpt5(t *testing.T) {
48-
out := cagent(t, "exec", "testdata/basic.yaml", "--model=openai/gpt-5", "What's 2+2?")
48+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=openai/gpt-5", "What's 2+2?")
4949

5050
// With thinking enabled by default, response may include reasoning summary
5151
require.Contains(t, out, "--- Agent: root ---")
5252
require.Contains(t, out, "4")
5353
}
5454

5555
func TestExec_OpenAI_gpt5_1(t *testing.T) {
56-
out := cagent(t, "exec", "testdata/basic.yaml", "--model=openai/gpt-5.1", "What's 2+2?")
56+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=openai/gpt-5.1", "What's 2+2?")
5757

5858
require.Equal(t, "\n--- Agent: root ---\n2 + 2 = 4.", out)
5959
}
6060

6161
func TestExec_OpenAI_gpt5_codex(t *testing.T) {
62-
out := cagent(t, "exec", "testdata/basic.yaml", "--model=openai/gpt-5-codex", "What's 2+2?")
62+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=openai/gpt-5-codex", "What's 2+2?")
6363

6464
// Model reasoning summary varies, just check for the core response
6565
require.Contains(t, out, "--- Agent: root ---")
6666
require.Contains(t, out, "4")
6767
}
6868

6969
func TestExec_Anthropic(t *testing.T) {
70-
out := cagent(t, "exec", "testdata/basic.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?")
70+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?")
7171

7272
// With interleaved thinking enabled by default, Anthropic responses include thinking content
7373
require.Contains(t, out, "--- Agent: root ---")
7474
require.Contains(t, out, "2 + 2 = 4")
7575
}
7676

7777
func TestExec_Anthropic_ToolCall(t *testing.T) {
78-
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.")
78+
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.")
7979

8080
// With interleaved thinking enabled by default, Anthropic responses include thinking content
8181
require.Contains(t, out, "--- Agent: root ---")
@@ -86,15 +86,15 @@ func TestExec_Anthropic_ToolCall(t *testing.T) {
8686
}
8787

8888
func TestExec_Anthropic_AgentsMd(t *testing.T) {
89-
out := cagent(t, "exec", "testdata/agents-md.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?")
89+
out := cagent(t, "run", "--exec", "testdata/agents-md.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?")
9090

9191
// With interleaved thinking enabled by default, Anthropic responses include thinking content
9292
require.Contains(t, out, "--- Agent: root ---")
9393
require.Contains(t, out, "2 + 2 = 4")
9494
}
9595

9696
func TestExec_Gemini(t *testing.T) {
97-
out := cagent(t, "exec", "testdata/basic.yaml", "--model=google/gemini-2.5-flash", "What's 2+2?")
97+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=google/gemini-2.5-flash", "What's 2+2?")
9898

9999
// With thinking enabled by default (dynamic thinking for Gemini 2.5), responses may include thinking content
100100
require.Contains(t, out, "--- Agent: root ---")
@@ -103,7 +103,7 @@ func TestExec_Gemini(t *testing.T) {
103103
}
104104

105105
func TestExec_Gemini_ToolCall(t *testing.T) {
106-
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.")
106+
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.")
107107

108108
// With thinking enabled by default (dynamic thinking for Gemini 2.5), responses include thinking content
109109
require.Contains(t, out, "--- Agent: root ---")
@@ -114,19 +114,19 @@ func TestExec_Gemini_ToolCall(t *testing.T) {
114114
}
115115

116116
func TestExec_Mistral(t *testing.T) {
117-
out := cagent(t, "exec", "testdata/basic.yaml", "--model=mistral/mistral-small", "What's 2+2?")
117+
out := cagent(t, "run", "--exec", "testdata/basic.yaml", "--model=mistral/mistral-small", "What's 2+2?")
118118

119119
require.Equal(t, "\n--- Agent: root ---\nThe sum of 2 + 2 is 4.", out)
120120
}
121121

122122
func TestExec_Mistral_ToolCall(t *testing.T) {
123-
out := cagent(t, "exec", "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.")
123+
out := cagent(t, "run", "--exec", "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.")
124124

125125
require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
126126
}
127127

128128
func TestExec_ToolCallsNeedAcceptance(t *testing.T) {
129-
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.")
129+
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.")
130130

131131
require.Contains(t, out, `Can I run this tool? ([y]es/[a]ll/[n]o)`)
132132
}

0 commit comments

Comments
 (0)