feat(rah-light): replace workflows with guides system

- Remove entire workflow system (API routes, services, tools, components)
- Add guides system from main ra-h repo (for external agent integration)
- Update pane types: 'workflows' → 'guides' throughout
- Update LeftToolbar to show guides icon instead of workflows
- Update SettingsModal with GuidesViewer tab
- Add GuidesPane for browsing guides in main UI
- Clean up prompts to remove workflow references
- Update tool registry to remove workflow tools
- Add gray-matter package for frontmatter parsing

Guides are essential for RA-H Light as they help external agents
(via MCP) understand the knowledge base context and usage patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-29 16:29:24 +11:00
co-authored by Claude Opus 4.5
parent f383d770ca
commit b31441b35f
46 changed files with 1126 additions and 2189 deletions
+25
View File
@@ -0,0 +1,25 @@
import { tool } from 'ai';
import { z } from 'zod';
import { listGuides } from '@/services/guides/guideService';
export const listGuidesTool = tool({
description: 'List all available guides with their names and descriptions.',
inputSchema: z.object({}),
execute: async () => {
try {
const guides = listGuides();
return {
success: true,
data: guides,
message: `Found ${guides.length} guides`,
};
} catch (error) {
console.error('[listGuides] error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to list guides',
data: [],
};
}
},
});
+34
View File
@@ -0,0 +1,34 @@
import { tool } from 'ai';
import { z } from 'zod';
import { readGuide } from '@/services/guides/guideService';
export const readGuideTool = tool({
description: 'Read a guide by name. Returns the full markdown content with instructions.',
inputSchema: z.object({
name: z.string().describe('The name of the guide to read'),
}),
execute: async ({ name }) => {
try {
const guide = readGuide(name);
if (!guide) {
return {
success: false,
error: `Guide "${name}" not found`,
data: null,
};
}
return {
success: true,
data: guide,
message: `Loaded guide: ${guide.name}`,
};
} catch (error) {
console.error('[readGuide] error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read guide',
data: null,
};
}
},
});
+30
View File
@@ -0,0 +1,30 @@
import { tool } from 'ai';
import { z } from 'zod';
import { writeGuide } from '@/services/guides/guideService';
import { eventBroadcaster } from '@/services/events';
export const writeGuideTool = tool({
description: 'Write or update a guide. Content should be full markdown with YAML frontmatter (name, description).',
inputSchema: z.object({
name: z.string().describe('The name of the guide to write'),
content: z.string().describe('Full markdown content including YAML frontmatter'),
}),
execute: async ({ name, content }) => {
try {
writeGuide(name, content);
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
return {
success: true,
data: { name },
message: `Guide "${name}" saved`,
};
} catch (error) {
console.error('[writeGuide] error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to write guide',
data: null,
};
}
},
});
+2 -6
View File
@@ -18,7 +18,7 @@ export const TOOL_GROUPS: Record<string, ToolGroup> = {
orchestration: {
id: 'orchestration',
name: 'Orchestration',
description: 'Workflows, web search, and reasoning (orchestrator only)',
description: 'Web search and reasoning tools',
icon: '●',
color: '#8b5cf6'
},
@@ -42,13 +42,9 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
queryDimensionNodes: 'core',
searchContentEmbeddings: 'core',
// Orchestration: Workflows and reasoning (orchestrator only)
// Orchestration: Web search and reasoning
webSearch: 'orchestration',
think: 'orchestration',
executeWorkflow: 'orchestration',
listWorkflows: 'orchestration',
getWorkflow: 'orchestration',
editWorkflow: 'orchestration',
// Execution: Write operations and extraction (workers only)
createNode: 'execution',
+1 -15
View File
@@ -16,10 +16,6 @@ import { queryDimensionNodesTool } from '../database/queryDimensionNodes';
import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings';
import { webSearchTool } from '../other/webSearch';
import { thinkTool } from '../other/think';
import { executeWorkflowTool } from '../orchestration/executeWorkflow';
import { listWorkflowsTool } from '../orchestration/listWorkflows';
import { getWorkflowTool } from '../orchestration/getWorkflow';
import { editWorkflowTool } from '../orchestration/editWorkflow';
import { youtubeExtractTool } from '../other/youtubeExtract';
import { websiteExtractTool } from '../other/websiteExtract';
import { paperExtractTool } from '../other/paperExtract';
@@ -41,10 +37,6 @@ const CORE_TOOLS: Record<string, any> = {
const ORCHESTRATION_TOOLS: Record<string, any> = {
webSearch: webSearchTool,
think: thinkTool,
executeWorkflow: executeWorkflowTool,
listWorkflows: listWorkflowsTool,
getWorkflow: getWorkflowTool,
editWorkflow: editWorkflowTool,
};
// Execution tools for worker agents (includes write operations)
@@ -77,10 +69,6 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
...Object.keys(CORE_TOOLS),
'webSearch',
'think',
'executeWorkflow',
'listWorkflows',
'getWorkflow',
'editWorkflow',
'createNode',
'updateNode',
'createEdge',
@@ -95,9 +83,7 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
const EXECUTOR_TOOL_NAMES = [
...Object.keys(CORE_TOOLS),
...Object.keys(ORCHESTRATION_TOOLS).filter(name =>
name !== 'executeWorkflow'
),
...Object.keys(ORCHESTRATION_TOOLS),
...Object.keys(EXECUTION_TOOLS),
];
-62
View File
@@ -1,62 +0,0 @@
import { tool } from 'ai';
import { z } from 'zod';
import { WorkflowRegistry } from '@/services/workflows/registry';
import { saveWorkflow, loadUserWorkflow } from '@/services/workflows/workflowFileService';
export const editWorkflowTool = tool({
description: 'Update a workflow definition. Use to refine instructions, update description, or toggle enabled state.',
inputSchema: z.object({
workflowKey: z.string().describe('The key of the workflow to edit'),
updates: z.object({
displayName: z.string().optional().describe('New display name'),
description: z.string().optional().describe('New description'),
instructions: z.string().optional().describe('New instructions (full replacement)'),
enabled: z.boolean().optional().describe('Enable or disable the workflow'),
requiresFocusedNode: z.boolean().optional().describe('Whether workflow requires a focused node'),
}).describe('Fields to update'),
}),
execute: async ({ workflowKey, updates }) => {
try {
// Get existing workflow (from user file or bundled default)
const existing = await WorkflowRegistry.getWorkflowByKey(workflowKey);
if (!existing) {
return {
success: false,
error: `Workflow '${workflowKey}' not found`,
};
}
// Merge updates with existing values
const updated = {
key: workflowKey,
displayName: updates.displayName ?? existing.displayName,
description: updates.description ?? existing.description,
instructions: updates.instructions ?? existing.instructions,
enabled: updates.enabled ?? existing.enabled,
requiresFocusedNode: updates.requiresFocusedNode ?? existing.requiresFocusedNode,
};
// Save to user workflow file (this will override bundled if same key)
saveWorkflow(updated);
return {
success: true,
message: `Workflow '${workflowKey}' updated`,
workflow: {
key: updated.key,
displayName: updated.displayName,
description: updated.description,
enabled: updated.enabled,
requiresFocusedNode: updated.requiresFocusedNode,
},
};
} catch (error) {
console.error('editWorkflow error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to edit workflow',
};
}
},
});
-127
View File
@@ -1,127 +0,0 @@
import { tool } from 'ai';
import { z } from 'zod';
import { WorkflowRegistry } from '@/services/workflows/registry';
import { getSQLiteClient } from '@/services/database/sqlite-client';
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
import { RequestContext } from '@/services/context/requestContext';
import { getAutoContextSummaries } from '@/services/context/autoContext';
export const executeWorkflowTool = tool({
description: 'Execute predefined workflow via wise ra-h',
inputSchema: z.object({
workflowKey: z.string().describe('Key of workflow to execute (e.g., "integrate")'),
nodeId: z.number().describe('ID of node to run workflow on (usually focused node)'),
userContext: z.string().optional().describe('Optional: Additional context or instructions from user'),
}),
execute: async ({ workflowKey, nodeId, userContext }) => {
// 1. Fetch workflow definition
const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey);
if (!workflow) {
return { success: false, error: `Workflow '${workflowKey}' not found` };
}
if (!workflow.enabled) {
return { success: false, error: `Workflow '${workflowKey}' is disabled` };
}
// 2. Validate node requirement
if (workflow.requiresFocusedNode && !nodeId) {
return { success: false, error: `Workflow '${workflowKey}' requires a focused node` };
}
// 2.5. Prevent re-running same workflow on same node within 1 hour
if (nodeId) {
const db = getSQLiteClient();
const recentRuns = db.query<{ id: number }>(
`SELECT id FROM chats
WHERE json_extract(metadata, '$.workflow_key') = ?
AND json_extract(metadata, '$.workflow_node_id') = ?
AND datetime(created_at) > datetime('now', '-1 hour')
LIMIT 1`,
[workflowKey, nodeId]
).rows;
if (recentRuns.length > 0) {
return {
success: false,
error: `Workflow '${workflowKey}' already ran on node ${nodeId} within the last hour. Check the node content - the Integration Analysis section should already be there.`
};
}
}
// 3. Validate node exists and fetch full context (if node provided)
let contextLines: string[] = [];
if (nodeId) {
const db = getSQLiteClient();
const stmt = db.prepare('SELECT * FROM nodes WHERE id = ?');
const node = stmt.get(nodeId) as any;
if (!node) {
return { success: false, error: `Node ${nodeId} not found` };
}
contextLines = [
`Focused Node: [NODE:${node.id}:"${node.title}"]`,
node.description ? `Description: ${node.description}` : null,
node.content ? `Content: ${node.content}` : null,
node.link ? `Link: ${node.link}` : null,
].filter(Boolean) as string[];
}
// Removed conflicting guardrail - wise-rah has updateNode access and should use it
if (userContext) {
contextLines.push(`User Context: ${userContext}`);
}
// 4. Build task with workflow instructions
RequestContext.set({ workflowKey, workflowNodeId: nodeId });
const autoContextSummaries = getAutoContextSummaries(6);
if (autoContextSummaries.length > 0) {
contextLines.push('Background Context (Top Hubs):');
autoContextSummaries.forEach((summary) => {
contextLines.push(
`[NODE:${summary.id}:"${summary.title}"] (${summary.edgeCount} edges)`
);
});
}
const task = `Execute workflow: ${workflow.displayName}
${workflow.instructions}
${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general workflow)'}`;
// 5. Execute workflow directly
const requestContext = RequestContext.get();
console.log('[executeWorkflowTool] Current traceId:', requestContext.traceId);
const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
try {
const result = await WorkflowExecutor.execute({
sessionId,
task,
context: contextLines,
expectedOutcome: workflow.expectedOutcome,
traceId: requestContext.traceId,
parentChatId: requestContext.parentChatId,
workflowKey,
workflowNodeId: nodeId,
});
RequestContext.set({ workflowKey: undefined, workflowNodeId: undefined });
const workflowLabel = workflow.displayName || workflowKey;
return {
success: true,
message: `Workflow **${workflowLabel}** completed.`,
summary: result.summary,
};
} catch (error) {
console.error('[executeWorkflowTool] Workflow execution failed', error);
RequestContext.set({ workflowKey: undefined, workflowNodeId: undefined });
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return { success: false, error: `Workflow execution failed: ${errorMessage}` };
}
},
});
-43
View File
@@ -1,43 +0,0 @@
import { tool } from 'ai';
import { z } from 'zod';
import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry';
import { userWorkflowExists } from '@/services/workflows/workflowFileService';
export const getWorkflowTool = tool({
description: 'Retrieve a workflow definition including its full instructions',
inputSchema: z.object({
workflowKey: z.string().describe('The key of the workflow to retrieve (e.g., "integrate", "prep")'),
}),
execute: async ({ workflowKey }) => {
try {
const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey);
if (!workflow) {
return {
success: false,
error: `Workflow '${workflowKey}' not found`,
};
}
return {
success: true,
workflow: {
key: workflow.key,
displayName: workflow.displayName,
description: workflow.description,
instructions: workflow.instructions,
enabled: workflow.enabled,
requiresFocusedNode: workflow.requiresFocusedNode,
isBundled: BUNDLED_WORKFLOW_KEYS.has(workflow.key),
hasUserOverride: userWorkflowExists(workflow.key),
},
};
} catch (error) {
console.error('getWorkflow error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get workflow',
};
}
},
});
-31
View File
@@ -1,31 +0,0 @@
import { tool } from 'ai';
import { z } from 'zod';
import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry';
export const listWorkflowsTool = tool({
description: 'List all available workflows',
inputSchema: z.object({}),
execute: async () => {
try {
const workflows = await WorkflowRegistry.getAllWorkflows();
return {
success: true,
workflows: workflows.map(w => ({
key: w.key,
displayName: w.displayName,
description: w.description,
enabled: w.enabled,
requiresFocusedNode: w.requiresFocusedNode,
isBundled: BUNDLED_WORKFLOW_KEYS.has(w.key),
})),
};
} catch (error) {
console.error('listWorkflows error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to list workflows',
};
}
},
});