1use sim_codec_bridge::{
2 BridgeBook, BridgeCallArgument, BridgeCallPayload, BridgeHeader, BridgePacket, BridgePart,
3 BridgeProvenance, CallArgumentMedia, canonical_packet_datum, content_id_string, expr_to_packet,
4 packet_content_id, stamp_packet_cid,
5};
6use sim_kernel::{ContentId, Cx, Datum, DatumStore, Error, EvalFabric, Expr, Result, Symbol};
7use sim_lib_agent_runner_core::InjectionFence;
8use sim_lib_bridge::{BridgeObligation, BridgeReport, RepairPolicy, run_bridge, rx_check};
9use sim_value::build::entry;
10
11use crate::normalize::normalize_prose;
12use crate::{CompiledIntent, IntentStatus, assert_return_shape_parses};
13
14const RETURN_SHAPE_PROMPT: &str = "Emit a concrete Return shape expression for the packet output. A missing or unparsable shape is an obligation.";
15const LIFT_TARGET: &str = "model:forge-lift";
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct LiftOptions {
20 pub name: Symbol,
22 pub max_repairs: u8,
24}
25
26impl LiftOptions {
27 pub fn new(name: Symbol) -> Self {
29 Self {
30 name,
31 max_repairs: 1,
32 }
33 }
34}
35
36impl Default for LiftOptions {
37 fn default() -> Self {
38 Self::new(Symbol::qualified("forge", "lifted"))
39 }
40}
41
42pub fn forge_lift_once(
48 cx: &mut Cx,
49 target: &dyn EvalFabric,
50 prose: &str,
51 opts: &LiftOptions,
52) -> Result<CompiledIntent> {
53 let (normalized, source) = normalize_prose(prose)?;
54 let book = BridgeBook::standard();
55 let policy = RepairPolicy::new(opts.max_repairs);
56 let mut repair_report = None;
57 let mut final_report = None;
58
59 for attempt in 0..=policy.max_retries {
60 let request = lift_request_packet(&normalized, &source, repair_report.as_ref(), attempt)?;
61 let (reply, _) = run_bridge(cx, target, &book, request)?;
62 let candidate = stamp_packet_cid(&candidate_packet_from_reply(&reply)?)?;
63 let report = validate_candidate(cx, &book, &candidate)?;
64 if report.accepted() {
65 cx.datum_store_mut()
66 .intern(canonical_packet_datum(&candidate))?;
67 return compiled_intent(opts, source, candidate);
68 }
69 if attempt < policy.max_retries {
70 repair_report = Some(report);
71 } else {
72 final_report = Some(report);
73 }
74 }
75
76 let report = final_report.expect("bounded lift loop records the terminal report");
77 Err(Error::Eval(format!(
78 "forge lift failed after {} attempt(s): {}",
79 usize::from(policy.max_retries) + 1,
80 report_summary(&report)
81 )))
82}
83
84fn lift_request_packet(
85 normalized: &str,
86 source: &ContentId,
87 repair_report: Option<&BridgeReport>,
88 attempt: u8,
89) -> Result<BridgePacket> {
90 let source_expr = Expr::String(normalized.to_owned());
91 let (_, fenced_source) = fenced_expr("source", &source_expr)?;
92 let given = BridgePart {
93 id: Symbol::new("G1"),
94 kind: Symbol::qualified("bridge", "Given"),
95 payload: Expr::Map(vec![
96 entry(
97 "kind",
98 Expr::Symbol(Symbol::qualified("forge", "ProseSource")),
99 ),
100 entry("content-id", Expr::String(content_id_string(source))),
101 entry("prose", Expr::String(fenced_source)),
102 entry(
103 "return-shape-instruction",
104 Expr::String(RETURN_SHAPE_PROMPT.to_owned()),
105 ),
106 ]),
107 };
108
109 let mut call = BridgeCallPayload::new(Symbol::qualified("forge", "lift"))
110 .with_arg(fenced_arg("source", &source_expr)?)
111 .with_arg(fenced_arg(
112 "return-shape-instruction",
113 &Expr::String(RETURN_SHAPE_PROMPT.to_owned()),
114 )?);
115 if let Some(report) = repair_report {
116 call = call.with_arg(fenced_arg(&format!("repair-{attempt}"), &report.to_expr())?);
117 }
118
119 Ok(BridgePacket {
120 header: BridgeHeader {
121 cid: None,
122 move_kind: Symbol::new("request"),
123 from: "sim".to_owned(),
124 to: vec![LIFT_TARGET.to_owned()],
125 role: Symbol::new("implementer"),
126 parents: Vec::new(),
127 task: Symbol::new("C1"),
128 output: Symbol::new("O1"),
129 ceiling: vec![Symbol::qualified("ai", "run")],
130 context: vec![Symbol::new("G1")],
131 provenance: BridgeProvenance::default(),
132 },
133 body: vec![
134 given,
135 BridgePart {
136 id: Symbol::new("C1"),
137 kind: Symbol::qualified("bridge", "Call"),
138 payload: call.to_expr(),
139 },
140 BridgePart {
141 id: Symbol::new("O1"),
142 kind: Symbol::qualified("bridge", "Return"),
143 payload: Expr::Map(vec![
144 entry("codec", Expr::Symbol(Symbol::qualified("codec", "bridge"))),
145 entry("shape", Expr::Symbol(Symbol::qualified("bridge", "Packet"))),
146 ]),
147 },
148 ],
149 warrant: None,
150 })
151}
152
153fn candidate_packet_from_reply(reply: &BridgePacket) -> Result<BridgePacket> {
154 let output = reply
155 .body
156 .iter()
157 .find(|part| part.id == reply.header.output)
158 .ok_or_else(|| {
159 Error::Eval(format!(
160 "forge lift reply output part {} is missing",
161 reply.header.output
162 ))
163 })?;
164 if output.kind != Symbol::qualified("bridge", "Return") {
165 return Err(Error::Eval(format!(
166 "forge lift reply output part {} is {}, expected bridge/Return",
167 output.id, output.kind
168 )));
169 }
170 expr_to_packet(&output.payload)
171}
172
173pub(crate) fn validate_candidate(
174 cx: &mut Cx,
175 book: &BridgeBook,
176 candidate: &BridgePacket,
177) -> Result<BridgeReport> {
178 let mut report = rx_check(cx, book, candidate, None)?;
179 if let Err(err) = assert_return_shape_parses(candidate) {
180 report.obligate(BridgeObligation::repair_packet(
181 format!("body/{}/payload/shape", candidate.header.output),
182 "return Shape does not parse",
183 "parseable Shape expression",
184 err.to_string(),
185 ));
186 }
187 Ok(report)
188}
189
190pub(crate) fn compiled_intent(
191 opts: &LiftOptions,
192 source: ContentId,
193 candidate: BridgePacket,
194) -> Result<CompiledIntent> {
195 Ok(CompiledIntent {
196 name: opts.name.clone(),
197 version: 1,
198 source,
199 packet: packet_content_id(&candidate)?,
200 verifiers: candidate_verifiers(&candidate),
201 probes: Vec::new(),
202 status: IntentStatus::Candidate,
203 compiler_card: None,
204 approval: None,
205 })
206}
207
208fn candidate_verifiers(packet: &BridgePacket) -> Vec<Symbol> {
209 packet
210 .body
211 .iter()
212 .filter(|part| {
213 part.kind == Symbol::qualified("bridge", "Check")
214 || part.kind == Symbol::qualified("bridge", "Evidence")
215 || part.kind == Symbol::qualified("bridge", "Vote")
216 })
217 .map(|part| part.id.clone())
218 .collect()
219}
220
221fn fenced_arg(name: &str, expr: &Expr) -> Result<BridgeCallArgument> {
222 let (content_id, fenced) = fenced_expr(name, expr)?;
223 Ok(BridgeCallArgument::new(
224 Symbol::new(name),
225 Symbol::qualified("codec", "json"),
226 CallArgumentMedia::Text,
227 content_id_string(&content_id),
228 fenced,
229 ))
230}
231
232fn fenced_expr(label: &str, expr: &Expr) -> Result<(ContentId, String)> {
233 let content_id = content_id_for_expr(expr)?;
234 let body = sim_codec_json::expr_to_json(expr).to_string();
235 let fence = InjectionFence::for_content(&content_id);
236 Ok((content_id, fence.wrap(label, &body)))
237}
238
239pub(crate) fn content_id_for_expr(expr: &Expr) -> Result<ContentId> {
240 Datum::try_from(expr.clone())?.content_id()
241}
242
243pub(crate) fn report_summary(report: &BridgeReport) -> String {
244 if report.obligations.is_empty() {
245 return "no obligations".to_owned();
246 }
247 report
248 .obligations
249 .iter()
250 .map(|obligation| {
251 format!(
252 "{}: {} (expected {}, actual {})",
253 obligation.path, obligation.reason, obligation.expected, obligation.actual
254 )
255 })
256 .collect::<Vec<_>>()
257 .join("; ")
258}