Phase 8 Production Hardening with complete governance infrastructure: - Vault integration with tiered policies (T0-T4) - DragonflyDB state management - SQLite audit ledger - Pipeline DSL and templates - Promotion/revocation engine - Checkpoint system for session persistence - Health manager and circuit breaker for fault tolerance - GitHub/Slack integrations - Architectural test pipeline with bug watcher, suggestion engine, council review - Multi-agent chaos testing framework Test Results: - Governance tests: 68/68 passing - E2E workflow: 16/16 passing - Phase 2 Vault: 14/14 passing - Integration tests: 27/27 passing Coverage: 57.6% average across 12 phases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
395 lines
12 KiB
Python
395 lines
12 KiB
Python
"""
|
|
MockBlackboard - Simulates shared memory for multi-agent coordination testing.
|
|
|
|
Provides deterministic blackboard operations for testing multi-agent
|
|
scenarios without DragonflyDB.
|
|
"""
|
|
|
|
from typing import Dict, List, Any, Optional, Set
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
import json
|
|
import threading
|
|
|
|
|
|
class BlackboardSection(Enum):
|
|
"""Standard blackboard sections"""
|
|
PROBLEM = "problem"
|
|
SOLUTIONS = "solutions"
|
|
PROGRESS = "progress"
|
|
CONSENSUS = "consensus"
|
|
|
|
|
|
@dataclass
|
|
class BlackboardEntry:
|
|
"""A single entry in a blackboard section"""
|
|
key: str
|
|
value: Any
|
|
author: str
|
|
timestamp: datetime
|
|
version: int = 1
|
|
|
|
|
|
@dataclass
|
|
class ConsensusVote:
|
|
"""A vote in the consensus section"""
|
|
agent: str
|
|
proposal_id: str
|
|
vote: str # ACCEPT, REJECT, ABSTAIN
|
|
reasoning: str
|
|
timestamp: datetime
|
|
|
|
|
|
class MockBlackboard:
|
|
"""
|
|
Mock Blackboard implementation for multi-agent testing.
|
|
|
|
Simulates:
|
|
- Section-based shared memory
|
|
- Read/write operations
|
|
- Consensus voting
|
|
- History tracking
|
|
- Conflict detection
|
|
"""
|
|
|
|
def __init__(self, task_id: str = "test-task"):
|
|
self.task_id = task_id
|
|
self._sections: Dict[str, Dict[str, BlackboardEntry]] = {
|
|
section.value: {} for section in BlackboardSection
|
|
}
|
|
self._votes: List[ConsensusVote] = []
|
|
self._history: List[Dict[str, Any]] = []
|
|
self._watchers: Dict[str, List[callable]] = {}
|
|
self._lock = threading.Lock()
|
|
|
|
def write(self, section: str, key: str, value: Any, author: str) -> bool:
|
|
"""
|
|
Write to a blackboard section.
|
|
|
|
Args:
|
|
section: Section name (problem, solutions, progress, consensus)
|
|
key: Entry key
|
|
value: Entry value
|
|
author: Agent ID writing the entry
|
|
"""
|
|
with self._lock:
|
|
if section not in self._sections:
|
|
self._sections[section] = {}
|
|
|
|
existing = self._sections[section].get(key)
|
|
version = existing.version + 1 if existing else 1
|
|
|
|
entry = BlackboardEntry(
|
|
key=key,
|
|
value=value,
|
|
author=author,
|
|
timestamp=datetime.utcnow(),
|
|
version=version
|
|
)
|
|
self._sections[section][key] = entry
|
|
|
|
# Record in history
|
|
self._history.append({
|
|
"operation": "write",
|
|
"section": section,
|
|
"key": key,
|
|
"value": value,
|
|
"author": author,
|
|
"version": version,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
# Notify watchers
|
|
self._notify_watchers(section, key, value, author)
|
|
|
|
return True
|
|
|
|
def read(self, section: str, key: str = None) -> Optional[Any]:
|
|
"""
|
|
Read from a blackboard section.
|
|
|
|
Args:
|
|
section: Section name
|
|
key: Optional specific key (if None, returns all entries)
|
|
"""
|
|
with self._lock:
|
|
if section not in self._sections:
|
|
return None
|
|
|
|
if key:
|
|
entry = self._sections[section].get(key)
|
|
return entry.value if entry else None
|
|
else:
|
|
return {
|
|
k: v.value
|
|
for k, v in self._sections[section].items()
|
|
}
|
|
|
|
def read_with_metadata(self, section: str, key: str) -> Optional[Dict[str, Any]]:
|
|
"""Read entry with full metadata"""
|
|
with self._lock:
|
|
if section not in self._sections:
|
|
return None
|
|
|
|
entry = self._sections[section].get(key)
|
|
if not entry:
|
|
return None
|
|
|
|
return {
|
|
"key": entry.key,
|
|
"value": entry.value,
|
|
"author": entry.author,
|
|
"version": entry.version,
|
|
"timestamp": entry.timestamp.isoformat()
|
|
}
|
|
|
|
def delete(self, section: str, key: str, author: str) -> bool:
|
|
"""Delete an entry from a section"""
|
|
with self._lock:
|
|
if section not in self._sections:
|
|
return False
|
|
|
|
if key not in self._sections[section]:
|
|
return False
|
|
|
|
del self._sections[section][key]
|
|
|
|
self._history.append({
|
|
"operation": "delete",
|
|
"section": section,
|
|
"key": key,
|
|
"author": author,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
return True
|
|
|
|
def list_keys(self, section: str) -> List[str]:
|
|
"""List all keys in a section"""
|
|
with self._lock:
|
|
if section not in self._sections:
|
|
return []
|
|
return list(self._sections[section].keys())
|
|
|
|
# === Consensus Operations ===
|
|
|
|
def submit_proposal(self, proposal_id: str, proposal: Any, author: str) -> bool:
|
|
"""Submit a proposal for consensus"""
|
|
return self.write(
|
|
BlackboardSection.SOLUTIONS.value,
|
|
proposal_id,
|
|
{"proposal": proposal, "status": "pending", "votes": []},
|
|
author
|
|
)
|
|
|
|
def vote(self, proposal_id: str, agent: str, vote: str, reasoning: str = "") -> bool:
|
|
"""
|
|
Vote on a proposal.
|
|
|
|
Args:
|
|
proposal_id: ID of the proposal
|
|
agent: Voting agent ID
|
|
vote: ACCEPT, REJECT, or ABSTAIN
|
|
reasoning: Optional explanation
|
|
"""
|
|
with self._lock:
|
|
if vote not in ["ACCEPT", "REJECT", "ABSTAIN"]:
|
|
return False
|
|
|
|
consensus_vote = ConsensusVote(
|
|
agent=agent,
|
|
proposal_id=proposal_id,
|
|
vote=vote,
|
|
reasoning=reasoning,
|
|
timestamp=datetime.utcnow()
|
|
)
|
|
self._votes.append(consensus_vote)
|
|
|
|
# Update proposal with vote
|
|
solutions = self._sections.get(BlackboardSection.SOLUTIONS.value, {})
|
|
if proposal_id in solutions:
|
|
proposal_entry = solutions[proposal_id]
|
|
proposal_data = proposal_entry.value
|
|
if "votes" not in proposal_data:
|
|
proposal_data["votes"] = []
|
|
proposal_data["votes"].append({
|
|
"agent": agent,
|
|
"vote": vote,
|
|
"reasoning": reasoning
|
|
})
|
|
|
|
self._history.append({
|
|
"operation": "vote",
|
|
"proposal_id": proposal_id,
|
|
"agent": agent,
|
|
"vote": vote,
|
|
"timestamp": datetime.utcnow().isoformat()
|
|
})
|
|
|
|
return True
|
|
|
|
def check_consensus(self, proposal_id: str, required_agents: List[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Check consensus status for a proposal.
|
|
|
|
Returns:
|
|
{
|
|
"reached": bool,
|
|
"votes": {"ACCEPT": n, "REJECT": n, "ABSTAIN": n},
|
|
"missing_votes": [agent_ids],
|
|
"result": "ACCEPT"|"REJECT"|"PENDING"
|
|
}
|
|
"""
|
|
with self._lock:
|
|
proposal_votes = [v for v in self._votes if v.proposal_id == proposal_id]
|
|
|
|
vote_counts = {"ACCEPT": 0, "REJECT": 0, "ABSTAIN": 0}
|
|
voted_agents = set()
|
|
|
|
for v in proposal_votes:
|
|
vote_counts[v.vote] += 1
|
|
voted_agents.add(v.agent)
|
|
|
|
missing = []
|
|
if required_agents:
|
|
missing = [a for a in required_agents if a not in voted_agents]
|
|
|
|
all_voted = len(missing) == 0 if required_agents else True
|
|
|
|
# Determine result
|
|
if not all_voted:
|
|
result = "PENDING"
|
|
elif vote_counts["ACCEPT"] > vote_counts["REJECT"]:
|
|
result = "ACCEPT"
|
|
elif vote_counts["REJECT"] > vote_counts["ACCEPT"]:
|
|
result = "REJECT"
|
|
else:
|
|
result = "TIE"
|
|
|
|
return {
|
|
"reached": all_voted and result in ["ACCEPT", "REJECT"],
|
|
"votes": vote_counts,
|
|
"missing_votes": missing,
|
|
"result": result
|
|
}
|
|
|
|
# === Progress Tracking ===
|
|
|
|
def update_progress(self, agent: str, phase: str, step: str, details: Dict = None) -> bool:
|
|
"""Update agent progress"""
|
|
return self.write(
|
|
BlackboardSection.PROGRESS.value,
|
|
agent,
|
|
{
|
|
"phase": phase,
|
|
"step": step,
|
|
"details": details or {},
|
|
"updated_at": datetime.utcnow().isoformat()
|
|
},
|
|
agent
|
|
)
|
|
|
|
def get_all_progress(self) -> Dict[str, Any]:
|
|
"""Get progress for all agents"""
|
|
return self.read(BlackboardSection.PROGRESS.value) or {}
|
|
|
|
# === Watchers ===
|
|
|
|
def watch(self, section: str, callback: callable):
|
|
"""Watch a section for changes"""
|
|
with self._lock:
|
|
if section not in self._watchers:
|
|
self._watchers[section] = []
|
|
self._watchers[section].append(callback)
|
|
|
|
def unwatch(self, section: str, callback: callable = None):
|
|
"""Remove a watcher"""
|
|
with self._lock:
|
|
if section not in self._watchers:
|
|
return
|
|
if callback:
|
|
self._watchers[section] = [
|
|
cb for cb in self._watchers[section] if cb != callback
|
|
]
|
|
else:
|
|
del self._watchers[section]
|
|
|
|
def _notify_watchers(self, section: str, key: str, value: Any, author: str):
|
|
"""Notify watchers of a change (called with lock held)"""
|
|
callbacks = self._watchers.get(section, []).copy()
|
|
|
|
# Release lock before calling callbacks
|
|
for callback in callbacks:
|
|
try:
|
|
callback(section, key, value, author)
|
|
except Exception:
|
|
pass
|
|
|
|
# === Test Helpers ===
|
|
|
|
def reset(self):
|
|
"""Reset all state for testing"""
|
|
with self._lock:
|
|
self._sections = {
|
|
section.value: {} for section in BlackboardSection
|
|
}
|
|
self._votes.clear()
|
|
self._history.clear()
|
|
self._watchers.clear()
|
|
|
|
def get_history(self) -> List[Dict[str, Any]]:
|
|
"""Get operation history for assertions"""
|
|
with self._lock:
|
|
return self._history.copy()
|
|
|
|
def get_all_state(self) -> Dict[str, Any]:
|
|
"""Get complete state for test assertions"""
|
|
with self._lock:
|
|
return {
|
|
"sections": {
|
|
section: {
|
|
k: {"value": v.value, "author": v.author, "version": v.version}
|
|
for k, v in entries.items()
|
|
}
|
|
for section, entries in self._sections.items()
|
|
},
|
|
"votes": [
|
|
{"proposal": v.proposal_id, "agent": v.agent, "vote": v.vote}
|
|
for v in self._votes
|
|
]
|
|
}
|
|
|
|
def inject_state(self, section: str, entries: Dict[str, Any], author: str = "test"):
|
|
"""Inject state for testing"""
|
|
with self._lock:
|
|
if section not in self._sections:
|
|
self._sections[section] = {}
|
|
|
|
for key, value in entries.items():
|
|
self._sections[section][key] = BlackboardEntry(
|
|
key=key,
|
|
value=value,
|
|
author=author,
|
|
timestamp=datetime.utcnow()
|
|
)
|
|
|
|
def simulate_conflict(self, section: str, key: str, agents: List[str],
|
|
values: List[Any]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Simulate a write conflict for testing.
|
|
|
|
Multiple agents write to the same key in rapid succession.
|
|
Returns the sequence of writes.
|
|
"""
|
|
writes = []
|
|
for agent, value in zip(agents, values):
|
|
self.write(section, key, value, agent)
|
|
entry = self._sections[section][key]
|
|
writes.append({
|
|
"agent": agent,
|
|
"value": value,
|
|
"version": entry.version
|
|
})
|
|
return writes
|