Skip to main content

sim_lib_bridge/
ask.rs

1use sim_codec::{Input, Output, decode_with_codec, encode_with_codec};
2use sim_codec_bridge::{
3    BridgeBook, BridgeCallArgument, BridgeCallPayload, BridgeHeader, BridgePacket, BridgePart,
4    BridgeProvenance, CallArgumentMedia, content_id_string, stamp_packet_cid,
5};
6use sim_kernel::{
7    Cx, Datum, EncodeOptions, EvalFabric, Expr, ReadPolicy, Result, Symbol, encode::EncodePosition,
8};
9use sim_lib_agent_runner_core::{
10    InjectionFence, ModelResponse, OutputContract, terminal_model_content,
11};
12use sim_shape::{check_value_report, shape_value};
13use sim_value::{access::field, build::entry};
14
15use crate::model::output_contract_for_packet;
16use crate::parent::parent_token;
17use crate::repair::{AskFailure, RepairPolicy};
18use crate::rx::{effective_caps, rx_check, shape_from_contract_expr};
19use crate::tx::{eval_request_for_checked_packet, prepare_packet};
20
21/// Default codec for packing ASK call arguments and answers.
22pub fn ask_default_codec() -> Symbol {
23    Symbol::qualified("codec", "json")
24}
25
26/// Builds an ASK request packet with model parameters omitted.
27pub fn ask_packet(
28    cx: &mut Cx,
29    name: &str,
30    params: Vec<(String, Expr)>,
31    return_shape: Expr,
32    to: &str,
33) -> Result<BridgePacket> {
34    ask_packet_with_model_params(cx, name, params, Vec::new(), return_shape, to)
35}
36
37/// Builds an ASK request packet.
38///
39/// Argument values are encoded through the default ASK codec at data position,
40/// wrapped in deterministic injection fences, and only then stored in the
41/// packet.
42pub fn ask_packet_with_model_params(
43    cx: &mut Cx,
44    name: &str,
45    params: Vec<(String, Expr)>,
46    model_params: Vec<(String, Expr)>,
47    return_shape: Expr,
48    to: &str,
49) -> Result<BridgePacket> {
50    let codec = ask_default_codec();
51    let mut call = BridgeCallPayload::new(symbol_from_name(name));
52    for (name, value) in params {
53        call = call.with_arg(pack_argument(cx, &name, &codec, &value)?);
54    }
55    for (name, value) in model_params {
56        call = call.with_model_param(symbol_from_name(&name), value);
57    }
58    Ok(BridgePacket {
59        header: BridgeHeader {
60            cid: None,
61            move_kind: Symbol::new("request"),
62            from: "sim".to_owned(),
63            to: vec![to.to_owned()],
64            role: Symbol::new("implementer"),
65            parents: Vec::new(),
66            task: Symbol::new("C1"),
67            output: Symbol::new("O1"),
68            ceiling: vec![Symbol::qualified("ai", "run")],
69            context: Vec::new(),
70            provenance: BridgeProvenance::default(),
71        },
72        body: vec![
73            BridgePart {
74                id: Symbol::new("C1"),
75                kind: Symbol::qualified("bridge", "Call"),
76                payload: call.to_expr(),
77            },
78            BridgePart {
79                id: Symbol::new("O1"),
80                kind: Symbol::qualified("bridge", "Return"),
81                payload: Expr::Map(vec![
82                    entry("codec", Expr::Symbol(codec)),
83                    entry("shape", return_shape),
84                ]),
85            },
86        ],
87        warrant: None,
88    })
89}
90
91/// Runs an ASK packet with the default bounded repair policy.
92pub fn run_ask(cx: &mut Cx, target: &dyn EvalFabric, packet: BridgePacket) -> Result<BridgePacket> {
93    run_ask_with_policy(cx, target, packet, RepairPolicy::default())
94}
95
96/// Runs an ASK packet with an explicit bounded repair policy.
97pub fn run_ask_with_policy(
98    cx: &mut Cx,
99    target: &dyn EvalFabric,
100    mut packet: BridgePacket,
101    policy: RepairPolicy,
102) -> Result<BridgePacket> {
103    let book = BridgeBook::standard();
104    let max_retries = policy.retries();
105    for attempt in 0..=max_retries {
106        let checked = prepare_packet(cx, &book, &packet)?;
107        let request = eval_request_for_checked_packet(cx, &book, &checked)?;
108        let caps = effective_caps(cx, &checked)?;
109        let reply = cx.with_capabilities(caps, |cx| target.realize(cx, request))?;
110        let response = ModelResponse::try_from(reply.value.object().as_expr(cx)?)?;
111        match answer_packet(cx, &book, &checked, &response)? {
112            Ok(answer) => return Ok(answer),
113            Err(failure) if attempt < max_retries => {
114                packet = repair_packet_for_failure(cx, &checked, &failure, attempt + 1)?;
115            }
116            Err(failure) => {
117                return Err(sim_kernel::Error::Eval(format!(
118                    "bridge ask failed after {} attempt(s): {}",
119                    attempt + 1,
120                    failure.message()
121                )));
122            }
123        }
124    }
125    unreachable!("bounded ASK loop always returns inside the retry range")
126}
127
128pub(crate) fn pack_argument(
129    cx: &mut Cx,
130    name: &str,
131    codec: &Symbol,
132    value: &Expr,
133) -> Result<BridgeCallArgument> {
134    let output = encode_with_codec(
135        cx,
136        codec,
137        value,
138        EncodeOptions {
139            position: EncodePosition::Data,
140            ..EncodeOptions::default()
141        },
142    )?;
143    let (media, datum, body) = match output {
144        Output::Text(text) => (CallArgumentMedia::Text, Datum::String(text.clone()), text),
145        Output::Bytes(bytes) => (
146            CallArgumentMedia::Bytes,
147            Datum::Bytes(bytes.clone()),
148            hex_text(&bytes),
149        ),
150    };
151    let content_id = datum.content_id()?;
152    let fence = InjectionFence::for_content(&content_id);
153    Ok(BridgeCallArgument::new(
154        symbol_from_name(name),
155        codec.clone(),
156        media,
157        content_id_string(&content_id),
158        fence.wrap(name, &body),
159    ))
160}
161
162fn answer_packet(
163    cx: &mut Cx,
164    book: &BridgeBook,
165    parent: &BridgePacket,
166    response: &ModelResponse,
167) -> Result<std::result::Result<BridgePacket, AskFailure>> {
168    let contract = output_contract_for_packet(parent)?;
169    let answer = match decode_terminal_answer(cx, response, &contract)? {
170        Ok(answer) => answer,
171        Err(failure) => return Ok(Err(failure)),
172    };
173    if let Err(failure) = validate_answer(cx, &contract, &answer)? {
174        return Ok(Err(failure));
175    }
176    let packet = stamp_packet_cid(&BridgePacket {
177        header: BridgeHeader {
178            cid: None,
179            move_kind: Symbol::new("reply"),
180            from: parent
181                .header
182                .to
183                .first()
184                .cloned()
185                .unwrap_or_else(|| "model".to_owned()),
186            to: vec![parent.header.from.clone()],
187            role: Symbol::new("implementer"),
188            parents: parent_token(parent).into_iter().collect(),
189            task: Symbol::new("A1"),
190            output: Symbol::new("A1"),
191            ceiling: Vec::new(),
192            context: Vec::new(),
193            provenance: BridgeProvenance::default(),
194        },
195        body: vec![BridgePart {
196            id: Symbol::new("A1"),
197            kind: Symbol::qualified("bridge", "Return"),
198            payload: answer,
199        }],
200        warrant: None,
201    })?;
202    let report = rx_check(cx, book, &packet, Some(parent))?;
203    if !report.accepted() {
204        return Err(sim_kernel::Error::Eval(format!(
205            "bridge ask reply failed rx check: {:?}",
206            report.obligations
207        )));
208    }
209    Ok(Ok(packet))
210}
211
212fn decode_terminal_answer(
213    cx: &mut Cx,
214    response: &ModelResponse,
215    contract: &OutputContract,
216) -> Result<std::result::Result<Expr, AskFailure>> {
217    let input = match terminal_model_content(response) {
218        Ok(Expr::String(text)) => Input::Text(text.clone()),
219        Ok(Expr::Bytes(bytes)) => Input::Bytes(bytes.clone()),
220        Ok(Expr::Map(_)) => match field(terminal_model_content(response)?, "text") {
221            Some(Expr::String(text)) => Input::Text(text.clone()),
222            _ => {
223                return Ok(Err(AskFailure::Decode {
224                    codec: contract.codec.clone(),
225                    message: "terminal content map must carry text".to_owned(),
226                }));
227            }
228        },
229        Ok(other) => {
230            return Ok(Err(AskFailure::Decode {
231                codec: contract.codec.clone(),
232                message: format!("terminal content must be text or bytes, found {other:?}"),
233            }));
234        }
235        Err(err) => {
236            return Ok(Err(AskFailure::Decode {
237                codec: contract.codec.clone(),
238                message: err.to_string(),
239            }));
240        }
241    };
242    if let Input::Text(text) = &input
243        && let Some(failure) = grammar_check_failure(cx, contract, text)?
244    {
245        return Ok(Err(failure));
246    }
247    match decode_with_codec(cx, &contract.codec, input, ReadPolicy::default()) {
248        Ok(answer) => Ok(Ok(answer)),
249        Err(err) => Ok(Err(AskFailure::Decode {
250            codec: contract.codec.clone(),
251            message: err.to_string(),
252        })),
253    }
254}
255
256fn grammar_check_failure(
257    cx: &mut Cx,
258    contract: &OutputContract,
259    text: &str,
260) -> Result<Option<AskFailure>> {
261    if contract.grammar.is_none()
262        && contract.grammar_dialect.is_none()
263        && contract.grammar_graph.is_none()
264    {
265        return Ok(None);
266    }
267    let Some(shape) = shape_from_contract_expr(&contract.shape_expr) else {
268        return Ok(Some(AskFailure::Shape {
269            expected: format!("{:?}", contract.shape_expr),
270            diagnostics: vec!["unsupported return Shape expression".to_owned()],
271        }));
272    };
273    let decoded = match decode_with_codec(
274        cx,
275        &contract.codec,
276        Input::Text(text.to_owned()),
277        ReadPolicy::default(),
278    ) {
279        Ok(decoded) => decoded,
280        Err(err) => {
281            return Ok(Some(AskFailure::Decode {
282                codec: contract.codec.clone(),
283                message: err.to_string(),
284            }));
285        }
286    };
287    let matched = shape.check_expr(cx, &decoded)?;
288    if matched.accepted {
289        Ok(None)
290    } else {
291        Ok(Some(AskFailure::Shape {
292            expected: format!("{:?}", contract.shape_expr),
293            diagnostics: matched
294                .diagnostics
295                .iter()
296                .map(|diagnostic| diagnostic.message.clone())
297                .collect(),
298        }))
299    }
300}
301
302fn validate_answer(
303    cx: &mut Cx,
304    contract: &OutputContract,
305    answer: &Expr,
306) -> Result<std::result::Result<(), AskFailure>> {
307    let Some(shape) = shape_from_contract_expr(&contract.shape_expr) else {
308        return Ok(Err(AskFailure::Shape {
309            expected: format!("{:?}", contract.shape_expr),
310            diagnostics: vec!["unsupported return Shape expression".to_owned()],
311        }));
312    };
313    let shape_ref = shape_value(Symbol::qualified("bridge", "AskReturn"), shape);
314    let value = cx.factory().expr(answer.clone())?;
315    let matched = check_value_report(cx, &shape_ref, value)?;
316    if matched.accepted {
317        Ok(Ok(()))
318    } else {
319        Ok(Err(AskFailure::Shape {
320            expected: format!("{:?}", contract.shape_expr),
321            diagnostics: matched
322                .diagnostics
323                .iter()
324                .map(|diagnostic| diagnostic.message.clone())
325                .collect(),
326        }))
327    }
328}
329
330fn repair_packet_for_failure(
331    cx: &mut Cx,
332    packet: &BridgePacket,
333    failure: &AskFailure,
334    attempt: u8,
335) -> Result<BridgePacket> {
336    let mut repaired = packet.canonicalized();
337    for part in &mut repaired.body {
338        if part.kind != Symbol::qualified("bridge", "Call") {
339            continue;
340        }
341        let payload = BridgeCallPayload::from_expr(&part.payload)?.with_arg(pack_argument(
342            cx,
343            &format!("repair-{attempt}"),
344            &ask_default_codec(),
345            &failure.to_expr(),
346        )?);
347        part.payload = payload.to_expr();
348        return Ok(repaired);
349    }
350    Err(sim_kernel::Error::Eval(
351        "bridge ask repair requires a Call part".to_owned(),
352    ))
353}
354
355fn symbol_from_name(name: &str) -> Symbol {
356    match name.split_once('/') {
357        Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
358            Symbol::qualified(namespace, name)
359        }
360        _ => Symbol::new(name),
361    }
362}
363
364fn hex_text(bytes: &[u8]) -> String {
365    bytes
366        .iter()
367        .map(|byte| format!("{byte:02x}"))
368        .collect::<String>()
369}