Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c22d3f8
Platform Extension MOIM (Minus One Info Message)
tlongwell-block Oct 6, 2025
c821645
update prompts
tlongwell-block Oct 6, 2025
fd6e6c4
remove todo read tool
tlongwell-block Oct 6, 2025
e9c48e2
cleanup comments. moim logic in separate file
tlongwell-block Oct 6, 2025
5c28a48
comment
tlongwell-block Oct 6, 2025
250b3c8
comment
tlongwell-block Oct 6, 2025
beb11cd
changes per review. Remove option to configure moim. Filter moim from…
tlongwell-block Oct 6, 2025
0acdc97
cargo fmt
tlongwell-block Oct 6, 2025
9ec9fe8
dont need serial in goose cli anymore
tlongwell-block Oct 6, 2025
7813fb5
move moim to very end of history as a user message
tlongwell-block Oct 7, 2025
7a444c0
clean up comments
tlongwell-block Oct 7, 2025
d1499c5
Merge main into moim2
tlongwell-block Oct 7, 2025
599f61c
filter moim for scenario
tlongwell-block Oct 7, 2025
f85335d
lint
tlongwell-block Oct 7, 2025
ba2004e
Merge remote-tracking branch 'origin/main' into moim2
tlongwell-block Oct 8, 2025
811da83
Merge remote-tracking branch 'origin/main' into moim2
tlongwell-block Oct 10, 2025
f099955
dont add moim immediately after tool calls
tlongwell-block Oct 11, 2025
780e0ad
moim works better when it is behind the latest message
tlongwell-block Oct 13, 2025
b286cbd
simplify moim by simply prepending to last user role message
tlongwell-block Nov 5, 2025
d2f075f
Merge main into moim2 branch
tlongwell-block Nov 5, 2025
07c0ce2
tests
tlongwell-block Nov 5, 2025
528282a
Refactor MOIM injection to use Conversation type, remove redundant co…
tlongwell-block Nov 5, 2025
fae6e77
remove datetime from snapshots
tlongwell-block Nov 5, 2025
0604895
moim config
tlongwell-block Nov 6, 2025
b7b00ae
scenario tests dont work with a dynamic timestamp
tlongwell-block Nov 6, 2025
94f83e2
test minimization
tlongwell-block Nov 6, 2025
4853b30
simplify injection logic
tlongwell-block Nov 6, 2025
5f228ab
tests
tlongwell-block Nov 7, 2025
5607fd5
Merge remote-tracking branch 'origin/main' into moim2
tlongwell-block Nov 7, 2025
b153859
moim disable function tested by scenario tests. dont need a dedicated…
tlongwell-block Nov 7, 2025
3cdc5be
cleanup
tlongwell-block Nov 7, 2025
7871fa0
comment cleanup
tlongwell-block Nov 7, 2025
2eb9c81
Merge remote-tracking branch 'origin/main' into moim2
tlongwell-block Nov 14, 2025
2774671
run fix_conversation before returning moim-enriched conversation. Sim…
tlongwell-block Nov 14, 2025
df80a59
thread local moim skip in scenario tests. No global/serial changes.
tlongwell-block Nov 14, 2025
a7344a1
simplify tests and insertion logic when no assistant message
tlongwell-block Nov 14, 2025
a954dc6
whitespace
tlongwell-block Nov 14, 2025
282775e
fmt
tlongwell-block Nov 14, 2025
29fc6f5
autonomous todo directive
tlongwell-block Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/goose-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ tempfile = "3"
temp-env = { version = "0.3.6", features = ["async_closure"] }
test-case = "3.3"
tokio = { version = "1.43", features = ["rt", "macros"] }
serial_test = "3.2.0"
2 changes: 1 addition & 1 deletion crates/goose-cli/src/commands/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,7 +871,7 @@ print(\"hello, world\")
format_tool_name("platform__manage_extensions"),
"Platform: Manage Extensions"
);
assert_eq!(format_tool_name("todo__read"), "Todo: Read");
assert_eq!(format_tool_name("todo__write"), "Todo: Write");
}

#[test]
Expand Down
26 changes: 26 additions & 0 deletions crates/goose-cli/src/scenario_tests/scenario_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,32 @@ where
use goose::config::ExtensionConfig;
use tokio::sync::Mutex;

let original_moim = goose::config::Config::global()
.get_param::<bool>("GOOSE_MOIM_ENABLED")
.ok();
goose::config::Config::global()
.set_param("GOOSE_MOIM_ENABLED", false)
.ok();

