Files
simstudioai--sim/apps/sim/lib/workflows/persistence/custom-tools-persistence.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

193 lines
5.0 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
const logger = createLogger('CustomToolsPersistence')
interface CustomTool {
id?: string
type: 'custom-tool'
title: string
toolId?: string
schema: {
function: {
name?: string
description: string
parameters: Record<string, any>
}
}
code: string
usageControl?: string
}
/**
* Stored tool format that may contain either reference or inline definition
*/
interface StoredCustomTool {
type: string
title?: string
toolId?: string
customToolId?: string
schema?: any
code?: string
usageControl?: string
}
/**
* Checks if a stored tool is a reference-only custom tool (no inline definition)
*/
function isCustomToolReference(tool: StoredCustomTool): boolean {
return tool.type === 'custom-tool' && !!tool.customToolId && !tool.code
}
/**
* Extract all custom tools from agent blocks in the workflow state
*
* @remarks
* Only extracts tools with inline definitions (legacy format).
* Reference-only tools (new format with customToolId) are skipped since they're already in the database.
*/
export function extractCustomToolsFromWorkflowState(workflowState: any): CustomTool[] {
const customToolsMap = new Map<string, CustomTool>()
if (!workflowState?.blocks) {
return []
}
for (const [blockId, block] of Object.entries(workflowState.blocks)) {
try {
const blockData = block as any
if (!blockData || blockData.type !== 'agent') {
continue
}
const subBlocks = blockData.subBlocks || {}
const toolsSubBlock = subBlocks.tools
if (!toolsSubBlock?.value) {
continue
}
let tools = toolsSubBlock.value
if (typeof tools === 'string') {
try {
tools = JSON.parse(tools)
} catch (error) {
logger.warn(`Failed to parse tools in block ${blockId}`, { error })
continue
}
}
if (!Array.isArray(tools)) {
continue
}
for (const tool of tools) {
if (!tool || typeof tool !== 'object' || tool.type !== 'custom-tool') {
continue
}
// Skip reference-only tools - they're already in the database
if (isCustomToolReference(tool)) {
logger.debug(`Skipping reference-only custom tool: ${tool.title || tool.customToolId}`)
continue
}
// Only persist tools with inline definitions (legacy format)
if (tool.title && tool.schema?.function && tool.code) {
const toolKey = tool.toolId || tool.title
if (!customToolsMap.has(toolKey)) {
customToolsMap.set(toolKey, tool as CustomTool)
}
}
}
} catch (error) {
logger.error(`Error extracting custom tools from block ${blockId}`, { error })
}
}
return Array.from(customToolsMap.values())
}
/**
* Persist custom tools to the database using the upsert function
* Creates new tools or updates existing ones
*/
export async function persistCustomToolsToDatabase(
customToolsList: CustomTool[],
workspaceId: string | null,
userId: string
): Promise<{ saved: number; errors: string[] }> {
if (!customToolsList || customToolsList.length === 0) {
return { saved: 0, errors: [] }
}
if (!workspaceId) {
logger.debug('Skipping custom tools persistence - no workspaceId provided (user-scoped tools)')
return { saved: 0, errors: [] }
}
const errors: string[] = []
let saved = 0
const validTools = customToolsList.filter((tool) => {
if (!tool.schema?.function?.name) {
logger.warn(`Skipping custom tool without function name: ${tool.title}`)
return false
}
return true
})
if (validTools.length === 0) {
return { saved: 0, errors: [] }
}
try {
await upsertCustomTools({
tools: validTools.map((tool) => ({
id: tool.toolId,
title: tool.title,
schema: tool.schema,
code: tool.code,
})),
workspaceId,
userId,
})
saved = validTools.length
logger.info(`Persisted ${saved} custom tool(s)`, { workspaceId })
} catch (error) {
const errorMsg = `Failed to persist custom tools: ${toError(error).message}`
logger.error(errorMsg, { error })
errors.push(errorMsg)
}
return { saved, errors }
}
/**
* Extract and persist custom tools from workflow state
*/
export async function extractAndPersistCustomTools(
workflowState: any,
workspaceId: string | null,
userId: string
): Promise<{ saved: number; errors: string[] }> {
const customToolsList = extractCustomToolsFromWorkflowState(workflowState)
if (customToolsList.length === 0) {
logger.debug('No custom tools found in workflow state')
return { saved: 0, errors: [] }
}
logger.info(`Found ${customToolsList.length} custom tool(s) to persist`, {
tools: customToolsList.map((t) => t.title),
workspaceId,
})
return await persistCustomToolsToDatabase(customToolsList, workspaceId, userId)
}