diff --git a/apps/sim/app/api/mothership/chat/route.ts b/apps/sim/app/api/mothership/chat/route.ts index 8637ba2bcd..d6275e33e7 100644 --- a/apps/sim/app/api/mothership/chat/route.ts +++ b/apps/sim/app/api/mothership/chat/route.ts @@ -189,7 +189,6 @@ export async function POST(req: NextRequest) { logger.error(`[${tracker.requestId}] Failed to process contexts`, e) } } - } if (Array.isArray(resourceAttachments) && resourceAttachments.length > 0) { const results = await Promise.allSettled( resourceAttachments.map(async (r) => { diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx index e4e086108a..c0503e5c4d 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx @@ -254,6 +254,9 @@ function TextEditor({ fetchedContent.endsWith(`\n${streamingContent}`) ? fetchedContent : `${fetchedContent}\n${streamingContent}` + // #region agent log + fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'file-viewer.tsx:TextEditor-merge',message:'streaming merge',data:{streamingMode,fetchedContentLen:fetchedContent?.length,streamingContentLen:streamingContent.length,nextContentLen:nextContent.length,fetchedUndefined:fetchedContent===undefined,usedReplace:streamingMode==='replace'||fetchedContent===undefined,nextPreview:nextContent.slice(0,200)},timestamp:Date.now(),hypothesisId:'H2-H3'})}).catch(()=>{}); + // #endregion setContent(nextContent) contentRef.current = nextContent initializedRef.current = true diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx index 61ca00db3b..b803b64006 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx @@ -106,7 +106,12 @@ export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }: if (!titleMatch?.[1]) return null const opMatch = streamingArgs.match(/"operation"\s*:\s*"(\w+)"/) const op = opMatch?.[1] ?? '' - const verb = op === 'patch' || op === 'update' ? 'Editing' : 'Writing' + const verb = + op === 'patch' || op === 'update' || op === 'rename' + ? 'Editing' + : op === 'delete' + ? 'Deleting' + : 'Writing' const unescaped = titleMatch[1] .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16)) 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 b9d403ef1f..fe34d53fa6 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 @@ -210,7 +210,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { if (block.type === 'tool_call') { if (!block.toolCall) continue const tc = block.toolCall - if (tc.name === ToolSearchToolRegex.id || tc.name === 'set_file_context') continue + if (tc.name === ToolSearchToolRegex.id) continue if (tc.name === ReadTool.id && isToolResultRead(tc.params)) continue const isDispatch = SUBAGENT_KEYS.has(tc.name) && !tc.calledBy diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index ffec433cf9..1d01af58eb 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -65,7 +65,15 @@ interface ResourceContentProps { workspaceId: string resource: MothershipResource previewMode?: PreviewMode - streamingFile?: { fileName: string; fileId?: string; content: string } | null + streamingFile?: { + toolCallId?: string + fileName: string + fileId?: string + targetKind?: 'new_file' | 'file_id' + operation?: string + edit?: Record + content: string + } | null genericResourceData?: GenericResourceData } @@ -87,11 +95,10 @@ export const ResourceContent = memo(function ResourceContent({ const streamOperation = useMemo(() => { if (!streamingFile) return undefined - const m = streamingFile.content.match(/"operation"\s*:\s*"(\w+)"/) - return m?.[1] + return streamingFile.operation }, [streamingFile]) - const isWriteStream = streamOperation === 'write' + const isWriteStream = streamOperation === 'create' || streamOperation === 'append' const isPatchStream = streamOperation === 'patch' const isUpdateStream = streamOperation === 'update' @@ -113,24 +120,36 @@ export const ResourceContent = memo(function ResourceContent({ isSourceMime ) + // #region agent log + if (streamingFile) { + fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:streaming-context',message:'streaming state',data:{resourceId:resource.id,resourceType:resource.type,streamOp:streamOperation,isPatch:isPatchStream,isWrite:isWriteStream,isUpdate:isUpdateStream,hasActiveFileRecord:!!activeFileRecord,hasFetchedContent:!!fetchedFileContent,fetchedContentLen:fetchedFileContent?.length,streamingFileContentLen:streamingFile.content.length,streamingFileName:streamingFile.fileName,streamingFileMode:isWriteStream?'append':'replace'},timestamp:Date.now()})}).catch(()=>{}); + } + // #endregion const streamingExtractedContent = useMemo(() => { if (!streamingFile) return undefined - const raw = streamingFile.content - - // Do not guess. Until the operation key has streamed in, we don't know - // whether the payload should append, replace, or splice into the file. - // Rendering early here can show content at the end of the file and then - // "snap" to the right place once the operation/mode becomes known. if (!streamOperation) return undefined if (isPatchStream) { - if (!fetchedFileContent) return undefined - return extractPatchPreview(raw, fetchedFileContent) + if (!fetchedFileContent) { + // #region agent log + fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:patch-no-fetched',message:'patch but no fetchedFileContent',data:{resourceId:resource.id,activeFileRecordId:activeFileRecord?.id},timestamp:Date.now(),hypothesisId:'H1'})}).catch(()=>{}); + // #endregion + return undefined + } + const patchResult = extractPatchPreview(streamingFile, fetchedFileContent) + // #region agent log + fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:patch-result',message:'extractPatchPreview result',data:{hasPatchResult:!!patchResult,patchResultLen:patchResult?.length,fetchedLen:fetchedFileContent.length,contentPreview:streamingFile.content.slice(0,200),edit:streamingFile.edit},timestamp:Date.now(),hypothesisId:'H4'})}).catch(()=>{}); + // #endregion + return patchResult } - const extracted = extractFileContent(raw) + const extracted = streamingFile.content if (extracted.length === 0) return undefined + // #region agent log + fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:write-update-content',message:'extracted content for write/update',data:{streamOp:streamOperation,extractedLen:extracted.length,extractedPreview:extracted.slice(0,150)},timestamp:Date.now(),hypothesisId:'H2'})}).catch(()=>{}); + // #endregion + if (isUpdateStream) return extracted if (isWriteStream) return extracted @@ -160,6 +179,15 @@ export const ResourceContent = memo(function ResourceContent({ const streamingFileMode: 'append' | 'replace' = isWriteStream ? 'append' : 'replace' + // For existing file resources (not streaming-file), only pass streaming + // content for patch operations where the preview splices new content into + // the displayed file. Update operations re-stream the entire file from + // scratch which causes visual duplication of already-visible content. + const embeddedStreamingContent = + resource.id !== 'streaming-file' && isUpdateStream + ? undefined + : streamingExtractedContent + if (streamingFile && resource.id === 'streaming-file') { return (
@@ -192,7 +220,7 @@ export const ResourceContent = memo(function ResourceContent({ workspaceId={workspaceId} fileId={resource.id} previewMode={previewMode} - streamingContent={streamingExtractedContent} + streamingContent={embeddedStreamingContent} streamingMode={streamingFileMode} /> ) @@ -587,65 +615,6 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) { ) } -function extractFileContent(raw: string): string { - const marker = '"content":' - const idx = raw.indexOf(marker) - if (idx === -1) return '' - const rest = raw.slice(idx + marker.length).trimStart() - if (!rest.startsWith('"')) return rest - - // Walk the JSON string value to find the unescaped closing quote. - // While streaming, the closing quote may not have arrived yet — in that - // case we treat everything received so far as the content (no trim). - let end = -1 - for (let i = 1; i < rest.length; i++) { - if (rest[i] === '\\') { - i++ // skip escaped character - continue - } - if (rest[i] === '"') { - end = i - break - } - } - - const inner = end === -1 ? rest.slice(1) : rest.slice(1, end) - return inner - .replace(/\\n/g, '\n') - .replace(/\\t/g, '\t') - .replace(/\\r/g, '\r') - .replace(/\\"/g, '"') - .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))) - .replace(/\\\\/g, '\\') -} - -function extractJsonString(raw: string, key: string): string | undefined { - const pattern = new RegExp(`"${key}"\\s*:\\s*"`) - const m = pattern.exec(raw) - if (!m) return undefined - const start = m.index + m[0].length - let end = -1 - for (let i = start; i < raw.length; i++) { - if (raw[i] === '\\') { - i++ - continue - } - if (raw[i] === '"') { - end = i - break - } - } - if (end === -1) return undefined - return raw - .slice(start, end) - .replace(/\\n/g, '\n') - .replace(/\\t/g, '\t') - .replace(/\\r/g, '\r') - .replace(/\\"/g, '"') - .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))) - .replace(/\\\\/g, '\\') -} - function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterIndex = -1): number { const trimmed = anchor.trim() let count = 0 @@ -658,24 +627,46 @@ function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterI return -1 } -function extractPatchPreview(raw: string, existingContent: string): string | undefined { - const mode = extractJsonString(raw, 'mode') +function extractPatchPreview( + streamingFile: { + content: string + edit?: Record + }, + existingContent: string +): string | undefined { + const edit = streamingFile.edit ?? {} + const strategy = typeof edit.strategy === 'string' ? edit.strategy : undefined + const lines = existingContent.split('\n') + const occurrence = + typeof edit.occurrence === 'number' && Number.isFinite(edit.occurrence) + ? edit.occurrence + : 1 + + if (strategy === 'search_replace') { + const search = typeof edit.search === 'string' ? edit.search : '' + if (!search) return undefined + const replace = streamingFile.content + if ((edit.replaceAll as boolean | undefined) === true) { + return existingContent.split(search).join(replace) + } + const firstIdx = existingContent.indexOf(search) + if (firstIdx === -1) return undefined + return existingContent.slice(0, firstIdx) + replace + existingContent.slice(firstIdx + search.length) + } + + const mode = typeof edit.mode === 'string' ? edit.mode : undefined if (!mode) return undefined - const lines = existingContent.split('\n') - const occurrenceMatch = raw.match(/"occurrence"\s*:\s*(\d+)/) - const occurrence = occurrenceMatch ? Number.parseInt(occurrenceMatch[1], 10) : 1 - if (mode === 'replace_between') { - const beforeAnchor = extractJsonString(raw, 'before_anchor') - const afterAnchor = extractJsonString(raw, 'after_anchor') + const beforeAnchor = typeof edit.before_anchor === 'string' ? edit.before_anchor : undefined + const afterAnchor = typeof edit.after_anchor === 'string' ? edit.after_anchor : undefined if (!beforeAnchor || !afterAnchor) return undefined const beforeIdx = findAnchorIndex(lines, beforeAnchor, occurrence) const afterIdx = findAnchorIndex(lines, afterAnchor, occurrence, beforeIdx) if (beforeIdx === -1 || afterIdx === -1 || afterIdx <= beforeIdx) return undefined - const newContent = extractFileContent(raw) + const newContent = streamingFile.content const spliced = [ ...lines.slice(0, beforeIdx + 1), ...(newContent.length > 0 ? newContent.split('\n') : []), @@ -685,13 +676,13 @@ function extractPatchPreview(raw: string, existingContent: string): string | und } if (mode === 'insert_after') { - const anchor = extractJsonString(raw, 'anchor') + const anchor = typeof edit.anchor === 'string' ? edit.anchor : undefined if (!anchor) return undefined const anchorIdx = findAnchorIndex(lines, anchor, occurrence) if (anchorIdx === -1) return undefined - const newContent = extractFileContent(raw) + const newContent = streamingFile.content const spliced = [ ...lines.slice(0, anchorIdx + 1), ...(newContent.length > 0 ? newContent.split('\n') : []), @@ -701,8 +692,8 @@ function extractPatchPreview(raw: string, existingContent: string): string | und } if (mode === 'delete_between') { - const startAnchor = extractJsonString(raw, 'start_anchor') - const endAnchor = extractJsonString(raw, 'end_anchor') + const startAnchor = typeof edit.start_anchor === 'string' ? edit.start_anchor : undefined + const endAnchor = typeof edit.end_anchor === 'string' ? edit.end_anchor : undefined if (!startAnchor || !endAnchor) return undefined const startIdx = findAnchorIndex(lines, startAnchor, occurrence) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx index 8a60efbca4..fb5c1c44d3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx @@ -19,30 +19,29 @@ const PREVIEW_CYCLE: Record = { preview: 'editor', } as const -function streamFileBasename(name: string): string { - const n = name.replace(/\\/g, '/').trim() - const parts = n.split('/').filter(Boolean) - return parts.length ? parts[parts.length - 1]! : n -} - -function fileTitlesEquivalent(streamFileName: string, resourceTitle: string): boolean { - return streamFileBasename(streamFileName) === streamFileBasename(resourceTitle) -} - /** * Whether the active resource should show the in-progress file stream. - * The synthetic `streaming-file` tab always shows it; a real file tab shows it when - * the streamed `fileName` matches that resource (so users who stay on the open file see live text). + * The synthetic `streaming-file` tab always shows it; a real file tab only shows it + * when the streamed fileId matches that exact resource. */ function shouldShowStreamingFilePanel( - streamingFile: { fileName: string; fileId?: string; content: string } | null | undefined, + streamingFile: + | { + toolCallId?: string + fileName: string + fileId?: string + targetKind?: 'new_file' | 'file_id' + operation?: string + edit?: Record + content: string + } + | null + | undefined, active: MothershipResource | null ): boolean { if (!streamingFile || !active) return false if (active.id === 'streaming-file') return true if (active.type !== 'file') return false - const fn = streamingFile.fileName.trim() - if (fn && fileTitlesEquivalent(fn, active.title)) return true if (active.id && streamingFile.fileId === active.id) return true return false } @@ -59,7 +58,17 @@ interface MothershipViewProps { onCollapse: () => void isCollapsed: boolean className?: string - streamingFile?: { fileName: string; fileId?: string; content: string } | null + streamingFile?: + | { + toolCallId?: string + fileName: string + fileId?: string + targetKind?: 'new_file' | 'file_id' + operation?: string + edit?: Record + content: string + } + | null genericResourceData?: GenericResourceData } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 789703b11b..d57f57d703 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -20,7 +20,6 @@ import { MothershipStreamV1ToolPhase, } from '@/lib/copilot/generated/mothership-stream-v1' import { - CreateFile, CreateFolder, DeleteFolder, DeleteWorkflow, @@ -33,7 +32,6 @@ import { Read as ReadTool, Redeploy, RenameWorkflow, - SetFileContext, ToolSearchToolRegex, WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' @@ -105,7 +103,7 @@ export interface UseChatReturn { removeFromQueue: (id: string) => void sendNow: (id: string) => Promise editQueuedMessage: (id: string) => QueuedMessage | undefined - streamingFile: { fileName: string; content: string } | null + streamingFile: StreamingFilePreview | null genericResourceData: GenericResourceData | null } @@ -140,6 +138,16 @@ type StreamToolUI = { clientExecutable?: boolean } +type StreamingFilePreview = { + toolCallId: string + fileName: string + fileId?: string + targetKind?: 'new_file' | 'file_id' + operation?: string + edit?: Record + content: string +} + type StreamBatchEvent = { eventId: number streamId: string @@ -341,14 +349,11 @@ export function useChat( const activeResourceIdRef = useRef(effectiveActiveResourceId) activeResourceIdRef.current = effectiveActiveResourceId - const [streamingFile, setStreamingFile] = useState<{ - fileName: string - fileId?: string - content: string - } | null>(null) + const [streamingFile, setStreamingFile] = useState(null) const streamingFileRef = useRef(streamingFile) streamingFileRef.current = streamingFile - const activeFileContextRef = useRef<{ fileId?: string; fileName?: string } | null>(null) + const filePreviewSessionsRef = useRef>(new Map()) + const activeFilePreviewToolCallIdRef = useRef(null) const [messageQueue, setMessageQueue] = useState([]) const messageQueueRef = useRef([]) @@ -511,6 +516,8 @@ export function useChat( setActiveResourceId(null) setStreamingFile(null) streamingFileRef.current = null + filePreviewSessionsRef.current.clear() + activeFilePreviewToolCallIdRef.current = null setMessageQueue([]) }, [initialChatId, queryClient]) @@ -532,6 +539,8 @@ export function useChat( setActiveResourceId(null) setStreamingFile(null) streamingFileRef.current = null + filePreviewSessionsRef.current.clear() + activeFilePreviewToolCallIdRef.current = null setMessageQueue([]) }, [isHomePage]) @@ -860,6 +869,8 @@ export function useChat( } case MothershipStreamV1EventType.tool: { const payload = getPayloadData(parsed) + const previewPhase = + typeof payload.previewPhase === 'string' ? payload.previewPhase : undefined const phase = typeof payload.phase === 'string' ? payload.phase : MothershipStreamV1ToolPhase.call const id = @@ -870,60 +881,43 @@ export function useChat( : undefined if (!id) break - if (phase === MothershipStreamV1ToolPhase.args_delta) { - const delta = - typeof payload.argumentsDelta === 'string' ? payload.argumentsDelta : '' - if (!delta) break + if (previewPhase) { + const sessions = filePreviewSessionsRef.current + const prevSession = sessions.get(id) ?? { + toolCallId: id, + fileName: '', + content: '', + } - const toolName = - typeof payload.toolName === 'string' - ? payload.toolName - : (blocks[toolMap.get(id) ?? -1]?.toolCall?.name ?? '') - const streamWorkspaceFile = toolName === WorkspaceFile.id - - if (streamWorkspaceFile) { - let prev = streamingFileRef.current - if (!prev || (!prev.fileName && !prev.fileId)) { - const ctx = activeFileContextRef.current - prev = { - fileName: ctx?.fileName || prev?.fileName || '', - fileId: ctx?.fileId || prev?.fileId, - content: prev?.content || '', - } - streamingFileRef.current = prev - setStreamingFile(prev) + if (previewPhase === 'file_preview_start') { + const nextSession: StreamingFilePreview = { + ...prevSession, + toolCallId: id, } - const raw = prev.content + delta - let fileName = prev.fileName - if (!fileName) { - fileName = activeFileContextRef.current?.fileName || '' - if (!fileName) { - const match = raw.match(/"fileName"\s*:\s*"([^"]+)"/) - if (match) { - fileName = match[1] - } - } - } - const fileIdMatch = raw.match(/"fileId"\s*:\s*"([^"]+)"/) - const matchedResourceId = - fileIdMatch?.[1] || prev.fileId || activeFileContextRef.current?.fileId - const existingFileMatch = - matchedResourceId && - resourcesRef.current.some( - (resource) => resource.type === 'file' && resource.id === matchedResourceId - ) + sessions.set(id, nextSession) + activeFilePreviewToolCallIdRef.current = id + setStreamingFile(nextSession) + break + } - if (existingFileMatch) { - const hadStreamingResource = resourcesRef.current.some( - (resource) => resource.id === 'streaming-file' - ) - if (hadStreamingResource) { - setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file')) - setActiveResourceId(matchedResourceId) - } else if (activeResourceIdRef.current === null) { - setActiveResourceId(matchedResourceId) - } - } else if (fileName || fileIdMatch || activeSubagent === FileTool.id) { + if (previewPhase === 'file_preview_target') { + const target = asPayloadRecord(payload.target) + const nextSession: StreamingFilePreview = { + ...prevSession, + operation: typeof payload.operation === 'string' ? payload.operation : prevSession.operation, + targetKind: + target?.kind === 'new_file' || target?.kind === 'file_id' + ? (target.kind as 'new_file' | 'file_id') + : prevSession.targetKind, + fileId: + typeof target?.fileId === 'string' ? target.fileId : prevSession.fileId, + fileName: + typeof target?.fileName === 'string' ? target.fileName : prevSession.fileName, + } + sessions.set(id, nextSession) + activeFilePreviewToolCallIdRef.current = id + + if (nextSession.targetKind === 'new_file') { const hasStreamingResource = resourcesRef.current.some( (resource) => resource.id === 'streaming-file' ) @@ -931,16 +925,55 @@ export function useChat( addResource({ type: 'file', id: 'streaming-file', - title: fileName || 'Writing file...', + title: nextSession.fileName || 'Writing file...', }) setActiveResourceId('streaming-file') } + } else if (nextSession.fileId) { + setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file')) + if ( + activeResourceIdRef.current === null || + activeResourceIdRef.current === 'streaming-file' + ) { + setActiveResourceId(nextSession.fileId) + } } - const next = { fileName, fileId: matchedResourceId, content: raw } - streamingFileRef.current = next - setStreamingFile(next) + + setStreamingFile(nextSession) + break } + if (previewPhase === 'file_preview_edit_meta') { + const nextSession: StreamingFilePreview = { + ...prevSession, + edit: asPayloadRecord(payload.edit), + } + sessions.set(id, nextSession) + activeFilePreviewToolCallIdRef.current = id + setStreamingFile(nextSession) + break + } + + if (previewPhase === 'file_preview_content_delta') { + const delta = + typeof payload.delta === 'string' ? payload.delta : '' + if (!delta) break + const nextSession: StreamingFilePreview = { + ...prevSession, + content: (prevSession.content ?? '') + delta, + } + sessions.set(id, nextSession) + activeFilePreviewToolCallIdRef.current = id + setStreamingFile(nextSession) + break + } + } + + if (phase === MothershipStreamV1ToolPhase.args_delta) { + const delta = + typeof payload.argumentsDelta === 'string' ? payload.argumentsDelta : '' + if (!delta) break + const idx = toolMap.get(id) if (idx !== undefined && blocks[idx].toolCall) { const tc = blocks[idx].toolCall! @@ -949,7 +982,12 @@ export function useChat( if (tc.name === WorkspaceFile.id) { const opMatch = tc.streamingArgs.match(/"operation"\s*:\s*"(\w+)"/) const op = opMatch?.[1] ?? '' - const verb = op === 'patch' || op === 'update' ? 'Editing' : 'Writing' + const verb = + op === 'patch' || op === 'update' || op === 'rename' + ? 'Editing' + : op === 'delete' + ? 'Deleting' + : 'Writing' const titleMatch = tc.streamingArgs.match(/"title"\s*:\s*"([^"]*)"/) if (titleMatch?.[1]) { const unescaped = titleMatch[1] @@ -969,21 +1007,6 @@ export function useChat( if (phase === MothershipStreamV1ToolPhase.result) { const resultToolName = typeof payload.toolName === 'string' ? payload.toolName : '' - if ( - (resultToolName === CreateFile.id || resultToolName === SetFileContext.id) && - (payload.success === true || - payload.status === MothershipStreamV1ToolOutcome.success) - ) { - const resultOutput = asPayloadRecord(payload.result) - const ctxFileId = - typeof resultOutput?.fileId === 'string' ? resultOutput.fileId : undefined - const ctxFileName = - typeof resultOutput?.fileName === 'string' ? resultOutput.fileName : undefined - if (ctxFileId || ctxFileName) { - activeFileContextRef.current = { fileId: ctxFileId, fileName: ctxFileName } - } - } - const idx = toolMap.get(id) if (idx === undefined || !blocks[idx].toolCall) { break @@ -1073,24 +1096,17 @@ export function useChat( onToolResultRef.current?.(tc.name, tc.status === 'success', tc.result?.output) - if ( - (tc.name === CreateFile.id || tc.name === SetFileContext.id) && - tc.status === 'success' - ) { - const output = tc.result?.output as Record | undefined - const fileId = typeof output?.fileId === 'string' ? output.fileId : undefined - const fileName = - typeof output?.fileName === 'string' ? output.fileName : undefined - if (fileId || fileName) { - activeFileContextRef.current = { fileId, fileName } - } - } - if (isWorkflowToolName(tc.name)) { clientExecutionStartedRef.current.delete(id) } if (tc.name === WorkspaceFile.id) { + filePreviewSessionsRef.current.delete(id) + if (activeFilePreviewToolCallIdRef.current === id) { + activeFilePreviewToolCallIdRef.current = null + setStreamingFile(null) + streamingFileRef.current = null + } const fileResource = extractedResources.find((r) => r.type === 'file') if (fileResource) { setResources((rs) => { @@ -1116,7 +1132,7 @@ export function useChat( ? payload.name : 'unknown' const isPartial = payload.partial === true - if (name === ToolSearchToolRegex.id || name === SetFileContext.id) { + if (name === ToolSearchToolRegex.id) { break } const ui = getToolUI(payload) @@ -1129,13 +1145,19 @@ export function useChat( if (name === WorkspaceFile.id) { const operation = typeof args?.operation === 'string' ? args.operation : '' - const verb = operation === 'patch' || operation === 'update' ? 'Editing' : 'Writing' - const innerArgs = args ? asPayloadRecord(args.args) : undefined - const chunkTitle = innerArgs?.title as string | undefined + const verb = + operation === 'patch' || operation === 'update' || operation === 'rename' + ? 'Editing' + : operation === 'delete' + ? 'Deleting' + : 'Writing' + const chunkTitle = args?.title as string | undefined + const target = args ? asPayloadRecord(args.target) : undefined + const targetFileName = target?.fileName as string | undefined if (chunkTitle) { displayTitle = `${verb} ${chunkTitle}` - } else if (activeFileContextRef.current?.fileName) { - displayTitle = `${verb} ${activeFileContextRef.current.fileName}` + } else if (targetFileName) { + displayTitle = `${verb} ${targetFileName}` } } @@ -1300,7 +1322,11 @@ export function useChat( blocks.push({ type: 'subagent', content: name }) } if (name === FileTool.id) { - const emptyFile = { fileName: '', content: '' } + const emptyFile: StreamingFilePreview = { + toolCallId: parentToolCallId || 'file-preview', + fileName: '', + content: '', + } streamingFileRef.current = emptyFile setStreamingFile(emptyFile) } @@ -1950,6 +1976,8 @@ export function useChat( invalidateChatQueries() setStreamingFile(null) streamingFileRef.current = null + filePreviewSessionsRef.current.clear() + activeFilePreviewToolCallIdRef.current = null setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file')) const execState = useExecutionStore.getState() diff --git a/apps/sim/lib/copilot/chat/display-message.ts b/apps/sim/lib/copilot/chat/display-message.ts index eec31d52ae..b3e6ea3a6c 100644 --- a/apps/sim/lib/copilot/chat/display-message.ts +++ b/apps/sim/lib/copilot/chat/display-message.ts @@ -5,6 +5,7 @@ import { MothershipStreamV1ToolOutcome, } from '@/lib/copilot/generated/mothership-stream-v1' import { + type ChatContextKind, type ChatMessage, type ChatMessageAttachment, type ChatMessageContext, @@ -83,12 +84,14 @@ function toDisplayContexts( ): ChatMessageContext[] | undefined { if (!contexts || contexts.length === 0) return undefined return contexts.map((c) => ({ - kind: c.kind, + kind: c.kind as ChatContextKind, label: c.label, ...(c.workflowId ? { workflowId: c.workflowId } : {}), ...(c.knowledgeId ? { knowledgeId: c.knowledgeId } : {}), ...(c.tableId ? { tableId: c.tableId } : {}), ...(c.fileId ? { fileId: c.fileId } : {}), + ...(c.folderId ? { folderId: c.folderId } : {}), + ...(c.chatId ? { chatId: c.chatId } : {}), })) } diff --git a/apps/sim/lib/copilot/chat/persisted-message.ts b/apps/sim/lib/copilot/chat/persisted-message.ts index 7caee7c1ef..012af71f1b 100644 --- a/apps/sim/lib/copilot/chat/persisted-message.ts +++ b/apps/sim/lib/copilot/chat/persisted-message.ts @@ -55,6 +55,8 @@ export interface PersistedMessageContext { knowledgeId?: string tableId?: string fileId?: string + folderId?: string + chatId?: string } export interface PersistedMessage { @@ -199,6 +201,8 @@ export function buildPersistedUserMessage(params: UserMessageParams): PersistedM ...(c.knowledgeId ? { knowledgeId: c.knowledgeId } : {}), ...(c.tableId ? { tableId: c.tableId } : {}), ...(c.fileId ? { fileId: c.fileId } : {}), + ...(c.folderId ? { folderId: c.folderId } : {}), + ...(c.chatId ? { chatId: c.chatId } : {}), })) } @@ -462,6 +466,8 @@ export function normalizeMessage(raw: Record): PersistedMessage ...(c.knowledgeId ? { knowledgeId: c.knowledgeId } : {}), ...(c.tableId ? { tableId: c.tableId } : {}), ...(c.fileId ? { fileId: c.fileId } : {}), + ...(c.folderId ? { folderId: c.folderId } : {}), + ...(c.chatId ? { chatId: c.chatId } : {}), })) } diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index b40064b502..b8ecf0188f 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -6,12 +6,14 @@ export interface ToolCatalogEntry { clientExecutable?: boolean; executor: "client" | "go" | "sim" | "subagent"; hidden?: boolean; - id: "agent" | "auth" | "check_deployment_status" | "complete_job" | "context_write" | "crawl_website" | "create_file" | "create_folder" | "create_job" | "create_workflow" | "create_workspace_mcp_server" | "debug" | "delete_folder" | "delete_workflow" | "delete_workspace_mcp_server" | "deploy" | "deploy_api" | "deploy_chat" | "deploy_mcp" | "download_to_workspace_file" | "edit_workflow" | "file" | "function_execute" | "generate_api_key" | "generate_image" | "generate_visualization" | "get_block_outputs" | "get_block_upstream_references" | "get_deployed_workflow_state" | "get_deployment_version" | "get_execution_summary" | "get_job_logs" | "get_page_contents" | "get_platform_actions" | "get_workflow_data" | "get_workflow_logs" | "glob" | "grep" | "job" | "knowledge" | "knowledge_base" | "list_folders" | "list_user_workspaces" | "list_workspace_mcp_servers" | "manage_credential" | "manage_custom_tool" | "manage_job" | "manage_mcp_tool" | "manage_skill" | "materialize_file" | "move_folder" | "move_workflow" | "oauth_get_auth_link" | "oauth_request_access" | "open_resource" | "read" | "redeploy" | "rename_workflow" | "research" | "respond" | "revert_to_version" | "run" | "run_block" | "run_from_block" | "run_workflow" | "run_workflow_until_block" | "scrape_page" | "search_documentation" | "search_library_docs" | "search_online" | "search_patterns" | "set_environment_variables" | "set_file_context" | "set_global_workflow_variables" | "superagent" | "table" | "tool_search_tool_regex" | "update_job_history" | "update_workspace_mcp_server" | "user_memory" | "user_table" | "workflow" | "workspace_file"; + id: "agent" | "auth" | "check_deployment_status" | "complete_job" | "context_write" | "crawl_website" | "create_folder" | "create_job" | "create_workflow" | "create_workspace_mcp_server" | "debug" | "delete_folder" | "delete_workflow" | "delete_workspace_mcp_server" | "deploy" | "deploy_api" | "deploy_chat" | "deploy_mcp" | "download_to_workspace_file" | "edit_workflow" | "file" | "function_execute" | "generate_api_key" | "generate_image" | "generate_visualization" | "get_block_outputs" | "get_block_upstream_references" | "get_deployed_workflow_state" | "get_deployment_version" | "get_execution_summary" | "get_job_logs" | "get_page_contents" | "get_platform_actions" | "get_workflow_data" | "get_workflow_logs" | "glob" | "grep" | "job" | "knowledge" | "knowledge_base" | "list_folders" | "list_user_workspaces" | "list_workspace_mcp_servers" | "manage_credential" | "manage_custom_tool" | "manage_job" | "manage_mcp_tool" | "manage_skill" | "materialize_file" | "move_folder" | "move_workflow" | "oauth_get_auth_link" | "oauth_request_access" | "open_resource" | "read" | "redeploy" | "rename_workflow" | "research" | "respond" | "revert_to_version" | "run" | "run_block" | "run_from_block" | "run_workflow" | "run_workflow_until_block" | "scrape_page" | "search_documentation" | "search_library_docs" | "search_online" | "search_patterns" | "set_environment_variables" | "set_global_workflow_variables" | "superagent" | "table" | "tool_search_tool_regex" | "update_job_history" | "update_workspace_mcp_server" | "user_memory" | "user_table" | "workflow" | "workspace_file"; internal?: boolean; mode: "async" | "sync"; - name: "agent" | "auth" | "check_deployment_status" | "complete_job" | "context_write" | "crawl_website" | "create_file" | "create_folder" | "create_job" | "create_workflow" | "create_workspace_mcp_server" | "debug" | "delete_folder" | "delete_workflow" | "delete_workspace_mcp_server" | "deploy" | "deploy_api" | "deploy_chat" | "deploy_mcp" | "download_to_workspace_file" | "edit_workflow" | "file" | "function_execute" | "generate_api_key" | "generate_image" | "generate_visualization" | "get_block_outputs" | "get_block_upstream_references" | "get_deployed_workflow_state" | "get_deployment_version" | "get_execution_summary" | "get_job_logs" | "get_page_contents" | "get_platform_actions" | "get_workflow_data" | "get_workflow_logs" | "glob" | "grep" | "job" | "knowledge" | "knowledge_base" | "list_folders" | "list_user_workspaces" | "list_workspace_mcp_servers" | "manage_credential" | "manage_custom_tool" | "manage_job" | "manage_mcp_tool" | "manage_skill" | "materialize_file" | "move_folder" | "move_workflow" | "oauth_get_auth_link" | "oauth_request_access" | "open_resource" | "read" | "redeploy" | "rename_workflow" | "research" | "respond" | "revert_to_version" | "run" | "run_block" | "run_from_block" | "run_workflow" | "run_workflow_until_block" | "scrape_page" | "search_documentation" | "search_library_docs" | "search_online" | "search_patterns" | "set_environment_variables" | "set_file_context" | "set_global_workflow_variables" | "superagent" | "table" | "tool_search_tool_regex" | "update_job_history" | "update_workspace_mcp_server" | "user_memory" | "user_table" | "workflow" | "workspace_file"; + name: "agent" | "auth" | "check_deployment_status" | "complete_job" | "context_write" | "crawl_website" | "create_folder" | "create_job" | "create_workflow" | "create_workspace_mcp_server" | "debug" | "delete_folder" | "delete_workflow" | "delete_workspace_mcp_server" | "deploy" | "deploy_api" | "deploy_chat" | "deploy_mcp" | "download_to_workspace_file" | "edit_workflow" | "file" | "function_execute" | "generate_api_key" | "generate_image" | "generate_visualization" | "get_block_outputs" | "get_block_upstream_references" | "get_deployed_workflow_state" | "get_deployment_version" | "get_execution_summary" | "get_job_logs" | "get_page_contents" | "get_platform_actions" | "get_workflow_data" | "get_workflow_logs" | "glob" | "grep" | "job" | "knowledge" | "knowledge_base" | "list_folders" | "list_user_workspaces" | "list_workspace_mcp_servers" | "manage_credential" | "manage_custom_tool" | "manage_job" | "manage_mcp_tool" | "manage_skill" | "materialize_file" | "move_folder" | "move_workflow" | "oauth_get_auth_link" | "oauth_request_access" | "open_resource" | "read" | "redeploy" | "rename_workflow" | "research" | "respond" | "revert_to_version" | "run" | "run_block" | "run_from_block" | "run_workflow" | "run_workflow_until_block" | "scrape_page" | "search_documentation" | "search_library_docs" | "search_online" | "search_patterns" | "set_environment_variables" | "set_global_workflow_variables" | "superagent" | "table" | "tool_search_tool_regex" | "update_job_history" | "update_workspace_mcp_server" | "user_memory" | "user_table" | "workflow" | "workspace_file"; + parameters: unknown; requiredPermission?: "admin" | "write"; requiresConfirmation?: boolean; + resultSchema?: unknown; subagentId?: "agent" | "auth" | "debug" | "deploy" | "file" | "job" | "knowledge" | "research" | "run" | "superagent" | "table" | "workflow"; } @@ -20,6 +22,7 @@ export const Agent: ToolCatalogEntry = { name: "agent", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"What tool/skill/MCP action is needed.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "agent", internal: true, requiredPermission: "write", @@ -30,6 +33,7 @@ export const Auth: ToolCatalogEntry = { name: "auth", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"What authentication/credential action is needed.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "auth", internal: true, }; @@ -39,6 +43,7 @@ export const CheckDeploymentStatus: ToolCatalogEntry = { name: "check_deployment_status", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"workflowId":{"type":"string","description":"Workflow ID to check (defaults to current workflow)"}}}, }; export const CompleteJob: ToolCatalogEntry = { @@ -46,6 +51,7 @@ export const CompleteJob: ToolCatalogEntry = { name: "complete_job", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"jobId":{"type":"string","description":"The ID of the job to mark as completed."}},"required":["jobId"]}, }; export const ContextWrite: ToolCatalogEntry = { @@ -53,6 +59,7 @@ export const ContextWrite: ToolCatalogEntry = { name: "context_write", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"content":{"type":"string","description":"Full content to write to the file (replaces existing content)"},"file_path":{"type":"string","description":"Path of the file to write (e.g. 'SESSION.md')"}},"required":["file_path","content"]}, }; export const CrawlWebsite: ToolCatalogEntry = { @@ -60,13 +67,7 @@ export const CrawlWebsite: ToolCatalogEntry = { name: "crawl_website", executor: "go", mode: "sync", -}; - -export const CreateFile: ToolCatalogEntry = { - id: "create_file", - name: "create_file", - executor: "sim", - mode: "async", + parameters: {"type":"object","properties":{"exclude_paths":{"type":"array","description":"Skip URLs matching these patterns","items":{"type":"string"}},"include_paths":{"type":"array","description":"Only crawl URLs matching these patterns","items":{"type":"string"}},"limit":{"type":"number","description":"Maximum pages to crawl (default 10, max 50)"},"max_depth":{"type":"number","description":"How deep to follow links (default 2)"},"url":{"type":"string","description":"Starting URL to crawl from"}},"required":["url"]}, }; export const CreateFolder: ToolCatalogEntry = { @@ -74,6 +75,7 @@ export const CreateFolder: ToolCatalogEntry = { name: "create_folder", executor: "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", }; @@ -82,6 +84,7 @@ export const CreateJob: ToolCatalogEntry = { name: "create_job", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"cron":{"type":"string","description":"Cron expression for recurring jobs (e.g., '*/5 * * * *' for every 5 minutes, '0 9 * * *' for daily at 9 AM). Omit for one-time jobs."},"lifecycle":{"type":"string","description":"'persistent' (default) or 'until_complete'. Until_complete jobs stop when complete_job is called after the success condition is met.","enum":["persistent","until_complete"]},"maxRuns":{"type":"integer","description":"Maximum number of executions before the job auto-completes. Safety limit to prevent runaway polling."},"prompt":{"type":"string","description":"The prompt to execute when the job fires. This is sent to the Mothership as a user message."},"successCondition":{"type":"string","description":"What must happen for the job to be considered complete. Used with until_complete lifecycle (e.g., 'John has replied to the partnership email')."},"time":{"type":"string","description":"ISO 8601 datetime for one-time execution or as the start time for a cron schedule (e.g., '2026-03-06T09:00:00'). Include timezone offset or use the timezone parameter."},"timezone":{"type":"string","description":"IANA timezone for the schedule (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC."},"title":{"type":"string","description":"A short, descriptive title for the job (e.g., 'Email Poller', 'Daily Report'). Used as the display name."}},"required":["title","prompt"]}, }; export const CreateWorkflow: ToolCatalogEntry = { @@ -89,6 +92,7 @@ export const CreateWorkflow: ToolCatalogEntry = { name: "create_workflow", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"description":{"type":"string","description":"Optional workflow description."},"folderId":{"type":"string","description":"Optional folder ID."},"name":{"type":"string","description":"Workflow name."},"workspaceId":{"type":"string","description":"Optional workspace ID."}},"required":["name"]}, requiredPermission: "write", }; @@ -97,6 +101,7 @@ export const CreateWorkspaceMcpServer: ToolCatalogEntry = { name: "create_workspace_mcp_server", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"description":{"type":"string","description":"Optional description for the server"},"name":{"type":"string","description":"Required: server name"},"workspaceId":{"type":"string","description":"Workspace ID (defaults to current workspace)"}},"required":["name"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -106,6 +111,7 @@ export const Debug: ToolCatalogEntry = { name: "debug", executor: "subagent", mode: "async", + parameters: {"properties":{"context":{"description":"Pre-gathered context: workflow state JSON, block schemas, error logs. The debug agent will skip re-reading anything included here.","type":"string"},"request":{"description":"What to debug. Include error messages, block IDs, and any context about the failure.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "debug", internal: true, }; @@ -115,6 +121,7 @@ export const DeleteFolder: ToolCatalogEntry = { name: "delete_folder", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"folderId":{"type":"string","description":"The folder ID to delete."}},"required":["folderId"]}, requiresConfirmation: true, requiredPermission: "write", }; @@ -124,6 +131,7 @@ export const DeleteWorkflow: ToolCatalogEntry = { name: "delete_workflow", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"workflowId":{"type":"string","description":"The workflow ID to delete."}},"required":["workflowId"]}, requiresConfirmation: true, requiredPermission: "write", }; @@ -133,6 +141,7 @@ export const DeleteWorkspaceMcpServer: ToolCatalogEntry = { name: "delete_workspace_mcp_server", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"serverId":{"type":"string","description":"Required: the MCP server ID to delete"}},"required":["serverId"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -142,6 +151,7 @@ export const Deploy: ToolCatalogEntry = { name: "deploy", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"Detailed deployment instructions. Include deployment type (api/chat) and ALL user-specified options: identifier, title, description, authType, password, allowedEmails, welcomeMessage, outputConfigs (block outputs to display).","type":"string"}},"required":["request"],"type":"object"}, subagentId: "deploy", internal: true, }; @@ -151,6 +161,7 @@ export const DeployApi: ToolCatalogEntry = { name: "deploy_api", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"action":{"type":"string","description":"Whether to deploy or undeploy the API endpoint","enum":["deploy","undeploy"],"default":"deploy"},"workflowId":{"type":"string","description":"Workflow ID to deploy (required in workspace context)"}}}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -160,6 +171,7 @@ export const DeployChat: ToolCatalogEntry = { name: "deploy_chat", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"action":{"type":"string","description":"Whether to deploy or undeploy the chat interface","enum":["deploy","undeploy"],"default":"deploy"},"allowedEmails":{"type":"array","description":"List of allowed emails/domains for email or SSO auth","items":{"type":"string"}},"authType":{"type":"string","description":"Authentication type: public, password, email, or sso","enum":["public","password","email","sso"],"default":"public"},"description":{"type":"string","description":"Optional description for the chat"},"identifier":{"type":"string","description":"URL slug for the chat (lowercase letters, numbers, hyphens only)"},"outputConfigs":{"type":"array","description":"Output configurations specifying which block outputs to display in chat","items":{"type":"object","properties":{"blockId":{"type":"string","description":"The block UUID"},"path":{"type":"string","description":"The output path (e.g. 'response', 'response.content')"}},"required":["blockId","path"]}},"password":{"type":"string","description":"Password for password-protected chats"},"title":{"type":"string","description":"Display title for the chat interface"},"welcomeMessage":{"type":"string","description":"Welcome message shown to users"},"workflowId":{"type":"string","description":"Workflow ID to deploy (required in workspace context)"}}}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -169,6 +181,7 @@ export const DeployMcp: ToolCatalogEntry = { name: "deploy_mcp", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"parameterDescriptions":{"type":"array","description":"Array of parameter descriptions for the tool","items":{"type":"object","properties":{"description":{"type":"string","description":"Parameter description"},"name":{"type":"string","description":"Parameter name"}},"required":["name","description"]}},"serverId":{"type":"string","description":"Required: server ID from list_workspace_mcp_servers"},"toolDescription":{"type":"string","description":"Description for the MCP tool"},"toolName":{"type":"string","description":"Name for the MCP tool (defaults to workflow name)"},"workflowId":{"type":"string","description":"Workflow ID (defaults to active workflow)"}},"required":["serverId"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -178,6 +191,7 @@ export const DownloadToWorkspaceFile: ToolCatalogEntry = { name: "download_to_workspace_file", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"fileName":{"type":"string","description":"Optional workspace file name to save as. If omitted, the name is inferred from the response or URL."},"url":{"type":"string","description":"Direct URL of the file to download, such as an image CDN URL ending in .png or .jpg"}},"required":["url"]}, requiredPermission: "write", }; @@ -186,6 +200,7 @@ export const EditWorkflow: ToolCatalogEntry = { name: "edit_workflow", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"operations":{"type":"array","description":"Array of edit operations","items":{"type":"object","properties":{"block_id":{"type":"string","description":"Block ID for the operation. For add operations, this will be the desired ID for the new block."},"operation_type":{"type":"string","description":"Type of operation to perform","enum":["add","edit","delete","insert_into_subflow","extract_from_subflow"]},"params":{"type":"object","description":"Parameters for the operation. \nFor edit: {\"inputs\": {\"temperature\": 0.5}} NOT {\"subBlocks\": {\"temperature\": {\"value\": 0.5}}}\nFor add: {\"type\": \"agent\", \"name\": \"My Agent\", \"inputs\": {\"model\": \"gpt-4o\"}}\nFor delete: {} (empty object)"}},"required":["operation_type","block_id","params"]}},"workflowId":{"type":"string","description":"Optional workflow ID to edit. If not provided, uses the current workflow in context."}},"required":["operations"]}, requiredPermission: "write", }; @@ -194,6 +209,7 @@ export const File: ToolCatalogEntry = { name: "file", executor: "subagent", mode: "async", + parameters: {"type":"object"}, subagentId: "file", internal: true, }; @@ -203,6 +219,7 @@ export const FunctionExecute: ToolCatalogEntry = { name: "function_execute", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"code":{"type":"string","description":"Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with access to pre-installed CLI tools and workspace env vars as $VAR_NAME."},"inputFiles":{"type":"array","description":"Canonical workspace file IDs to mount in the sandbox. Discover IDs via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\"). Mounted path: /home/user/files/{fileId}/{originalName}. Example: [\"wf_123\"]","items":{"type":"string"}},"inputTables":{"type":"array","description":"Table IDs to mount as CSV files in the sandbox. Each table appears at /home/user/tables/{tableId}.csv with a header row. Example: [\"tbl_abc123\"]","items":{"type":"string"}},"language":{"type":"string","description":"Execution language.","enum":["javascript","python","shell"]},"outputFormat":{"type":"string","description":"Format for outputPath. Determines how the code result is serialized. If omitted, inferred from outputPath file extension.","enum":["json","csv","txt","md","html"]},"outputMimeType":{"type":"string","description":"MIME type for outputSandboxPath export. Required for binary files: image/png, image/jpeg, application/pdf, etc. Omit for text files."},"outputPath":{"type":"string","description":"Pipe output directly to a NEW workspace file instead of returning in context. ALWAYS use this instead of a separate workspace_file write call. Use a flat path like \"files/result.json\" — nested paths are not supported."},"outputSandboxPath":{"type":"string","description":"Path to a file created inside the sandbox that should be exported to the workspace. Use together with outputPath."},"outputTable":{"type":"string","description":"Table ID to overwrite with the code's return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: \"tbl_abc123\""}},"required":["code"]}, requiredPermission: "write", }; @@ -211,6 +228,7 @@ export const GenerateApiKey: ToolCatalogEntry = { name: "generate_api_key", executor: "sim", mode: "async", + parameters: {"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"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -220,6 +238,7 @@ export const GenerateImage: ToolCatalogEntry = { name: "generate_image", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"aspectRatio":{"type":"string","description":"Aspect ratio for the generated image.","enum":["1:1","16:9","9:16","4:3","3:4"]},"fileName":{"type":"string","description":"Output file name. Defaults to \"generated-image.png\". Workspace files are flat, so pass a plain file name, not a nested path."},"overwriteFileId":{"type":"string","description":"If provided, overwrites the existing workspace file with this ID instead of creating a new file. Use this when the user asks to update or redo a previously generated image. The file ID is returned by previous generate_image or generate_visualization calls (fileId field), or can be found via read(\"files/by-id/{fileId}/meta.json\")."},"prompt":{"type":"string","description":"Detailed text description of the image to generate, or editing instructions when used with editFileId."},"referenceFileIds":{"type":"array","description":"File IDs of workspace images to include as context for the generation. All images are sent alongside the prompt. Use for: editing a single image (1 file), compositing multiple images together (2+ files), style transfer, face swapping, etc. Order matters — list the primary/base image first.","items":{"type":"string"}}},"required":["prompt"]}, requiredPermission: "write", }; @@ -228,6 +247,7 @@ export const GenerateVisualization: ToolCatalogEntry = { name: "generate_visualization", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"code":{"type":"string","description":"Python code that generates a visualization using matplotlib. MUST call plt.savefig('/home/user/output.png', dpi=150, bbox_inches='tight') to produce output."},"fileName":{"type":"string","description":"Output file name. Defaults to \"chart.png\". Workspace files are flat, so pass a plain file name, not a nested path."},"inputFiles":{"type":"array","description":"Canonical workspace file IDs to mount in the sandbox. Discover IDs via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\"). Mounted path: /home/user/files/{fileId}/{originalName}.","items":{"type":"string"}},"inputTables":{"type":"array","description":"Table IDs to mount as CSV files in the sandbox. Each table appears at /home/user/tables/{tableId}.csv with a header row. Read with pandas: pd.read_csv('/home/user/tables/tbl_xxx.csv')","items":{"type":"string"}},"overwriteFileId":{"type":"string","description":"If provided, overwrites the existing workspace file with this ID instead of creating a new file. Use this when the user asks to update or redo a previously generated chart. The file ID is returned by previous generate_visualization or generate_image calls (fileId field), or can be found via read(\"files/by-id/{fileId}/meta.json\")."}},"required":["code"]}, requiredPermission: "write", }; @@ -236,6 +256,7 @@ export const GetBlockOutputs: ToolCatalogEntry = { name: "get_block_outputs", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"blockIds":{"type":"array","description":"Optional array of block UUIDs. If provided, returns outputs only for those blocks. If not provided, returns outputs for all blocks in the workflow.","items":{"type":"string"}},"workflowId":{"type":"string","description":"Optional workflow ID. If not provided, uses the current workflow in context."}}}, }; export const GetBlockUpstreamReferences: ToolCatalogEntry = { @@ -243,6 +264,7 @@ export const GetBlockUpstreamReferences: ToolCatalogEntry = { name: "get_block_upstream_references", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"blockIds":{"type":"array","description":"Required array of block UUIDs (minimum 1). Returns what each block can reference based on its position in the workflow graph.","items":{"type":"string"}},"workflowId":{"type":"string","description":"Optional workflow ID. If not provided, uses the current workflow in context."}},"required":["blockIds"]}, }; export const GetDeployedWorkflowState: ToolCatalogEntry = { @@ -250,6 +272,7 @@ export const GetDeployedWorkflowState: ToolCatalogEntry = { name: "get_deployed_workflow_state", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"workflowId":{"type":"string","description":"Optional workflow ID. If not provided, uses the current workflow in context."}}}, }; export const GetDeploymentVersion: ToolCatalogEntry = { @@ -257,6 +280,7 @@ export const GetDeploymentVersion: ToolCatalogEntry = { name: "get_deployment_version", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"version":{"type":"number","description":"The deployment version number"},"workflowId":{"type":"string","description":"The workflow ID"}},"required":["workflowId","version"]}, }; export const GetExecutionSummary: ToolCatalogEntry = { @@ -264,6 +288,7 @@ export const GetExecutionSummary: ToolCatalogEntry = { name: "get_execution_summary", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"limit":{"type":"number","description":"Max number of executions to return (default: 10, max: 20)."},"status":{"type":"string","description":"Filter by status: 'success', 'error', or 'all' (default: 'all').","enum":["success","error","all"]},"workflowId":{"type":"string","description":"Optional workflow ID. If omitted, returns executions across all workflows in the workspace."},"workspaceId":{"type":"string","description":"Workspace ID to scope executions to."}},"required":["workspaceId"]}, }; export const GetJobLogs: ToolCatalogEntry = { @@ -271,6 +296,7 @@ export const GetJobLogs: ToolCatalogEntry = { name: "get_job_logs", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"executionId":{"type":"string","description":"Optional execution ID for a specific run."},"includeDetails":{"type":"boolean","description":"Include tool calls, outputs, and cost details."},"jobId":{"type":"string","description":"The job (schedule) ID to get logs for."},"limit":{"type":"number","description":"Max number of entries (default: 3, max: 5)"}},"required":["jobId"]}, }; export const GetPageContents: ToolCatalogEntry = { @@ -278,6 +304,7 @@ export const GetPageContents: ToolCatalogEntry = { name: "get_page_contents", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"include_highlights":{"type":"boolean","description":"Include key highlights (default false)"},"include_summary":{"type":"boolean","description":"Include AI-generated summary (default false)"},"include_text":{"type":"boolean","description":"Include full page text (default true)"},"urls":{"type":"array","description":"URLs to get content from (max 10)","items":{"type":"string"}}},"required":["urls"]}, }; export const GetPlatformActions: ToolCatalogEntry = { @@ -285,6 +312,7 @@ export const GetPlatformActions: ToolCatalogEntry = { name: "get_platform_actions", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{}}, }; export const GetWorkflowData: ToolCatalogEntry = { @@ -292,6 +320,7 @@ export const GetWorkflowData: ToolCatalogEntry = { name: "get_workflow_data", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"data_type":{"type":"string","description":"The type of workflow data to retrieve","enum":["global_variables","custom_tools","mcp_tools","files"]},"workflowId":{"type":"string","description":"Optional workflow ID. If not provided, uses the current workflow in context."}},"required":["data_type"]}, }; export const GetWorkflowLogs: ToolCatalogEntry = { @@ -299,6 +328,7 @@ export const GetWorkflowLogs: ToolCatalogEntry = { name: "get_workflow_logs", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"executionId":{"type":"string","description":"Optional execution ID to get logs for a specific execution. Use with get_execution_summary to find execution IDs first."},"includeDetails":{"type":"boolean","description":"Include detailed info"},"limit":{"type":"number","description":"Max number of entries (hard limit: 3)"},"workflowId":{"type":"string","description":"Optional workflow ID. If not provided, uses the current workflow in context."}}}, }; export const Glob: ToolCatalogEntry = { @@ -306,6 +336,7 @@ export const Glob: ToolCatalogEntry = { name: "glob", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"pattern":{"type":"string","description":"Glob pattern to match file paths. Supports * (any segment) and ** (any depth)."}},"required":["pattern"]}, }; export const Grep: ToolCatalogEntry = { @@ -313,6 +344,7 @@ export const Grep: ToolCatalogEntry = { name: "grep", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"context":{"type":"number","description":"Number of lines to show before and after each match. Only applies to output_mode 'content'."},"ignoreCase":{"type":"boolean","description":"Case insensitive search (default false)."},"lineNumbers":{"type":"boolean","description":"Include line numbers in output (default true). Only applies to output_mode 'content'."},"maxResults":{"type":"number","description":"Maximum number of matches to return (default 50)."},"output_mode":{"type":"string","description":"Output mode: 'content' shows matching lines (default), 'files_with_matches' shows only file paths, 'count' shows match counts per file.","enum":["content","files_with_matches","count"]},"path":{"type":"string","description":"Optional path prefix to scope the search (e.g. 'workflows/', 'environment/', 'internal/', 'components/blocks/')."},"pattern":{"type":"string","description":"Regex pattern to search for in file contents."}},"required":["pattern"]}, }; export const Job: ToolCatalogEntry = { @@ -320,6 +352,7 @@ export const Job: ToolCatalogEntry = { name: "job", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"What job action is needed.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "job", internal: true, }; @@ -329,6 +362,7 @@ export const Knowledge: ToolCatalogEntry = { name: "knowledge", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"What knowledge base action is needed.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "knowledge", internal: true, }; @@ -338,6 +372,8 @@ export const KnowledgeBase: ToolCatalogEntry = { name: "knowledge_base", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"args":{"type":"object","description":"Arguments for the operation","properties":{"apiKey":{"type":"string","description":"API key for API-key-based connectors (required when connector auth mode is apiKey)"},"chunkingConfig":{"type":"object","description":"Chunking configuration (optional for 'create')","properties":{"maxSize":{"type":"number","description":"Maximum chunk size (100-4000, default: 1024)","default":1024},"minSize":{"type":"number","description":"Minimum chunk size (1-2000, default: 1)","default":1},"overlap":{"type":"number","description":"Overlap between chunks (0-500, default: 200)","default":200}}},"connectorId":{"type":"string","description":"Connector ID (required for update_connector, delete_connector, sync_connector)"},"connectorStatus":{"type":"string","description":"Connector status (optional for update_connector)","enum":["active","paused"]},"connectorType":{"type":"string","description":"Connector type from registry, e.g. 'confluence', 'google_drive', 'notion' (required for add_connector). Read knowledgebases/connectors/{type}.json for the config schema."},"credentialId":{"type":"string","description":"OAuth credential ID from environment/credentials.json (required for OAuth connectors)"},"description":{"type":"string","description":"Description of the knowledge base (optional for 'create')"},"disabledTagIds":{"type":"array","description":"Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema."},"documentId":{"type":"string","description":"Document ID (required for delete_document, update_document)"},"enabled":{"type":"boolean","description":"Enable/disable a document (optional for update_document)"},"fileId":{"type":"string","description":"Canonical workspace file ID to add as a document (preferred for add_file). Discover via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\")."},"filePath":{"type":"string","description":"Legacy workspace file reference for add_file. Prefer fileId."},"filename":{"type":"string","description":"New filename for a document (optional for update_document)"},"knowledgeBaseId":{"type":"string","description":"Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage)"},"name":{"type":"string","description":"Name of the knowledge base (required for 'create')"},"query":{"type":"string","description":"Search query text (required for 'query')"},"sourceConfig":{"type":"object","description":"Connector-specific configuration matching the configFields in knowledgebases/connectors/{type}.json"},"syncIntervalMinutes":{"type":"number","description":"Sync interval in minutes: 60 (hourly), 360 (6h), 1440 (daily), 10080 (weekly), 0 (manual only). Default: 1440","default":1440},"tagDefinitionId":{"type":"string","description":"Tag definition ID (required for update_tag, delete_tag)"},"tagDisplayName":{"type":"string","description":"Display name for the tag (required for create_tag, optional for update_tag)"},"tagFieldType":{"type":"string","description":"Field type: text, number, date, boolean (optional for create_tag, defaults to text)","enum":["text","number","date","boolean"]},"topK":{"type":"number","description":"Number of results to return (1-50, default: 5)","default":5},"workspaceId":{"type":"string","description":"Workspace ID (required for 'create', optional filter for 'list')"}}},"operation":{"type":"string","description":"The operation to perform","enum":["create","get","query","add_file","update","delete","delete_document","update_document","list_tags","create_tag","update_tag","delete_tag","get_tag_usage","add_connector","update_connector","delete_connector","sync_connector"]}},"required":["operation","args"]}, + resultSchema: {"type":"object","properties":{"data":{"type":"object","description":"Operation-specific result payload."},"message":{"type":"string","description":"Human-readable outcome summary."},"success":{"type":"boolean","description":"Whether the operation succeeded."}},"required":["success","message"]}, requiresConfirmation: true, }; @@ -346,6 +382,7 @@ export const ListFolders: ToolCatalogEntry = { name: "list_folders", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"workspaceId":{"type":"string","description":"Optional workspace ID to list folders for."}}}, }; export const ListUserWorkspaces: ToolCatalogEntry = { @@ -353,6 +390,7 @@ export const ListUserWorkspaces: ToolCatalogEntry = { name: "list_user_workspaces", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{}}, }; export const ListWorkspaceMcpServers: ToolCatalogEntry = { @@ -360,6 +398,7 @@ export const ListWorkspaceMcpServers: ToolCatalogEntry = { name: "list_workspace_mcp_servers", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"workspaceId":{"type":"string","description":"Workspace ID (defaults to current workspace)"}}}, }; export const ManageCredential: ToolCatalogEntry = { @@ -367,6 +406,7 @@ export const ManageCredential: ToolCatalogEntry = { name: "manage_credential", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"credentialId":{"type":"string","description":"The credential ID (from environment/credentials.json)"},"displayName":{"type":"string","description":"New display name (required for rename)"},"operation":{"type":"string","description":"The operation to perform","enum":["rename","delete"]}},"required":["operation","credentialId"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -376,6 +416,7 @@ export const ManageCustomTool: ToolCatalogEntry = { name: "manage_custom_tool", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"code":{"type":"string","description":"The JavaScript code that executes when the tool is called (required for add). Parameters from schema are available as variables. Function body only - no signature or wrapping braces."},"operation":{"type":"string","description":"The operation to perform: 'add', 'edit', 'list', or 'delete'","enum":["add","edit","delete","list"]},"schema":{"type":"object","description":"The tool schema in OpenAI function calling format (required for add).","properties":{"function":{"type":"object","description":"The function definition","properties":{"description":{"type":"string","description":"What the function does"},"name":{"type":"string","description":"The function name (camelCase)"},"parameters":{"type":"object","description":"The function parameters schema","properties":{"properties":{"type":"object","description":"Parameter definitions as key-value pairs"},"required":{"type":"array","description":"Array of required parameter names","items":{"type":"string"}},"type":{"type":"string","description":"Must be 'object'"}},"required":["type","properties"]}},"required":["name","parameters"]},"type":{"type":"string","description":"Must be 'function'"}},"required":["type","function"]},"toolId":{"type":"string","description":"The ID of the custom tool (required for edit/delete). Must be the exact toolId from the get_workflow_data custom tool response - do not guess or construct it. DO NOT PROVIDE THE TOOL ID IF THE OPERATION IS 'ADD'."}},"required":["operation"]}, requiresConfirmation: true, }; @@ -384,6 +425,7 @@ export const ManageJob: ToolCatalogEntry = { name: "manage_job", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"args":{"type":"object","description":"Operation-specific arguments. For create: {title, prompt, cron?, time?, timezone?, lifecycle?, successCondition?, maxRuns?}. For get/delete: {jobId}. For update: {jobId, title?, prompt?, cron?, timezone?, status?, lifecycle?, successCondition?, maxRuns?}. For list: no args needed.","properties":{"cron":{"type":"string","description":"Cron expression for recurring jobs"},"jobId":{"type":"string","description":"Job ID (required for get, update, delete)"},"lifecycle":{"type":"string","description":"'persistent' (default) or 'until_complete'. Until_complete jobs stop when complete_job is called."},"maxRuns":{"type":"integer","description":"Max executions before auto-completing. Safety limit."},"prompt":{"type":"string","description":"The prompt to execute when the job fires"},"status":{"type":"string","description":"Job status: active, paused"},"successCondition":{"type":"string","description":"What must happen for the job to be considered complete (until_complete lifecycle)."},"time":{"type":"string","description":"ISO 8601 datetime for one-time jobs or cron start time"},"timezone":{"type":"string","description":"IANA timezone (e.g. America/New_York). Defaults to UTC."},"title":{"type":"string","description":"Short descriptive title for the job (e.g. 'Email Poller')"}}},"operation":{"type":"string","description":"The operation to perform: create, list, get, update, delete","enum":["create","list","get","update","delete"]}},"required":["operation"]}, }; export const ManageMcpTool: ToolCatalogEntry = { @@ -391,6 +433,7 @@ export const ManageMcpTool: ToolCatalogEntry = { name: "manage_mcp_tool", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"config":{"type":"object","description":"Required for add and edit. The MCP server configuration.","properties":{"enabled":{"type":"boolean","description":"Whether the server is enabled (default: true)"},"headers":{"type":"object","description":"Optional HTTP headers to send with requests (key-value pairs)"},"name":{"type":"string","description":"Display name for the MCP server"},"timeout":{"type":"number","description":"Request timeout in milliseconds (default: 30000)"},"transport":{"type":"string","description":"Transport protocol: 'streamable-http' or 'sse'","enum":["streamable-http","sse"],"default":"streamable-http"},"url":{"type":"string","description":"The MCP server endpoint URL (required for add)"}}},"operation":{"type":"string","description":"The operation to perform: 'add', 'edit', 'list', or 'delete'","enum":["add","edit","delete","list"]},"serverId":{"type":"string","description":"Required for edit and delete. The database ID of the MCP server. DO NOT PROVIDE if operation is 'add' or 'list'."}},"required":["operation"]}, requiresConfirmation: true, requiredPermission: "write", }; @@ -400,6 +443,7 @@ export const ManageSkill: ToolCatalogEntry = { name: "manage_skill", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"content":{"type":"string","description":"Markdown instructions for the skill. Required for add, optional for edit."},"description":{"type":"string","description":"Short description of the skill. Required for add, optional for edit."},"name":{"type":"string","description":"Skill name in kebab-case (e.g. 'my-skill'). Required for add, optional for edit."},"operation":{"type":"string","description":"The operation to perform: 'add', 'edit', 'list', or 'delete'","enum":["add","edit","delete","list"]},"skillId":{"type":"string","description":"The ID of the skill (required for edit/delete). Must be the exact ID from the VFS or list. DO NOT PROVIDE if operation is 'add' or 'list'."}},"required":["operation"]}, requiresConfirmation: true, requiredPermission: "write", }; @@ -409,6 +453,7 @@ export const MaterializeFile: ToolCatalogEntry = { name: "materialize_file", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"fileName":{"type":"string","description":"The name of the uploaded file to materialize (e.g. \"report.pdf\")"},"knowledgeBaseId":{"type":"string","description":"ID of an existing knowledge base to add the file to (only used with operation \"knowledge_base\"). If omitted, a new KB is created."},"operation":{"type":"string","description":"What to do with the file. \"save\" promotes it to files/. \"import\" imports a workflow JSON. \"table\" converts CSV/TSV/JSON to a table. \"knowledge_base\" saves and adds to a KB. Defaults to \"save\".","enum":["save","import","table","knowledge_base"],"default":"save"},"tableName":{"type":"string","description":"Custom name for the table (only used with operation \"table\"). Defaults to the file name without extension."}},"required":["fileName"]}, requiredPermission: "write", }; @@ -417,6 +462,7 @@ export const MoveFolder: ToolCatalogEntry = { name: "move_folder", executor: "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", }; @@ -425,6 +471,7 @@ export const MoveWorkflow: ToolCatalogEntry = { name: "move_workflow", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"folderId":{"type":"string","description":"Target folder ID. Omit or pass empty string to move to workspace root."},"workflowId":{"type":"string","description":"The workflow ID to move."}},"required":["workflowId"]}, requiredPermission: "write", }; @@ -433,6 +480,7 @@ export const OauthGetAuthLink: ToolCatalogEntry = { name: "oauth_get_auth_link", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"providerName":{"type":"string","description":"The name of the OAuth provider to connect (e.g., 'Slack', 'Gmail', 'Google Calendar', 'GitHub')"}},"required":["providerName"]}, }; export const OauthRequestAccess: ToolCatalogEntry = { @@ -440,6 +488,7 @@ export const OauthRequestAccess: ToolCatalogEntry = { name: "oauth_request_access", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"providerName":{"type":"string","description":"The name of the OAuth provider to connect (e.g., 'Slack', 'Gmail', 'Google Calendar')"}},"required":["providerName"]}, requiresConfirmation: true, }; @@ -448,6 +497,7 @@ export const OpenResource: ToolCatalogEntry = { name: "open_resource", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"id":{"type":"string","description":"The resource ID to open."},"type":{"type":"string","description":"The resource type to open.","enum":["workflow","table","knowledgebase","file"]}},"required":["type","id"]}, }; export const Read: ToolCatalogEntry = { @@ -455,6 +505,7 @@ export const Read: ToolCatalogEntry = { name: "read", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"limit":{"type":"number","description":"Maximum number of lines to read."},"offset":{"type":"number","description":"Line offset to start reading from (0-indexed)."},"outputTable":{"type":"string","description":"Table ID to import the file contents into (CSV/JSON). All existing rows are replaced. Example: \"tbl_abc123\""},"path":{"type":"string","description":"Path to the file to read (e.g. 'workflows/My Workflow/state.json')."}},"required":["path"]}, }; export const Redeploy: ToolCatalogEntry = { @@ -462,6 +513,7 @@ export const Redeploy: ToolCatalogEntry = { name: "redeploy", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"workflowId":{"type":"string","description":"Workflow ID to redeploy (required in workspace context)"}}}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -471,6 +523,7 @@ export const RenameWorkflow: ToolCatalogEntry = { name: "rename_workflow", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"name":{"type":"string","description":"The new name for the workflow."},"workflowId":{"type":"string","description":"The workflow ID to rename."}},"required":["workflowId","name"]}, requiredPermission: "write", }; @@ -479,6 +532,7 @@ export const Research: ToolCatalogEntry = { name: "research", executor: "subagent", mode: "async", + parameters: {"properties":{"topic":{"description":"The topic to research.","type":"string"}},"required":["topic"],"type":"object"}, subagentId: "research", internal: true, }; @@ -488,6 +542,7 @@ export const Respond: ToolCatalogEntry = { name: "respond", executor: "sim", mode: "async", + parameters: {"additionalProperties":true,"properties":{"output":{"description":"The result — facts, status, VFS paths to persisted data, whatever the caller needs to act on.","type":"string"},"success":{"description":"Whether the task completed successfully","type":"boolean"},"type":{"description":"Optional logical result type override","type":"string"}},"required":["output","success"],"type":"object"}, internal: true, hidden: true, }; @@ -497,6 +552,7 @@ export const RevertToVersion: ToolCatalogEntry = { name: "revert_to_version", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"version":{"type":"number","description":"The deployment version number to revert to"},"workflowId":{"type":"string","description":"The workflow ID"}},"required":["workflowId","version"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -506,6 +562,7 @@ export const Run: ToolCatalogEntry = { name: "run", executor: "subagent", mode: "async", + parameters: {"properties":{"context":{"description":"Pre-gathered context: workflow state, block IDs, input requirements.","type":"string"},"request":{"description":"What to run or what logs to check.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "run", internal: true, }; @@ -515,6 +572,7 @@ export const RunBlock: ToolCatalogEntry = { name: "run_block", executor: "client", mode: "async", + parameters: {"type":"object","properties":{"blockId":{"type":"string","description":"The block ID to run in isolation."},"executionId":{"type":"string","description":"Optional execution ID to load the snapshot from. Uses latest execution if omitted."},"useDeployedState":{"type":"boolean","description":"When true, runs the deployed version instead of the live draft. Default: false (draft)."},"workflowId":{"type":"string","description":"Optional workflow ID to run. If not provided, uses the current workflow in context."},"workflow_input":{"type":"object","description":"JSON object with key-value mappings where each key is an input field name"}},"required":["blockId"]}, clientExecutable: true, requiresConfirmation: true, }; @@ -524,6 +582,7 @@ export const RunFromBlock: ToolCatalogEntry = { name: "run_from_block", executor: "client", mode: "async", + parameters: {"type":"object","properties":{"executionId":{"type":"string","description":"Optional execution ID to load the snapshot from. Uses latest execution if omitted."},"startBlockId":{"type":"string","description":"The block ID to start execution from."},"useDeployedState":{"type":"boolean","description":"When true, runs the deployed version instead of the live draft. Default: false (draft)."},"workflowId":{"type":"string","description":"Optional workflow ID to run. If not provided, uses the current workflow in context."},"workflow_input":{"type":"object","description":"JSON object with key-value mappings where each key is an input field name"}},"required":["startBlockId"]}, clientExecutable: true, requiresConfirmation: true, }; @@ -533,6 +592,7 @@ export const RunWorkflow: ToolCatalogEntry = { name: "run_workflow", executor: "client", mode: "async", + parameters: {"type":"object","properties":{"useDeployedState":{"type":"boolean","description":"When true, runs the deployed version instead of the live draft. Default: false (draft)."},"workflowId":{"type":"string","description":"Optional workflow ID to run. If not provided, uses the current workflow in context."},"workflow_input":{"type":"object","description":"JSON object with key-value mappings where each key is an input field name"}},"required":["workflow_input"]}, clientExecutable: true, requiresConfirmation: true, }; @@ -542,6 +602,7 @@ export const RunWorkflowUntilBlock: ToolCatalogEntry = { name: "run_workflow_until_block", executor: "client", mode: "async", + parameters: {"type":"object","properties":{"stopAfterBlockId":{"type":"string","description":"The block ID to stop after. Execution halts once this block completes."},"useDeployedState":{"type":"boolean","description":"When true, runs the deployed version instead of the live draft. Default: false (draft)."},"workflowId":{"type":"string","description":"Optional workflow ID to run. If not provided, uses the current workflow in context."},"workflow_input":{"type":"object","description":"JSON object with key-value mappings where each key is an input field name"}},"required":["stopAfterBlockId"]}, clientExecutable: true, requiresConfirmation: true, }; @@ -551,6 +612,7 @@ export const ScrapePage: ToolCatalogEntry = { name: "scrape_page", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"include_links":{"type":"boolean","description":"Extract all links from the page (default false)"},"url":{"type":"string","description":"The URL to scrape (must include https://)"},"wait_for":{"type":"string","description":"CSS selector to wait for before scraping (for JS-heavy pages)"}},"required":["url"]}, }; export const SearchDocumentation: ToolCatalogEntry = { @@ -558,6 +620,7 @@ export const SearchDocumentation: ToolCatalogEntry = { name: "search_documentation", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"query":{"type":"string","description":"The search query"},"topK":{"type":"number","description":"Number of results (max 10)"}},"required":["query"]}, }; export const SearchLibraryDocs: ToolCatalogEntry = { @@ -565,6 +628,7 @@ export const SearchLibraryDocs: ToolCatalogEntry = { name: "search_library_docs", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"library_name":{"type":"string","description":"Name of the library to search for (e.g., 'nextjs', 'stripe', 'langchain')"},"query":{"type":"string","description":"The question or topic to find documentation for - be specific"},"version":{"type":"string","description":"Specific version (optional, e.g., '14', 'v2')"}},"required":["library_name","query"]}, }; export const SearchOnline: ToolCatalogEntry = { @@ -572,6 +636,7 @@ export const SearchOnline: ToolCatalogEntry = { name: "search_online", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"category":{"type":"string","description":"Filter by category","enum":["news","tweet","github","paper","company","research paper","linkedin profile","pdf","personal site"]},"include_text":{"type":"boolean","description":"Include page text content (default true)"},"num_results":{"type":"number","description":"Number of results (default 10, max 25)"},"query":{"type":"string","description":"Natural language search query"}},"required":["query"]}, }; export const SearchPatterns: ToolCatalogEntry = { @@ -579,6 +644,7 @@ export const SearchPatterns: ToolCatalogEntry = { name: "search_patterns", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"limit":{"type":"integer","description":"Maximum number of unique pattern examples to return (defaults to 3)."},"queries":{"type":"array","description":"Up to 3 descriptive strings explaining the workflow pattern(s) you need. Focus on intent and desired outcomes.","items":{"type":"string","description":"Example: \"how to automate wealthbox meeting notes into follow-up tasks\""}}},"required":["queries"]}, }; export const SetEnvironmentVariables: ToolCatalogEntry = { @@ -586,23 +652,17 @@ export const SetEnvironmentVariables: ToolCatalogEntry = { name: "set_environment_variables", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"variables":{"type":"array","description":"List of env vars to set","items":{"type":"object","properties":{"name":{"type":"string","description":"Variable name"},"value":{"type":"string","description":"Variable value"}},"required":["name","value"]}}},"required":["variables"]}, requiresConfirmation: true, requiredPermission: "write", }; -export const SetFileContext: ToolCatalogEntry = { - id: "set_file_context", - name: "set_file_context", - executor: "sim", - mode: "async", - hidden: true, -}; - export const SetGlobalWorkflowVariables: ToolCatalogEntry = { id: "set_global_workflow_variables", name: "set_global_workflow_variables", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"operations":{"type":"array","description":"List of operations to apply","items":{"type":"object","properties":{"name":{"type":"string"},"operation":{"type":"string","enum":["add","delete","edit"]},"type":{"type":"string","enum":["plain","number","boolean","array","object"]},"value":{"type":"string"}},"required":["operation","name","type","value"]}},"workflowId":{"type":"string","description":"Optional workflow ID. If not provided, uses the current workflow in context."}},"required":["operations"]}, requiresConfirmation: true, requiredPermission: "write", }; @@ -612,6 +672,7 @@ export const Superagent: ToolCatalogEntry = { name: "superagent", executor: "subagent", mode: "async", + parameters: {"properties":{"task":{"description":"A single sentence — the agent has full conversation context. Do NOT pre-read credentials or look up configs. Example: 'send the email we discussed' or 'check my calendar for tomorrow'.","type":"string"}},"required":["task"],"type":"object"}, subagentId: "superagent", internal: true, }; @@ -621,6 +682,7 @@ export const Table: ToolCatalogEntry = { name: "table", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"What table action is needed.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "table", internal: true, }; @@ -630,6 +692,7 @@ export const ToolSearchToolRegex: ToolCatalogEntry = { name: "tool_search_tool_regex", executor: "sim", mode: "async", + parameters: {"properties":{"case_insensitive":{"description":"Whether the regex should be case-insensitive (default true).","type":"boolean"},"max_results":{"description":"Maximum number of tools to return (optional).","type":"integer"},"pattern":{"description":"Regular expression to match tool names or descriptions.","type":"string"}},"required":["pattern"],"type":"object"}, }; export const UpdateJobHistory: ToolCatalogEntry = { @@ -637,6 +700,7 @@ export const UpdateJobHistory: ToolCatalogEntry = { name: "update_job_history", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"jobId":{"type":"string","description":"The job ID."},"summary":{"type":"string","description":"A concise summary of what was done this run (e.g., 'Sent follow-up emails to 3 leads: Alice, Bob, Carol')."}},"required":["jobId","summary"]}, }; export const UpdateWorkspaceMcpServer: ToolCatalogEntry = { @@ -644,6 +708,7 @@ export const UpdateWorkspaceMcpServer: ToolCatalogEntry = { name: "update_workspace_mcp_server", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"description":{"type":"string","description":"New description for the server"},"isPublic":{"type":"boolean","description":"Whether the server is publicly accessible"},"name":{"type":"string","description":"New name for the server"},"serverId":{"type":"string","description":"Required: the MCP server ID to update"}},"required":["serverId"]}, requiresConfirmation: true, requiredPermission: "admin", }; @@ -653,6 +718,7 @@ export const UserMemory: ToolCatalogEntry = { name: "user_memory", executor: "go", mode: "sync", + parameters: {"type":"object","properties":{"confidence":{"type":"number","description":"Confidence level 0-1 (default 1.0 for explicit, 0.8 for inferred)"},"correct_value":{"type":"string","description":"The correct value to replace the wrong one (for 'correct' operation)"},"key":{"type":"string","description":"Unique key for the memory (e.g., 'preferred_model', 'slack_credential')"},"limit":{"type":"number","description":"Number of results for search (default 10)"},"memory_type":{"type":"string","description":"Type of memory: 'preference', 'entity', 'history', or 'correction'","enum":["preference","entity","history","correction"]},"operation":{"type":"string","description":"Operation: 'add', 'search', 'delete', 'correct', or 'list'","enum":["add","search","delete","correct","list"]},"query":{"type":"string","description":"Search query to find relevant memories"},"source":{"type":"string","description":"Source: 'explicit' (user told you) or 'inferred' (you observed)","enum":["explicit","inferred"]},"value":{"type":"string","description":"Value to remember"}},"required":["operation"]}, }; export const UserTable: ToolCatalogEntry = { @@ -660,6 +726,8 @@ export const UserTable: ToolCatalogEntry = { name: "user_table", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"args":{"type":"object","description":"Arguments for the operation","properties":{"column":{"type":"object","description":"Column definition for add_column: { name, type, unique?, position? }"},"columnName":{"type":"string","description":"Column name (required for rename_column, update_column; use columnNames array for batch delete_column)"},"columnNames":{"type":"array","description":"Array of column names to delete at once (for delete_column). Preferred over columnName when deleting multiple columns."},"data":{"type":"object","description":"Row data as key-value pairs (required for insert_row, update_row)"},"description":{"type":"string","description":"Table description (optional for 'create')"},"fileId":{"type":"string","description":"Canonical workspace file ID for create_from_file/import_file. Discover via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\")."},"filePath":{"type":"string","description":"Legacy workspace file reference for create_from_file/import_file. Prefer fileId."},"filter":{"type":"object","description":"MongoDB-style filter for query_rows, update_rows_by_filter, delete_rows_by_filter"},"limit":{"type":"number","description":"Maximum rows to return or affect (optional, default 100)"},"name":{"type":"string","description":"Table name (required for 'create')"},"newName":{"type":"string","description":"New column name (required for rename_column)"},"newType":{"type":"string","description":"New column type (optional for update_column). Types: string, number, boolean, date, json"},"offset":{"type":"number","description":"Number of rows to skip (optional for query_rows, default 0)"},"outputFormat":{"type":"string","description":"Explicit format override for outputPath. Usually unnecessary — the file extension determines the format automatically. Only use this to force a different format than what the extension implies.","enum":["json","csv","txt","md","html"]},"outputPath":{"type":"string","description":"Pipe query_rows results directly to a NEW workspace file. The format is auto-inferred from the file extension: .csv → CSV, .json → JSON, .md → Markdown, etc. Use .csv for tabular exports. Use a flat path like \"files/export.csv\" — nested paths are not supported."},"rowId":{"type":"string","description":"Row ID (required for get_row, update_row, delete_row)"},"rowIds":{"type":"array","description":"Array of row IDs to delete (for batch_delete_rows)"},"rows":{"type":"array","description":"Array of row data objects (required for batch_insert_rows)"},"schema":{"type":"object","description":"Table schema with columns array (required for 'create'). Each column: { name, type, unique? }"},"sort":{"type":"object","description":"Sort specification as { field: 'asc' | 'desc' } (optional for query_rows)"},"tableId":{"type":"string","description":"Table ID (required for most operations except 'create')"},"unique":{"type":"boolean","description":"Set column unique constraint (optional for update_column)"},"updates":{"type":"array","description":"Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows)"},"values":{"type":"object","description":"Map of rowId to value for single-column batch update: { \"rowId1\": val1, \"rowId2\": val2 } (for batch_update_rows with columnName)"}}},"operation":{"type":"string","description":"The operation to perform","enum":["create","create_from_file","import_file","get","get_schema","delete","insert_row","batch_insert_rows","get_row","query_rows","update_row","delete_row","update_rows_by_filter","delete_rows_by_filter","batch_update_rows","batch_delete_rows","add_column","rename_column","delete_column","update_column"]}},"required":["operation","args"]}, + resultSchema: {"type":"object","properties":{"data":{"type":"object","description":"Operation-specific result payload."},"message":{"type":"string","description":"Human-readable outcome summary."},"success":{"type":"boolean","description":"Whether the operation succeeded."}},"required":["success","message"]}, requiresConfirmation: true, }; @@ -668,6 +736,7 @@ export const Workflow: ToolCatalogEntry = { name: "workflow", executor: "subagent", mode: "async", + parameters: {"properties":{"request":{"description":"A single sentence — the agent has full conversation context and VFS access. Do NOT look up IDs or pre-read data; the workflow agent does its own research. Example: 'move all the return letter workflows into a folder called Letters'.","type":"string"}},"required":["request"],"type":"object"}, subagentId: "workflow", internal: true, }; @@ -677,6 +746,8 @@ export const WorkspaceFile: ToolCatalogEntry = { name: "workspace_file", executor: "sim", mode: "async", + parameters: {"type":"object","properties":{"operation":{"type":"string","description":"The file operation to perform.","enum":["create","append","update","patch","rename","delete"]},"target":{"type":"object","description":"Explicit file target. Use kind=new_file + fileName for create. Use kind=file_id + fileId for append, update, patch, rename, and delete. Emit target keys in this order: kind, fileId, fileName.","properties":{"kind":{"type":"string","description":"How the file target is identified.","enum":["new_file","file_id"]},"fileId":{"type":"string","description":"Canonical existing workspace file ID. Required when target.kind=file_id."},"fileName":{"type":"string","description":"Plain workspace filename including extension, e.g. \"main.py\" or \"report.docx\". Required when target.kind=new_file."}},"required":["kind"]},"title":{"type":"string","description":"Optional short UI label for create/append chunks, e.g. \"Chapter 1\" or \"Slide 3\"."},"contentType":{"type":"string","description":"Optional MIME type override. Usually omit and let the system infer from the target file extension.","enum":["text/markdown","text/html","text/plain","application/json","text/csv","application/vnd.openxmlformats-officedocument.presentationml.presentation","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/pdf"]},"edit":{"type":"object","description":"Patch metadata. Use strategy=search_replace for exact text replacement, or strategy=anchored for line-based inserts/replacements/deletions. Emit edit keys in this order: strategy, search, replace, replaceAll, mode, occurrence, before_anchor, after_anchor, anchor, start_anchor, end_anchor, content.","properties":{"strategy":{"type":"string","description":"Patch strategy.","enum":["search_replace","anchored"]},"search":{"type":"string","description":"Exact text to find when strategy=search_replace. Must match exactly once unless replaceAll=true."},"replace":{"type":"string","description":"Replacement text when strategy=search_replace."},"replaceAll":{"type":"boolean","description":"When true and strategy=search_replace, replace every match instead of requiring a unique single match."},"mode":{"type":"string","description":"Anchored edit mode when strategy=anchored.","enum":["replace_between","insert_after","delete_between"]},"occurrence":{"type":"number","description":"1-based occurrence for repeated anchor lines. Optional; defaults to 1."},"before_anchor":{"type":"string","description":"Boundary line kept before inserted replacement content. Required for mode=replace_between."},"after_anchor":{"type":"string","description":"Boundary line kept after inserted replacement content. Required for mode=replace_between."},"anchor":{"type":"string","description":"Anchor line after which new content is inserted. Required for mode=insert_after."},"start_anchor":{"type":"string","description":"First line to delete. Required for mode=delete_between."},"end_anchor":{"type":"string","description":"First line to keep after deletion. Required for mode=delete_between."},"content":{"type":"string","description":"Inserted or replacement content for anchored edits. Not used for delete_between."}}},"newName":{"type":"string","description":"New file name for rename. Must be a plain workspace filename like \"main.py\"."},"content":{"type":"string","description":"File content for create, append, or update. For .pptx/.docx/.pdf this must be JavaScript source code for the corresponding generator runtime."}},"required":["operation","target"]}, + resultSchema: {"type":"object","properties":{"data":{"type":"object","description":"Optional operation metadata such as file id, file name, size, and content type."},"message":{"type":"string","description":"Human-readable summary of the outcome."},"success":{"type":"boolean","description":"Whether the file operation succeeded."}},"required":["success","message"]}, requiredPermission: "write", }; @@ -687,7 +758,6 @@ export const TOOL_CATALOG: Record = { [CompleteJob.id]: CompleteJob, [ContextWrite.id]: ContextWrite, [CrawlWebsite.id]: CrawlWebsite, - [CreateFile.id]: CreateFile, [CreateFolder.id]: CreateFolder, [CreateJob.id]: CreateJob, [CreateWorkflow.id]: CreateWorkflow, @@ -753,7 +823,6 @@ export const TOOL_CATALOG: Record = { [SearchOnline.id]: SearchOnline, [SearchPatterns.id]: SearchPatterns, [SetEnvironmentVariables.id]: SetEnvironmentVariables, - [SetFileContext.id]: SetFileContext, [SetGlobalWorkflowVariables.id]: SetGlobalWorkflowVariables, [Superagent.id]: Superagent, [Table.id]: Table, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts new file mode 100644 index 0000000000..c4b5e27484 --- /dev/null +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -0,0 +1,2605 @@ +// AUTO-GENERATED FILE. DO NOT EDIT. +// Generated from copilot/contracts/tool-catalog-v1.json +// + +export type JsonSchema = unknown + +export interface ToolRuntimeSchemaEntry { + parameters?: JsonSchema; + resultSchema?: JsonSchema; +} + +export const TOOL_RUNTIME_SCHEMAS: Record = { + ["agent"]: { + parameters: { + "properties": { + "request": { + "description": "What tool/skill/MCP action is needed.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["auth"]: { + parameters: { + "properties": { + "request": { + "description": "What authentication/credential action is needed.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["check_deployment_status"]: { + parameters: { + "type": "object", + "properties": { + "workflowId": { + "type": "string", + "description": "Workflow ID to check (defaults to current workflow)" + } + } + }, + resultSchema: undefined, + }, + ["complete_job"]: { + parameters: { + "type": "object", + "properties": { + "jobId": { + "type": "string", + "description": "The ID of the job to mark as completed." + } + }, + "required": [ + "jobId" + ] + }, + resultSchema: undefined, + }, + ["context_write"]: { + parameters: { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Full content to write to the file (replaces existing content)" + }, + "file_path": { + "type": "string", + "description": "Path of the file to write (e.g. 'SESSION.md')" + } + }, + "required": [ + "file_path", + "content" + ] + }, + resultSchema: undefined, + }, + ["crawl_website"]: { + parameters: { + "type": "object", + "properties": { + "exclude_paths": { + "type": "array", + "description": "Skip URLs matching these patterns", + "items": { + "type": "string" + } + }, + "include_paths": { + "type": "array", + "description": "Only crawl URLs matching these patterns", + "items": { + "type": "string" + } + }, + "limit": { + "type": "number", + "description": "Maximum pages to crawl (default 10, max 50)" + }, + "max_depth": { + "type": "number", + "description": "How deep to follow links (default 2)" + }, + "url": { + "type": "string", + "description": "Starting URL to crawl from" + } + }, + "required": [ + "url" + ] + }, + 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_job"]: { + parameters: { + "type": "object", + "properties": { + "cron": { + "type": "string", + "description": "Cron expression for recurring jobs (e.g., '*/5 * * * *' for every 5 minutes, '0 9 * * *' for daily at 9 AM). Omit for one-time jobs." + }, + "lifecycle": { + "type": "string", + "description": "'persistent' (default) or 'until_complete'. Until_complete jobs stop when complete_job is called after the success condition is met.", + "enum": [ + "persistent", + "until_complete" + ] + }, + "maxRuns": { + "type": "integer", + "description": "Maximum number of executions before the job auto-completes. Safety limit to prevent runaway polling." + }, + "prompt": { + "type": "string", + "description": "The prompt to execute when the job fires. This is sent to the Mothership as a user message." + }, + "successCondition": { + "type": "string", + "description": "What must happen for the job to be considered complete. Used with until_complete lifecycle (e.g., 'John has replied to the partnership email')." + }, + "time": { + "type": "string", + "description": "ISO 8601 datetime for one-time execution or as the start time for a cron schedule (e.g., '2026-03-06T09:00:00'). Include timezone offset or use the timezone parameter." + }, + "timezone": { + "type": "string", + "description": "IANA timezone for the schedule (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC." + }, + "title": { + "type": "string", + "description": "A short, descriptive title for the job (e.g., 'Email Poller', 'Daily Report'). Used as the display name." + } + }, + "required": [ + "title", + "prompt" + ] + }, + resultSchema: undefined, + }, + ["create_workflow"]: { + parameters: { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Optional workflow description." + }, + "folderId": { + "type": "string", + "description": "Optional folder ID." + }, + "name": { + "type": "string", + "description": "Workflow name." + }, + "workspaceId": { + "type": "string", + "description": "Optional workspace ID." + } + }, + "required": [ + "name" + ] + }, + resultSchema: undefined, + }, + ["create_workspace_mcp_server"]: { + parameters: { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Optional description for the server" + }, + "name": { + "type": "string", + "description": "Required: server name" + }, + "workspaceId": { + "type": "string", + "description": "Workspace ID (defaults to current workspace)" + } + }, + "required": [ + "name" + ] + }, + resultSchema: undefined, + }, + ["debug"]: { + parameters: { + "properties": { + "context": { + "description": "Pre-gathered context: workflow state JSON, block schemas, error logs. The debug agent will skip re-reading anything included here.", + "type": "string" + }, + "request": { + "description": "What to debug. Include error messages, block IDs, and any context about the failure.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["delete_folder"]: { + parameters: { + "type": "object", + "properties": { + "folderId": { + "type": "string", + "description": "The folder ID to delete." + } + }, + "required": [ + "folderId" + ] + }, + resultSchema: undefined, + }, + ["delete_workflow"]: { + parameters: { + "type": "object", + "properties": { + "workflowId": { + "type": "string", + "description": "The workflow ID to delete." + } + }, + "required": [ + "workflowId" + ] + }, + resultSchema: undefined, + }, + ["delete_workspace_mcp_server"]: { + parameters: { + "type": "object", + "properties": { + "serverId": { + "type": "string", + "description": "Required: the MCP server ID to delete" + } + }, + "required": [ + "serverId" + ] + }, + resultSchema: undefined, + }, + ["deploy"]: { + parameters: { + "properties": { + "request": { + "description": "Detailed deployment instructions. Include deployment type (api/chat) and ALL user-specified options: identifier, title, description, authType, password, allowedEmails, welcomeMessage, outputConfigs (block outputs to display).", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["deploy_api"]: { + parameters: { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Whether to deploy or undeploy the API endpoint", + "enum": [ + "deploy", + "undeploy" + ], + "default": "deploy" + }, + "workflowId": { + "type": "string", + "description": "Workflow ID to deploy (required in workspace context)" + } + } + }, + resultSchema: undefined, + }, + ["deploy_chat"]: { + parameters: { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Whether to deploy or undeploy the chat interface", + "enum": [ + "deploy", + "undeploy" + ], + "default": "deploy" + }, + "allowedEmails": { + "type": "array", + "description": "List of allowed emails/domains for email or SSO auth", + "items": { + "type": "string" + } + }, + "authType": { + "type": "string", + "description": "Authentication type: public, password, email, or sso", + "enum": [ + "public", + "password", + "email", + "sso" + ], + "default": "public" + }, + "description": { + "type": "string", + "description": "Optional description for the chat" + }, + "identifier": { + "type": "string", + "description": "URL slug for the chat (lowercase letters, numbers, hyphens only)" + }, + "outputConfigs": { + "type": "array", + "description": "Output configurations specifying which block outputs to display in chat", + "items": { + "type": "object", + "properties": { + "blockId": { + "type": "string", + "description": "The block UUID" + }, + "path": { + "type": "string", + "description": "The output path (e.g. 'response', 'response.content')" + } + }, + "required": [ + "blockId", + "path" + ] + } + }, + "password": { + "type": "string", + "description": "Password for password-protected chats" + }, + "title": { + "type": "string", + "description": "Display title for the chat interface" + }, + "welcomeMessage": { + "type": "string", + "description": "Welcome message shown to users" + }, + "workflowId": { + "type": "string", + "description": "Workflow ID to deploy (required in workspace context)" + } + } + }, + resultSchema: undefined, + }, + ["deploy_mcp"]: { + parameters: { + "type": "object", + "properties": { + "parameterDescriptions": { + "type": "array", + "description": "Array of parameter descriptions for the tool", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Parameter description" + }, + "name": { + "type": "string", + "description": "Parameter name" + } + }, + "required": [ + "name", + "description" + ] + } + }, + "serverId": { + "type": "string", + "description": "Required: server ID from list_workspace_mcp_servers" + }, + "toolDescription": { + "type": "string", + "description": "Description for the MCP tool" + }, + "toolName": { + "type": "string", + "description": "Name for the MCP tool (defaults to workflow name)" + }, + "workflowId": { + "type": "string", + "description": "Workflow ID (defaults to active workflow)" + } + }, + "required": [ + "serverId" + ] + }, + resultSchema: undefined, + }, + ["download_to_workspace_file"]: { + parameters: { + "type": "object", + "properties": { + "fileName": { + "type": "string", + "description": "Optional workspace file name to save as. If omitted, the name is inferred from the response or URL." + }, + "url": { + "type": "string", + "description": "Direct URL of the file to download, such as an image CDN URL ending in .png or .jpg" + } + }, + "required": [ + "url" + ] + }, + resultSchema: undefined, + }, + ["edit_workflow"]: { + parameters: { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "Array of edit operations", + "items": { + "type": "object", + "properties": { + "block_id": { + "type": "string", + "description": "Block ID for the operation. For add operations, this will be the desired ID for the new block." + }, + "operation_type": { + "type": "string", + "description": "Type of operation to perform", + "enum": [ + "add", + "edit", + "delete", + "insert_into_subflow", + "extract_from_subflow" + ] + }, + "params": { + "type": "object", + "description": "Parameters for the operation. \nFor edit: {\"inputs\": {\"temperature\": 0.5}} NOT {\"subBlocks\": {\"temperature\": {\"value\": 0.5}}}\nFor add: {\"type\": \"agent\", \"name\": \"My Agent\", \"inputs\": {\"model\": \"gpt-4o\"}}\nFor delete: {} (empty object)" + } + }, + "required": [ + "operation_type", + "block_id", + "params" + ] + } + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID to edit. If not provided, uses the current workflow in context." + } + }, + "required": [ + "operations" + ] + }, + resultSchema: undefined, + }, + ["file"]: { + parameters: { + "type": "object" + }, + resultSchema: undefined, + }, + ["function_execute"]: { + parameters: { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with access to pre-installed CLI tools and workspace env vars as $VAR_NAME." + }, + "inputFiles": { + "type": "array", + "description": "Canonical workspace file IDs to mount in the sandbox. Discover IDs via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\"). Mounted path: /home/user/files/{fileId}/{originalName}. Example: [\"wf_123\"]", + "items": { + "type": "string" + } + }, + "inputTables": { + "type": "array", + "description": "Table IDs to mount as CSV files in the sandbox. Each table appears at /home/user/tables/{tableId}.csv with a header row. Example: [\"tbl_abc123\"]", + "items": { + "type": "string" + } + }, + "language": { + "type": "string", + "description": "Execution language.", + "enum": [ + "javascript", + "python", + "shell" + ] + }, + "outputFormat": { + "type": "string", + "description": "Format for outputPath. Determines how the code result is serialized. If omitted, inferred from outputPath file extension.", + "enum": [ + "json", + "csv", + "txt", + "md", + "html" + ] + }, + "outputMimeType": { + "type": "string", + "description": "MIME type for outputSandboxPath export. Required for binary files: image/png, image/jpeg, application/pdf, etc. Omit for text files." + }, + "outputPath": { + "type": "string", + "description": "Pipe output directly to a NEW workspace file instead of returning in context. ALWAYS use this instead of a separate workspace_file write call. Use a flat path like \"files/result.json\" — nested paths are not supported." + }, + "outputSandboxPath": { + "type": "string", + "description": "Path to a file created inside the sandbox that should be exported to the workspace. Use together with outputPath." + }, + "outputTable": { + "type": "string", + "description": "Table ID to overwrite with the code's return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: \"tbl_abc123\"" + } + }, + "required": [ + "code" + ] + }, + resultSchema: undefined, + }, + ["generate_api_key"]: { + parameters: { + "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" + ] + }, + resultSchema: undefined, + }, + ["generate_image"]: { + parameters: { + "type": "object", + "properties": { + "aspectRatio": { + "type": "string", + "description": "Aspect ratio for the generated image.", + "enum": [ + "1:1", + "16:9", + "9:16", + "4:3", + "3:4" + ] + }, + "fileName": { + "type": "string", + "description": "Output file name. Defaults to \"generated-image.png\". Workspace files are flat, so pass a plain file name, not a nested path." + }, + "overwriteFileId": { + "type": "string", + "description": "If provided, overwrites the existing workspace file with this ID instead of creating a new file. Use this when the user asks to update or redo a previously generated image. The file ID is returned by previous generate_image or generate_visualization calls (fileId field), or can be found via read(\"files/by-id/{fileId}/meta.json\")." + }, + "prompt": { + "type": "string", + "description": "Detailed text description of the image to generate, or editing instructions when used with editFileId." + }, + "referenceFileIds": { + "type": "array", + "description": "File IDs of workspace images to include as context for the generation. All images are sent alongside the prompt. Use for: editing a single image (1 file), compositing multiple images together (2+ files), style transfer, face swapping, etc. Order matters — list the primary/base image first.", + "items": { + "type": "string" + } + } + }, + "required": [ + "prompt" + ] + }, + resultSchema: undefined, + }, + ["generate_visualization"]: { + parameters: { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Python code that generates a visualization using matplotlib. MUST call plt.savefig('/home/user/output.png', dpi=150, bbox_inches='tight') to produce output." + }, + "fileName": { + "type": "string", + "description": "Output file name. Defaults to \"chart.png\". Workspace files are flat, so pass a plain file name, not a nested path." + }, + "inputFiles": { + "type": "array", + "description": "Canonical workspace file IDs to mount in the sandbox. Discover IDs via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\"). Mounted path: /home/user/files/{fileId}/{originalName}.", + "items": { + "type": "string" + } + }, + "inputTables": { + "type": "array", + "description": "Table IDs to mount as CSV files in the sandbox. Each table appears at /home/user/tables/{tableId}.csv with a header row. Read with pandas: pd.read_csv('/home/user/tables/tbl_xxx.csv')", + "items": { + "type": "string" + } + }, + "overwriteFileId": { + "type": "string", + "description": "If provided, overwrites the existing workspace file with this ID instead of creating a new file. Use this when the user asks to update or redo a previously generated chart. The file ID is returned by previous generate_visualization or generate_image calls (fileId field), or can be found via read(\"files/by-id/{fileId}/meta.json\")." + } + }, + "required": [ + "code" + ] + }, + resultSchema: undefined, + }, + ["get_block_outputs"]: { + parameters: { + "type": "object", + "properties": { + "blockIds": { + "type": "array", + "description": "Optional array of block UUIDs. If provided, returns outputs only for those blocks. If not provided, returns outputs for all blocks in the workflow.", + "items": { + "type": "string" + } + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If not provided, uses the current workflow in context." + } + } + }, + resultSchema: undefined, + }, + ["get_block_upstream_references"]: { + parameters: { + "type": "object", + "properties": { + "blockIds": { + "type": "array", + "description": "Required array of block UUIDs (minimum 1). Returns what each block can reference based on its position in the workflow graph.", + "items": { + "type": "string" + } + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If not provided, uses the current workflow in context." + } + }, + "required": [ + "blockIds" + ] + }, + resultSchema: undefined, + }, + ["get_deployed_workflow_state"]: { + parameters: { + "type": "object", + "properties": { + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If not provided, uses the current workflow in context." + } + } + }, + resultSchema: undefined, + }, + ["get_deployment_version"]: { + parameters: { + "type": "object", + "properties": { + "version": { + "type": "number", + "description": "The deployment version number" + }, + "workflowId": { + "type": "string", + "description": "The workflow ID" + } + }, + "required": [ + "workflowId", + "version" + ] + }, + resultSchema: undefined, + }, + ["get_execution_summary"]: { + parameters: { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "Max number of executions to return (default: 10, max: 20)." + }, + "status": { + "type": "string", + "description": "Filter by status: 'success', 'error', or 'all' (default: 'all').", + "enum": [ + "success", + "error", + "all" + ] + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If omitted, returns executions across all workflows in the workspace." + }, + "workspaceId": { + "type": "string", + "description": "Workspace ID to scope executions to." + } + }, + "required": [ + "workspaceId" + ] + }, + resultSchema: undefined, + }, + ["get_job_logs"]: { + parameters: { + "type": "object", + "properties": { + "executionId": { + "type": "string", + "description": "Optional execution ID for a specific run." + }, + "includeDetails": { + "type": "boolean", + "description": "Include tool calls, outputs, and cost details." + }, + "jobId": { + "type": "string", + "description": "The job (schedule) ID to get logs for." + }, + "limit": { + "type": "number", + "description": "Max number of entries (default: 3, max: 5)" + } + }, + "required": [ + "jobId" + ] + }, + resultSchema: undefined, + }, + ["get_page_contents"]: { + parameters: { + "type": "object", + "properties": { + "include_highlights": { + "type": "boolean", + "description": "Include key highlights (default false)" + }, + "include_summary": { + "type": "boolean", + "description": "Include AI-generated summary (default false)" + }, + "include_text": { + "type": "boolean", + "description": "Include full page text (default true)" + }, + "urls": { + "type": "array", + "description": "URLs to get content from (max 10)", + "items": { + "type": "string" + } + } + }, + "required": [ + "urls" + ] + }, + resultSchema: undefined, + }, + ["get_platform_actions"]: { + parameters: { + "type": "object", + "properties": {} + }, + resultSchema: undefined, + }, + ["get_workflow_data"]: { + parameters: { + "type": "object", + "properties": { + "data_type": { + "type": "string", + "description": "The type of workflow data to retrieve", + "enum": [ + "global_variables", + "custom_tools", + "mcp_tools", + "files" + ] + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If not provided, uses the current workflow in context." + } + }, + "required": [ + "data_type" + ] + }, + resultSchema: undefined, + }, + ["get_workflow_logs"]: { + parameters: { + "type": "object", + "properties": { + "executionId": { + "type": "string", + "description": "Optional execution ID to get logs for a specific execution. Use with get_execution_summary to find execution IDs first." + }, + "includeDetails": { + "type": "boolean", + "description": "Include detailed info" + }, + "limit": { + "type": "number", + "description": "Max number of entries (hard limit: 3)" + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If not provided, uses the current workflow in context." + } + } + }, + resultSchema: undefined, + }, + ["glob"]: { + parameters: { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Glob pattern to match file paths. Supports * (any segment) and ** (any depth)." + } + }, + "required": [ + "pattern" + ] + }, + resultSchema: undefined, + }, + ["grep"]: { + parameters: { + "type": "object", + "properties": { + "context": { + "type": "number", + "description": "Number of lines to show before and after each match. Only applies to output_mode 'content'." + }, + "ignoreCase": { + "type": "boolean", + "description": "Case insensitive search (default false)." + }, + "lineNumbers": { + "type": "boolean", + "description": "Include line numbers in output (default true). Only applies to output_mode 'content'." + }, + "maxResults": { + "type": "number", + "description": "Maximum number of matches to return (default 50)." + }, + "output_mode": { + "type": "string", + "description": "Output mode: 'content' shows matching lines (default), 'files_with_matches' shows only file paths, 'count' shows match counts per file.", + "enum": [ + "content", + "files_with_matches", + "count" + ] + }, + "path": { + "type": "string", + "description": "Optional path prefix to scope the search (e.g. 'workflows/', 'environment/', 'internal/', 'components/blocks/')." + }, + "pattern": { + "type": "string", + "description": "Regex pattern to search for in file contents." + } + }, + "required": [ + "pattern" + ] + }, + resultSchema: undefined, + }, + ["job"]: { + parameters: { + "properties": { + "request": { + "description": "What job action is needed.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["knowledge"]: { + parameters: { + "properties": { + "request": { + "description": "What knowledge base action is needed.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["knowledge_base"]: { + parameters: { + "type": "object", + "properties": { + "args": { + "type": "object", + "description": "Arguments for the operation", + "properties": { + "apiKey": { + "type": "string", + "description": "API key for API-key-based connectors (required when connector auth mode is apiKey)" + }, + "chunkingConfig": { + "type": "object", + "description": "Chunking configuration (optional for 'create')", + "properties": { + "maxSize": { + "type": "number", + "description": "Maximum chunk size (100-4000, default: 1024)", + "default": 1024 + }, + "minSize": { + "type": "number", + "description": "Minimum chunk size (1-2000, default: 1)", + "default": 1 + }, + "overlap": { + "type": "number", + "description": "Overlap between chunks (0-500, default: 200)", + "default": 200 + } + } + }, + "connectorId": { + "type": "string", + "description": "Connector ID (required for update_connector, delete_connector, sync_connector)" + }, + "connectorStatus": { + "type": "string", + "description": "Connector status (optional for update_connector)", + "enum": [ + "active", + "paused" + ] + }, + "connectorType": { + "type": "string", + "description": "Connector type from registry, e.g. 'confluence', 'google_drive', 'notion' (required for add_connector). Read knowledgebases/connectors/{type}.json for the config schema." + }, + "credentialId": { + "type": "string", + "description": "OAuth credential ID from environment/credentials.json (required for OAuth connectors)" + }, + "description": { + "type": "string", + "description": "Description of the knowledge base (optional for 'create')" + }, + "disabledTagIds": { + "type": "array", + "description": "Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema." + }, + "documentId": { + "type": "string", + "description": "Document ID (required for delete_document, update_document)" + }, + "enabled": { + "type": "boolean", + "description": "Enable/disable a document (optional for update_document)" + }, + "fileId": { + "type": "string", + "description": "Canonical workspace file ID to add as a document (preferred for add_file). Discover via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\")." + }, + "filePath": { + "type": "string", + "description": "Legacy workspace file reference for add_file. Prefer fileId." + }, + "filename": { + "type": "string", + "description": "New filename for a document (optional for update_document)" + }, + "knowledgeBaseId": { + "type": "string", + "description": "Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage)" + }, + "name": { + "type": "string", + "description": "Name of the knowledge base (required for 'create')" + }, + "query": { + "type": "string", + "description": "Search query text (required for 'query')" + }, + "sourceConfig": { + "type": "object", + "description": "Connector-specific configuration matching the configFields in knowledgebases/connectors/{type}.json" + }, + "syncIntervalMinutes": { + "type": "number", + "description": "Sync interval in minutes: 60 (hourly), 360 (6h), 1440 (daily), 10080 (weekly), 0 (manual only). Default: 1440", + "default": 1440 + }, + "tagDefinitionId": { + "type": "string", + "description": "Tag definition ID (required for update_tag, delete_tag)" + }, + "tagDisplayName": { + "type": "string", + "description": "Display name for the tag (required for create_tag, optional for update_tag)" + }, + "tagFieldType": { + "type": "string", + "description": "Field type: text, number, date, boolean (optional for create_tag, defaults to text)", + "enum": [ + "text", + "number", + "date", + "boolean" + ] + }, + "topK": { + "type": "number", + "description": "Number of results to return (1-50, default: 5)", + "default": 5 + }, + "workspaceId": { + "type": "string", + "description": "Workspace ID (required for 'create', optional filter for 'list')" + } + } + }, + "operation": { + "type": "string", + "description": "The operation to perform", + "enum": [ + "create", + "get", + "query", + "add_file", + "update", + "delete", + "delete_document", + "update_document", + "list_tags", + "create_tag", + "update_tag", + "delete_tag", + "get_tag_usage", + "add_connector", + "update_connector", + "delete_connector", + "sync_connector" + ] + } + }, + "required": [ + "operation", + "args" + ] + }, + resultSchema: { + "type": "object", + "properties": { + "data": { + "type": "object", + "description": "Operation-specific result payload." + }, + "message": { + "type": "string", + "description": "Human-readable outcome summary." + }, + "success": { + "type": "boolean", + "description": "Whether the operation succeeded." + } + }, + "required": [ + "success", + "message" + ] + }, + }, + ["list_folders"]: { + parameters: { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "Optional workspace ID to list folders for." + } + } + }, + resultSchema: undefined, + }, + ["list_user_workspaces"]: { + parameters: { + "type": "object", + "properties": {} + }, + resultSchema: undefined, + }, + ["list_workspace_mcp_servers"]: { + parameters: { + "type": "object", + "properties": { + "workspaceId": { + "type": "string", + "description": "Workspace ID (defaults to current workspace)" + } + } + }, + resultSchema: undefined, + }, + ["manage_credential"]: { + parameters: { + "type": "object", + "properties": { + "credentialId": { + "type": "string", + "description": "The credential ID (from environment/credentials.json)" + }, + "displayName": { + "type": "string", + "description": "New display name (required for rename)" + }, + "operation": { + "type": "string", + "description": "The operation to perform", + "enum": [ + "rename", + "delete" + ] + } + }, + "required": [ + "operation", + "credentialId" + ] + }, + resultSchema: undefined, + }, + ["manage_custom_tool"]: { + parameters: { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The JavaScript code that executes when the tool is called (required for add). Parameters from schema are available as variables. Function body only - no signature or wrapping braces." + }, + "operation": { + "type": "string", + "description": "The operation to perform: 'add', 'edit', 'list', or 'delete'", + "enum": [ + "add", + "edit", + "delete", + "list" + ] + }, + "schema": { + "type": "object", + "description": "The tool schema in OpenAI function calling format (required for add).", + "properties": { + "function": { + "type": "object", + "description": "The function definition", + "properties": { + "description": { + "type": "string", + "description": "What the function does" + }, + "name": { + "type": "string", + "description": "The function name (camelCase)" + }, + "parameters": { + "type": "object", + "description": "The function parameters schema", + "properties": { + "properties": { + "type": "object", + "description": "Parameter definitions as key-value pairs" + }, + "required": { + "type": "array", + "description": "Array of required parameter names", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "description": "Must be 'object'" + } + }, + "required": [ + "type", + "properties" + ] + } + }, + "required": [ + "name", + "parameters" + ] + }, + "type": { + "type": "string", + "description": "Must be 'function'" + } + }, + "required": [ + "type", + "function" + ] + }, + "toolId": { + "type": "string", + "description": "The ID of the custom tool (required for edit/delete). Must be the exact toolId from the get_workflow_data custom tool response - do not guess or construct it. DO NOT PROVIDE THE TOOL ID IF THE OPERATION IS 'ADD'." + } + }, + "required": [ + "operation" + ] + }, + resultSchema: undefined, + }, + ["manage_job"]: { + parameters: { + "type": "object", + "properties": { + "args": { + "type": "object", + "description": "Operation-specific arguments. For create: {title, prompt, cron?, time?, timezone?, lifecycle?, successCondition?, maxRuns?}. For get/delete: {jobId}. For update: {jobId, title?, prompt?, cron?, timezone?, status?, lifecycle?, successCondition?, maxRuns?}. For list: no args needed.", + "properties": { + "cron": { + "type": "string", + "description": "Cron expression for recurring jobs" + }, + "jobId": { + "type": "string", + "description": "Job ID (required for get, update, delete)" + }, + "lifecycle": { + "type": "string", + "description": "'persistent' (default) or 'until_complete'. Until_complete jobs stop when complete_job is called." + }, + "maxRuns": { + "type": "integer", + "description": "Max executions before auto-completing. Safety limit." + }, + "prompt": { + "type": "string", + "description": "The prompt to execute when the job fires" + }, + "status": { + "type": "string", + "description": "Job status: active, paused" + }, + "successCondition": { + "type": "string", + "description": "What must happen for the job to be considered complete (until_complete lifecycle)." + }, + "time": { + "type": "string", + "description": "ISO 8601 datetime for one-time jobs or cron start time" + }, + "timezone": { + "type": "string", + "description": "IANA timezone (e.g. America/New_York). Defaults to UTC." + }, + "title": { + "type": "string", + "description": "Short descriptive title for the job (e.g. 'Email Poller')" + } + } + }, + "operation": { + "type": "string", + "description": "The operation to perform: create, list, get, update, delete", + "enum": [ + "create", + "list", + "get", + "update", + "delete" + ] + } + }, + "required": [ + "operation" + ] + }, + resultSchema: undefined, + }, + ["manage_mcp_tool"]: { + parameters: { + "type": "object", + "properties": { + "config": { + "type": "object", + "description": "Required for add and edit. The MCP server configuration.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether the server is enabled (default: true)" + }, + "headers": { + "type": "object", + "description": "Optional HTTP headers to send with requests (key-value pairs)" + }, + "name": { + "type": "string", + "description": "Display name for the MCP server" + }, + "timeout": { + "type": "number", + "description": "Request timeout in milliseconds (default: 30000)" + }, + "transport": { + "type": "string", + "description": "Transport protocol: 'streamable-http' or 'sse'", + "enum": [ + "streamable-http", + "sse" + ], + "default": "streamable-http" + }, + "url": { + "type": "string", + "description": "The MCP server endpoint URL (required for add)" + } + } + }, + "operation": { + "type": "string", + "description": "The operation to perform: 'add', 'edit', 'list', or 'delete'", + "enum": [ + "add", + "edit", + "delete", + "list" + ] + }, + "serverId": { + "type": "string", + "description": "Required for edit and delete. The database ID of the MCP server. DO NOT PROVIDE if operation is 'add' or 'list'." + } + }, + "required": [ + "operation" + ] + }, + resultSchema: undefined, + }, + ["manage_skill"]: { + parameters: { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "Markdown instructions for the skill. Required for add, optional for edit." + }, + "description": { + "type": "string", + "description": "Short description of the skill. Required for add, optional for edit." + }, + "name": { + "type": "string", + "description": "Skill name in kebab-case (e.g. 'my-skill'). Required for add, optional for edit." + }, + "operation": { + "type": "string", + "description": "The operation to perform: 'add', 'edit', 'list', or 'delete'", + "enum": [ + "add", + "edit", + "delete", + "list" + ] + }, + "skillId": { + "type": "string", + "description": "The ID of the skill (required for edit/delete). Must be the exact ID from the VFS or list. DO NOT PROVIDE if operation is 'add' or 'list'." + } + }, + "required": [ + "operation" + ] + }, + resultSchema: undefined, + }, + ["materialize_file"]: { + parameters: { + "type": "object", + "properties": { + "fileName": { + "type": "string", + "description": "The name of the uploaded file to materialize (e.g. \"report.pdf\")" + }, + "knowledgeBaseId": { + "type": "string", + "description": "ID of an existing knowledge base to add the file to (only used with operation \"knowledge_base\"). If omitted, a new KB is created." + }, + "operation": { + "type": "string", + "description": "What to do with the file. \"save\" promotes it to files/. \"import\" imports a workflow JSON. \"table\" converts CSV/TSV/JSON to a table. \"knowledge_base\" saves and adds to a KB. Defaults to \"save\".", + "enum": [ + "save", + "import", + "table", + "knowledge_base" + ], + "default": "save" + }, + "tableName": { + "type": "string", + "description": "Custom name for the table (only used with operation \"table\"). Defaults to the file name without extension." + } + }, + "required": [ + "fileName" + ] + }, + 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", + "properties": { + "folderId": { + "type": "string", + "description": "Target folder ID. Omit or pass empty string to move to workspace root." + }, + "workflowId": { + "type": "string", + "description": "The workflow ID to move." + } + }, + "required": [ + "workflowId" + ] + }, + resultSchema: undefined, + }, + ["oauth_get_auth_link"]: { + parameters: { + "type": "object", + "properties": { + "providerName": { + "type": "string", + "description": "The name of the OAuth provider to connect (e.g., 'Slack', 'Gmail', 'Google Calendar', 'GitHub')" + } + }, + "required": [ + "providerName" + ] + }, + resultSchema: undefined, + }, + ["oauth_request_access"]: { + parameters: { + "type": "object", + "properties": { + "providerName": { + "type": "string", + "description": "The name of the OAuth provider to connect (e.g., 'Slack', 'Gmail', 'Google Calendar')" + } + }, + "required": [ + "providerName" + ] + }, + resultSchema: undefined, + }, + ["open_resource"]: { + parameters: { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The resource ID to open." + }, + "type": { + "type": "string", + "description": "The resource type to open.", + "enum": [ + "workflow", + "table", + "knowledgebase", + "file" + ] + } + }, + "required": [ + "type", + "id" + ] + }, + resultSchema: undefined, + }, + ["read"]: { + parameters: { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "Maximum number of lines to read." + }, + "offset": { + "type": "number", + "description": "Line offset to start reading from (0-indexed)." + }, + "outputTable": { + "type": "string", + "description": "Table ID to import the file contents into (CSV/JSON). All existing rows are replaced. Example: \"tbl_abc123\"" + }, + "path": { + "type": "string", + "description": "Path to the file to read (e.g. 'workflows/My Workflow/state.json')." + } + }, + "required": [ + "path" + ] + }, + resultSchema: undefined, + }, + ["redeploy"]: { + parameters: { + "type": "object", + "properties": { + "workflowId": { + "type": "string", + "description": "Workflow ID to redeploy (required in workspace context)" + } + } + }, + resultSchema: undefined, + }, + ["rename_workflow"]: { + parameters: { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The new name for the workflow." + }, + "workflowId": { + "type": "string", + "description": "The workflow ID to rename." + } + }, + "required": [ + "workflowId", + "name" + ] + }, + resultSchema: undefined, + }, + ["research"]: { + parameters: { + "properties": { + "topic": { + "description": "The topic to research.", + "type": "string" + } + }, + "required": [ + "topic" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["respond"]: { + parameters: { + "additionalProperties": true, + "properties": { + "output": { + "description": "The result — facts, status, VFS paths to persisted data, whatever the caller needs to act on.", + "type": "string" + }, + "success": { + "description": "Whether the task completed successfully", + "type": "boolean" + }, + "type": { + "description": "Optional logical result type override", + "type": "string" + } + }, + "required": [ + "output", + "success" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["revert_to_version"]: { + parameters: { + "type": "object", + "properties": { + "version": { + "type": "number", + "description": "The deployment version number to revert to" + }, + "workflowId": { + "type": "string", + "description": "The workflow ID" + } + }, + "required": [ + "workflowId", + "version" + ] + }, + resultSchema: undefined, + }, + ["run"]: { + parameters: { + "properties": { + "context": { + "description": "Pre-gathered context: workflow state, block IDs, input requirements.", + "type": "string" + }, + "request": { + "description": "What to run or what logs to check.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["run_block"]: { + parameters: { + "type": "object", + "properties": { + "blockId": { + "type": "string", + "description": "The block ID to run in isolation." + }, + "executionId": { + "type": "string", + "description": "Optional execution ID to load the snapshot from. Uses latest execution if omitted." + }, + "useDeployedState": { + "type": "boolean", + "description": "When true, runs the deployed version instead of the live draft. Default: false (draft)." + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID to run. If not provided, uses the current workflow in context." + }, + "workflow_input": { + "type": "object", + "description": "JSON object with key-value mappings where each key is an input field name" + } + }, + "required": [ + "blockId" + ] + }, + resultSchema: undefined, + }, + ["run_from_block"]: { + parameters: { + "type": "object", + "properties": { + "executionId": { + "type": "string", + "description": "Optional execution ID to load the snapshot from. Uses latest execution if omitted." + }, + "startBlockId": { + "type": "string", + "description": "The block ID to start execution from." + }, + "useDeployedState": { + "type": "boolean", + "description": "When true, runs the deployed version instead of the live draft. Default: false (draft)." + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID to run. If not provided, uses the current workflow in context." + }, + "workflow_input": { + "type": "object", + "description": "JSON object with key-value mappings where each key is an input field name" + } + }, + "required": [ + "startBlockId" + ] + }, + resultSchema: undefined, + }, + ["run_workflow"]: { + parameters: { + "type": "object", + "properties": { + "useDeployedState": { + "type": "boolean", + "description": "When true, runs the deployed version instead of the live draft. Default: false (draft)." + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID to run. If not provided, uses the current workflow in context." + }, + "workflow_input": { + "type": "object", + "description": "JSON object with key-value mappings where each key is an input field name" + } + }, + "required": [ + "workflow_input" + ] + }, + resultSchema: undefined, + }, + ["run_workflow_until_block"]: { + parameters: { + "type": "object", + "properties": { + "stopAfterBlockId": { + "type": "string", + "description": "The block ID to stop after. Execution halts once this block completes." + }, + "useDeployedState": { + "type": "boolean", + "description": "When true, runs the deployed version instead of the live draft. Default: false (draft)." + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID to run. If not provided, uses the current workflow in context." + }, + "workflow_input": { + "type": "object", + "description": "JSON object with key-value mappings where each key is an input field name" + } + }, + "required": [ + "stopAfterBlockId" + ] + }, + resultSchema: undefined, + }, + ["scrape_page"]: { + parameters: { + "type": "object", + "properties": { + "include_links": { + "type": "boolean", + "description": "Extract all links from the page (default false)" + }, + "url": { + "type": "string", + "description": "The URL to scrape (must include https://)" + }, + "wait_for": { + "type": "string", + "description": "CSS selector to wait for before scraping (for JS-heavy pages)" + } + }, + "required": [ + "url" + ] + }, + resultSchema: undefined, + }, + ["search_documentation"]: { + parameters: { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query" + }, + "topK": { + "type": "number", + "description": "Number of results (max 10)" + } + }, + "required": [ + "query" + ] + }, + resultSchema: undefined, + }, + ["search_library_docs"]: { + parameters: { + "type": "object", + "properties": { + "library_name": { + "type": "string", + "description": "Name of the library to search for (e.g., 'nextjs', 'stripe', 'langchain')" + }, + "query": { + "type": "string", + "description": "The question or topic to find documentation for - be specific" + }, + "version": { + "type": "string", + "description": "Specific version (optional, e.g., '14', 'v2')" + } + }, + "required": [ + "library_name", + "query" + ] + }, + resultSchema: undefined, + }, + ["search_online"]: { + parameters: { + "type": "object", + "properties": { + "category": { + "type": "string", + "description": "Filter by category", + "enum": [ + "news", + "tweet", + "github", + "paper", + "company", + "research paper", + "linkedin profile", + "pdf", + "personal site" + ] + }, + "include_text": { + "type": "boolean", + "description": "Include page text content (default true)" + }, + "num_results": { + "type": "number", + "description": "Number of results (default 10, max 25)" + }, + "query": { + "type": "string", + "description": "Natural language search query" + } + }, + "required": [ + "query" + ] + }, + resultSchema: undefined, + }, + ["search_patterns"]: { + parameters: { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "description": "Maximum number of unique pattern examples to return (defaults to 3)." + }, + "queries": { + "type": "array", + "description": "Up to 3 descriptive strings explaining the workflow pattern(s) you need. Focus on intent and desired outcomes.", + "items": { + "type": "string", + "description": "Example: \"how to automate wealthbox meeting notes into follow-up tasks\"" + } + } + }, + "required": [ + "queries" + ] + }, + resultSchema: undefined, + }, + ["set_environment_variables"]: { + parameters: { + "type": "object", + "properties": { + "variables": { + "type": "array", + "description": "List of env vars to set", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Variable name" + }, + "value": { + "type": "string", + "description": "Variable value" + } + }, + "required": [ + "name", + "value" + ] + } + } + }, + "required": [ + "variables" + ] + }, + resultSchema: undefined, + }, + ["set_global_workflow_variables"]: { + parameters: { + "type": "object", + "properties": { + "operations": { + "type": "array", + "description": "List of operations to apply", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "operation": { + "type": "string", + "enum": [ + "add", + "delete", + "edit" + ] + }, + "type": { + "type": "string", + "enum": [ + "plain", + "number", + "boolean", + "array", + "object" + ] + }, + "value": { + "type": "string" + } + }, + "required": [ + "operation", + "name", + "type", + "value" + ] + } + }, + "workflowId": { + "type": "string", + "description": "Optional workflow ID. If not provided, uses the current workflow in context." + } + }, + "required": [ + "operations" + ] + }, + resultSchema: undefined, + }, + ["superagent"]: { + parameters: { + "properties": { + "task": { + "description": "A single sentence — the agent has full conversation context. Do NOT pre-read credentials or look up configs. Example: 'send the email we discussed' or 'check my calendar for tomorrow'.", + "type": "string" + } + }, + "required": [ + "task" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["table"]: { + parameters: { + "properties": { + "request": { + "description": "What table action is needed.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["tool_search_tool_regex"]: { + parameters: { + "properties": { + "case_insensitive": { + "description": "Whether the regex should be case-insensitive (default true).", + "type": "boolean" + }, + "max_results": { + "description": "Maximum number of tools to return (optional).", + "type": "integer" + }, + "pattern": { + "description": "Regular expression to match tool names or descriptions.", + "type": "string" + } + }, + "required": [ + "pattern" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["update_job_history"]: { + parameters: { + "type": "object", + "properties": { + "jobId": { + "type": "string", + "description": "The job ID." + }, + "summary": { + "type": "string", + "description": "A concise summary of what was done this run (e.g., 'Sent follow-up emails to 3 leads: Alice, Bob, Carol')." + } + }, + "required": [ + "jobId", + "summary" + ] + }, + resultSchema: undefined, + }, + ["update_workspace_mcp_server"]: { + parameters: { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "New description for the server" + }, + "isPublic": { + "type": "boolean", + "description": "Whether the server is publicly accessible" + }, + "name": { + "type": "string", + "description": "New name for the server" + }, + "serverId": { + "type": "string", + "description": "Required: the MCP server ID to update" + } + }, + "required": [ + "serverId" + ] + }, + resultSchema: undefined, + }, + ["user_memory"]: { + parameters: { + "type": "object", + "properties": { + "confidence": { + "type": "number", + "description": "Confidence level 0-1 (default 1.0 for explicit, 0.8 for inferred)" + }, + "correct_value": { + "type": "string", + "description": "The correct value to replace the wrong one (for 'correct' operation)" + }, + "key": { + "type": "string", + "description": "Unique key for the memory (e.g., 'preferred_model', 'slack_credential')" + }, + "limit": { + "type": "number", + "description": "Number of results for search (default 10)" + }, + "memory_type": { + "type": "string", + "description": "Type of memory: 'preference', 'entity', 'history', or 'correction'", + "enum": [ + "preference", + "entity", + "history", + "correction" + ] + }, + "operation": { + "type": "string", + "description": "Operation: 'add', 'search', 'delete', 'correct', or 'list'", + "enum": [ + "add", + "search", + "delete", + "correct", + "list" + ] + }, + "query": { + "type": "string", + "description": "Search query to find relevant memories" + }, + "source": { + "type": "string", + "description": "Source: 'explicit' (user told you) or 'inferred' (you observed)", + "enum": [ + "explicit", + "inferred" + ] + }, + "value": { + "type": "string", + "description": "Value to remember" + } + }, + "required": [ + "operation" + ] + }, + resultSchema: undefined, + }, + ["user_table"]: { + parameters: { + "type": "object", + "properties": { + "args": { + "type": "object", + "description": "Arguments for the operation", + "properties": { + "column": { + "type": "object", + "description": "Column definition for add_column: { name, type, unique?, position? }" + }, + "columnName": { + "type": "string", + "description": "Column name (required for rename_column, update_column; use columnNames array for batch delete_column)" + }, + "columnNames": { + "type": "array", + "description": "Array of column names to delete at once (for delete_column). Preferred over columnName when deleting multiple columns." + }, + "data": { + "type": "object", + "description": "Row data as key-value pairs (required for insert_row, update_row)" + }, + "description": { + "type": "string", + "description": "Table description (optional for 'create')" + }, + "fileId": { + "type": "string", + "description": "Canonical workspace file ID for create_from_file/import_file. Discover via read(\"files/{name}/meta.json\") or glob(\"files/by-id/*/meta.json\")." + }, + "filePath": { + "type": "string", + "description": "Legacy workspace file reference for create_from_file/import_file. Prefer fileId." + }, + "filter": { + "type": "object", + "description": "MongoDB-style filter for query_rows, update_rows_by_filter, delete_rows_by_filter" + }, + "limit": { + "type": "number", + "description": "Maximum rows to return or affect (optional, default 100)" + }, + "name": { + "type": "string", + "description": "Table name (required for 'create')" + }, + "newName": { + "type": "string", + "description": "New column name (required for rename_column)" + }, + "newType": { + "type": "string", + "description": "New column type (optional for update_column). Types: string, number, boolean, date, json" + }, + "offset": { + "type": "number", + "description": "Number of rows to skip (optional for query_rows, default 0)" + }, + "outputFormat": { + "type": "string", + "description": "Explicit format override for outputPath. Usually unnecessary — the file extension determines the format automatically. Only use this to force a different format than what the extension implies.", + "enum": [ + "json", + "csv", + "txt", + "md", + "html" + ] + }, + "outputPath": { + "type": "string", + "description": "Pipe query_rows results directly to a NEW workspace file. The format is auto-inferred from the file extension: .csv → CSV, .json → JSON, .md → Markdown, etc. Use .csv for tabular exports. Use a flat path like \"files/export.csv\" — nested paths are not supported." + }, + "rowId": { + "type": "string", + "description": "Row ID (required for get_row, update_row, delete_row)" + }, + "rowIds": { + "type": "array", + "description": "Array of row IDs to delete (for batch_delete_rows)" + }, + "rows": { + "type": "array", + "description": "Array of row data objects (required for batch_insert_rows)" + }, + "schema": { + "type": "object", + "description": "Table schema with columns array (required for 'create'). Each column: { name, type, unique? }" + }, + "sort": { + "type": "object", + "description": "Sort specification as { field: 'asc' | 'desc' } (optional for query_rows)" + }, + "tableId": { + "type": "string", + "description": "Table ID (required for most operations except 'create')" + }, + "unique": { + "type": "boolean", + "description": "Set column unique constraint (optional for update_column)" + }, + "updates": { + "type": "array", + "description": "Array of per-row updates: [{ rowId, data: { col: val } }] (for batch_update_rows)" + }, + "values": { + "type": "object", + "description": "Map of rowId to value for single-column batch update: { \"rowId1\": val1, \"rowId2\": val2 } (for batch_update_rows with columnName)" + } + } + }, + "operation": { + "type": "string", + "description": "The operation to perform", + "enum": [ + "create", + "create_from_file", + "import_file", + "get", + "get_schema", + "delete", + "insert_row", + "batch_insert_rows", + "get_row", + "query_rows", + "update_row", + "delete_row", + "update_rows_by_filter", + "delete_rows_by_filter", + "batch_update_rows", + "batch_delete_rows", + "add_column", + "rename_column", + "delete_column", + "update_column" + ] + } + }, + "required": [ + "operation", + "args" + ] + }, + resultSchema: { + "type": "object", + "properties": { + "data": { + "type": "object", + "description": "Operation-specific result payload." + }, + "message": { + "type": "string", + "description": "Human-readable outcome summary." + }, + "success": { + "type": "boolean", + "description": "Whether the operation succeeded." + } + }, + "required": [ + "success", + "message" + ] + }, + }, + ["workflow"]: { + parameters: { + "properties": { + "request": { + "description": "A single sentence — the agent has full conversation context and VFS access. Do NOT look up IDs or pre-read data; the workflow agent does its own research. Example: 'move all the return letter workflows into a folder called Letters'.", + "type": "string" + } + }, + "required": [ + "request" + ], + "type": "object" + }, + resultSchema: undefined, + }, + ["workspace_file"]: { + parameters: { + "type": "object", + "properties": { + "operation": { + "type": "string", + "description": "The file operation to perform.", + "enum": [ + "create", + "append", + "update", + "patch", + "rename", + "delete" + ] + }, + "target": { + "type": "object", + "description": "Explicit file target. Use kind=new_file + fileName for create. Use kind=file_id + fileId for append, update, patch, rename, and delete. Emit target keys in this order: kind, fileId, fileName.", + "properties": { + "kind": { + "type": "string", + "description": "How the file target is identified.", + "enum": [ + "new_file", + "file_id" + ] + }, + "fileId": { + "type": "string", + "description": "Canonical existing workspace file ID. Required when target.kind=file_id." + }, + "fileName": { + "type": "string", + "description": "Plain workspace filename including extension, e.g. \"main.py\" or \"report.docx\". Required when target.kind=new_file." + } + }, + "required": [ + "kind" + ] + }, + "title": { + "type": "string", + "description": "Optional short UI label for create/append chunks, e.g. \"Chapter 1\" or \"Slide 3\"." + }, + "contentType": { + "type": "string", + "description": "Optional MIME type override. Usually omit and let the system infer from the target file extension.", + "enum": [ + "text/markdown", + "text/html", + "text/plain", + "application/json", + "text/csv", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/pdf" + ] + }, + "edit": { + "type": "object", + "description": "Patch metadata. Use strategy=search_replace for exact text replacement, or strategy=anchored for line-based inserts/replacements/deletions. Emit edit keys in this order: strategy, search, replace, replaceAll, mode, occurrence, before_anchor, after_anchor, anchor, start_anchor, end_anchor, content.", + "properties": { + "strategy": { + "type": "string", + "description": "Patch strategy.", + "enum": [ + "search_replace", + "anchored" + ] + }, + "search": { + "type": "string", + "description": "Exact text to find when strategy=search_replace. Must match exactly once unless replaceAll=true." + }, + "replace": { + "type": "string", + "description": "Replacement text when strategy=search_replace." + }, + "replaceAll": { + "type": "boolean", + "description": "When true and strategy=search_replace, replace every match instead of requiring a unique single match." + }, + "mode": { + "type": "string", + "description": "Anchored edit mode when strategy=anchored.", + "enum": [ + "replace_between", + "insert_after", + "delete_between" + ] + }, + "occurrence": { + "type": "number", + "description": "1-based occurrence for repeated anchor lines. Optional; defaults to 1." + }, + "before_anchor": { + "type": "string", + "description": "Boundary line kept before inserted replacement content. Required for mode=replace_between." + }, + "after_anchor": { + "type": "string", + "description": "Boundary line kept after inserted replacement content. Required for mode=replace_between." + }, + "anchor": { + "type": "string", + "description": "Anchor line after which new content is inserted. Required for mode=insert_after." + }, + "start_anchor": { + "type": "string", + "description": "First line to delete. Required for mode=delete_between." + }, + "end_anchor": { + "type": "string", + "description": "First line to keep after deletion. Required for mode=delete_between." + }, + "content": { + "type": "string", + "description": "Inserted or replacement content for anchored edits. Not used for delete_between." + } + } + }, + "newName": { + "type": "string", + "description": "New file name for rename. Must be a plain workspace filename like \"main.py\"." + }, + "content": { + "type": "string", + "description": "File content for create, append, or update. For .pptx/.docx/.pdf this must be JavaScript source code for the corresponding generator runtime." + } + }, + "required": [ + "operation", + "target" + ] + }, + resultSchema: { + "type": "object", + "properties": { + "data": { + "type": "object", + "description": "Optional operation metadata such as file id, file name, size, and content type." + }, + "message": { + "type": "string", + "description": "Human-readable summary of the outcome." + }, + "success": { + "type": "boolean", + "description": "Whether the file operation succeeded." + } + }, + "required": [ + "success", + "message" + ] + }, + }, +} diff --git a/apps/sim/lib/copilot/request/go/stream.ts b/apps/sim/lib/copilot/request/go/stream.ts index 197004a253..bce8fe9bd0 100644 --- a/apps/sim/lib/copilot/request/go/stream.ts +++ b/apps/sim/lib/copilot/request/go/stream.ts @@ -22,6 +22,92 @@ import type { const logger = createLogger('CopilotGoStream') +type FilePreviewServerState = { + raw: string + started: boolean + operation?: string + targetKind?: string + fileId?: string + fileName?: string + title?: string + editMetaKey?: string + targetKey?: string + emittedContentLength: number +} + +function extractJsonString(raw: string, key: string): string | undefined { + const pattern = new RegExp(`"${key}"\\s*:\\s*"`) + const m = pattern.exec(raw) + if (!m) return undefined + const start = m.index + m[0].length + let end = -1 + for (let i = start; i < raw.length; i++) { + if (raw[i] === '\\') { + i++ + continue + } + if (raw[i] === '"') { + end = i + break + } + } + if (end === -1) return undefined + return raw + .slice(start, end) + .replace(/\\n/g, '\n') + .replace(/\\t/g, '\t') + .replace(/\\r/g, '\r') + .replace(/\\"/g, '"') + .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))) + .replace(/\\\\/g, '\\') +} + +function extractJsonBoolean(raw: string, key: string): boolean | undefined { + const match = raw.match(new RegExp(`"${key}"\\s*:\\s*(true|false)`)) + if (!match) return undefined + return match[1] === 'true' +} + +function extractJsonNumber(raw: string, key: string): number | undefined { + const match = raw.match(new RegExp(`"${key}"\\s*:\\s*(\\d+)`)) + if (!match) return undefined + return Number.parseInt(match[1], 10) +} + +function extractStreamedContent(raw: string, preferredKey: 'content' | 'replace'): string { + const marker = `"${preferredKey}":` + const idx = raw.indexOf(marker) + if (idx === -1) return '' + const rest = raw.slice(idx + marker.length).trimStart() + if (!rest.startsWith('"')) return rest + let end = -1 + for (let i = 1; i < rest.length; i++) { + if (rest[i] === '\\') { + i++ + continue + } + if (rest[i] === '"') { + end = i + break + } + } + const inner = end === -1 ? rest.slice(1) : rest.slice(1, end) + return inner + .replace(/\\n/g, '\n') + .replace(/\\t/g, '\t') + .replace(/\\r/g, '\r') + .replace(/\\"/g, '"') + .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))) + .replace(/\\\\/g, '\\') +} + +function buildPreviewContent(raw: string, strategy?: string): string { + if (strategy === 'search_replace') { + return extractStreamedContent(raw, 'replace') + } + return extractStreamedContent(raw, 'content') +} + export class CopilotBackendError extends Error { status?: number body?: string @@ -74,6 +160,7 @@ export async function runStreamLoop( options: StreamLoopOptions ): Promise { const { timeout = ORCHESTRATION_TIMEOUT_MS, abortSignal } = options + const filePreviewState = new Map() const fetchSpan = context.trace.startSpan( `HTTP Request → ${new URL(fetchUrl).pathname}`, @@ -136,6 +223,144 @@ export async function runStreamLoop( return } + if ( + streamEvent.type === MothershipStreamV1EventType.tool && + streamEvent.payload.phase === 'args_delta' && + streamEvent.payload.toolName === 'workspace_file' && + typeof streamEvent.payload.toolCallId === 'string' && + typeof streamEvent.payload.argumentsDelta === 'string' + ) { + const toolCallId = streamEvent.payload.toolCallId as string + const delta = streamEvent.payload.argumentsDelta as string + const state = filePreviewState.get(toolCallId) ?? { + raw: '', + started: false, + emittedContentLength: 0, + } + state.raw += delta + + if (!state.started) { + state.started = true + await options.onEvent?.({ + type: MothershipStreamV1EventType.tool, + payload: { + toolCallId, + toolName: 'workspace_file', + previewPhase: 'file_preview_start', + }, + ...(streamEvent.scope ? { scope: streamEvent.scope } : {}), + }) + } + + const operation = extractJsonString(state.raw, 'operation') + const targetKind = extractJsonString(state.raw, 'kind') + const fileId = extractJsonString(state.raw, 'fileId') + const fileName = extractJsonString(state.raw, 'fileName') + const title = extractJsonString(state.raw, 'title') + if (operation) state.operation = operation + if (targetKind) state.targetKind = targetKind + if (fileId) state.fileId = fileId + if (fileName) state.fileName = fileName + if (title) state.title = title + + const targetKey = JSON.stringify({ + operation: state.operation, + targetKind: state.targetKind, + fileId: state.fileId, + fileName: state.fileName, + title: state.title, + }) + if ( + state.targetKind && + (state.targetKind === 'new_file' ? !!state.fileName : !!state.fileId) && + state.targetKey !== targetKey + ) { + state.targetKey = targetKey + await options.onEvent?.({ + type: MothershipStreamV1EventType.tool, + payload: { + toolCallId, + toolName: 'workspace_file', + previewPhase: 'file_preview_target', + operation: state.operation, + target: { + kind: state.targetKind, + ...(state.fileId ? { fileId: state.fileId } : {}), + ...(state.fileName ? { fileName: state.fileName } : {}), + }, + ...(state.title ? { title: state.title } : {}), + }, + ...(streamEvent.scope ? { scope: streamEvent.scope } : {}), + }) + } + + const strategy = extractJsonString(state.raw, 'strategy') + const editMetaPayload = strategy + ? { + strategy, + ...(extractJsonString(state.raw, 'mode') + ? { mode: extractJsonString(state.raw, 'mode') } + : {}), + ...(extractJsonNumber(state.raw, 'occurrence') !== undefined + ? { occurrence: extractJsonNumber(state.raw, 'occurrence') } + : {}), + ...(extractJsonString(state.raw, 'search') + ? { search: extractJsonString(state.raw, 'search') } + : {}), + ...(extractJsonBoolean(state.raw, 'replaceAll') !== undefined + ? { replaceAll: extractJsonBoolean(state.raw, 'replaceAll') } + : {}), + ...(extractJsonString(state.raw, 'before_anchor') + ? { before_anchor: extractJsonString(state.raw, 'before_anchor') } + : {}), + ...(extractJsonString(state.raw, 'after_anchor') + ? { after_anchor: extractJsonString(state.raw, 'after_anchor') } + : {}), + ...(extractJsonString(state.raw, 'anchor') + ? { anchor: extractJsonString(state.raw, 'anchor') } + : {}), + ...(extractJsonString(state.raw, 'start_anchor') + ? { start_anchor: extractJsonString(state.raw, 'start_anchor') } + : {}), + ...(extractJsonString(state.raw, 'end_anchor') + ? { end_anchor: extractJsonString(state.raw, 'end_anchor') } + : {}), + } + : undefined + const editMetaKey = editMetaPayload ? JSON.stringify(editMetaPayload) : undefined + if (editMetaPayload && state.editMetaKey !== editMetaKey) { + state.editMetaKey = editMetaKey + await options.onEvent?.({ + type: MothershipStreamV1EventType.tool, + payload: { + toolCallId, + toolName: 'workspace_file', + previewPhase: 'file_preview_edit_meta', + edit: editMetaPayload, + }, + ...(streamEvent.scope ? { scope: streamEvent.scope } : {}), + }) + } + + const streamedContent = buildPreviewContent(state.raw, strategy) + if (streamedContent.length > state.emittedContentLength) { + const contentDelta = streamedContent.slice(state.emittedContentLength) + state.emittedContentLength = streamedContent.length + await options.onEvent?.({ + type: MothershipStreamV1EventType.tool, + payload: { + toolCallId, + toolName: 'workspace_file', + previewPhase: 'file_preview_content_delta', + delta: contentDelta, + }, + ...(streamEvent.scope ? { scope: streamEvent.scope } : {}), + }) + } + + filePreviewState.set(toolCallId, state) + } + try { await options.onEvent?.(streamEvent) } catch (error) { diff --git a/apps/sim/lib/copilot/resources/extraction.ts b/apps/sim/lib/copilot/resources/extraction.ts index ca16417fb8..2c9fdf7015 100644 --- a/apps/sim/lib/copilot/resources/extraction.ts +++ b/apps/sim/lib/copilot/resources/extraction.ts @@ -42,6 +42,10 @@ function getOperation(params: Record | undefined): string | und return (args.operation ?? params?.operation) as string | undefined } +function getWorkspaceFileTarget(params: Record | undefined): Record { + return asRecord(params?.target) +} + const READ_ONLY_TABLE_OPS = new Set(['get', 'get_schema', 'get_row', 'query_rows']) const READ_ONLY_KB_OPS = new Set(['get', 'query', 'list_tags', 'get_tag_usage']) const READ_ONLY_KNOWLEDGE_ACTIONS = new Set(['listed', 'queried']) @@ -250,7 +254,11 @@ export function extractDeletedResourcesFromToolResult( case WorkspaceFile.id: { if (operation !== 'delete') return [] - const fileId = (data.id as string) ?? (args.fileId as string) + const target = getWorkspaceFileTarget(params) + const fileId = + (data.id as string) ?? + (target.fileId as string) ?? + (args.fileId as string) if (fileId) { return [{ type: resourceType, id: fileId, title: (data.name as string) || 'File' }] } diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index 9d5d1d141f..4ee4debeb9 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -2,7 +2,6 @@ import { createLogger } from '@sim/logger' import { CheckDeploymentStatus, CompleteJob, - CreateFile, CreateFolder, CreateJob, CreateWorkflow, @@ -45,7 +44,6 @@ import { RunFromBlock, RunWorkflow, RunWorkflowUntilBlock, - SetFileContext, SetGlobalWorkflowVariables, UpdateJobHistory, UpdateWorkspaceMcpServer, @@ -67,8 +65,6 @@ import { executeRevertToVersion, executeUpdateWorkspaceMcpServer, } from '../tools/handlers/deployment/manage' -import { executeCreateFile } from '../tools/handlers/files/create-file' -import { executeSetFileContext } from '../tools/handlers/files/set-file-context' import { executeFunctionExecute } from '../tools/handlers/function-execute' import { executeCompleteJob, @@ -183,8 +179,6 @@ function buildHandlerMap(): Record { [GetPlatformActions.id]: h(executeGetPlatformActions), [MaterializeFile.id]: h(executeMaterializeFile), [FunctionExecute.id]: h(executeFunctionExecute), - [CreateFile.id]: h(executeCreateFile), - [SetFileContext.id]: h(executeSetFileContext), ...buildServerToolHandlers(), } diff --git a/apps/sim/lib/copilot/tools/handlers/files/create-file.ts b/apps/sim/lib/copilot/tools/handlers/files/create-file.ts deleted file mode 100644 index b72296faf8..0000000000 --- a/apps/sim/lib/copilot/tools/handlers/files/create-file.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { createLogger } from '@sim/logger' -import type { ToolExecutionContext, ToolExecutionResult } from '@/lib/copilot/tool-executor/types' -import { - getWorkspaceFileByName, - uploadWorkspaceFile, -} from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' - -const logger = createLogger('CreateFile') - -interface CreateFileParams { - fileName: string -} - -export async function executeCreateFile( - params: CreateFileParams, - context: ToolExecutionContext -): Promise { - const { fileName } = params - const workspaceId = context.workspaceId - - if (!fileName) { - return { success: false, error: 'fileName is required' } - } - if (!workspaceId) { - return { success: false, error: 'workspaceId is required' } - } - - try { - const existing = await getWorkspaceFileByName(workspaceId, fileName) - if (existing) { - logger.warn('Create file rejected because file already exists', { - fileId: existing.id, - fileName, - workspaceId, - }) - return { - success: false, - error: `File "${existing.name}" already exists. Use set_file_context with fileId "${existing.id}" to append to it, or choose a new fileName.`, - } - } - - const emptyBuffer = Buffer.from('', 'utf-8') - const extension = fileName.includes('.') ? fileName.split('.').pop() || '' : '' - const mimeType = getMimeTypeFromExtension(extension) - const record = await uploadWorkspaceFile( - workspaceId, - context.userId, - emptyBuffer, - fileName, - mimeType - ) - - logger.info('File created', { fileId: record.id, fileName: record.name, workspaceId }) - - return { - success: true, - output: { - fileId: record.id, - fileName: record.name, - contentType: record.type, - size: 0, - message: `File "${record.name}" created. File context is now set — subsequent workspace_file.write calls will automatically target this file.`, - }, - resources: [ - { - type: 'file', - id: record.id, - title: record.name, - }, - ], - } - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - logger.error('Failed to create file', { fileName, error: msg }) - return { success: false, error: `Failed to create file: ${msg}` } - } -} diff --git a/apps/sim/lib/copilot/tools/handlers/files/set-file-context.ts b/apps/sim/lib/copilot/tools/handlers/files/set-file-context.ts deleted file mode 100644 index ad3a54064e..0000000000 --- a/apps/sim/lib/copilot/tools/handlers/files/set-file-context.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { createLogger } from '@sim/logger' -import type { ToolExecutionContext, ToolExecutionResult } from '@/lib/copilot/tool-executor/types' -import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' - -const logger = createLogger('SetFileContext') - -interface SetFileContextParams { - fileId: string -} - -export async function executeSetFileContext( - params: SetFileContextParams, - context: ToolExecutionContext -): Promise { - const { fileId } = params - const workspaceId = context.workspaceId - - if (!fileId) { - return { success: false, error: 'fileId is required' } - } - if (!workspaceId) { - return { success: false, error: 'workspaceId is required' } - } - - try { - const file = await getWorkspaceFile(workspaceId, fileId) - if (!file) { - return { success: false, error: `File not found: ${fileId}` } - } - - logger.info('File context set', { fileId, fileName: file.name, workspaceId }) - - return { - success: true, - output: { - fileId: file.id, - fileName: file.name, - contentType: file.type, - size: file.size, - message: `File context switched to "${file.name}". Subsequent workspace_file.write calls will now target this file.`, - }, - } - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - logger.error('Failed to validate file context', { fileId, error: msg }) - return { success: false, error: `Failed to validate file: ${msg}` } - } -} diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index b916d63e7b..d9063e9211 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -1,9 +1,9 @@ import { existsSync, readFileSync } from 'fs' import { join } from 'path' import { createLogger } from '@sim/logger' +import { z } from 'zod' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' -import { GetBlocksMetadataInput, GetBlocksMetadataResult } from '@/lib/copilot/tools/shared/schemas' import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/feature-flags' import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils' import { registry as blockRegistry } from '@/blocks/registry' @@ -100,17 +100,20 @@ export interface CopilotBlockMetadata { yamlDocumentation?: string } +const GetBlocksMetadataInputSchema = z.object({ blockIds: z.array(z.string()).min(1) }) +const GetBlocksMetadataResultSchema = z.object({ metadata: z.record(z.any()) }) + export const getBlocksMetadataServerTool: BaseServerTool< - ReturnType, - ReturnType + z.infer, + z.infer > = { name: 'get_blocks_metadata', - inputSchema: GetBlocksMetadataInput, - outputSchema: GetBlocksMetadataResult, + inputSchema: GetBlocksMetadataInputSchema, + outputSchema: GetBlocksMetadataResultSchema, async execute( - { blockIds }: ReturnType, + { blockIds }: z.infer, context?: { userId: string } - ): Promise> { + ): Promise> { const logger = createLogger('GetBlocksMetadataServerTool') logger.debug('Executing get_blocks_metadata', { count: blockIds?.length }) @@ -319,7 +322,7 @@ export const getBlocksMetadataServerTool: BaseServerTool< transformedResult[blockId] = transformBlockMetadata(metadata) } - return GetBlocksMetadataResult.parse({ metadata: transformedResult }) + return GetBlocksMetadataResultSchema.parse({ metadata: transformedResult }) }, } diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index 2a2a95b7a0..e1d7a2baa6 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -5,7 +5,6 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' -import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools/shared/schemas' import { generateDocxFromCode, generatePdfFromCode, @@ -30,6 +29,57 @@ const PPTX_SOURCE_MIME = 'text/x-pptxgenjs' const DOCX_SOURCE_MIME = 'text/x-docxjs' const PDF_SOURCE_MIME = 'text/x-pdflibjs' +type WorkspaceFileOperation = 'create' | 'append' | 'update' | 'delete' | 'rename' | 'patch' + +type WorkspaceFileTarget = + | { + kind: 'new_file' + fileName: string + fileId?: string + } + | { + kind: 'file_id' + fileId: string + fileName?: string + } + +type WorkspaceFileEdit = + | { + strategy: 'search_replace' + search: string + replace: string + replaceAll?: boolean + } + | { + strategy: 'anchored' + mode: 'replace_between' | 'insert_after' | 'delete_between' + occurrence?: number + before_anchor?: string + after_anchor?: string + start_anchor?: string + end_anchor?: string + anchor?: string + content?: string + } + +type WorkspaceFileArgs = { + operation: WorkspaceFileOperation + target?: WorkspaceFileTarget + title?: string + content?: string + contentType?: string + newName?: string + edit?: WorkspaceFileEdit + // Legacy nested shape kept temporarily for compatibility during migration. + args?: Record +} + +type WorkspaceFileResult = { + success: boolean + message: string + data?: Record +} + const EXT_TO_MIME: Record = { '.txt': 'text/plain', '.md': 'text/markdown', @@ -56,6 +106,136 @@ function validateFlatWorkspaceFileName(fileName: string): string | null { return null } +function getDocumentFormatInfo(fileName: string): { + isDoc: boolean + formatName?: 'PPTX' | 'DOCX' | 'PDF' + sourceMime?: string + generator?: (code: string, workspaceId: string, signal?: AbortSignal) => Promise +} { + const lowerName = fileName.toLowerCase() + if (lowerName.endsWith('.pptx')) { + return { + isDoc: true, + formatName: 'PPTX', + sourceMime: PPTX_SOURCE_MIME, + generator: generatePptxFromCode, + } + } + if (lowerName.endsWith('.docx')) { + return { + isDoc: true, + formatName: 'DOCX', + sourceMime: DOCX_SOURCE_MIME, + generator: generateDocxFromCode, + } + } + if (lowerName.endsWith('.pdf')) { + return { + isDoc: true, + formatName: 'PDF', + sourceMime: PDF_SOURCE_MIME, + generator: generatePdfFromCode, + } + } + return { isDoc: false } +} + +function normalizeWorkspaceFileParams(params: WorkspaceFileArgs): { + operation: WorkspaceFileOperation + target?: WorkspaceFileTarget + title?: string + content?: string + contentType?: string + newName?: string + edit?: WorkspaceFileEdit +} { + if (params.target || params.edit || params.content !== undefined || params.newName !== undefined) { + return { + operation: params.operation, + target: params.target, + title: params.title, + content: params.content, + contentType: params.contentType, + newName: params.newName, + edit: params.edit, + } + } + + const legacyArgs = (params.args ?? {}) as Record + const legacyOperation = params.operation + const legacyTarget: WorkspaceFileTarget | undefined = + legacyOperation === 'create' + ? ({ + kind: 'new_file', + fileName: String(legacyArgs.fileName ?? ''), + } as WorkspaceFileTarget) + : legacyArgs.fileId + ? ({ + kind: 'file_id', + fileId: String(legacyArgs.fileId), + ...(legacyArgs.fileName ? { fileName: String(legacyArgs.fileName) } : {}), + } as WorkspaceFileTarget) + : legacyArgs.fileName + ? ({ + kind: 'new_file', + fileName: String(legacyArgs.fileName), + } as WorkspaceFileTarget) + : undefined + + const legacyEdit = (() => { + const structured = legacyArgs.edit as Record | undefined + if (structured && typeof structured.mode === 'string') { + return { + strategy: 'anchored', + mode: structured.mode as 'replace_between' | 'insert_after' | 'delete_between', + occurrence: + typeof structured.occurrence === 'number' ? structured.occurrence : undefined, + before_anchor: + typeof structured.before_anchor === 'string' ? structured.before_anchor : undefined, + after_anchor: + typeof structured.after_anchor === 'string' ? structured.after_anchor : undefined, + start_anchor: + typeof structured.start_anchor === 'string' ? structured.start_anchor : undefined, + end_anchor: + typeof structured.end_anchor === 'string' ? structured.end_anchor : undefined, + anchor: typeof structured.anchor === 'string' ? structured.anchor : undefined, + content: typeof structured.content === 'string' ? structured.content : undefined, + } satisfies WorkspaceFileEdit + } + + const edits = legacyArgs.edits as Array<{ search?: unknown; replace?: unknown }> | undefined + if (Array.isArray(edits) && edits.length > 0) { + const first = edits[0] + if (typeof first?.search === 'string' && typeof first?.replace === 'string') { + return { + strategy: 'search_replace', + search: first.search, + replace: first.replace, + } satisfies WorkspaceFileEdit + } + } + + return undefined + })() + + const normalizedOperation: WorkspaceFileOperation = + (legacyOperation as string) === 'write' + ? legacyTarget?.kind === 'new_file' + ? 'create' + : 'append' + : (legacyOperation as WorkspaceFileOperation) + + return { + operation: normalizedOperation, + target: legacyTarget, + title: typeof legacyArgs.title === 'string' ? legacyArgs.title : undefined, + content: typeof legacyArgs.content === 'string' ? legacyArgs.content : undefined, + contentType: typeof legacyArgs.contentType === 'string' ? legacyArgs.contentType : undefined, + newName: typeof legacyArgs.newName === 'string' ? legacyArgs.newName : undefined, + edit: legacyEdit, + } +} + export const workspaceFileServerTool: BaseServerTool = { name: WorkspaceFile.id, async execute( @@ -70,9 +250,9 @@ export const workspaceFileServerTool: BaseServerTool).workspaceId as string | undefined) + const normalized = normalizeWorkspaceFileParams(params) + const { operation } = normalized + const workspaceId = context.workspaceId if (!workspaceId) { return { success: false, message: 'Workspace ID is required' } @@ -80,118 +260,42 @@ export const workspaceFileServerTool: BaseServerTool).fileName as string | undefined - const fileId = (args as Record).fileId as string | undefined - const content = (args as Record).content as string | undefined - const explicitType = (args as Record).contentType as string | undefined - - if (!fileName) { - return { success: false, message: 'fileName is required for write operation' } - } - if (content === undefined || content === null) { - return { success: false, message: 'content is required for write operation' } - } - const fileNameValidationError = validateFlatWorkspaceFileName(fileName) - if (fileNameValidationError) { - return { success: false, message: fileNameValidationError } - } - - const lowerName = fileName.toLowerCase() - const isPptx = lowerName.endsWith('.pptx') - const isDocx = lowerName.endsWith('.docx') - const isPdf = lowerName.endsWith('.pdf') - const isDoc = isPptx || isDocx || isPdf - const sourceMime = isPptx - ? PPTX_SOURCE_MIME - : isDocx - ? DOCX_SOURCE_MIME - : isPdf - ? PDF_SOURCE_MIME - : undefined - - const existingFile = fileId - ? await getWorkspaceFile(workspaceId, fileId) - : await getWorkspaceFileByName(workspaceId, fileName) - - if (existingFile) { - const currentBuffer = await downloadWsFile(existingFile) - const combined = isDoc - ? `${currentBuffer.toString('utf-8')}\n{\n${content}\n}` - : `${currentBuffer.toString('utf-8')}\n${content}` - - if (isDoc) { - const formatName = isPptx ? 'PPTX' : isDocx ? 'DOCX' : 'PDF' - const generator = isPptx - ? generatePptxFromCode - : isDocx - ? generateDocxFromCode - : generatePdfFromCode - try { - await generator(combined, workspaceId) - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - return { - success: false, - message: `${formatName} generation failed after append: ${msg}. Fix the content and retry.`, - } - } - } - - const combinedBuffer = Buffer.from(combined, 'utf-8') - assertServerToolNotAborted(context) - await updateWorkspaceFileContent( - workspaceId, - existingFile.id, - context.userId, - combinedBuffer, - sourceMime - ) - - logger.info('Workspace file appended via write', { - fileId: existingFile.id, - name: existingFile.name, - appendedSize: content.length, - totalSize: combinedBuffer.length, - userId: context.userId, - }) - + case 'create': { + const target = normalized.target + if (!target || target.kind != 'new_file') { return { - success: true, - message: `Content appended to "${existingFile.name}" (${content.length} bytes added, ${combinedBuffer.length} bytes total)`, - data: { - id: existingFile.id, - name: existingFile.name, - size: combinedBuffer.length, - }, + success: false, + message: 'create requires target.kind=new_file with target.fileName', } } - let contentType: string - if (isDoc) { - const formatName = isPptx ? 'PPTX' : isDocx ? 'DOCX' : 'PDF' - const generator = isPptx - ? generatePptxFromCode - : isDocx - ? generateDocxFromCode - : generatePdfFromCode + const fileName = target.fileName + const content = normalized.content ?? '' + const explicitType = normalized.contentType + const fileNameValidationError = validateFlatWorkspaceFileName(fileName) + if (fileNameValidationError) return { success: false, message: fileNameValidationError } + + const existingFile = await getWorkspaceFileByName(workspaceId, fileName) + if (existingFile) { + return { success: false, message: `File "${fileName}" already exists` } + } + + const docInfo = getDocumentFormatInfo(fileName) + let contentType = inferContentType(fileName, explicitType) + if (docInfo.isDoc) { try { - await generator(content, workspaceId) + await docInfo.generator!(content, workspaceId) } catch (err) { const msg = err instanceof Error ? err.message : String(err) - logger.error(`${formatName} code validation failed`, { error: msg, fileName }) return { success: false, - message: `${formatName} generation failed: ${msg}. Fix the code and retry.`, + message: `${docInfo.formatName} generation failed: ${msg}. Fix the code and retry.`, } } - contentType = sourceMime! - } else { - contentType = inferContentType(fileName, explicitType) + contentType = docInfo.sourceMime! } const fileBuffer = Buffer.from(content, 'utf-8') - assertServerToolNotAborted(context) const result = await uploadWorkspaceFile( workspaceId, @@ -201,7 +305,7 @@ export const workspaceFileServerTool: BaseServerTool).fileId as string | undefined - const content = (args as Record).content as string | undefined - - if (!fileId) { - return { success: false, message: 'fileId is required for update operation' } + case 'append': { + const target = normalized.target + if (!target || target.kind !== 'file_id') { + return { + success: false, + message: 'append requires target.kind=file_id with target.fileId', + } } - if (content === undefined || content === null) { - return { success: false, message: 'content is required for update operation' } + if (normalized.content === undefined || normalized.content === null) { + return { success: false, message: 'content is required for append operation' } } - const fileRecord = await getWorkspaceFile(workspaceId, fileId) - if (!fileRecord) { - return { success: false, message: `File with ID "${fileId}" not found` } + const existingFile = await getWorkspaceFile(workspaceId, target.fileId) + if (!existingFile) { + return { success: false, message: `File with ID "${target.fileId}" not found` } + } + if (target.fileName && target.fileName != existingFile.name) { + return { + success: false, + message: `Target mismatch: fileId "${target.fileId}" is "${existingFile.name}", not "${target.fileName}"`, + } } - const updateLowerName = fileRecord.name?.toLowerCase() ?? '' - const isPptxUpdate = updateLowerName.endsWith('.pptx') - const isDocxUpdate = updateLowerName.endsWith('.docx') - const isPdfUpdate = updateLowerName.endsWith('.pdf') - const isDocUpdate = isPptxUpdate || isDocxUpdate || isPdfUpdate + const docInfo = getDocumentFormatInfo(existingFile.name) + const currentBuffer = await downloadWsFile(existingFile) + const combined = docInfo.isDoc + ? `${currentBuffer.toString('utf-8')}\n{\n${normalized.content}\n}` + : `${currentBuffer.toString('utf-8')}\n${normalized.content}` - if (isDocUpdate) { - const formatName = isPptxUpdate ? 'PPTX' : isDocxUpdate ? 'DOCX' : 'PDF' - const generator = isPptxUpdate - ? generatePptxFromCode - : isDocxUpdate - ? generateDocxFromCode - : generatePdfFromCode + if (docInfo.isDoc) { try { - await generator(content, workspaceId) + await docInfo.generator!(combined, workspaceId) } catch (err) { const msg = err instanceof Error ? err.message : String(err) return { success: false, - message: `${formatName} generation failed: ${msg}. Fix the code and retry.`, + message: `${docInfo.formatName} generation failed after append: ${msg}. Fix the content and retry.`, } } } - const updateSourceMime = isPptxUpdate - ? PPTX_SOURCE_MIME - : isDocxUpdate - ? DOCX_SOURCE_MIME - : isPdfUpdate - ? PDF_SOURCE_MIME - : undefined - const fileBuffer = Buffer.from(content, 'utf-8') - + const combinedBuffer = Buffer.from(combined, 'utf-8') assertServerToolNotAborted(context) + const appendMime = docInfo.sourceMime || inferContentType(existingFile.name, normalized.contentType) await updateWorkspaceFileContent( workspaceId, - fileId, + existingFile.id, + context.userId, + combinedBuffer, + appendMime + ) + + logger.info('Workspace file appended via copilot', { + fileId: existingFile.id, + name: existingFile.name, + appendedSize: normalized.content.length, + totalSize: combinedBuffer.length, + userId: context.userId, + }) + + return { + success: true, + message: `Content appended to "${existingFile.name}" (${normalized.content.length} bytes added, ${combinedBuffer.length} bytes total)`, + data: { + id: existingFile.id, + name: existingFile.name, + size: combinedBuffer.length, + contentType: appendMime, + }, + } + } + + case 'update': { + const target = normalized.target + if (!target || target.kind !== 'file_id') { + return { + success: false, + message: 'update requires target.kind=file_id with target.fileId', + } + } + if (normalized.content === undefined || normalized.content === null) { + return { success: false, message: 'content is required for update operation' } + } + + const fileRecord = await getWorkspaceFile(workspaceId, target.fileId) + if (!fileRecord) { + return { success: false, message: `File with ID "${target.fileId}" not found` } + } + if (target.fileName && target.fileName != fileRecord.name) { + return { + success: false, + message: `Target mismatch: fileId "${target.fileId}" is "${fileRecord.name}", not "${target.fileName}"`, + } + } + + const docInfo = getDocumentFormatInfo(fileRecord.name) + if (docInfo.isDoc) { + try { + await docInfo.generator!(normalized.content, workspaceId) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + return { + success: false, + message: `${docInfo.formatName} generation failed: ${msg}. Fix the code and retry.`, + } + } + } + + const fileBuffer = Buffer.from(normalized.content, 'utf-8') + assertServerToolNotAborted(context) + const updateMime = docInfo.sourceMime || inferContentType(fileRecord.name, normalized.contentType) + await updateWorkspaceFileContent( + workspaceId, + target.fileId, context.userId, fileBuffer, - updateSourceMime + updateMime ) logger.info('Workspace file updated via copilot', { - fileId, + fileId: target.fileId, name: fileRecord.name, size: fileBuffer.length, userId: context.userId, @@ -291,67 +456,70 @@ export const workspaceFileServerTool: BaseServerTool).fileId as string | undefined - const newName = (args as Record).newName as string | undefined - - if (!fileId) { - return { success: false, message: 'fileId is required for rename operation' } + const target = normalized.target + if (!target || target.kind !== 'file_id') { + return { + success: false, + message: 'rename requires target.kind=file_id with target.fileId', + } } - if (!newName) { + if (!normalized.newName) { return { success: false, message: 'newName is required for rename operation' } } - const fileNameValidationError = validateFlatWorkspaceFileName(newName) - if (fileNameValidationError) { - return { success: false, message: fileNameValidationError } - } + const fileNameValidationError = validateFlatWorkspaceFileName(normalized.newName) + if (fileNameValidationError) return { success: false, message: fileNameValidationError } - const fileRecord = await getWorkspaceFile(workspaceId, fileId) + const fileRecord = await getWorkspaceFile(workspaceId, target.fileId) if (!fileRecord) { - return { success: false, message: `File with ID "${fileId}" not found` } + return { success: false, message: `File with ID "${target.fileId}" not found` } } const oldName = fileRecord.name assertServerToolNotAborted(context) - await renameWorkspaceFile(workspaceId, fileId, newName) + await renameWorkspaceFile(workspaceId, target.fileId, normalized.newName) logger.info('Workspace file renamed via copilot', { - fileId, + fileId: target.fileId, oldName, - newName, + newName: normalized.newName, userId: context.userId, }) return { success: true, - message: `File renamed from "${oldName}" to "${newName}"`, - data: { id: fileId, name: newName }, + message: `File renamed from "${oldName}" to "${normalized.newName}"`, + data: { id: target.fileId, name: normalized.newName }, } } case 'delete': { - const fileId = (args as Record).fileId as string | undefined - if (!fileId) { - return { success: false, message: 'fileId is required for delete operation' } + const target = normalized.target + if (!target || target.kind !== 'file_id') { + return { + success: false, + message: 'delete requires target.kind=file_id with target.fileId', + } } - const fileRecord = await getWorkspaceFile(workspaceId, fileId) + const fileRecord = await getWorkspaceFile(workspaceId, target.fileId) if (!fileRecord) { - return { success: false, message: `File with ID "${fileId}" not found` } + return { success: false, message: `File with ID "${target.fileId}" not found` } } assertServerToolNotAborted(context) - await deleteWorkspaceFile(workspaceId, fileId) + await deleteWorkspaceFile(workspaceId, target.fileId) logger.info('Workspace file deleted via copilot', { - fileId, + fileId: target.fileId, name: fileRecord.name, userId: context.userId, }) @@ -359,44 +527,33 @@ export const workspaceFileServerTool: BaseServerTool).fileId as string | undefined - const edit = (args as Record).edit as - | { - mode: string - before_anchor?: string - after_anchor?: string - start_anchor?: string - end_anchor?: string - anchor?: string - content?: string - occurrence?: number - } - | undefined - const legacyEdits = (args as Record).edits as - | { search: string; replace: string }[] - | undefined - - if (!fileId) { - return { success: false, message: 'fileId is required for patch operation' } + const target = normalized.target + if (!target || target.kind !== 'file_id') { + return { + success: false, + message: 'patch requires target.kind=file_id with target.fileId', + } + } + if (!normalized.edit) { + return { success: false, message: 'edit is required for patch operation' } } - const fileRecord = await getWorkspaceFile(workspaceId, fileId) + const fileRecord = await getWorkspaceFile(workspaceId, target.fileId) if (!fileRecord) { - return { success: false, message: `File with ID "${fileId}" not found` } + return { success: false, message: `File with ID "${target.fileId}" not found` } } const currentBuffer = await downloadWsFile(fileRecord) let content = currentBuffer.toString('utf-8') - if (edit && typeof edit.mode === 'string') { + if (normalized.edit.strategy === 'anchored') { const lines = content.split('\n') - - const defaultOccurrence = edit.occurrence ?? 1 + const defaultOccurrence = normalized.edit.occurrence ?? 1 const findAnchorLine = ( anchor: string, @@ -423,16 +580,16 @@ export const workspaceFileServerTool: BaseServerTool 0) { - for (const le of legacyEdits) { - const firstIdx = content.indexOf(le.search) - if (firstIdx === -1) { - return { - success: false, - message: `Patch failed: search string not found in file "${fileRecord.name}". Search: "${le.search.slice(0, 100)}${le.search.length > 100 ? '...' : ''}"`, - } + } else if (normalized.edit.strategy === 'search_replace') { + const search = normalized.edit.search + const replace = normalized.edit.replace + const firstIdx = content.indexOf(search) + if (firstIdx === -1) { + return { + success: false, + message: `Patch failed: search string not found in file "${fileRecord.name}". Search: "${search.slice(0, 100)}${search.length > 100 ? '...' : ''}"`, } - if (content.indexOf(le.search, firstIdx + 1) !== -1) { - return { - success: false, - message: `Patch failed: search string is ambiguous — found at multiple locations in "${fileRecord.name}". Use a longer, unique search string.`, - } - } - content = - content.slice(0, firstIdx) + le.replace + content.slice(firstIdx + le.search.length) } + if (!normalized.edit.replaceAll && content.indexOf(search, firstIdx + 1) !== -1) { + return { + success: false, + message: `Patch failed: search string is ambiguous — found at multiple locations in "${fileRecord.name}". Use a longer unique search string or replaceAll.`, + } + } + content = normalized.edit.replaceAll + ? content.split(search).join(replace) + : content.slice(0, firstIdx) + replace + content.slice(firstIdx + search.length) } else { return { success: false, - message: 'patch requires either an edit object (with mode) or a legacy edits array', + message: `Unknown patch strategy: "${(normalized.edit as { strategy?: string }).strategy}"`, } } - const patchLowerName = fileRecord.name?.toLowerCase() ?? '' - const isPptxPatch = patchLowerName.endsWith('.pptx') - const isDocxPatch = patchLowerName.endsWith('.docx') - const isPdfPatch = patchLowerName.endsWith('.pdf') - const isDocPatch = isPptxPatch || isDocxPatch || isPdfPatch - - if (isDocPatch) { - const formatName = isPptxPatch ? 'PPTX' : isDocxPatch ? 'DOCX' : 'PDF' - const generator = isPptxPatch - ? generatePptxFromCode - : isDocxPatch - ? generateDocxFromCode - : generatePdfFromCode + const docInfo = getDocumentFormatInfo(fileRecord.name) + if (docInfo.isDoc) { try { - await generator(content, workspaceId) + await docInfo.generator!(content, workspaceId) } catch (err) { const msg = err instanceof Error ? err.message : String(err) return { success: false, - message: `Patched ${formatName} code failed to compile: ${msg}. Fix the edits and retry.`, + message: `Patched ${docInfo.formatName} code failed to compile: ${msg}. Fix the edit and retry.`, } } } - const patchSourceMime = isPptxPatch - ? PPTX_SOURCE_MIME - : isDocxPatch - ? DOCX_SOURCE_MIME - : isPdfPatch - ? PDF_SOURCE_MIME - : undefined const patchedBuffer = Buffer.from(content, 'utf-8') assertServerToolNotAborted(context) + const patchMime = docInfo.sourceMime || inferContentType(fileRecord.name) await updateWorkspaceFileContent( workspaceId, - fileId, + target.fileId, context.userId, patchedBuffer, - patchSourceMime + patchMime ) - const editMode = edit?.mode ?? 'legacy' logger.info('Workspace file patched via copilot', { - fileId, + fileId: target.fileId, name: fileRecord.name, - editMode, + strategy: normalized.edit.strategy, userId: context.userId, }) return { success: true, - message: `File "${fileRecord.name}" patched successfully (${editMode} edit applied)`, + message: `File "${fileRecord.name}" patched successfully (${normalized.edit.strategy} edit applied)`, data: { - id: fileId, + id: target.fileId, name: fileRecord.name, size: patchedBuffer.length, + contentType: patchMime, }, } } @@ -574,7 +712,7 @@ export const workspaceFileServerTool: BaseServerTool() + +function formatErrors(errors: ErrorObject[] | null | undefined): string { + if (!errors || errors.length === 0) return 'unknown validation error' + return errors + .slice(0, 5) + .map((error) => `${error.instancePath || '/'} ${error.message || 'is invalid'}`.trim()) + .join('; ') +} + +function getValidator(toolName: string, schemaKind: 'parameters' | 'resultSchema'): ValidateFunction | null { + const cacheKey = `${toolName}:${schemaKind}` + const cached = validatorCache.get(cacheKey) + if (cached) return cached + + const schema = TOOL_RUNTIME_SCHEMAS[toolName]?.[schemaKind] + if (!schema) return null + + const validator = ajv.compile(schema as object) + validatorCache.set(cacheKey, validator) + return validator +} + +export function validateGeneratedToolPayload( + toolName: string, + schemaKind: 'parameters' | 'resultSchema', + payload: T +): T { + const validator = getValidator(toolName, schemaKind) + if (!validator) return payload + + if (!validator(payload)) { + const label = schemaKind === 'parameters' ? 'input' : 'output' + throw new Error(`${toolName} ${label} validation failed: ${formatErrors(validator.errors)}`) + } + + return payload +} diff --git a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts index 2293cb6cf8..b34fc6552d 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts @@ -9,7 +9,6 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' -import type { KnowledgeBaseArgs, KnowledgeBaseResult } from '@/lib/copilot/tools/shared/schemas' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { @@ -40,6 +39,17 @@ import { getQueryStrategy, handleVectorOnlySearch } from '@/app/api/knowledge/se const logger = createLogger('KnowledgeBaseServerTool') +type KnowledgeBaseArgs = { + operation: string + args?: Record +} + +type KnowledgeBaseResult = { + success: boolean + message: string + data?: any +} + /** * Knowledge base tool for copilot to create, list, and get knowledge bases */ diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 87321de3a1..2fec0a5075 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -33,11 +33,17 @@ import { generateVisualizationServerTool } from '@/lib/copilot/tools/server/visu import { editWorkflowServerTool } from '@/lib/copilot/tools/server/workflow/edit-workflow' import { getExecutionSummaryServerTool } from '@/lib/copilot/tools/server/workflow/get-execution-summary' import { getWorkflowLogsServerTool } from '@/lib/copilot/tools/server/workflow/get-workflow-logs' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' +import { z } from 'zod' +import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema' export { ExecuteResponseSuccessSchema } export type ExecuteResponseSuccess = (typeof ExecuteResponseSuccessSchema)['_type'] +const ExecuteResponseSuccessSchema = z.object({ + success: z.literal(true), + result: z.unknown(), +}) + const logger = createLogger('ServerToolRouter') const WRITE_ACTIONS: Record = { @@ -76,7 +82,7 @@ const WRITE_ACTIONS: Record = { [ManageMcpTool.id]: ['add', 'edit', 'delete'], [ManageSkill.id]: ['add', 'edit', 'delete'], [ManageCredential.id]: ['rename', 'delete'], - [WorkspaceFile.id]: ['write', 'update', 'delete', 'rename', 'patch'], + [WorkspaceFile.id]: ['create', 'append', 'update', 'delete', 'rename', 'patch'], [DownloadToWorkspaceFile.id]: ['*'], [GenerateVisualization.id]: ['generate'], [GenerateImage.id]: ['generate'], @@ -153,14 +159,20 @@ export async function routeExecution( assertServerToolNotAborted(context) - // Validate input if tool declares a schema - const args = tool.inputSchema ? tool.inputSchema.parse(payload ?? {}) : (payload ?? {}) + // Validate input if tool declares a schema; otherwise fall back to the + // generated JSON schema contract emitted from Go. + const args = tool.inputSchema + ? tool.inputSchema.parse(payload ?? {}) + : validateGeneratedToolPayload(toolName, 'parameters', payload ?? {}) assertServerToolNotAborted(context) // Execute const result = await tool.execute(args, context) - // Validate output if tool declares a schema - return tool.outputSchema ? tool.outputSchema.parse(result) : result + // Validate output if tool declares a schema; otherwise fall back to the + // generated JSON schema contract emitted from Go. + return tool.outputSchema + ? tool.outputSchema.parse(result) + : validateGeneratedToolPayload(toolName, 'resultSchema', result) } diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts index 3586b45e8f..b69c12c20e 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -5,7 +5,6 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' -import type { UserTableArgs, UserTableResult } from '@/lib/copilot/tools/shared/schemas' import { generateId } from '@/lib/core/utils/uuid' import { COLUMN_TYPES } from '@/lib/table/constants' import { @@ -38,6 +37,17 @@ import { const logger = createLogger('UserTableServerTool') +type UserTableArgs = { + operation: string + args?: Record +} + +type UserTableResult = { + success: boolean + message: string + data?: any +} + const MAX_BATCH_SIZE = 1000 const SCHEMA_SAMPLE_SIZE = 100 diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts deleted file mode 100644 index c59200e846..0000000000 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { z } from 'zod' - -// Generic envelope used by client to validate API responses -export const ExecuteResponseSuccessSchema = z.object({ - success: z.literal(true), - result: z.unknown(), -}) -export type ExecuteResponseSuccess = z.infer - -// get_blocks_metadata -export const GetBlocksMetadataInput = z.object({ blockIds: z.array(z.string()).min(1) }) -export const GetBlocksMetadataResult = z.object({ metadata: z.record(z.any()) }) -export type GetBlocksMetadataResultType = z.infer - -// get_trigger_blocks -export const GetTriggerBlocksInput = z.object({}) -export const GetTriggerBlocksResult = z.object({ - triggerBlockIds: z.array(z.string()), -}) -export type GetTriggerBlocksResultType = z.infer - -// knowledge_base - shared schema used by client tool, server tool, and registry -export const KnowledgeBaseArgsSchema = z.object({ - operation: z.enum([ - 'create', - 'get', - 'query', - 'update', - 'delete', - 'add_file', - 'delete_document', - 'update_document', - 'list_tags', - 'create_tag', - 'update_tag', - 'delete_tag', - 'get_tag_usage', - 'add_connector', - 'update_connector', - 'delete_connector', - 'sync_connector', - ]), - args: z - .object({ - /** Name of the knowledge base (required for create) */ - name: z.string().optional(), - /** Description of the knowledge base (optional for create) */ - description: z.string().optional(), - /** Workspace ID to associate with (required for create, optional for list) */ - workspaceId: z.string().optional(), - /** Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage, add_connector) */ - knowledgeBaseId: z.string().optional(), - /** Workspace file ID to add as a document (required for add_file). */ - fileId: z.string().optional(), - /** Legacy workspace file reference for add_file. Prefer fileId. */ - filePath: z.string().optional(), - /** Search query text (required for query) */ - query: z.string().optional(), - /** Number of results to return (optional for query, defaults to 5) */ - topK: z.number().min(1).max(50).optional(), - /** Chunking configuration (optional for create) */ - chunkingConfig: z - .object({ - maxSize: z.number().min(100).max(4000).default(1024), - minSize: z.number().min(1).max(2000).default(1), - overlap: z.number().min(0).max(500).default(200), - }) - .optional(), - /** Tag definition ID (required for update_tag, delete_tag) */ - tagDefinitionId: z.string().optional(), - /** Tag display name (required for create_tag, optional for update_tag) */ - tagDisplayName: z.string().optional(), - /** Tag field type: text, number, date, boolean (optional for create_tag, defaults to text) */ - tagFieldType: z.enum(['text', 'number', 'date', 'boolean']).optional(), - /** Connector type from registry, e.g. "confluence" (required for add_connector) */ - connectorType: z.string().optional(), - /** OAuth credential ID from environment/credentials.json (required for OAuth connectors) */ - credentialId: z.string().optional(), - /** API key for API key-based connectors (required for API key connectors) */ - apiKey: z.string().optional(), - /** Connector-specific config matching the schema in knowledgebases/connectors/{type}.json */ - sourceConfig: z.record(z.unknown()).optional(), - /** Sync interval: 60, 360, 1440, 10080, or 0 for manual only (optional for add_connector, defaults to 1440) */ - syncIntervalMinutes: z.number().int().min(0).optional(), - /** Connector ID (required for update_connector, delete_connector, sync_connector) */ - connectorId: z.string().optional(), - /** Connector status: "active" or "paused" (optional for update_connector) */ - connectorStatus: z.enum(['active', 'paused']).optional(), - /** Tag definition IDs to disable (optional for add_connector) */ - disabledTagIds: z.array(z.string()).optional(), - /** Document ID (required for delete_document, update_document) */ - documentId: z.string().optional(), - /** Enable/disable a document (optional for update_document) */ - enabled: z.boolean().optional(), - /** New filename for a document (optional for update_document) */ - filename: z.string().optional(), - }) - .optional(), -}) -export type KnowledgeBaseArgs = z.infer - -export const KnowledgeBaseResultSchema = z.object({ - success: z.boolean(), - message: z.string(), - data: z.any().optional(), -}) -export type KnowledgeBaseResult = z.infer - -// user_table - shared schema used by server tool and registry -export const UserTableArgsSchema = z.object({ - operation: z.enum([ - 'create', - 'create_from_file', - 'import_file', - 'get', - 'get_schema', - 'delete', - 'insert_row', - 'batch_insert_rows', - 'get_row', - 'query_rows', - 'update_row', - 'delete_row', - 'update_rows_by_filter', - 'delete_rows_by_filter', - 'batch_update_rows', - 'batch_delete_rows', - 'add_column', - 'rename_column', - 'delete_column', - 'update_column', - 'rename', - ]), - args: z - .object({ - name: z.string().optional(), - description: z.string().optional(), - schema: z.any().optional(), - tableId: z.string().optional(), - rowId: z.string().optional(), - data: z.record(z.any()).optional(), - rows: z.array(z.record(z.any())).optional(), - updates: z.array(z.object({ rowId: z.string(), data: z.record(z.any()) })).optional(), - rowIds: z.array(z.string()).optional(), - values: z.record(z.any()).optional(), - filter: z.any().optional(), - sort: z.record(z.enum(['asc', 'desc'])).optional(), - limit: z.number().optional(), - offset: z.number().optional(), - fileId: z.string().optional(), - filePath: z.string().optional(), - column: z - .object({ - name: z.string(), - type: z.string(), - unique: z.boolean().optional(), - position: z.number().optional(), - }) - .optional(), - columnName: z.string().optional(), - columnNames: z.array(z.string()).optional(), - newName: z.string().optional(), - newType: z.string().optional(), - unique: z.boolean().optional(), - }) - .optional(), -}) -export type UserTableArgs = z.infer - -export const UserTableResultSchema = z.object({ - success: z.boolean(), - message: z.string(), - data: z.any().optional(), -}) -export type UserTableResult = z.infer - -// workspace_file - shared schema used by server tool and Go catalog -export const WorkspaceFileArgsSchema = z.object({ - operation: z.enum(['write', 'update', 'delete', 'rename', 'patch']), - args: z - .object({ - fileId: z.string().optional(), - fileName: z.string().optional(), - content: z.string().optional(), - contentType: z.string().optional(), - workspaceId: z.string().optional(), - newName: z.string().optional(), - edits: z - .array(z.object({ search: z.string(), replace: z.string() })) - .describe( - 'List of search/replace pairs applied sequentially — each edit operates on the result of the previous one. Search strings must be unique within the file.' - ) - .optional(), - }) - .optional(), -}) -export type WorkspaceFileArgs = z.infer - -export const WorkspaceFileResultSchema = z.object({ - success: z.boolean(), - message: z.string(), - data: z.any().optional(), -}) -export type WorkspaceFileResult = z.infer - -export const GetBlockOutputsInput = z.object({ - blockIds: z.array(z.string()).optional(), -}) -export const GetBlockOutputsResult = z.object({ - blocks: z.array( - z.object({ - blockId: z.string(), - blockName: z.string(), - blockType: z.string(), - triggerMode: z.boolean().optional(), - outputs: z.array(z.string()), - insideSubflowOutputs: z.array(z.string()).optional(), - outsideSubflowOutputs: z.array(z.string()).optional(), - }) - ), - variables: z - .array( - z.object({ - id: z.string(), - name: z.string(), - type: z.string(), - tag: z.string(), - }) - ) - .optional(), -}) -export type GetBlockOutputsInputType = z.infer -export type GetBlockOutputsResultType = z.infer - -export const GetBlockUpstreamReferencesInput = z.object({ - blockIds: z.array(z.string()).min(1), -}) -export const GetBlockUpstreamReferencesResult = z.object({ - results: z.array( - z.object({ - blockId: z.string(), - blockName: z.string(), - insideSubflows: z - .array( - z.object({ - blockId: z.string(), - blockName: z.string(), - blockType: z.string(), - }) - ) - .optional(), - accessibleBlocks: z.array( - z.object({ - blockId: z.string(), - blockName: z.string(), - blockType: z.string(), - triggerMode: z.boolean().optional(), - outputs: z.array(z.string()), - accessContext: z.enum(['inside', 'outside']).optional(), - }) - ), - variables: z.array( - z.object({ - id: z.string(), - name: z.string(), - type: z.string(), - tag: z.string(), - }) - ), - }) - ), -}) -export type GetBlockUpstreamReferencesInputType = z.infer -export type GetBlockUpstreamReferencesResultType = z.infer diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index ac94726090..59a7cfb900 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -204,6 +204,49 @@ const EXTENSION_TO_MIME: Record = { yml: 'application/x-yaml', rtf: 'application/rtf', + // Code / plain-text source + py: 'text/x-python', + js: 'text/javascript', + mjs: 'text/javascript', + cjs: 'text/javascript', + ts: 'text/typescript', + tsx: 'text/typescript', + jsx: 'text/javascript', + go: 'text/x-go', + rs: 'text/x-rust', + java: 'text/x-java', + kt: 'text/x-kotlin', + c: 'text/x-c', + cpp: 'text/x-c++', + h: 'text/x-c', + hpp: 'text/x-c++', + cs: 'text/x-csharp', + rb: 'text/x-ruby', + php: 'text/x-php', + swift: 'text/x-swift', + sh: 'text/x-shellscript', + bash: 'text/x-shellscript', + zsh: 'text/x-shellscript', + r: 'text/x-r', + sql: 'text/x-sql', + scala: 'text/x-scala', + lua: 'text/x-lua', + pl: 'text/x-perl', + toml: 'text/x-toml', + ini: 'text/plain', + cfg: 'text/plain', + conf: 'text/plain', + env: 'text/plain', + log: 'text/plain', + makefile: 'text/x-makefile', + dockerfile: 'text/x-dockerfile', + css: 'text/css', + scss: 'text/x-scss', + less: 'text/x-less', + graphql: 'text/x-graphql', + gql: 'text/x-graphql', + proto: 'text/x-protobuf', + // Audio mp3: 'audio/mpeg', m4a: 'audio/mp4', diff --git a/scripts/sync-tool-catalog.ts b/scripts/sync-tool-catalog.ts index 78893a6a67..657de6373f 100644 --- a/scripts/sync-tool-catalog.ts +++ b/scripts/sync-tool-catalog.ts @@ -9,6 +9,10 @@ const DEFAULT_CATALOG_PATH = resolve( '../copilot/copilot/contracts/tool-catalog-v1.json' ) const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/tool-catalog-v1.ts') +const RUNTIME_SCHEMA_OUTPUT_PATH = resolve( + ROOT, + 'apps/sim/lib/copilot/generated/tool-schemas-v1.ts' +) function snakeToPascal(s: string): string { return s.split('_').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('') @@ -22,7 +26,43 @@ function inferTSType(values: unknown[]): string { } if (unique.every((v) => typeof v === 'boolean')) return 'boolean' if (unique.every((v) => typeof v === 'number')) return 'number' - return 'string' + return 'unknown' +} + +function renderRuntimeSchemaModule(catalog: { tools: Record[] }): string { + const lines: string[] = [ + '// AUTO-GENERATED FILE. DO NOT EDIT.', + '// Generated from copilot/contracts/tool-catalog-v1.json', + '//', + '', + 'export type JsonSchema = unknown', + '', + 'export interface ToolRuntimeSchemaEntry {', + ' parameters?: JsonSchema;', + ' resultSchema?: JsonSchema;', + '}', + '', + 'export const TOOL_RUNTIME_SCHEMAS: Record = {', + ] + + for (const tool of catalog.tools) { + const id = JSON.stringify(tool.id) + const parameters = 'parameters' in tool ? JSON.stringify(tool.parameters ?? null, null, 2) : 'undefined' + const resultSchema = + 'resultSchema' in tool ? JSON.stringify(tool.resultSchema ?? null, null, 2) : 'undefined' + lines.push(` [${id}]: {`) + lines.push( + ` parameters: ${parameters === 'null' ? 'undefined' : parameters.replace(/\n/g, '\n ')},` + ) + lines.push( + ` resultSchema: ${resultSchema === 'null' ? 'undefined' : resultSchema.replace(/\n/g, '\n ')},` + ) + lines.push(' },') + } + + lines.push('}') + lines.push('') + return lines.join('\n') } function generateInterface(tools: Record[]): string { @@ -95,10 +135,12 @@ async function main() { lines.push('') const rendered = lines.join('\n') + const runtimeSchemaRendered = renderRuntimeSchemaModule(catalog) if (checkOnly) { const existing = await readFile(OUTPUT_PATH, 'utf8').catch(() => null) - if (existing !== rendered) { + const existingRuntime = await readFile(RUNTIME_SCHEMA_OUTPUT_PATH, 'utf8').catch(() => null) + if (existing !== rendered || existingRuntime !== runtimeSchemaRendered) { throw new Error( `Generated tool catalog is stale. Run: bun run mship-tools:generate` ) @@ -108,6 +150,8 @@ async function main() { await mkdir(dirname(OUTPUT_PATH), { recursive: true }) await writeFile(OUTPUT_PATH, rendered, 'utf8') + await mkdir(dirname(RUNTIME_SCHEMA_OUTPUT_PATH), { recursive: true }) + await writeFile(RUNTIME_SCHEMA_OUTPUT_PATH, runtimeSchemaRendered, 'utf8') } await main()