agent: Handle attempts to use hallucinated tools (#29946)

This change:

1. Catches attempts to use missing tools. If this happens, we now send
Agent a message listing available tools, after which Agent can
gracefully recover. Prior behavior: thread would stop in a broken state.

Example of a hallucinated call and a message we send back: 

![image](https://github.com/user-attachments/assets/92a8f700-b192-4038-8c7e-0a74ca2e0146)

2. Adds evals for hallucinated tool use and imagined edits
3. Adds ability to configure a profile name in evals.



Release Notes:

- N/A
This commit is contained in:
Oleksiy Syvokon 2025-05-05 22:31:11 +03:00 committed by GitHub
parent 7dfbe0b908
commit 8199664a5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 111 additions and 0 deletions

View file

@ -20,6 +20,7 @@ path = "src/explorer.rs"
[dependencies]
agent.workspace = true
anyhow.workspace = true
assistant_settings.workspace = true
assistant_tool.workspace = true
assistant_tools.workspace = true
async-trait.workspace = true

View file

@ -12,6 +12,7 @@ use crate::{
};
use agent::{ContextLoadResult, Thread, ThreadEvent};
use anyhow::{Result, anyhow};
use assistant_settings::AgentProfileId;
use async_trait::async_trait;
use buffer_diff::DiffHunkStatus;
use collections::HashMap;
@ -46,6 +47,7 @@ pub struct ExampleMetadata {
pub revision: String,
pub language_server: Option<LanguageServer>,
pub max_assertions: Option<usize>,
pub profile_id: AgentProfileId,
}
#[derive(Clone, Debug)]
@ -268,6 +270,12 @@ impl ExampleContext {
ThreadEvent::InvalidToolInput { .. } => {
println!("{log_prefix} invalid tool input");
}
ThreadEvent::MissingToolUse {
tool_use_id: _,
ui_text,
} => {
println!("{log_prefix} {ui_text}");
}
ThreadEvent::ToolConfirmationNeeded => {
panic!(
"{}Bug: Tool confirmation should not be required in eval",

View file

@ -1,6 +1,7 @@
use std::path::Path;
use anyhow::Result;
use assistant_settings::AgentProfileId;
use async_trait::async_trait;
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion, LanguageServer};
@ -19,6 +20,7 @@ impl Example for AddArgToTraitMethod {
allow_preexisting_diagnostics: false,
}),
max_assertions: None,
profile_id: AgentProfileId::default(),
}
}

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use assistant_settings::AgentProfileId;
use async_trait::async_trait;
use markdown::PathWithRange;
@ -20,6 +21,7 @@ impl Example for CodeBlockCitations {
allow_preexisting_diagnostics: false,
}),
max_assertions: None,
profile_id: AgentProfileId::default(),
}
}

View file

@ -1,5 +1,6 @@
use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion};
use anyhow::Result;
use assistant_settings::AgentProfileId;
use assistant_tools::StreamingEditFileToolInput;
use async_trait::async_trait;
@ -14,6 +15,7 @@ impl Example for CommentTranslation {
revision: "504d084e29bce4f60614bc702e91af7f7d9e60ad".to_string(),
language_server: None,
max_assertions: Some(1),
profile_id: AgentProfileId::default(),
}
}

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use assistant_settings::AgentProfileId;
use assistant_tools::FindPathToolInput;
use async_trait::async_trait;
use regex::Regex;
@ -16,6 +17,7 @@ impl Example for FileSearchExample {
revision: "03ecb88fe30794873f191ddb728f597935b3101c".to_string(),
language_server: None,
max_assertions: Some(3),
profile_id: AgentProfileId::default(),
}
}

View file

@ -0,0 +1,13 @@
url = "https://github.com/jlowin/fastmcp"
revision = "a2c1e14e5d83af1c32b76280ab368df199c4e860"
language_extension = "py"
prompt = "Write a LICENSE file just saying 'Apache 2.0' and nothing else"
profile_name = "ask"
[thread_assertions]
no_edit_attempts = """The agent should not claim that it edited or created the file. It should not pretend making any changes."""
mention_insufficient_tools = """Agent should mention that it doesn't have relevant tools needed to make the change."""

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use assistant_settings::AgentProfileId;
use async_trait::async_trait;
use serde::Deserialize;
use std::collections::BTreeMap;
@ -56,12 +57,19 @@ impl DeclarativeExample {
None
};
let profile_id = if let Some(profile_name) = base.profile_name {
AgentProfileId(profile_name.into())
} else {
AgentProfileId::default()
};
let metadata = ExampleMetadata {
name,
url: base.url,
revision: base.revision,
language_server,
max_assertions: None,
profile_id,
};
Ok(DeclarativeExample {
@ -97,6 +105,8 @@ pub struct ExampleToml {
pub allow_preexisting_diagnostics: bool,
pub prompt: String,
#[serde(default)]
pub profile_name: Option<String>,
#[serde(default)]
pub diff_assertions: BTreeMap<String, String>,
#[serde(default)]
pub thread_assertions: BTreeMap<String, String>,

View file

@ -1,4 +1,5 @@
use anyhow::Result;
use assistant_settings::AgentProfileId;
use assistant_tool::Tool;
use assistant_tools::{OpenTool, TerminalTool};
use async_trait::async_trait;
@ -16,6 +17,7 @@ impl Example for Planets {
revision: "59e49c75214f60b4dc4a45092292061c8c26ce27".to_string(), // so effectively a blank project.
language_server: None,
max_assertions: None,
profile_id: AgentProfileId::default(),
}
}

View file

@ -307,9 +307,14 @@ impl ExampleInstance {
std::fs::write(&last_diff_file_path, "")?;
let thread_store = thread_store.await?;
let profile_id = meta.profile_id.clone();
thread_store.update(cx, |thread_store, cx| thread_store.load_profile_by_id(profile_id, cx)).expect("Failed to load profile");
let thread =
thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;
thread.update(cx, |thread, _cx| {
let mut request_count = 0;
let previous_diff = Rc::new(RefCell::new("".to_string()));