Skip to content

Commit 6328c99

Browse files
author
Theodore Li
committed
fix(tool): Fix issue with custom tools spreading out string output
1 parent cef321b commit 6328c99

File tree

3 files changed

+192
-2
lines changed

3 files changed

+192
-2
lines changed

apps/sim/executor/utils/output-filter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export function filterOutputForLog(
2424
additionalHiddenKeys?: string[]
2525
}
2626
): NormalizedBlockOutput {
27+
if (typeof output !== 'object' || output === null || Array.isArray(output)) {
28+
return output as NormalizedBlockOutput
29+
}
2730
const blockConfig = blockType ? getBlock(blockType) : undefined
2831
const filtered: NormalizedBlockOutput = {}
2932
const additionalHiddenKeys = options?.additionalHiddenKeys ?? []

apps/sim/tools/index.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,186 @@ describe('Rate Limiting and Retry Logic', () => {
18301830
})
18311831
})
18321832

1833+
describe('stripInternalFields Safety', () => {
1834+
let cleanupEnvVars: () => void
1835+
1836+
beforeEach(() => {
1837+
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
1838+
cleanupEnvVars = setupEnvVars({ NEXT_PUBLIC_APP_URL: 'http://localhost:3000' })
1839+
})
1840+
1841+
afterEach(() => {
1842+
vi.resetAllMocks()
1843+
cleanupEnvVars()
1844+
})
1845+
1846+
it('should preserve string output from tools without character-indexing', async () => {
1847+
const stringOutput = '{"type":"button","phone":"917899658001"}'
1848+
1849+
const mockTool = {
1850+
id: 'test_string_output',
1851+
name: 'Test String Output',
1852+
description: 'A tool that returns a string as output',
1853+
version: '1.0.0',
1854+
params: {},
1855+
request: {
1856+
url: '/api/test/string-output',
1857+
method: 'POST' as const,
1858+
headers: () => ({ 'Content-Type': 'application/json' }),
1859+
},
1860+
transformResponse: vi.fn().mockResolvedValue({
1861+
success: true,
1862+
output: stringOutput,
1863+
}),
1864+
}
1865+
1866+
const originalTools = { ...tools }
1867+
;(tools as any).test_string_output = mockTool
1868+
1869+
global.fetch = Object.assign(
1870+
vi.fn().mockImplementation(async () => ({
1871+
ok: true,
1872+
status: 200,
1873+
headers: new Headers(),
1874+
json: () => Promise.resolve({ success: true }),
1875+
})),
1876+
{ preconnect: vi.fn() }
1877+
) as typeof fetch
1878+
1879+
const result = await executeTool('test_string_output', {}, true)
1880+
1881+
expect(result.success).toBe(true)
1882+
expect(result.output).toBe(stringOutput)
1883+
expect(typeof result.output).toBe('string')
1884+
1885+
Object.assign(tools, originalTools)
1886+
})
1887+
1888+
it('should preserve array output from tools', async () => {
1889+
const arrayOutput = [{ id: 1 }, { id: 2 }]
1890+
1891+
const mockTool = {
1892+
id: 'test_array_output',
1893+
name: 'Test Array Output',
1894+
description: 'A tool that returns an array as output',
1895+
version: '1.0.0',
1896+
params: {},
1897+
request: {
1898+
url: '/api/test/array-output',
1899+
method: 'POST' as const,
1900+
headers: () => ({ 'Content-Type': 'application/json' }),
1901+
},
1902+
transformResponse: vi.fn().mockResolvedValue({
1903+
success: true,
1904+
output: arrayOutput,
1905+
}),
1906+
}
1907+
1908+
const originalTools = { ...tools }
1909+
;(tools as any).test_array_output = mockTool
1910+
1911+
global.fetch = Object.assign(
1912+
vi.fn().mockImplementation(async () => ({
1913+
ok: true,
1914+
status: 200,
1915+
headers: new Headers(),
1916+
json: () => Promise.resolve({ success: true }),
1917+
})),
1918+
{ preconnect: vi.fn() }
1919+
) as typeof fetch
1920+
1921+
const result = await executeTool('test_array_output', {}, true)
1922+
1923+
expect(result.success).toBe(true)
1924+
expect(Array.isArray(result.output)).toBe(true)
1925+
expect(result.output).toEqual(arrayOutput)
1926+
1927+
Object.assign(tools, originalTools)
1928+
})
1929+
1930+
it('should still strip __-prefixed fields from object output', async () => {
1931+
const mockTool = {
1932+
id: 'test_strip_internal',
1933+
name: 'Test Strip Internal',
1934+
description: 'A tool with __internal fields in output',
1935+
version: '1.0.0',
1936+
params: {},
1937+
request: {
1938+
url: '/api/test/strip-internal',
1939+
method: 'POST' as const,
1940+
headers: () => ({ 'Content-Type': 'application/json' }),
1941+
},
1942+
transformResponse: vi.fn().mockResolvedValue({
1943+
success: true,
1944+
output: { result: 'ok', __costDollars: 0.05, _id: 'keep-this' },
1945+
}),
1946+
}
1947+
1948+
const originalTools = { ...tools }
1949+
;(tools as any).test_strip_internal = mockTool
1950+
1951+
global.fetch = Object.assign(
1952+
vi.fn().mockImplementation(async () => ({
1953+
ok: true,
1954+
status: 200,
1955+
headers: new Headers(),
1956+
json: () => Promise.resolve({ success: true }),
1957+
})),
1958+
{ preconnect: vi.fn() }
1959+
) as typeof fetch
1960+
1961+
const result = await executeTool('test_strip_internal', {}, true)
1962+
1963+
expect(result.success).toBe(true)
1964+
expect(result.output.result).toBe('ok')
1965+
expect(result.output.__costDollars).toBeUndefined()
1966+
expect(result.output._id).toBe('keep-this')
1967+
1968+
Object.assign(tools, originalTools)
1969+
})
1970+
1971+
it('should preserve __-prefixed fields in custom tool output', async () => {
1972+
const mockTool = {
1973+
id: 'custom_test-preserve-dunder',
1974+
name: 'Custom Preserve Dunder',
1975+
description: 'A custom tool whose output has __ fields',
1976+
version: '1.0.0',
1977+
params: {},
1978+
request: {
1979+
url: '/api/function/execute',
1980+
method: 'POST' as const,
1981+
headers: () => ({ 'Content-Type': 'application/json' }),
1982+
},
1983+
transformResponse: vi.fn().mockResolvedValue({
1984+
success: true,
1985+
output: { result: 'ok', __metadata: { source: 'user' }, __tag: 'important' },
1986+
}),
1987+
}
1988+
1989+
const originalTools = { ...tools }
1990+
;(tools as any)['custom_test-preserve-dunder'] = mockTool
1991+
1992+
global.fetch = Object.assign(
1993+
vi.fn().mockImplementation(async () => ({
1994+
ok: true,
1995+
status: 200,
1996+
headers: new Headers(),
1997+
json: () => Promise.resolve({ success: true }),
1998+
})),
1999+
{ preconnect: vi.fn() }
2000+
) as typeof fetch
2001+
2002+
const result = await executeTool('custom_test-preserve-dunder', {}, true)
2003+
2004+
expect(result.success).toBe(true)
2005+
expect(result.output.result).toBe('ok')
2006+
expect(result.output.__metadata).toEqual({ source: 'user' })
2007+
expect(result.output.__tag).toBe('important')
2008+
2009+
Object.assign(tools, originalTools)
2010+
})
2011+
})
2012+
18332013
describe('Cost Field Handling', () => {
18342014
let cleanupEnvVars: () => void
18352015

apps/sim/tools/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ async function reportCustomDimensionUsage(
363363
* fields like `_id`.
364364
*/
365365
function stripInternalFields(output: Record<string, unknown>): Record<string, unknown> {
366+
if (typeof output !== 'object' || output === null || Array.isArray(output)) {
367+
return output
368+
}
366369
const result: Record<string, unknown> = {}
367370
for (const [key, value] of Object.entries(output)) {
368371
if (!key.startsWith('__')) {
@@ -825,7 +828,9 @@ export async function executeTool(
825828
)
826829
}
827830

828-
const strippedOutput = stripInternalFields(finalResult.output || {})
831+
const strippedOutput = isCustomTool(normalizedToolId)
832+
? (finalResult.output || {})
833+
: stripInternalFields(finalResult.output || {})
829834

830835
return {
831836
...finalResult,
@@ -880,7 +885,9 @@ export async function executeTool(
880885
)
881886
}
882887

883-
const strippedOutput = stripInternalFields(finalResult.output || {})
888+
const strippedOutput = isCustomTool(normalizedToolId)
889+
? (finalResult.output || {})
890+
: stripInternalFields(finalResult.output || {})
884891

885892
return {
886893
...finalResult,

0 commit comments

Comments
 (0)