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
-1
View File
@@ -6,7 +6,6 @@ This directory contains examples of how to integrate bd with AI agents and workf
### Agent Integration
- **[python-agent/](python-agent/)** - Simple Python agent that discovers ready work and completes tasks
- **[AGENT_MAIL_EXAMPLE.md](python-agent/AGENT_MAIL_EXAMPLE.md)** - Multi-agent coordination with Agent Mail
- **[bash-agent/](bash-agent/)** - Bash script showing the full agent workflow
- **[startup-hooks/](startup-hooks/)** - Session startup scripts for automatic bd upgrade detection
- **[claude-desktop-mcp/](claude-desktop-mcp/)** - MCP server for Claude Desktop integration
-26
View File
@@ -10,7 +10,6 @@ A bash script demonstrating how an AI agent can use bd to manage tasks autonomou
- Random issue creation to simulate real agent behavior
- Dependency linking with `discovered-from`
- Statistics display
- **Optional Agent Mail integration** for multi-agent coordination
## Prerequisites
@@ -21,8 +20,6 @@ A bash script demonstrating how an AI agent can use bd to manage tasks autonomou
## Usage
### Basic (Single Agent)
```bash
# Make executable
chmod +x agent.sh
@@ -34,29 +31,6 @@ chmod +x agent.sh
./agent.sh 20
```
### Multi-Agent Mode (with Agent Mail)
```bash
# Terminal 1: Start Agent Mail server
cd ~/src/mcp_agent_mail
source .venv/bin/activate
python -m mcp_agent_mail.cli serve-http
# Terminal 2: Run first agent
export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765
export BEADS_AGENT_NAME=bash-agent-1
export BEADS_PROJECT_ID=my-project
./agent.sh 10
# Terminal 3: Run second agent (simultaneously)
export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765
export BEADS_AGENT_NAME=bash-agent-2
export BEADS_PROJECT_ID=my-project
./agent.sh 10
```
Agents will coordinate via Agent Mail to prevent claiming the same issues.
## What It Does
The agent runs in a loop:
+6 -104
View File
@@ -4,15 +4,10 @@
#
# This demonstrates the full lifecycle of an agent managing tasks:
# - Find ready work
# - Claim and execute (with optional Agent Mail reservation)
# - Claim and execute
# - Discover new issues
# - Link discoveries
# - Complete and move on
#
# Optional Agent Mail integration:
# export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765
# export BEADS_AGENT_NAME=bash-agent-1
# export BEADS_PROJECT_ID=my-project
set -euo pipefail
@@ -39,87 +34,7 @@ log_error() {
echo -e "${RED}${NC} $1"
}
# Agent Mail integration (optional, graceful degradation)
AGENT_MAIL_ENABLED=false
AGENT_MAIL_URL="${BEADS_AGENT_MAIL_URL:-}"
AGENT_NAME="${BEADS_AGENT_NAME:-bash-agent-$$}"
PROJECT_ID="${BEADS_PROJECT_ID:-default}"
# Check Agent Mail health
check_agent_mail() {
if [[ -z "$AGENT_MAIL_URL" ]]; then
return 1
fi
if ! command -v curl &> /dev/null; then
log_warning "curl not available, Agent Mail disabled"
return 1
fi
if curl -s -f "$AGENT_MAIL_URL/health" > /dev/null 2>&1; then
return 0
fi
return 1
}
# Reserve an issue (prevents other agents from claiming)
reserve_issue() {
local issue_id="$1"
if ! $AGENT_MAIL_ENABLED; then
return 0 # Skip if disabled
fi
local response=$(curl -s -X POST "$AGENT_MAIL_URL/api/reserve" \
-H "Content-Type: application/json" \
-d "{\"file_path\": \"$issue_id\", \"agent_name\": \"$AGENT_NAME\", \"project_id\": \"$PROJECT_ID\", \"ttl_seconds\": 300}" \
2>/dev/null || echo "")
if [[ -z "$response" ]] || echo "$response" | grep -q "error"; then
log_warning "Failed to reserve $issue_id (may be claimed by another agent)"
return 1
fi
return 0
}
# Release an issue reservation
release_issue() {
local issue_id="$1"
if ! $AGENT_MAIL_ENABLED; then
return 0 # Skip if disabled
fi
curl -s -X POST "$AGENT_MAIL_URL/api/release" \
-H "Content-Type: application/json" \
-d "{\"file_path\": \"$issue_id\", \"agent_name\": \"$AGENT_NAME\", \"project_id\": \"$PROJECT_ID\"}" \
> /dev/null 2>&1 || true
}
# Send notification to other agents
notify() {
local event_type="$1"
local issue_id="$2"
local message="$3"
if ! $AGENT_MAIL_ENABLED; then
return 0 # Skip if disabled
fi
curl -s -X POST "$AGENT_MAIL_URL/api/notify" \
-H "Content-Type: application/json" \
-d "{\"event_type\": \"$event_type\", \"from_agent\": \"$AGENT_NAME\", \"project_id\": \"$PROJECT_ID\", \"payload\": {\"issue_id\": \"$issue_id\", \"message\": \"$message\"}}" \
> /dev/null 2>&1 || true
}
# Initialize Agent Mail
if check_agent_mail; then
AGENT_MAIL_ENABLED=true
log_success "Agent Mail enabled (agent: $AGENT_NAME)"
else
log_info "Agent Mail disabled (Beads-only mode)"
fi
# Check if bd is installed
if ! command -v bd &> /dev/null; then
@@ -150,19 +65,10 @@ get_field() {
# Claim a task
claim_task() {
local issue_id="$1"
# Try to reserve first (Agent Mail)
if ! reserve_issue "$issue_id"; then
log_error "Could not reserve $issue_id - skipping"
return 1
fi
log_info "Claiming task: $issue_id"
bd update "$issue_id" --status in_progress --json > /dev/null
# Notify other agents
notify "status_changed" "$issue_id" "Claimed by $AGENT_NAME"
log_success "Task claimed"
return 0
}
@@ -212,11 +118,7 @@ complete_task() {
log_info "Completing task: $issue_id"
bd close "$issue_id" --reason "$reason" --json > /dev/null
# Notify completion and release reservation
notify "issue_completed" "$issue_id" "$reason"
release_issue "$issue_id"
log_success "Task completed: $issue_id"
}
@@ -259,9 +161,9 @@ run_agent() {
issue_id=$(get_field "$ready_work" "id")
# Claim it (may fail if another agent reserved it)
# Claim it
if ! claim_task "$issue_id"; then
log_warning "Skipping already-claimed task, trying next iteration"
log_warning "Failed to claim task, trying next iteration"
continue
fi
-97
View File
@@ -1,97 +0,0 @@
# Go Agent Example
Example Go agent that uses bd with optional Agent Mail coordination for multi-agent workflows.
## Features
- Uses native Go Agent Mail client (`pkg/agentmail`)
- Graceful degradation when Agent Mail unavailable
- Handles reservation conflicts
- Discovers and links new work
- Environment-based configuration
## Usage
### Git-only mode (no Agent Mail)
```bash
cd examples/go-agent
go run main.go --agent-name agent-alpha --max-iterations 5
```
### With Agent Mail coordination
```bash
# Start Agent Mail server (in separate terminal)
cd integrations/agent-mail
python server.py
# Run agent
cd examples/go-agent
go run main.go \
--agent-name agent-alpha \
--project-id my-project \
--agent-mail-url http://127.0.0.1:8765 \
--max-iterations 10
```
### Environment Variables
```bash
export BEADS_AGENT_NAME=agent-alpha
export BEADS_PROJECT_ID=my-project
export BEADS_AGENT_MAIL_URL=http://127.0.0.1:8765
go run main.go
```
## Multi-Agent Demo
Run multiple agents concurrently with Agent Mail:
```bash
# Terminal 1: Start Agent Mail server
cd integrations/agent-mail
python server.py
# Terminal 2: Agent Alpha
cd examples/go-agent
go run main.go --agent-name agent-alpha --agent-mail-url http://127.0.0.1:8765
# Terminal 3: Agent Beta
go run main.go --agent-name agent-beta --agent-mail-url http://127.0.0.1:8765
```
## How It Works
1. **Initialization**: Creates Agent Mail client with health check
2. **Find work**: Queries `bd ready` for unblocked issues
3. **Claim issue**: Reserves via Agent Mail (if enabled) and updates status to `in_progress`
4. **Work simulation**: Processes the issue (sleeps 1s in this example)
5. **Discover work**: 33% chance to create linked issue via `discovered-from` dependency
6. **Complete**: Closes issue and releases Agent Mail reservation
## Collision Handling
When Agent Mail is enabled:
- Issues are reserved before claiming (prevents race conditions)
- Conflicts return immediately (<100ms latency)
- Agents gracefully skip reserved issues
Without Agent Mail:
- Relies on git-based eventual consistency
- Higher latency (2-5s for sync)
- Collision detection via git merge conflicts
## Comparison with Python Agent
The Go implementation mirrors the Python agent (`examples/python-agent/agent_with_mail.py`):
- ✅ Same API surface (ReserveIssue, ReleaseIssue, Notify, CheckInbox)
- ✅ Same graceful degradation behavior
- ✅ Same environment variable configuration
- ✅ Native Go types and idioms (no shell exec for Agent Mail)
Key differences:
- Go uses `pkg/agentmail.Client` instead of `lib/beads_mail_adapter.py`
- Go struct methods vs Python class methods
- Type safety with Go structs
-7
View File
@@ -1,7 +0,0 @@
module github.com/steveyegge/beads/examples/go-agent
go 1.24.0
replace github.com/steveyegge/beads => ../..
require github.com/steveyegge/beads v0.0.0-00010101000000-000000000000
-238
View File
@@ -1,238 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"math/rand"
"os"
"os/exec"
"strings"
"time"
"github.com/steveyegge/beads/pkg/agentmail"
)
type Issue struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Priority int `json:"priority"`
IssueType string `json:"issue_type"`
}
type BeadsAgent struct {
agentName string
projectID string
agentMailURL string
mailClient *agentmail.Client
maxIterations int
}
func NewBeadsAgent(agentName, projectID, agentMailURL string, maxIterations int) *BeadsAgent {
agent := &BeadsAgent{
agentName: agentName,
projectID: projectID,
agentMailURL: agentMailURL,
maxIterations: maxIterations,
}
if agentMailURL != "" {
_ = os.Setenv("BEADS_AGENT_MAIL_URL", agentMailURL)
_ = os.Setenv("BEADS_AGENT_NAME", agentName)
_ = os.Setenv("BEADS_PROJECT_ID", projectID)
agent.mailClient = agentmail.NewClient(
agentmail.WithURL(agentMailURL),
agentmail.WithAgentName(agentName),
)
if agent.mailClient.Enabled {
fmt.Printf("✨ Agent Mail enabled: %s @ %s\n", agentName, agentMailURL)
} else {
fmt.Printf("📝 Git-only mode: %s (Agent Mail unavailable)\n", agentName)
}
} else {
fmt.Printf("📝 Git-only mode: %s\n", agentName)
}
return agent
}
func (a *BeadsAgent) runBD(args ...string) ([]byte, error) {
args = append(args, "--json")
cmd := exec.Command("bd", args...)
output, err := cmd.CombinedOutput()
if err != nil {
if strings.Contains(string(output), "already reserved") || strings.Contains(string(output), "reservation conflict") {
return output, fmt.Errorf("reservation_conflict")
}
return output, err
}
return output, nil
}
func (a *BeadsAgent) getReadyWork() ([]Issue, error) {
output, err := a.runBD("ready")
if err != nil {
return nil, err
}
var issues []Issue
if err := json.Unmarshal(output, &issues); err != nil {
return nil, fmt.Errorf("failed to parse ready work: %w", err)
}
return issues, nil
}
func (a *BeadsAgent) claimIssue(issueID string) bool {
fmt.Printf("📋 Claiming issue: %s\n", issueID)
if a.mailClient != nil && a.mailClient.Enabled {
if !a.mailClient.ReserveIssue(issueID, 3600) {
fmt.Printf(" ⚠️ Issue %s already claimed by another agent\n", issueID)
return false
}
}
_, err := a.runBD("update", issueID, "--status", "in_progress")
if err != nil {
if err.Error() == "reservation_conflict" {
fmt.Printf(" ⚠️ Issue %s already claimed by another agent\n", issueID)
return false
}
fmt.Printf(" ❌ Failed to claim %s: %v\n", issueID, err)
return false
}
fmt.Printf(" ✅ Successfully claimed %s\n", issueID)
return true
}
func (a *BeadsAgent) completeIssue(issueID, reason string) bool {
fmt.Printf("✅ Completing issue: %s\n", issueID)
_, err := a.runBD("close", issueID, "--reason", reason)
if err != nil {
fmt.Printf(" ❌ Failed to complete %s: %v\n", issueID, err)
return false
}
if a.mailClient != nil && a.mailClient.Enabled {
a.mailClient.ReleaseIssue(issueID)
a.mailClient.Notify("issue_completed", map[string]interface{}{
"issue_id": issueID,
"agent": a.agentName,
})
}
fmt.Printf(" ✅ Issue %s completed\n", issueID)
return true
}
func (a *BeadsAgent) createDiscoveredIssue(title, parentID string, priority int, issueType string) string {
fmt.Printf("💡 Creating discovered issue: %s\n", title)
output, err := a.runBD("create", title,
"-t", issueType,
"-p", fmt.Sprintf("%d", priority),
"--deps", fmt.Sprintf("discovered-from:%s", parentID),
)
if err != nil {
fmt.Printf(" ❌ Failed to create issue: %v\n", err)
return ""
}
var result struct {
ID string `json:"id"`
}
if err := json.Unmarshal(output, &result); err != nil {
fmt.Printf(" ❌ Failed to parse created issue: %v\n", err)
return ""
}
fmt.Printf(" ✅ Created %s\n", result.ID)
return result.ID
}
func (a *BeadsAgent) simulateWork(issue Issue) {
fmt.Printf("🤖 Working on: %s (%s)\n", issue.Title, issue.ID)
fmt.Printf(" Priority: %d, Type: %s\n", issue.Priority, issue.IssueType)
time.Sleep(1 * time.Second)
}
func (a *BeadsAgent) run() {
fmt.Printf("\n🚀 Agent '%s' starting...\n", a.agentName)
fmt.Printf(" Project: %s\n", a.projectID)
if a.agentMailURL != "" {
fmt.Printf(" Agent Mail: Enabled\n\n")
} else {
fmt.Printf(" Agent Mail: Disabled (git-only mode)\n\n")
}
for iteration := 1; iteration <= a.maxIterations; iteration++ {
fmt.Println(strings.Repeat("=", 60))
fmt.Printf("Iteration %d/%d\n", iteration, a.maxIterations)
fmt.Println(strings.Repeat("=", 60))
readyIssues, err := a.getReadyWork()
if err != nil {
fmt.Printf("❌ Failed to get ready work: %v\n", err)
continue
}
if len(readyIssues) == 0 {
fmt.Println("📭 No ready work available. Stopping.")
break
}
claimed := false
for _, issue := range readyIssues {
if a.claimIssue(issue.ID) {
claimed = true
a.simulateWork(issue)
// 33% chance to discover new work
if rand.Float32() < 0.33 {
discoveredTitle := fmt.Sprintf("Follow-up work for %s", issue.Title)
newID := a.createDiscoveredIssue(discoveredTitle, issue.ID, issue.Priority, "task")
if newID != "" {
fmt.Printf("🔗 Linked %s ← discovered-from ← %s\n", newID, issue.ID)
}
}
a.completeIssue(issue.ID, "Implemented successfully")
break
}
}
if !claimed {
fmt.Println("⚠️ All ready issues are reserved by other agents. Waiting...")
time.Sleep(2 * time.Second)
}
fmt.Println()
}
fmt.Printf("🏁 Agent '%s' finished\n", a.agentName)
}
func main() {
agentName := flag.String("agent-name", getEnv("BEADS_AGENT_NAME", fmt.Sprintf("agent-%d", os.Getpid())), "Unique agent identifier")
projectID := flag.String("project-id", getEnv("BEADS_PROJECT_ID", "default"), "Project namespace for Agent Mail")
agentMailURL := flag.String("agent-mail-url", os.Getenv("BEADS_AGENT_MAIL_URL"), "Agent Mail server URL")
maxIterations := flag.Int("max-iterations", 10, "Maximum number of issues to process")
flag.Parse()
agent := NewBeadsAgent(*agentName, *projectID, *agentMailURL, *maxIterations)
agent.run()
}
func getEnv(key, defaultValue string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultValue
}
-418
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)
-1
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
+3 -62
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:
-288
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()