- 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 первым)
119 lines
5 KiB
Python
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."""
|