mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-22 03:01:08 -05:00
Compare commits
3 Commits
feat/mult-
...
fix/loops
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9697710569 | ||
|
|
68f44b8df4 | ||
|
|
9920882dc5 |
@@ -454,6 +454,8 @@ Enables AI-assisted field generation.
|
||||
|
||||
## Tools Configuration
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization before variable resolution. Put `Number()` and other type coercions in `tools.config.params` instead, which runs at execution time after variables are resolved.
|
||||
|
||||
**Preferred:** Use tool names directly as dropdown option IDs to avoid switch cases:
|
||||
```typescript
|
||||
// Dropdown options use tool IDs directly
|
||||
|
||||
@@ -238,7 +238,7 @@ export const ServiceBlock: BlockConfig = {
|
||||
bgColor: '#hexcolor',
|
||||
icon: ServiceIcon,
|
||||
subBlocks: [ /* see SubBlock Properties */ ],
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}` } },
|
||||
tools: { access: ['service_action'], config: { tool: (p) => `service_${p.operation}`, params: (p) => ({ /* type coercions here */ }) } },
|
||||
inputs: { /* ... */ },
|
||||
outputs: { /* ... */ },
|
||||
}
|
||||
@@ -246,6 +246,8 @@ export const ServiceBlock: BlockConfig = {
|
||||
|
||||
Register in `blocks/registry.ts` (alphabetically).
|
||||
|
||||
**Important:** `tools.config.tool` runs during serialization (before variable resolution). Never do `Number()` or other type coercions there — dynamic references like `<Block.output>` will be destroyed. Use `tools.config.params` for type coercions (it runs during execution, after variables are resolved).
|
||||
|
||||
**SubBlock Properties:**
|
||||
```typescript
|
||||
{
|
||||
|
||||
@@ -485,14 +485,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric string inputs to numbers
|
||||
if (params.limit) {
|
||||
params.limit = Number(params.limit)
|
||||
}
|
||||
if (params.offset) {
|
||||
params.offset = Number(params.offset)
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'ahrefs_domain_rating':
|
||||
return 'ahrefs_domain_rating'
|
||||
@@ -514,6 +506,12 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return 'ahrefs_domain_rating'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
if (params.offset) result.offset = Number(params.offset)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -110,11 +110,6 @@ export const ArxivBlock: BlockConfig<ArxivResponse> = {
|
||||
access: ['arxiv_search', 'arxiv_get_paper', 'arxiv_get_author_papers'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert maxResults to a number for operations that use it
|
||||
if (params.maxResults) {
|
||||
params.maxResults = Number(params.maxResults)
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'arxiv_search':
|
||||
return 'arxiv_search'
|
||||
@@ -126,6 +121,11 @@ export const ArxivBlock: BlockConfig<ArxivResponse> = {
|
||||
return 'arxiv_search'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.maxResults) result.maxResults = Number(params.maxResults)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -309,20 +309,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric params
|
||||
if (params.limit) {
|
||||
params.limit = Number(params.limit)
|
||||
}
|
||||
if (params.maxResults) {
|
||||
params.maxResults = Number(params.maxResults)
|
||||
}
|
||||
|
||||
// Normalize file input for upload operation - use canonical 'file' param
|
||||
const normalizedFile = normalizeFileInput(params.file, { single: true })
|
||||
if (normalizedFile) {
|
||||
params.file = normalizedFile
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'dropbox_upload':
|
||||
return 'dropbox_upload'
|
||||
@@ -348,6 +334,16 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
return 'dropbox_upload'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
if (params.maxResults) result.maxResults = Number(params.maxResults)
|
||||
const normalizedFile = normalizeFileInput(params.file, { single: true })
|
||||
if (normalizedFile) {
|
||||
result.file = normalizedFile
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -457,24 +457,19 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric strings to numbers
|
||||
if (params.size) {
|
||||
params.size = Number(params.size)
|
||||
}
|
||||
if (params.from) {
|
||||
params.from = Number(params.from)
|
||||
}
|
||||
if (params.retryOnConflict) {
|
||||
params.retryOnConflict = Number(params.retryOnConflict)
|
||||
}
|
||||
// Append 's' to timeout for Elasticsearch time format
|
||||
if (params.timeout && !params.timeout.endsWith('s')) {
|
||||
params.timeout = `${params.timeout}s`
|
||||
}
|
||||
|
||||
// Return the operation as the tool ID
|
||||
return params.operation || 'elasticsearch_search'
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.size) result.size = Number(params.size)
|
||||
if (params.from) result.from = Number(params.from)
|
||||
if (params.retryOnConflict) result.retryOnConflict = Number(params.retryOnConflict)
|
||||
if (params.timeout && typeof params.timeout === 'string') {
|
||||
result.timeout = params.timeout.endsWith('s') ? params.timeout : `${params.timeout}s`
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
title: 'Use Autoprompt',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
@@ -62,6 +63,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
],
|
||||
value: () => 'auto',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'includeDomains',
|
||||
@@ -69,6 +71,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'long-input',
|
||||
placeholder: 'example.com, another.com (comma-separated)',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'excludeDomains',
|
||||
@@ -76,6 +79,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'long-input',
|
||||
placeholder: 'exclude.com, another.com (comma-separated)',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'category',
|
||||
@@ -95,6 +99,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'text',
|
||||
@@ -107,12 +112,14 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
title: 'Include Highlights',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'Include Summary',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'livecrawl',
|
||||
@@ -125,6 +132,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
],
|
||||
value: () => 'never',
|
||||
condition: { field: 'operation', value: 'exa_search' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Get Contents operation inputs
|
||||
{
|
||||
@@ -147,6 +155,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'long-input',
|
||||
placeholder: 'Enter a query to guide the summary generation...',
|
||||
condition: { field: 'operation', value: 'exa_get_contents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'subpages',
|
||||
@@ -154,6 +163,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: '5',
|
||||
condition: { field: 'operation', value: 'exa_get_contents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'subpageTarget',
|
||||
@@ -161,12 +171,14 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'long-input',
|
||||
placeholder: 'docs, tutorial, about (comma-separated)',
|
||||
condition: { field: 'operation', value: 'exa_get_contents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'highlights',
|
||||
title: 'Include Highlights',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_get_contents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Find Similar Links operation inputs
|
||||
{
|
||||
@@ -196,6 +208,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'long-input',
|
||||
placeholder: 'example.com, another.com (comma-separated)',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'excludeDomains',
|
||||
@@ -203,12 +216,14 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
type: 'long-input',
|
||||
placeholder: 'exclude.com, another.com (comma-separated)',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'excludeSourceDomain',
|
||||
title: 'Exclude Source Domain',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'category',
|
||||
@@ -228,18 +243,21 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'highlights',
|
||||
title: 'Include Highlights',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'Include Summary',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'livecrawl',
|
||||
@@ -252,6 +270,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
],
|
||||
value: () => 'never',
|
||||
condition: { field: 'operation', value: 'exa_find_similar_links' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Answer operation inputs
|
||||
{
|
||||
@@ -267,6 +286,7 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
title: 'Include Text',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'exa_answer' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Research operation inputs
|
||||
{
|
||||
@@ -309,16 +329,6 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numResults to a number for operations that use it
|
||||
if (params.numResults) {
|
||||
params.numResults = Number(params.numResults)
|
||||
}
|
||||
|
||||
// Convert subpages to a number if provided
|
||||
if (params.subpages) {
|
||||
params.subpages = Number(params.subpages)
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'exa_search':
|
||||
return 'exa_search'
|
||||
@@ -334,6 +344,16 @@ export const ExaBlock: BlockConfig<ExaResponse> = {
|
||||
return 'exa_search'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.numResults) {
|
||||
result.numResults = Number(params.numResults)
|
||||
}
|
||||
if (params.subpages) {
|
||||
result.subpages = Number(params.subpages)
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -606,45 +606,23 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric string fields to numbers
|
||||
if (params.panelId) {
|
||||
params.panelId = Number(params.panelId)
|
||||
}
|
||||
if (params.annotationId) {
|
||||
params.annotationId = Number(params.annotationId)
|
||||
}
|
||||
if (params.time) {
|
||||
params.time = Number(params.time)
|
||||
}
|
||||
if (params.timeEnd) {
|
||||
params.timeEnd = Number(params.timeEnd)
|
||||
}
|
||||
if (params.from) {
|
||||
params.from = Number(params.from)
|
||||
}
|
||||
if (params.to) {
|
||||
params.to = Number(params.to)
|
||||
}
|
||||
|
||||
// Map subblock fields to tool parameter names
|
||||
if (params.alertTitle) {
|
||||
params.title = params.alertTitle
|
||||
}
|
||||
if (params.folderTitle) {
|
||||
params.title = params.folderTitle
|
||||
}
|
||||
if (params.folderUidNew) {
|
||||
params.uid = params.folderUidNew
|
||||
}
|
||||
if (params.annotationTags) {
|
||||
params.tags = params.annotationTags
|
||||
}
|
||||
if (params.annotationDashboardUid) {
|
||||
params.dashboardUid = params.annotationDashboardUid
|
||||
}
|
||||
|
||||
if (params.alertTitle) params.title = params.alertTitle
|
||||
if (params.folderTitle) params.title = params.folderTitle
|
||||
if (params.folderUidNew) params.uid = params.folderUidNew
|
||||
if (params.annotationTags) params.tags = params.annotationTags
|
||||
if (params.annotationDashboardUid) params.dashboardUid = params.annotationDashboardUid
|
||||
return params.operation
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.panelId) result.panelId = Number(params.panelId)
|
||||
if (params.annotationId) result.annotationId = Number(params.annotationId)
|
||||
if (params.time) result.time = Number(params.time)
|
||||
if (params.timeEnd) result.timeEnd = Number(params.timeEnd)
|
||||
if (params.from) result.from = Number(params.from)
|
||||
if (params.to) result.to = Number(params.to)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -204,11 +204,6 @@ Return ONLY the search query text - no explanations.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric parameters
|
||||
if (params.limit) {
|
||||
params.limit = Number(params.limit)
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'hunter_discover':
|
||||
return 'hunter_discover'
|
||||
@@ -226,6 +221,11 @@ Return ONLY the search query text - no explanations.`,
|
||||
return 'hunter_domain_search'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -826,16 +826,6 @@ Return ONLY the JSON array - no explanations or markdown formatting.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert page_size to a number if provided
|
||||
if (params.page_size) {
|
||||
params.page_size = Number(params.page_size)
|
||||
}
|
||||
|
||||
// Convert notify_incident_channel from string to boolean
|
||||
if (params.notify_incident_channel !== undefined) {
|
||||
params.notify_incident_channel = params.notify_incident_channel === 'true'
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'incidentio_incidents_list':
|
||||
return 'incidentio_incidents_list'
|
||||
@@ -929,6 +919,14 @@ Return ONLY the JSON array - no explanations or markdown formatting.`,
|
||||
return 'incidentio_incidents_list'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.page_size) result.page_size = Number(params.page_size)
|
||||
if (params.notify_incident_channel !== undefined) {
|
||||
result.notify_incident_channel = params.notify_incident_channel === 'true'
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -169,17 +169,7 @@ export const LemlistBlock: BlockConfig<LemlistResponse> = {
|
||||
access: ['lemlist_get_activities', 'lemlist_get_lead', 'lemlist_send_email'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
if (params.limit) {
|
||||
params.limit = Number(params.limit)
|
||||
}
|
||||
if (params.offset) {
|
||||
params.offset = Number(params.offset)
|
||||
}
|
||||
// Map filterLeadId to leadId for get_activities tool
|
||||
if (params.filterLeadId) {
|
||||
params.leadId = params.filterLeadId
|
||||
}
|
||||
|
||||
if (params.filterLeadId) params.leadId = params.filterLeadId
|
||||
switch (params.operation) {
|
||||
case 'get_activities':
|
||||
return 'lemlist_get_activities'
|
||||
@@ -191,6 +181,12 @@ export const LemlistBlock: BlockConfig<LemlistResponse> = {
|
||||
return 'lemlist_get_activities'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
if (params.offset) result.offset = Number(params.offset)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -149,66 +149,48 @@ export const ParallelBlock: BlockConfig<ToolResponse> = {
|
||||
access: ['parallel_search', 'parallel_extract', 'parallel_deep_research'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
if (params.extract_objective) params.objective = params.extract_objective
|
||||
if (params.research_input) params.input = params.research_input
|
||||
switch (params.operation) {
|
||||
case 'search':
|
||||
// Convert search_queries from comma-separated string to array (if provided)
|
||||
if (params.search_queries && typeof params.search_queries === 'string') {
|
||||
const queries = params.search_queries
|
||||
.split(',')
|
||||
.map((query: string) => query.trim())
|
||||
.filter((query: string) => query.length > 0)
|
||||
// Only set if we have actual queries
|
||||
if (queries.length > 0) {
|
||||
params.search_queries = queries
|
||||
} else {
|
||||
params.search_queries = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Convert numeric parameters
|
||||
if (params.max_results) {
|
||||
params.max_results = Number(params.max_results)
|
||||
}
|
||||
if (params.max_chars_per_result) {
|
||||
params.max_chars_per_result = Number(params.max_chars_per_result)
|
||||
}
|
||||
|
||||
return 'parallel_search'
|
||||
|
||||
case 'extract':
|
||||
// Map extract_objective to objective for the tool
|
||||
params.objective = params.extract_objective
|
||||
|
||||
// Convert boolean strings to actual booleans with defaults
|
||||
if (params.excerpts === 'true' || params.excerpts === true) {
|
||||
params.excerpts = true
|
||||
} else if (params.excerpts === 'false' || params.excerpts === false) {
|
||||
params.excerpts = false
|
||||
} else {
|
||||
// Default to true if not provided
|
||||
params.excerpts = true
|
||||
}
|
||||
|
||||
if (params.full_content === 'true' || params.full_content === true) {
|
||||
params.full_content = true
|
||||
} else if (params.full_content === 'false' || params.full_content === false) {
|
||||
params.full_content = false
|
||||
} else {
|
||||
// Default to false if not provided
|
||||
params.full_content = false
|
||||
}
|
||||
|
||||
return 'parallel_extract'
|
||||
|
||||
case 'deep_research':
|
||||
// Map research_input to input for the tool
|
||||
params.input = params.research_input
|
||||
return 'parallel_deep_research'
|
||||
|
||||
default:
|
||||
return 'parallel_search'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
const operation = params.operation
|
||||
|
||||
if (operation === 'search') {
|
||||
if (params.search_queries && typeof params.search_queries === 'string') {
|
||||
const queries = params.search_queries
|
||||
.split(',')
|
||||
.map((query: string) => query.trim())
|
||||
.filter((query: string) => query.length > 0)
|
||||
if (queries.length > 0) {
|
||||
result.search_queries = queries
|
||||
} else {
|
||||
result.search_queries = undefined
|
||||
}
|
||||
}
|
||||
if (params.max_results) result.max_results = Number(params.max_results)
|
||||
if (params.max_chars_per_result) {
|
||||
result.max_chars_per_result = Number(params.max_chars_per_result)
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'extract') {
|
||||
result.excerpts = !(params.excerpts === 'false' || params.excerpts === false)
|
||||
result.full_content = params.full_content === 'true' || params.full_content === true
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -1185,22 +1185,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric parameters
|
||||
if (params.limit) params.limit = Number(params.limit)
|
||||
if (params.offset) params.offset = Number(params.offset)
|
||||
if (params.rolloutPercentage) params.rolloutPercentage = Number(params.rolloutPercentage)
|
||||
|
||||
// Map projectIdParam to projectId for get_project operation
|
||||
// Field renames in tool() are safe (they copy values, not coerce types)
|
||||
// and are needed for serialization-time validation of required fields
|
||||
if (params.operation === 'posthog_get_project' && params.projectIdParam) {
|
||||
params.projectId = params.projectIdParam
|
||||
}
|
||||
|
||||
// Map personalApiKey to apiKey for all private endpoint tools
|
||||
if (params.personalApiKey) {
|
||||
params.apiKey = params.personalApiKey
|
||||
}
|
||||
|
||||
// Map featureFlagId to flagId for feature flag operations
|
||||
const flagOps = [
|
||||
'posthog_get_feature_flag',
|
||||
'posthog_update_feature_flag',
|
||||
@@ -1210,7 +1203,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
params.flagId = params.featureFlagId
|
||||
}
|
||||
|
||||
// Map surveyType to type for survey operations
|
||||
if (
|
||||
(params.operation === 'posthog_create_survey' ||
|
||||
params.operation === 'posthog_update_survey') &&
|
||||
@@ -1219,37 +1211,30 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
params.type = params.surveyType
|
||||
}
|
||||
|
||||
// Map isStatic for cohorts
|
||||
if (params.operation === 'posthog_create_cohort' && params.isStatic !== undefined) {
|
||||
params.is_static = params.isStatic
|
||||
}
|
||||
|
||||
// Map dateMarker to date_marker for annotations
|
||||
if (params.operation === 'posthog_create_annotation' && params.dateMarker) {
|
||||
params.date_marker = params.dateMarker
|
||||
}
|
||||
|
||||
// Map propertyType to property_type
|
||||
if (params.operation === 'posthog_update_property_definition' && params.propertyType) {
|
||||
params.property_type = params.propertyType
|
||||
}
|
||||
|
||||
// Map insightQuery to query for insights
|
||||
if (params.operation === 'posthog_create_insight' && params.insightQuery) {
|
||||
params.query = params.insightQuery
|
||||
}
|
||||
|
||||
// Map insightTags to tags for insights
|
||||
if (params.operation === 'posthog_create_insight' && params.insightTags) {
|
||||
params.tags = params.insightTags
|
||||
}
|
||||
|
||||
// Map distinctIdFilter to distinctId for list_persons
|
||||
if (params.operation === 'posthog_list_persons' && params.distinctIdFilter) {
|
||||
params.distinctId = params.distinctIdFilter
|
||||
}
|
||||
|
||||
// Map experiment date fields
|
||||
if (params.operation === 'posthog_create_experiment') {
|
||||
if (params.experimentStartDate) {
|
||||
params.startDate = params.experimentStartDate
|
||||
@@ -1259,7 +1244,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Map survey date fields
|
||||
if (
|
||||
params.operation === 'posthog_create_survey' ||
|
||||
params.operation === 'posthog_update_survey'
|
||||
@@ -1272,13 +1256,17 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert responsesLimit to number
|
||||
if (params.responsesLimit) {
|
||||
params.responsesLimit = Number(params.responsesLimit)
|
||||
}
|
||||
|
||||
return params.operation as string
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
if (params.offset) result.offset = Number(params.offset)
|
||||
if (params.rolloutPercentage) result.rolloutPercentage = Number(params.rolloutPercentage)
|
||||
if (params.responsesLimit) result.responsesLimit = Number(params.responsesLimit)
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -602,11 +602,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric fields
|
||||
if (params.limit) {
|
||||
params.limit = Number(params.limit)
|
||||
}
|
||||
|
||||
// Return the appropriate tool based on operation
|
||||
switch (params.operation) {
|
||||
case 'sentry_issues_list':
|
||||
@@ -637,6 +632,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
return 'sentry_issues_list'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -755,43 +755,24 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric parameters
|
||||
if (params.limit) {
|
||||
params.limit = Number(params.limit)
|
||||
}
|
||||
if (params.volume_percent) {
|
||||
params.volume_percent = Number(params.volume_percent)
|
||||
}
|
||||
if (params.range_start) {
|
||||
params.range_start = Number(params.range_start)
|
||||
}
|
||||
if (params.insert_before) {
|
||||
params.insert_before = Number(params.insert_before)
|
||||
}
|
||||
if (params.range_length) {
|
||||
params.range_length = Number(params.range_length)
|
||||
}
|
||||
if (params.position_ms) {
|
||||
params.position_ms = Number(params.position_ms)
|
||||
}
|
||||
// Map followType to type for check_following
|
||||
if (params.followType) {
|
||||
params.type = params.followType
|
||||
}
|
||||
// Map newName to name for update_playlist
|
||||
if (params.newName) {
|
||||
params.name = params.newName
|
||||
}
|
||||
// Map playUris to uris for play
|
||||
if (params.playUris) {
|
||||
params.uris = params.playUris
|
||||
}
|
||||
// Normalize file input for cover image
|
||||
if (params.coverImage !== undefined) {
|
||||
params.coverImage = normalizeFileInput(params.coverImage, { single: true })
|
||||
}
|
||||
if (params.followType) params.type = params.followType
|
||||
if (params.newName) params.name = params.newName
|
||||
if (params.playUris) params.uris = params.playUris
|
||||
return params.operation || 'spotify_search'
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.limit) result.limit = Number(params.limit)
|
||||
if (params.volume_percent) result.volume_percent = Number(params.volume_percent)
|
||||
if (params.range_start) result.range_start = Number(params.range_start)
|
||||
if (params.insert_before) result.insert_before = Number(params.insert_before)
|
||||
if (params.range_length) result.range_length = Number(params.range_length)
|
||||
if (params.position_ms) result.position_ms = Number(params.position_ms)
|
||||
if (params.coverImage !== undefined) {
|
||||
result.coverImage = normalizeFileInput(params.coverImage, { single: true })
|
||||
}
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -64,11 +64,6 @@ export const WikipediaBlock: BlockConfig<WikipediaResponse> = {
|
||||
access: ['wikipedia_summary', 'wikipedia_search', 'wikipedia_content', 'wikipedia_random'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert searchLimit to a number for search operation
|
||||
if (params.searchLimit) {
|
||||
params.searchLimit = Number(params.searchLimit)
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'wikipedia_summary':
|
||||
return 'wikipedia_summary'
|
||||
@@ -82,6 +77,11 @@ export const WikipediaBlock: BlockConfig<WikipediaResponse> = {
|
||||
return 'wikipedia_summary'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.searchLimit) result.searchLimit = Number(params.searchLimit)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -442,11 +442,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
// Convert numeric parameters
|
||||
if (params.maxResults) {
|
||||
params.maxResults = Number(params.maxResults)
|
||||
}
|
||||
|
||||
switch (params.operation) {
|
||||
case 'youtube_search':
|
||||
return 'youtube_search'
|
||||
@@ -470,6 +465,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
return 'youtube_search'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const result: Record<string, unknown> = {}
|
||||
if (params.maxResults) result.maxResults = Number(params.maxResults)
|
||||
return result
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
|
||||
@@ -7,7 +7,11 @@ import { BlockResolver } from '@/executor/variables/resolvers/block'
|
||||
import { EnvResolver } from '@/executor/variables/resolvers/env'
|
||||
import { LoopResolver } from '@/executor/variables/resolvers/loop'
|
||||
import { ParallelResolver } from '@/executor/variables/resolvers/parallel'
|
||||
import type { ResolutionContext, Resolver } from '@/executor/variables/resolvers/reference'
|
||||
import {
|
||||
RESOLVED_EMPTY,
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import { WorkflowResolver } from '@/executor/variables/resolvers/workflow'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
@@ -104,7 +108,11 @@ export class VariableResolver {
|
||||
loopScope,
|
||||
}
|
||||
|
||||
return this.resolveReference(trimmed, resolutionContext)
|
||||
const result = this.resolveReference(trimmed, resolutionContext)
|
||||
if (result === RESOLVED_EMPTY) {
|
||||
return null
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +182,13 @@ export class VariableResolver {
|
||||
return match
|
||||
}
|
||||
|
||||
if (resolved === RESOLVED_EMPTY) {
|
||||
if (blockType === BlockType.FUNCTION) {
|
||||
return this.blockResolver.formatValueForBlock(null, blockType, language)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return this.blockResolver.formatValueForBlock(resolved, blockType, language)
|
||||
} catch (error) {
|
||||
replacementError = error instanceof Error ? error : new Error(String(error))
|
||||
@@ -207,7 +222,6 @@ export class VariableResolver {
|
||||
|
||||
let replacementError: Error | null = null
|
||||
|
||||
// Use generic utility for smart variable reference replacement
|
||||
let result = replaceValidReferences(template, (match) => {
|
||||
if (replacementError) return match
|
||||
|
||||
@@ -217,6 +231,10 @@ export class VariableResolver {
|
||||
return match
|
||||
}
|
||||
|
||||
if (resolved === RESOLVED_EMPTY) {
|
||||
return 'null'
|
||||
}
|
||||
|
||||
if (typeof resolved === 'string') {
|
||||
const escaped = resolved.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
||||
return `'${escaped}'`
|
||||
|
||||
@@ -2,7 +2,7 @@ import { loggerMock } from '@sim/testing'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ExecutionState } from '@/executor/execution/state'
|
||||
import { BlockResolver } from './block'
|
||||
import type { ResolutionContext } from './reference'
|
||||
import { RESOLVED_EMPTY, type ResolutionContext } from './reference'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
vi.mock('@/blocks/registry', async () => {
|
||||
@@ -134,15 +134,18 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.items.1.id>', ctx)).toBe(2)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent path when no schema defined', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { existing: 'value' },
|
||||
})
|
||||
it.concurrent(
|
||||
'should return RESOLVED_EMPTY for non-existent path when no schema defined',
|
||||
() => {
|
||||
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { existing: 'value' },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.nonexistent>', ctx)).toBeUndefined()
|
||||
})
|
||||
expect(resolver.resolve('<source.nonexistent>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('should throw error for path not in output schema', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
@@ -162,7 +165,7 @@ describe('BlockResolver', () => {
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(/Available fields:/)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for path in schema but missing in data', () => {
|
||||
it.concurrent('should return RESOLVED_EMPTY for path in schema but missing in data', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
@@ -175,7 +178,7 @@ describe('BlockResolver', () => {
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.stdout>', ctx)).toBe('log output')
|
||||
expect(resolver.resolve('<source.result>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<source.result>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
@@ -191,7 +194,7 @@ describe('BlockResolver', () => {
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {})
|
||||
|
||||
expect(resolver.resolve('<workflow.childTraceSpans>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<workflow.childTraceSpans>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -208,7 +211,7 @@ describe('BlockResolver', () => {
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {})
|
||||
|
||||
expect(resolver.resolve('<workflowinput.childTraceSpans>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<workflowinput.childTraceSpans>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -225,13 +228,13 @@ describe('BlockResolver', () => {
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {})
|
||||
|
||||
expect(resolver.resolve('<hitl.response>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<hitl.submission>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<hitl.resumeInput>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<hitl.response>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
expect(resolver.resolve('<hitl.submission>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
expect(resolver.resolve('<hitl.resumeInput>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('should return undefined for non-existent block', () => {
|
||||
it.concurrent('should return undefined for block not in workflow', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'existing' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {})
|
||||
@@ -239,6 +242,21 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<nonexistent>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should return RESOLVED_EMPTY for block in workflow that did not execute', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{ id: 'start-block', name: 'Start', type: 'start_trigger' },
|
||||
{ id: 'slack-block', name: 'Slack', type: 'slack_trigger' },
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
'slack-block': { message: 'hello from slack' },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<slack.message>', ctx)).toBe('hello from slack')
|
||||
expect(resolver.resolve('<start>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
expect(resolver.resolve('<start.input>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
})
|
||||
|
||||
it.concurrent('should fall back to context blockStates', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
@@ -1012,24 +1030,24 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.other>', ctx)).toBe('exists')
|
||||
})
|
||||
|
||||
it.concurrent('should handle output with undefined values', () => {
|
||||
it.concurrent('should return RESOLVED_EMPTY for output with undefined values', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { value: undefined, other: 'exists' },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.value>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<source.value>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for deeply nested non-existent path', () => {
|
||||
it.concurrent('should return RESOLVED_EMPTY for deeply nested non-existent path', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source', type: 'unknown_block_type' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { level1: { level2: {} } },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.level1.level2.level3>', ctx)).toBeUndefined()
|
||||
expect(resolver.resolve('<source.level1.level2.level3>', ctx)).toBe(RESOLVED_EMPTY)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
|
||||
import {
|
||||
navigatePath,
|
||||
RESOLVED_EMPTY,
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
@@ -84,7 +85,12 @@ export class BlockResolver implements Resolver {
|
||||
return result.value
|
||||
}
|
||||
|
||||
return this.handleBackwardsCompat(block, output, pathParts)
|
||||
const backwardsCompat = this.handleBackwardsCompat(block, output, pathParts)
|
||||
if (backwardsCompat !== undefined) {
|
||||
return backwardsCompat
|
||||
}
|
||||
|
||||
return RESOLVED_EMPTY
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidFieldError) {
|
||||
const fallback = this.handleBackwardsCompat(block, output, pathParts)
|
||||
|
||||
@@ -12,6 +12,14 @@ export interface Resolver {
|
||||
resolve(reference: string, context: ResolutionContext): any
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentinel value indicating a reference was resolved to a known block
|
||||
* that produced no output (e.g., the block exists in the workflow but
|
||||
* didn't execute on this path). Distinct from `undefined`, which means
|
||||
* the reference couldn't be matched to any block at all.
|
||||
*/
|
||||
export const RESOLVED_EMPTY = Symbol('RESOLVED_EMPTY')
|
||||
|
||||
/**
|
||||
* Navigate through nested object properties using a path array.
|
||||
* Supports dot notation and array indices.
|
||||
|
||||
@@ -679,11 +679,15 @@ function spawnWorker(): Promise<WorkerInfo> {
|
||||
}
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const workerPath = path.join(currentDir, 'isolated-vm-worker.cjs')
|
||||
const candidatePaths = [
|
||||
path.join(currentDir, 'isolated-vm-worker.cjs'),
|
||||
path.join(process.cwd(), 'lib', 'execution', 'isolated-vm-worker.cjs'),
|
||||
]
|
||||
const workerPath = candidatePaths.find((p) => fs.existsSync(p))
|
||||
|
||||
if (!fs.existsSync(workerPath)) {
|
||||
if (!workerPath) {
|
||||
settleSpawnInProgress()
|
||||
reject(new Error(`Worker file not found at ${workerPath}`))
|
||||
reject(new Error(`Worker file not found at any of: ${candidatePaths.join(', ')}`))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { additionalPackages } from '@trigger.dev/build/extensions/core'
|
||||
import { additionalFiles, additionalPackages } from '@trigger.dev/build/extensions/core'
|
||||
import { defineConfig } from '@trigger.dev/sdk'
|
||||
import { env } from './lib/core/config/env'
|
||||
|
||||
@@ -15,9 +15,11 @@ export default defineConfig({
|
||||
},
|
||||
dirs: ['./background'],
|
||||
build: {
|
||||
external: ['isolated-vm'],
|
||||
extensions: [
|
||||
additionalFiles({ files: ['./lib/execution/isolated-vm-worker.cjs'] }),
|
||||
additionalPackages({
|
||||
packages: ['unpdf', 'pdf-lib'],
|
||||
packages: ['unpdf', 'pdf-lib', 'isolated-vm'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user