feat(fase2): thread tools + budget middleware through fresh_workflow onto agents

This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 00:36:06 +02:00
commit 434ecb92c9
2 changed files with 80 additions and 13 deletions

View file

@ -42,10 +42,26 @@ def make_termination(n_turns: int) -> Callable[[Sequence[Message]], bool]:
return terminate
def maker_checker_agents(client_factory: Callable[[str], BaseChatClient]) -> list[Agent]:
"""FRESH proposer + checker agents, each backed by a FRESH client (zero cross-run state)."""
def maker_checker_agents(
client_factory: Callable[[str], BaseChatClient],
*,
tools: Sequence[Any] | None = None,
middleware: Sequence[Any] | None = None,
) -> list[Agent]:
"""FRESH proposer + checker agents, each backed by a FRESH client (zero cross-run state).
``tools`` (the citation-bearing data-source tool, F7) and ``middleware`` (the budget
``ChatMiddleware``, F2) are attached to EVERY agent so the debate reaches the data source
as a tool and is metered/short-circuited via the middleware. Both are constructed by the
orchestrator (``run_project``) and threaded through ``fresh_workflow``."""
return [
Agent(client_factory(role), _INSTRUCTIONS[role], name=role)
Agent(
client_factory(role),
_INSTRUCTIONS[role],
name=role,
tools=tools,
middleware=middleware,
)
for role in _MAKER_CHECKER_ROLES
]
@ -55,12 +71,20 @@ def fresh_workflow(
*,
max_rounds: int = 3,
enable_layer1_hitl: bool = False,
tools: Sequence[Any] | None = None,
middleware: Sequence[Any] | None = None,
) -> Any:
"""Build a FRESH maker-checker GroupChat with FRESH clients per call (B7). Bounded by
``with_max_rounds`` (B4) plus a higher turn-count termination safety net. ``client_factory``
is called once per role, so each run owns its own clients no state survives between runs.
``tools`` + ``middleware`` are attached to each agent (F2/F7; constructed by the
orchestrator). ``output_from=[proposer]`` makes ``WorkflowRunResult.get_outputs()`` surface
the proposer's converged output — without it, ``get_outputs()`` yields only the
orchestrator's "reached max rounds" notice, so the F1 debate->generation dataflow could not
read the debate result (verified against installed 1.9.0).
"""
agents = maker_checker_agents(client_factory)
agents = maker_checker_agents(client_factory, tools=tools, middleware=middleware)
# Agents are built from _MAKER_CHECKER_ROLES in order with name=role, so the role tuple
# IS the (typed, non-None) name list the selector cycles over.
names: list[str] = list(_MAKER_CHECKER_ROLES)
@ -71,15 +95,15 @@ def fresh_workflow(
counter["n"] += 1
return choice
builder = (
GroupChatBuilder(
participants=agents,
selection_func=select,
# Safety net well above the hard cap; with_max_rounds is the binding bound (B4).
termination_condition=make_termination(max_rounds * len(names) + 1),
)
.with_max_rounds(max_rounds)
)
builder = GroupChatBuilder(
participants=agents,
selection_func=select,
# Safety net well above the hard cap; with_max_rounds is the binding bound (B4).
termination_condition=make_termination(max_rounds * len(names) + 1),
# Surface the PROPOSER's converged output so get_outputs() carries the debate
# result (F1); the default surfaces only the orchestrator's termination notice.
output_from=[agents[0]],
).with_max_rounds(max_rounds)
if enable_layer1_hitl:
# Layer-1: in-run synchronous review on the checker (no checkpoint — research 01).
builder = builder.with_request_info(agents=[agents[-1]])