Skip to content

Commit 12fa734

Browse files
authored
Create recorder.py
1 parent 78fc5b0 commit 12fa734

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

src/ohip_logging/recorder.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""
2+
High-level event recording helpers for IX-HapticSight.
3+
4+
This module provides a small orchestration layer over the lower-level event
5+
builders and JSONL writer so runtime code can record a coherent event trail
6+
without duplicating event-construction boilerplate everywhere.
7+
8+
The recorder is intentionally narrow:
9+
- it does not own runtime state
10+
- it does not own execution logic
11+
- it does not replace replay or benchmark tooling
12+
13+
Its job is to turn already-known runtime decisions into a structured, durable,
14+
append-oriented event stream.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
from dataclasses import dataclass, field
20+
from pathlib import Path
21+
from typing import Iterable, List, Optional
22+
23+
from ohip_runtime.requests import CoordinationDecision, InteractionRequest
24+
from ohip_runtime.state import (
25+
ExecutionState,
26+
InteractionSession,
27+
InteractionState,
28+
RuntimeFault,
29+
RuntimeHealth,
30+
)
31+
from .events import (
32+
EventRecord,
33+
event_from_consent_assessment,
34+
event_from_coordination_decision,
35+
event_from_fault,
36+
event_from_planning_outcome,
37+
event_from_request,
38+
event_from_safety_assessment,
39+
execution_status_event,
40+
state_transition_event,
41+
)
42+
from .jsonl import EventLogWriter
43+
44+
45+
@dataclass
46+
class EventRecorder:
47+
"""
48+
Append-oriented structured event recorder.
49+
50+
The recorder can be used either:
51+
- with a live JSONL writer for durable artifacts
52+
- in memory only for tests and benchmark harnesses
53+
54+
It preserves the exact event order that the caller chooses.
55+
"""
56+
57+
writer: Optional[EventLogWriter] = None
58+
_buffer: List[EventRecord] = field(default_factory=list)
59+
60+
@classmethod
61+
def from_path(cls, path: str | Path) -> "EventRecorder":
62+
return cls(writer=EventLogWriter(path))
63+
64+
def append(self, event: EventRecord, *, persist: bool = True) -> EventRecord:
65+
"""
66+
Append one event to the in-memory buffer and optionally persist it.
67+
"""
68+
self._buffer.append(event)
69+
if persist and self.writer is not None:
70+
self.writer.append(event)
71+
return event
72+
73+
def append_many(
74+
self,
75+
events: Iterable[EventRecord],
76+
*,
77+
persist: bool = True,
78+
) -> list[EventRecord]:
79+
"""
80+
Append multiple events, preserving caller order.
81+
"""
82+
appended: list[EventRecord] = []
83+
for event in events:
84+
appended.append(self.append(event, persist=persist))
85+
return appended
86+
87+
def buffer(self) -> list[EventRecord]:
88+
"""
89+
Return a shallow copy of the buffered events.
90+
"""
91+
return list(self._buffer)
92+
93+
def clear_buffer(self) -> None:
94+
self._buffer.clear()
95+
96+
def persist_buffer(self) -> int:
97+
"""
98+
Persist the current in-memory buffer to the writer without clearing it.
99+
100+
Returns the number of events written.
101+
"""
102+
if self.writer is None:
103+
return 0
104+
return self.writer.append_many(self._buffer)
105+
106+
def record_request(self, request: InteractionRequest, *, persist: bool = True) -> EventRecord:
107+
return self.append(event_from_request(request), persist=persist)
108+
109+
def record_decision_cycle(
110+
self,
111+
*,
112+
session: InteractionSession,
113+
request: InteractionRequest,
114+
decision: CoordinationDecision,
115+
persist: bool = True,
116+
) -> list[EventRecord]:
117+
"""
118+
Record the canonical event sequence for one decision cycle.
119+
120+
Sequence:
121+
1. request received
122+
2. consent evaluated
123+
3. safety evaluated
124+
4. planning outcome, if present
125+
5. final coordination decision
126+
"""
127+
events: list[EventRecord] = [
128+
event_from_request(request),
129+
event_from_consent_assessment(session, decision.consent),
130+
event_from_safety_assessment(session, decision.safety),
131+
]
132+
if decision.planning is not None:
133+
events.append(event_from_planning_outcome(session, decision.planning))
134+
events.append(event_from_coordination_decision(session, decision))
135+
return self.append_many(events, persist=persist)
136+
137+
def record_fault(
138+
self,
139+
*,
140+
session: InteractionSession,
141+
fault: RuntimeFault,
142+
persist: bool = True,
143+
) -> EventRecord:
144+
return self.append(event_from_fault(session, fault), persist=persist)
145+
146+
def record_state_transition(
147+
self,
148+
*,
149+
event_id: str,
150+
session_id: str,
151+
from_interaction_state: InteractionState,
152+
to_interaction_state: InteractionState,
153+
from_execution_state: ExecutionState,
154+
to_execution_state: ExecutionState,
155+
runtime_health: RuntimeHealth,
156+
reason_code: str,
157+
persist: bool = True,
158+
) -> EventRecord:
159+
event = state_transition_event(
160+
event_id=event_id,
161+
session_id=session_id,
162+
from_interaction_state=from_interaction_state,
163+
to_interaction_state=to_interaction_state,
164+
from_execution_state=from_execution_state,
165+
to_execution_state=to_execution_state,
166+
runtime_health=runtime_health,
167+
reason_code=reason_code,
168+
)
169+
return self.append(event, persist=persist)
170+
171+
def record_execution_status(
172+
self,
173+
*,
174+
event_id: str,
175+
session: InteractionSession,
176+
request_id: Optional[str],
177+
reason_code: str,
178+
accepted: bool,
179+
backend_status: str = "",
180+
progress: float = 0.0,
181+
persist: bool = True,
182+
) -> EventRecord:
183+
event = execution_status_event(
184+
event_id=event_id,
185+
session_id=session.session_id,
186+
request_id=request_id,
187+
interaction_state=session.interaction_state,
188+
execution_state=session.execution_state,
189+
runtime_health=session.runtime_health,
190+
reason_code=reason_code,
191+
safety_level=session.safety_level,
192+
accepted=accepted,
193+
backend_status=backend_status,
194+
progress=progress,
195+
)
196+
return self.append(event, persist=persist)
197+
198+
199+
__all__ = [
200+
"EventRecorder",
201+
]

0 commit comments

Comments
 (0)