layr8 0.2.0__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.
- layr8-0.2.0/.claude/CLAUDE.md +37 -0
- layr8-0.2.0/.claude/skills/build-layr8-agent.md +371 -0
- layr8-0.2.0/.github/workflows/ci.yaml +42 -0
- layr8-0.2.0/.github/workflows/release.yaml +120 -0
- layr8-0.2.0/.gitignore +7 -0
- layr8-0.2.0/CONTEXT.md +17 -0
- layr8-0.2.0/PKG-INFO +14 -0
- layr8-0.2.0/README.md +537 -0
- layr8-0.2.0/compat/Dockerfile +12 -0
- layr8-0.2.0/compat/bin/__init__.py +1 -0
- layr8-0.2.0/compat/bin/compat.py +104 -0
- layr8-0.2.0/compat/cloud_nodes.json +7 -0
- layr8-0.2.0/compat/conftest.py +6 -0
- layr8-0.2.0/compat/pyproject.toml +27 -0
- layr8-0.2.0/compat/scenarios/__init__.py +0 -0
- layr8-0.2.0/compat/scenarios/disconnected.py +56 -0
- layr8-0.2.0/compat/scenarios/echo.py +68 -0
- layr8-0.2.0/compat/scenarios/pass_scenario.py +66 -0
- layr8-0.2.0/compat/scenarios/types.py +39 -0
- layr8-0.2.0/compat/scenarios/wildcard.py +71 -0
- layr8-0.2.0/compat/tests/conftest.py +88 -0
- layr8-0.2.0/compat/tests/test_cli.py +37 -0
- layr8-0.2.0/compat/tests/test_disconnected.py +33 -0
- layr8-0.2.0/compat/tests/test_echo.py +186 -0
- layr8-0.2.0/compat/tests/test_pass.py +51 -0
- layr8-0.2.0/compat/tests/test_types.py +72 -0
- layr8-0.2.0/compat/tests/test_wildcard.py +50 -0
- layr8-0.2.0/docs/RELEASE-SETUP.md +72 -0
- layr8-0.2.0/docs/superpowers/plans/2026-05-15-reply-protocol.md +1241 -0
- layr8-0.2.0/docs/superpowers/plans/2026-05-17-compat-cli-fixes.md +1012 -0
- layr8-0.2.0/docs/superpowers/specs/2026-05-15-reply-protocol-design.md +112 -0
- layr8-0.2.0/docs/superpowers/specs/2026-05-17-compat-cli-fixes-release-pipeline.md +180 -0
- layr8-0.2.0/examples/chat.py +88 -0
- layr8-0.2.0/examples/durable_handler.py +66 -0
- layr8-0.2.0/examples/echo_agent.py +194 -0
- layr8-0.2.0/notes/features/prd-python-sdk-compat.md +251 -0
- layr8-0.2.0/pyproject.toml +28 -0
- layr8-0.2.0/src/layr8/__init__.py +46 -0
- layr8-0.2.0/src/layr8/backoff.py +22 -0
- layr8-0.2.0/src/layr8/channel.py +454 -0
- layr8-0.2.0/src/layr8/client.py +685 -0
- layr8-0.2.0/src/layr8/config.py +59 -0
- layr8-0.2.0/src/layr8/credentials.py +58 -0
- layr8-0.2.0/src/layr8/errors.py +130 -0
- layr8-0.2.0/src/layr8/handler.py +71 -0
- layr8-0.2.0/src/layr8/message.py +196 -0
- layr8-0.2.0/src/layr8/presentations.py +14 -0
- layr8-0.2.0/src/layr8/rest.py +146 -0
- layr8-0.2.0/src/layr8/sentinel.py +23 -0
- layr8-0.2.0/tests/__init__.py +0 -0
- layr8-0.2.0/tests/integration_test.py +361 -0
- layr8-0.2.0/tests/test_client.py +988 -0
- layr8-0.2.0/tests/test_config.py +58 -0
- layr8-0.2.0/tests/test_credentials.py +331 -0
- layr8-0.2.0/tests/test_handler.py +97 -0
- layr8-0.2.0/tests/test_message.py +244 -0
- layr8-0.2.0/tests/test_presentations.py +168 -0
- layr8-0.2.0/tests/test_reconnect.py +158 -0
- layr8-0.2.0/tests/test_rest.py +159 -0
- layr8-0.2.0/tests/test_sentinel.py +28 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python SDK Principles
|
|
2
|
+
|
|
3
|
+
## Public Repository
|
|
4
|
+
|
|
5
|
+
This is a public repository. Every commit is visible to the world.
|
|
6
|
+
|
|
7
|
+
### Before every commit, verify:
|
|
8
|
+
|
|
9
|
+
- **No real API keys, passwords, or tokens.** Test keys must be obviously fake (e.g., contain `testkey` in the string).
|
|
10
|
+
- **No internal infrastructure references.** No cloud account IDs, cluster names, or internal domain names. Only `*.localhost` and `example.com` are acceptable.
|
|
11
|
+
- **No internal documentation links.** No references to private repos, internal wikis, or private channels.
|
|
12
|
+
- **No customer data or PII.**
|
|
13
|
+
|
|
14
|
+
### Acceptable
|
|
15
|
+
|
|
16
|
+
- Local-dev test keys with obvious patterns (e.g., `alice_abcd1234_testkeyalicetestkeyali24`)
|
|
17
|
+
- `*.localhost` URLs for local development
|
|
18
|
+
- `did:web:*.localhost:*` test DIDs
|
|
19
|
+
- Unit test sentinel values like `"test-api-key"`
|
|
20
|
+
|
|
21
|
+
### Not acceptable
|
|
22
|
+
|
|
23
|
+
- Keys that follow production format without obvious test markers
|
|
24
|
+
- Internal service URLs (`.internal`, `.corp`, `.svc.cluster.local`)
|
|
25
|
+
- `.env` files with real values (`.env.example` with placeholders is fine)
|
|
26
|
+
|
|
27
|
+
## Testing
|
|
28
|
+
|
|
29
|
+
- Run tests before every commit
|
|
30
|
+
- Integration tests require a local dev environment with two cloud-nodes (alice-test, bob-test)
|
|
31
|
+
|
|
32
|
+
## Conventions
|
|
33
|
+
|
|
34
|
+
- Async/await with `asyncio`
|
|
35
|
+
- `@dataclass` for structured types (`Credential`, `StoredCredential`, `VerifiedPresentation`)
|
|
36
|
+
- Custom `aiohttp` resolver (`_LocalhostResolver`) for `*.localhost` resolution (RFC 6761)
|
|
37
|
+
- Snake_case for all public API methods (e.g., `sign_credential`, `verify_presentation`)
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-layr8-agent
|
|
3
|
+
description: Use when building a Python agent for the Layr8 platform. Covers the full SDK API — config, handlers, messaging, error handling, and DIDComm conventions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building Layr8 Agents with the Python SDK
|
|
7
|
+
|
|
8
|
+
Full documentation: https://docs.layr8.io/reference/python-sdk
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from layr8 import Client, Config, Message, log_errors
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Python 3.11+. The SDK is fully async, built on `asyncio` and `websockets`.
|
|
17
|
+
|
|
18
|
+
## Config
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
client = Client(Config(
|
|
22
|
+
node_url="ws://mynode.localhost/plugin_socket/websocket",
|
|
23
|
+
api_key="my_api_key",
|
|
24
|
+
agent_did="did:web:mynode.localhost:my-agent",
|
|
25
|
+
), log_errors())
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `Client` constructor takes two required arguments:
|
|
29
|
+
1. `Config(...)` — connection configuration
|
|
30
|
+
2. `on_error` — a `Callable[[SDKError], None]` that receives all SDK-level errors
|
|
31
|
+
|
|
32
|
+
Use `log_errors()` for a convenient default that logs errors via `logging.getLogger("layr8")`.
|
|
33
|
+
|
|
34
|
+
All Config fields fall back to environment variables if empty:
|
|
35
|
+
- `node_url` → `LAYR8_NODE_URL`
|
|
36
|
+
- `api_key` → `LAYR8_API_KEY`
|
|
37
|
+
- `agent_did` → `LAYR8_AGENT_DID`
|
|
38
|
+
|
|
39
|
+
`agent_did` is optional — if omitted, the node assigns an ephemeral DID on connect.
|
|
40
|
+
|
|
41
|
+
## Lifecycle
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Client(Config, on_error) → handle (register handlers) → connect → ... → close
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or using the async context manager:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Client(Config, on_error) → handle → async with client: ...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `handle` must be called BEFORE `connect` — raises `AlreadyConnectedError` after.
|
|
54
|
+
- `connect()` establishes WebSocket and joins the Phoenix Channel. Returns a coroutine.
|
|
55
|
+
- `close()` sends `phx_leave` and shuts down gracefully. Returns a coroutine.
|
|
56
|
+
- `did` (property) returns the agent's DID (explicit or node-assigned).
|
|
57
|
+
- Supports `async with` context manager (`__aenter__`/`__aexit__`).
|
|
58
|
+
|
|
59
|
+
## Registering Handlers
|
|
60
|
+
|
|
61
|
+
### Decorator Syntax
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
@client.handle("https://layr8.io/protocols/echo/1.0/request")
|
|
65
|
+
async def echo(msg: Message) -> Message:
|
|
66
|
+
body = msg.unmarshal_body()
|
|
67
|
+
return Message(
|
|
68
|
+
type="https://layr8.io/protocols/echo/1.0/response",
|
|
69
|
+
body={"echo": body["message"]},
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Direct Call
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
async def echo(msg: Message) -> Message:
|
|
77
|
+
body = msg.unmarshal_body()
|
|
78
|
+
return Message(
|
|
79
|
+
type="https://layr8.io/protocols/echo/1.0/response",
|
|
80
|
+
body={"echo": body["message"]},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
client.handle("https://layr8.io/protocols/echo/1.0/request", echo)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Handler return values:
|
|
87
|
+
- `Message(...)` → send response to sender (auto-fills `id`, `from_`, `to`, `thread_id`)
|
|
88
|
+
- `None` → no response (fire-and-forget inbound)
|
|
89
|
+
- Raised exception → send DIDComm problem report to sender
|
|
90
|
+
|
|
91
|
+
The protocol base URI is derived automatically from the message type
|
|
92
|
+
(last path segment removed) and registered with the node on connect.
|
|
93
|
+
|
|
94
|
+
## Sending Messages
|
|
95
|
+
|
|
96
|
+
### Send (with server ack)
|
|
97
|
+
|
|
98
|
+
By default, `send()` waits for the server to acknowledge receipt. If the server rejects the message, a `RuntimeError` is raised.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
await client.send(Message(
|
|
102
|
+
type="https://didcomm.org/basicmessage/2.0/message",
|
|
103
|
+
to=["did:web:other-node:agent"],
|
|
104
|
+
body={"content": "Hello!"},
|
|
105
|
+
))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Fire-and-forget
|
|
109
|
+
|
|
110
|
+
To skip waiting for the server acknowledgment, pass `fire_and_forget=True`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
await client.send(
|
|
114
|
+
Message(
|
|
115
|
+
type="https://didcomm.org/basicmessage/2.0/message",
|
|
116
|
+
to=["did:web:other-node:agent"],
|
|
117
|
+
body={"content": "Hello!"},
|
|
118
|
+
),
|
|
119
|
+
fire_and_forget=True,
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Request/Response
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
resp = await client.request(
|
|
127
|
+
Message(
|
|
128
|
+
type="https://layr8.io/protocols/echo/1.0/request",
|
|
129
|
+
to=["did:web:other-node:agent"],
|
|
130
|
+
body={"message": "ping"},
|
|
131
|
+
),
|
|
132
|
+
timeout=5.0,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
body = resp.unmarshal_body()
|
|
136
|
+
# resp is the correlated response (matched by thread ID)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Message Structure
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
@dataclass
|
|
143
|
+
class Message:
|
|
144
|
+
id: str = "" # auto-generated if empty
|
|
145
|
+
type: str = "" # DIDComm message type URI
|
|
146
|
+
from_: str = "" # auto-filled with agent DID (wire: "from")
|
|
147
|
+
to: list[str] = field(default_factory=list) # recipient DIDs
|
|
148
|
+
thread_id: str = "" # auto-generated for request (wire: "thid")
|
|
149
|
+
parent_thread_id: str = "" # set via parent_thread param (wire: "pthid")
|
|
150
|
+
body: Any = None # serialized to JSON
|
|
151
|
+
context: MessageContext | None = None # populated on inbound messages
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Important:** The `from` field is named `from_` because `from` is a Python reserved word. On the wire, it serializes as `"from"`.
|
|
155
|
+
|
|
156
|
+
### Inbound Message Context
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
if msg.context:
|
|
160
|
+
msg.context.authorized # bool — node authorization result
|
|
161
|
+
msg.context.recipient # str — recipient DID
|
|
162
|
+
msg.context.sender_credentials # list[Credential] — each has .id, .name
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Unmarshaling Body
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# As a dict
|
|
169
|
+
body = msg.unmarshal_body()
|
|
170
|
+
|
|
171
|
+
# As a typed dataclass
|
|
172
|
+
@dataclass
|
|
173
|
+
class EchoRequest:
|
|
174
|
+
message: str
|
|
175
|
+
|
|
176
|
+
body = msg.unmarshal_body(EchoRequest)
|
|
177
|
+
print(body.message) # typed attribute access
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Options
|
|
181
|
+
|
|
182
|
+
### Manual Ack
|
|
183
|
+
|
|
184
|
+
By default, messages are auto-acked before the handler runs.
|
|
185
|
+
Use `manual_ack` to control ack timing (e.g., ack only after DB write):
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
@client.handle(msg_type, manual_ack=True)
|
|
189
|
+
async def handler(msg: Message) -> Message:
|
|
190
|
+
result = process(msg)
|
|
191
|
+
msg.ack() # explicitly ack after processing
|
|
192
|
+
return Message(type=result_type, body=result)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Parent Thread
|
|
196
|
+
|
|
197
|
+
For nested thread correlation:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
resp = await client.request(msg, parent_thread="parent-thread-id", timeout=10.0)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Error Handling
|
|
204
|
+
|
|
205
|
+
### ErrorHandler (on_error callback)
|
|
206
|
+
|
|
207
|
+
The `on_error` callback receives `SDKError` instances for all SDK-level errors:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from layr8 import SDKError, ErrorKind
|
|
211
|
+
|
|
212
|
+
def my_handler(err: SDKError) -> None:
|
|
213
|
+
print(f"[{err.kind.value}] {err.cause}")
|
|
214
|
+
|
|
215
|
+
client = Client(Config(...), my_handler)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`ErrorKind` values:
|
|
219
|
+
- `PARSE_FAILURE` — inbound message could not be parsed
|
|
220
|
+
- `NO_HANDLER` — no handler registered for the message type
|
|
221
|
+
- `HANDLER_EXCEPTION` — a handler raised an exception
|
|
222
|
+
- `SERVER_REJECT` — the server rejected a sent message
|
|
223
|
+
- `TRANSPORT_WRITE` — failed to write to WebSocket
|
|
224
|
+
|
|
225
|
+
### Problem Reports
|
|
226
|
+
|
|
227
|
+
When a remote handler raises, `request` raises `ProblemReportError`:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from layr8 import ProblemReportError
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
resp = await client.request(msg)
|
|
234
|
+
except ProblemReportError as e:
|
|
235
|
+
print(f"remote error [{e.code}]: {e.comment}")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Error Classes
|
|
239
|
+
|
|
240
|
+
- `NotConnectedError` — `send`/`request` called before `connect`
|
|
241
|
+
- `AlreadyConnectedError` — `handle` called after `connect`
|
|
242
|
+
- `ClientClosedError` — `connect` called after `close`
|
|
243
|
+
- `Layr8ConnectionError` — failed to connect to node (`.url`, `.reason`)
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from layr8.errors import Layr8ConnectionError
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
await client.connect()
|
|
250
|
+
except Layr8ConnectionError as e:
|
|
251
|
+
print(f"failed to connect to {e.url}: {e.reason}")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Note: `request()` raises `asyncio.TimeoutError` on timeout (uses `asyncio.wait_for`).
|
|
255
|
+
|
|
256
|
+
## Connection Resilience
|
|
257
|
+
|
|
258
|
+
The SDK automatically reconnects when the WebSocket connection drops (node restart, network interruption). Reconnection uses exponential backoff (1s → 2s → 4s → ... → 30s max).
|
|
259
|
+
|
|
260
|
+
During reconnection:
|
|
261
|
+
- `send()`, `request()`, and other operations raise `NotConnectedError` immediately — no message queuing
|
|
262
|
+
- `close()` stops the reconnect loop
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
@client.on_disconnect
|
|
266
|
+
def handle_disconnect(err: Exception):
|
|
267
|
+
print(f"connection lost: {err}")
|
|
268
|
+
|
|
269
|
+
@client.on_reconnect
|
|
270
|
+
def handle_reconnect():
|
|
271
|
+
print("reconnected")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`on_disconnect` fires only on unexpected drops, not on `close()`.
|
|
275
|
+
|
|
276
|
+
## DID and Protocol Conventions
|
|
277
|
+
|
|
278
|
+
### DID Format
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
did:web:{node-domain}:{agent-path}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Examples:
|
|
285
|
+
- `did:web:alice-test.localhost:my-agent`
|
|
286
|
+
- `did:web:earth.node.layr8.org:echo-service`
|
|
287
|
+
|
|
288
|
+
### Protocol URI Format
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
https://layr8.io/protocols/{name}/{version}/{message-type}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The base URI (without the last segment) is the protocol identifier.
|
|
295
|
+
Example: `https://layr8.io/protocols/echo/1.0/request` → protocol `https://layr8.io/protocols/echo/1.0`
|
|
296
|
+
|
|
297
|
+
### Standard Protocols
|
|
298
|
+
|
|
299
|
+
- Basic message: `https://didcomm.org/basicmessage/2.0/message`
|
|
300
|
+
- Problem report: `https://didcomm.org/report-problem/2.0/problem-report`
|
|
301
|
+
|
|
302
|
+
## Complete Example: Echo Agent
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
import asyncio
|
|
306
|
+
from layr8 import Client, Config, Message, log_errors
|
|
307
|
+
|
|
308
|
+
ECHO_REQUEST = "https://layr8.io/protocols/echo/1.0/request"
|
|
309
|
+
ECHO_RESPONSE = "https://layr8.io/protocols/echo/1.0/response"
|
|
310
|
+
|
|
311
|
+
client = Client(Config(), log_errors())
|
|
312
|
+
|
|
313
|
+
@client.handle(ECHO_REQUEST)
|
|
314
|
+
async def echo(msg: Message) -> Message:
|
|
315
|
+
body = msg.unmarshal_body()
|
|
316
|
+
return Message(
|
|
317
|
+
type=ECHO_RESPONSE,
|
|
318
|
+
body={"echo": body["message"]},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
async def main():
|
|
322
|
+
async with client:
|
|
323
|
+
print(f"echo agent running as {client.did}")
|
|
324
|
+
await asyncio.Event().wait()
|
|
325
|
+
|
|
326
|
+
asyncio.run(main())
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Complete Example: Request/Response Client
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
import asyncio
|
|
333
|
+
from layr8 import Client, Config, Message, ProblemReportError, log_errors
|
|
334
|
+
|
|
335
|
+
ECHO_REQUEST = "https://layr8.io/protocols/echo/1.0/request"
|
|
336
|
+
|
|
337
|
+
client = Client(Config(), log_errors())
|
|
338
|
+
|
|
339
|
+
# Must register the protocol even if not handling inbound
|
|
340
|
+
@client.handle(ECHO_REQUEST)
|
|
341
|
+
async def noop(msg: Message) -> None:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
async def main():
|
|
345
|
+
async with client:
|
|
346
|
+
try:
|
|
347
|
+
resp = await client.request(
|
|
348
|
+
Message(
|
|
349
|
+
type=ECHO_REQUEST,
|
|
350
|
+
to=["did:web:other-node:echo-agent"],
|
|
351
|
+
body={"message": "Hello!"},
|
|
352
|
+
),
|
|
353
|
+
timeout=5.0,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
body = resp.unmarshal_body()
|
|
357
|
+
print(f"response: {body['echo']}")
|
|
358
|
+
except ProblemReportError as e:
|
|
359
|
+
print(f"remote error [{e.code}]: {e.comment}")
|
|
360
|
+
except asyncio.TimeoutError:
|
|
361
|
+
print("request timed out")
|
|
362
|
+
|
|
363
|
+
asyncio.run(main())
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## More Examples
|
|
367
|
+
|
|
368
|
+
See the `examples/` directory in the SDK repo for complete working agents:
|
|
369
|
+
- `examples/echo_agent.py` — minimal echo service
|
|
370
|
+
- `examples/chat.py` — interactive chat client
|
|
371
|
+
- `examples/durable_handler.py` — persist-then-ack with JSON-lines
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v5
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: pip install -e ".[dev]"
|
|
22
|
+
|
|
23
|
+
- name: Test
|
|
24
|
+
run: pytest -v
|
|
25
|
+
|
|
26
|
+
compat-unit:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v5
|
|
30
|
+
|
|
31
|
+
- name: Set up Python
|
|
32
|
+
uses: actions/setup-python@v6
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
|
|
36
|
+
- name: Install SDK and compat deps
|
|
37
|
+
run: |
|
|
38
|
+
pip install -e .
|
|
39
|
+
pip install -e "compat/[test]"
|
|
40
|
+
|
|
41
|
+
- name: Compat unit tests
|
|
42
|
+
run: cd compat && pytest tests/ -v --ignore=tests/conftest.py -k "not layer1"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
|
|
24
|
+
- name: Test
|
|
25
|
+
run: pytest -v
|
|
26
|
+
|
|
27
|
+
compat-unit:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v5
|
|
31
|
+
|
|
32
|
+
- name: Set up Python
|
|
33
|
+
uses: actions/setup-python@v6
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
|
|
37
|
+
- name: Install SDK and compat deps
|
|
38
|
+
run: |
|
|
39
|
+
pip install -e .
|
|
40
|
+
pip install -e "compat/[test]"
|
|
41
|
+
|
|
42
|
+
- name: Compat unit tests
|
|
43
|
+
run: cd compat && pytest tests/ -v --ignore=tests/conftest.py -k "not layer1"
|
|
44
|
+
|
|
45
|
+
validate-version:
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
outputs:
|
|
48
|
+
version: ${{ steps.check.outputs.version }}
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@v5
|
|
51
|
+
|
|
52
|
+
- name: Set up Python
|
|
53
|
+
uses: actions/setup-python@v6
|
|
54
|
+
with:
|
|
55
|
+
python-version: "3.12"
|
|
56
|
+
|
|
57
|
+
- name: Validate tag matches pyproject.toml
|
|
58
|
+
id: check
|
|
59
|
+
run: |
|
|
60
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
61
|
+
TOML_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
62
|
+
if [ "$TAG_VERSION" != "$TOML_VERSION" ]; then
|
|
63
|
+
echo "::error::Tag $TAG_VERSION != pyproject.toml $TOML_VERSION"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
|
|
67
|
+
|
|
68
|
+
publish-pypi:
|
|
69
|
+
needs: [test, compat-unit, validate-version]
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
permissions:
|
|
72
|
+
contents: read
|
|
73
|
+
id-token: write
|
|
74
|
+
environment: pypi
|
|
75
|
+
steps:
|
|
76
|
+
- uses: actions/checkout@v5
|
|
77
|
+
|
|
78
|
+
- name: Set up Python
|
|
79
|
+
uses: actions/setup-python@v6
|
|
80
|
+
with:
|
|
81
|
+
python-version: "3.12"
|
|
82
|
+
|
|
83
|
+
- name: Build
|
|
84
|
+
run: pip install build && python -m build
|
|
85
|
+
|
|
86
|
+
- name: Publish to PyPI
|
|
87
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
88
|
+
|
|
89
|
+
publish-compat-image:
|
|
90
|
+
needs: [publish-pypi, validate-version]
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
permissions:
|
|
93
|
+
contents: read
|
|
94
|
+
packages: write
|
|
95
|
+
steps:
|
|
96
|
+
- uses: actions/checkout@v5
|
|
97
|
+
|
|
98
|
+
- name: Log in to ghcr.io
|
|
99
|
+
uses: docker/login-action@v4
|
|
100
|
+
with:
|
|
101
|
+
registry: ghcr.io
|
|
102
|
+
username: ${{ github.actor }}
|
|
103
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
104
|
+
|
|
105
|
+
- name: Build and push compat image
|
|
106
|
+
run: |
|
|
107
|
+
VERSION=${{ needs.validate-version.outputs.version }}
|
|
108
|
+
IMAGE=ghcr.io/layr8/python-sdk/compat
|
|
109
|
+
docker build \
|
|
110
|
+
--build-arg SDK_VERSION=$VERSION \
|
|
111
|
+
-t $IMAGE:$VERSION \
|
|
112
|
+
-f compat/Dockerfile compat/
|
|
113
|
+
docker push $IMAGE:$VERSION
|
|
114
|
+
|
|
115
|
+
# compat-gate:
|
|
116
|
+
# needs: [publish-compat-image, validate-version]
|
|
117
|
+
# uses: layr8/compat-suite/.github/workflows/gate.yml@main
|
|
118
|
+
# with:
|
|
119
|
+
# sdk: python
|
|
120
|
+
# version: ${{ needs.validate-version.outputs.version }}
|
layr8-0.2.0/.gitignore
ADDED
layr8-0.2.0/CONTEXT.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Context — Layr8 Python SDK
|
|
2
|
+
|
|
3
|
+
## Ubiquitous Language
|
|
4
|
+
|
|
5
|
+
| Term | Definition |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Agent** | A software process that connects to a cloud-node and exchanges DIDComm v2 messages. An agent is identified by a DID. |
|
|
8
|
+
| **Cloud-node** | A Layr8 infrastructure component that routes DIDComm messages between agents. Agents connect via WebSocket using the Phoenix Channel V2 protocol. |
|
|
9
|
+
| **DID** | Decentralized Identifier — a globally unique agent identity (e.g., `did:web:myorg:my-agent`). May be configured explicitly or assigned by the cloud-node on connect. |
|
|
10
|
+
| **Handler** | An async function registered for a specific DIDComm message type. Receives a `Message`, returns a response `Message`, `None`, or `PASS`. |
|
|
11
|
+
| **PASS** | A sentinel value returned by a handler to decline a message — signals to the cloud-node that this agent does not handle this message type. |
|
|
12
|
+
| **Scenario** | A compat-suite test case. Each scenario is a pair of async functions (`run_receiver`, `run_sender`) that exercise a specific SDK behavior against a cloud-node. |
|
|
13
|
+
| **Compat image** | A Docker image (`ghcr.io/layr8/python-sdk/compat:{version}`) that packages the scenario code and CLI adapter. Consumed by the compat-suite orchestrator. |
|
|
14
|
+
| **Ready signal** | A JSON line (`{"status":"ready","did":"..."}`) printed to stdout by a receiver process after connecting and registering handlers. The compat-suite orchestrator waits for this before launching the sender. |
|
|
15
|
+
| **Layer 1** | Pytest + testcontainers adapter — runs scenarios against real cloud-node Docker containers. |
|
|
16
|
+
| **Layer 2** | CLI adapter — implements the compat-suite orchestrator's interface (`--mode`, `--scenario`, `--node`, `--did`). |
|
|
17
|
+
| **Compat-suite orchestrator** | A separate repo (`layr8/compat-suite`) that pairs SDK compat images across languages and cloud-node versions, runs test matrices, and produces compatibility reports. |
|
layr8-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: layr8
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Layr8 DIDComm Agent SDK for Python
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: aiohttp>=3.9
|
|
8
|
+
Requires-Dist: websockets>=13.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/plain
|
|
13
|
+
|
|
14
|
+
Layr8 DIDComm Agent SDK for Python
|