Skip to main content

formal_ai/
seed.rs

1//! Universal Links Notation seed shared by every formal-ai interface.
2//!
3//! `data/seed/*.lino` is the canonical source of truth for the agent's
4//! multilingual responses, concept knowledge base, tool registry, language
5//! detection rules, prompt-question patterns, and metadata. The browser
6//! worker, the Rust library, the CLI, the HTTP server, and the Telegram bot
7//! all read from the same files.
8//!
9//! In the browser the files are fetched at runtime by `seed_loader.js`. In
10//! Rust they are compiled into the binary with [`include_str!`] so even
11//! offline builds expose the same data. The two implementations stay
12//! consistent through `scripts/sync-seed.sh`, which mirrors `data/seed/` into
13//! `src/web/seed/` for GitHub Pages deployment.
14//!
15//! See `VISION.md` and `REQUIREMENTS.md` (R97-R104) for the universal
16//! data-driven configuration goal.
17//!
18//! # Stability
19//!
20//! The parser is intentionally tiny — Links Notation files in this repo are
21//! shallow trees of `name "value"` lines with two-space indentation. The
22//! schema for each category is documented in the corresponding `.lino` file.
23
24mod agentic_tool_capabilities;
25mod brainstorm;
26mod client_integrations;
27mod coreference;
28mod embedded;
29mod facts;
30mod grounding_overrides;
31mod handler_precedence;
32mod market_price_references;
33mod meanings;
34mod model_aliases;
35mod operation_vocabulary;
36pub(crate) mod parser;
37mod personas;
38mod projects;
39mod roles;
40mod shell_intents;
41mod summary_topics;
42mod terminal_commands;
43
44use std::collections::BTreeMap;
45
46use parser::{
47    escape_value, find_closing_quote, parse_codepoint, parse_lino, split_pipe_list, unescape_value,
48    LinoNode,
49};
50
51pub use agentic_tool_capabilities::{agentic_tool_capabilities, AgenticToolCapability};
52pub use brainstorm::{brainstorm_seeds, BrainstormCategory, BrainstormSeeds};
53pub use client_integrations::{
54    client_integrations, ClientIntegration, ClientIntegrationGlobalConfig,
55    ClientIntegrationInvocation, ConfigFormat, ModeArgPosition, ModelArgPosition, TemplateEnv,
56};
57pub use coreference::{coreference_seeds, Antecedent, CoreferenceSeeds, Pronoun};
58pub use embedded::{
59    seed_files, AGENTIC_TOOL_CAPABILITIES_LINO, AGENT_INFO_LINO, BRAINSTORM_SEEDS_LINO,
60    CLIENT_INTEGRATIONS_LINO, CODING_IDIOMS_LINO, CONCEPTS_LINO, CONCEPT_CONTEXTS_LINO,
61    COREFERENCE_LINO, DEMO_DIALOGS_LINO, ENVIRONMENTS_LINO, FACTS_LINO, GREETINGS_LINO,
62    HANDLER_PRECEDENCE_LINO, HELLO_WORLD_PROGRAMS_LINO, IDENTITY_LINO, INTENT_ROUTING_LINO,
63    LANGUAGE_DETECTION_LINO, LEARNING_SOURCES_LINO, MARKET_PRICE_REFERENCES_LINO,
64    MEANINGS_CALENDAR_LINO, MEANINGS_FACTS_LINO, MEANINGS_LINKS_ROOT_LINO, MEANINGS_LINO,
65    MEANINGS_SEMANTIC_META_LINO, MEANINGS_SOFTWARE_PROJECT_LINO, MEANINGS_UNITS_LINO,
66    MEANING_FILES, MODEL_ALIASES_LINO, MULTILINGUAL_RESPONSES_LINO, NUMERIC_LIST_OPERATIONS_LINO,
67    OPERATION_VOCABULARY_LINO, PERSONAS_LINO, PROGRAM_CST_GRAMMARS_LINO, PROGRAM_PLAN_RULES_LINO,
68    PROJECTS_LINO, PROMPT_PATTERNS_LINO, SELF_IMPROVEMENT_LOOP_LINO, SHELL_INTENTS_LINO,
69    SUMMARY_TOPICS_LINO, TERMINAL_COMMANDS_LINO, TOOLS_LINO,
70};
71pub use facts::{facts, FactRecord, LocalizedFact};
72pub use grounding_overrides::{
73    cache_contains, override_facts, override_reason, parse_record, resolve, OverrideFact,
74};
75pub use handler_precedence::{handler_precedence, handler_precedence_from};
76pub use market_price_references::{market_price_assets, MarketPriceAsset, MarketPricePeriod};
77pub use meanings::{
78    lexicon, parse_lexicon_text, ArithmeticOperator, Lexeme, Lexicon, Meaning, SemanticFacet, Slot,
79    WordForm,
80};
81pub use model_aliases::{
82    canonical_model_id, model_aliases, resolve_model_id, try_resolve_model_id, ModelAliasRegistry,
83};
84pub use operation_vocabulary::{
85    operation_vocabulary, OperationLanguageForms, OperationTrigger, OperationVocabulary,
86};
87pub use personas::{persona_seeds, Persona, PersonaSeeds, PersonaTopic};
88pub use projects::{
89    projects_registry, LocalizedProject, ProjectRecord, ProjectStatement, ProjectsRegistry,
90};
91pub use roles::{
92    ROLE_AGENT_ACTION_REPORT_SUBJECT, ROLE_AGENT_ACTION_REPORT_VERB, ROLE_ANSWER_RATIONALE_LEAD,
93    ROLE_ARCHITECTURE_CONCEPT, ROLE_ARITHMETIC_OPERATOR_WORD, ROLE_ASSISTANT_MECHANISM_INQUIRY,
94    ROLE_ASSISTANT_SELF_REFERENCE, ROLE_BEHAVIOR_RULE_EDIT_DIRECTIVE,
95    ROLE_BINARY_RELATION_PROPERTY, ROLE_CALCULATION_BASIS_REFERENCE, ROLE_CALCULATION_DOMAIN_TERM,
96    ROLE_CALCULATION_REQUEST_CUE, ROLE_CALCULATION_RESULT_QUERY_CUE, ROLE_CALCULATOR_TOOL_NAME,
97    ROLE_CALENDAR_DAY_REFERENCE, ROLE_CALENDAR_DIRECTION_NEXT, ROLE_CALENDAR_DIRECTION_PREVIOUS,
98    ROLE_CALENDAR_EVENT, ROLE_CALENDAR_QUESTION, ROLE_CALENDAR_RELATIVE_DATE,
99    ROLE_CALENDAR_SCHEDULE_ACTION, ROLE_CALENDAR_TIME, ROLE_CALENDAR_TIMEZONE_ALIAS,
100    ROLE_CALENDAR_TODAY, ROLE_CALENDAR_WEEKDAY, ROLE_CAPABILITY_QUERY, ROLE_CAPABILITY_QUERY_MORE,
101    ROLE_CARDINAL_NUMBER_WORD, ROLE_CAUSAL_INTERROGATIVE, ROLE_CIRCULAR_JOKE_PHRASE,
102    ROLE_CLARIFICATION_REQUEST, ROLE_CLAUSE_CONTINUATION_MARKER, ROLE_CODE_METHOD_NOUN,
103    ROLE_COMMON_TYPO, ROLE_COMPARISON_DIFFERENCE_CUE, ROLE_COMPARISON_TABLE_NOUN,
104    ROLE_COMPARISON_TABLE_TRIGGER, ROLE_COMPOSITIONAL_GENITIVE_HEAD, ROLE_COMPOSITIONAL_LEMMA,
105    ROLE_COMPOSITIONAL_PHRASE, ROLE_COMPOUNDING_ACTION_CUE, ROLE_COMPOUNDING_FREQUENCY_CUE,
106    ROLE_COMPREHENSION_FAILURE_MARKER, ROLE_CONVERSATION_RECALL_OTHER_QUERY,
107    ROLE_CONVERSATION_RECALL_PREVIOUS_MESSAGE, ROLE_CONVERSATION_RECALL_PREVIOUS_USER_MESSAGE,
108    ROLE_CONVERSATION_RECALL_QUERY, ROLE_CONVERSATION_REFERENCE,
109    ROLE_CONVERSATION_SUMMARY_COURTESY, ROLE_CONVERSATION_SUMMARY_DIRECTIVE,
110    ROLE_CONVERSATION_SUMMARY_PHRASE, ROLE_CONVERSATION_TOPIC_OPENER, ROLE_CONVERSION_ACTION_CUE,
111    ROLE_CURRENCY_EUR_REFERENCE, ROLE_CURRENCY_RUB_REFERENCE, ROLE_CURRENCY_USD_REFERENCE,
112    ROLE_DEFINITION_ARTIFACT_REQUEST, ROLE_DEFINITION_COMMAND, ROLE_DEFINITION_MERGE_ACTION,
113    ROLE_DEFINITION_MERGE_MARKER, ROLE_DEFINITION_MERGE_TAIL_BOUNDARY, ROLE_DETAIL_MODIFIER,
114    ROLE_DOCUMENT_ORIGINALITY_CHECK_ACTION, ROLE_DOCUMENT_ORIGINALITY_DOCUMENT,
115    ROLE_DOCUMENT_ORIGINALITY_SUBJECT, ROLE_ENUMERATION_CONSTRAINT,
116    ROLE_ENUMERATION_REQUEST_OPENER, ROLE_EXCHANGE_RATE_REFERENCE, ROLE_EXPLANATION_REQUEST_LEAD,
117    ROLE_FACT_RELATION, ROLE_FEATURE_ACTION_ARITHMETIC, ROLE_FEATURE_ACTION_PLANNING,
118    ROLE_FEATURE_CAPABILITY_ALIAS, ROLE_FEATURE_CAPABILITY_QUESTION, ROLE_FILE_EDIT_ACTION_CUE,
119    ROLE_FILE_EDIT_NEW_LEAD_CUE, ROLE_FILE_EDIT_TARGET_CUE, ROLE_FILE_READ_ACTION_CUE,
120    ROLE_FILE_WRITE_ACTION_CUE, ROLE_FILE_WRITE_CONTENT_LEAD, ROLE_FILE_WRITE_DESTINATION_CUE,
121    ROLE_FILE_WRITE_TARGET_CUE, ROLE_FINAL_AMOUNT_REFERENCE, ROLE_FOLLOWUP_INSTRUCTION_VERB,
122    ROLE_GAME_TRACKER_DOMAIN, ROLE_GAME_TRACKER_MECHANIC, ROLE_GITHUB_REPOSITORY_PLATFORM,
123    ROLE_GITHUB_REPOSITORY_TRAFFIC_QUESTION, ROLE_GITHUB_REPOSITORY_TRAFFIC_SIGNAL,
124    ROLE_HELLO_WORLD_REFERENCE, ROLE_HTTP_FETCH, ROLE_IMPLEMENTATION_LANGUAGE_NOUN,
125    ROLE_IMPLEMENTATION_LANGUAGE_PREPOSITION, ROLE_INTEREST_CUE, ROLE_INTERROGATIVE_OPENER,
126    ROLE_INVESTMENT_CUE, ROLE_KNOWLEDGE_INVENTORY_INTERROGATIVE, ROLE_KNOWLEDGE_INVENTORY_NOUN,
127    ROLE_KNOWLEDGE_INVENTORY_PHRASE, ROLE_KNOWLEDGE_POSSESSION, ROLE_LINKS_NOTATION_FORMAT,
128    ROLE_LIVE_RATE_FRESHNESS_CUE, ROLE_LOCAL_SHELL_REQUEST_CUE, ROLE_MATH_FUNCTION_NAME,
129    ROLE_MEASUREMENT_UNIT, ROLE_MECHANISM_INQUIRY, ROLE_MECHANISM_PREDICATE,
130    ROLE_MEMORY_APPEND_DIRECTIVE, ROLE_MEMORY_SCOPE, ROLE_MEMORY_SUBSTITUTION_CONNECTOR,
131    ROLE_MEMORY_SUBSTITUTION_DIRECTIVE, ROLE_NETWORK_CAPABILITY_CUE, ROLE_NONDETERMINISTIC_MARKER,
132    ROLE_NON_REFERENTIAL_SUBJECT, ROLE_OPERATING_PRINCIPLE, ROLE_OUTPUT_DISPLAY_REQUEST,
133    ROLE_PHYSICAL_ACTION_TRIGGER, ROLE_PHYSICAL_DIMENSION, ROLE_PLAYWRIGHT_SCRIPT_CUE,
134    ROLE_PLAYWRIGHT_TOOL_NAME, ROLE_POLITENESS_CUE, ROLE_PRIOR_ANSWER_REFERENCE,
135    ROLE_PROCEDURAL_ACTION_VERB, ROLE_PROCEDURAL_ELABORATION, ROLE_PROCEDURAL_REQUEST,
136    ROLE_PROCEDURAL_REQUEST_ELIDED_LEAD, ROLE_PROCEDURAL_TASK_MODIFIER, ROLE_PROGRAM_ARTIFACT,
137    ROLE_PROGRAM_GENUS, ROLE_PROGRAM_KIND, ROLE_PROGRAM_LANGUAGE_ALIAS, ROLE_PROGRAM_MODIFICATION,
138    ROLE_PROGRAM_REQUEST, ROLE_PROGRAM_SYNTHESIS_ACTION, ROLE_PROGRAM_SYNTHESIS_DOMAIN,
139    ROLE_PROGRAM_SYNTHESIS_SIGNAL, ROLE_PROGRAM_SYNTHESIS_SUBJECT, ROLE_PROGRAM_SYNTHESIS_TASK,
140    ROLE_PROGRAM_TASK_ALIAS, ROLE_PROOF_CLAIM_SCAFFOLD, ROLE_PROOF_CONCEPT_DETERMINISM,
141    ROLE_PROOF_CONCEPT_GODEL, ROLE_PROOF_DIRECTIVE, ROLE_PROOF_MARKER, ROLE_PROOF_REQUEST_LEAD,
142    ROLE_QUANTITY_CONVERSION_CUE, ROLE_REACHABILITY_OPERAND_FRAMING, ROLE_REACHABILITY_SEARCH_CUE,
143    ROLE_REACHABILITY_TARGET_MARKER, ROLE_REPOSITORY_REFERENCE, ROLE_RESEARCH_CRITERION,
144    ROLE_RESEARCH_EVALUATION_DOMAIN, ROLE_RESEARCH_EVIDENCE_DOMAIN, ROLE_RESEARCH_PROMPT_SIGNAL,
145    ROLE_RESEARCH_QUESTION_OPENER, ROLE_RESEARCH_SUPERLATIVE_MODIFIER,
146    ROLE_RESPONSE_LANGUAGE_MARKER, ROLE_RULE_BRIEF_REQUEST, ROLE_RULE_COUNT_REQUEST,
147    ROLE_RULE_COUNT_SCOPE, ROLE_RULE_LISTING_PHRASE, ROLE_RULE_LISTING_REQUEST,
148    ROLE_RULE_LISTING_SCOPE, ROLE_RULE_LISTING_SUBJECT, ROLE_SCRIPT_AUTHORING_VERB,
149    ROLE_SCRIPT_OR_CODE_ARTIFACT, ROLE_SELF_FACT_QUERY, ROLE_SELF_INTRODUCTION_REQUEST,
150    ROLE_SHELL_CAPABILITY_CUE, ROLE_SKILL_TEACHING_RESPONSE_VERB, ROLE_SKILL_TEACHING_TRIGGER_LEAD,
151    ROLE_SKILL_WHEN_THEN_PAIR, ROLE_SOFTWARE_APPROVAL_TRIGGER, ROLE_SOFTWARE_ARTIFACT_KIND,
152    ROLE_SOFTWARE_AUTHORING_ACTION, ROLE_SOFTWARE_BASH_COMMAND, ROLE_SOFTWARE_DELIVERY_MODE,
153    ROLE_SOFTWARE_FEATURE, ROLE_SOFTWARE_FOLLOWUP_DEMONSTRATION, ROLE_SOFTWARE_FOLLOWUP_EXECUTION,
154    ROLE_SOFTWARE_FOLLOWUP_VERIFICATION, ROLE_SOFTWARE_IMPLEMENTATION_LANGUAGE,
155    ROLE_SOFTWARE_REQUIREMENT_CATEGORY, ROLE_SOFTWARE_STEP_GRANULARITY,
156    ROLE_SUMMARY_CLASSIFICATION_CUE, ROLE_TERM_INFORMATION_REQUEST_OPENER, ROLE_TIME_DURATION_CUE,
157    ROLE_TOOL_ARGUMENT_MARKER, ROLE_TOOL_INVOCATION_CUE, ROLE_TOOL_RESULT_DETAIL_REQUEST,
158    ROLE_TOOL_RESULT_FIRST_REFERENCE, ROLE_TOOL_RESULT_LINE_REQUEST,
159    ROLE_TOOL_RESULT_SECOND_REFERENCE, ROLE_TOOL_RESULT_URL_REQUEST, ROLE_TOPIC_SCAN_STOP_WORD,
160    ROLE_TRANSLATION_ACTION, ROLE_TRANSLATION_INTO_MARKER, ROLE_TRANSLATION_OBJECT_MARKER,
161    ROLE_TRANSLATION_PROPERTY, ROLE_TRANSLATION_SOURCE_MARKER, ROLE_TRANSLATION_TARGET_DIRECTION,
162    ROLE_TRANSLATION_TARGET_MARKER, ROLE_TRANSLATION_UNQUOTED_FRAME, ROLE_URL_NAVIGATE,
163    ROLE_VULGAR_CONTENT_MARKER, ROLE_WEB_MEDIUM, ROLE_WEB_SEARCH_ACTION,
164    ROLE_WEB_SEARCH_EXPLICIT_PREFIX, ROLE_WEB_SEARCH_HISTORY_SIGNAL,
165    ROLE_WEB_SEARCH_IMPERATIVE_LEAD, ROLE_WEB_SEARCH_NEWS_RECENCY, ROLE_WEB_SEARCH_NEWS_SUBJECT,
166    ROLE_WEB_SEARCH_PUBLIC_EVENT_SUBJECT, ROLE_WEB_SEARCH_QUERY_LEADING_NOISE,
167    ROLE_WEB_SEARCH_QUERY_TRAILING_NOISE, ROLE_WEB_SEARCH_RECORDS_SUBJECT, ROLE_WEB_SEARCH_SIGNAL,
168    ROLE_WEB_SEARCH_SOURCE_ONLY, ROLE_WEB_SEARCH_STRONG_ACTION, ROLE_WEB_SEARCH_TOOL_NAME,
169    ROLE_WEB_SEARCH_TOPIC_MARKER, ROLE_WHO_QUESTION_LEAD, ROLE_WHO_QUESTION_TAIL,
170    ROLE_WIKIDATA_ENTITY_ANCHOR, ROLE_YEAR_UNIT_CUE,
171};
172pub use shell_intents::{
173    shell_intent_vocabulary, ShellIntent, ShellIntentArgument, ShellIntentVocabulary,
174};
175pub use summary_topics::{summary_topic_seeds, SummaryTopic, SummaryTopicSeeds};
176pub use terminal_commands::{terminal_command_vocabulary, TerminalCommandVocabulary};
177
178/// Merge every embedded seed file into a single Links Notation document.
179///
180/// The output uses the `formal_ai_seed_bundle` header and is exactly what the
181/// browser `Download bundle` action produces minus the user-specific event
182/// log: it represents the AI's static knowledge surface, fully portable in
183/// one file.
184#[must_use]
185pub fn merged_bundle() -> String {
186    bundle_from_files(&seed_files())
187}
188
189/// Render an arbitrary list of `(file_name, contents)` pairs as a bundle.
190///
191/// The output uses the `formal_ai_seed_bundle` header. Used by
192/// [`merged_bundle`] for the compile-time seed and by tooling that needs to
193/// bundle a custom seed (for example a user-edited overlay).
194#[must_use]
195pub fn bundle_from_files(files: &[(&str, &str)]) -> String {
196    let mut out = String::new();
197    out.push_str("formal_ai_seed_bundle\n");
198    for (name, contents) in files {
199        out.push_str("  file \"");
200        out.push_str(&escape_value(name));
201        out.push_str("\"\n");
202        for line in contents.lines() {
203            if line.is_empty() {
204                continue;
205            }
206            out.push_str("    ");
207            out.push_str(line);
208            out.push('\n');
209        }
210    }
211    out
212}
213
214/// Parse a bundle produced by [`merged_bundle`] back into split file pairs.
215///
216/// The result is a list of `(file_name, contents)` pairs. The inverse of
217/// [`bundle_from_files`] — callers can round-trip the universal seed through
218/// a single `.lino` document for import/export, while still recovering the
219/// per-category split files that drive the rest of the loader.
220///
221/// The parser accepts both bundle dialects:
222///
223/// - flat `formal_ai_seed_bundle` — `file "name"` directly at indent 2,
224/// - nested `formal_ai_bundle` (the format the browser demo writes and the
225///   one [`memory::export_bundle`](crate::memory::export_bundle) produces)
226///   where `seed_files` wraps the file list, so each `file "name"` sits at
227///   indent 4 and the body at indent 6.
228///
229/// Sections with no body produce an empty contents string. Indentation
230/// inside a section is reproduced verbatim (with the leading bundle prefix
231/// stripped) so the round-trip preserves shape.
232#[must_use]
233pub fn parse_bundle(text: &str) -> Vec<(String, String)> {
234    let mut sections: Vec<(String, String)> = Vec::new();
235    let mut current_name: Option<String> = None;
236    let mut current_body = String::new();
237    let mut file_indent: usize = 2;
238    let mut body_indent: usize = 4;
239    let mut inside_seed_files = false;
240    for line in text.lines() {
241        if line.is_empty() {
242            if current_name.is_some() {
243                current_body.push('\n');
244            }
245            continue;
246        }
247        let indent = line.chars().take_while(|c| *c == ' ').count();
248        let trimmed = &line[indent..];
249        // Top-level header (e.g. `formal_ai_seed_bundle` or
250        // `formal_ai_bundle`). Start of document.
251        if indent == 0 {
252            if let Some(name) = current_name.take() {
253                sections.push((name, std::mem::take(&mut current_body)));
254            }
255            inside_seed_files = false;
256            file_indent = 2;
257            body_indent = 4;
258            continue;
259        }
260        // Wrapper section for the nested dialect: `  seed_files`.
261        if indent == 2 && trimmed == "seed_files" {
262            if let Some(name) = current_name.take() {
263                sections.push((name, std::mem::take(&mut current_body)));
264            }
265            inside_seed_files = true;
266            file_indent = 4;
267            body_indent = 6;
268            continue;
269        }
270        // Sibling section at the same indent as `seed_files` (e.g.
271        // `demo_memory`) ends the seed list in the nested dialect.
272        if inside_seed_files && indent == 2 {
273            if let Some(name) = current_name.take() {
274                sections.push((name, std::mem::take(&mut current_body)));
275            }
276            inside_seed_files = false;
277            continue;
278        }
279        // Section header: `file "name"` at the dialect's file_indent.
280        if indent == file_indent && trimmed.starts_with("file ") {
281            if let Some(name) = current_name.take() {
282                sections.push((name, std::mem::take(&mut current_body)));
283            }
284            if let Some(rest) = trimmed.strip_prefix("file ") {
285                let rest = rest.trim();
286                if let Some(stripped) = rest.strip_prefix('"') {
287                    if let Some(close) = find_closing_quote(stripped) {
288                        current_name = Some(unescape_value(&stripped[..close]));
289                    }
290                }
291            }
292            continue;
293        }
294        // Section body: strip the body_indent prefix.
295        if current_name.is_some() {
296            let prefix: String = " ".repeat(body_indent);
297            let stripped = line
298                .strip_prefix(prefix.as_str())
299                .unwrap_or_else(|| line.trim_start());
300            current_body.push_str(stripped);
301            current_body.push('\n');
302        }
303    }
304    if let Some(name) = current_name.take() {
305        sections.push((name, current_body));
306    }
307    sections
308}
309
310/// A single response variant for an intent in a particular language.
311#[derive(Debug, Clone)]
312pub struct ResponseRecord {
313    pub id: String,
314    pub intent: String,
315    pub language: String,
316    pub text: String,
317}
318
319/// Parse `multilingual-responses.lino` into structured records.
320#[must_use]
321pub fn multilingual_responses() -> Vec<ResponseRecord> {
322    let tree = parse_lino(MULTILINGUAL_RESPONSES_LINO);
323    let mut out = Vec::new();
324    if let Some(root) = tree.children.first() {
325        for entry in root.children.iter().filter(|c| c.name == "response") {
326            let intent = entry.find_child_value("intent").to_string();
327            let language = entry.find_child_value("language").to_string();
328            let text = entry.find_child_value("text").to_string();
329            if intent.is_empty() || language.is_empty() {
330                continue;
331            }
332            out.push(ResponseRecord {
333                id: entry.id.clone(),
334                intent,
335                language,
336                text,
337            });
338        }
339    }
340    out
341}
342
343/// Look up a localized response by intent and language, returning `None` if
344/// the seed has no matching record.
345#[must_use]
346pub fn response_for(intent: &str, language: &str) -> Option<String> {
347    for record in multilingual_responses() {
348        if record.intent == intent && record.language == language {
349            return Some(record.text);
350        }
351    }
352    None
353}
354
355/// Generic key/value config from `agent-info.lino`.
356#[must_use]
357pub fn agent_info() -> BTreeMap<String, String> {
358    let tree = parse_lino(AGENT_INFO_LINO);
359    let mut out = BTreeMap::new();
360    if let Some(root) = tree.children.first() {
361        for entry in root.children.iter().filter(|c| c.name == "field") {
362            let key = entry.id.clone();
363            let value = entry.find_child_value("value").to_string();
364            if !key.is_empty() {
365                out.insert(key, value);
366            }
367        }
368    }
369    out
370}
371
372/// The languages the agent answers in, declared by `agent-info.lino`.
373///
374/// Stored as a reference list (`supported_languages ("en" "ru" "hi" "zh")`) so
375/// the multi-value is a sequence of separate references rather than a single
376/// `|`-packed string. This resolves it to the individual language ids in
377/// declaration order.
378#[must_use]
379pub fn supported_languages() -> Vec<String> {
380    agent_info()
381        .get("supported_languages")
382        .map(|value| split_pipe_list(value))
383        .unwrap_or_default()
384}
385
386/// A Unicode-range based language detection rule.
387#[derive(Debug, Clone)]
388pub struct LanguageRule {
389    pub id: String,
390    pub language: String,
391    pub label: String,
392    pub start: u32,
393    pub end: u32,
394}
395
396#[must_use]
397pub fn language_rules() -> Vec<LanguageRule> {
398    let tree = parse_lino(LANGUAGE_DETECTION_LINO);
399    let mut out = Vec::new();
400    if let Some(root) = tree.children.first() {
401        for entry in root.children.iter().filter(|c| c.name == "rule") {
402            let language = entry.find_child_value("language").to_string();
403            if language.is_empty() {
404                continue;
405            }
406            out.push(LanguageRule {
407                id: entry.id.clone(),
408                language,
409                label: entry.find_child_value("label").to_string(),
410                start: parse_codepoint(entry.find_child_value("start")),
411                end: parse_codepoint(entry.find_child_value("end")),
412            });
413        }
414    }
415    out
416}
417
418/// A multilingual question pattern for routing intents.
419#[derive(Debug, Clone)]
420pub struct PromptPattern {
421    pub id: String,
422    pub intent: String,
423    pub language: String,
424    pub kind: String,
425    pub text: String,
426}
427
428/// A language-specific variant of a concept (term, aliases, summary, source).
429///
430/// Used to deliver a localized definition to the user when their prevailing
431/// language matches one of the records nested under `localized "<lang>"` in
432/// `data/seed/concepts.lino`. Empty fields fall back to the parent concept.
433#[derive(Debug, Clone, Default)]
434pub struct LocalizedConcept {
435    pub language: String,
436    pub term: String,
437    pub aliases: Vec<String>,
438    pub summary: String,
439    pub source: String,
440    pub source_kind: String,
441}
442
443/// A concept record from the offline knowledge base.
444///
445/// `contexts` is optional and lists `|`-separated context labels in any of the
446/// supported languages (e.g. "ml|machine learning|машинное обучение|机器学习").
447/// When a concept can be disambiguated by an in-question context delimiter
448/// (e.g. "what is IIR in ML"), the lookup ranker prefers the record whose
449/// `contexts` list contains the parsed context over context-less records.
450///
451/// `wikidata` (optional) anchors the concept to a Wikidata Q-ID so cross-
452/// language fall-back goes via the structured knowledge graph the same way
453/// the human-language / meta-expression repositories already model it.
454///
455/// `context_links` (optional) lists the slugs of `concept_contexts.lino`
456/// records that disambiguate this concept; the response handler can resolve
457/// the localized context label from there.
458///
459/// `localized` (optional) carries per-language overrides of `term`,
460/// `aliases`, `summary`, `source`, and `source_kind`. The solver picks the
461/// override matching the user's prevailing language and falls back to the
462/// outer (English) values when no override exists.
463#[derive(Debug, Clone)]
464pub struct ConceptRecord {
465    pub slug: String,
466    pub term: String,
467    pub category: String,
468    pub aliases: Vec<String>,
469    pub contexts: Vec<String>,
470    pub context_links: Vec<String>,
471    pub wikidata: String,
472    pub summary: String,
473    pub source: String,
474    pub source_kind: String,
475    pub localized: Vec<LocalizedConcept>,
476}
477
478impl ConceptRecord {
479    /// Pick the localized variant matching `language`, falling back to the
480    /// English variant or to `None` if no overrides exist for this concept.
481    #[must_use]
482    pub fn localized_for(&self, language: &str) -> Option<&LocalizedConcept> {
483        self.localized
484            .iter()
485            .find(|loc| loc.language == language)
486            .or_else(|| self.localized.iter().find(|loc| loc.language == "en"))
487    }
488}
489
490#[must_use]
491pub fn concepts() -> Vec<ConceptRecord> {
492    let tree = parse_lino(CONCEPTS_LINO);
493    let mut out = Vec::new();
494    let entries: &[LinoNode] = if tree.name.is_empty() {
495        tree.children.as_slice()
496    } else {
497        std::slice::from_ref(&tree)
498    };
499    for entry in entries {
500        if !entry.name.starts_with("concept_") {
501            continue;
502        }
503        let aliases = split_pipe_list(entry.find_child_value("aliases"));
504        let contexts = split_pipe_list(entry.find_child_value("contexts"));
505        let context_links = split_pipe_list(entry.find_child_value("context_links"));
506        let summary = entry.find_child_value("summary").to_string();
507        let term = entry.find_child_value("term").to_string();
508        if term.is_empty() || summary.is_empty() {
509            continue;
510        }
511        let mut localized = Vec::new();
512        for child in entry.children.iter().filter(|c| c.name == "localized") {
513            let lang = child.id.clone();
514            if lang.is_empty() {
515                continue;
516            }
517            localized.push(LocalizedConcept {
518                language: lang,
519                term: child.find_child_value("term").to_string(),
520                aliases: split_pipe_list(child.find_child_value("aliases")),
521                summary: child.find_child_value("summary").to_string(),
522                source: child.find_child_value("source").to_string(),
523                source_kind: child.find_child_value("source_kind").to_string(),
524            });
525        }
526        out.push(ConceptRecord {
527            slug: entry.name.clone(),
528            term,
529            category: entry.find_child_value("category").to_string(),
530            aliases,
531            contexts,
532            context_links,
533            wikidata: entry.find_child_value("wikidata").to_string(),
534            summary,
535            source: entry.find_child_value("source").to_string(),
536            source_kind: entry.find_child_value("source_kind").to_string(),
537            localized,
538        });
539    }
540    out
541}
542
543/// A localized label for a disambiguating concept context.
544#[derive(Debug, Clone, Default)]
545pub struct LocalizedContextLabel {
546    pub language: String,
547    pub text: String,
548}
549
550/// A disambiguating concept context (e.g. "machine learning") with a Wikidata
551/// Q-ID anchor and per-language localized labels. Loaded from
552/// `data/seed/concept-contexts.lino`.
553#[derive(Debug, Clone, Default)]
554pub struct ContextRecord {
555    pub slug: String,
556    pub wikidata: String,
557    pub aliases: Vec<String>,
558    pub labels: Vec<LocalizedContextLabel>,
559}
560
561impl ContextRecord {
562    /// Pick the localized label matching `language`, falling back to the
563    /// English label or the slug.
564    #[must_use]
565    pub fn label_for(&self, language: &str) -> &str {
566        if let Some(label) = self.labels.iter().find(|l| l.language == language) {
567            return &label.text;
568        }
569        if let Some(label) = self.labels.iter().find(|l| l.language == "en") {
570            return &label.text;
571        }
572        &self.slug
573    }
574
575    /// Returns true when `value` (normalized lowercase) matches one of this
576    /// record's aliases or localized labels.
577    #[must_use]
578    pub fn matches(&self, value: &str) -> bool {
579        let needle = value.trim().to_lowercase();
580        if needle.is_empty() {
581            return false;
582        }
583        if self
584            .aliases
585            .iter()
586            .any(|alias| alias.trim().to_lowercase() == needle)
587        {
588            return true;
589        }
590        self.labels
591            .iter()
592            .any(|label| label.text.trim().to_lowercase() == needle)
593    }
594}
595
596#[must_use]
597pub fn concept_contexts() -> Vec<ContextRecord> {
598    let tree = parse_lino(CONCEPT_CONTEXTS_LINO);
599    let mut out = Vec::new();
600    if let Some(root) = tree.children.first() {
601        for entry in root.children.iter().filter(|c| c.name == "context") {
602            let slug = entry.id.clone();
603            if slug.is_empty() {
604                continue;
605            }
606            let aliases = split_pipe_list(entry.find_child_value("aliases"));
607            let mut labels = Vec::new();
608            for child in entry.children.iter().filter(|c| c.name == "label") {
609                let lang = child.id.clone();
610                if lang.is_empty() {
611                    continue;
612                }
613                labels.push(LocalizedContextLabel {
614                    language: lang,
615                    text: child.find_child_value("text").to_string(),
616                });
617            }
618            out.push(ContextRecord {
619                slug,
620                wikidata: entry.find_child_value("wikidata").to_string(),
621                aliases,
622                labels,
623            });
624        }
625    }
626    out
627}
628
629/// Intent routing record from `data/seed/intent-routing.lino`.
630///
631/// Match semantics (mirrored in `src/web/formal_ai_worker.js`):
632/// - `keywords`: exact match of the entire normalized prompt
633/// - `phrases`: exact match of the entire normalized prompt (kept as a
634///   separate label so multi-word entries are easy to spot in `.lino`)
635/// - `tokens`: any single whitespace-separated token equals the value
636/// - `combos`: every token in the combo appears as a whitespace-separated
637///   token in the prompt (in any order)
638#[derive(Debug, Clone, Default)]
639pub struct IntentRoute {
640    pub id: String,
641    pub slug: String,
642    pub response_link: String,
643    pub keywords: Vec<String>,
644    pub phrases: Vec<String>,
645    pub tokens: Vec<String>,
646    pub combos: Vec<Vec<String>>,
647}
648
649#[derive(Debug, Clone, Default)]
650pub struct IntentRouting {
651    pub intents: Vec<IntentRoute>,
652    pub article_prefixes: Vec<String>,
653    pub trace_prefixes: Vec<String>,
654}
655
656#[must_use]
657pub fn intent_routing() -> IntentRouting {
658    let tree = parse_lino(INTENT_ROUTING_LINO);
659    let mut routing = IntentRouting::default();
660    if let Some(root) = tree.children.first() {
661        for child in &root.children {
662            match child.name.as_str() {
663                "intent" => {
664                    let mut keywords = Vec::new();
665                    let mut phrases = Vec::new();
666                    let mut tokens = Vec::new();
667                    let mut combos = Vec::new();
668                    for entry in &child.children {
669                        match entry.name.as_str() {
670                            "keyword" => keywords.push(entry.id.clone()),
671                            "phrase" => phrases.push(entry.id.clone()),
672                            "token" => tokens.push(entry.id.clone()),
673                            "combo" => combos.push(
674                                entry
675                                    .id
676                                    .split('+')
677                                    .map(str::trim)
678                                    .filter(|s| !s.is_empty())
679                                    .map(ToOwned::to_owned)
680                                    .collect(),
681                            ),
682                            _ => {}
683                        }
684                    }
685                    routing.intents.push(IntentRoute {
686                        id: child.id.clone(),
687                        slug: child.find_child_value("slug").to_string(),
688                        response_link: child.find_child_value("response_link").to_string(),
689                        keywords,
690                        phrases,
691                        tokens,
692                        combos,
693                    });
694                }
695                "article" => routing.article_prefixes.push(child.id.clone()),
696                "trace_prefix" => routing.trace_prefixes.push(child.id.clone()),
697                _ => {}
698            }
699        }
700    }
701    routing
702}
703
704/// One learnable data source declared by `learning-sources.lino` (issue #499).
705///
706/// The seed names each external data source the engine can *learn from* when a
707/// user points it there — a host, the natural-language keywords that name it in
708/// any supported language, and the `capability` slug that says which learning
709/// loop ingests it. Routing reads this data rather than branching on a specific
710/// URL, so a new source is a data edit, never a code change.
711#[derive(Debug, Clone, Default)]
712pub struct LearningSource {
713    pub id: String,
714    pub capability: String,
715    pub host: String,
716    pub keywords: Vec<String>,
717}
718
719/// The learnable-source registry plus the shared, language-agnostic directive
720/// cues that mark a "learn from this source" request (issue #499).
721#[derive(Debug, Clone, Default)]
722pub struct LearningSources {
723    pub sources: Vec<LearningSource>,
724    pub directive_cues: Vec<String>,
725}
726
727impl LearningSources {
728    /// Match a lowercased prompt against the registry and return the source the
729    /// user is teaching the engine to learn from, if any.
730    ///
731    /// A directive is only recognized when the prompt carries **both** a
732    /// language-agnostic learning cue (e.g. "learn from", "узнаешь",
733    /// "यहाँ से सीख", "在这里了解") **and** a reference to a declared source — its
734    /// host or one of its native-language keywords. This is the single source of
735    /// truth shared by the chat handler (`crate::solver_handlers::try_learn_from_source`)
736    /// and the Agent CLI planner
737    /// ([`crate::agentic_coding::google_trends_learning::is_google_trends_learning_task`]),
738    /// so the *same* natural-language teaching directive drives both the chat
739    /// acknowledgement and the artifact-writing recipe. Callers pass an
740    /// already-lowercased prompt so the seed's lowercased cues/keywords match
741    /// directly (issue #499).
742    #[must_use]
743    pub fn match_directive(&self, lowercased: &str) -> Option<&LearningSource> {
744        let has_cue = self
745            .directive_cues
746            .iter()
747            .any(|cue| lowercased.contains(cue.as_str()));
748        if !has_cue {
749            return None;
750        }
751        self.sources.iter().find(|source| {
752            (!source.host.is_empty() && lowercased.contains(source.host.as_str()))
753                || source
754                    .keywords
755                    .iter()
756                    .any(|keyword| lowercased.contains(keyword.as_str()))
757        })
758    }
759}
760
761/// Parse the learnable-source registry from `learning-sources.lino`.
762///
763/// The keywords and directive cues are stored lowercased in the seed so a
764/// lowercased prompt matches them directly (Rust's `to_lowercase` folds the
765/// Cyrillic, Devanagari, and Han text too).
766#[must_use]
767pub fn learning_sources() -> LearningSources {
768    let tree = parse_lino(LEARNING_SOURCES_LINO);
769    let mut registry = LearningSources::default();
770    if let Some(root) = tree.children.first() {
771        for child in &root.children {
772            match child.name.as_str() {
773                "source" => {
774                    let keywords = child
775                        .children
776                        .iter()
777                        .filter(|entry| entry.name == "keyword")
778                        .map(|entry| entry.id.clone())
779                        .collect();
780                    registry.sources.push(LearningSource {
781                        id: child.id.clone(),
782                        capability: child.find_child_value("capability").to_string(),
783                        host: child.find_child_value("host").to_string(),
784                        keywords,
785                    });
786                }
787                "directive" => {
788                    for entry in child.children.iter().filter(|entry| entry.name == "cue") {
789                        registry.directive_cues.push(entry.id.clone());
790                    }
791                }
792                _ => {}
793            }
794        }
795    }
796    registry
797}
798
799#[must_use]
800pub fn prompt_patterns() -> Vec<PromptPattern> {
801    let tree = parse_lino(PROMPT_PATTERNS_LINO);
802    let mut out = Vec::new();
803    if let Some(root) = tree.children.first() {
804        for entry in root.children.iter().filter(|c| c.name == "pattern") {
805            let text = entry.find_child_value("text").to_string();
806            if text.is_empty() {
807                continue;
808            }
809            out.push(PromptPattern {
810                id: entry.id.clone(),
811                intent: entry.find_child_value("intent").to_string(),
812                language: entry.find_child_value("language").to_string(),
813                kind: entry.find_child_value("kind").to_string(),
814                text,
815            });
816        }
817    }
818    out
819}
820
821/// One self-describing entry from `environments.lino`.
822///
823/// The seed declares every supported surface (browser demo, Rust library,
824/// CLI, HTTP server, desktop shell, Telegram bot, Docker microservice) and how memory
825/// migrates between them. The AI itself can therefore answer "where can I
826/// run?" and "how do I move my memory from CLI to web?" from data rather
827/// than from hardcoded strings.
828#[derive(Debug, Clone, Default)]
829pub struct EnvironmentRecord {
830    pub id: String,
831    pub label: String,
832    pub runtime: String,
833    pub seed_path: String,
834    pub memory_store: String,
835    pub memory_export_command: String,
836    pub bundle_export_command: String,
837    pub bundle_import_command: String,
838    pub start_command: String,
839    pub package_command: String,
840    pub tools: Vec<String>,
841}
842
843#[derive(Debug, Clone, Default)]
844pub struct MigrationFlow {
845    pub id: String,
846    pub description: String,
847    pub file_format: String,
848}
849
850#[derive(Debug, Clone, Default)]
851pub struct EnvironmentDirectory {
852    pub environments: Vec<EnvironmentRecord>,
853    pub migration_description: String,
854    pub flows: Vec<MigrationFlow>,
855}
856
857#[must_use]
858pub fn environment_directory() -> EnvironmentDirectory {
859    let tree = parse_lino(ENVIRONMENTS_LINO);
860    let mut directory = EnvironmentDirectory::default();
861    for root in &tree.children {
862        match root.name.as_str() {
863            "environments" => {
864                for entry in root.children.iter().filter(|c| c.name == "environment") {
865                    let tools = split_pipe_list(entry.find_child_value("tools"));
866                    directory.environments.push(EnvironmentRecord {
867                        id: entry.id.clone(),
868                        label: entry.find_child_value("label").to_string(),
869                        runtime: entry.find_child_value("runtime").to_string(),
870                        seed_path: entry.find_child_value("seed_path").to_string(),
871                        memory_store: entry.find_child_value("memory_store").to_string(),
872                        memory_export_command: entry
873                            .find_child_value("memory_export_command")
874                            .to_string(),
875                        bundle_export_command: entry
876                            .find_child_value("bundle_export_command")
877                            .to_string(),
878                        bundle_import_command: entry
879                            .find_child_value("bundle_import_command")
880                            .to_string(),
881                        start_command: entry.find_child_value("start_command").to_string(),
882                        package_command: entry.find_child_value("package_command").to_string(),
883                        tools,
884                    });
885                }
886            }
887            "migration" => {
888                directory.migration_description =
889                    child_value_alias(root, "note", "description").to_string();
890                for entry in root.children.iter().filter(|c| c.name == "flow") {
891                    directory.flows.push(MigrationFlow {
892                        id: entry.id.clone(),
893                        description: child_value_alias(entry, "note", "description").to_string(),
894                        file_format: entry.find_child_value("file_format").to_string(),
895                    });
896                }
897            }
898            _ => {}
899        }
900    }
901    directory
902}
903
904fn child_value_alias<'a>(node: &'a LinoNode, primary: &str, fallback: &str) -> &'a str {
905    let value = node.find_child_value(primary);
906    if value.is_empty() {
907        node.find_child_value(fallback)
908    } else {
909        value
910    }
911}
912
913/// Convenience accessor returning just the environment records (without the
914/// migration flow descriptions). Used by the CLI/HTTP `bundle` printers and
915/// by tests that pin self-awareness coverage.
916#[must_use]
917pub fn environment_records() -> Vec<EnvironmentRecord> {
918    environment_directory().environments
919}