Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/v1.0' into wtang/memoryserver
Browse files Browse the repository at this point in the history
* origin/v1.0:
  feat: add a truncating agent (#579)
  feat: Release devtools opening keyboard shortcut when goose window is not focused (#614)
  feat: performance tweaks for partial release (#611)
  Alexhancock/inline code (#612)
  feat: add more tracing logs, trim loaded prompt  (#603)
  feature: new welcome page and real keychain  (#604)
  fix: model names in UI API key warning (#609)
  feat: map non-ok responses from provider to human readable error msgs (#610)
  fix: update BufReader capacity to 2MB (#605)
  fix: escape double quotes in error part (#602)
  refactor: remove unused errors in mcp-client (#598)
  add more context from mcp server errors (#599)
  fix: error formatting for vercel data format (#597)
  feat: Add endpoint to store secrets in keychain (#595)
  • Loading branch information
salman1993 committed Jan 16, 2025
2 parents eec5d79 + ee0359a commit c4f831e
Show file tree
Hide file tree
Showing 61 changed files with 1,558 additions and 1,606 deletions.
9 changes: 2 additions & 7 deletions .github/workflows/ci-desktop.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Desktop App CI
name: Desktop App Lint

on:
push:
Expand Down Expand Up @@ -27,9 +27,4 @@ jobs:

- name: Run Lint check
run: npm run lint:check
working-directory: ui/desktop


- name: Run E2E tests
run: npm run test-e2e
working-directory: ui/desktop
working-directory: ui/desktop
47 changes: 46 additions & 1 deletion .github/workflows/desktop-app-release.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Desktop App Release
name: Desktop App Build

on:
push:
Expand Down Expand Up @@ -103,6 +103,51 @@ jobs:
name: Goose-darwin-arm64
path: ui/desktop/out/Goose-darwin-arm64/Goose.zip

- name: Quick launch test (macOS)
run: |
# Ensure no quarantine attributes (if needed)
xattr -cr "ui/desktop/out/Goose-darwin-arm64/Goose.app"
echo "Opening Goose.app..."
open -g "ui/desktop/out/Goose-darwin-arm64/Goose.app"
# Give the app a few seconds to start and write logs
sleep 5
# Check if it's running
if pgrep -f "Goose.app/Contents/MacOS/Goose" > /dev/null; then
echo "App appears to be running."
else
echo "App did not stay open. Possible crash or startup error."
exit 1
fi
LOGFILE="$HOME/Library/Application Support/Goose/logs/main.log"
# Print the log and verify "Starting goosed"
if [ -f "$LOGFILE" ]; then
echo "===== Log file contents ====="
cat "$LOGFILE"
echo "============================="
# Check for evidence it ran in the logs:
if grep -F "ChatWindow loaded" "$LOGFILE"; then
echo "Confirmed: 'Starting goosed' found in logs!"
else
echo "Did not find 'Starting goosed' in logs. Failing..."
exit 1
fi
else
echo "No log file found at $LOGFILE. Exiting with failure."
exit 1
fi
# Kill the app to clean up
pkill -f "Goose.app/Contents/MacOS/Goose"
release:
name: Release
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ Cargo.lock
# Hermit
/.hermit/
/bin/

debug_*.txt
8 changes: 5 additions & 3 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::profile::{
find_existing_profile, get_provider_config, profile_path, save_profile, Profile,
find_existing_profile, profile_path, save_profile, set_provider_env_vars, Profile,
};
use cliclack::spinner;
use console::style;
Expand Down Expand Up @@ -112,11 +112,13 @@ pub async fn handle_configure(
estimate_factor: None,
};

// Set environment variables for provider configuration
set_provider_env_vars(&provider_name, &profile);

// Confirm everything is configured correctly by calling a model!
let provider_config = get_provider_config(&provider_name, profile.clone());
let spin = spinner();
spin.start("Checking your configuration...");
let provider = factory::get_provider(provider_config).unwrap();
let provider = factory::get_provider(&provider_name).unwrap();
let message = Message::user().with_text("Please give a nice welcome messsage (one sentence) and let them know they are all set to use this agent");
let result = provider.complete("You are an AI agent called Goose. You use tools of connected systems to solve problems.", &[message], &[]).await;

Expand Down
3 changes: 3 additions & 0 deletions crates/goose-cli/src/commands/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use mcp_server::{BoundedService, ByteTransport, Server};
use tokio::io::{stdin, stdout};

pub async fn run_server(name: &str) -> Result<()> {
// Initialize logging
crate::logging::setup_logging(Some(&format!("mcp-{name}")))?;

tracing::info!("Starting MCP server");

let router: Option<Box<dyn BoundedService>> = match name {
Expand Down
7 changes: 4 additions & 3 deletions crates/goose-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rand::{distributions::Alphanumeric, Rng};
use std::path::{Path, PathBuf};
use std::process;

use crate::profile::{get_provider_config, load_profiles, Profile};
use crate::profile::{load_profiles, set_provider_env_vars, Profile};
use crate::prompt::rustyline::RustylinePrompt;
use crate::prompt::Prompt;
use crate::session::{ensure_session_dir, get_most_recent_session, Session};
Expand Down Expand Up @@ -43,9 +43,10 @@ pub async fn build_session<'a>(

let loaded_profile = load_profile(profile);

let provider_config = get_provider_config(&loaded_profile.provider, (*loaded_profile).clone());
// Set environment variables for provider configuration
set_provider_env_vars(&loaded_profile.provider, &loaded_profile);

let provider = factory::get_provider(provider_config).unwrap();
let provider = factory::get_provider(&loaded_profile.provider).unwrap();

let mut agent =
AgentFactory::create(agent_version.as_deref().unwrap_or("default"), provider).unwrap();
Expand Down
21 changes: 15 additions & 6 deletions crates/goose-cli/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,21 @@ fn get_log_directory() -> Result<PathBuf> {
/// - File-based logging with JSON formatting (DEBUG level)
/// - Console output for development (INFO level)
/// - Optional Langfuse integration (DEBUG level)
pub fn setup_logging(session_name: Option<&str>) -> Result<()> {
pub fn setup_logging(name: Option<&str>) -> Result<()> {
// Set up file appender for goose module logs
let log_dir = get_log_directory()?;
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();

// Create log file name by prefixing with timestamp
let log_filename = if name.is_some() {
format!("{}-{}.log", timestamp, name.unwrap())
} else {
format!("{}.log", timestamp)
};

// Create non-rolling file appender for detailed logs
let file_appender = tracing_appender::rolling::RollingFileAppender::new(
Rotation::NEVER,
log_dir,
&format!("{}.log", session_name.unwrap_or(&timestamp)),
);
let file_appender =
tracing_appender::rolling::RollingFileAppender::new(Rotation::NEVER, log_dir, log_filename);

// Create JSON file logging layer with all logs (DEBUG and above)
let file_layer = fmt::layer()
Expand All @@ -68,6 +72,11 @@ pub fn setup_logging(session_name: Option<&str>) -> Result<()> {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
// Set default levels for different modules
EnvFilter::new("")
// Set mcp-server module to DEBUG
.add_directive("mcp_server=debug".parse().unwrap())
// Set mcp-client to DEBUG
.add_directive("mcp_client=debug".parse().unwrap())
// Set goose module to DEBUG
.add_directive("goose=debug".parse().unwrap())
// Set goose-cli to INFO
.add_directive("goose_cli=info".parse().unwrap())
Expand Down
116 changes: 30 additions & 86 deletions crates/goose-cli/src/profile.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
use anyhow::Result;
use goose::key_manager::{get_keyring_secret, KeyRetrievalStrategy};
use goose::providers::configs::{
AnthropicProviderConfig, DatabricksAuth, DatabricksProviderConfig, GoogleProviderConfig,
GroqProviderConfig, ModelConfig, OllamaProviderConfig, OpenAiProviderConfig, ProviderConfig,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
Expand Down Expand Up @@ -67,87 +62,41 @@ pub fn has_no_profiles() -> Result<bool> {
load_profiles().map(|profiles| Ok(profiles.is_empty()))?
}

pub fn get_provider_config(provider_name: &str, profile: Profile) -> ProviderConfig {
let model_config = ModelConfig::new(profile.model)
.with_context_limit(profile.context_limit)
.with_temperature(profile.temperature)
.with_max_tokens(profile.max_tokens)
.with_estimate_factor(profile.estimate_factor);

match provider_name.to_lowercase().as_str() {
"openai" => {
// TODO error propagation throughout the CLI
let api_key = get_keyring_secret("OPENAI_API_KEY", KeyRetrievalStrategy::Both)
.expect("OPENAI_API_KEY not available in env or the keychain\nSet an env var or rerun `goose configure`");

ProviderConfig::OpenAi(OpenAiProviderConfig {
host: "https://api.openai.com".to_string(),
api_key,
model: model_config,
})
}
"databricks" => {
let host = get_keyring_secret("DATABRICKS_HOST", KeyRetrievalStrategy::Both)
.expect("DATABRICKS_HOST not available in env or the keychain\nSet an env var or rerun `goose configure`");

ProviderConfig::Databricks(DatabricksProviderConfig {
host: host.clone(),
// TODO revisit configuration
auth: DatabricksAuth::oauth(host),
model: model_config,
image_format: goose::providers::utils::ImageFormat::Anthropic,
})
}
"ollama" => {
let host = get_keyring_secret("OLLAMA_HOST", KeyRetrievalStrategy::Both)
.expect("OLLAMA_HOST not available in env or the keychain\nSet an env var or rerun `goose configure`");

ProviderConfig::Ollama(OllamaProviderConfig {
host,
model: model_config,
})
}
"anthropic" => {
let api_key = get_keyring_secret("ANTHROPIC_API_KEY", KeyRetrievalStrategy::Both)
.expect("ANTHROPIC_API_KEY not available in env or the keychain\nSet an env var or rerun `goose configure`");

ProviderConfig::Anthropic(AnthropicProviderConfig {
host: "https://api.anthropic.com".to_string(),
api_key,
model: model_config,
})
}
"google" => {
let api_key = get_keyring_secret("GOOGLE_API_KEY", KeyRetrievalStrategy::Both)
.expect("GOOGLE_API_KEY not available in env or the keychain\nSet an env var or rerun `goose configure`");

ProviderConfig::Google(GoogleProviderConfig {
host: "https://generativelanguage.googleapis.com".to_string(),
api_key,
model: model_config,
})
}
"groq" => {
let api_key = get_keyring_secret("GROQ_API_KEY", KeyRetrievalStrategy::Both)
.expect("GROQ_API_KEY not available in env or the keychain\nSet an env var or rerun `goose configure`");

ProviderConfig::Groq(GroqProviderConfig {
host: "https://api.groq.com".to_string(),
api_key,
model: model_config,
})
}
_ => panic!("Invalid provider name"),
pub fn set_provider_env_vars(provider_name: &str, profile: &Profile) {
if let Some(temp) = profile.temperature {
std::env::set_var(
format!("{}_TEMPERATURE", provider_name.to_uppercase()),
temp.to_string(),
);
}
if let Some(limit) = profile.context_limit {
std::env::set_var(
format!("{}_CONTEXT_LIMIT", provider_name.to_uppercase()),
limit.to_string(),
);
}
if let Some(tokens) = profile.max_tokens {
std::env::set_var(
format!("{}_MAX_TOKENS", provider_name.to_uppercase()),
tokens.to_string(),
);
}
if let Some(factor) = profile.estimate_factor {
std::env::set_var(
format!("{}_ESTIMATE_FACTOR", provider_name.to_uppercase()),
factor.to_string(),
);
}
std::env::set_var(
format!("{}_MODEL", provider_name.to_uppercase()),
&profile.model,
);
}

#[cfg(test)]
mod tests {
use goose::providers::configs::ProviderModelConfig;

use crate::test_helpers::run_profile_with_tmp_dir;

use super::*;
use crate::test_helpers::run_profile_with_tmp_dir;

#[test]
fn test_partial_profile_config() -> Result<()> {
Expand All @@ -172,12 +121,7 @@ mod tests {
assert_eq!(profile.max_tokens, None);
assert_eq!(profile.estimate_factor, None);

let provider_config = get_provider_config(&profile.provider, profile.clone());

if let ProviderConfig::Databricks(config) = provider_config {
assert_eq!(config.model_config().estimate_factor(), 0.8);
assert_eq!(config.model_config().context_limit(), 50_000);
}
// Skip provider creation test since it requires environment variables
Ok(())
})
}
Expand Down
25 changes: 9 additions & 16 deletions crates/goose-mcp/src/developer2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ impl Developer2Router {
The `command` parameter specifies the operation to perform. Allowed options are:
- `view`: View the content of a file.
- `create`: Create a new file with the given content (it will fail if the file already exists)
- `write`: Create or overwrite a file with the given content
- `str_replace`: Replace a string in a file with a new string.
- `undo_edit`: Undo the last edit made to a file.
To use the create command, you must specify `file_text` which will become the content of the new file.
To use the write command, you must specify `file_text` which will become the new content of the file. Be careful with
existing files! This is a full overwrite, so you must include everything - not just sections you are modifying.
To use the str_replace command, you must specify both `old_str` and `new_str` - the `old_str` needs to exactly match one
unique section of the original file, including any whitespace. Make sure to include enough context that the match is not
Expand All @@ -102,8 +103,8 @@ impl Developer2Router {
},
"command": {
"type": "string",
"enum": ["view", "create", "str_replace", "undo_edit"],
"description": "Allowed options are: `view`, `create`, `str_replace`, undo_edit`."
"enum": ["view", "write", "str_replace", "undo_edit"],
"description": "Allowed options are: `view`, `write`, `str_replace`, undo_edit`."
},
"old_str": {"type": "string"},
"new_str": {"type": "string"},
Expand Down Expand Up @@ -261,15 +262,15 @@ impl Developer2Router {

match command {
"view" => self.text_editor_view(&path).await,
"create" => {
"write" => {
let file_text = params
.get("file_text")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'file_text' parameter".into())
})?;

self.text_editor_create(&path, file_text).await
self.text_editor_write(&path, file_text).await
}
"str_replace" => {
let old_str = params
Expand Down Expand Up @@ -360,19 +361,11 @@ impl Developer2Router {
}
}

async fn text_editor_create(
async fn text_editor_write(
&self,
path: &PathBuf,
file_text: &str,
) -> Result<Vec<Content>, ToolError> {
// Check if file already exists
if path.exists() {
return Err(ToolError::InvalidParameters(format!(
"File '{}' already exists - you will need to edit it with the `str_replace` command",
path.display()
)));
}

// Write to the file
std::fs::write(path, file_text)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write file: {}", e)))?;
Expand Down Expand Up @@ -409,7 +402,7 @@ impl Developer2Router {
// Check if file exists and is active
if !path.exists() {
return Err(ToolError::InvalidParameters(format!(
"File '{}' does not exist, you can write a new file with the `create` command",
"File '{}' does not exist, you can write a new file with the `write` command",
path.display()
)));
}
Expand Down
Loading

0 comments on commit c4f831e

Please sign in to comment.