@net3/queuer
Version:
346 lines (304 loc) • 11.3 kB
text/typescript
import {
createAction,
Property,
ActionContext,
StoreScope,
DynamicPropsValue,
} from '@activepieces/pieces-framework';
import { PauseType } from '@activepieces/shared';
import { QueueManager } from '../common/queue-manager';
import { MCPManager } from '../common/mcp-manager';
/**
* Standalone helper function to execute a queued item.
* This is called after the pause resumes.
*/
async function executeQueuedItem(context: ActionContext, queueItemId: string) {
let debugInfo: any = { step: 'starting', queueItemId };
try {
debugInfo.step = 'getQueueItem';
let item = await QueueManager.getQueueItem(context, queueItemId);
if (!item) {
throw new Error(`Queue item not found for ID: ${queueItemId} [Step: getQueueItem]`);
}
debugInfo.item = { queueId: item.queueId, status: item.status, releaseTime: item.releaseTime };
/* Validate position / repause if needed */
debugInfo.step = 'validateQueuePosition';
const validation = await QueueManager.validateQueuePosition(
context,
queueItemId,
);
debugInfo.validation = validation;
if (validation.shouldRepause && validation.newReleaseTime) {
debugInfo.step = 'repause';
debugInfo.newReleaseTime = validation.newReleaseTime;
await context.run.pause({
pauseMetadata: {
type: PauseType.DELAY,
resumeDateTime: new Date(validation.newReleaseTime).toUTCString(),
},
});
// After pause, re-run this function
return await executeQueuedItem(context, queueItemId);
}
if (!validation.isValid) {
throw new Error(`Queue validation failed: ${validation.reason} [Step: validateQueuePosition] [Debug: ${JSON.stringify(debugInfo)}]`);
}
/* Execute */
debugInfo.step = 'getQueueConfiguration';
const queue = await QueueManager.getQueueConfiguration(context, item.queueId);
if (!queue) {
throw new Error(`Queue config missing for ID: ${item.queueId} [Step: getQueueConfiguration] [Debug: ${JSON.stringify(debugInfo)}]`);
}
debugInfo.queue = { id: queue.id, mcpToolName: queue.mcpToolName };
debugInfo.step = 'updateItemStatus-processing';
await QueueManager.updateItemStatus(context, queueItemId, 'processing');
// Get MCP server URL from auth
debugInfo.step = 'checkAuth';
const authConfig = context.auth as { mcpServerUrl?: string };
if (!authConfig.mcpServerUrl) {
throw new Error(`MCP Server URL is required in auth configuration [Step: checkAuth] [Auth: ${JSON.stringify(authConfig)}] [Debug: ${JSON.stringify(debugInfo)}]`);
}
debugInfo.mcpServerUrl = authConfig.mcpServerUrl;
const toolName = queue.mcpToolName;
if (!toolName) {
throw new Error(`Queue configuration missing tool name [Step: checkAuth] [Queue: ${JSON.stringify(queue)}] [Debug: ${JSON.stringify(debugInfo)}]`);
}
debugInfo.toolName = toolName;
debugInfo.step = 'executeMCPAction';
const config = {
mcpServerUrl: authConfig.mcpServerUrl
};
const result = await MCPManager.executeMCPAction(toolName, item.actionConfig, config);
debugInfo.mcpResult = { success: result.success, hasError: !!result.error };
if (result.success) {
debugInfo.step = 'cleanup-success';
await QueueManager.incrementUsageCounters(context, queue.id);
await QueueManager.markExecutionComplete(context, queue.id, queueItemId);
await QueueManager.updateItemStatus(context, queueItemId, 'completed');
await QueueManager.cleanupQueueItem(
context,
queue.id,
queueItemId,
item.releaseTime,
);
const finalStatus = await QueueManager.getQueueStatus(context, queue.id);
return {
success: true,
queueItemId,
executedAt: new Date().toISOString(),
result: result.result,
status: finalStatus,
debugInfo: debugInfo,
};
} else {
debugInfo.step = 'cleanup-failure';
debugInfo.mcpError = result.error;
await QueueManager.markExecutionComplete(context, queue.id, queueItemId);
await QueueManager.updateItemStatus(
context,
queueItemId,
'failed',
result.error,
);
await QueueManager.cleanupQueueItem(
context,
queue.id,
queueItemId,
item.releaseTime,
);
throw new Error(`MCP execution failed: ${result.error || 'Unknown error'} [Step: executeMCPAction] [Debug: ${JSON.stringify(debugInfo)}]`);
}
} catch (error) {
// Add debug context to the original error for post-pause execution debugging
if (error instanceof Error) {
error.message = `${error.message} [DEBUG - LastStep: ${debugInfo.step}, Context: ${JSON.stringify(debugInfo)}]`;
}
throw error;
}
}
/**
* Helper to generate dynamic properties based on MCP tool schema
*/
async function generateDynamicProps(data: DynamicPropsValue): Promise<Record<string, any>> {
const queueId = data.queueId as string | undefined;
if (!queueId) return {};
try {
// Extract tool name from queue ID (format: mcp_toolname_queue)
const toolNameMatch = queueId.match(/^mcp_(.+)_queue$/);
if (!toolNameMatch) {
throw new Error(`Invalid queue ID format: ${queueId}`);
}
const toolName = toolNameMatch[1];
const authConfig = data.auth as { mcpServerUrl?: string };
if (!authConfig.mcpServerUrl) {
throw new Error('MCP Server URL not found in auth config');
}
const config = { mcpServerUrl: authConfig.mcpServerUrl };
const tools = await MCPManager.listMCPTools(config);
const tool = tools.find((t: any) => t.name === toolName);
if (!tool) {
throw new Error(`Tool not found: ${toolName}`);
}
if (!tool.inputSchema || !tool.inputSchema.properties) {
throw new Error(`Tool has no input schema: ${toolName}`);
}
const dynamicProps: Record<string, any> = {};
const schema = tool.inputSchema;
const required = schema.required || [];
// Generate properties based on the tool's input schema
Object.keys(schema.properties).forEach(propName => {
const propDef = schema.properties[propName];
const isRequired = required.includes(propName);
const displayName = propName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
const description = propDef.description || `${propName} parameter for ${tool.name}`;
if (propDef.type === 'string') {
dynamicProps[propName] = Property.ShortText({
displayName,
description,
required: isRequired,
});
} else if (propDef.type === 'number' || propDef.type === 'integer') {
dynamicProps[propName] = Property.Number({
displayName,
description,
required: isRequired,
});
} else if (propDef.type === 'boolean') {
dynamicProps[propName] = Property.Checkbox({
displayName,
description,
required: isRequired,
});
} else if (propDef.type === 'array') {
dynamicProps[propName] = Property.Array({
displayName,
description,
required: isRequired,
});
} else {
// For objects or unknown types, use JSON
dynamicProps[propName] = Property.Json({
displayName,
description,
required: isRequired,
});
}
});
return dynamicProps;
} catch (error) {
// Return empty object if dynamic props generation fails
// This allows the form to load, error will surface during execution
return {};
}
}
export const addToQueue = createAction({
name: 'add_to_queue',
displayName: 'Add to Queue',
description: 'Add an item to an existing queue and pause until execution time',
props: {
queueId: Property.Dropdown({
displayName: 'Select Queue',
description: 'Available MCP tool queues (run Create/Update Queue first to create queues)',
required: true,
refreshers: [],
options: async ({ auth }) => {
const authConfig = auth as { mcpServerUrl?: string };
if (!authConfig.mcpServerUrl) {
return {
options: [],
placeholder: 'MCP Server URL required in auth configuration'
};
}
try {
const config = { mcpServerUrl: authConfig.mcpServerUrl };
const tools = await MCPManager.listMCPTools(config);
if (tools.length === 0) {
return {
options: [],
placeholder: 'No MCP tools found'
};
}
// Generate queue options based on available tools
// The actual existence check will happen during execution
const queueOptions = tools.map((tool: any) => ({
label: `Queue: ${tool.name} - ${tool.description || 'No description'}`,
value: `mcp_${tool.name}_queue`,
}));
return {
options: queueOptions,
placeholder: 'Select a tool queue'
};
} catch (error) {
return {
options: [],
placeholder: `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}`
};
}
},
}),
toolParameters: Property.DynamicProperties({
displayName: 'Tool Parameters',
description: 'Parameters for the MCP tool (generated from tool schema)',
required: false,
refreshers: ['queueId'],
props: generateDynamicProps,
}),
},
async run(context: ActionContext) {
const {
queueId,
...otherProps
} = context.propsValue;
if (!queueId) {
throw new Error('Queue ID is required');
}
const queue = await QueueManager.getQueueConfiguration(
context,
queueId as string,
);
if (!queue) {
throw new Error(
`Queue "${queueId}" not found. Run Create/Update Queue first.`,
);
}
const limits = await QueueManager.checkRateLimits(context, queueId as string);
const { delayMs } = await QueueManager.calculateItemDelay(
context,
queue,
limits,
);
// Extract the dynamic properties as action config (excluding queueId)
const actionConfig = otherProps;
const toolName = queue.mcpToolName;
if (!toolName) {
throw new Error('Queue configuration missing tool name');
}
const { queueItemId, releaseTime } = await QueueManager.addItemToQueue(
context,
queueId as string,
toolName,
actionConfig,
delayMs,
);
await context.store.put(
'current_queue_item_id',
queueItemId,
StoreScope.PROJECT,
);
await context.run.pause({
pauseMetadata: {
type: PauseType.DELAY,
resumeDateTime: new Date(releaseTime).toUTCString(),
},
});
/* ============ RESUME ============ */
const storedQueueItemId = await context.store.get<string>(
'current_queue_item_id',
StoreScope.PROJECT,
);
if (!storedQueueItemId) {
throw new Error('Could not find queue item ID after resume');
}
return await executeQueuedItem(context, storedQueueItemId);
},
});