Skip to main content

Message reviews

Checking a reply before the customer sees it​

A message review is a second check on a reply before your customer reads it. Parlant holds the drafted reply back and compares it with your instructions and approved wording. If the review finds that the draft breaks a rule, or claims something happened when it did not, the customer never sees it. The agent writes it again with a correction. You turn reviews on where a wrong sentence would be costly.

What you'll learn
  • What a message review checks, and the kind of reply it catches
  • Two ways to turn it on: for the whole agent or per rule
  • How approved wording and unsupported action claims are judged
  • What review costs in streaming and retries, and how to change the budget
Talk this page through with your coding agent

Consider a simple case. Your agent looks up a refund, and the tool reports that the refund needs approval. The agent, eager to help, writes back: "Your refund has been issued." Nothing was issued. That is exactly the kind of reply a message review is designed to catch.

The review checks the complete drafted reply against your instructions. If the draft would breach those instructions, or claims an action nobody performed, Parlant discards it and regenerates the step with the reviewer's correction. The customer never sees the rejected draft. The reviewer is itself a model, so it makes such replies much rarer rather than impossible; test the situations that matter with conversations that try to provoke the mistake.

This matters most in conversations governed by hard policy: compliance, legal, brand. In those settings, an off-script sentence is far less acceptable than it would be in an assistant-style agent.

Message review is also the enforcement behind canned responses. A canned response is an approved example message that your agent imitates while writing. When review is active, that approved wording becomes the yardstick the reviewer measures the draft against.

info

A turn can take several steps. Each one reasons and may call tools, and the last one drafts the reply. Message review holds that final draft back and checks it as a whole, not sentence by sentence.

Turning it on​

There are two triggers that request a message review. Parlant runs the review if either of them does.

Agent-wide. Parlant reviews every drafted message:

import parlant.sdk as p

agent = await server.create_agent(
id="banking",
name="Banking Agent",
instructions="Help customers manage their accounts.",
message_review=True,
)

Per rule. A rule is either a policy or an observation that Parlant checks against the conversation. A policy is a rule that says what to do. An observation only names a situation, with no instruction. Either kind can request review only on the turns where it matched, so ordinary small talk stays on the fast path:

await agent.create_policy(
title="Refund limits",
content=(
"Refunds above $500 require a manager approval reference. "
"Never promise such a refund without one."
),
trigger_message_review=True, # review only on turns where this policy matched
)

Combining review with effort​

Effort is the per-turn setting that decides how much model work a turn gets, from MIN to MAX. Your agent sets a baseline, and a rule can raise it for one turn.

Observations combine especially well with effort. When a situation is serious, you can raise the whole turn's effort and review its output in the same declaration:

await agent.create_observation(
condition="the customer disputes a charge or threatens legal action",
trigger_effort=p.Effort.HIGH, # this turn runs with more model work
trigger_message_review=True, # and its reply is checked before delivery
)
Learn more about effort

Adaptive effort covers each of the levels and how rules raise them.

Note that, as with tool reviews, requesting review on a rule upgrades how carefully Parlant matches that rule. And at Effort.MIN, no reviews run at all.

What the reviewer checks​

The reviewer answers one question: does this draft, taken as a whole, follow the turn's instructions?

Parlant holds the drafted message back and reviews it against those instructions. Two more things are always part of the check, whatever your instructions say.

Approved wording. If your agent has canned responses, Parlant renders the ones most similar to the draft into the review prompt, as style and compliance guidance. A draft that diverges from the approved form is sent back.

Unsupported action claims. A message that states or implies an action was performed, such as an order cancelled or a payment processed, must be backed by an actual tool result. Even promising that an action is about to happen, with no tool run behind it, counts as a breach. This holds however compliant the rest of the message is.

Learn more about canned responses

Shaping tone and wording covers how Parlant chooses which responses to show.

What happens after the verdict​

On a pass, Parlant delivers the message. On a breach, the reviewer's adjusted_reasoning replaces the step's reasoning, and the step regenerates.

Note that the regenerating step may not see the rejected draft at all. So the adjusted reasoning has to stand on its own: what the customer asked, which constraint applies, and what a compliant reply must do.

What it costs​

Message review is not free. You trade two things for it: streaming, and retries.

Streaming​

A reviewed message cannot be half-shown. So when review is active, Parlant buffers the reply and reviews it whole rather than streaming it. On those turns, a streaming agent behaves like a block-mode one.

info

Streams and blocks covers the difference between the two delivery modes.

Retries​

Each rejection restarts the step. Parlant does not restart forever, though. The retry budget depends on the turn's effort:

EffortMax restarts
MINReviews do not run
LOW2
MEDIUM3
HIGH5
MAX10

If the budget runs out, Parlant stops and tells the customer it could not complete the request. It will not deliver output that repeatedly failed review.

Changing the defaults​

The defaults live on p.CompassOptimizationPolicy, the component that holds the engine's tuning decisions. To change them, your coding agent subclasses it and rebinds it through configure_container.

configure_container is a callback that the server runs at startup. It receives the container (the object that holds the server's components) and can replace one before anything uses it:

class MyOptimizationPolicy(p.CompassOptimizationPolicy):
def get_max_review_restarts(self, effort: p.Effort) -> int: # called once per turn
if effort >= p.Effort.HIGH:
return 8
return super().get_max_review_restarts(effort)


async def configure_container(container: p.Container) -> p.Container:
container[p.CompassOptimizationPolicy] = MyOptimizationPolicy() # replaces the default
return container


async with p.Server(configure_container=configure_container) as server:
...
Learn more about the container

Overriding engine behavior covers what else you can rebind this way.

Choosing the model and watching verdicts​

Two controls remain: which model does the reviewing, and how you find out what it decided.

Choosing the model​

A model route is a named kind of work in the models mapping you pass to the server, so that each kind of work can run on its own model.

The reviewer uses the review.message route. This means it can run on a different model than generation, varied by effort the same way as review.tools in Tool reviews.

Note that without an explicit reasoning on the route, the reviewer picks its own depth. It considers the turn's effort, and whether high-weight rules matched.

Watching verdicts​

An engine hook is a function you register once at startup, and Parlant calls it at a named point in every turn.

Two hooks bracket the review path: on_reviewing_message and on_message_reviewed. The first fires with the drafted text. The second carries a p.CompassHookPayload.MessageReviewed with message, passed, and adjusted_reasoning, so you can see exactly what the reviewer decided, and why.

You register hooks through initialize_container, which runs at startup just like configure_container above. p.CompassHooks is the component that keeps the hook lists:

async def initialize_container(container: p.Container) -> None:
hooks = container[p.CompassHooks] # the component that holds every hook list

async def on_message_reviewed(
context: p.EngineContext,
payload: p.CompassHookPayload.MessageReviewed,
exc: Exception | None,
) -> p.CompassHookResult:
if not payload.passed:
context.logger.warning(f"Message rejected: {payload.adjusted_reasoning}")
return p.CompassHookResult.CALL_NEXT # let any later hook on this point run too

hooks.on_message_reviewed.append(on_message_reviewed)

Pass it as p.Server(initialize_container=initialize_container).

Learn more about hooks

Hooking into the engine lists the other hook points.

Note that Parlant resolves review requests once per turn, from the agent default plus this turn's matched rules. A rule that matched earlier in the session, but not on this turn, does not trigger review.