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
5 changes: 4 additions & 1 deletion cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,10 @@ func (f *runExecFlags) createLocalRuntimeAndSession(ctx context.Context, loadRes

func (f *runExecFlags) handleExecMode(ctx context.Context, out *cli.Printer, rt runtime.Runtime, sess *session.Session, args []string) error {
// args[0] is the agent file; args[1:] are user messages for multi-turn conversation
userMessages := args[1:]
var userMessages []string
if len(args) > 1 {
userMessages = args[1:]
}

err := cli.Run(ctx, out, cli.Config{
AppName: AppName,
Expand Down
22 changes: 7 additions & 15 deletions e2e/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestExec_OpenAI(t *testing.T) {
out := runCLI(t, "run", "--exec", "testdata/basic.yaml", "What's 2+2?")

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

// TestExec_OpenAI_V3Config tests that v3 configs work correctly with thinking disabled by default.
Expand All @@ -18,7 +18,7 @@ func TestExec_OpenAI_V3Config(t *testing.T) {
out := runCLI(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)
require.Equal(t, "4", out)
}

// TestExec_OpenAI_WithThinkingBudget tests that when thinking_budget is explicitly configured
Expand All @@ -28,57 +28,52 @@ func TestExec_OpenAI_WithThinkingBudget(t *testing.T) {

// With thinking_budget explicitly configured, response should include reasoning
// The output format includes the reasoning summary when thinking is enabled
require.Contains(t, out, "--- Agent: root ---")
require.Contains(t, out, "4")
}

func TestExec_OpenAI_ToolCall(t *testing.T) {
out := runCLI(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)
require.Equal(t, "\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 := runCLI(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)
require.Equal(t, "1", out)
}

func TestExec_OpenAI_gpt5(t *testing.T) {
out := runCLI(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 ---")
require.Contains(t, out, "4")
}

func TestExec_OpenAI_gpt5_1(t *testing.T) {
out := runCLI(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)
require.Equal(t, "2 + 2 = 4.", out)
}

func TestExec_OpenAI_gpt5_codex(t *testing.T) {
out := runCLI(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 ---")
require.Contains(t, out, "4")
}

func TestExec_Anthropic(t *testing.T) {
out := runCLI(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 ---")
require.Contains(t, out, "2 + 2 = 4")
}

func TestExec_Anthropic_ToolCall(t *testing.T) {
out := runCLI(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 ---")
require.Contains(t, out, `Calling list_directory(path: "testdata/working_dir")`)
require.Contains(t, out, `list_directory response → "FILE README.me\n"`)
// The response should end with "1" (the count)
Expand All @@ -89,15 +84,13 @@ func TestExec_Anthropic_AgentsMd(t *testing.T) {
out := runCLI(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 ---")
require.Contains(t, out, "2 + 2 = 4")
}

func TestExec_Gemini(t *testing.T) {
out := runCLI(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 ---")
// The response should contain the answer "4" somewhere
require.Contains(t, out, "4")
}
Expand All @@ -106,7 +99,6 @@ func TestExec_Gemini_ToolCall(t *testing.T) {
out := runCLI(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 ---")
require.Contains(t, out, `Calling list_directory(path: "testdata/working_dir")`)
require.Contains(t, out, `list_directory response → "FILE README.me\n"`)
// The response should end with "1" (the count)
Expand All @@ -116,13 +108,13 @@ func TestExec_Gemini_ToolCall(t *testing.T) {
func TestExec_Mistral(t *testing.T) {
out := runCLI(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)
require.Equal(t, "The sum of 2 + 2 is 4.", out)
}

func TestExec_Mistral_ToolCall(t *testing.T) {
out := runCLI(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)
require.Equal(t, "\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
}

func TestExec_ToolCallsNeedAcceptance(t *testing.T) {
Expand Down
13 changes: 11 additions & 2 deletions pkg/cli/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ const (
var bold = color.New(color.Bold).SprintfFunc()

type Printer struct {
out io.Writer
out io.Writer
isTTYOut bool
}

func NewPrinter(out io.Writer) *Printer {
isTTY := false
if f, ok := out.(*os.File); ok {
isTTY = isatty.IsTerminal(f.Fd())
}
return &Printer{
out: out,
out: out,
isTTYOut: isTTY,
}
}

Expand Down Expand Up @@ -63,6 +69,9 @@ func (p *Printer) PrintError(err error) {

// PrintAgentName prints the agent name header
func (p *Printer) PrintAgentName(agentName string) {
if !p.isTTYOut {
return
}
p.Printf("\n--- Agent: %s ---\n", bold(agentName))
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/cli/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"path/filepath"
"strings"

"github.com/mattn/go-isatty"

"github.com/docker/docker-agent/pkg/chat"
"github.com/docker/docker-agent/pkg/input"
"github.com/docker/docker-agent/pkg/runtime"
Expand Down Expand Up @@ -267,6 +269,16 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess
return err
}
}
case !isatty.IsTerminal(os.Stdin.Fd()):
// Stdin is not a terminal: read all input from stdin
buf, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read from stdin: %w", err)
}

if err := oneLoop(string(buf), os.Stdin); err != nil {
return err
}
default:
// No messages: interactive prompt loop
out.PrintWelcomeMessage(cfg.AppName)
Expand Down
Loading