reasoning-ledger 0.1.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.
- reasoning_ledger-0.1.0/PKG-INFO +333 -0
- reasoning_ledger-0.1.0/README.md +321 -0
- reasoning_ledger-0.1.0/pyproject.toml +37 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/__init__.py +83 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/client.py +287 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/constants.py +49 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/errors.py +73 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/generated/__init__.py +2 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/generated/records.py +208 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/http.py +160 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/py.typed +0 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/session.py +36 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/types.py +169 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/utils.py +28 -0
- reasoning_ledger-0.1.0/src/reasoning_ledger/validate.py +191 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reasoning-ledger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for interacting with the Reasoning Ledger.
|
|
5
|
+
Author: Arslan Ablikim
|
|
6
|
+
Author-email: Arslan Ablikim <arslan.ablikim0@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: pydantic>=2.13.3
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# reasoning-ledger
|
|
14
|
+
|
|
15
|
+
Python SDK for the [Reasoning Ledger](https://github.com/StairAI/Reasoning-Ledger) — a tamper-evident audit trail for AI agent reasoning.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pip install reasoning-ledger
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires **Python 3.12+**. Dependencies: `pydantic>=2`, `httpx>=0.27`.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
### 1. Register an agent
|
|
28
|
+
|
|
29
|
+
Agent registration is idempotent on `(owner, name)` — calling it again with the same name returns the existing agent.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import os
|
|
33
|
+
from reasoning_ledger import LedgerClient, RegisterAgentOpts
|
|
34
|
+
|
|
35
|
+
reg = LedgerClient.register_agent(RegisterAgentOpts(
|
|
36
|
+
api_key=os.environ["STAIRAI_API_KEY"],
|
|
37
|
+
name="my-agent",
|
|
38
|
+
))
|
|
39
|
+
|
|
40
|
+
agent_id = reg["agent_id"]
|
|
41
|
+
# Store agent_id — you'll need it every time you construct LedgerClient.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you already have an `agent_id` (e.g. stored in config), skip registration. To look up an agent ID by name at startup:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from reasoning_ledger import LedgerClient, ResolveAgentOpts
|
|
48
|
+
|
|
49
|
+
agent_id = LedgerClient.resolve_agent_id(ResolveAgentOpts(
|
|
50
|
+
api_key=os.environ["STAIRAI_API_KEY"],
|
|
51
|
+
name="my-agent",
|
|
52
|
+
))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Create a client
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from reasoning_ledger import LedgerClient, LedgerClientConfig
|
|
59
|
+
|
|
60
|
+
client = LedgerClient(LedgerClientConfig(
|
|
61
|
+
api_key=os.environ["STAIRAI_API_KEY"],
|
|
62
|
+
agent_id=agent_id,
|
|
63
|
+
))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The constructor performs no network call. The API key and agent ID are validated lazily on the first request.
|
|
67
|
+
|
|
68
|
+
### 3. Open a session and submit records
|
|
69
|
+
|
|
70
|
+
A `Session` pins a `session_id` so you don't have to pass it on every record. It is purely local sugar — there is no server-side session lifecycle.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import json
|
|
74
|
+
|
|
75
|
+
session = client.new_session() # auto-generates a session_id
|
|
76
|
+
|
|
77
|
+
# Observing — the trigger that woke your agent
|
|
78
|
+
session.submit({
|
|
79
|
+
"behavior": "Observing",
|
|
80
|
+
"trigger_source": "sportradar",
|
|
81
|
+
"trigger_type": "signal_trigger",
|
|
82
|
+
"trigger_description": "Match update: Spain vs Morocco, minute 47",
|
|
83
|
+
"trigger_payload_summary": "Spain xG 0.41, possession 62%, shots 8-2",
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
# ToolCalling — external data fetch
|
|
87
|
+
session.submit({
|
|
88
|
+
"behavior": "ToolCalling",
|
|
89
|
+
"tool_meta": {"tool_id": "polymarket_api", "category": "external_api"},
|
|
90
|
+
"description": "Fetch current Spain win odds",
|
|
91
|
+
"input_payload": json.dumps({"market": "esp_mar"}),
|
|
92
|
+
"output_payload": json.dumps({"spain_win": 0.73}),
|
|
93
|
+
"success": True,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# Thinking — analysis and decision
|
|
97
|
+
session.submit({
|
|
98
|
+
"behavior": "Thinking",
|
|
99
|
+
"prompt": "Given xG 0.41 and odds 0.73, should I adjust the position?",
|
|
100
|
+
"inputs": [],
|
|
101
|
+
"output_payload": json.dumps({"recommendation": "hold", "confidence": 0.81}),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# Acting — the commitment
|
|
105
|
+
session.submit({
|
|
106
|
+
"behavior": "Acting",
|
|
107
|
+
"action_type": "trade",
|
|
108
|
+
"target_system": "broker-api",
|
|
109
|
+
"action_summary": "Hold current Spain win position",
|
|
110
|
+
"parameters": {"symbol": "ESP_WIN", "action": "hold"},
|
|
111
|
+
"dry_run": False,
|
|
112
|
+
"execution_status": "confirmed",
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 4. Submit a batch
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
batch_ack = session.submit_batch([
|
|
120
|
+
{"behavior": "Thinking", "prompt": "...", "inputs": [], "output_payload": "..."},
|
|
121
|
+
{"behavior": "Acting", "action_type": "...", ...},
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
for result in batch_ack["results"]:
|
|
125
|
+
if "code" in result:
|
|
126
|
+
print("Record failed:", result["record_id"], result["code"], result["message"])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Up to 50 records per batch. Per-record validation runs locally before the network call; only locally-valid records are sent. Partial server-side failure does not raise — inspect `BatchAck["results"]`.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Behavior types
|
|
134
|
+
|
|
135
|
+
All seven behaviors extend the base record fields. The `"behavior"` key is the discriminant.
|
|
136
|
+
|
|
137
|
+
| Behavior | Required fields (beyond base) |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `"Observing"` | `trigger_source`, `trigger_type`, `trigger_description`, `trigger_payload_summary` |
|
|
140
|
+
| `"Planning"` | `goal`, `steps` |
|
|
141
|
+
| `"Thinking"` | `prompt`, `inputs`, `output_payload` |
|
|
142
|
+
| `"Acting"` | `action_type`, `target_system`, `action_summary`, `parameters`, `dry_run`, `execution_status` |
|
|
143
|
+
| `"Reflecting"` | `inputs`, `output_payload` |
|
|
144
|
+
| `"ToolCalling"` | `tool_meta`, `description`, `input_payload`, `output_payload`, `success` |
|
|
145
|
+
| `"Other"` | `label`, `data` |
|
|
146
|
+
|
|
147
|
+
### Auto-filled fields
|
|
148
|
+
|
|
149
|
+
The SDK fills these if you omit them:
|
|
150
|
+
|
|
151
|
+
| Field | SDK default |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `record_id` | Fresh UUID v4 |
|
|
154
|
+
| `schema_version` | `"1.0"` (bundled constant) |
|
|
155
|
+
| `client_ts_utc` | Current epoch milliseconds |
|
|
156
|
+
| `agent_id` | From `LedgerClientConfig.agent_id` |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Error handling
|
|
161
|
+
|
|
162
|
+
All errors inherit from `LedgerError` and carry a stable `code` string:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from reasoning_ledger import (
|
|
166
|
+
AuthError,
|
|
167
|
+
IdempotencyConflictError,
|
|
168
|
+
LedgerError,
|
|
169
|
+
NetworkError,
|
|
170
|
+
NotFoundError,
|
|
171
|
+
RateLimitError,
|
|
172
|
+
ServerError,
|
|
173
|
+
ValidationError,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
session.submit({...})
|
|
178
|
+
except ValidationError as e:
|
|
179
|
+
# Local schema check failed — never reached the network
|
|
180
|
+
print(e.details.get("field"), e.details.get("reason"))
|
|
181
|
+
except RateLimitError as e:
|
|
182
|
+
wait_ms = e.details.get("retry_after_ms")
|
|
183
|
+
# back off and retry
|
|
184
|
+
except LedgerError as e:
|
|
185
|
+
print(e.code, e.message)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Class | `code` | When |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `ValidationError` | `validation_failed` | Local schema check failed; record never sent |
|
|
191
|
+
| `AuthError` | `auth_invalid` | API key rejected |
|
|
192
|
+
| `RateLimitError` | `rate_limited` | Server rate-limited the request |
|
|
193
|
+
| `NetworkError` | `network_failed` | Request never reached the server after retries |
|
|
194
|
+
| `ServerError` | `server_5xx` | Non-retryable 5xx from server |
|
|
195
|
+
| `IdempotencyConflictError` | `record_id_conflict` | Same `record_id` submitted with different body |
|
|
196
|
+
| `NotFoundError` | `not_found` | Lookup target does not exist |
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from reasoning_ledger import LedgerClientConfig
|
|
204
|
+
|
|
205
|
+
config = LedgerClientConfig(
|
|
206
|
+
api_key="sl_...",
|
|
207
|
+
agent_id="uuid-v4",
|
|
208
|
+
|
|
209
|
+
# Target environment — defaults to "production"
|
|
210
|
+
environment="production", # | "staging" | "development"
|
|
211
|
+
|
|
212
|
+
# Override base URL (takes precedence over `environment`)
|
|
213
|
+
endpoint="https://custom.api.example.com",
|
|
214
|
+
|
|
215
|
+
# Default model invocation stamped on every record unless overridden per-record
|
|
216
|
+
default_model_invocation={
|
|
217
|
+
"provider": "anthropic",
|
|
218
|
+
"model_name": "claude-opus-4-7",
|
|
219
|
+
"tokens_in": 0,
|
|
220
|
+
"tokens_out": 0,
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
# Retry: 3 total attempts with 500 ms / 1 s / 2 s backoff (these are the defaults)
|
|
224
|
+
retry={"attempts": 3, "backoff_ms": [500, 1000, 2000]},
|
|
225
|
+
|
|
226
|
+
# Custom HTTP transport — useful for tests
|
|
227
|
+
http_transport=my_mock_transport,
|
|
228
|
+
)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Custom HTTP transport
|
|
232
|
+
|
|
233
|
+
Inject any object implementing the `HttpTransport` protocol to intercept or mock network calls:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from reasoning_ledger import HttpRequest, HttpResponse, HttpTransport
|
|
237
|
+
|
|
238
|
+
class LoggingTransport:
|
|
239
|
+
def request(self, req: HttpRequest) -> HttpResponse:
|
|
240
|
+
print(req["method"], req["url"])
|
|
241
|
+
# delegate to real httpx ...
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## API reference
|
|
247
|
+
|
|
248
|
+
### Static / class methods
|
|
249
|
+
|
|
250
|
+
#### `LedgerClient.register_agent(opts)` → `AgentRegistration`
|
|
251
|
+
|
|
252
|
+
Register a new agent. Idempotent on `(owner, name)`.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
opts = RegisterAgentOpts(
|
|
256
|
+
api_key="sl_...",
|
|
257
|
+
name="my-agent",
|
|
258
|
+
metadata=AgentMetadata(description="...", tags=["tag1"]),
|
|
259
|
+
wallet=AgentWalletInput(address="0x..."), # BYOW only
|
|
260
|
+
)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### `LedgerClient.resolve_agent_id(opts)` → `str`
|
|
264
|
+
|
|
265
|
+
Look up an `agent_id` by human-readable name.
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
opts = ResolveAgentOpts(api_key="sl_...", name="my-agent")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Instance methods
|
|
272
|
+
|
|
273
|
+
#### `client.submit(record)` → `RecordAck`
|
|
274
|
+
|
|
275
|
+
Submit one record.
|
|
276
|
+
|
|
277
|
+
#### `client.submit_batch(records)` → `BatchAck`
|
|
278
|
+
|
|
279
|
+
Submit up to 50 records in one request.
|
|
280
|
+
|
|
281
|
+
#### `client.get_record(record_id)` → `dict`
|
|
282
|
+
|
|
283
|
+
Fetch a single stored record.
|
|
284
|
+
|
|
285
|
+
#### `client.get_session(session_id)` → `SessionFetch`
|
|
286
|
+
|
|
287
|
+
Fetch every record in a session, ordered by `server_ts_utc`.
|
|
288
|
+
|
|
289
|
+
#### `client.get_trace(opts?)` → `TracePage`
|
|
290
|
+
|
|
291
|
+
Paginated read of the agent's full trace.
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from reasoning_ledger import GetTraceOpts
|
|
295
|
+
|
|
296
|
+
page = client.get_trace(GetTraceOpts(before=cursor, limit=100))
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### `client.new_session(session_id=None)` → `Session`
|
|
300
|
+
|
|
301
|
+
Create a local session handle. Generates a `session_id` if not supplied.
|
|
302
|
+
|
|
303
|
+
### Session methods
|
|
304
|
+
|
|
305
|
+
#### `session.submit(record)` → `RecordAck`
|
|
306
|
+
|
|
307
|
+
Same as `client.submit`; `session_id` is auto-injected.
|
|
308
|
+
|
|
309
|
+
#### `session.submit_batch(records)` → `BatchAck`
|
|
310
|
+
|
|
311
|
+
Same as `client.submit_batch`; `session_id` is auto-injected on each record.
|
|
312
|
+
|
|
313
|
+
#### `session.id` → `str`
|
|
314
|
+
|
|
315
|
+
The bound `session_id` (read-only property).
|
|
316
|
+
|
|
317
|
+
### Utility functions
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
from reasoning_ledger import is_valid_record_id, new_record_id, now_epoch_ms
|
|
321
|
+
|
|
322
|
+
new_record_id() # → fresh UUID v4 string
|
|
323
|
+
now_epoch_ms() # → current epoch milliseconds (int)
|
|
324
|
+
is_valid_record_id("...") # → bool — is the string a valid UUID v4?
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Use `new_record_id()` when building dependency edges where a child needs to reference an as-yet-unsubmitted record via `upstream_record_id` or `parent_record_id`.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
MIT
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# reasoning-ledger
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Reasoning Ledger](https://github.com/StairAI/Reasoning-Ledger) — a tamper-evident audit trail for AI agent reasoning.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install reasoning-ledger
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires **Python 3.12+**. Dependencies: `pydantic>=2`, `httpx>=0.27`.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
### 1. Register an agent
|
|
16
|
+
|
|
17
|
+
Agent registration is idempotent on `(owner, name)` — calling it again with the same name returns the existing agent.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import os
|
|
21
|
+
from reasoning_ledger import LedgerClient, RegisterAgentOpts
|
|
22
|
+
|
|
23
|
+
reg = LedgerClient.register_agent(RegisterAgentOpts(
|
|
24
|
+
api_key=os.environ["STAIRAI_API_KEY"],
|
|
25
|
+
name="my-agent",
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
agent_id = reg["agent_id"]
|
|
29
|
+
# Store agent_id — you'll need it every time you construct LedgerClient.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If you already have an `agent_id` (e.g. stored in config), skip registration. To look up an agent ID by name at startup:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from reasoning_ledger import LedgerClient, ResolveAgentOpts
|
|
36
|
+
|
|
37
|
+
agent_id = LedgerClient.resolve_agent_id(ResolveAgentOpts(
|
|
38
|
+
api_key=os.environ["STAIRAI_API_KEY"],
|
|
39
|
+
name="my-agent",
|
|
40
|
+
))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Create a client
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from reasoning_ledger import LedgerClient, LedgerClientConfig
|
|
47
|
+
|
|
48
|
+
client = LedgerClient(LedgerClientConfig(
|
|
49
|
+
api_key=os.environ["STAIRAI_API_KEY"],
|
|
50
|
+
agent_id=agent_id,
|
|
51
|
+
))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The constructor performs no network call. The API key and agent ID are validated lazily on the first request.
|
|
55
|
+
|
|
56
|
+
### 3. Open a session and submit records
|
|
57
|
+
|
|
58
|
+
A `Session` pins a `session_id` so you don't have to pass it on every record. It is purely local sugar — there is no server-side session lifecycle.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import json
|
|
62
|
+
|
|
63
|
+
session = client.new_session() # auto-generates a session_id
|
|
64
|
+
|
|
65
|
+
# Observing — the trigger that woke your agent
|
|
66
|
+
session.submit({
|
|
67
|
+
"behavior": "Observing",
|
|
68
|
+
"trigger_source": "sportradar",
|
|
69
|
+
"trigger_type": "signal_trigger",
|
|
70
|
+
"trigger_description": "Match update: Spain vs Morocco, minute 47",
|
|
71
|
+
"trigger_payload_summary": "Spain xG 0.41, possession 62%, shots 8-2",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
# ToolCalling — external data fetch
|
|
75
|
+
session.submit({
|
|
76
|
+
"behavior": "ToolCalling",
|
|
77
|
+
"tool_meta": {"tool_id": "polymarket_api", "category": "external_api"},
|
|
78
|
+
"description": "Fetch current Spain win odds",
|
|
79
|
+
"input_payload": json.dumps({"market": "esp_mar"}),
|
|
80
|
+
"output_payload": json.dumps({"spain_win": 0.73}),
|
|
81
|
+
"success": True,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
# Thinking — analysis and decision
|
|
85
|
+
session.submit({
|
|
86
|
+
"behavior": "Thinking",
|
|
87
|
+
"prompt": "Given xG 0.41 and odds 0.73, should I adjust the position?",
|
|
88
|
+
"inputs": [],
|
|
89
|
+
"output_payload": json.dumps({"recommendation": "hold", "confidence": 0.81}),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
# Acting — the commitment
|
|
93
|
+
session.submit({
|
|
94
|
+
"behavior": "Acting",
|
|
95
|
+
"action_type": "trade",
|
|
96
|
+
"target_system": "broker-api",
|
|
97
|
+
"action_summary": "Hold current Spain win position",
|
|
98
|
+
"parameters": {"symbol": "ESP_WIN", "action": "hold"},
|
|
99
|
+
"dry_run": False,
|
|
100
|
+
"execution_status": "confirmed",
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 4. Submit a batch
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
batch_ack = session.submit_batch([
|
|
108
|
+
{"behavior": "Thinking", "prompt": "...", "inputs": [], "output_payload": "..."},
|
|
109
|
+
{"behavior": "Acting", "action_type": "...", ...},
|
|
110
|
+
])
|
|
111
|
+
|
|
112
|
+
for result in batch_ack["results"]:
|
|
113
|
+
if "code" in result:
|
|
114
|
+
print("Record failed:", result["record_id"], result["code"], result["message"])
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Up to 50 records per batch. Per-record validation runs locally before the network call; only locally-valid records are sent. Partial server-side failure does not raise — inspect `BatchAck["results"]`.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Behavior types
|
|
122
|
+
|
|
123
|
+
All seven behaviors extend the base record fields. The `"behavior"` key is the discriminant.
|
|
124
|
+
|
|
125
|
+
| Behavior | Required fields (beyond base) |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `"Observing"` | `trigger_source`, `trigger_type`, `trigger_description`, `trigger_payload_summary` |
|
|
128
|
+
| `"Planning"` | `goal`, `steps` |
|
|
129
|
+
| `"Thinking"` | `prompt`, `inputs`, `output_payload` |
|
|
130
|
+
| `"Acting"` | `action_type`, `target_system`, `action_summary`, `parameters`, `dry_run`, `execution_status` |
|
|
131
|
+
| `"Reflecting"` | `inputs`, `output_payload` |
|
|
132
|
+
| `"ToolCalling"` | `tool_meta`, `description`, `input_payload`, `output_payload`, `success` |
|
|
133
|
+
| `"Other"` | `label`, `data` |
|
|
134
|
+
|
|
135
|
+
### Auto-filled fields
|
|
136
|
+
|
|
137
|
+
The SDK fills these if you omit them:
|
|
138
|
+
|
|
139
|
+
| Field | SDK default |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `record_id` | Fresh UUID v4 |
|
|
142
|
+
| `schema_version` | `"1.0"` (bundled constant) |
|
|
143
|
+
| `client_ts_utc` | Current epoch milliseconds |
|
|
144
|
+
| `agent_id` | From `LedgerClientConfig.agent_id` |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Error handling
|
|
149
|
+
|
|
150
|
+
All errors inherit from `LedgerError` and carry a stable `code` string:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from reasoning_ledger import (
|
|
154
|
+
AuthError,
|
|
155
|
+
IdempotencyConflictError,
|
|
156
|
+
LedgerError,
|
|
157
|
+
NetworkError,
|
|
158
|
+
NotFoundError,
|
|
159
|
+
RateLimitError,
|
|
160
|
+
ServerError,
|
|
161
|
+
ValidationError,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
session.submit({...})
|
|
166
|
+
except ValidationError as e:
|
|
167
|
+
# Local schema check failed — never reached the network
|
|
168
|
+
print(e.details.get("field"), e.details.get("reason"))
|
|
169
|
+
except RateLimitError as e:
|
|
170
|
+
wait_ms = e.details.get("retry_after_ms")
|
|
171
|
+
# back off and retry
|
|
172
|
+
except LedgerError as e:
|
|
173
|
+
print(e.code, e.message)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Class | `code` | When |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `ValidationError` | `validation_failed` | Local schema check failed; record never sent |
|
|
179
|
+
| `AuthError` | `auth_invalid` | API key rejected |
|
|
180
|
+
| `RateLimitError` | `rate_limited` | Server rate-limited the request |
|
|
181
|
+
| `NetworkError` | `network_failed` | Request never reached the server after retries |
|
|
182
|
+
| `ServerError` | `server_5xx` | Non-retryable 5xx from server |
|
|
183
|
+
| `IdempotencyConflictError` | `record_id_conflict` | Same `record_id` submitted with different body |
|
|
184
|
+
| `NotFoundError` | `not_found` | Lookup target does not exist |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Configuration
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from reasoning_ledger import LedgerClientConfig
|
|
192
|
+
|
|
193
|
+
config = LedgerClientConfig(
|
|
194
|
+
api_key="sl_...",
|
|
195
|
+
agent_id="uuid-v4",
|
|
196
|
+
|
|
197
|
+
# Target environment — defaults to "production"
|
|
198
|
+
environment="production", # | "staging" | "development"
|
|
199
|
+
|
|
200
|
+
# Override base URL (takes precedence over `environment`)
|
|
201
|
+
endpoint="https://custom.api.example.com",
|
|
202
|
+
|
|
203
|
+
# Default model invocation stamped on every record unless overridden per-record
|
|
204
|
+
default_model_invocation={
|
|
205
|
+
"provider": "anthropic",
|
|
206
|
+
"model_name": "claude-opus-4-7",
|
|
207
|
+
"tokens_in": 0,
|
|
208
|
+
"tokens_out": 0,
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
# Retry: 3 total attempts with 500 ms / 1 s / 2 s backoff (these are the defaults)
|
|
212
|
+
retry={"attempts": 3, "backoff_ms": [500, 1000, 2000]},
|
|
213
|
+
|
|
214
|
+
# Custom HTTP transport — useful for tests
|
|
215
|
+
http_transport=my_mock_transport,
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Custom HTTP transport
|
|
220
|
+
|
|
221
|
+
Inject any object implementing the `HttpTransport` protocol to intercept or mock network calls:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from reasoning_ledger import HttpRequest, HttpResponse, HttpTransport
|
|
225
|
+
|
|
226
|
+
class LoggingTransport:
|
|
227
|
+
def request(self, req: HttpRequest) -> HttpResponse:
|
|
228
|
+
print(req["method"], req["url"])
|
|
229
|
+
# delegate to real httpx ...
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## API reference
|
|
235
|
+
|
|
236
|
+
### Static / class methods
|
|
237
|
+
|
|
238
|
+
#### `LedgerClient.register_agent(opts)` → `AgentRegistration`
|
|
239
|
+
|
|
240
|
+
Register a new agent. Idempotent on `(owner, name)`.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
opts = RegisterAgentOpts(
|
|
244
|
+
api_key="sl_...",
|
|
245
|
+
name="my-agent",
|
|
246
|
+
metadata=AgentMetadata(description="...", tags=["tag1"]),
|
|
247
|
+
wallet=AgentWalletInput(address="0x..."), # BYOW only
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
#### `LedgerClient.resolve_agent_id(opts)` → `str`
|
|
252
|
+
|
|
253
|
+
Look up an `agent_id` by human-readable name.
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
opts = ResolveAgentOpts(api_key="sl_...", name="my-agent")
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Instance methods
|
|
260
|
+
|
|
261
|
+
#### `client.submit(record)` → `RecordAck`
|
|
262
|
+
|
|
263
|
+
Submit one record.
|
|
264
|
+
|
|
265
|
+
#### `client.submit_batch(records)` → `BatchAck`
|
|
266
|
+
|
|
267
|
+
Submit up to 50 records in one request.
|
|
268
|
+
|
|
269
|
+
#### `client.get_record(record_id)` → `dict`
|
|
270
|
+
|
|
271
|
+
Fetch a single stored record.
|
|
272
|
+
|
|
273
|
+
#### `client.get_session(session_id)` → `SessionFetch`
|
|
274
|
+
|
|
275
|
+
Fetch every record in a session, ordered by `server_ts_utc`.
|
|
276
|
+
|
|
277
|
+
#### `client.get_trace(opts?)` → `TracePage`
|
|
278
|
+
|
|
279
|
+
Paginated read of the agent's full trace.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from reasoning_ledger import GetTraceOpts
|
|
283
|
+
|
|
284
|
+
page = client.get_trace(GetTraceOpts(before=cursor, limit=100))
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `client.new_session(session_id=None)` → `Session`
|
|
288
|
+
|
|
289
|
+
Create a local session handle. Generates a `session_id` if not supplied.
|
|
290
|
+
|
|
291
|
+
### Session methods
|
|
292
|
+
|
|
293
|
+
#### `session.submit(record)` → `RecordAck`
|
|
294
|
+
|
|
295
|
+
Same as `client.submit`; `session_id` is auto-injected.
|
|
296
|
+
|
|
297
|
+
#### `session.submit_batch(records)` → `BatchAck`
|
|
298
|
+
|
|
299
|
+
Same as `client.submit_batch`; `session_id` is auto-injected on each record.
|
|
300
|
+
|
|
301
|
+
#### `session.id` → `str`
|
|
302
|
+
|
|
303
|
+
The bound `session_id` (read-only property).
|
|
304
|
+
|
|
305
|
+
### Utility functions
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from reasoning_ledger import is_valid_record_id, new_record_id, now_epoch_ms
|
|
309
|
+
|
|
310
|
+
new_record_id() # → fresh UUID v4 string
|
|
311
|
+
now_epoch_ms() # → current epoch milliseconds (int)
|
|
312
|
+
is_valid_record_id("...") # → bool — is the string a valid UUID v4?
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Use `new_record_id()` when building dependency edges where a child needs to reference an as-yet-unsubmitted record via `upstream_record_id` or `parent_record_id`.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## License
|
|
320
|
+
|
|
321
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "reasoning-ledger"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
license = "MIT"
|
|
5
|
+
description = "Python SDK for interacting with the Reasoning Ledger."
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Arslan Ablikim", email = "arslan.ablikim0@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pydantic>=2.13.3",
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8",
|
|
19
|
+
"ruff>=0.9",
|
|
20
|
+
"ty>=0.0.3",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.11.7,<0.12.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
line-length = 100
|
|
32
|
+
target-version = "py312"
|
|
33
|
+
extend-exclude = ["src/reasoning_ledger/generated"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E", "F", "I", "UP", "B", "RUF", "PERF", "S"]
|
|
37
|
+
ignore = ["S101"]
|