use crate::brain::provider::{ContentBlock, LLMRequest, Message, Provider, Tool};
use super::compaction::CompactionDataset;
use super::scorer::{Judge, Scorecard};
pub const CONTINUATION_INSTRUCTION: &str = "The conversation must be compacted now. Produce a \
COMPREHENSIVE CONTINUATION DOCUMENT so a fresh agent can resume with only this summary. \
Analyze the entire conversation and preserve, with exact detail:\n\
## Immediate task — the user's last instruction and the exact next action.\n\
## Files modified — every file created/edited/read, with paths and what changed.\n\
## User preferences & constraints — everything the user said to do or never do.\n\
## Errors & corrections — every error message and how it was resolved.\n\
## Pending tasks — everything not yet done.\n\
Preserve exact identifiers (file paths, error codes, commands) verbatim.";
pub fn eval_tool_set() -> Vec<Tool> {
let tool = |name: &str, description: &str, schema: serde_json::Value| Tool {
name: name.to_string(),
description: description.to_string(),
input_schema: schema,
};
vec![
tool(
"config_manager",
"Read or write OpenCrabs configuration (config.toml): enable and configure providers, \
compiled features, channels, STT/TTS, and models.",
serde_json::json!({"type":"object","properties":{"operation":{"type":"string"},"section":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}}),
),
tool(
"tool_search",
"Discover available tools by task description; returns matching tool schemas to activate.",
serde_json::json!({"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}),
),
tool(
"load_brain_file",
"Load an OpenCrabs brain file (TOOLS.md, CODE.md, SECURITY.md, ...) for guidance.",
serde_json::json!({"type":"object","properties":{"name":{"type":"string"}}}),
),
tool(
"bash",
"Run a shell command.",
serde_json::json!({"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}),
),
tool(
"write_file",
"Create a new file with the given content.",
serde_json::json!({"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"}}}),
),
]
}
fn response_text(blocks: &[ContentBlock]) -> String {
blocks
.iter()
.filter_map(|b| match b {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
fn render_response(blocks: &[ContentBlock]) -> String {
let mut out = String::new();
for b in blocks {
match b {
ContentBlock::Text { text } if !text.trim().is_empty() => {
out.push_str(text);
out.push('\n');
}
ContentBlock::ToolUse { name, input, .. } => {
out.push_str(&format!("[tool_call] {name} {input}\n"));
}
_ => {}
}
}
out.trim().to_string()
}
pub async fn produce_response(
provider: &dyn Provider,
model: &str,
prompt: &str,
system: Option<&str>,
tools: &[Tool],
) -> String {
let mut request = LLMRequest::new(model.to_string(), vec![Message::user(prompt.to_string())]);
if let Some(sys) = system {
request = request.with_system(sys);
}
if !tools.is_empty() {
request = request.with_tools(tools.to_vec());
}
match provider.complete(request).await {
Ok(resp) => render_response(&resp.content),
Err(e) => format!("[produce failed: {e}]"),
}
}
pub async fn produce_compaction_summary(
provider: &dyn Provider,
model: &str,
conversation: &[Message],
system: Option<&str>,
) -> String {
let mut messages = conversation.to_vec();
messages.push(Message::user(CONTINUATION_INSTRUCTION.to_string()));
let mut request = LLMRequest::new(model.to_string(), messages);
if let Some(sys) = system {
request = request.with_system(sys);
}
match provider.complete(request).await {
Ok(resp) => response_text(&resp.content),
Err(e) => format!("[compaction failed: {e}]"),
}
}
pub async fn run_compaction_eval(
producer: &dyn Provider,
producer_model: &str,
judge: &dyn Judge,
dataset: &CompactionDataset,
system: Option<&str>,
) -> Scorecard {
let summary =
produce_compaction_summary(producer, producer_model, &dataset.messages(), system).await;
dataset.judge_scorecard(judge, &summary).await
}