ActorCritic
from loomflow.architecture import ActorCriticActorCritic pairs two agents: an actor that writes an answer and a
critic that scores it and lists problems. The actor revises until the
critic’s score clears a threshold or the rounds run out. Use it when
quality matters more than cost and a second opinion catches real
mistakes, such as code review or security checks. Because actor and
critic are separate Agents, you can give them different models so
they don’t share the same blind spots.
For the conceptual page see ActorCritic.
Class signature
class ActorCritic:
name: str = "actor-critic"
def __init__(
self,
*,
actor: Agent,
critic: Agent,
max_rounds: int = 3,
approval_threshold: float = 0.9,
critique_template: str | None = None,
refine_template: str | None = None,
worker_registry: dict[str, Any] | None = None,
role_to_worker_id: dict[str, str] | None = None,
) -> None: ...Constructor parameters
actor
| Type | Agent |
| Default | required |
The Agent that generates and refines outputs. Typically your main
production model.
critic
| Type | Agent |
| Default | required |
The Agent that critiques outputs. Use a different model from the
actor for blind-spot diversity (Anthropic for actor, OpenAI for
critic, or vice versa). The critic must return structured JSON
matching CriticOutput:
class CriticOutput(BaseModel):
score: float # in [0.0, 1.0]
issues: list[str] # specific problems to address
summary: str # one-line assessmentThe default critique_template instructs the critic to emit this
shape; override only if you customize the schema. The JSON is found
even when the critic wraps it in prose or a code fence, and a score
written on a scale is normalised ("score": "7/10" reads as 0.7; see
parse_score). If the
reply has no JSON object, the whole reply becomes the single issue
and the score is read from its text (Score: 9/10), or is 0.0 when
there is none, so the loop keeps going instead of crashing. Actor and
critic spend is charged to the coordinator’s budget.
max_rounds
| Type | int |
| Default | 3 |
Maximum critique → refine rounds (after the initial actor generation).
Each round is one critic call + one refine call. Must be >= 1.
approval_threshold
| Type | float |
| Default | 0.9 |
Critic score (in [0.0, 1.0]) at or above which the loop terminates.
Must be in [0.0, 1.0]. Higher = stricter (more refinement rounds);
lower = lenient.
critique_template
| Type | str | None |
| Default | None (uses built-in default) |
Override the prompt sent to the critic. Must instruct the model to
return strict JSON {"score": 0..1, "issues": [...], "summary": "..."}.
Use {prompt} and {output} for the original task and the output
under review. The template is filled with str.format when it
formats cleanly, so write literal braces as {{ / }}; when it
doesn’t (a single-brace JSON example like the one above), only those
placeholders are replaced and the rest is left as written.
refine_template
| Type | str | None |
| Default | None (uses built-in default) |
Override the prompt sent to the actor for refinement rounds. The
default takes the current output + the critic’s issues as a bulleted
list and asks for a revised output addressing them. Placeholders:
{prompt}, {output}, {critique} (the critic’s summary) and
{issues_bulleted}.
worker_registry / role_to_worker_id
Internal wiring set by Team.actor_critic
when persistent_subagents=True (the default), so the actor and the
critic each keep one conversation per user across rounds and runs.
You normally don’t pass these yourself.
Methods
declared_workers
def declared_workers(self) -> dict[str, Agent]:
return {"actor": self._actor, "critic": self._critic}Both workers exposed for AgentGraph visualization and introspection.
run
- Round 0 (actor). After a budget check,
actor.run(prompt)produces an initial output. If the actor interrupts itself (max_turns, budget), stop. - Critique → refine loop (1..max_rounds):
- Budget check. Stop or warn as needed.
- Critic.
critic.run(critique_prompt)returnsCriticOutput. Ifscore >= approval_threshold, stop. - Refine.
actor.run(refine_prompt)produces a revised output, which replaces the current one.
Events: actor_critic.actor_started / actor_critic.actor_completed
(for the first draft and each revision), actor_critic.critic_started,
actor_critic.critique (score and issues), then
actor_critic.approved or actor_critic.max_rounds_reached.
Cost model
Round 0: 1 actor run. Each subsequent round: 1 critic + 1 actor
refine. For max_rounds=3 worst case: 1 + 3 × 2 = 7 agent runs
(each agent run is itself N model calls under ReAct or whatever
architecture the actor / critic uses).
Typical use case (code generation with security critic): 2–4× a single-actor ReAct run, depending on rounds.
Why a separate SelfRefine?
SelfRefine runs the critic
and refiner as calls inside one agent. ActorCritic is worth its extra
setup only when the actor and critic have different blind spots,
which usually means different models. It needs two Agent
instances; for same-model self-critique, use SelfRefine.
Example
from loomflow import Agent
from loomflow.architecture import ActorCritic
from loomflow.team import Team
actor = Agent(
"Generate the code.",
model="claude-opus-4-7",
)
critic = Agent(
"Find bugs, security issues, and edge cases. Return strict JSON: "
'{"score": 0..1, "issues": [...], "summary": "..."}',
model="gpt-4o", # different model on purpose
)
team = Team.actor_critic(
actor=actor,
critic=critic,
max_rounds=3,
approval_threshold=0.9,
model="claude-opus-4-7", # the coordinator
)
result = await team.run("Write a function that sanitizes user input.")Or via the explicit Architecture form for nesting:
from loomflow import Agent
from loomflow.architecture import ActorCritic
agent = Agent(
"Manage actor + critic.",
model="claude-opus-4-7",
architecture=ActorCritic(
actor=actor,
critic=critic,
max_rounds=3,
approval_threshold=0.9,
),
)Source
loomflow/architecture/actor_critic.py
Different models matter. The whole point is blind-spot diversity. Same-model actor and critic share the same blind spots, so the gain collapses. Use Anthropic for one and OpenAI for the other, or two different sizes (a large actor and a small, fast critic to save cost). Anything but identical.