Skip to main content

sim_lib_forge/
lift.rs

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/// Options for one FORGE prose-to-packet lift.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct LiftOptions {
20    /// Stable symbolic name assigned to the compiled intent artifact.
21    pub name: Symbol,
22    /// Maximum repair attempts after the first candidate. Values above 2 are clamped.
23    pub max_repairs: u8,
24}
25
26impl LiftOptions {
27    /// Builds lift options with the default bounded repair count.
28    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
42/// Compiles prose into a structurally checked candidate BRIDGE packet artifact.
43///
44/// The lift itself is a BRIDGE ASK exchange: the prose is supplied as fenced
45/// data, the outer ASK return shape is `bridge/Packet`, and the returned packet
46/// is accepted only after local return-shape parsing and `rx_check`.
47pub 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}