- protocol.py: pydantic models for all AICC message types (envelope, session_init, tool_call, tool_result, event, error, heartbeat) - bridge.py: Bridge with @bridge.tool registration, capability checks, session management, serve_forever - client.py: AICCClient with single background reader (safe on concurrent transports like WebSocket), call_tool, events, manifest - tool.py: @tool decorator with schema generation from type hints - schema.py: JSON Schema generation (str/int/float/bool, list, dict, Optional, pydantic models) - transport: Transport protocol, InProcessTransport, WebSocket client+server - tests: 12 passing (integration, schema, websocket roundtrip) - examples: bridge_minimal.py + agent_minimal.py (verified end-to-end)
194 lines
4.6 KiB
Python
194 lines
4.6 KiB
Python
"""AICC protocol message types and enums."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from enum import Enum
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
PROTOCOL_VERSION = "aicc/0.1"
|
|
|
|
|
|
class MessageType(str, Enum):
|
|
SESSION_INIT = "session_init"
|
|
SESSION_RESUME = "session_resume"
|
|
SESSION_CLOSE = "session_close"
|
|
MANIFEST_REQUEST = "manifest_request"
|
|
TOOL_CALL = "tool_call"
|
|
TOOL_RESULT = "tool_result"
|
|
EVENT = "event"
|
|
ERROR = "error"
|
|
HEARTBEAT = "heartbeat"
|
|
|
|
|
|
class TickMode(str, Enum):
|
|
FIXED = "fixed"
|
|
EVENT = "event"
|
|
HYBRID = "hybrid"
|
|
|
|
|
|
class WorldKind(str, Enum):
|
|
TWO_D = "2d"
|
|
THREE_D = "3d"
|
|
TEXT = "text"
|
|
ABSTRACT = "abstract"
|
|
|
|
|
|
class ModelClass(str, Enum):
|
|
EDGE_SMALL = "edge_small"
|
|
EDGE_MEDIUM = "edge_medium"
|
|
CLOUD_MEDIUM = "cloud_medium"
|
|
CLOUD_LARGE = "cloud_large"
|
|
|
|
|
|
class ToolClass(str, Enum):
|
|
SENSOR = "sensor"
|
|
ACTUATOR = "actuator"
|
|
GENERATOR = "generator"
|
|
|
|
|
|
class ErrorCode(str, Enum):
|
|
PROTOCOL_MISMATCH = "protocol_mismatch"
|
|
SESSION_EXPIRED = "session_expired"
|
|
TOOL_UNKNOWN = "tool_unknown"
|
|
TOOL_UNAVAILABLE = "tool_unavailable"
|
|
INVALID_INPUT = "invalid_input"
|
|
EXECUTION_FAILED = "execution_failed"
|
|
TIMEOUT = "timeout"
|
|
INTERNAL_ERROR = "internal_error"
|
|
|
|
|
|
class EventTopic(str, Enum):
|
|
TICK = "tick"
|
|
COLLISION = "collision"
|
|
AUDIO = "audio"
|
|
STATE_CHANGE = "state_change"
|
|
AGENT_MESSAGE = "agent_message"
|
|
|
|
|
|
def _uuid() -> str:
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
class Envelope(BaseModel):
|
|
"""Common envelope for all AICC messages."""
|
|
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
protocol: Literal["aicc/0.1"] = PROTOCOL_VERSION
|
|
session_id: str = Field(default_factory=_uuid)
|
|
message_id: str = Field(default_factory=_uuid)
|
|
|
|
|
|
class WorldSpec(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str
|
|
kind: WorldKind
|
|
|
|
|
|
class ModelDecl(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
|
|
class_: ModelClass = Field(alias="class")
|
|
expected_first_token_ms: int | None = None
|
|
expected_full_response_ms: int | None = None
|
|
|
|
|
|
class ToolLimits(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
calls_per_minute: int | None = None
|
|
calls_per_session: int | None = None
|
|
|
|
|
|
class ToolSpec(BaseModel):
|
|
"""Tool definition advertised in the session manifest."""
|
|
|
|
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
|
|
id: str = Field(pattern=r"^[a-z][a-z0-9_]*$")
|
|
class_: ToolClass = Field(alias="class")
|
|
description: str
|
|
input_schema: dict[str, Any]
|
|
output_schema: dict[str, Any]
|
|
requires_capability: str | None = None
|
|
limits: ToolLimits | None = None
|
|
strict_input: bool = False
|
|
|
|
|
|
class Capabilities(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
sensors: list[str] = Field(default_factory=list)
|
|
actuators: list[str] = Field(default_factory=list)
|
|
generators: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class SessionInit(Envelope):
|
|
type: Literal[MessageType.SESSION_INIT] = MessageType.SESSION_INIT
|
|
tick_rate_hz: float = Field(gt=0)
|
|
tick_mode: TickMode
|
|
world: WorldSpec
|
|
agent_model: ModelDecl
|
|
capabilities: Capabilities
|
|
tools: list[ToolSpec]
|
|
event_subscriptions: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class SessionClose(Envelope):
|
|
type: Literal[MessageType.SESSION_CLOSE] = MessageType.SESSION_CLOSE
|
|
reason: str | None = None
|
|
|
|
|
|
class ToolCall(Envelope):
|
|
type: Literal[MessageType.TOOL_CALL] = MessageType.TOOL_CALL
|
|
call_id: str
|
|
tool: str = Field(pattern=r"^[a-z][a-z0-9_]*$")
|
|
input: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class Meta(BaseModel):
|
|
model_config = ConfigDict(extra="allow")
|
|
|
|
tick: int | None = None
|
|
latency_ms: int | None = None
|
|
source: str | None = None
|
|
|
|
|
|
class ErrorPayload(BaseModel):
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
code: ErrorCode
|
|
message: str
|
|
retryable: bool
|
|
|
|
|
|
class ToolResult(Envelope):
|
|
type: Literal[MessageType.TOOL_RESULT] = MessageType.TOOL_RESULT
|
|
call_id: str
|
|
ok: bool
|
|
output: dict[str, Any] | None = None
|
|
error: ErrorPayload | None = None
|
|
meta: Meta | None = None
|
|
|
|
|
|
class EventMessage(Envelope):
|
|
type: Literal[MessageType.EVENT] = MessageType.EVENT
|
|
topic: EventTopic | str
|
|
payload: dict[str, Any]
|
|
meta: Meta | None = None
|
|
|
|
|
|
class ErrorMessage(Envelope):
|
|
type: Literal[MessageType.ERROR] = MessageType.ERROR
|
|
error: ErrorPayload
|
|
|
|
|
|
class Heartbeat(Envelope):
|
|
type: Literal[MessageType.HEARTBEAT] = MessageType.HEARTBEAT
|
|
manifest_update: dict[str, Any] | None = None
|