use serde_json::json;
use super::change_request;
use super::diagram;
use super::dreaming_audit;
use super::explain;
use super::file_read::{file_read_task_for, plan_file_read_step};
use super::formalize::{
coverage_line, formalize_text_to_links, FormalizedKnowledgeBase, CANONICAL_FISHERMAN_SYNOPSIS,
FISHERMAN_DOC_ID,
};
use super::general_planner::{compose_general_change_plan, GeneralChangePlan, PLAN_PATH};
use super::google_trends_catalog;
use super::google_trends_learning;
use super::ledger;
use super::meaning_detail;
use super::question_catalog;
use super::rebuild_plan;
use super::repair_strategy;
use super::self_ast;
use super::self_heal;
use super::shell_command;
use super::source_graph;
use crate::protocol::ChatMessage;
pub const SEARCH_QUERY: &str = "Пушкин Сказка о рыбаке и рыбке полный текст";
pub const CANONICAL_SOURCE_URL: &str =
"https://ru.wikisource.org/wiki/Сказка_о_рыбаке_и_рыбке_(Пушкин)";
pub const KB_PATH: &str = "knowledge-base.lino";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AgenticPlan {
ToolCalls(Vec<PlannedToolCall>),
Final(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlannedToolCall {
pub tool: String,
pub arguments: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Capability {
Search,
Fetch,
Read,
Write,
Run,
}
impl Capability {
#[must_use]
pub const fn permission_key(self) -> &'static str {
match self {
Self::Search => "tool:capability:search",
Self::Fetch => "tool:capability:fetch",
Self::Read => "tool:capability:read",
Self::Write => "tool:capability:write",
Self::Run => "tool:capability:run",
}
}
}
#[must_use]
pub fn tool_capability(name: &str) -> Option<Capability> {
classify_tool(name)
}
#[must_use]
pub fn plan_chat_step(messages: &[ChatMessage], tool_names: &[&str]) -> Option<AgenticPlan> {
let task = latest_user_text(messages)?;
if self_heal::is_self_heal_task(&task) {
return Some(plan_self_heal_step(messages, tool_names));
}
if dreaming_audit::is_dreaming_audit_task(&task) {
return Some(plan_dreaming_audit_step(messages, tool_names));
}
if self_ast::is_self_ast_task(&task) {
return Some(plan_self_ast_step(messages, tool_names));
}
if source_graph::is_source_graph_task(&task) {
return Some(plan_source_graph_step(messages, tool_names));
}
if ledger::is_ledger_task(&task) {
return Some(plan_ledger_step(messages, tool_names));
}
if explain::is_explain_task(&task) {
return Some(plan_explain_step(messages, tool_names));
}
if change_request::is_change_request_task(&task) {
return Some(plan_change_request_step(messages, tool_names));
}
if repair_strategy::is_repair_strategy_task(&task) {
return Some(plan_repair_strategy_step(messages, tool_names));
}
if rebuild_plan::is_rebuild_task(&task) {
return Some(plan_rebuild_step(messages, tool_names));
}
if google_trends_learning::is_google_trends_learning_task(&task) {
return Some(plan_google_trends_learning_step(messages, tool_names));
}
if google_trends_catalog::is_google_trends_catalog_task(&task) {
return Some(plan_google_trends_catalog_step(messages, tool_names));
}
if question_catalog::is_question_catalog_task(&task) {
return Some(plan_question_catalog_step(messages, tool_names));
}
if let Some(file_task) = file_read_task_for(&task) {
return Some(plan_file_read_step(&file_task, messages, tool_names));
}
if let Some(command) = shell_command::shell_command_for_task(&task) {
return Some(plan_shell_step(messages, tool_names, &command));
}
if is_formalization_task(&task) {
return Some(plan_formalization_step(messages, tool_names));
}
if meaning_detail::is_meaning_detail_task(&task) {
return Some(plan_meaning_detail_step(&task, messages, tool_names));
}
if diagram::is_diagram_task(&task) {
return Some(plan_diagram_step(messages, tool_names));
}
compose_general_change_plan(&task)
.map(|plan| plan_general_change_step(messages, tool_names, &plan))
}
fn plan_general_change_step(
messages: &[ChatMessage],
tool_names: &[&str],
plan: &GeneralChangePlan,
) -> AgenticPlan {
let progress = Progress::scan(messages);
let writes = progress.count(Capability::Write);
if let Some(tool) = tool_for(tool_names, Capability::Write) {
if writes == 0 {
return plan_one(tool, write_arguments(PLAN_PATH, &plan.links_notation()));
}
if writes == 1 {
return plan_one(tool, write_arguments(&plan.target, &plan.content));
}
}
if let Some(tool) =
tool_for(tool_names, Capability::Run).filter(|_| !progress.done(Capability::Run))
{
return plan_one(
tool,
json!({ "command": plan.verification_command }).to_string(),
);
}
AgenticPlan::Final(format!(
"Completed the general change request for {} and verified it with `{}`.\n\nPlan event ({}):\n\n{}",
plan.target,
plan.verification_command,
PLAN_PATH,
plan.links_notation().trim_end(),
))
}
fn plan_shell_step(messages: &[ChatMessage], tool_names: &[&str], command: &str) -> AgenticPlan {
let progress = Progress::scan(messages);
if progress.done(Capability::Run) {
return AgenticPlan::Final(shell_final_answer(
command,
progress.run_output.as_deref().unwrap_or_default(),
));
}
if let Some(tool) = tool_for(tool_names, Capability::Run) {
return plan_one(tool, json!({ "command": command }).to_string());
}
AgenticPlan::Final(format!(
"I can run `{command}` when the client advertises a shell tool such as `bash`, `shell`, or `run_command`."
))
}
struct DocumentRecipe {
path: &'static str,
document: String,
verify_command: String,
final_answer: String,
}
fn plan_document_recipe(
messages: &[ChatMessage],
tool_names: &[&str],
recipe: DocumentRecipe,
) -> AgenticPlan {
let progress = Progress::scan(messages);
if let Some(tool) =
tool_for(tool_names, Capability::Write).filter(|_| !progress.done(Capability::Write))
{
return plan_one(tool, write_arguments(recipe.path, &recipe.document));
}
if let Some(tool) =
tool_for(tool_names, Capability::Run).filter(|_| !progress.done(Capability::Run))
{
return plan_one(
tool,
json!({ "command": recipe.verify_command }).to_string(),
);
}
AgenticPlan::Final(recipe.final_answer)
}
fn plan_formalization_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let search_tool = tool_for(tool_names, Capability::Search);
let fetch_tool = tool_for(tool_names, Capability::Fetch);
let write_tool = tool_for(tool_names, Capability::Write);
let run_tool = tool_for(tool_names, Capability::Run);
let progress = Progress::scan(messages);
if let Some(tool) = search_tool {
if !progress.done(Capability::Search) {
return plan_one(tool, json!({ "query": SEARCH_QUERY }).to_string());
}
}
if let Some(tool) = fetch_tool {
if !progress.done(Capability::Fetch) {
return plan_one(tool, fetch_arguments(CANONICAL_SOURCE_URL));
}
}
let source = progress
.fetched_text
.as_deref()
.unwrap_or(CANONICAL_FISHERMAN_SYNOPSIS);
let formalized = formalize_text_to_links(source, "");
if let Some(tool) = write_tool {
if !progress.done(Capability::Write) {
return plan_one(tool, write_arguments(KB_PATH, &formalized.links_notation));
}
}
if let Some(tool) = run_tool {
if !progress.done(Capability::Run) {
let arguments = json!({ "command": format!("cat {KB_PATH}") });
return plan_one(tool, arguments.to_string());
}
}
AgenticPlan::Final(final_answer(&formalized))
}
fn plan_meaning_detail_step(
task: &str,
messages: &[ChatMessage],
tool_names: &[&str],
) -> AgenticPlan {
let concept = meaning_detail::concept_for_task(task).unwrap_or(&meaning_detail::TOMATO);
let search_tool = tool_for(tool_names, Capability::Search);
let fetch_tool = tool_for(tool_names, Capability::Fetch);
let write_tool = tool_for(tool_names, Capability::Write);
let run_tool = tool_for(tool_names, Capability::Run);
let progress = Progress::scan(messages);
if let Some(tool) = search_tool {
if !progress.done(Capability::Search) {
return plan_one(tool, json!({ "query": concept.search_query }).to_string());
}
}
if let Some(tool) = fetch_tool {
if !progress.done(Capability::Fetch) {
return plan_one(tool, fetch_arguments(concept.source_url));
}
}
let block = meaning_detail::enrich_block(concept, progress.fetched_text.as_deref());
if let Some(tool) = write_tool {
if !progress.done(Capability::Write) {
return plan_one(tool, write_arguments(concept.kb_path, &block));
}
}
if let Some(tool) = run_tool {
if !progress.done(Capability::Run) {
let arguments = json!({ "command": format!("cat {}", concept.kb_path) });
return plan_one(tool, arguments.to_string());
}
}
AgenticPlan::Final(meaning_detail::final_answer_for(concept, &block))
}
fn plan_diagram_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = diagram::render_document();
let final_answer = diagram::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: diagram::DIAGRAM_PATH,
verify_command: format!("cat {}", diagram::DIAGRAM_PATH),
final_answer,
document,
},
)
}
fn plan_self_ast_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = self_ast::render_document();
let final_answer = self_ast::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: self_ast::AST_PATH,
verify_command: format!("cat {}", self_ast::AST_PATH),
final_answer,
document,
},
)
}
fn plan_self_heal_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = self_heal::render_document();
let final_answer = self_heal::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: self_heal::SELF_HEAL_PATH,
verify_command: format!("cat {}", self_heal::SELF_HEAL_PATH),
final_answer,
document,
},
)
}
fn plan_source_graph_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = source_graph::render_document();
let final_answer = source_graph::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: source_graph::SOURCE_GRAPH_PATH,
verify_command: format!("cat {}", source_graph::SOURCE_GRAPH_PATH),
final_answer,
document,
},
)
}
fn plan_ledger_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = ledger::render_document();
let final_answer = ledger::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: ledger::LEDGER_PATH,
verify_command: format!("cat {}", ledger::LEDGER_PATH),
final_answer,
document,
},
)
}
fn plan_explain_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = explain::render_document();
let final_answer = explain::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: explain::EXPLAIN_PATH,
verify_command: format!("cat {}", explain::EXPLAIN_PATH),
final_answer,
document,
},
)
}
fn plan_change_request_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = change_request::render_document();
let final_answer = change_request::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: change_request::CHANGE_PATH,
verify_command: format!("cat {}", change_request::CHANGE_PATH),
final_answer,
document,
},
)
}
fn plan_repair_strategy_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = repair_strategy::render_document();
let final_answer = repair_strategy::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: repair_strategy::REPAIR_STRATEGY_PATH,
verify_command: format!("cat {}", repair_strategy::REPAIR_STRATEGY_PATH),
final_answer,
document,
},
)
}
fn plan_rebuild_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = rebuild_plan::render_document();
let final_answer = rebuild_plan::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: rebuild_plan::REBUILD_PATH,
verify_command: format!("cat {}", rebuild_plan::REBUILD_PATH),
final_answer,
document,
},
)
}
fn plan_question_catalog_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = question_catalog::render_document();
let final_answer = question_catalog::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: question_catalog::QUESTION_CATALOG_PATH,
verify_command: format!("cat {}", question_catalog::QUESTION_CATALOG_PATH),
final_answer,
document,
},
)
}
fn plan_dreaming_audit_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = dreaming_audit::render_document();
let final_answer = dreaming_audit::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: dreaming_audit::DREAMING_AUDIT_PATH,
verify_command: format!("cat {}", dreaming_audit::DREAMING_AUDIT_PATH),
final_answer,
document,
},
)
}
fn plan_google_trends_learning_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = google_trends_learning::render_document();
let final_answer = google_trends_learning::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: google_trends_learning::GOOGLE_TRENDS_LEARNING_PATH,
verify_command: google_trends_learning::verification_command(),
final_answer,
document,
},
)
}
fn plan_google_trends_catalog_step(messages: &[ChatMessage], tool_names: &[&str]) -> AgenticPlan {
let document = google_trends_catalog::render_document();
let final_answer = google_trends_catalog::final_answer(&document);
plan_document_recipe(
messages,
tool_names,
DocumentRecipe {
path: google_trends_catalog::GOOGLE_TRENDS_CATALOG_PATH,
verify_command: google_trends_catalog::verification_command(),
final_answer,
document,
},
)
}
struct Progress {
completed: Vec<Capability>,
fetched_text: Option<String>,
run_output: Option<String>,
}
impl Progress {
fn scan(messages: &[ChatMessage]) -> Self {
let mut completed = Vec::new();
let mut fetched_text = None;
let mut run_output = None;
for (index, message) in messages.iter().enumerate() {
if !message.role.eq_ignore_ascii_case("tool") {
continue;
}
let Some(capability) = result_capability(messages, index) else {
continue;
};
if capability == Capability::Fetch {
let text = message.content.plain_text();
if !looks_like_error(&text) && !text.trim().is_empty() {
fetched_text = Some(text);
}
}
if capability == Capability::Run {
run_output = Some(message.content.plain_text());
}
completed.push(capability);
}
Self {
completed,
fetched_text,
run_output,
}
}
fn done(&self, capability: Capability) -> bool {
self.completed.contains(&capability)
}
fn count(&self, capability: Capability) -> usize {
self.completed
.iter()
.filter(|done| **done == capability)
.count()
}
}
fn plan_one(tool: &str, arguments: String) -> AgenticPlan {
AgenticPlan::ToolCalls(vec![PlannedToolCall {
tool: tool.to_owned(),
arguments,
}])
}
fn write_arguments(path: &str, content: &str) -> String {
json!({
"path": path,
"filePath": path,
"file_path": path,
"content": content,
})
.to_string()
}
fn fetch_arguments(url: &str) -> String {
json!({
"url": url,
"format": "text",
})
.to_string()
}
fn tool_for<'a>(tool_names: &[&'a str], capability: Capability) -> Option<&'a str> {
tool_names
.iter()
.copied()
.find(|name| classify_tool(name) == Some(capability))
}
fn classify_tool(name: &str) -> Option<Capability> {
let lower = name.to_ascii_lowercase();
if lower.contains("todo") {
return None;
}
if lower.contains("search") {
(!lower.contains("code")).then_some(Capability::Search)
} else if lower == "read"
|| lower.contains("read_file")
|| lower.contains("read_local_file")
|| lower.contains("file_read")
|| lower.contains("open_file")
|| lower.contains("view_file")
{
Some(Capability::Read)
} else if lower.contains("fetch")
|| lower.contains("open")
|| lower.contains("browse")
|| lower.contains("get_url")
|| lower.contains("read_url")
{
Some(Capability::Fetch)
} else if lower.contains("write") || lower.contains("create_file") {
Some(Capability::Write)
} else if lower.contains("run")
|| lower.contains("bash")
|| lower.contains("command")
|| lower.contains("exec")
|| lower.contains("shell")
{
Some(Capability::Run)
} else {
None
}
}
fn result_capability(messages: &[ChatMessage], index: usize) -> Option<Capability> {
let message = &messages[index];
if let Some(name) = &message.name {
if let Some(capability) = classify_tool(name) {
return Some(capability);
}
}
let call_id = message.tool_call_id.as_ref()?;
messages[..index]
.iter()
.flat_map(|prior| prior.tool_calls.iter())
.find(|call| &call.id == call_id)
.and_then(|call| classify_tool(&call.function.name))
}
fn latest_user_text(messages: &[ChatMessage]) -> Option<String> {
messages
.iter()
.rev()
.find(|message| message.role.eq_ignore_ascii_case("user"))
.map(|message| message.content.plain_text())
}
const FORMALIZATION_KEYWORDS: [&str; 7] = [
"formaliz",
"формализ",
"knowledge base",
"links notation",
"рыбак",
"fisherman",
"сказк",
];
fn is_formalization_task(prompt: &str) -> bool {
let lower = prompt.to_lowercase();
FORMALIZATION_KEYWORDS
.iter()
.any(|keyword| lower.contains(keyword))
}
fn looks_like_error(text: &str) -> bool {
let lower = text.to_lowercase();
["error", "failed", "not found", "404"]
.iter()
.any(|needle| lower.contains(needle))
}
fn final_answer(formalized: &FormalizedKnowledgeBase) -> String {
let summary = &formalized.summary;
let subject = if summary.doc_id == FISHERMAN_DOC_ID {
"«Сказка о рыбаке и рыбке»".to_owned()
} else {
format!("the source text ({})", summary.doc_id)
};
format!(
"Formalized {subject} into a Links Notation knowledge base: {records} records realising \
all nine protocol primitives ({coverage}).\n\nKnowledge base ({KB_PATH}):\n\n{kb}",
records = summary.total_records(),
coverage = coverage_line(summary),
kb = formalized.links_notation.trim_end(),
)
}
fn shell_final_answer(command: &str, output: &str) -> String {
let trimmed = output.trim_end();
if trimmed.is_empty() {
format!("The `{command}` command completed with no output.")
} else {
format!("The `{command}` command completed. Output:\n\n```text\n{trimmed}\n```")
}
}