diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja index a7d6f8c2..d348788c 100644 --- a/strix/agents/prompts/system_prompt.jinja +++ b/strix/agents/prompts/system_prompt.jinja @@ -188,9 +188,11 @@ EFFICIENCY TACTICS: the instant there is new output or the process exits, or give the original `exec_command` a bigger `yield_time_ms` up front so it doesn't background. - When you expect a command to take a while (a scan like `nmap`/`nuclei`/`ffuf`, - a build, a long crawl), estimate its runtime and pass that as - `yield_time_ms` on the first `exec_command` — one call that waits beats a - backgrounded process you then poll for many turns. Default is 30s. + a build, a long crawl), pass the time you expect to need as `yield_time_ms` + on the first `exec_command` — one call that waits beats a backgrounded + process you then poll for many turns. The shell yields at most 30s per call + (the default), so anything slower than that still backgrounds: harvest it + with one `write_stdin(chars="")` poll per 30s rather than many short ones. - Before importing a third-party Python library, make sure it is installed. The sandbox's `python3` runs inside a preconfigured virtualenv that ships `requests`, `httpx`, `beautifulsoup4` (bs4), `lxml`, `pyjwt`, and diff --git a/strix/config/settings.py b/strix/config/settings.py index 43a319f5..dabd6775 100644 --- a/strix/config/settings.py +++ b/strix/config/settings.py @@ -109,13 +109,18 @@ class ShellSettings(BaseSettings): after only 250ms on a ``write_stdin`` poll and 10s on ``exec_command``. Raising these defaults lets one call return a meaningful result instead of a no-op round-trip. An explicit ``yield_time_ms`` from the model always wins. + + The SDK's PTY layer clamps every yield to 30s, so a larger value here would + be silently ineffective; keep both yields at or below that ceiling. """ model_config = _BASE_CONFIG - # Default yield for exec_command when the model omits yield_time_ms. + # Default yield for exec_command when the model omits yield_time_ms. 30s is + # the most the PTY layer honours, so this sits right at that ceiling. exec_yield_ms: int = Field(default=30_000, gt=0, alias="STRIX_SHELL_EXEC_YIELD_MS") - # Default yield for an empty (polling) write_stdin call. + # Default yield for an empty (polling) write_stdin call. The SDK already + # floors an empty poll at 5s; this trades a little latency for far fewer turns. write_stdin_poll_yield_ms: int = Field( default=20_000, gt=0, alias="STRIX_SHELL_WRITE_STDIN_POLL_YIELD_MS" ) diff --git a/tests/test_agent_factory_shell.py b/tests/test_agent_factory_shell.py index 1c8569be..fd690eb6 100644 --- a/tests/test_agent_factory_shell.py +++ b/tests/test_agent_factory_shell.py @@ -164,16 +164,20 @@ async def test_wrap_exec_command_default_does_not_depend_on_the_binary(cmd: str) @pytest.mark.asyncio -async def test_wrap_exec_command_preserves_long_explicit_yield() -> None: +async def test_wrap_exec_command_preserves_longer_explicit_yield() -> None: + """A slow command gets the yield the agent asked for, not a guessed one. + + The SDK's PTY layer clamps anything above 30s, so a longer wait than that + cannot be bought with a bigger argument.""" captured: dict[str, str] = {} wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured)) await wrapped.on_invoke_tool( cast("Any", None), - json.dumps({"cmd": "nmap -p- example.com", "yield_time_ms": 300_000}), + json.dumps({"cmd": "nmap -p- example.com", "yield_time_ms": 25_000}), ) - assert json.loads(captured["raw_input"])["yield_time_ms"] == 300_000 + assert json.loads(captured["raw_input"])["yield_time_ms"] == 25_000 @pytest.mark.asyncio