scitrera-aether-client 0.1.58__tar.gz
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.
- scitrera_aether_client-0.1.58/PKG-INFO +788 -0
- scitrera_aether_client-0.1.58/README.md +753 -0
- scitrera_aether_client-0.1.58/pyproject.toml +78 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/__init__.py +367 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/_common.py +406 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/admin.py +373 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/admin_async.py +358 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/authority.py +292 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/authority_cache.py +447 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/client.py +2939 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/client_async.py +3918 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/echo_test/__init__.py +29 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/echo_test/echo_agent.py +294 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/echo_test/echo_agent_async.py +309 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/echo_test/echo_orchestrator.py +170 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/exceptions.py +504 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/httpx_transport.py +257 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/metrics.py +43 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/orchestrator/__init__.py +623 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/orchestrator/multiprocess.py +637 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/proto/__init__.py +0 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/proto/aether_pb2.py +439 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/proto/aether_pb2.pyi +2739 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/proto/aether_pb2_grpc.py +100 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/proxy.py +846 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/proxy_terminator.py +1082 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/py.typed +0 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/requests_adapter.py +178 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/tunnel.py +940 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client/types.py +563 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client.egg-info/PKG-INFO +788 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client.egg-info/SOURCES.txt +45 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client.egg-info/dependency_links.txt +1 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client.egg-info/requires.txt +11 -0
- scitrera_aether_client-0.1.58/scitrera_aether_client.egg-info/top_level.txt +1 -0
- scitrera_aether_client-0.1.58/setup.cfg +4 -0
- scitrera_aether_client-0.1.58/tests/test_admin.py +241 -0
- scitrera_aether_client-0.1.58/tests/test_authority.py +418 -0
- scitrera_aether_client-0.1.58/tests/test_authority_cache.py +588 -0
- scitrera_aether_client-0.1.58/tests/test_client.py +2558 -0
- scitrera_aether_client-0.1.58/tests/test_client_async.py +2023 -0
- scitrera_aether_client-0.1.58/tests/test_orchestrator.py +1086 -0
- scitrera_aether_client-0.1.58/tests/test_proxy.py +580 -0
- scitrera_aether_client-0.1.58/tests/test_proxy_streaming.py +189 -0
- scitrera_aether_client-0.1.58/tests/test_proxy_terminator.py +1095 -0
- scitrera_aether_client-0.1.58/tests/test_requests_adapter.py +403 -0
- scitrera_aether_client-0.1.58/tests/test_tunnel.py +572 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scitrera-aether-client
|
|
3
|
+
Version: 0.1.58
|
|
4
|
+
Summary: Python client SDK for Scitrera Aether distributed control plane
|
|
5
|
+
Author-email: "scitrera.ai" <open-source-team@scitrera.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://aetherlayer.ai
|
|
8
|
+
Project-URL: Documentation, https://github.com/scitrera/aether#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/scitrera/aether
|
|
10
|
+
Project-URL: Issues, https://github.com/scitrera/aether/issues
|
|
11
|
+
Keywords: aether,scitrera,grpc,distributed,agents,control-plane,async,asyncio
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Classifier: Framework :: AsyncIO
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: grpcio>=1.76.0
|
|
26
|
+
Requires-Dist: protobuf<8.0,>=5.29.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: grpcio-tools>=1.76.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: types-protobuf>=4.24.0; extra == "dev"
|
|
33
|
+
Requires-Dist: grpc-stubs>=1.53.0; extra == "dev"
|
|
34
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
35
|
+
|
|
36
|
+
# Aether Python SDK
|
|
37
|
+
|
|
38
|
+
Aether Python SDK — client library for the Aether distributed control plane. Aether is a system for routing structured messages, tracking tasks, and managing connection lifecycles for agents, tasks, users, and other principals.
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- **Sync and Async Support**: Both synchronous (threading-based) and asynchronous (asyncio-based) client implementations
|
|
43
|
+
- **Multiple Client Types**: Agent, Task, User, Orchestrator, WorkflowEngine, and MetricsBridge clients
|
|
44
|
+
- **Key-Value Store**: Hierarchical configuration store with multiple scopes (global, workspace, user, user-workspace)
|
|
45
|
+
- **Task Management**: Create and manage tasks with different assignment modes
|
|
46
|
+
- **Checkpointing**: Persist and restore agent/task state across restarts
|
|
47
|
+
- **Auto-Reconnection**: Configurable exponential backoff with automatic reconnection
|
|
48
|
+
- **TLS/mTLS Support**: Secure connections with optional mutual TLS authentication
|
|
49
|
+
- **Typed + Catch-all Handlers**: Register `on_chat_message`, `on_control_message`, etc. alongside `on_message` — both fire for matching messages
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install scitrera-aether-client
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For development:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install scitrera-aether-client[dev]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Synchronous Client
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from scitrera_aether_client import AgentClient, CHAT
|
|
69
|
+
|
|
70
|
+
# Create an agent client
|
|
71
|
+
client = AgentClient(
|
|
72
|
+
workspace="default",
|
|
73
|
+
implementation="my-agent",
|
|
74
|
+
specifier="agent-01"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Set up message callback
|
|
78
|
+
def on_message(msg):
|
|
79
|
+
print(f"Received from {msg.source_topic}: {msg.payload.decode()}")
|
|
80
|
+
|
|
81
|
+
client.on_message = on_message
|
|
82
|
+
|
|
83
|
+
# Connect to the gateway
|
|
84
|
+
client.connect("localhost:50051")
|
|
85
|
+
|
|
86
|
+
# Send a message to another agent
|
|
87
|
+
client.send_message_to_agent(
|
|
88
|
+
workspace="default",
|
|
89
|
+
implementation="other-agent",
|
|
90
|
+
specifier="01",
|
|
91
|
+
payload=b"Hello!"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Keep running until interrupted
|
|
95
|
+
try:
|
|
96
|
+
while True:
|
|
97
|
+
import time
|
|
98
|
+
time.sleep(1)
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
pass
|
|
101
|
+
finally:
|
|
102
|
+
client.close()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Asynchronous Client
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import asyncio
|
|
109
|
+
from scitrera_aether_client import AsyncAgentClient
|
|
110
|
+
|
|
111
|
+
async def main():
|
|
112
|
+
client = AsyncAgentClient(
|
|
113
|
+
workspace="default",
|
|
114
|
+
implementation="my-async-agent",
|
|
115
|
+
specifier="agent-01"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def on_message(msg):
|
|
119
|
+
print(f"Received: {msg.payload.decode()}")
|
|
120
|
+
|
|
121
|
+
client.on_message = on_message
|
|
122
|
+
|
|
123
|
+
await client.connect("localhost:50051")
|
|
124
|
+
|
|
125
|
+
await client.send_message_to_agent(
|
|
126
|
+
workspace="default",
|
|
127
|
+
implementation="my-async-agent",
|
|
128
|
+
specifier="agent-01",
|
|
129
|
+
payload=b"Hello from async!"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Wait until disconnected
|
|
133
|
+
await client.wait_until_disconnected()
|
|
134
|
+
|
|
135
|
+
asyncio.run(main())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Using Async Context Manager
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
async with AsyncAgentClient("default", "my-agent", "01") as client:
|
|
142
|
+
await client.connect("localhost:50051")
|
|
143
|
+
await client.send_message_to_agent("default", "other", "01", b"Hello!")
|
|
144
|
+
await asyncio.sleep(1)
|
|
145
|
+
# Connection automatically closed
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Client Types
|
|
149
|
+
|
|
150
|
+
### AgentClient / AsyncAgentClient
|
|
151
|
+
|
|
152
|
+
For long-running agent processes that need unique identities.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from scitrera_aether_client import AgentClient
|
|
156
|
+
|
|
157
|
+
client = AgentClient(
|
|
158
|
+
workspace="default",
|
|
159
|
+
implementation="python-worker",
|
|
160
|
+
specifier="worker-01"
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### TaskClient / AsyncTaskClient
|
|
165
|
+
|
|
166
|
+
For task execution. Supports both unique (named) and non-unique (pooled) tasks.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from scitrera_aether_client import TaskClient
|
|
170
|
+
|
|
171
|
+
# Unique task (named)
|
|
172
|
+
unique_task = TaskClient(
|
|
173
|
+
workspace="default",
|
|
174
|
+
implementation="data-processor",
|
|
175
|
+
unique_specifier="job-123"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Non-unique task (pooled, server assigns ID)
|
|
179
|
+
pooled_task = TaskClient(
|
|
180
|
+
workspace="default",
|
|
181
|
+
implementation="worker"
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### UserClient / AsyncUserClient
|
|
186
|
+
|
|
187
|
+
For user session connections (e.g., from browser windows).
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from scitrera_aether_client import UserClient
|
|
191
|
+
|
|
192
|
+
client = UserClient(
|
|
193
|
+
user_id="user-123",
|
|
194
|
+
window_id="window-abc"
|
|
195
|
+
)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### OrchestratorClient / AsyncOrchestratorClient
|
|
199
|
+
|
|
200
|
+
For managing agent/task lifecycle and compute resources.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from scitrera_aether_client import OrchestratorClient
|
|
204
|
+
|
|
205
|
+
client = OrchestratorClient(
|
|
206
|
+
implementation="kubernetes-orchestrator",
|
|
207
|
+
supported_profiles=["docker", "kubernetes"]
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### WorkflowEngineClient / AsyncWorkflowEngineClient
|
|
212
|
+
|
|
213
|
+
For processing events and triggering downstream actions.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from scitrera_aether_client import WorkflowEngineClient
|
|
217
|
+
|
|
218
|
+
client = WorkflowEngineClient()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### MetricsBridgeClient / AsyncMetricsBridgeClient
|
|
222
|
+
|
|
223
|
+
For collecting telemetry data from agents and tasks.
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from scitrera_aether_client import MetricsBridgeClient
|
|
227
|
+
|
|
228
|
+
client = MetricsBridgeClient()
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### ServiceClient / AsyncServiceClient
|
|
232
|
+
|
|
233
|
+
For trusted backend intermediaries (e.g., app servers or WebSocket backends) that act on behalf of users.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from scitrera_aether_client import ServiceClient, Credentials
|
|
237
|
+
|
|
238
|
+
client = ServiceClient(
|
|
239
|
+
implementation="platform-server",
|
|
240
|
+
specifier="pod-abc",
|
|
241
|
+
credentials=Credentials.api_key(api_key)
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`ServiceClient` differs from `AgentClient` in that it is workspace-less — it authenticates as itself and uses `AuthorizationContext` (on-behalf-of mode) to perform operations scoped to a user. Use `AgentClient` for long-running autonomous agents; use `ServiceClient` for backend services that proxy user actions.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Principal Types
|
|
250
|
+
|
|
251
|
+
Aether defines the following principal types. Each maps to a dedicated client class in the Python SDK.
|
|
252
|
+
|
|
253
|
+
| Principal Type | Python SDK Class | Identity Format | Description |
|
|
254
|
+
|---------------|-----------------|-----------------|-------------|
|
|
255
|
+
| **Agent** | `AgentClient` / `AsyncAgentClient` | `ag.{workspace}.{impl}.{spec}` | Long-running autonomous process with a globally unique identity. Receives messages, can create tasks, and persists state via KV and checkpoints. |
|
|
256
|
+
| **Task** | `TaskClient` / `AsyncTaskClient` | `tu.{workspace}.{impl}.{spec}` (unique) or `ta.{workspace}.{impl}.{id}` (pooled) | Short-lived compute unit. Unique tasks have named identities; non-unique (pooled) tasks receive a server-assigned ID and participate in load-balanced dispatch via `tb.*` topics. |
|
|
257
|
+
| **User** | `UserClient` / `AsyncUserClient` | `us.{user_id}.{window_id}` | Represents a human user session (e.g., a browser window). Multiple tabs may connect simultaneously using different `window_id` values. Can also receive workspace-scoped messages via `uw.{user_id}.{workspace}`. |
|
|
258
|
+
| **Orchestrator** | `OrchestratorClient` / `AsyncOrchestratorClient` | `oc.{workspace}.{impl}.{spec}` | Manages compute lifecycle — receives `TaskAssignment` messages and launches agents or tasks on demand. Registers supported profiles to receive matching assignments. |
|
|
259
|
+
| **WorkflowEngine** | `WorkflowEngineClient` / `AsyncWorkflowEngineClient` | `we.{workspace}.{impl}.{spec}` | Processes `EVENT`-type messages and triggers downstream automation. Has the broadest send permissions — can target any principal type. |
|
|
260
|
+
| **MetricsBridge** | `MetricsBridgeClient` / `AsyncMetricsBridgeClient` | `mb.{workspace}.{impl}.{spec}` | Receive-only telemetry sink. Collects `METRIC`-type messages from agents and tasks; cannot send messages. |
|
|
261
|
+
| **Service** | `ServiceClient` / `AsyncServiceClient` | `sv.{impl}.{spec}` | Trusted backend intermediary. Workspace-less; authenticates as itself and performs privileged operations on behalf of users via `AuthorizationContext`. Use for app/WebSocket backends proxying user actions. |
|
|
262
|
+
| **Bridge** | *(not yet in Python SDK)* | `br.{impl}.{spec}` | Cross-workspace messaging integration (e.g., Discord, Teams, Email). Has no workspace component and can send to any workspace. Implemented as a standalone server (`cmd/msgbridge`). |
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Callbacks
|
|
267
|
+
|
|
268
|
+
All clients support the following callbacks:
|
|
269
|
+
|
|
270
|
+
| Callback | Description | Signature |
|
|
271
|
+
|----------|-------------|-----------|
|
|
272
|
+
| `on_message` | Every incoming message (catch-all) | `(msg: IncomingMessage) -> None` |
|
|
273
|
+
| `on_chat_message` | CHAT-typed messages | `(msg: IncomingMessage) -> None` |
|
|
274
|
+
| `on_control_message` | CONTROL-typed messages | `(msg: IncomingMessage) -> None` |
|
|
275
|
+
| `on_tool_call` | TOOL_CALL-typed messages | `(msg: IncomingMessage) -> None` |
|
|
276
|
+
| `on_event` | EVENT-typed messages | `(msg: IncomingMessage) -> None` |
|
|
277
|
+
| `on_metric` | METRIC-typed messages | `(msg: IncomingMessage) -> None` |
|
|
278
|
+
| `on_config` | Configuration snapshot received | `(config: ConfigSnapshot) -> None` |
|
|
279
|
+
| `on_signal` | Signal received | `(signal: Signal) -> None` |
|
|
280
|
+
| `on_error` | Error occurred | `(error: ErrorResponse) -> None` |
|
|
281
|
+
| `on_kv_response` | Async KV operation response | `(kv: KVResponse) -> None` |
|
|
282
|
+
| `on_task_assignment` | Task assigned (Orchestrators) | `(assignment: TaskAssignment) -> None` |
|
|
283
|
+
| `on_checkpoint_response` | Async checkpoint response | `(response: CheckpointResponse) -> None` |
|
|
284
|
+
| `on_connect` | Connection established | `() -> None` |
|
|
285
|
+
| `on_disconnect` | Connection lost | `(reason: str) -> None` |
|
|
286
|
+
|
|
287
|
+
Typed handlers (`on_chat_message`, etc.) and the catch-all `on_message` are independent. If both are registered, the typed handler fires first, then `on_message` fires as well. This matches the behavior of the Go and TypeScript SDKs.
|
|
288
|
+
|
|
289
|
+
For async clients, callbacks can be either sync or async functions:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
# Sync callback
|
|
293
|
+
def on_message(msg):
|
|
294
|
+
print(msg.payload.decode())
|
|
295
|
+
|
|
296
|
+
# Async callback
|
|
297
|
+
async def on_message(msg):
|
|
298
|
+
await process_message(msg)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Messaging
|
|
302
|
+
|
|
303
|
+
### Message Types
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from scitrera_aether_client import CHAT, CONTROL, TOOL_CALL, EVENT, METRIC
|
|
307
|
+
|
|
308
|
+
# CHAT - Regular chat messages
|
|
309
|
+
client.send_message_to_agent(..., message_type=CHAT)
|
|
310
|
+
|
|
311
|
+
# CONTROL - Control/command messages
|
|
312
|
+
client.send_message_to_agent(..., message_type=CONTROL)
|
|
313
|
+
|
|
314
|
+
# EVENT - Events for workflow engine
|
|
315
|
+
client.send_event(payload)
|
|
316
|
+
|
|
317
|
+
# METRIC - Telemetry data for metrics bridge
|
|
318
|
+
client.send_metric(payload)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Sending Messages
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# To a specific agent
|
|
325
|
+
client.send_message_to_agent(
|
|
326
|
+
workspace="default",
|
|
327
|
+
implementation="worker",
|
|
328
|
+
specifier="01",
|
|
329
|
+
payload=b"Hello!"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# To a task
|
|
333
|
+
client.send_message_to_task(
|
|
334
|
+
workspace="default",
|
|
335
|
+
implementation="processor",
|
|
336
|
+
payload=b"Process this",
|
|
337
|
+
unique_specifier="task-123" # Optional for unique tasks
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# To a user session
|
|
341
|
+
client.send_message_to_user_session(
|
|
342
|
+
user_id="user-123",
|
|
343
|
+
window_id="window-abc",
|
|
344
|
+
payload=b"Notification"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Broadcast to all agents in workspace
|
|
348
|
+
client.send_broadcast_to_agents(
|
|
349
|
+
workspace="default",
|
|
350
|
+
payload=b"Broadcast message"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Send event (agents/tasks only)
|
|
354
|
+
client.send_event(b'{"event": "completed"}')
|
|
355
|
+
|
|
356
|
+
# Send metric (agents/tasks only)
|
|
357
|
+
client.send_metric(b'{"metric": "latency", "value": 42}')
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## KV Operations
|
|
361
|
+
|
|
362
|
+
The KV store supports multiple scopes:
|
|
363
|
+
|
|
364
|
+
| Scope | Description | Required Parameters |
|
|
365
|
+
|-------|-------------|---------------------|
|
|
366
|
+
| `global` | Global configuration | None |
|
|
367
|
+
| `workspace` | Workspace-specific | `workspace` |
|
|
368
|
+
| `user` | User-specific | `user_id` |
|
|
369
|
+
| `user-workspace` | User + workspace scoped | `user_id`, `workspace` |
|
|
370
|
+
|
|
371
|
+
### Synchronous Client KV
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
# Store a value (fire-and-forget)
|
|
375
|
+
client.kv_put(
|
|
376
|
+
key="config/setting",
|
|
377
|
+
value=b"value",
|
|
378
|
+
scope="global"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Store with workspace scope
|
|
382
|
+
client.kv_put(
|
|
383
|
+
key="team/setting",
|
|
384
|
+
value=b"team-value",
|
|
385
|
+
scope="workspace",
|
|
386
|
+
workspace="default"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Get a value (response via callback)
|
|
390
|
+
client.kv_get(key="config/setting", scope="global")
|
|
391
|
+
|
|
392
|
+
# List keys
|
|
393
|
+
client.kv_list(key_prefix="config/", scope="global")
|
|
394
|
+
|
|
395
|
+
# Delete a key
|
|
396
|
+
client.kv_delete(key="config/old", scope="global")
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Async Client KV
|
|
400
|
+
|
|
401
|
+
Async clients support both fire-and-forget (`_nowait`) and awaitable operations:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
# Fire-and-forget
|
|
405
|
+
await client.kv_put_nowait(
|
|
406
|
+
key="setting",
|
|
407
|
+
value=b"value",
|
|
408
|
+
scope="global"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Await response
|
|
412
|
+
response = await client.kv_get(
|
|
413
|
+
key="setting",
|
|
414
|
+
scope="global",
|
|
415
|
+
timeout=5.0
|
|
416
|
+
)
|
|
417
|
+
if response:
|
|
418
|
+
print(f"Value: {response.value}")
|
|
419
|
+
|
|
420
|
+
# Put and await confirmation
|
|
421
|
+
response = await client.kv_put(
|
|
422
|
+
key="setting",
|
|
423
|
+
value=b"new-value",
|
|
424
|
+
scope="global",
|
|
425
|
+
timeout=5.0
|
|
426
|
+
)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Task Creation
|
|
430
|
+
|
|
431
|
+
Create tasks with different assignment modes:
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from scitrera_aether_client import SELF_ASSIGN, TARGETED, POOL
|
|
435
|
+
|
|
436
|
+
# Self-assigned task (creator handles it)
|
|
437
|
+
client.create_task(
|
|
438
|
+
task_type="data-processing",
|
|
439
|
+
workspace="default",
|
|
440
|
+
assignment_mode=SELF_ASSIGN,
|
|
441
|
+
metadata={"priority": "high"}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Targeted task (assign to specific agent)
|
|
445
|
+
client.create_task(
|
|
446
|
+
task_type="specialized-work",
|
|
447
|
+
workspace="default",
|
|
448
|
+
assignment_mode=TARGETED,
|
|
449
|
+
target_agent_id="ag.default.worker.specialist-01",
|
|
450
|
+
launch_param_overrides={"memory": "4G"}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Pool task (load-balanced to available workers)
|
|
454
|
+
client.create_task(
|
|
455
|
+
task_type="batch-job",
|
|
456
|
+
workspace="default",
|
|
457
|
+
assignment_mode=POOL
|
|
458
|
+
)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Checkpointing
|
|
462
|
+
|
|
463
|
+
Save and restore agent/task state:
|
|
464
|
+
|
|
465
|
+
### Synchronous
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
# Save checkpoint
|
|
469
|
+
client.checkpoint_save(data=b"state data", key="my-checkpoint")
|
|
470
|
+
|
|
471
|
+
# Save and wait for confirmation
|
|
472
|
+
response = client.checkpoint_save_sync(
|
|
473
|
+
data=b"state data",
|
|
474
|
+
key="my-checkpoint",
|
|
475
|
+
timeout=5.0
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Load checkpoint
|
|
479
|
+
response = client.checkpoint_load_sync(key="my-checkpoint", timeout=5.0)
|
|
480
|
+
if response and response.data:
|
|
481
|
+
print(f"Restored state: {response.data}")
|
|
482
|
+
|
|
483
|
+
# List checkpoints
|
|
484
|
+
response = client.checkpoint_list_sync(timeout=5.0)
|
|
485
|
+
if response:
|
|
486
|
+
print(f"Checkpoints: {response.keys}")
|
|
487
|
+
|
|
488
|
+
# Delete checkpoint
|
|
489
|
+
client.checkpoint_delete_sync(key="my-checkpoint", timeout=5.0)
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Asynchronous
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
# Save and wait
|
|
496
|
+
response = await client.checkpoint_save(
|
|
497
|
+
data=b"state data",
|
|
498
|
+
key="my-checkpoint",
|
|
499
|
+
timeout=5.0
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Load
|
|
503
|
+
response = await client.checkpoint_load(key="my-checkpoint", timeout=5.0)
|
|
504
|
+
|
|
505
|
+
# Fire-and-forget operations
|
|
506
|
+
await client.checkpoint_save_nowait(data=b"state", key="quick-save")
|
|
507
|
+
await client.checkpoint_delete_nowait(key="old-checkpoint")
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Admin Operations
|
|
511
|
+
|
|
512
|
+
The SDK exposes named admin helpers via `AdminClient` (sync) and
|
|
513
|
+
`AsyncAdminClient` (async). Both wrap an already-connected client whose
|
|
514
|
+
authenticated identity has admin permissions on the gateway. The wrappers
|
|
515
|
+
own no connection of their own — pass in any connected
|
|
516
|
+
`AgentClient` / `UserClient` / `ServiceClient` (or their async
|
|
517
|
+
equivalents).
|
|
518
|
+
|
|
519
|
+
Covered surface: workspace CRUD, agent registry inspection, ACL rule
|
|
520
|
+
management and fallback policies, API token lifecycle, and read-side
|
|
521
|
+
workflow rule queries. The async wrapper additionally forwards
|
|
522
|
+
`list_connections` / `disconnect_session` to the underlying session-op
|
|
523
|
+
primitive.
|
|
524
|
+
|
|
525
|
+
### Synchronous
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
from scitrera_aether_client import AgentClient, AdminClient, Credentials
|
|
529
|
+
|
|
530
|
+
agent = AgentClient(
|
|
531
|
+
workspace="default",
|
|
532
|
+
implementation="admin-agent",
|
|
533
|
+
specifier="ops-1",
|
|
534
|
+
credentials=Credentials.api_key("my-admin-key"),
|
|
535
|
+
)
|
|
536
|
+
agent.connect("localhost:50051")
|
|
537
|
+
|
|
538
|
+
admin = AdminClient(agent)
|
|
539
|
+
|
|
540
|
+
# Workspaces
|
|
541
|
+
admin.create_workspace(workspace_id="ws-1", display_name="Workspace One")
|
|
542
|
+
workspaces = admin.list_workspaces(limit=50)
|
|
543
|
+
|
|
544
|
+
# Tokens
|
|
545
|
+
token = admin.create_token(name="ci-token", principal_type="agent")
|
|
546
|
+
print(token.plaintext_token)
|
|
547
|
+
admin.revoke_token(token_id=token.token.id)
|
|
548
|
+
|
|
549
|
+
# ACL
|
|
550
|
+
admin.create_acl_rule(
|
|
551
|
+
principal_type="user",
|
|
552
|
+
principal_id="alice",
|
|
553
|
+
resource_type="workspace",
|
|
554
|
+
resource_id="ws-1",
|
|
555
|
+
access_level=20, # READWRITE
|
|
556
|
+
granted_by="ops",
|
|
557
|
+
)
|
|
558
|
+
rules = admin.list_acl_rules(principal_type="user")
|
|
559
|
+
|
|
560
|
+
# Agents (read-only inspection)
|
|
561
|
+
agents = admin.list_agents()
|
|
562
|
+
info = admin.get_agent(implementation="scitrera/echo-bot")
|
|
563
|
+
|
|
564
|
+
# Workflow rules (read-only)
|
|
565
|
+
admin.list_workflow_rules(workspace="ws-1")
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Asynchronous
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
import asyncio
|
|
572
|
+
from scitrera_aether_client import (
|
|
573
|
+
AsyncAgentClient,
|
|
574
|
+
AsyncAdminClient,
|
|
575
|
+
Credentials,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
async def main():
|
|
579
|
+
agent = AsyncAgentClient(
|
|
580
|
+
workspace="default",
|
|
581
|
+
implementation="admin-agent",
|
|
582
|
+
specifier="ops-1",
|
|
583
|
+
credentials=Credentials.api_key("my-admin-key"),
|
|
584
|
+
)
|
|
585
|
+
await agent.connect("localhost:50051")
|
|
586
|
+
|
|
587
|
+
admin = AsyncAdminClient(agent)
|
|
588
|
+
|
|
589
|
+
await admin.create_workspace(workspace_id="ws-1", display_name="Workspace One")
|
|
590
|
+
workspaces = await admin.list_workspaces(limit=50)
|
|
591
|
+
|
|
592
|
+
token = await admin.create_token(name="ci-token", principal_type="agent")
|
|
593
|
+
print(token.plaintext_token)
|
|
594
|
+
|
|
595
|
+
# Session ops are forwarded to the underlying async client.
|
|
596
|
+
conns = await admin.list_connections(workspace="ws-1")
|
|
597
|
+
await admin.disconnect_session(session_id="abc-123", reason="ops cleanup")
|
|
598
|
+
|
|
599
|
+
asyncio.run(main())
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
> **Note**: A few methods on the TypeScript `AdminClient` (notably
|
|
603
|
+
> `getHealth`) rely on streaming pathways that are not yet exposed as
|
|
604
|
+
> primitives on the Python client. Use the REST admin endpoints for
|
|
605
|
+
> gateway health checks until the corresponding `admin_query` primitive
|
|
606
|
+
> is added.
|
|
607
|
+
|
|
608
|
+
## Connection Configuration
|
|
609
|
+
|
|
610
|
+
All clients support configurable connection behavior:
|
|
611
|
+
|
|
612
|
+
```python
|
|
613
|
+
client = AgentClient(
|
|
614
|
+
workspace="default",
|
|
615
|
+
implementation="worker",
|
|
616
|
+
specifier="01",
|
|
617
|
+
# Retry configuration
|
|
618
|
+
max_retries=5, # Max connection attempts (0 = infinite)
|
|
619
|
+
initial_backoff=1.0, # Initial retry delay in seconds
|
|
620
|
+
max_backoff=30.0, # Maximum retry delay
|
|
621
|
+
auto_reconnect=True # Auto-reconnect on connection loss
|
|
622
|
+
)
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Reconnection Behavior
|
|
626
|
+
|
|
627
|
+
- On connection loss, clients automatically attempt to reconnect (if `auto_reconnect=True`)
|
|
628
|
+
- Exponential backoff with jitter prevents thundering herd
|
|
629
|
+
- Non-recoverable errors (authentication failures, etc.) stop reconnection attempts
|
|
630
|
+
- Session IDs are preserved for session resumption when possible
|
|
631
|
+
|
|
632
|
+
## Workspace Switching
|
|
633
|
+
|
|
634
|
+
Agents and tasks can switch workspaces:
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
client.switch_workspace("new-workspace")
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
## Constants
|
|
641
|
+
|
|
642
|
+
```python
|
|
643
|
+
from scitrera_aether_client import (
|
|
644
|
+
# Message types
|
|
645
|
+
CHAT, # Regular messages
|
|
646
|
+
CONTROL, # Control/command messages
|
|
647
|
+
TOOL_CALL, # Tool invocations
|
|
648
|
+
EVENT, # Events for workflow engine
|
|
649
|
+
METRIC, # Telemetry data
|
|
650
|
+
|
|
651
|
+
# Task assignment modes
|
|
652
|
+
SELF_ASSIGN, # Creator handles the task
|
|
653
|
+
TARGETED, # Assign to specific agent
|
|
654
|
+
POOL, # Load-balanced assignment
|
|
655
|
+
|
|
656
|
+
# KV operations
|
|
657
|
+
KV_GET,
|
|
658
|
+
KV_PUT,
|
|
659
|
+
KV_LIST,
|
|
660
|
+
KV_DELETE,
|
|
661
|
+
|
|
662
|
+
# KV scopes
|
|
663
|
+
KV_SCOPE_GLOBAL,
|
|
664
|
+
KV_SCOPE_WORKSPACE,
|
|
665
|
+
KV_SCOPE_USER,
|
|
666
|
+
KV_SCOPE_USER_WORKSPACE,
|
|
667
|
+
)
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
## Examples
|
|
671
|
+
|
|
672
|
+
See the `example.py` and `example_async.py` files for comprehensive examples including:
|
|
673
|
+
|
|
674
|
+
- Agent client with messaging, KV operations, and task creation
|
|
675
|
+
- Orchestrator for managing agent lifecycle
|
|
676
|
+
- Workflow engine for event processing
|
|
677
|
+
- Metrics bridge for telemetry collection
|
|
678
|
+
- Concurrent async clients
|
|
679
|
+
- Context manager usage
|
|
680
|
+
|
|
681
|
+
Run examples:
|
|
682
|
+
|
|
683
|
+
```bash
|
|
684
|
+
# Sync examples
|
|
685
|
+
python example.py agent
|
|
686
|
+
python example.py orchestrator
|
|
687
|
+
python example.py workflow
|
|
688
|
+
python example.py metrics
|
|
689
|
+
|
|
690
|
+
# Async examples
|
|
691
|
+
python example_async.py agent
|
|
692
|
+
python example_async.py concurrent
|
|
693
|
+
python example_async.py context
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## TLS Configuration
|
|
697
|
+
|
|
698
|
+
```python
|
|
699
|
+
# Simple TLS (server authentication using system CA)
|
|
700
|
+
client = AgentClient(
|
|
701
|
+
workspace="default", implementation="worker", specifier="w1",
|
|
702
|
+
tls_enabled=True,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# Custom CA certificate
|
|
706
|
+
client = AgentClient(
|
|
707
|
+
workspace="default", implementation="worker", specifier="w1",
|
|
708
|
+
tls_enabled=True,
|
|
709
|
+
tls_root_cert_path="/path/to/ca.pem",
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# mTLS (mutual authentication)
|
|
713
|
+
client = AgentClient(
|
|
714
|
+
workspace="default", implementation="worker", specifier="w1",
|
|
715
|
+
tls_enabled=True,
|
|
716
|
+
tls_root_cert_path="/path/to/ca.pem",
|
|
717
|
+
tls_client_cert_path="/path/to/client.pem",
|
|
718
|
+
tls_client_key_path="/path/to/client-key.pem",
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
# Pass certificate bytes directly instead of paths
|
|
722
|
+
client = AgentClient(
|
|
723
|
+
workspace="default", implementation="worker", specifier="w1",
|
|
724
|
+
tls_enabled=True,
|
|
725
|
+
tls_root_cert=ca_cert_bytes,
|
|
726
|
+
tls_client_cert=client_cert_bytes,
|
|
727
|
+
tls_client_key=client_key_bytes,
|
|
728
|
+
)
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
## Proxy
|
|
732
|
+
|
|
733
|
+
Route HTTP requests through the Aether connection to a service principal using
|
|
734
|
+
the httpx transport adapters:
|
|
735
|
+
|
|
736
|
+
```python
|
|
737
|
+
import httpx
|
|
738
|
+
from scitrera_aether_client.httpx_transport import AetherHTTPXTransport
|
|
739
|
+
|
|
740
|
+
transport = AetherHTTPXTransport(aether_client, "sv::memorylayer::default")
|
|
741
|
+
with httpx.Client(transport=transport) as http:
|
|
742
|
+
resp = http.get("http://ignored/v1/memories/abc")
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Async variant:
|
|
746
|
+
|
|
747
|
+
```python
|
|
748
|
+
from scitrera_aether_client.httpx_transport import AetherAsyncHTTPXTransport
|
|
749
|
+
|
|
750
|
+
transport = AetherAsyncHTTPXTransport(aether_client, "sv::memorylayer::default")
|
|
751
|
+
async with httpx.AsyncClient(transport=transport) as http:
|
|
752
|
+
resp = await http.get("http://ignored/v1/memories/abc")
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
For `requests` users, mount `AetherRequestsAdapter` on a session:
|
|
756
|
+
|
|
757
|
+
```python
|
|
758
|
+
import requests
|
|
759
|
+
from scitrera_aether_client.requests_adapter import AetherRequestsAdapter
|
|
760
|
+
|
|
761
|
+
session = requests.Session()
|
|
762
|
+
session.mount("aether+sv://", AetherRequestsAdapter(aether_client))
|
|
763
|
+
session.get("aether+sv://memorylayer--default/v1/memories/abc")
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
For full details on sidecar deployment, service addressing, ACL/OBO model,
|
|
767
|
+
limits, audit events, and failure modes, see
|
|
768
|
+
[server/docs/proxy.md](../../server/docs/proxy.md).
|
|
769
|
+
|
|
770
|
+
## Key Architectural Principle
|
|
771
|
+
|
|
772
|
+
**Connection = Lock = Heartbeat**: The active gRPC stream IS the distributed lock AND the liveness proof for the connected identity. No separate heartbeat API exists. When the stream closes, the lock is released automatically.
|
|
773
|
+
|
|
774
|
+
## Requirements
|
|
775
|
+
|
|
776
|
+
- Python 3.11+
|
|
777
|
+
- grpcio >= 1.76.0
|
|
778
|
+
- protobuf >= 5.29.0
|
|
779
|
+
|
|
780
|
+
## License
|
|
781
|
+
|
|
782
|
+
Copyright 2024-2025 Scitrera. Licensed under the Apache License, Version 2.0.
|
|
783
|
+
|
|
784
|
+
## Links
|
|
785
|
+
|
|
786
|
+
- [GitHub Repository](https://github.com/scitrera/aether)
|
|
787
|
+
- [Documentation](https://github.com/scitrera/aether#readme)
|
|
788
|
+
- [Issue Tracker](https://github.com/scitrera/aether/issues)
|