Add session to agents (#4216)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Jack Amadeo <jackamadeo@squareup.com>
Co-authored-by: Jack Amadeo <jackamadeo@block.xyz>
This commit is contained in:
Douwe Osinga
2025-09-03 10:31:24 -04:00
committed by GitHub
parent db94c5b7c3
commit d2195289cf
65 changed files with 1473 additions and 1601 deletions
-253
View File
@@ -1,253 +0,0 @@
import { ChatType } from '../types/chat';
import { Recipe } from '../recipe';
import { initializeSystem } from './providerUtils';
import { initializeCostDatabase } from './costDatabase';
import {
type ExtensionConfig,
type FixedExtensionEntry,
MalformedConfigError,
} from '../components/ConfigContext';
import { backupConfig, initConfig, readAllConfig, recoverConfig, validateConfig } from '../api';
import { COST_TRACKING_ENABLED } from '../updates';
import { toastService } from '../toasts';
interface InitializationDependencies {
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void;
setMessage: (message: string | null) => void;
setIsExtensionsLoading: (loading: boolean) => void;
provider: string;
model: string;
}
export const initializeApp = async ({
getExtensions,
addExtension,
setPairChat,
setMessage,
setIsExtensionsLoading,
provider,
model,
}: InitializationDependencies) => {
console.log(`Initializing app`);
const urlParams = new URLSearchParams(window.location.search);
const viewType = urlParams.get('view');
const resumeSessionId = urlParams.get('resumeSessionId');
const recipeConfig = window.appConfig.get('recipe');
if (resumeSessionId) {
console.log('Session resume detected, letting useChat hook handle navigation');
await initializeForSessionResume({
getExtensions,
addExtension,
setIsExtensionsLoading,
provider,
model,
});
return;
}
if (recipeConfig && typeof recipeConfig === 'object') {
console.log('Recipe deeplink detected, initializing system for recipe');
await initializeForRecipe({
recipeConfig: recipeConfig as Recipe,
getExtensions,
addExtension,
setPairChat,
setIsExtensionsLoading,
provider,
model,
});
return;
}
if (viewType) {
handleViewTypeDeepLink(viewType, recipeConfig);
return;
}
const costDbPromise = COST_TRACKING_ENABLED
? initializeCostDatabase().catch((error) => {
console.error('Failed to initialize cost database:', error);
})
: (() => {
console.log('Cost tracking disabled, skipping cost database initialization');
return Promise.resolve();
})();
await initConfig();
try {
await readAllConfig({ throwOnError: true });
} catch (error) {
console.warn('Initial config read failed, attempting recovery:', error);
await handleConfigRecovery();
}
if (provider && model) {
try {
const initPromises = [
initializeSystem(provider, model, {
getExtensions,
addExtension,
setIsExtensionsLoading,
}),
];
if (COST_TRACKING_ENABLED) {
initPromises.push(costDbPromise);
}
setMessage('starting extensions...');
await Promise.all(initPromises);
} catch (error) {
console.error('Error in system initialization:', error);
if (error instanceof MalformedConfigError) {
throw error;
}
}
}
// Only redirect to home if we're still on the initial empty hash or root
// This prevents redirecting users who have already navigated elsewhere during initialization
const currentHash = window.location.hash;
const currentPathname = window.location.pathname;
const isOnRootRoute =
currentPathname === '/' && (!currentHash || currentHash === '#' || currentHash === '#/');
if (isOnRootRoute) {
window.location.hash = '#/';
window.history.replaceState({}, '', '#/');
}
};
const initializeForSessionResume = async ({
getExtensions,
addExtension,
setIsExtensionsLoading,
provider,
model,
}: Pick<
InitializationDependencies,
'getExtensions' | 'addExtension' | 'setIsExtensionsLoading' | 'provider' | 'model'
>) => {
await initConfig();
await readAllConfig({ throwOnError: true });
await initializeSystem(provider, model, {
getExtensions,
addExtension,
setIsExtensionsLoading,
});
};
const initializeForRecipe = async ({
recipeConfig,
getExtensions,
addExtension,
setPairChat,
setIsExtensionsLoading,
provider,
model,
}: Pick<
InitializationDependencies,
'getExtensions' | 'addExtension' | 'setPairChat' | 'setIsExtensionsLoading' | 'provider' | 'model'
> & {
recipeConfig: Recipe;
}) => {
toastService.configure({ silent: false });
const loadingToastId = toastService.loading({
title: `Loading recipe: ${recipeConfig.title}`,
msg: 'Setting up environment...',
});
await initConfig();
await readAllConfig({ throwOnError: true });
await initializeSystem(provider, model, {
getExtensions,
addExtension,
setIsExtensionsLoading,
});
toastService.dismiss(loadingToastId);
toastService.success({ title: 'Recipe loaded', msg: `Recipe is ready to use` });
setPairChat((prevChat) => ({
...prevChat,
recipeConfig: recipeConfig,
title: recipeConfig?.title || 'Recipe Chat',
messages: [],
messageHistoryIndex: 0,
}));
window.location.hash = '#/pair';
window.history.replaceState(
{
recipeConfig: recipeConfig,
resetChat: true,
},
'',
'#/pair'
);
};
const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => {
if (viewType === 'recipeEditor' && recipeConfig) {
window.location.hash = '#/recipe-editor';
window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor');
} else {
const routeMap: Record<string, string> = {
chat: '#/',
pair: '#/pair',
settings: '#/settings',
sessions: '#/sessions',
schedules: '#/schedules',
recipes: '#/recipes',
permission: '#/permission',
ConfigureProviders: '#/configure-providers',
sharedSession: '#/shared-session',
recipeEditor: '#/recipe-editor',
welcome: '#/welcome',
};
const route = routeMap[viewType];
if (route) {
window.location.hash = route;
window.history.replaceState({}, '', route);
}
}
};
const handleConfigRecovery = async () => {
const configVersion = localStorage.getItem('configVersion');
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
if (shouldMigrateExtensions) {
console.log('Performing extension migration...');
try {
await backupConfig({ throwOnError: true });
await initConfig();
} catch (migrationError) {
console.error('Migration failed:', migrationError);
}
}
console.log('Attempting config recovery...');
try {
await validateConfig({ throwOnError: true });
await readAllConfig({ throwOnError: true });
} catch {
console.log('Config validation failed, attempting recovery...');
try {
await recoverConfig({ throwOnError: true });
await readAllConfig({ throwOnError: true });
} catch {
console.warn('Config recovery failed, reinitializing...');
await initConfig();
}
}
};
+5 -3
View File
@@ -19,19 +19,21 @@ export type View =
| 'recipes'
| 'permission';
// TODO(Douwe): check these for usage, especially key: string for resetChat
export type ViewOptions = {
extensionId?: string;
showEnvVars?: boolean;
deepLinkConfig?: unknown;
resumedSession?: unknown;
sessionDetails?: unknown;
error?: string;
shareToken?: string;
baseUrl?: string;
config?: unknown;
parentView?: View;
parentViewOptions?: ViewOptions;
[key: string]: unknown;
disableAnimation?: boolean;
initialMessage?: string;
resetChat?: boolean;
shareToken?: string;
};
export const createNavigationHandler = (navigate: NavigateFunction) => {
+1 -2
View File
@@ -20,6 +20,5 @@ export async function startOpenRouterSetup(): Promise<{ success: boolean; messag
};
}
const result = await response.json();
return result;
return await response.json();
}
+38 -28
View File
@@ -1,5 +1,3 @@
import { getApiUrl } from '../config';
import { initializeAgent } from '../agent';
import {
initializeBundledExtensions,
syncBundledExtensions,
@@ -7,16 +5,14 @@ import {
} from '../components/settings/extensions';
import { extractExtensionConfig } from '../components/settings/extensions/utils';
import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigContext';
import { RecipeParameter, SubRecipe, updateSessionConfig, extendPrompt } from '../api';
import { addSubRecipesToAgent } from '../recipe/add_sub_recipe_on_agent';
export interface Provider {
id: string; // Lowercase key (e.g., "openai")
name: string; // Provider name (e.g., "OpenAI")
description: string; // Description of the provider
models: string[]; // List of supported models
requiredKeys: string[]; // List of required keys
}
import {
extendPrompt,
RecipeParameter,
SubRecipe,
updateAgentProvider,
updateSessionConfig,
} from '../api';
// Desktop-specific system prompt extension
const desktopPrompt = `You are being accessed through the Goose Desktop application.
@@ -68,6 +64,7 @@ export const substituteParameters = (text: string, params: Record<string, string
* This should be called after recipe parameters are collected
*/
export const updateSystemPromptWithParameters = async (
sessionId: string,
recipeParameters: Record<string, string>,
recipeConfig?: {
instructions?: string | null;
@@ -88,6 +85,7 @@ export const updateSystemPromptWithParameters = async (
// Update the system prompt with substituted instructions
const response = await extendPrompt({
body: {
session_id: sessionId,
extension: `${desktopPromptBot}\nIMPORTANT instructions for you to operate as agent:\n${substitutedInstructions}`,
},
});
@@ -105,11 +103,12 @@ export const updateSystemPromptWithParameters = async (
}
}
}
await addSubRecipesToAgent(subRecipes);
await addSubRecipesToAgent(sessionId, subRecipes);
}
};
export const initializeSystem = async (
sessionId: string,
provider: string,
model: string,
options?: {
@@ -119,8 +118,26 @@ export const initializeSystem = async (
}
) => {
try {
console.log('initializing agent with provider', provider, 'model', model);
await initializeAgent({ provider, model });
console.log(
'initializing agent with provider',
provider,
'model',
model,
'sessionId',
sessionId
);
await updateAgentProvider({
body: {
session_id: sessionId,
provider,
model,
},
throwOnError: true,
});
if (!sessionId) {
console.log('This will not end well');
}
// Get recipeConfig directly here
const recipeConfig = window.appConfig?.get?.('recipe');
@@ -135,28 +152,21 @@ export const initializeSystem = async (
prompt = `${desktopPromptBot}\nIMPORTANT instructions for you to operate as agent:\n${recipe_instructions}`;
}
// Extend the system prompt with desktop-specific information
const response = await fetch(getApiUrl('/agent/prompt'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': await window.electron.getSecretKey(),
},
body: JSON.stringify({
await extendPrompt({
body: {
session_id: sessionId,
extension: prompt,
}),
},
});
if (!response.ok) {
console.warn(`Failed to extend system prompt: ${response.statusText}`);
} else {
console.log('Extended system prompt with desktop-specific information');
}
if (!hasParameters && hasSubRecipes) {
await addSubRecipesToAgent(subRecipes);
await addSubRecipesToAgent(sessionId, subRecipes);
}
// Configure session with response config if present
if (responseConfig?.json_schema) {
const sessionConfigResponse = await updateSessionConfig({
body: {
session_id: sessionId,
response: responseConfig,
},
});
+2 -2
View File
@@ -56,5 +56,5 @@ export function shouldHideMessage(messageIndex: number, chains: number[][]): boo
}
export function getChainForMessage(messageIndex: number, chains: number[][]): number[] | null {
return chains.find(chain => chain.includes(messageIndex)) || null;
}
return chains.find((chain) => chain.includes(messageIndex)) || null;
}