Skip to content
Merged
Show file tree
Hide file tree
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
23 changes: 13 additions & 10 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use goose::conversation::message::Message;
use goose::conversation::Conversation;
use goose::model::ModelConfig;
use goose::providers::create;
use goose::recipe::Response;
use goose::recipe::{Recipe, Response};
use goose::session;
use goose::session::SessionMetadata;
use goose::{
Expand Down Expand Up @@ -81,6 +81,7 @@ pub struct UpdateRouterToolSelectorRequest {
#[derive(Deserialize, utoipa::ToSchema)]
pub struct StartAgentRequest {
working_dir: String,
recipe: Option<Recipe>,
}

#[derive(Deserialize, utoipa::ToSchema)]
Expand Down Expand Up @@ -116,15 +117,8 @@ async fn start_agent(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(payload): Json<StartAgentRequest>,
) -> Result<Json<StartAgentResponse>, (StatusCode, Json<ErrorResponse>)> {
verify_secret_key(&headers, &state).map_err(|_| {
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Unauthorized - Invalid or missing API key".to_string(),
}),
)
})?;
) -> Result<Json<StartAgentResponse>, StatusCode> {
verify_secret_key(&headers, &state)?;

state.reset().await;

Expand All @@ -143,9 +137,18 @@ async fn start_agent(
accumulated_input_tokens: Some(0),
accumulated_output_tokens: Some(0),
extension_data: Default::default(),
recipe: payload.recipe,
};

let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) {
Ok(path) => path,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};

let conversation = Conversation::empty();
session::storage::save_messages_with_metadata(&session_path, &metadata, &conversation)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

