mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Feature/at agent mention (#8571)
Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -136,6 +136,7 @@ pub enum CommandType {
|
||||
Builtin,
|
||||
Recipe,
|
||||
Skill,
|
||||
Agent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
@@ -436,6 +437,26 @@ pub async fn get_slash_commands(
|
||||
});
|
||||
}
|
||||
|
||||
let discover_dir = working_dir
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| std::path::Path::new("."));
|
||||
for source in
|
||||
goose::agents::platform_extensions::summon::discover_filesystem_sources(discover_dir)
|
||||
{
|
||||
use goose::agents::platform_extensions::SourceKind;
|
||||
if matches!(
|
||||
source.kind,
|
||||
SourceKind::Agent | SourceKind::Recipe | SourceKind::Subrecipe
|
||||
) && !source.content.is_empty()
|
||||
{
|
||||
commands.push(SlashCommand {
|
||||
command: source.name,
|
||||
help: source.description,
|
||||
command_type: CommandType::Agent,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(SlashCommandsResponse { commands }))
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::agents::extension_manager::{
|
||||
get_parameter_names, ExtensionManager, ExtensionManagerCapabilities,
|
||||
};
|
||||
use crate::agents::final_output_tool::{FINAL_OUTPUT_CONTINUATION_MESSAGE, FINAL_OUTPUT_TOOL_NAME};
|
||||
use crate::agents::platform_extensions::summon::discover_filesystem_sources;
|
||||
use crate::agents::platform_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
|
||||
use crate::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME;
|
||||
use crate::agents::prompt_manager::PromptManager;
|
||||
@@ -354,10 +355,17 @@ impl Agent {
|
||||
}
|
||||
let initial_messages = conversation.messages().clone();
|
||||
|
||||
let (tools, toolshim_tools, system_prompt) = self
|
||||
let (tools, toolshim_tools, mut system_prompt) = self
|
||||
.prepare_tools_and_prompt(session_id, working_dir)
|
||||
.await?;
|
||||
|
||||
if let Some(instructions) = self.resolve_at_mention(&conversation, working_dir) {
|
||||
system_prompt = format!(
|
||||
"{}\n\n# Instructions from active agent:\n\n{}",
|
||||
system_prompt, instructions
|
||||
);
|
||||
}
|
||||
|
||||
let goose_mode = *self.current_goose_mode.lock().await;
|
||||
|
||||
if goose_mode == GooseMode::SmartApprove {
|
||||
@@ -391,6 +399,30 @@ impl Agent {
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_at_mention(
|
||||
&self,
|
||||
conversation: &Conversation,
|
||||
working_dir: &std::path::Path,
|
||||
) -> Option<String> {
|
||||
let last_message = conversation.messages().last()?;
|
||||
if last_message.role == rmcp::model::Role::User {
|
||||
let after_at = last_message
|
||||
.as_concat_text()
|
||||
.trim()
|
||||
.strip_prefix('@')?
|
||||
.to_lowercase();
|
||||
|
||||
for source in discover_filesystem_sources(working_dir) {
|
||||
let name = source.name.to_lowercase();
|
||||
let is_match = after_at == name || after_at.starts_with(&format!("{} ", name));
|
||||
if is_match && !source.content.is_empty() {
|
||||
return Some(source.content.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn categorize_tools(
|
||||
&self,
|
||||
response: &Message,
|
||||
|
||||
@@ -218,7 +218,7 @@ fn scan_agents_from_dir(
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_filesystem_sources(working_dir: &Path) -> Vec<Source> {
|
||||
pub fn discover_filesystem_sources(working_dir: &Path) -> Vec<Source> {
|
||||
let mut sources: Vec<Source> = Vec::new();
|
||||
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
@@ -240,6 +240,7 @@ fn discover_filesystem_sources(working_dir: &Path) -> Vec<Source> {
|
||||
})
|
||||
.chain(
|
||||
[
|
||||
home.as_ref().map(|h| h.join(".goose/recipes")),
|
||||
Some(config.join("recipes")),
|
||||
home.as_ref().map(|h| h.join(".agents/recipes")),
|
||||
]
|
||||
@@ -255,6 +256,7 @@ fn discover_filesystem_sources(working_dir: &Path) -> Vec<Source> {
|
||||
];
|
||||
|
||||
let global_agent_dirs: Vec<PathBuf> = [
|
||||
home.as_ref().map(|h| h.join(".goose/agents")),
|
||||
home.as_ref().map(|h| h.join(".agents/agents")),
|
||||
Some(config.join("agents")),
|
||||
home.as_ref().map(|h| h.join(".claude/agents")),
|
||||
|
||||
@@ -4165,7 +4165,8 @@
|
||||
"enum": [
|
||||
"Builtin",
|
||||
"Recipe",
|
||||
"Skill"
|
||||
"Skill",
|
||||
"Agent"
|
||||
]
|
||||
},
|
||||
"ConfigKey": {
|
||||
|
||||
@@ -80,7 +80,7 @@ export type CheckProviderRequest = {
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export type CommandType = 'Builtin' | 'Recipe' | 'Skill';
|
||||
export type CommandType = 'Builtin' | 'Recipe' | 'Skill' | 'Agent';
|
||||
|
||||
/**
|
||||
* Configuration key metadata for provider setup
|
||||
|
||||
@@ -36,6 +36,8 @@ export const getItemIcon = (item: DisplayItem): IconInfo => {
|
||||
return { Icon: BookOpen, color: '#10b981' }; // Green
|
||||
case 'Skill':
|
||||
return { Icon: Sparkles, color: '#8b5cf6' }; // Purple
|
||||
case 'Agent':
|
||||
return { Icon: Terminal, color: '#f59e0b' }; // Amber
|
||||
case 'Directory':
|
||||
return { Icon: Folder, color: '#f59e0b' }; // Amber
|
||||
default: {
|
||||
|
||||
@@ -38,11 +38,12 @@ const i18n = defineMessages({
|
||||
type DisplayItemType = CommandType | 'Directory' | 'File';
|
||||
|
||||
const typeOrder: Record<DisplayItemType, number> = {
|
||||
Directory: 0,
|
||||
File: 1,
|
||||
Builtin: 2,
|
||||
Skill: 3,
|
||||
Recipe: 4,
|
||||
Agent: 0,
|
||||
Directory: 1,
|
||||
File: 2,
|
||||
Builtin: 3,
|
||||
Skill: 4,
|
||||
Recipe: 5,
|
||||
};
|
||||
|
||||
export interface DisplayItem {
|
||||
@@ -426,7 +427,9 @@ const MentionPopover = forwardRef<
|
||||
);
|
||||
|
||||
let finalScore = bestMatch.score;
|
||||
if (finalScore > 0 && currentWorkingDir) {
|
||||
if (finalScore > 0 && file.itemType === 'Agent') {
|
||||
finalScore += 100;
|
||||
} else if (finalScore > 0 && currentWorkingDir) {
|
||||
const depth = file.extra.replace(currentWorkingDir, '').split('/').length - 1;
|
||||
finalScore += depth <= 1 ? 50 : depth <= 2 ? 30 : depth <= 3 ? 15 : 0;
|
||||
}
|
||||
@@ -449,6 +452,9 @@ const MentionPopover = forwardRef<
|
||||
}, [items, query, currentWorkingDir]);
|
||||
|
||||
const getSelectionText = (item: DisplayItem): string => {
|
||||
if (item.itemType === 'Agent') {
|
||||
return '@' + item.name + ' ';
|
||||
}
|
||||
if (item.itemType === 'Skill') {
|
||||
return `Use the ${item.name} skill to `;
|
||||
}
|
||||
@@ -486,17 +492,34 @@ const MentionPopover = forwardRef<
|
||||
throwOnError: true,
|
||||
});
|
||||
if (cancelled) return;
|
||||
const commandItems: DisplayItem[] = (response.data?.commands || []).map((cmd) => ({
|
||||
name: cmd.command,
|
||||
extra: cmd.help,
|
||||
itemType: cmd.command_type,
|
||||
relativePath: cmd.command,
|
||||
}));
|
||||
const commandItems: DisplayItem[] = (response.data?.commands || [])
|
||||
.filter((cmd) => cmd.command_type !== 'Agent')
|
||||
.map((cmd) => ({
|
||||
name: cmd.command,
|
||||
extra: cmd.help,
|
||||
itemType: cmd.command_type,
|
||||
relativePath: cmd.command,
|
||||
}));
|
||||
setItems(commandItems);
|
||||
} else {
|
||||
const scannedFiles = await scanDirectoryFromRoot(currentWorkingDir || getDefaultStartPath());
|
||||
// Fetch agents from server and scan files in parallel
|
||||
const [agentResponse, scannedFiles] = await Promise.all([
|
||||
getSlashCommands({
|
||||
query: { working_dir: currentWorkingDir },
|
||||
throwOnError: true,
|
||||
}).catch(() => null),
|
||||
scanDirectoryFromRoot(currentWorkingDir || getDefaultStartPath()),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setItems(scannedFiles);
|
||||
const agentItems: DisplayItem[] = (agentResponse?.data?.commands || [])
|
||||
.filter((cmd) => cmd.command_type === 'Agent')
|
||||
.map((cmd) => ({
|
||||
name: cmd.command,
|
||||
extra: cmd.help,
|
||||
itemType: cmd.command_type,
|
||||
relativePath: cmd.command,
|
||||
}));
|
||||
setItems([...agentItems, ...scannedFiles]);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
|
||||
Reference in New Issue
Block a user