Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
322 lines
9 KiB
JavaScript
322 lines
9 KiB
JavaScript
/**
|
|
* Labs Node Execution Operations Routes
|
|
* Rerun and split operations for lab nodes
|
|
* @see ADR-043: MindWorkflow Integration
|
|
*/
|
|
import { Router } from 'express';
|
|
import crypto from 'crypto';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success } from '../../../utils/response.js';
|
|
import { LabsService } from '../../../services/labs/index.js';
|
|
import {
|
|
getTableRows,
|
|
insertRow,
|
|
updateRowByField
|
|
} from './_helpers.js';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* POST /api/v3/labs/:labTableId/nodes/:nodeId/rerun
|
|
* Re-execute node with optional clone
|
|
*/
|
|
router.post('/:labTableId/nodes/:nodeId/rerun', async (req, res) => {
|
|
const startTime = Date.now();
|
|
const runId = crypto.randomUUID();
|
|
const logs = [];
|
|
|
|
try {
|
|
const { labTableId, nodeId } = req.params;
|
|
const { input, context = {}, clone = false } = req.body;
|
|
|
|
logs.push(`[${new Date().toISOString()}] Starting node re-execution (clone: ${clone})`);
|
|
|
|
// Get the node data
|
|
const rows = await getTableRows(labTableId);
|
|
const nodeRow = rows.find(row => row.node_id === nodeId);
|
|
|
|
if (!nodeRow) {
|
|
logs.push(`[${new Date().toISOString()}] Node not found: ${nodeId}`);
|
|
return success(res, {
|
|
status: 'error',
|
|
nodeId,
|
|
content: null,
|
|
contentType: null,
|
|
logs,
|
|
runId,
|
|
cloned: false,
|
|
executionTime: Date.now() - startTime
|
|
});
|
|
}
|
|
|
|
logs.push(`[${new Date().toISOString()}] Found node: ${nodeRow.title} (type: ${nodeRow.type_key})`);
|
|
|
|
let targetNodeId = nodeId;
|
|
let cloned = false;
|
|
|
|
// If clone requested, create a copy of the node
|
|
if (clone) {
|
|
const newNodeId = crypto.randomUUID();
|
|
const clonedNodeData = {
|
|
node_id: newNodeId,
|
|
type_key: nodeRow.type_key,
|
|
title: `${nodeRow.title} (copy)`,
|
|
content: nodeRow.content || '',
|
|
position_x: (nodeRow.position_x || 0) + 50,
|
|
position_y: (nodeRow.position_y || 0) + 50,
|
|
width: nodeRow.width,
|
|
height: nodeRow.height,
|
|
edges: [], // Don't copy edges for cloned node
|
|
ai_agent_id: nodeRow.ai_agent_id,
|
|
config: nodeRow.config,
|
|
order_index: (nodeRow.order_index || 0) + 1
|
|
};
|
|
|
|
await insertRow(labTableId, clonedNodeData, req.user?.id);
|
|
targetNodeId = newNodeId;
|
|
cloned = true;
|
|
logs.push(`[${new Date().toISOString()}] Created clone: ${newNodeId}`);
|
|
}
|
|
|
|
// Parse config if it's a string
|
|
if (typeof nodeRow.config === 'string') {
|
|
try {
|
|
nodeRow.config = JSON.parse(nodeRow.config);
|
|
} catch (e) {
|
|
nodeRow.config = {};
|
|
}
|
|
}
|
|
|
|
// Enhanced context with input
|
|
const enhancedContext = {
|
|
...context,
|
|
input
|
|
};
|
|
|
|
// Execute the node
|
|
logs.push(`[${new Date().toISOString()}] Executing node...`);
|
|
const result = await LabsService.executeNode(nodeRow, enhancedContext);
|
|
|
|
const executionTime = Date.now() - startTime;
|
|
logs.push(`[${new Date().toISOString()}] Execution completed in ${executionTime}ms`);
|
|
|
|
if (result.success) {
|
|
logs.push(`[${new Date().toISOString()}] Result: success`);
|
|
|
|
// Update the target node with the result
|
|
if (result.output || result.content) {
|
|
await updateRowByField(labTableId, 'node_id', targetNodeId, {
|
|
content: result.output || result.content
|
|
});
|
|
logs.push(`[${new Date().toISOString()}] Updated node content`);
|
|
}
|
|
} else {
|
|
logs.push(`[${new Date().toISOString()}] Result: error - ${result.error || 'Unknown error'}`);
|
|
}
|
|
|
|
apiLogger.info({
|
|
nodeId,
|
|
targetNodeId,
|
|
runId,
|
|
type: nodeRow.type_key,
|
|
success: result.success,
|
|
cloned,
|
|
tokensUsed: result.tokensUsed,
|
|
executionTime
|
|
}, 'Node rerun completed');
|
|
|
|
// Return MindWorkflow RunResponse format
|
|
success(res, {
|
|
status: result.success ? 'success' : 'error',
|
|
nodeId,
|
|
targetNodeId,
|
|
content: result.output || result.content || null,
|
|
contentType: result.contentType || nodeRow.type_key || null,
|
|
logs,
|
|
runId,
|
|
cloned,
|
|
tokensUsed: result.tokensUsed || result.usage?.totalTokens || undefined,
|
|
executionTime,
|
|
provider: result.provider || undefined,
|
|
model: result.model || undefined
|
|
});
|
|
} catch (err) {
|
|
const executionTime = Date.now() - startTime;
|
|
logs.push(`[${new Date().toISOString()}] Error: ${err.message}`);
|
|
apiLogger.error({ err, runId }, 'Failed to rerun lab node');
|
|
|
|
success(res, {
|
|
status: 'error',
|
|
nodeId: req.params.nodeId,
|
|
content: null,
|
|
contentType: null,
|
|
logs,
|
|
runId,
|
|
cloned: false,
|
|
executionTime
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/v3/labs/:labTableId/nodes/:nodeId/split
|
|
* Split text node into multiple nodes
|
|
*/
|
|
router.post('/:labTableId/nodes/:nodeId/split', async (req, res) => {
|
|
const startTime = Date.now();
|
|
const runId = crypto.randomUUID();
|
|
const logs = [];
|
|
|
|
try {
|
|
const { labTableId, nodeId } = req.params;
|
|
const { separator = '\n\n' } = req.body;
|
|
|
|
logs.push(`[${new Date().toISOString()}] Starting node split (separator: ${JSON.stringify(separator)})`);
|
|
|
|
// Get the node data
|
|
const rows = await getTableRows(labTableId);
|
|
const nodeRow = rows.find(row => row.node_id === nodeId);
|
|
|
|
if (!nodeRow) {
|
|
logs.push(`[${new Date().toISOString()}] Node not found: ${nodeId}`);
|
|
return success(res, {
|
|
status: 'error',
|
|
nodeId,
|
|
content: null,
|
|
contentType: null,
|
|
logs,
|
|
runId,
|
|
createdNodes: [],
|
|
executionTime: Date.now() - startTime
|
|
});
|
|
}
|
|
|
|
logs.push(`[${new Date().toISOString()}] Found node: ${nodeRow.title} (type: ${nodeRow.type_key})`);
|
|
|
|
// Get content to split
|
|
const content = nodeRow.content || '';
|
|
if (!content.trim()) {
|
|
logs.push(`[${new Date().toISOString()}] Node has no content to split`);
|
|
return success(res, {
|
|
status: 'error',
|
|
nodeId,
|
|
content: null,
|
|
contentType: null,
|
|
logs,
|
|
runId,
|
|
createdNodes: [],
|
|
executionTime: Date.now() - startTime
|
|
});
|
|
}
|
|
|
|
// Split content
|
|
const parts = content.split(separator).filter(part => part.trim());
|
|
logs.push(`[${new Date().toISOString()}] Split into ${parts.length} parts`);
|
|
|
|
if (parts.length <= 1) {
|
|
logs.push(`[${new Date().toISOString()}] Content cannot be split (only 1 part)`);
|
|
return success(res, {
|
|
status: 'success',
|
|
nodeId,
|
|
content: content,
|
|
contentType: nodeRow.type_key,
|
|
logs,
|
|
runId,
|
|
createdNodes: [],
|
|
executionTime: Date.now() - startTime
|
|
});
|
|
}
|
|
|
|
// Parse config if it's a string
|
|
let nodeConfig = nodeRow.config;
|
|
if (typeof nodeConfig === 'string') {
|
|
try {
|
|
nodeConfig = JSON.parse(nodeConfig);
|
|
} catch (e) {
|
|
nodeConfig = {};
|
|
}
|
|
}
|
|
|
|
// Create new nodes for each part
|
|
const createdNodes = [];
|
|
const baseX = nodeRow.position_x || 0;
|
|
const baseY = nodeRow.position_y || 0;
|
|
const offsetY = (nodeRow.height || 150) + 30; // Vertical spacing
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const newNodeId = crypto.randomUUID();
|
|
const partContent = parts[i].trim();
|
|
|
|
// Generate title from first line or first 50 chars
|
|
const firstLine = partContent.split('\n')[0];
|
|
const title = firstLine.length > 50
|
|
? firstLine.substring(0, 47) + '...'
|
|
: firstLine || `Part ${i + 1}`;
|
|
|
|
const newNodeData = {
|
|
node_id: newNodeId,
|
|
type_key: nodeRow.type_key,
|
|
title: title,
|
|
content: partContent,
|
|
position_x: baseX,
|
|
position_y: baseY + (offsetY * (i + 1)),
|
|
width: nodeRow.width,
|
|
height: nodeRow.height,
|
|
edges: [],
|
|
ai_agent_id: nodeRow.ai_agent_id,
|
|
config: nodeConfig,
|
|
order_index: (nodeRow.order_index || 0) + i + 1
|
|
};
|
|
|
|
await insertRow(labTableId, newNodeData, req.user?.id);
|
|
|
|
createdNodes.push({
|
|
node_id: newNodeId,
|
|
type: nodeRow.type_key,
|
|
title: title
|
|
});
|
|
|
|
logs.push(`[${new Date().toISOString()}] Created node ${i + 1}: ${newNodeId}`);
|
|
}
|
|
|
|
const executionTime = Date.now() - startTime;
|
|
logs.push(`[${new Date().toISOString()}] Split completed in ${executionTime}ms`);
|
|
|
|
apiLogger.info({
|
|
nodeId,
|
|
runId,
|
|
partsCount: parts.length,
|
|
createdNodesCount: createdNodes.length,
|
|
executionTime
|
|
}, 'Node split completed');
|
|
|
|
// Return MindWorkflow RunResponse format
|
|
success(res, {
|
|
status: 'success',
|
|
nodeId,
|
|
content: content,
|
|
contentType: nodeRow.type_key,
|
|
logs,
|
|
runId,
|
|
createdNodes,
|
|
executionTime
|
|
});
|
|
} catch (err) {
|
|
const executionTime = Date.now() - startTime;
|
|
logs.push(`[${new Date().toISOString()}] Error: ${err.message}`);
|
|
apiLogger.error({ err, runId }, 'Failed to split lab node');
|
|
|
|
success(res, {
|
|
status: 'error',
|
|
nodeId: req.params.nodeId,
|
|
content: null,
|
|
contentType: null,
|
|
logs,
|
|
runId,
|
|
createdNodes: [],
|
|
executionTime
|
|
});
|
|
}
|
|
});
|
|
|
|
export default router;
|