from dataclasses import dataclass from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator class AgentRequestMetadata(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) locale: str | None = None client_version: str | None = None ui_entry: str | None = None class AgentRequest(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) agent_type: str message: str session_id: str idempotency_key: str metadata: AgentRequestMetadata = Field(default_factory=AgentRequestMetadata) @field_validator("message") @classmethod def message_must_not_be_blank(cls, value: str) -> str: if not value.strip(): raise ValueError("message must not be blank") return value @field_validator("idempotency_key") @classmethod def idempotency_key_must_be_valid(cls, value: str) -> str: if not 16 <= len(value) <= 128 or not value.replace("-", "").replace("_", "").isalnum(): raise ValueError("idempotency_key must be 16-128 alphanumeric characters") return value class RequestContext(BaseModel): model_config = ConfigDict(frozen=True) user_id: str trace_id: str roles: tuple[str, ...] = () customer_ids: tuple[str, ...] = () data_scope: str = "self" portal: str = "api" clarification_round: int = Field(default=0, ge=0, le=10) permissions: tuple[str, ...] = () permission_scopes: dict[str, str] = Field(default_factory=dict) class AgentDefinition(BaseModel): model_config = ConfigDict(frozen=True) agent_type: str version: str allowed_tools: tuple[str, ...] = () allowed_roles: tuple[str, ...] = () allowed_portals: tuple[str, ...] = () supported_intents: tuple[str, ...] = ("general",) class ResolvedAgentConfig(BaseModel): model_config = ConfigDict(frozen=True) config_version: str prompt_version: str model_endpoint: str allowed_tools: tuple[str, ...] = () timeout_seconds: int = Field(default=60, gt=0) release_id: int | None = None allowed_tools_by_intent: dict[str, tuple[str, ...]] = Field(default_factory=dict) negative_rules: tuple[tuple[str, str], ...] = () class RecalledMemory(BaseModel): model_config = ConfigDict(frozen=True) memory_uuid: str customer_id: str content: str class SourceReference(BaseModel): model_config = ConfigDict(frozen=True) source_type: Literal["knowledge", "memory", "relationship", "tool"] source_id: str title: str | None = None score: float | None = Field(default=None, ge=0, le=1) class ToolCallRecord(BaseModel): model_config = ConfigDict(frozen=True) tool_name: str status: Literal["succeeded", "failed", "denied"] input_summary: dict[str, Any] = Field(default_factory=dict) output_summary: dict[str, Any] = Field(default_factory=dict) class IntentResult(BaseModel): model_config = ConfigDict(frozen=True) intent: str confidence: float = Field(ge=0, le=1) needs_clarification: bool = False class CoreResult(BaseModel): model_config = ConfigDict(frozen=True) text: str intent: IntentResult | None = None source_references: tuple[SourceReference, ...] = () tool_calls: tuple[ToolCallRecord, ...] = () transfer_required: bool = False transfer_reason: str | None = None class AgentResult(BaseModel): model_config = ConfigDict(frozen=True) run_id: str result: CoreResult usage: dict[str, int] = Field(default_factory=dict) class RunProgressEvent(BaseModel): model_config = ConfigDict(frozen=True) event_type: Literal["start", "tools", "delta", "replace", "done", "error"] run_id: str payload: dict[str, Any] = Field(default_factory=dict) @dataclass(frozen=True) class DomainEvent: event_id: str event_type: str aggregate_type: str aggregate_id: str trace_id: str payload: dict[str, Any] occurred_at: datetime