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