feat(*): t3code-mcp connector - machines, projects, dispatch, wait, interrupt
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user