- harness/es.py: append-only event store + проекции (образец — кооп-портал) - harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу - harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы - harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе (глобальный asyncio-рантайм Strands не переживает 2+ create_harness) - cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом) - 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
"""Shell tool for executing commands through a sandbox.
|
|
|
|
Provides :func:`make_shell` (a factory for a stateless, sandbox-routed shell tool)
|
|
and :data:`shell` (the default instance that reads the sandbox from the agent at
|
|
call time). Each call runs in a fresh shell; state such as variables and the
|
|
working directory does not persist across calls.
|
|
|
|
The command runs in whichever shell the sandbox provides -- ``sh`` for the Docker
|
|
and local environments, the remote login shell over SSH -- so it must not rely on
|
|
shell-specific syntax.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ...sandbox.errors import SandboxTimeoutError
|
|
from ...tools.decorator import tool
|
|
from ...types.tools import ToolContext
|
|
from .types import SANDBOX_SHELL_DESCRIPTION, ShellExecutionError, ShellOutput
|
|
|
|
if TYPE_CHECKING:
|
|
from ...sandbox.base import Sandbox
|
|
from ...tools.decorator import DecoratedFunctionTool
|
|
|
|
_DEFAULT_TIMEOUT = 120
|
|
|
|
|
|
def make_shell(
|
|
*,
|
|
sandbox: Sandbox | None = None,
|
|
name: str = "shell",
|
|
description: str = SANDBOX_SHELL_DESCRIPTION,
|
|
) -> DecoratedFunctionTool:
|
|
"""Create a stateless, sandbox-routed shell tool.
|
|
|
|
If a ``sandbox`` is passed, it is bound at creation time. Otherwise the tool
|
|
reads the sandbox from ``tool_context.agent.sandbox`` at call time. Used by
|
|
sandbox implementations in :meth:`~strands.sandbox.base.Sandbox.get_tools`
|
|
and by users who want a customized shell tool.
|
|
|
|
Args:
|
|
sandbox: Sandbox to bind at creation. When ``None``, the agent's
|
|
configured sandbox is used at call time.
|
|
name: Tool name. Defaults to ``"shell"``.
|
|
description: Tool description shown to the model.
|
|
|
|
Returns:
|
|
A decorated tool that executes shell commands through the sandbox.
|
|
"""
|
|
|
|
@tool(name=name, description=description, context="tool_context")
|
|
async def shell_tool(command: str, tool_context: ToolContext, timeout: int = _DEFAULT_TIMEOUT) -> ShellOutput:
|
|
"""Executes a shell command and returns its output and exit code.
|
|
|
|
Args:
|
|
command: The shell command to execute.
|
|
tool_context: Injected by the framework. Not user-facing.
|
|
timeout: Timeout in seconds (default: 120).
|
|
"""
|
|
active = sandbox if sandbox is not None else tool_context.agent.sandbox
|
|
try:
|
|
result = await active.execute(command, timeout=timeout)
|
|
except SandboxTimeoutError as e:
|
|
# The model only sees str(e), so the partial output rides in the message with the success field names.
|
|
partial: ShellOutput = {"output": e.stdout, "error": e.stderr, "exit_code": 124}
|
|
e.args = (f"{e}\n{json.dumps(partial)}",)
|
|
raise
|
|
except Exception as e:
|
|
# ShellExecutionError subclasses RuntimeError, so prior handlers still match.
|
|
raise ShellExecutionError(str(e)) from e
|
|
return {"output": result.stdout, "error": result.stderr, "exit_code": result.exit_code}
|
|
|
|
return shell_tool
|
|
|
|
|
|
shell = make_shell()
|
|
"""Default shell tool. Reads the sandbox from the agent's context at call time."""
|