154 lines
4.4 KiB
Python
154 lines
4.4 KiB
Python
"""HTTP client for one T3 Code server (`packages/contracts/src/environmentHttp.ts`).
|
|
|
|
Four routes are used: the unauthenticated descriptor, the shell read-model
|
|
(projects + threads without bodies), one thread with a turn window, and
|
|
`dispatch` with a `ClientOrchestrationCommand`. The WebSocket RPC
|
|
(`terminal.*` and friends) is deliberately not wrapped.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
DESCRIPTOR = "/.well-known/t3/environment"
|
|
SHELL = "/api/orchestration/shell"
|
|
THREADS = "/api/orchestration/threads"
|
|
DISPATCH = "/api/orchestration/dispatch"
|
|
|
|
RUNTIME_MODE = "full-access"
|
|
INTERACTION_MODE = "default"
|
|
PROBE_TIMEOUT = 5.0
|
|
|
|
|
|
class T3Error(Exception):
|
|
def __init__(self, status: int, body: Any) -> None:
|
|
self.status = status
|
|
self.body = body
|
|
detail = (
|
|
f"{body.get('code')}: {body.get('reason') or body.get('message')}"
|
|
if isinstance(body, dict)
|
|
else str(body)[:300]
|
|
)
|
|
super().__init__(f"T3 responded {status} - {detail}")
|
|
|
|
|
|
def now_iso() -> str:
|
|
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
|
|
|
|
def new_id() -> str:
|
|
return str(uuid.uuid4())
|
|
|
|
|
|
def thread_create(
|
|
thread_id: str, project_id: str, title: str, selection: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"type": "thread.create",
|
|
"commandId": new_id(),
|
|
"threadId": thread_id,
|
|
"projectId": project_id,
|
|
"title": title,
|
|
"modelSelection": selection,
|
|
"runtimeMode": RUNTIME_MODE,
|
|
"interactionMode": INTERACTION_MODE,
|
|
"branch": None,
|
|
"worktreePath": None,
|
|
"createdAt": now_iso(),
|
|
}
|
|
|
|
|
|
def turn_start(
|
|
thread_id: str,
|
|
text: str,
|
|
selection: dict[str, Any] | None = None,
|
|
title_seed: str | None = None,
|
|
) -> dict[str, Any]:
|
|
command: dict[str, Any] = {
|
|
"type": "thread.turn.start",
|
|
"commandId": new_id(),
|
|
"threadId": thread_id,
|
|
"message": {
|
|
"messageId": new_id(),
|
|
"role": "user",
|
|
"text": text,
|
|
"attachments": [],
|
|
},
|
|
"runtimeMode": RUNTIME_MODE,
|
|
"interactionMode": INTERACTION_MODE,
|
|
"createdAt": now_iso(),
|
|
}
|
|
if selection is not None:
|
|
command["modelSelection"] = selection
|
|
if title_seed:
|
|
command["titleSeed"] = title_seed
|
|
return command
|
|
|
|
|
|
def turn_interrupt(thread_id: str, turn_id: str | None = None) -> dict[str, Any]:
|
|
command: dict[str, Any] = {
|
|
"type": "thread.turn.interrupt",
|
|
"commandId": new_id(),
|
|
"threadId": thread_id,
|
|
"createdAt": now_iso(),
|
|
}
|
|
if turn_id:
|
|
command["turnId"] = turn_id
|
|
return command
|
|
|
|
|
|
class T3Client:
|
|
def __init__(
|
|
self,
|
|
url: str,
|
|
token: str,
|
|
*,
|
|
transport: httpx.AsyncBaseTransport | None = None,
|
|
timeout: float = 30.0,
|
|
) -> None:
|
|
self.url = url.rstrip("/")
|
|
self._http = httpx.AsyncClient(
|
|
base_url=self.url,
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
timeout=timeout,
|
|
transport=transport,
|
|
)
|
|
|
|
async def aclose(self) -> None:
|
|
await self._http.aclose()
|
|
|
|
async def descriptor(self) -> dict[str, Any]:
|
|
response = await self._http.get(
|
|
DESCRIPTOR, headers={"Authorization": ""}, timeout=PROBE_TIMEOUT
|
|
)
|
|
return self._json(response)
|
|
|
|
async def shell(self) -> dict[str, Any]:
|
|
return self._json(await self._http.get(SHELL))
|
|
|
|
async def thread(
|
|
self, thread_id: str, turn_limit: int | None = None
|
|
) -> dict[str, Any] | None:
|
|
params = {"turnLimit": turn_limit} if turn_limit else None
|
|
response = await self._http.get(f"{THREADS}/{thread_id}", params=params)
|
|
if response.status_code == httpx.codes.NOT_FOUND:
|
|
return None
|
|
return self._json(response)
|
|
|
|
async def dispatch(self, command: dict[str, Any]) -> dict[str, Any]:
|
|
return self._json(await self._http.post(DISPATCH, json=command))
|
|
|
|
@staticmethod
|
|
def _json(response: httpx.Response) -> dict[str, Any]:
|
|
try:
|
|
body: Any = response.json()
|
|
except ValueError:
|
|
body = response.text
|
|
if response.is_error or not isinstance(body, dict):
|
|
raise T3Error(response.status_code, body)
|
|
return body
|