Skip to content

Commit a171f35

Browse files
authored
Create execution_adapter.py
1 parent b904a63 commit a171f35

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
"""
2+
Execution adapter contracts for IX-HapticSight.
3+
4+
This module defines backend-agnostic execution interfaces that sit between:
5+
- runtime coordination / planning
6+
and
7+
- a concrete simulator, middleware bridge, or robot controller adapter
8+
9+
The goal is to keep execution transport replaceable without weakening the
10+
authority of consent, safety, or bounded planning logic.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from abc import ABC, abstractmethod
16+
from dataclasses import dataclass, field
17+
from enum import Enum
18+
from time import time
19+
from typing import Optional
20+
21+
from ohip.schemas import ContactPlan, Pose, SafetyLevel
22+
23+
24+
class ExecutionCommandKind(str, Enum):
25+
"""
26+
High-level command classes that the runtime may ask an execution backend
27+
to honor.
28+
"""
29+
30+
PLAN = "PLAN"
31+
POSE_TARGET = "POSE_TARGET"
32+
ABORT = "ABORT"
33+
RETREAT = "RETREAT"
34+
SAFE_HOLD = "SAFE_HOLD"
35+
36+
37+
class ExecutionResultStatus(str, Enum):
38+
"""
39+
Normalized execution result / state values exposed by an adapter.
40+
"""
41+
42+
ACCEPTED = "ACCEPTED"
43+
REJECTED = "REJECTED"
44+
RUNNING = "RUNNING"
45+
COMPLETED = "COMPLETED"
46+
ABORTED = "ABORTED"
47+
RETREATING = "RETREATING"
48+
SAFE_HOLD = "SAFE_HOLD"
49+
FAULTED = "FAULTED"
50+
UNAVAILABLE = "UNAVAILABLE"
51+
52+
53+
@dataclass(frozen=True)
54+
class ExecutionBackendCapabilities:
55+
"""
56+
Small capability summary for one execution backend.
57+
58+
This is intentionally conservative. It helps runtime code understand what
59+
the backend claims to support without embedding backend-specific logic in
60+
the protocol core.
61+
"""
62+
63+
backend_name: str
64+
supports_plan_execution: bool = False
65+
supports_pose_targets: bool = False
66+
supports_abort: bool = True
67+
supports_retreat: bool = True
68+
supports_safe_hold: bool = True
69+
supports_progress_updates: bool = False
70+
supports_collision_aware_execution: bool = False
71+
supports_velocity_scaling: bool = True
72+
supports_force_limited_execution: bool = False
73+
74+
def to_dict(self) -> dict:
75+
return {
76+
"backend_name": self.backend_name,
77+
"supports_plan_execution": bool(self.supports_plan_execution),
78+
"supports_pose_targets": bool(self.supports_pose_targets),
79+
"supports_abort": bool(self.supports_abort),
80+
"supports_retreat": bool(self.supports_retreat),
81+
"supports_safe_hold": bool(self.supports_safe_hold),
82+
"supports_progress_updates": bool(self.supports_progress_updates),
83+
"supports_collision_aware_execution": bool(self.supports_collision_aware_execution),
84+
"supports_velocity_scaling": bool(self.supports_velocity_scaling),
85+
"supports_force_limited_execution": bool(self.supports_force_limited_execution),
86+
}
87+
88+
89+
@dataclass(frozen=True)
90+
class BoundedExecutionRequest:
91+
"""
92+
A runtime-approved execution request.
93+
94+
Exactly one of `plan` or `target_pose` may be present for motion-like
95+
commands. Abort / retreat / safe-hold commands may omit both and rely on
96+
reason codes and backend semantics.
97+
"""
98+
99+
request_id: str
100+
session_id: str
101+
command_kind: ExecutionCommandKind
102+
safety_level: SafetyLevel
103+
plan: Optional[ContactPlan] = None
104+
target_pose: Optional[Pose] = None
105+
max_speed_scale: float = 1.0
106+
timeout_s: Optional[float] = None
107+
reason_code: str = ""
108+
created_at_utc_s: float = field(default_factory=time)
109+
110+
def validate(self) -> None:
111+
if self.max_speed_scale <= 0.0:
112+
raise ValueError("max_speed_scale must be > 0.0")
113+
114+
if self.command_kind in {
115+
ExecutionCommandKind.PLAN,
116+
ExecutionCommandKind.RETREAT,
117+
} and self.plan is None and self.target_pose is None:
118+
raise ValueError(
119+
"PLAN or RETREAT requests require a plan or target_pose"
120+
)
121+
122+
if self.command_kind == ExecutionCommandKind.POSE_TARGET and self.target_pose is None:
123+
raise ValueError("POSE_TARGET requests require target_pose")
124+
125+
if self.plan is not None and self.target_pose is not None:
126+
raise ValueError("request may not include both plan and target_pose")
127+
128+
if self.timeout_s is not None and self.timeout_s <= 0.0:
129+
raise ValueError("timeout_s must be > 0.0 when provided")
130+
131+
def to_dict(self) -> dict:
132+
return {
133+
"request_id": self.request_id,
134+
"session_id": self.session_id,
135+
"command_kind": self.command_kind.value,
136+
"safety_level": self.safety_level.value,
137+
"plan": None if self.plan is None else self.plan.to_dict(),
138+
"target_pose": None if self.target_pose is None else self.target_pose.to_dict(),
139+
"max_speed_scale": float(self.max_speed_scale),
140+
"timeout_s": None if self.timeout_s is None else float(self.timeout_s),
141+
"reason_code": self.reason_code,
142+
"created_at_utc_s": float(self.created_at_utc_s),
143+
}
144+
145+
146+
@dataclass(frozen=True)
147+
class ExecutionResponse:
148+
"""
149+
Immediate adapter response to an execution request.
150+
"""
151+
152+
request_id: str
153+
session_id: str
154+
status: ExecutionResultStatus
155+
accepted: bool
156+
backend_name: str
157+
reason_code: str = ""
158+
backend_execution_id: Optional[str] = None
159+
created_at_utc_s: float = field(default_factory=time)
160+
161+
def to_dict(self) -> dict:
162+
return {
163+
"request_id": self.request_id,
164+
"session_id": self.session_id,
165+
"status": self.status.value,
166+
"accepted": bool(self.accepted),
167+
"backend_name": self.backend_name,
168+
"reason_code": self.reason_code,
169+
"backend_execution_id": self.backend_execution_id,
170+
"created_at_utc_s": float(self.created_at_utc_s),
171+
}
172+
173+
174+
@dataclass(frozen=True)
175+
class ExecutionUpdate:
176+
"""
177+
Periodic or terminal execution status update from an adapter.
178+
"""
179+
180+
request_id: str
181+
session_id: str
182+
status: ExecutionResultStatus
183+
backend_name: str
184+
progress: float = 0.0
185+
reason_code: str = ""
186+
backend_execution_id: Optional[str] = None
187+
created_at_utc_s: float = field(default_factory=time)
188+
189+
def validate(self) -> None:
190+
if self.progress < 0.0 or self.progress > 1.0:
191+
raise ValueError("progress must be between 0.0 and 1.0")
192+
193+
def to_dict(self) -> dict:
194+
return {
195+
"request_id": self.request_id,
196+
"session_id": self.session_id,
197+
"status": self.status.value,
198+
"backend_name": self.backend_name,
199+
"progress": float(self.progress),
200+
"reason_code": self.reason_code,
201+
"backend_execution_id": self.backend_execution_id,
202+
"created_at_utc_s": float(self.created_at_utc_s),
203+
}
204+
205+
206+
class ExecutionAdapter(ABC):
207+
"""
208+
Abstract contract for runtime execution backends.
209+
210+
Design rules:
211+
- execution may reject a request it cannot honor safely
212+
- execution may not expand the authority of the request
213+
- abort and safe-hold paths must remain explicit
214+
- status reporting should be structured and replay-friendly
215+
"""
216+
217+
@abstractmethod
218+
def capabilities(self) -> ExecutionBackendCapabilities:
219+
"""
220+
Return a summary of backend capabilities.
221+
"""
222+
raise NotImplementedError
223+
224+
@abstractmethod
225+
def submit(self, request: BoundedExecutionRequest) -> ExecutionResponse:
226+
"""
227+
Submit a bounded execution request to the backend.
228+
"""
229+
raise NotImplementedError
230+
231+
@abstractmethod
232+
def current_update(self, *, session_id: str) -> Optional[ExecutionUpdate]:
233+
"""
234+
Return the most recent execution update for a session, if any.
235+
"""
236+
raise NotImplementedError
237+
238+
@abstractmethod
239+
def abort(self, *, session_id: str, reason_code: str = "") -> ExecutionResponse:
240+
"""
241+
Ask the backend to abort execution for a session.
242+
"""
243+
raise NotImplementedError
244+
245+
@abstractmethod
246+
def safe_hold(self, *, session_id: str, reason_code: str = "") -> ExecutionResponse:
247+
"""
248+
Ask the backend to enter safe hold for a session.
249+
"""
250+
raise NotImplementedError
251+
252+
253+
__all__ = [
254+
"ExecutionCommandKind",
255+
"ExecutionResultStatus",
256+
"ExecutionBackendCapabilities",
257+
"BoundedExecutionRequest",
258+
"ExecutionResponse",
259+
"ExecutionUpdate",
260+
"ExecutionAdapter",
261+
]

0 commit comments

Comments
 (0)