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