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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user