1use crate::pb::contracts::{
11 robonix_system_soma_get_health_client::RobonixSystemSomaGetHealthClient,
12 robonix_system_soma_get_urdf_client::RobonixSystemSomaGetUrdfClient,
13 robonix_system_soma_get_yaml_client::RobonixSystemSomaGetYamlClient,
14};
15use crate::pb::soma::{GetHealthRequest, GetUrdfRequest, GetYamlRequest};
16use anyhow::{Context, Result};
17use robonix_atlas::client::{self as atlas_client, AtlasClient};
18use robonix_scribe::warn;
19
20const GET_YAML_CONTRACT: &str = "robonix/system/soma/get_yaml";
21const GET_URDF_CONTRACT: &str = "robonix/system/soma/get_urdf";
22const GET_HEALTH_CONTRACT: &str = "robonix/system/soma/get_health";
23
24pub async fn fetch_runtime_prompt_block(atlas: &mut AtlasClient, consumer_id: &str) -> String {
25 match fetch_health(atlas, consumer_id).await {
26 Ok(value) => format!(
27 "\n\n## Current embodiment state (from Soma)\n\
28 This snapshot was refreshed immediately before planning. Fresh fields are \
29 authoritative. Stale or missing fields mean unknown; never reconstruct them \
30 from conversation history. `likely_holding` means the calibrated gripper is \
31 not fully open; it does not identify the object.\n\n{}\n",
32 serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".into())
33 ),
34 Err(error) => format!(
35 "\n\n## Current embodiment state (from Soma)\n\
36 {{\"available\":false,\"error\":{}}}\n",
37 serde_json::to_string(&error.to_string()).unwrap_or_else(|_| "\"unknown\"".into())
38 ),
39 }
40}
41
42async fn fetch_health(atlas: &mut AtlasClient, consumer_id: &str) -> Result<serde_json::Value> {
43 let (channel_id, _provider_id, channel) =
44 atlas_client::connect_to_capability(atlas, consumer_id, GET_HEALTH_CONTRACT)
45 .await
46 .context("connect to Soma get_health")?;
47 let result = async {
48 let response = RobonixSystemSomaGetHealthClient::new(channel)
49 .get_health(GetHealthRequest {})
50 .await
51 .context("call Soma get_health")?
52 .into_inner();
53 let snapshot = response
54 .snapshot
55 .context("Soma has not published a health snapshot yet")?;
56 let components: Vec<_> = snapshot
57 .components
58 .into_iter()
59 .map(|component| {
60 serde_json::json!({
61 "id": component.id,
62 "parent_id": component.parent_id,
63 "kind": component.kind,
64 "health": component.health,
65 "operational_state": component.operational_state,
66 "online": component.online,
67 "detail": component.detail,
68 })
69 })
70 .collect();
71 let actuators: Vec<_> = snapshot
72 .actuators
73 .into_iter()
74 .map(|actuator| {
75 serde_json::json!({
76 "component_id": actuator.component_id,
77 "joint_name": actuator.joint_name,
78 "position": actuator.position.map(|value| serde_json::json!({
79 "value": value.value, "unit": value.unit, "quality": value.quality,
80 })),
81 "communication_ok": actuator.communication_ok,
82 })
83 })
84 .collect();
85 let metrics: Vec<_> = snapshot
86 .metrics
87 .into_iter()
88 .map(|metric| {
89 serde_json::json!({
90 "component_id": metric.component_id,
91 "name": metric.name,
92 "value": metric.value.map(|value| serde_json::json!({
93 "value": value.value, "unit": value.unit, "quality": value.quality,
94 })),
95 })
96 })
97 .collect();
98 Ok::<_, anyhow::Error>(serde_json::json!({
99 "available": true,
100 "body_id": snapshot.body_id,
101 "seq": snapshot.seq,
102 "source_ts_ns": snapshot.source_ts_ns,
103 "ttl_ms": snapshot.ttl_ms,
104 "components": components,
105 "actuators": actuators,
106 "metrics": metrics,
107 "safety": snapshot.safety.map(|safety| serde_json::json!({
108 "motion_allowed": safety.motion_allowed,
109 "motor_power_allowed": safety.motor_power_allowed,
110 "aggregate_state": safety.aggregate_state,
111 "detail": safety.detail,
112 })),
113 }))
114 }
115 .await;
116 let _ = atlas.disconnect_capability(&channel_id).await;
117 result
118}
119
120pub async fn fetch_system_prompt_block(
121 atlas: &mut AtlasClient,
122 consumer_id: &str,
123) -> Result<Option<String>> {
124 let yaml = match fetch_yaml(atlas, consumer_id).await {
125 Ok(text) => text,
126 Err(e) => {
127 warn!("[pilot/soma] get_yaml unavailable; continuing without Soma context: {e:#}");
128 return Ok(None);
129 }
130 };
131 let urdf = match fetch_urdf(atlas, consumer_id).await {
132 Ok(text) => text,
133 Err(e) => {
134 warn!("[pilot/soma] get_urdf unavailable; continuing with YAML only: {e:#}");
135 String::new()
136 }
137 };
138
139 let mut block = String::from(
140 "\n\n## Robot Body Context (from Soma)\n\n\
141 This is the robot's self-description, loaded automatically at Pilot startup. \
142 Treat it as authoritative HARD CONSTRAINTS for the robot's body, sensors, \
143 frames, limits, and deployment-specific notes. Do not ask the user to call \
144 Soma manually unless this context is absent or stale.\n\n\
145 ### Hard planning rules from Soma\n\n\
146 - Sensor placement and modality in `soma.yaml` are binding. Do not invent \
147 sensors, viewpoints, arms, grippers, or degrees of freedom that are not listed.\n\
148 - Before planning an observation, match the user's requested viewpoint \
149 (front / rear / left / right / top, etc.) against the listed sensors' \
150 `placement`, `human_label`, and `cannot_do` notes.\n\
151 - If the requested viewpoint is not directly available from the sensors \
152 listed in Soma, say so explicitly. Do NOT call a camera with one placement \
153 and describe its image as if it came from a different placement.\n\
154 - If a viewpoint can be achieved only by moving the base (for example, \
155 rotate 180 degrees, then use the front camera), state that plan clearly \
156 and use motion + observation capabilities rather than pretending a missing \
157 sensor exists.\n\n\
158 ### soma.yaml\n\n```yaml\n",
159 );
160 block.push_str(yaml.trim());
161 block.push_str("\n```\n");
162 if !urdf.trim().is_empty() {
163 block.push_str("\n### URDF\n\n```xml\n");
164 block.push_str(urdf.trim());
165 block.push_str("\n```\n");
166 }
167 Ok(Some(block))
168}
169
170async fn fetch_yaml(atlas: &mut AtlasClient, consumer_id: &str) -> Result<String> {
171 let (channel_id, _provider_id, channel) =
172 atlas_client::connect_to_capability(atlas, consumer_id, GET_YAML_CONTRACT)
173 .await
174 .context("connect to Soma get_yaml")?;
175 let result = async {
176 let mut client = RobonixSystemSomaGetYamlClient::new(channel);
177 let response = client
178 .get_yaml(GetYamlRequest {
179 robot_id: String::new(),
180 })
181 .await
182 .context("call Soma get_yaml")?
183 .into_inner();
184 Ok::<_, anyhow::Error>(response.yaml_text)
185 }
186 .await;
187 let _ = atlas.disconnect_capability(&channel_id).await;
188 result
189}
190
191async fn fetch_urdf(atlas: &mut AtlasClient, consumer_id: &str) -> Result<String> {
192 let (channel_id, _provider_id, channel) =
193 atlas_client::connect_to_capability(atlas, consumer_id, GET_URDF_CONTRACT)
194 .await
195 .context("connect to Soma get_urdf")?;
196 let result = async {
197 let mut client = RobonixSystemSomaGetUrdfClient::new(channel);
198 let response = client
199 .get_urdf(GetUrdfRequest {
200 robot_id: String::new(),
201 })
202 .await
203 .context("call Soma get_urdf")?
204 .into_inner();
205 Ok::<_, anyhow::Error>(response.urdf_xml)
206 }
207 .await;
208 let _ = atlas.disconnect_capability(&channel_id).await;
209 result
210}