1use sim_codec_bridge::{
2 BridgeBook, BridgePacket, BridgePart, BridgeWeavePayload, BridgeWeaveRow, WeavePart,
3 stamp_packet_cid,
4};
5use sim_kernel::{
6 Consistency, Cx, Error, EvalFabric, EvalMode, EvalReply, EvalRequest, Expr, Result, Symbol,
7};
8use sim_lib_agent_runner_core::{
9 ModelRequest, ModelResponse, OUTPUT_GRAMMAR_EXTRA, OUTPUT_GRAMMAR_REQUIRED_EXTRA,
10 RETURN_CODEC_EXTRA, RETURN_SHAPE_EXTRA, terminal_model_content,
11};
12use sim_value::{access::field, build::entry};
13
14use crate::frontier::FrontierMenu;
15use crate::loom_validate::{next_frontier_menu, validate_weave};
16use crate::report::{BridgeObligation, BridgeReport};
17use crate::rx::{effective_caps, rx_check};
18
19pub fn validate_woven_row(
22 cx: &mut Cx,
23 packet: &BridgePacket,
24 accepted_rows: &[BridgeWeaveRow],
25 row: BridgeWeaveRow,
26) -> Result<BridgeReport> {
27 let rows = accepted_rows
28 .iter()
29 .cloned()
30 .chain(std::iter::once(row))
31 .collect();
32 let weave = BridgeWeavePayload::new(rows);
33 validate_weave(cx, &packet_with_weave(packet, &weave)?, &weave)
34}
35
36pub fn weave_row_by_row(
44 cx: &mut Cx,
45 target: &dyn EvalFabric,
46 packet: BridgePacket,
47 weave: &WeavePart,
48) -> Result<BridgePacket> {
49 if weave.rows.is_empty() {
50 return Err(Error::Eval(
51 "LOOM woven mode requires at least one row budget entry".to_owned(),
52 ));
53 }
54 require_weave_part(&packet)?;
55
56 let mut accepted_rows = Vec::new();
57 for row_index in 0..weave.rows.len() {
58 let row = request_woven_row(cx, target, &packet, &accepted_rows, row_index, &[])?;
59 let report = validate_woven_row(cx, &packet, &accepted_rows, row.clone())?;
60 if report.accepted() {
61 accepted_rows.push(row);
62 continue;
63 }
64
65 let replacement = request_woven_row(
66 cx,
67 target,
68 &packet,
69 &accepted_rows,
70 row_index,
71 &report.obligations,
72 )?;
73 let replacement_report =
74 validate_woven_row(cx, &packet, &accepted_rows, replacement.clone())?;
75 if !replacement_report.accepted() {
76 return Err(Error::Eval(format!(
77 "LOOM woven repair failed at loom/rows/{row_index}: {:?}",
78 replacement_report.obligations
79 )));
80 }
81 accepted_rows.push(replacement);
82 }
83
84 let mut completed = packet_with_weave(&packet, &BridgeWeavePayload::new(accepted_rows))?;
85 completed.header.cid = None;
86 let completed = stamp_packet_cid(&completed)?;
87 let book = BridgeBook::standard();
88 let report = rx_check(cx, &book, &completed, None)?;
89 if !report.accepted() {
90 return Err(Error::Eval(format!(
91 "LOOM woven packet failed final check: {:?}",
92 report.obligations
93 )));
94 }
95 Ok(completed)
96}
97
98fn request_woven_row(
99 cx: &mut Cx,
100 target: &dyn EvalFabric,
101 packet: &BridgePacket,
102 accepted_rows: &[BridgeWeaveRow],
103 row_index: usize,
104 obligations: &[BridgeObligation],
105) -> Result<BridgeWeaveRow> {
106 let request = woven_row_request(cx, packet, accepted_rows, row_index, obligations)?;
107 let caps = effective_caps(cx, packet)?;
108 let reply = cx.with_capabilities(caps, |cx| target.realize(cx, request))?;
109 decode_row_reply(cx, reply)
110}
111
112fn woven_row_request(
113 cx: &mut Cx,
114 packet: &BridgePacket,
115 accepted_rows: &[BridgeWeaveRow],
116 row_index: usize,
117 obligations: &[BridgeObligation],
118) -> Result<EvalRequest> {
119 let partial_weave = BridgeWeavePayload::new(accepted_rows.to_vec());
120 let partial_packet = packet_with_optional_weave(packet, Some(&partial_weave))?;
121 let menu = next_frontier_menu(cx, &partial_packet)?;
122 let role_refs = role_ref_menu(packet, accepted_rows);
123 let mut model = ModelRequest::new(
124 Expr::Map(vec![
125 entry(
126 "mode",
127 Expr::Symbol(Symbol::qualified("bridge", "loom-woven-row")),
128 ),
129 entry("row-path", row_path(row_index)),
130 entry("head-menu", menu.heads.clone()),
131 entry("role-ref-menu", string_vector(&role_refs)),
132 entry("slot-menu", slot_menu_expr(&menu)),
133 entry("obligations", obligations_expr(obligations)),
134 ]),
135 Vec::new(),
136 );
137 model.extra.push(entry(
138 "bridge-mode",
139 Expr::Symbol(Symbol::qualified("bridge", "loom-woven-row")),
140 ));
141 model
142 .extra
143 .push(entry("bridge-row-path", row_path(row_index)));
144 model
145 .extra
146 .push(entry("bridge-frontier-heads", menu.heads.clone()));
147 model
148 .extra
149 .push(entry("bridge-frontier-slots", slot_menu_expr(&menu)));
150 model
151 .extra
152 .push(entry("bridge-role-refs", string_vector(&role_refs)));
153 model.extra.push(entry(
154 "bridge-frontier-grammar",
155 Expr::String(menu.grammar.clone()),
156 ));
157 if !obligations.is_empty() {
158 model
159 .extra
160 .push(entry("bridge-obligations", obligations_expr(obligations)));
161 }
162 model.extra.push(entry(
163 RETURN_CODEC_EXTRA,
164 Expr::Symbol(Symbol::qualified("codec", "bridge")),
165 ));
166 model
167 .extra
168 .push(entry(RETURN_SHAPE_EXTRA, row_shape_expr(&menu, &role_refs)));
169 model.extra.push(entry(
170 OUTPUT_GRAMMAR_EXTRA,
171 Expr::String(row_grammar(&menu, &role_refs)),
172 ));
173 model
174 .extra
175 .push(entry(OUTPUT_GRAMMAR_REQUIRED_EXTRA, Expr::Bool(true)));
176
177 Ok(EvalRequest {
178 expr: Expr::from(model),
179 result_shape: None,
180 required_capabilities: effective_caps(cx, packet)?.iter().cloned().collect(),
181 deadline: None,
182 consistency: Consistency::default(),
183 mode: EvalMode::default(),
184 answer_limit: None,
185 stream_buffer: None,
186 stream: false,
187 trace: false,
188 })
189}
190
191fn decode_row_reply(cx: &mut Cx, reply: EvalReply) -> Result<BridgeWeaveRow> {
192 let response = ModelResponse::try_from(reply.value.object().as_expr(cx)?)?;
193 let content = terminal_model_content(&response)?;
194 BridgeWeaveRow::from_expr(field(content, "row").unwrap_or(content))
195}
196
197fn packet_with_weave(packet: &BridgePacket, weave: &BridgeWeavePayload) -> Result<BridgePacket> {
198 packet_with_optional_weave(packet, Some(weave))
199}
200
201fn packet_with_optional_weave(
202 packet: &BridgePacket,
203 weave: Option<&BridgeWeavePayload>,
204) -> Result<BridgePacket> {
205 let mut packet = packet.clone();
206 let index = require_weave_part(&packet)?;
207 if let Some(weave) = weave {
208 packet.body[index].payload = weave.to_expr();
209 }
210 packet.header.cid = None;
211 Ok(packet)
212}
213
214fn require_weave_part(packet: &BridgePacket) -> Result<usize> {
215 packet
216 .body
217 .iter()
218 .position(is_weave_part)
219 .ok_or_else(|| Error::Eval("LOOM woven mode requires a bridge/Weave part".to_owned()))
220}
221
222fn is_weave_part(part: &BridgePart) -> bool {
223 part.kind == Symbol::qualified("bridge", "Weave")
224}
225
226fn role_ref_menu(packet: &BridgePacket, accepted_rows: &[BridgeWeaveRow]) -> Vec<String> {
227 let mut refs = packet
228 .body
229 .iter()
230 .filter(|part| !is_weave_part(part))
231 .map(|part| part.id.as_qualified_str())
232 .chain(packet.header.context.iter().map(Symbol::as_qualified_str))
233 .chain(accepted_rows.iter().map(|row| row.slot.clone()))
234 .collect::<Vec<_>>();
235 refs.sort();
236 refs.dedup();
237 refs
238}
239
240fn row_shape_expr(menu: &FrontierMenu, role_refs: &[String]) -> Expr {
241 Expr::Map(vec![
242 entry(
243 "shape",
244 Expr::Symbol(Symbol::qualified("bridge", "WeaveRow")),
245 ),
246 entry("slot", Expr::Symbol(Symbol::qualified("core", "String"))),
247 entry("head", menu.heads.clone()),
248 entry("roles", string_vector(role_refs)),
249 ])
250}
251
252fn row_grammar(menu: &FrontierMenu, role_refs: &[String]) -> String {
253 format!(
254 "{{\"type\":\"object\",\"required\":[\"slot\",\"head\",\"roles\"],\"properties\":{{\"slot\":{{\"type\":\"string\"}},\"head\":{},\"roles\":{{\"type\":\"object\",\"additionalProperties\":{{\"enum\":{}}}}}}}}}",
255 menu.grammar,
256 json_string_array(role_refs)
257 )
258}
259
260fn slot_menu_expr(menu: &FrontierMenu) -> Expr {
261 Expr::Vector(
262 menu.slots
263 .iter()
264 .map(|(slot, shape)| {
265 Expr::Map(vec![
266 entry("slot", Expr::String(slot.clone())),
267 entry("shape", shape.clone()),
268 ])
269 })
270 .collect(),
271 )
272}
273
274fn obligations_expr(obligations: &[BridgeObligation]) -> Expr {
275 Expr::Vector(obligations.iter().map(BridgeObligation::to_expr).collect())
276}
277
278fn string_vector(items: &[String]) -> Expr {
279 Expr::Vector(items.iter().cloned().map(Expr::String).collect())
280}
281
282fn row_path(row_index: usize) -> Expr {
283 Expr::String(format!("loom/rows/{row_index}"))
284}
285
286fn json_string_array(items: &[String]) -> String {
287 let values = items
288 .iter()
289 .map(|item| format!("\"{}\"", json_escape(item)))
290 .collect::<Vec<_>>();
291 format!("[{}]", values.join(","))
292}
293
294fn json_escape(text: &str) -> String {
295 let mut escaped = String::new();
296 for ch in text.chars() {
297 match ch {
298 '"' => escaped.push_str("\\\""),
299 '\\' => escaped.push_str("\\\\"),
300 '\n' => escaped.push_str("\\n"),
301 '\r' => escaped.push_str("\\r"),
302 '\t' => escaped.push_str("\\t"),
303 other => escaped.push(other),
304 }
305 }
306 escaped
307}