Compare commits

..

5 Commits

Author SHA1 Message Date
Siddharth Ganesan
f67caf0798 Fix tests 2025-12-15 10:56:33 -08:00
Siddharth Ganesan
0b853a7d95 Fixes 2025-12-15 10:33:41 -08:00
Vikhyath Mondreti
4f31560a0e use isCallEndRef correctly 2025-12-13 12:50:24 -08:00
Vikhyath Mondreti
8b5027f2a6 Merge branch 'staging' into fix/chat-tools 2025-12-13 12:34:12 -08:00
Siddharth Ganesan
c6c658a6e1 Fix chat tools 2025-12-13 11:56:56 -08:00
54 changed files with 599 additions and 645 deletions

View File

@@ -80,6 +80,10 @@ export function VoiceInterface({
const currentStateRef = useRef<'idle' | 'listening' | 'agent_speaking'>('idle')
const isCallEndedRef = useRef(false)
useEffect(() => {
isCallEndedRef.current = false
}, [])
useEffect(() => {
currentStateRef.current = state
}, [state])
@@ -119,6 +123,8 @@ export function VoiceInterface({
}, [])
useEffect(() => {
if (isCallEndedRef.current) return
if (isPlayingAudio && state !== 'agent_speaking') {
clearResponseTimeout()
setState('agent_speaking')
@@ -139,6 +145,9 @@ export function VoiceInterface({
}
}
} else if (!isPlayingAudio && state === 'agent_speaking') {
// Don't unmute/restart if call has ended
if (isCallEndedRef.current) return
setState('idle')
setCurrentTranscript('')
@@ -226,6 +235,8 @@ export function VoiceInterface({
recognition.onstart = () => {}
recognition.onresult = (event: SpeechRecognitionEvent) => {
if (isCallEndedRef.current) return
const currentState = currentStateRef.current
if (isMutedRef.current || currentState !== 'listening') {
@@ -303,6 +314,8 @@ export function VoiceInterface({
}, [isSupported, onVoiceTranscript, setResponseTimeout])
const startListening = useCallback(() => {
if (isCallEndedRef.current) return
if (!isInitialized || isMuted || state !== 'idle') {
return
}
@@ -320,6 +333,9 @@ export function VoiceInterface({
}, [isInitialized, isMuted, state])
const stopListening = useCallback(() => {
// Don't process if call has ended
if (isCallEndedRef.current) return
setState('idle')
setCurrentTranscript('')
@@ -333,12 +349,15 @@ export function VoiceInterface({
}, [])
const handleInterrupt = useCallback(() => {
if (isCallEndedRef.current) return
if (state === 'agent_speaking') {
onInterrupt?.()
setState('listening')
setCurrentTranscript('')
setIsMuted(false)
isMutedRef.current = false
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
track.enabled = true
@@ -356,11 +375,22 @@ export function VoiceInterface({
}, [state, onInterrupt])
const handleCallEnd = useCallback(() => {
// Mark call as ended FIRST to prevent any effects from restarting recognition
isCallEndedRef.current = true
// Set muted to true to prevent auto-start effect from triggering
setIsMuted(true)
isMutedRef.current = true
setState('idle')
setCurrentTranscript('')
setIsMuted(false)
// Immediately disable audio tracks to stop listening
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
track.enabled = false
})
}
if (recognitionRef.current) {
try {
@@ -377,6 +407,8 @@ export function VoiceInterface({
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (isCallEndedRef.current) return
if (event.code === 'Space') {
event.preventDefault()
handleInterrupt()
@@ -388,6 +420,8 @@ export function VoiceInterface({
}, [handleInterrupt])
const toggleMute = useCallback(() => {
if (isCallEndedRef.current) return
if (state === 'agent_speaking') {
handleInterrupt()
return
@@ -395,6 +429,7 @@ export function VoiceInterface({
const newMutedState = !isMuted
setIsMuted(newMutedState)
isMutedRef.current = newMutedState
if (mediaStreamRef.current) {
mediaStreamRef.current.getAudioTracks().forEach((track) => {
@@ -417,6 +452,8 @@ export function VoiceInterface({
}, [isSupported, setupSpeechRecognition, setupAudio])
useEffect(() => {
if (isCallEndedRef.current) return
if (isInitialized && !isMuted && state === 'idle') {
startListening()
}

View File

@@ -41,16 +41,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
],
placeholder: 'Select Airtable account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'baseId',
@@ -134,8 +124,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
}
},
params: (params) => {
const { credential, accessToken, records, fields, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const { credential, records, fields, ...rest } = params
let parsedRecords: any | undefined
let parsedFields: any | undefined
@@ -153,7 +142,7 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
// Construct parameters based on operation
const baseParams = {
...authParam,
credential,
...rest,
}

View File

@@ -32,20 +32,11 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
id: 'credential',
title: 'Asana Account',
type: 'oauth-input',
required: true,
serviceId: 'asana',
requiredScopes: ['default'],
placeholder: 'Select Asana account',
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'workspace',
@@ -211,7 +202,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
},
params: (params) => {
const { credential, accessToken, operation } = params
const { credential, operation } = params
const projectsArray = params.projects
? params.projects
@@ -220,12 +211,14 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
.filter((p: string) => p.length > 0)
: undefined
const authParam = credential ? { credential } : { accessToken }
const baseParams = {
accessToken: credential?.accessToken,
}
switch (operation) {
case 'get_task':
return {
...authParam,
...baseParams,
taskGid: params.taskGid,
workspace: params.getTasks_workspace,
project: params.getTasks_project,
@@ -233,7 +226,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'create_task':
return {
...authParam,
...baseParams,
workspace: params.workspace,
name: params.name,
notes: params.notes,
@@ -242,7 +235,7 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'update_task':
return {
...authParam,
...baseParams,
taskGid: params.taskGid,
name: params.name,
notes: params.notes,
@@ -252,12 +245,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'get_projects':
return {
...authParam,
...baseParams,
workspace: params.workspace,
}
case 'search_tasks':
return {
...authParam,
...baseParams,
workspace: params.workspace,
text: params.searchText,
assignee: params.assignee,
@@ -266,12 +259,12 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
}
case 'add_comment':
return {
...authParam,
...baseParams,
taskGid: params.taskGid,
text: params.commentText,
}
default:
return authParam
return baseParams
}
},
},

View File

@@ -76,16 +76,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
],
placeholder: 'Select Confluence account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'pageId',
@@ -265,7 +255,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
params: (params) => {
const {
credential,
accessToken,
pageId,
manualPageId,
operation,
@@ -275,7 +264,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
...rest
} = params
const authParam = credential ? { credential } : { accessToken }
const effectivePageId = (pageId || manualPageId || '').trim()
const requiresPageId = [
@@ -301,7 +289,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
if (operation === 'upload_attachment') {
return {
...authParam,
credential,
pageId: effectivePageId,
operation,
file: attachmentFile,
@@ -312,7 +300,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
}
return {
...authParam,
credential,
pageId: effectivePageId || undefined,
operation,
...rest,

View File

@@ -49,16 +49,6 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
],
placeholder: 'Select Dropbox account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Upload operation inputs
{

View File

@@ -38,7 +38,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
],
value: () => 'send_gmail',
},
// Gmail Credentials (basic mode)
// Gmail Credentials
{
id: 'credential',
title: 'Gmail Account',
@@ -51,17 +51,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
],
placeholder: 'Select Gmail account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Send Email Fields
{
@@ -388,7 +377,6 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
params: (params) => {
const {
credential,
accessToken,
folder,
manualFolder,
destinationLabel,
@@ -449,7 +437,7 @@ export const GmailBlock: BlockConfig<GmailToolResponse> = {
return {
...rest,
...(credential ? { credential } : { accessToken }),
credential,
}
},
},

View File

@@ -28,7 +28,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
],
value: () => 'create',
},
// Google Calendar Credentials (basic mode)
{
id: 'credential',
title: 'Google Calendar Account',
@@ -37,17 +36,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
serviceId: 'google-calendar',
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
placeholder: 'Select Google Calendar account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Calendar selector (basic mode)
{
@@ -61,7 +49,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
dependsOn: ['credential'],
mode: 'basic',
},
// Manual calendar ID input (advanced mode) - no dependsOn needed for text input
// Manual calendar ID input (advanced mode)
{
id: 'manualCalendarId',
title: 'Calendar ID',
@@ -225,7 +213,6 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
params: (params) => {
const {
credential,
accessToken,
operation,
attendees,
replaceExisting,
@@ -266,7 +253,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
}
return {
...(credential ? { credential } : { accessToken }),
credential,
...processedParams,
}
},

View File

@@ -27,7 +27,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
],
value: () => 'read',
},
// Google Docs Credentials (basic mode)
// Google Docs Credentials
{
id: 'credential',
title: 'Google Account',
@@ -39,17 +39,6 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Document selector (basic mode)
{
@@ -72,6 +61,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
type: 'short-input',
canonicalParamId: 'documentId',
placeholder: 'Enter document ID',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: ['read', 'write'] },
},
@@ -105,6 +95,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create' },
},
@@ -142,15 +133,8 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
}
},
params: (params) => {
const {
credential,
accessToken,
documentId,
manualDocumentId,
folderSelector,
folderId,
...rest
} = params
const { credential, documentId, manualDocumentId, folderSelector, folderId, ...rest } =
params
const effectiveDocumentId = (documentId || manualDocumentId || '').trim()
const effectiveFolderId = (folderSelector || folderId || '').trim()
@@ -159,7 +143,7 @@ export const GoogleDocsBlock: BlockConfig<GoogleDocsResponse> = {
...rest,
documentId: effectiveDocumentId || undefined,
folderId: effectiveFolderId || undefined,
...(credential ? { credential } : { accessToken }),
credential,
}
},
},

View File

@@ -28,7 +28,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
],
value: () => 'create_folder',
},
// Google Drive Credentials (basic mode)
// Google Drive Credentials
{
id: 'credential',
title: 'Google Drive Account',
@@ -40,17 +40,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google Drive account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Create/Upload File Fields
{
@@ -335,7 +324,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
params: (params) => {
const {
credential,
accessToken,
folderSelector,
manualFolderId,
fileSelector,
@@ -351,7 +339,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
const effectiveFileId = (fileSelector || manualFileId || '').trim()
return {
...(credential ? { credential } : { accessToken }),
credential,
folderId: effectiveFolderId || undefined,
fileId: effectiveFileId || undefined,
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,

View File

@@ -13,7 +13,6 @@ export const GoogleFormsBlock: BlockConfig = {
bgColor: '#E0E0E0',
icon: GoogleFormsIcon,
subBlocks: [
// Google Forms Credentials (basic mode)
{
id: 'credential',
title: 'Google Account',
@@ -26,17 +25,6 @@ export const GoogleFormsBlock: BlockConfig = {
'https://www.googleapis.com/auth/forms.responses.readonly',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'formId',
@@ -44,6 +32,7 @@ export const GoogleFormsBlock: BlockConfig = {
type: 'short-input',
required: true,
placeholder: 'Enter the Google Form ID',
dependsOn: ['credential'],
},
{
id: 'responseId',
@@ -64,7 +53,7 @@ export const GoogleFormsBlock: BlockConfig = {
config: {
tool: () => 'google_forms_get_responses',
params: (params) => {
const { credential, accessToken, formId, responseId, pageSize, ...rest } = params
const { credential, formId, responseId, pageSize, ...rest } = params
const effectiveFormId = String(formId || '').trim()
if (!effectiveFormId) {
@@ -76,7 +65,7 @@ export const GoogleFormsBlock: BlockConfig = {
formId: effectiveFormId,
responseId: responseId ? String(responseId).trim() : undefined,
pageSize: pageSize ? Number(pageSize) : undefined,
...(credential ? { credential } : { accessToken }),
credential,
}
},
},

View File

@@ -33,7 +33,6 @@ export const GoogleGroupsBlock: BlockConfig = {
],
value: () => 'list_groups',
},
// Google Groups Credentials (basic mode)
{
id: 'credential',
title: 'Google Groups Account',
@@ -45,17 +44,6 @@ export const GoogleGroupsBlock: BlockConfig = {
'https://www.googleapis.com/auth/admin.directory.group.member',
],
placeholder: 'Select Google Workspace account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
@@ -233,13 +221,12 @@ export const GoogleGroupsBlock: BlockConfig = {
}
},
params: (params) => {
const { credential, accessToken, operation, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const { credential, operation, ...rest } = params
switch (operation) {
case 'list_groups':
return {
...authParam,
credential,
customer: rest.customer,
domain: rest.domain,
query: rest.query,
@@ -248,19 +235,19 @@ export const GoogleGroupsBlock: BlockConfig = {
case 'get_group':
case 'delete_group':
return {
...authParam,
credential,
groupKey: rest.groupKey,
}
case 'create_group':
return {
...authParam,
credential,
email: rest.email,
name: rest.name,
description: rest.description,
}
case 'update_group':
return {
...authParam,
credential,
groupKey: rest.groupKey,
name: rest.newName,
email: rest.newEmail,
@@ -268,7 +255,7 @@ export const GoogleGroupsBlock: BlockConfig = {
}
case 'list_members':
return {
...authParam,
credential,
groupKey: rest.groupKey,
maxResults: rest.maxResults ? Number(rest.maxResults) : undefined,
roles: rest.roles,
@@ -276,32 +263,32 @@ export const GoogleGroupsBlock: BlockConfig = {
case 'get_member':
case 'remove_member':
return {
...authParam,
credential,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
}
case 'add_member':
return {
...authParam,
credential,
groupKey: rest.groupKey,
email: rest.memberEmail,
role: rest.role,
}
case 'update_member':
return {
...authParam,
credential,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
role: rest.role,
}
case 'has_member':
return {
...authParam,
credential,
groupKey: rest.groupKey,
memberKey: rest.memberKey,
}
default:
return { ...(credential ? { credential } : { accessToken }), ...rest }
return { credential, ...rest }
}
},
},

View File

@@ -28,7 +28,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
],
value: () => 'read',
},
// Google Sheets Credentials (basic mode)
// Google Sheets Credentials
{
id: 'credential',
title: 'Google Account',
@@ -40,17 +40,6 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Spreadsheet Selector
{
@@ -75,6 +64,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
type: 'short-input',
canonicalParamId: 'spreadsheetId',
placeholder: 'ID of the spreadsheet (from URL)',
dependsOn: ['credential'],
mode: 'advanced',
},
// Range
@@ -178,8 +168,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
}
},
params: (params) => {
const { credential, accessToken, values, spreadsheetId, manualSpreadsheetId, ...rest } =
params
const { credential, values, spreadsheetId, manualSpreadsheetId, ...rest } = params
const parsedValues = values ? JSON.parse(values as string) : undefined
@@ -193,7 +182,7 @@ export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
...rest,
spreadsheetId: effectiveSpreadsheetId,
values: parsedValues,
...(credential ? { credential } : { accessToken }),
credential,
}
},
},

View File

@@ -31,7 +31,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
],
value: () => 'read',
},
// Google Slides Credentials (basic mode)
// Google Slides Credentials
{
id: 'credential',
title: 'Google Account',
@@ -43,17 +43,6 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
'https://www.googleapis.com/auth/drive',
],
placeholder: 'Select Google account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Presentation selector (basic mode) - for operations that need an existing presentation
{
@@ -79,6 +68,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
type: 'short-input',
canonicalParamId: 'presentationId',
placeholder: 'Enter presentation ID',
dependsOn: ['credential'],
mode: 'advanced',
condition: {
field: 'operation',
@@ -133,6 +123,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create' },
},
@@ -325,7 +316,6 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
params: (params) => {
const {
credential,
accessToken,
presentationId,
manualPresentationId,
folderSelector,
@@ -344,7 +334,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
const result: Record<string, any> = {
...rest,
presentationId: effectivePresentationId || undefined,
...(credential ? { credential } : { accessToken }),
credential,
}
// Handle operation-specific params

View File

@@ -30,7 +30,6 @@ export const GoogleVaultBlock: BlockConfig = {
value: () => 'list_matters_export',
},
// Google Vault Credentials (basic mode)
{
id: 'credential',
title: 'Google Vault Account',
@@ -42,17 +41,6 @@ export const GoogleVaultBlock: BlockConfig = {
'https://www.googleapis.com/auth/devstorage.read_only',
],
placeholder: 'Select Google Vault account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Create Hold inputs
{
@@ -230,10 +218,10 @@ export const GoogleVaultBlock: BlockConfig = {
}
},
params: (params) => {
const { credential, accessToken, ...rest } = params
const { credential, ...rest } = params
return {
...rest,
...(credential ? { credential } : { accessToken }),
credential,
}
},
},

View File

@@ -67,16 +67,6 @@ export const HubSpotBlock: BlockConfig<HubSpotResponse> = {
],
placeholder: 'Select HubSpot account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'contactId',
@@ -834,7 +824,6 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
params: (params) => {
const {
credential,
accessToken,
operation,
propertiesToSet,
properties,
@@ -845,9 +834,8 @@ Return ONLY the JSON array of property names - no explanations, no markdown, no
...rest
} = params
const authParam = credential ? { credential } : { accessToken }
const cleanParams: Record<string, any> = {
...authParam,
credential,
}
const createUpdateOps = [

View File

@@ -92,16 +92,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
'delete:issue-link:jira',
],
placeholder: 'Select Jira account',
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Project selector (basic mode)
{
@@ -453,23 +443,14 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
}
},
params: (params) => {
const {
credential,
accessToken,
projectId,
manualProjectId,
issueKey,
manualIssueKey,
...rest
} = params
const { credential, projectId, manualProjectId, issueKey, manualIssueKey, ...rest } = params
// Use the selected IDs or the manually entered ones
const effectiveProjectId = (projectId || manualProjectId || '').trim()
const effectiveIssueKey = (issueKey || manualIssueKey || '').trim()
const authParam = credential ? { credential } : { accessToken }
const baseParams = {
...authParam,
credential,
domain: params.domain,
}

View File

@@ -133,16 +133,6 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
requiredScopes: ['read', 'write'],
placeholder: 'Select Linear account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Team selector (for most operations)
{
@@ -1271,14 +1261,9 @@ export const LinearBlock: BlockConfig<LinearResponse> = {
const effectiveTeamId = (params.teamId || params.manualTeamId || '').trim()
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
// Auth param handling
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
// Base params that most operations need
const baseParams: Record<string, any> = {
...authParam,
credential: params.credential,
}
// Operation-specific param mapping

View File

@@ -27,7 +27,7 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
value: () => 'share_post',
},
// LinkedIn OAuth Authentication (basic mode)
// LinkedIn OAuth Authentication
{
id: 'credential',
title: 'LinkedIn Account',
@@ -35,17 +35,6 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
serviceId: 'linkedin',
requiredScopes: ['profile', 'openid', 'email', 'w_member_social'],
placeholder: 'Select LinkedIn account',
mode: 'basic',
required: true,
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
@@ -91,18 +80,18 @@ export const LinkedInBlock: BlockConfig<LinkedInResponse> = {
},
params: (inputs) => {
const operation = inputs.operation || 'share_post'
const { credential, accessToken, ...rest } = inputs
const authParam = credential ? { credential } : { accessToken }
const { credential, ...rest } = inputs
if (operation === 'get_profile') {
return authParam
return {
accessToken: credential,
}
}
return {
text: rest.text,
visibility: rest.visibility || 'PUBLIC',
...authParam,
accessToken: credential,
}
},
},

View File

@@ -27,7 +27,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
],
value: () => 'read',
},
// Microsoft Excel Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -43,17 +42,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
],
placeholder: 'Select Microsoft account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'spreadsheetId',
@@ -73,6 +61,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
type: 'short-input',
canonicalParamId: 'spreadsheetId',
placeholder: 'Enter spreadsheet ID',
dependsOn: ['credential'],
mode: 'advanced',
},
{
@@ -171,7 +160,6 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
params: (params) => {
const {
credential,
accessToken,
values,
spreadsheetId,
manualSpreadsheetId,
@@ -205,7 +193,7 @@ export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
...rest,
spreadsheetId: effectiveSpreadsheetId,
values: parsedValues,
...(credential ? { credential } : { accessToken }),
credential,
}
if (params.operation === 'table_add') {

View File

@@ -57,7 +57,6 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
{ label: 'Update Task Details', id: 'update_task_details' },
],
},
// Microsoft Planner Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -73,17 +72,6 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
'offline_access',
],
placeholder: 'Select Microsoft account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Plan ID - for various operations
@@ -354,7 +342,7 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
const baseParams: MicrosoftPlannerBlockParams = {
...rest,
...(credential ? { credential } : { accessToken }),
credential,
}
// Handle different task ID fields

View File

@@ -39,7 +39,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
],
value: () => 'read_chat',
},
// Microsoft Teams Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -69,17 +68,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
],
placeholder: 'Select Microsoft account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'teamId',
@@ -327,7 +315,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
params: (params) => {
const {
credential,
accessToken,
operation,
teamId,
manualTeamId,
@@ -349,7 +336,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
const baseParams: Record<string, any> = {
...rest,
...(credential ? { credential } : { accessToken }),
credential,
}
if ((operation === 'read_chat' || operation === 'read_channel') && includeAttachments) {

View File

@@ -38,16 +38,6 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
requiredScopes: ['workspace.content', 'workspace.name', 'page.read', 'page.write'],
placeholder: 'Select Notion account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Read/Write operation - Page ID
{
@@ -232,8 +222,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
}
},
params: (params) => {
const { credential, accessToken, operation, properties, filter, sorts, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const { credential, operation, properties, filter, sorts, ...rest } = params
// Parse properties from JSON string for create operations
let parsedProperties
@@ -276,7 +265,7 @@ export const NotionBlock: BlockConfig<NotionResponse> = {
return {
...rest,
...authParam,
credential,
...(parsedProperties ? { properties: parsedProperties } : {}),
...(parsedFilter ? { filter: JSON.stringify(parsedFilter) } : {}),
...(parsedSorts ? { sorts: JSON.stringify(parsedSorts) } : {}),

View File

@@ -33,7 +33,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
{ label: 'Delete File', id: 'delete' },
],
},
// OneDrive Credentials (basic mode)
// One Drive Credentials
{
id: 'credential',
title: 'Microsoft Account',
@@ -48,17 +48,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
'offline_access',
],
placeholder: 'Select Microsoft account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Create File Fields
{
@@ -175,6 +164,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: ['create_file', 'upload'] },
},
@@ -212,6 +202,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter parent folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create_folder' },
},
@@ -243,6 +234,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
type: 'short-input',
canonicalParamId: 'folderId',
placeholder: 'Enter folder ID (leave empty for root folder)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'list' },
},
@@ -360,16 +352,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
}
},
params: (params) => {
const {
credential,
accessToken,
folderId,
fileId,
mimeType,
values,
downloadFileName,
...rest
} = params
const { credential, folderId, fileId, mimeType, values, downloadFileName, ...rest } = params
let normalizedValues: ReturnType<typeof normalizeExcelValuesForToolParams>
if (values !== undefined) {
@@ -377,7 +360,7 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
}
return {
...(credential ? { credential } : { accessToken }),
credential,
...rest,
values: normalizedValues,
folderId: folderId || undefined,

View File

@@ -34,7 +34,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
],
value: () => 'send_outlook',
},
// Microsoft Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -52,17 +51,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
],
placeholder: 'Select Microsoft account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'to',
@@ -338,7 +326,6 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
params: (params) => {
const {
credential,
accessToken,
folder,
manualFolder,
destinationFolder,
@@ -391,7 +378,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
return {
...rest,
...(credential ? { credential } : { accessToken }),
credential,
}
},
},

View File

@@ -57,16 +57,6 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
],
placeholder: 'Select Pipedrive account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'status',
@@ -670,11 +660,10 @@ export const PipedriveBlock: BlockConfig<PipedriveResponse> = {
}
},
params: (params) => {
const { credential, accessToken, operation, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const { credential, operation, ...rest } = params
const cleanParams: Record<string, any> = {
...authParam,
credential,
}
Object.entries(rest).forEach(([key, value]) => {

View File

@@ -37,7 +37,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
value: () => 'get_posts',
},
// Reddit OAuth Authentication (basic mode)
// Reddit OAuth Authentication
{
id: 'credential',
title: 'Reddit Account',
@@ -63,17 +63,6 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
],
placeholder: 'Select Reddit account',
required: true,
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Common fields - appear for all actions
@@ -566,9 +555,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
},
params: (inputs) => {
const operation = inputs.operation || 'get_posts'
const { credential, accessToken, ...rest } = inputs
const authParam = credential ? { credential } : { accessToken }
const { credential, ...rest } = inputs
if (operation === 'get_comments') {
return {
@@ -576,7 +563,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
subreddit: rest.subreddit,
sort: rest.commentSort,
limit: rest.commentLimit ? Number.parseInt(rest.commentLimit) : undefined,
...authParam,
credential: credential,
}
}
@@ -585,7 +572,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
subreddit: rest.subreddit,
time: rest.controversialTime,
limit: rest.controversialLimit ? Number.parseInt(rest.controversialLimit) : undefined,
...authParam,
credential: credential,
}
}
@@ -596,7 +583,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
sort: rest.searchSort,
time: rest.searchTime,
limit: rest.searchLimit ? Number.parseInt(rest.searchLimit) : undefined,
...authParam,
credential: credential,
}
}
@@ -608,7 +595,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
url: rest.postType === 'link' ? rest.url : undefined,
nsfw: rest.nsfw === 'true',
spoiler: rest.spoiler === 'true',
...authParam,
credential: credential,
}
}
@@ -616,7 +603,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
id: rest.voteId,
dir: Number.parseInt(rest.voteDirection),
...authParam,
credential: credential,
}
}
@@ -624,14 +611,14 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
id: rest.saveId,
category: rest.saveCategory,
...authParam,
credential: credential,
}
}
if (operation === 'unsave') {
return {
id: rest.saveId,
...authParam,
credential: credential,
}
}
@@ -639,7 +626,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
parent_id: rest.replyParentId,
text: rest.replyText,
...authParam,
credential: credential,
}
}
@@ -647,14 +634,14 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
thing_id: rest.editThingId,
text: rest.editText,
...authParam,
credential: credential,
}
}
if (operation === 'delete') {
return {
id: rest.deleteId,
...authParam,
credential: credential,
}
}
@@ -662,7 +649,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
return {
subreddit: rest.subscribeSubreddit,
action: rest.subscribeAction,
...authParam,
credential: credential,
}
}
@@ -671,7 +658,7 @@ export const RedditBlock: BlockConfig<RedditResponse> = {
sort: rest.sort,
limit: rest.limit ? Number.parseInt(rest.limit) : undefined,
time: rest.sort === 'top' ? rest.time : undefined,
...authParam,
credential: credential,
}
},
},

View File

@@ -66,16 +66,6 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
requiredScopes: ['api', 'refresh_token', 'openid', 'offline_access'],
placeholder: 'Select Salesforce account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Common fields for GET operations
{
@@ -600,9 +590,8 @@ export const SalesforceBlock: BlockConfig<SalesforceResponse> = {
}
},
params: (params) => {
const { credential, accessToken, operation, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const cleanParams: Record<string, any> = { ...authParam }
const { credential, operation, ...rest } = params
const cleanParams: Record<string, any> = { credential }
Object.entries(rest).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
cleanParams[key] = value

View File

@@ -33,7 +33,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
{ label: 'Upload File', id: 'upload_file' },
],
},
// SharePoint Credentials (basic mode)
{
id: 'credential',
title: 'Microsoft Account',
@@ -49,17 +48,6 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
'offline_access',
],
placeholder: 'Select Microsoft account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
@@ -167,6 +155,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
type: 'short-input',
canonicalParamId: 'siteId',
placeholder: 'Enter site ID (leave empty for root site)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create_page' },
},
@@ -266,7 +255,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
}
},
params: (params) => {
const { credential, accessToken, siteSelector, manualSiteId, mimeType, ...rest } = params
const { credential, siteSelector, manualSiteId, mimeType, ...rest } = params
const effectiveSiteId = (siteSelector || manualSiteId || '').trim()
@@ -326,7 +315,7 @@ export const SharepointBlock: BlockConfig<SharepointResponse> = {
// Handle file upload files parameter
const fileParam = uploadFiles || files
const baseParams = {
...(credential ? { credential } : { accessToken }),
credential,
siteId: effectiveSiteId || undefined,
pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined,
mimeType: mimeType,

View File

@@ -71,16 +71,6 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
],
placeholder: 'Select Shopify account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'shopDomain',
@@ -536,11 +526,8 @@ export const ShopifyBlock: BlockConfig<ShopifyResponse> = {
return params.operation || 'shopify_list_products'
},
params: (params) => {
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
const baseParams: Record<string, unknown> = {
...authParam,
credential: params.credential,
shopDomain: params.shopDomain?.trim(),
}

View File

@@ -159,16 +159,6 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
type: 'oauth-input',
serviceId: 'spotify',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// === SEARCH ===

View File

@@ -45,16 +45,6 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
requiredScopes: ['read', 'write'],
placeholder: 'Select Trello account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
@@ -340,10 +330,9 @@ export const TrelloBlock: BlockConfig<ToolResponse> = {
}
},
params: (params) => {
const { credential, accessToken, operation, limit, closed, dueComplete, ...rest } = params
const { operation, limit, closed, dueComplete, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const result: Record<string, any> = { ...rest, ...authParam }
const result: Record<string, any> = { ...rest }
if (limit && operation === 'trello_get_actions') {
result.limit = Number.parseInt(limit, 10)

View File

@@ -37,16 +37,6 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
requiredScopes: ['login', 'data'],
placeholder: 'Select Wealthbox account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'noteId',
@@ -177,25 +167,16 @@ export const WealthboxBlock: BlockConfig<WealthboxResponse> = {
}
},
params: (params) => {
const {
credential,
accessToken,
operation,
contactId,
manualContactId,
taskId,
manualTaskId,
...rest
} = params
const { credential, operation, contactId, manualContactId, taskId, manualTaskId, ...rest } =
params
const authParam = credential ? { credential } : { accessToken }
// Handle both selector and manual inputs
const effectiveContactId = (contactId || manualContactId || '').trim()
const effectiveTaskId = (taskId || manualTaskId || '').trim()
const baseParams = {
...rest,
...authParam,
credential,
}
if (operation === 'read_note' || operation === 'write_note') {

View File

@@ -38,16 +38,6 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'],
placeholder: 'Select Webflow account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'collectionId',
@@ -118,8 +108,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
}
},
params: (params) => {
const { credential, accessToken, fieldData, ...rest } = params
const authParam = credential ? { credential } : { accessToken }
const { credential, fieldData, ...rest } = params
let parsedFieldData: any | undefined
try {
@@ -131,7 +120,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
}
const baseParams = {
...authParam,
credential,
...rest,
}

View File

@@ -59,7 +59,7 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
value: () => 'wordpress_create_post',
},
// Credential selector for OAuth (basic mode)
// Credential selector for OAuth
{
id: 'credential',
title: 'WordPress Account',
@@ -68,16 +68,6 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
requiredScopes: ['global'],
placeholder: 'Select WordPress account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// Site ID for WordPress.com (required for OAuth)
@@ -675,11 +665,8 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
tool: (params) => params.operation || 'wordpress_create_post',
params: (params) => {
// OAuth authentication for WordPress.com
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
const baseParams: Record<string, any> = {
...authParam,
credential: params.credential,
siteId: params.siteId,
}

View File

@@ -27,7 +27,6 @@ export const XBlock: BlockConfig<XResponse> = {
],
value: () => 'x_write',
},
// X Credentials (basic mode)
{
id: 'credential',
title: 'X Account',
@@ -35,17 +34,6 @@ export const XBlock: BlockConfig<XResponse> = {
serviceId: 'x',
requiredScopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
placeholder: 'Select X account',
mode: 'basic',
},
// Direct access token (advanced mode)
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
{
id: 'text',
@@ -155,9 +143,11 @@ export const XBlock: BlockConfig<XResponse> = {
}
},
params: (params) => {
const { credential, accessToken, ...rest } = params
const { credential, ...rest } = params
const parsedParams: Record<string, any> = credential ? { credential } : { accessToken }
const parsedParams: Record<string, any> = {
credential: credential,
}
Object.keys(rest).forEach((key) => {
const value = rest[key]

View File

@@ -53,16 +53,6 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
],
placeholder: 'Select Zoom account',
required: true,
mode: 'basic',
},
{
id: 'accessToken',
title: 'Access Token',
type: 'short-input',
password: true,
placeholder: 'Enter OAuth access token',
mode: 'advanced',
required: true,
},
// User ID for create/list operations
{
@@ -376,11 +366,8 @@ export const ZoomBlock: BlockConfig<ZoomResponse> = {
return params.operation || 'zoom_create_meeting'
},
params: (params) => {
const authParam = params.credential
? { credential: params.credential }
: { accessToken: params.accessToken }
const baseParams: Record<string, any> = {
...authParam,
credential: params.credential,
}
switch (params.operation) {

View File

@@ -987,18 +987,21 @@ export class AgentBlockHandler implements BlockHandler {
try {
const executionData = JSON.parse(executionDataHeader)
// If execution data contains full content, persist to memory
if (ctx && inputs && executionData.output?.content) {
const assistantMessage: Message = {
role: 'assistant',
content: executionData.output.content,
}
// Fire and forget - don't await
memoryService
.persistMemoryMessage(ctx, inputs, assistantMessage, block.id)
.catch((error) =>
logger.error('Failed to persist streaming response to memory:', error)
// If execution data contains content or tool calls, persist to memory
if (
ctx &&
inputs &&
(executionData.output?.content || executionData.output?.toolCalls?.list?.length)
) {
const toolCalls = executionData.output?.toolCalls?.list
const messages = this.buildMessagesForMemory(executionData.output.content, toolCalls)
// Fire and forget - don't await, persist all messages
Promise.all(
messages.map((message) =>
memoryService.persistMemoryMessage(ctx, inputs, message, block.id)
)
).catch((error) => logger.error('Failed to persist streaming response to memory:', error))
}
return {
@@ -1117,25 +1120,28 @@ export class AgentBlockHandler implements BlockHandler {
return
}
// Extract content from regular response
// Extract content and tool calls from regular response
const blockOutput = result as any
const content = blockOutput?.content
const toolCalls = blockOutput?.toolCalls?.list
if (!content || typeof content !== 'string') {
// Build messages to persist
const messages = this.buildMessagesForMemory(content, toolCalls)
if (messages.length === 0) {
return
}
const assistantMessage: Message = {
role: 'assistant',
content,
// Persist all messages
for (const message of messages) {
await memoryService.persistMemoryMessage(ctx, inputs, message, blockId)
}
await memoryService.persistMemoryMessage(ctx, inputs, assistantMessage, blockId)
logger.debug('Persisted assistant response to memory', {
workflowId: ctx.workflowId,
memoryType: inputs.memoryType,
conversationId: inputs.conversationId,
messageCount: messages.length,
})
} catch (error) {
logger.error('Failed to persist response to memory:', error)
@@ -1143,6 +1149,69 @@ export class AgentBlockHandler implements BlockHandler {
}
}
/**
* Builds messages for memory storage including tool calls and results
* Returns proper OpenAI-compatible message format:
* - Assistant message with tool_calls array (if tools were used)
* - Tool role messages with results (one per tool call)
* - Final assistant message with content (if present)
*/
private buildMessagesForMemory(
content: string | undefined,
toolCalls: any[] | undefined
): Message[] {
const messages: Message[] = []
if (toolCalls?.length) {
// Generate stable IDs for each tool call (only if not provided by provider)
// Use index to ensure uniqueness even for same tool name in same millisecond
const toolCallsWithIds = toolCalls.map((tc: any, index: number) => ({
...tc,
_stableId:
tc.id ||
`call_${tc.name}_${Date.now()}_${index}_${Math.random().toString(36).slice(2, 7)}`,
}))
// Add assistant message with tool_calls
const formattedToolCalls = toolCallsWithIds.map((tc: any) => ({
id: tc._stableId,
type: 'function' as const,
function: {
name: tc.name,
arguments: tc.rawArguments || JSON.stringify(tc.arguments || {}),
},
}))
messages.push({
role: 'assistant',
content: null,
tool_calls: formattedToolCalls,
})
// Add tool result messages using the same stable IDs
for (const tc of toolCallsWithIds) {
const resultContent =
typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result || {})
messages.push({
role: 'tool',
content: resultContent,
tool_call_id: tc._stableId,
name: tc.name, // Store tool name for providers that need it (e.g., Google/Gemini)
})
}
}
// Add final assistant response if present
if (content && typeof content === 'string') {
messages.push({
role: 'assistant',
content,
})
}
return messages
}
private processProviderResponse(
response: any,
block: SerializedBlock,

View File

@@ -32,7 +32,7 @@ describe('Memory', () => {
})
describe('applySlidingWindow (message-based)', () => {
it('should keep last N conversation messages', () => {
it('should keep last N turns (turn = user message + assistant response)', () => {
const messages: Message[] = [
{ role: 'system', content: 'System prompt' },
{ role: 'user', content: 'Message 1' },
@@ -43,9 +43,10 @@ describe('Memory', () => {
{ role: 'assistant', content: 'Response 3' },
]
const result = (memoryService as any).applySlidingWindow(messages, '4')
// Limit to 2 turns: should keep turns 2 and 3
const result = (memoryService as any).applySlidingWindow(messages, '2')
expect(result.length).toBe(5)
expect(result.length).toBe(5) // system + 2 turns (4 messages)
expect(result[0].role).toBe('system')
expect(result[0].content).toBe('System prompt')
expect(result[1].content).toBe('Message 2')
@@ -113,19 +114,18 @@ describe('Memory', () => {
it('should preserve first system message and exclude it from token count', () => {
const messages: Message[] = [
{ role: 'system', content: 'A' }, // System message - always preserved
{ role: 'user', content: 'B' }, // ~1 token
{ role: 'assistant', content: 'C' }, // ~1 token
{ role: 'user', content: 'D' }, // ~1 token
{ role: 'user', content: 'B' }, // ~1 token (turn 1)
{ role: 'assistant', content: 'C' }, // ~1 token (turn 1)
{ role: 'user', content: 'D' }, // ~1 token (turn 2)
]
// Limit to 2 tokens - should fit system message + last 2 conversation messages (D, C)
// Limit to 2 tokens - fits turn 2 (D=1 token), but turn 1 (B+C=2 tokens) would exceed
const result = (memoryService as any).applySlidingWindowByTokens(messages, '2', 'gpt-4o')
// Should have: system message + 2 conversation messages = 3 total
expect(result.length).toBe(3)
// Should have: system message + turn 2 (1 message) = 2 total
expect(result.length).toBe(2)
expect(result[0].role).toBe('system') // First system message preserved
expect(result[1].content).toBe('C') // Second most recent conversation message
expect(result[2].content).toBe('D') // Most recent conversation message
expect(result[1].content).toBe('D') // Most recent turn
})
it('should process messages from newest to oldest', () => {
@@ -249,29 +249,29 @@ describe('Memory', () => {
})
describe('Token-based vs Message-based comparison', () => {
it('should produce different results for same message count limit', () => {
it('should produce different results based on turn limits vs token limits', () => {
const messages: Message[] = [
{ role: 'user', content: 'A' }, // Short message (~1 token)
{ role: 'user', content: 'A' }, // Short message (~1 token) - turn 1
{
role: 'assistant',
content: 'This is a much longer response that takes many more tokens',
}, // Long message (~15 tokens)
{ role: 'user', content: 'B' }, // Short message (~1 token)
}, // Long message (~15 tokens) - turn 1
{ role: 'user', content: 'B' }, // Short message (~1 token) - turn 2
]
// Message-based: last 2 messages
const messageResult = (memoryService as any).applySlidingWindow(messages, '2')
expect(messageResult.length).toBe(2)
// Turn-based with limit 1: keeps last turn only
const messageResult = (memoryService as any).applySlidingWindow(messages, '1')
expect(messageResult.length).toBe(1) // Only turn 2 (message B)
// Token-based: with limit of 10 tokens, might fit all 3 messages or just last 2
// Token-based: with limit of 10 tokens, fits turn 2 (1 token) but not turn 1 (~16 tokens)
const tokenResult = (memoryService as any).applySlidingWindowByTokens(
messages,
'10',
'gpt-4o'
)
// The long message should affect what fits
expect(tokenResult.length).toBeGreaterThanOrEqual(1)
// Both should only fit the last turn due to the long assistant message
expect(tokenResult.length).toBe(1)
})
})
})

View File

@@ -202,13 +202,51 @@ export class Memory {
const systemMessages = messages.filter((msg) => msg.role === 'system')
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
const recentMessages = conversationMessages.slice(-limit)
// Group messages into conversation turns
// A turn = user message + any tool calls/results + assistant response
const turns = this.groupMessagesIntoTurns(conversationMessages)
// Take the last N turns
const recentTurns = turns.slice(-limit)
// Flatten back to messages
const recentMessages = recentTurns.flat()
const firstSystemMessage = systemMessages.length > 0 ? [systemMessages[0]] : []
return [...firstSystemMessage, ...recentMessages]
}
/**
* Groups messages into conversation turns.
* A turn starts with a user message and includes all subsequent messages
* until the next user message (tool calls, tool results, assistant response).
*/
private groupMessagesIntoTurns(messages: Message[]): Message[][] {
const turns: Message[][] = []
let currentTurn: Message[] = []
for (const msg of messages) {
if (msg.role === 'user') {
// Start a new turn
if (currentTurn.length > 0) {
turns.push(currentTurn)
}
currentTurn = [msg]
} else {
// Add to current turn (assistant, tool, etc.)
currentTurn.push(msg)
}
}
// Don't forget the last turn
if (currentTurn.length > 0) {
turns.push(currentTurn)
}
return turns
}
/**
* Apply token-based sliding window to limit conversation by token count
*
@@ -216,6 +254,11 @@ export class Memory {
* - For consistency with message-based sliding window, the first system message is preserved
* - System messages are excluded from the token count
* - This ensures system prompts are always available while limiting conversation history
*
* Turn handling:
* - Messages are grouped into turns (user + tool calls/results + assistant response)
* - Complete turns are added to stay within token limit
* - This prevents breaking tool call/result pairs
*/
private applySlidingWindowByTokens(
messages: Message[],
@@ -233,25 +276,31 @@ export class Memory {
const systemMessages = messages.filter((msg) => msg.role === 'system')
const conversationMessages = messages.filter((msg) => msg.role !== 'system')
// Group into turns to keep tool call/result pairs together
const turns = this.groupMessagesIntoTurns(conversationMessages)
const result: Message[] = []
let currentTokenCount = 0
// Add conversation messages from most recent backwards
for (let i = conversationMessages.length - 1; i >= 0; i--) {
const message = conversationMessages[i]
const messageTokens = getAccurateTokenCount(message.content, model)
// Add turns from most recent backwards
for (let i = turns.length - 1; i >= 0; i--) {
const turn = turns[i]
const turnTokens = turn.reduce(
(sum, msg) => sum + getAccurateTokenCount(msg.content || '', model),
0
)
if (currentTokenCount + messageTokens <= tokenLimit) {
result.unshift(message)
currentTokenCount += messageTokens
if (currentTokenCount + turnTokens <= tokenLimit) {
result.unshift(...turn)
currentTokenCount += turnTokens
} else if (result.length === 0) {
logger.warn('Single message exceeds token limit, including anyway', {
messageTokens,
logger.warn('Single turn exceeds token limit, including anyway', {
turnTokens,
tokenLimit,
messageRole: message.role,
turnMessages: turn.length,
})
result.unshift(message)
currentTokenCount += messageTokens
result.unshift(...turn)
currentTokenCount += turnTokens
break
} else {
// Token limit reached, stop processing
@@ -259,17 +308,20 @@ export class Memory {
}
}
// No need to remove orphaned messages - turns are already complete
const cleanedResult = result
logger.debug('Applied token-based sliding window', {
totalMessages: messages.length,
conversationMessages: conversationMessages.length,
includedMessages: result.length,
includedMessages: cleanedResult.length,
totalTokens: currentTokenCount,
tokenLimit,
})
// Preserve first system message and prepend to results (consistent with message-based window)
const firstSystemMessage = systemMessages.length > 0 ? [systemMessages[0]] : []
return [...firstSystemMessage, ...result]
return [...firstSystemMessage, ...cleanedResult]
}
/**
@@ -324,7 +376,7 @@ export class Memory {
// Count tokens used by system messages first
let systemTokenCount = 0
for (const msg of systemMessages) {
systemTokenCount += getAccurateTokenCount(msg.content, model)
systemTokenCount += getAccurateTokenCount(msg.content || '', model)
}
// Calculate remaining tokens available for conversation messages
@@ -339,30 +391,36 @@ export class Memory {
return systemMessages
}
// Group into turns to keep tool call/result pairs together
const turns = this.groupMessagesIntoTurns(conversationMessages)
const result: Message[] = []
let currentTokenCount = 0
for (let i = conversationMessages.length - 1; i >= 0; i--) {
const message = conversationMessages[i]
const messageTokens = getAccurateTokenCount(message.content, model)
for (let i = turns.length - 1; i >= 0; i--) {
const turn = turns[i]
const turnTokens = turn.reduce(
(sum, msg) => sum + getAccurateTokenCount(msg.content || '', model),
0
)
if (currentTokenCount + messageTokens <= remainingTokens) {
result.unshift(message)
currentTokenCount += messageTokens
if (currentTokenCount + turnTokens <= remainingTokens) {
result.unshift(...turn)
currentTokenCount += turnTokens
} else if (result.length === 0) {
logger.warn('Single message exceeds remaining context window, including anyway', {
messageTokens,
logger.warn('Single turn exceeds remaining context window, including anyway', {
turnTokens,
remainingTokens,
systemTokenCount,
messageRole: message.role,
turnMessages: turn.length,
})
result.unshift(message)
currentTokenCount += messageTokens
result.unshift(...turn)
currentTokenCount += turnTokens
break
} else {
logger.info('Auto-trimmed conversation history to fit context window', {
originalMessages: conversationMessages.length,
trimmedMessages: result.length,
originalTurns: turns.length,
trimmedTurns: turns.length - i - 1,
conversationTokens: currentTokenCount,
systemTokens: systemTokenCount,
totalTokens: currentTokenCount + systemTokenCount,
@@ -372,6 +430,7 @@ export class Memory {
}
}
// No need to remove orphaned messages - turns are already complete
return [...systemMessages, ...result]
}
@@ -638,7 +697,7 @@ export class Memory {
/**
* Validate inputs to prevent malicious data or performance issues
*/
private validateInputs(conversationId?: string, content?: string): void {
private validateInputs(conversationId?: string, content?: string | null): void {
if (conversationId) {
if (conversationId.length > 255) {
throw new Error('Conversation ID too long (max 255 characters)')

View File

@@ -37,10 +37,22 @@ export interface ToolInput {
}
export interface Message {
role: 'system' | 'user' | 'assistant'
content: string
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | null
function_call?: any
tool_calls?: any[]
tool_calls?: ToolCallMessage[]
tool_call_id?: string
/** Tool name for tool role messages (used by providers like Google/Gemini) */
name?: string
}
export interface ToolCallMessage {
id: string
type: 'function'
function: {
name: string
arguments: string
}
}
export interface StreamingConfig {

View File

@@ -4,7 +4,12 @@ import type { StreamingExecution } from '@/executor/types'
import { executeTool } from '@/tools'
import { getProviderDefaultModel, getProviderModels } from '../models'
import type { ProviderConfig, ProviderRequest, ProviderResponse, TimeSegment } from '../types'
import { prepareToolExecution, prepareToolsWithUsageControl, trackForcedToolUsage } from '../utils'
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '../utils'
const logger = createLogger('AnthropicProvider')
@@ -68,8 +73,12 @@ export const anthropicProvider: ProviderConfig = {
// Add remaining messages
if (request.messages) {
request.messages.forEach((msg) => {
// Sanitize messages to ensure proper tool call/result pairing
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
sanitizedMessages.forEach((msg) => {
if (msg.role === 'function') {
// Legacy function role format
messages.push({
role: 'user',
content: [
@@ -80,7 +89,41 @@ export const anthropicProvider: ProviderConfig = {
},
],
})
} else if (msg.role === 'tool') {
// Modern tool role format (OpenAI-compatible)
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: (msg as any).tool_call_id,
content: msg.content || '',
},
],
})
} else if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
// Modern tool_calls format (OpenAI-compatible)
const toolUseContent = msg.tool_calls.map((tc: any) => ({
type: 'tool_use',
id: tc.id,
name: tc.function?.name || tc.name,
input:
typeof tc.function?.arguments === 'string'
? (() => {
try {
return JSON.parse(tc.function.arguments)
} catch {
return {}
}
})()
: tc.function?.arguments || tc.arguments || {},
}))
messages.push({
role: 'assistant',
content: toolUseContent,
})
} else if (msg.function_call) {
// Legacy function_call format
const toolUseId = `${msg.function_call.name}-${Date.now()}`
messages.push({
role: 'assistant',
@@ -490,9 +533,14 @@ ${fieldDescriptions}
}
}
// Use the original tool use ID from the API response
const toolUseId = toolUse.id || generateToolUseId(toolName)
toolCalls.push({
id: toolUseId,
name: toolName,
arguments: toolParams,
rawArguments: JSON.stringify(toolArgs),
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
@@ -501,7 +549,6 @@ ${fieldDescriptions}
})
// Add the tool call and result to messages (both success and failure)
const toolUseId = generateToolUseId(toolName)
currentMessages.push({
role: 'assistant',
@@ -840,9 +887,14 @@ ${fieldDescriptions}
}
}
// Use the original tool use ID from the API response
const toolUseId = toolUse.id || generateToolUseId(toolName)
toolCalls.push({
id: toolUseId,
name: toolName,
arguments: toolParams,
rawArguments: JSON.stringify(toolArgs),
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
@@ -851,7 +903,6 @@ ${fieldDescriptions}
})
// Add the tool call and result to messages (both success and failure)
const toolUseId = generateToolUseId(toolName)
currentMessages.push({
role: 'assistant',

View File

@@ -12,6 +12,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -120,9 +121,10 @@ export const azureOpenAIProvider: ProviderConfig = {
})
}
// Add remaining messages
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Transform tools to Azure OpenAI format if provided
@@ -417,8 +419,10 @@ export const azureOpenAIProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -86,9 +87,10 @@ export const cerebrasProvider: ProviderConfig = {
})
}
// Add remaining messages
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Transform tools to Cerebras format if provided
@@ -323,8 +325,10 @@ export const cerebrasProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -84,9 +85,10 @@ export const deepseekProvider: ProviderConfig = {
})
}
// Add remaining messages
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Transform tools to OpenAI format if provided
@@ -323,8 +325,10 @@ export const deepseekProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -10,6 +10,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -552,9 +553,14 @@ export const googleProvider: ProviderConfig = {
}
}
// Generate a unique ID for this tool call (Google doesn't provide one)
const toolCallId = `call_${toolName}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
toolCalls.push({
id: toolCallId,
name: toolName,
arguments: toolParams,
rawArguments: JSON.stringify(toolArgs),
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,
@@ -1087,9 +1093,10 @@ function convertToGeminiFormat(request: ProviderRequest): {
contents.push({ role: 'user', parts: [{ text: request.context }] })
}
// Process messages
// Process messages (sanitized to ensure proper tool call/result pairing)
if (request.messages && request.messages.length > 0) {
for (const message of request.messages) {
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
for (const message of sanitizedMessages) {
if (message.role === 'system') {
// Add to system instruction
if (!systemInstruction) {
@@ -1119,10 +1126,28 @@ function convertToGeminiFormat(request: ProviderRequest): {
contents.push({ role: 'model', parts: functionCalls })
}
} else if (message.role === 'tool') {
// Convert tool response (Gemini only accepts user/model roles)
// Convert tool response to Gemini's functionResponse format
// Gemini uses 'user' role for function responses
const functionName = (message as any).name || 'function'
let responseData: any
try {
responseData =
typeof message.content === 'string' ? JSON.parse(message.content) : message.content
} catch {
responseData = { result: message.content }
}
contents.push({
role: 'user',
parts: [{ text: `Function result: ${message.content}` }],
parts: [
{
functionResponse: {
name: functionName,
response: responseData,
},
},
],
})
}
}

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -75,9 +76,10 @@ export const groqProvider: ProviderConfig = {
})
}
// Add remaining messages
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Transform tools to function format if provided
@@ -296,8 +298,10 @@ export const groqProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -100,8 +101,10 @@ export const mistralProvider: ProviderConfig = {
})
}
// Sanitize messages to ensure proper tool call/result pairing
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
const tools = request.tools?.length
@@ -355,8 +358,10 @@ export const mistralProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -9,7 +9,7 @@ import type {
ProviderResponse,
TimeSegment,
} from '@/providers/types'
import { prepareToolExecution } from '@/providers/utils'
import { prepareToolExecution, sanitizeMessagesForProvider } from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers/store'
import { executeTool } from '@/tools'
@@ -126,9 +126,10 @@ export const ollamaProvider: ProviderConfig = {
})
}
// Add remaining messages
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Transform tools to OpenAI format if provided
@@ -407,8 +408,10 @@ export const ollamaProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -103,9 +104,10 @@ export const openaiProvider: ProviderConfig = {
})
}
// Add remaining messages
// Add remaining messages (sanitized to ensure proper tool call/result pairing)
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Transform tools to OpenAI format if provided
@@ -398,8 +400,10 @@ export const openaiProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -93,8 +94,10 @@ export const openRouterProvider: ProviderConfig = {
allMessages.push({ role: 'user', content: request.context })
}
// Sanitize messages to ensure proper tool call/result pairing
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
const tools = request.tools?.length
@@ -303,8 +306,10 @@ export const openRouterProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -1049,3 +1049,96 @@ export function prepareToolExecution(
return { toolParams, executionParams }
}
/**
* Sanitizes messages array to ensure proper tool call/result pairing
* This prevents provider errors like "tool_result without corresponding tool_use"
*
* Rules enforced:
* 1. Every tool message must have a matching tool_calls message before it
* 2. Every tool_calls in an assistant message should have corresponding tool results
* 3. Messages maintain their original order
*/
export function sanitizeMessagesForProvider(
messages: Array<{
role: string
content?: string | null
tool_calls?: Array<{ id: string; [key: string]: any }>
tool_call_id?: string
[key: string]: any
}>
): typeof messages {
if (!messages || messages.length === 0) {
return messages
}
// Build a map of tool_call IDs to their positions
const toolCallIdToIndex = new Map<string, number>()
const toolResultIds = new Set<string>()
// First pass: collect all tool_call IDs and tool result IDs
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
for (const tc of msg.tool_calls) {
if (tc.id) {
toolCallIdToIndex.set(tc.id, i)
}
}
}
if (msg.role === 'tool' && msg.tool_call_id) {
toolResultIds.add(msg.tool_call_id)
}
}
// Second pass: filter messages
const result: typeof messages = []
for (const msg of messages) {
// For tool messages: only include if there's a matching tool_calls before it
if (msg.role === 'tool') {
const toolCallId = msg.tool_call_id
if (toolCallId && toolCallIdToIndex.has(toolCallId)) {
result.push(msg)
} else {
logger.debug('Removing orphaned tool message', { toolCallId })
}
continue
}
// For assistant messages with tool_calls: only include tool_calls that have results
if (msg.role === 'assistant' && msg.tool_calls && Array.isArray(msg.tool_calls)) {
const validToolCalls = msg.tool_calls.filter((tc) => tc.id && toolResultIds.has(tc.id))
if (validToolCalls.length === 0) {
// No valid tool calls - if there's content, keep as regular message
if (msg.content) {
const { tool_calls, ...msgWithoutToolCalls } = msg
result.push(msgWithoutToolCalls)
} else {
logger.debug('Removing assistant message with orphaned tool_calls', {
toolCallIds: msg.tool_calls.map((tc) => tc.id),
})
}
} else if (validToolCalls.length === msg.tool_calls.length) {
// All tool calls are valid
result.push(msg)
} else {
// Some tool calls are orphaned - keep only valid ones
result.push({ ...msg, tool_calls: validToolCalls })
logger.debug('Filtered orphaned tool_calls from message', {
original: msg.tool_calls.length,
kept: validToolCalls.length,
})
}
continue
}
// All other messages pass through
result.push(msg)
}
return result
}

View File

@@ -12,6 +12,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { useProvidersStore } from '@/stores/providers/store'
@@ -140,8 +141,10 @@ export const vllmProvider: ProviderConfig = {
})
}
// Sanitize messages to ensure proper tool call/result pairing
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
const tools = request.tools?.length
@@ -400,8 +403,10 @@ export const vllmProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -11,6 +11,7 @@ import type {
import {
prepareToolExecution,
prepareToolsWithUsageControl,
sanitizeMessagesForProvider,
trackForcedToolUsage,
} from '@/providers/utils'
import { executeTool } from '@/tools'
@@ -83,8 +84,10 @@ export const xAIProvider: ProviderConfig = {
})
}
// Sanitize messages to ensure proper tool call/result pairing
if (request.messages) {
allMessages.push(...request.messages)
const sanitizedMessages = sanitizeMessagesForProvider(request.messages)
allMessages.push(...sanitizedMessages)
}
// Set up tools
@@ -364,8 +367,10 @@ export const xAIProvider: ProviderConfig = {
}
toolCalls.push({
id: toolCall.id,
name: toolName,
arguments: toolParams,
rawArguments: toolCall.function.arguments,
startTime: new Date(toolCallStartTime).toISOString(),
endTime: new Date(toolCallEndTime).toISOString(),
duration: toolCallDuration,

View File

@@ -32,11 +32,7 @@ function shouldIncludeField(subBlockConfig: SubBlockConfig, isAdvancedMode: bool
const fieldMode = subBlockConfig.mode
if (fieldMode === 'advanced' && !isAdvancedMode) {
return false
}
if (fieldMode === 'basic' && isAdvancedMode) {
return false
return false // Skip advanced-only fields when in basic mode
}
return true