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.
@@ -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,2 @@
1
+ # Generated package — do not edit manually.
2
+ from .records import *
@@ -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
+ ]