struct MoimGuard(Option<bool>);
impl Drop for MoimGuard {
fn drop(&mut self) {
match self.0 {
Some(val) => {
goose::config::Config::global()
.set_param("GOOSE_MOIM_ENABLED", val)
.ok();
}
None => {
goose::config::Config::global()
.delete("GOOSE_MOIM_ENABLED")
.ok();
}
}
}
}
let _restore_guard = MoimGuard(original_moim);

if let Ok(path) = dotenv() {
println!("Loaded environment from {:?}", path);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/goose-cli/src/scenario_tests/scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ mod tests {
use crate::scenario_tests::mock_client::WEATHER_TYPE;
use crate::scenario_tests::scenario_runner::run_scenario;
use anyhow::Result;
use serial_test::serial;

#[tokio::test]
#[serial]
async fn test_what_is_your_name() -> Result<()> {
run_scenario(
"what_is_your_name",
Expand All @@ -28,6 +30,7 @@ mod tests {
}

#[tokio::test]
#[serial]
async fn test_weather_tool() -> Result<()> {
// Google tells me it only knows about the weather in the US, so we skip it.
run_scenario(
Expand Down Expand Up @@ -58,6 +61,7 @@ mod tests {
}

#[tokio::test]
#[serial]
async fn test_image_analysis() -> Result<()> {
// Google says it doesn't know about images, the other providers complain about
// the image format, so we only run this for OpenAI and Anthropic.
Expand Down
6 changes: 1 addition & 5 deletions crates/goose-cli/src/session/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) {
"developer__text_editor" => render_text_editor_request(call, debug),
"developer__shell" => render_shell_request(call, debug),
"dynamic_task__create_task" => render_dynamic_task_request(call, debug),
"todo__read" | "todo__write" => render_todo_request(call, debug),
"todo__write" => render_todo_request(call, debug),
_ => render_default_request(call, debug),
},
Err(e) => print_markdown(&e.to_string(), theme),
Expand Down Expand Up @@ -505,13 +505,9 @@ fn render_dynamic_task_request(call: &CallToolRequestParam, debug: bool) {
fn render_todo_request(call: &CallToolRequestParam, _debug: bool) {
print_tool_header(call);

// For todo tools, always show the full content without redaction
if let Some(args) = &call.arguments {
if let Some(Value::String(content)) = args.get("content") {
println!("{}: {}", style("content").dim(), style(content).green());
} else {
// For todo__read, there are no arguments
// Just print an empty line for consistency
}
}
println!();
Expand Down
7 changes: 6 additions & 1 deletion crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,10 +946,15 @@ impl Agent {
}
}

let conversation_with_moim = super::moim::inject_moim(
conversation.clone(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this replaces conversation, so I don't think we need to clone here

&self.extension_manager,
).await;

let mut stream = Self::stream_response_from_provider(
self.provider().await?,
&system_prompt,
conversation.messages(),
conversation_with_moim.messages(),
&tools,
&toolshim_tools,
).await?;
Expand Down
22 changes: 22 additions & 0 deletions crates/goose/src/agents/extension_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,28 @@ impl ExtensionManager {
.get(&name.into())
.map(|ext| ext.get_client())
}

pub async fn collect_moim(&self) -> Option<String> {
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let mut content = format!("<info-msg>\nDatetime: {}\n", timestamp);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a list with join would be more memory effiicient


let extensions = self.extensions.lock().await;
for (name, extension) in extensions.iter() {
if let ExtensionConfig::Platform { .. } = &extension.config {
let client = extension.get_client();
let client_guard = client.lock().await;
if let Some(moim_content) = client_guard.get_moim().await {
tracing::debug!("MOIM content from {}: {} chars", name, moim_content.len());
content.push('\n');
content.push_str(&moim_content);
}
}
}

content.push_str("\n</info-msg>");

Some(content)
}
}

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions crates/goose/src/agents/mcp_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ pub trait McpClientTrait: Send + Sync {
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification>;

fn get_info(&self) -> Option<&InitializeResult>;

async fn get_moim(&self) -> Option<String> {
None
}
}

pub struct GooseClient {
Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod final_output_tool;
mod large_response_handler;
pub mod mcp_client;
pub mod model_selector;
mod moim;
pub mod platform_tools;
pub mod prompt_manager;
pub mod recipe_tools;
Expand Down
147 changes: 147 additions & 0 deletions crates/goose/src/agents/moim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use crate::agents::extension_manager::ExtensionManager;
use crate::conversation::message::Message;
use crate::conversation::{fix_conversation, Conversation};
use rmcp::model::Role;

pub async fn inject_moim(
conversation: Conversation,
extension_manager: &ExtensionManager,
) -> Conversation {
let config = crate::config::Config::global();
if !config.get_param("GOOSE_MOIM_ENABLED").unwrap_or(true) {
return conversation;
}

if let Some(moim) = extension_manager.collect_moim().await {
let mut messages = conversation.messages().clone();
let idx = messages
.iter()
.rposition(|m| m.role == Role::Assistant)
.unwrap_or(messages.len());
messages.insert(idx, Message::user().with_text(moim));

let (fixed, _issues) = fix_conversation(Conversation::new_unvalidated(messages));
return fixed;
}
conversation
}

#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::CallToolRequestParam;

#[tokio::test]
async fn test_moim_injection_before_assistant() {
let em = ExtensionManager::new_without_provider();

let conv = Conversation::new_unvalidated(vec![
Message::user().with_text("Hello"),
Message::assistant().with_text("Hi"),
Message::user().with_text("Bye"),
Message::assistant().with_text("Goodbye"),
]);
let result = inject_moim(conv, &em).await;
let msgs = result.messages();

// MOIM gets inserted before last assistant, then merged with "Bye",
// but then the trailing assistant gets removed by fix_conversation
assert_eq!(msgs.len(), 3);
assert_eq!(msgs[0].content[0].as_text().unwrap(), "Hello");
assert_eq!(msgs[1].content[0].as_text().unwrap(), "Hi");

// The third message should be the merged "Bye" + MOIM
let merged_content = msgs[2]
.content
.iter()
.filter_map(|c| c.as_text())
.collect::<Vec<_>>()
.join("");
assert!(merged_content.contains("Bye"));
assert!(merged_content.contains("<info-msg>"));
}

#[tokio::test]
async fn test_moim_injection_no_assistant() {
let em = ExtensionManager::new_without_provider();

let conv = Conversation::new_unvalidated(vec![Message::user().with_text("Hello")]);
let result = inject_moim(conv, &em).await;

// MOIM gets merged with the existing user message
assert_eq!(result.messages().len(), 1);

let merged_content = result.messages()[0]
.content
.iter()
.filter_map(|c| c.as_text())
.collect::<Vec<_>>()
.join("");
assert!(merged_content.contains("Hello"));
assert!(merged_content.contains("<info-msg>"));
}

#[tokio::test]
async fn test_moim_with_tool_calls() {
let em = ExtensionManager::new_without_provider();

let conv = Conversation::new_unvalidated(vec![
Message::user().with_text("Search for something"),
Message::assistant()
.with_text("I'll search for you")
.with_tool_request(
"search_1",
Ok(CallToolRequestParam {
name: "search".into(),
arguments: None,
}),
),
Message::user().with_tool_response("search_1", Ok(vec![])),
Message::assistant().with_text("Found results"),
]);

let result = inject_moim(conv, &em).await;
let msgs = result.messages();

// MOIM gets inserted as separate message, trailing assistant removed
assert_eq!(msgs.len(), 4);

let tool_request_idx = msgs
.iter()
.position(|m| {
m.content.iter().any(|c| {
matches!(
c,
crate::conversation::message::MessageContent::ToolRequest(_)
)
})
})
.unwrap();

let tool_response_idx = msgs
.iter()
.position(|m| {
m.content.iter().any(|c| {
matches!(
c,
crate::conversation::message::MessageContent::ToolResponse(_)
)
})
})
.unwrap();

// MOIM should be in a separate message after tool response
let moim_msg = &msgs[3];
let has_moim = moim_msg
.content
.iter()
.any(|c| c.as_text().map_or(false, |t| t.contains("<info-msg>")));

assert!(has_moim, "MOIM should be in the last message");
assert_eq!(
tool_response_idx,
tool_request_idx + 1,
"Tool response should immediately follow tool request"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ expression: system_prompt
You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.
goose is being developed as an open-source software project.

The current date is 1970-01-01 00:00:00.

goose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,
claude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).
These models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ expression: system_prompt
You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.
goose is being developed as an open-source software project.

The current date is 1970-01-01 00:00:00.

goose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,
claude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).
These models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ expression: system_prompt
You are a general-purpose AI agent called goose, created by Block, the parent company of Square, CashApp, and Tidal.
goose is being developed as an open-source software project.

The current date is 1970-01-01 00:00:00.

goose uses LLM providers with tool calling capability. You can be used with different language models (gpt-4o,
claude-sonnet-4, o1, llama-3.2, deepseek-r1, etc).
These models have varying knowledge cut-off dates depending on when they were trained, but typically it's between 5-10
Expand Down
Loading
Loading