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
21pub fn ask_default_codec() -> Symbol {
23 Symbol::qualified("codec", "json")
24}
25
26pub 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
37pub 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
91pub 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
96pub 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}