simbiot-harness/venv/lib/python3.12/site-packages/strands/_middleware/registry.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

170 lines
6.7 KiB
Python

"""Middleware registry for composing handler chains."""
from __future__ import annotations
import inspect
from collections.abc import AsyncGenerator
from dataclasses import dataclass
from typing import Any
from .types import (
InterruptControlEvent,
MiddlewareHandler,
MiddlewareInputHandler,
MiddlewareInputPhase,
MiddlewareNext,
MiddlewareOutputHandler,
MiddlewareOutputPhase,
MiddlewareResult,
MiddlewareStage,
MiddlewareWrapPhase,
)
_PHASE_ORDER: dict[str, int] = {"input": 0, "output": 1, "wrap": 2}
@dataclass
class _TaggedHandler:
phase: str
handler: MiddlewareHandler
class MiddlewareRegistry:
"""Registry that stores middleware handlers keyed by stage tokens and composes them into chains."""
def __init__(self) -> None:
self._handlers: dict[MiddlewareStage[Any, Any, Any], list[_TaggedHandler]] = {}
def add_middleware(
self,
stage_or_phase: (
MiddlewareStage[Any, Any, Any]
| MiddlewareInputPhase[Any, Any, Any]
| MiddlewareWrapPhase[Any, Any, Any]
| MiddlewareOutputPhase[Any, Any, Any]
),
handler: Any,
) -> None:
"""Register middleware for a stage or phase sub-token."""
if isinstance(stage_or_phase, MiddlewareInputPhase):
self._add_input(stage_or_phase, handler)
elif isinstance(stage_or_phase, MiddlewareOutputPhase):
self._add_output(stage_or_phase, handler)
elif isinstance(stage_or_phase, MiddlewareWrapPhase):
self._add_wrap(stage_or_phase._stage, handler)
else:
self._add_wrap(stage_or_phase, handler)
def _add_wrap(self, stage: MiddlewareStage[Any, Any, Any], handler: MiddlewareHandler) -> None:
handlers = self._handlers.setdefault(stage, [])
handlers.append(_TaggedHandler(phase="wrap", handler=handler))
def _add_input(self, phase: MiddlewareInputPhase[Any, Any, Any], handler: MiddlewareInputHandler) -> None:
stage = phase._stage
async def adapted(context: Any, next_fn: MiddlewareNext) -> AsyncGenerator[Any, None]:
transformed = handler(context)
if inspect.isawaitable(transformed):
transformed = await transformed
async for event in next_fn(transformed):
yield event
handlers = self._handlers.setdefault(stage, [])
handlers.append(_TaggedHandler(phase="input", handler=adapted))
def _add_output(self, phase: MiddlewareOutputPhase[Any, Any, Any], handler: MiddlewareOutputHandler) -> None:
stage = phase._stage
# Output handlers receive and return a MiddlewareResult wrapping the result event
# (the last event in the chain, e.g. ModelStopReason). The wrapper lets handlers
# carry metadata alongside the result without touching the streamed events. The
# registry wraps the result event before calling the handler and unwraps the
# returned wrapper back into the event stream, so the rest of the chain (and the
# event-loop integration) continues to see a plain result event.
#
# Control-flow events (those matching InterruptControlEvent) mean the stage halted
# mid-stream, so it has no result to transform. When one appears, forward it and any
# pending buffered event, then stop tracking a result: the Output handler must not run
# and no buffered non-result event may be mistaken for the result.
async def adapted(context: Any, next_fn: MiddlewareNext) -> AsyncGenerator[Any, None]:
last_event = None
interrupted = False
async for event in next_fn(context):
if isinstance(event, InterruptControlEvent) and event.is_interrupt:
if last_event is not None:
yield last_event
last_event = None
yield event
interrupted = True
continue
if last_event is not None:
yield last_event
last_event = event
if not interrupted and last_event is not None:
transformed = handler(MiddlewareResult(value=last_event))
if inspect.isawaitable(transformed):
transformed = await transformed
if not isinstance(transformed, MiddlewareResult):
raise TypeError(f"Output handler must return a MiddlewareResult, got {type(transformed).__name__}")
yield transformed.value
elif last_event is not None:
# Halted after buffering a trailing non-result event: forward it untransformed.
yield last_event
handlers = self._handlers.setdefault(stage, [])
handlers.append(_TaggedHandler(phase="output", handler=adapted))
def compose(self, stage: MiddlewareStage[Any, Any, Any], terminal: MiddlewareNext) -> MiddlewareNext:
"""Compose all registered handlers for a stage into a single chain.
Returns the terminal directly if no handlers are registered (zero overhead fast path).
"""
tagged = self._handlers.get(stage)
if not tagged:
return terminal
sorted_handlers = sorted(tagged, key=lambda t: _PHASE_ORDER[t.phase])
current: MiddlewareNext = terminal
for i in range(len(sorted_handlers) - 1, -1, -1):
handler = sorted_handlers[i].handler
next_fn = current
def _make_layer(h: MiddlewareHandler, nf: MiddlewareNext) -> MiddlewareNext:
async def layer(ctx: Any) -> AsyncGenerator[Any, None]:
inner_gens: list[AsyncGenerator[Any, None]] = []
def tracking_next(c: Any) -> AsyncGenerator[Any, None]:
gen = nf(c)
inner_gens.append(gen)
return gen
handler_gen = h(ctx, tracking_next)
try:
async for event in handler_gen:
yield event
finally:
await handler_gen.aclose()
for gen in inner_gens:
await gen.aclose()
return layer
current = _make_layer(handler, next_fn)
return current
async def invoke(
self,
stage: MiddlewareStage[Any, Any, Any],
context: Any,
terminal: MiddlewareNext,
) -> AsyncGenerator[Any, None]:
"""Compose and invoke the middleware chain for a stage."""
chain = self.compose(stage, terminal)
gen = chain(context)
try:
async for event in gen:
yield event
finally:
await gen.aclose()