reasoning-ledger 0.1.0__py3-none-any.whl
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/__init__.py +83 -0
- reasoning_ledger/client.py +287 -0
- reasoning_ledger/constants.py +49 -0
- reasoning_ledger/errors.py +73 -0
- reasoning_ledger/generated/__init__.py +2 -0
- reasoning_ledger/generated/records.py +208 -0
- reasoning_ledger/http.py +160 -0
- reasoning_ledger/py.typed +0 -0
- reasoning_ledger/session.py +36 -0
- reasoning_ledger/types.py +169 -0
- reasoning_ledger/utils.py +28 -0
- reasoning_ledger/validate.py +191 -0
- reasoning_ledger-0.1.0.dist-info/METADATA +333 -0
- reasoning_ledger-0.1.0.dist-info/RECORD +15 -0
- reasoning_ledger-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
reasoning-ledger — Python SDK for the Reasoning Ledger API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import LedgerClient
|
|
6
|
+
from .constants import ENDPOINTS, SCHEMA_VERSION, SIZE_LIMITS
|
|
7
|
+
from .errors import (
|
|
8
|
+
AuthError,
|
|
9
|
+
IdempotencyConflictError,
|
|
10
|
+
LedgerError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
from .http import DEFAULT_RETRY, HttpxTransport, build_url, map_http_error, should_retry, with_retry
|
|
18
|
+
from .session import Session
|
|
19
|
+
from .types import (
|
|
20
|
+
AgentMetadata,
|
|
21
|
+
AgentRegistration,
|
|
22
|
+
AgentWalletInput,
|
|
23
|
+
BatchAck,
|
|
24
|
+
GetTraceOpts,
|
|
25
|
+
HttpRequest,
|
|
26
|
+
HttpResponse,
|
|
27
|
+
HttpTransport,
|
|
28
|
+
LedgerClientConfig,
|
|
29
|
+
RecordAck,
|
|
30
|
+
RecordError,
|
|
31
|
+
RegisterAgentOpts,
|
|
32
|
+
ResolveAgentOpts,
|
|
33
|
+
RetryConfig,
|
|
34
|
+
SessionFetch,
|
|
35
|
+
SubmitInput,
|
|
36
|
+
TracePage,
|
|
37
|
+
)
|
|
38
|
+
from .utils import is_valid_record_id, new_record_id, now_epoch_ms
|
|
39
|
+
from .validate import validate_batch, validate_record
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"DEFAULT_RETRY",
|
|
43
|
+
"ENDPOINTS",
|
|
44
|
+
"SCHEMA_VERSION",
|
|
45
|
+
"SIZE_LIMITS",
|
|
46
|
+
"AgentMetadata",
|
|
47
|
+
"AgentRegistration",
|
|
48
|
+
"AgentWalletInput",
|
|
49
|
+
"AuthError",
|
|
50
|
+
"BatchAck",
|
|
51
|
+
"GetTraceOpts",
|
|
52
|
+
"HttpRequest",
|
|
53
|
+
"HttpResponse",
|
|
54
|
+
"HttpTransport",
|
|
55
|
+
"HttpxTransport",
|
|
56
|
+
"IdempotencyConflictError",
|
|
57
|
+
"LedgerClient",
|
|
58
|
+
"LedgerClientConfig",
|
|
59
|
+
"LedgerError",
|
|
60
|
+
"NetworkError",
|
|
61
|
+
"NotFoundError",
|
|
62
|
+
"RateLimitError",
|
|
63
|
+
"RecordAck",
|
|
64
|
+
"RecordError",
|
|
65
|
+
"RegisterAgentOpts",
|
|
66
|
+
"ResolveAgentOpts",
|
|
67
|
+
"RetryConfig",
|
|
68
|
+
"ServerError",
|
|
69
|
+
"Session",
|
|
70
|
+
"SessionFetch",
|
|
71
|
+
"SubmitInput",
|
|
72
|
+
"TracePage",
|
|
73
|
+
"ValidationError",
|
|
74
|
+
"build_url",
|
|
75
|
+
"is_valid_record_id",
|
|
76
|
+
"map_http_error",
|
|
77
|
+
"new_record_id",
|
|
78
|
+
"now_epoch_ms",
|
|
79
|
+
"should_retry",
|
|
80
|
+
"validate_batch",
|
|
81
|
+
"validate_record",
|
|
82
|
+
"with_retry",
|
|
83
|
+
]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from .constants import ENDPOINTS, SCHEMA_VERSION
|
|
8
|
+
from .http import DEFAULT_RETRY, HttpxTransport, build_url, map_http_error, with_retry
|
|
9
|
+
from .types import (
|
|
10
|
+
AgentRegistration,
|
|
11
|
+
BatchAck,
|
|
12
|
+
GetTraceOpts,
|
|
13
|
+
HttpTransport,
|
|
14
|
+
LedgerClientConfig,
|
|
15
|
+
RecordAck,
|
|
16
|
+
RecordError,
|
|
17
|
+
RegisterAgentOpts,
|
|
18
|
+
ResolveAgentOpts,
|
|
19
|
+
RetryConfig,
|
|
20
|
+
SessionFetch,
|
|
21
|
+
SubmitInput,
|
|
22
|
+
TracePage,
|
|
23
|
+
)
|
|
24
|
+
from .utils import new_record_id, now_epoch_ms
|
|
25
|
+
from .validate import validate_batch, validate_record
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .session import Session
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Internal helpers
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve_base_url(config: LedgerClientConfig) -> str:
|
|
36
|
+
if config.endpoint is not None:
|
|
37
|
+
return config.endpoint
|
|
38
|
+
return ENDPOINTS.get(config.environment, ENDPOINTS["production"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_retry(config: LedgerClientConfig) -> RetryConfig:
|
|
42
|
+
return config.retry if config.retry is not None else DEFAULT_RETRY
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _complete_record(
|
|
46
|
+
input_record: SubmitInput,
|
|
47
|
+
agent_id: str,
|
|
48
|
+
default_model_invocation: dict[str, Any] | None,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
record: dict[str, Any] = dict(input_record)
|
|
51
|
+
record.setdefault("agent_id", agent_id)
|
|
52
|
+
record.setdefault("client_ts_utc", now_epoch_ms())
|
|
53
|
+
record.setdefault("record_id", new_record_id())
|
|
54
|
+
record.setdefault("schema_version", SCHEMA_VERSION)
|
|
55
|
+
if record.get("model_invocation") is None and default_model_invocation is not None:
|
|
56
|
+
record["model_invocation"] = default_model_invocation
|
|
57
|
+
return record
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _call_api(
|
|
61
|
+
transport: HttpTransport,
|
|
62
|
+
retry: RetryConfig,
|
|
63
|
+
*,
|
|
64
|
+
api_key: str,
|
|
65
|
+
method: str,
|
|
66
|
+
url: str,
|
|
67
|
+
body: Any = None,
|
|
68
|
+
) -> Any:
|
|
69
|
+
headers: dict[str, str] = {
|
|
70
|
+
"content-type": "application/json",
|
|
71
|
+
"x-api-key": api_key,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def _do() -> Any:
|
|
75
|
+
req_body: str | None = json.dumps(body) if body is not None else None
|
|
76
|
+
res = transport.request(
|
|
77
|
+
{
|
|
78
|
+
"body": req_body,
|
|
79
|
+
"headers": headers,
|
|
80
|
+
"method": method,
|
|
81
|
+
"url": url,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
status = res["status"]
|
|
85
|
+
if status < 200 or status >= 300:
|
|
86
|
+
map_http_error(res)
|
|
87
|
+
return json.loads(res["body"])
|
|
88
|
+
|
|
89
|
+
return with_retry(_do, retry)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# LedgerClient
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LedgerClient:
|
|
98
|
+
def __init__(self, config: LedgerClientConfig) -> None:
|
|
99
|
+
self._config = config
|
|
100
|
+
self._transport: HttpTransport = config.http_transport or HttpxTransport()
|
|
101
|
+
self._retry = _resolve_retry(config)
|
|
102
|
+
self._base_url = _resolve_base_url(config)
|
|
103
|
+
|
|
104
|
+
# -------------------------------------------------------------------------
|
|
105
|
+
# Class methods (static factory equivalents)
|
|
106
|
+
# -------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def register_agent(
|
|
110
|
+
cls,
|
|
111
|
+
opts: RegisterAgentOpts,
|
|
112
|
+
*,
|
|
113
|
+
_transport: HttpTransport | None = None,
|
|
114
|
+
) -> AgentRegistration:
|
|
115
|
+
"""Register a new agent. Idempotent on (owner, name). See §6.4."""
|
|
116
|
+
transport: HttpTransport = _transport or HttpxTransport()
|
|
117
|
+
body: dict[str, Any] = {"name": opts.name}
|
|
118
|
+
if opts.metadata is not None:
|
|
119
|
+
if opts.metadata.description is not None:
|
|
120
|
+
body["description"] = opts.metadata.description
|
|
121
|
+
if opts.metadata.website is not None:
|
|
122
|
+
body["website"] = opts.metadata.website
|
|
123
|
+
if opts.metadata.tags is not None:
|
|
124
|
+
body["tags"] = opts.metadata.tags
|
|
125
|
+
# signer is a client-side callback — never sent to the server.
|
|
126
|
+
if opts.wallet is not None:
|
|
127
|
+
body["wallet"] = {"address": opts.wallet.address}
|
|
128
|
+
|
|
129
|
+
result = _call_api(
|
|
130
|
+
transport,
|
|
131
|
+
DEFAULT_RETRY,
|
|
132
|
+
api_key=opts.api_key,
|
|
133
|
+
method="POST",
|
|
134
|
+
url=f"{ENDPOINTS['production']}/v1/agents",
|
|
135
|
+
body=body,
|
|
136
|
+
)
|
|
137
|
+
return AgentRegistration(**result) # type: ignore[misc]
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def resolve_agent_id(
|
|
141
|
+
cls,
|
|
142
|
+
opts: ResolveAgentOpts,
|
|
143
|
+
*,
|
|
144
|
+
_transport: HttpTransport | None = None,
|
|
145
|
+
) -> str:
|
|
146
|
+
"""Resolve an agent's UUID by its human-readable name. See §7.3."""
|
|
147
|
+
transport: HttpTransport = _transport or HttpxTransport()
|
|
148
|
+
url = build_url(f"{ENDPOINTS['production']}/v1/agents", {"name": opts.name})
|
|
149
|
+
result = _call_api(
|
|
150
|
+
transport,
|
|
151
|
+
DEFAULT_RETRY,
|
|
152
|
+
api_key=opts.api_key,
|
|
153
|
+
method="GET",
|
|
154
|
+
url=url,
|
|
155
|
+
)
|
|
156
|
+
return str(result["agent_id"])
|
|
157
|
+
|
|
158
|
+
# -------------------------------------------------------------------------
|
|
159
|
+
# Instance methods
|
|
160
|
+
# -------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def submit(self, input_record: SubmitInput) -> RecordAck:
|
|
163
|
+
"""Submit a single record with auto-fill. Validates locally first."""
|
|
164
|
+
record = _complete_record(
|
|
165
|
+
input_record,
|
|
166
|
+
self._config.agent_id,
|
|
167
|
+
self._config.default_model_invocation,
|
|
168
|
+
)
|
|
169
|
+
validate_record(record) # raises ValidationError on failure
|
|
170
|
+
|
|
171
|
+
result = _call_api(
|
|
172
|
+
self._transport,
|
|
173
|
+
self._retry,
|
|
174
|
+
api_key=self._config.api_key,
|
|
175
|
+
method="POST",
|
|
176
|
+
url=f"{self._base_url}/v1/records",
|
|
177
|
+
body=record,
|
|
178
|
+
)
|
|
179
|
+
return RecordAck(**result) # type: ignore[misc]
|
|
180
|
+
|
|
181
|
+
def submit_batch(self, inputs: list[SubmitInput]) -> BatchAck:
|
|
182
|
+
"""Submit up to 50 records. Per-record errors don't abort the batch."""
|
|
183
|
+
completed = [
|
|
184
|
+
_complete_record(inp, self._config.agent_id, self._config.default_model_invocation)
|
|
185
|
+
for inp in inputs
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
# Raises on batch-level violations (count > 50, total > 1 MB).
|
|
189
|
+
errors = validate_batch(completed)
|
|
190
|
+
|
|
191
|
+
valid_records: list[tuple[int, dict[str, Any]]] = []
|
|
192
|
+
results: list[RecordAck | RecordError | None] = [None] * len(inputs)
|
|
193
|
+
|
|
194
|
+
for i, (rec, err) in enumerate(zip(completed, errors, strict=True)):
|
|
195
|
+
if err is not None:
|
|
196
|
+
record_id = str(rec.get("record_id", "(unknown)"))
|
|
197
|
+
results[i] = RecordError(
|
|
198
|
+
code=err.code,
|
|
199
|
+
message=str(err),
|
|
200
|
+
record_id=record_id,
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
valid_records.append((i, rec))
|
|
204
|
+
|
|
205
|
+
if not valid_records:
|
|
206
|
+
# All records failed local validation; synthesize a batch_id.
|
|
207
|
+
return BatchAck(
|
|
208
|
+
batch_id=str(uuid.uuid4()),
|
|
209
|
+
results=[r for r in results if r is not None],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
server_response = _call_api(
|
|
213
|
+
self._transport,
|
|
214
|
+
self._retry,
|
|
215
|
+
api_key=self._config.api_key,
|
|
216
|
+
method="POST",
|
|
217
|
+
url=f"{self._base_url}/v1/records:batch",
|
|
218
|
+
body={"records": [rec for _, rec in valid_records]},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Merge server results back at the correct indices.
|
|
222
|
+
server_results: list[Any] = server_response.get("results", [])
|
|
223
|
+
for j, (orig_idx, _) in enumerate(valid_records):
|
|
224
|
+
if j < len(server_results):
|
|
225
|
+
results[orig_idx] = server_results[j]
|
|
226
|
+
|
|
227
|
+
return BatchAck(
|
|
228
|
+
batch_id=str(server_response["batch_id"]),
|
|
229
|
+
results=[r for r in results if r is not None],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def get_record(self, record_id: str) -> dict[str, Any]:
|
|
233
|
+
"""Fetch a single stored record by record_id."""
|
|
234
|
+
return _call_api( # type: ignore[return-value]
|
|
235
|
+
self._transport,
|
|
236
|
+
self._retry,
|
|
237
|
+
api_key=self._config.api_key,
|
|
238
|
+
method="GET",
|
|
239
|
+
url=f"{self._base_url}/v1/records/{record_id}",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def get_session(self, session_id: str) -> SessionFetch:
|
|
243
|
+
"""Fetch all records in a session. agent_id is auto-filled."""
|
|
244
|
+
url = build_url(
|
|
245
|
+
f"{self._base_url}/v1/sessions/{session_id}",
|
|
246
|
+
{"agent_id": self._config.agent_id},
|
|
247
|
+
)
|
|
248
|
+
result = _call_api(
|
|
249
|
+
self._transport,
|
|
250
|
+
self._retry,
|
|
251
|
+
api_key=self._config.api_key,
|
|
252
|
+
method="GET",
|
|
253
|
+
url=url,
|
|
254
|
+
)
|
|
255
|
+
return SessionFetch(**result) # type: ignore[misc]
|
|
256
|
+
|
|
257
|
+
def get_trace(self, opts: GetTraceOpts | None = None) -> TracePage:
|
|
258
|
+
"""Fetch paginated agent trace, newest-first. agent_id is auto-filled."""
|
|
259
|
+
_opts = opts or GetTraceOpts()
|
|
260
|
+
url = build_url(
|
|
261
|
+
f"{self._base_url}/v1/traces/{self._config.agent_id}",
|
|
262
|
+
{"before": _opts.before, "limit": _opts.limit},
|
|
263
|
+
)
|
|
264
|
+
result = _call_api(
|
|
265
|
+
self._transport,
|
|
266
|
+
self._retry,
|
|
267
|
+
api_key=self._config.api_key,
|
|
268
|
+
method="GET",
|
|
269
|
+
url=url,
|
|
270
|
+
)
|
|
271
|
+
return TracePage(**result) # type: ignore[misc]
|
|
272
|
+
|
|
273
|
+
def new_session(self, session_id: str | None = None) -> Session:
|
|
274
|
+
"""Create a Session bound to session_id (or a fresh UUID)."""
|
|
275
|
+
from .session import Session # local import avoids circular
|
|
276
|
+
|
|
277
|
+
return Session(self, session_id)
|
|
278
|
+
|
|
279
|
+
# -------------------------------------------------------------------------
|
|
280
|
+
# Internal helpers exposed for Session
|
|
281
|
+
# -------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def _submit(self, input_record: SubmitInput) -> RecordAck:
|
|
284
|
+
return self.submit(input_record)
|
|
285
|
+
|
|
286
|
+
def _submit_batch(self, inputs: list[SubmitInput]) -> BatchAck:
|
|
287
|
+
return self.submit_batch(inputs)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# Schema version — stamped on every submitted record.
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
SCHEMA_VERSION = "1.0"
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# Base URLs per environment.
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
ENDPOINTS: dict[str, str] = {
|
|
12
|
+
"development": "http://localhost:3000",
|
|
13
|
+
"production": "https://api.stairai.com",
|
|
14
|
+
"staging": "https://staging.api.stairai.com",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Size limits (§10.2) — enforced client-side before any network call.
|
|
19
|
+
# Values are in bytes for JSON-encoded fields, or item counts for arrays.
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
SIZE_LIMITS: dict[str, int] = {
|
|
23
|
+
# Acting.parameters JSON-encoded size (16 KB).
|
|
24
|
+
"ACTING_PARAMETERS": 16 * 1024,
|
|
25
|
+
# Per-batch total JSON-encoded size (1 MB).
|
|
26
|
+
"BATCH_JSON": 1024 * 1024,
|
|
27
|
+
# notes field on BaseRecord (2 KB).
|
|
28
|
+
"NOTES": 2048,
|
|
29
|
+
# Other.data JSON-encoded size (16 KB).
|
|
30
|
+
"OTHER_DATA": 16 * 1024,
|
|
31
|
+
# Per-record total JSON-encoded size (64 KB).
|
|
32
|
+
"RECORD_JSON": 64 * 1024,
|
|
33
|
+
# Maximum number of tags on a record (32 items).
|
|
34
|
+
"TAGS_COUNT": 32,
|
|
35
|
+
# Each individual tag string (64 chars).
|
|
36
|
+
"TAG_LENGTH": 64,
|
|
37
|
+
# Thinking.output_payload (32 KB).
|
|
38
|
+
"THINKING_OUTPUT": 32 * 1024,
|
|
39
|
+
# Thinking.prompt (16 KB).
|
|
40
|
+
"THINKING_PROMPT": 16 * 1024,
|
|
41
|
+
# ToolCalling.input_payload JSON-encoded size (16 KB).
|
|
42
|
+
"TOOL_INPUT": 16 * 1024,
|
|
43
|
+
# ToolCalling.tool_meta JSON-encoded size (16 KB).
|
|
44
|
+
"TOOL_META": 16 * 1024,
|
|
45
|
+
# ToolCalling.output_payload JSON-encoded size (32 KB).
|
|
46
|
+
"TOOL_OUTPUT": 32 * 1024,
|
|
47
|
+
# Observing.trigger_payload_summary (4 KB).
|
|
48
|
+
"TRIGGER_PAYLOAD_SUMMARY": 4096,
|
|
49
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ---------------------------------------------------------------------------
|
|
2
|
+
# LedgerError — base class for all SDK errors.
|
|
3
|
+
# Partners may branch on the class hierarchy or on the stable `code` string.
|
|
4
|
+
# ---------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LedgerError(Exception):
|
|
8
|
+
code: str
|
|
9
|
+
details: dict[str, object] | None
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
code: str,
|
|
14
|
+
message: str,
|
|
15
|
+
details: dict[str, object] | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
self.code = code
|
|
19
|
+
self.details = details
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Subclasses (§7.10)
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ValidationError(LedgerError):
|
|
28
|
+
"""Local schema check failed; the record never reached the network."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
31
|
+
super().__init__("validation_failed", message, details)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AuthError(LedgerError):
|
|
35
|
+
"""API key rejected or unknown to the server."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
38
|
+
super().__init__("auth_invalid", message, details)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RateLimitError(LedgerError):
|
|
42
|
+
"""Server returned a rate-limit signal."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
45
|
+
super().__init__("rate_limited", message, details)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NetworkError(LedgerError):
|
|
49
|
+
"""Request never reached the server after exhausting retries."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
52
|
+
super().__init__("network_failed", message, details)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ServerError(LedgerError):
|
|
56
|
+
"""Server returned a non-retryable 5xx."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
59
|
+
super().__init__("server_5xx", message, details)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class IdempotencyConflictError(LedgerError):
|
|
63
|
+
"""The same record_id was previously submitted with a different body."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
66
|
+
super().__init__("record_id_conflict", message, details)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NotFoundError(LedgerError):
|
|
70
|
+
"""Lookup target does not exist or is not visible to the calling owner."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, message: str, details: dict[str, object] | None = None) -> None:
|
|
73
|
+
super().__init__("not_found", message, details)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: records.schema.json
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Annotated, Any, Literal
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, RootModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BehaviorType(StrEnum):
|
|
14
|
+
Observing = "Observing"
|
|
15
|
+
ToolCalling = "ToolCalling"
|
|
16
|
+
Planning = "Planning"
|
|
17
|
+
Thinking = "Thinking"
|
|
18
|
+
Acting = "Acting"
|
|
19
|
+
Reflecting = "Reflecting"
|
|
20
|
+
Other = "Other"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UuidV4(RootModel[UUID]):
|
|
24
|
+
root: UUID
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ModelInvocation(BaseModel):
|
|
28
|
+
model_config = ConfigDict(
|
|
29
|
+
extra="forbid",
|
|
30
|
+
)
|
|
31
|
+
provider: Annotated[str, Field(min_length=1)]
|
|
32
|
+
model_name: Annotated[str, Field(min_length=1)]
|
|
33
|
+
model_version: str | None = None
|
|
34
|
+
tokens_in: Annotated[int | None, Field(ge=0)] = None
|
|
35
|
+
tokens_out: Annotated[int | None, Field(ge=0)] = None
|
|
36
|
+
cost_usd: Annotated[float | None, Field(ge=0.0)] = None
|
|
37
|
+
temperature: float | None = None
|
|
38
|
+
finish_reason: str | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Tag(RootModel[str]):
|
|
42
|
+
root: Annotated[str, Field(max_length=64)]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaseRecord(BaseModel):
|
|
46
|
+
schema_version: Annotated[str, Field(examples=["1.0"], min_length=1)]
|
|
47
|
+
agent_id: UUID
|
|
48
|
+
session_id: Annotated[str, Field(min_length=1)]
|
|
49
|
+
record_id: UUID
|
|
50
|
+
behavior: BehaviorType
|
|
51
|
+
client_ts_utc: Annotated[int, Field(ge=0)]
|
|
52
|
+
notes: Annotated[str | None, Field(max_length=2048)] = None
|
|
53
|
+
tags: Annotated[list[Tag] | None, Field(max_length=32)] = None
|
|
54
|
+
model_invocation: ModelInvocation | None = None
|
|
55
|
+
upstream_record_id: Annotated[
|
|
56
|
+
list[UUID] | None,
|
|
57
|
+
Field(
|
|
58
|
+
description="DAG dependency / trace sequence. Records this one builds on. May be empty/omitted.",
|
|
59
|
+
max_length=32,
|
|
60
|
+
),
|
|
61
|
+
] = None
|
|
62
|
+
parent_record_id: Annotated[
|
|
63
|
+
UUID | None,
|
|
64
|
+
Field(
|
|
65
|
+
description="Sub-thread / sub-process containment. Set when this record is produced inside a sub-thread spawned by another record. Records outside the sub-thread do not list internal sub-thread records as upstream. For ordinary DAG dependencies use upstream_record_id instead."
|
|
66
|
+
),
|
|
67
|
+
] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ThinkingInput(BaseModel):
|
|
71
|
+
model_config = ConfigDict(
|
|
72
|
+
extra="forbid",
|
|
73
|
+
)
|
|
74
|
+
input_record_id: UUID | None = None
|
|
75
|
+
input_payload: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ReflectingInput(ThinkingInput):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DependsOnItem(RootModel[int]):
|
|
83
|
+
root: Annotated[int, Field(ge=0)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PlanningStep(BaseModel):
|
|
87
|
+
model_config = ConfigDict(
|
|
88
|
+
extra="forbid",
|
|
89
|
+
)
|
|
90
|
+
index: Annotated[int, Field(ge=0)]
|
|
91
|
+
description: Annotated[str, Field(min_length=1)]
|
|
92
|
+
depends_on: list[DependsOnItem] | None = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TriggerType(StrEnum):
|
|
96
|
+
signal_trigger = "signal_trigger"
|
|
97
|
+
cron_trigger = "cron_trigger"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ObservingRecord(BaseRecord):
|
|
101
|
+
model_config = ConfigDict(
|
|
102
|
+
extra="forbid",
|
|
103
|
+
)
|
|
104
|
+
behavior: Literal["Observing"]
|
|
105
|
+
trigger_source: Annotated[str, Field(min_length=1)]
|
|
106
|
+
trigger_type: TriggerType
|
|
107
|
+
external_trigger_id: str | None = None
|
|
108
|
+
event_ts_utc: Annotated[int | None, Field(ge=0)] = None
|
|
109
|
+
trigger_description: Annotated[str, Field(min_length=1)]
|
|
110
|
+
trigger_payload_summary: Annotated[str, Field(max_length=4096)]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ToolCallingRecord(BaseRecord):
|
|
114
|
+
model_config = ConfigDict(
|
|
115
|
+
extra="forbid",
|
|
116
|
+
)
|
|
117
|
+
behavior: Literal["ToolCalling"]
|
|
118
|
+
tool_meta: dict[str, Any]
|
|
119
|
+
description: Annotated[str, Field(min_length=1)]
|
|
120
|
+
input_payload: Any
|
|
121
|
+
output_payload: Any
|
|
122
|
+
success: bool
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PlanningRecord(BaseRecord):
|
|
126
|
+
model_config = ConfigDict(
|
|
127
|
+
extra="forbid",
|
|
128
|
+
)
|
|
129
|
+
behavior: Literal["Planning"]
|
|
130
|
+
goal: Annotated[str, Field(min_length=1)]
|
|
131
|
+
steps: list[PlanningStep]
|
|
132
|
+
contingencies: list[str] | None = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class ThinkingRecord(BaseRecord):
|
|
136
|
+
model_config = ConfigDict(
|
|
137
|
+
extra="forbid",
|
|
138
|
+
)
|
|
139
|
+
behavior: Literal["Thinking"]
|
|
140
|
+
prompt: Annotated[str, Field(min_length=1)]
|
|
141
|
+
inputs: list[ThinkingInput]
|
|
142
|
+
output_payload: Annotated[str, Field(min_length=1)]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ExecutionStatus(StrEnum):
|
|
146
|
+
confirmed = "confirmed"
|
|
147
|
+
failed = "failed"
|
|
148
|
+
simulated = "simulated"
|
|
149
|
+
pending = "pending"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ActingRecord(BaseRecord):
|
|
153
|
+
model_config = ConfigDict(
|
|
154
|
+
extra="forbid",
|
|
155
|
+
)
|
|
156
|
+
behavior: Literal["Acting"]
|
|
157
|
+
action_type: Annotated[str, Field(min_length=1)]
|
|
158
|
+
target_system: Annotated[str, Field(min_length=1)]
|
|
159
|
+
action_summary: Annotated[str, Field(min_length=1)]
|
|
160
|
+
parameters: dict[str, Any]
|
|
161
|
+
dry_run: bool
|
|
162
|
+
execution_id: str | None = None
|
|
163
|
+
execution_status: ExecutionStatus
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ReflectingRecord(BaseRecord):
|
|
167
|
+
model_config = ConfigDict(
|
|
168
|
+
extra="forbid",
|
|
169
|
+
)
|
|
170
|
+
behavior: Literal["Reflecting"]
|
|
171
|
+
inputs: list[ReflectingInput]
|
|
172
|
+
output_payload: Annotated[str, Field(min_length=1)]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class OtherRecord(BaseRecord):
|
|
176
|
+
model_config = ConfigDict(
|
|
177
|
+
extra="forbid",
|
|
178
|
+
)
|
|
179
|
+
behavior: Literal["Other"]
|
|
180
|
+
label: Annotated[str, Field(min_length=1)]
|
|
181
|
+
data: dict[str, Any]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ReasoningLedgerRecordSchemas(
|
|
185
|
+
RootModel[
|
|
186
|
+
ObservingRecord
|
|
187
|
+
| ToolCallingRecord
|
|
188
|
+
| PlanningRecord
|
|
189
|
+
| ThinkingRecord
|
|
190
|
+
| ActingRecord
|
|
191
|
+
| ReflectingRecord
|
|
192
|
+
| OtherRecord
|
|
193
|
+
]
|
|
194
|
+
):
|
|
195
|
+
root: Annotated[
|
|
196
|
+
ObservingRecord
|
|
197
|
+
| ToolCallingRecord
|
|
198
|
+
| PlanningRecord
|
|
199
|
+
| ThinkingRecord
|
|
200
|
+
| ActingRecord
|
|
201
|
+
| ReflectingRecord
|
|
202
|
+
| OtherRecord,
|
|
203
|
+
Field(
|
|
204
|
+
description="Canonical record schemas for sections 4.1-4.8 of Reasoning_Ledger_SDK_Design_v1.",
|
|
205
|
+
discriminator="behavior",
|
|
206
|
+
title="Reasoning Ledger Record Schemas",
|
|
207
|
+
),
|
|
208
|
+
]
|