fix(agents): properly initialize form with API data defaults

Root cause: The demo data's model_params was missing `top_p`, but the
Zod schema required all three fields (temperature, max_tokens, top_p).
This caused silent validation failures when editing agent types.

Fixes:
1. Add getInitialValues() that ensures all required fields have defaults
2. Handle nested validation errors in handleFormError (e.g., model_params.top_p)
3. Add useEffect to reset form when agentType changes
4. Add console.error logging for debugging validation failures
5. Update demo data to include top_p in all agent types

The form now properly initializes with safe defaults for any missing
fields from the API response, preventing silent validation failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-06 11:54:45 +01:00
parent c9d0d079b3
commit 600657adc4
2 changed files with 90 additions and 38 deletions

View File

@@ -9,7 +9,8 @@
"fallback_models": ["claude-haiku-3-5-20241022"],
"model_params": {
"temperature": 0.7,
"max_tokens": 4096
"max_tokens": 4096,
"top_p": 0.95
},
"mcp_servers": ["gitea", "knowledge-base"],
"tool_permissions": {
@@ -29,7 +30,8 @@
"fallback_models": ["claude-haiku-3-5-20241022"],
"model_params": {
"temperature": 0.5,
"max_tokens": 8192
"max_tokens": 8192,
"top_p": 0.95
},
"mcp_servers": ["gitea", "knowledge-base"],
"tool_permissions": {
@@ -49,7 +51,8 @@
"fallback_models": ["claude-haiku-3-5-20241022"],
"model_params": {
"temperature": 0.6,
"max_tokens": 8192
"max_tokens": 8192,
"top_p": 0.95
},
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
"tool_permissions": {
@@ -69,7 +72,8 @@
"fallback_models": ["claude-haiku-3-5-20241022"],
"model_params": {
"temperature": 0.3,
"max_tokens": 16384
"max_tokens": 16384,
"top_p": 0.95
},
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
"tool_permissions": {
@@ -89,7 +93,8 @@
"fallback_models": ["claude-haiku-3-5-20241022"],
"model_params": {
"temperature": 0.4,
"max_tokens": 8192
"max_tokens": 8192,
"top_p": 0.95
},
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
"tool_permissions": {
@@ -109,7 +114,8 @@
"fallback_models": ["claude-haiku-3-5-20241022"],
"model_params": {
"temperature": 0.4,
"max_tokens": 8192
"max_tokens": 8192,
"top_p": 0.95
},
"mcp_servers": ["gitea", "knowledge-base", "filesystem"],
"tool_permissions": {

View File

@@ -57,28 +57,37 @@ export function AgentTypeForm({
const [activeTab, setActiveTab] = useState('basic');
const [expertiseInput, setExpertiseInput] = useState('');
// Build initial values with proper defaults for missing fields
const getInitialValues = useCallback((): AgentTypeCreateFormValues => {
if (!agentType) return defaultAgentTypeValues;
// Ensure model_params has all required fields with proper defaults
const modelParams = agentType.model_params ?? {};
const safeModelParams = {
temperature: typeof modelParams.temperature === 'number' ? modelParams.temperature : 0.7,
max_tokens: typeof modelParams.max_tokens === 'number' ? modelParams.max_tokens : 8192,
top_p: typeof modelParams.top_p === 'number' ? modelParams.top_p : 0.95,
};
return {
name: agentType.name,
slug: agentType.slug,
description: agentType.description,
expertise: agentType.expertise ?? [],
personality_prompt: agentType.personality_prompt ?? '',
primary_model: agentType.primary_model ?? 'claude-opus-4-5-20251101',
fallback_models: agentType.fallback_models ?? [],
model_params: safeModelParams,
mcp_servers: agentType.mcp_servers ?? [],
tool_permissions: agentType.tool_permissions ?? {},
is_active: agentType.is_active ?? false,
};
}, [agentType]);
// Always use create schema for validation - editing requires all fields too
const form = useForm<AgentTypeCreateFormValues>({
resolver: zodResolver(agentTypeCreateSchema),
defaultValues: agentType
? {
name: agentType.name,
slug: agentType.slug,
description: agentType.description,
expertise: agentType.expertise,
personality_prompt: agentType.personality_prompt,
primary_model: agentType.primary_model,
fallback_models: agentType.fallback_models,
model_params: (agentType.model_params ?? {
temperature: 0.7,
max_tokens: 8192,
top_p: 0.95,
}) as AgentTypeCreateFormValues['model_params'],
mcp_servers: agentType.mcp_servers,
tool_permissions: agentType.tool_permissions,
is_active: agentType.is_active,
}
: defaultAgentTypeValues,
defaultValues: getInitialValues(),
});
const {
@@ -90,29 +99,58 @@ export function AgentTypeForm({
formState: { errors },
} = form;
// Helper to extract first error message from nested errors
const getFirstErrorMessage = useCallback(
(formErrors: FieldErrors<AgentTypeCreateFormValues>): { field: string; message: string } => {
for (const key of Object.keys(formErrors)) {
const error = formErrors[key as keyof typeof formErrors];
if (!error) continue;
// Handle nested errors (like model_params.temperature)
if (typeof error === 'object' && !('message' in error)) {
for (const nestedKey of Object.keys(error)) {
const nestedError = (error as Record<string, { message?: string }>)[nestedKey];
if (nestedError?.message) {
return { field: `${key}.${nestedKey}`, message: nestedError.message };
}
}
}
// Handle direct error
if ('message' in error && error.message) {
return { field: key, message: error.message as string };
}
}
return { field: 'unknown', message: 'Validation error' };
},
[]
);
// Handle form validation errors - show toast with first error
const handleFormError = useCallback((formErrors: FieldErrors<AgentTypeCreateFormValues>) => {
// Find the first error and show it
const firstErrorKey = Object.keys(formErrors)[0] as keyof AgentTypeCreateFormValues;
if (firstErrorKey) {
const error = formErrors[firstErrorKey];
const message = error && 'message' in error ? error.message : 'Validation error';
const handleFormError = useCallback(
(formErrors: FieldErrors<AgentTypeCreateFormValues>) => {
// Log for debugging
console.error('[AgentTypeForm] Validation errors:', formErrors);
const { field, message } = getFirstErrorMessage(formErrors);
toast.error('Please fix form errors', {
description: `${String(firstErrorKey)}: ${message}`,
description: `${field}: ${message}`,
});
// Navigate to the tab containing the error
if (['name', 'slug', 'description', 'expertise', 'is_active'].includes(firstErrorKey)) {
const topLevelField = field.split('.')[0];
if (['name', 'slug', 'description', 'expertise', 'is_active'].includes(topLevelField)) {
setActiveTab('basic');
} else if (['primary_model', 'fallback_models', 'model_params'].includes(firstErrorKey)) {
} else if (['primary_model', 'fallback_models', 'model_params'].includes(topLevelField)) {
setActiveTab('model');
} else if (['mcp_servers', 'tool_permissions'].includes(firstErrorKey)) {
} else if (['mcp_servers', 'tool_permissions'].includes(topLevelField)) {
setActiveTab('permissions');
} else if (firstErrorKey === 'personality_prompt') {
} else if (topLevelField === 'personality_prompt') {
setActiveTab('personality');
}
}
}, []);
},
[getFirstErrorMessage]
);
const watchName = watch('name');
/* istanbul ignore next -- defensive fallback, expertise always has default */
@@ -120,6 +158,14 @@ export function AgentTypeForm({
/* istanbul ignore next -- defensive fallback, mcp_servers always has default */
const watchMcpServers = watch('mcp_servers') || [];
// Reset form when agentType changes (e.g., switching to edit mode)
useEffect(() => {
if (agentType) {
const values = getInitialValues();
form.reset(values);
}
}, [agentType?.id, form, getInitialValues]);
// Auto-generate slug from name for new agent types
useEffect(() => {
if (!isEditing && watchName) {