Classify Tasks to Avoid Fragile Orchestrators
Central agents fail in long workflows by accidentally managing state, scheduling, and reviews. Instead, use this Python classifier for each WorkItem based on five signals: expected_minutes >5, needs_state, transfers_responsibility, needs_human_review, not deterministic_result. If >=2 signals, choose AGENT_HANDOFF (A2A-style); else TOOL_CALL (MCP-style).
Example: document_search (1min, stateless, deterministic) → TOOL_CALL. proposal_review (45min, stateful, transfers responsibility, needs review, non-deterministic) → AGENT_HANDOFF.
This forces explicit design: tools stay bounded (input → output, caller owns workflow); collaboration models roles, handoffs, and accountability.
from enum import Enum
class Surface(str, Enum):
TOOL_CALL = "tool_call"
AGENT_HANDOFF = "agent_handoff"
@dataclass(frozen=True)
class WorkItem:
name: str
expected_minutes: int
needs_state: bool
transfers_responsibility: bool
needs_human_review: bool
deterministic_result: bool
def choose_surface(item: WorkItem) -> Surface:
collaboration_signals = sum([
item.expected_minutes > 5,
item.needs_state,
item.transfers_responsibility,
item.needs_human_review,
not item.deterministic_result,
])
if collaboration_signals >= 2:
return Surface.AGENT_HANDOFF
return Surface.TOOL_CALL
Smell to watch: growing central agent prompts tracking stages, owners, timeouts—move to explicit task models with status and history.
Build MCP Tools with Strict Contracts
Expose capabilities via schemas enforcing inputs, permissions, outputs. Caller requests result; tool executes immediately without owning workflow.
Research assistant example: search documents, read calendar → pure tools. No negotiation; just bounded ops.
Minimal Python contract:
@dataclass(frozen=True)
class DocumentSearchRequest:
query: str
max_results: int = 5
@dataclass(frozen=True)
class SearchResult:
title: str
snippet: str
class DocumentIndex:
def search(self, request: DocumentSearchRequest) -> tuple[SearchResult, ...]:
# Validates query, limits results 1-20, returns matches
pass
Test for schema compliance, errors, idempotency: test_document_search_stays_a_tool() asserts result shape and Surface.TOOL_CALL.
Mistake to avoid: forcing coordination into tools—leads to central agent as unintended orchestrator when steps take hours or need retries.
Model A2A Collaboration with Task Lifecycle
For publishing workflows (qualify manuscript → compare books → proposal → track submissions), track ownership, status, history across participants.
Key: handoffs transfer responsibility, not just payloads. Use Task with TaskStatus (OPEN, IN_PROGRESS, WAITING_FOR_REVIEW, DONE), Participant (name, role), history tuple.
Functions: handoff(task, new_owner, reason) updates owner/status/history; request_review(task, reviewer) sets WAITING_FOR_REVIEW.
Example:
class TaskStatus(str, Enum):
OPEN = "open"
IN_PROGRESS = "in_progress"
WAITING_FOR_REVIEW = "waiting_for_review"
DONE = "done"
@dataclass(frozen=True)
class Task:
task_id: str
title: str
owner: Participant
status: TaskStatus
history: tuple[str, ...] = ()
def handoff(task: Task, new_owner: Participant, reason: str) -> Task:
event = f"handoff:{task.owner.name}->{new_owner.name}:{reason}"
return replace(task, owner=new_owner, status=TaskStatus.IN_PROGRESS, history=task.history + (event,))
Test handoffs: test_manuscript_review_becomes_a_handoff() verifies owner change, status, history entry, Surface.AGENT_HANDOFF.
Observability shifts: tools track call/input/duration/result; collaboration tracks owner, blockers, decisions.
Mistake to avoid: agent-ifying simple tools—adds unneeded ceremony to deterministic, single-owner tasks.
Ask These to Nail the Design
- Expose capability or own multi-step role?
- Need result or responsibility transfer?
Tools → MCP: immediate, stateless. Collaboration → A2A: stateful handoffs. Combine both in one system: tools for access, A2A for division of labor.