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, try_recall_approved_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 recalled_rule = try_recall_approved_rule(selected_rule, prompt, history, &mut log);
518        let rule = try_construct_unknown_rule(recalled_rule, prompt, history, &mut log);
519        let rule =
520            if let Some(rewrite) = rewrite_bare_program_coreference_rule(&rule, prompt, history) {
521                log.append("write_program_coreference_rewrite", rewrite.trace);
522                rewrite.rule
523            } else {
524                rule
525            };
526
527        // Issue #324: a follow-up modification ("make the program accept a path
528        // argument") routes to write_program but names no concrete task or
529        // language — they came from the previous turn. Recover the missing
530        // parameters from the conversation so the request completes instead of
531        // surfacing the "language `missing` and task `missing`" error.
532        let rule = if matches!(rule, SelectedRule::UnsupportedWriteProgram { .. }) {
533            let recovery = recover_write_program_rule(rule, prompt, history);
534            if let Some(trace) = recovery.trace {
535                log.append("write_program_context_recovery", trace);
536            }
537            if let Some(plan) = recovery.plan {
538                log.append("write_program_plan", plan);
539            }
540            recovery.rule
541        } else {
542            rule
543        };
544
545        // Issue #458: some composite program prompts also contain strong
546        // non-program signals such as "search current prices". Let a recognized
547        // blueprint recipe preempt those broader fallback handlers before they
548        // can claim the request as generic web search. Concrete catalog programs
549        // still win above this path.
550        if !matches!(rule, SelectedRule::WriteProgram(_)) {
551            let language_hint = match &rule {
552                SelectedRule::UnsupportedWriteProgram { language, .. } => language.as_deref(),
553                _ => None,
554            };
555            let normalized_for_blueprint = normalize_prompt(prompt);
556            if let Some(answer) = try_program_blueprint(
557                prompt,
558                &normalized_for_blueprint,
559                language_hint,
560                self.config.blueprint_composition,
561                &mut log,
562            ) {
563                return answer;
564            }
565        }
566
567        // Issue #340: a `write_program` request can name a supported language but
568        // a composite task the verified catalog has no single template for
569        // (HTTP GET -> parse JSON -> compute mean/median -> output). Rather than
570        // dead-ending on `write_program_unsupported`, decompose the request into
571        // capabilities and, when they match a curated blueprint recipe, return a
572        // real, idiomatic program with an honest "not run" execution report. The
573        // verified catalog stays untouched, so its "compiled and ran" guarantee
574        // is preserved.
575        // Issue #340 + #412: rescue an `UnsupportedWriteProgram` request via the
576        // composite blueprint, then the cached coding oracle (uncatalogued
577        // languages), so "write a hello world program in Kotlin" returns code.
578        if let SelectedRule::UnsupportedWriteProgram { task, language } = &rule {
579            if let Some(answer) = try_unsupported_write_program(
580                prompt,
581                task.as_deref(),
582                language.as_deref(),
583                self.config.blueprint_composition,
584                &mut log,
585            ) {
586                return answer;
587            }
588        }
589
590        if let Some(answer) = try_synthesize_from_sub_results(
591            prompt,
592            &mut log,
593            &sub_results,
594            probability_store,
595            self.config,
596        ) {
597            return answer;
598        }
599
600        // Issue #312: a concrete write_program request (recognized task and
601        // language with a matching template) must take precedence over the
602        // specialized handlers. Otherwise concept_lookup answers the language
603        // name ("Rust") as an encyclopedia definition instead of returning the
604        // requested program. Policy guards still run for these prompts below.
605        let is_concrete_write_program = matches!(rule, SelectedRule::WriteProgram(_));
606        if !is_concrete_write_program {
607            if let Some(answer) = crate::meta_method_dispatch::try_dispatch(
608                self,
609                prompt,
610                &intent_formalization,
611                history,
612                &mut log,
613            ) {
614                return answer;
615            }
616        }
617
618        if let Some(answer) = self.handle_policy(prompt, &mut log, language) {
619            return answer;
620        }
621
622        if matches!(rule, SelectedRule::Unknown) {
623            let intent = language_aware_intent_for(&rule, language);
624            record_candidates(&mut log, prompt, &intent);
625            if let Some(choice) = record_validation(&mut log, prompt) {
626                let response_link = response_link_for_intent(&rule, &intent);
627                return finalize_simple(
628                    prompt,
629                    &mut log,
630                    &intent,
631                    &response_link,
632                    &choice.answer,
633                    1.0,
634                );
635            }
636            // Issue #513: recognize terminal-command requests (visible fix for
637            // #511) before falling through to the unknown answer, so a shell
638            // request returns an agent_suggestion intent in both engines.
639            if let Some(answer) =
640                crate::solver_terminal::try_terminal_command(prompt, language, &mut log)
641            {
642                return answer;
643            }
644            // Issue #662: no reusable part or rule matched. Combine reasoning,
645            // random search, and evolutionary search within the configured
646            // compute budget (GOALS.md Universal Solver Goals) before giving up.
647            // On budget exhaustion the `search:` evidence stays on the log and
648            // the honest unknown-reasoning reply below takes over.
649            if let Some(answer) =
650                crate::solver_search::try_budget_search(prompt, &mut log, self.config)
651            {
652                return answer;
653            }
654            if requires_external_lookup(prompt) {
655                self.record_external_search(&mut log, prompt);
656            }
657            return answer_unknown_prompt(
658                prompt,
659                language,
660                &mut log,
661                UnknownReasoningConfig {
662                    questioning_rigor: self.config.questioning_rigor,
663                    offline: self.config.offline,
664                },
665            );
666        }
667
668        let intent = language_aware_intent_for(&rule, language);
669        log.append("intent", intent.clone());
670
671        if let SelectedRule::WriteProgram(spec) = &rule {
672            log.append(
673                "execution_status",
674                spec.language.execution.status.label().to_owned(),
675            );
676            log.append(
677                "execution_environment",
678                spec.language.execution.environment.to_owned(),
679            );
680            log.append("program_parameter:language", spec.language.slug.to_owned());
681            log.append("program_parameter:task", spec.task.slug.to_owned());
682            log.append("program_parameters", spec.parameter_summary());
683            log.append("legacy_intent", spec.legacy_intent());
684        }
685
686        record_candidates(&mut log, prompt, &intent);
687
688        let validation_choice = record_validation(&mut log, prompt);
689        if validation_choice.is_none() && log.first_of("validation").is_none() {
690            log.append(
691                "validation",
692                "accepted_without_extra_constraints".to_owned(),
693            );
694        }
695        let prior = coding_guidance::history_has_prior_code(history);
696        let base_answer = match (&validation_choice, &rule) {
697            (Some(choice), SelectedRule::Unknown) => choice.answer.clone(),
698            _ => language_aware_answer_for(&rule, language, prompt, prior),
699        };
700
701        let response_link = response_link_for_intent(&rule, &intent);
702        log.append("response", response_link.clone());
703
704        log.append("trace:simplification", "smallest_sufficient".to_owned());
705        let trace_id = log.append("trace", intent.clone());
706
707        let evidence_links = build_evidence_links(prompt, &log, &response_link);
708        let links_notation = answer_links_notation(prompt, &intent, &base_answer, &log, &trace_id);
709        let thinking_steps = log.thinking_steps_for_answer(&base_answer);
710        let answer =
711            append_diagnostic_trace(self.config.diagnostic_mode, base_answer, &links_notation);
712
713        let execution_recipe = match &rule {
714            SelectedRule::WriteProgram(spec) => Some(Box::new(ExecutionRecipe {
715                language: spec.language.code_fence.to_owned(),
716                source: crate::code_editing::apply_inline_hello_world_source_replacement(
717                    prompt,
718                    spec.template.code,
719                    *spec,
720                ),
721                path: spec.language.save_as.to_owned(),
722                commands: spec
723                    .language
724                    .execution
725                    .check_command
726                    .into_iter()
727                    .chain(std::iter::once(spec.language.execution.run_command))
728                    .map(str::to_owned)
729                    .collect(),
730            })),
731            _ => None,
732        };
733
734        SymbolicAnswer {
735            intent,
736            answer,
737            confidence: confidence_for(&rule, validation_choice.as_ref()),
738            evidence_links,
739            thinking_steps,
740            links_notation,
741            execution_recipe,
742        }
743    }
744
745    fn handle_policy(
746        &self,
747        prompt: &str,
748        log: &mut EventLog,
749        language: Language,
750    ) -> Option<SymbolicAnswer> {
751        let normalized = prompt.to_lowercase();
752
753        if is_inappropriate_content(&normalized) {
754            log.append("policy:inappropriate_content", prompt.to_owned());
755            let lang_slug = language.slug();
756            let fallback = "That message contains inappropriate content. Please keep the conversation respectful.";
757            let body = seed::response_for("inappropriate_content", lang_slug)
758                .unwrap_or_else(|| String::from(fallback));
759            return Some(Self::finalize_policy(
760                prompt,
761                log,
762                "inappropriate_content",
763                language,
764                &body,
765            ));
766        }
767
768        if is_unbounded_autonomy(&normalized) && !is_agent_opt_in(&normalized) {
769            log.append("policy:chat_bounded_autonomy", prompt.to_owned());
770            return Some(Self::finalize_policy(
771                prompt,
772                log,
773                "bounded_autonomy",
774                language,
775                concat!(
776                    "I can only run a bounded chat reply per message. To take repeated, ",
777                    "open-ended actions I need an explicit opt-in to agent mode, and agent ",
778                    "mode runs in an isolated sandbox so the host stays safe."
779                ),
780            ));
781        }
782
783        if is_forget_request(&normalized) {
784            log.append("policy:add_only_history", prompt.to_owned());
785            return Some(Self::finalize_policy(
786                prompt,
787                log,
788                "add_only_history",
789                language,
790                concat!(
791                    "The link network is append-only. To retract a fact, send the explicit ",
792                    "retraction protocol; it will append a superseding event without erasing ",
793                    "history."
794                ),
795            ));
796        }
797
798        if is_cache_flush_request(&normalized) {
799            log.append(
800                "policy:cache_flush_requires_confirmation",
801                prompt.to_owned(),
802            );
803            return Some(Self::finalize_policy(
804                prompt,
805                log,
806                "cache_flush_requires_confirmation",
807                language,
808                "Flushing the source cache is an auditable action. Confirm explicitly.",
809            ));
810        }
811
812        if is_agent_request(&normalized) && is_destructive_action(&normalized) {
813            log.append("agent_mode:opted_in", prompt.to_owned());
814            log.append(
815                "policy:destructive_action_requires_confirmation",
816                prompt.to_owned(),
817            );
818            return Some(Self::finalize_policy(
819                prompt,
820                log,
821                "destructive_action_requires_confirmation",
822                language,
823                concat!(
824                    "Destructive agent actions require an explicit human confirmation. ",
825                    "The action will run inside an isolated sandbox once confirmed."
826                ),
827            ));
828        }
829
830        if is_agent_request(&normalized) && is_unbounded_loop(&normalized) {
831            log.append("agent_mode:opted_in", prompt.to_owned());
832            log.append("policy:agent_time_budget", prompt.to_owned());
833            return Some(Self::finalize_policy(
834                prompt,
835                log,
836                "agent_time_budget",
837                language,
838                concat!(
839                    "Agent execution is bounded by a documented time budget; unbounded ",
840                    "loops are refused. Re-send a bounded version inside an isolated sandbox."
841                ),
842            ));
843        }
844
845        if is_agent_request(&normalized) {
846            // The HTTP surface is embedded in an agentic CLI harness. Executing
847            // here would mutate the server's private temporary workspace while
848            // the caller sees no tool call and cannot audit or approve it. API
849            // requests therefore stay declarative; `protocol` routes concrete
850            // actions through the tools advertised by the client.
851            if self.config.execution_surface != ExecutionSurface::HttpServer {
852                if let Some(answer) = try_agent_workspace_task(prompt, &normalized, log) {
853                    return Some(answer);
854                }
855            }
856            log.append("agent_mode:opted_in", prompt.to_owned());
857            log.append("agent_mode:active", prompt.to_owned());
858            log.append("action_log", prompt.to_owned());
859            return Some(Self::finalize_policy(
860                prompt,
861                log,
862                "agent_action",
863                language,
864                concat!(
865                    "Agent mode is opted in for this message. The action will run inside ",
866                    "an isolated sandbox (docker, webvm or sandbox-equivalent) and every ",
867                    "step will be appended to the action log."
868                ),
869            ));
870        }
871
872        None
873    }
874
875    fn finalize_policy(
876        prompt: &str,
877        log: &mut EventLog,
878        intent_slug: &str,
879        _language: Language,
880        body: &str,
881    ) -> SymbolicAnswer {
882        let intent = format!("policy_{intent_slug}");
883        let response_link = format!("response:policy:{intent_slug}");
884        finalize_simple(prompt, log, &intent, &response_link, body, 0.5)
885    }
886
887    fn record_external_search(&self, log: &mut EventLog, prompt: &str) {
888        if self.config.offline {
889            log.append("search:external", "skipped:offline".to_owned());
890            return;
891        }
892        log.append("search:external", prompt.to_owned());
893        let source_id = stable_id("source", prompt);
894        let fetched_at = "1970-01-01T00:00:00Z";
895        let sha256 = stable_id("sha256", prompt);
896        log.append(
897            "source:http",
898            format!("https://example.org/{source_id} fetched_at={fetched_at} sha256={sha256}"),
899        );
900        log.append("cache_hit", source_id);
901    }
902}
903
904/// Convenience entry point that mirrors [`UniversalSolver::solve`] using the
905/// environment-derived [`SolverConfig`]. The deterministic-projection
906/// guarantee from `NON-GOALS.md` is preserved.
907#[must_use]
908pub fn solve(prompt: &str) -> SymbolicAnswer {
909    UniversalSolver::default().solve(prompt)
910}
911
912/// Convenience entry point that mirrors [`UniversalSolver::solve_with_history`].
913#[must_use]
914pub fn solve_with_history(prompt: &str, history: &[ConversationTurn]) -> SymbolicAnswer {
915    UniversalSolver::default().solve_with_history(prompt, history)
916}