Skip to content

Commit 07330ae

Browse files
authored
fix(blocks): keep agent-supplied tool args that block mappers remap from dedicated fields (#10006)
* fix(blocks): keep agent-supplied tool args that block mappers remap from dedicated fields * fix(blocks): gate every agent fallback on the call context, type the new mapper tests * chore(types): shrink test-types baseline for the fixed block tests
1 parent aa0ff21 commit 07330ae

18 files changed

Lines changed: 385 additions & 53 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { AzureDevOpsBlock } from '@/blocks/blocks/azure_devops'
3+
import type { BlockConfig } from '@/blocks/types'
4+
5+
/** The block's `tools.config.params` mapper, which every block under test defines. */
6+
function requireParamsMapper(block: BlockConfig) {
7+
const mapper = block.tools.config?.params
8+
if (!mapper) throw new Error(`${block.type} has no params mapper`)
9+
return mapper
10+
}
11+
12+
/**
13+
* On an agent call the model supplies `continuationToken`/`top`/`iterationPath`/`format` under the
14+
* tool's own names; the per-operation fields (`pipelinesContinuationToken`, `queryTop`, …) are never
15+
* rendered in the agent tool input. The handler forwards `{ ...inputs, ...mapped }`.
16+
*/
17+
describe('AzureDevOpsBlock agent tool calls', () => {
18+
const buildParams = requireParamsMapper(AzureDevOpsBlock)
19+
const agentCall = (operation: string, args: Record<string, unknown>): Record<string, unknown> => {
20+
const inputs = { ...args, operation, organization: 'org', project: 'proj' }
21+
return { ...inputs, ...buildParams(inputs, { operation }) }
22+
}
23+
24+
it.each(['azure_devops_list_pipelines', 'azure_devops_get_comments'])(
25+
'keeps the model continuationToken on %s',
26+
(operation) => {
27+
expect(
28+
agentCall(operation, { workItemId: 1, continuationToken: 'ct' }).continuationToken
29+
).toBe('ct')
30+
}
31+
)
32+
33+
it('keeps the model top on azure_devops_query_work_items', () => {
34+
expect(agentCall('azure_devops_query_work_items', { wiqlQuery: 'q', top: 5 }).top).toBe(5)
35+
})
36+
37+
it('keeps the model iterationPath on azure_devops_update_work_item', () => {
38+
const mapped = agentCall('azure_devops_update_work_item', {
39+
workItemId: 1,
40+
iterationPath: 'proj\\Sprint 2',
41+
})
42+
expect(mapped.iterationPath).toBe('proj\\Sprint 2')
43+
})
44+
45+
it('keeps the model format on azure_devops_add_comment', () => {
46+
const mapped = agentCall('azure_devops_add_comment', { workItemId: 1, format: 'markdown' })
47+
expect(mapped.format).toBe('markdown')
48+
})
49+
50+
it('drops a List Builds token left over on the canvas under list pipelines', () => {
51+
const inputs = { operation: 'azure_devops_list_pipelines', continuationToken: 'stale' }
52+
expect({ ...inputs, ...buildParams(inputs) }.continuationToken).toBeUndefined()
53+
})
54+
55+
it('drops a Create Work Item iteration path left over on the canvas under update', () => {
56+
const inputs = {
57+
operation: 'azure_devops_update_work_item',
58+
workItemId: 1,
59+
iterationPath: 'stale',
60+
}
61+
expect({ ...inputs, ...buildParams(inputs) }.iterationPath).toBeUndefined()
62+
})
63+
})

‎apps/sim/blocks/blocks/azure_devops.ts‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,7 @@ export const AzureDevOpsBlock: BlockConfig = {
718718
],
719719
config: {
720720
tool: (params) => params.operation as string,
721-
params: (params) => {
721+
params: (params, context) => {
722722
const base = {
723723
accessToken: params.accessToken as string,
724724
organization: params.organization as string,
@@ -729,7 +729,10 @@ export const AzureDevOpsBlock: BlockConfig = {
729729
return {
730730
...base,
731731
top: resolveTop(params.pipelinesTop, params.top),
732-
continuationToken: (params.pipelinesContinuationToken as string) || undefined,
732+
continuationToken:
733+
((params.pipelinesContinuationToken ||
734+
(context?.operation ? params.continuationToken : undefined)) as string) ||
735+
undefined,
733736
}
734737
case 'azure_devops_get_pipeline':
735738
return { ...base, pipelineId: Number(params.pipelineId) }
@@ -774,7 +777,7 @@ export const AzureDevOpsBlock: BlockConfig = {
774777
return {
775778
...base,
776779
wiqlQuery: params.wiqlQuery as string,
777-
top: params.queryTop ? Number(params.queryTop) : undefined,
780+
top: resolveTop(params.queryTop, context?.operation ? params.top : undefined),
778781
}
779782
case 'azure_devops_get_work_item':
780783
return { ...base, workItemId: Number(params.workItemId) }
@@ -814,22 +817,29 @@ export const AzureDevOpsBlock: BlockConfig = {
814817
completedWork: params.completedWork ? Number(params.completedWork) : undefined,
815818
description: (params.description as string) || undefined,
816819
areaPath: (params.areaPath as string) || undefined,
817-
iterationPath: (params.updateIterationPath as string) || undefined,
820+
iterationPath:
821+
((params.updateIterationPath ||
822+
(context?.operation ? params.iterationPath : undefined)) as string) || undefined,
818823
tags: (params.tags as string) || undefined,
819824
}
820825
case 'azure_devops_add_comment':
821826
return {
822827
...base,
823828
workItemId: Number(params.workItemId),
824829
text: params.commentText as string,
825-
format: (params.commentFormat as string) || undefined,
830+
format:
831+
((params.commentFormat ||
832+
(context?.operation ? params.format : undefined)) as string) || undefined,
826833
}
827834
case 'azure_devops_get_comments':
828835
return {
829836
...base,
830837
workItemId: Number(params.workItemId),
831838
top: resolveTop(params.commentsTop, params.top),
832-
continuationToken: (params.commentsContinuationToken as string) || undefined,
839+
continuationToken:
840+
((params.commentsContinuationToken ||
841+
(context?.operation ? params.continuationToken : undefined)) as string) ||
842+
undefined,
833843
}
834844
default:
835845
return base
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { GmailV2Block } from '@/blocks/blocks/gmail'
3+
import type { BlockConfig } from '@/blocks/types'
4+
5+
/** The block's `tools.config.params` mapper, which every block under test defines. */
6+
function requireParamsMapper(block: BlockConfig) {
7+
const mapper = block.tools.config?.params
8+
if (!mapper) throw new Error(`${block.type} has no params mapper`)
9+
return mapper
10+
}
11+
12+
/**
13+
* On an agent call the model supplies `threadId`/`format`/`name`/`query`/`labelIds` under the
14+
* tool's own names; the dedicated V2 fields (`targetThreadId`, `threadFormat`, `labelName`,
15+
* `listQuery`, `threadLabelIds`) are never rendered in the agent tool input.
16+
*/
17+
describe('GmailV2Block agent tool calls', () => {
18+
const buildParams = requireParamsMapper(GmailV2Block)
19+
const agentCall = (operation: string, args: Record<string, unknown>): Record<string, unknown> => {
20+
const inputs = { ...args, operation }
21+
return { ...inputs, ...buildParams(inputs, { operation }) }
22+
}
23+
24+
it('keeps the model threadId and format on get_thread_gmail', () => {
25+
const mapped = agentCall('get_thread_gmail', { threadId: 't-1', format: 'minimal' })
26+
expect(mapped.threadId).toBe('t-1')
27+
expect(mapped.format).toBe('minimal')
28+
})
29+
30+
it.each(['create_label_gmail', 'update_label_gmail'])(
31+
'keeps the model name on %s',
32+
(operation) => {
33+
expect(agentCall(operation, { labelId: 'l-1', name: 'Receipts' }).name).toBe('Receipts')
34+
}
35+
)
36+
37+
it.each(['list_drafts_gmail', 'list_threads_gmail'])(
38+
'keeps the model query and labelIds on %s',
39+
(operation) => {
40+
const mapped = agentCall(operation, { query: 'is:unread', labelIds: 'INBOX' })
41+
expect(mapped.query).toBe('is:unread')
42+
expect(mapped.labelIds).toBe('INBOX')
43+
}
44+
)
45+
46+
it('drops a Send threadId left over on the canvas under get_thread_gmail', () => {
47+
const inputs = { operation: 'get_thread_gmail', threadId: 'stale' }
48+
expect({ ...inputs, ...buildParams(inputs) }.threadId).toBeUndefined()
49+
})
50+
})

‎apps/sim/blocks/blocks/gmail.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -969,8 +969,10 @@ export const GmailV2Block: BlockConfig = {
969969
suffix: '_v2',
970970
fallbackToolId: 'gmail_send_v2',
971971
}),
972-
params: (params) => {
973-
const mapped = GmailBlock.tools?.config?.params?.(params) ?? params
972+
params: (params, context) => {
973+
const mapped = GmailBlock.tools?.config?.params?.(params, context) ?? params
974+
/** Agent calls carry the model's tool-named args; on the canvas those keys may be stale. */
975+
const agentArg = (value: unknown) => (context?.operation ? value : undefined)
974976
const {
975977
labelName,
976978
targetThreadId,
@@ -982,17 +984,18 @@ export const GmailV2Block: BlockConfig = {
982984
...rest
983985
} = mapped
984986
if (LABEL_WRITE_OPERATIONS.includes(rest.operation)) {
985-
rest.name = labelName || undefined
987+
rest.name = labelName || agentArg(rest.name) || undefined
986988
rest.messageListVisibility = messageListVisibility || undefined
987989
rest.labelListVisibility = labelListVisibility || undefined
988990
}
989991
if (THREAD_TARGET_OPERATIONS.includes(rest.operation)) {
990-
rest.threadId = targetThreadId
991-
rest.format = threadFormat || undefined
992+
rest.threadId = context?.operation ? targetThreadId || rest.threadId : targetThreadId
993+
rest.format = threadFormat || agentArg(rest.format) || undefined
992994
}
993995
if (LIST_OPERATIONS.includes(rest.operation)) {
994-
rest.query = listQuery || undefined
995-
rest.labelIds = threadLabelIds ? String(threadLabelIds).trim() : undefined
996+
rest.query = listQuery || agentArg(rest.query) || undefined
997+
const labelIds = threadLabelIds || agentArg(rest.labelIds)
998+
rest.labelIds = labelIds ? String(labelIds).trim() : undefined
996999
}
9971000
return rest
9981001
},
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { GoogleCalendarBlock } from '@/blocks/blocks/google_calendar'
3+
import type { BlockConfig } from '@/blocks/types'
4+
5+
/** The block's `tools.config.params` mapper, which every block under test defines. */
6+
function requireParamsMapper(block: BlockConfig) {
7+
const mapper = block.tools.config?.params
8+
if (!mapper) throw new Error(`${block.type} has no params mapper`)
9+
return mapper
10+
}
11+
12+
/**
13+
* On an agent call the model supplies `timeZone` under the tool's own name; the dedicated
14+
* `listTimeZone` field is never rendered in the agent tool input.
15+
*/
16+
describe('GoogleCalendarBlock list timeZone', () => {
17+
const buildParams = requireParamsMapper(GoogleCalendarBlock)
18+
19+
it('keeps the model timeZone on an agent list call', () => {
20+
const inputs = { operation: 'list', timeZone: 'America/New_York' }
21+
const mapped = buildParams(inputs, { operation: 'list' })
22+
expect({ ...inputs, ...mapped }.timeZone).toBe('America/New_York')
23+
})
24+
25+
it('drops a Create/Update timeZone left over on the canvas under list', () => {
26+
const inputs = { operation: 'list', timeZone: 'America/New_York' }
27+
expect({ ...inputs, ...buildParams(inputs) }.timeZone).toBeUndefined()
28+
})
29+
})

‎apps/sim/blocks/blocks/google_calendar.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
991991
throw new Error(`Invalid Google Calendar operation: ${params.operation}`)
992992
}
993993
},
994-
params: (params) => {
994+
params: (params, context) => {
995995
const {
996996
oauthCredential,
997997
operation,
@@ -1075,7 +1075,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
10751075
processedParams.timeZone =
10761076
typeof listTimeZone === 'string' && listTimeZone.trim()
10771077
? listTimeZone.trim()
1078-
: undefined
1078+
: context?.operation
1079+
? processedParams.timeZone
1080+
: undefined
10791081
}
10801082

10811083
return {

‎apps/sim/blocks/blocks/google_drive.test.ts‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { triggersMock } from '@sim/testing/mocks/triggers.mock'
22
import { describe, expect, it, vi } from 'vitest'
3+
import type { BlockConfig } from '@/blocks/types'
34

45
vi.mock('@/triggers', () => triggersMock)
56

67
import { GoogleDriveBlock } from '@/blocks/blocks/google_drive'
78

9+
/** The block's `tools.config.params` mapper, which every block under test defines. */
10+
function requireParamsMapper(block: BlockConfig) {
11+
const mapper = block.tools.config?.params
12+
if (!mapper) throw new Error(`${block.type} has no params mapper`)
13+
return mapper
14+
}
15+
816
const paginationCases = [
917
{ operation: 'list', subBlockId: 'pageToken' },
1018
{ operation: 'search', subBlockId: 'searchPageToken' },
@@ -14,7 +22,7 @@ const paginationCases = [
1422
] as const
1523

1624
describe('GoogleDriveBlock pagination', () => {
17-
const buildParams = GoogleDriveBlock.tools.config.params!
25+
const buildParams = requireParamsMapper(GoogleDriveBlock)
1826

1927
describe.each(paginationCases)('$operation', ({ operation, subBlockId }) => {
2028
/**
@@ -70,3 +78,32 @@ describe('GoogleDriveBlock pagination', () => {
7078
expect(params.searchPageToken).toBeUndefined()
7179
})
7280
})
81+
82+
/**
83+
* On an agent call the model supplies `pageSize`/`pageToken`/`query` under the tool's own names;
84+
* the per-operation fields (`permissionsPageSize`, `searchPageToken`, `searchQuery`) are never rendered.
85+
*/
86+
describe('GoogleDriveBlock agent tool calls', () => {
87+
const buildParams = requireParamsMapper(GoogleDriveBlock)
88+
const agentCall = (operation: string, args: Record<string, unknown>): Record<string, unknown> => {
89+
const inputs = { ...args, operation }
90+
return { ...inputs, ...buildParams(inputs, { operation }) }
91+
}
92+
93+
it.each(['search', 'list_permissions', 'list_revisions', 'list_comments'])(
94+
'keeps the model pageSize and pageToken on %s',
95+
(operation) => {
96+
const mapped = agentCall(operation, { fileId: 'f-1', pageSize: 5, pageToken: 'next' })
97+
expect(mapped.pageSize).toBe(5)
98+
expect(mapped.pageToken).toBe('next')
99+
}
100+
)
101+
102+
it('keeps the model query on search', () => {
103+
expect(agentCall('search', { query: 'budget' }).query).toBe('budget')
104+
})
105+
106+
it('drops a list page token left over on the canvas under search', () => {
107+
expect(buildParams({ operation: 'search', pageToken: 'stale' }).pageToken).toBeUndefined()
108+
})
109+
})

‎apps/sim/blocks/blocks/google_drive.ts‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,7 +1530,7 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.`
15301530
throw new Error(`Invalid Google Drive operation: ${params.operation}`)
15311531
}
15321532
},
1533-
params: (params) => {
1533+
params: (params, context) => {
15341534
const {
15351535
oauthCredential,
15361536
uploadFolderId,
@@ -1685,19 +1685,33 @@ Return ONLY the comment text - no explanations, no quotes, no extra formatting.`
16851685
includeDeleted === 'true' ? true : includeDeleted === 'false' ? false : undefined
16861686

16871687
let effectivePageSize: string | undefined = pageSize
1688-
if (params.operation === 'search') effectivePageSize = searchPageSize
1689-
else if (params.operation === 'list_revisions') effectivePageSize = revisionsPageSize
1690-
else if (params.operation === 'list_comments') effectivePageSize = commentsPageSize
1691-
else if (params.operation === 'list_permissions') effectivePageSize = permissionsPageSize
1688+
// Basic `pageSize`/`query` and advanced `pageToken` belong to List; only an agent call's value is trusted for other operations
1689+
const agentPageSize = context?.operation ? pageSize : undefined
1690+
if (params.operation === 'search') effectivePageSize = searchPageSize || agentPageSize
1691+
else if (params.operation === 'list_revisions')
1692+
effectivePageSize = revisionsPageSize || agentPageSize
1693+
else if (params.operation === 'list_comments')
1694+
effectivePageSize = commentsPageSize || agentPageSize
1695+
else if (params.operation === 'list_permissions')
1696+
effectivePageSize = permissionsPageSize || agentPageSize
16921697

1698+
const agentPageToken = context?.operation ? pageToken : undefined
16931699
let effectivePageToken: string | undefined = pageToken
1694-
if (params.operation === 'search') effectivePageToken = searchPageToken
1695-
else if (params.operation === 'list_permissions') effectivePageToken = permissionsPageToken
1696-
else if (params.operation === 'list_revisions') effectivePageToken = revisionsPageToken
1697-
else if (params.operation === 'list_comments') effectivePageToken = commentsPageToken
1700+
if (params.operation === 'search') effectivePageToken = searchPageToken || agentPageToken
1701+
else if (params.operation === 'list_permissions')
1702+
effectivePageToken = permissionsPageToken || agentPageToken
1703+
else if (params.operation === 'list_revisions')
1704+
effectivePageToken = revisionsPageToken || agentPageToken
1705+
else if (params.operation === 'list_comments')
1706+
effectivePageToken = commentsPageToken || agentPageToken
16981707
else if (params.operation !== 'list') effectivePageToken = undefined
16991708

1700-
const effectiveQuery = params.operation === 'search' ? searchQuery : query
1709+
const effectiveQuery =
1710+
params.operation === 'search'
1711+
? context?.operation && !searchQuery
1712+
? query
1713+
: searchQuery
1714+
: query
17011715

17021716
const effectiveOrderBy =
17031717
params.operation === 'list'

‎apps/sim/blocks/blocks/google_groups.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,11 @@ describe('GoogleGroupsBlock update settings', () => {
4444
expect(mapped.whoCanJoin).toBe('INVITED_CAN_JOIN')
4545
})
4646
})
47+
48+
describe('GoogleGroupsBlock agent tool calls', () => {
49+
it('keeps the model orderBy on list_groups when no sort order is chosen', () => {
50+
const inputs = { operation: 'list_groups', customer: 'my_customer', orderBy: 'email' }
51+
const mapped = GoogleGroupsBlock.tools.config?.params?.(inputs, { operation: 'list_groups' })
52+
expect({ ...inputs, ...mapped }.orderBy).toBe('email')
53+
})
54+
})

0 commit comments

Comments
 (0)