Ok(Json(StartAgentResponse {
session_id,
metadata,
Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/context_mgmt/auto_compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ mod tests {
accumulated_input_tokens: Some(50),
accumulated_output_tokens: Some(50),
extension_data: crate::session::ExtensionData::new(),
recipe: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,7 @@ async fn run_scheduled_job_internal(
accumulated_input_tokens: None,
accumulated_output_tokens: None,
extension_data: crate::session::ExtensionData::new(),
recipe: None,
};
if let Err(e_fb) = crate::session::storage::save_messages_with_metadata(
&session_file_path,
Expand Down
6 changes: 6 additions & 0 deletions crates/goose/src/session/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use crate::conversation::message::Message;
use crate::conversation::Conversation;
use crate::providers::base::Provider;
use crate::recipe::Recipe;
use crate::session::extension_data::ExtensionData;
use crate::utils::safe_truncate;
use anyhow::Result;
Expand Down Expand Up @@ -69,6 +70,8 @@ pub struct SessionMetadata {
/// Extension data containing extension states
#[serde(default)]
pub extension_data: ExtensionData,

pub recipe: Option<Recipe>,
}

// Custom deserializer to handle old sessions without working_dir
Expand All @@ -91,6 +94,7 @@ impl<'de> Deserialize<'de> for SessionMetadata {
working_dir: Option<PathBuf>,
#[serde(default)]
extension_data: ExtensionData,
recipe: Option<Recipe>,
}

let helper = Helper::deserialize(deserializer)?;
Expand All @@ -113,6 +117,7 @@ impl<'de> Deserialize<'de> for SessionMetadata {
accumulated_output_tokens: helper.accumulated_output_tokens,
working_dir,
extension_data: helper.extension_data,
recipe: helper.recipe,
})
}
}
Expand All @@ -138,6 +143,7 @@ impl SessionMetadata {
accumulated_input_tokens: None,
accumulated_output_tokens: None,
extension_data: ExtensionData::new(),
recipe: None,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/goose/tests/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,5 +412,6 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) ->
accumulated_input_tokens: Some(50),
accumulated_output_tokens: Some(50),
extension_data: Default::default(),
recipe: None,
}
}
16 changes: 16 additions & 0 deletions ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3364,6 +3364,14 @@
"description": "The number of output tokens used in the session. Retrieved from the provider's last usage.",
"nullable": true
},
"recipe": {
"allOf": [
{
"$ref": "#/components/schemas/Recipe"
}
],
"nullable": true
},
"schedule_id": {
"type": "string",
"description": "ID of the schedule that triggered this session, if any",
Expand Down Expand Up @@ -3416,6 +3424,14 @@
"working_dir"
],
"properties": {
"recipe": {
"allOf": [
{
"$ref": "#/components/schemas/Recipe"
}
],
"nullable": true
},
"working_dir": {
"type": "string"
}
Expand Down
16 changes: 10 additions & 6 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,10 @@ export default function App() {
const [viewType, setViewType] = useState<string | null>(null);
const [resumeSessionId, setResumeSessionId] = useState<string | null>(null);

const [recipeFromAppConfig, setRecipeFromAppConfig] = useState<Recipe | null>(
(window.appConfig?.get('recipe') as Recipe) || null
);

useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);

Expand Down Expand Up @@ -349,6 +353,7 @@ export default function App() {
const resetChatIfNecessary = useCallback(() => {
if (chat.messages.length > 0) {
setResumeSessionId(null);
setRecipeFromAppConfig(null);
resetChat();
}
}, [resetChat, chat.messages.length]);
Expand All @@ -371,11 +376,9 @@ export default function App() {
return;
}

const recipeConfig = (window.appConfig?.get('recipe') || undefined) as Recipe;

const stateData: PairRouteState = {
resumeSessionId: resumeSessionId || undefined,
recipeConfig: recipeConfig,
recipeConfig: recipeFromAppConfig || undefined,
};
(async () => {
try {
Expand All @@ -389,7 +392,7 @@ export default function App() {
}
})();

if (resumeSessionId || (recipeConfig && typeof recipeConfig === 'object')) {
if (resumeSessionId || recipeFromAppConfig) {
window.location.hash = '#/pair';
window.history.replaceState(stateData, '', '#/pair');
return;
Expand All @@ -401,9 +404,9 @@ export default function App() {
window.history.replaceState({}, '', '#/');
}
} else {
if (viewType === 'recipeEditor' && recipeConfig) {
if (viewType === 'recipeEditor' && recipeFromAppConfig) {
window.location.hash = '#/recipe-editor';
window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor');
window.history.replaceState({ config: recipeFromAppConfig }, '', '#/recipe-editor');
} else {
const routeMap: Record<string, string> = {
chat: '#/',
Expand All @@ -427,6 +430,7 @@ export default function App() {
}
}
}, [
recipeFromAppConfig,
resetChat,
loadCurrentChat,
setAgentWaitingMessage,
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/api/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,7 @@ export type SessionMetadata = {
* The number of output tokens used in the session. Retrieved from the provider's last usage.
*/
output_tokens?: number | null;
recipe?: Recipe | null;
/**
* ID of the schedule that triggered this session, if any
*/
Expand All @@ -737,6 +738,7 @@ export type Settings = {
};

export type StartAgentRequest = {
recipe?: Recipe | null;
working_dir: string;
};

Expand Down
15 changes: 7 additions & 8 deletions ui/desktop/src/components/pair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,11 @@ export default function Pair({
const [recipeResetOverride, setRecipeResetOverride] = useState(false);
const [loadingChat, setLoadingChat] = useState(false);

const recipeJson = JSON.stringify(routeState.recipeConfig);

useEffect(() => {
const initializeFromState = async () => {
setLoadingChat(true);
try {
const chat = await loadCurrentChat({
recipeConfig: routeState.recipeConfig,
resumeSessionId: routeState.resumeSessionId,
setAgentWaitingMessage,
});
Expand All @@ -78,7 +75,6 @@ export default function Pair({
loadCurrentChat,
routeState.resumeSessionId,
routeState.recipeConfig,
recipeJson, // TODO: Hacky object comparison, but works for now
]);

// Followed by sending the initialMessage if we have one. This will happen
Expand Down Expand Up @@ -114,10 +110,13 @@ export default function Pair({
console.log('Message submitted:', message);
};

const initialValue =
messageToSubmit ||
(agentState === 'initialized' && !recipeResetOverride ? recipeInitialPrompt : undefined) ||
undefined;
const recipePrompt =
agentState === 'initialized' &&
!recipeResetOverride &&
chat.messages.length === 0 &&
recipeInitialPrompt;

const initialValue = messageToSubmit || recipePrompt || undefined;

const customChatInputProps = {
// Pass initial message from Hub or recipe prompt
Expand Down
20 changes: 1 addition & 19 deletions ui/desktop/src/contexts/ChatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ interface ChatContextType {
hasActiveSession: boolean;
setRecipeConfig: (recipe: Recipe | null) => void;
clearRecipeConfig: () => void;
setRecipeParameters: (parameters: Record<string, string> | null) => void;
clearRecipeParameters: () => void;
// Draft functionality
draft: string;
setDraft: (draft: string) => void;
Expand Down Expand Up @@ -63,7 +61,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
messages: [],
messageHistoryIndex: 0,
recipeConfig: null, // Clear recipe when resetting chat
recipeParameters: null, // Clear parameters when resetting chat
recipeParameters: null, // Clear when resetting chat
});
// Clear draft when resetting chat
clearDraft();
Expand All @@ -83,20 +81,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
});
};

const setRecipeParameters = (parameters: Record<string, string> | null) => {
setChat({
...chat,
recipeParameters: parameters,
});
};

const clearRecipeParameters = () => {
setChat({
...chat,
recipeParameters: null,
});
};

const hasActiveSession = chat.messages.length > 0;

const value: ChatContextType = {
Expand All @@ -106,8 +90,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
hasActiveSession,
setRecipeConfig,
clearRecipeConfig,
setRecipeParameters,
clearRecipeParameters,
draft,
setDraft,
clearDraft,
Expand Down
Loading