Finished reorganization

This commit is contained in:
Renn F
2025-12-14 03:40:48 +01:00
parent 855536eb18
commit 348795c50b
48 changed files with 2491 additions and 1988 deletions
+2 -62
View File
@@ -11,14 +11,12 @@ import contextlib
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from uuid import UUID, uuid4 from uuid import UUID
import httpx import httpx
import structlog import structlog
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
from pydantic import BaseModel, Field
if TYPE_CHECKING: if TYPE_CHECKING:
from anthropic.types import MessageParam from anthropic.types import MessageParam
@@ -26,6 +24,7 @@ if TYPE_CHECKING:
from roboco.config import settings from roboco.config import settings
from roboco.llm import ToonAdapter from roboco.llm import ToonAdapter
from roboco.models import AgentRole, AgentStatus, Team from roboco.models import AgentRole, AgentStatus, Team
from roboco.models.agents import AgentConfig, AgentState
# Type for reasoning stream callback (injected to avoid API layer coupling) # Type for reasoning stream callback (injected to avoid API layer coupling)
ReasoningStreamCallback = Callable[[UUID, str], Awaitable[None]] ReasoningStreamCallback = Callable[[UUID, str], Awaitable[None]]
@@ -55,65 +54,6 @@ def get_reasoning_stream_callback() -> ReasoningStreamCallback | None:
logger = structlog.get_logger() logger = structlog.get_logger()
# =============================================================================
# CONFIGURATION
# =============================================================================
class ModelProvider(str, Enum):
"""LLM provider options."""
ANTHROPIC = "anthropic"
OPENAI = "openai"
LOCAL = "local"
class AgentConfig(BaseModel):
"""Configuration for an agent instance."""
# Identity
id: UUID = Field(default_factory=uuid4)
name: str
slug: str = Field(..., pattern=r"^[a-z0-9-]+$")
role: AgentRole
team: Team | None = None
# Model configuration
provider: ModelProvider = ModelProvider.ANTHROPIC
model: str = "claude-sonnet-4-20250514"
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
max_tokens: int = Field(default=4096, ge=1)
# System prompt (loaded from blueprints)
system_prompt: str
# Capabilities
capabilities: list[str] = Field(default_factory=list)
# Permissions
can_notify: bool = False
channel_ids: list[UUID] = Field(default_factory=list)
# =============================================================================
# AGENT LIFECYCLE STATE
# =============================================================================
class AgentState(BaseModel):
"""Current state of an agent."""
status: AgentStatus = AgentStatus.OFFLINE
current_task_id: UUID | None = None
current_session_id: UUID | None = None
last_activity: datetime | None = None
error: str | None = None
# Metrics
messages_sent: int = 0
tasks_completed: int = 0
# ============================================================================= # =============================================================================
# BASE AGENT CLASS # BASE AGENT CLASS
# ============================================================================= # =============================================================================
+19 -101
View File
@@ -5,17 +5,26 @@ Implementation of Board-level workflows from the blueprint.
""" """
import re import re
from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import structlog import structlog
from roboco.agents.base import Agent, AgentConfig from roboco.agents.base import Agent
from roboco.models import AgentRole, Team from roboco.models import AgentRole, Team
from roboco.models.agents import (
AgentConfig,
AuditFlag,
AuditorFlagSeverity,
AuditorPhase,
AuditReport,
Campaign,
Feature,
HeadMarketingPhase,
ProductOwnerPhase,
)
logger = structlog.get_logger() logger = structlog.get_logger()
@@ -25,29 +34,6 @@ logger = structlog.get_logger()
# ============================================================================= # =============================================================================
class ProductOwnerPhase(str, Enum):
"""Phases of the Product Owner lifecycle."""
VISION = "vision"
ROADMAP = "roadmap"
DEFINE = "define"
PRIORITIZE = "prioritize"
REVIEW = "review"
FEEDBACK = "feedback"
@dataclass
class Feature:
"""A feature or epic."""
id: UUID
title: str
description: str
acceptance_criteria: list[str]
priority: int # 0-3
status: str = "backlog"
class ProductOwnerAgent(Agent): class ProductOwnerAgent(Agent):
""" """
Product Owner agent that defines what to build. Product Owner agent that defines what to build.
@@ -193,31 +179,6 @@ ACCEPTED: [reason] or NEEDS_CHANGES: [what's missing]
# ============================================================================= # =============================================================================
class HeadMarketingPhase(str, Enum):
"""Phases of the Head of Marketing lifecycle."""
RESEARCH = "research"
STRATEGY = "strategy"
PLAN = "plan"
CREATE = "create"
EXECUTE = "execute"
ANALYZE = "analyze"
@dataclass
class Campaign:
"""A marketing campaign."""
id: UUID
name: str
objective: str
channels: list[str]
start_date: datetime | None = None
end_date: datetime | None = None
status: str = "planning"
metrics: dict[str, Any] = field(default_factory=dict)
class HeadMarketingAgent(Agent): class HeadMarketingAgent(Agent):
""" """
Head of Marketing agent. Head of Marketing agent.
@@ -320,52 +281,6 @@ class HeadMarketingAgent(Agent):
# ============================================================================= # =============================================================================
class AuditorPhase(str, Enum):
"""Phases of the Auditor lifecycle."""
OBSERVE = "observe"
ANALYZE = "analyze"
FLAG = "flag"
REPORT = "report"
AUDIT = "audit"
ADVISE = "advise"
class FlagSeverity(str, Enum):
"""Severity of flagged issues."""
INFO = "info"
WARNING = "warning"
CONCERN = "concern"
CRITICAL = "critical"
@dataclass
class AuditFlag:
"""A flagged issue from audit observation."""
id: UUID
severity: FlagSeverity
category: str # quality, process, communication, efficiency
description: str
evidence: list[str]
recommendation: str | None = None
reported_to_ceo: bool = False
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@dataclass
class AuditReport:
"""A report to the CEO."""
period: str
summary: str
flags: list[AuditFlag]
metrics: dict[str, Any]
recommendations: list[str]
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
class AuditorAgent(Agent): class AuditorAgent(Agent):
""" """
Auditor agent - the CEO's secret ally. Auditor agent - the CEO's secret ally.
@@ -527,7 +442,7 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
self._flags.append( self._flags.append(
AuditFlag( AuditFlag(
id=uuid4(), id=uuid4(),
severity=FlagSeverity.CONCERN, severity=AuditorFlagSeverity.CONCERN,
category="analysis", category="analysis",
description=analysis[:500], description=analysis[:500],
evidence=["Automated analysis"], evidence=["Automated analysis"],
@@ -542,7 +457,9 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
""" """
self.log.debug("FLAG phase") self.log.debug("FLAG phase")
critical_flags = [f for f in self._flags if f.severity == FlagSeverity.CRITICAL] critical_flags = [
f for f in self._flags if f.severity == AuditorFlagSeverity.CRITICAL
]
if critical_flags: if critical_flags:
# Immediate alert to CEO # Immediate alert to CEO
await self._alert_ceo(critical_flags) await self._alert_ceo(critical_flags)
@@ -564,7 +481,8 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
self._last_report is None self._last_report is None
or hours_elapsed >= hours_in_day or hours_elapsed >= hours_in_day
or any( or any(
f.severity in [FlagSeverity.CONCERN, FlagSeverity.CRITICAL] f.severity
in [AuditorFlagSeverity.CONCERN, AuditorFlagSeverity.CRITICAL]
for f in self._flags for f in self._flags
) )
) )
@@ -607,7 +525,7 @@ efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff step
self._flags.append( self._flags.append(
AuditFlag( AuditFlag(
id=uuid4(), id=uuid4(),
severity=FlagSeverity.INFO, severity=AuditorFlagSeverity.INFO,
category=audit_type, category=audit_type,
description=findings, description=findings,
evidence=[f"{audit_type} audit"], evidence=[f"{audit_type} audit"],
+2 -33
View File
@@ -7,50 +7,19 @@ Handles task lifecycle:
""" """
import re import re
from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any
from uuid import UUID from uuid import UUID
import structlog import structlog
from roboco.agents.base import Agent, AgentConfig from roboco.agents.base import Agent
from roboco.models import AgentRole, TaskStatus, Team from roboco.models import AgentRole, TaskStatus, Team
from roboco.models.agents import AgentConfig, DevTaskPhase, TaskContext
logger = structlog.get_logger() logger = structlog.get_logger()
class DevTaskPhase(str, Enum):
"""Phases of the developer task lifecycle."""
SCAN = "scan"
CLAIM = "claim"
UNDERSTAND = "understand"
PLAN = "plan"
EXECUTE = "execute"
VERIFY = "verify"
NOTES = "notes"
CLOSE = "close"
BLOCKED = "blocked"
@dataclass
class TaskContext:
"""Context for the current task being worked on."""
task_id: UUID
title: str
phase: DevTaskPhase = DevTaskPhase.CLAIM
subtasks: list[dict[str, Any]] = field(default_factory=list)
current_subtask: int = 0
blockers: list[str] = field(default_factory=list)
commits: list[str] = field(default_factory=list)
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
journal_entries: list[str] = field(default_factory=list)
class DeveloperAgent(Agent): class DeveloperAgent(Agent):
""" """
Developer agent that follows the Dev Lifecycle. Developer agent that follows the Dev Lifecycle.
+8 -61
View File
@@ -7,79 +7,26 @@ Handles documentation lifecycle:
""" """
import re import re
from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from uuid import UUID from uuid import UUID
import aiofiles import aiofiles
import structlog import structlog
from roboco.agents.base import Agent, AgentConfig from roboco.agents.base import Agent
from roboco.models import AgentRole, TaskStatus, Team from roboco.models import AgentRole, TaskStatus, Team
from roboco.models.agents import (
AgentConfig,
DocContext,
DocTaskPhase,
DocType,
DocumentSpec,
)
logger = structlog.get_logger() logger = structlog.get_logger()
class DocTaskPhase(str, Enum):
"""Phases of the Documenter lifecycle."""
MONITOR = "monitor"
RECEIVE = "receive"
GATHER = "gather"
SYNTHESIZE = "synthesize"
WRITE = "write"
REVIEW = "review"
PUBLISH = "publish"
class DocType(str, Enum):
"""Types of documentation."""
API = "api"
README = "readme"
ARCHITECTURE = "architecture"
CHANGELOG = "changelog"
KNOWLEDGE_BASE = "knowledge_base"
COMPONENT = "component"
DESIGN_SYSTEM = "design_system"
@dataclass
class DocumentSpec:
"""Specification for a document to create/update."""
doc_type: DocType
title: str
path: str
priority: str = "required" # required, optional
content: str | None = None
@dataclass
class DocContext:
"""Context for the current documentation task."""
task_id: UUID
title: str
phase: DocTaskPhase = DocTaskPhase.RECEIVE
# Gathered materials
dev_notes: str | None = None
qa_feedback: str | None = None
commits: list[str] = field(default_factory=list)
conversations: list[str] = field(default_factory=list)
code_changes: list[str] = field(default_factory=list)
# Synthesis
summary: str | None = None
documents_needed: list[DocumentSpec] = field(default_factory=list)
current_doc: int = 0
# Output
written_docs: list[str] = field(default_factory=list)
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
notes: list[str] = field(default_factory=list)
class DocumenterAgent(Agent): class DocumenterAgent(Agent):
""" """
Documenter agent that follows the Documenter Lifecycle. Documenter agent that follows the Documenter Lifecycle.
+1 -146
View File
@@ -5,181 +5,36 @@ Provides factory functions for creating all agent types and
deploying complete cells with their full agent complement. deploying complete cells with their full agent complement.
""" """
from dataclasses import dataclass
from typing import Any from typing import Any
from uuid import UUID
import structlog
from roboco.agents.base import Agent
from roboco.agents.board import ( from roboco.agents.board import (
AuditorAgent,
HeadMarketingAgent,
ProductOwnerAgent,
create_auditor, create_auditor,
create_head_marketing, create_head_marketing,
create_product_owner, create_product_owner,
) )
from roboco.agents.developer import ( from roboco.agents.developer import (
DeveloperAgent,
create_backend_developer, create_backend_developer,
create_frontend_developer, create_frontend_developer,
create_ux_developer, create_ux_developer,
) )
from roboco.agents.documenter import ( from roboco.agents.documenter import (
DocumenterAgent,
create_backend_documenter, create_backend_documenter,
create_frontend_documenter, create_frontend_documenter,
create_ux_documenter, create_ux_documenter,
) )
from roboco.agents.pm import ( from roboco.agents.pm import (
CellPMAgent,
MainPMAgent,
create_backend_pm, create_backend_pm,
create_frontend_pm, create_frontend_pm,
create_main_pm, create_main_pm,
create_ux_pm, create_ux_pm,
) )
from roboco.agents.qa import ( from roboco.agents.qa import (
QAAgent,
create_backend_qa, create_backend_qa,
create_frontend_qa, create_frontend_qa,
create_ux_qa, create_ux_qa,
) )
from roboco.models import Team from roboco.models import Team
from roboco.models.organization import Board, Cell, Organization
logger = structlog.get_logger()
@dataclass
class Cell:
"""A complete cell with all its agents."""
name: str
team: Team
pm: CellPMAgent
developers: list[DeveloperAgent]
qa: QAAgent
documenter: DocumenterAgent
@property
def all_agents(self) -> list[Agent]:
"""Get all agents in the cell."""
return [self.pm, *self.developers, self.qa, self.documenter]
async def start_all(self) -> None:
"""Start all agents in the cell."""
for agent in self.all_agents:
await agent.start()
logger.info("Cell started", cell=self.name, agents=len(self.all_agents))
async def stop_all(self) -> None:
"""Stop all agents in the cell."""
for agent in self.all_agents:
await agent.stop()
logger.info("Cell stopped", cell=self.name)
@dataclass
class Board:
"""The board level with all board agents."""
product_owner: ProductOwnerAgent
head_marketing: HeadMarketingAgent
auditor: AuditorAgent
@property
def all_agents(self) -> list[Agent]:
"""Get all board agents."""
return [self.product_owner, self.head_marketing, self.auditor]
async def start_all(self) -> None:
"""Start all board agents."""
for agent in self.all_agents:
await agent.start()
logger.info("Board started", agents=len(self.all_agents))
async def stop_all(self) -> None:
"""Stop all board agents."""
for agent in self.all_agents:
await agent.stop()
logger.info("Board stopped")
@dataclass
class Organization:
"""The complete AI organization."""
board: Board
main_pm: MainPMAgent
backend_cell: Cell
frontend_cell: Cell
ux_cell: Cell
@property
def all_agents(self) -> list[Agent]:
"""Get all agents in the organization."""
agents: list[Agent] = []
agents.extend(self.board.all_agents)
agents.append(self.main_pm)
agents.extend(self.backend_cell.all_agents)
agents.extend(self.frontend_cell.all_agents)
agents.extend(self.ux_cell.all_agents)
return agents
@property
def agent_count(self) -> int:
"""Total number of agents."""
return len(self.all_agents)
async def start_all(self) -> None:
"""Start the entire organization."""
logger.info("Starting organization")
# Start board first
await self.board.start_all()
await self.main_pm.start()
# Then cells
await self.backend_cell.start_all()
await self.frontend_cell.start_all()
await self.ux_cell.start_all()
logger.info("Organization started", total_agents=self.agent_count)
async def stop_all(self) -> None:
"""Stop the entire organization."""
logger.info("Stopping organization")
# Stop cells first
await self.ux_cell.stop_all()
await self.frontend_cell.stop_all()
await self.backend_cell.stop_all()
# Then management
await self.main_pm.stop()
await self.board.stop_all()
logger.info("Organization stopped")
def get_agent_by_id(self, agent_id: UUID) -> Agent | None:
"""Find an agent by ID."""
for agent in self.all_agents:
if agent.id == agent_id:
return agent
return None
def get_agent_by_slug(self, slug: str) -> Agent | None:
"""Find an agent by slug."""
for agent in self.all_agents:
if agent.config.slug == slug:
return agent
return None
def get_agents_by_team(self, team: Team) -> list[Agent]:
"""Get all agents in a team."""
return [a for a in self.all_agents if a.team == team]
# ============================================================================= # =============================================================================
# CELL FACTORIES # CELL FACTORIES
+9 -59
View File
@@ -9,76 +9,26 @@ Main PM:
""" """
import re import re
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
import structlog import structlog
from roboco.agents.base import Agent, AgentConfig from roboco.agents.base import Agent
from roboco.models import AgentRole, NotificationType, TaskStatus, Team from roboco.models import AgentRole, NotificationType, TaskStatus, Team
from roboco.models.agents import (
AgentConfig,
CellPMPhase,
CellStatus,
Escalation,
MainPMPhase,
TaskAssignment,
)
logger = structlog.get_logger() logger = structlog.get_logger()
class CellPMPhase(str, Enum):
"""Phases of the Cell PM lifecycle."""
MONITOR = "monitor"
TRIAGE = "triage"
ASSIGN = "assign"
FACILITATE = "facilitate"
ESCALATE = "escalate"
TRACK = "track"
REPORT = "report"
class MainPMPhase(str, Enum):
"""Phases of the Main PM lifecycle."""
OVERSEE = "oversee"
RECEIVE = "receive"
PRIORITIZE = "prioritize"
COORDINATE = "coordinate"
DISTRIBUTE = "distribute"
REPORT_UP = "report_up"
FACILITATE = "facilitate"
@dataclass
class CellStatus:
"""Status of a cell."""
name: str
active_tasks: int = 0
blocked_tasks: int = 0
completed_today: int = 0
available_devs: int = 0
concerns: list[str] = field(default_factory=list)
@dataclass
class TaskAssignment:
"""A task assignment decision."""
task_id: UUID
agent_id: UUID
agent_name: str
reason: str
@dataclass
class Escalation:
"""An escalation to higher management."""
issue: str
severity: str # low, medium, high, critical
task_id: UUID | None = None
proposed_solution: str | None = None
class CellPMAgent(Agent): class CellPMAgent(Agent):
""" """
Cell PM agent that manages a single cell (Backend, Frontend, or UX/UI). Cell PM agent that manages a single cell (Backend, Frontend, or UX/UI).
+8 -51
View File
@@ -7,68 +7,25 @@ Handles review lifecycle:
""" """
import re import re
from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from uuid import UUID from uuid import UUID
import structlog import structlog
from roboco.agents.base import Agent, AgentConfig from roboco.agents.base import Agent
from roboco.models import AgentRole, TaskStatus, Team from roboco.models import AgentRole, TaskStatus, Team
from roboco.models.agents import (
AgentConfig,
QATaskPhase,
ReviewContext,
TestCase,
TestResult,
)
logger = structlog.get_logger() logger = structlog.get_logger()
class QATaskPhase(str, Enum):
"""Phases of the QA lifecycle."""
MONITOR = "monitor"
RECEIVE = "receive"
UNDERSTAND = "understand"
TEST = "test"
VERDICT = "verdict"
DOCUMENT = "document"
RETURN = "return"
class TestResult(str, Enum):
"""Test result outcomes."""
PASS = "pass"
FAIL = "fail"
BLOCKED = "blocked"
@dataclass
class TestCase:
"""A single test case."""
name: str
description: str
steps: list[str]
expected: str
result: TestResult | None = None
actual: str | None = None
notes: str | None = None
@dataclass
class ReviewContext:
"""Context for the current review being conducted."""
task_id: UUID
title: str
phase: QATaskPhase = QATaskPhase.RECEIVE
test_cases: list[TestCase] = field(default_factory=list)
current_test: int = 0
findings: list[str] = field(default_factory=list)
verdict: TestResult | None = None
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
notes: list[str] = field(default_factory=list)
class QAAgent(Agent): class QAAgent(Agent):
""" """
QA agent that follows the QA Lifecycle. QA agent that follows the QA Lifecycle.
+8 -63
View File
@@ -8,79 +8,24 @@ from typing import Annotated
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, HTTPException, Query, status from fastapi import APIRouter, HTTPException, Query, status
from pydantic import BaseModel, Field
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from roboco.api.deps import CurrentAgentContext, DbSession, PermissionServiceDep from roboco.api.deps import CurrentAgentContext, DbSession, PermissionServiceDep
from roboco.api.schemas.channels import (
ChannelDetailResponse,
ChannelListResponse,
ChannelResponse,
GroupResponse,
ListChannelsQuery,
)
from roboco.db.tables import ChannelTable from roboco.db.tables import ChannelTable
from roboco.models import AgentRole, ChannelCreate, ChannelType, ChannelUpdate from roboco.models import AgentRole, ChannelCreate, ChannelUpdate
from roboco.utils.converters import require_uuid, to_python_uuid from roboco.utils.converters import require_uuid, to_python_uuid
router = APIRouter() router = APIRouter()
# =============================================================================
# Response Models
# =============================================================================
class ChannelResponse(BaseModel):
"""Channel response with computed fields."""
id: UUID
name: str
slug: str
type: ChannelType
description: str | None
topic: str | None
member_count: int
message_count: int
group_count: int
is_archived: bool
is_private: bool
can_write: bool # Whether current agent can write
class ChannelListResponse(BaseModel):
"""Paginated list of channels."""
items: list[ChannelResponse]
total: int
page: int
page_size: int
class ChannelDetailResponse(ChannelResponse):
"""Detailed channel response with groups."""
groups: list[dict]
class GroupResponse(BaseModel):
"""Group within a channel."""
id: UUID
name: str
hierarchy_level: int
is_active: bool
total_messages: int
active_session_id: UUID | None = None
# =============================================================================
# Query Parameter Models
# =============================================================================
class ListChannelsQuery(BaseModel):
"""Query params for listing channels."""
page: int = Field(1, ge=1)
page_size: int = Field(20, ge=1, le=100)
include_archived: bool = False
# ============================================================================= # =============================================================================
# Routes # Routes
# ============================================================================= # =============================================================================
+11 -102
View File
@@ -6,15 +6,24 @@ Provides aggregated views, alerts, and reporting.
""" """
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from enum import Enum
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, HTTPException, Query, status from fastapi import APIRouter, HTTPException, Query, status
from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import select
from roboco.api.deps import DbSession from roboco.api.deps import DbSession
from roboco.api.schemas.dashboard import (
AuditorDashboard,
AuditorFlag,
AuditorReport,
CEOOverview,
ChannelFeed,
CreateFlagRequest,
CreateReportRequest,
FlagSeverity,
TeamHealth,
)
from roboco.db.tables import AgentTable, MessageTable, TaskTable from roboco.db.tables import AgentTable, MessageTable, TaskTable
from roboco.models.base import AgentStatus, Team from roboco.models.base import AgentStatus, Team
from roboco.models.dashboard import CreateFlagParams from roboco.models.dashboard import CreateFlagParams
@@ -25,106 +34,6 @@ from roboco.services.metrics import get_metrics_service
router = APIRouter(prefix="/dashboard", tags=["dashboard"]) router = APIRouter(prefix="/dashboard", tags=["dashboard"])
# =============================================================================
# MODELS
# =============================================================================
class FlagSeverity(str, Enum):
"""Severity levels for auditor flags."""
INFO = "info"
WARNING = "warning"
URGENT = "urgent"
class AuditorFlag(BaseModel):
"""A flag raised by the auditor."""
id: UUID
severity: FlagSeverity
category: str # quality, process, communication, blocked, documentation
title: str
description: str
related_task_id: UUID | None = None
related_agent_id: UUID | None = None
created_at: datetime
resolved_at: datetime | None = None
notes: str | None = None
class AuditorReport(BaseModel):
"""An auditor report for the CEO."""
id: UUID
report_type: str # daily, weekly, alert
title: str
summary: str
sections: list[dict[str, Any]]
created_at: datetime
sent_at: datetime | None = None
class ChannelFeed(BaseModel):
"""Live feed status for a channel."""
id: UUID
name: str
status: str # streaming, idle, offline
last_activity: datetime | None
message_count_24h: int
class AuditorDashboard(BaseModel):
"""Complete auditor dashboard data."""
live_feeds: list[ChannelFeed]
flagged_items: list[AuditorFlag]
metrics: dict[str, Any]
audit_queue: list[dict[str, Any]]
recent_reports: list[AuditorReport]
class TeamHealth(BaseModel):
"""Health status for a team."""
team: str
status: str # ok, slow, critical
active_tasks: int
blocked_tasks: int
blocked_ratio: float
completed_this_week: int
class CEOOverview(BaseModel):
"""Complete CEO overview data."""
health_status: list[TeamHealth]
key_metrics: dict[str, Any]
auditor_alerts: dict[str, Any]
roadmap_progress: dict[str, Any]
class CreateFlagRequest(BaseModel):
"""Request to create an auditor flag."""
severity: FlagSeverity
category: str
title: str
description: str
related_task_id: UUID | None = None
related_agent_id: UUID | None = None
class CreateReportRequest(BaseModel):
"""Request to create an auditor report."""
report_type: str
title: str
summary: str
sections: list[dict[str, Any]] = Field(default_factory=list)
# ============================================================================= # =============================================================================
# AUDITOR DASHBOARD # AUDITOR DASHBOARD
# ============================================================================= # =============================================================================
+1 -17
View File
@@ -6,9 +6,9 @@ Endpoints for monitoring application health and readiness.
import redis.asyncio as redis import redis.asyncio as redis
from fastapi import APIRouter, status from fastapi import APIRouter, status
from pydantic import BaseModel
from sqlalchemy import text from sqlalchemy import text
from roboco.api.schemas.health import HealthResponse, ReadinessResponse
from roboco.config import settings from roboco.config import settings
from roboco.db.base import get_db_context from roboco.db.base import get_db_context
@@ -36,22 +36,6 @@ async def _check_redis() -> tuple[str, bool]:
return str(e), False return str(e), False
class HealthResponse(BaseModel):
"""Health check response."""
status: str
version: str
environment: str
class ReadinessResponse(BaseModel):
"""Readiness check response."""
status: str
database: str
redis: str
@router.get( @router.get(
"/health", "/health",
response_model=HealthResponse, response_model=HealthResponse,
+14 -156
View File
@@ -4,14 +4,26 @@ Journal API Routes
Agent personal journals for reflection, growth tracking, and debugging. Agent personal journals for reflection, growth tracking, and debugging.
""" """
from datetime import datetime
from typing import Annotated from typing import Annotated
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from roboco.api.deps import CurrentAgentContext, DbSession from roboco.api.deps import CurrentAgentContext, DbSession
from roboco.api.schemas.journals import (
CreateEntryRequest,
DecisionLogRequest,
GeneralEntryRequest,
GrowthMetricsResponse,
JournalEntryResponse,
JournalResponse,
JournalStatsResponse,
LearningRequest,
ListEntriesParams,
SearchEntriesRequest,
StruggleRequest,
TaskReflectionRequest,
)
from roboco.models.base import AgentRole, JournalEntryType from roboco.models.base import AgentRole, JournalEntryType
from roboco.models.journal import ( from roboco.models.journal import (
DecisionLogParams, DecisionLogParams,
@@ -23,163 +35,9 @@ from roboco.models.journal import (
) )
from roboco.services.journal import ListEntriesFilter, get_journal_service from roboco.services.journal import ListEntriesFilter, get_journal_service
# =============================================================================
# QUERY PARAMETER SCHEMAS
# =============================================================================
class ListEntriesParams(BaseModel):
"""Query parameters for listing journal entries."""
entry_type: str | None = Field(None, description="Filter by entry type")
task_id: UUID | None = Field(None, description="Filter by task")
limit: int = Field(50, ge=1, le=100, description="Maximum entries to return")
offset: int = Field(0, ge=0, description="Number of entries to skip")
router = APIRouter(prefix="/journals", tags=["journals"]) router = APIRouter(prefix="/journals", tags=["journals"])
# =============================================================================
# REQUEST/RESPONSE SCHEMAS
# =============================================================================
class JournalResponse(BaseModel):
"""Journal response."""
id: UUID
agent_id: UUID
total_entries: int
last_entry_at: datetime | None
latest_summary: str | None
summary_updated_at: datetime | None
entries_by_type: dict[str, int]
created_at: datetime
updated_at: datetime | None = None
class JournalEntryResponse(BaseModel):
"""Journal entry response."""
id: UUID
journal_id: UUID
type: str
title: str
content: str
task_id: UUID | None
session_id: UUID | None
timestamp: datetime
tags: list[str]
sentiment: str | None
is_private: bool
created_at: datetime
updated_at: datetime | None = None
class CreateEntryRequest(BaseModel):
"""Request to create a journal entry."""
type: str = Field(
...,
description="Entry type (task_reflection, decision_log, learning, etc.)",
)
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1)
task_id: UUID | None = None
session_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
sentiment: str | None = None
is_private: bool = False
class TaskReflectionRequest(BaseModel):
"""Request to create a task reflection entry."""
task_id: UUID
title: str = Field(..., min_length=1, max_length=200)
what_done: str
what_learned: str
what_struggled: str
next_steps: list[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class DecisionLogRequest(BaseModel):
"""Request to create a decision log entry."""
title: str = Field(..., min_length=1, max_length=200)
context: str
options: list[dict[str, str]] = Field(..., min_length=2)
chosen: str
rationale: str
consequences: list[str] = Field(default_factory=list)
task_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
class LearningRequest(BaseModel):
"""Request to create a learning entry."""
title: str = Field(..., min_length=1, max_length=200)
what_learned: str
how_applied: str | None = None
source: str | None = None
task_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
class StruggleRequest(BaseModel):
"""Request to create a struggle entry."""
title: str = Field(..., min_length=1, max_length=200)
what_struggled: str
attempted_solutions: list[str] = Field(default_factory=list)
resolution: str | None = None
help_needed: str | None = None
task_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
class GeneralEntryRequest(BaseModel):
"""Request to create a general journal entry."""
title: str = Field(..., min_length=1, max_length=200)
content: str
task_id: UUID | None = None
session_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
is_private: bool = False
class JournalStatsResponse(BaseModel):
"""Journal statistics response."""
total_entries: int
entries_by_type: dict[str, int]
last_entry_at: datetime | None
has_summary: bool
class GrowthMetricsResponse(BaseModel):
"""Growth metrics response."""
total_reflections: int
total_learnings: int
total_struggles: int
total_decisions: int
struggle_resolution_rate: float
learning_frequency: float
sentiment_trend: str
class SearchEntriesRequest(BaseModel):
"""Request to search journal entries."""
query: str = Field(..., min_length=1)
top_k: int = Field(5, ge=1, le=20)
# ============================================================================= # =============================================================================
# JOURNAL ENDPOINTS # JOURNAL ENDPOINTS
# ============================================================================= # =============================================================================
+7 -71
View File
@@ -9,13 +9,18 @@ from typing import Annotated
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from roboco.api.deps import CurrentAgentId, DbSession from roboco.api.deps import CurrentAgentId, DbSession
from roboco.api.schemas.messages import (
ListMessagesParams,
MessageCreateRequest,
MessageEditRequest,
MessageListResponse,
MessageResponse,
)
from roboco.db.tables import MessageTable, SessionTable from roboco.db.tables import MessageTable, SessionTable
from roboco.models import MessageType
from roboco.services.messaging import ( from roboco.services.messaging import (
MessageCreateRequest as ServiceMessageRequest, MessageCreateRequest as ServiceMessageRequest,
) )
@@ -27,75 +32,6 @@ from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid
router = APIRouter() router = APIRouter()
# =============================================================================
# Query Parameter Models
# =============================================================================
class ListMessagesParams(BaseModel):
"""Query parameters for listing messages."""
session_id: UUID
before: datetime | None = None
after: datetime | None = None
type_filter: MessageType | None = None
limit: int = Field(50, ge=1, le=100)
# =============================================================================
# Response Models
# =============================================================================
class MessageResponse(BaseModel):
"""Message response."""
id: UUID
agent_id: UUID
channel_id: UUID
group_id: UUID
session_id: UUID
type: MessageType
content: str
content_length: int
is_reply: bool
reply_to: UUID | None
mentions: list[UUID]
task_id: UUID | None
commit_ref: str | None
timestamp: datetime
edited_at: datetime | None
was_edited: bool
class MessageListResponse(BaseModel):
"""List of messages."""
items: list[MessageResponse]
total: int
has_more: bool
class MessageCreateRequest(BaseModel):
"""Request to create a message."""
session_id: UUID
type: MessageType
content: str = Field(..., min_length=1, max_length=10000)
is_reply: bool = False
reply_to: UUID | None = None
mentions: list[UUID] = Field(default_factory=list)
task_id: UUID | None = None
commit_ref: str | None = None
class MessageEditRequest(BaseModel):
"""Request to edit a message."""
content: str = Field(..., min_length=1, max_length=10000)
reason: str | None = None
# ============================================================================= # =============================================================================
# Routes # Routes
# ============================================================================= # =============================================================================
+6 -62
View File
@@ -10,81 +10,25 @@ from typing import Annotated, Any
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import select
from roboco.api.deps import CurrentAgentId, DbSession from roboco.api.deps import CurrentAgentId, DbSession
from roboco.api.schemas.notifications import (
ListNotificationsParams,
NotificationCreateRequest,
NotificationListResponse,
NotificationResponse,
)
from roboco.db.tables import AgentTable, NotificationTable from roboco.db.tables import AgentTable, NotificationTable
from roboco.enforcement import ( from roboco.enforcement import (
NotificationPermissionError, NotificationPermissionError,
validate_notification_permission, validate_notification_permission,
) )
from roboco.models import NotificationPriority, NotificationType
from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid_list from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid_list
router = APIRouter() router = APIRouter()
# =============================================================================
# Query Parameter Models
# =============================================================================
class ListNotificationsParams(BaseModel):
"""Query parameters for listing notifications."""
unread_only: bool = False
pending_ack_only: bool = False
type_filter: NotificationType | None = None
limit: int = Field(50, ge=1, le=100)
# =============================================================================
# Response Models
# =============================================================================
class NotificationResponse(BaseModel):
"""Notification response."""
id: UUID
type: NotificationType
priority: NotificationPriority
from_agent: UUID
to_agents: list[UUID]
subject: str
body: str
requires_ack: bool
is_acknowledged: bool
is_fully_acknowledged: bool
is_read: bool
related_task_id: UUID | None
timestamp: datetime
expires_at: datetime | None
class NotificationListResponse(BaseModel):
"""List of notifications."""
items: list[NotificationResponse]
total: int
unread_count: int
pending_ack_count: int
class NotificationCreateRequest(BaseModel):
"""Request to create a notification."""
type: NotificationType
priority: NotificationPriority = NotificationPriority.NORMAL
to_agents: list[UUID] = Field(..., min_length=1)
subject: str = Field(..., min_length=1, max_length=200)
body: str
requires_ack: bool = True
related_task_id: UUID | None = None
expires_at: datetime | None = None
# ============================================================================= # =============================================================================
# Routes # Routes
# ============================================================================= # =============================================================================
+45 -151
View File
@@ -6,12 +6,29 @@ Knowledge base, RAG queries, and semantic search endpoints.
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from uuid import UUID, uuid4 from uuid import uuid4
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field
from roboco.api.deps import CurrentAgentContext from roboco.api.deps import CurrentAgentContext
from roboco.api.schemas.optimal import (
ClearIndexResponse,
IndexCodeRequest,
IndexDocsRequest,
IndexResponse,
IndexStatsResponse,
PromptTemplateRequest,
PromptTemplateResponse,
RAGQueryRequest,
RAGQueryResponse,
RefreshIndexResponse,
RefreshRequest,
SearchRequest,
SearchResponse,
SearchResultResponse,
TokenEstimateRequest,
TokenEstimateResponse,
)
from roboco.models import AgentRole from roboco.models import AgentRole
from roboco.services.optimal import ( from roboco.services.optimal import (
IndexType, IndexType,
@@ -22,150 +39,6 @@ from roboco.services.optimal import (
router = APIRouter(prefix="/optimal", tags=["optimal"]) router = APIRouter(prefix="/optimal", tags=["optimal"])
# =============================================================================
# REQUEST/RESPONSE SCHEMAS
# =============================================================================
class IndexCodeRequest(BaseModel):
"""Request to index code files."""
sources: list[str] = Field(
..., min_length=1, description="File paths, directories, or globs"
)
project: str | None = Field(None, description="Project identifier for filtering")
class IndexDocsRequest(BaseModel):
"""Request to index documentation."""
sources: list[str] = Field(
..., min_length=1, description="File paths, URLs, or globs"
)
project: str | None = Field(None, description="Project identifier for filtering")
class SearchRequest(BaseModel):
"""Request for semantic search."""
query: str = Field(..., min_length=1, description="Natural language query")
project: str | None = Field(None, description="Filter by project")
task_id: UUID | None = Field(None, description="Filter by task")
index_types: list[str] | None = Field(None, description="Index types to search")
top_k: int = Field(5, ge=1, le=20, description="Number of results")
class RAGQueryRequest(BaseModel):
"""Request for RAG query."""
query: str = Field(..., min_length=1, description="Natural language question")
project: str | None = Field(None, description="Filter by project")
task_id: UUID | None = Field(None, description="Filter by task")
index_types: list[str] | None = Field(None, description="Index types to query")
top_k: int = Field(5, ge=1, le=20, description="Context chunks to use")
class SearchResultResponse(BaseModel):
"""A single search result."""
content: str
source: str
score: float
index_type: str
metadata: dict[str, Any] = Field(default_factory=dict)
class SearchResponse(BaseModel):
"""Response from semantic search."""
results: list[SearchResultResponse]
query: str
total: int
class RAGQueryResponse(BaseModel):
"""Response from RAG query."""
answer: str
citations: list[SearchResultResponse]
query: str
context_used: int
class IndexStatsResponse(BaseModel):
"""Statistics for all indexes."""
initialized: bool
indexes: dict[str, dict[str, Any]]
class RefreshRequest(BaseModel):
"""Request to refresh an index."""
index_type: str = Field(..., description="Index type to refresh")
sources: list[str] = Field(..., min_length=1, description="Sources to refresh")
class IndexResponse(BaseModel):
"""Response from indexing operations."""
indexed: int
sources: list[str]
project: str | None
class ClearIndexResponse(BaseModel):
"""Response from clearing an index."""
status: str
index_type: str
class RefreshIndexResponse(BaseModel):
"""Response from refreshing an index."""
status: str
index_type: str
sources: list[str]
class PromptTemplateRequest(BaseModel):
"""Request to create/manage a prompt template."""
name: str = Field(..., min_length=1, max_length=100, description="Template name")
template: str = Field(..., min_length=1, description="Prompt template")
description: str | None = Field(None, description="Template description")
variables: list[str] = Field(default_factory=list, description="Variables")
category: str | None = Field(None, description="Template category")
class PromptTemplateResponse(BaseModel):
"""Response for prompt template."""
id: str
name: str
template: str
description: str | None
variables: list[str]
category: str | None
created_at: str
class TokenEstimateRequest(BaseModel):
"""Request to estimate token count."""
content: str = Field(..., min_length=1, description="Content to estimate")
model: str = Field("claude-sonnet-4-20250514", description="Model")
class TokenEstimateResponse(BaseModel):
"""Response with token count estimate."""
token_count: int
model: str
content_length: int
# ============================================================================= # =============================================================================
# INDEXING ENDPOINTS # INDEXING ENDPOINTS
# ============================================================================= # =============================================================================
@@ -546,11 +419,31 @@ async def refresh_index(
# ============================================================================= # =============================================================================
# PROMPT TEMPLATE ENDPOINTS # PROMPT TEMPLATE STORAGE
# ============================================================================= # =============================================================================
# In-memory prompt template storage (would be database in production)
_prompt_templates: dict[str, dict[str, Any]] = {} class _PromptTemplateStorageHolder:
"""Holder for prompt template storage (would be database in production)."""
templates: dict[str, dict[str, Any]] | None = None
def _get_prompt_templates() -> dict[str, dict[str, Any]]:
"""Get the prompt templates storage."""
if _PromptTemplateStorageHolder.templates is None:
_PromptTemplateStorageHolder.templates = {}
return _PromptTemplateStorageHolder.templates
def reset_prompt_templates() -> None:
"""Reset prompt templates (for testing)."""
_PromptTemplateStorageHolder.templates = {}
# =============================================================================
# PROMPT TEMPLATE ENDPOINTS
# =============================================================================
@router.post( @router.post(
@@ -571,7 +464,8 @@ async def create_prompt_template(
template_id = str(uuid4()) template_id = str(uuid4())
created_at = datetime.now(UTC).isoformat() created_at = datetime.now(UTC).isoformat()
_prompt_templates[template_id] = { templates = _get_prompt_templates()
templates[template_id] = {
"id": template_id, "id": template_id,
"name": request.name, "name": request.name,
"template": request.template, "template": request.template,
@@ -601,7 +495,7 @@ async def list_prompt_templates(
"""List all prompt templates, optionally filtered by category.""" """List all prompt templates, optionally filtered by category."""
# Any authenticated agent can list templates # Any authenticated agent can list templates
_ = agent # Used for authentication _ = agent # Used for authentication
templates = list(_prompt_templates.values()) templates = list(_get_prompt_templates().values())
if category: if category:
templates = [t for t in templates if t.get("category") == category] templates = [t for t in templates if t.get("category") == category]
+7 -52
View File
@@ -5,11 +5,16 @@ API endpoints for managing the Agent Orchestrator.
""" """
from datetime import datetime from datetime import datetime
from typing import Any
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field
from roboco.api.schemas.orchestrator import (
AgentStatusResponse,
OrchestratorStatusResponse,
ResolveWaitRequest,
SpawnAgentRequest,
WaitingAgentResponse,
)
from roboco.runtime import AgentOrchestrator from roboco.runtime import AgentOrchestrator
router = APIRouter() router = APIRouter()
@@ -36,56 +41,6 @@ def get_orchestrator() -> AgentOrchestrator:
return _OrchestratorHolder.instance return _OrchestratorHolder.instance
# =============================================================================
# Response Models
# =============================================================================
class AgentStatusResponse(BaseModel):
"""Status of a single agent."""
agent_id: str
state: str
task_id: str | None
error_count: int
started_at: datetime | None
waiting_for: str | None
class OrchestratorStatusResponse(BaseModel):
"""Overall orchestrator status."""
total_agents: int
by_state: dict[str, int]
waiting_count: int
agents: list[AgentStatusResponse]
class WaitingAgentResponse(BaseModel):
"""Agent in WAITING_LONG state."""
agent_id: str
task_id: str | None
waiting_for: str
waiting_since: datetime
context: dict[str, Any]
class SpawnAgentRequest(BaseModel):
"""Request to spawn an agent."""
agent_id: str
initial_prompt: str | None = None
task_id: str | None = None
model: str | None = None
class ResolveWaitRequest(BaseModel):
"""Request to resolve a wait condition."""
resolution: dict[str, Any] = Field(default_factory=dict)
# ============================================================================= # =============================================================================
# Routes # Routes
# ============================================================================= # =============================================================================
+6 -49
View File
@@ -10,11 +10,16 @@ from typing import Annotated
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from roboco.api.deps import CurrentAgentId, DbSession from roboco.api.deps import CurrentAgentId, DbSession
from roboco.api.schemas.sessions import (
ListSessionsParams,
SessionCreateRequest,
SessionListResponse,
SessionResponse,
)
from roboco.db.tables import GroupTable, SessionTable from roboco.db.tables import GroupTable, SessionTable
from roboco.models import SessionStatus from roboco.models import SessionStatus
from roboco.utils.converters import require_uuid from roboco.utils.converters import require_uuid
@@ -22,54 +27,6 @@ from roboco.utils.converters import require_uuid
router = APIRouter() router = APIRouter()
# =============================================================================
# Query Parameter Models
# =============================================================================
class ListSessionsParams(BaseModel):
"""Query parameters for listing sessions."""
group_id: UUID
status_filter: SessionStatus | None = None
limit: int = Field(20, ge=1, le=100)
# =============================================================================
# Response Models
# =============================================================================
class SessionResponse(BaseModel):
"""Session response."""
id: UUID
group_id: UUID
status: SessionStatus
message_count: int
total_content_length: int
started_at: datetime
last_activity_at: datetime
closed_at: datetime | None
class SessionListResponse(BaseModel):
"""List of sessions."""
items: list[SessionResponse]
total: int
class SessionCreateRequest(BaseModel):
"""Request to create a session."""
group_id: UUID
max_time_window_minutes: int | None = 30
max_message_count: int | None = 100
max_content_length: int | None = 50000
timeout_seconds: int = 300
# ============================================================================= # =============================================================================
# Routes # Routes
# ============================================================================= # =============================================================================
+8 -58
View File
@@ -6,74 +6,24 @@ transcription and extraction pipelines.
""" """
from typing import Any from typing import Any
from uuid import UUID
from fastapi import APIRouter, HTTPException, Request, status from fastapi import APIRouter, HTTPException, Request, status
from pydantic import BaseModel, Field
from roboco.api.deps import CurrentAgentContext, CurrentAgentId, PermissionServiceDep from roboco.api.deps import CurrentAgentContext, CurrentAgentId, PermissionServiceDep
from roboco.api.schemas.stream import (
ExtractedMessageResponse,
ExtractionResponse,
ExtractRequest,
StreamChunkRequest,
StreamCompleteRequest,
TranscriptionStatsResponse,
)
from roboco.models import MessageType from roboco.models import MessageType
from roboco.models.message import RawStream from roboco.models.message import RawStream
router = APIRouter() router = APIRouter()
# =============================================================================
# REQUEST/RESPONSE SCHEMAS
# =============================================================================
class StreamChunkRequest(BaseModel):
"""Request to process a stream chunk."""
channel_id: UUID = Field(..., description="Target channel")
session_id: UUID = Field(..., description="Current session")
chunk: str = Field(..., description="Raw LLM output chunk")
class StreamCompleteRequest(BaseModel):
"""Request to mark a stream as complete."""
session_id: UUID = Field(..., description="Session to complete")
class ExtractRequest(BaseModel):
"""Request to extract messages from content."""
channel_id: UUID = Field(..., description="Target channel")
session_id: UUID = Field(..., description="Current session")
group_id: UUID = Field(..., description="Group within channel")
content: str = Field(..., description="Content to extract from")
task_id: UUID | None = Field(default=None, description="Related task")
class ExtractedMessageResponse(BaseModel):
"""Response for an extracted message."""
id: UUID
type: str
content: str
content_length: int
confidence: float
class ExtractionResponse(BaseModel):
"""Response from extraction."""
message_count: int
messages: list[ExtractedMessageResponse]
types_extracted: list[str]
class TranscriptionStatsResponse(BaseModel):
"""Response for transcription service stats."""
active_agents: int
total_buffers: int
total_buffered_chars: int
running: bool
# ============================================================================= # =============================================================================
# STREAM PROCESSING ROUTES # STREAM PROCESSING ROUTES
# ============================================================================= # =============================================================================
+12 -108
View File
@@ -4,20 +4,29 @@ Task API Routes
Full CRUD operations and lifecycle management for tasks. Full CRUD operations and lifecycle management for tasks.
""" """
from datetime import datetime
from typing import Annotated from typing import Annotated
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, HTTPException, Query, status from fastapi import APIRouter, HTTPException, Query, status
from pydantic import BaseModel, Field
from roboco.api.deps import ( from roboco.api.deps import (
CurrentAgentContext, CurrentAgentContext,
DbSession, DbSession,
PermissionServiceDep, PermissionServiceDep,
) )
from roboco.api.schemas.tasks import (
CheckpointRequest,
CommitRequest,
ListTasksQuery,
ProgressRequest,
QANotes,
TaskCountResponse,
TaskResponse,
TaskUpdate,
TeamTasksQuery,
)
from roboco.db.tables import TaskTable from roboco.db.tables import TaskTable
from roboco.models.base import Complexity, TaskStatus, Team from roboco.models.base import TaskStatus, Team
from roboco.models.task import TaskCreate from roboco.models.task import TaskCreate
from roboco.services.audit import get_audit_service from roboco.services.audit import get_audit_service
from roboco.services.permissions import TaskAction from roboco.services.permissions import TaskAction
@@ -27,56 +36,6 @@ from roboco.utils.converters import require_uuid, to_python_uuid, to_python_uuid
router = APIRouter(prefix="/tasks", tags=["tasks"]) router = APIRouter(prefix="/tasks", tags=["tasks"])
# =============================================================================
# REQUEST/RESPONSE MODELS
# =============================================================================
class TaskUpdate(BaseModel):
"""Request to update a task."""
title: str | None = None
description: str | None = None
acceptance_criteria: list[str] | None = None
priority: int | None = Field(default=None, ge=0, le=3)
target_date: datetime | None = None
estimated_complexity: Complexity | None = None
dev_notes: str | None = None
quick_context: str | None = None
class TaskResponse(BaseModel):
"""Task response model."""
id: UUID
title: str
description: str
acceptance_criteria: list[str]
status: TaskStatus
priority: int
team: Team
created_by: UUID
assigned_to: UUID | None
parent_task_id: UUID | None
dependency_ids: list[UUID]
blocker_ids: list[UUID]
created_at: datetime
updated_at: datetime | None
claimed_at: datetime | None
started_at: datetime | None
completed_at: datetime | None
target_date: datetime | None
estimated_complexity: Complexity
self_verified: bool
qa_verified: bool | None
dev_notes: str | None
qa_notes: str | None
quick_context: str | None
class Config:
from_attributes = True
def _to_response(task: TaskTable) -> TaskResponse: def _to_response(task: TaskTable) -> TaskResponse:
"""Convert TaskTable to TaskResponse with proper UUID conversion.""" """Convert TaskTable to TaskResponse with proper UUID conversion."""
return TaskResponse( return TaskResponse(
@@ -112,61 +71,6 @@ def _to_response_list(tasks: list[TaskTable]) -> list[TaskResponse]:
return [_to_response(t) for t in tasks] return [_to_response(t) for t in tasks]
class ProgressRequest(BaseModel):
"""Request to add progress update."""
message: str
percentage: int | None = Field(default=None, ge=0, le=100)
class CheckpointRequest(BaseModel):
"""Request to add checkpoint."""
state_summary: str
remaining_work: list[str]
notes: str | None = None
class CommitRequest(BaseModel):
"""Request to link a commit."""
hash: str = Field(..., min_length=7, max_length=40)
message: str
class QANotes(BaseModel):
"""QA review notes."""
notes: str
class TaskCountResponse(BaseModel):
"""Task count by category."""
counts: dict[str, int]
# =============================================================================
# QUERY PARAMETER MODELS
# =============================================================================
class ListTasksQuery(BaseModel):
"""Query params for listing tasks."""
team: Team | None = None
status: TaskStatus | None = None
limit: int = Field(100, ge=1, le=500)
offset: int = Field(0, ge=0)
class TeamTasksQuery(BaseModel):
"""Query params for team tasks."""
task_status: TaskStatus | None = None
limit: int = Field(100, ge=1, le=500)
# ============================================================================= # =============================================================================
# CRUD ENDPOINTS # CRUD ENDPOINTS
# ============================================================================= # =============================================================================
+193
View File
@@ -0,0 +1,193 @@
"""
API Schemas
Pydantic models for request/response serialization.
"""
from roboco.api.schemas.channels import (
ChannelDetailResponse,
ChannelListResponse,
ChannelResponse,
GroupResponse,
ListChannelsQuery,
)
from roboco.api.schemas.dashboard import (
AuditorDashboard,
AuditorFlag,
AuditorReport,
CEOOverview,
ChannelFeed,
CreateFlagRequest,
CreateReportRequest,
FlagSeverity,
TeamHealth,
)
from roboco.api.schemas.health import HealthResponse, ReadinessResponse
from roboco.api.schemas.journals import (
CreateEntryRequest,
DecisionLogRequest,
GeneralEntryRequest,
GrowthMetricsResponse,
JournalEntryResponse,
JournalResponse,
JournalStatsResponse,
LearningRequest,
ListEntriesParams,
SearchEntriesRequest,
StruggleRequest,
TaskReflectionRequest,
)
from roboco.api.schemas.messages import (
ListMessagesParams,
MessageCreateRequest,
MessageEditRequest,
MessageListResponse,
MessageResponse,
)
from roboco.api.schemas.notifications import (
ListNotificationsParams,
NotificationCreateRequest,
NotificationListResponse,
NotificationResponse,
)
from roboco.api.schemas.optimal import (
ClearIndexResponse,
IndexCodeRequest,
IndexDocsRequest,
IndexResponse,
IndexStatsResponse,
PromptTemplateRequest,
PromptTemplateResponse,
RAGQueryRequest,
RAGQueryResponse,
RefreshIndexResponse,
RefreshRequest,
SearchRequest,
SearchResponse,
SearchResultResponse,
TokenEstimateRequest,
TokenEstimateResponse,
)
from roboco.api.schemas.orchestrator import (
AgentStatusResponse,
OrchestratorStatusResponse,
ResolveWaitRequest,
SpawnAgentRequest,
WaitingAgentResponse,
)
from roboco.api.schemas.sessions import (
ListSessionsParams,
SessionCreateRequest,
SessionListResponse,
SessionResponse,
)
from roboco.api.schemas.stream import (
ExtractedMessageResponse,
ExtractionResponse,
ExtractRequest,
StreamChunkRequest,
StreamCompleteRequest,
TranscriptionStatsResponse,
)
from roboco.api.schemas.tasks import (
CheckpointRequest,
CommitRequest,
ListTasksQuery,
ProgressRequest,
QANotes,
TaskCountResponse,
TaskResponse,
TaskUpdate,
TeamTasksQuery,
)
__all__ = [
# Orchestrator
"AgentStatusResponse",
# Dashboard
"AuditorDashboard",
"AuditorFlag",
"AuditorReport",
"CEOOverview",
"ChannelDetailResponse",
"ChannelFeed",
# Channels
"ChannelListResponse",
"ChannelResponse",
# Tasks
"CheckpointRequest",
# Optimal
"ClearIndexResponse",
"CommitRequest",
# Journals
"CreateEntryRequest",
"CreateFlagRequest",
"CreateReportRequest",
"DecisionLogRequest",
"ExtractRequest",
# Stream
"ExtractedMessageResponse",
"ExtractionResponse",
"FlagSeverity",
"GeneralEntryRequest",
"GroupResponse",
"GrowthMetricsResponse",
# Health
"HealthResponse",
"IndexCodeRequest",
"IndexDocsRequest",
"IndexResponse",
"IndexStatsResponse",
"JournalEntryResponse",
"JournalResponse",
"JournalStatsResponse",
"LearningRequest",
"ListChannelsQuery",
"ListEntriesParams",
# Messages
"ListMessagesParams",
# Notifications
"ListNotificationsParams",
# Sessions
"ListSessionsParams",
"ListTasksQuery",
"MessageCreateRequest",
"MessageEditRequest",
"MessageListResponse",
"MessageResponse",
"NotificationCreateRequest",
"NotificationListResponse",
"NotificationResponse",
"OrchestratorStatusResponse",
"ProgressRequest",
"PromptTemplateRequest",
"PromptTemplateResponse",
"QANotes",
"RAGQueryRequest",
"RAGQueryResponse",
"ReadinessResponse",
"RefreshIndexResponse",
"RefreshRequest",
"ResolveWaitRequest",
"SearchEntriesRequest",
"SearchRequest",
"SearchResponse",
"SearchResultResponse",
"SessionCreateRequest",
"SessionListResponse",
"SessionResponse",
"SpawnAgentRequest",
"StreamChunkRequest",
"StreamCompleteRequest",
"StruggleRequest",
"TaskCountResponse",
"TaskReflectionRequest",
"TaskResponse",
"TaskUpdate",
"TeamHealth",
"TeamTasksQuery",
"TokenEstimateRequest",
"TokenEstimateResponse",
"TranscriptionStatsResponse",
"WaitingAgentResponse",
]
+62
View File
@@ -0,0 +1,62 @@
"""
Channels API Schemas
Request/response models for channel endpoints.
"""
from uuid import UUID
from pydantic import BaseModel, Field
from roboco.models import ChannelType
class ChannelResponse(BaseModel):
"""Channel response with computed fields."""
id: UUID
name: str
slug: str
type: ChannelType
description: str | None
topic: str | None
member_count: int
message_count: int
group_count: int
is_archived: bool
is_private: bool
can_write: bool # Whether current agent can write
class ChannelListResponse(BaseModel):
"""Paginated list of channels."""
items: list[ChannelResponse]
total: int
page: int
page_size: int
class ChannelDetailResponse(ChannelResponse):
"""Detailed channel response with groups."""
groups: list[dict]
class GroupResponse(BaseModel):
"""Group within a channel."""
id: UUID
name: str
hierarchy_level: int
is_active: bool
total_messages: int
active_session_id: UUID | None = None
class ListChannelsQuery(BaseModel):
"""Query params for listing channels."""
page: int = Field(1, ge=1)
page_size: int = Field(20, ge=1, le=100)
include_archived: bool = False
+107
View File
@@ -0,0 +1,107 @@
"""
Dashboard API Schemas
Request/response models for auditor and CEO dashboards.
"""
from datetime import datetime
from enum import Enum
from typing import Any
from uuid import UUID
from pydantic import BaseModel, Field
class FlagSeverity(str, Enum):
"""Severity levels for auditor flags."""
INFO = "info"
WARNING = "warning"
URGENT = "urgent"
class AuditorFlag(BaseModel):
"""A flag raised by the auditor."""
id: UUID
severity: FlagSeverity
category: str # quality, process, communication, blocked, documentation
title: str
description: str
related_task_id: UUID | None = None
related_agent_id: UUID | None = None
created_at: datetime
resolved_at: datetime | None = None
notes: str | None = None
class AuditorReport(BaseModel):
"""An auditor report for the CEO."""
id: UUID
report_type: str # daily, weekly, alert
title: str
summary: str
sections: list[dict[str, Any]]
created_at: datetime
sent_at: datetime | None = None
class ChannelFeed(BaseModel):
"""Live feed status for a channel."""
id: UUID
name: str
status: str # streaming, idle, offline
last_activity: datetime | None
message_count_24h: int
class AuditorDashboard(BaseModel):
"""Complete auditor dashboard data."""
live_feeds: list[ChannelFeed]
flagged_items: list[AuditorFlag]
metrics: dict[str, Any]
audit_queue: list[dict[str, Any]]
recent_reports: list[AuditorReport]
class TeamHealth(BaseModel):
"""Health status for a team."""
team: str
status: str # ok, slow, critical
active_tasks: int
blocked_tasks: int
blocked_ratio: float
completed_this_week: int
class CEOOverview(BaseModel):
"""Complete CEO overview data."""
health_status: list[TeamHealth]
key_metrics: dict[str, Any]
auditor_alerts: dict[str, Any]
roadmap_progress: dict[str, Any]
class CreateFlagRequest(BaseModel):
"""Request to create an auditor flag."""
severity: FlagSeverity
category: str
title: str
description: str
related_task_id: UUID | None = None
related_agent_id: UUID | None = None
class CreateReportRequest(BaseModel):
"""Request to create an auditor report."""
report_type: str
title: str
summary: str
sections: list[dict[str, Any]] = Field(default_factory=list)
+23
View File
@@ -0,0 +1,23 @@
"""
Health Check API Schemas
Request/response models for health check endpoints.
"""
from pydantic import BaseModel
class HealthResponse(BaseModel):
"""Health check response."""
status: str
version: str
environment: str
class ReadinessResponse(BaseModel):
"""Readiness check response."""
status: str
database: str
redis: str
+154
View File
@@ -0,0 +1,154 @@
"""
Journals API Schemas
Request/response models for agent journal endpoints.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
class ListEntriesParams(BaseModel):
"""Query parameters for listing journal entries."""
entry_type: str | None = Field(None, description="Filter by entry type")
task_id: UUID | None = Field(None, description="Filter by task")
limit: int = Field(50, ge=1, le=100, description="Maximum entries to return")
offset: int = Field(0, ge=0, description="Number of entries to skip")
class JournalResponse(BaseModel):
"""Journal response."""
id: UUID
agent_id: UUID
total_entries: int
last_entry_at: datetime | None
latest_summary: str | None
summary_updated_at: datetime | None
entries_by_type: dict[str, int]
created_at: datetime
updated_at: datetime | None = None
class JournalEntryResponse(BaseModel):
"""Journal entry response."""
id: UUID
journal_id: UUID
type: str
title: str
content: str
task_id: UUID | None
session_id: UUID | None
timestamp: datetime
tags: list[str]
sentiment: str | None
is_private: bool
created_at: datetime
updated_at: datetime | None = None
class CreateEntryRequest(BaseModel):
"""Request to create a journal entry."""
type: str = Field(
...,
description="Entry type (task_reflection, decision_log, learning, etc.)",
)
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1)
task_id: UUID | None = None
session_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
sentiment: str | None = None
is_private: bool = False
class TaskReflectionRequest(BaseModel):
"""Request to create a task reflection entry."""
task_id: UUID
title: str = Field(..., min_length=1, max_length=200)
what_done: str
what_learned: str
what_struggled: str
next_steps: list[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class DecisionLogRequest(BaseModel):
"""Request to create a decision log entry."""
title: str = Field(..., min_length=1, max_length=200)
context: str
options: list[dict[str, str]] = Field(..., min_length=2)
chosen: str
rationale: str
consequences: list[str] = Field(default_factory=list)
task_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
class LearningRequest(BaseModel):
"""Request to create a learning entry."""
title: str = Field(..., min_length=1, max_length=200)
what_learned: str
how_applied: str | None = None
source: str | None = None
task_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
class StruggleRequest(BaseModel):
"""Request to create a struggle entry."""
title: str = Field(..., min_length=1, max_length=200)
what_struggled: str
attempted_solutions: list[str] = Field(default_factory=list)
resolution: str | None = None
help_needed: str | None = None
task_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
class GeneralEntryRequest(BaseModel):
"""Request to create a general journal entry."""
title: str = Field(..., min_length=1, max_length=200)
content: str
task_id: UUID | None = None
session_id: UUID | None = None
tags: list[str] = Field(default_factory=list)
is_private: bool = False
class JournalStatsResponse(BaseModel):
"""Journal statistics response."""
total_entries: int
entries_by_type: dict[str, int]
last_entry_at: datetime | None
has_summary: bool
class GrowthMetricsResponse(BaseModel):
"""Growth metrics response."""
total_reflections: int
total_learnings: int
total_struggles: int
total_decisions: int
struggle_resolution_rate: float
learning_frequency: float
sentiment_trend: str
class SearchEntriesRequest(BaseModel):
"""Request to search journal entries."""
query: str = Field(..., min_length=1)
top_k: int = Field(5, ge=1, le=20)
+71
View File
@@ -0,0 +1,71 @@
"""
Messages API Schemas
Request/response models for message endpoints.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from roboco.models import MessageType
class ListMessagesParams(BaseModel):
"""Query parameters for listing messages."""
session_id: UUID
before: datetime | None = None
after: datetime | None = None
type_filter: MessageType | None = None
limit: int = Field(50, ge=1, le=100)
class MessageResponse(BaseModel):
"""Message response."""
id: UUID
agent_id: UUID
channel_id: UUID
group_id: UUID
session_id: UUID
type: MessageType
content: str
content_length: int
is_reply: bool
reply_to: UUID | None
mentions: list[UUID]
task_id: UUID | None
commit_ref: str | None
timestamp: datetime
edited_at: datetime | None
was_edited: bool
class MessageListResponse(BaseModel):
"""List of messages."""
items: list[MessageResponse]
total: int
has_more: bool
class MessageCreateRequest(BaseModel):
"""Request to create a message."""
session_id: UUID
type: MessageType
content: str = Field(..., min_length=1, max_length=10000)
is_reply: bool = False
reply_to: UUID | None = None
mentions: list[UUID] = Field(default_factory=list)
task_id: UUID | None = None
commit_ref: str | None = None
class MessageEditRequest(BaseModel):
"""Request to edit a message."""
content: str = Field(..., min_length=1, max_length=10000)
reason: str | None = None
+62
View File
@@ -0,0 +1,62 @@
"""
Notifications API Schemas
Request/response models for the notification system.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from roboco.models import NotificationPriority, NotificationType
class ListNotificationsParams(BaseModel):
"""Query parameters for listing notifications."""
unread_only: bool = False
pending_ack_only: bool = False
type_filter: NotificationType | None = None
limit: int = Field(50, ge=1, le=100)
class NotificationResponse(BaseModel):
"""Notification response."""
id: UUID
type: NotificationType
priority: NotificationPriority
from_agent: UUID
to_agents: list[UUID]
subject: str
body: str
requires_ack: bool
is_acknowledged: bool
is_fully_acknowledged: bool
is_read: bool
related_task_id: UUID | None
timestamp: datetime
expires_at: datetime | None
class NotificationListResponse(BaseModel):
"""List of notifications."""
items: list[NotificationResponse]
total: int
unread_count: int
pending_ack_count: int
class NotificationCreateRequest(BaseModel):
"""Request to create a notification."""
type: NotificationType
priority: NotificationPriority = NotificationPriority.NORMAL
to_agents: list[UUID] = Field(..., min_length=1)
subject: str = Field(..., min_length=1, max_length=200)
body: str
requires_ack: bool = True
related_task_id: UUID | None = None
expires_at: datetime | None = None
+149
View File
@@ -0,0 +1,149 @@
"""
Optimal API Schemas
Request/response models for knowledge base and RAG endpoints.
"""
from typing import Any
from uuid import UUID
from pydantic import BaseModel, Field
class IndexCodeRequest(BaseModel):
"""Request to index code files."""
sources: list[str] = Field(
..., min_length=1, description="File paths, directories, or globs"
)
project: str | None = Field(None, description="Project identifier for filtering")
class IndexDocsRequest(BaseModel):
"""Request to index documentation."""
sources: list[str] = Field(
..., min_length=1, description="File paths, URLs, or globs"
)
project: str | None = Field(None, description="Project identifier for filtering")
class SearchRequest(BaseModel):
"""Request for semantic search."""
query: str = Field(..., min_length=1, description="Natural language query")
project: str | None = Field(None, description="Filter by project")
task_id: UUID | None = Field(None, description="Filter by task")
index_types: list[str] | None = Field(None, description="Index types to search")
top_k: int = Field(5, ge=1, le=20, description="Number of results")
class RAGQueryRequest(BaseModel):
"""Request for RAG query."""
query: str = Field(..., min_length=1, description="Natural language question")
project: str | None = Field(None, description="Filter by project")
task_id: UUID | None = Field(None, description="Filter by task")
index_types: list[str] | None = Field(None, description="Index types to query")
top_k: int = Field(5, ge=1, le=20, description="Context chunks to use")
class SearchResultResponse(BaseModel):
"""A single search result."""
content: str
source: str
score: float
index_type: str
metadata: dict[str, Any] = Field(default_factory=dict)
class SearchResponse(BaseModel):
"""Response from semantic search."""
results: list[SearchResultResponse]
query: str
total: int
class RAGQueryResponse(BaseModel):
"""Response from RAG query."""
answer: str
citations: list[SearchResultResponse]
query: str
context_used: int
class IndexStatsResponse(BaseModel):
"""Statistics for all indexes."""
initialized: bool
indexes: dict[str, dict[str, Any]]
class RefreshRequest(BaseModel):
"""Request to refresh an index."""
index_type: str = Field(..., description="Index type to refresh")
sources: list[str] = Field(..., min_length=1, description="Sources to refresh")
class IndexResponse(BaseModel):
"""Response from indexing operations."""
indexed: int
sources: list[str]
project: str | None
class ClearIndexResponse(BaseModel):
"""Response from clearing an index."""
status: str
index_type: str
class RefreshIndexResponse(BaseModel):
"""Response from refreshing an index."""
status: str
index_type: str
sources: list[str]
class PromptTemplateRequest(BaseModel):
"""Request to create/manage a prompt template."""
name: str = Field(..., min_length=1, max_length=100, description="Template name")
template: str = Field(..., min_length=1, description="Prompt template")
description: str | None = Field(None, description="Template description")
variables: list[str] = Field(default_factory=list, description="Variables")
category: str | None = Field(None, description="Template category")
class PromptTemplateResponse(BaseModel):
"""Response for prompt template."""
id: str
name: str
template: str
description: str | None
variables: list[str]
category: str | None
created_at: str
class TokenEstimateRequest(BaseModel):
"""Request to estimate token count."""
content: str = Field(..., min_length=1, description="Content to estimate")
model: str = Field("claude-sonnet-4-20250514", description="Model")
class TokenEstimateResponse(BaseModel):
"""Response with token count estimate."""
token_count: int
model: str
content_length: int
+55
View File
@@ -0,0 +1,55 @@
"""
Orchestrator API Schemas
Request/response models for agent orchestrator endpoints.
"""
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
class AgentStatusResponse(BaseModel):
"""Status of a single agent."""
agent_id: str
state: str
task_id: str | None
error_count: int
started_at: datetime | None
waiting_for: str | None
class OrchestratorStatusResponse(BaseModel):
"""Overall orchestrator status."""
total_agents: int
by_state: dict[str, int]
waiting_count: int
agents: list[AgentStatusResponse]
class WaitingAgentResponse(BaseModel):
"""Agent in WAITING_LONG state."""
agent_id: str
task_id: str | None
waiting_for: str
waiting_since: datetime
context: dict[str, Any]
class SpawnAgentRequest(BaseModel):
"""Request to spawn an agent."""
agent_id: str
initial_prompt: str | None = None
task_id: str | None = None
model: str | None = None
class ResolveWaitRequest(BaseModel):
"""Request to resolve a wait condition."""
resolution: dict[str, Any] = Field(default_factory=dict)
+50
View File
@@ -0,0 +1,50 @@
"""
Sessions API Schemas
Request/response models for session endpoints.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from roboco.models import SessionStatus
class ListSessionsParams(BaseModel):
"""Query parameters for listing sessions."""
group_id: UUID
status_filter: SessionStatus | None = None
limit: int = Field(20, ge=1, le=100)
class SessionResponse(BaseModel):
"""Session response."""
id: UUID
group_id: UUID
status: SessionStatus
message_count: int
total_content_length: int
started_at: datetime
last_activity_at: datetime
closed_at: datetime | None
class SessionListResponse(BaseModel):
"""List of sessions."""
items: list[SessionResponse]
total: int
class SessionCreateRequest(BaseModel):
"""Request to create a session."""
group_id: UUID
max_time_window_minutes: int | None = 30
max_message_count: int | None = 100
max_content_length: int | None = 50000
timeout_seconds: int = 300
+60
View File
@@ -0,0 +1,60 @@
"""
Stream Processing API Schemas
Request/response models for stream processing endpoints.
"""
from uuid import UUID
from pydantic import BaseModel, Field
class StreamChunkRequest(BaseModel):
"""Request to process a stream chunk."""
channel_id: UUID = Field(..., description="Target channel")
session_id: UUID = Field(..., description="Current session")
chunk: str = Field(..., description="Raw LLM output chunk")
class StreamCompleteRequest(BaseModel):
"""Request to mark a stream as complete."""
session_id: UUID = Field(..., description="Session to complete")
class ExtractRequest(BaseModel):
"""Request to extract messages from content."""
channel_id: UUID = Field(..., description="Target channel")
session_id: UUID = Field(..., description="Current session")
group_id: UUID = Field(..., description="Group within channel")
content: str = Field(..., description="Content to extract from")
task_id: UUID | None = Field(default=None, description="Related task")
class ExtractedMessageResponse(BaseModel):
"""Response for an extracted message."""
id: UUID
type: str
content: str
content_length: int
confidence: float
class ExtractionResponse(BaseModel):
"""Response from extraction."""
message_count: int
messages: list[ExtractedMessageResponse]
types_extracted: list[str]
class TranscriptionStatsResponse(BaseModel):
"""Response for transcription service stats."""
active_agents: int
total_buffers: int
total_buffered_chars: int
running: bool
+107
View File
@@ -0,0 +1,107 @@
"""
Tasks API Schemas
Request/response models for task endpoints.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from roboco.models.base import Complexity, TaskStatus, Team
class TaskUpdate(BaseModel):
"""Request to update a task."""
title: str | None = None
description: str | None = None
acceptance_criteria: list[str] | None = None
priority: int | None = Field(default=None, ge=0, le=3)
target_date: datetime | None = None
estimated_complexity: Complexity | None = None
dev_notes: str | None = None
quick_context: str | None = None
class TaskResponse(BaseModel):
"""Task response model."""
id: UUID
title: str
description: str
acceptance_criteria: list[str]
status: TaskStatus
priority: int
team: Team
created_by: UUID
assigned_to: UUID | None
parent_task_id: UUID | None
dependency_ids: list[UUID]
blocker_ids: list[UUID]
created_at: datetime
updated_at: datetime | None
claimed_at: datetime | None
started_at: datetime | None
completed_at: datetime | None
target_date: datetime | None
estimated_complexity: Complexity
self_verified: bool
qa_verified: bool | None
dev_notes: str | None
qa_notes: str | None
quick_context: str | None
class Config:
from_attributes = True
class ProgressRequest(BaseModel):
"""Request to add progress update."""
message: str
percentage: int | None = Field(default=None, ge=0, le=100)
class CheckpointRequest(BaseModel):
"""Request to add checkpoint."""
state_summary: str
remaining_work: list[str]
notes: str | None = None
class CommitRequest(BaseModel):
"""Request to link a commit."""
hash: str = Field(..., min_length=7, max_length=40)
message: str
class QANotes(BaseModel):
"""QA review notes."""
notes: str
class TaskCountResponse(BaseModel):
"""Task count by category."""
counts: dict[str, int]
class ListTasksQuery(BaseModel):
"""Query params for listing tasks."""
team: Team | None = None
status: TaskStatus | None = None
limit: int = Field(100, ge=1, le=500)
offset: int = Field(0, ge=0)
class TeamTasksQuery(BaseModel):
"""Query params for team tasks."""
task_status: TaskStatus | None = None
limit: int = Field(100, ge=1, le=500)
+81
View File
@@ -0,0 +1,81 @@
"""
WebSocket API Schemas
Request/response models for WebSocket messages.
"""
from dataclasses import dataclass
from datetime import UTC, datetime
from uuid import UUID
from pydantic import BaseModel
@dataclass
class NewMessageBroadcast:
"""Data for broadcasting a new message."""
channel_id: UUID
session_id: UUID
message_id: UUID
agent_id: UUID
content: str
message_type: str
class WSMessage(BaseModel):
"""Base WebSocket message."""
type: str
timestamp: datetime = datetime.now(UTC)
class WSMessageNew(WSMessage):
"""New message event."""
type: str = "message.new"
message_id: UUID
agent_id: UUID
content: str
message_type: str
class WSMessageEdit(WSMessage):
"""Message edited event."""
type: str = "message.edit"
message_id: UUID
content: str
class WSMessageDelete(WSMessage):
"""Message deleted event."""
type: str = "message.delete"
message_id: UUID
class WSAgentStream(WSMessage):
"""Agent stream chunk."""
type: str = "agent.stream"
agent_id: UUID
chunk: str
class WSSessionClosed(WSMessage):
"""Session closed event."""
type: str = "session.closed"
session_id: UUID
reason: str
class WSNotification(WSMessage):
"""Notification event."""
type: str = "notification"
notification_id: UUID
notification_type: str
subject: str
priority: str
+3 -78
View File
@@ -9,30 +9,18 @@ Real-time communication via WebSocket connections for:
import asyncio import asyncio
import json import json
from dataclasses import dataclass
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
import httpx import httpx
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
from pydantic import BaseModel
from roboco.api.schemas.websocket import (
NewMessageBroadcast,
)
from roboco.config import settings from roboco.config import settings
@dataclass
class NewMessageBroadcast:
"""Data for broadcasting a new message."""
channel_id: UUID
session_id: UUID
message_id: UUID
agent_id: UUID
content: str
message_type: str
router = APIRouter() router = APIRouter()
@@ -215,69 +203,6 @@ async def validate_channel_access(channel_id: UUID, agent_id: UUID) -> bool:
return False return False
# =============================================================================
# WebSocket Message Types
# =============================================================================
class WSMessage(BaseModel):
"""Base WebSocket message."""
type: str
timestamp: datetime = datetime.now(UTC)
class WSMessageNew(WSMessage):
"""New message event."""
type: str = "message.new"
message_id: UUID
agent_id: UUID
content: str
message_type: str
class WSMessageEdit(WSMessage):
"""Message edited event."""
type: str = "message.edit"
message_id: UUID
content: str
class WSMessageDelete(WSMessage):
"""Message deleted event."""
type: str = "message.delete"
message_id: UUID
class WSAgentStream(WSMessage):
"""Agent stream chunk."""
type: str = "agent.stream"
agent_id: UUID
chunk: str
class WSSessionClosed(WSMessage):
"""Session closed event."""
type: str = "session.closed"
session_id: UUID
reason: str
class WSNotification(WSMessage):
"""Notification event."""
type: str = "notification"
notification_id: UUID
notification_type: str
subject: str
priority: str
# ============================================================================= # =============================================================================
# WebSocket Routes # WebSocket Routes
# ============================================================================= # =============================================================================
+1 -14
View File
@@ -4,23 +4,10 @@ Task Ownership Enforcement
Validates task ownership and claim rules. Validates task ownership and claim rules.
""" """
from dataclasses import dataclass
from roboco.agents_config import get_agent_role, get_agent_team from roboco.agents_config import get_agent_role, get_agent_team
from roboco.enforcement.task_lifecycle import is_waiting_state from roboco.enforcement.task_lifecycle import is_waiting_state
from roboco.exceptions import RobocoError from roboco.exceptions import RobocoError
from roboco.models.enforcement import TaskClaimContext
@dataclass
class TaskClaimContext:
"""Context for validating a task claim."""
agent_id: str
task_id: str
task_status: str
task_team: str
agent_active_tasks: list[dict]
agent_paused_tasks: list[dict]
class TaskOwnershipError(RobocoError): class TaskOwnershipError(RobocoError):
+1 -91
View File
@@ -6,108 +6,18 @@ Redis-based pub/sub event system for workflow triggers.
import asyncio import asyncio
import contextlib import contextlib
import json
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import Any from typing import Any
from uuid import UUID, uuid4
import redis.asyncio as redis import redis.asyncio as redis
import structlog import structlog
from roboco.config import settings from roboco.config import settings
from roboco.models.events import Event, EventType
logger = structlog.get_logger() logger = structlog.get_logger()
class EventType(str, Enum):
"""Types of events in the system."""
# Task lifecycle events
TASK_CREATED = "task.created"
TASK_CLAIMED = "task.claimed"
TASK_STARTED = "task.started"
TASK_BLOCKED = "task.blocked"
TASK_UNBLOCKED = "task.unblocked"
TASK_PAUSED = "task.paused"
TASK_RESUMED = "task.resumed"
TASK_VERIFYING = "task.verifying"
TASK_AWAITING_QA = "task.awaiting_qa"
TASK_QA_PASSED = "task.qa_passed"
TASK_QA_FAILED = "task.qa_failed"
TASK_AWAITING_DOCS = "task.awaiting_docs"
TASK_COMPLETED = "task.completed"
TASK_CANCELLED = "task.cancelled"
# Session events
SESSION_CREATED = "session.created"
SESSION_CLOSED = "session.closed"
SESSION_TIMEOUT = "session.timeout"
# Handoff events
HANDOFF_CREATED = "handoff.created"
HANDOFF_ACCEPTED = "handoff.accepted"
# Agent events
AGENT_SPAWNED = "agent.spawned"
AGENT_STOPPED = "agent.stopped"
AGENT_WAITING = "agent.waiting"
AGENT_RESUMED = "agent.resumed"
AGENT_ERROR = "agent.error"
# Notification events
NOTIFICATION_SENT = "notification.sent"
NOTIFICATION_ACKED = "notification.acked"
# Blocker events
BLOCKER_REPORTED = "blocker.reported"
BLOCKER_RESOLVED = "blocker.resolved"
# Question events
QUESTION_ASKED = "question.asked"
QUESTION_ANSWERED = "question.answered"
@dataclass
class Event:
"""An event in the system."""
type: EventType
data: dict[str, Any]
id: UUID = field(default_factory=uuid4)
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
source_agent: str | None = None
correlation_id: str | None = None # For tracking related events
def to_json(self) -> str:
"""Serialize to JSON."""
return json.dumps(
{
"id": str(self.id),
"type": self.type.value,
"data": self.data,
"timestamp": self.timestamp.isoformat(),
"source_agent": self.source_agent,
"correlation_id": self.correlation_id,
}
)
@classmethod
def from_json(cls, json_str: str) -> "Event":
"""Deserialize from JSON."""
data = json.loads(json_str)
return cls(
id=UUID(data["id"]),
type=EventType(data["type"]),
data=data["data"],
timestamp=datetime.fromisoformat(data["timestamp"]),
source_agent=data.get("source_agent"),
correlation_id=data.get("correlation_id"),
)
# Type for event handlers # Type for event handlers
EventHandler = Callable[[Event], Coroutine[Any, Any, None]] EventHandler = Callable[[Event], Coroutine[Any, Any, None]]
+6 -77
View File
@@ -5,91 +5,20 @@ Workflow trigger handlers that respond to system events.
Uses dependency injection pattern for services to maintain separation of concerns. Uses dependency injection pattern for services to maintain separation of concerns.
""" """
from dataclasses import dataclass from typing import Any
from typing import TYPE_CHECKING, Any, Protocol
import structlog import structlog
from roboco.events.bus import Event, EventType, get_event_bus from roboco.events.bus import Event, EventType, get_event_bus
from roboco.models.events import (
if TYPE_CHECKING: EventContext,
from roboco.runtime.orchestrator import WaitingRecord NotificationServiceProtocol,
OrchestratorAccessProtocol,
)
logger = structlog.get_logger() logger = structlog.get_logger()
# =============================================================================
# SERVICE PROTOCOLS (for dependency injection)
# =============================================================================
class NotificationServiceProtocol(Protocol):
"""Protocol for notification service."""
async def send_blocker_notification(
self,
task_id: str,
blocker_reason: str,
from_agent: str | None,
to_pm: str,
) -> None: ...
async def send_qa_ready_notification(
self,
task_id: str,
from_agent: str | None,
to_qa: str,
) -> None: ...
async def send_qa_failed_notification(
self,
task_id: str,
qa_notes: str,
to_developer: str,
) -> None: ...
async def send_docs_ready_notification(
self,
task_id: str,
from_agent: str | None,
to_documenter: str,
) -> None: ...
async def send_handoff_notification(
self,
task_id: str,
handoff_id: str,
from_agent: str | None,
to_documenter: str,
) -> None: ...
class OrchestratorAccessProtocol(Protocol):
"""Protocol for orchestrator access."""
def get_waiting_agents(self) -> dict[str, "WaitingRecord"]: ...
async def resolve_wait(self, agent_id: str, resolution: dict[str, Any]) -> Any: ...
# =============================================================================
# EVENT CONTEXT (dependency container)
# =============================================================================
@dataclass
class EventContext:
"""
Dependency container for event handlers.
Set once during application initialization, then used by all handlers.
This avoids runtime imports inside handler functions.
"""
notification_service: NotificationServiceProtocol | None = None
orchestrator: OrchestratorAccessProtocol | None = None
# Global context instance - set during initialization # Global context instance - set during initialization
_context = EventContext() _context = EventContext()
+1 -66
View File
@@ -4,72 +4,7 @@ TOON Metrics
Tracks token savings and usage statistics for TOON vs JSON serialization. Tracks token savings and usage statistics for TOON vs JSON serialization.
""" """
from dataclasses import dataclass, field from roboco.models.llm import ToonMetrics
from datetime import UTC, datetime
@dataclass
class ToonMetrics:
"""
Metrics for tracking TOON serialization efficiency.
Tracks character counts (as proxy for tokens) for JSON vs TOON
to measure actual savings in production.
"""
json_chars: int = 0
toon_chars: int = 0
encode_count: int = 0
decode_count: int = 0
decode_fallback_count: int = 0
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def savings_percent(self) -> float:
"""Calculate percentage of characters saved using TOON."""
if self.json_chars == 0:
return 0.0
return (1 - self.toon_chars / self.json_chars) * 100
@property
def fallback_rate(self) -> float:
"""Calculate rate of fallback to JSON decoding."""
if self.decode_count == 0:
return 0.0
return (self.decode_fallback_count / self.decode_count) * 100
def record_encode(self, json_chars: int, toon_chars: int) -> None:
"""Record an encode operation with character counts."""
self.json_chars += json_chars
self.toon_chars += toon_chars
self.encode_count += 1
def record_decode(self, used_fallback: bool = False) -> None:
"""Record a decode operation."""
self.decode_count += 1
if used_fallback:
self.decode_fallback_count += 1
def to_dict(self) -> dict:
"""Convert metrics to dictionary for logging/reporting."""
return {
"json_chars": self.json_chars,
"toon_chars": self.toon_chars,
"savings_percent": round(self.savings_percent, 2),
"encode_count": self.encode_count,
"decode_count": self.decode_count,
"fallback_rate": round(self.fallback_rate, 2),
"started_at": self.started_at.isoformat(),
}
def reset(self) -> None:
"""Reset all metrics."""
self.json_chars = 0
self.toon_chars = 0
self.encode_count = 0
self.decode_count = 0
self.decode_fallback_count = 0
self.started_at = datetime.now(UTC)
# Global metrics holder # Global metrics holder
+2 -10
View File
@@ -19,25 +19,17 @@ Usage:
""" """
import json import json
from dataclasses import dataclass
from typing import Any from typing import Any
import structlog import structlog
import toon import toon
from pydantic import BaseModel from pydantic import BaseModel
from roboco.models.llm import ToonConfig
logger = structlog.get_logger() logger = structlog.get_logger()
@dataclass
class ToonConfig:
"""Configuration for TOON encoding."""
delimiter: str = ","
indent: int = 2
include_length: bool = True
class ToonAdapter: class ToonAdapter:
""" """
Adapter for TOON serialization at LLM boundaries. Adapter for TOON serialization at LLM boundaries.
+7 -93
View File
@@ -19,107 +19,21 @@ from typing import Any
import httpx import httpx
from fastapi import status from fastapi import status
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from roboco.config import settings from roboco.config import settings
from roboco.llm import ToonAdapter from roboco.llm import ToonAdapter
from roboco.mcp.schemas import (
DecisionLogInput,
JournalEntryInput,
LearningInput,
StruggleInput,
TaskReflectionInput,
)
# Global TOON adapter for encoding journal data # Global TOON adapter for encoding journal data
_toon = ToonAdapter() _toon = ToonAdapter()
# =============================================================================
# INPUT MODELS (Pydantic models to reduce argument count)
# =============================================================================
class JournalEntryInput(BaseModel):
"""Input for creating a general journal entry."""
title: str = Field(..., description="Entry title (short description)")
content: str = Field(..., description="Entry content (detailed text)")
entry_type: str = Field(
default="general",
description="Type: general, task_reflection, decision_log, learning, struggle",
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
is_private: bool = Field(
default=False, description="If true, only you and CEO/Auditor can see"
)
class TaskReflectionInput(BaseModel):
"""Input for creating a task reflection entry."""
task_id: str = Field(..., description="The task UUID you're reflecting on")
title: str = Field(..., description="Reflection title")
what_done: str = Field(..., description="What was accomplished")
what_learned: str = Field(..., description="Key learnings from this task")
what_struggled: str = Field(..., description="What was difficult or challenging")
next_steps: list[str] = Field(
default_factory=list, description="Optional follow-up items"
)
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
class DecisionOption(BaseModel):
"""A decision option with pros/cons."""
option: str
pros_cons: str
class DecisionLogInput(BaseModel):
"""Input for logging a decision."""
title: str = Field(..., description="Decision title")
context: str = Field(..., description="What situation led to this decision")
options: list[DecisionOption] = Field(
..., min_length=2, description="Options considered (at least 2)"
)
chosen: str = Field(..., description="Which option was chosen")
rationale: str = Field(..., description="Why this option was chosen")
consequences: list[str] = Field(
default_factory=list, description="Expected consequences"
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
class LearningInput(BaseModel):
"""Input for logging a learning."""
title: str = Field(..., description="Learning title")
what_learned: str = Field(..., description="The actual learning/insight")
how_applied: str | None = Field(
default=None, description="How you applied or plan to apply this"
)
source: str | None = Field(
default=None, description="Where you learned this (docs, experiment, etc.)"
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
class StruggleInput(BaseModel):
"""Input for logging a struggle."""
title: str = Field(..., description="Struggle title")
what_struggled: str = Field(..., description="What the challenge was")
attempted_solutions: list[str] = Field(
default_factory=list, description="What you tried (even if it didn't work)"
)
resolution: str | None = Field(
default=None, description="How it was resolved (if resolved)"
)
help_needed: str | None = Field(
default=None, description="What help you need (if unresolved)"
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
# ============================================================================= # =============================================================================
# HELPER FUNCTIONS # HELPER FUNCTIONS
# ============================================================================= # =============================================================================
+5 -38
View File
@@ -19,35 +19,20 @@ from typing import Any
import httpx import httpx
from fastapi import status from fastapi import status
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from roboco.agents_config import CHANNEL_ACCESS from roboco.agents_config import CHANNEL_ACCESS
from roboco.config import settings from roboco.config import settings
from roboco.llm import ToonAdapter from roboco.llm import ToonAdapter
from roboco.mcp.schemas import (
AskQuestionInput,
ReportBlockerInput,
SendMessageInput,
)
# Global TOON adapter for encoding message data # Global TOON adapter for encoding message data
_toon = ToonAdapter() _toon = ToonAdapter()
# =============================================================================
# INPUT MODELS
# =============================================================================
class SendMessageInput(BaseModel):
"""Input for sending a message."""
channel_slug: str = Field(..., description="Channel slug (e.g., 'backend-cell')")
content: str = Field(..., description="Message content")
message_type: str = Field(
default="dialogue",
description="Type: reasoning, dialogue, decision, action, blocker, technical",
)
task_id: str | None = Field(default=None, description="Optional related task ID")
reply_to: str | None = Field(default=None, description="Message ID to reply to")
mentions: list[str] = Field(default_factory=list, description="Agents to mention")
# ============================================================================= # =============================================================================
# HELPER FUNCTIONS # HELPER FUNCTIONS
# ============================================================================= # =============================================================================
@@ -307,24 +292,6 @@ async def _handle_message_get(message_id: str) -> dict[str, Any]:
return {"message": resp.json()} return {"message": resp.json()}
class AskQuestionInput(BaseModel):
"""Input for asking a question."""
channel_slug: str
question: str
context: str | None = None
task_id: str | None = None
class ReportBlockerInput(BaseModel):
"""Input for reporting a blocker."""
channel_slug: str
blocker_description: str
what_needed: str
task_id: str | None = None
async def _handle_ask_question( async def _handle_ask_question(
data: AskQuestionInput, data: AskQuestionInput,
send_fn: Callable[[SendMessageInput], Awaitable[dict[str, Any]]], send_fn: Callable[[SendMessageInput], Awaitable[dict[str, Any]]],
+1 -20
View File
@@ -16,7 +16,6 @@ from typing import Any
import httpx import httpx
from fastapi import status from fastapi import status
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from roboco.agents_config import ( from roboco.agents_config import (
NOTIFICATION_PERMISSIONS, NOTIFICATION_PERMISSIONS,
@@ -24,25 +23,7 @@ from roboco.agents_config import (
get_agent_role, get_agent_role,
) )
from roboco.config import settings from roboco.config import settings
from roboco.mcp.schemas import SendNotificationInput
# =============================================================================
# INPUT MODELS
# =============================================================================
class SendNotificationInput(BaseModel):
"""Input for sending a notification."""
recipients: list[str] = Field(..., description="Agent IDs to notify")
subject: str = Field(..., description="Notification subject")
body: str = Field(..., description="Notification body")
notification_type: str = Field(
default="info", description="Type: info, alert, task, escalation, approval"
)
priority: str = Field(default="normal", description="low, normal, high, urgent")
requires_ack: bool = Field(default=True, description="Require acknowledgment")
related_task_id: str | None = Field(default=None, description="Related task")
# ============================================================================= # =============================================================================
# HELPER FUNCTIONS # HELPER FUNCTIONS
+154
View File
@@ -0,0 +1,154 @@
"""
MCP Input Schemas
Pydantic models for MCP tool input validation.
"""
from pydantic import BaseModel, Field
# =============================================================================
# JOURNAL SCHEMAS
# =============================================================================
class JournalEntryInput(BaseModel):
"""Input for creating a general journal entry."""
title: str = Field(..., description="Entry title (short description)")
content: str = Field(..., description="Entry content (detailed text)")
entry_type: str = Field(
default="general",
description="Type: general, task_reflection, decision_log, learning, struggle",
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
is_private: bool = Field(
default=False, description="If true, only you and CEO/Auditor can see"
)
class TaskReflectionInput(BaseModel):
"""Input for creating a task reflection entry."""
task_id: str = Field(..., description="The task UUID you're reflecting on")
title: str = Field(..., description="Reflection title")
what_done: str = Field(..., description="What was accomplished")
what_learned: str = Field(..., description="Key learnings from this task")
what_struggled: str = Field(..., description="What was difficult or challenging")
next_steps: list[str] = Field(
default_factory=list, description="Optional follow-up items"
)
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
class DecisionOption(BaseModel):
"""A decision option with pros/cons."""
option: str
pros_cons: str
class DecisionLogInput(BaseModel):
"""Input for logging a decision."""
title: str = Field(..., description="Decision title")
context: str = Field(..., description="What situation led to this decision")
options: list[DecisionOption] = Field(
..., min_length=2, description="Options considered (at least 2)"
)
chosen: str = Field(..., description="Which option was chosen")
rationale: str = Field(..., description="Why this option was chosen")
consequences: list[str] = Field(
default_factory=list, description="Expected consequences"
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
class LearningInput(BaseModel):
"""Input for logging a learning."""
title: str = Field(..., description="Learning title")
what_learned: str = Field(..., description="The actual learning/insight")
how_applied: str | None = Field(
default=None, description="How you applied or plan to apply this"
)
source: str | None = Field(
default=None, description="Where you learned this (docs, experiment, etc.)"
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
class StruggleInput(BaseModel):
"""Input for logging a struggle."""
title: str = Field(..., description="Struggle title")
what_struggled: str = Field(..., description="What the challenge was")
attempted_solutions: list[str] = Field(
default_factory=list, description="What you tried (even if it didn't work)"
)
resolution: str | None = Field(
default=None, description="How it was resolved (if resolved)"
)
help_needed: str | None = Field(
default=None, description="What help you need (if unresolved)"
)
task_id: str | None = Field(default=None, description="Optional related task")
tags: list[str] = Field(default_factory=list, description="Optional list of tags")
# =============================================================================
# MESSAGE SCHEMAS
# =============================================================================
class SendMessageInput(BaseModel):
"""Input for sending a message."""
channel_slug: str = Field(..., description="Channel slug (e.g., 'backend-cell')")
content: str = Field(..., description="Message content")
message_type: str = Field(
default="dialogue",
description="Type: reasoning, dialogue, decision, action, blocker, technical",
)
task_id: str | None = Field(default=None, description="Optional related task ID")
reply_to: str | None = Field(default=None, description="Message ID to reply to")
mentions: list[str] = Field(default_factory=list, description="Agents to mention")
class AskQuestionInput(BaseModel):
"""Input for asking a question."""
channel_slug: str
question: str
context: str | None = None
task_id: str | None = None
class ReportBlockerInput(BaseModel):
"""Input for reporting a blocker."""
channel_slug: str
blocker_description: str
what_needed: str
task_id: str | None = None
# =============================================================================
# NOTIFICATION SCHEMAS
# =============================================================================
class SendNotificationInput(BaseModel):
"""Input for sending a notification."""
recipients: list[str] = Field(..., description="Agent IDs to notify")
subject: str = Field(..., description="Notification subject")
body: str = Field(..., description="Notification body")
notification_type: str = Field(
default="info", description="Type: info, alert, task, escalation, approval"
)
priority: str = Field(default="normal", description="low, normal, high, urgent")
requires_ack: bool = Field(default=True, description="Require acknowledgment")
related_task_id: str | None = Field(default=None, description="Related task")
+383
View File
@@ -0,0 +1,383 @@
"""
Agent Models
Domain types for the agent system including phases, configs, and contexts.
"""
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import Any
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
from roboco.models import AgentRole, AgentStatus, Team
# =============================================================================
# MODEL PROVIDER
# =============================================================================
class ModelProvider(str, Enum):
"""LLM provider options."""
ANTHROPIC = "anthropic"
OPENAI = "openai"
LOCAL = "local"
# =============================================================================
# AGENT CONFIGURATION
# =============================================================================
class AgentConfig(BaseModel):
"""Configuration for an agent instance."""
# Identity
id: UUID = Field(default_factory=uuid4)
name: str
slug: str = Field(..., pattern=r"^[a-z0-9-]+$")
role: AgentRole
team: Team | None = None
# Model configuration
provider: ModelProvider = ModelProvider.ANTHROPIC
model: str = "claude-sonnet-4-20250514"
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
max_tokens: int = Field(default=4096, ge=1)
# System prompt (loaded from blueprints)
system_prompt: str
# Capabilities
capabilities: list[str] = Field(default_factory=list)
# Permissions
can_notify: bool = False
channel_ids: list[UUID] = Field(default_factory=list)
class AgentState(BaseModel):
"""Current state of an agent."""
status: AgentStatus = AgentStatus.OFFLINE
current_task_id: UUID | None = None
current_session_id: UUID | None = None
last_activity: datetime | None = None
error: str | None = None
# Metrics
messages_sent: int = 0
tasks_completed: int = 0
# =============================================================================
# DEVELOPER PHASES AND CONTEXT
# =============================================================================
class DevTaskPhase(str, Enum):
"""Phases of the developer task lifecycle."""
SCAN = "scan"
CLAIM = "claim"
UNDERSTAND = "understand"
PLAN = "plan"
EXECUTE = "execute"
VERIFY = "verify"
NOTES = "notes"
CLOSE = "close"
BLOCKED = "blocked"
@dataclass
class TaskContext:
"""Context for the current task being worked on by a developer."""
task_id: UUID
title: str
phase: DevTaskPhase = DevTaskPhase.CLAIM
subtasks: list[dict[str, Any]] = field(default_factory=list)
current_subtask: int = 0
blockers: list[str] = field(default_factory=list)
commits: list[str] = field(default_factory=list)
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
journal_entries: list[str] = field(default_factory=list)
# =============================================================================
# QA PHASES AND CONTEXT
# =============================================================================
class QATaskPhase(str, Enum):
"""Phases of the QA lifecycle."""
MONITOR = "monitor"
RECEIVE = "receive"
UNDERSTAND = "understand"
TEST = "test"
VERDICT = "verdict"
DOCUMENT = "document"
RETURN = "return"
class TestResult(str, Enum):
"""Test result outcomes."""
PASS = "pass"
FAIL = "fail"
BLOCKED = "blocked"
@dataclass
class TestCase:
"""A single test case."""
name: str
description: str
steps: list[str]
expected: str
result: TestResult | None = None
actual: str | None = None
notes: str | None = None
@dataclass
class ReviewContext:
"""Context for the current review being conducted by QA."""
task_id: UUID
title: str
phase: QATaskPhase = QATaskPhase.RECEIVE
test_cases: list[TestCase] = field(default_factory=list)
current_test: int = 0
findings: list[str] = field(default_factory=list)
verdict: TestResult | None = None
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
notes: list[str] = field(default_factory=list)
# =============================================================================
# PM PHASES AND CONTEXT
# =============================================================================
class CellPMPhase(str, Enum):
"""Phases of the Cell PM lifecycle."""
MONITOR = "monitor"
TRIAGE = "triage"
ASSIGN = "assign"
FACILITATE = "facilitate"
ESCALATE = "escalate"
TRACK = "track"
REPORT = "report"
class MainPMPhase(str, Enum):
"""Phases of the Main PM lifecycle."""
OVERSEE = "oversee"
RECEIVE = "receive"
PRIORITIZE = "prioritize"
COORDINATE = "coordinate"
DISTRIBUTE = "distribute"
REPORT_UP = "report_up"
FACILITATE = "facilitate"
@dataclass
class CellStatus:
"""Status of a cell."""
name: str
active_tasks: int = 0
blocked_tasks: int = 0
completed_today: int = 0
available_devs: int = 0
concerns: list[str] = field(default_factory=list)
@dataclass
class TaskAssignment:
"""A task assignment decision."""
task_id: UUID
agent_id: UUID
agent_name: str
reason: str
@dataclass
class Escalation:
"""An escalation to higher management."""
issue: str
severity: str # low, medium, high, critical
task_id: UUID | None = None
proposed_solution: str | None = None
# =============================================================================
# DOCUMENTER PHASES AND CONTEXT
# =============================================================================
class DocTaskPhase(str, Enum):
"""Phases of the Documenter lifecycle."""
MONITOR = "monitor"
RECEIVE = "receive"
GATHER = "gather"
SYNTHESIZE = "synthesize"
WRITE = "write"
REVIEW = "review"
PUBLISH = "publish"
class DocType(str, Enum):
"""Types of documentation."""
API = "api"
README = "readme"
ARCHITECTURE = "architecture"
CHANGELOG = "changelog"
KNOWLEDGE_BASE = "knowledge_base"
COMPONENT = "component"
DESIGN_SYSTEM = "design_system"
@dataclass
class DocumentSpec:
"""Specification for a document to create/update."""
doc_type: DocType
title: str
path: str
priority: str = "required" # required, optional
content: str | None = None
@dataclass
class DocContext:
"""Context for the current documentation task."""
task_id: UUID
title: str
phase: DocTaskPhase = DocTaskPhase.RECEIVE
# Gathered materials
dev_notes: str | None = None
qa_feedback: str | None = None
commits: list[str] = field(default_factory=list)
conversations: list[str] = field(default_factory=list)
code_changes: list[str] = field(default_factory=list)
# Synthesis
summary: str | None = None
documents_needed: list[DocumentSpec] = field(default_factory=list)
current_doc: int = 0
# Output
written_docs: list[str] = field(default_factory=list)
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
notes: list[str] = field(default_factory=list)
# =============================================================================
# BOARD PHASES AND CONTEXT
# =============================================================================
class ProductOwnerPhase(str, Enum):
"""Phases of the Product Owner lifecycle."""
VISION = "vision"
ROADMAP = "roadmap"
DEFINE = "define"
PRIORITIZE = "prioritize"
REVIEW = "review"
FEEDBACK = "feedback"
@dataclass
class Feature:
"""A feature or epic."""
id: UUID
title: str
description: str
acceptance_criteria: list[str]
priority: int # 0-3
status: str = "backlog"
class HeadMarketingPhase(str, Enum):
"""Phases of the Head of Marketing lifecycle."""
RESEARCH = "research"
STRATEGY = "strategy"
PLAN = "plan"
CREATE = "create"
EXECUTE = "execute"
ANALYZE = "analyze"
@dataclass
class Campaign:
"""A marketing campaign."""
id: UUID
name: str
objective: str
channels: list[str]
start_date: datetime | None = None
end_date: datetime | None = None
status: str = "planning"
metrics: dict[str, Any] = field(default_factory=dict)
class AuditorPhase(str, Enum):
"""Phases of the Auditor lifecycle."""
OBSERVE = "observe"
ANALYZE = "analyze"
FLAG = "flag"
REPORT = "report"
AUDIT = "audit"
ADVISE = "advise"
class AuditorFlagSeverity(str, Enum):
"""Severity of flagged issues from auditor."""
INFO = "info"
WARNING = "warning"
CONCERN = "concern"
CRITICAL = "critical"
@dataclass
class AuditFlag:
"""A flagged issue from audit observation."""
id: UUID
severity: AuditorFlagSeverity
category: str # quality, process, communication, efficiency
description: str
evidence: list[str]
recommendation: str | None = None
reported_to_ceo: bool = False
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
@dataclass
class AuditReport:
"""A report to the CEO."""
period: str
summary: str
flags: list[AuditFlag]
metrics: dict[str, Any]
recommendations: list[str]
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
+29
View File
@@ -0,0 +1,29 @@
"""
Enforcement Models
Domain types for enforcement rules.
"""
from dataclasses import dataclass
@dataclass
class TaskClaimContext:
"""Context for validating a task claim."""
agent_id: str
task_id: str
task_status: str
task_team: str
agent_active_tasks: list[dict]
agent_paused_tasks: list[dict]
@dataclass
class OwnershipContext:
"""Context for validating task ownership."""
agent_id: str
task_id: str
current_owner: str | None
current_status: str
+173
View File
@@ -0,0 +1,173 @@
"""
Event Models
Domain types for the event bus system.
"""
import json
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from typing import TYPE_CHECKING, Any, Protocol
from uuid import UUID, uuid4
if TYPE_CHECKING:
from roboco.runtime.orchestrator import WaitingRecord
class EventType(str, Enum):
"""Types of events in the system."""
# Task lifecycle events
TASK_CREATED = "task.created"
TASK_CLAIMED = "task.claimed"
TASK_STARTED = "task.started"
TASK_BLOCKED = "task.blocked"
TASK_UNBLOCKED = "task.unblocked"
TASK_PAUSED = "task.paused"
TASK_RESUMED = "task.resumed"
TASK_VERIFYING = "task.verifying"
TASK_AWAITING_QA = "task.awaiting_qa"
TASK_QA_PASSED = "task.qa_passed"
TASK_QA_FAILED = "task.qa_failed"
TASK_AWAITING_DOCS = "task.awaiting_docs"
TASK_COMPLETED = "task.completed"
TASK_CANCELLED = "task.cancelled"
# Session events
SESSION_CREATED = "session.created"
SESSION_CLOSED = "session.closed"
SESSION_TIMEOUT = "session.timeout"
# Handoff events
HANDOFF_CREATED = "handoff.created"
HANDOFF_ACCEPTED = "handoff.accepted"
# Agent events
AGENT_SPAWNED = "agent.spawned"
AGENT_STOPPED = "agent.stopped"
AGENT_WAITING = "agent.waiting"
AGENT_RESUMED = "agent.resumed"
AGENT_ERROR = "agent.error"
# Notification events
NOTIFICATION_SENT = "notification.sent"
NOTIFICATION_ACKED = "notification.acked"
# Blocker events
BLOCKER_REPORTED = "blocker.reported"
BLOCKER_RESOLVED = "blocker.resolved"
# Question events
QUESTION_ASKED = "question.asked"
QUESTION_ANSWERED = "question.answered"
@dataclass
class Event:
"""An event in the system."""
type: EventType
data: dict[str, Any]
id: UUID = field(default_factory=uuid4)
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
source_agent: str | None = None
correlation_id: str | None = None # For tracking related events
def to_json(self) -> str:
"""Serialize to JSON."""
return json.dumps(
{
"id": str(self.id),
"type": self.type.value,
"data": self.data,
"timestamp": self.timestamp.isoformat(),
"source_agent": self.source_agent,
"correlation_id": self.correlation_id,
}
)
@classmethod
def from_json(cls, json_str: str) -> "Event":
"""Deserialize from JSON."""
data = json.loads(json_str)
return cls(
id=UUID(data["id"]),
type=EventType(data["type"]),
data=data["data"],
timestamp=datetime.fromisoformat(data["timestamp"]),
source_agent=data.get("source_agent"),
correlation_id=data.get("correlation_id"),
)
# =============================================================================
# SERVICE PROTOCOLS (for dependency injection)
# =============================================================================
class NotificationServiceProtocol(Protocol):
"""Protocol for notification service."""
async def send_blocker_notification(
self,
task_id: str,
blocker_reason: str,
from_agent: str | None,
to_pm: str,
) -> None: ...
async def send_qa_ready_notification(
self,
task_id: str,
from_agent: str | None,
to_qa: str,
) -> None: ...
async def send_qa_failed_notification(
self,
task_id: str,
qa_notes: str,
to_developer: str,
) -> None: ...
async def send_docs_ready_notification(
self,
task_id: str,
from_agent: str | None,
to_documenter: str,
) -> None: ...
async def send_handoff_notification(
self,
task_id: str,
handoff_id: str,
from_agent: str | None,
to_documenter: str,
) -> None: ...
class OrchestratorAccessProtocol(Protocol):
"""Protocol for orchestrator access."""
def get_waiting_agents(self) -> dict[str, "WaitingRecord"]: ...
async def resolve_wait(self, agent_id: str, resolution: dict[str, Any]) -> Any: ...
# =============================================================================
# EVENT CONTEXT (dependency container)
# =============================================================================
@dataclass
class EventContext:
"""
Dependency container for event handlers.
Set once during application initialization, then used by all handlers.
This avoids runtime imports inside handler functions.
"""
notification_service: NotificationServiceProtocol | None = None
orchestrator: OrchestratorAccessProtocol | None = None
+118
View File
@@ -0,0 +1,118 @@
"""
LLM Models
Domain types for LLM integration.
"""
from dataclasses import dataclass, field
from datetime import UTC, datetime
@dataclass
class ToonConfig:
"""Configuration for TOON encoding."""
delimiter: str = ","
indent: int = 2
include_length: bool = True
@dataclass
class EncodedBlock:
"""A TOON-encoded block with metadata."""
content: str
label: str
token_estimate: int = 0
def __str__(self) -> str:
"""Return the formatted block."""
return f"[{self.label}]\n{self.content}"
@dataclass
class LLMUsage:
"""Token usage statistics for an LLM call."""
input_tokens: int = 0
output_tokens: int = 0
cache_creation_input_tokens: int = 0
cache_read_input_tokens: int = 0
@property
def total_tokens(self) -> int:
"""Total tokens used."""
return self.input_tokens + self.output_tokens
@property
def total_input_with_cache(self) -> int:
"""Total input including cache operations."""
return (
self.input_tokens
+ self.cache_creation_input_tokens
+ self.cache_read_input_tokens
)
@dataclass
class ToonMetrics:
"""
Metrics for tracking TOON serialization efficiency.
Tracks character counts (as proxy for tokens) for JSON vs TOON
to measure actual savings in production.
"""
json_chars: int = 0
toon_chars: int = 0
encode_count: int = 0
decode_count: int = 0
decode_fallback_count: int = 0
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def savings_percent(self) -> float:
"""Calculate percentage of characters saved using TOON."""
if self.json_chars == 0:
return 0.0
return (1 - self.toon_chars / self.json_chars) * 100
@property
def fallback_rate(self) -> float:
"""Calculate rate of fallback to JSON decoding."""
if self.decode_count == 0:
return 0.0
return (self.decode_fallback_count / self.decode_count) * 100
def record_encode(self, json_chars: int, toon_chars: int) -> None:
"""Record an encode operation with character counts."""
self.json_chars += json_chars
self.toon_chars += toon_chars
self.encode_count += 1
def record_decode(self, used_fallback: bool = False) -> None:
"""Record a decode operation."""
self.decode_count += 1
if used_fallback:
self.decode_fallback_count += 1
def to_dict(self) -> dict:
"""Convert metrics to dictionary for logging/reporting."""
return {
"json_chars": self.json_chars,
"toon_chars": self.toon_chars,
"savings_percent": round(self.savings_percent, 2),
"encode_count": self.encode_count,
"decode_count": self.decode_count,
"fallback_rate": round(self.fallback_rate, 2),
"started_at": self.started_at.isoformat(),
}
def reset(self) -> None:
"""Reset all metrics."""
self.json_chars = 0
self.toon_chars = 0
self.encode_count = 0
self.decode_count = 0
self.decode_fallback_count = 0
self.started_at = datetime.now(UTC)
+157
View File
@@ -0,0 +1,157 @@
"""
Organization Models
Domain types for the organizational structure (cells, board, organization).
"""
from dataclasses import dataclass
from typing import TYPE_CHECKING
from uuid import UUID
import structlog
from roboco.models import Team
if TYPE_CHECKING:
from roboco.agents.base import Agent
from roboco.agents.board import (
AuditorAgent,
HeadMarketingAgent,
ProductOwnerAgent,
)
from roboco.agents.developer import DeveloperAgent
from roboco.agents.documenter import DocumenterAgent
from roboco.agents.pm import CellPMAgent, MainPMAgent
from roboco.agents.qa import QAAgent
logger = structlog.get_logger()
@dataclass
class Cell:
"""A complete cell with all its agents."""
name: str
team: Team
pm: "CellPMAgent"
developers: list["DeveloperAgent"]
qa: "QAAgent"
documenter: "DocumenterAgent"
@property
def all_agents(self) -> list["Agent"]:
"""Get all agents in the cell."""
return [self.pm, *self.developers, self.qa, self.documenter]
async def start_all(self) -> None:
"""Start all agents in the cell."""
for agent in self.all_agents:
await agent.start()
logger.info("Cell started", cell=self.name, agents=len(self.all_agents))
async def stop_all(self) -> None:
"""Stop all agents in the cell."""
for agent in self.all_agents:
await agent.stop()
logger.info("Cell stopped", cell=self.name)
@dataclass
class Board:
"""The board level with all board agents."""
product_owner: "ProductOwnerAgent"
head_marketing: "HeadMarketingAgent"
auditor: "AuditorAgent"
@property
def all_agents(self) -> list["Agent"]:
"""Get all board agents."""
return [self.product_owner, self.head_marketing, self.auditor]
async def start_all(self) -> None:
"""Start all board agents."""
for agent in self.all_agents:
await agent.start()
logger.info("Board started", agents=len(self.all_agents))
async def stop_all(self) -> None:
"""Stop all board agents."""
for agent in self.all_agents:
await agent.stop()
logger.info("Board stopped")
@dataclass
class Organization:
"""The complete AI organization."""
board: Board
main_pm: "MainPMAgent"
backend_cell: Cell
frontend_cell: Cell
ux_cell: Cell
@property
def all_agents(self) -> list["Agent"]:
"""Get all agents in the organization."""
agents: list[Agent] = []
agents.extend(self.board.all_agents)
agents.append(self.main_pm)
agents.extend(self.backend_cell.all_agents)
agents.extend(self.frontend_cell.all_agents)
agents.extend(self.ux_cell.all_agents)
return agents
@property
def agent_count(self) -> int:
"""Total number of agents."""
return len(self.all_agents)
async def start_all(self) -> None:
"""Start the entire organization."""
logger.info("Starting organization")
# Start board first
await self.board.start_all()
await self.main_pm.start()
# Then cells
await self.backend_cell.start_all()
await self.frontend_cell.start_all()
await self.ux_cell.start_all()
logger.info("Organization started", total_agents=self.agent_count)
async def stop_all(self) -> None:
"""Stop the entire organization."""
logger.info("Stopping organization")
# Stop cells first
await self.ux_cell.stop_all()
await self.frontend_cell.stop_all()
await self.backend_cell.stop_all()
# Then management
await self.main_pm.stop()
await self.board.stop_all()
logger.info("Organization stopped")
def get_agent_by_id(self, agent_id: UUID) -> "Agent | None":
"""Find an agent by ID."""
for agent in self.all_agents:
if agent.id == agent_id:
return agent
return None
def get_agent_by_slug(self, slug: str) -> "Agent | None":
"""Find an agent by slug."""
for agent in self.all_agents:
if agent.config.slug == slug:
return agent
return None
def get_agents_by_team(self, team: Team) -> list["Agent"]:
"""Get all agents in a team."""
return [a for a in self.all_agents if a.team == team]
+90
View File
@@ -0,0 +1,90 @@
"""
Runtime Models
Domain types for the agent orchestrator system.
"""
import asyncio
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any
from uuid import UUID, uuid4
class OrchestratorAgentState(str, Enum):
"""Agent lifecycle states in the orchestrator."""
OFFLINE = "offline"
STARTING = "starting"
ACTIVE = "active"
WAITING_SHORT = "waiting_short" # Polling, agent still running
WAITING_LONG = "waiting_long" # Terminated, will respawn on event
IDLE = "idle"
STOPPING = "stopping"
@dataclass
class OrchestratorAgentConfig:
"""Configuration for an agent in the orchestrator."""
agent_id: str
blueprint_path: Path
model: str = "sonnet" # sonnet, opus, haiku
mcp_config_path: Path | None = None
working_directory: Path | None = None
@dataclass
class AgentInstance:
"""A running Claude Code agent instance."""
id: UUID = field(default_factory=uuid4)
agent_id: str = ""
state: OrchestratorAgentState = OrchestratorAgentState.OFFLINE
process: asyncio.subprocess.Process | None = None
config: OrchestratorAgentConfig | None = None
started_at: datetime | None = None
last_activity: datetime | None = None
current_task_id: str | None = None
error_count: int = 0
waiting_for: str | None = None # For WAITING_LONG state
waiting_context: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.id:
self.id = uuid4()
@dataclass
class WaitingRecord:
"""Tracks what a WAITING_LONG agent is waiting for."""
agent_id: str
task_id: str | None
waiting_for: str # "blocker_resolution", "qa_result", "answer", "assignment"
waiting_since: datetime
context: dict[str, Any] = field(default_factory=dict)
# Model mapping for cost optimization
MODEL_MAP: dict[str, str] = {
"sonnet": "claude-sonnet-4-20250514",
"opus": "claude-opus-4-20250514",
"haiku": "claude-haiku-4-20250514",
}
# Default model by role
ROLE_MODEL_MAP: dict[str, str] = {
"developer": "sonnet",
"qa": "sonnet",
"documenter": "haiku",
"cell_pm": "sonnet",
"main_pm": "sonnet",
"auditor": "sonnet",
"product_owner": "opus",
"head_marketing": "opus",
"ceo": "opus",
}
+12 -99
View File
@@ -10,113 +10,26 @@ import contextlib
import json import json
import os import os
import tempfile import tempfile
from dataclasses import dataclass, field
from datetime import UTC, datetime from datetime import UTC, datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from uuid import UUID, uuid4
import structlog import structlog
from roboco.models.runtime import (
MODEL_MAP,
ROLE_MODEL_MAP,
AgentInstance,
OrchestratorAgentConfig,
OrchestratorAgentState,
WaitingRecord,
)
logger = structlog.get_logger() logger = structlog.get_logger()
# Re-export for backwards compatibility
# ============================================================================= AgentState = OrchestratorAgentState
# AGENT STATE AgentConfig = OrchestratorAgentConfig
# =============================================================================
class AgentState(str, Enum):
"""Agent lifecycle states."""
OFFLINE = "offline"
STARTING = "starting"
ACTIVE = "active"
WAITING_SHORT = "waiting_short" # Polling, agent still running
WAITING_LONG = "waiting_long" # Terminated, will respawn on event
IDLE = "idle"
STOPPING = "stopping"
# =============================================================================
# AGENT CONFIGURATION
# =============================================================================
@dataclass
class AgentConfig:
"""Configuration for an agent."""
agent_id: str
blueprint_path: Path
model: str = "sonnet" # sonnet, opus, haiku
mcp_config_path: Path | None = None
working_directory: Path | None = None
# Model mapping for cost optimization
MODEL_MAP = {
"sonnet": "claude-sonnet-4-20250514",
"opus": "claude-opus-4-20250514",
"haiku": "claude-haiku-4-20250514",
}
# Default model by role
ROLE_MODEL_MAP = {
"developer": "sonnet",
"qa": "sonnet",
"documenter": "haiku",
"cell_pm": "sonnet",
"main_pm": "sonnet",
"auditor": "sonnet",
"product_owner": "opus",
"head_marketing": "opus",
"ceo": "opus",
}
# =============================================================================
# AGENT INSTANCE
# =============================================================================
@dataclass
class AgentInstance:
"""A running Claude Code agent instance."""
id: UUID = field(default_factory=uuid4)
agent_id: str = ""
state: AgentState = AgentState.OFFLINE
process: asyncio.subprocess.Process | None = None
config: AgentConfig | None = None
started_at: datetime | None = None
last_activity: datetime | None = None
current_task_id: str | None = None
error_count: int = 0
waiting_for: str | None = None # For WAITING_LONG state
waiting_context: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.id:
self.id = uuid4()
# =============================================================================
# WAITING RECORD
# =============================================================================
@dataclass
class WaitingRecord:
"""Tracks what a WAITING_LONG agent is waiting for."""
agent_id: str
task_id: str | None
waiting_for: str # "blocker_resolution", "qa_result", "answer", "assignment"
waiting_since: datetime
context: dict[str, Any] = field(default_factory=dict)
# ============================================================================= # =============================================================================