mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): route explicit --stdin + --skill to headless path (#4611)
`dcode --skill ... --stdin` with piped input now runs headless instead of launching the interactive TUI. --- When a user explicitly passes `--stdin` together with `--skill`, `dcode` previously routed the piped text into the interactive TUI seed (`initial_prompt`), which hangs/fails on non-TTY environments. `apply_stdin_pipe` now skips the skill→interactive convenience when `--stdin` is explicit, falling through to the headless `non_interactive_message` path (which already supports `--skill`). Auto-detected pipes (no `--stdin`) keep the interactive seeding behavior. Made by [Open SWE](https://openswe.vercel.app/agents/057272c4-7946-e220-f7b8-ea929d2fe9ce) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -2305,15 +2305,25 @@ def apply_stdin_pipe(args: argparse.Namespace) -> None:
|
||||
# initial_prompt = "{contents of error.log}\n\nexplain this"
|
||||
```
|
||||
|
||||
- If `initial_skill` is already set (`--skill`, but not `-n`), stores the
|
||||
piped text in `initial_prompt` so the skill receives it as the
|
||||
startup request:
|
||||
- If `initial_skill` is already set (`--skill`, but not `-n`/`-m`) and the
|
||||
pipe was auto-detected (no explicit `--stdin`), stores the piped text in
|
||||
`initial_prompt` so the skill receives it as the seed for the
|
||||
interactive TUI:
|
||||
|
||||
```bash
|
||||
cat diff.txt | dcode --skill code-review
|
||||
# initial_prompt = "{contents of diff.txt}"
|
||||
```
|
||||
|
||||
When `--stdin` is passed explicitly, this convenience is skipped: the
|
||||
piped text falls through to `non_interactive_message` so the skill runs
|
||||
headless (see below):
|
||||
|
||||
```bash
|
||||
cat diff.txt | dcode --skill code-review --stdin
|
||||
# non_interactive_message = "{contents of diff.txt}"
|
||||
```
|
||||
|
||||
- Otherwise, sets `non_interactive_message` to the piped text, causing
|
||||
the CLI to run non-interactively with it as the prompt:
|
||||
|
||||
@@ -2389,12 +2399,20 @@ def apply_stdin_pipe(args: argparse.Namespace) -> None:
|
||||
if not stdin_text:
|
||||
return
|
||||
|
||||
# Priority: -n message > -m prompt > --skill (no -m) > fallback to -n.
|
||||
# Priority: -n message > -m prompt > --skill (no -m, no explicit --stdin)
|
||||
# > fallback to -n.
|
||||
# The initial_prompt branch uses `is not None` (not truthiness) so that
|
||||
# `-m ""` is distinguished from "no -m at all", allowing stdin to land
|
||||
# in initial_prompt even when the explicit value is empty. The --skill
|
||||
# branch only fires when -m was NOT provided; when both -m and --skill
|
||||
# are set, stdin merges with the -m value (previous branch).
|
||||
#
|
||||
# The --skill -> interactive `initial_prompt` routing applies only to
|
||||
# auto-detected pipes (no explicit `--stdin`), where seeding an interactive
|
||||
# TUI is a deliberate convenience. When the user passes `--stdin`
|
||||
# explicitly, that signals non-interactive intent, so we skip this branch
|
||||
# and fall through to `non_interactive_message` (headless), which also
|
||||
# supports `--skill`.
|
||||
if args.non_interactive_message:
|
||||
args.non_interactive_message = f"{stdin_text}\n\n{args.non_interactive_message}"
|
||||
elif args.initial_prompt is not None:
|
||||
@@ -2402,7 +2420,7 @@ def apply_stdin_pipe(args: argparse.Namespace) -> None:
|
||||
args.initial_prompt = f"{stdin_text}\n\n{args.initial_prompt}"
|
||||
else:
|
||||
args.initial_prompt = stdin_text
|
||||
elif getattr(args, "initial_skill", None):
|
||||
elif getattr(args, "initial_skill", None) and not explicit_stdin:
|
||||
args.initial_prompt = stdin_text
|
||||
else:
|
||||
args.non_interactive_message = stdin_text
|
||||
|
||||
@@ -612,6 +612,46 @@ class TestSkillFlagValidation:
|
||||
cli_main()
|
||||
assert exc_info.value.code == 2
|
||||
|
||||
def test_skill_with_explicit_stdin_and_quiet_runs_headless(self) -> None:
|
||||
"""`--skill --stdin -q` clears the guard and forwards the skill headless.
|
||||
|
||||
Explicit `--stdin` routes the piped text to `non_interactive_message`
|
||||
(not the interactive `-m` seed), which satisfies the `--skill` +
|
||||
`--quiet` guard and reaches `run_non_interactive` with both the piped
|
||||
message and `initial_skill`.
|
||||
"""
|
||||
from deepagents_code.main import cli_main
|
||||
|
||||
mock_stdin = MagicMock()
|
||||
mock_stdin.isatty.return_value = False
|
||||
mock_stdin.read.return_value = "review this repo"
|
||||
with (
|
||||
patch.object(
|
||||
sys,
|
||||
"argv",
|
||||
["deepagents", "--skill", "code-review", "--stdin", "-q"],
|
||||
),
|
||||
patch.object(sys, "stdin", mock_stdin),
|
||||
patch("deepagents_code.main.check_optional_tools", return_value=[]),
|
||||
patch(
|
||||
"deepagents_code.main._should_ensure_managed_ripgrep",
|
||||
return_value=False,
|
||||
),
|
||||
# Skip the /dev/tty dance — os.open would fail in test sandboxes
|
||||
# and the real code path already tolerates that failure.
|
||||
patch("os.open", side_effect=OSError("No tty in test sandbox")),
|
||||
patch(
|
||||
"deepagents_code.client.non_interactive.run_non_interactive",
|
||||
new_callable=AsyncMock,
|
||||
return_value=0,
|
||||
) as mock_run,
|
||||
pytest.raises(SystemExit) as exc_info,
|
||||
):
|
||||
cli_main()
|
||||
assert exc_info.value.code == 0
|
||||
assert mock_run.await_args.kwargs["initial_skill"] == "code-review" # ty: ignore
|
||||
assert mock_run.await_args.kwargs["message"] == "review this repo" # ty: ignore
|
||||
|
||||
|
||||
class TestMaxTurnsArgument:
|
||||
"""Tests for --max-turns argument parsing and validation."""
|
||||
@@ -1183,6 +1223,46 @@ class TestApplyStdinPipe:
|
||||
assert args.initial_prompt == "diff contents\n\nreview this"
|
||||
assert args.non_interactive_message is None
|
||||
|
||||
def test_explicit_stdin_with_skill_runs_headless(self) -> None:
|
||||
"""Explicit `--stdin` + `--skill` runs headless, not the seeded TUI."""
|
||||
args = _make_args(initial_skill="code-review", stdin=True)
|
||||
fake_stdin = io.StringIO("review this repo")
|
||||
fake_stdin.isatty = lambda: False # ty: ignore
|
||||
with patch.object(sys, "stdin", fake_stdin):
|
||||
apply_stdin_pipe(args)
|
||||
assert args.non_interactive_message == "review this repo"
|
||||
assert args.initial_prompt is None
|
||||
|
||||
def test_explicit_stdin_without_skill_sets_non_interactive(self) -> None:
|
||||
"""Explicit `--stdin` with no skill/`-n`/`-m` sets non_interactive_message."""
|
||||
args = _make_args(stdin=True)
|
||||
fake_stdin = io.StringIO("my prompt")
|
||||
fake_stdin.isatty = lambda: False # ty: ignore
|
||||
with patch.object(sys, "stdin", fake_stdin):
|
||||
apply_stdin_pipe(args)
|
||||
assert args.non_interactive_message == "my prompt"
|
||||
assert args.initial_prompt is None
|
||||
|
||||
def test_explicit_stdin_prepends_to_non_interactive(self) -> None:
|
||||
"""Explicit `--stdin` still prepends to an existing -n message."""
|
||||
args = _make_args(non_interactive_message="do something", stdin=True)
|
||||
fake_stdin = io.StringIO("context from pipe")
|
||||
fake_stdin.isatty = lambda: False # ty: ignore
|
||||
with patch.object(sys, "stdin", fake_stdin):
|
||||
apply_stdin_pipe(args)
|
||||
assert args.non_interactive_message == "context from pipe\n\ndo something"
|
||||
assert args.initial_prompt is None
|
||||
|
||||
def test_explicit_stdin_prepends_to_initial_prompt(self) -> None:
|
||||
"""Explicit `--stdin` still merges into an existing -m message."""
|
||||
args = _make_args(initial_prompt="explain this", stdin=True)
|
||||
fake_stdin = io.StringIO("error log contents")
|
||||
fake_stdin.isatty = lambda: False # ty: ignore
|
||||
with patch.object(sys, "stdin", fake_stdin):
|
||||
apply_stdin_pipe(args)
|
||||
assert args.initial_prompt == "error log contents\n\nexplain this"
|
||||
assert args.non_interactive_message is None
|
||||
|
||||
def test_non_interactive_takes_priority_over_initial_prompt(self) -> None:
|
||||
"""When both -n and -m are set, stdin is prepended to -n."""
|
||||
args = _make_args(non_interactive_message="task", initial_prompt="ignored")
|
||||
|
||||
Reference in New Issue
Block a user