This commit is contained in:
Ben Younes 2026-09-18 07:19:31 +02:00 committed by GitHub
commit a59de106a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 56 additions and 5 deletions

View file

@ -7,7 +7,7 @@ import inspect
import json
import logging
import re
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from agents.agent import ToolsToFinalOutputResult
from agents.sandbox import SandboxAgent
@ -378,6 +378,11 @@ def _make_filesystem_configurator(*, chat_completions: bool, strict_schemas: boo
_CHARS_ESCAPE_RE = re.compile(r"\\(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0abtnvfr\\])")
_PTY_SESSION_NOT_FOUND_RE = re.compile(r"PTY session not found: (?P<session_id>\d+)")
_WRITE_STDIN_DEAD_SESSION_RESULT_TEMPLATE = (
"write_stdin: session {session_id} is already terminal/non-retryable "
"after PTY session not found."
)
_CHARS_ESCAPE_MAP = {
"\\\\": "\\",
"\\n": "\n",
@ -456,21 +461,34 @@ def _wrap_exec_command(tool: FunctionTool) -> FunctionTool:
def _wrap_write_stdin(tool: FunctionTool) -> FunctionTool:
invoke_tool = tool.on_invoke_tool
dead_pty_session_ids: set[int] = set()
async def invoke(ctx: Any, raw_input: str) -> Any:
parsed: dict[str, Any] | None
try:
parsed = json.loads(raw_input)
raw_parsed: Any = json.loads(raw_input)
except json.JSONDecodeError:
parsed = None
else:
parsed = cast("dict[str, Any]", raw_parsed) if isinstance(raw_parsed, dict) else None
session_id = parsed.get("session_id") if isinstance(parsed, dict) else None
chars = parsed.get("chars") if isinstance(parsed, dict) else None
if isinstance(session_id, int) and chars == "" and session_id in dead_pty_session_ids:
return _WRITE_STDIN_DEAD_SESSION_RESULT_TEMPLATE.format(session_id=session_id)
if isinstance(parsed, dict):
if isinstance(parsed.get("chars"), str):
parsed["chars"] = _decode_chars_escape(parsed["chars"])
if isinstance(chars, str):
parsed["chars"] = _decode_chars_escape(chars)
_apply_shell_output_cap(parsed)
raw_input = json.dumps(parsed)
try:
return await invoke_tool(ctx, raw_input)
result = await invoke_tool(ctx, raw_input)
except ValidationError as exc:
return _format_validation_error(tool.name, exc)
if isinstance(result, str):
match = _PTY_SESSION_NOT_FOUND_RE.search(result)
if match is not None:
dead_pty_session_ids.add(int(match.group("session_id")))
return result
tool.on_invoke_tool = invoke
return tool

View file

@ -13,6 +13,10 @@ from strix.agents import factory
from strix.config import load_settings
_MISSING_PTY_SESSION_ID = 60418
_MISSING_PTY_RESULT = f"write_stdin failed: PTY session not found: {_MISSING_PTY_SESSION_ID}"
def _capturing_exec_tool(captured: dict[str, str]) -> FunctionTool:
async def invoke(_ctx: Any, raw_input: str) -> str:
captured["raw_input"] = raw_input
@ -26,6 +30,19 @@ def _capturing_exec_tool(captured: dict[str, str]) -> FunctionTool:
)
def _capturing_write_stdin_tool(results: list[str], captured: list[str]) -> FunctionTool:
async def invoke(_ctx: Any, raw_input: str) -> str:
captured.append(raw_input)
return results.pop(0)
return FunctionTool(
name="write_stdin",
description="test tool",
params_json_schema={"type": "object", "properties": {}},
on_invoke_tool=invoke,
)
@pytest.mark.asyncio
async def test_wrap_exec_command_defaults_shell_to_bash() -> None:
captured: dict[str, str] = {}
@ -115,3 +132,19 @@ def test_function_tools_are_result_bounded() -> None:
by_name = {t.name: t for t in agent.tools}
assert getattr(by_name["think"], "_strix_bounded", False) is True
@pytest.mark.asyncio
async def test_wrap_write_stdin_blocks_repeat_empty_poll_after_dead_pty() -> None:
captured: list[str] = []
wrapped = factory._wrap_write_stdin(
_capturing_write_stdin_tool([_MISSING_PTY_RESULT], captured)
)
raw_input = json.dumps({"session_id": _MISSING_PTY_SESSION_ID, "chars": ""})
first_result = await wrapped.on_invoke_tool(cast("Any", None), raw_input)
second_result = await wrapped.on_invoke_tool(cast("Any", None), raw_input)
assert first_result == _MISSING_PTY_RESULT
assert "non-retryable" in second_result
assert len(captured) == 1