lang2sim 2.0.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.
Files changed (65) hide show
  1. lang2sim/__init__.py +47 -0
  2. lang2sim/agent/__init__.py +5 -0
  3. lang2sim/agent/nemotron_client.py +109 -0
  4. lang2sim/agent/react_loop.py +848 -0
  5. lang2sim/agent/tools.py +998 -0
  6. lang2sim/cli/app.py +183 -0
  7. lang2sim/cli/screens/__init__.py +8 -0
  8. lang2sim/cli/screens/command_palette_modal.py +146 -0
  9. lang2sim/cli/screens/landing_screen.py +96 -0
  10. lang2sim/cli/screens/sessions_modal.py +236 -0
  11. lang2sim/cli/screens/workspace_screen.py +709 -0
  12. lang2sim/cli/widgets/__init__.py +16 -0
  13. lang2sim/cli/widgets/action_pill.py +67 -0
  14. lang2sim/cli/widgets/command_card.py +265 -0
  15. lang2sim/cli/widgets/plan_card.py +106 -0
  16. lang2sim/cli/widgets/prompt_card.py +96 -0
  17. lang2sim/cli/widgets/telemetry_sidebar.py +117 -0
  18. lang2sim/cli/widgets/thought_box.py +64 -0
  19. lang2sim/compiler/__init__.py +15 -0
  20. lang2sim/compiler/sdf_compiler.py +65 -0
  21. lang2sim/compiler/sir_generator.py +46 -0
  22. lang2sim/compiler/sir_validator.py +87 -0
  23. lang2sim/core/__init__.py +50 -0
  24. lang2sim/core/config.py +116 -0
  25. lang2sim/core/models.py +279 -0
  26. lang2sim/core/session_manager.py +189 -0
  27. lang2sim/perturbation/__init__.py +50 -0
  28. lang2sim/perturbation/_compat.py +19 -0
  29. lang2sim/perturbation/gazebo_bridge.py +520 -0
  30. lang2sim/perturbation/mcp_server.py +129 -0
  31. lang2sim/perturbation/routers/__init__.py +19 -0
  32. lang2sim/perturbation/routers/world.py +110 -0
  33. lang2sim/perturbation/sdf_snippets.py +112 -0
  34. lang2sim/runtime/__init__.py +7 -0
  35. lang2sim/runtime/control_api/__init__.py +7 -0
  36. lang2sim/runtime/control_api/client.py +187 -0
  37. lang2sim/runtime/control_api/server.py +504 -0
  38. lang2sim/runtime/display.py +141 -0
  39. lang2sim/runtime/docker_runner.py +286 -0
  40. lang2sim/runtime/doctor.py +220 -0
  41. lang2sim/runtime/hardware_probe.py +144 -0
  42. lang2sim/runtime/health_monitor.py +66 -0
  43. lang2sim/runtime/launch_builder.py +137 -0
  44. lang2sim/runtime/nav2_dispatcher.py +215 -0
  45. lang2sim/runtime/platform.py +137 -0
  46. lang2sim/runtime/ros_bridge.py +183 -0
  47. lang2sim/runtime/run_folder.py +86 -0
  48. lang2sim/runtime/self_healing.py +387 -0
  49. lang2sim/runtime/session.py +136 -0
  50. lang2sim/runtime/world_check.py +197 -0
  51. lang2sim/telemetry/__init__.py +85 -0
  52. lang2sim/telemetry/nqi_scorer.py +645 -0
  53. lang2sim/telemetry/ros_subscribers.py +489 -0
  54. lang2sim/telemetry/trajectory.py +258 -0
  55. lang2sim/telemetry/vision.py +477 -0
  56. lang2sim/world_gen/__init__.py +16 -0
  57. lang2sim/world_gen/catalog_loader.py +83 -0
  58. lang2sim/world_gen/map_slicer.py +131 -0
  59. lang2sim/world_gen/shapely_solver.py +256 -0
  60. lang2sim-2.0.0.dist-info/METADATA +53 -0
  61. lang2sim-2.0.0.dist-info/RECORD +65 -0
  62. lang2sim-2.0.0.dist-info/WHEEL +5 -0
  63. lang2sim-2.0.0.dist-info/entry_points.txt +2 -0
  64. lang2sim-2.0.0.dist-info/licenses/LICENSE +21 -0
  65. lang2sim-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,279 @@
