feat: Add tom (Top Of Mind) platform extension (#7073)
This commit is contained in:
@@ -4,6 +4,7 @@ use crate::agents::code_execution_extension;
|
|||||||
use crate::agents::extension_manager_extension;
|
use crate::agents::extension_manager_extension;
|
||||||
use crate::agents::skills_extension;
|
use crate::agents::skills_extension;
|
||||||
use crate::agents::todo_extension;
|
use crate::agents::todo_extension;
|
||||||
|
use crate::agents::tom_extension;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::agents::mcp_client::McpClientTrait;
|
use crate::agents::mcp_client::McpClientTrait;
|
||||||
@@ -119,6 +120,18 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
tom_extension::EXTENSION_NAME,
|
||||||
|
PlatformExtensionDef {
|
||||||
|
name: tom_extension::EXTENSION_NAME,
|
||||||
|
display_name: "Top Of Mind",
|
||||||
|
description:
|
||||||
|
"Inject custom context into every turn via GOOSE_MOIM_MESSAGE_TEXT and GOOSE_MOIM_MESSAGE_FILE environment variables",
|
||||||
|
default_enabled: false,
|
||||||
|
client_factory: |ctx| Box::new(tom_extension::TomClient::new(ctx).unwrap()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
map
|
map
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub mod subagent_handler;
|
|||||||
mod subagent_task_config;
|
mod subagent_task_config;
|
||||||
pub mod subagent_tool;
|
pub mod subagent_tool;
|
||||||
pub(crate) mod todo_extension;
|
pub(crate) mod todo_extension;
|
||||||
|
pub(crate) mod tom_extension;
|
||||||
mod tool_execution;
|
mod tool_execution;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use crate::agents::extension::PlatformExtensionContext;
|
||||||
|
use crate::agents::mcp_client::{Error, McpClientTrait};
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use rmcp::model::{
|
||||||
|
CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult,
|
||||||
|
ProtocolVersion, ServerCapabilities,
|
||||||
|
};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
pub static EXTENSION_NAME: &str = "tom";
|
||||||
|
|
||||||
|
const MAX_BYTES: usize = 65_536;
|
||||||
|
|
||||||
|
pub struct TomClient {
|
||||||
|
info: InitializeResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TomClient {
|
||||||
|
pub fn new(_context: PlatformExtensionContext) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
info: InitializeResult {
|
||||||
|
protocol_version: ProtocolVersion::V_2025_03_26,
|
||||||
|
capabilities: ServerCapabilities {
|
||||||
|
tools: None,
|
||||||
|
tasks: None,
|
||||||
|
resources: None,
|
||||||
|
prompts: None,
|
||||||
|
completions: None,
|
||||||
|
experimental: None,
|
||||||
|
logging: None,
|
||||||
|
},
|
||||||
|
server_info: Implementation {
|
||||||
|
name: EXTENSION_NAME.to_string(),
|
||||||
|
title: Some("Top Of Mind".to_string()),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
icons: None,
|
||||||
|
website_url: None,
|
||||||
|
},
|
||||||
|
instructions: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl McpClientTrait for TomClient {
|
||||||
|
async fn list_tools(
|
||||||
|
&self,
|
||||||
|
_session_id: &str,
|
||||||
|
_next_cursor: Option<String>,
|
||||||
|
_cancellation_token: CancellationToken,
|
||||||
|
) -> Result<ListToolsResult, Error> {
|
||||||
|
Ok(ListToolsResult {
|
||||||
|
tools: vec![],
|
||||||
|
next_cursor: None,
|
||||||
|
meta: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call_tool(
|
||||||
|
&self,
|
||||||
|
_session_id: &str,
|
||||||
|
name: &str,
|
||||||
|
_arguments: Option<JsonObject>,
|
||||||
|
_working_dir: Option<&str>,
|
||||||
|
_cancellation_token: CancellationToken,
|
||||||
|
) -> Result<CallToolResult, Error> {
|
||||||
|
Ok(CallToolResult::error(vec![Content::text(format!(
|
||||||
|
"tom has no tools (called: {name})"
|
||||||
|
))]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self) -> Option<&InitializeResult> {
|
||||||
|
Some(&self.info)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_moim(&self, _session_id: &str) -> Option<String> {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(text) = std::env::var("GOOSE_MOIM_MESSAGE_TEXT") {
|
||||||
|
if !text.trim().is_empty() {
|
||||||
|
parts.push(truncate_utf8(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(path) = std::env::var("GOOSE_MOIM_MESSAGE_FILE") {
|
||||||
|
let expanded = shellexpand::tilde(&path);
|
||||||
|
if let Some(content) = read_bounded(&expanded).await {
|
||||||
|
if !content.trim().is_empty() {
|
||||||
|
parts.push(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(parts.join("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_bounded(path: &str) -> Option<String> {
|
||||||
|
let mut file = tokio::fs::File::open(path).await.ok()?;
|
||||||
|
let mut buf = vec![0u8; MAX_BYTES];
|
||||||
|
let mut total = 0;
|
||||||
|
loop {
|
||||||
|
let n = file.read(&mut buf[total..]).await.ok()?;
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
total += n;
|
||||||
|
if total >= MAX_BYTES {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.truncate(total);
|
||||||
|
let s = String::from_utf8_lossy(&buf).into_owned();
|
||||||
|
Some(truncate_utf8(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_utf8(s: String) -> String {
|
||||||
|
if s.len() <= MAX_BYTES {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
s.char_indices()
|
||||||
|
.take_while(|(i, c)| i + c.len_utf8() <= MAX_BYTES)
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user