From 7ef555ad198f58a664cab628ee083544968dfce5 Mon Sep 17 00:00:00 2001 From: Alex Schapiro Date: Tue, 25 Aug 2026 18:47:56 +0000 Subject: [PATCH] docs(shell): note the PTY layer's 30s yield ceiling The SDK clamps every PTY yield to 30s and floors an empty poll at 5s. Record that where the defaults are set, and tell the agent a slower command still backgrounds so it harvests it with one poll per 30s instead of asking for an unreachable yield. --- strix/agents/prompts/system_prompt.jinja | 8 +++++--- strix/config/settings.py | 9 +++++++-- tests/test_agent_factory_shell.py | 10 +++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) 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