Skip to main content

sim_lib_bridge/
loom_woven.rs

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
19/// Validates one candidate woven row by appending it to the accepted prefix and
20/// running the shared LOOM checker.
21pub 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
36/// Runs LOOM woven mode, requesting and committing one row at a time.
37///
38/// The `weave` argument supplies the row budget for this run. Each requested
39/// row is decoded as a single [`BridgeWeaveRow`], checked through the same
40/// validator used by validated mode, and committed before the next request is
41/// built. A rejected row receives one constrained replacement request carrying
42/// the exact row path, current frontier, role-reference menu, and obligations.
43pub 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}