1use sim_codec_bridge::{
2 BridgeBook, BridgeFramePayload, BridgeHeader, BridgePacket, BridgePart, BridgeProvenance,
3 FrameHoleKind, canonical_packet_datum, content_id_string, packet_content_id, packet_to_expr,
4 stamp_packet_cid,
5};
6use sim_kernel::{
7 Consistency, ContentId, Cx, DatumStore, Error, EvalFabric, EvalMode, EvalRequest, Expr, Result,
8 Symbol,
9};
10use sim_lib_agent_runner_core::{
11 ModelRequest, ModelResponse, OUTPUT_GRAMMAR_EXTRA, OUTPUT_GRAMMAR_REQUIRED_EXTRA,
12 RETURN_CODEC_EXTRA, RETURN_SHAPE_EXTRA, terminal_model_content,
13};
14use sim_lib_bridge::{
15 BridgeObligation, BridgeReport, FrontierMenu, effective_caps, frontier, rx_check,
16};
17use sim_value::{access::field, build::entry};
18
19use crate::frame_propose::propose_frame;
20use crate::lift::{compiled_intent, report_summary, validate_candidate};
21use crate::normalize::normalize_prose;
22use crate::{CompiledIntent, LiftOptions};
23
24const FRONTIER_TARGET: &str = "model:forge-frontier";
25const MAX_FRONTIER_ATTEMPTS: usize = 16;
26
27pub fn forge_lift_frontier(
30 cx: &mut Cx,
31 target: &dyn EvalFabric,
32 prose: &str,
33 opts: &LiftOptions,
34) -> Result<CompiledIntent> {
35 let (normalized, source) = normalize_prose(prose)?;
36 let book = BridgeBook::standard();
37 let mut packet = seed_packet(&normalized, &source)?;
38 let mut row_index = 0usize;
39 let mut obligations = Vec::new();
40
41 for _ in 0..MAX_FRONTIER_ATTEMPTS {
42 if let Some(intent) = completed_intent(cx, &book, opts, source.clone(), &packet)? {
43 return Ok(intent);
44 }
45
46 let expected = expected_next_part(&packet)?;
47 let menu = frontier(cx, &packet)?;
48 let row = request_frontier_part(
49 cx,
50 target,
51 &packet,
52 &menu,
53 &expected,
54 row_index,
55 &obligations,
56 )?;
57 let report = validate_frontier_part(cx, &book, &packet, &menu, &expected, row_index, &row)?;
58 if !report.accepted() {
59 obligations = report.obligations;
60 continue;
61 }
62
63 packet.body.push(row.part);
64 packet.header.cid = None;
65 obligations.clear();
66 row_index += 1;
67 }
68
69 Err(Error::Eval(format!(
70 "forge frontier lift failed after {MAX_FRONTIER_ATTEMPTS} attempt(s): {}",
71 report_summary(&BridgeReport {
72 packet_cid: packet
73 .header
74 .cid
75 .clone()
76 .unwrap_or_else(|| "unstamped".to_owned()),
77 accepted_parts: Vec::new(),
78 rejected_parts: Vec::new(),
79 obligations,
80 })
81 )))
82}
83
84fn seed_packet(normalized: &str, source: &ContentId) -> Result<BridgePacket> {
85 Ok(BridgePacket {
86 header: BridgeHeader {
87 cid: None,
88 move_kind: Symbol::new("request"),
89 from: "sim".to_owned(),
90 to: vec![FRONTIER_TARGET.to_owned()],
91 role: Symbol::new("implementer"),
92 parents: Vec::new(),
93 task: Symbol::new("T1"),
94 output: Symbol::new("O1"),
95 ceiling: vec![Symbol::qualified("ai", "run")],
96 context: vec![Symbol::new("G1")],
97 provenance: BridgeProvenance::default(),
98 },
99 body: vec![BridgePart {
100 id: Symbol::new("G1"),
101 kind: Symbol::qualified("bridge", "Given"),
102 payload: Expr::Map(vec![
103 entry(
104 "kind",
105 Expr::Symbol(Symbol::qualified("forge", "ProseSource")),
106 ),
107 entry("content-id", Expr::String(content_id_string(source))),
108 entry("prose", Expr::String(normalized.to_owned())),
109 ]),
110 }],
111 warrant: None,
112 })
113}
114
115fn completed_intent(
116 cx: &mut Cx,
117 book: &BridgeBook,
118 opts: &LiftOptions,
119 source: ContentId,
120 packet: &BridgePacket,
121) -> Result<Option<CompiledIntent>> {
122 if !packet.body.iter().any(|part| part.id == packet.header.task)
123 || !packet
124 .body
125 .iter()
126 .any(|part| part.id == packet.header.output)
127 {
128 return Ok(None);
129 }
130 let stamped = stamp_packet_cid(packet)?;
131 let report = validate_candidate(cx, book, &stamped)?;
132 if report.accepted() {
133 cx.datum_store_mut()
134 .intern(canonical_packet_datum(&stamped))?;
135 Ok(Some(compiled_intent(opts, source, stamped)?))
136 } else {
137 Ok(None)
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142enum ExpectedPart {
143 TaskFrame,
144 Return,
145}
146
147impl ExpectedPart {
148 fn id(self) -> Symbol {
149 match self {
150 Self::TaskFrame => Symbol::new("T1"),
151 Self::Return => Symbol::new("O1"),
152 }
153 }
154
155 fn kind(self) -> Symbol {
156 match self {
157 Self::TaskFrame => Symbol::qualified("bridge", "Frame"),
158 Self::Return => Symbol::qualified("bridge", "Return"),
159 }
160 }
161
162 fn label(self) -> &'static str {
163 match self {
164 Self::TaskFrame => "task Frame",
165 Self::Return => "Return",
166 }
167 }
168
169 fn to_expr(self) -> Expr {
170 Expr::Map(vec![
171 entry("id", Expr::Symbol(self.id())),
172 entry("kind", Expr::Symbol(self.kind())),
173 entry("label", Expr::String(self.label().to_owned())),
174 ])
175 }
176}
177
178fn expected_next_part(packet: &BridgePacket) -> Result<ExpectedPart> {
179 if !packet.body.iter().any(|part| part.id == packet.header.task) {
180 return Ok(ExpectedPart::TaskFrame);
181 }
182 if !packet
183 .body
184 .iter()
185 .any(|part| part.id == packet.header.output)
186 {
187 return Ok(ExpectedPart::Return);
188 }
189 Err(Error::Eval(
190 "forge frontier packet has task and return but is still incomplete".to_owned(),
191 ))
192}
193
194struct FrontierPartRow {
195 head: Symbol,
196 part: BridgePart,
197}
198
199fn request_frontier_part(
200 cx: &mut Cx,
201 target: &dyn EvalFabric,
202 packet: &BridgePacket,
203 menu: &FrontierMenu,
204 expected: &ExpectedPart,
205 row_index: usize,
206 obligations: &[BridgeObligation],
207) -> Result<FrontierPartRow> {
208 let request = frontier_part_request(cx, packet, menu, expected, row_index, obligations)?;
209 let reply = target.realize(cx, request)?;
210 decode_frontier_part_reply(cx, reply)
211}
212
213fn frontier_part_request(
214 cx: &mut Cx,
215 packet: &BridgePacket,
216 menu: &FrontierMenu,
217 expected: &ExpectedPart,
218 row_index: usize,
219 obligations: &[BridgeObligation],
220) -> Result<EvalRequest> {
221 let row_path = format!("frontier/rows/{row_index}");
222 let frame_proposal = if matches!(expected, ExpectedPart::TaskFrame) {
223 frame_proposal_expr(cx, packet)?
224 } else {
225 None
226 };
227 let mut body_fields = vec![
228 entry(
229 "mode",
230 Expr::Symbol(Symbol::qualified("forge", "frontier-part")),
231 ),
232 entry("row-path", Expr::String(row_path.clone())),
233 entry("expected-part", expected.to_expr()),
234 entry("head-menu", menu.heads.clone()),
235 entry("slot-menu", slot_menu_expr(menu)),
236 entry("partial-packet", packet_to_expr(packet)),
237 entry("obligations", obligations_expr(obligations)),
238 ];
239 if let Some(proposal) = &frame_proposal {
240 body_fields.push(entry("frame-proposal", proposal.clone()));
241 }
242
243 let mut model = ModelRequest::new(Expr::Map(body_fields), Vec::new());
244 model.extra.push(entry(
245 "forge-mode",
246 Expr::Symbol(Symbol::qualified("forge", "frontier-part")),
247 ));
248 model
249 .extra
250 .push(entry("forge-row-path", Expr::String(row_path)));
251 model
252 .extra
253 .push(entry("forge-expected-part", expected.to_expr()));
254 model
255 .extra
256 .push(entry("forge-frontier-heads", menu.heads.clone()));
257 model
258 .extra
259 .push(entry("forge-frontier-slots", slot_menu_expr(menu)));
260 model.extra.push(entry(
261 "forge-frontier-grammar",
262 Expr::String(menu.grammar.clone()),
263 ));
264 if !obligations.is_empty() {
265 model
266 .extra
267 .push(entry("forge-obligations", obligations_expr(obligations)));
268 }
269 if let Some(proposal) = frame_proposal {
270 model.extra.push(entry("forge-frame-proposal", proposal));
271 }
272 model.extra.push(entry(
273 RETURN_CODEC_EXTRA,
274 Expr::Symbol(Symbol::qualified("codec", "json")),
275 ));
276 model
277 .extra
278 .push(entry(RETURN_SHAPE_EXTRA, row_shape_expr(menu, expected)));
279 model
280 .extra
281 .push(entry(OUTPUT_GRAMMAR_EXTRA, Expr::String(row_grammar(menu))));
282 model
283 .extra
284 .push(entry(OUTPUT_GRAMMAR_REQUIRED_EXTRA, Expr::Bool(true)));
285
286 Ok(EvalRequest {
287 expr: Expr::from(model),
288 result_shape: None,
289 required_capabilities: effective_caps(cx, packet)?.iter().cloned().collect(),
290 deadline: None,
291 consistency: Consistency::default(),
292 mode: EvalMode::default(),
293 answer_limit: None,
294 stream_buffer: None,
295 stream: false,
296 trace: false,
297 })
298}
299
300fn decode_frontier_part_reply(
301 cx: &mut Cx,
302 reply: sim_kernel::EvalReply,
303) -> Result<FrontierPartRow> {
304 let response = ModelResponse::try_from(reply.value.object().as_expr(cx)?)?;
305 let content = terminal_model_content(&response)?;
306 let expr = field(content, "row").unwrap_or(content);
307 let head = match field(expr, "head") {
308 Some(Expr::Symbol(symbol)) => symbol.clone(),
309 Some(Expr::String(text)) => Symbol::new(text.as_str()),
310 _ => {
311 return Err(Error::Eval(
312 "forge frontier row is missing symbolic head".to_owned(),
313 ));
314 }
315 };
316 let part = part_from_expr(
317 field(expr, "part")
318 .ok_or_else(|| Error::Eval("forge frontier row is missing part".to_owned()))?,
319 )?;
320 Ok(FrontierPartRow { head, part })
321}
322
323fn part_from_expr(expr: &Expr) -> Result<BridgePart> {
324 let id = match field(expr, "id") {
325 Some(Expr::Symbol(symbol)) => symbol.clone(),
326 _ => return Err(Error::Eval("forge frontier part is missing id".to_owned())),
327 };
328 let kind = match field(expr, "kind") {
329 Some(Expr::Symbol(symbol)) => symbol.clone(),
330 _ => {
331 return Err(Error::Eval(
332 "forge frontier part is missing kind".to_owned(),
333 ));
334 }
335 };
336 let payload = field(expr, "payload")
337 .ok_or_else(|| Error::Eval("forge frontier part is missing payload".to_owned()))?
338 .clone();
339 Ok(BridgePart { id, kind, payload })
340}
341
342fn validate_frontier_part(
343 cx: &mut Cx,
344 book: &BridgeBook,
345 packet: &BridgePacket,
346 menu: &FrontierMenu,
347 expected: &ExpectedPart,
348 row_index: usize,
349 row: &FrontierPartRow,
350) -> Result<BridgeReport> {
351 let mut report = BridgeReport::new(packet_report_cid(packet));
352 let head_choices = head_choices(menu);
353 if !head_choices.contains(&row.head) {
354 report.obligate(BridgeObligation::new(
355 format!("frontier/rows/{row_index}/head"),
356 "frontier row selected an off-menu head",
357 head_choices
358 .iter()
359 .map(Symbol::as_qualified_str)
360 .collect::<Vec<_>>()
361 .join(", "),
362 row.head.as_qualified_str(),
363 head_choices
364 .iter()
365 .map(Symbol::as_qualified_str)
366 .collect::<Vec<_>>(),
367 ));
368 return Ok(report);
369 }
370
371 if row.part.id != expected.id() || row.part.kind != expected.kind() {
372 report.obligate(BridgeObligation::new(
373 format!("frontier/rows/{row_index}/part"),
374 "frontier row supplied the wrong part slot",
375 format!("{} {}", expected.id(), expected.kind()),
376 format!("{} {}", row.part.id, row.part.kind),
377 vec![expected.label().to_owned()],
378 ));
379 return Ok(report);
380 }
381
382 if matches!(expected, ExpectedPart::TaskFrame) && data_degrade_frame(book, &row.part.payload) {
383 report.obligate(BridgeObligation::new(
384 format!("frontier/rows/{row_index}/part"),
385 "frontier row fell through to data because no frame matched",
386 "approved typed bridge/Frame or candidate FrameSpec proposal",
387 "prose data frame",
388 vec!["propose-frame".to_owned()],
389 ));
390 return Ok(report);
391 }
392
393 if matches!(expected, ExpectedPart::TaskFrame)
394 && let Err(err) = book.frames.validate_payload(&row.part.payload)
395 {
396 report.obligate(BridgeObligation::new(
397 format!("frontier/rows/{row_index}/part"),
398 "frontier row part did not satisfy the expected Shape",
399 "registered bridge/Frame payload",
400 err.to_string(),
401 vec![expected.label().to_owned()],
402 ));
403 return Ok(report);
404 }
405
406 let mut candidate = packet.clone();
407 candidate.body.push(row.part.clone());
408 candidate.header.cid = None;
409 let candidate_report = if matches!(expected, ExpectedPart::Return) {
410 validate_candidate(cx, book, &candidate)?
411 } else {
412 rx_check(cx, book, &candidate, None)?
413 };
414 let part_id = row.part.id.as_qualified_str();
415 let part_path = format!("body/{part_id}");
416 let part_obligations = candidate_report
417 .obligations
418 .iter()
419 .filter(|obligation| obligation.path.starts_with(&part_path))
420 .cloned()
421 .collect::<Vec<_>>();
422 if candidate_report.accepted_parts.contains(&part_id) && part_obligations.is_empty() {
423 report.accept(&row.part.id);
424 } else {
425 report.reject(&row.part.id);
426 if part_obligations.is_empty() {
427 report.obligate(BridgeObligation::new(
428 format!("frontier/rows/{row_index}/part"),
429 "frontier row part did not satisfy the expected Shape",
430 expected.label(),
431 format!("{} {}", row.part.id, row.part.kind),
432 vec![expected.label().to_owned()],
433 ));
434 } else {
435 for obligation in part_obligations {
436 report.obligate(row_obligation(row_index, obligation));
437 }
438 }
439 }
440 Ok(report)
441}
442
443fn packet_report_cid(packet: &BridgePacket) -> String {
444 packet
445 .header
446 .cid
447 .clone()
448 .unwrap_or_else(|| match packet_content_id(packet) {
449 Ok(id) => content_id_string(&id),
450 Err(_) => "unhashable".to_owned(),
451 })
452}
453
454fn row_obligation(row_index: usize, obligation: BridgeObligation) -> BridgeObligation {
455 BridgeObligation::new(
456 format!("frontier/rows/{row_index}/part"),
457 obligation.reason,
458 obligation.expected,
459 obligation.actual,
460 obligation.repair_menu,
461 )
462}
463
464fn head_choices(menu: &FrontierMenu) -> Vec<Symbol> {
465 match field(&menu.heads, "choices") {
466 Some(Expr::Vector(items)) | Some(Expr::List(items)) => items
467 .iter()
468 .filter_map(|item| match item {
469 Expr::Symbol(symbol) => Some(symbol.clone()),
470 _ => None,
471 })
472 .collect(),
473 _ => Vec::new(),
474 }
475}
476
477fn data_degrade_frame(book: &BridgeBook, payload: &Expr) -> bool {
478 let Ok(payload) = BridgeFramePayload::from_expr(payload) else {
479 return false;
480 };
481 let Ok(spec) = book.frames.require_spec(&payload.frame) else {
482 return false;
483 };
484 spec.holes
485 .iter()
486 .any(|hole| matches!(hole.kind, FrameHoleKind::Prose))
487}
488
489fn frame_proposal_expr(cx: &mut Cx, packet: &BridgePacket) -> Result<Option<Expr>> {
490 let Some(prose) = packet_source_prose(packet) else {
491 return Ok(None);
492 };
493 Ok(Some(propose_frame(cx, prose)?.to_expr()))
494}
495
496fn packet_source_prose(packet: &BridgePacket) -> Option<&str> {
497 packet.body.iter().find_map(|part| {
498 if part.id != Symbol::new("G1") {
499 return None;
500 }
501 match field(&part.payload, "prose") {
502 Some(Expr::String(prose)) => Some(prose.as_str()),
503 _ => None,
504 }
505 })
506}
507
508fn row_shape_expr(menu: &FrontierMenu, expected: &ExpectedPart) -> Expr {
509 Expr::Map(vec![
510 entry(
511 "shape",
512 Expr::Symbol(Symbol::qualified("forge", "FrontierPart")),
513 ),
514 entry("head", menu.heads.clone()),
515 entry("expected-part", expected.to_expr()),
516 ])
517}
518
519fn row_grammar(menu: &FrontierMenu) -> String {
520 format!(
521 "{{\"type\":\"object\",\"required\":[\"head\",\"part\"],\"properties\":{{\"head\":{},\"part\":{{\"type\":\"object\",\"required\":[\"id\",\"kind\",\"payload\"]}}}}}}",
522 menu.grammar
523 )
524}
525
526fn slot_menu_expr(menu: &FrontierMenu) -> Expr {
527 Expr::Vector(
528 menu.slots
529 .iter()
530 .map(|(slot, shape)| {
531 Expr::Map(vec![
532 entry("slot", Expr::String(slot.clone())),
533 entry("shape", shape.clone()),
534 ])
535 })
536 .collect(),
537 )
538}
539
540fn obligations_expr(obligations: &[BridgeObligation]) -> Expr {
541 Expr::Vector(obligations.iter().map(BridgeObligation::to_expr).collect())
542}