Overview
The ball is Watercooler’s coordination primitive for turn-taking in multi-agent workflows. Like passing a ball in a game, agents signal whose turn it is to respond by passing the ball to the next participant.
Why “ball”? It’s a simple, intuitive metaphor:
Only one agent has the ball at a time
You pass the ball when you’re done
Everyone knows whose turn it is
No complex state machines or coordination protocols needed
Core Concepts
Ball Ownership
The ball field in a thread header indicates who should respond next:
# feature-auth — Thread
Status: OPEN
Ball: Claude (alice) ← Claude's turn to respond
Topic: feature-auth
Ball ownership is advisory, not enforced. Any agent can post to any thread, but the ball signals intent and prevents duplicate work.
Ball Operations
Watercooler provides three primitives for ball coordination:
say Post entry and flip ball to counterpart
ack Post entry and keep ball (no change)
handoff Post entry and pass ball to counterpart
Ball Passing
say - Auto-Flip
The say command posts an entry and automatically flips the ball to your configured counterpart .
# Before: Ball: Claude (alice)
watercooler_say(
topic = "feature-auth" ,
title = "Implementation complete" ,
body = "OAuth flow is ready for review." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# After: Ball: Codex (alice)
Entry Created
New entry appended to thread with your agent identity
Ball Flipped
Ball automatically passes to your configured counterpart
Graph Updated
Both entry and ball change written to graph atomically
Git Synced
Changes committed and pushed to remote (async)
ack - Keep Ball
The ack command posts an entry without changing ball ownership. Use it for:
Quick acknowledgments
Status updates while working
Questions that don’t require a handoff
# Before: Ball: Claude (alice)
watercooler_ack(
topic = "feature-auth" ,
title = "Making progress" ,
body = "OAuth implementation 80 % c omplete. Will finish today." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# After: Ball: Claude (alice) - unchanged
handoff - Explicit Pass
The handoff command explicitly passes the ball to your counterpart with an optional note.
# Before: Ball: Claude (alice)
watercooler_handoff(
topic = "feature-auth" ,
note = "Ready for QA testing" ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# After: Ball: Codex (alice)
handoff is semantically equivalent to say but signals explicit coordination. Use it when the handoff itself is the important action.
Counterpart Configuration
Counterparts define who receives the ball during auto-flip. Configure them in your agent registry:
Simple Two-Agent Setup
{
"canonical" : {
"claude" : "Claude" ,
"codex" : "Codex"
},
"counterpart" : {
"Claude" : "Codex" ,
"Codex" : "Claude"
},
"default_ball" : "Team"
}
In this setup:
When Claude posts with say, ball goes to Codex
When Codex posts with say, ball goes to Claude
Multi-Agent Chains
For multi-agent workflows, define a chain:
{
"canonical" : {
"planner" : "Planner" ,
"implementer" : "Implementer" ,
"reviewer" : "Reviewer"
},
"counterpart" : {
"Planner" : "Implementer" ,
"Implementer" : "Reviewer" ,
"Reviewer" : "Planner"
},
"default_ball" : "Planner"
}
Workflow: Planner → Implementer → Reviewer → Planner
Multi-agent chains can lead to collision if multiple agents respond simultaneously. This is rare but expected. Future versions may add optimistic locking.
Implementation Details
Ball mechanics are implemented in src/watercooler/commands_graph.py and src/watercooler/agents.py.
Ball Flip Logic
From commands_graph.py:say():
def say (
topic : str ,
* ,
threads_dir : Path,
agent : str | None = None ,
role : str | None = None ,
title : str ,
body : str ,
ball : str | None = None ,
registry : dict | None = None ,
user_tag : str | None = None ,
** kwargs
) -> Path:
"""Post entry and flip ball to counterpart."""
# Default agent and role
default_agent, default_role = _default_agent_and_role(registry)
final_agent = agent if agent is not None else default_agent
final_role = role if role is not None else default_role
# Auto-flip ball if not explicitly provided
final_ball = ball
if final_ball is None :
canonical = _canonical_agent(final_agent, registry, user_tag = user_tag)
final_ball = _counterpart_of(canonical, registry)
return append_entry(
topic,
threads_dir = threads_dir,
agent = final_agent,
role = final_role,
title = title,
body = body,
ball = final_ball, # Auto-flipped
registry = registry,
user_tag = user_tag,
** kwargs
)
Counterpart Resolution
From agents.py:_counterpart_of():
def _counterpart_of ( agent : str , registry : dict | None = None ) -> str :
"""Return the counterpart agent after resolving chains.
Uses registry["counterpart"] mapping to follow chains.
Preserves user tags in the form " (tag)".
"""
counterpart_map = (registry or {}).get(
"counterpart" ,
{ "Codex" : "Claude" , "Claude" : "Codex" }
)
# Separate agent base and user tag
canon_with_tag = _canonical_agent(agent, registry)
base, tag = _split_agent_and_tag(canon_with_tag)
# Look up counterpart
current = counterpart_map.get(base, base)
# Reattach user tag
return f " { current } ( { tag } )" if tag else current
Ball States
Common Patterns
Ping-Pong Collaboration
Scenario: Claude and Codex alternate on implementation.
# Claude: Initial plan
watercooler_say(
topic = "feature-auth" ,
title = "Design proposal" ,
body = "Here's the OAuth flow..." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:planner"
)
# Ball → Codex
# Codex: Implementation
watercooler_say(
topic = "feature-auth" ,
title = "Implementation complete" ,
body = "Implemented the flow in src/auth/oauth.py" ,
code_path = "." ,
agent_func = "Codex:gpt-4:implementer"
)
# Ball → Claude
# Claude: Review and handoff
watercooler_say(
topic = "feature-auth" ,
title = "Looks good, ready for testing" ,
body = "Code review passed. Ready for QA." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:critic"
)
# Ball → Codex
Work-in-Progress Updates
Scenario: Claude provides progress updates without handing off.
# Claude starts work
watercooler_say(
topic = "feature-auth" ,
title = "Starting OAuth implementation" ,
body = "Beginning work on OAuth flow." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# Ball → Codex
# Claude keeps ball for updates
watercooler_ack(
topic = "feature-auth" ,
title = "Progress update" ,
body = "OAuth provider configured. Working on token refresh." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# Ball: Claude (unchanged)
watercooler_ack(
topic = "feature-auth" ,
title = "Almost done" ,
body = "Token refresh working. Adding tests." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# Ball: Claude (unchanged)
# Claude hands off when complete
watercooler_say(
topic = "feature-auth" ,
title = "Ready for review" ,
body = "All tests passing. Please review." ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# Ball → Codex
Explicit Re-assignment
Scenario: Manually set ball to specific agent.
# Override auto-flip with explicit ball assignment
watercooler_say(
topic = "feature-auth" ,
title = "Needs security review" ,
body = "OAuth implementation complete. Needs security audit." ,
ball = "SecurityBot (team)" , # Explicit assignment
code_path = "." ,
agent_func = "Claude Code:sonnet-4:implementer"
)
# Ball: SecurityBot (team)
# Or use set_ball for metadata-only change
watercooler_set_ball(
topic = "feature-auth" ,
ball = "SecurityBot (team)" ,
code_path = "." ,
agent_func = "Claude Code:sonnet-4:pm"
)
Best Practices
Use say for Handoffs Default to say for most entries - it signals “I’m done, your turn” # ✅ Clear handoff
watercooler_say( topic = "..." , title = "..." , body = "..." )
# ❌ Don't ack when done
watercooler_ack( topic = "..." , title = "Done" , body = "..." )
Use ack for WIP Use ack only for work-in-progress updates where you’re not ready to hand off # ✅ Progress update while keeping ball
watercooler_ack( topic = "..." , title = "Progress" , body = "80 % d one" )
# ❌ Don't say for every update
watercooler_say( topic = "..." , title = "Progress" , body = "80 % d one" )
Configure Counterparts Set up counterpart mapping in your agent registry for automatic ball routing {
"counterpart" : {
"Claude" : "Codex" ,
"Codex" : "Claude"
}
}
Manual Ball for Special Cases Override auto-flip for special routing (security review, QA, specific expert) watercooler_say(
topic = "feature-auth" ,
title = "Needs specialist review" ,
body = "..." ,
ball = "SecurityExpert (team)" # Manual override
)
Troubleshooting
Ball keeps returning to me
Check your counterpart configuration in the agent registry. If Claude → Claude, you’ll get the ball back immediately. // ❌ Wrong
{ "counterpart" : { "Claude" : "Claude" }}
// ✅ Correct
{ "counterpart" : { "Claude" : "Codex" , "Codex" : "Claude" }}
Verify your agent_func parameter matches your registry canonical names: # Registry has "Claude" (capital C)
agent_func = "Claude Code:sonnet-4:implementer" # ✅
agent_func = "claude:sonnet-4:implementer" # ✅ (normalized)
Multiple agents responding
This is expected in multi-agent scenarios without strict locking. Ball ownership is advisory. Mitigation:
Use ack to signal “I’m working on this”
Check ball before starting work
Accept occasional collisions as non-critical
Ball stuck on departed agent
Manually reassign the ball: watercooler_set_ball(
topic = "feature-auth" ,
ball = "Claude (alice)" ,
code_path = "." ,
agent_func = "Team:human:pm"
)
Next Steps
Agent Identity Configure your agent identity and counterpart mappings
Threads Learn about thread structure and lifecycle
Entries Understand entry types and structure
Architecture Deep dive into technical implementation