Skip to main content

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)
1

Entry Created

New entry appended to thread with your agent identity
2

Ball Flipped

Ball automatically passes to your configured counterpart
3

Graph Updated

Both entry and ball change written to graph atomically
4

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% complete. 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% done")

# ❌ Don't say for every update
watercooler_say(topic="...", title="Progress", body="80% done")

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

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)
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
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

Build docs developers (and LLMs) love