simbiot-harness/venv/lib/python3.12/site-packages/strands_harness/plugins/todos.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

112 lines
4.6 KiB
Python

"""Todos: a structured task list the agent maintains for multi-step work.
A single ``todo_write`` tool writes the current list to agent state; an internal
``ContextInjector`` re-surfaces that list to the model before each call so it keeps the plan
in view without the list ever entering durable history.
Re-surfacing goes through ``ContextInjector`` rather than editing ``agent.messages`` from a hook.
The list changes on every ``todo_write`` call, so a durable edit would either stack stale
reminders in history or force a remove-then-reappend dance; injection is ephemeral by
construction (it augments one model call and never persists), which is exactly what a
constantly-changing task list wants. It also happens to be symmetric across both SDKs.
The injector fires on ``everyTurn``, not ``userTurn``: the agent updates the list mid-loop via
``todo_write``, so it should see the refreshed list on the next model call within the same
invocation, not only at the start of the next user turn.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
from strands.plugins import Plugin
from strands.tools.decorator import tool
from strands.types.tools import ToolContext
from strands.vended_plugins.context_injector import ContextInjector, InjectionContext
from typing_extensions import TypedDict
if TYPE_CHECKING:
from strands import Agent
_STATE_KEY = "todos"
_DEFAULT_NAME = "strands:todos"
class TodoItem(TypedDict):
"""One entry in the task list."""
content: str
activeForm: str
status: Literal["pending", "in_progress", "completed"]
def _todo_line(todo: TodoItem) -> str:
status = todo.get("status", "pending")
label = todo["activeForm"] if status == "in_progress" and todo.get("activeForm") else todo.get("content", "")
return f" [{status}] {label}"
def _render_list(todos: list[TodoItem]) -> str:
return "\n".join(_todo_line(t) for t in todos)
class Todos(Plugin):
"""Gives the agent a ``todo_write`` tool and keeps the current list in view.
The tool persists the list to ``agent.state`` under ``state_key``; before each model call
the plugin re-surfaces the list as a ``<system-reminder>`` (ephemeral, never written to
durable history). Sharing one instance across agents is safe: state is per-agent.
Args:
name: Plugin name, for logging and duplicate detection. Defaults to ``"strands:todos"``.
state_key: Agent-state key the list is stored under. Defaults to ``"todos"``.
"""
def __init__(self, *, name: str = _DEFAULT_NAME, state_key: str = _STATE_KEY) -> None:
self._name = name
self._state_key = state_key
super().__init__()
@property
def name(self) -> str:
return self._name
def init_agent(self, agent: Agent) -> None:
"""Register the re-surfacing injector. The ``todo_write`` tool is auto-registered."""
ContextInjector(
self._render_reminder,
name=f"{self._name}:injector",
trigger="everyTurn",
).init_agent(agent)
def _render_reminder(self, context: InjectionContext) -> str | None:
todos = context.state.get(self._state_key)
if not todos:
return None
return (
"<system-reminder>\nYour current todo list:\n"
f"{_render_list(todos)}\n"
"Keep it up to date with todo_write as you work.\n</system-reminder>"
)
@tool(name="todo_write", context="tool_context")
def todo_write(self, todos: list[TodoItem], tool_context: ToolContext) -> str:
"""Create and maintain a structured task list for the current session.
Use proactively for multi-step work (roughly 3+ distinct steps). Keep exactly one item
``in_progress`` at a time, and update status as you go rather than batching. The current
list is re-surfaced to you before each step, so you do not need to restate it. Pass an
empty list to clear it when the work is done.
Args:
todos: The full updated list. Each item has ``content`` (the task, imperative:
"Run tests"), ``activeForm`` (present continuous, shown while in progress:
"Running tests"), and ``status`` (``pending``, ``in_progress``, ``completed``).
tool_context: Injected by the framework. Not user-facing.
"""
if not todos:
tool_context.agent.state.delete(self._state_key)
return "Todo list cleared"
tool_context.agent.state.set(self._state_key, todos)
remaining = sum(1 for t in todos if t.get("status") != "completed")
return f"{remaining} todos remaining\n{_render_list(todos)}"