refactor(monorepo): extract coord-core

This commit is contained in:
2026-03-20 13:01:16 +08:00
parent 3cf7a15626
commit 1938eb8f07
65 changed files with 6586 additions and 85 deletions
+50
View File
@@ -0,0 +1,50 @@
package db
import (
"context"
"database/sql"
"embed"
"fmt"
"sort"
)
//go:embed schema/*.sql
var schemaFS embed.FS
func ApplyMigrations(ctx context.Context, db *sql.DB) error {
files, err := schemaFS.ReadDir("schema")
if err != nil {
return fmt.Errorf("read embedded schema directory: %w", err)
}
names := make([]string, 0, len(files))
for _, file := range files {
if file.IsDir() {
continue
}
names = append(names, file.Name())
}
sort.Strings(names)
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin schema transaction: %w", err)
}
defer tx.Rollback()
for _, name := range names {
content, err := schemaFS.ReadFile("schema/" + name)
if err != nil {
return fmt.Errorf("read embedded schema file %q: %w", name, err)
}
if _, err := tx.ExecContext(ctx, string(content)); err != nil {
return fmt.Errorf("apply schema file %q: %w", name, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit schema transaction: %w", err)
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package db
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
func Open(ctx context.Context, dbPath string) (*sql.DB, error) {
if err := ensureParentDir(dbPath); err != nil {
return nil, err
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("open sqlite database: %w", err)
}
if err := applyPragmas(ctx, db); err != nil {
_ = db.Close()
return nil, err
}
return db, nil
}
func ensureParentDir(dbPath string) error {
parent := filepath.Dir(dbPath)
if parent == "." || parent == "" {
return nil
}
return os.MkdirAll(parent, 0o755)
}
+23
View File
@@ -0,0 +1,23 @@
package db
import (
"context"
"database/sql"
"fmt"
)
func applyPragmas(ctx context.Context, db *sql.DB) error {
pragmas := []string{
"PRAGMA foreign_keys = ON;",
"PRAGMA journal_mode = WAL;",
"PRAGMA busy_timeout = 5000;",
}
for _, pragma := range pragmas {
if _, err := db.ExecContext(ctx, pragma); err != nil {
return fmt.Errorf("apply pragma %q: %w", pragma, err)
}
}
return nil
}
@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS threads (
thread_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL,
task_id TEXT NOT NULL,
subject TEXT NOT NULL,
created_by TEXT NOT NULL,
assigned_to TEXT NOT NULL,
status TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'normal',
latest_message_id TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
message_id TEXT PRIMARY KEY,
thread_id TEXT NOT NULL,
from_agent TEXT NOT NULL,
to_agent TEXT NOT NULL,
kind TEXT NOT NULL,
summary TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
payload_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
FOREIGN KEY(thread_id) REFERENCES threads(thread_id)
);
CREATE TABLE IF NOT EXISTS leases (
thread_id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
lease_token TEXT NOT NULL,
claimed_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
released_at TEXT
);
CREATE TABLE IF NOT EXISTS artifacts (
artifact_id TEXT PRIMARY KEY,
message_id TEXT NOT NULL,
path TEXT NOT NULL,
kind TEXT NOT NULL,
metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
FOREIGN KEY(message_id) REFERENCES messages(message_id)
);
CREATE INDEX IF NOT EXISTS idx_threads_status_assigned
ON threads(status, assigned_to, updated_at);
CREATE INDEX IF NOT EXISTS idx_messages_thread_created
ON messages(thread_id, created_at);
@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS runs (
run_id TEXT PRIMARY KEY,
goal TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
run_id TEXT NOT NULL,
task_id TEXT NOT NULL,
title TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
default_to TEXT,
priority TEXT NOT NULL DEFAULT 'normal',
acceptance_json TEXT NOT NULL DEFAULT '[]',
latest_attempt_no INTEGER,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (run_id, task_id),
FOREIGN KEY(run_id) REFERENCES runs(run_id)
);
CREATE TABLE IF NOT EXISTS task_dependencies (
run_id TEXT NOT NULL,
task_id TEXT NOT NULL,
depends_on_task_id TEXT NOT NULL,
PRIMARY KEY (run_id, task_id, depends_on_task_id)
);
CREATE TABLE IF NOT EXISTS task_attempts (
run_id TEXT NOT NULL,
task_id TEXT NOT NULL,
attempt_no INTEGER NOT NULL,
assigned_to TEXT NOT NULL,
thread_id TEXT NOT NULL,
base_ref TEXT,
base_commit TEXT,
branch_name TEXT,
worktree_path TEXT,
workspace_status TEXT,
result_commit TEXT,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (run_id, task_id, attempt_no)
);
CREATE INDEX IF NOT EXISTS idx_tasks_run_status
ON tasks(run_id, status, priority, updated_at);
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS events (
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
task_id TEXT NOT NULL,
thread_id TEXT,
source TEXT NOT NULL,
event_type TEXT NOT NULL,
message_id TEXT,
summary TEXT NOT NULL DEFAULT '',
payload_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_events_run_event
ON events(run_id, event_id);
CREATE INDEX IF NOT EXISTS idx_events_thread_event
ON events(thread_id, event_id);
@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS council_runs (
run_id TEXT PRIMARY KEY,
mode TEXT NOT NULL,
target_type TEXT NOT NULL,
output_mode TEXT NOT NULL,
only_unanimous INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS council_reviewers (
run_id TEXT NOT NULL,
reviewer_role TEXT NOT NULL,
task_id TEXT NOT NULL,
status TEXT NOT NULL,
PRIMARY KEY (run_id, reviewer_role)
);
CREATE TABLE IF NOT EXISTS council_findings (
run_id TEXT NOT NULL,
reviewer_role TEXT NOT NULL,
finding_id TEXT NOT NULL,
title TEXT NOT NULL,
summary TEXT NOT NULL,
proposal TEXT NOT NULL,
rationale TEXT NOT NULL,
confidence TEXT NOT NULL,
tags_json TEXT NOT NULL DEFAULT '[]',
target_refs_json TEXT NOT NULL DEFAULT '{}',
PRIMARY KEY (run_id, reviewer_role, finding_id)
);
CREATE TABLE IF NOT EXISTS council_groups (
run_id TEXT NOT NULL,
group_id TEXT NOT NULL,
proposal TEXT NOT NULL,
bucket TEXT NOT NULL,
support_count INTEGER NOT NULL,
supporters_json TEXT NOT NULL DEFAULT '[]',
dissenters_json TEXT NOT NULL DEFAULT '[]',
rationale_summary TEXT NOT NULL DEFAULT '',
tags_json TEXT NOT NULL DEFAULT '[]',
source_finding_ids_json TEXT NOT NULL DEFAULT '[]',
PRIMARY KEY (run_id, group_id)
);
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS thread_reads (
thread_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
last_read_message_id TEXT NOT NULL,
last_read_at TEXT NOT NULL,
PRIMARY KEY(thread_id, agent_id),
FOREIGN KEY(thread_id) REFERENCES threads(thread_id),
FOREIGN KEY(last_read_message_id) REFERENCES messages(message_id)
);
CREATE INDEX IF NOT EXISTS idx_thread_reads_agent
ON thread_reads(agent_id, last_read_at);
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS council_inputs (
run_id TEXT PRIMARY KEY,
prompt TEXT NOT NULL DEFAULT '',
target_file TEXT NOT NULL DEFAULT '',
repo_path TEXT NOT NULL DEFAULT '',
target_task_id TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS council_reports (
run_id TEXT PRIMARY KEY,
show_json TEXT NOT NULL DEFAULT '[]',
summary_json TEXT NOT NULL DEFAULT '{}',
markdown_path TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
+5
View File
@@ -1,3 +1,8 @@
module ai-workflow-skill/packages/coord-core
go 1.26
require (
github.com/google/uuid v1.6.0
modernc.org/sqlite v1.40.1
)
+33
View File
@@ -0,0 +1,33 @@
package protocol
type CLIError struct {
Code string
ExitCode int
Message string
Err error
}
func (e *CLIError) Error() string {
return e.Message
}
func (e *CLIError) Unwrap() error {
return e.Err
}
func NewCLIError(code string, exitCode int, message string, err error) error {
return &CLIError{
Code: code,
ExitCode: exitCode,
Message: message,
Err: err,
}
}
func InvalidInput(message string, err error) error {
return NewCLIError("invalid_input", 30, message, err)
}
func NoMatchingWork(message string) error {
return NewCLIError("no_matching_work", 10, message, nil)
}
+28
View File
@@ -0,0 +1,28 @@
package protocol
import (
"encoding/json"
"io"
)
type Success struct {
OK bool `json:"ok"`
Command string `json:"command"`
Data map[string]any `json:"data,omitempty"`
}
type Error struct {
OK bool `json:"ok"`
Error ErrorPayload `json:"error"`
}
type ErrorPayload struct {
Code string `json:"code"`
Message string `json:"message"`
}
func WriteJSON(w io.Writer, v any) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(v)
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
package store
// Package store contains higher-level database access helpers.
File diff suppressed because it is too large Load Diff
+107
View File
@@ -0,0 +1,107 @@
package store
import (
"context"
"errors"
"path/filepath"
"testing"
"time"
dbpkg "ai-workflow-skill/packages/coord-core/db"
)
func TestClaimThreadReturnsLeaseConflictAfterBusyWrite(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
sqlDB, err := dbpkg.Open(ctx, dbPath)
if err != nil {
t.Fatalf("open base db: %v", err)
}
defer sqlDB.Close()
if err := dbpkg.ApplyMigrations(ctx, sqlDB); err != nil {
t.Fatalf("apply migrations: %v", err)
}
baseStore := NewInboxStore(sqlDB)
thread, _, err := baseStore.Send(ctx, SendInput{
FromAgent: "leader",
ToAgent: "worker-a",
Subject: "race claim",
Summary: "race claim",
})
if err != nil {
t.Fatalf("seed thread: %v", err)
}
lockerDB, err := dbpkg.Open(ctx, dbPath)
if err != nil {
t.Fatalf("open locker db: %v", err)
}
defer lockerDB.Close()
lockTx, err := lockerDB.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin locker tx: %v", err)
}
now := nowUTC()
if _, err := lockTx.ExecContext(
ctx,
`INSERT INTO leases (
thread_id, agent_id, lease_token, claimed_at, expires_at, released_at
) VALUES (?, ?, ?, ?, ?, NULL)`,
thread.ThreadID,
"worker-a",
"lease_locked",
formatTime(now),
formatTime(now.Add(5*time.Minute)),
); err != nil {
t.Fatalf("seed active lease in tx: %v", err)
}
if _, err := lockTx.ExecContext(
ctx,
`UPDATE threads
SET status = ?, assigned_to = ?, latest_message_id = ?, updated_at = ?
WHERE thread_id = ?`,
"claimed",
"worker-a",
"msg_locked",
formatTime(now),
thread.ThreadID,
); err != nil {
t.Fatalf("seed claimed thread in tx: %v", err)
}
commitDone := make(chan error, 1)
go func() {
time.Sleep(100 * time.Millisecond)
commitDone <- lockTx.Commit()
}()
claimDB, err := dbpkg.Open(ctx, dbPath)
if err != nil {
t.Fatalf("open claim db: %v", err)
}
defer claimDB.Close()
claimStore := NewInboxStore(claimDB)
_, err = claimStore.ClaimThread(ctx, ClaimInput{
ThreadID: thread.ThreadID,
Agent: "worker-b",
LeaseSeconds: 300,
})
if !errors.Is(err, ErrLeaseConflict) {
t.Fatalf("expected lease conflict after busy retry, got %v", err)
}
if err := <-commitDone; err != nil {
t.Fatalf("commit locker tx: %v", err)
}
}
File diff suppressed because it is too large Load Diff