Skip to main content

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 "1.0" (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.idstr

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reasoning_ledger-0.1.0.tar.gz (14.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reasoning_ledger-0.1.0-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

Details for the file reasoning_ledger-0.1.0.tar.gz.

File metadata

  • Download URL: reasoning_ledger-0.1.0.tar.gz
  • Upload date:
  • Size: 14.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","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

Hashes for reasoning_ledger-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d4e5b29515930de43e856c6a75d00c58090620f271d570cbe9522b2ec850b90a
MD5 eb65f160707f52f462806d6dc68cf748
BLAKE2b-256 b4c428ad8521c407c08ab1f59d17ac1a56e97771cc3c091406ce8f0ee318b3ab

See more details on using hashes here.

File details

Details for the file reasoning_ledger-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: reasoning_ledger-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","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

Hashes for reasoning_ledger-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24c6dcac30aacde6913eaeadd0ac52ce7449f1460891f5a621749ffd2b69e1c2
MD5 d4da5132f5c4a8f6943749f5b242d070
BLAKE2b-256 4a350721798f57f360572f938ab7addf234155b3db02ea2af7b676b3aa030d35

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page