Add a goosed over HTTP integration test, and test the developer tool PATH (#7178)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jack Amadeo
2026-02-13 14:41:18 -05:00
committed by GitHub
parent 0206035dba
commit 5ba5b9899d
17 changed files with 864 additions and 354 deletions
-5
View File
@@ -28,11 +28,6 @@ Object.defineProperty(window, 'history', {
writable: true,
});
// Mock dependencies
vi.mock('./utils/providerUtils', () => ({
initializeSystem: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('./utils/costDatabase', () => ({
initializeCostDatabase: vi.fn().mockResolvedValue(undefined),
}));
+1 -1
View File
@@ -34,7 +34,7 @@ import RecipeActivities from './recipes/RecipeActivities';
import { useToolCount } from './alerts/useToolCount';
import { getThinkingMessage, getTextAndImageContent } from '../types/message';
import ParameterInputModal from './ParameterInputModal';
import { substituteParameters } from '../utils/providerUtils';
import { substituteParameters } from '../utils/parameterSubstitution';
import { useModelAndProvider } from './ModelAndProviderContext';
import CreateRecipeFromSessionModal from './recipes/CreateRecipeFromSessionModal';
import { toastSuccess } from '../toasts';
@@ -2,12 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { OllamaSetup } from './OllamaSetup';
import * as ollamaDetection from '../utils/ollamaDetection';
import * as providerUtils from '../utils/providerUtils';
import { toastService } from '../toasts';
// Mock dependencies
vi.mock('../utils/ollamaDetection');
vi.mock('../utils/providerUtils');
vi.mock('../toasts');
// Mock useConfig hook
@@ -162,8 +160,6 @@ describe('OllamaSetup', () => {
});
it('should handle successful connection', async () => {
vi.mocked(providerUtils.initializeSystem).mockResolvedValue(undefined);
render(<OllamaSetup onSuccess={mockOnSuccess} onCancel={mockOnCancel} />);
await waitFor(() => {
@@ -174,20 +170,12 @@ describe('OllamaSetup', () => {
expect(mockUpsert).toHaveBeenCalledWith('GOOSE_PROVIDER', 'ollama', false);
expect(mockUpsert).toHaveBeenCalledWith('GOOSE_MODEL', 'gpt-oss:20b', false);
expect(mockUpsert).toHaveBeenCalledWith('OLLAMA_HOST', 'localhost', false);
expect(providerUtils.initializeSystem).toHaveBeenCalledWith(
'ollama',
'gpt-oss:20b',
expect.any(Object)
);
expect(toastService.success).toHaveBeenCalled();
expect(mockOnSuccess).toHaveBeenCalled();
});
});
it('should handle connection failure', async () => {
const testError = new Error('Initialization failed');
vi.mocked(providerUtils.initializeSystem).mockRejectedValue(testError);
render(<OllamaSetup onSuccess={mockOnSuccess} onCancel={mockOnCancel} />);
await waitFor(() => {
@@ -1,7 +1,7 @@
import { Card } from '../ui/card';
import GooseLogo from '../GooseLogo';
import MarkdownContent from '../MarkdownContent';
import { substituteParameters } from '../../utils/providerUtils';
import { substituteParameters } from '../../utils/parameterSubstitution';
interface RecipeActivitiesProps {
append: (text: string) => void;
+304 -233
View File
@@ -1,16 +1,21 @@
import Electron from 'electron';
import fs from 'node:fs';
import { spawn, ChildProcess } from 'child_process';
import { createServer } from 'net';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import log from './utils/logger';
import { App } from 'electron';
import { createServer } from 'net';
import { Buffer } from 'node:buffer';
import { status } from './api';
import { Client } from './api/client';
import { ExternalGoosedConfig } from './utils/settings';
import { Client, createClient, createConfig } from './api/client';
export interface Logger {
info: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
}
export const defaultLogger: Logger = {
info: (...args) => console.log('[goosed]', ...args),
error: (...args) => console.error('[goosed]', ...args),
};
export const findAvailablePort = (): Promise<number> => {
return new Promise((resolve, _reject) => {
@@ -19,244 +24,310 @@ export const findAvailablePort = (): Promise<number> => {
server.listen(0, '127.0.0.1', () => {
const { port } = server.address() as { port: number };
server.close(() => {
log.info(`Found available port: ${port}`);
resolve(port);
});
});
});
};
// Check if goosed server is ready by polling the status endpoint
export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
const interval = 100; // ms
const maxAttempts = 100; // 10s
export interface FindBinaryOptions {
isPackaged?: boolean;
resourcesPath?: string;
}
const fatal = (line: string) => {
const trimmed = line.trim().toLowerCase();
return trimmed.startsWith("thread 'main' panicked at") || trimmed.startsWith('error:');
};
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (errorLog.some(fatal)) {
log.error('Detected fatal error in server logs');
return false;
export const findGoosedBinaryPath = (options: FindBinaryOptions = {}): string => {
const pathFromEnv = process.env.GOOSED_BINARY;
if (pathFromEnv) {
if (fs.existsSync(pathFromEnv) && fs.statSync(pathFromEnv).isFile()) {
return path.resolve(pathFromEnv);
} else {
throw new Error(`Invalid GOOSED_BINARY path: ${pathFromEnv} (pwd is ${process.cwd()})`);
}
}
const { isPackaged = false, resourcesPath } = options;
const binaryName = process.platform === 'win32' ? 'goosed.exe' : 'goosed';
const possiblePaths: string[] = [];
// Packaged app paths
if (isPackaged && resourcesPath) {
possiblePaths.push(path.join(resourcesPath, 'bin', binaryName));
possiblePaths.push(path.join(resourcesPath, binaryName));
}
// Development paths
possiblePaths.push(
path.join(process.cwd(), 'src', 'bin', binaryName),
path.join(process.cwd(), '..', '..', 'target', 'release', binaryName),
path.join(process.cwd(), '..', '..', 'target', 'debug', binaryName)
);
for (const p of possiblePaths) {
try {
await status({ client, throwOnError: true });
return true;
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
return p;
}
} catch {
if (attempt === maxAttempts) {
log.error(`Server failed to respond after ${(interval * maxAttempts) / 1000} seconds`);
}
}
await new Promise((resolve) => setTimeout(resolve, interval));
}
return false;
};
export interface GoosedResult {
baseUrl: string;
workingDir: string;
process: ChildProcess;
errorLog: string[];
}
const connectToExternalBackend = (workingDir: string, url: string): GoosedResult => {
log.info(`Using external goosed backend at ${url}`);
const mockProcess = {
pid: undefined,
kill: () => {
log.info(`Not killing external process that is managed externally`);
},
} as ChildProcess;
return { baseUrl: url, workingDir, process: mockProcess, errorLog: [] };
};
interface GooseProcessEnv {
[key: string]: string | undefined;
HOME: string;
USERPROFILE: string;
APPDATA: string;
LOCALAPPDATA: string;
PATH: string;
GOOSE_PORT: string;
GOOSE_SERVER__SECRET_KEY?: string;
}
export interface StartGoosedOptions {
app: App;
serverSecret: string;
dir: string;
env?: Partial<GooseProcessEnv>;
externalGoosed?: ExternalGoosedConfig;
}
export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedResult> => {
const { app, serverSecret, dir: inputDir, env = {}, externalGoosed } = options;
const isWindows = process.platform === 'win32';
const homeDir = os.homedir();
const dir = path.resolve(path.normalize(inputDir));
if (externalGoosed?.enabled && externalGoosed.url) {
return connectToExternalBackend(dir, externalGoosed.url);
}
if (process.env.GOOSE_EXTERNAL_BACKEND) {
const port = process.env.GOOSE_PORT || '3000';
return connectToExternalBackend(dir, `http://127.0.0.1:${port}`);
}
let goosedPath = getGoosedBinaryPath(app);
const resolvedGoosedPath = path.resolve(goosedPath);
const port = await findAvailablePort();
const stderrLines: string[] = [];
log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`);
const additionalEnv: GooseProcessEnv = {
HOME: homeDir,
USERPROFILE: homeDir,
APPDATA: process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
LOCALAPPDATA: process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
PATH: `${path.dirname(resolvedGoosedPath)}${path.delimiter}${process.env.PATH || ''}`,
GOOSE_PORT: String(port),
GOOSE_SERVER__SECRET_KEY: serverSecret,
...env,
} as GooseProcessEnv;
const processEnv: GooseProcessEnv = { ...process.env, ...additionalEnv } as GooseProcessEnv;
if (isWindows && !resolvedGoosedPath.toLowerCase().endsWith('.exe')) {
goosedPath = resolvedGoosedPath + '.exe';
} else {
goosedPath = resolvedGoosedPath;
}
log.info(`Binary path resolved to: ${goosedPath}`);
const spawnOptions = {
cwd: dir,
env: processEnv,
stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
windowsHide: true,
detached: isWindows,
shell: false,
};
const safeSpawnOptions = {
...spawnOptions,
env: Object.keys(spawnOptions.env || {}).reduce(
(acc, key) => {
if (key.includes('SECRET') || key.includes('PASSWORD') || key.includes('TOKEN')) {
acc[key] = '[REDACTED]';
} else {
acc[key] = spawnOptions.env![key] || '';
}
return acc;
},
{} as Record<string, string>
),
};
log.info('Spawn options:', JSON.stringify(safeSpawnOptions, null, 2));
const safeArgs = ['agent'];
const goosedProcess: ChildProcess = spawn(goosedPath, safeArgs, spawnOptions);
if (isWindows && goosedProcess.unref) {
goosedProcess.unref();
}
goosedProcess.stdout?.on('data', (data: Buffer) => {
log.info(`goosed stdout for port ${port} and dir ${dir}: ${data.toString()}`);
});
goosedProcess.stderr?.on('data', (data: Buffer) => {
const lines = data
.toString()
.split('\n')
.filter((l) => l.trim());
lines.forEach((line) => {
log.error(`goosed stderr for port ${port} and dir ${dir}: ${line}`);
stderrLines.push(line);
});
});
goosedProcess.on('close', (code: number | null) => {
log.info(`goosed process exited with code ${code} for port ${port} and dir ${dir}`);
});
goosedProcess.on('error', (err: Error) => {
log.error(`Failed to start goosed on port ${port} and dir ${dir}`, err);
throw err;
});
const try_kill_goose = () => {
try {
if (isWindows) {
const pid = goosedProcess.pid?.toString() || '0';
spawn('taskkill', ['/pid', pid, '/T', '/F'], { shell: false });
} else {
goosedProcess.kill?.();
}
} catch (error) {
log.error('Error while terminating goosed process:', error);
}
};
app.on('will-quit', () => {
log.info('App quitting, terminating goosed server');
try_kill_goose();
});
log.info(`Goosed server successfully started on port ${port}`);
return {
baseUrl: `http://127.0.0.1:${port}`,
workingDir: dir,
process: goosedProcess,
errorLog: stderrLines,
};
};
const getGoosedBinaryPath = (app: Electron.App): string => {
let executableName = process.platform === 'win32' ? 'goosed.exe' : 'goosed';
let possiblePaths: string[];
if (!app.isPackaged) {
possiblePaths = [
path.join(process.cwd(), 'src', 'bin', executableName),
path.join(process.cwd(), 'bin', executableName),
path.join(process.cwd(), '..', '..', 'target', 'debug', executableName),
path.join(process.cwd(), '..', '..', 'target', 'release', executableName),
];
} else {
possiblePaths = [path.join(process.resourcesPath, 'bin', executableName)];
}
for (const binPath of possiblePaths) {
try {
const resolvedPath = path.resolve(binPath);
if (fs.existsSync(resolvedPath)) {
const stats = fs.statSync(resolvedPath);
if (stats.isFile()) {
return resolvedPath;
} else {
log.error(`Path exists but is not a regular file: ${resolvedPath}`);
}
}
} catch (error) {
log.error(`Error checking path ${binPath}:`, error);
// continue
}
}
throw new Error(
`Could not find ${executableName} binary in any of the expected locations: ${possiblePaths.join(
', '
)}`
`Goosed binary not found in any of the possible paths: ${possiblePaths.join(', ')}`
);
};
export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
const timeout = 10000;
const interval = 100;
const maxAttempts = Math.ceil(timeout / interval);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (errorLog.some(isFatalError)) {
return false;
}
try {
await status({ client, throwOnError: true });
return true;
} catch {
await new Promise((resolve) => setTimeout(resolve, interval));
}
}
return false;
};
export const isFatalError = (line: string): boolean => {
const fatalPatterns = [/panicked at/, /RUST_BACKTRACE/, /fatal error/i];
return fatalPatterns.some((pattern) => pattern.test(line));
};
export const buildGoosedEnv = (
port: number,
secretKey: string,
binaryPath?: string
): Record<string, string> => {
// Environment variable naming follows the config crate convention:
// - GOOSE_ prefix with _ separator for top-level fields (GOOSE_PORT, GOOSE_HOST)
// - __ separator for nested fields (GOOSE_SERVER__SECRET_KEY)
const homeDir = process.env.HOME || os.homedir();
const env: Record<string, string> = {
GOOSE_PORT: port.toString(),
GOOSE_SERVER__SECRET_KEY: secretKey,
HOME: homeDir,
};
// Windows-specific environment variables
if (process.platform === 'win32') {
env.USERPROFILE = homeDir;
env.APPDATA = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
env.LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
}
// Add binary directory to PATH for any dependencies
const pathKey = process.platform === 'win32' ? 'Path' : 'PATH';
const currentPath = process.env[pathKey] || '';
if (binaryPath) {
env[pathKey] = `${path.dirname(binaryPath)}${path.delimiter}${currentPath}`;
} else if (currentPath) {
env[pathKey] = currentPath;
}
return env;
};
// Configuration for external goosed server
export interface ExternalGoosedConfig {
enabled: boolean;
url?: string;
secret?: string;
}
export interface StartGoosedOptions {
dir?: string;
serverSecret: string;
env?: Record<string, string | undefined>;
externalGoosed?: ExternalGoosedConfig;
isPackaged?: boolean;
resourcesPath?: string;
logger?: Logger;
}
export interface GoosedResult {
baseUrl: string;
workingDir: string;
process: ChildProcess | null;
errorLog: string[];
cleanup: () => Promise<void>;
client: Client;
}
const goosedClientForUrlAndSecret = (url: string, secret: string): Client => {
return createClient(
createConfig({
baseUrl: url,
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': secret,
},
})
);
};
export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedResult> => {
const {
dir,
isPackaged = false,
resourcesPath,
serverSecret,
env: additionalEnv = {},
externalGoosed,
logger = defaultLogger,
} = options;
const errorLog: string[] = [];
const workingDir = dir || os.homedir();
if (externalGoosed?.enabled && externalGoosed.url) {
const url = externalGoosed.url.replace(/\/$/, '');
logger.info(`Using external goosed backend at ${url}`);
return {
baseUrl: url,
workingDir,
process: null,
errorLog,
cleanup: async () => {
logger.info('Not killing external process that is managed externally');
},
client: goosedClientForUrlAndSecret(url, serverSecret),
};
}
if (process.env.GOOSE_EXTERNAL_BACKEND) {
const port = process.env.GOOSE_PORT || '3000';
const url = `http://127.0.0.1:${port}`;
logger.info(`Using external goosed backend from env at ${url}`);
return {
baseUrl: url,
workingDir,
process: null,
errorLog,
cleanup: async () => {
logger.info('Not killing external process that is managed externally');
},
client: goosedClientForUrlAndSecret(url, serverSecret),
};
}
const goosedPath = findGoosedBinaryPath({ isPackaged, resourcesPath });
const port = await findAvailablePort();
logger.info(`Starting goosed from: ${goosedPath} on port ${port} in dir ${workingDir}`);
const baseUrl = `http://127.0.0.1:${port}`;
const spawnEnv = {
...process.env,
...buildGoosedEnv(port, serverSecret, goosedPath),
};
for (const [key, value] of Object.entries(additionalEnv)) {
if (value !== undefined) {
spawnEnv[key] = value;
}
}
const isWindows = process.platform === 'win32';
const spawnOptions = {
env: spawnEnv,
cwd: workingDir,
windowsHide: true,
detached: isWindows,
shell: false as const,
stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
};
const safeSpawnOptions = {
...spawnOptions,
env: Object.fromEntries(
Object.entries(spawnOptions.env).map(([k, v]) =>
k.toLowerCase().includes('secret') || k.toLowerCase().includes('key')
? [k, '[REDACTED]']
: [k, v]
)
),
};
logger.info('Spawn options:', JSON.stringify(safeSpawnOptions, null, 2));
const goosedProcess = spawn(goosedPath, ['agent'], spawnOptions);
goosedProcess.stdout?.on('data', (data: Buffer) => {
logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${data.toString()}`);
});
goosedProcess.stderr?.on('data', (data: Buffer) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
errorLog.push(line);
if (isFatalError(line)) {
logger.error(`goosed stderr for port ${port} and dir ${workingDir}: ${line}`);
}
}
}
});
goosedProcess.on('exit', (code) => {
logger.info(`goosed process exited with code ${code} for port ${port} and dir ${workingDir}`);
});
goosedProcess.on('error', (err) => {
logger.error(`Failed to start goosed on port ${port} and dir ${workingDir}`, err);
errorLog.push(err.message);
});
const cleanup = async (): Promise<void> => {
return new Promise<void>((resolve) => {
if (!goosedProcess || goosedProcess.killed) {
resolve();
return;
}
goosedProcess.on('close', () => {
resolve();
});
logger.info('Terminating goosed server');
try {
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', goosedProcess.pid!.toString(), '/f', '/t']);
} else {
goosedProcess.kill('SIGTERM');
}
} catch (error) {
logger.error('Error while terminating goosed process:', error);
}
setTimeout(() => {
if (goosedProcess && !goosedProcess.killed && process.platform !== 'win32') {
goosedProcess.kill('SIGKILL');
}
resolve();
}, 5000);
});
};
logger.info(`Goosed server successfully started on port ${port}`);
return {
baseUrl,
workingDir,
process: goosedProcess,
errorLog,
cleanup,
client: goosedClientForUrlAndSecret(baseUrl, serverSecret),
};
};
+1 -1
View File
@@ -3,7 +3,7 @@ import { Recipe, scanRecipe } from '../recipe';
import { createUserMessage } from '../types/message';
import { Message } from '../api';
import { substituteParameters } from '../utils/providerUtils';
import { substituteParameters } from '../utils/parameterSubstitution';
import { updateSessionUserRecipeValues } from '../api';
import { useChatContext } from '../contexts/ChatContext';
import { ChatType } from '../types/chat';
+18 -13
View File
@@ -23,7 +23,8 @@ import path from 'node:path';
import os from 'node:os';
import { spawn } from 'child_process';
import 'dotenv/config';
import { checkServerStatus, startGoosed } from './goosed';
import { checkServerStatus } from './goosed';
import { startGoosed } from './goosed';
import { expandTilde } from './utils/pathUtils';
import log from './utils/logger';
import { ensureWinShims } from './utils/winShims';
@@ -43,7 +44,7 @@ import {
} from './utils/autoUpdater';
import { UPDATES_ENABLED } from './updates';
import './utils/recipeHash';
import { Client, createClient, createConfig } from './api/client';
import { Client } from './api/client';
import { GooseApp } from './api';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
import { BLOCKED_PROTOCOLS, WEB_PROTOCOLS } from './utils/urlSecurity';
@@ -483,14 +484,27 @@ const createChat = async (
const serverSecret = getServerSecret(settings);
const goosedResult = await startGoosed({
app,
serverSecret,
dir: dir || os.homedir(),
env: { GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT },
externalGoosed: settings.externalGoosed,
isPackaged: app.isPackaged,
resourcesPath: app.isPackaged ? process.resourcesPath : undefined,
logger: log,
});
const { baseUrl, workingDir, process: goosedProcess, errorLog } = goosedResult;
app.on('will-quit', async () => {
log.info('App quitting, terminating goosed server');
await goosedResult.cleanup();
});
const {
baseUrl,
workingDir,
process: goosedProcess,
errorLog,
client: goosedClient,
} = goosedResult;
const mainWindowState = windowStateKeeper({
defaultWidth: 940,
@@ -543,15 +557,6 @@ const createChat = async (
.catch((err) => log.info('failed to install react dev tools:', err));
}
const goosedClient = createClient(
createConfig({
baseUrl,
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': serverSecret,
},
})
);
goosedClients.set(mainWindow.id, goosedClient);
const serverReady = await checkServerStatus(goosedClient, errorLog);
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { substituteParameters } from '../providerUtils';
import { substituteParameters } from '../parameterSubstitution';
describe('providerUtils', () => {
describe('parameterSubstitution', () => {
describe('substituteParameters', () => {
it('should substitute simple parameters', () => {
const text = 'Hello {{name}}, welcome to {{app}}!';
@@ -83,12 +83,12 @@ describe('providerUtils', () => {
it('should handle complex substitution scenario', () => {
const text = `
Welcome {{user_name}}!
Your account details:
- ID: {{user_id}}
- Email: {{user_email}}
- App: {{app_name}}
Thank you for using {{app_name}}!
`;
@@ -102,12 +102,12 @@ describe('providerUtils', () => {
const result = substituteParameters(text, params);
const expected = `
Welcome John Doe!
Your account details:
- ID: 12345
- Email: john@example.com
- App: MyApp
Thank you for using MyApp!
`;
@@ -0,0 +1,11 @@
export const substituteParameters = (text: string, params: Record<string, string>): string => {
let substitutedText = text;
for (const key in params) {
// Escape special characters in the key (parameter) and match optional whitespace
const regex = new RegExp(`{{\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*}}`, 'g');
substitutedText = substitutedText.replace(regex, params[key]);
}
return substitutedText;
};
-77
View File
@@ -1,77 +0,0 @@
import {
initializeBundledExtensions,
syncBundledExtensions,
} from '../components/settings/extensions';
import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigContext';
import { Recipe, updateAgentProvider, updateFromSession } from '../api';
// Helper function to substitute parameters in text
export const substituteParameters = (text: string, params: Record<string, string>): string => {
let substitutedText = text;
for (const key in params) {
// Escape special characters in the key (parameter) and match optional whitespace
const regex = new RegExp(`{{\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*}}`, 'g');
substitutedText = substitutedText.replace(regex, params[key]);
}
return substitutedText;
};
export const initializeSystem = async (
sessionId: string,
provider: string,
model: string,
options?: {
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
recipeParameters?: Record<string, string> | null;
recipe?: Recipe;
}
) => {
try {
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');
}
await updateFromSession({
body: {
session_id: sessionId,
},
throwOnError: true,
});
if (!options?.getExtensions || !options?.addExtension) {
console.warn('Extension helpers not provided in alpha mode');
return;
}
// Initialize or sync built-in extensions into config.yaml
let refreshedExtensions = await options.getExtensions(false);
if (refreshedExtensions.length === 0) {
await initializeBundledExtensions(options.addExtension);
} else {
await syncBundledExtensions(refreshedExtensions, options.addExtension);
}
} catch (error) {
console.error('Failed to initialize agent:', error);
throw error;
}
};