From c8e8c50586b1852de93191bbac683f31947edeee Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Wed, 17 Jun 2026 17:58:33 -0700 Subject: [PATCH 1/5] fix(mship): add folder rename tools and locked workflow status --- .../home/hooks/stream/stream-helpers.ts | 37 ++++- .../lib/copilot/generated/tool-catalog-v1.ts | 150 ++++++++---------- .../lib/copilot/generated/tool-schemas-v1.ts | 109 +++++-------- .../tool-executor/register-handlers.ts | 15 +- .../lib/copilot/tools/handlers/param-types.ts | 13 +- .../tools/handlers/workflow/mutations.ts | 144 +++++++++++++++++ .../tools/handlers/workflow/queries.ts | 31 +--- apps/sim/lib/copilot/tools/mcp/definitions.ts | 69 +++----- apps/sim/lib/copilot/vfs/serializers.ts | 41 +++-- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 35 +++- apps/sim/lib/workflows/utils.ts | 1 + 11 files changed, 378 insertions(+), 267 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index 9209614ec4f..db8307be8c2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -4,8 +4,6 @@ import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome import type { MothershipStreamV1ToolUI } from '@/lib/copilot/generated/mothership-stream-v1' import { CrawlWebsite, - CreateFolder, - DeleteFolder, DeleteWorkflow, DeployApi, DeployChat, @@ -18,13 +16,14 @@ import { ManageCredentialOperation, ManageCustomTool, ManageCustomToolOperation, + ManageFolder, + ManageFolderOperation, ManageMcpTool, ManageMcpToolOperation, ManageScheduledTask, ManageScheduledTaskOperation, ManageSkill, ManageSkillOperation, - MoveFolder, MoveWorkflow, QueryLogs, Redeploy, @@ -55,11 +54,7 @@ export const DEPLOY_TOOL_NAMES: Set = new Set([ Redeploy.id, ]) -export const FOLDER_TOOL_NAMES: Set = new Set([ - CreateFolder.id, - DeleteFolder.id, - MoveFolder.id, -]) +export const FOLDER_TOOL_NAMES: Set = new Set([ManageFolder.id]) export const WORKFLOW_MUTATION_TOOL_NAMES: Set = new Set([ MoveWorkflow.id, @@ -370,6 +365,19 @@ export function resolveToolDisplayTitle( ) } + if (name === ManageFolder.id) { + return resolveOperationDisplayTitle( + args.operation, + { + [ManageFolderOperation.create]: 'Creating folder', + [ManageFolderOperation.rename]: 'Renaming folder', + [ManageFolderOperation.move]: 'Moving folder', + [ManageFolderOperation.delete]: 'Deleting folder', + }, + 'Folder action' + ) + } + if (name === RunWorkflow.id) { const workflowName = resolveWorkflowNameForDisplay(args.workflowId) return workflowName ? `Running ${workflowName}` : 'Running workflow' @@ -521,5 +529,18 @@ export function resolveStreamingToolDisplayTitle( ) } + if (name === ManageFolder.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageFolderOperation.create]: 'Creating folder', + [ManageFolderOperation.rename]: 'Renaming folder', + [ManageFolderOperation.move]: 'Moving folder', + [ManageFolderOperation.delete]: 'Deleting folder', + }, + 'Folder action' + ) + } + return undefined } diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 0cd92c61c81..ac293c6ad1d 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -14,12 +14,10 @@ export interface ToolCatalogEntry { | 'crawl_website' | 'create_file' | 'create_file_folder' - | 'create_folder' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_file' | 'delete_file_folder' - | 'delete_folder' | 'delete_workflow' | 'delete_workspace_mcp_server' | 'deploy' @@ -52,7 +50,6 @@ export interface ToolCatalogEntry { | 'knowledge' | 'knowledge_base' | 'list_file_folders' - | 'list_folders' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -60,6 +57,7 @@ export interface ToolCatalogEntry { | 'load_integration_tool' | 'manage_credential' | 'manage_custom_tool' + | 'manage_folder' | 'manage_mcp_tool' | 'manage_scheduled_task' | 'manage_skill' @@ -67,7 +65,6 @@ export interface ToolCatalogEntry { | 'media' | 'move_file' | 'move_file_folder' - | 'move_folder' | 'move_workflow' | 'oauth_get_auth_link' | 'oauth_request_access' @@ -115,12 +112,10 @@ export interface ToolCatalogEntry { | 'crawl_website' | 'create_file' | 'create_file_folder' - | 'create_folder' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_file' | 'delete_file_folder' - | 'delete_folder' | 'delete_workflow' | 'delete_workspace_mcp_server' | 'deploy' @@ -153,7 +148,6 @@ export interface ToolCatalogEntry { | 'knowledge' | 'knowledge_base' | 'list_file_folders' - | 'list_folders' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -161,6 +155,7 @@ export interface ToolCatalogEntry { | 'load_integration_tool' | 'manage_credential' | 'manage_custom_tool' + | 'manage_folder' | 'manage_mcp_tool' | 'manage_scheduled_task' | 'manage_skill' @@ -168,7 +163,6 @@ export interface ToolCatalogEntry { | 'media' | 'move_file' | 'move_file_folder' - | 'move_folder' | 'move_workflow' | 'oauth_get_auth_link' | 'oauth_request_access' @@ -405,23 +399,6 @@ export const CreateFileFolder: ToolCatalogEntry = { requiredPermission: 'write', } -export const CreateFolder: ToolCatalogEntry = { - id: 'create_folder', - name: 'create_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - name: { type: 'string', description: 'Folder name.' }, - parentId: { type: 'string', description: 'Optional parent folder ID.' }, - workspaceId: { type: 'string', description: 'Optional workspace ID.' }, - }, - required: ['name'], - }, - requiredPermission: 'write', -} - export const CreateWorkflow: ToolCatalogEntry = { id: 'create_workflow', name: 'create_workflow', @@ -519,26 +496,6 @@ export const DeleteFileFolder: ToolCatalogEntry = { requiredPermission: 'write', } -export const DeleteFolder: ToolCatalogEntry = { - id: 'delete_folder', - name: 'delete_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - folderIds: { - type: 'array', - description: 'The folder IDs to delete.', - items: { type: 'string' }, - }, - }, - required: ['folderIds'], - }, - requiresConfirmation: true, - requiredPermission: 'write', -} - export const DeleteWorkflow: ToolCatalogEntry = { id: 'delete_workflow', name: 'delete_workflow', @@ -2366,19 +2323,6 @@ export const ListFileFolders: ToolCatalogEntry = { requiredPermission: 'read', } -export const ListFolders: ToolCatalogEntry = { - id: 'list_folders', - name: 'list_folders', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - workspaceId: { type: 'string', description: 'Optional workspace ID to list folders for.' }, - }, - }, -} - export const ListIntegrationTools: ToolCatalogEntry = { id: 'list_integration_tools', name: 'list_integration_tools', @@ -2562,6 +2506,51 @@ export const ManageCustomTool: ToolCatalogEntry = { requiredPermission: 'write', } +export const ManageFolder: ToolCatalogEntry = { + id: 'manage_folder', + name: 'manage_folder', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + destinationPath: { + type: 'string', + description: + 'Destination parent folder\'s VFS path for move/create. Omit (or pass "workflows") to target the workspace root.', + }, + folderId: { + type: 'string', + description: + 'Target folder ID, used as a fallback when path is not given. Readable from a contained workflow\'s meta.json "folderId".', + }, + name: { + type: 'string', + description: + 'Folder name. Required for rename (the new name); for create when you pass a destination parent instead of a full path.', + }, + operation: { + type: 'string', + description: 'The operation to perform.', + enum: ['create', 'rename', 'move', 'delete'], + }, + parentId: { + type: 'string', + description: + 'Destination parent folder ID, used as a fallback when destinationPath is not given.', + }, + path: { + type: 'string', + description: + 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path. Identifies the folder for rename/move/delete; for create it is the new folder\'s full path (its parent must already exist).', + }, + }, + required: ['operation'], + }, + requiresConfirmation: true, + requiredPermission: 'write', +} + export const ManageMcpTool: ToolCatalogEntry = { id: 'manage_mcp_tool', name: 'manage_mcp_tool', @@ -2820,26 +2809,6 @@ export const MoveFileFolder: ToolCatalogEntry = { requiredPermission: 'write', } -export const MoveFolder: ToolCatalogEntry = { - id: 'move_folder', - name: 'move_folder', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - folderId: { type: 'string', description: 'The folder ID to move.' }, - parentId: { - type: 'string', - description: - 'Target parent folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['folderId'], - }, - requiredPermission: 'write', -} - export const MoveWorkflow: ToolCatalogEntry = { id: 'move_workflow', name: 'move_workflow', @@ -3958,8 +3927,7 @@ export const UserTable: ToolCatalogEntry = { }, limit: { type: 'number', - description: - 'Maximum rows to return or affect (optional, default 100). Omit on update_rows_by_filter / delete_rows_by_filter to act on every match.', + description: 'Maximum rows to return or affect (optional, default 100)', }, mapping: { type: 'object', @@ -4439,6 +4407,23 @@ export const ManageCustomToolOperationValues = [ ManageCustomToolOperation.list, ] as const +export const ManageFolderOperation = { + create: 'create', + rename: 'rename', + move: 'move', + delete: 'delete', +} as const + +export type ManageFolderOperation = + (typeof ManageFolderOperation)[keyof typeof ManageFolderOperation] + +export const ManageFolderOperationValues = [ + ManageFolderOperation.create, + ManageFolderOperation.rename, + ManageFolderOperation.move, + ManageFolderOperation.delete, +] as const + export const ManageMcpToolOperation = { add: 'add', edit: 'edit', @@ -4615,12 +4600,10 @@ export const TOOL_CATALOG: Record = { [CrawlWebsite.id]: CrawlWebsite, [CreateFile.id]: CreateFile, [CreateFileFolder.id]: CreateFileFolder, - [CreateFolder.id]: CreateFolder, [CreateWorkflow.id]: CreateWorkflow, [CreateWorkspaceMcpServer.id]: CreateWorkspaceMcpServer, [DeleteFile.id]: DeleteFile, [DeleteFileFolder.id]: DeleteFileFolder, - [DeleteFolder.id]: DeleteFolder, [DeleteWorkflow.id]: DeleteWorkflow, [DeleteWorkspaceMcpServer.id]: DeleteWorkspaceMcpServer, [Deploy.id]: Deploy, @@ -4653,7 +4636,6 @@ export const TOOL_CATALOG: Record = { [Knowledge.id]: Knowledge, [KnowledgeBase.id]: KnowledgeBase, [ListFileFolders.id]: ListFileFolders, - [ListFolders.id]: ListFolders, [ListIntegrationTools.id]: ListIntegrationTools, [ListUserWorkspaces.id]: ListUserWorkspaces, [ListWorkspaceMcpServers.id]: ListWorkspaceMcpServers, @@ -4661,6 +4643,7 @@ export const TOOL_CATALOG: Record = { [LoadIntegrationTool.id]: LoadIntegrationTool, [ManageCredential.id]: ManageCredential, [ManageCustomTool.id]: ManageCustomTool, + [ManageFolder.id]: ManageFolder, [ManageMcpTool.id]: ManageMcpTool, [ManageScheduledTask.id]: ManageScheduledTask, [ManageSkill.id]: ManageSkill, @@ -4668,7 +4651,6 @@ export const TOOL_CATALOG: Record = { [Media.id]: Media, [MoveFile.id]: MoveFile, [MoveFileFolder.id]: MoveFileFolder, - [MoveFolder.id]: MoveFolder, [MoveWorkflow.id]: MoveWorkflow, [OauthGetAuthLink.id]: OauthGetAuthLink, [OauthRequestAccess.id]: OauthRequestAccess, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 31171237e7e..6c5536f4352 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -180,27 +180,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - create_folder: { - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Folder name.', - }, - parentId: { - type: 'string', - description: 'Optional parent folder ID.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID.', - }, - }, - required: ['name'], - }, - resultSchema: undefined, - }, create_workflow: { parameters: { type: 'object', @@ -305,22 +284,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - delete_folder: { - parameters: { - type: 'object', - properties: { - folderIds: { - type: 'array', - description: 'The folder IDs to delete.', - items: { - type: 'string', - }, - }, - }, - required: ['folderIds'], - }, - resultSchema: undefined, - }, delete_workflow: { parameters: { type: 'object', @@ -2154,18 +2117,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - list_folders: { - parameters: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Optional workspace ID to list folders for.', - }, - }, - }, - resultSchema: undefined, - }, list_integration_tools: { parameters: { properties: { @@ -2345,6 +2296,45 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + manage_folder: { + parameters: { + type: 'object', + properties: { + destinationPath: { + type: 'string', + description: + 'Destination parent folder\'s VFS path for move/create. Omit (or pass "workflows") to target the workspace root.', + }, + folderId: { + type: 'string', + description: + 'Target folder ID, used as a fallback when path is not given. Readable from a contained workflow\'s meta.json "folderId".', + }, + name: { + type: 'string', + description: + 'Folder name. Required for rename (the new name); for create when you pass a destination parent instead of a full path.', + }, + operation: { + type: 'string', + description: 'The operation to perform.', + enum: ['create', 'rename', 'move', 'delete'], + }, + parentId: { + type: 'string', + description: + 'Destination parent folder ID, used as a fallback when destinationPath is not given.', + }, + path: { + type: 'string', + description: + 'Target folder\'s VFS path (e.g. "workflows/Marketing/Q3 Campaigns"), per-segment percent-encoded like every VFS path. Identifies the folder for rename/move/delete; for create it is the new folder\'s full path (its parent must already exist).', + }, + }, + required: ['operation'], + }, + resultSchema: undefined, + }, manage_mcp_tool: { parameters: { type: 'object', @@ -2581,24 +2571,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - move_folder: { - parameters: { - type: 'object', - properties: { - folderId: { - type: 'string', - description: 'The folder ID to move.', - }, - parentId: { - type: 'string', - description: - 'Target parent folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['folderId'], - }, - resultSchema: undefined, - }, move_workflow: { parameters: { type: 'object', @@ -3686,8 +3658,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, limit: { type: 'number', - description: - 'Maximum rows to return or affect (optional, default 100). Omit on update_rows_by_filter / delete_rows_by_filter to act on every match.', + description: 'Maximum rows to return or affect (optional, default 100)', }, mapping: { type: 'object', diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index 48784f1a6bb..b39027e3526 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -2,10 +2,8 @@ import { createLogger } from '@sim/logger' import { CheckDeploymentStatus, CompleteScheduledTask, - CreateFolder, CreateWorkflow, CreateWorkspaceMcpServer, - DeleteFolder, DeleteWorkflow, DeleteWorkspaceMcpServer, DeployApi, @@ -23,18 +21,17 @@ import { GetWorkflowRunOptions, Glob as GlobTool, Grep as GrepTool, - ListFolders, ListIntegrationTools, ListUserWorkspaces, ListWorkspaceMcpServers, LoadDeployment, ManageCredential, ManageCustomTool, + ManageFolder, ManageMcpTool, ManageScheduledTask, ManageSkill, MaterializeFile, - MoveFolder, MoveWorkflow, OauthGetAuthLink, OauthRequestAccess, @@ -92,12 +89,10 @@ import { executeOpenResource } from '../tools/handlers/resources' import { executeRestoreResource } from '../tools/handlers/restore-resource' import { executeVfsGlob, executeVfsGrep, executeVfsRead } from '../tools/handlers/vfs' import { - executeCreateFolder, executeCreateWorkflow, - executeDeleteFolder, executeDeleteWorkflow, executeGenerateApiKey, - executeMoveFolder, + executeManageFolder, executeMoveWorkflow, executeRenameWorkflow, executeRunBlock, @@ -113,7 +108,6 @@ import { executeGetDeployedWorkflowState, executeGetWorkflowData, executeGetWorkflowRunOptions, - executeListFolders, executeListUserWorkspaces, } from '../tools/handlers/workflow/queries' import { registerHandlers } from './executor' @@ -140,7 +134,6 @@ function h(fn: (params: any, context: any) => Promise): ToolHandler { function buildHandlerMap(): Record { return { [ListUserWorkspaces.id]: h((_p, c) => executeListUserWorkspaces(c)), - [ListFolders.id]: h(executeListFolders), [GetWorkflowData.id]: h(executeGetWorkflowData), [GetWorkflowRunOptions.id]: h(executeGetWorkflowRunOptions), [GetBlockOutputs.id]: h(executeGetBlockOutputs), @@ -148,12 +141,10 @@ function buildHandlerMap(): Record { [GetDeployedWorkflowState.id]: h(executeGetDeployedWorkflowState), [CreateWorkflow.id]: h(executeCreateWorkflow), - [CreateFolder.id]: h(executeCreateFolder), [DeleteWorkflow.id]: h(executeDeleteWorkflow), - [DeleteFolder.id]: h(executeDeleteFolder), [RenameWorkflow.id]: h(executeRenameWorkflow), [MoveWorkflow.id]: h(executeMoveWorkflow), - [MoveFolder.id]: h(executeMoveFolder), + [ManageFolder.id]: h(executeManageFolder), [RunWorkflow.id]: h(executeRunWorkflow), [RunWorkflowUntilBlock.id]: h(executeRunWorkflowUntilBlock), [RunFromBlock.id]: h(executeRunFromBlock), diff --git a/apps/sim/lib/copilot/tools/handlers/param-types.ts b/apps/sim/lib/copilot/tools/handlers/param-types.ts index b0c28e25b7a..da437b4993a 100644 --- a/apps/sim/lib/copilot/tools/handlers/param-types.ts +++ b/apps/sim/lib/copilot/tools/handlers/param-types.ts @@ -27,10 +27,6 @@ export interface GetBlockUpstreamReferencesParams { blockIds: string[] } -export interface ListFoldersParams { - workspaceId?: string -} - // === Workflow Mutation Params === export interface CreateWorkflowParams { @@ -262,6 +258,15 @@ export interface DeleteFolderParams { folderIds: string[] } +export interface ManageFolderParams { + operation: string + path?: string + folderId?: string + name?: string + destinationPath?: string + parentId?: string | null +} + export interface UpdateWorkspaceMcpServerParams { serverId: string name?: string diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 7ae8c2e6a4d..80f68ba61f7 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -8,6 +8,11 @@ import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblock import { eq } from 'drizzle-orm' import { performCreateWorkspaceApiKey } from '@/lib/api-key/orchestration' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' +import { + buildVfsFolderPathMap, + decodeVfsPathSegments, + encodeVfsPathSegments, +} from '@/lib/copilot/vfs/path-utils' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' import { getSocketServerUrl } from '@/lib/core/utils/urls' @@ -307,6 +312,7 @@ import type { DeleteFolderParams, DeleteWorkflowParams, GenerateApiKeyParams, + ManageFolderParams, MoveFolderParams, MoveWorkflowParams, RenameFolderParams, @@ -1242,6 +1248,144 @@ async function executeRenameFolder( } } +/** + * Strip the `workflows/` VFS prefix from a folder path, returning the + * folder-relative remainder. `workflows` (or an empty path) maps to the + * workspace root and yields an empty string. + */ +function workflowFolderRelativePath(rawPath: string): string { + const trimmed = rawPath.trim().replace(/^\/+|\/+$/g, '') + if (!trimmed || trimmed === 'workflows') return '' + return trimmed.startsWith('workflows/') ? trimmed.slice('workflows/'.length) : trimmed +} + +/** + * Resolve a workflow-folder VFS path (e.g. `workflows/Marketing/Q3 Campaigns`) + * to its folderId by inverting the same {@link buildVfsFolderPathMap} the VFS + * uses to serve folder paths, so a path the agent sees via glob round-trips to + * the right id. Returns null for the workspace root or an unknown path. + */ +async function resolveWorkflowFolderIdByPath( + workspaceId: string, + rawPath: string +): Promise { + const relative = workflowFolderRelativePath(rawPath) + if (!relative) return null + const canonical = encodeVfsPathSegments(decodeVfsPathSegments(relative)) + const folders = await listFolders(workspaceId) + for (const [folderId, encodedPath] of buildVfsFolderPathMap(folders).entries()) { + if (encodedPath === canonical) return folderId + } + return null +} + +/** Resolve the folder a manage_folder op targets, preferring folderId over path. */ +async function resolveManageFolderTarget( + workspaceId: string, + params: ManageFolderParams +): Promise<{ folderId: string } | { error: string }> { + const directId = typeof params.folderId === 'string' ? params.folderId.trim() : '' + if (directId) return { folderId: directId } + const path = typeof params.path === 'string' ? params.path.trim() : '' + if (!path) return { error: 'Provide the folder path (e.g. "workflows/Marketing") or folderId.' } + const folderId = await resolveWorkflowFolderIdByPath(workspaceId, path) + if (!folderId) return { error: `Folder not found at ${path}` } + return { folderId } +} + +/** + * Resolve the destination parent for move/create. parentId/destinationPath are + * optional; their absence (or an explicit root) targets the workspace root + * (parentId null). + */ +async function resolveManageFolderParent( + workspaceId: string, + params: ManageFolderParams +): Promise<{ parentId: string | null } | { error: string }> { + const directId = typeof params.parentId === 'string' ? params.parentId.trim() : '' + if (directId) return { parentId: directId } + if (params.parentId === null) return { parentId: null } + const dest = typeof params.destinationPath === 'string' ? params.destinationPath.trim() : '' + if (!dest || !workflowFolderRelativePath(dest)) return { parentId: null } + const parentId = await resolveWorkflowFolderIdByPath(workspaceId, dest) + if (!parentId) return { error: `Destination folder not found at ${dest}` } + return { parentId } +} + +/** + * Single entry point for folder CRUD (create/rename/move/delete). Resolves the + * VFS-path/folderId handles, then delegates to the existing folder handlers so + * all DB orchestration (performCreateFolder / performUpdateFolder / + * performDeleteFolder) stays in one place. + */ +export async function executeManageFolder( + params: ManageFolderParams, + context: ExecutionContext +): Promise { + try { + const operation = typeof params?.operation === 'string' ? params.operation.trim() : '' + const workspaceId = context.workspaceId || (await getDefaultWorkspaceId(context.userId)) + + switch (operation) { + case 'create': { + let name = typeof params.name === 'string' ? params.name.trim() : '' + let parentId: string | null = null + const path = typeof params.path === 'string' ? params.path.trim() : '' + if (!name && path) { + const segments = decodeVfsPathSegments(workflowFolderRelativePath(path)) + if (segments.length === 0) { + return { success: false, error: 'create requires a folder name or path' } + } + name = segments[segments.length - 1] + const parentSegments = segments.slice(0, -1) + if (parentSegments.length > 0) { + const resolved = await resolveWorkflowFolderIdByPath( + workspaceId, + encodeVfsPathSegments(parentSegments) + ) + if (!resolved) { + return { success: false, error: `Parent folder not found for ${path}` } + } + parentId = resolved + } + } else { + const parent = await resolveManageFolderParent(workspaceId, params) + if ('error' in parent) return { success: false, error: parent.error } + parentId = parent.parentId + } + if (!name) return { success: false, error: 'create requires a folder name or path' } + return executeCreateFolder({ name, parentId: parentId ?? undefined, workspaceId }, context) + } + case 'rename': { + const name = typeof params.name === 'string' ? params.name.trim() : '' + if (!name) return { success: false, error: 'rename requires a new name' } + const target = await resolveManageFolderTarget(workspaceId, params) + if ('error' in target) return { success: false, error: target.error } + return executeRenameFolder({ folderId: target.folderId, name }, context) + } + case 'move': { + const target = await resolveManageFolderTarget(workspaceId, params) + if ('error' in target) return { success: false, error: target.error } + const parent = await resolveManageFolderParent(workspaceId, params) + if ('error' in parent) return { success: false, error: parent.error } + return executeMoveFolder({ folderId: target.folderId, parentId: parent.parentId }, context) + } + case 'delete': { + const target = await resolveManageFolderTarget(workspaceId, params) + if ('error' in target) return { success: false, error: target.error } + return executeDeleteFolder({ folderIds: [target.folderId] }, context) + } + default: + return { + success: false, + error: `Unknown operation "${operation}". Use create, rename, move, or delete.`, + } + } + } catch (error) { + return { success: false, error: toError(error).message } + } +} + export async function executeRunBlock( params: RunBlockParams, context: ExecutionContext diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts b/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts index 41ea582105f..f7b556ca453 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/queries.ts @@ -14,19 +14,18 @@ import { } from '@/lib/workflows/persistence/utils' import { resolveTriggerRunOptions, toPublicRunOption } from '@/lib/workflows/triggers/run-options' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' -import { getWorkflowById, listFolders } from '@/lib/workflows/utils' +import { getWorkflowById } from '@/lib/workflows/utils' import { listUserWorkspaces } from '@/lib/workspaces/utils' import { getBlock } from '@/blocks/registry' import { normalizeName } from '@/executor/constants' import type { Loop, Parallel } from '@/stores/workflows/workflow/types' -import { ensureWorkflowAccess, ensureWorkspaceAccess, getDefaultWorkspaceId } from '../access' +import { ensureWorkflowAccess } from '../access' import type { GetBlockOutputsParams, GetBlockUpstreamReferencesParams, GetDeployedWorkflowStateParams, GetWorkflowDataParams, GetWorkflowRunOptionsParams, - ListFoldersParams, } from '../param-types' export async function executeListUserWorkspaces( @@ -41,32 +40,6 @@ export async function executeListUserWorkspaces( } } -export async function executeListFolders( - params: ListFoldersParams, - context: ExecutionContext -): Promise { - try { - const workspaceId = - (params?.workspaceId as string | undefined) || - context.workspaceId || - (await getDefaultWorkspaceId(context.userId)) - - await ensureWorkspaceAccess(workspaceId, context.userId, 'read') - - const folders = await listFolders(workspaceId) - - return { - success: true, - output: { - workspaceId, - folders, - }, - } - } catch (error) { - return { success: false, error: toError(error).message } - } -} - export async function executeGetWorkflowRunOptions( params: GetWorkflowRunOptionsParams, context: ExecutionContext diff --git a/apps/sim/lib/copilot/tools/mcp/definitions.ts b/apps/sim/lib/copilot/tools/mcp/definitions.ts index 8adede9671b..94dc7ebfe44 100644 --- a/apps/sim/lib/copilot/tools/mcp/definitions.ts +++ b/apps/sim/lib/copilot/tools/mcp/definitions.ts @@ -34,23 +34,6 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ }, annotations: { readOnlyHint: true }, }, - { - name: 'list_folders', - toolId: 'list_folders', - description: - 'List all folders in a workspace. Returns folder IDs, names, and parent relationships for organizing workflows.', - inputSchema: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: 'Workspace ID to list folders from.', - }, - }, - required: ['workspaceId'], - }, - annotations: { readOnlyHint: true }, - }, { name: 'create_workflow', toolId: 'create_workflow', @@ -81,27 +64,41 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ annotations: { destructiveHint: false }, }, { - name: 'create_folder', - toolId: 'create_folder', + name: 'manage_folder', + toolId: 'manage_folder', description: - 'Create a new folder for organizing workflows. Use parentId to create nested folder hierarchies.', + 'Create, rename, move, or delete workflow folders. Reference a folder by its VFS path (e.g. "workflows/Marketing/Q3 Campaigns") or folderId.', inputSchema: { type: 'object', properties: { + operation: { + type: 'string', + enum: ['create', 'rename', 'move', 'delete'], + description: 'The operation to perform.', + }, + path: { + type: 'string', + description: + 'Target folder VFS path (e.g. "workflows/Marketing"). For create, the new folder\'s full path.', + }, + folderId: { + type: 'string', + description: 'Target folder ID (fallback to path).', + }, name: { type: 'string', - description: 'Name for the new folder.', + description: 'Folder name. Required for rename; for create when not using a full path.', }, - workspaceId: { + destinationPath: { type: 'string', - description: 'Optional workspace ID. Uses default workspace if not provided.', + description: 'Destination parent folder path for move/create (omit for workspace root).', }, parentId: { type: 'string', - description: 'Optional parent folder ID for nested folders.', + description: 'Destination parent folder ID (fallback to destinationPath).', }, }, - required: ['name'], + required: ['operation'], }, annotations: { destructiveHint: false }, }, @@ -146,28 +143,6 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ }, annotations: { destructiveHint: false, idempotentHint: true }, }, - { - name: 'move_folder', - toolId: 'move_folder', - description: - 'Move a folder into another folder. Omit parentId or pass empty string to move to workspace root.', - inputSchema: { - type: 'object', - properties: { - folderId: { - type: 'string', - description: 'The folder ID to move.', - }, - parentId: { - type: 'string', - description: - 'Target parent folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['folderId'], - }, - annotations: { destructiveHint: false, idempotentHint: true }, - }, { name: 'get_deployed_workflow_state', toolId: 'get_deployed_workflow_state', diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index b5a8701421d..f2e7f2fb477 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -7,26 +7,41 @@ import { DYNAMIC_MODEL_PROVIDERS, PROVIDER_DEFINITIONS } from '@/providers/model import type { ToolConfig } from '@/tools/types' /** - * Serialize workflow metadata for VFS meta.json + * Serialize workflow metadata for VFS meta.json. + * + * `locked` is the EFFECTIVE lock — true when the workflow is locked directly or + * sits inside a locked folder. A locked workflow cannot be edited, moved, + * renamed, or deleted (mutations are rejected server-side with a 423). The + * mothership should read this before attempting any workflow mutation. + * `inheritedFolderLock` carries the resolved containing-folder lock (the + * caller computes folder inheritance; see workspace-vfs materializeWorkflows). */ -export function serializeWorkflowMeta(wf: { - id: string - name: string - description?: string | null - folderId?: string | null - isDeployed: boolean - deployedAt?: Date | null - runCount: number - lastRunAt?: Date | null - createdAt: Date - updatedAt: Date -}): string { +export function serializeWorkflowMeta( + wf: { + id: string + name: string + description?: string | null + folderId?: string | null + isDeployed: boolean + deployedAt?: Date | null + runCount: number + lastRunAt?: Date | null + createdAt: Date + updatedAt: Date + locked?: boolean + }, + options?: { inheritedFolderLock?: boolean } +): string { + const directLock = wf.locked ?? false + const locked = directLock || (options?.inheritedFolderLock ?? false) return JSON.stringify( { id: wf.id, name: wf.name, description: wf.description || undefined, folderId: wf.folderId || undefined, + locked, + lockedBy: locked ? (directLock ? 'workflow' : 'folder') : undefined, isDeployed: wf.isDeployed, deployedAt: wf.deployedAt?.toISOString(), runCount: wf.runCount, diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 3b289b41081..0038755e603 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -1016,6 +1016,37 @@ export class WorkspaceVFS { return buildVfsFolderPathMap(folders) } + /** + * Resolve the set of folder IDs that are effectively locked — locked directly + * or via a locked ancestor folder. A workflow inside any of these folders is + * itself immutable, so its meta.json must report `locked: true`. Mirrors the + * folder-chain walk in `@sim/workflow-authz` getFolderLockStatus, but resolves + * the whole workspace in memory to avoid a per-workflow DB round trip. + */ + private computeLockedFolderIds( + folders: Array<{ folderId: string; parentId: string | null; locked: boolean }> + ): Set { + const byId = new Map(folders.map((f) => [f.folderId, f])) + const lockedFolderIds = new Set() + + for (const folder of folders) { + let current: string | null = folder.folderId + const visited = new Set() + while (current && !visited.has(current)) { + visited.add(current) + const node = byId.get(current) + if (!node) break + if (node.locked) { + lockedFolderIds.add(folder.folderId) + break + } + current = node.parentId + } + } + + return lockedFolderIds + } + /** * Materialize all workflows using the shared listWorkflows function. * Workflows are nested under their folder paths in the VFS: @@ -1031,6 +1062,7 @@ export class WorkspaceVFS { ]) const folderPaths = this.buildFolderPaths(folderRows) + const lockedFolderIds = this.computeLockedFolderIds(folderRows) // NOTE: materialization is a pure READ. Alias backing (changelog/plan // folders + files) is ensured at write time — workflow create/rename @@ -1056,7 +1088,8 @@ export class WorkspaceVFS { const prefix = `${canonicalWorkflowVfsDir({ name: wf.name, folderPath })}/` const workflowPath = prefix.replace(/\/$/, '') - this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf)) + const inheritedFolderLock = wf.folderId ? lockedFolderIds.has(wf.folderId) : false + this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf, { inheritedFolderLock })) if (workflowArtifactsEnabled) { const changelog = findWorkspaceFileRecord( diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts index 219c194e080..2f20e012a99 100644 --- a/apps/sim/lib/workflows/utils.ts +++ b/apps/sim/lib/workflows/utils.ts @@ -608,6 +608,7 @@ export async function listFolders(workspaceId: string) { folderName: workflowFolder.name, parentId: workflowFolder.parentId, sortOrder: workflowFolder.sortOrder, + locked: workflowFolder.locked, }) .from(workflowFolder) .where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt))) From 4280949bdcfb69bca98671963b742e115e1835fd Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Jun 2026 10:42:24 -0700 Subject: [PATCH 2/5] fix(mship): manage_folder bug fixes --- .../tools/handlers/workflow/mutations.ts | 64 ++- apps/sim/lib/copilot/tools/mcp/definitions.ts | 510 ------------------ 2 files changed, 36 insertions(+), 538 deletions(-) delete mode 100644 apps/sim/lib/copilot/tools/mcp/definitions.ts diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 80f68ba61f7..e72abc1082d 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -1260,35 +1260,37 @@ function workflowFolderRelativePath(rawPath: string): string { } /** - * Resolve a workflow-folder VFS path (e.g. `workflows/Marketing/Q3 Campaigns`) - * to its folderId by inverting the same {@link buildVfsFolderPathMap} the VFS - * uses to serve folder paths, so a path the agent sees via glob round-trips to - * the right id. Returns null for the workspace root or an unknown path. + * Load a lookup from each folder's canonical encoded VFS path to its id by + * inverting the same {@link buildVfsFolderPathMap} the VFS uses to serve folder + * paths, so a path the agent sees via glob round-trips to the right id. Fetched + * once per manage_folder call and reused across target + parent resolution. */ -async function resolveWorkflowFolderIdByPath( - workspaceId: string, - rawPath: string -): Promise { +async function loadFolderPathToIdMap(workspaceId: string): Promise> { + const byPath = new Map() + for (const [folderId, encodedPath] of buildVfsFolderPathMap( + await listFolders(workspaceId) + ).entries()) { + byPath.set(encodedPath, folderId) + } + return byPath +} + +function lookupFolderIdByPath(rawPath: string, byPath: Map): string | null { const relative = workflowFolderRelativePath(rawPath) if (!relative) return null - const canonical = encodeVfsPathSegments(decodeVfsPathSegments(relative)) - const folders = await listFolders(workspaceId) - for (const [folderId, encodedPath] of buildVfsFolderPathMap(folders).entries()) { - if (encodedPath === canonical) return folderId - } - return null + return byPath.get(encodeVfsPathSegments(decodeVfsPathSegments(relative))) ?? null } /** Resolve the folder a manage_folder op targets, preferring folderId over path. */ async function resolveManageFolderTarget( - workspaceId: string, - params: ManageFolderParams + params: ManageFolderParams, + getFolderPaths: () => Promise> ): Promise<{ folderId: string } | { error: string }> { const directId = typeof params.folderId === 'string' ? params.folderId.trim() : '' if (directId) return { folderId: directId } const path = typeof params.path === 'string' ? params.path.trim() : '' if (!path) return { error: 'Provide the folder path (e.g. "workflows/Marketing") or folderId.' } - const folderId = await resolveWorkflowFolderIdByPath(workspaceId, path) + const folderId = lookupFolderIdByPath(path, await getFolderPaths()) if (!folderId) return { error: `Folder not found at ${path}` } return { folderId } } @@ -1299,15 +1301,15 @@ async function resolveManageFolderTarget( * (parentId null). */ async function resolveManageFolderParent( - workspaceId: string, - params: ManageFolderParams + params: ManageFolderParams, + getFolderPaths: () => Promise> ): Promise<{ parentId: string | null } | { error: string }> { const directId = typeof params.parentId === 'string' ? params.parentId.trim() : '' if (directId) return { parentId: directId } if (params.parentId === null) return { parentId: null } const dest = typeof params.destinationPath === 'string' ? params.destinationPath.trim() : '' if (!dest || !workflowFolderRelativePath(dest)) return { parentId: null } - const parentId = await resolveWorkflowFolderIdByPath(workspaceId, dest) + const parentId = lookupFolderIdByPath(dest, await getFolderPaths()) if (!parentId) return { error: `Destination folder not found at ${dest}` } return { parentId } } @@ -1326,6 +1328,12 @@ export async function executeManageFolder( const operation = typeof params?.operation === 'string' ? params.operation.trim() : '' const workspaceId = context.workspaceId || (await getDefaultWorkspaceId(context.userId)) + // Fetch the workspace folder list at most once, lazily — only when a path + // (vs an explicit id) actually needs resolving, and shared across the + // target + parent lookups a single move/create performs. + let folderPathsPromise: Promise> | undefined + const getFolderPaths = () => (folderPathsPromise ??= loadFolderPathToIdMap(workspaceId)) + switch (operation) { case 'create': { let name = typeof params.name === 'string' ? params.name.trim() : '' @@ -1339,9 +1347,9 @@ export async function executeManageFolder( name = segments[segments.length - 1] const parentSegments = segments.slice(0, -1) if (parentSegments.length > 0) { - const resolved = await resolveWorkflowFolderIdByPath( - workspaceId, - encodeVfsPathSegments(parentSegments) + const resolved = lookupFolderIdByPath( + encodeVfsPathSegments(parentSegments), + await getFolderPaths() ) if (!resolved) { return { success: false, error: `Parent folder not found for ${path}` } @@ -1349,7 +1357,7 @@ export async function executeManageFolder( parentId = resolved } } else { - const parent = await resolveManageFolderParent(workspaceId, params) + const parent = await resolveManageFolderParent(params, getFolderPaths) if ('error' in parent) return { success: false, error: parent.error } parentId = parent.parentId } @@ -1359,19 +1367,19 @@ export async function executeManageFolder( case 'rename': { const name = typeof params.name === 'string' ? params.name.trim() : '' if (!name) return { success: false, error: 'rename requires a new name' } - const target = await resolveManageFolderTarget(workspaceId, params) + const target = await resolveManageFolderTarget(params, getFolderPaths) if ('error' in target) return { success: false, error: target.error } return executeRenameFolder({ folderId: target.folderId, name }, context) } case 'move': { - const target = await resolveManageFolderTarget(workspaceId, params) + const target = await resolveManageFolderTarget(params, getFolderPaths) if ('error' in target) return { success: false, error: target.error } - const parent = await resolveManageFolderParent(workspaceId, params) + const parent = await resolveManageFolderParent(params, getFolderPaths) if ('error' in parent) return { success: false, error: parent.error } return executeMoveFolder({ folderId: target.folderId, parentId: parent.parentId }, context) } case 'delete': { - const target = await resolveManageFolderTarget(workspaceId, params) + const target = await resolveManageFolderTarget(params, getFolderPaths) if ('error' in target) return { success: false, error: target.error } return executeDeleteFolder({ folderIds: [target.folderId] }, context) } diff --git a/apps/sim/lib/copilot/tools/mcp/definitions.ts b/apps/sim/lib/copilot/tools/mcp/definitions.ts deleted file mode 100644 index 94dc7ebfe44..00000000000 --- a/apps/sim/lib/copilot/tools/mcp/definitions.ts +++ /dev/null @@ -1,510 +0,0 @@ -import type { Tool } from '@modelcontextprotocol/sdk/types.js' - -export type ToolAnnotations = NonNullable - -export type DirectToolDef = { - name: string - description: string - inputSchema: Tool['inputSchema'] - toolId: string - annotations?: ToolAnnotations -} - -export type SubagentToolDef = { - name: string - description: string - inputSchema: Tool['inputSchema'] - agentId: string - annotations?: ToolAnnotations -} - -/** - * Direct tools that execute immediately without LLM orchestration. - * These are fast database queries that don't need AI reasoning. - */ -export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ - { - name: 'list_workspaces', - toolId: 'list_user_workspaces', - description: - 'List all workspaces the user has access to. Returns workspace IDs, names, and roles. Use this first to determine which workspace to operate in.', - inputSchema: { - type: 'object', - properties: {}, - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'create_workflow', - toolId: 'create_workflow', - description: - 'Create a new empty workflow. Returns the new workflow ID. Always call this FIRST before sim_workflow for new workflows. Use workspaceId to place it in a specific workspace.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: 'Name for the new workflow.', - }, - workspaceId: { - type: 'string', - description: 'Optional workspace ID. Uses default workspace if not provided.', - }, - folderId: { - type: 'string', - description: 'Optional folder ID to place the workflow in.', - }, - description: { - type: 'string', - description: 'Optional description for the workflow.', - }, - }, - required: ['name'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'manage_folder', - toolId: 'manage_folder', - description: - 'Create, rename, move, or delete workflow folders. Reference a folder by its VFS path (e.g. "workflows/Marketing/Q3 Campaigns") or folderId.', - inputSchema: { - type: 'object', - properties: { - operation: { - type: 'string', - enum: ['create', 'rename', 'move', 'delete'], - description: 'The operation to perform.', - }, - path: { - type: 'string', - description: - 'Target folder VFS path (e.g. "workflows/Marketing"). For create, the new folder\'s full path.', - }, - folderId: { - type: 'string', - description: 'Target folder ID (fallback to path).', - }, - name: { - type: 'string', - description: 'Folder name. Required for rename; for create when not using a full path.', - }, - destinationPath: { - type: 'string', - description: 'Destination parent folder path for move/create (omit for workspace root).', - }, - parentId: { - type: 'string', - description: 'Destination parent folder ID (fallback to destinationPath).', - }, - }, - required: ['operation'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'rename_workflow', - toolId: 'rename_workflow', - description: 'Rename an existing workflow.', - inputSchema: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'The workflow ID to rename.', - }, - name: { - type: 'string', - description: 'The new name for the workflow.', - }, - }, - required: ['workflowId', 'name'], - }, - annotations: { destructiveHint: false, idempotentHint: true }, - }, - { - name: 'move_workflow', - toolId: 'move_workflow', - description: - 'Move a workflow into a different folder. Omit folderId or pass empty string to move to workspace root.', - inputSchema: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'The workflow ID to move.', - }, - folderId: { - type: 'string', - description: 'Target folder ID. Omit or pass empty string to move to workspace root.', - }, - }, - required: ['workflowId'], - }, - annotations: { destructiveHint: false, idempotentHint: true }, - }, - { - name: 'get_deployed_workflow_state', - toolId: 'get_deployed_workflow_state', - description: - 'Get the deployed (production) state of a workflow. Returns the full workflow definition as deployed, or indicates if the workflow is not yet deployed.', - inputSchema: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to get the deployed state for.', - }, - }, - required: ['workflowId'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'generate_api_key', - toolId: 'generate_api_key', - description: - 'Generate a new workspace API key for calling workflow API endpoints. The key is only shown once — tell the user to save it immediately.', - inputSchema: { - type: 'object', - properties: { - name: { - type: 'string', - description: - 'A descriptive name for the API key (e.g., "production-key", "dev-testing").', - }, - workspaceId: { - type: 'string', - description: "Optional workspace ID. Defaults to user's default workspace.", - }, - }, - required: ['name'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'create_job', - toolId: 'create_job', - description: - 'Create a scheduled background job that runs a prompt against Sim at a specified frequency or time. Use for polling, reminders, or deferred tasks. Provide cron for recurring jobs or time for one-time execution.', - inputSchema: { - type: 'object', - properties: { - title: { - type: 'string', - description: 'A short descriptive title for the job (e.g., "Email Poller").', - }, - prompt: { - type: 'string', - description: 'The prompt to execute when the job fires.', - }, - cron: { - type: 'string', - description: - 'Cron expression for recurring jobs (e.g., "*/5 * * * *" for every 5 minutes).', - }, - time: { - type: 'string', - description: - 'ISO 8601 datetime for one-time jobs or cron start time (e.g., "2026-03-06T09:00:00").', - }, - timezone: { - type: 'string', - description: 'IANA timezone (default: UTC).', - }, - lifecycle: { - type: 'string', - description: - '"persistent" (default, runs indefinitely) or "until_complete" (runs until complete_scheduled_task is called).', - }, - successCondition: { - type: 'string', - description: - 'What must happen for the job to be considered complete. Used with until_complete lifecycle.', - }, - maxRuns: { - type: 'number', - description: 'Maximum number of executions before the job auto-completes. Safety limit.', - }, - }, - required: ['title', 'prompt'], - }, - annotations: { destructiveHint: false }, - }, -] - -export const SUBAGENT_TOOL_DEFS: SubagentToolDef[] = [ - { - name: 'sim_workflow', - agentId: 'workflow', - description: `Create, modify, test, debug, and organize workflows end-to-end in a single step. - -USE THIS WHEN: -- Building a new workflow from scratch -- Modifying an existing workflow -- You want to gather information and build in one pass -- Moving, renaming, or organizing workflows and folders - -WORKFLOW ID (REQUIRED): -- For NEW workflows: First call create_workflow to get a workflowId, then pass it here -- For EXISTING workflows: Always pass the workflowId parameter - -CAN DO: -- Gather information about blocks, credentials, patterns -- Search documentation and patterns for best practices -- Add, modify, or remove blocks -- Configure block settings and connections -- Set environment variables and workflow variables -- Move, rename, delete workflows and folders -- Run or inspect workflows through the nested run/debug specialists when validation is needed -- Delegate deployment or auth setup to the nested specialists when needed - -CANNOT DO: -- Replace dedicated testing flows like sim_test when you want a standalone execution-only pass -- Replace dedicated deploy flows like sim_deploy when you want deployment as a separate step - -WORKFLOW: -1. Call create_workflow to get a workflowId (for new workflows) -2. Call sim_workflow with the request and workflowId -3. Workflow agent gathers info, builds, and can delegate run/debug/auth/deploy help in one pass -4. Call sim_test when you want a dedicated execution-only verification pass -5. Optionally call sim_deploy to make it externally accessible`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'What you want to build, modify, or organize.', - }, - workflowId: { - type: 'string', - description: - 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_discovery', - agentId: 'discovery', - description: `Find workflows by their contents or functionality when the user doesn't know the exact name or ID. - -USE THIS WHEN: -- User describes a workflow by what it does: "the one that sends emails", "my Slack notification workflow" -- User refers to workflow contents: "the workflow with the OpenAI block" -- User needs to search/match workflows by functionality or description - -DO NOT USE (use direct tools instead): -- User knows the workflow name → use get_workflow -- User wants to list all workflows → use list_workflows -- User wants to list workspaces → use list_workspaces -- User wants to list folders → use list_folders`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workspaceId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'sim_deploy', - agentId: 'deploy', - description: `Deploy a workflow to make it accessible externally. Workflows can be tested without deploying, but deployment is needed for API access, chat UIs, or MCP exposure. - -DEPLOYMENT TYPES: -- "deploy as api" - REST API endpoint for programmatic access -- "deploy as chat" - Managed chat UI with auth options -- "deploy as mcp" - Expose as MCP tool on an MCP server for AI agents to call - -MCP DEPLOYMENT FLOW: -The deploy subagent will automatically: list available MCP servers → create one if needed → deploy the workflow as an MCP tool to that server. You can specify server name, tool name, and tool description. - -ALSO CAN: -- Get the deployed (production) state to compare with draft -- Generate workspace API keys for calling deployed workflows -- List and create MCP servers in the workspace`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'The deployment request, e.g. "deploy as api" or "deploy as chat"', - }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to deploy.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_test', - agentId: 'run', - description: `Run a workflow and verify its outputs. Works on both deployed and undeployed (draft) workflows. Use after building to verify correctness. - -Supports full and partial execution: -- Full run with test inputs -- Stop after a specific block (run_workflow_until_block) -- Run a single block in isolation (run_block) -- Resume from a specific block (run_from_block)`, - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { - type: 'string', - description: 'REQUIRED. The workflow ID to test.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_auth', - agentId: 'auth', - description: - 'Check OAuth connection status, list connected services, and initiate new OAuth connections. Use when a workflow needs third-party service access (Google, Slack, GitHub, etc.). In MCP/headless mode, returns an authorization URL the user must open in their browser to complete the OAuth flow.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, - { - name: 'sim_knowledge', - agentId: 'knowledge', - description: - 'Manage knowledge bases for RAG-powered document retrieval. Supports listing, creating, updating, and deleting knowledge bases. Knowledge bases can be attached to agent blocks for context-aware responses.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_table', - agentId: 'table', - description: - 'Manage user-defined tables for structured data storage. Supports creating tables with typed schemas, inserting/updating/deleting rows, querying with filters and sorting, and batch operations.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_job', - agentId: 'scheduled_task', - description: - 'Manage scheduled tasks. Supports creating, listing, updating, pausing, resuming, and deleting scheduled tasks that run prompts against Sim on a schedule or at a specific time.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_agent', - agentId: 'agent', - description: - 'Manage custom tools, MCP server connections, and skills for agent blocks. Supports creating, editing, deleting, and listing custom JavaScript tools, external MCP server connections, and workspace skills. Can also research external MCP tools and add deployed workflows as MCP tools.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: false }, - }, - { - name: 'sim_info', - agentId: 'info', - description: - "Inspect a workflow's blocks, connections, outputs, variables, and metadata. Use for questions about the Sim platform itself — how blocks work, what integrations are available, platform concepts, etc. Provide workflowId when you want results scoped to a specific workflow.", - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - workflowId: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'sim_research', - agentId: 'research', - description: - 'Research external APIs and documentation. Use when you need to understand third-party services, external APIs, authentication flows, or data formats OUTSIDE of Sim. For questions about Sim itself, use sim_info instead.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true, openWorldHint: true }, - }, - { - name: 'sim_superagent', - agentId: 'superagent', - description: - 'Execute direct actions NOW: send an email, post to Slack, make an API call, etc. Use when the user wants to DO something immediately rather than build a workflow for it.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { destructiveHint: true, openWorldHint: true }, - }, - { - name: 'sim_platform', - agentId: 'tour', - description: - 'Get help with Sim platform navigation, keyboard shortcuts, and UI actions. Use when the user asks "how do I..." about the Sim editor, wants keyboard shortcuts, or needs to know what actions are available in the UI.', - inputSchema: { - type: 'object', - properties: { - request: { type: 'string' }, - context: { type: 'object' }, - }, - required: ['request'], - }, - annotations: { readOnlyHint: true }, - }, -] From e87adf0e9fa36a0b05a19739c2f0ab7ad0ccf630 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Jun 2026 11:23:49 -0700 Subject: [PATCH 3/5] improvement(mship): clean up deprecated fields from contracts --- .../message-content/message-content.tsx | 18 +- .../home/hooks/stream/handle-tool-event.ts | 4 +- .../home/hooks/stream/stream-helpers.ts | 163 ++----------- .../app/workspace/[workspaceId]/home/types.ts | 79 ------- .../lib/copilot/chat/effective-transcript.ts | 16 +- .../generated/mothership-stream-v1-schema.ts | 15 -- .../copilot/generated/mothership-stream-v1.ts | 5 - .../lib/copilot/generated/tool-catalog-v1.ts | 29 --- apps/sim/lib/copilot/request/handlers/tool.ts | 28 +-- .../sim/lib/copilot/request/handlers/types.ts | 11 +- apps/sim/lib/copilot/tools/tool-display.ts | 223 ++++++++++++++++++ 11 files changed, 265 insertions(+), 326 deletions(-) create mode 100644 apps/sim/lib/copilot/tools/tool-display.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 05cc1544389..d8a99402e9d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -1,14 +1,14 @@ 'use client' import { memo, useMemo } from 'react' -import { stripVersionSuffix } from '@sim/utils/string' import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' +import { getToolDisplayTitle, humanizeToolName } from '@/lib/copilot/tools/tool-display' import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context' import type { ContentBlock, OptionItem, ToolCallData } from '../../types' -import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' +import { SUBAGENT_LABELS } from '../../types' import type { AgentGroupItem } from './components' import { AgentGroup, @@ -81,16 +81,9 @@ function isHiddenToolCall(toolName: string | undefined): boolean { return isToolHiddenInUi(toolName) } -function formatToolName(name: string): string { - return stripVersionSuffix(name) - .split('_') - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' ') -} - function resolveAgentLabel(key: string): string { if (key === 'mothership') return 'Sim' - return SUBAGENT_LABELS[key] ?? formatToolName(key) + return SUBAGENT_LABELS[key] ?? humanizeToolName(key) } function isToolDone(status: ToolCallData['status']): boolean { @@ -137,10 +130,7 @@ function getOverrideDisplayTitle(tc: NonNullable): str function toToolData(tc: NonNullable): ToolCallData { const overrideDisplayTitle = getOverrideDisplayTitle(tc) const displayTitle = - overrideDisplayTitle || - tc.displayTitle || - TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title || - formatToolName(tc.name) + overrideDisplayTitle || tc.displayTitle || getToolDisplayTitle(tc.name, tc.params) return { id: tc.id, diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts index 20e36e0b145..fed454b260c 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts @@ -214,10 +214,8 @@ export function handleToolEvent( } const ui = getToolUI(payload.ui) if (ui?.hidden) return - let displayTitle = ui?.title const args = payload.arguments as Record | undefined - - displayTitle = resolveToolDisplayTitle(name, args) ?? displayTitle + const displayTitle = resolveToolDisplayTitle(name, args) if (name === 'edit_content') { const parentToolCallId = deps.latestPreviewTargetToolCallIdRef.current diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index db8307be8c2..e67d9d9d491 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -9,7 +9,6 @@ import { DeployChat, DeployMcp, FunctionExecute, - GetPageContents, Glob, Grep, ManageCredential, @@ -37,6 +36,7 @@ import { WorkspaceFileOperation, } from '@/lib/copilot/generated/tool-catalog-v1' import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types' +import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display' import type { ContentBlock, MothershipResource } from '@/app/workspace/[workspaceId]/home/types' import { ToolCallStatus } from '@/app/workspace/[workspaceId]/home/types' import { getWorkflowById } from '@/hooks/queries/utils/workflow-cache' @@ -66,7 +66,6 @@ export type StreamPayload = Record export type StreamToolUI = { hidden?: boolean - title?: string clientExecutable?: boolean } @@ -85,17 +84,10 @@ export function getToolUI(ui?: MothershipStreamV1ToolUI): StreamToolUI | undefin if (!ui) { return undefined } - - const title = - typeof ui.title === 'string' - ? ui.title - : typeof ui.phaseLabel === 'string' - ? ui.phaseLabel - : undefined - + // The stream carries only behavioral flags now; display (title/icon) is + // derived client-side from the tool name via getToolDisplayTitle/getToolIcon. return { ...(typeof ui.hidden === 'boolean' ? { hidden: ui.hidden } : {}), - ...(title ? { title } : {}), ...(typeof ui.clientExecutable === 'boolean' ? { clientExecutable: ui.clientExecutable } : {}), } } @@ -196,11 +188,6 @@ function stringParam(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined } -function stringArrayParam(value: unknown): string[] { - if (!Array.isArray(value)) return [] - return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) -} - function resolveWorkflowNameForDisplay(workflowId: unknown): string | undefined { const id = stringParam(workflowId) if (!id) return undefined @@ -254,138 +241,18 @@ function functionExecuteTitle(title: string | undefined): string { return title ?? 'Running code' } -export function resolveToolDisplayTitle( - name: string, - args?: Record -): string | undefined { - if (!args) return undefined - - if (name === FunctionExecute.id) { - return functionExecuteTitle(stringParam(args.title)) - } - - if (name === WorkspaceFile.id) { - const target = asPayloadRecord(args.target) - return resolveWorkspaceFileDisplayTitle(args.operation, args.title, target?.fileName) - } - - if (name === SearchOnline.id) { - const toolTitle = stringParam(args.toolTitle) - return toolTitle ? `Searching online for ${toolTitle}` : 'Searching online' - } - - if (name === Grep.id) { - const toolTitle = stringParam(args.toolTitle) - return toolTitle ? `Searching for ${toolTitle}` : 'Searching' - } - - if (name === Glob.id) { - const toolTitle = stringParam(args.toolTitle) - return toolTitle ? `Finding ${toolTitle}` : 'Finding files' - } - - if (name === ScrapePage.id) { - const url = stringParam(args.url) - return url ? `Scraping ${url}` : 'Scraping page' - } - - if (name === CrawlWebsite.id) { - const url = stringParam(args.url) - return url ? `Crawling ${url}` : 'Crawling website' - } - - if (name === GetPageContents.id) { - const urls = stringArrayParam(args.urls) - if (urls.length === 1) return `Getting ${urls[0]}` - if (urls.length > 1) return `Getting ${urls.length} pages` - return 'Getting page contents' - } - - if (name === ManageCustomTool.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageCustomToolOperation.add]: 'Creating custom tool', - [ManageCustomToolOperation.edit]: 'Updating custom tool', - [ManageCustomToolOperation.delete]: 'Deleting custom tool', - [ManageCustomToolOperation.list]: 'Listing custom tools', - }, - 'Custom tool action' - ) - } - - if (name === ManageMcpTool.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageMcpToolOperation.add]: 'Creating MCP server', - [ManageMcpToolOperation.edit]: 'Updating MCP server', - [ManageMcpToolOperation.delete]: 'Deleting MCP server', - [ManageMcpToolOperation.list]: 'Listing MCP servers', - }, - 'MCP server action' - ) - } - - if (name === ManageSkill.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageSkillOperation.add]: 'Creating skill', - [ManageSkillOperation.edit]: 'Updating skill', - [ManageSkillOperation.delete]: 'Deleting skill', - [ManageSkillOperation.list]: 'Listing skills', - }, - 'Skill action' - ) - } - - if (name === ManageScheduledTask.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageScheduledTaskOperation.create]: 'Creating scheduled task', - [ManageScheduledTaskOperation.get]: 'Getting scheduled task', - [ManageScheduledTaskOperation.update]: 'Updating scheduled task', - [ManageScheduledTaskOperation.delete]: 'Deleting scheduled task', - [ManageScheduledTaskOperation.list]: 'Listing scheduled tasks', - }, - 'Scheduled task action' - ) - } - - if (name === ManageCredential.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageCredentialOperation.rename]: 'Renaming credential', - [ManageCredentialOperation.delete]: 'Deleting credential', - }, - 'Credential action' - ) - } - - if (name === ManageFolder.id) { - return resolveOperationDisplayTitle( - args.operation, - { - [ManageFolderOperation.create]: 'Creating folder', - [ManageFolderOperation.rename]: 'Renaming folder', - [ManageFolderOperation.move]: 'Moving folder', - [ManageFolderOperation.delete]: 'Deleting folder', - }, - 'Folder action' - ) - } - +export function resolveToolDisplayTitle(name: string, args?: Record): string { + // Cases that enrich the title with live workspace/block names from the client + // stores. Everything else is resolved by the shared name+args resolver, which + // is the single source of truth for tool-call titles. if (name === RunWorkflow.id) { - const workflowName = resolveWorkflowNameForDisplay(args.workflowId) + const workflowName = resolveWorkflowNameForDisplay(args?.workflowId) return workflowName ? `Running ${workflowName}` : 'Running workflow' } if (name === RunFromBlock.id) { - const workflowName = resolveWorkflowNameForDisplay(args.workflowId) - const blockName = resolveBlockNameForDisplay(args.startBlockId) + const workflowName = resolveWorkflowNameForDisplay(args?.workflowId) + const blockName = resolveBlockNameForDisplay(args?.startBlockId) if (workflowName && blockName) return `Running ${workflowName} from ${blockName}` if (workflowName) return `Running ${workflowName}` if (blockName) return `Running from ${blockName}` @@ -393,8 +260,8 @@ export function resolveToolDisplayTitle( } if (name === RunWorkflowUntilBlock.id) { - const workflowName = resolveWorkflowNameForDisplay(args.workflowId) - const blockName = resolveBlockNameForDisplay(args.stopAfterBlockId) + const workflowName = resolveWorkflowNameForDisplay(args?.workflowId) + const blockName = resolveBlockNameForDisplay(args?.stopAfterBlockId) if (workflowName && blockName) return `Running ${workflowName} until ${blockName}` if (workflowName) return `Running ${workflowName}` if (blockName) return `Running until ${blockName}` @@ -403,11 +270,11 @@ export function resolveToolDisplayTitle( if (name === QueryLogs.id) { const workflowName = - resolveWorkflowNameForDisplay(args.workflowId) ?? stringParam(args.workflowName) - return workflowName ? `Querying logs for ${workflowName}` : undefined + resolveWorkflowNameForDisplay(args?.workflowId) ?? stringParam(args?.workflowName) + if (workflowName) return `Querying logs for ${workflowName}` } - return undefined + return getToolDisplayTitle(name, args) } function decodeStreamingString(value: string): string { diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 531a5a18639..20bc2513dab 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -1,36 +1,3 @@ -import { - Agent, - Auth, - CreateWorkflow, - Deploy, - EditWorkflow, - Ffmpeg, - FunctionExecute, - GenerateAudio, - GenerateImage, - GenerateVideo, - GetPageContents, - Glob, - Grep, - Knowledge, - KnowledgeBase, - ManageMcpTool, - ManageSkill, - Media, - OpenResource, - Read as ReadTool, - Research, - ScheduledTask, - ScrapePage, - SearchLibraryDocs, - SearchOnline, - Superagent, - Table, - UserMemory, - UserTable, - Workflow, - WorkspaceFile, -} from '@/lib/copilot/generated/tool-catalog-v1' import type { ChatContext } from '@/stores/panel' const EDIT_CONTENT_TOOL_ID = 'edit_content' @@ -199,49 +166,3 @@ export const SUBAGENT_LABELS: Record = { file: 'File Agent', media: 'Media Agent', } as const - -interface ToolTitleMetadata { - title: string -} - -/** - * Fallback titles for tool calls when the stream did not provide one. - */ -export const TOOL_UI_METADATA: Record = { - [Glob.id]: { title: 'Finding files' }, - [Grep.id]: { title: 'Searching' }, - [ReadTool.id]: { title: 'Reading file' }, - [SearchOnline.id]: { title: 'Searching online' }, - [ScrapePage.id]: { title: 'Scraping page' }, - [GetPageContents.id]: { title: 'Getting page contents' }, - [SearchLibraryDocs.id]: { title: 'Searching library docs' }, - [ManageMcpTool.id]: { title: 'MCP server action' }, - [ManageSkill.id]: { title: 'Skill action' }, - [UserMemory.id]: { title: 'Accessing memory' }, - [FunctionExecute.id]: { title: 'Running code' }, - [Superagent.id]: { title: 'Executing action' }, - [UserTable.id]: { title: 'Managing table' }, - [WorkspaceFile.id]: { title: 'Editing file' }, - [EDIT_CONTENT_TOOL_ID]: { title: 'Applying file content' }, - [CreateWorkflow.id]: { title: 'Creating workflow' }, - [EditWorkflow.id]: { title: 'Editing workflow' }, - [Workflow.id]: { title: 'Workflow Agent' }, - [RUN_SUBAGENT_ID]: { title: 'Run Agent' }, - [Deploy.id]: { title: 'Deploy Agent' }, - [Auth.id]: { title: 'Auth Agent' }, - [Knowledge.id]: { title: 'Knowledge Agent' }, - [KnowledgeBase.id]: { title: 'Managing knowledge base' }, - [Table.id]: { title: 'Table Agent' }, - [ScheduledTask.id]: { title: 'Scheduled Task Agent' }, - job: { title: 'Job Agent' }, - [Agent.id]: { title: 'Tools Agent' }, - custom_tool: { title: 'Creating tool' }, - [Research.id]: { title: 'Research Agent' }, - [OpenResource.id]: { title: 'Opening resource' }, - [Media.id]: { title: 'Media Agent' }, - [GenerateImage.id]: { title: 'Generating image' }, - [GenerateVideo.id]: { title: 'Generating video' }, - [GenerateAudio.id]: { title: 'Generating audio' }, - [Ffmpeg.id]: { title: 'Processing media' }, - context_compaction: { title: 'Compacted context' }, -} diff --git a/apps/sim/lib/copilot/chat/effective-transcript.ts b/apps/sim/lib/copilot/chat/effective-transcript.ts index cd5f432b142..30651c78403 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.ts @@ -14,6 +14,7 @@ import { } from '@/lib/copilot/generated/mothership-stream-v1' import type { FilePreviewSession } from '@/lib/copilot/request/session/file-preview-session-contract' import type { StreamBatchEvent } from '@/lib/copilot/request/session/types' +import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display' interface StreamSnapshotLike { events: StreamBatchEvent[] @@ -60,15 +61,6 @@ function buildInlineErrorTag(payload: MothershipStreamV1ErrorPayload): string { })}` } -function resolveToolDisplayTitle(ui: unknown): string | undefined { - if (!isRecordLike(ui)) return undefined - return typeof ui.title === 'string' - ? ui.title - : typeof ui.phaseLabel === 'string' - ? ui.phaseLabel - : undefined -} - function appendTextBlock( blocks: RawPersistedBlock[], content: string, @@ -279,7 +271,6 @@ function buildLiveAssistantMessage(params: { case MothershipStreamV1EventType.tool: { const payload = parsed.payload const toolCallId = payload.toolCallId - const displayTitle = resolveToolDisplayTitle('ui' in payload ? payload.ui : undefined) if ('previewPhase' in payload) { continue @@ -314,7 +305,10 @@ function buildLiveAssistantMessage(params: { calledBy: scopedSubagent, ...(parentForBlock ? { parentToolCallId: parentForBlock } : {}), ...spanIdentity, - displayTitle, + displayTitle: getToolDisplayTitle( + payload.toolName, + isRecordLike(payload.arguments) ? payload.arguments : undefined + ), params: isRecordLike(payload.arguments) ? payload.arguments : undefined, state: typeof payload.status === 'string' ? payload.status : 'executing', }) diff --git a/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts b/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts index 88e8344e560..c2121813327 100644 --- a/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts +++ b/apps/sim/lib/copilot/generated/mothership-stream-v1-schema.ts @@ -1157,9 +1157,6 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = { enum: ['call'], type: 'string', }, - requiresConfirmation: { - type: 'boolean', - }, status: { $ref: '#/$defs/MothershipStreamV1ToolStatus', }, @@ -1304,21 +1301,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = { hidden: { type: 'boolean', }, - icon: { - type: 'string', - }, internal: { type: 'boolean', }, - phaseLabel: { - type: 'string', - }, - requiresConfirmation: { - type: 'boolean', - }, - title: { - type: 'string', - }, }, type: 'object', }, diff --git a/apps/sim/lib/copilot/generated/mothership-stream-v1.ts b/apps/sim/lib/copilot/generated/mothership-stream-v1.ts index d7194f13757..1b2d88ac36c 100644 --- a/apps/sim/lib/copilot/generated/mothership-stream-v1.ts +++ b/apps/sim/lib/copilot/generated/mothership-stream-v1.ts @@ -148,7 +148,6 @@ export interface MothershipStreamV1ToolCallDescriptor { mode: MothershipStreamV1ToolMode partial?: boolean phase: 'call' - requiresConfirmation?: boolean status?: MothershipStreamV1ToolStatus toolCallId: string toolName: string @@ -160,11 +159,7 @@ export interface MothershipStreamV1AdditionalPropertiesMap { export interface MothershipStreamV1ToolUI { clientExecutable?: boolean hidden?: boolean - icon?: string internal?: boolean - phaseLabel?: string - requiresConfirmation?: boolean - title?: string } export interface MothershipStreamV1ToolArgsDeltaEventEnvelope { payload: MothershipStreamV1ToolArgsDeltaPayload diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index ac293c6ad1d..e3c264aded4 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -202,7 +202,6 @@ export interface ToolCatalogEntry { | 'workspace_file' parameters: unknown requiredPermission?: 'admin' | 'read' | 'write' - requiresConfirmation?: boolean resultSchema?: unknown route: 'client' | 'go' | 'sim' | 'subagent' subagentId?: @@ -444,7 +443,6 @@ export const CreateWorkspaceMcpServer: ToolCatalogEntry = { }, required: ['name'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -492,7 +490,6 @@ export const DeleteFileFolder: ToolCatalogEntry = { }, required: ['paths'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -512,7 +509,6 @@ export const DeleteWorkflow: ToolCatalogEntry = { }, required: ['workflowIds'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -528,7 +524,6 @@ export const DeleteWorkspaceMcpServer: ToolCatalogEntry = { }, required: ['serverId'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -629,7 +624,6 @@ export const DeployApi: ToolCatalogEntry = { 'examples', ], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -775,7 +769,6 @@ export const DeployChat: ToolCatalogEntry = { 'examples', ], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -868,7 +861,6 @@ export const DeployMcp: ToolCatalogEntry = { }, required: ['deploymentType', 'deploymentStatus'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -1422,7 +1414,6 @@ export const GenerateApiKey: ToolCatalogEntry = { }, required: ['name'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -2303,7 +2294,6 @@ export const KnowledgeBase: ToolCatalogEntry = { }, required: ['success', 'message'], }, - requiresConfirmation: true, } export const ListFileFolders: ToolCatalogEntry = { @@ -2386,7 +2376,6 @@ export const LoadDeployment: ToolCatalogEntry = { }, required: ['version'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -2432,7 +2421,6 @@ export const ManageCredential: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -2502,7 +2490,6 @@ export const ManageCustomTool: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -2547,7 +2534,6 @@ export const ManageFolder: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -2599,7 +2585,6 @@ export const ManageMcpTool: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -2712,7 +2697,6 @@ export const ManageSkill: ToolCatalogEntry = { }, required: ['operation'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -2866,7 +2850,6 @@ export const OauthRequestAccess: ToolCatalogEntry = { }, required: ['providerName'], }, - requiresConfirmation: true, } export const OpenResource: ToolCatalogEntry = { @@ -2924,7 +2907,6 @@ export const PromoteToLive: ToolCatalogEntry = { }, required: ['version'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -3130,7 +3112,6 @@ export const Redeploy: ToolCatalogEntry = { 'examples', ], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -3255,7 +3236,6 @@ export const RestoreResource: ToolCatalogEntry = { }, required: ['type', 'id'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -3311,7 +3291,6 @@ export const RunBlock: ToolCatalogEntry = { required: ['blockId'], }, clientExecutable: true, - requiresConfirmation: true, } export const RunFromBlock: ToolCatalogEntry = { @@ -3346,7 +3325,6 @@ export const RunFromBlock: ToolCatalogEntry = { required: ['startBlockId'], }, clientExecutable: true, - requiresConfirmation: true, } export const RunWorkflow: ToolCatalogEntry = { @@ -3390,7 +3368,6 @@ export const RunWorkflow: ToolCatalogEntry = { }, }, clientExecutable: true, - requiresConfirmation: true, } export const RunWorkflowUntilBlock: ToolCatalogEntry = { @@ -3439,7 +3416,6 @@ export const RunWorkflowUntilBlock: ToolCatalogEntry = { required: ['stopAfterBlockId'], }, clientExecutable: true, - requiresConfirmation: true, } export const ScheduledTask: ToolCatalogEntry = { @@ -3635,7 +3611,6 @@ export const SetEnvironmentVariables: ToolCatalogEntry = { }, required: ['variables'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -3675,7 +3650,6 @@ export const SetGlobalWorkflowVariables: ToolCatalogEntry = { }, required: ['operations'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -3741,7 +3715,6 @@ export const UpdateDeploymentVersion: ToolCatalogEntry = { }, required: ['version'], }, - requiresConfirmation: true, requiredPermission: 'write', } @@ -3779,7 +3752,6 @@ export const UpdateWorkspaceMcpServer: ToolCatalogEntry = { }, required: ['serverId'], }, - requiresConfirmation: true, requiredPermission: 'admin', } @@ -4157,7 +4129,6 @@ export const UserTable: ToolCatalogEntry = { }, required: ['success', 'message'], }, - requiresConfirmation: true, } export const Workflow: ToolCatalogEntry = { diff --git a/apps/sim/lib/copilot/request/handlers/tool.ts b/apps/sim/lib/copilot/request/handlers/tool.ts index 0811917fd1f..eb24a2f8795 100644 --- a/apps/sim/lib/copilot/request/handlers/tool.ts +++ b/apps/sim/lib/copilot/request/handlers/tool.ts @@ -30,6 +30,7 @@ import type { } from '@/lib/copilot/request/types' import { getToolEntry, isSimExecuted } from '@/lib/copilot/tool-executor' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' +import { getToolDisplayTitle } from '@/lib/copilot/tools/tool-display' import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools' import type { ToolScope } from './types' import { @@ -50,13 +51,12 @@ import { const logger = createLogger('CopilotToolHandler') -function applyToolDisplay( - toolCall: ToolCallState | undefined, - ui: { title?: string; phaseLabel?: string; hidden?: boolean } -): void { - if (!toolCall) return - const displayTitle = ui.title || ui.phaseLabel - if (displayTitle) toolCall.displayTitle = displayTitle +function applyToolDisplay(toolCall: ToolCallState | undefined): void { + if (!toolCall?.name) return + toolCall.displayTitle = getToolDisplayTitle( + toolCall.name, + toolCall.params as Record | undefined + ) } /** @@ -256,7 +256,7 @@ async function handleCallPhase( if (wasToolResultSeen(toolCallId) || existing?.endTime) { if (existing && !existing.name && toolName) existing.name = toolName if (existing && !existing.params && args) existing.params = args - applyToolDisplay(existing, ui) + applyToolDisplay(existing) return } } else { @@ -266,7 +266,7 @@ async function handleCallPhase( ) { if (!existing.name && toolName) existing.name = toolName if (!existing.params && args) existing.params = args - applyToolDisplay(existing, ui) + applyToolDisplay(existing) return } } @@ -366,7 +366,7 @@ function registerSubagentToolCall( if (toolCall) { if (!toolCall.name && toolName) toolCall.name = toolName if (args && !toolCall.params) toolCall.params = args - applyToolDisplay(toolCall, ui) + applyToolDisplay(toolCall) if (hideFromUi) removeToolCallContentBlock(context, toolCallId) } else { toolCall = { @@ -376,7 +376,7 @@ function registerSubagentToolCall( params: args, startTime: Date.now(), } - applyToolDisplay(toolCall, ui) + applyToolDisplay(toolCall) context.toolCalls.set(toolCallId, toolCall) const parentToolCall = context.toolCalls.get(parentToolCallId) if (!hideFromUi) { @@ -395,7 +395,7 @@ function registerSubagentToolCall( if (existingSubagentToolCall) { if (!existingSubagentToolCall.name && toolName) existingSubagentToolCall.name = toolName if (args && !existingSubagentToolCall.params) existingSubagentToolCall.params = args - applyToolDisplay(existingSubagentToolCall, ui) + applyToolDisplay(existingSubagentToolCall) } else { subagentToolCalls.push(toolCall) } @@ -412,7 +412,7 @@ function registerMainToolCall( const hideFromUi = isToolHiddenInUi(toolName) || ui.hidden === true if (existing) { if (args && !existing.params) existing.params = args - applyToolDisplay(existing, ui) + applyToolDisplay(existing) if (hideFromUi) { removeToolCallContentBlock(context, toolCallId) return @@ -431,7 +431,7 @@ function registerMainToolCall( params: args, startTime: Date.now(), } - applyToolDisplay(created, ui) + applyToolDisplay(created) context.toolCalls.set(toolCallId, created) if (!hideFromUi) { addContentBlock(context, { type: 'tool_call', toolCall: created }) diff --git a/apps/sim/lib/copilot/request/handlers/types.ts b/apps/sim/lib/copilot/request/handlers/types.ts index 9f41a996adf..4d2a650694b 100644 --- a/apps/sim/lib/copilot/request/handlers/types.ts +++ b/apps/sim/lib/copilot/request/handlers/types.ts @@ -141,28 +141,23 @@ export function abortPendingToolIfStreamDead( } /** - * Extract the `ui` object from a typed tool_call payload. The Go backend enriches - * tool_call events with `ui: { requiresConfirmation, clientExecutable, ... }`. + * Extract the behavioral `ui` flags from a typed tool_call payload. The Go + * backend enriches tool_call events with `ui: { clientExecutable, internal, + * hidden }`; presentation (title/icon) is derived client-side from the tool name. */ export function getToolCallUI(data: MothershipStreamV1ToolCallDescriptor): { - requiresConfirmation: boolean clientExecutable: boolean simExecutable: boolean internal: boolean hidden: boolean - title?: string - phaseLabel?: string } { const raw = asRecord(data.ui) return { - requiresConfirmation: raw.requiresConfirmation === true || data.requiresConfirmation === true, clientExecutable: raw.clientExecutable === true || data.executor === MothershipStreamV1ToolExecutor.client, simExecutable: data.executor === MothershipStreamV1ToolExecutor.sim, internal: raw.internal === true, hidden: raw.hidden === true, - title: typeof raw.title === 'string' ? raw.title : undefined, - phaseLabel: typeof raw.phaseLabel === 'string' ? raw.phaseLabel : undefined, } } diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts new file mode 100644 index 00000000000..5ee25a69959 --- /dev/null +++ b/apps/sim/lib/copilot/tools/tool-display.ts @@ -0,0 +1,223 @@ +import { stripVersionSuffix } from '@sim/utils/string' + +/** + * Single source of truth for copilot tool-call display titles. + * + * The mothership (Go) no longer emits any presentation metadata on the stream — + * tool-call titles are derived entirely here, keyed by tool name (plus arguments + * for the dynamic cases). The live client render layer (see + * `home/hooks/stream/stream-helpers.ts`) wraps this with workspace/block-name + * enrichment for the run_* tools; every other surface (server persistence, + * transcript replay, fallback rendering) calls `getToolDisplayTitle` directly. + * + * Icons are likewise client-owned — see `getToolIcon` in the message-content + * utils. Nothing about tool presentation lives on the Go side anymore. + */ + +type ToolArgs = Record | undefined + +function stringArg(args: ToolArgs, key: string): string { + const value = args?.[key] + return typeof value === 'string' ? value.trim() : '' +} + +function firstStringArg(args: ToolArgs, ...keys: string[]): string { + for (const key of keys) { + const value = stringArg(args, key) + if (value) return value + } + return '' +} + +function stringArrayArg(args: ToolArgs, key: string): string[] { + const value = args?.[key] + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) +} + +function nestedStringArg(args: ToolArgs, parentKey: string, ...keys: string[]): string { + const parent = args?.[parentKey] + if (!parent || typeof parent !== 'object') return '' + return firstStringArg(parent as Record, ...keys) +} + +function operationTitle( + args: ToolArgs, + placeholder: string, + labels: Record +): string { + const operation = stringArg(args, 'operation') + return labels[operation] ?? placeholder +} + +function isWorkflowArtifactPath(path: string, filename: string): boolean { + const trimmed = path.trim() + return trimmed.startsWith('workflows/') && trimmed.endsWith(`/${filename}`) +} + +function workspaceFileTitle(args: ToolArgs): string { + const title = stringArg(args, 'title') + if (!title) return '' + const verbByOperation: Record = { + create: 'Creating', + append: 'Adding', + patch: 'Editing', + update: 'Writing', + rename: 'Renaming', + delete: 'Deleting', + } + const verb = verbByOperation[stringArg(args, 'operation')] ?? 'Writing' + return `${verb} ${title}` +} + +/** Static fallback titles for tools without an argument-aware title. */ +const TOOL_TITLES: Record = { + read: 'Reading file', + search_library_docs: 'Searching library docs', + user_memory: 'Accessing memory', + user_table: 'Managing table', + workspace_file: 'Editing file', + edit_content: 'Applying file content', + create_workflow: 'Creating workflow', + edit_workflow: 'Editing workflow', + knowledge_base: 'Managing knowledge base', + open_resource: 'Opening resource', + generate_image: 'Generating image', + generate_video: 'Generating video', + generate_audio: 'Generating audio', + ffmpeg: 'Processing media', + manage_folder: 'Folder action', + // Subagent trigger tools, when surfaced as a tool call. + workflow: 'Workflow Agent', + run: 'Run Agent', + deploy: 'Deploy Agent', + auth: 'Auth Agent', + knowledge: 'Knowledge Agent', + table: 'Table Agent', + scheduled_task: 'Scheduled Task Agent', + agent: 'Tools Agent', + research: 'Research Agent', + media: 'Media Agent', + superagent: 'Executing action', +} + +/** + * Final fallback: humanize a raw tool name (e.g. `manage_folder` -> "Manage + * Folder"), matching the legacy client humanizer so labels never render blank. + */ +export function humanizeToolName(name: string): string { + const words = stripVersionSuffix(name).split('_').filter(Boolean) + if (words.length === 0) return name + return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') +} + +/** + * Resolve a tool-call display title from its name and arguments. Argument-aware + * cases come first, then the static map, then a humanized fallback. This never + * returns an empty string. + */ +export function getToolDisplayTitle(name: string, args?: Record): string { + switch (name) { + case 'search_online': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Searching online for ${target}` : 'Searching online' + } + case 'grep': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Searching for ${target}` : 'Searching' + } + case 'glob': { + const target = firstStringArg(args, 'toolTitle', 'title') + return target ? `Finding ${target}` : 'Finding files' + } + case 'enrichment_run': { + const subject = nestedStringArg( + args, + 'inputs', + 'fullName', + 'companyName', + 'domain', + 'email', + 'companyDomain' + ) + return subject ? `Searching for ${subject}` : 'Searching' + } + case 'scrape_page': { + const url = stringArg(args, 'url') + return url ? `Scraping ${url}` : 'Scraping page' + } + case 'crawl_website': { + const url = stringArg(args, 'url') + return url ? `Crawling ${url}` : 'Crawling website' + } + case 'get_page_contents': { + const urls = stringArrayArg(args, 'urls') + if (urls.length === 1) return `Getting ${urls[0]}` + if (urls.length > 1) return `Getting ${urls.length} pages` + return 'Getting page contents' + } + case 'manage_custom_tool': + return operationTitle(args, 'Custom tool action', { + add: 'Creating custom tool', + edit: 'Updating custom tool', + delete: 'Deleting custom tool', + list: 'Listing custom tools', + }) + case 'manage_mcp_tool': + return operationTitle(args, 'MCP server action', { + add: 'Creating MCP server', + edit: 'Updating MCP server', + delete: 'Deleting MCP server', + list: 'Listing MCP servers', + }) + case 'manage_skill': + return operationTitle(args, 'Skill action', { + add: 'Creating skill', + edit: 'Updating skill', + delete: 'Deleting skill', + list: 'Listing skills', + }) + case 'manage_scheduled_task': + return operationTitle(args, 'Scheduled task action', { + create: 'Creating scheduled task', + get: 'Getting scheduled task', + update: 'Updating scheduled task', + delete: 'Deleting scheduled task', + list: 'Listing scheduled tasks', + }) + case 'manage_credential': + return operationTitle(args, 'Credential action', { + rename: 'Renaming credential', + delete: 'Deleting credential', + }) + case 'manage_folder': + return operationTitle(args, 'Folder action', { + create: 'Creating folder', + rename: 'Renaming folder', + move: 'Moving folder', + delete: 'Deleting folder', + }) + case 'run_workflow': + case 'run_from_block': + case 'run_workflow_until_block': + return 'Running workflow' + case 'query_logs': { + const workflowName = stringArg(args, 'workflowName') + return workflowName ? `Querying logs for ${workflowName}` : 'Querying logs' + } + case 'read': { + if (isWorkflowArtifactPath(stringArg(args, 'path'), 'lint.json')) { + return 'Validating workflow state' + } + break + } + case 'workspace_file': + case 'function_execute': { + const title = name === 'workspace_file' ? workspaceFileTitle(args) : stringArg(args, 'title') + if (title) return title + break + } + } + + return TOOL_TITLES[name] ?? humanizeToolName(name) +} From 48f6a8ac69e00afaf821f5aa756260b86af76ee0 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Jun 2026 11:40:43 -0700 Subject: [PATCH 4/5] test(mship): update tool-call display title tests for client-derived titles Display titles now come from the Sim-side name resolver, not the stream's ui.title/phaseLabel. Update the read lifecycle test to expect the name-derived title and drop the obsolete phaseLabel-fallback test. --- .../copilot/request/handlers/handlers.test.ts | 50 ++----------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/apps/sim/lib/copilot/request/handlers/handlers.test.ts b/apps/sim/lib/copilot/request/handlers/handlers.test.ts index d55fe63bbaa..d50e28f3437 100644 --- a/apps/sim/lib/copilot/request/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/request/handlers/handlers.test.ts @@ -169,10 +169,7 @@ describe('sse-handlers tool lifecycle', () => { executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.call, - ui: { - title: 'Reading foo.txt', - phaseLabel: 'Workspace', - }, + ui: {}, }, } satisfies StreamEvent, context, @@ -198,53 +195,16 @@ describe('sse-handlers tool lifecycle', () => { const updated = context.toolCalls.get('tool-1') expect(updated?.status).toBe(MothershipStreamV1ToolOutcome.success) - expect(updated?.displayTitle).toBe('Reading foo.txt') + // Display titles are derived client-side from the tool name (+args), not the + // stream; read with no path resolves to the static "Reading file". + expect(updated?.displayTitle).toBe('Reading file') expect(updated?.result?.output).toEqual({ ok: true }) expect(context.contentBlocks.at(0)).toEqual( expect.objectContaining({ type: 'tool_call', toolCall: expect.objectContaining({ id: 'tool-1', - displayTitle: 'Reading foo.txt', - }), - }) - ) - }) - - it('uses phaseLabel as a display title fallback when no title is provided', async () => { - executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } }) - const onEvent = vi.fn() - - await sseHandlers.tool( - { - type: MothershipStreamV1EventType.tool, - payload: { - toolCallId: 'tool-phase-label', - toolName: ReadTool.id, - arguments: { workflowId: 'workflow-1' }, - executor: MothershipStreamV1ToolExecutor.sim, - mode: MothershipStreamV1ToolMode.async, - phase: MothershipStreamV1ToolPhase.call, - ui: { - phaseLabel: 'Workspace', - }, - }, - } satisfies StreamEvent, - context, - execContext, - { onEvent, interactive: false, timeout: 1000 } - ) - - await sleep(0) - - const updated = context.toolCalls.get('tool-phase-label') - expect(updated?.displayTitle).toBe('Workspace') - expect(context.contentBlocks.at(0)).toEqual( - expect.objectContaining({ - type: 'tool_call', - toolCall: expect.objectContaining({ - id: 'tool-phase-label', - displayTitle: 'Workspace', + displayTitle: 'Reading file', }), }) ) From 15b657f9f5c0c4114bb03d6ecf4b91435d83d18a Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Jun 2026 11:51:23 -0700 Subject: [PATCH 5/5] fix(contracts): lint --- .../lib/copilot/generated/tool-schemas-v1.ts | 190 +++++++++--------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index caf4be1dd65..dcaea0db6ea 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_scheduled_task']: { + complete_scheduled_task: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -96,7 +96,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -162,7 +162,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_file_folder']: { + create_file_folder: { parameters: { type: 'object', properties: { @@ -180,7 +180,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -205,7 +205,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -238,7 +238,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -268,7 +268,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_file_folder']: { + delete_file_folder: { parameters: { type: 'object', properties: { @@ -284,7 +284,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -300,7 +300,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -313,7 +313,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -327,7 +327,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -411,7 +411,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -570,7 +570,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -686,7 +686,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['diff_workflows']: { + diff_workflows: { parameters: { type: 'object', properties: { @@ -710,7 +710,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -759,7 +759,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -791,7 +791,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -830,7 +830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['enrichment_run']: { + enrichment_run: { parameters: { type: 'object', properties: { @@ -874,7 +874,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['matched', 'result'], }, }, - ['ffmpeg']: { + ffmpeg: { parameters: { type: 'object', properties: { @@ -1055,7 +1055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { properties: { prompt: { @@ -1068,7 +1068,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -1206,7 +1206,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -1224,7 +1224,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_audio']: { + generate_audio: { parameters: { type: 'object', properties: { @@ -1376,7 +1376,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -1504,7 +1504,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_video']: { + generate_video: { parameters: { type: 'object', properties: { @@ -1671,7 +1671,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -1692,7 +1692,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1714,7 +1714,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1727,7 +1727,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_log']: { + get_deployment_log: { parameters: { type: 'object', properties: { @@ -1740,7 +1740,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1768,14 +1768,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_scheduled_task_logs']: { + get_scheduled_task_logs: { parameters: { type: 'object', properties: { @@ -1800,7 +1800,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1819,7 +1819,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_run_options']: { + get_workflow_run_options: { parameters: { type: 'object', properties: { @@ -1832,7 +1832,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1851,7 +1851,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1912,7 +1912,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -2105,7 +2105,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_file_folders']: { + list_file_folders: { parameters: { type: 'object', properties: { @@ -2117,7 +2117,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_integration_tools']: { + list_integration_tools: { parameters: { properties: { integration: { @@ -2131,14 +2131,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -2151,7 +2151,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['load_deployment']: { + load_deployment: { parameters: { type: 'object', properties: { @@ -2170,7 +2170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['load_integration_tool']: { + load_integration_tool: { parameters: { properties: { tool_ids: { @@ -2187,7 +2187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -2216,7 +2216,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -2296,7 +2296,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_folder']: { + manage_folder: { parameters: { type: 'object', properties: { @@ -2335,7 +2335,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -2387,7 +2387,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_scheduled_task']: { + manage_scheduled_task: { parameters: { type: 'object', properties: { @@ -2462,7 +2462,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -2519,7 +2519,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['media']: { + media: { parameters: { properties: { prompt: { @@ -2532,7 +2532,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_file']: { + move_file: { parameters: { type: 'object', properties: { @@ -2553,7 +2553,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_file_folder']: { + move_file_folder: { parameters: { type: 'object', properties: { @@ -2571,7 +2571,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -2591,7 +2591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -2605,7 +2605,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -2619,7 +2619,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -2653,7 +2653,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['promote_to_live']: { + promote_to_live: { parameters: { type: 'object', properties: { @@ -2672,7 +2672,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['query_logs']: { + query_logs: { parameters: { type: 'object', properties: { @@ -2783,7 +2783,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -2810,7 +2810,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -2889,7 +2889,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2925,7 +2925,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_file_folder']: { + rename_file_folder: { parameters: { type: 'object', properties: { @@ -2942,7 +2942,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -2959,7 +2959,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -2972,7 +2972,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -2995,7 +2995,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -3013,7 +3013,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -3030,7 +3030,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -3062,7 +3062,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -3094,7 +3094,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -3132,7 +3132,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -3175,7 +3175,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scheduled_task']: { + scheduled_task: { parameters: { properties: { request: { @@ -3188,7 +3188,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -3209,7 +3209,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -3226,7 +3226,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -3247,7 +3247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -3287,7 +3287,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -3309,7 +3309,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -3331,7 +3331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -3365,7 +3365,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -3406,7 +3406,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -3420,7 +3420,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -3433,7 +3433,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_deployment_version']: { + update_deployment_version: { parameters: { type: 'object', properties: { @@ -3462,7 +3462,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_scheduled_task_history']: { + update_scheduled_task_history: { parameters: { type: 'object', properties: { @@ -3480,7 +3480,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -3505,7 +3505,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -3554,7 +3554,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -3917,7 +3917,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { properties: { prompt: { @@ -3930,7 +3930,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: {