Skip to content

Commit 91695ab

Browse files
authored
Merge pull request #2197 from dgageot/piping
Support `echo "hello" | docker agent | cat`
2 parents a099feb + 6182de9 commit 91695ab

4 files changed

Lines changed: 34 additions & 18 deletions

File tree

cmd/root/run.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,10 @@ func (f *runExecFlags) createLocalRuntimeAndSession(ctx context.Context, loadRes
416416

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

421424
err := cli.Run(ctx, out, cli.Config{
422425
AppName: AppName,

e2e/exec_test.go

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func TestExec_OpenAI(t *testing.T) {
1010
out := runCLI(t, "run", "--exec", "testdata/basic.yaml", "What's 2+2?")
1111

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

1515
// TestExec_OpenAI_V3Config tests that v3 configs work correctly with thinking disabled by default.
@@ -18,7 +18,7 @@ func TestExec_OpenAI_V3Config(t *testing.T) {
1818
out := runCLI(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)
21-
require.Equal(t, "\n--- Agent: root ---\n4", out)
21+
require.Equal(t, "4", out)
2222
}
2323

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

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

3534
func TestExec_OpenAI_ToolCall(t *testing.T) {
3635
out := runCLI(t, "run", "--exec", "testdata/fs_tools.yaml", "How many files in testdata/working_dir? Only output the number.")
3736

38-
require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
37+
require.Equal(t, "\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
3938
}
4039

4140
func TestExec_OpenAI_HideToolCalls(t *testing.T) {
4241
out := runCLI(t, "run", "--exec", "testdata/fs_tools.yaml", "--hide-tool-calls", "How many files in testdata/working_dir? Only output the number.")
4342

44-
require.Equal(t, "\n--- Agent: root ---\n1", out)
43+
require.Equal(t, "1", out)
4544
}
4645

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

5049
// With thinking enabled by default, response may include reasoning summary
51-
require.Contains(t, out, "--- Agent: root ---")
5250
require.Contains(t, out, "4")
5351
}
5452

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

58-
require.Equal(t, "\n--- Agent: root ---\n2 + 2 = 4.", out)
56+
require.Equal(t, "2 + 2 = 4.", out)
5957
}
6058

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

6462
// Model reasoning summary varies, just check for the core response
65-
require.Contains(t, out, "--- Agent: root ---")
6663
require.Contains(t, out, "4")
6764
}
6865

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

7269
// With interleaved thinking enabled by default, Anthropic responses include thinking content
73-
require.Contains(t, out, "--- Agent: root ---")
7470
require.Contains(t, out, "2 + 2 = 4")
7571
}
7672

7773
func TestExec_Anthropic_ToolCall(t *testing.T) {
7874
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.")
7975

8076
// With interleaved thinking enabled by default, Anthropic responses include thinking content
81-
require.Contains(t, out, "--- Agent: root ---")
8277
require.Contains(t, out, `Calling list_directory(path: "testdata/working_dir")`)
8378
require.Contains(t, out, `list_directory response → "FILE README.me\n"`)
8479
// The response should end with "1" (the count)
@@ -89,15 +84,13 @@ func TestExec_Anthropic_AgentsMd(t *testing.T) {
8984
out := runCLI(t, "run", "--exec", "testdata/agents-md.yaml", "--model=anthropic/claude-sonnet-4-0", "What's 2+2?")
9085

9186
// With interleaved thinking enabled by default, Anthropic responses include thinking content
92-
require.Contains(t, out, "--- Agent: root ---")
9387
require.Contains(t, out, "2 + 2 = 4")
9488
}
9589

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

9993
// With thinking enabled by default (dynamic thinking for Gemini 2.5), responses may include thinking content
100-
require.Contains(t, out, "--- Agent: root ---")
10194
// The response should contain the answer "4" somewhere
10295
require.Contains(t, out, "4")
10396
}
@@ -106,7 +99,6 @@ func TestExec_Gemini_ToolCall(t *testing.T) {
10699
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.")
107100

108101
// With thinking enabled by default (dynamic thinking for Gemini 2.5), responses include thinking content
109-
require.Contains(t, out, "--- Agent: root ---")
110102
require.Contains(t, out, `Calling list_directory(path: "testdata/working_dir")`)
111103
require.Contains(t, out, `list_directory response → "FILE README.me\n"`)
112104
// The response should end with "1" (the count)
@@ -116,13 +108,13 @@ func TestExec_Gemini_ToolCall(t *testing.T) {
116108
func TestExec_Mistral(t *testing.T) {
117109
out := runCLI(t, "run", "--exec", "testdata/basic.yaml", "--model=mistral/mistral-small", "What's 2+2?")
118110

119-
require.Equal(t, "\n--- Agent: root ---\nThe sum of 2 + 2 is 4.", out)
111+
require.Equal(t, "The sum of 2 + 2 is 4.", out)
120112
}
121113

122114
func TestExec_Mistral_ToolCall(t *testing.T) {
123115
out := runCLI(t, "run", "--exec", "testdata/fs_tools.yaml", "--model=mistral/mistral-small", "How many files in testdata/working_dir? Only output the number.")
124116

125-
require.Equal(t, "\n--- Agent: root ---\n\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
117+
require.Equal(t, "\nCalling list_directory(path: \"testdata/working_dir\")\n\nlist_directory response → \"FILE README.me\\n\"\n1", out)
126118
}
127119

128120
func TestExec_ToolCallsNeedAcceptance(t *testing.T) {

pkg/cli/printer.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@ const (
3030
var bold = color.New(color.Bold).SprintfFunc()
3131

3232
type Printer struct {
33-
out io.Writer
33+
out io.Writer
34+
isTTYOut bool
3435
}
3536

3637
func NewPrinter(out io.Writer) *Printer {
38+
isTTY := false
39+
if f, ok := out.(*os.File); ok {
40+
isTTY = isatty.IsTerminal(f.Fd())
41+
}
3742
return &Printer{
38-
out: out,
43+
out: out,
44+
isTTYOut: isTTY,
3945
}
4046
}
4147

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

6470
// PrintAgentName prints the agent name header
6571
func (p *Printer) PrintAgentName(agentName string) {
72+
if !p.isTTYOut {
73+
return
74+
}
6675
p.Printf("\n--- Agent: %s ---\n", bold(agentName))
6776
}
6877

pkg/cli/runner.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"path/filepath"
1313
"strings"
1414

15+
"github.com/mattn/go-isatty"
16+
1517
"github.com/docker/docker-agent/pkg/chat"
1618
"github.com/docker/docker-agent/pkg/input"
1719
"github.com/docker/docker-agent/pkg/runtime"
@@ -267,6 +269,16 @@ func Run(ctx context.Context, out *Printer, cfg Config, rt runtime.Runtime, sess
267269
return err
268270
}
269271
}
272+
case !isatty.IsTerminal(os.Stdin.Fd()):
273+
// Stdin is not a terminal: read all input from stdin
274+
buf, err := io.ReadAll(os.Stdin)
275+
if err != nil {
276+
return fmt.Errorf("failed to read from stdin: %w", err)
277+
}
278+
279+
if err := oneLoop(string(buf), os.Stdin); err != nil {
280+
return err
281+
}
270282
default:
271283
// No messages: interactive prompt loop
272284
out.PrintWelcomeMessage(cfg.AppName)

0 commit comments

Comments
 (0)