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::skills_extension;
|
||||
use crate::agents::todo_extension;
|
||||
use crate::agents::tom_extension;
|
||||
use std::collections::HashMap;
|
||||
|
||||
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
|
||||
},
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ pub mod subagent_handler;
|
||||
mod subagent_task_config;
|
||||
pub mod subagent_tool;
|
||||
pub(crate) mod todo_extension;
|
||||
pub(crate) mod tom_extension;
|
||||
mod tool_execution;
|
||||
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