import { randomUUID } from 'node:crypto' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import type { WorkflowNode, WorkflowPort } from './workflows.ts' import type { ConditionCase } from './workflow-builder-types.ts' import { defaultPorts, inOnly } from './workflow-builder-ports.ts' const DEFAULT_LLM_MODEL = 'gpt-5' const DEFAULT_LLM_TEMPERATURE = 0 const DEFAULT_AGENT_MODEL = 'claude-opus-4-5' const DEFAULT_AGENT_MAX_ROUNDS = 20 const DEFAULT_CODE_LANGUAGE = 'javascript' const DEFAULT_CODE_LIBRARIES = ['@meistrari/vault-sdk@1.9.0', '@meistrari/tela-sdk-js@2.6.1', '@meistrari/document-sdk@1.4.0'] function createNode(params: { name: string actionId: string input: Record | null ports?: WorkflowPort[] }): WorkflowNode { return { id: randomUUID(), name: params.name, actionId: params.actionId, input: params.input, ports: params.ports ?? defaultPorts(), } } export function llmNode(params: { name: string prompt: string model?: string temperature?: number schema?: Record | null }): WorkflowNode { if (params.schema && params.schema.type === 'object' && params.schema.properties) { throw new Error( `llmNode '${params.name}': schema must be a properties-only map, not a full JSON Schema. ` + 'Pass the properties directly (e.g. { field: { type: \'string\' } }) — the runtime wraps it automatically.', ) } return createNode({ name: params.name, actionId: 'llm-completion', input: { prompt: params.prompt, model: params.model ?? DEFAULT_LLM_MODEL, temperature: params.temperature ?? DEFAULT_LLM_TEMPERATURE, schema: params.schema ?? null, }, }) } export function agentNode(params: { name: string prompt: string model?: string maxRounds?: number schema?: Record | null telaTools?: Array<{ name: string description: string canvasId: string variables?: Array<{ name: string, description: string, type: 'text' | 'file' }> }> | null tools?: { parseFileTool?: boolean docTool?: boolean telaSdkTool?: boolean subAgentsTool?: boolean } }): WorkflowNode { return createNode({ name: params.name, actionId: 'agent', input: { prompt: params.prompt, model: params.model ?? DEFAULT_AGENT_MODEL, maxRounds: params.maxRounds ?? DEFAULT_AGENT_MAX_ROUNDS, schema: params.schema ?? null, telaTools: params.telaTools ?? null, ...(params.tools && { tools: params.tools }), }, }) } export function codeExecutionNode(params: { name: string code?: string codeFile?: string language?: string libraries?: string[] }): WorkflowNode { let code: string if (params.codeFile) { const filePath = resolve(process.cwd(), params.codeFile) try { code = readFileSync(filePath, 'utf-8') } catch (err) { throw new Error( `codeExecutionNode '${params.name}': failed to read codeFile '${params.codeFile}' (resolved to '${filePath}'): ${String(err)}`, ) } } else if (params.code) { code = params.code } else { throw new Error( `codeExecutionNode '${params.name}': either 'code' or 'codeFile' must be provided.`, ) } if (!/function\s+execute\s*\(/.test(code)) { throw new Error( `codeExecutionNode '${params.name}': code must declare a \`function execute(input) { ... }\` function. ` + 'The runtime requires this exact signature.', ) } return createNode({ name: params.name, actionId: 'code-execution', input: { code, language: params.language ?? DEFAULT_CODE_LANGUAGE, libraries: params.libraries ?? DEFAULT_CODE_LIBRARIES, }, }) } export function conditionNode(params: { name: string cases: ConditionCase[] includeDefault?: boolean }): WorkflowNode { const ports: WorkflowPort[] = [ { id: randomUUID(), dir: 'in', type: 'default' }, ] const cases = params.cases.map((workflowCase) => { const portId = randomUUID() ports.push({ id: portId, dir: 'out', type: 'branch' }) return { name: workflowCase.name, condition: workflowCase.condition, steps: [], type: workflowCase.type ?? 'selector', portId, } }) let defaultCase: { name: 'Default', condition: null, steps: unknown[], portId: string } | undefined if (params.includeDefault !== false) { const portId = randomUUID() ports.push({ id: portId, dir: 'out', type: 'branch' }) defaultCase = { name: 'Default', condition: null, steps: [], portId } } return createNode({ name: params.name, actionId: 'condition', input: { cases, ...(defaultCase && { default: defaultCase }), }, ports, }) } export function mapNode(params: { name: string over: string mapVariable: string }): WorkflowNode { return createNode({ name: params.name, actionId: 'map', input: { over: params.over, mapVariable: params.mapVariable, }, ports: [ { id: randomUUID(), dir: 'in', type: 'default' }, { id: randomUUID(), dir: 'out', type: 'subgraph' }, { id: randomUUID(), dir: 'out', type: 'default' }, ], }) } export function stopNode(params: { name: string }): WorkflowNode { return createNode({ name: params.name, actionId: 'stop', input: null, ports: inOnly(), }) } export function canvasNode(params: { name: string canvasId: string variables?: Record tags?: string[] }): WorkflowNode { return createNode({ name: params.name, actionId: 'canvas', input: { canvasId: params.canvasId, variables: params.variables ?? {}, tags: params.tags ?? [], }, }) } export function templateNode(params: { name: string templateId: string variables?: Record tags?: string[] }): WorkflowNode { return createNode({ name: params.name, actionId: 'template', input: { templateId: params.templateId, variables: params.variables ?? {}, tags: params.tags ?? [], }, }) } export function documentSplitterNode(params: { name: string file: string chunkSize?: number type?: 'pdf' | 'csv' }): WorkflowNode { return createNode({ name: params.name, actionId: 'document-splitter', input: { file: params.file, chunkSize: params.chunkSize ?? 1, type: params.type ?? 'pdf', }, }) } export function documentCropperNode(params: { name: string file: string indexes: (number | string)[] type?: 'pdf' }): WorkflowNode { for (const idx of params.indexes) { if (typeof idx === 'number' && idx < 1) { throw new Error( `documentCropperNode '${params.name}': indexes are 1-based. Got invalid index ${idx}. Use 1 for the first page.`, ) } } return createNode({ name: params.name, actionId: 'document-cropper', input: { file: params.file, indexes: params.indexes, type: params.type ?? 'pdf', }, }) } export function documentTemplaterNode(params: { name: string vaultUrl: string placeholders?: Array<{ key: string, reference: string }> outputFilename?: string mustache?: boolean createPermalink?: boolean templateFilename?: string templateFileSize?: number }): WorkflowNode { if (!params.vaultUrl) { throw new Error(`documentTemplaterNode '${params.name}': vaultUrl is required.`) } if (params.templateFilename && !params.templateFilename.toLowerCase().endsWith('.docx')) { throw new Error(`documentTemplaterNode '${params.name}': templateFilename must end with .docx.`) } return createNode({ name: params.name, actionId: 'document-templater', input: { vaultUrl: params.vaultUrl, placeholders: params.placeholders ?? [], outputFilename: params.outputFilename ?? params.templateFilename ?? 'output.docx', mustache: params.mustache ?? false, createPermalink: params.createPermalink ?? false, ...(params.templateFilename && { templateFilename: params.templateFilename }), ...(params.templateFileSize !== undefined && { templateFileSize: params.templateFileSize }), }, }) }