formal-ai 0.274.0

Formal symbolic AI implementation with OpenAI-compatible APIs
//! Fifth agentic recipe — driving Formal AI to *run its own self-healing loop*
//! and store the resulting repair case in our data (issue #558).
//!
//! Issue #558 ("Auto learning") asks the system, when it cannot answer an input,
//! to reason about the failure, map it onto the source that would change, propose a
//! fix, gate it against a benchmark, and — only with human approval — promote the
//! lesson. [`crate::self_healing`] realises that closed loop as an auditable,
//! proposal-only [`RepairCase`](crate::self_healing::RepairCase). This module makes
//! the loop reachable *through the agentic interface*: an external agent CLI
//! (`Codex`, `OpenCode`, `Gemini`, `Agent CLI`) — or the in-repo driver — asks Formal AI
//! to self-heal, and the deterministic planner walks a write → verify → final
//! recipe that emits the repair case as Links Notation, exactly like the self-AST
//! and diagram recipes emit their self-inspection documents.
//!
//! Nothing here applies a change: the recipe's product is the reviewable repair
//! case, so the "recompile and reattach" guardrail (observable, testable,
//! reversible, human-approved) is preserved. Neural inference stays a NON-GOAL —
//! the document is a deterministic function of the canonical failure case.

use std::sync::OnceLock;

use crate::self_healing::{canonical_case, RepairCase};

/// The workspace path the planner writes the generated repair-case document to.
pub const SELF_HEAL_PATH: &str = "self-healing-case.lino";

/// The canonical repair case, computed once per process.
///
/// Building it parses a real module through the CST/AST engine (a source → links
/// round-trip), which is deliberately non-trivial; the recipe touches it several
/// times per run (planner write step, verify step, final answer) and a server may
/// serve it repeatedly, so memoising keeps the loop responsive without changing its
/// deterministic result.
fn cached_case() -> &'static RepairCase {
    static CASE: OnceLock<RepairCase> = OnceLock::new();
    CASE.get_or_init(canonical_case)
}

/// A *differently worded* request for the self-healing recipe.
///
/// The router recognises the intent from the words, not a hardcoded string.
/// Deliberately avoids the self-AST keywords ("cst/ast", "abstract-syntax",
/// "reason about itself") so the two self-inspection recipes never collide.
pub const SELF_HEAL_TASK: &str =
    "When you cannot answer an input, run your self-healing loop: reason about the \
     failure, map it onto the source that would change with a source-to-links \
     round-trip, learn a benchmark-gated lesson, and record the repair case in \
     Links Notation for human approval.";

/// Keywords that mark a user turn as the self-healing recipe.
const SELF_HEAL_KEYWORDS: [&str; 6] = [
    "self-healing",
    "self-heal",
    "auto-learning",
    "auto learning",
    "repair case",
    "repair loop",
];

/// Whether `prompt` asks the system to run its self-healing / auto-learning loop
/// (issue #558).
#[must_use]
pub fn is_self_heal_task(prompt: &str) -> bool {
    let lower = prompt.to_lowercase();
    // A dedicated keyword, or the phrase pairing a failure with self-repair, routes
    // here. Kept narrow so ordinary "fix this" requests do not match.
    SELF_HEAL_KEYWORDS
        .iter()
        .any(|keyword| lower.contains(keyword))
        || ((lower.contains("cannot answer")
            || lower.contains("can't answer")
            || lower.contains("failure"))
            && (lower.contains("heal") || lower.contains("learn a")))
}

/// Render the self-healing repair-case document (Links Notation) for the canonical
/// worked case. Deterministic and ends with exactly one trailing newline.
///
/// The output is what the Agent CLI writes to [`SELF_HEAL_PATH`] and what
/// `data/meta/self-healing-case.lino` is committed as, asserted byte-for-byte in
/// the issue-#558 tests.
#[must_use]
pub fn render_document() -> String {
    format!("{}\n", cached_case().links_notation().trim_end())
}

/// The canonical worked repair case backing the recipe. Exposed so tests can assert
/// the loop reached a reviewable, human-gated outcome without rebuilding it.
#[must_use]
pub fn case() -> RepairCase {
    cached_case().clone()
}

/// The self-contained final answer: a natural-language summary plus the generated
/// repair-case document inline.
#[must_use]
pub fn final_answer(document: &str) -> String {
    let case = cached_case();
    format!(
        "Ran the self-healing loop on an input the system could not answer directly: reasoned about \
         the failure trace, mapped it onto {module} with a verified source-to-links round-trip, \
         learned a benchmark-gated lesson, and recorded an auditable repair case ({outcome}). \
         Nothing was applied — adoption stays a human-approved review step.\n\n\
         Generated document ({SELF_HEAL_PATH}):\n\n{document}",
        module = case.source_round_trip.module_path,
        outcome = case.outcome.slug(),
        document = document.trim_end(),
    )
}