fix: fixes to agent behaviour and llm steering

This commit is contained in:
Georg Bauer
2026-07-28 20:07:41 +02:00
parent 1f028950b3
commit ed8f7c4a01
2 changed files with 79 additions and 13 deletions

View File

@@ -187,7 +187,9 @@ fn correction_already_sent(conversation: &[ChatMessage], correction: &str) -> bo
.iter() .iter()
.rev() .rev()
.skip(1) .skip(1)
.take_while(|message| !message.user) .take_while(|message| {
!message.user && !(message.tool && message.content.starts_with("Tool result "))
})
.any(|message| message.tool && message.content == correction) .any(|message| message.tool && message.content == correction)
} }
@@ -2089,6 +2091,22 @@ mod tests {
&conversation, &conversation,
TOOL_PROTOCOL_CORRECTION TOOL_PROTOCOL_CORRECTION
)); ));
let mut result = assistant(None, "Tool result 1 (bash):\nok\n");
result.tool = true;
let mut prior_correction = assistant(None, TOOL_PROTOCOL_CORRECTION);
prior_correction.tool = true;
let conversation = vec![
assistant(Some(call), ""),
prior_correction,
assistant(None, call),
result,
assistant(Some(call), ""),
];
assert!(!correction_already_sent(
&conversation,
TOOL_PROTOCOL_CORRECTION
));
} }
#[test] #[test]

View File

