Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -420,6 +420,8 @@ derive_utoipa!(IconTheme as IconThemeSchema);
|
||||
super::routes::prompts::save_prompt,
|
||||
super::routes::prompts::reset_prompt,
|
||||
super::routes::agent::start_agent,
|
||||
super::routes::agent::harness_bootstrap,
|
||||
super::routes::agent::harness_remember,
|
||||
super::routes::agent::resume_agent,
|
||||
super::routes::agent::stop_agent,
|
||||
super::routes::agent::restart_agent,
|
||||
@@ -646,6 +648,10 @@ derive_utoipa!(IconTheme as IconThemeSchema);
|
||||
super::routes::agent::ImportAppRequest,
|
||||
super::routes::agent::ImportAppResponse,
|
||||
super::routes::agent::StartAgentRequest,
|
||||
super::routes::agent::HarnessBootstrapRequest,
|
||||
super::routes::agent::HarnessBootstrapResponse,
|
||||
super::routes::agent::HarnessRememberRequest,
|
||||
super::routes::agent::HarnessRememberResponse,
|
||||
super::routes::agent::ResumeAgentRequest,
|
||||
super::routes::agent::StopAgentRequest,
|
||||
super::routes::agent::RestartAgentRequest,
|
||||
|
||||
@@ -15,6 +15,7 @@ use goose::agents::{Container, ExtensionLoadResult};
|
||||
use goose::goose_apps::{fetch_mcp_apps, GooseApp, McpAppCache};
|
||||
|
||||
use base64::Engine;
|
||||
use goose::agents::platform_extensions::{chatrecall, projectmemory, PLATFORM_EXTENSIONS};
|
||||
use goose::agents::reply_parts::is_tool_visible_to_app;
|
||||
use goose::agents::ExtensionConfig;
|
||||
use goose::config::resolve_extensions_for_new_session;
|
||||
@@ -33,8 +34,10 @@ use rmcp::model::CallToolRequestParams;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{error, warn};
|
||||
|
||||
@@ -75,6 +78,8 @@ pub struct StartAgentRequest {
|
||||
recipe_deeplink: Option<String>,
|
||||
#[serde(default)]
|
||||
extension_overrides: Option<Vec<ExtensionConfig>>,
|
||||
#[serde(default)]
|
||||
enable_context_memory: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
@@ -177,6 +182,223 @@ pub struct ResumeAgentResponse {
|
||||
pub extension_results: Option<Vec<ExtensionLoadResult>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HarnessBootstrapRequest {
|
||||
session_id: String,
|
||||
query: Option<String>,
|
||||
#[serde(default)]
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HarnessBootstrapResponse {
|
||||
summary: String,
|
||||
source: String,
|
||||
refreshed: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HarnessRememberRequest {
|
||||
session_id: String,
|
||||
content: String,
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HarnessRememberResponse {
|
||||
remembered: bool,
|
||||
}
|
||||
|
||||
const DEFAULT_BOOTSTRAP_QUERY: &str =
|
||||
"project architecture conventions current goals decisions risks and recent work";
|
||||
const DEFAULT_BOOTSTRAP_MAX_CHARS: usize = 6_000;
|
||||
|
||||
fn platform_extension_config(name: &str) -> Option<ExtensionConfig> {
|
||||
PLATFORM_EXTENSIONS
|
||||
.get(name)
|
||||
.map(|definition| ExtensionConfig::Platform {
|
||||
name: definition.name.to_string(),
|
||||
description: definition.description.to_string(),
|
||||
display_name: Some(definition.display_name.to_string()),
|
||||
bundled: Some(true),
|
||||
available_tools: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn append_context_memory_extensions(extensions: &mut Vec<ExtensionConfig>) {
|
||||
for name in [chatrecall::EXTENSION_NAME, projectmemory::EXTENSION_NAME] {
|
||||
if extensions.iter().any(|extension| extension.name() == name) {
|
||||
continue;
|
||||
}
|
||||
if let Some(extension) = platform_extension_config(name) {
|
||||
extensions.push(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bootstrap_max_chars() -> usize {
|
||||
std::env::var("GOOSE_HARNESS_BOOTSTRAP_MAX_CHARS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.filter(|value| *value >= 1_000)
|
||||
.unwrap_or(DEFAULT_BOOTSTRAP_MAX_CHARS)
|
||||
}
|
||||
|
||||
fn truncate_chars(value: &str, max_chars: usize) -> String {
|
||||
value.chars().take(max_chars).collect()
|
||||
}
|
||||
|
||||
fn filter_recall_output(output: &str, max_chars: usize) -> String {
|
||||
let filtered = output
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let lower = line.to_ascii_lowercase();
|
||||
!line.trim().is_empty()
|
||||
&& !lower.contains("\trisk\t[monitor]")
|
||||
&& !lower.contains("\tvault-note\tnew project")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
truncate_chars(filtered.trim(), max_chars)
|
||||
}
|
||||
|
||||
fn harness_binary() -> PathBuf {
|
||||
if let Some(path) = std::env::var_os("GOOSE_HARNESS_BIN") {
|
||||
return PathBuf::from(path);
|
||||
}
|
||||
let home = std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_default();
|
||||
home.join(".codex/harness/bin/harness")
|
||||
}
|
||||
|
||||
async fn recall_harness(repo: &str, query: &str, working_dir: &Path) -> Option<String> {
|
||||
let output = timeout(
|
||||
Duration::from_secs(8),
|
||||
Command::new(harness_binary())
|
||||
.arg("recall")
|
||||
.arg("--repo")
|
||||
.arg(repo)
|
||||
.arg("--query")
|
||||
.arg(query)
|
||||
.current_dir(working_dir)
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
tracing::warn!(
|
||||
"Harness recall failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
let recalled = filter_recall_output(
|
||||
&String::from_utf8_lossy(&output.stdout),
|
||||
bootstrap_max_chars() / 2,
|
||||
);
|
||||
(!recalled.is_empty()).then_some(recalled)
|
||||
}
|
||||
|
||||
async fn remember_harness(
|
||||
repo: &str,
|
||||
title: &str,
|
||||
content: &str,
|
||||
working_dir: &Path,
|
||||
) -> Result<(), String> {
|
||||
let output = timeout(
|
||||
Duration::from_secs(8),
|
||||
Command::new(harness_binary())
|
||||
.arg("remember")
|
||||
.arg("--repo")
|
||||
.arg(repo)
|
||||
.arg("--kind")
|
||||
.arg("conversation_memory")
|
||||
.arg("--title")
|
||||
.arg(title)
|
||||
.arg("--content")
|
||||
.arg(content)
|
||||
.current_dir(working_dir)
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "Harness remember timed out".to_string())?
|
||||
.map_err(|error| format!("Failed to start harness remember: {error}"))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_project_context(working_dir: &Path, max_chars: usize) -> Option<String> {
|
||||
let mut sections = Vec::new();
|
||||
let mut remaining = max_chars;
|
||||
|
||||
for filename in ["AGENTS.md", ".tkmindhints", ".goosehints", "CLAUDE.md"] {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
let path = working_dir.join(filename);
|
||||
let Ok(content) = tokio::fs::read_to_string(&path).await else {
|
||||
continue;
|
||||
};
|
||||
let content = truncate_chars(content.trim(), remaining);
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
remaining = remaining.saturating_sub(content.chars().count());
|
||||
sections.push(format!("## {filename}\n{content}"));
|
||||
}
|
||||
|
||||
(!sections.is_empty()).then(|| sections.join("\n\n"))
|
||||
}
|
||||
|
||||
async fn build_project_bootstrap(working_dir: &Path, query: &str) -> (String, String) {
|
||||
let max_chars = bootstrap_max_chars();
|
||||
let repo = working_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or("unknown");
|
||||
|
||||
let recalled = recall_harness(repo, query, working_dir).await;
|
||||
let recall_len = recalled
|
||||
.as_deref()
|
||||
.map(|value| value.chars().count())
|
||||
.unwrap_or_default();
|
||||
let project_context =
|
||||
read_project_context(working_dir, max_chars.saturating_sub(recall_len)).await;
|
||||
|
||||
let mut sections = Vec::new();
|
||||
let mut sources = Vec::new();
|
||||
if let Some(recalled) = recalled {
|
||||
sections.push(format!("## Recalled project knowledge\n{recalled}"));
|
||||
sources.push("harness");
|
||||
}
|
||||
if let Some(project_context) = project_context {
|
||||
sections.push(format!("## Project instructions\n{project_context}"));
|
||||
sources.push("project_files");
|
||||
}
|
||||
|
||||
(
|
||||
truncate_chars(§ions.join("\n\n"), max_chars),
|
||||
if sources.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
sources.join("+")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct RestartAgentResponse {
|
||||
pub extension_results: Vec<ExtensionLoadResult>,
|
||||
@@ -207,6 +429,7 @@ async fn start_agent(
|
||||
recipe_id,
|
||||
recipe_deeplink,
|
||||
extension_overrides,
|
||||
enable_context_memory,
|
||||
} = payload;
|
||||
|
||||
let original_recipe = if let Some(deeplink) = recipe_deeplink {
|
||||
@@ -267,8 +490,11 @@ async fn start_agent(
|
||||
let recipe_extensions = original_recipe
|
||||
.as_ref()
|
||||
.and_then(|r| r.extensions.as_deref());
|
||||
let extensions_to_use =
|
||||
let mut extensions_to_use =
|
||||
resolve_extensions_for_new_session(recipe_extensions, extension_overrides);
|
||||
if enable_context_memory {
|
||||
append_context_memory_extensions(&mut extensions_to_use);
|
||||
}
|
||||
|
||||
let mut extension_data = session.extension_data.clone();
|
||||
let extensions_state = EnabledExtensionsState::new(extensions_to_use);
|
||||
@@ -1359,9 +1585,166 @@ async fn import_app(
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/harness_bootstrap",
|
||||
request_body = HarnessBootstrapRequest,
|
||||
responses(
|
||||
(status = 200, description = "Project memory bootstrap completed", body = HarnessBootstrapResponse),
|
||||
(status = 404, description = "Session not found", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
tag = "Agent"
|
||||
)]
|
||||
async fn harness_bootstrap(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<HarnessBootstrapRequest>,
|
||||
) -> Result<Json<HarnessBootstrapResponse>, ErrorResponse> {
|
||||
let mut session = state
|
||||
.session_manager()
|
||||
.get_session(&request.session_id, false)
|
||||
.await
|
||||
.map_err(|_| ErrorResponse::not_found("Session not found"))?;
|
||||
|
||||
let mut enabled_extensions = EnabledExtensionsState::extensions_or_default(
|
||||
Some(&session.extension_data),
|
||||
Config::global(),
|
||||
);
|
||||
let previous_extension_count = enabled_extensions.len();
|
||||
append_context_memory_extensions(&mut enabled_extensions);
|
||||
if enabled_extensions.len() != previous_extension_count {
|
||||
EnabledExtensionsState::new(enabled_extensions)
|
||||
.to_extension_data(&mut session.extension_data)
|
||||
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
|
||||
state
|
||||
.session_manager()
|
||||
.update(&request.session_id)
|
||||
.extension_data(session.extension_data.clone())
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
|
||||
|
||||
let agent = state
|
||||
.get_agent_for_route(request.session_id.clone())
|
||||
.await
|
||||
.map_err(ErrorResponse::from)?;
|
||||
agent.load_extensions_from_session(&session).await;
|
||||
}
|
||||
|
||||
if !request.force {
|
||||
if let Some(memory) =
|
||||
projectmemory::ProjectMemoryState::from_extension_data(&session.extension_data)
|
||||
{
|
||||
if !memory.summary.trim().is_empty() {
|
||||
return Ok(Json(HarnessBootstrapResponse {
|
||||
summary: memory.summary,
|
||||
source: memory.source.unwrap_or_else(|| "session".to_string()),
|
||||
refreshed: false,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let query = request
|
||||
.query
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|query| !query.is_empty())
|
||||
.unwrap_or(DEFAULT_BOOTSTRAP_QUERY);
|
||||
let (summary, source) = build_project_bootstrap(&session.working_dir, query).await;
|
||||
|
||||
if summary.is_empty() {
|
||||
return Ok(Json(HarnessBootstrapResponse {
|
||||
summary,
|
||||
source,
|
||||
refreshed: false,
|
||||
}));
|
||||
}
|
||||
|
||||
let memory = projectmemory::ProjectMemoryState {
|
||||
summary: summary.clone(),
|
||||
source: Some(source.clone()),
|
||||
updated_at: Some(chrono::Utc::now().to_rfc3339()),
|
||||
};
|
||||
memory
|
||||
.to_extension_data(&mut session.extension_data)
|
||||
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
|
||||
|
||||
state
|
||||
.session_manager()
|
||||
.update(&request.session_id)
|
||||
.extension_data(session.extension_data)
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|error| ErrorResponse::internal(error.to_string()))?;
|
||||
|
||||
Ok(Json(HarnessBootstrapResponse {
|
||||
summary,
|
||||
source,
|
||||
refreshed: true,
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/harness_remember",
|
||||
request_body = HarnessRememberRequest,
|
||||
responses(
|
||||
(status = 200, description = "Conversation context saved to harness", body = HarnessRememberResponse),
|
||||
(status = 400, description = "Invalid memory content", body = ErrorResponse),
|
||||
(status = 404, description = "Session not found", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
tag = "Agent"
|
||||
)]
|
||||
async fn harness_remember(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<HarnessRememberRequest>,
|
||||
) -> Result<Json<HarnessRememberResponse>, ErrorResponse> {
|
||||
let session = state
|
||||
.session_manager()
|
||||
.get_session(&request.session_id, false)
|
||||
.await
|
||||
.map_err(|_| ErrorResponse::not_found("Session not found"))?;
|
||||
let content = request.content.trim();
|
||||
if content.is_empty() {
|
||||
return Err(ErrorResponse::bad_request("Memory content cannot be empty"));
|
||||
}
|
||||
if content.chars().count() > bootstrap_max_chars() {
|
||||
return Err(ErrorResponse::bad_request("Memory content is too long"));
|
||||
}
|
||||
|
||||
let repo = session
|
||||
.working_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or("unknown");
|
||||
let title = request
|
||||
.title
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|title| !title.is_empty())
|
||||
.unwrap_or("H5 conversation memory");
|
||||
|
||||
remember_harness(repo, title, content, &session.working_dir)
|
||||
.await
|
||||
.map_err(ErrorResponse::internal)?;
|
||||
|
||||
Ok(Json(HarnessRememberResponse { remembered: true }))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/agent/start", post(start_agent))
|
||||
.route("/agent/harness_bootstrap", post(harness_bootstrap))
|
||||
.route("/agent/harness_remember", post(harness_remember))
|
||||
.route("/agent/resume", post(resume_agent))
|
||||
.route("/agent/restart", post(restart_agent))
|
||||
.route("/agent/update_working_dir", post(update_working_dir))
|
||||
@@ -1410,6 +1793,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_memory_extensions_are_added_once() {
|
||||
let mut extensions = vec![frontend_extension()];
|
||||
|
||||
append_context_memory_extensions(&mut extensions);
|
||||
append_context_memory_extensions(&mut extensions);
|
||||
|
||||
assert_eq!(
|
||||
extensions
|
||||
.iter()
|
||||
.filter(|extension| extension.name() == chatrecall::EXTENSION_NAME)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
extensions
|
||||
.iter()
|
||||
.filter(|extension| extension.name() == projectmemory::EXTENSION_NAME)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recall_filter_removes_monitor_noise_and_limits_output() {
|
||||
let output = concat!(
|
||||
"harness-db\trisk\t[monitor] HTTP health\n",
|
||||
"harness-db\tarchitecture\tUseful project fact\n",
|
||||
);
|
||||
|
||||
let filtered = filter_recall_output(output, 40);
|
||||
assert!(!filtered.contains("[monitor]"));
|
||||
assert!(filtered.starts_with("harness-db\tarchitecture"));
|
||||
assert!(filtered.chars().count() <= 40);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn frontend_extensions_are_listed_and_rejected_cleanly_by_call_tool() {
|
||||
let state = AppState::new(true).await.unwrap();
|
||||
|
||||
@@ -33,6 +33,7 @@ use serde_yaml;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -940,7 +941,17 @@ pub async fn get_provider_models(
|
||||
let model_config = ModelConfig::new(&metadata.default_model)?.with_canonical_limits(&name);
|
||||
let provider = goose::providers::create(&name, model_config, Vec::new()).await?;
|
||||
|
||||
let models_result = provider.fetch_recommended_model_info().await;
|
||||
let models_result = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
provider.fetch_recommended_model_info(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ErrorResponse::bad_request(format!(
|
||||
"Timed out fetching models for provider '{}'. The provider may be unreachable.",
|
||||
name
|
||||
))
|
||||
})?;
|
||||
|
||||
match models_result {
|
||||
Ok(models) => Ok(Json(models)),
|
||||
|
||||
Reference in New Issue
Block a user