feat: first cut at dev brain

This commit is contained in:
Georg Bauer
2026-07-27 21:16:10 +02:00
parent 56be87d288
commit cfc4fce8d7
14 changed files with 2119 additions and 141 deletions

View File

@@ -302,6 +302,7 @@ pub(crate) struct Tools {
jobs: HashMap<u32, BashJob>,
next_job: u32,
browser: Browser,
dev_brain: Option<crate::dev_brain::DevBrain>,
}
impl Tools {
@@ -316,9 +317,19 @@ impl Tools {
jobs: HashMap::new(),
next_job: 1,
browser: Browser::new()?,
dev_brain: None,
})
}
pub(crate) fn enable_dev_brain(
&mut self,
config: &crate::config::DevBrainConfig,
projects: &[crate::database::Project],
) -> Result<(), String> {
self.dev_brain = Some(crate::dev_brain::DevBrain::open(config, projects)?);
Ok(())
}
fn execute(&mut self, call: &ToolCall, cancel: &AtomicBool) -> String {
let result = match call.name.as_str() {
"read" => self.read(call),
@@ -332,6 +343,9 @@ impl Tools {
"bash_stop" => self.bash_observe(call, true, cancel),
"google_search" => self.google_search(call, cancel),
"visit_page" => self.visit_page(call, cancel),
"dev_brain_search" => self.dev_brain_search(call),
"dev_brain_read" => self.dev_brain_read(call),
"dev_brain_publish" => self.dev_brain_publish(call),
name => Err(format!("unknown tool: {name}")),
};
match result {
@@ -349,6 +363,54 @@ impl Tools {
}
}
fn dev_brain_search(&mut self, call: &ToolCall) -> Result<String, String> {
let query = required_string(call, "query")?;
let limit = integer(call, "limit", 8, 1, 50);
let authoritative = boolean(call, "authoritative", true);
self.dev_brain
.as_mut()
.ok_or_else(|| "Dev Brain is disabled for this session.".to_owned())?
.search(query, limit, authoritative)
}
fn dev_brain_read(&mut self, call: &ToolCall) -> Result<String, String> {
let path = required_string(call, "path")?;
let authoritative = boolean(call, "authoritative", false);
self.dev_brain
.as_mut()
.ok_or_else(|| "Dev Brain is disabled for this session.".to_owned())?
.read(path, authoritative)
}
fn dev_brain_publish(&mut self, call: &ToolCall) -> Result<String, String> {
let files = call
.arguments
.get("files")
.and_then(Value::as_object)
.ok_or_else(|| "dev_brain_publish requires a files object.".to_owned())?;
let remove = call
.arguments
.get("remove")
.map(|value| {
value
.as_array()
.ok_or_else(|| "remove must be an array of paths.".to_owned())?
.iter()
.map(|path| {
path.as_str()
.map(str::to_owned)
.ok_or_else(|| "remove paths must be strings.".to_owned())
})
.collect::<Result<Vec<_>, String>>()
})
.transpose()?
.unwrap_or_default();
self.dev_brain
.as_mut()
.ok_or_else(|| "Dev Brain is disabled for this session.".to_owned())?
.publish(files, &remove)
}
fn result_limit(&self) -> usize {
(self.context_tokens.max(4096) as usize * 2).min(512 * 1024)
}
@@ -1229,16 +1291,26 @@ pub(crate) fn parse_tool_calls(
.map(|calls| (content, calls))
}
pub(crate) fn system_prompt(model: ModelChoice, extra: &str) -> String {
pub(crate) fn system_prompt(model: ModelChoice, extra: &str, dev_brain: bool) -> String {
let schemas = if dev_brain {
format!("{TOOL_SCHEMAS}\n{}", crate::dev_brain::TOOL_SCHEMAS)
} else {
TOOL_SCHEMAS.to_owned()
};
let tools = if model == ModelChoice::Glm52 {
format!(
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n# Tools\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{TOOL_SCHEMAS}\n</tools>\n\nFor a function call, output exactly: <tool_call>function-name<arg_key>key</arg_key><arg_value>value</arg_value></tool_call>\nTool calls are not allowed inside <think></think>. Use read/search for focused context, edit with exact unique old text, and [upto] only between unique head and tail anchors. Use refresh_sec for long bash jobs and poll with bash_status or stop with bash_stop. Preserve the current system configuration unless the user explicitly asks otherwise."
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n# Tools\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n{schemas}\n</tools>\n\nFor a function call, output exactly: <tool_call>function-name<arg_key>key</arg_key><arg_value>value</arg_value></tool_call>\nTool calls are not allowed inside <think></think>. Use read/search for focused context, edit with exact unique old text, and [upto] only between unique head and tail anchors. Use refresh_sec for long bash jobs and poll with bash_status or stop with bash_stop. Preserve the current system configuration unless the user explicitly asks otherwise."
)
} else {
format!(
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n## Tools\n\nInvoke native DSML tools exactly as:\n<DSMLtool_calls>\n<DSMLinvoke name=\"$TOOL_NAME\">\n<DSMLparameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</DSMLparameter>\n</DSMLinvoke>\n</DSMLtool_calls>\n\nTool calls are not allowed inside <think></think>. String parameters use raw text and string=\"true\"; numbers and booleans use JSON text and string=\"false\". Read defaults to a bounded chunk; use more to continue and whole=true only when needed. Use write for new files or whole-file replacement. Use edit with path first and exact unique old text; old may contain one [upto] marker between unique head and tail anchors. For long bash commands pass refresh_sec, then use bash_status or bash_stop. The first web call asks permission to start visible Chrome.\n\n### Available Tool Schemas\n\n{TOOL_SCHEMAS}\n\n# Rules\n- Always use strict DSML syntax.\n- Use read/search to get anchors before editing.\n- Preserve the current system configuration unless explicitly asked otherwise."
"You are a coding agent running in a local workspace. Use tools for local file and system work. Avoid printing large file contents or large code blocks as answers; create or edit files with tools, then summarize results briefly.\n\n## Tools\n\nInvoke native DSML tools exactly as:\n<DSMLtool_calls>\n<DSMLinvoke name=\"$TOOL_NAME\">\n<DSMLparameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</DSMLparameter>\n</DSMLinvoke>\n</DSMLtool_calls>\n\nTool calls are not allowed inside <think></think>. String parameters use raw text and string=\"true\"; numbers and booleans use JSON text and string=\"false\". Read defaults to a bounded chunk; use more to continue and whole=true only when needed. Use write for new files or whole-file replacement. Use edit with path first and exact unique old text; old may contain one [upto] marker between unique head and tail anchors. For long bash commands pass refresh_sec, then use bash_status or bash_stop. The first web call asks permission to start visible Chrome.\n\n### Available Tool Schemas\n\n{schemas}\n\n# Rules\n- Always use strict DSML syntax.\n- Use read/search to get anchors before editing.\n- Preserve the current system configuration unless explicitly asked otherwise."
)
};
let tools = if dev_brain {
format!("{tools}\n\n{}", crate::dev_brain::PROMPT)
} else {
tools
};
if extra.trim().is_empty() {
tools
} else {
@@ -1246,10 +1318,10 @@ pub(crate) fn system_prompt(model: ModelChoice, extra: &str) -> String {
}
}
pub(crate) fn system_prompt_reminder(model: ModelChoice) -> String {
pub(crate) fn system_prompt_reminder(model: ModelChoice, dev_brain: bool) -> String {
format!(
"[System prompt reminder follows.]\n{}\n[End system prompt reminder.]",
system_prompt(model, "")
system_prompt(model, "", dev_brain)
)
}
@@ -1654,7 +1726,7 @@ mod tests {
#[test]
fn prompts_and_parsers_expose_the_reference_tool_set() {
let prompt = system_prompt(ModelChoice::DeepSeekV4Flash, "extra");
let prompt = system_prompt(ModelChoice::DeepSeekV4Flash, "extra", false);
for name in [
"google_search",
"visit_page",
@@ -1672,9 +1744,11 @@ mod tests {
}
assert!(prompt.ends_with("extra"));
assert!(
system_prompt_reminder(ModelChoice::DeepSeekV4Flash)
system_prompt_reminder(ModelChoice::DeepSeekV4Flash, false)
.contains("[System prompt reminder follows.]")
);
assert!(!prompt.contains("dev_brain_search"));
assert!(system_prompt(ModelChoice::DeepSeekV4Flash, "", true).contains("dev_brain_search"));
assert!(datetime_context().starts_with("Current local date and time at session start:"));
assert!(!prompt_reminder_due(49_999, 0));
assert!(prompt_reminder_due(50_000, 0));