@@ -32,9 +32,9 @@ const TOPIC_DIRS: [&str; 7] = [
static VAULT_LOCK: RwLock<()> = RwLock::new(()); static VAULT_LOCK: RwLock<()> = RwLock::new(());
pub(crate) const PROMPT: &str = r#"# Dev Brain pub(crate) const PROMPT: &str = r#"# Dev Brain
Dev Brain is a managed Obsidian wiki, not a project directory or generic memory. Call dev_brain_info to get its real folder, then use the ordinary list, read, search, write, and edit tools on those Markdown files. Do not invent a .brain path or use bash for wiki maintenance. Read schema.md before maintaining pages and append material changes to log.md. Call dev_brain_validate after edits; it rebuilds index.md and skills.md and reports broken links as repairable warnings without discarding content. Fix warnings with ordinary edits or by creating the missing page. The system prompt lists verified skills by name, description, and Markdown path. When a task matches a skill, read that complete skill file before acting and follow its instructions."#; Dev Brain is a managed Obsidian wiki, not a project directory or generic memory. Call dev_brain_info to get both its real folder and the separate registered project folders. Use the Dev Brain folder only for wiki maintenance; use a registered project folder for cited source files and Git commands. Do not invent a .brain path or use bash for wiki maintenance. Read schema.md before maintaining pages and append material changes to log.md. Call dev_brain_validate after edits; it rebuilds index.md and skills.md and reports broken links as repairable warnings without discarding content. Fix warnings with ordinary edits or by creating the missing page. The system prompt lists verified skills by name, description, and Markdown path. When a task matches a skill, read that complete skill file before acting and follow its instructions."#;
pub(crate) const TOOL_SCHEMAS: &str = r#"{"type":"function","function":{"name":"dev_brain_info","description":"Return the Dev Brain folder and its managed layout. Use ordinary file tools on the returned paths.","parameters":{"type":"object","properties":{}}}} pub(crate) const TOOL_SCHEMAS: &str = r#"{"type":"function","function":{"name":"dev_brain_info","description":"Return the separate Dev Brain wiki folder and registered project source folders. Use ordinary file tools on the returned paths.","parameters":{"type":"object","properties":{}}}}
{"type":"function","function":{"name":"dev_brain_search","description":"Search the validated Dev Brain index with freshness and project evidence. Use ordinary search for literal or regex file search.","parameters":{"type":"object","properties":{"query":{"type":"string"},"limit":{"type":"number"},"authoritative":{"type":"boolean"}},"required":["query"]}}} {"type":"function","function":{"name":"dev_brain_search","description":"Search the validated Dev Brain index with freshness and project evidence. Use ordinary search for literal or regex file search.","parameters":{"type":"object","properties":{"query":{"type":"string"},"limit":{"type":"number"},"authoritative":{"type":"boolean"}},"required":["query"]}}}
{"type":"function","function":{"name":"dev_brain_validate","description":"Validate managed pages after ordinary file edits, deterministically rebuild index.md and skills.md, refresh search, and report repairable link warnings.","parameters":{"type":"object","properties":{}}}}"#; {"type":"function","function":{"name":"dev_brain_validate","description":"Validate managed pages after ordinary file edits, deterministically rebuild index.md and skills.md, refresh search, and report repairable link warnings.","parameters":{"type":"object","properties":{}}}}"#;
@@ -74,13 +74,14 @@ sources:
- project: Registered project name - project: Registered project name
path: src/example.rs path: src/example.rs
symbol: optional_symbol symbol: optional_symbol
revision: full-or-unique-short-clean-git-revision revision: current-project-head-revision
# Use `git rev-parse HEAD`, or a unique lowercase hex prefix of at least 7 characters. # Use `git rev-parse HEAD`, or a unique lowercase hex prefix of at least 7 characters,
# Use hash instead when the worktree is dirty or is not Git. # when this source file matches HEAD. Use hash when this file differs from HEAD,
# is untracked, or the registered project is not Git.
--- ---
``` ```
Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. A revision is the current clean commit's full object ID or a unique lowercase hexadecimal prefix of at least 7 characters. Paths are project-relative and may not escape the registered project. `project` names a registered project, not this Dev Brain vault; `dev_brain_info` lists the exact registered names and folders. Each source `path` is relative to that registered project's folder; never resolve it inside the vault. Cite only files that support the page's claims, not every dirty file in the project. Each source has exactly one evidence version: `revision` or a lowercase SHA-256 `hash`. A revision is the current project HEAD's full object ID or a unique lowercase hexadecimal prefix of at least 7 characters, and is valid when that specific source file matches HEAD even if unrelated files are dirty. Use a hash when that specific file differs from HEAD, is untracked, or its registered project is not Git.
## Skills ## Skills
@@ -279,10 +280,14 @@ impl DevBrain {
pub(crate) fn info(&self) -> String { pub(crate) fn info(&self) -> String {
let mut info = format!( let mut info = format!(
"Dev Brain folder: {}\nManaged roots: purpose.md, schema.md, index.md, skills.md, log.md, {}/\nUse ordinary file tools with these absolute paths. Topic pages need dev_brain: true frontmatter. Skills use type: skill with name and description. Run dev_brain_validate after changes; index.md and skills.md are generated.\n", "Dev Brain wiki folder (not a project): {}\nManaged roots: purpose.md, schema.md, index.md, skills.md, log.md, {}/\nRegistered project folders (source and Git roots, separate from the wiki):\n",
self.vault.display(), self.vault.display(),
TOPIC_DIRS.join("/, ") TOPIC_DIRS.join("/, ")
); );
for project in &self.projects {
info.push_str(&format!("- {}: {}\n", project.name, project.root.display()));
}
info.push_str("Source paths are relative to the named registered project folder. Use ordinary file tools on these absolute folders. Topic pages need dev_brain: true frontmatter. Skills use type: skill with name and description. Run dev_brain_validate after changes; index.md and skills.md are generated.\n");
if let Some(error) = &self.index_error { if let Some(error) = &self.index_error {
info.push_str(&format!( info.push_str(&format!(
"The current wiki needs repair before indexed search: {error}\n" "The current wiki needs repair before indexed search: {error}\n"
@@ -1241,9 +1246,11 @@ fn validate_source(source: &SourceRecord, projects: &[RegisteredProject]) -> Res
)); ));
} }
if let Some(revision) = &source.revision { if let Some(revision) = &source.revision {
return Ok(git_state(&project.root).is_some_and(|(current, clean)| { return Ok(
clean && git_revision_matches(&project.root, revision, &current) git_source_state(&project.root, &resolved).is_some_and(|(current, clean)| {
})); clean && git_revision_matches(&project.root, revision, &current)
}),
);
} }
let expected = source.hash.as_deref().unwrap(); let expected = source.hash.as_deref().unwrap();
if expected.len() != 64 if expected.len() != 64
@@ -1257,7 +1264,21 @@ fn validate_source(source: &SourceRecord, projects: &[RegisteredProject]) -> Res
)); ));
} }
Ok(hash_file(&resolved)? == expected Ok(hash_file(&resolved)? == expected
&& !git_state(&project.root).is_some_and(|(_, clean)| clean)) && !git_source_state(&project.root, &resolved).is_some_and(|(_, clean)| clean))
}
fn git_source_state(root: &Path, source: &Path) -> Option<(String, bool)> {
let repository = git2::Repository::discover(root).ok()?;
let relative = source.strip_prefix(repository.workdir()?).ok()?;
let clean = repository.status_file(relative).ok()? == git2::Status::CURRENT;
let revision = repository
.head()
.ok()?
.peel_to_commit()
.ok()?
.id()
.to_string();
Some((revision, clean))
} }
fn git_revision_matches(root: &Path, revision: &str, current: &str) -> bool { fn git_revision_matches(root: &Path, revision: &str, current: &str) -> bool {
@@ -1736,6 +1757,12 @@ mod tests {
fn path_confinement_and_unrelated_notes_are_preserved() { fn path_confinement_and_unrelated_notes_are_preserved() {
let fixture = Fixture::new(); let fixture = Fixture::new();
let brain = fixture.brain(); let brain = fixture.brain();
let info = brain.info();
assert!(info.contains("Dev Brain wiki folder (not a project)"));
assert!(info.contains(&format!(
"- Fixture: {}",
fixture.project.canonicalize().unwrap().display()
)));
fs::write(fixture.vault.join("concepts/private.md"), "# Private\n").unwrap(); fs::write(fixture.vault.join("concepts/private.md"), "# Private\n").unwrap();
assert!(!brain.allows_tool_write(&fixture.vault.join("concepts/private.md"))); assert!(!brain.allows_tool_write(&fixture.vault.join("concepts/private.md")));
assert!(brain.allows_tool_write(&brain.folder().join("concepts/new.md"))); assert!(brain.allows_tool_write(&brain.folder().join("concepts/new.md")));
@@ -1916,11 +1943,13 @@ mod tests {
} }
#[test] #[test]
fn clean_full_and_short_git_revisions_become_stale_when_the_worktree_changes() { fn source_versions_follow_that_file_instead_of_the_whole_worktree() {
let fixture = Fixture::new(); let fixture = Fixture::new();
fs::write(fixture.project.join("unrelated.rs"), "unrelated\n").unwrap();
let repository = git2::Repository::init(&fixture.project).unwrap(); let repository = git2::Repository::init(&fixture.project).unwrap();
let mut index = repository.index().unwrap(); let mut index = repository.index().unwrap();
index.add_path(Path::new("source.rs")).unwrap(); index.add_path(Path::new("source.rs")).unwrap();
index.add_path(Path::new("unrelated.rs")).unwrap();
let tree_id = index.write_tree().unwrap(); let tree_id = index.write_tree().unwrap();
index.write().unwrap(); index.write().unwrap();
let tree = repository.find_tree(tree_id).unwrap(); let tree = repository.find_tree(tree_id).unwrap();
@@ -1955,6 +1984,13 @@ mod tests {
.unwrap() .unwrap()
.contains("concepts/answer.md") .contains("concepts/answer.md")
); );
fs::write(fixture.project.join("unrelated.rs"), "dirty elsewhere\n").unwrap();
assert!(
brain
.search("Revision-backed knowledge", 10, true)
.unwrap()
.contains("concepts/answer.md")
);
fs::write(fixture.project.join("source.rs"), "dirty\n").unwrap(); fs::write(fixture.project.join("source.rs"), "dirty\n").unwrap();
assert!( assert!(
brain brain
@@ -1962,6 +1998,18 @@ mod tests {
.unwrap() .unwrap()
.contains("status: stale") .contains("status: stale")
); );
let topic = fixture.topic(
"verified",
&source_hash(&fixture),
"Hash-backed dirty knowledge.",
);
write_topic(&fixture, &mut brain, &topic).unwrap();
assert!(
brain
.search("Hash-backed dirty knowledge", 10, true)
.unwrap()
.contains("concepts/answer.md")
);
} }
#[test] #[test]