Skip to content

Commit 248ac70

Browse files
authored
Create requests.py
1 parent 563f1e2 commit 248ac70

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

src/ohip_runtime/requests.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"""
2+
Runtime request and decision models for IX-HapticSight.
3+
4+
This module defines the minimal coordination-facing contracts that sit between
5+
the protocol core and the future runtime coordinator.
6+
7+
These models are intentionally backend-agnostic:
8+
- no ROS 2 imports
9+
- no transport assumptions
10+
- no hardware-specific payloads
11+
12+
They exist so the runtime can reason explicitly about:
13+
- what was requested
14+
- what consent decision was returned
15+
- what safety decision was returned
16+
- whether a plan was approved, denied, or downgraded
17+
"""
18+
19+
from __future__ import annotations
20+
21+
from dataclasses import dataclass, field
22+
from enum import Enum
23+
from time import time
24+
from typing import Optional
25+
26+
from ohip.schemas import ConsentMode, ContactPlan, SafetyLevel
27+
28+
29+
class InteractionKind(str, Enum):
30+
"""
31+
High-level interaction intent classes.
32+
33+
These are deliberately narrow. They represent runtime intent categories,
34+
not broad social behavior claims.
35+
"""
36+
37+
OBSERVE_ONLY = "OBSERVE_ONLY"
38+
APPROACH_ONLY = "APPROACH_ONLY"
39+
PRECONTACT_VERIFY = "PRECONTACT_VERIFY"
40+
SUPPORT_CONTACT = "SUPPORT_CONTACT"
41+
OBJECT_INTERACTION = "OBJECT_INTERACTION"
42+
RETREAT = "RETREAT"
43+
SAFE_HOLD = "SAFE_HOLD"
44+
45+
46+
class RequestSource(str, Enum):
47+
"""
48+
Origin of an interaction request.
49+
50+
This will matter later for trust boundaries, replay separation,
51+
and benchmark provenance.
52+
"""
53+
54+
OPERATOR = "OPERATOR"
55+
SUPERVISOR = "SUPERVISOR"
56+
POLICY_AUTOMATION = "POLICY_AUTOMATION"
57+
BENCHMARK = "BENCHMARK"
58+
REPLAY = "REPLAY"
59+
60+
61+
class DecisionStatus(str, Enum):
62+
APPROVED = "APPROVED"
63+
DENIED = "DENIED"
64+
REQUIRES_VERIFICATION = "REQUIRES_VERIFICATION"
65+
DEGRADED_APPROVAL = "DEGRADED_APPROVAL"
66+
67+
68+
@dataclass(frozen=True)
69+
class InteractionRequest:
70+
"""
71+
Runtime-facing request for a bounded interaction behavior.
72+
73+
This object does not replace the protocol-level data structures.
74+
It gives the runtime coordinator a stable input contract.
75+
"""
76+
77+
request_id: str
78+
session_id: str
79+
subject_id: Optional[str]
80+
interaction_kind: InteractionKind
81+
source: RequestSource
82+
target_name: str = ""
83+
requested_scope: str = ""
84+
requires_contact: bool = False
85+
requires_consent_freshness: bool = True
86+
requested_at_utc_s: float = field(default_factory=time)
87+
notes: str = ""
88+
89+
90+
@dataclass(frozen=True)
91+
class ConsentAssessment:
92+
"""
93+
Result of consent/policy evaluation for one runtime request.
94+
"""
95+
96+
request_id: str
97+
status: DecisionStatus
98+
consent_mode: ConsentMode
99+
consent_valid: bool
100+
consent_fresh: bool
101+
scope_allowed: bool
102+
reason_code: str
103+
evaluated_at_utc_s: float = field(default_factory=time)
104+
105+
@property
106+
def approved(self) -> bool:
107+
return self.status in {
108+
DecisionStatus.APPROVED,
109+
DecisionStatus.DEGRADED_APPROVAL,
110+
}
111+
112+
113+
@dataclass(frozen=True)
114+
class SafetyAssessment:
115+
"""
116+
Result of safety evaluation for one runtime request.
117+
"""
118+
119+
request_id: str
120+
status: DecisionStatus
121+
safety_level: SafetyLevel
122+
may_approach: bool
123+
may_contact: bool
124+
requires_retreat: bool
125+
requires_safe_hold: bool
126+
reason_code: str
127+
evaluated_at_utc_s: float = field(default_factory=time)
128+
129+
@property
130+
def approved(self) -> bool:
131+
return self.status in {
132+
DecisionStatus.APPROVED,
133+
DecisionStatus.DEGRADED_APPROVAL,
134+
}
135+
136+
137+
@dataclass(frozen=True)
138+
class PlanningOutcome:
139+
"""
140+
Planner result associated with a runtime request.
141+
142+
A request may be denied before planning, may require additional
143+
verification, or may produce an actual ContactPlan.
144+
"""
145+
146+
request_id: str
147+
status: DecisionStatus
148+
reason_code: str
149+
plan: Optional[ContactPlan] = None
150+
degraded: bool = False
151+
created_at_utc_s: float = field(default_factory=time)
152+
153+
@property
154+
def has_plan(self) -> bool:
155+
return self.plan is not None and self.status in {
156+
DecisionStatus.APPROVED,
157+
DecisionStatus.DEGRADED_APPROVAL,
158+
}
159+
160+
161+
@dataclass(frozen=True)
162+
class CoordinationDecision:
163+
"""
164+
Final coordinator-facing summary for a runtime request.
165+
166+
This is the compact artifact that can be passed to a future execution
167+
adapter, logger, replay system, or benchmark harness.
168+
"""
169+
170+
request_id: str
171+
status: DecisionStatus
172+
reason_code: str
173+
consent: ConsentAssessment
174+
safety: SafetyAssessment
175+
planning: Optional[PlanningOutcome] = None
176+
created_at_utc_s: float = field(default_factory=time)
177+
178+
@property
179+
def executable(self) -> bool:
180+
if self.status not in {
181+
DecisionStatus.APPROVED,
182+
DecisionStatus.DEGRADED_APPROVAL,
183+
}:
184+
return False
185+
if not self.consent.approved:
186+
return False
187+
if not self.safety.approved:
188+
return False
189+
if self.planning is None:
190+
return False
191+
return self.planning.has_plan
192+
193+
194+
__all__ = [
195+
"InteractionKind",
196+
"RequestSource",
197+
"DecisionStatus",
198+
"InteractionRequest",
199+
"ConsentAssessment",
200+
"SafetyAssessment",
201+
"PlanningOutcome",
202+
"CoordinationDecision",
203+
]

0 commit comments

Comments
 (0)