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