Python SDK for interacting with the Reasoning Ledger.
Project description
reasoning-ledger
Python SDK for the Reasoning Ledger — a tamper-evident audit trail for AI agent reasoning.
Install
pip install reasoning-ledger
Requires Python 3.12+. Dependencies: pydantic>=2, httpx>=0.27.
Quick start
1. Register an agent
Agent registration is idempotent on (owner, name) — calling it again with the same name returns the existing agent.
import os
from reasoning_ledger import LedgerClient, RegisterAgentOpts
reg = LedgerClient.register_agent(RegisterAgentOpts(
api_key=os.environ["STAIRAI_API_KEY"],
name="my-agent",
))
agent_id = reg["agent_id"]
# Store agent_id — you'll need it every time you construct LedgerClient.
If you already have an agent_id (e.g. stored in config), skip registration. To look up an agent ID by name at startup:
from reasoning_ledger import LedgerClient, ResolveAgentOpts
agent_id = LedgerClient.resolve_agent_id(ResolveAgentOpts(
api_key=os.environ["STAIRAI_API_KEY"],
name="my-agent",
))
2. Create a client
from reasoning_ledger import LedgerClient, LedgerClientConfig
client = LedgerClient(LedgerClientConfig(
api_key=os.environ["STAIRAI_API_KEY"],
agent_id=agent_id,
))
The constructor performs no network call. The API key and agent ID are validated lazily on the first request.
3. Open a session and submit records
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.
import json
session = client.new_session() # auto-generates a session_id
# Observing — the trigger that woke your agent
session.submit({
"behavior": "Observing",
"trigger_source": "sportradar",
"trigger_type": "signal_trigger",
"trigger_description": "Match update: Spain vs Morocco, minute 47",
"trigger_payload_summary": "Spain xG 0.41, possession 62%, shots 8-2",
})
# ToolCalling — external data fetch
session.submit({
"behavior": "ToolCalling",
"tool_meta": {"tool_id": "polymarket_api", "category": "external_api"},
"description": "Fetch current Spain win odds",
"input_payload": json.dumps({"market": "esp_mar"}),
"output_payload": json.dumps({"spain_win": 0.73}),
"success": True,
})
# Thinking — analysis and decision
session.submit({
"behavior": "Thinking",
"prompt": "Given xG 0.41 and odds 0.73, should I adjust the position?",
"inputs": [],
"output_payload": json.dumps({"recommendation": "hold", "confidence": 0.81}),
})
# Acting — the commitment
session.submit({
"behavior": "Acting",
"action_type": "trade",
"target_system": "broker-api",
"action_summary": "Hold current Spain win position",
"parameters": {"symbol": "ESP_WIN", "action": "hold"},
"dry_run": False,
"execution_status": "confirmed",
})
4. Submit a batch
batch_ack = session.submit_batch([
{"behavior": "Thinking", "prompt": "...", "inputs": [], "output_payload": "..."},
{"behavior": "Acting", "action_type": "...", ...},
])
for result in batch_ack["results"]:
if "code" in result:
print("Record failed:", result["record_id"], result["code"], result["message"])
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"].
Behavior types
All seven behaviors extend the base record fields. The "behavior" key is the discriminant.
| Behavior | Required fields (beyond base) |
|---|---|
"Observing" |
trigger_source, trigger_type, trigger_description, trigger_payload_summary |
"Planning" |
goal, steps |
"Thinking" |
prompt, inputs, output_payload |
"Acting" |
action_type, target_system, action_summary, parameters, dry_run, execution_status |
"Reflecting" |
inputs, output_payload |
"ToolCalling" |
tool_meta, description, input_payload, output_payload, success |
"Other" |
label, data |
Auto-filled fields
The SDK fills these if you omit them:
| Field | SDK default |
|---|---|
record_id |
Fresh UUID v4 |
schema_version |
"0.3" (bundled constant) |
client_ts_utc |
Current epoch milliseconds |
agent_id |
From LedgerClientConfig.agent_id |
Error handling
All errors inherit from LedgerError and carry a stable code string:
from reasoning_ledger import (
AuthError,
IdempotencyConflictError,
LedgerError,
NetworkError,
NotFoundError,
RateLimitError,
ServerError,
ValidationError,
)
try:
session.submit({...})
except ValidationError as e:
# Local schema check failed — never reached the network
print(e.details.get("field"), e.details.get("reason"))
except RateLimitError as e:
wait_ms = e.details.get("retry_after_ms")
# back off and retry
except LedgerError as e:
print(e.code, e.message)
| Class | code |
When |
|---|---|---|
ValidationError |
validation_failed |
Local schema check failed; record never sent |
AuthError |
auth_invalid |
API key rejected |
RateLimitError |
rate_limited |
Server rate-limited the request |
NetworkError |
network_failed |
Request never reached the server after retries |
ServerError |
server_5xx |
Non-retryable 5xx from server |
IdempotencyConflictError |
record_id_conflict |
Same record_id submitted with different body |
NotFoundError |
not_found |
Lookup target does not exist |
Configuration
from reasoning_ledger import LedgerClientConfig
config = LedgerClientConfig(
api_key="sl_...",
agent_id="uuid-v4",
# Target environment — defaults to "production"
environment="production", # | "staging" | "development"
# Override base URL (takes precedence over `environment`)
endpoint="https://custom.api.example.com",
# Default model invocation stamped on every record unless overridden per-record
default_model_invocation={
"provider": "anthropic",
"model_name": "claude-opus-4-7",
"tokens_in": 0,
"tokens_out": 0,
},
# Retry: 3 total attempts with 500 ms / 1 s / 2 s backoff (these are the defaults)
retry={"attempts": 3, "backoff_ms": [500, 1000, 2000]},
# Custom HTTP transport — useful for tests
http_transport=my_mock_transport,
)
Custom HTTP transport
Inject any object implementing the HttpTransport protocol to intercept or mock network calls:
from reasoning_ledger import HttpRequest, HttpResponse, HttpTransport
class LoggingTransport:
def request(self, req: HttpRequest) -> HttpResponse:
print(req["method"], req["url"])
# delegate to real httpx ...
API reference
Static / class methods
LedgerClient.register_agent(opts) → AgentRegistration
Register a new agent. Idempotent on (owner, name).
opts = RegisterAgentOpts(
api_key="sl_...",
name="my-agent",
metadata=AgentMetadata(description="...", tags=["tag1"]),
wallet=AgentWalletInput(address="0x..."), # BYOW only
)
LedgerClient.resolve_agent_id(opts) → str
Look up an agent_id by human-readable name.
opts = ResolveAgentOpts(api_key="sl_...", name="my-agent")
Instance methods
client.submit(record) → RecordAck
Submit one record.
client.submit_batch(records) → BatchAck
Submit up to 50 records in one request.
client.get_record(record_id) → dict
Fetch a single stored record.
client.get_session(session_id) → SessionFetch
Fetch every record in a session, ordered by server_ts_utc.
client.get_trace(opts?) → TracePage
Paginated read of the agent's full trace.
from reasoning_ledger import GetTraceOpts
page = client.get_trace(GetTraceOpts(before=cursor, limit=100))
client.new_session(session_id=None) → Session
Create a local session handle. Generates a session_id if not supplied.
Session methods
session.submit(record) → RecordAck
Same as client.submit; session_id is auto-injected.
session.submit_batch(records) → BatchAck
Same as client.submit_batch; session_id is auto-injected on each record.
session.id → str
The bound session_id (read-only property).
Utility functions
from reasoning_ledger import is_valid_record_id, new_record_id, now_epoch_ms
new_record_id() # → fresh UUID v4 string
now_epoch_ms() # → current epoch milliseconds (int)
is_valid_record_id("...") # → bool — is the string a valid UUID v4?
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.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reasoning_ledger-0.3.0.tar.gz.
File metadata
- Download URL: reasoning_ledger-0.3.0.tar.gz
- Upload date:
- Size: 14.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6d684a47ea58612fb7b01658b1d9c337e537a75fdfbf8ab461bc78d14d2bb2a
|
|
| MD5 |
270e4563ab69ca5fafb7b83130f6317c
|
|
| BLAKE2b-256 |
f3fd4e857ff9798dced3642d4e36386f29acf45c6a43f836c21ffd7d799e42df
|
File details
Details for the file reasoning_ledger-0.3.0-py3-none-any.whl.
File metadata
- Download URL: reasoning_ledger-0.3.0-py3-none-any.whl
- Upload date:
- Size: 19.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7cb4d9b5da4e1dddf550a72cb6cf249af26d9cdcd0fb00562dae0125c21a8287
|
|
| MD5 |
8520f27d7667d247ab11389abdc84593
|
|
| BLAKE2b-256 |
87f78b9b41dcf2c27486be6d83facc563cb2a6190c7b9a80d342c9b0cbe237f0
|