simbiot-harness/venv/lib/python3.12/site-packages/strands/experimental/tools/stop/stop.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- 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 первым)
2026-09-29 15:28:50 +00:00

119 lines
5 KiB
Python

"""Tool for gracefully ending the agent loop.
This tool is experimental and subject to change in future revisions without notice.
Provides :func:`make_stop` (a factory for customized stop tools) and :data:`stop`
(the default instance). The tool shims onto the SDK's existing loop-termination
primitive: it sets ``invocation_state["request_state"]["stop_event_loop"] = True``,
which the event loop already checks after tool execution
(see :mod:`strands.event_loop.event_loop`). The tool returns the model-supplied
message when one was given, or a default when the model passed ``None`` or an
empty string; the returned value becomes the tool result the model sees for
its stop request.
The Python event loop halts on this flag with ``stop_reason == "tool_use"`` and
the final ``AgentResult.message`` set to the model's tool-use assistant message
(the batch that included the stop call). The tool's returned string appears in
history as the corresponding ``toolResult``, not as a separate final assistant
turn. This differs from the TypeScript side, whose ``AfterToolsEvent.endTurn``
primitive synthesizes a new assistant message with the stop text and
``stopReason == "endTurn"``. Callers that need the stop text as the last
assistant message on Python should read it from the tool result on the final
message, or append it themselves.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from ....tools.decorator import tool
from ....types.tools import ToolContext
if TYPE_CHECKING:
from ....tools.decorator import DecoratedFunctionTool
_DEFAULT_MESSAGE = "Agent loop stopped."
DEFAULT_MAX_MESSAGE_LENGTH = 4096
"""Default cap on the stop ``message`` length. The cap exists so a runaway model
can't blow the conversation history in one shot; adjust via
``make_stop(max_message_length=...)`` when a longer summary is legitimate."""
DEFAULT_STOP_DESCRIPTION = (
"Gracefully ends the agent loop when the task is complete. "
"Call this tool once with an optional final message when no further work is needed. "
"This is a cooperative stop, not an abort: any tools already requested in this turn still run."
)
def _validate_message(message: str | None, max_length: int) -> str:
"""Validate an optional stop message and return the effective value.
Args:
message: The model-supplied message, or ``None`` for the default.
max_length: The configured upper bound on the returned message length.
Returns:
The validated message string. Empty or ``None`` becomes the default so
the assistant-facing final turn is never blank.
Raises:
ValueError: If ``message`` is not a string or exceeds the length cap.
"""
if message is None or message == "":
return _DEFAULT_MESSAGE
if not isinstance(message, str):
raise ValueError(f"`message` must be a string, got {type(message).__name__}")
if len(message) > max_length:
raise ValueError(f"`message` length exceeds the maximum of {max_length} characters")
return message
def make_stop(
*,
name: str = "stop",
description: str = DEFAULT_STOP_DESCRIPTION,
max_message_length: int = DEFAULT_MAX_MESSAGE_LENGTH,
) -> DecoratedFunctionTool:
"""Create a stop tool that gracefully ends the agent loop.
The tool sets ``invocation_state["request_state"]["stop_event_loop"] = True``,
which the event loop checks after tool execution to end the loop without
invoking the model again.
Args:
name: Tool name. Defaults to ``"stop"``.
description: Tool description shown to the model.
max_message_length: Maximum accepted length for the model-supplied
``message`` argument, in characters. Must be a positive integer.
Defaults to :data:`DEFAULT_MAX_MESSAGE_LENGTH` (4096).
Returns:
A decorated tool that signals the event loop to stop after the current
tool batch completes.
Raises:
ValueError: If ``max_message_length`` is not a positive integer.
"""
if not isinstance(max_message_length, int) or isinstance(max_message_length, bool) or max_message_length <= 0:
raise ValueError(f"max_message_length must be a positive integer, got {max_message_length!r}")
@tool(name=name, description=description, context="tool_context")
async def stop_tool(tool_context: ToolContext, message: str | None = None) -> str:
"""Ends the agent loop gracefully. Call once when the task is complete.
Args:
tool_context: Injected by the framework. Not user-facing.
message: Optional final message describing why the loop is ending.
Capped at the tool's configured ``max_message_length``; longer
values are rejected.
"""
final_message = _validate_message(message, max_message_length)
request_state = tool_context.invocation_state.setdefault("request_state", {})
request_state["stop_event_loop"] = True
return final_message
return stop_tool
stop = make_stop()
"""Default stop tool. Ends the agent loop when called by the model."""