Skip to main content

formal_ai/
solver.rs

1//! Universal problem-solving algorithm.
2//!
3//! Every prompt the assistant ever receives walks the same 11-step loop
4//! described in `VISION.md` and `REQUIREMENTS.md`:
5//!
6//! 1. **Impulse** — append the raw user message to the event log.
7//! 2. **Formalization** — derive an intent (the smallest formal requirement).
8//! 3. **Context** — detect the surface language and mode flags.
9//! 4. **History lookup** — search local Links Notation knowledge first.
10//! 5. **Decomposition** — split composite prompts into sub-impulses.
11//! 6. **TDD-style validation** — when the requirement implies a constraint,
12//!    generate at least one executable check and record the validation event.
13//! 7. **Solution synthesis** — gather candidate answers.
14//! 8. **Combination** — pick the smallest sufficient candidate.
15//! 9. **Verification** — when execution is implied, surface execution events.
16//! 10. **Simplification** — collapse meaning-preserving redundancies.
17//! 11. **Documentation** — emit the user-facing reply with a `trace:` pointer.
18//!
19//! The solver is deterministic for a given [`SolverConfig`] and impulse: the
20//! same input always produces the same event log and the same answer. Any
21//! "random guessing" is seeded from the content-addressed impulse id so the
22//! deterministic-projection invariant from `NON-GOALS.md` is preserved.
23
24use crate::coding::guidance as coding_guidance;
25use crate::engine::{
26    answer_links_notation, language_aware_answer_for, language_aware_intent_for, normalize_prompt,
27    response_link_for_intent, stable_id, ExecutionRecipe, SelectedRule, SymbolicAnswer,
28};
29use crate::event_log::{build_evidence_links, EventLog};
30use crate::intent_formalization::{
31    record_intent_formalization, recover_write_program_rule, rewrite_bare_program_coreference_rule,
32    select_rule_for_intent, IntentFormalizationCache, IntentFormalizationCacheEntry,
33};
34use crate::language::{detect as detect_language, Language};
35use crate::probability::{ProbabilityDecisionPolicy, ProbabilityStore};
36use crate::rule_synthesis::try_construct_unknown_rule;
37use crate::seed;
38use crate::solver_diagnostics::append_diagnostic_trace;
39use crate::solver_formalization::{record_formalization, record_formalization_selection};
40use crate::solver_handler_oracle::try_unsupported_write_program;
41use crate::solver_handlers::{finalize_simple, try_agent_workspace_task, try_program_blueprint};
42use crate::solver_helpers::{
43    confidence_for, is_agent_opt_in, is_agent_request, is_cache_flush_request,
44    is_destructive_action, is_forget_request, is_inappropriate_content, is_unbounded_autonomy,
45    is_unbounded_loop, record_candidates, record_decomposition, record_validation,
46    requires_external_lookup,
47};
48use crate::solver_synthesis::try_synthesize_from_sub_results;
49use crate::solver_unknown_reasoning::{answer_unknown_prompt, UnknownReasoningConfig};
50use crate::translation::{
51    formalize_prompt_candidates, select_formalization_candidate_with_policy, FormalizationDecision,
52    FormalizationSelectionConfig,
53};
54
55/// Runtime surface where the solver is embedded.
56///
57/// Self-awareness answers use this to avoid claiming browser-only, CLI-only, or
58/// server-only affordances in the wrong environment.
59#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
60pub enum ExecutionSurface {
61    #[default]
62    RustLibrary,
63    Cli,
64    HttpServer,
65    Browser,
66    Telegram,
67    DockerMicroservice,
68}
69
70impl ExecutionSurface {
71    #[must_use]
72    pub const fn slug(self) -> &'static str {
73        match self {
74            Self::RustLibrary => "rust_library",
75            Self::Cli => "cli",
76            Self::HttpServer => "http_server",
77            Self::Browser => "browser",
78            Self::Telegram => "telegram",
79            Self::DockerMicroservice => "docker_microservice",
80        }
81    }
82
83    pub(crate) fn from_env_value(raw: &str) -> Option<Self> {
84        match raw.trim().to_ascii_lowercase().as_str() {
85            "rust" | "rust_library" | "library" | "lib" => Some(Self::RustLibrary),
86            "cli" | "terminal" | "shell" => Some(Self::Cli),
87            "http" | "http_server" | "server" | "api" => Some(Self::HttpServer),
88            "browser" | "web" | "wasm" | "demo" => Some(Self::Browser),
89            "telegram" | "telegram_bot" | "bot" => Some(Self::Telegram),
90            "docker" | "docker_microservice" | "container" => Some(Self::DockerMicroservice),
91            _ => None,
92        }
93    }
94}
95
96/// How the composite-program `blueprint` synthesizer
97/// turns its annotated recipe template into the program shown to the user.
98///
99/// Issue #340 asked the engine to "try all directions" of program synthesis and
100/// let the user switch between them. A blueprint recipe is stored as an annotated
101/// template whose optional sub-tasks (error handling, comments, …) are wrapped in
102/// `region:<capability>` markers; every emitted program is a *projection* of that
103/// template (never the raw, marker-bearing string — markers are always stripped).
104/// This knob selects which projection to emit:
105///
106/// - [`Composed`](Self::Composed) (default, the most promising direction): the
107///   program is assembled from exactly the capabilities the request decomposed
108///   into — optional regions whose capability the prompt did not ask for are
109///   dropped, and when comments were not requested the documentation is stripped
110///   too. The same recipe therefore yields genuinely different programs for
111///   different requests, which is the honest, anti-memoization demonstration that
112///   the code is composed from the decomposition (`NON-GOALS.md`).
113/// - [`Documented`](Self::Documented): always emit the fully documented program
114///   with every optional region present, regardless of which sub-tasks the
115///   request named. Useful as a stable reference and for users who want the
116///   maximal annotated program every time.
117#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
118pub enum BlueprintComposition {
119    /// Project the program from the detected capabilities (default).
120    #[default]
121    Composed,
122    /// Always emit the fully documented program with every region present.
123    Documented,
124}
125
126impl BlueprintComposition {
127    /// Stable slug used in the event log and the demo preference value.
128    #[must_use]
129    pub const fn slug(self) -> &'static str {
130        match self {
131            Self::Composed => "composed",
132            Self::Documented => "documented",
133        }
134    }
135
136    /// Parse a configuration value (env var or demo preference). Accepts the
137    /// canonical slugs plus a few intuitive aliases; returns `None` for anything
138    /// unrecognized so callers keep the default.
139    #[must_use]
140    pub fn from_value(raw: &str) -> Option<Self> {
141        match raw.trim().to_ascii_lowercase().as_str() {
142            "composed" | "compose" | "projection" | "project" | "decomposed" => {
143                Some(Self::Composed)
144            }
145            "documented" | "document" | "full" | "verbatim" | "curated" => Some(Self::Documented),
146            _ => None,
147        }
148    }
149}
150
151/// Runtime configuration for the universal solver.
152///
153/// These knobs control the universal loop's tradeoffs and let the same engine
154/// be tuned per surface (CLI, HTTP, Telegram) or per user. The default
155/// configuration matches the bounded-chat, offline-friendly stance from
156/// `GOALS.md` so the engine is safe to embed without further setup.
157#[allow(clippy::struct_excessive_bools)]
158#[derive(Debug, Clone, Copy, PartialEq)]
159pub struct SolverConfig {
160    /// `0.0` = always ask a clarifying question, `1.0` = always guess.
161    ///
162    /// When this is high the engine commits to its best interpretation of the
163    /// prompt, shows that interpretation, translates the claim into the
164    /// chosen formal system, and executes the proof. When it is low the
165    /// engine stays literal and avoids speculative reductions.
166    pub guess_probability: f32,
167    /// `0.0` = stay action-only, `1.0` = always invite the user to refine the
168    /// proof inputs before final execution.
169    ///
170    /// Independent of `guess_probability`. When this is high the proof engine
171    /// appends a "Clarifying questions" section listing every input the user
172    /// still has to confirm (axiom set, definitions, proof technique) so the
173    /// final research execution is unambiguous.
174    pub follow_up_probability: f32,
175    /// `0.0` = ignore surrounding context, `1.0` = use all available context.
176    pub context_sensitivity: f32,
177    /// `0.0` = accept any phrasing, `1.0` = demand fully formal phrasing.
178    pub questioning_rigor: f32,
179    /// `0.0` = deterministic projection, `1.0` = allow maximum variation.
180    pub temperature: f32,
181    /// Hard upper bound on recursive sub-impulse expansion.
182    pub max_decomposition_depth: u8,
183    /// Which directions of the meta core's recursive reasoning are traced
184    /// (issue #559): `Down` (default) reasons about the downward decomposition
185    /// only and reproduces the pre-knob trace exactly; `Up`/`Both` additionally
186    /// trace the upward construction pass. Trace-only either way (R13).
187    pub recursion_mode: crate::meta_construction::RecursionMode,
188    /// Whether the meta core records the method-selection trace (issue #559,
189    /// R339): `Off` (default) records nothing; `Record` names the method the
190    /// registry resolves for every atomic leaf, or marks the leaf unresolved.
191    /// The registry is the sole dispatch authority (R344), so there is no
192    /// legacy baseline to compare against. Trace-only in either mode (R13).
193    pub selection_mode: crate::selection::SelectionMode,
194    /// Whether the meta core records the skill-accumulation ledger (issue #559,
195    /// R342): `Off` (default) records nothing; `Accumulate` distills each satisfied
196    /// need into a proposed reusable skill and each blocked need into a curriculum
197    /// item. Proposal-only — no skill is auto-promoted without review — and
198    /// trace-only either way (R13/C3).
199    pub skill_mode: crate::skill_ledger::SkillMode,
200    /// Whether agent mode is opted in. Off by default.
201    pub agent_mode: bool,
202    /// Whether diagnostic links are echoed inside the user-facing reply.
203    pub diagnostic_mode: bool,
204    /// When true, the solver must not perform any external lookup.
205    pub offline: bool,
206    /// Time-to-live for cached external sources, in seconds.
207    pub cache_ttl_seconds: u64,
208    /// When true, plain definition prompts such as "What is IIR?" use
209    /// cross-language definition fusion before falling back to concept lookup.
210    pub definition_fusion_by_default: bool,
211    /// When true, repository/project questions prefer known projects from
212    /// Link Assistant, Link Foundation, and `LinksPlatform` before showing the
213    /// generic multi-host repository lookup path.
214    pub associative_project_promotion: bool,
215    /// Embedding surface used for environment-aware self-description.
216    pub execution_surface: ExecutionSurface,
217    /// How composite-program blueprints (issue #340) project their annotated
218    /// recipe template into the program shown to the user.
219    pub blueprint_composition: BlueprintComposition,
220    /// Interpretable decision-policy knobs (`CU`/`TU`/`TC`/`SS`) from
221    /// arXiv:2605.00940 that govern how symbolic probability evidence ranks
222    /// candidates. The default is the paper's recommended baseline, which keeps
223    /// the additive exact-evidence behaviour the solver shipped before the
224    /// policy existed, so every existing surface is unaffected unless it opts in.
225    pub probability_policy: ProbabilityDecisionPolicy,
226    /// Response language forced onto every localizable handler for one replay
227    /// (issue #556). `None` is the normal case: each handler renders in the
228    /// language detected from the prompt. When a response-language follow-up
229    /// ("I do not understand English, write in Russian") replays the previous
230    /// request through the whole solver, it sets this to the requested ISO
231    /// 639-1 code so *every* answer family that can localize — concept lookup,
232    /// repository/project lookup, … — re-renders in that language rather than
233    /// only a single hardcoded handler. It also serves as the recursion guard:
234    /// a solve whose config already carries a forced language never fires the
235    /// follow-up again.
236    pub forced_response_language: Option<&'static str>,
237    /// Compute budget for the step-7 random/evolutionary search stage (issue
238    /// #662), counted in candidate evaluations. When reuse and rule reasoning
239    /// produce no candidate for a recognized search problem, the solver spends
240    /// up to this many evaluations combining known parts against the generated
241    /// tests before falling back to the honest unknown-reasoning reply. `0`
242    /// disables the search entirely; the default is intentionally small. The
243    /// stream is seeded from the impulse content hash, so the stage stays
244    /// deterministic for a given config per the `VISION.md` contract.
245    pub compute_budget: u32,
246}
247
248impl Default for SolverConfig {
249    fn default() -> Self {
250        Self {
251            guess_probability: 0.8,
252            follow_up_probability: 0.75,
253            context_sensitivity: 0.6,
254            questioning_rigor: 0.4,
255            temperature: 0.7,
256            max_decomposition_depth: 4,
257            recursion_mode: crate::meta_construction::RecursionMode::default(),
258            selection_mode: crate::selection::SelectionMode::default(),
259            skill_mode: crate::skill_ledger::SkillMode::default(),
260            agent_mode: false,
261            diagnostic_mode: false,
262            offline: false,
263            cache_ttl_seconds: 60 * 60 * 24 * 60,
264            definition_fusion_by_default: false,
265            associative_project_promotion: true,
266            execution_surface: ExecutionSurface::default(),
267            blueprint_composition: BlueprintComposition::default(),
268            probability_policy: ProbabilityDecisionPolicy::default(),
269            forced_response_language: None,
270            compute_budget: 512,
271        }
272    }
273}
274
275impl SolverConfig {
276    /// Build a [`SolverConfig`] using the documented environment overrides.
277    ///
278    /// The parsing body lives in `crate::solver_helpers::config_from_env` to keep
279    /// this module under the 1000-line cap enforced by `scripts/check-file-size.rs`.
280    #[must_use]
281    pub fn from_env() -> Self {
282        crate::solver_helpers::config_from_env()
283    }
284}
285
286/// Speaker role for [`ConversationTurn`]. The solver only inspects user
287/// turns when recalling prior context; assistant turns are kept in the log
288/// so the trace stays balanced.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum ConversationRole {
291    User,
292    Assistant,
293}
294
295impl ConversationRole {
296    /// Lowercase slug used in `prior_turn:<role>` event kinds.
297    #[must_use]
298    pub const fn slug(self) -> &'static str {
299        match self {
300            Self::User => "user",
301            Self::Assistant => "assistant",
302        }
303    }
304}
305
306/// A single message in a multi-turn conversation.
307///
308/// The solver records every turn as a `prior_turn:<role>` event before
309/// processing the current impulse so memory recall is grounded in the
310/// append-only log, not in implicit state.
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub struct ConversationTurn {
313    pub role: ConversationRole,
314    pub content: String,
315}
316
317impl ConversationTurn {
318    /// Construct a user turn.
319    #[must_use]
320    pub fn user(content: impl Into<String>) -> Self {
321        Self {
322            role: ConversationRole::User,
323            content: content.into(),
324        }
325    }
326
327    /// Construct an assistant turn.
328    #[must_use]
329    pub fn assistant(content: impl Into<String>) -> Self {
330        Self {
331            role: ConversationRole::Assistant,
332            content: content.into(),
333        }
334    }
335}
336
337/// The universal solver itself. See module docs for the 11-step loop.
338#[derive(Debug, Clone, Copy)]
339pub struct UniversalSolver {
340    pub config: SolverConfig,
341}
342
343impl Default for UniversalSolver {
344    fn default() -> Self {
345        Self {
346            config: SolverConfig::from_env(),
347        }
348    }
349}
350
351impl UniversalSolver {
352    /// Construct a solver with an explicit configuration.
353    #[must_use]
354    pub const fn new(config: SolverConfig) -> Self {
355        Self { config }
356    }
357
358    /// Run the universal loop against a single user impulse and return the
359    /// projected [`SymbolicAnswer`]. Every step is recorded in the in-process
360    /// append-only log so the user-facing answer is, by construction, a
361    /// projection of an inspectable trace.
362    #[must_use]
363    pub fn solve(&self, prompt: &str) -> SymbolicAnswer {
364        self.solve_with_history(prompt, &[])
365    }
366
367    /// Run the universal loop with conversational context. Each prior turn is
368    /// appended to the event log as `prior_turn:user` or `prior_turn:assistant`
369    /// before the current impulse, so memory-recall handlers can search the
370    /// log instead of holding implicit state.
371    #[must_use]
372    pub fn solve_with_history(&self, prompt: &str, history: &[ConversationTurn]) -> SymbolicAnswer {
373        self.solve_with_history_and_probability_store(prompt, history, &ProbabilityStore::new())
374    }
375
376    #[must_use]
377    pub fn solve_with_probability_store(
378        &self,
379        prompt: &str,
380        probability_store: &ProbabilityStore,
381    ) -> SymbolicAnswer {
382        self.solve_with_history_and_probability_store(prompt, &[], probability_store)
383    }
384
385    #[must_use]
386    pub fn solve_with_history_and_probability_store(
387        &self,
388        prompt: &str,
389        history: &[ConversationTurn],
390        probability_store: &ProbabilityStore,
391    ) -> SymbolicAnswer {
392        let mut intent_cache = IntentFormalizationCache::new();
393        self.solve_with_history_probability_store_and_intent_cache(
394            prompt,
395            history,
396            probability_store,
397            &mut intent_cache,
398        )
399    }
400
401    pub(crate) fn solve_with_history_probability_store_and_intent_cache(
402        &self,
403        prompt: &str,
404        history: &[ConversationTurn],
405        probability_store: &ProbabilityStore,
406        intent_cache: &mut IntentFormalizationCache,
407    ) -> SymbolicAnswer {
408        let mut log = EventLog::new();
409
410        // Issue #556: when this solve is a forced-language replay, force
411        // detection so every localizable handler renders in the requested
412        // language. The guard restores the previous value when this function
413        // returns, keeping nested replays balanced.
414        let _forced_language_guard = crate::language::set_forced_language(
415            self.config
416                .forced_response_language
417                .and_then(crate::language::from_slug),
418        );
419
420        for turn in history {
421            let kind: &'static str = match turn.role {
422                ConversationRole::User => "prior_turn:user",
423                ConversationRole::Assistant => "prior_turn:assistant",
424            };
425            log.append(kind, turn.content.clone());
426        }
427
428        log.append("impulse", prompt.to_owned());
429
430        let language = detect_language(prompt);
431        log.append("language", language.slug().to_owned());
432        probability_store.replay_into_event_log(&mut log, self.config.offline);
433
434        let intent_entry = if let Some(formalization) = intent_cache.get(prompt).cloned() {
435            IntentFormalizationCacheEntry {
436                formalization,
437                cache_hit: true,
438            }
439        } else {
440            let formalization_candidates = formalize_prompt_candidates(prompt, language.slug());
441            let formalization_selection = select_formalization_candidate_with_policy(
442                &formalization_candidates,
443                FormalizationSelectionConfig {
444                    temperature: self.config.temperature,
445                    guess_probability: self.config.guess_probability,
446                    questioning_rigor: self.config.questioning_rigor,
447                },
448                prompt,
449                probability_store,
450                self.config.offline,
451                self.config.probability_policy,
452            );
453            record_formalization_selection(&mut log, &formalization_selection);
454            if let FormalizationDecision::Clarify { question, .. } =
455                &formalization_selection.decision
456            {
457                return finalize_simple(
458                    prompt,
459                    &mut log,
460                    "clarify_interpretation",
461                    "response:clarify_interpretation",
462                    question,
463                    0.5,
464                );
465            }
466            if let Some(candidate) = formalization_selection.selected_candidate() {
467                record_formalization(&mut log, candidate);
468            }
469            intent_cache.formalize_or_insert(
470                prompt,
471                language.slug(),
472                formalization_selection.selected_candidate(),
473            )
474        };
475        record_intent_formalization(&mut log, &intent_entry);
476        let intent_formalization = intent_entry.formalization;
477
478        // Issue #661 (R384): before any contextual handler runs (a language
479        // directive would otherwise be replayed by the response-language
480        // follow-up), check whether this newly formalized requirement
481        // contradicts a retained one. A clash — same subject, opposite polarity
482        // — is surfaced as a warning naming both statements, their weights, and
483        // a resolution that reuses the append-only retraction protocol.
484        if let Some(answer) = crate::requirement_contradiction::detect_and_report(
485            prompt,
486            language,
487            history,
488            self.config.temperature,
489            &mut log,
490        ) {
491            return answer;
492        }
493
494        // Issue #559: record the general recursive meta core — problem frame
495        // (R330), recursive work-unit decomposition (R332), need-satisfaction
496        // ledger (R333), method registry (R331), and the end-to-end solution
497        // evidence (R334) — as one cohesive pass. Method selection below is
498        // registry-backed, so the trace and the executable dispatch share the
499        // same method vocabulary.
500        crate::meta_core::record_meta_core(
501            &mut log,
502            &intent_formalization,
503            self.config.max_decomposition_depth,
504            self.config.recursion_mode,
505            self.config.selection_mode,
506            self.config.skill_mode,
507        );
508
509        log.append("search:local", prompt.to_owned());
510
511        let sub_impulses =
512            record_decomposition(&mut log, prompt, self.config.max_decomposition_depth);
513        let sub_results =
514            self.solve_sub_impulses(&mut log, &sub_impulses, probability_store, intent_cache);
515
516        let selected_rule = select_rule_for_intent(&intent_formalization);
517        let rule = try_construct_unknown_rule(selected_rule, prompt, history, &mut log);
518        let rule =
519            if let Some(rewrite) = rewrite_bare_program_coreference_rule(&rule, prompt, history) {
520                log.append("write_program_coreference_rewrite", rewrite.trace);
521                rewrite.rule
522            } else {
523                rule
524            };
525
526        // Issue #324: a follow-up modification ("make the program accept a path
527        // argument") routes to write_program but names no concrete task or
528        // language — they came from the previous turn. Recover the missing
529        // parameters from the conversation so the request completes instead of
530        // surfacing the "language `missing` and task `missing`" error.
531        let rule = if matches!(rule, SelectedRule::UnsupportedWriteProgram { .. }) {
532            let recovery = recover_write_program_rule(rule, prompt, history);
533            if let Some(trace) = recovery.trace {
534                log.append("write_program_context_recovery", trace);
535            }
536            if let Some(plan) = recovery.plan {
537                log.append("write_program_plan", plan);
538            }
539            recovery.rule
540        } else {
541            rule
542        };
543
544        // Issue #458: some composite program prompts also contain strong
545        // non-program signals such as "search current prices". Let a recognized
546        // blueprint recipe preempt those broader fallback handlers before they
547        // can claim the request as generic web search. Concrete catalog programs
548        // still win above this path.
549        if !matches!(rule, SelectedRule::WriteProgram(_)) {
550            let language_hint = match &rule {
551                SelectedRule::UnsupportedWriteProgram { language, .. } => language.as_deref(),
552                _ => None,
553            };
554            let normalized_for_blueprint = normalize_prompt(prompt);
555            if let Some(answer) = try_program_blueprint(
556                prompt,
557                &normalized_for_blueprint,
558                language_hint,
559                self.config.blueprint_composition,
560                &mut log,
561            ) {
562                return answer;
563            }
564        }
565
566        // Issue #340: a `write_program` request can name a supported language but
567        // a composite task the verified catalog has no single template for
568        // (HTTP GET -> parse JSON -> compute mean/median -> output). Rather than
569        // dead-ending on `write_program_unsupported`, decompose the request into
570        // capabilities and, when they match a curated blueprint recipe, return a
571        // real, idiomatic program with an honest "not run" execution report. The
572        // verified catalog stays untouched, so its "compiled and ran" guarantee
573        // is preserved.
574        // Issue #340 + #412: rescue an `UnsupportedWriteProgram` request via the
575        // composite blueprint, then the cached coding oracle (uncatalogued
576        // languages), so "write a hello world program in Kotlin" returns code.
577        if let SelectedRule::UnsupportedWriteProgram { task, language } = &rule {
578            if let Some(answer) = try_unsupported_write_program(
579                prompt,
580                task.as_deref(),
581                language.as_deref(),
582                self.config.blueprint_composition,
583                &mut log,
584            ) {
585                return answer;
586            }
587        }
588
589        if let Some(answer) = try_synthesize_from_sub_results(
590            prompt,
591            &mut log,
592            &sub_results,
593            probability_store,
594            self.config,
595        ) {
596            return answer;
597        }
598
599        // Issue #312: a concrete write_program request (recognized task and
600        // language with a matching template) must take precedence over the
601        // specialized handlers. Otherwise concept_lookup answers the language
602        // name ("Rust") as an encyclopedia definition instead of returning the
603        // requested program. Policy guards still run for these prompts below.
604        let is_concrete_write_program = matches!(rule, SelectedRule::WriteProgram(_));
605        if !is_concrete_write_program {
606            if let Some(answer) = crate::meta_method_dispatch::try_dispatch(
607                self,
608                prompt,
609                &intent_formalization,
610                history,
611                &mut log,
612            ) {
613                return answer;
614            }
615        }
616
617        if let Some(answer) = self.handle_policy(prompt, &mut log, language) {
618            return answer;
619        }
620
621        if matches!(rule, SelectedRule::Unknown) {
622            let intent = language_aware_intent_for(&rule, language);
623            record_candidates(&mut log, prompt, &intent);
624            if let Some(choice) = record_validation(&mut log, prompt) {
625                let response_link = response_link_for_intent(&rule, &intent);
626                return finalize_simple(
627                    prompt,
628                    &mut log,
629                    &intent,
630                    &response_link,
631                    &choice.answer,
632                    1.0,
633                );
634            }
635            // Issue #513: recognize terminal-command requests (visible fix for
636            // #511) before falling through to the unknown answer, so a shell
637            // request returns an agent_suggestion intent in both engines.
638            if let Some(answer) =
639                crate::solver_terminal::try_terminal_command(prompt, language, &mut log)
640            {
641                return answer;
642            }
643            // Issue #662: no reusable part or rule matched. Combine reasoning,
644            // random search, and evolutionary search within the configured
645            // compute budget (GOALS.md Universal Solver Goals) before giving up.
646            // On budget exhaustion the `search:` evidence stays on the log and
647            // the honest unknown-reasoning reply below takes over.
648            if let Some(answer) =
649                crate::solver_search::try_budget_search(prompt, &mut log, self.config)
650            {
651                return answer;
652            }
653            if requires_external_lookup(prompt) {
654                self.record_external_search(&mut log, prompt);
655            }
656            return answer_unknown_prompt(
657                prompt,
658                language,
659                &mut log,
660                UnknownReasoningConfig {
661                    questioning_rigor: self.config.questioning_rigor,
662                    offline: self.config.offline,
663                },
664            );
665        }
666
667        let intent = language_aware_intent_for(&rule, language);
668        log.append("intent", intent.clone());
669
670        if let SelectedRule::WriteProgram(spec) = &rule {
671            log.append(
672                "execution_status",
673                spec.language.execution.status.label().to_owned(),
674            );
675            log.append(
676                "execution_environment",
677                spec.language.execution.environment.to_owned(),
678            );
679            log.append("program_parameter:language", spec.language.slug.to_owned());
680            log.append("program_parameter:task", spec.task.slug.to_owned());
681            log.append("program_parameters", spec.parameter_summary());
682            log.append("legacy_intent", spec.legacy_intent());
683        }
684
685        record_candidates(&mut log, prompt, &intent);
686
687        let validation_choice = record_validation(&mut log, prompt);
688        if validation_choice.is_none() && log.first_of("validation").is_none() {
689            log.append(
690                "validation",
691                "accepted_without_extra_constraints".to_owned(),
692            );
693        }
694        let prior = coding_guidance::history_has_prior_code(history);
695        let base_answer = match (&validation_choice, &rule) {
696            (Some(choice), SelectedRule::Unknown) => choice.answer.clone(),
697            _ => language_aware_answer_for(&rule, language, prompt, prior),
698        };
699
700        let response_link = response_link_for_intent(&rule, &intent);
701        log.append("response", response_link.clone());
702
703        log.append("trace:simplification", "smallest_sufficient".to_owned());
704        let trace_id = log.append("trace", intent.clone());
705
706        let evidence_links = build_evidence_links(prompt, &log, &response_link);
707        let links_notation = answer_links_notation(prompt, &intent, &base_answer, &log, &trace_id);
708        let thinking_steps = log.thinking_steps_for_answer(&base_answer);
709        let answer =
710            append_diagnostic_trace(self.config.diagnostic_mode, base_answer, &links_notation);
711
712        let execution_recipe = match &rule {
713            SelectedRule::WriteProgram(spec) => Some(Box::new(ExecutionRecipe {
714                language: spec.language.code_fence.to_owned(),
715                source: crate::code_editing::apply_inline_hello_world_source_replacement(
716                    prompt,
717                    spec.template.code,
718                    *spec,
719                ),
720                path: spec.language.save_as.to_owned(),
721                commands: spec
722                    .language
723                    .execution
724                    .check_command
725                    .into_iter()
726                    .chain(std::iter::once(spec.language.execution.run_command))
727                    .map(str::to_owned)
728                    .collect(),
729            })),
730            _ => None,
731        };
732
733        SymbolicAnswer {
734            intent,
735            answer,
736            confidence: confidence_for(&rule, validation_choice.as_ref()),
737            evidence_links,
738            thinking_steps,
739            links_notation,
740            execution_recipe,
741        }
742    }
743
744    fn handle_policy(
745        &self,
746        prompt: &str,
747        log: &mut EventLog,
748        language: Language,
749    ) -> Option<SymbolicAnswer> {
750        let normalized = prompt.to_lowercase();
751
752        if is_inappropriate_content(&normalized) {
753            log.append("policy:inappropriate_content", prompt.to_owned());
754            let lang_slug = language.slug();
755            let fallback = "That message contains inappropriate content. Please keep the conversation respectful.";
756            let body = seed::response_for("inappropriate_content", lang_slug)
757                .unwrap_or_else(|| String::from(fallback));
758            return Some(Self::finalize_policy(
759                prompt,
760                log,
761                "inappropriate_content",
762                language,
763                &body,
764            ));
765        }
766
767        if is_unbounded_autonomy(&normalized) && !is_agent_opt_in(&normalized) {
768            log.append("policy:chat_bounded_autonomy", prompt.to_owned());
769            return Some(Self::finalize_policy(
770                prompt,
771                log,
772                "bounded_autonomy",
773                language,
774                concat!(
775                    "I can only run a bounded chat reply per message. To take repeated, ",
776                    "open-ended actions I need an explicit opt-in to agent mode, and agent ",
777                    "mode runs in an isolated sandbox so the host stays safe."
778                ),
779            ));
780        }
781
782        if is_forget_request(&normalized) {
783            log.append("policy:add_only_history", prompt.to_owned());
784            return Some(Self::finalize_policy(
785                prompt,
786                log,
787                "add_only_history",
788                language,
789                concat!(
790                    "The link network is append-only. To retract a fact, send the explicit ",
791                    "retraction protocol; it will append a superseding event without erasing ",
792                    "history."
793                ),
794            ));
795        }
796
797        if is_cache_flush_request(&normalized) {
798            log.append(
799                "policy:cache_flush_requires_confirmation",
800                prompt.to_owned(),
801            );
802            return Some(Self::finalize_policy(
803                prompt,
804                log,
805                "cache_flush_requires_confirmation",
806                language,
807                "Flushing the source cache is an auditable action. Confirm explicitly.",
808            ));
809        }
810
811        if is_agent_request(&normalized) && is_destructive_action(&normalized) {
812            log.append("agent_mode:opted_in", prompt.to_owned());
813            log.append(
814                "policy:destructive_action_requires_confirmation",
815                prompt.to_owned(),
816            );
817            return Some(Self::finalize_policy(
818                prompt,
819                log,
820                "destructive_action_requires_confirmation",
821                language,
822                concat!(
823                    "Destructive agent actions require an explicit human confirmation. ",
824                    "The action will run inside an isolated sandbox once confirmed."
825                ),
826            ));
827        }
828
829        if is_agent_request(&normalized) && is_unbounded_loop(&normalized) {
830            log.append("agent_mode:opted_in", prompt.to_owned());
831            log.append("policy:agent_time_budget", prompt.to_owned());
832            return Some(Self::finalize_policy(
833                prompt,
834                log,
835                "agent_time_budget",
836                language,
837                concat!(
838                    "Agent execution is bounded by a documented time budget; unbounded ",
839                    "loops are refused. Re-send a bounded version inside an isolated sandbox."
840                ),
841            ));
842        }
843
844        if is_agent_request(&normalized) {
845            // The HTTP surface is embedded in an agentic CLI harness. Executing
846            // here would mutate the server's private temporary workspace while
847            // the caller sees no tool call and cannot audit or approve it. API
848            // requests therefore stay declarative; `protocol` routes concrete
849            // actions through the tools advertised by the client.
850            if self.config.execution_surface != ExecutionSurface::HttpServer {
851                if let Some(answer) = try_agent_workspace_task(prompt, &normalized, log) {
852                    return Some(answer);
853                }
854            }
855            log.append("agent_mode:opted_in", prompt.to_owned());
856            log.append("agent_mode:active", prompt.to_owned());
857            log.append("action_log", prompt.to_owned());
858            return Some(Self::finalize_policy(
859                prompt,
860                log,
861                "agent_action",
862                language,
863                concat!(
864                    "Agent mode is opted in for this message. The action will run inside ",
865                    "an isolated sandbox (docker, webvm or sandbox-equivalent) and every ",
866                    "step will be appended to the action log."
867                ),
868            ));
869        }
870
871        None
872    }
873
874    fn finalize_policy(
875        prompt: &str,
876        log: &mut EventLog,
877        intent_slug: &str,
878        _language: Language,
879        body: &str,
880    ) -> SymbolicAnswer {
881        let intent = format!("policy_{intent_slug}");
882        let response_link = format!("response:policy:{intent_slug}");
883        finalize_simple(prompt, log, &intent, &response_link, body, 0.5)
884    }
885
886    fn record_external_search(&self, log: &mut EventLog, prompt: &str) {
887        if self.config.offline {
888            log.append("search:external", "skipped:offline".to_owned());
889            return;
890        }
891        log.append("search:external", prompt.to_owned());
892        let source_id = stable_id("source", prompt);
893        let fetched_at = "1970-01-01T00:00:00Z";
894        let sha256 = stable_id("sha256", prompt);
895        log.append(
896            "source:http",
897            format!("https://example.org/{source_id} fetched_at={fetched_at} sha256={sha256}"),
898        );
899        log.append("cache_hit", source_id);
900    }
901}
902
903/// Convenience entry point that mirrors [`UniversalSolver::solve`] using the
904/// environment-derived [`SolverConfig`]. The deterministic-projection
905/// guarantee from `NON-GOALS.md` is preserved.
906#[must_use]
907pub fn solve(prompt: &str) -> SymbolicAnswer {
908    UniversalSolver::default().solve(prompt)
909}
910
911/// Convenience entry point that mirrors [`UniversalSolver::solve_with_history`].
912#[must_use]
913pub fn solve_with_history(prompt: &str, history: &[ConversationTurn]) -> SymbolicAnswer {
914    UniversalSolver::default().solve_with_history(prompt, history)
915}