Files
roboco/roboco/api/schemas/a2a_chat.py
T
da563487b8 Wave 2 features: A2A live view (CEO chime-in + reply budget) and prompter memory (#297)
* feat(a2a): live view — watch fleet conversations, CEO chime-in, reply budget

A2A_MESSAGE_SENT published from A2AService.send (excerpt-capped) and
fanned through the existing /ws/system bridge; CEO-only admin REST for
conversations/messages + a reply route on the publish-bearing send path;
panel /a2a page with live transcript and a composer gated on task-linked
conversations. The matrix gains its one asymmetric rule: CEO may message
anyone, nobody may target the CEO — and agent replies inside a
CEO-opened conversation are hard-budgeted to one per CEO message
(per conversation, per agent), rejected with wait-don't-retry guidance.
Built subagent-driven (Sonnet 5), reviewed; v1 seams documented in the
map delta.

* feat(prompter): intake remembers the task history

Intake spawns now carry a per-project chronological digest of recent
tasks (capped: 15 lines/project, 4000 chars total — ~300-1000 tokens)
merged into the ambient layer, and the interviewer gets a bounded
search_past_tasks tool (one shared implementation behind the grok MCP
tool and the Claude SDK in-process tool) to check precedent
mid-conversation. Informational memory only — the sequencing analyzer
keeps ownership of ordering. Built subagent-driven (Sonnet 5), reviewed;
pre-existing conventions-ambient MegaTask-scope gap flagged, untouched.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-03 00:07:55 +02:00

197 lines
5.0 KiB
Python

"""
A2A Chat API Schemas
Request/response models for persistent A2A conversation endpoints.
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field
from roboco.models.a2a import A2AConversationStatus, A2AMessageKind
# =============================================================================
# CONVERSATION SCHEMAS
# =============================================================================
class ConversationCreateRequest(BaseModel):
"""Request to create/start a conversation."""
target_agent: str = Field(..., description="Agent slug to chat with")
topic: str | None = Field(default=None, description="Optional topic")
task_id: UUID | None = Field(default=None, description="Optional task link")
initial_message: str = Field(..., min_length=1, max_length=10000)
requires_response: bool = Field(default=False)
class ConversationCloseRequest(BaseModel):
"""Request to close a conversation."""
resolution: str | None = Field(default=None, description="Why closing")
class ConversationResponse(BaseModel):
"""Conversation response."""
id: UUID
agent_a: str
agent_b: str
topic: str | None
task_id: UUID | None
status: A2AConversationStatus
resolution: str | None
message_count: int
unread_by_a: int
unread_by_b: int
created_at: datetime
updated_at: datetime
last_message_at: datetime | None
class ConversationSummaryResponse(BaseModel):
"""Summary for list views."""
id: UUID
other_agent: str
topic: str | None
task_id: UUID | None
status: A2AConversationStatus
message_count: int
unread_count: int
last_message_at: datetime | None
last_message_preview: str | None
class ConversationListResponse(BaseModel):
"""List of conversation summaries."""
items: list[ConversationSummaryResponse]
total: int
class ListConversationsParams(BaseModel):
"""Query params for listing conversations."""
status: A2AConversationStatus | None = None
with_agent: str | None = None
task_id: UUID | None = None
limit: int = Field(50, ge=1, le=100)
# =============================================================================
# MESSAGE SCHEMAS
# =============================================================================
class MessageCreateRequest(BaseModel):
"""Request to send a message."""
content: str = Field(..., min_length=1, max_length=10000)
message_kind: A2AMessageKind = A2AMessageKind.MESSAGE
response_to_id: UUID | None = None
requires_response: bool = False
class MessageResponse(BaseModel):
"""A2A chat message response."""
id: UUID
conversation_id: UUID
from_agent: str
content: str
message_kind: A2AMessageKind
response_to_id: UUID | None
requires_response: bool
read_at: datetime | None
created_at: datetime
edited_at: datetime | None
class MessageListResponse(BaseModel):
"""List of messages."""
items: list[MessageResponse]
total: int
has_more: bool
class ListMessagesParams(BaseModel):
"""Query params for listing messages."""
limit: int = Field(100, ge=1, le=500)
before: datetime | None = None
# =============================================================================
# INBOX SCHEMAS
# =============================================================================
class InboxSummaryResponse(BaseModel):
"""Inbox summary."""
total_unread: int
conversations_with_unread: int
pending_responses: int
unanswered_requests: int
# =============================================================================
# PAIRS SCHEMAS
# =============================================================================
class PairResponse(BaseModel):
"""Agent pair for frontend."""
agent_a: str
agent_b: str
conversation_count: int
total_unread: int
last_activity: datetime | None
class PairListResponse(BaseModel):
"""List of pairs."""
items: list[PairResponse]
total: int
# =============================================================================
# ADMIN / LIVE VIEW SCHEMAS (CEO-only)
# =============================================================================
class AdminConversationSummaryResponse(BaseModel):
"""Conversation summary for the CEO's cross-agent live view."""
id: UUID
agent_a: str
agent_b: str
topic: str | None
task_id: UUID | None
status: A2AConversationStatus
message_count: int
last_message_at: datetime | None
last_message_preview: str | None
created_at: datetime
updated_at: datetime
class AdminConversationListResponse(BaseModel):
"""List of admin conversation summaries."""
items: list[AdminConversationSummaryResponse]
total: int
class AdminReplyRequest(BaseModel):
"""Request for the CEO to chime into an existing A2A conversation."""
to_agent: str = Field(..., description="Which participant to address")
content: str = Field(..., min_length=1, max_length=10000)
skill: str | None = None