trytet-client 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ """Trytet Python SDK — client for the Trytet Engine API, MCP, and telemetry."""
2
+
3
+ from .client import TrytetClient
4
+ from .models import (
5
+ AgentManifest,
6
+ CartridgeInvocation,
7
+ CartridgeResult,
8
+ CrashReport,
9
+ EgressPolicy,
10
+ ExecutionStatus,
11
+ FuelVoucher,
12
+ McpPrompt,
13
+ McpResource,
14
+ McpTool,
15
+ NorthstarReport,
16
+ SnapshotResponse,
17
+ StructuredTelemetry,
18
+ TelemetryEvent,
19
+ TetExecutionRequest,
20
+ TetExecutionResult,
21
+ TopologyEdge,
22
+ )
23
+ from .telemetry import TelemetryStream
24
+
25
+ __all__ = [
26
+ "TrytetClient",
27
+ "TelemetryStream",
28
+ "AgentManifest",
29
+ "CartridgeInvocation",
30
+ "CartridgeResult",
31
+ "CrashReport",
32
+ "EgressPolicy",
33
+ "ExecutionStatus",
34
+ "FuelVoucher",
35
+ "McpPrompt",
36
+ "McpResource",
37
+ "McpTool",
38
+ "NorthstarReport",
39
+ "SnapshotResponse",
40
+ "StructuredTelemetry",
41
+ "TelemetryEvent",
42
+ "TetExecutionRequest",
43
+ "TetExecutionResult",
44
+ "TopologyEdge",
45
+ ]
@@ -0,0 +1,169 @@
1
+ """Trytet API client with retry, MCP support, and cartridge management."""
2
+
3
+ import asyncio
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import httpx
7
+
8
+ from .models import (
9
+ CartridgeInvocation,
10
+ CartridgeResult,
11
+ McpPrompt,
12
+ McpResource,
13
+ McpTool,
14
+ NorthstarReport,
15
+ SnapshotResponse,
16
+ TetExecutionRequest,
17
+ TetExecutionResult,
18
+ TopologyEdge,
19
+ )
20
+ from .telemetry import TelemetryStream
21
+
22
+
23
+ class TrytetClient:
24
+ """Async HTTP client for the Trytet Engine API."""
25
+
26
+ def __init__(
27
+ self,
28
+ base_url: str = "http://localhost:3000",
29
+ max_retries: int = 3,
30
+ initial_delay_ms: int = 100,
31
+ ):
32
+ self._base = base_url.rstrip("/")
33
+ self._max_retries = max_retries
34
+ self._initial_delay = initial_delay_ms / 1000.0
35
+ self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0))
36
+
37
+ async def __aenter__(self) -> "TrytetClient":
38
+ return self
39
+
40
+ async def __aexit__(self, *args: Any) -> None:
41
+ await self.close()
42
+
43
+ async def close(self) -> None:
44
+ await self._client.aclose()
45
+
46
+ # -- retry helper -------------------------------------------------------
47
+
48
+ async def _request(
49
+ self,
50
+ method: str,
51
+ path: str,
52
+ *,
53
+ json_body: Any = None,
54
+ retry_on_status: Optional[List[int]] = None,
55
+ ) -> httpx.Response:
56
+ statuses = retry_on_status or [502, 503, 504]
57
+ delay = self._initial_delay
58
+
59
+ for attempt in range(self._max_retries + 1):
60
+ try:
61
+ resp = await self._client.request(
62
+ method, f"{self._base}{path}", json=json_body
63
+ )
64
+ if resp.status_code in statuses and attempt < self._max_retries:
65
+ raise httpx.TransportError(f"Retryable status {resp.status_code}")
66
+ return resp
67
+ except (httpx.TransportError, httpx.ConnectError) as e:
68
+ if attempt == self._max_retries:
69
+ raise
70
+ await asyncio.sleep(delay)
71
+ delay = min(delay * 2, 5.0)
72
+
73
+ raise RuntimeError("unreachable")
74
+
75
+ async def _post(self, path: str, body: Any = None) -> httpx.Response:
76
+ return await self._request("POST", path, json_body=body)
77
+
78
+ async def _get(self, path: str) -> httpx.Response:
79
+ return await self._request("GET", path)
80
+
81
+ async def _check(self, resp: httpx.Response) -> Dict[str, Any]:
82
+ if not resp.is_success:
83
+ text = resp.text[:500]
84
+ raise RuntimeError(f"Request failed ({resp.status_code}): {text}")
85
+ return resp.json()
86
+
87
+ # -- Agent lifecycle ----------------------------------------------------
88
+
89
+ async def execute(self, req: TetExecutionRequest) -> TetExecutionResult:
90
+ resp = await self._post("/v1/tet/execute", req.model_dump(exclude_none=True))
91
+ data = await self._check(resp)
92
+ return TetExecutionResult(**data)
93
+
94
+ async def snapshot(self, tet_id: str) -> SnapshotResponse:
95
+ resp = await self._post(f"/v1/tet/snapshot/{tet_id}")
96
+ data = await self._check(resp)
97
+ return SnapshotResponse(**data)
98
+
99
+ async def fork(
100
+ self, snapshot_id: str, req: TetExecutionRequest
101
+ ) -> TetExecutionResult:
102
+ resp = await self._post(
103
+ f"/v1/tet/fork/{snapshot_id}", req.model_dump(exclude_none=True)
104
+ )
105
+ data = await self._check(resp)
106
+ return TetExecutionResult(**data)
107
+
108
+ async def teleport(self, alias: str, target_node: str) -> None:
109
+ resp = await self._post(f"/v1/tet/teleport/{alias}", target_node)
110
+ await self._check(resp)
111
+
112
+ async def get_topology(self) -> List[TopologyEdge]:
113
+ resp = await self._get("/v1/topology")
114
+ data = await self._check(resp)
115
+ return [TopologyEdge(**e) for e in data]
116
+
117
+ async def get_swarm_metrics(self) -> NorthstarReport:
118
+ resp = await self._get("/v1/swarm/metrics")
119
+ data = await self._check(resp)
120
+ return NorthstarReport(**data)
121
+
122
+ async def get_health(self) -> Dict[str, Any]:
123
+ resp = await self._get("/health")
124
+ return await self._check(resp)
125
+
126
+ # -- Cartridge management -----------------------------------------------
127
+
128
+ async def invoke_cartridge(self, invocation: CartridgeInvocation) -> CartridgeResult:
129
+ resp = await self._post(
130
+ "/v1/cartridge/invoke", invocation.model_dump()
131
+ )
132
+ data = await self._check(resp)
133
+ return CartridgeResult(**data)
134
+
135
+ # -- MCP ----------------------------------------------------------------
136
+
137
+ async def _mcp_call(self, method: str, params: Any = None) -> Any:
138
+ import time
139
+
140
+ request = {
141
+ "jsonrpc": "2.0",
142
+ "id": int(time.time() * 1000),
143
+ "method": method,
144
+ "params": params,
145
+ }
146
+ resp = await self._post("/v1/mcp", request)
147
+ data = await self._check(resp)
148
+ return data.get("result")
149
+
150
+ async def list_tools(self) -> List[McpTool]:
151
+ result = await self._mcp_call("tools/list")
152
+ return [McpTool(**t) for t in (result or {}).get("tools", [])]
153
+
154
+ async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
155
+ return await self._mcp_call("tools/call", {"name": name, "arguments": arguments})
156
+
157
+ async def list_resources(self) -> List[McpResource]:
158
+ result = await self._mcp_call("resources/list")
159
+ return [McpResource(**r) for r in (result or {}).get("resources", [])]
160
+
161
+ async def list_prompts(self) -> List[McpPrompt]:
162
+ result = await self._mcp_call("prompts/list")
163
+ return [McpPrompt(**p) for p in (result or {}).get("prompts", [])]
164
+
165
+ # -- Telemetry ----------------------------------------------------------
166
+
167
+ def create_telemetry_stream(self) -> TelemetryStream:
168
+ ws_url = self._base.replace("http", "ws") + "/v1/swarm/stream"
169
+ return TelemetryStream(ws_url)
@@ -0,0 +1,151 @@
1
+ """Pydantic models for all Trytet API types."""
2
+
3
+ from typing import Optional, Union, List, Dict, Any
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class AgentManifestMetadata(BaseModel):
8
+ name: str
9
+ version: str
10
+ author_pubkey: Optional[str] = None
11
+
12
+
13
+ class AgentManifestConstraints(BaseModel):
14
+ max_memory_pages: int
15
+ fuel_limit: int
16
+ max_egress_bytes: int
17
+
18
+
19
+ class AgentManifestPermissions(BaseModel):
20
+ can_egress: List[str] = Field(default_factory=list)
21
+ can_persist: bool = False
22
+ can_teleport: bool = False
23
+ is_genesis_factory: bool = False
24
+ can_fork: bool = False
25
+
26
+
27
+ class AgentManifest(BaseModel):
28
+ metadata: AgentManifestMetadata
29
+ constraints: AgentManifestConstraints
30
+ permissions: AgentManifestPermissions
31
+
32
+
33
+ class FuelVoucher(BaseModel):
34
+ tet_id: str
35
+ fuel_limit: int
36
+ nonce: int
37
+ signature: List[int]
38
+
39
+
40
+ class EgressPolicy(BaseModel):
41
+ allowed_domains: List[str]
42
+ max_daily_bytes: int
43
+ require_https: bool = True
44
+
45
+
46
+ class TetExecutionRequest(BaseModel):
47
+ payload: Optional[List[int]] = None
48
+ alias: Optional[str] = None
49
+ env: Optional[Dict[str, str]] = None
50
+ injected_files: Optional[Dict[str, str]] = None
51
+ allocated_fuel: Optional[int] = None
52
+ max_memory_mb: Optional[int] = None
53
+ parent_snapshot_id: Optional[str] = None
54
+ target_function: Optional[str] = None
55
+ call_depth: int = 0
56
+ voucher: Optional[FuelVoucher] = None
57
+ manifest: Optional[AgentManifest] = None
58
+ egress_policy: Optional[EgressPolicy] = None
59
+
60
+
61
+ class StructuredTelemetry(BaseModel):
62
+ stdout_lines: List[str] = Field(default_factory=list)
63
+ stderr_lines: List[str] = Field(default_factory=list)
64
+ memory_used_kb: int = 0
65
+
66
+
67
+ class CrashReport(BaseModel):
68
+ error_type: str
69
+ message: str
70
+ instruction_offset: Optional[int] = None
71
+
72
+
73
+ ExecutionStatus = Union[
74
+ str, Dict[str, Any]
75
+ ] # "Success" | "OutOfFuel" | ... | {"Crash": {...}}
76
+
77
+
78
+ class TetExecutionResult(BaseModel):
79
+ tet_id: str
80
+ status: Any # ExecutionStatus
81
+ telemetry: StructuredTelemetry
82
+ execution_duration_us: int
83
+ fuel_consumed: int
84
+ mutated_files: Dict[str, str] = Field(default_factory=dict)
85
+ migrated_to: Optional[str] = None
86
+
87
+
88
+ class SnapshotResponse(BaseModel):
89
+ snapshot_id: str
90
+ size_bytes: int
91
+
92
+
93
+ class TopologyEdge(BaseModel):
94
+ source: str
95
+ target: str
96
+ latency_us: int
97
+ bytes_transferred: int
98
+
99
+
100
+ class NorthstarReport(BaseModel):
101
+ teleport_warp_us: int
102
+ mitosis_constant_us: int
103
+ oracle_fidelity_us: int
104
+ market_evacuation_us: int
105
+ cartridge_spinup_us: int
106
+ timestamp: str
107
+
108
+
109
+ class CartridgeInvocation(BaseModel):
110
+ component_id: str
111
+ payload: str
112
+ fuel: int
113
+ max_memory_mb: int = 512
114
+
115
+
116
+ class CartridgeResult(BaseModel):
117
+ output: str
118
+ fuel_consumed: int
119
+ duration_us: int
120
+
121
+
122
+ class McpTool(BaseModel):
123
+ name: str
124
+ description: str
125
+ inputSchema: Dict[str, Any] = Field(default_factory=dict)
126
+
127
+
128
+ class McpResource(BaseModel):
129
+ uri: str
130
+ name: str
131
+ description: Optional[str] = None
132
+ mimeType: Optional[str] = None
133
+
134
+
135
+ class McpPromptArgument(BaseModel):
136
+ name: str
137
+ description: Optional[str] = None
138
+ required: Optional[bool] = None
139
+
140
+
141
+ class McpPrompt(BaseModel):
142
+ name: str
143
+ description: Optional[str] = None
144
+ arguments: Optional[List[McpPromptArgument]] = None
145
+
146
+
147
+ class TelemetryEvent(BaseModel):
148
+ event_type: str
149
+ tet_id: str
150
+ timestamp_us: int
151
+ data: Dict[str, Any] = Field(default_factory=dict)
@@ -0,0 +1,62 @@
1
+ """WebSocket telemetry stream for real-time Trytet observability."""
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Any, Callable, Dict, Set
6
+
7
+ import websockets
8
+ from websockets.asyncio.client import ClientConnection
9
+
10
+ from .models import TelemetryEvent
11
+
12
+ TelemetryCallback = Callable[[TelemetryEvent], None]
13
+
14
+
15
+ class TelemetryStream:
16
+ """Async WebSocket client for the Trytet telemetry stream."""
17
+
18
+ def __init__(self, ws_url: str):
19
+ self._url = ws_url
20
+ self._ws: ClientConnection | None = None
21
+ self._listeners: Set[TelemetryCallback] = set()
22
+ self._closed = False
23
+ self._reconnect_delay = 1.0
24
+
25
+ async def connect(self) -> None:
26
+ """Connect and begin receiving telemetry events."""
27
+ while not self._closed:
28
+ try:
29
+ async with websockets.connect(self._url) as ws:
30
+ self._ws = ws
31
+ self._reconnect_delay = 1.0
32
+ await self._read_loop(ws)
33
+ except Exception:
34
+ if not self._closed:
35
+ await asyncio.sleep(self._reconnect_delay)
36
+ self._reconnect_delay = min(self._reconnect_delay * 2, 30.0)
37
+
38
+ async def _read_loop(self, ws: ClientConnection) -> None:
39
+ async for message in ws:
40
+ try:
41
+ data = json.loads(message)
42
+ event = TelemetryEvent(**data)
43
+ await self._emit(event)
44
+ except Exception:
45
+ pass
46
+
47
+ async def _emit(self, event: TelemetryEvent) -> None:
48
+ for listener in list(self._listeners):
49
+ try:
50
+ listener(event)
51
+ except Exception:
52
+ pass
53
+
54
+ def on_event(self, callback: TelemetryCallback) -> Callable[[], None]:
55
+ """Register a callback. Returns an unregister function."""
56
+ self._listeners.add(callback)
57
+ return lambda: self._listeners.discard(callback)
58
+
59
+ def close(self) -> None:
60
+ """Close the stream and stop reconnecting."""
61
+ self._closed = True
62
+ self._listeners.clear()
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: trytet-client
3
+ Version: 0.2.0
4
+ Summary: Official Python SDK for the Trytet Engine — sub-millisecond Wasm sandbox for AI agents
5
+ Author: Trytet
6
+ License: MIT
7
+ Keywords: agent,llm,sandbox,trytet,wasm
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: httpx>=0.25
17
+ Requires-Dist: pydantic>=2.0
18
+ Requires-Dist: websockets>=12.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == 'dev'
21
+ Requires-Dist: pytest-asyncio; extra == 'dev'
22
+ Requires-Dist: ruff; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Trytet Python SDK
26
+
27
+ Official Python client for the [Trytet Engine](https://trytet.io) — a sub-millisecond, hyper-ephemeral Wasm execution substrate for AI agents.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install trytet-client
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ import asyncio
39
+ from trytet_client import TrytetClient, TetExecutionRequest
40
+
41
+ async def main():
42
+ async with TrytetClient(base_url="http://localhost:3000") as client:
43
+ # Execute a Wasm payload
44
+ with open("my_agent.wasm", "rb") as f:
45
+ payload = list(f.read())
46
+
47
+ result = await client.execute(TetExecutionRequest(
48
+ payload=payload,
49
+ allocated_fuel=1_000_000,
50
+ alias="my-agent",
51
+ ))
52
+ print(f"Status: {result.status}")
53
+ print(f"Fuel consumed: {result.fuel_consumed}")
54
+
55
+ # List available MCP tools
56
+ tools = await client.list_tools()
57
+ for tool in tools:
58
+ print(f" {tool.name}: {tool.description}")
59
+
60
+ # Invoke a cartridge
61
+ from trytet_client import CartridgeInvocation
62
+ cart_result = await client.invoke_cartridge(CartridgeInvocation(
63
+ component_id="js-evaluator",
64
+ payload="2 + 2",
65
+ fuel=1_000_000,
66
+ ))
67
+ print(cart_result.output)
68
+
69
+ asyncio.run(main())
70
+ ```
71
+
72
+ ## Features
73
+
74
+ - Full async HTTP client with retry and backoff
75
+ - MCP (Model Context Protocol) support — tools, resources, prompts
76
+ - Cartridge management API
77
+ - WebSocket telemetry streaming
78
+ - Pydantic models for all API types
79
+ - Type-safe request/response handling
@@ -0,0 +1,7 @@
1
+ trytet_client/__init__.py,sha256=42WjFrZB3puFosTfxD3MznsIu1M8ARqG2OyCz8SI3EU,947
2
+ trytet_client/client.py,sha256=6tjuGTRqpvDslm9Q3xMKGwaUQXDuuRltE3QFHC95Bdc,5907
3
+ trytet_client/models.py,sha256=4j05SQlcKn3VP8kY4Lx5AZjXnv0X3OtExpx19bJJSZw,3468
4
+ trytet_client/telemetry.py,sha256=2uusXvANG_rYc-DA1PpKK-99SMUq8IgWpv4F8RWAG88,2063
5
+ trytet_client-0.2.0.dist-info/METADATA,sha256=poJDadTLPfZs6DMY3kpcGbHKgd_vf9taizIJJLDwD1E,2405
6
+ trytet_client-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ trytet_client-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any