Tutorial: 4 Equal Agents in a Peer Mesh (No Single "Main" Agent)
View SourceThis tutorial shows how to design four equal agents in Synaptic that can call each other as needed through the Agent Directory + Router control plane.
Unlike a strict coordinator/worker model, this pattern treats each agent as a peer:
- any agent can initiate work
- any agent can delegate to another agent
- any agent can resume or inspect tasks (if authorized)
- there is no permanent "boss" agent in the architecture
Topic: Product Launch Intelligence Mesh
We will use a practical topic where peer communication makes sense:
"Product launch intelligence" for a company preparing a new release.
The four agents:
market.scan- scans market signals and competitor movementssocial.listening- summarizes community/social chatter and sentimentpricing.strategy- proposes pricing hypotheses and scenariosrisk.review- challenges assumptions and generates risk checks
Each of these agents can call the others when needed.
Examples:
pricing.strategymay callmarket.scanto fetch competitor movesrisk.reviewmay callsocial.listeningto check backlash signalssocial.listeningmay callmarket.scanto verify whether chatter maps to real market eventsmarket.scanmay callrisk.reviewbefore returning a recommendation summary
Why This Pattern Matters
This design demonstrates the actual flexibility of the new Synaptic control plane:
- not voice-specific
- not coordinator-only
- not a single static workflow graph
- dynamic peer-to-peer delegation via service registry + router
It is closer to a real multi-agent system / agent mesh.
Architecture (Peer Mesh)
All four agents are registered as services and can discover/invoke each other.
market.scan <--> social.listening
| |
v v
pricing.strategy <--> risk.reviewIn practice, communication flows through Synaptic's router (not direct process coupling):
Synaptic.AgentDirectory(service + task references)Synaptic.AgentRouter(authorized calls)Synaptic.WorkloadManager(spawn/reuse workflow-backed instances)
1. Register the 4 Peer Agents
alias MyApp.Agents.{MarketScanWorkflow, SocialListeningWorkflow, PricingStrategyWorkflow, RiskReviewWorkflow}
services = [
{"market.scan", MarketScanWorkflow, ["market.scan"]},
{"social.listening", SocialListeningWorkflow, ["social.listening"]},
{"pricing.strategy", PricingStrategyWorkflow, ["pricing.strategy"]},
{"risk.review", RiskReviewWorkflow, ["risk.review"]}
]
Enum.each(services, fn {service_id, mod, capabilities} ->
{:ok, _} =
Synaptic.register_agent_service(
service_id,
%{
kind: :workflow,
capabilities: capabilities,
visibility: :tenant,
lifecycle_mode: :spawn_on_demand,
provider: {:workflow_module, mod}
}
)
end)2. Shared Calling Contract (Recommended)
To make peer-to-peer collaboration stable, define a shared payload convention.
Each agent call should include:
request_id- stable correlation id for a user requestgoal- what the caller is trying to achievecontext_pack- structured facts gathered so farpurpose- task labeling for recovery/queryingdelegation_reason- why this agent is calling another one
Example payload shape:
%{
request_id: "launch-intel-2026-02-24-001",
goal: "Assess launch viability for Product X",
delegation_reason: "Need competitor pricing baselines",
context_pack: %{
product: "Product X",
region: "US",
launch_window: "Q2",
known_competitors: ["A", "B", "C"]
},
purpose: "launch_intelligence_mesh"
}3. Peer-to-Peer Call Example (Pricing -> Market)
pricing.strategy may need current competitor pricing movement before finalizing a recommendation.
caller_ctx = %{
tenant_id: "default",
user_id: "user_42",
caller_agent_id: "pricing.strategy",
session_id: "launch_mesh_session_1"
}
{:ok, market_result} =
Synaptic.agent_call(
"market.scan",
%{
request_id: "launch-intel-2026-02-24-001",
goal: "Assess launch viability for Product X",
delegation_reason: "Need competitor pricing baselines",
context_pack: %{
product: "Product X",
region: "US",
launch_window: "Q2",
competitors: ["A", "B", "C"]
},
purpose: "launch_intelligence_mesh"
},
caller_ctx: caller_ctx,
aliases: ["last_market_scan"],
timeout: 10_000
)If market.scan suspends, pricing.strategy can resume it through the returned handle.
4. Another Peer Call (Risk -> Social)
risk.review may ask social.listening for sentiment/complaint patterns.
risk_ctx = %{
tenant_id: "default",
user_id: "user_42",
caller_agent_id: "risk.review",
session_id: "launch_mesh_session_1"
}
{:ok, social_result} =
Synaptic.agent_call(
"social.listening",
%{
request_id: "launch-intel-2026-02-24-001",
goal: "Assess launch viability for Product X",
delegation_reason: "Need pre-launch sentiment and complaint themes",
context_pack: %{
product: "Product X",
audience: ["devs", "teams", "ops"],
channels: ["X", "Reddit", "Discord"]
},
purpose: "launch_intelligence_mesh"
},
caller_ctx: risk_ctx,
aliases: ["last_social_listening"],
timeout: 10_000
)5. No Single Main Agent: How State Still Stays Coherent
When agents are peers, the main challenge is not calling each other. The challenge is maintaining coherence.
Use these conventions:
- Shared
request_idacross all related tasks - Shared
purpose(e.g."launch_intelligence_mesh") - Agent-specific aliases (e.g.
last_market_scan,last_risk_review) - Structured
context_packpayloads instead of raw text only
This lets any peer recover the chain.
6. Recovery Example (Any Peer Can Recover Another Task)
Suppose pricing.strategy crashes and restarts. It can recover the latest market.scan task for the same user/request.
{:ok, task_ref} =
Synaptic.AgentDirectory.resolve_task_reference(%{
user_id: "user_42",
capability: "market.scan",
alias: "last_market_scan",
require_active: true
})
{:ok, status_result} =
Synaptic.agent_call(
%{task_ref_id: task_ref.task_ref_id},
%{action: :inspect},
caller_ctx: %{
tenant_id: "default",
user_id: "user_42",
caller_agent_id: "pricing.strategy"
}
)7. Example Peer Workflow Behaviors (Suggested)
These are not exact implementations, but practical shapes.
market.scan
Responsibilities:
- identify competitor actions
- summarize launch timing/messaging moves
- return evidence and confidence
Can delegate to:
risk.review("What assumptions are weak?")social.listening("Is this signal reflected in user chatter?")
social.listening
Responsibilities:
- summarize sentiment themes
- identify recurring complaints/questions
- estimate volatility in discussion
Can delegate to:
market.scan("Did a competitor trigger this conversation?")risk.review("Are these signals operationally relevant?")
pricing.strategy
Responsibilities:
- produce pricing options (not final decision)
- compare scenarios
- explain tradeoffs
Can delegate to:
market.scan(competitor baselines)social.listening(price sensitivity chatter)risk.review(risk constraints before presenting options)
risk.review
Responsibilities:
- challenge assumptions
- identify blind spots and downside scenarios
- propose monitoring checks
Can delegate to:
market.scan(verify competitor reality)social.listening(reputation/sentiment risks)pricing.strategy(stress-test plan under constraints)
8. Communication Rules (Highly Recommended)
When everyone is a peer, define simple rules to avoid chaos.
Rule A: Include caller identity in caller_ctx
Always set caller_agent_id correctly.
This improves:
- auditability
- policy decisions
- debugging logs
Rule B: Use capability-specific aliases
Avoid generic aliases like last_task.
Prefer:
last_market_scanlast_social_listeninglast_pricing_strategylast_risk_review
Rule C: Keep delegations narrow
A peer call should request one thing, not an entire orchestration.
Good:
- "Get competitor price changes for last 30 days"
Bad:
- "Do everything and tell me what to do"
Rule D: Return structured outputs
Use maps with stable keys so downstream peers can consume them reliably.
9. Optional Pattern: Temporary Convening Agent (Still Peer-Friendly)
Even in a peer mesh, you may sometimes create a temporary orchestration task for a single request.
That is still consistent with this design as long as:
- it is not a permanent privileged coordinator
- other peers can initiate their own delegations independently
- all interactions still go through the same directory/router/policy layer
Think of it as a temporary convening role, not a hard-coded hierarchy.
10. Observability Checklist for Peer Mesh Testing
When testing peer-to-peer calls, log these values on each hop:
caller_agent_id- target
service_id handle.instance_idhandle.task_ref_idhandle.run_idsnapshot.statusrequest_idpurpose
This makes it much easier to validate peer-call chains and recover tasks after failures.
11. Common Failure Modes (and how this architecture helps)
Failure: Caller agent loses local memory
Fix:
- recover by task reference (
capability + alias + user_id + recency)
Failure: Too many similar tasks for one user
Fix:
- use better aliases and
purpose - include
request_idin payload/context and surface it in task metadata (recommended future improvement)
Failure: Peer loops / circular delegation
Fix (design rule):
- add delegation depth counters in payload
- set max delegation depth
- log
delegation_reason
12. Practical Implementation Recipe
If you want to build this in your app, do it in this order:
- Create 4 workflows (one per peer agent)
- Register them as services in app startup
- Standardize a shared payload contract (
request_id,goal,context_pack,purpose) - Add aliases per capability
- Add policy rules (who may call whom)
- Add recovery logic using
resolve_task_reference/2 - Add logs/telemetry for each peer hop
13. Minimal Manual Test Script (IEx)
Use this to validate the architecture concept quickly.
# 1. Register the 4 services (see section 1)
# 2. Start one peer call (pricing -> market)
caller_ctx = %{tenant_id: "default", user_id: "user_42", caller_agent_id: "pricing.strategy"}
{:ok, market_result} =
Synaptic.agent_call(
"market.scan",
%{
request_id: "launch-intel-001",
goal: "Assess launch viability",
delegation_reason: "Need competitor movement summary",
context_pack: %{product: "Product X", region: "US"},
purpose: "launch_intelligence_mesh"
},
caller_ctx: caller_ctx,
aliases: ["last_market_scan"]
)
# 3. Recover it from another peer identity (risk.review)
{:ok, task_ref} =
Synaptic.AgentDirectory.resolve_task_reference(%{
user_id: "user_42",
capability: "market.scan",
alias: "last_market_scan",
require_active: true
})
{:ok, status_result} =
Synaptic.agent_call(
%{task_ref_id: task_ref.task_ref_id},
%{action: :inspect},
caller_ctx: %{tenant_id: "default", user_id: "user_42", caller_agent_id: "risk.review"}
)If this works, you have verified a peer-mesh style agent system where all four agents are equals and can communicate through Synaptic's shared control plane.