1
+ """
2
+ Canonical Simulation Specification Schemas and Control API Data Contracts.
3
+ File: lang2sim/core/models.py
4
+ """
5
+
6
+ import math
7
+ from typing import List, Optional, Literal, Dict, Any
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # 1. Spatial and Kinematic Primitives
13
+ # ---------------------------------------------------------------------------
14
+
15
+ class Pose2D(BaseModel):
16
+ """2D ground-plane coordinate pose with yaw orientation."""
17
+ x: float = Field(..., description="X coordinate in meters relative to world origin")
18
+ y: float = Field(..., description="Y coordinate in meters relative to world origin")
19
+ yaw: float = Field(0.0, ge=-math.pi, le=math.pi, description="Yaw orientation in radians, [-pi, pi]")
20
+
21
+ @classmethod
22
+ def from_anchor(cls, anchor: Any) -> "Pose2D":
23
+ """Convert either a list [x, y, yaw] or a dict {'x':..., 'y':..., 'yaw':...}."""
24
+ if isinstance(anchor, (list, tuple)):
25
+ yaw = float(anchor[2]) if len(anchor) > 2 else 0.0
26
+ return cls(x=float(anchor[0]), y=float(anchor[1]), yaw=yaw)
27
+ elif isinstance(anchor, dict):
28
+ return cls(**anchor)
29
+ elif isinstance(anchor, cls):
30
+ return anchor
31
+ raise ValueError(f"Cannot parse anchor of type {type(anchor)}: {anchor}")
32
+
33
+
34
+ class Vector3D(BaseModel):
35
+ """3D bounding dimensions in meters."""
36
+ x: float = Field(..., description="Bounding size X (length) in meters")
37
+ y: float = Field(..., description="Bounding size Y (width) in meters")
38
+ z: float = Field(..., description="Bounding size Z (height) in meters")
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # 2. Environmental & Weather Specifications
43
+ # ---------------------------------------------------------------------------
44
+
45
+ class SkyWeatherSpec(BaseModel):
46
+ """Procedural atmospheric sky, sunlight angle, and volumetric fog controls."""
47
+ sky_enabled: bool = True
48
+ clouds_speed: float = Field(10.0, ge=0.0, le=50.0, description="Procedural cloud animation speed")
49
+ fog_enabled: bool = False
50
+ fog_density: float = Field(0.0, ge=0.0, le=0.5, description="Volumetric fog density (0.05 = light, 0.2 = heavy mist)")
51
+ sun_elevation: float = Field(0.8, ge=0.0, le=1.57, description="Solar elevation angle in radians (0.0 = sunset/dusk)")
52
+
53
+
54
+ class EnvironmentSpec(BaseModel):
55
+ """Complete physical room and terrain parameters."""
56
+ environment_type: Literal[
57
+ "indoor_warehouse", "indoor_office", "outdoor_field", "outdoor_terrain"
58
+ ] = "indoor_warehouse"
59
+ room_dimensions: Vector3D = Field(..., description="Outer room/field boundaries in meters")
60
+ wall_height: float = Field(2.5, ge=0.0, le=10.0, description="Wall boundary height (0.0 for open outdoor fields)")
61
+ ambient_light_luminance: float = Field(
62
+ 1.0, ge=0.0, le=1.0, description="Ambient light factor (1.0 = nominal, 0.15 = degraded night)"
63
+ )
64
+ ground_terrain: Literal[
65
+ "concrete", "grass_soil", "dirt_mud", "asphalt_road", "uneven_heightmap", "tiles"
66
+ ] = "concrete"
67
+ surface_friction: float = Field(
68
+ 0.8, ge=0.05, le=1.5, description="Surface friction coefficient (0.8 = standard, 0.15 = slippery ice/mud)"
69
+ )
70
+ sky_weather: SkyWeatherSpec = Field(default_factory=SkyWeatherSpec)
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # 3. Object & Obstacle Specifications
75
+ # ---------------------------------------------------------------------------
76
+
77
+ class ObjectSpec(BaseModel):
78
+ """Physical entity to be placed in the simulation world."""
79
+ id: str = Field(..., description="Unique alphanumeric identifier (e.g., 'rack_1')")
80
+ model_name: str = Field(..., description="Asset identifier matching catalog (e.g., 'aws_warehouse_rack')")
81
+ dimensions: Vector3D = Field(..., description="Bounding dimensions of the object in meters")
82
+ pose: Pose2D = Field(..., description="2D ground plane pose")
83
+ is_static: bool = Field(True, description="Static obstacle vs dynamically mutable entity")
84
+ semantic_parent_id: Optional[str] = Field(None, description="Anchor ID for grouped objects")
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # 4. Sensor Perturbations & Noise Models
89
+ # ---------------------------------------------------------------------------
90
+
91
+ class CameraPerturbation(BaseModel):
92
+ """Optical degradation filters for on-board robot camera."""
93
+ enabled: bool = False
94
+ motion_blur_kernel: int = Field(0, ge=0, le=31, description="Blur kernel size (odd integer, 0 = off)")
95
+ brightness_attenuation: float = Field(1.0, ge=0.0, le=1.0, description="Illumination multiplier")
96
+ gaussian_noise_stddev: float = Field(0.0, ge=0.0, le=0.5, description="Additive noise standard deviation")
97
+
98
+
99
+ class LidarPerturbation(BaseModel):
100
+ """LiDAR beam degradation filters."""
101
+ enabled: bool = False
102
+ dropout_probability: float = Field(0.0, ge=0.0, le=0.9, description="Random beam dropout rate")
103
+ range_noise_stddev: float = Field(0.0, ge=0.0, le=0.2, description="Gaussian noise standard deviation in meters")
104
+
105
+
106
+ class SensorNoiseSpec(BaseModel):
107
+ """Aggregated sensor degradation models."""
108
+ camera: CameraPerturbation = Field(default_factory=CameraPerturbation)
109
+ lidar: LidarPerturbation = Field(default_factory=LidarPerturbation)
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # 5. Dynamic Perturbations & Adversarial Triggers
114
+ # ---------------------------------------------------------------------------
115
+
116
+ class DynamicObstacleTrigger(BaseModel):
117
+ """Runtime perturbation trigger dispatched via FastMCP."""
118
+ trigger_type: Literal["time", "waypoint_distance", "immediate"]
119
+ trigger_value: float = Field(..., description="Seconds from start or distance in meters to target")
120
+ model_name: str = Field("wooden_crate", description="Asset identifier to spawn dynamically")
121
+ spawn_pose: Pose2D = Field(..., description="Spawn coordinate in world reference frame")
122
+ velocity: Optional[Pose2D] = Field(None, description="Linear and angular velocity vector")
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # 6. Mission & Simulation Specifications
127
+ # ---------------------------------------------------------------------------
128
+
129
+ class MissionSpec(BaseModel):
130
+ """Navigation task definition dispatched via Nav2 Simple Commander."""
131
+ initial_pose: Pose2D = Field(..., description="Robot starting coordinate and heading")
132
+ waypoints: List[Pose2D] = Field(default_factory=list, description="Intermediate navigation waypoints")
133
+ goal_pose: Pose2D = Field(..., description="Final navigation goal pose")
134
+ timeout_seconds: float = Field(120.0, ge=10.0, le=600.0, description="Maximum operational allowance")
135
+
136
+
137
+ class SimSpec(BaseModel):
138
+ """Canonical top-level specification emitted by the supervisor agent."""
139
+ spec_version: str = "2.0.0"
140
+ environment: EnvironmentSpec
141
+ objects: List[ObjectSpec] = Field(default_factory=list)
142
+ robot_model: Literal["turtlebot3_waffle", "turtlebot4", "ridgeback"] = "turtlebot3_waffle"
143
+ sensor_noise: SensorNoiseSpec = Field(default_factory=SensorNoiseSpec)
144
+ adversarial_triggers: List[DynamicObstacleTrigger] = Field(default_factory=list)
145
+ mission: MissionSpec
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # 7. Simulation Intermediate Representation (SIR)
150
+ # ---------------------------------------------------------------------------
151
+
152
+ class AisleCorridor(BaseModel):
153
+ """Validated navigation corridor between obstacles."""
154
+ corridor_id: str
155
+ width: float = Field(..., ge=2.2, description="Aisle width in meters (must be >= 2.2m)")
156
+ start_point: Pose2D
157
+ end_point: Pose2D
158
+
159
+
160
+ class SimulationIR(BaseModel):
161
+ """Validated intermediate representation graph linking entities and corridors."""
162
+ spec: SimSpec
163
+ world_sdf_path: Optional[str] = None
164
+ map_yaml_path: Optional[str] = None
165
+ map_pgm_path: Optional[str] = None
166
+ resolved_anchors: Dict[str, List[float]] = Field(default_factory=dict)
167
+ aisle_corridors: List[AisleCorridor] = Field(default_factory=list)
168
+ is_valid: bool = False
169
+ validation_messages: List[str] = Field(default_factory=list)
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # 8. Status Models (PR #7 Aligned)
174
+ # ---------------------------------------------------------------------------
175
+
176
+ class NavStatus(BaseModel):
177
+ """Navigation task state streamed via WebSocket or GET /nav/status."""
178
+ is_active: bool = False
179
+ current_task: Optional[str] = None
180
+ distance_remaining: float = 0.0
181
+ target_pose: Optional[Pose2D] = None
182
+ status: Literal["idle", "navigating", "succeeded", "failed", "canceled"] = "idle"
183
+ waypoint_index: int = Field(0, description="0-based index of the waypoint being driven to")
184
+ waypoints_total: int = Field(0, description="Number of poses in the current task (1 for a single goal)")
185
+ navigation_time: float = Field(0.0, description="Sim seconds spent on the current leg")
186
+ eta_seconds: Optional[float] = Field(None, description="Nav2's estimated time remaining on this leg")
187
+ recoveries: int = Field(0, description="Nav2 recovery behaviours run on this leg")
188
+ message: Optional[str] = Field(None, description="Why a task failed or was canceled")
189
+
190
+
191
+ class SimStatus(BaseModel):
192
+ """Simulation state monitor model (PR #7 and HealthMonitor aligned)."""
193
+ is_running: bool = False
194
+ sim_time: float = 0.0
195
+ real_time_factor: float = 1.0
196
+ robot_pose: Pose2D = Field(default_factory=lambda: Pose2D(x=0.0, y=0.0, yaw=0.0))
197
+ robot_velocity: Optional[Pose2D] = None
198
+ render_mode: Literal["gpu", "dri", "software", "headless"] = "software"
199
+ health_status: Literal["healthy", "degraded", "crashed"] = "healthy"
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # 9. Control API Contracts (Dev 3 Container Server & Dev 4)
204
+ # ---------------------------------------------------------------------------
205
+
206
+ class SimStartRequest(BaseModel):
207
+ """Request payload sent to POST /sim/start."""
208
+ run_dir: str = Field(..., description="Absolute path to active run directory inside container")
209
+ world_sdf_path: str = Field(..., description="Path to world.sdf")
210
+ map_yaml_path: str = Field(..., description="Path to map.yaml")
211
+ robot_model: str = "turtlebot3_waffle"
212
+ initial_pose: Pose2D
213
+ use_gui: bool = True
214
+ use_rviz: bool = Field(True, description="Open RViz2 with the Nav2 panels (ignored without a display)")
215
+ world_name: Optional[str] = Field(None, description="World name; read from the SDF when omitted")
216
+ nav2_params_path: Optional[str] = Field(
217
+ None, description="Nav2 params file inside the container; default: TurtleBot3 Waffle params"
218
+ )
219
+
220
+
221
+ class RunManifest(BaseModel):
222
+ """Contract for one run folder (decision 1): ~/.lang2sim/projects/<project>/runs/<timestamp>/manifest.json.
223
+
224
+ Paths are relative to the run folder. Part 1 writes the files, Part 3 only reads them.
225
+ """
226
+ manifest_version: Literal["1.0"] = "1.0"
227
+ world_sdf: str = Field("world.sdf", description="Gazebo Harmonic world (SDF 1.9+), no robot inside")
228
+ world_name: Optional[str] = Field(None, description="<world name=...>; read from the SDF when omitted")
229
+ map_yaml: str = Field("map.yaml", description="Nav2 map whose frame equals the world frame")
230
+ spec_json: Optional[str] = Field("spec.json", description="The SimSpec this run was generated from")
231
+ robot_model: Literal["turtlebot3_waffle"] = Field(
232
+ "turtlebot3_waffle", description="Only TurtleBot3 Waffle is supported for the MVP (decision 4)"
233
+ )
234
+ initial_pose: Pose2D = Field(..., description="Robot spawn pose in the world/map frame")
235
+ nav2_params: Optional[str] = Field(None, description="Custom Nav2 params; default: TurtleBot3 Waffle params")
236
+
237
+
238
+ class SimStopRequest(BaseModel):
239
+ """Request payload sent to POST /sim/stop."""
240
+ force: bool = False
241
+
242
+
243
+ class SimStatusResponse(BaseModel):
244
+ """Response payload returned by GET /sim/status."""
245
+ status: Literal["idle", "launching", "running", "degraded", "stopped", "error"]
246
+ rtf: float = Field(0.0, description="Real-Time Factor (Δsim/Δwall)")
247
+ sim_time: float = Field(0.0, description="Current simulation time in seconds")
248
+ active_nodes: List[str] = Field(default_factory=list)
249
+ active_topics: List[str] = Field(default_factory=list)
250
+ ready: bool = Field(False, description="All readiness checks pass: robot can be given goals")
251
+ readiness: Dict[str, bool] = Field(
252
+ default_factory=dict, description="gazebo_clock, odometry, laser_scan, nav2_active, localized"
253
+ )
254
+ world_report: List[str] = Field(default_factory=list, description="World pre-flight findings for this run")
255
+
256
+
257
+ class NavInitialPoseRequest(BaseModel):
258
+ """Request payload sent to POST /nav/initial_pose."""
259
+ pose: Pose2D
260
+
261
+
262
+ class NavGoalRequest(BaseModel):
263
+ """Request payload sent to POST /nav/goal."""
264
+ goal_pose: Pose2D
265
+ timeout_seconds: float = 120.0
266
+
267
+
268
+ class NavWaypointsRequest(BaseModel):
269
+ """Request payload sent to POST /nav/waypoints."""
270
+ waypoints: List[Pose2D]
271
+ timeout_seconds: float = 180.0
272
+
273
+
274
+ class HealEvent(BaseModel):
275
+ """Log record of self-healing error recovery."""
276
+ timestamp: float
277
+ error_signature: str
278
+ action_taken: str
279
+ success: bool
@@ -0,0 +1,189 @@
1
+ """
2
+ Chat Session Memory & Persistence Manager
3
+ File: lang2sim/core/session_manager.py
4
+ """
5
+
6
+ import json
7
+ import time
8
+ import uuid
9
+ from datetime import datetime, date, timedelta
10
+ from pathlib import Path
11
+ from typing import List, Dict, Any, Optional
12
+ from pydantic import BaseModel, Field
13
+ from .config import DEFAULT_LANG2SIM_ROOT
14
+
15
+ SESSIONS_DIR = DEFAULT_LANG2SIM_ROOT / "sessions"
16
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
17
+
18
+
19
+ class ToolCallRecord(BaseModel):
20
+ """Execution record of an agentic tool call."""
21
+ tool_name: str
22
+ arguments: Dict[str, Any] = Field(default_factory=dict)
23
+ duration_str: str = "0.0s"
24
+ status: str = "completed" # "running", "completed", "failed"
25
+ output: Optional[Any] = None
26
+ error: Optional[str] = None
27
+
28
+
29
+ class ArtifactRecord(BaseModel):
30
+ """Artifact produced during the session (worlds, maps, benchmark reports)."""
31
+ type: str # "world_sdf", "map_pgm", "map_yaml", "benchmark_report"
32
+ path: str
33
+ created_at: float = Field(default_factory=time.time)
34
+ size_bytes: int = 0
35
+
36
+
37
+ class SimulationStateSnapshot(BaseModel):
38
+ """Real-time simulation state snapshot."""
39
+ world_sdf: Optional[str] = None
40
+ map_yaml: Optional[str] = None
41
+ active_robot: str = "turtlebot3_waffle"
42
+ ate_drift_m: float = 0.0
43
+ status: str = "idle" # "idle", "sim_running", "navigating", "paused"
44
+
45
+
46
+ class ChatMessage(BaseModel):
47
+ id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
48
+ role: str # "user", "assistant", "system", "tool"
49
+ content: str = ""
50
+ timestamp: float = Field(default_factory=time.time)
51
+ tokens: int = 0
52
+ # Thinking & Cognitive Reasoning
53
+ thought: Optional[str] = None
54
+ thought_duration_str: Optional[str] = None
55
+ # Tool Invocation Actions
56
+ actions: List[ToolCallRecord] = Field(default_factory=list)
57
+
58
+
59
+ class ChatSession(BaseModel):
60
+ id: str = Field(
61
+ default_factory=lambda: datetime.now().strftime("session_%Y%m%d_%H%M%S_") + str(uuid.uuid4())[:4]
62
+ )
63
+ title: str = "New Session"
64
+ created_at: float = Field(default_factory=time.time)
65
+ updated_at: float = Field(default_factory=time.time)
66
+ pinned: bool = False
67
+ messages: List[ChatMessage] = Field(default_factory=list)
68
+ total_tokens: int = 0
69
+ workspace_name: str = "active_project"
70
+ active_run_dir: Optional[str] = None
71
+ artifacts: List[ArtifactRecord] = Field(default_factory=list)
72
+ simulation_state: SimulationStateSnapshot = Field(default_factory=SimulationStateSnapshot)
73
+
74
+
75
+ class SessionManager:
76
+ """Manages creation, persistence, retrieval, and grouping of chat sessions."""
77
+
78
+ def __init__(self, sessions_dir: Optional[Path] = None):
79
+ self.sessions_dir = sessions_dir or SESSIONS_DIR
80
+ self.sessions_dir.mkdir(parents=True, exist_ok=True)
81
+
82
+ def _file_path(self, session_id: str) -> Path:
83
+ return self.sessions_dir / f"{session_id}.json"
84
+
85
+ def create_session(
86
+ self,
87
+ title: str = "New Session",
88
+ workspace_name: str = "active_project",
89
+ system_prompt: Optional[str] = None,
90
+ ) -> ChatSession:
91
+ session = ChatSession(title=title, workspace_name=workspace_name)
92
+ if system_prompt:
93
+ session.messages.append(ChatMessage(role="system", content=system_prompt))
94
+ self.save_session(session)
95
+ return session
96
+
97
+ def get_session(self, session_id: str) -> Optional[ChatSession]:
98
+ p = self._file_path(session_id)
99
+ if not p.is_file():
100
+ return None
101
+ try:
102
+ data = json.loads(p.read_text(encoding="utf-8"))
103
+ return ChatSession.model_validate(data)
104
+ except Exception:
105
+ return None
106
+
107
+ def save_session(self, session: ChatSession) -> None:
108
+ session.updated_at = time.time()
109
+ p = self._file_path(session.id)
110
+ p.write_text(session.model_dump_json(indent=2), encoding="utf-8")
111
+
112
+ def delete_session(self, session_id: str) -> bool:
113
+ p = self._file_path(session_id)
114
+ if p.is_file():
115
+ try:
116
+ p.unlink()
117
+ return True
118
+ except OSError:
119
+ return False
120
+ return False
121
+
122
+ def pin_session(self, session_id: str, pinned: Optional[bool] = None) -> bool:
123
+ session = self.get_session(session_id)
124
+ if not session:
125
+ return False
126
+ if pinned is None:
127
+ session.pinned = not session.pinned
128
+ else:
129
+ session.pinned = pinned
130
+ self.save_session(session)
131
+ return True
132
+
133
+ def rename_session(self, session_id: str, new_title: str) -> bool:
134
+ session = self.get_session(session_id)
135
+ if not session:
136
+ return False
137
+ session.title = new_title.strip() or "Untitled Session"
138
+ self.save_session(session)
139
+ return True
140
+
141
+ def list_sessions(self, workspace_name: Optional[str] = None) -> List[ChatSession]:
142
+ """Lists all stored sessions sorted by pinned first, then by updated_at descending."""
143
+ sessions: List[ChatSession] = []
144
+ for p in self.sessions_dir.glob("*.json"):
145
+ try:
146
+ data = json.loads(p.read_text(encoding="utf-8"))
147
+ s = ChatSession.model_validate(data)
148
+ if workspace_name is None or s.workspace_name == workspace_name:
149
+ sessions.append(s)
150
+ except Exception:
151
+ continue
152
+
153
+ # Sort: pinned first (True > False), then updated_at descending
154
+ sessions.sort(key=lambda s: (not s.pinned, -s.updated_at))
155
+ return sessions
156
+
157
+ def group_sessions_by_date(
158
+ self, sessions: List[ChatSession]
159
+ ) -> Dict[str, List[ChatSession]]:
160
+ """
161
+ Groups sessions chronologically matching OpenCode:
162
+ - "Today"
163
+ - "Yesterday"
164
+ - "%a %b %d %Y" (e.g. "Sun Oct 25 2026", "Wed Oct 07 2026")
165
+ """
166
+ today = date.today()
167
+ yesterday = today - timedelta(days=1)
168
+ grouped: Dict[str, List[ChatSession]] = {}
169
+
170
+ for s in sessions:
171
+ s_date = datetime.fromtimestamp(s.updated_at).date()
172
+ if s.pinned:
173
+ header = "Pinned"
174
+ elif s_date == today:
175
+ header = "Today"
176
+ elif s_date == yesterday:
177
+ header = "Yesterday"
178
+ else:
179
+ header = datetime.fromtimestamp(s.updated_at).strftime("%a %b %d %Y")
180
+
181
+ if header not in grouped:
182
+ grouped[header] = []
183
+ grouped[header].append(s)
184
+
185
+ return grouped
186
+
187
+
188
+ # Global instance
189
+ session_manager = SessionManager()
@@ -0,0 +1,50 @@
1
+ """Perturbation and FastMCP package for Lang2Sim."""
2
+
3
+ import sys
4
+
5
+ # Backward-compatibility alias
6
+ sys.modules.setdefault("sim2lang.perturbation", sys.modules[__name__])
7
+
8
+ from lang2sim.perturbation.gazebo_bridge import (
9
+ BridgeResult,
10
+ GazeboBridge,
11
+ DryRunBridge,
12
+ HarmonicBridge,
13
+ ClassicBridge,
14
+ get_bridge,
15
+ reset_bridge_instance,
16
+ )
17
+ from lang2sim.perturbation.mcp_server import (
18
+ mcp,
19
+ call_tool_inprocess,
20
+ spawn_dynamic_entity,
21
+ set_ambient_lighting,
22
+ reset_simulation,
23
+ apply_chassis_perturbation,
24
+ bridge_health,
25
+ list_spawned_entities,
26
+ )
27
+ from lang2sim.perturbation.routers.world import (
28
+ router as world_router,
29
+ mount_world_router,
30
+ )
31
+
32
+ __all__ = [
33
+ "BridgeResult",
34
+ "GazeboBridge",
35
+ "DryRunBridge",
36
+ "HarmonicBridge",
37
+ "ClassicBridge",
38
+ "get_bridge",
39
+ "reset_bridge_instance",
40
+ "mcp",
41
+ "call_tool_inprocess",
42
+ "spawn_dynamic_entity",
43
+ "set_ambient_lighting",
44
+ "reset_simulation",
45
+ "apply_chassis_perturbation",
46
+ "bridge_health",
47
+ "list_spawned_entities",
48
+ "world_router",
49
+ "mount_world_router",
50
+ ]
@@ -0,0 +1,19 @@
1
+ """Compatibility shim for Pose2D schema until Day-0 core models are merged."""
2
+
3
+ from __future__ import annotations
4
+
5
+ try:
6
+ from lang2sim.core.models import Pose2D
7
+ except ImportError:
8
+ try:
9
+ from lang2sim.core.models import Pose2D
10
+ except ImportError:
11
+ from pydantic import BaseModel, Field
12
+
13
+ class Pose2D(BaseModel):
14
+ """2D ground pose (x, y, yaw)."""
15
+ x: float = Field(..., description="X coordinate in meters")
16
+ y: float = Field(..., description="Y coordinate in meters")
17
+ yaw: float = Field(0.0, ge=-3.14159, le=3.14159, description="Heading in radians")
18
+
19
+ __all__ = ["Pose2D"]