aicc-py 0.1.0: initial SDK
- 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)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""In-process transport: bridges AICC clients to a Bridge within one event loop.
|
||||
|
||||
Useful for tests, embedded use, and single-process agents that drive the bridge directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from aicc.bridge import Bridge
|
||||
from aicc.errors import ConnectionError_
|
||||
from aicc.protocol import PROTOCOL_VERSION, SessionInit
|
||||
|
||||
|
||||
class InProcessTransport:
|
||||
"""Two-sided transport: one side is the bridge, the other is the client.
|
||||
|
||||
Both sides share the same asyncio event loop. The bridge side pushes its
|
||||
manifest into the client's receive queue when start() is awaited.
|
||||
|
||||
Example:
|
||||
async with InProcessTransport.start(bridge) as t:
|
||||
client = AICCClient(t)
|
||||
async with client:
|
||||
manifest = await client.handshake()
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._send_q: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
||||
self._recv_q: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
|
||||
self._bridge: Bridge | None = None
|
||||
self._session_id: str | None = None
|
||||
self._connected = False
|
||||
|
||||
@classmethod
|
||||
def start(cls, bridge: Bridge) -> "InProcessTransport":
|
||||
t = cls()
|
||||
t._bridge = bridge
|
||||
return t
|
||||
|
||||
async def connect(self) -> None:
|
||||
if self._bridge is None:
|
||||
raise ConnectionError_("InProcessTransport.start(bridge) must be used for server-side.")
|
||||
if self._connected:
|
||||
return
|
||||
self._connected = True
|
||||
session = self._bridge.open_session()
|
||||
self._session_id = session.session_id
|
||||
manifest: SessionInit = session.manifest # type: ignore[assignment]
|
||||
await self._recv_q.put(manifest.model_dump(by_alias=True))
|
||||
|
||||
async def close(self) -> None:
|
||||
if self._bridge is not None and self._session_id is not None:
|
||||
self._bridge.close_session(self._session_id)
|
||||
self._session_id = None
|
||||
self._connected = False
|
||||
|
||||
async def send(self, message: dict[str, Any]) -> None:
|
||||
if not self._connected or self._bridge is None or self._session_id is None:
|
||||
raise ConnectionError_("Transport is not connected.")
|
||||
sid = message.get("session_id") or self._session_id
|
||||
msg_type = message.get("type")
|
||||
# session_resume carries the session_id; bind it
|
||||
if msg_type == "session_resume" and not self._bridge._sessions.get(sid): # noqa: SLF001
|
||||
# Auto-open if bridge forgot
|
||||
pass
|
||||
response = await self._bridge.handle_message(sid, message)
|
||||
if response is not None:
|
||||
await self._recv_q.put(response)
|
||||
|
||||
async def receive(self) -> dict[str, Any]:
|
||||
if not self._connected:
|
||||
raise ConnectionError_("Transport is not connected.")
|
||||
return await self._recv_q.get()
|
||||
|
||||
async def push_event(self, event: dict[str, Any]) -> None:
|
||||
"""Bridge-side helper to push an event to the connected client."""
|
||||
await self._recv_q.put(event)
|
||||
|
||||
async def __aenter__(self) -> "InProcessTransport":
|
||||
await self.connect()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||
await self.close()
|
||||
Reference in New Issue
Block a user