refactor: Remove legacy MCP Agent Mail integration (bd-6gd)

Remove the external MCP Agent Mail server integration that required
running a separate HTTP server and configuring environment variables.

The native `bd mail` system (stored as git-synced issues) remains
unchanged and is the recommended approach for inter-agent messaging.

Files removed:
- cmd/bd/message.go - Legacy `bd message` command
- integrations/beads-mcp/src/beads_mcp/mail.py, mail_tools.py
- lib/beads_mail_adapter.py - Python adapter library
- examples/go-agent/ - Agent Mail-focused example
- examples/python-agent/agent_with_mail.py, AGENT_MAIL_EXAMPLE.md
- docs/AGENT_MAIL*.md, docs/adr/002-agent-mail-integration.md
- tests/integration/test_agent_race.py, test_mail_failures.py, etc.
- tests/benchmarks/ - Agent Mail benchmarks

Updated documentation to remove Agent Mail references while keeping
native `bd mail` documentation intact.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-17 23:14:05 -08:00
parent 6920cd5224
commit 83ae110508
38 changed files with 267 additions and 10253 deletions

View File

@@ -1,418 +0,0 @@
# Agent Mail Integration Example
This example demonstrates using bd with **Agent Mail** for multi-agent coordination. It shows how to handle reservation conflicts, graceful degradation, and best practices for real-time collaboration.
## Quick Start
### Prerequisites
1. **Install bd** (0.21.0+):
```bash
go install github.com/steveyegge/beads/cmd/bd@latest
```
2. **Install Agent Mail server**:
```bash
git clone https://github.com/Dicklesworthstone/mcp_agent_mail.git
cd mcp_agent_mail
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```
3. **Initialize beads database**:
```bash
bd init --prefix bd
```
4. **Create some test issues**:
```bash
bd create "Implement login feature" -t feature -p 1
bd create "Add database migrations" -t task -p 1
bd create "Fix bug in auth flow" -t bug -p 0
bd create "Write integration tests" -t task -p 2
```
## Usage Scenarios
### Scenario 1: Single Agent (Git-Only Mode)
No Agent Mail server required. The agent works in traditional git-sync mode:
```bash
# Run agent without Agent Mail
./agent_with_mail.py --agent-name alice --project-id myproject
```
**What happens:**
- Agent finds ready work using `bd ready`
- Claims issues by updating status to `in_progress`
- Completes work and closes issues
- All coordination happens via git (2-5 second latency)
### Scenario 2: Multi-Agent with Agent Mail
Start the Agent Mail server and run multiple agents:
**Terminal 1 - Start Agent Mail server:**
```bash
cd ~/mcp_agent_mail
source .venv/bin/activate
python -m mcp_agent_mail.cli serve-http
# Server runs on http://127.0.0.1:8765
```
**Terminal 2 - First agent:**
```bash
./agent_with_mail.py \
--agent-name alice \
--project-id myproject \
--agent-mail-url http://127.0.0.1:8765 \
--max-iterations 5
```
**Terminal 3 - Second agent:**
```bash
./agent_with_mail.py \
--agent-name bob \
--project-id myproject \
--agent-mail-url http://127.0.0.1:8765 \
--max-iterations 5
```
**Terminal 4 - Monitor (optional):**
```bash
# Watch reservations in real-time
open http://127.0.0.1:8765/mail
```
**What happens:**
- Both agents query for ready work
- First agent to claim an issue gets exclusive reservation
- Second agent gets reservation conflict and tries different work
- Coordination happens in <100ms via Agent Mail
- No duplicate work, no git collisions
### Scenario 3: Environment Variables
Set Agent Mail configuration globally:
```bash
# In your shell profile (~/.bashrc, ~/.zshrc)
export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765
export BEADS_AGENT_NAME=my-agent
export BEADS_PROJECT_ID=my-project
# Now all bd commands use Agent Mail automatically
./agent_with_mail.py --max-iterations 3
```
### Scenario 4: Graceful Degradation
Start an agent with Agent Mail enabled, then stop the server mid-run:
**Terminal 1:**
```bash
# Start server
cd ~/mcp_agent_mail
source .venv/bin/activate
python -m mcp_agent_mail.cli serve-http
```
**Terminal 2:**
```bash
# Start agent
./agent_with_mail.py \
--agent-name charlie \
--agent-mail-url http://127.0.0.1:8765 \
--max-iterations 10
```
**Terminal 1 (after a few iterations):**
```bash
# Stop server (Ctrl+C)
^C
```
**What happens:**
- Agent starts in Agent Mail mode (<100ms latency)
- After server stops, agent automatically falls back to git-only mode
- No errors, no crashes - work continues normally
- Only difference is increased latency (2-5 seconds)
## Example Output
### With Agent Mail (Successful Reservation)
```
✨ Agent Mail enabled: alice @ http://127.0.0.1:8765
🚀 Agent 'alice' starting...
Project: myproject
Agent Mail: Enabled
============================================================
Iteration 1/5
============================================================
📋 Claiming issue: bd-42
✅ Successfully claimed bd-42
🤖 Working on: Implement login feature (bd-42)
Priority: 1, Type: feature
💡 Creating discovered issue: Follow-up work for Implement login feature
✅ Created bd-43
🔗 Linked bd-43 ← discovered-from ← bd-42
✅ Completing issue: bd-42
✅ Issue bd-42 completed
```
### With Agent Mail (Reservation Conflict)
```
✨ Agent Mail enabled: bob @ http://127.0.0.1:8765
🚀 Agent 'bob' starting...
Project: myproject
Agent Mail: Enabled
============================================================
Iteration 1/5
============================================================
📋 Claiming issue: bd-42
⚠️ Reservation conflict: Error: bd-42 already reserved by alice
⚠️ Issue bd-42 already claimed by another agent
📋 Claiming issue: bd-44
✅ Successfully claimed bd-44
🤖 Working on: Write integration tests (bd-44)
Priority: 2, Type: task
```
### Git-Only Mode (No Agent Mail)
```
📝 Git-only mode: charlie
🚀 Agent 'charlie' starting...
Project: myproject
Agent Mail: Disabled (git-only mode)
============================================================
Iteration 1/5
============================================================
📋 Claiming issue: bd-42
✅ Successfully claimed bd-42
🤖 Working on: Implement login feature (bd-42)
Priority: 1, Type: feature
```
## Code Walkthrough
### Key Methods
**`__init__`**: Configure Agent Mail environment variables
```python
if self.agent_mail_url:
os.environ["BEADS_AGENT_MAIL_URL"] = self.agent_mail_url
os.environ["BEADS_AGENT_NAME"] = self.agent_name
os.environ["BEADS_PROJECT_ID"] = self.project_id
```
**`run_bd`**: Execute bd commands with error handling
```python
result = subprocess.run(["bd"] + list(args) + ["--json"], ...)
if "already reserved" in result.stderr:
return {"error": "reservation_conflict"}
```
**`claim_issue`**: Try to claim an issue, handle conflicts
```python
result = self.run_bd("update", issue_id, "--status", "in_progress")
if result["error"] == "reservation_conflict":
return False # Try different issue
```
**`complete_issue`**: Close issue and release reservation
```python
self.run_bd("close", issue_id, "--reason", reason)
# Agent Mail automatically releases reservation
```
### Error Handling
The agent handles three types of failures:
1. **Reservation conflicts** - Expected in multi-agent workflows:
```python
if "reservation_conflict" in result:
print("⚠️ Issue already claimed by another agent")
return False # Try different work
```
2. **Agent Mail unavailable** - Graceful degradation:
```python
# bd automatically falls back to git-only mode
# No special handling needed!
```
3. **Command failures** - General errors:
```python
if returncode != 0:
print(f"❌ Command failed: {stderr}")
return {"error": "command_failed"}
```
## Integration Tips
### Real LLM Agents
To integrate with Claude, GPT-4, or other LLMs:
1. **Replace `simulate_work()` with LLM calls**:
```python
def simulate_work(self, issue: Dict[str, Any]) -> None:
# Call LLM with issue context
prompt = f"Implement: {issue['title']}\nDescription: {issue['description']}"
response = llm_client.generate(prompt)
# Parse response for new issues/bugs
if "TODO" in response or "BUG" in response:
self.create_discovered_issue(
"Found during work",
issue["id"]
)
```
2. **Use issue IDs for conversation context**:
```python
# Track conversation history per issue
conversation_history[issue["id"]].append({
"role": "user",
"content": issue["description"]
})
```
3. **Export state after each iteration**:
```python
# Ensure git state is synced
subprocess.run(["bd", "sync"])
```
### CI/CD Integration
Run agents in GitHub Actions with Agent Mail:
```yaml
jobs:
agent-workflow:
runs-on: ubuntu-latest
services:
agent-mail:
image: ghcr.io/dicklesworthstone/mcp_agent_mail:latest
ports:
- 8765:8765
strategy:
matrix:
agent: [alice, bob, charlie]
steps:
- uses: actions/checkout@v4
- name: Run agent
env:
BEADS_AGENT_MAIL_URL: http://localhost:8765
BEADS_AGENT_NAME: ${{ matrix.agent }}
BEADS_PROJECT_ID: ${{ github.repository }}
run: |
./examples/python-agent/agent_with_mail.py --max-iterations 3
```
### Monitoring & Debugging
**View reservations in real-time:**
```bash
# Web UI
open http://127.0.0.1:8765/mail
# API
curl http://127.0.0.1:8765/api/reservations | jq
```
**Check Agent Mail connectivity:**
```bash
# Health check
curl http://127.0.0.1:8765/health
# Test reservation
curl -X POST http://127.0.0.1:8765/api/reservations \
-H "Content-Type: application/json" \
-d '{"resource_id": "bd-test", "agent_id": "test-agent", "project_id": "test"}'
```
**Debug agent behavior:**
```bash
# Increase verbosity
./agent_with_mail.py --agent-name debug-agent --max-iterations 1
# Check bd Agent Mail status
bd info --json | grep -A5 agent_mail
```
## Common Issues
### "Agent Mail unavailable" warnings
**Cause:** Server not running or wrong URL
**Solution:**
```bash
# Verify server is running
curl http://127.0.0.1:8765/health
# Check environment variables
echo $BEADS_AGENT_MAIL_URL
echo $BEADS_AGENT_NAME
echo $BEADS_PROJECT_ID
```
### Reservations not released after crash
**Cause:** Agent crashed before calling `bd close`
**Solution:**
```bash
# Manual release via API
curl -X DELETE http://127.0.0.1:8765/api/reservations/bd-42
# Or restart server (clears all ephemeral state)
pkill -f mcp_agent_mail
python -m mcp_agent_mail.cli serve-http
```
### Agents don't see each other's reservations
**Cause:** Different `BEADS_PROJECT_ID` values
**Solution:**
```bash
# Ensure all agents use SAME project ID
export BEADS_PROJECT_ID=my-project # All agents must use this!
# Verify
./agent_with_mail.py --agent-name alice &
./agent_with_mail.py --agent-name bob &
# Both should coordinate on same namespace
```
## See Also
- [../../docs/AGENT_MAIL.md](../../docs/AGENT_MAIL.md) - Complete Agent Mail integration guide
- [../../docs/adr/002-agent-mail-integration.md](../../docs/adr/002-agent-mail-integration.md) - Architecture decision record
- [agent.py](agent.py) - Original agent example (git-only mode)
- [Agent Mail Repository](https://github.com/Dicklesworthstone/mcp_agent_mail)
## License
Apache 2.0 (same as beads)

View File

@@ -84,6 +84,5 @@ tree = agent.run_bd("dep", "tree", "bd-1")
## See Also
- [AGENT_MAIL_EXAMPLE.md](AGENT_MAIL_EXAMPLE.md) - Multi-agent coordination with Agent Mail
- [../bash-agent/](../bash-agent/) - Bash version of this example
- [../claude-desktop-mcp/](../claude-desktop-mcp/) - MCP server for Claude Desktop

View File

@@ -8,34 +8,19 @@ This demonstrates how an agent can:
3. Discover new issues during work
4. Link discoveries back to parent tasks
5. Complete work and move on
6. Coordinate with other agents via Agent Mail (optional)
"""
import json
import subprocess
import sys
import os
from pathlib import Path
from typing import Optional
# Add lib directory to path for beads_mail_adapter
lib_path = Path(__file__).parent.parent.parent / "lib"
sys.path.insert(0, str(lib_path))
from beads_mail_adapter import AgentMailAdapter
class BeadsAgent:
"""Simple agent that manages tasks using bd."""
def __init__(self):
self.current_task = None
self.mail = AgentMailAdapter()
if self.mail.enabled:
print(f"📬 Agent Mail enabled (agent: {self.mail.agent_name})")
else:
print("📭 Agent Mail disabled (Beads-only mode)")
def run_bd(self, *args) -> dict:
"""Run bd command and parse JSON output."""
@@ -47,20 +32,7 @@ class BeadsAgent:
return {}
def find_ready_work(self) -> Optional[dict]:
"""Find the highest priority ready work.
Integration Point 1: Check inbox before finding work.
"""
# Check inbox for notifications from other agents
messages = self.mail.check_inbox()
if messages:
print(f"📨 Received {len(messages)} messages:")
for msg in messages:
event_type = msg.get("event_type", "unknown")
payload = msg.get("payload", {})
from_agent = msg.get("from_agent", "unknown")
print(f"{event_type} from {from_agent}: {payload}")
"""Find the highest priority ready work."""
ready = self.run_bd("ready", "--limit", "1")
if isinstance(ready, list) and len(ready) > 0:
@@ -68,26 +40,9 @@ class BeadsAgent:
return None
def claim_task(self, issue_id: str) -> dict:
"""Claim a task by setting status to in_progress.
Integration Point 2: Reserve issue before claiming.
Integration Point 3: Notify other agents of status change.
"""
# Reserve the issue to prevent conflicts with other agents
if not self.mail.reserve_issue(issue_id):
print(f"⚠️ Failed to reserve {issue_id} - already claimed by another agent")
return {}
"""Claim a task by setting status to in_progress."""
print(f"📋 Claiming task: {issue_id}")
result = self.run_bd("update", issue_id, "--status", "in_progress")
# Notify other agents of status change
self.mail.notify("status_changed", {
"issue_id": issue_id,
"status": "in_progress",
"agent": self.mail.agent_name
})
return result
def create_issue(self, title: str, description: str = "",
@@ -108,23 +63,9 @@ class BeadsAgent:
)
def complete_task(self, issue_id: str, reason: str = "Completed"):
"""Mark task as complete.
Integration Point 4: Release reservation and notify completion.
"""
"""Mark task as complete."""
print(f"✅ Completing task: {issue_id} - {reason}")
result = self.run_bd("close", issue_id, "--reason", reason)
# Notify other agents of completion
self.mail.notify("issue_completed", {
"issue_id": issue_id,
"reason": reason,
"agent": self.mail.agent_name
})
# Release the reservation
self.mail.release_issue(issue_id)
return result
def simulate_work(self, issue: dict) -> bool:

View File

@@ -1,288 +0,0 @@
#!/usr/bin/env python3
"""
Beads Agent with Agent Mail Integration Example
Demonstrates how to use bd with optional Agent Mail coordination for multi-agent workflows.
Shows collision handling, graceful degradation, and best practices.
"""
import json
import os
import subprocess
import sys
import time
from typing import Optional, Dict, Any, List
class BeadsAgent:
"""A simple agent that uses bd with optional Agent Mail coordination."""
def __init__(self, agent_name: str, project_id: str, agent_mail_url: Optional[str] = None):
"""
Initialize the agent.
Args:
agent_name: Unique identifier for this agent (e.g., "assistant-alpha")
project_id: Project namespace for Agent Mail
agent_mail_url: Agent Mail server URL (optional, e.g., "http://127.0.0.1:8765")
"""
self.agent_name = agent_name
self.project_id = project_id
self.agent_mail_url = agent_mail_url
# Configure environment for Agent Mail if URL provided
if self.agent_mail_url:
os.environ["BEADS_AGENT_MAIL_URL"] = self.agent_mail_url
os.environ["BEADS_AGENT_NAME"] = self.agent_name
os.environ["BEADS_PROJECT_ID"] = self.project_id
print(f"✨ Agent Mail enabled: {agent_name} @ {agent_mail_url}")
else:
print(f"📝 Git-only mode: {agent_name}")
def run_bd(self, *args) -> Dict[str, Any]:
"""
Run a bd command and return parsed JSON output.
Args:
*args: Command arguments (e.g., "ready", "--json")
Returns:
Parsed JSON output from bd
"""
cmd = ["bd"] + list(args)
if "--json" not in args:
cmd.append("--json")
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False # Don't raise on non-zero exit
)
# Handle reservation conflicts gracefully
if result.returncode != 0:
# Check if it's a reservation conflict
if "already reserved" in result.stderr or "reservation conflict" in result.stderr:
print(f"⚠️ Reservation conflict: {result.stderr.strip()}")
return {"error": "reservation_conflict", "stderr": result.stderr}
else:
print(f"❌ Command failed: {' '.join(cmd)}")
print(f" Error: {result.stderr}")
return {"error": "command_failed", "stderr": result.stderr}
# Parse JSON output
if result.stdout.strip():
return json.loads(result.stdout)
else:
return {}
except json.JSONDecodeError as e:
print(f"❌ Failed to parse JSON from bd: {e}")
print(f" Output: {result.stdout}")
return {"error": "json_parse_failed"}
except Exception as e:
print(f"❌ Failed to run bd: {e}")
return {"error": str(e)}
def get_ready_work(self) -> List[Dict[str, Any]]:
"""Get list of unblocked issues ready to work on."""
result = self.run_bd("ready", "--json")
if "error" in result:
return []
# bd ready returns array of issues
if isinstance(result, list):
return result
else:
return []
def claim_issue(self, issue_id: str) -> bool:
"""
Claim an issue by setting status to in_progress.
Returns:
True if successful, False if reservation conflict or error
"""
print(f"📋 Claiming issue: {issue_id}")
result = self.run_bd("update", issue_id, "--status", "in_progress")
if "error" in result:
if result["error"] == "reservation_conflict":
print(f" ⚠️ Issue {issue_id} already claimed by another agent")
return False
else:
print(f" ❌ Failed to claim {issue_id}")
return False
print(f" ✅ Successfully claimed {issue_id}")
return True
def complete_issue(self, issue_id: str, reason: str = "Completed") -> bool:
"""
Complete an issue and release reservation.
Returns:
True if successful, False otherwise
"""
print(f"✅ Completing issue: {issue_id}")
result = self.run_bd("close", issue_id, "--reason", reason)
if "error" in result:
print(f" ❌ Failed to complete {issue_id}")
return False
print(f" ✅ Issue {issue_id} completed")
return True
def create_discovered_issue(
self,
title: str,
parent_id: str,
priority: int = 2,
issue_type: str = "task"
) -> Optional[str]:
"""
Create an issue discovered during work on another issue.
Args:
title: Issue title
parent_id: ID of the issue this was discovered from
priority: Priority level (0-4)
issue_type: Issue type (bug, feature, task, etc.)
Returns:
New issue ID if successful, None otherwise
"""
print(f"💡 Creating discovered issue: {title}")
result = self.run_bd(
"create",
title,
"-t", issue_type,
"-p", str(priority),
"--deps", f"discovered-from:{parent_id}"
)
if "error" in result or "id" not in result:
print(f" ❌ Failed to create issue")
return None
new_id = result["id"]
print(f" ✅ Created {new_id}")
return new_id
def simulate_work(self, issue: Dict[str, Any]) -> None:
"""Simulate working on an issue."""
print(f"🤖 Working on: {issue['title']} ({issue['id']})")
print(f" Priority: {issue['priority']}, Type: {issue['issue_type']}")
time.sleep(1) # Simulate work
def run(self, max_iterations: int = 10) -> None:
"""
Main agent loop: find work, claim it, complete it.
Args:
max_iterations: Maximum number of issues to process
"""
print(f"\n🚀 Agent '{self.agent_name}' starting...")
print(f" Project: {self.project_id}")
print(f" Agent Mail: {'Enabled' if self.agent_mail_url else 'Disabled (git-only mode)'}\n")
for iteration in range(1, max_iterations + 1):
print("=" * 60)
print(f"Iteration {iteration}/{max_iterations}")
print("=" * 60)
# Get ready work
ready_issues = self.get_ready_work()
if not ready_issues:
print("📭 No ready work available. Stopping.")
break
# Sort by priority (lower number = higher priority)
ready_issues.sort(key=lambda x: x.get("priority", 99))
# Try to claim the highest priority issue
claimed = False
for issue in ready_issues:
if self.claim_issue(issue["id"]):
claimed = True
# Simulate work
self.simulate_work(issue)
# Randomly discover new work (33% chance)
import random
if random.random() < 0.33:
discovered_title = f"Follow-up work for {issue['title']}"
new_id = self.create_discovered_issue(
discovered_title,
issue["id"],
priority=issue.get("priority", 2)
)
if new_id:
print(f"🔗 Linked {new_id} ← discovered-from ← {issue['id']}")
# Complete the issue
self.complete_issue(issue["id"], "Implemented successfully")
break
if not claimed:
print("⚠️ All ready issues are reserved by other agents. Waiting...")
time.sleep(2) # Wait before retrying
print()
print(f"🏁 Agent '{self.agent_name}' finished after {iteration} iterations.")
def main():
"""Main entry point."""
# Parse command line arguments
import argparse
parser = argparse.ArgumentParser(
description="Beads agent with optional Agent Mail coordination"
)
parser.add_argument(
"--agent-name",
default=os.getenv("BEADS_AGENT_NAME", f"agent-{os.getpid()}"),
help="Unique agent identifier (default: agent-<pid>)"
)
parser.add_argument(
"--project-id",
default=os.getenv("BEADS_PROJECT_ID", "default"),
help="Project namespace for Agent Mail"
)
parser.add_argument(
"--agent-mail-url",
default=os.getenv("BEADS_AGENT_MAIL_URL"),
help="Agent Mail server URL (optional, e.g., http://127.0.0.1:8765)"
)
parser.add_argument(
"--max-iterations",
type=int,
default=10,
help="Maximum number of issues to process (default: 10)"
)
args = parser.parse_args()
# Create and run agent
agent = BeadsAgent(
agent_name=args.agent_name,
project_id=args.project_id,
agent_mail_url=args.agent_mail_url
)
try:
agent.run(max_iterations=args.max_iterations)
except KeyboardInterrupt:
print("\n\n⚠️ Agent interrupted by user. Exiting...")
sys.exit(0)
if __name__ == "__main__":
main()