Skip to content
Merged
Changes from all commits
Commits
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
99 changes: 99 additions & 0 deletions crates/goose/src/execution/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ impl AgentManager {
}
}
extension_results = agent.load_extensions_from_session(&session).await;
if let Some(recipe) = &session.recipe {
agent
.apply_recipe_components(recipe.response.clone(), true)
.await;
Comment on lines +226 to +229

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Render recipe parameters before restoring response schema

This restores the final-output tool from the raw session.recipe.response, but ACP recipe sessions persist the original template recipe and store the selected values separately (new_session.rs stores recipe and user_recipe_values; apply_session_recipe later re-renders before applying). If a valid schema templates a field name such as "{{ output_field }}", any AgentManager-only restore path after eviction advertises and validates the literal placeholder schema instead of the user's rendered schema, forcing the model to return the wrong JSON shape. Please render the stored recipe with session.user_recipe_values before installing the final output tool.

Useful? React with 👍 / 👎.

}
}

if agent.provider().await.is_err() {
Expand Down Expand Up @@ -763,6 +768,100 @@ mod tests {
assert_eq!(agent.goose_mode().await, mode);
}

#[tokio::test]
async fn test_final_output_tool_restored_after_lru_eviction() {
use crate::agents::final_output_tool::FINAL_OUTPUT_TOOL_NAME;
use crate::recipe::{Recipe, Response};
use serde_json::json;

let temp_dir = TempDir::new().unwrap();
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
let agent_config = AgentConfig::new(
Arc::clone(&session_manager),
PermissionManager::instance(),
None,
GooseMode::default(),
false,
GoosePlatform::GooseDesktop,
);
let manager = AgentManager::new(agent_config, Some(1)).await.unwrap();

let session = session_manager
.create_session(
temp_dir.path().to_path_buf(),
"recipe-session".into(),
crate::session::SessionType::User,
GooseMode::default(),
)
.await
.unwrap();

let recipe = Recipe {
version: "1.0.0".into(),
title: "Test".into(),
description: "Test recipe".into(),
response: Some(Response {
json_schema: Some(json!({
"type": "object",
"properties": { "result": { "type": "string" } },
"required": ["result"]
})),
}),
instructions: None,
prompt: None,
extensions: None,
settings: None,
activities: None,
author: None,
parameters: None,
sub_recipes: None,
retry: None,
};

session_manager
.update(&session.id)
.recipe(Some(recipe))
.apply()
.await
.unwrap();

// Fill the cache (capacity 1) then evict it
let agent = manager
.get_or_create_agent(session.id.clone())
.await
.unwrap();
let tools = agent.list_tools(&session.id, None).await;
assert!(
tools
.iter()
.any(|t| t.name.as_ref() == FINAL_OUTPUT_TOOL_NAME),
"final_output_tool must be present on first creation"
);

// Evict by adding a second session
manager
.get_or_create_agent("evict-trigger".into())
.await
.unwrap();
assert!(
!manager.has_session(&session.id).await,
"session should be evicted"
);

// Recreate agent via slow path (create_agent_locked)
let restored_agent = manager
.get_or_create_agent(session.id.clone())
.await
.unwrap();
let tools = restored_agent.list_tools(&session.id, None).await;
assert!(
tools
.iter()
.any(|t| t.name.as_ref() == FINAL_OUTPUT_TOOL_NAME),
"final_output_tool must be restored after LRU eviction"
);
}

#[tokio::test]
async fn test_session_mode_isolation() {
let temp_dir = TempDir::new().unwrap();
Expand Down