- 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 первым)
94 lines
4.2 KiB
Python
94 lines
4.2 KiB
Python
"""Resolve the harness's ``interventions`` sugar into SDK intervention handlers.
|
|
|
|
The harness's ``interventions`` option accepts a preset name, a natural-language policy, a Cedar policy
|
|
file, an SDK handler instance, or a list of these, and coerces them into the handlers that
|
|
``Agent(interventions=...)`` expects. A raw handler instance passes through untouched, so anything
|
|
the presets don't cover (a Slack ``ask`` callback, a Cedar principal resolver, custom trust rules)
|
|
stays reachable by constructing the SDK handler yourself.
|
|
|
|
The string grammar is deterministic — no content sniffing:
|
|
|
|
- a preset keyword (``off``/``ask``/``smart``) maps to a ``HumanInTheLoop`` config,
|
|
- a path ending in ``.cedar`` loads a ``CedarAuthorization`` policy,
|
|
- any other string is a natural-language risk policy: it becomes the LLM risk classifier's prompt.
|
|
|
|
Inline Cedar policy text is intentionally *not* auto-detected — it is indistinguishable from prose,
|
|
so pass ``CedarAuthorization(policies=...)`` directly for that.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any, Literal
|
|
|
|
from strands.interventions import InterventionHandler
|
|
from strands.vended_interventions.hitl import HumanInTheLoop, LLMClassifierConfig
|
|
|
|
InterventionValue = str | InterventionHandler
|
|
InterventionsOption = InterventionValue | list[InterventionValue] | None
|
|
|
|
# How a preset collects approval: the SDK's ``"stdio"``, a custom callback, or interrupt/resume.
|
|
InterventionAsk = Literal["stdio"] | Callable[..., Any] | None
|
|
|
|
|
|
def _cedar_handler(policies: str) -> InterventionHandler:
|
|
try:
|
|
from strands.vended_interventions.cedar import CedarAuthorization
|
|
except ImportError as e:
|
|
raise ValueError(
|
|
f"Cedar policy {policies!r} needs the optional cedar dependency. "
|
|
"Install it with: pip install 'strands-agents[cedar]'."
|
|
) from e
|
|
return CedarAuthorization(policies=policies)
|
|
|
|
|
|
def _resolve_one(value: InterventionValue, ask: InterventionAsk) -> InterventionHandler | None:
|
|
if isinstance(value, InterventionHandler):
|
|
return value
|
|
if not isinstance(value, str):
|
|
raise ValueError(
|
|
f"Invalid interventions value {value!r}; expected a preset name, a policy string, or a handler instance."
|
|
)
|
|
text = value.strip()
|
|
if text == "off":
|
|
return None
|
|
if text == "ask":
|
|
return HumanInTheLoop(ask=ask)
|
|
if text == "smart":
|
|
return HumanInTheLoop(classifier=True, ask=ask)
|
|
# ``.cedar`` suffix only: the SDK loader treats a non-``.cedar`` string as inline policy, and
|
|
# sniffing file existence would misroute a prose policy that happened to match a filename.
|
|
if text.endswith(".cedar"):
|
|
return _cedar_handler(text)
|
|
# Natural-language policy: the LLM risk classifier judges each call against this prompt and
|
|
# escalates a flagged one for approval — like ``smart``, but with your own rubric.
|
|
return HumanInTheLoop(classifier=LLMClassifierConfig(system_prompt=value), ask=ask)
|
|
|
|
|
|
def _check_handler_collisions(handlers: list[InterventionHandler]) -> None:
|
|
"""Raise if two handlers share a name — the SDK registers at most one per ``name``, so a second
|
|
would silently win or be dropped. Different kinds (a Cedar policy plus one human-approval preset)
|
|
have different names and coexist; two of the same kind collide."""
|
|
seen: set[str] = set()
|
|
for handler in handlers:
|
|
if handler.name in seen:
|
|
raise ValueError(
|
|
f"Two interventions share the handler name {handler.name!r}, but an agent registers at "
|
|
"most one per name. Layer different kinds (e.g. a Cedar policy plus one human-approval "
|
|
"preset), not two of the same kind."
|
|
)
|
|
seen.add(handler.name)
|
|
|
|
|
|
def resolve_interventions(
|
|
value: InterventionsOption,
|
|
*,
|
|
ask: InterventionAsk = None,
|
|
) -> list[InterventionHandler]:
|
|
"""Coerce the ``interventions`` sugar into a list of SDK handlers. ``None``/``"off"`` yield ``[]``."""
|
|
if value is None:
|
|
return []
|
|
values = value if isinstance(value, list) else [value]
|
|
handlers = [handler for v in values if (handler := _resolve_one(v, ask)) is not None]
|
|
_check_handler_collisions(handlers)
|
|
return handlers
|