refactor: move schema updates to executor

- Executor now updates TPM.js database directly when loading tools
- Update /api/tools/update-schema to use packageName+exportName lookup
- Remove schema update logic from HLLM proxy (no longer needed)
This commit is contained in:
Ajax Davis 2025-12-15 13:07:42 +10:00
parent 7aabba7e8f
commit 72bdc5e854
2 changed files with 110 additions and 18 deletions

View file

@ -48,6 +48,45 @@ async function reportToolHealth(
}
}
/**
* Update tool schema in TPM.js database
* Non-blocking - fires and forgets to keep schemas up to date
*/
async function updateToolSchema(
packageName: string,
exportName: string,
description: string,
// biome-ignore lint/suspicious/noExplicitAny: JSON Schema can have any structure
inputSchema: any
): Promise<void> {
try {
const response = await fetch(`${TPMJS_API_URL}/api/tools/update-schema`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
packageName,
exportName,
description,
inputSchema,
}),
});
if (response.ok) {
const data = await response.json();
console.log(
`📋 Schema updated for ${packageName}/${exportName}:`,
data.updated ? 'UPDATED' : 'NO CHANGE'
);
} else {
const errorText = await response.text();
console.warn(`⚠️ Failed to update schema: ${response.status}`, errorText);
}
} catch (err) {
// Non-blocking - just log
console.error('❌ Failed to update tool schema:', err);
}
}
/**
* Sanitize JSON Schema to fix common issues
* - Replaces invalid type "None" with "object"
@ -354,6 +393,13 @@ async function loadAndDescribe(req: Request): Promise<Response> {
// Sanitize schema - fix common issues with invalid schemas
const sanitizedSchema = sanitizeJsonSchema(rawJsonSchema);
// Update TPM.js database with the schema (async, non-blocking)
updateToolSchema(packageName, exportName, toolModule.description, sanitizedSchema).catch(
(err) => {
console.warn('⚠️ Failed to update schema in database:', err);
}
);
return Response.json({
success: true,
tool: {

View file

@ -8,21 +8,25 @@ export const dynamic = 'force-dynamic';
* POST /api/tools/update-schema
* Update a tool's input schema (parameters field)
*
* This is called when a tool is executed and we discover its actual schema
* from the executor's /load-and-describe endpoint.
* Called by the executor when it loads a tool and discovers its schema.
* Looks up tool by packageName + exportName (unique constraint).
*
* Body:
* - toolId: The tool's database ID
* - packageName: npm package name
* - exportName: exported function name
* - inputSchema: The JSON Schema for the tool's input parameters
* - description: Optional updated description from the tool
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { toolId, inputSchema, description } = body;
const { packageName, exportName, inputSchema, description } = body;
if (!toolId) {
return NextResponse.json({ success: false, error: 'toolId is required' }, { status: 400 });
if (!packageName || !exportName) {
return NextResponse.json(
{ success: false, error: 'packageName and exportName are required' },
{ status: 400 }
);
}
if (!inputSchema) {
@ -32,12 +36,37 @@ export async function POST(request: NextRequest) {
);
}
console.log('[Update Schema] Updating tool:', toolId);
console.log('[Update Schema] Schema properties:', Object.keys(inputSchema.properties || {}));
console.log('[Update Schema] Looking up tool:', { packageName, exportName });
// Find the tool by package name and export name
const tool = await prisma.tool.findFirst({
where: {
exportName,
package: {
npmPackageName: packageName,
},
},
select: {
id: true,
parameters: true,
},
});
if (!tool) {
console.log('[Update Schema] Tool not found:', { packageName, exportName });
return NextResponse.json(
{ success: false, error: 'Tool not found', updated: false },
{ status: 404 }
);
}
// Convert JSON Schema to our parameters format
// The parameters field stores an array of parameter objects
const parameters = [];
const parameters: Array<{
name: string;
type: string;
required: boolean;
description: string;
}> = [];
if (inputSchema.properties) {
for (const [name, prop] of Object.entries(inputSchema.properties)) {
const propDef = prop as { type?: string; description?: string };
@ -50,18 +79,38 @@ export async function POST(request: NextRequest) {
}
}
// Update the tool in the database
// Check if parameters already match (avoid unnecessary updates)
const existingParams = tool.parameters as Array<{ name: string }> | null;
const existingParamNames =
existingParams
?.map((p) => p.name)
.sort()
.join(',') || '';
const newParamNames = parameters
.map((p) => p.name)
.sort()
.join(',');
if (existingParamNames === newParamNames && parameters.length > 0) {
console.log('[Update Schema] Schema already up to date:', { packageName, exportName });
return NextResponse.json({
success: true,
updated: false,
message: 'Schema already up to date',
});
}
// Update the tool
const updateData: { parameters: typeof parameters; description?: string } = {
parameters,
};
// Only update description if provided and different
if (description) {
updateData.description = description;
}
const updatedTool = await prisma.tool.update({
where: { id: toolId },
where: { id: tool.id },
data: updateData,
select: {
id: true,
@ -75,20 +124,17 @@ export async function POST(request: NextRequest) {
id: updatedTool.id,
exportName: updatedTool.exportName,
parameterCount: parameters.length,
parameterNames: parameters.map((p) => p.name),
});
return NextResponse.json({
success: true,
updated: true,
tool: updatedTool,
});
} catch (error) {
console.error('[Update Schema] Error:', error);
// Handle not found error
if (error instanceof Error && error.message.includes('Record to update not found')) {
return NextResponse.json({ success: false, error: 'Tool not found' }, { status: 404 });
}
return NextResponse.json(
{
success: false,