Ctrl K
ComunidadGuías…

soc_ai/agent/loop_result.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""What the investigation loop hands the rest of the triage pipeline.

The synth-first pipeline (:func:`soc_ai.agent.orchestrator._run_synth_first_pipeline`)
runs an investigation loop between the round-1 synthesis and the gates. The
loop used to leave its outputs in a dozen local variables. They live here now,
in one object, so a strategy other than the one investigator can produce the
same outputs and the code after the loop cannot tell the strategies apart
(docs/dev/specs/2026-10-08-swarm-triage-experiment-design.md, "The seam").

The loop streams its events as it runs. This object holds what is left when
the stream ends.

Dependency-light on purpose: the orchestrator imports it at module load, and
the swarm package imports it beside the orchestrator.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Final

from pydantic_ai.usage import RunUsage

# The strategy values of ``Settings.investigator_strategy``.
SINGLE: Final = "single"
SPEND: Final = "spend"
ENSEMBLE: Final = "ensemble"
ADVOCATES: Final = "advocates"
PLANES: Final = "planes"

STRATEGIES: tuple[str, ...] = (SINGLE, SPEND, ENSEMBLE, ADVOCATES, PLANES)

# The strategies that run the one investigator. ``spend`` and ``ensemble`` are
# the single loop under the limits of their arm's environment.
SINGLE_LOOP_STRATEGIES: frozenset[str] = frozenset({SINGLE, SPEND, ENSEMBLE})

# The strategies that run the swarm package (soc_ai.agent.swarm).
SWARM_STRATEGIES: frozenset[str] = frozenset({ADVOCATES, PLANES})


@dataclass
class LoopResult:
    """The outputs of one investigation loop run, whichever strategy ran it.

    ``output`` is the final output of the run: a ``TriageReport`` when
    ``emits_report`` is True, else an ``InvestigationTranscript``. None when the
    run ended without one: a budget cut, a timeout, a blocked egress, an error.

    ``messages`` is the full message history the gates read. Its tool returns
    are the tool results of the run. ``gathered`` is the history streamed node
    by node, which the partial synthesis replays after a budget cut.

    ``usage`` is the summed model usage of every agent the strategy ran. The
    ``usage`` events were already emitted on the way, one per agent run.

    ``report_path`` names the call that wrote the report: ``investigator`` for
    the one loop, ``judge`` for a swarm. ``rerun`` is ``(agent, message,
    usage_limits)`` for the self-consistency vote, or None when the report
    must not be re-run.

    ``budget_exc`` is set when the run stopped at its budget or a turn timeout.
    ``egress_blocked`` is set when the fail-closed residue sweep blocked the
    first model call. ``stopped`` is True when the run failed outright: its
    error event already landed and the pipeline ends there.

    ``cited_ids`` are the document ids the agents of a swarm cited in their
    briefs. Empty for the one loop, whose citations live on its report.
    ``counters`` are the swarm counters the report carries: the agents, the
    briefs and the citations the judge lost. Empty for the one loop.
    """

    strategy: str
    emits_report: bool
    output: Any = None
    messages: list[Any] = field(default_factory=list)
    gathered: list[Any] = field(default_factory=list)
    usage: RunUsage | None = None
    served_backend: dict[str, Any] | None = None
    report_path: str | None = None
    rerun: tuple[Any, str, Any] | None = None
    budget_exc: BaseException | None = None
    egress_blocked: BaseException | None = None
    stopped: bool = False
    cited_ids: list[str] = field(default_factory=list)
    counters: dict[str, Any] = field(default_factory=dict)


def strategy_of(settings: Any) -> str:
    """The strategy the settings name. Anything unknown is ``single``.

    ``Settings`` validates the value, so an unknown one only comes from a test
    double. The default path must stay the default path for it.
    """
    value = getattr(settings, "investigator_strategy", SINGLE)
    return value if isinstance(value, str) and value in STRATEGIES else SINGLE


__all__ = [
    "ADVOCATES",
    "ENSEMBLE",
    "PLANES",
    "SINGLE",
    "SINGLE_LOOP_STRATEGIES",
    "SPEND",
    "STRATEGIES",
    "SWARM_STRATEGIES",
    "LoopResult",
    "strategy_of",
]