forked from cardosofelipe/fast-next-template
- Add EventBus class for real-time event communication - Add Event schema with type-safe event types (agent, issue, sprint events) - Add typed payload schemas (AgentSpawnedPayload, AgentMessagePayload) - Add channel helpers for project/agent/user scoping - Add subscribe_sse generator for SSE streaming - Add reconnection support via Last-Event-ID - Add keepalive mechanism for connection health - Add 44 comprehensive tests with mocked Redis Implements #33 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
276 lines
9.3 KiB
Python
276 lines
9.3 KiB
Python
"""
|
|
Event schemas for the Syndarix EventBus (Redis Pub/Sub).
|
|
|
|
This module defines event types and payload schemas for real-time communication
|
|
between services, agents, and the frontend.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class EventType(str, Enum):
|
|
"""
|
|
Event types for the EventBus.
|
|
|
|
Naming convention: {domain}.{action}
|
|
"""
|
|
|
|
# Agent Events
|
|
AGENT_SPAWNED = "agent.spawned"
|
|
AGENT_STATUS_CHANGED = "agent.status_changed"
|
|
AGENT_MESSAGE = "agent.message"
|
|
AGENT_TERMINATED = "agent.terminated"
|
|
|
|
# Issue Events
|
|
ISSUE_CREATED = "issue.created"
|
|
ISSUE_UPDATED = "issue.updated"
|
|
ISSUE_ASSIGNED = "issue.assigned"
|
|
ISSUE_CLOSED = "issue.closed"
|
|
|
|
# Sprint Events
|
|
SPRINT_STARTED = "sprint.started"
|
|
SPRINT_COMPLETED = "sprint.completed"
|
|
|
|
# Approval Events
|
|
APPROVAL_REQUESTED = "approval.requested"
|
|
APPROVAL_GRANTED = "approval.granted"
|
|
APPROVAL_DENIED = "approval.denied"
|
|
|
|
# Project Events
|
|
PROJECT_CREATED = "project.created"
|
|
PROJECT_UPDATED = "project.updated"
|
|
PROJECT_ARCHIVED = "project.archived"
|
|
|
|
# Workflow Events
|
|
WORKFLOW_STARTED = "workflow.started"
|
|
WORKFLOW_STEP_COMPLETED = "workflow.step_completed"
|
|
WORKFLOW_COMPLETED = "workflow.completed"
|
|
WORKFLOW_FAILED = "workflow.failed"
|
|
|
|
|
|
ActorType = Literal["agent", "user", "system"]
|
|
|
|
|
|
class Event(BaseModel):
|
|
"""
|
|
Base event schema for the EventBus.
|
|
|
|
All events published to the EventBus must conform to this schema.
|
|
"""
|
|
|
|
id: str = Field(
|
|
...,
|
|
description="Unique event identifier (UUID string)",
|
|
examples=["550e8400-e29b-41d4-a716-446655440000"],
|
|
)
|
|
type: EventType = Field(
|
|
...,
|
|
description="Event type enum value",
|
|
examples=[EventType.AGENT_MESSAGE],
|
|
)
|
|
timestamp: datetime = Field(
|
|
...,
|
|
description="When the event occurred (UTC)",
|
|
examples=["2024-01-15T10:30:00Z"],
|
|
)
|
|
project_id: UUID = Field(
|
|
...,
|
|
description="Project this event belongs to",
|
|
examples=["550e8400-e29b-41d4-a716-446655440001"],
|
|
)
|
|
actor_id: UUID | None = Field(
|
|
default=None,
|
|
description="ID of the agent or user who triggered the event",
|
|
examples=["550e8400-e29b-41d4-a716-446655440002"],
|
|
)
|
|
actor_type: ActorType = Field(
|
|
...,
|
|
description="Type of actor: 'agent', 'user', or 'system'",
|
|
examples=["agent"],
|
|
)
|
|
payload: dict = Field(
|
|
default_factory=dict,
|
|
description="Event-specific payload data",
|
|
)
|
|
|
|
model_config = {
|
|
"json_schema_extra": {
|
|
"example": {
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"type": "agent.message",
|
|
"timestamp": "2024-01-15T10:30:00Z",
|
|
"project_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
"actor_id": "550e8400-e29b-41d4-a716-446655440002",
|
|
"actor_type": "agent",
|
|
"payload": {"message": "Processing task...", "progress": 50},
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
# Specific payload schemas for type safety
|
|
|
|
|
|
class AgentSpawnedPayload(BaseModel):
|
|
"""Payload for AGENT_SPAWNED events."""
|
|
|
|
agent_instance_id: UUID = Field(..., description="ID of the spawned agent instance")
|
|
agent_type_id: UUID = Field(..., description="ID of the agent type")
|
|
agent_name: str = Field(..., description="Human-readable name of the agent")
|
|
role: str = Field(..., description="Agent role (e.g., 'product_owner', 'engineer')")
|
|
|
|
|
|
class AgentStatusChangedPayload(BaseModel):
|
|
"""Payload for AGENT_STATUS_CHANGED events."""
|
|
|
|
agent_instance_id: UUID = Field(..., description="ID of the agent instance")
|
|
previous_status: str = Field(..., description="Previous status")
|
|
new_status: str = Field(..., description="New status")
|
|
reason: str | None = Field(default=None, description="Reason for status change")
|
|
|
|
|
|
class AgentMessagePayload(BaseModel):
|
|
"""Payload for AGENT_MESSAGE events."""
|
|
|
|
agent_instance_id: UUID = Field(..., description="ID of the agent instance")
|
|
message: str = Field(..., description="Message content")
|
|
message_type: str = Field(
|
|
default="info",
|
|
description="Message type: 'info', 'warning', 'error', 'debug'",
|
|
)
|
|
metadata: dict = Field(
|
|
default_factory=dict,
|
|
description="Additional metadata (e.g., token usage, model info)",
|
|
)
|
|
|
|
|
|
class AgentTerminatedPayload(BaseModel):
|
|
"""Payload for AGENT_TERMINATED events."""
|
|
|
|
agent_instance_id: UUID = Field(..., description="ID of the agent instance")
|
|
termination_reason: str = Field(..., description="Reason for termination")
|
|
final_status: str = Field(..., description="Final status at termination")
|
|
|
|
|
|
class IssueCreatedPayload(BaseModel):
|
|
"""Payload for ISSUE_CREATED events."""
|
|
|
|
issue_id: str = Field(..., description="Issue ID (from external tracker)")
|
|
title: str = Field(..., description="Issue title")
|
|
priority: str | None = Field(default=None, description="Issue priority")
|
|
labels: list[str] = Field(default_factory=list, description="Issue labels")
|
|
|
|
|
|
class IssueUpdatedPayload(BaseModel):
|
|
"""Payload for ISSUE_UPDATED events."""
|
|
|
|
issue_id: str = Field(..., description="Issue ID (from external tracker)")
|
|
changes: dict = Field(..., description="Dictionary of field changes")
|
|
|
|
|
|
class IssueAssignedPayload(BaseModel):
|
|
"""Payload for ISSUE_ASSIGNED events."""
|
|
|
|
issue_id: str = Field(..., description="Issue ID (from external tracker)")
|
|
assignee_id: UUID | None = Field(
|
|
default=None, description="Agent or user assigned to"
|
|
)
|
|
assignee_name: str | None = Field(default=None, description="Assignee name")
|
|
|
|
|
|
class IssueClosedPayload(BaseModel):
|
|
"""Payload for ISSUE_CLOSED events."""
|
|
|
|
issue_id: str = Field(..., description="Issue ID (from external tracker)")
|
|
resolution: str = Field(..., description="Resolution status")
|
|
|
|
|
|
class SprintStartedPayload(BaseModel):
|
|
"""Payload for SPRINT_STARTED events."""
|
|
|
|
sprint_id: UUID = Field(..., description="Sprint ID")
|
|
sprint_name: str = Field(..., description="Sprint name")
|
|
goal: str | None = Field(default=None, description="Sprint goal")
|
|
issue_count: int = Field(default=0, description="Number of issues in sprint")
|
|
|
|
|
|
class SprintCompletedPayload(BaseModel):
|
|
"""Payload for SPRINT_COMPLETED events."""
|
|
|
|
sprint_id: UUID = Field(..., description="Sprint ID")
|
|
sprint_name: str = Field(..., description="Sprint name")
|
|
completed_issues: int = Field(default=0, description="Number of completed issues")
|
|
incomplete_issues: int = Field(
|
|
default=0, description="Number of incomplete issues"
|
|
)
|
|
|
|
|
|
class ApprovalRequestedPayload(BaseModel):
|
|
"""Payload for APPROVAL_REQUESTED events."""
|
|
|
|
approval_id: UUID = Field(..., description="Approval request ID")
|
|
approval_type: str = Field(..., description="Type of approval needed")
|
|
description: str = Field(..., description="Description of what needs approval")
|
|
requested_by: UUID | None = Field(
|
|
default=None, description="Agent/user requesting approval"
|
|
)
|
|
timeout_minutes: int | None = Field(
|
|
default=None, description="Minutes before auto-escalation"
|
|
)
|
|
|
|
|
|
class ApprovalGrantedPayload(BaseModel):
|
|
"""Payload for APPROVAL_GRANTED events."""
|
|
|
|
approval_id: UUID = Field(..., description="Approval request ID")
|
|
approved_by: UUID = Field(..., description="User who granted approval")
|
|
comments: str | None = Field(default=None, description="Approval comments")
|
|
|
|
|
|
class ApprovalDeniedPayload(BaseModel):
|
|
"""Payload for APPROVAL_DENIED events."""
|
|
|
|
approval_id: UUID = Field(..., description="Approval request ID")
|
|
denied_by: UUID = Field(..., description="User who denied approval")
|
|
reason: str = Field(..., description="Reason for denial")
|
|
|
|
|
|
class WorkflowStartedPayload(BaseModel):
|
|
"""Payload for WORKFLOW_STARTED events."""
|
|
|
|
workflow_id: UUID = Field(..., description="Workflow execution ID")
|
|
workflow_type: str = Field(..., description="Type of workflow")
|
|
total_steps: int = Field(default=0, description="Total number of steps")
|
|
|
|
|
|
class WorkflowStepCompletedPayload(BaseModel):
|
|
"""Payload for WORKFLOW_STEP_COMPLETED events."""
|
|
|
|
workflow_id: UUID = Field(..., description="Workflow execution ID")
|
|
step_name: str = Field(..., description="Name of completed step")
|
|
step_number: int = Field(..., description="Step number (1-indexed)")
|
|
total_steps: int = Field(..., description="Total number of steps")
|
|
result: dict = Field(default_factory=dict, description="Step result data")
|
|
|
|
|
|
class WorkflowCompletedPayload(BaseModel):
|
|
"""Payload for WORKFLOW_COMPLETED events."""
|
|
|
|
workflow_id: UUID = Field(..., description="Workflow execution ID")
|
|
duration_seconds: float = Field(..., description="Total execution duration")
|
|
result: dict = Field(default_factory=dict, description="Workflow result data")
|
|
|
|
|
|
class WorkflowFailedPayload(BaseModel):
|
|
"""Payload for WORKFLOW_FAILED events."""
|
|
|
|
workflow_id: UUID = Field(..., description="Workflow execution ID")
|
|
error_message: str = Field(..., description="Error message")
|
|
failed_step: str | None = Field(default=None, description="Step that failed")
|
|
recoverable: bool = Field(default=False, description="Whether error is recoverable")
|