use rig_core::agent::{Agent, AgentBuilder};
use rig_core::completion::{CompletionModel, Prompt, PromptError};
use crate::sandbox::CorpusRoot;
use crate::tools::CorpusTools;
pub const DEFAULT_MAX_TURNS: usize = 24;
pub const DEFAULT_PREAMBLE: &str = "\
You are a Direct Corpus Interaction (DCI) analyst. You answer questions about a \
corpus of files (code, logs, documents) by issuing search commands directly \
against the raw text — there is no vector database and no pre-built index.
You have four tools:
- corpus_list: list a directory to orient yourself.
- corpus_find: locate files by a path glob (e.g. '**/*.log').
- corpus_search: search file contents with a regular expression; returns
file:line:text evidence.
- corpus_read: read a bounded, line-numbered window from one file.
Method:
1. Start broad: search for the most specific term or pattern in the question.
2. Narrow using path globs and follow-up searches; pivot on identifiers,
error codes, IPs, hashes, or usernames you discover.
3. Read the surrounding lines of promising hits to confirm before concluding.
4. Cite concrete evidence as `path:line` for every claim. If the corpus does
not support a conclusion, say so plainly rather than guessing.
Prefer precise regular expressions over broad ones, and prefer reading a few
lines of real evidence over speculating.";
pub struct DciAgentBuilder<M: CompletionModel> {
model: M,
corpus: CorpusRoot,
preamble: Option<String>,
appended: Vec<String>,
max_turns: usize,
temperature: Option<f64>,
max_tokens: Option<u64>,
model_label: Option<String>,
}
impl<M: CompletionModel + 'static> DciAgentBuilder<M> {
pub fn new(model: M, corpus: CorpusRoot) -> Self {
Self {
model,
corpus,
preamble: None,
appended: Vec::new(),
max_turns: DEFAULT_MAX_TURNS,
temperature: None,
max_tokens: None,
model_label: None,
}
}
pub fn preamble(mut self, preamble: impl Into<String>) -> Self {
self.preamble = Some(preamble.into());
self
}
pub fn append_preamble(mut self, extra: impl Into<String>) -> Self {
self.appended.push(extra.into());
self
}
pub fn max_turns(mut self, turns: usize) -> Self {
self.max_turns = turns;
self
}
pub fn temperature(mut self, temperature: f64) -> Self {
self.temperature = Some(temperature);
self
}
pub fn max_tokens(mut self, max_tokens: u64) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn model_label(mut self, label: impl Into<String>) -> Self {
self.model_label = Some(label.into());
self
}
pub fn build(self) -> DciAgent<M> {
let tools = CorpusTools::new(self.corpus);
let mut preamble = self
.preamble
.unwrap_or_else(|| DEFAULT_PREAMBLE.to_string());
for extra in &self.appended {
preamble.push_str("\n\n");
preamble.push_str(extra);
}
let mut builder = AgentBuilder::new(self.model).preamble(&preamble);
if let Some(temp) = self.temperature {
builder = builder.temperature(temp);
}
if let Some(max_tokens) = self.max_tokens {
builder = builder.max_tokens(max_tokens);
}
let agent = builder
.tool(tools.search)
.tool(tools.find)
.tool(tools.read)
.tool(tools.list)
.build();
DciAgent {
agent,
max_turns: self.max_turns,
model_label: self.model_label.unwrap_or_else(|| "unknown".to_string()),
}
}
}
pub struct DciAgent<M: CompletionModel, P: rig_core::agent::PromptHook<M> = ()> {
agent: Agent<M, P>,
max_turns: usize,
model_label: String,
}
impl<M: CompletionModel + 'static> DciAgent<M, ()> {
pub fn builder(model: M, corpus: CorpusRoot) -> DciAgentBuilder<M> {
DciAgentBuilder::new(model, corpus)
}
}
impl<M: CompletionModel + 'static, P: rig_core::agent::PromptHook<M> + 'static> DciAgent<M, P> {
pub fn agent(&self) -> &Agent<M, P> {
&self.agent
}
pub fn max_turns(&self) -> usize {
self.max_turns
}
pub async fn investigate(&self, question: &str) -> Result<String, PromptError> {
let start = std::time::Instant::now();
let response = self
.agent
.prompt(question)
.max_turns(self.max_turns)
.extended_details()
.await?;
let usage = &response.usage;
crate::telemetry::record_prompt(
&self.model_label,
usage.input_tokens,
usage.output_tokens,
usage.cached_input_tokens,
usage.reasoning_tokens,
start.elapsed().as_millis() as u64,
);
Ok(response.output)
}
}