Grill-Me Collaboration Protocol¤
This guide shows how to use GrillMeProtocol
to have two agents engage in structured critique-and-revision cycles. A
critic rigorously evaluates the respondent's output, the respondent revises
based on feedback, and this loop continues until the output converges or a
maximum number of turns is reached.
Concepts covered
GrillMeProtocol— critic + respondent loopGrillMeConfig— configures max turns, stalemate detectionCollaborationResult— typed output with per-participant historyOperation— withcollaboration=fieldContextProjector— domain-to-template projectionContextStore— domain object registryCapability— tool with rich metadata forrequest_capabilityexecute()— execution entry point with collaboration supportrequest_capability— dynamic tool retrieval viamax_retrieval_rounds
What you'll build¤
A business initiative review where a respondent drafts a proposal and a critic challenges it across multiple rounds:
- Respondent produces an
InitiativeProposal(executive summary, problem, solution, metrics, risks) - Critic evaluates each iteration and provides specific feedback
- The loop terminates at convergence (
stalemate_threshold) ormax_turns - Both agents can use
request_capabilityto dynamically fetch tools
1 Define domain models¤
from pydantic import BaseModel, ConfigDict, Field
class InitiativeSeed(BaseModel):
"""Seed data for a business initiative proposal."""
model_config = ConfigDict(frozen=True)
title: str = Field(description="Initiative title")
industry: str = Field(description="Industry sector")
description: str = Field(description="What the initiative entails")
rationale: str = Field(description="Business justification")
budget: str = Field(description="Estimated budget")
timeline: str = Field(description="Projected timeline")
class InitiativeProposal(BaseModel):
"""The artifact produced and revised by the respondent."""
model_config = ConfigDict(frozen=True)
title: str = Field(description="Initiative title")
executive_summary: str = Field(description="One-paragraph executive summary")
problem_statement: str = Field(description="Problem being solved")
proposed_solution: str = Field(description="Detailed solution approach")
success_metrics: str = Field(description="How success will be measured")
risk_assessment: str = Field(description="Key risks and mitigations")
2 Create a context projector¤
Projects the seed data into template variables that the respondent's task blocks can reference:
from typing import Any
from crewmaster.agents.context.projector import ContextProjector
class InitiativeProjector(ContextProjector):
"""Projects InitiativeSeed into template context."""
def __init__(self, title, industry, description, rationale,
budget, timeline):
self._title = title
self._industry = industry
self._description = description
self._rationale = rationale
self._budget = budget
self._timeline = timeline
@classmethod
def from_domain(cls, data: InitiativeSeed) -> "InitiativeProjector":
return cls(
title=data.title,
industry=data.industry,
description=data.description,
rationale=data.rationale,
budget=data.budget,
timeline=data.timeline,
)
def to_context(self) -> dict[str, Any]:
return {
"initiative_title": self._title,
"initiative_industry": self._industry,
"initiative_description": self._description,
"initiative_rationale": self._rationale,
"initiative_budget": self._budget,
"initiative_timeline": self._timeline,
}
3 Define tools with rich capabilities¤
The grill-me protocol supports request_capability — agents can dynamically
request tools at runtime by describing what they need. Register tools as
Capability objects with descriptive
text to enable this semantic matching:
from crewmaster.tools.models import Capability, ToolSchema
from crewmaster.tools.registry import ToolRegistry
# ── Respondent tools ──────────────────────────────────────────────────
MARKET_RESEARCH = ToolSchema(
name="market_research",
description="Access market research data for industry trends and competitors",
input_schema={
"type": "object",
"properties": {
"industry": {"type": "string"},
"focus_area": {"type": "string"},
},
"required": ["industry"],
},
)
RISK_ANALYSIS = ToolSchema(
name="risk_analysis",
description="Analyze financial, operational, and market risks",
input_schema={
"type": "object",
"properties": {
"initiative_type": {"type": "string"},
"budget_range": {"type": "string"},
},
},
)
BENCHMARKS = ToolSchema(
name="industry_benchmarks",
description="Retrieve ROI and success rate benchmarks",
input_schema={
"type": "object",
"properties": {
"industry": {"type": "string"},
"initiative_category": {"type": "string"},
},
},
)
# ── Critic tool ──────────────────────────────────────────────────────
EVIDENCE_CHECK = ToolSchema(
name="evidence_check",
description="Verify claims and check for supporting evidence",
input_schema={
"type": "object",
"properties": {"claim": {"type": "string"}},
},
)
# ── Registry ─────────────────────────────────────────────────────────
tool_registry = ToolRegistry()
# Register responder tools with rich descriptions for semantic retrieval
tool_registry.register(
"proposal_writing",
Capability(
name="market_research",
description=(
"Market research data for SaaS and enterprise software. "
"Covers industry trends, competitor analysis, customer "
"demographics, and market sizing."
),
tool_schema=MARKET_RESEARCH,
),
)
tool_registry.register(
"proposal_writing",
Capability(
name="risk_analysis",
description=(
"Comprehensive risk analysis for business initiatives. "
"Evaluates financial risk, operational risk, market risk, "
"and technical risk with mitigation recommendations."
),
tool_schema=RISK_ANALYSIS,
),
)
tool_registry.register(
"proposal_writing",
Capability(
name="industry_benchmarks",
description=(
"Industry benchmark database with ROI ranges, success "
"rates, and adoption curves for technology initiatives."
),
tool_schema=BENCHMARKS,
),
)
# Register critic tools in their scope
tool_registry.register("critique", EVIDENCE_CHECK)
4 Configure agents¤
Critic — evaluates proposals rigorously. Respondent — drafts and revises.
The respondent declares default_context=[InitiativeProjector] so the seed
data is projected into its prompts:
from crewmaster.agents.agent import AgentConfig
critic = AgentConfig(
name="critic",
identity_blocks=[
"""You are an expert Critic specializing in rigorous review
of business initiatives.
Your role is to critically examine proposals and identify weaknesses,
risks, blind spots, and areas for improvement. Be constructive but
unflinching.
Structure your critiques: strengths, weaknesses, risks, and specific
recommendations for improvement.""",
],
default_tool_scope="critique",
)
respondent = AgentConfig(
name="respondent",
identity_blocks=[
"""You are a business strategist who drafts and refines
initiative proposals.
You produce structured InitiativeProposal outputs with: executive
summary, problem statement, proposed solution, success metrics,
and risk assessment.
When receiving critique, address every point raised. If the critic
identifies a gap, fill it with data or reasoning. If you need
additional data, call request_capability to access research tools.""",
],
default_tool_scope="proposal_writing",
default_context=[InitiativeProjector],
)
5 Build the operation with GrillMeProtocol¤
The Operation uses the
collaboration= field to attach the protocol. Set
max_retrieval_rounds to enable
request_capability:
from crewmaster.collaboration.protocol import GrillMeConfig, GrillMeProtocol
from crewmaster.operations.operation import Operation
operation = Operation(
name="initiative_review",
produces=InitiativeProposal,
kind="artifact",
agent=respondent,
task_blocks=["blocks://grill_me/task/respondent"],
consumes=[InitiativeProjector],
collaboration=GrillMeProtocol(
GrillMeConfig(
critic=critic,
respondent=respondent,
max_turns=3, # Up to 3 critique-revision cycles
stalemate_threshold=2, # Stop if output unchanged 2x in a row
)
),
max_retrieval_rounds=2, # Allow 2 request_capability calls
)
6 Set up prompt blocks¤
The respondent's task block uses template variables from the projector:
import tempfile
from pathlib import Path
from crewmaster.agents.prompts.local_disk_store import LocalDiskStore
blocks_dir = Path(tempfile.mkdtemp())
(blocks_dir / "grill_me" / "task" / "respondent.j2").mkdir(parents=True)
(blocks_dir / "grill_me" / "task" / "respondent.j2").write_text("""\
---
id: blocks://grill_me/task/respondent
kind: task
provides: proposal_output
---
Draft a business initiative proposal for:
**Title:** {{ ctx.initiative_title }}
**Industry:** {{ ctx.initiative_industry }}
**Description:** {{ ctx.initiative_description }}
**Rationale:** {{ ctx.initiative_rationale }}
**Budget:** {{ ctx.initiative_budget }}
**Timeline:** {{ ctx.initiative_timeline }}
Include: executive_summary, problem_statement, proposed_solution,
success_metrics, and risk_assessment.
""")
block_store = LocalDiskStore(root=str(blocks_dir))
7 Execute and inspect the result¤
Call execute() as usual. The runtime detects the
collaboration protocol and delegates to the GrillMeProtocol
executor:
import asyncio
from crewmaster import execute
from crewmaster.agents.context.store import ContextStore
async def main():
from crewmaster.execution.drivers.pydantic_ai import PydanticAIDriver
driver = PydanticAIDriver(model_name="openai:gpt-4o-mini")
# Register seed data
seed = InitiativeSeed(
title="AI-Powered Customer Support Platform",
industry="SaaS & Enterprise Software",
description=(
"Deploy an AI-powered customer support platform that "
"handles tier-1 tickets automatically, reducing response "
"time from 4 hours to under 5 minutes for 60% of inquiries."
),
rationale=(
"Support costs have grown 35% YoY while CSAT dropped to "
"3.2/5. Competitors using AI report 40% cost reduction."
),
budget="$1.2M annual (platform + integration + training)",
timeline="Q3 2026: Pilot, Q4 2026: Full rollout",
)
store = ContextStore()
store.register(InitiativeSeed, seed)
result = await execute(
operation=operation,
context_store=store,
default_runtime=driver,
block_store=block_store,
tool_registry=tool_registry,
)
proposal: InitiativeProposal = result
print(f"Title: {proposal.title}")
print(f"Executive Summary: {proposal.executive_summary}")
print(f"Risk Assessment: {proposal.risk_assessment}")
asyncio.run(main())
8 Inspect collaboration internals¤
If you need per-participant history (e.g., to show the critique trail), access
the CollaborationResult.
The execute() function unwraps it by default, but
you can intercept it by running the collaboration executor directly:
from crewmaster.collaboration.protocol import CollaborationResult
# The execute() function handles CollaborationResult internally.
# If you need raw inspection, access it through the plan introspection:
async def run_with_inspection():
from crewmaster.operations.plans import resolve_plan
plan = resolve_plan(
operation=operation,
context_store=store,
block_store=block_store,
default_runtime=driver,
tool_registry=tool_registry,
)
# Plan nodes for collaboration protocols carry the protocol executor
for node in plan.nodes:
if node.collaboration is not None:
collab_result = await node.collaboration.execute(
operation=node.operation,
runtime=driver,
context_store=store,
)
print(f"Protocol: {collab_result.protocol_type}")
print(f"Turns: {collab_result.turn_count}")
for agent_name, outputs in collab_result.participant_outputs.items():
print(f" {agent_name}: {len(outputs)} contributions")
9 Stalemate detection¤
When the respondent produces the same output across consecutive turns, the protocol terminates early. This saves API costs and prevents infinite loops:
GrillMeConfig(
critic=critic,
respondent=respondent,
max_turns=5,
stalemate_threshold=2, # Stop after 2 identical iterations
)
The comparison uses structural equality — for Pydantic models, this means
model_dump() must return identical dictionaries. For string outputs,
exact text comparison is used.
10 Complete script¤
grill_me_review.py
"""Grill-me collaboration: initiative proposal review."""
import asyncio
import tempfile
from pathlib import Path
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from crewmaster import execute
from crewmaster.agents.agent import AgentConfig
from crewmaster.agents.context.projector import ContextProjector
from crewmaster.agents.context.store import ContextStore
from crewmaster.agents.prompts.local_disk_store import LocalDiskStore
from crewmaster.collaboration.protocol import GrillMeConfig, GrillMeProtocol
from crewmaster.execution.drivers.pydantic_ai import PydanticAIDriver
from crewmaster.operations.operation import Operation
from crewmaster.tools.models import Capability, ToolSchema
from crewmaster.tools.registry import ToolRegistry
# ── Domain models ─────────────────────────────────────────────────
class InitiativeSeed(BaseModel):
model_config = ConfigDict(frozen=True)
title: str
industry: str
description: str
rationale: str
budget: str
timeline: str
class InitiativeProposal(BaseModel):
model_config = ConfigDict(frozen=True)
title: str
executive_summary: str
problem_statement: str
proposed_solution: str
success_metrics: str
risk_assessment: str
# ── Context projector ─────────────────────────────────────────────
class InitiativeProjector(ContextProjector):
def __init__(self, title, industry, description, rationale,
budget, timeline):
self._title = title
self._industry = industry
self._description = description
self._rationale = rationale
self._budget = budget
self._timeline = timeline
@classmethod
def from_domain(cls, data: InitiativeSeed) -> "InitiativeProjector":
return cls(
title=data.title, industry=data.industry,
description=data.description, rationale=data.rationale,
budget=data.budget, timeline=data.timeline,
)
def to_context(self) -> dict[str, Any]:
return {
"initiative_title": self._title,
"initiative_industry": self._industry,
"initiative_description": self._description,
"initiative_rationale": self._rationale,
"initiative_budget": self._budget,
"initiative_timeline": self._timeline,
}
# ── Tools ─────────────────────────────────────────────────────────
MARKET_RESEARCH = ToolSchema(
name="market_research",
description="Access market research data",
input_schema={
"type": "object",
"properties": {"industry": {"type": "string"}},
"required": ["industry"],
},
)
EVIDENCE_CHECK = ToolSchema(
name="evidence_check",
description="Verify claims",
input_schema={
"type": "object",
"properties": {"claim": {"type": "string"}},
},
)
tool_registry = ToolRegistry()
tool_registry.register("proposal_writing", MARKET_RESEARCH)
tool_registry.register("critique", EVIDENCE_CHECK)
# ── Agents ────────────────────────────────────────────────────────
critic = AgentConfig(
name="critic",
identity_blocks=[
"""You are an expert Critic. Examine proposals rigorously.
Structure: strengths, weaknesses, risks, recommendations."""
],
default_tool_scope="critique",
)
respondent = AgentConfig(
name="respondent",
identity_blocks=[
"""You are a business strategist. Draft and refine
initiative proposals with: executive summary, problem,
solution, metrics, risk assessment. Address all critique."""
],
default_tool_scope="proposal_writing",
default_context=[InitiativeProjector],
)
# ── Prompt block ─────────────────────────────────────────────────
BLOCKS_DIR = Path(tempfile.mkdtemp())
(BLOCKS_DIR / "grill_me" / "task" / "respondent.j2").mkdir(parents=True)
(BLOCKS_DIR / "grill_me" / "task" / "respondent.j2").write_text("""\
---
id: blocks://grill_me/task/respondent
kind: task
provides: proposal_output
---
Draft a proposal for {{ ctx.initiative_title }}.
Include: executive_summary, problem_statement, proposed_solution,
success_metrics, risk_assessment.
""")
block_store = LocalDiskStore(root=str(BLOCKS_DIR))
# ── Operation ─────────────────────────────────────────────────────
operation = Operation(
name="initiative_review",
produces=InitiativeProposal,
kind="artifact",
agent=respondent,
task_blocks=["blocks://grill_me/task/respondent"],
consumes=[InitiativeProjector],
collaboration=GrillMeProtocol(
GrillMeConfig(
critic=critic,
respondent=respondent,
max_turns=3,
stalemate_threshold=2,
)
),
max_retrieval_rounds=2,
)
# ── Execute ──────────────────────────────────────────────────────
async def main():
driver = PydanticAIDriver(model_name="openai:gpt-4o-mini")
seed = InitiativeSeed(
title="AI-Powered Customer Support Platform",
industry="SaaS & Enterprise Software",
description="Deploy AI-powered support handling tier-1 tickets.",
rationale="Support costs up 35% YoY, CSAT dropped to 3.2/5.",
budget="$1.2M annual",
timeline="Q3-Q4 2026",
)
store = ContextStore()
store.register(InitiativeSeed, seed)
result = await execute(
operation=operation,
context_store=store,
default_runtime=driver,
block_store=block_store,
tool_registry=tool_registry,
)
proposal: InitiativeProposal = result
print(f"Title: {proposal.title}")
print(f"Summary: {proposal.executive_summary}")
print(f"Risk: {proposal.risk_assessment}")
asyncio.run(main())
Next steps¤
- Combine grill-me with the multi-node pipeline — use collaboration on individual pipeline nodes for quality gates.
- Explore other collaboration protocols:
DebateProtocol,BlackboardProtocol, andSupervisorProtocol. - Read the Collaboration API reference for all protocol implementations.
- Learn about
request_capabilityfor dynamic tool discovery during collaboration.