From 662945f5797b17038a435459960d409eeef472ed Mon Sep 17 00:00:00 2001 From: cfdaily Date: Sun, 17 May 2026 00:34:18 +0800 Subject: [PATCH] auto-sync: 2026-05-17 00:34:18 --- src/blackboard/db.py | 292 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 src/blackboard/db.py diff --git a/src/blackboard/db.py b/src/blackboard/db.py new file mode 100644 index 0000000..ce3fc5f --- /dev/null +++ b/src/blackboard/db.py @@ -0,0 +1,292 @@ +"""黑板数据库连接管理(per-project SQLite)""" + +from __future__ import annotations + +import sqlite3 +from pathlib import Path +from typing import Optional + + +def init_db(db_path: Path) -> None: + """初始化数据库,创建全部表""" + db_path.parent.mkdir(parents=True, exist_ok=True) + conn = _connect(db_path) + try: + conn.executescript(SCHEMA_SQL) + conn.commit() + finally: + conn.close() + + +def get_connection(db_path: Path) -> sqlite3.Connection: + """获取数据库连接(WAL + busy_timeout + foreign_keys)""" + return _connect(db_path) + + +def _connect(db_path: Path) -> sqlite3.Connection: + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA foreign_keys=ON") + conn.execute("PRAGMA busy_timeout=5000") + return conn + + +# --------------------------------------------------------------------------- +# 状态机 +# --------------------------------------------------------------------------- + +VALID_STATUSES = frozenset({ + "pending", "claimed", "working", "review", + "done", "failed", "blocked", "cancelled", +}) + +TERMINAL_STATUSES = frozenset({"done", "cancelled"}) + +VALID_TRANSITIONS = { + "pending": {"claimed", "cancelled"}, + "claimed": {"working", "pending", "cancelled"}, + "working": {"review", "blocked", "failed", "cancelled"}, + "review": {"done", "pending", "failed", "cancelled"}, + "blocked": {"pending", "cancelled"}, + "done": set(), + "failed": {"pending"}, + "cancelled": set(), +} + +COMMENT_TYPES = frozenset({ + "general", "handoff", "observation", "rebuttal", + "rebuttal_response", "debate_argument", "debate_rebuttal", "debate_judgment", +}) + +SEVERITY_LEVELS = frozenset({"blocking", "warning", "info", "audit"}) + +EVENT_TYPES = frozenset({ + "task_created", "task_claimed", "task_started", "task_completed", + "task_failed", "task_blocked", "task_unblocked", "task_reviewed", + "task_cancelled", "task_retried", + "comment_added", "output_written", "observation_added", "decision_recorded", + "agent_spawned", "agent_completed", "agent_zombie_detected", + "session_spawned", "session_archived", "session_cleanup", + "daemon_tick", "daemon_manual_tick", +}) + +OUTPUT_TYPES = frozenset({"code", "document", "data", "config", "other"}) + +REVIEW_TYPES = frozenset({"plan_review", "output_review", "guardrail", "final_review"}) +VERDICT_TYPES = frozenset({"approved", "rejected", "needs_revision"}) + +EXPERIENCE_SOURCES = frozenset({ + "task_completion", "error_correction", "review_finding", "manual", +}) +EXPERIENCE_CATEGORIES = frozenset({ + "pitfall", "best_practice", "pattern", "anti_pattern", +}) +EXPERIENCE_STATUSES = frozenset({"draft", "active", "deprecated"}) + +ATTEMPT_OUTCOMES = frozenset({ + "completed", "blocked", "crashed", "timed_out", "spawn_failed", "reclaimed", +}) + + +# --------------------------------------------------------------------------- +# Schema SQL +# --------------------------------------------------------------------------- + +SCHEMA_SQL = """ +-- ===== 任务表 ===== +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'pending', + CHECK (status IN ('pending','claimed','working','review','done','failed','blocked','cancelled')), + + assignee TEXT, + assigned_by TEXT, + + depends_on TEXT, + parent_task TEXT, + + priority INTEGER NOT NULL DEFAULT 5, + task_type TEXT, + + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + claimed_at TEXT, + started_at TEXT, + completed_at TEXT, + deadline TEXT, + + retry_count INTEGER NOT NULL DEFAULT 0, + max_retries INTEGER NOT NULL DEFAULT 2, + + must_haves TEXT, + risk_level TEXT DEFAULT 'standard', + estimated_duration_minutes INTEGER, + + escalated INTEGER DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); +CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee); +CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task); + +-- ===== 评论线程表 ===== +CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + author TEXT NOT NULL, + comment_type TEXT NOT NULL DEFAULT 'general', + body TEXT NOT NULL, + mentions TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (task_id) REFERENCES tasks(id), + CHECK (comment_type IN ('general','handoff','observation','rebuttal','rebuttal_response','debate_argument','debate_rebuttal','debate_judgment')) +); + +CREATE INDEX IF NOT EXISTS idx_comments_task ON comments(task_id); +CREATE INDEX IF NOT EXISTS idx_comments_type ON comments(task_id, comment_type); +CREATE INDEX IF NOT EXISTS idx_comments_author ON comments(author); + +-- ===== 产出表 ===== +CREATE TABLE IF NOT EXISTS outputs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + agent TEXT NOT NULL, + output_type TEXT NOT NULL, + title TEXT NOT NULL, + content_path TEXT, + summary TEXT, + metadata TEXT, + attempt_number INTEGER DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (task_id) REFERENCES tasks(id), + CHECK (output_type IN ('code','document','data','config','other')) +); + +CREATE INDEX IF NOT EXISTS idx_outputs_task ON outputs(task_id); + +-- ===== 决策记录表 ===== +CREATE TABLE IF NOT EXISTS decisions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + decider TEXT NOT NULL, + decision TEXT NOT NULL, + rationale TEXT NOT NULL, + alternatives TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (task_id) REFERENCES tasks(id) +); + +CREATE INDEX IF NOT EXISTS idx_decisions_task ON decisions(task_id); + +-- ===== 观察表 ===== +CREATE TABLE IF NOT EXISTS observations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + observer TEXT NOT NULL, + severity TEXT NOT NULL DEFAULT 'info', + CHECK (severity IN ('blocking','warning','info','audit')), + body TEXT NOT NULL, + resolved_by TEXT, + resolved_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (task_id) REFERENCES tasks(id) +); + +-- ===== 事件日志 ===== +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT, + agent TEXT, + event_type TEXT NOT NULL, + detail TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_events_task ON events(task_id); +CREATE INDEX IF NOT EXISTS idx_events_time ON events(created_at); + +-- ===== Agent 注册表 ===== +CREATE TABLE IF NOT EXISTS agents ( + agent_id TEXT PRIMARY KEY, + role TEXT, + current_status TEXT DEFAULT 'idle', + current_task TEXT, + last_active TEXT, + capabilities TEXT +); + +-- ===== 任务尝试记录 ===== +CREATE TABLE IF NOT EXISTS task_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + attempt_number INTEGER NOT NULL, + agent TEXT NOT NULL, + outcome TEXT NOT NULL, + CHECK (outcome IN ('completed','blocked','crashed','timed_out','spawn_failed','reclaimed')), + exit_code INTEGER, + log_path TEXT, + summary TEXT, + metadata TEXT, + started_at TEXT NOT NULL DEFAULT (datetime('now')), + completed_at TEXT, + FOREIGN KEY (task_id) REFERENCES tasks(id) +); + +CREATE INDEX IF NOT EXISTS idx_attempts_task ON task_attempts(task_id); + +-- ===== 评审表 ===== +CREATE TABLE IF NOT EXISTS reviews ( + id TEXT PRIMARY KEY, + task_id TEXT NOT NULL, + output_id TEXT, + reviewer TEXT NOT NULL, + review_type TEXT NOT NULL, + CHECK (review_type IN ('plan_review','output_review','guardrail','final_review')), + verdict TEXT NOT NULL, + CHECK (verdict IN ('approved','rejected','needs_revision')), + confidence REAL, + round INTEGER NOT NULL DEFAULT 1, + max_rounds INTEGER NOT NULL DEFAULT 3, + consensus_reached INTEGER DEFAULT 0, + summary TEXT NOT NULL, + detail_path TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (task_id) REFERENCES tasks(id) +); + +CREATE INDEX IF NOT EXISTS idx_reviews_task ON reviews(task_id); +CREATE INDEX IF NOT EXISTS idx_reviews_output ON reviews(output_id); + +-- ===== 经验表 ===== +CREATE TABLE IF NOT EXISTS experiences ( + experience_id TEXT PRIMARY KEY, + source TEXT NOT NULL, + CHECK (source IN ('task_completion','error_correction','review_finding','manual')), + task_id TEXT, + summary TEXT NOT NULL, + category TEXT NOT NULL, + CHECK (category IN ('pitfall','best_practice','pattern','anti_pattern')), + confidence REAL DEFAULT 0.8, + status TEXT DEFAULT 'active', + CHECK (status IN ('draft','active','deprecated')), + skill_id TEXT, + usage_count INTEGER DEFAULT 0, + last_used_at TEXT, + created_at TEXT NOT NULL, + created_by TEXT NOT NULL, + updated_at TEXT, + deprecated_reason TEXT +); + +-- ===== 经验标签关联表 ===== +CREATE TABLE IF NOT EXISTS experience_tags ( + experience_id TEXT NOT NULL REFERENCES experiences(experience_id), + tag TEXT NOT NULL, + PRIMARY KEY (experience_id, tag) +); + +CREATE INDEX IF NOT EXISTS idx_exptags_tag ON experience_tags(tag); +"""