feat(rah-light): clean up unused agent services

- Delete src/services/ai.ts (not imported anywhere)
- Delete src/services/agents/registry.ts (only used by contextBuilder)
- Delete src/services/helpers/ directory (contextBuilder.ts, logger.ts - unused)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-29 15:33:37 +11:00
co-authored by Claude Opus 4.5
parent cc5397e9ba
commit 8a3b6df609
4 changed files with 0 additions and 776 deletions
-103
View File
@@ -1,103 +0,0 @@
import { getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
import { RAH_MAIN_SYSTEM_PROMPT } from '@/config/prompts/rah-main';
import { RAH_EASY_SYSTEM_PROMPT } from '@/config/prompts/rah-easy';
import { WORKFLOW_EXECUTOR_SYSTEM_PROMPT } from '@/config/prompts/workflow-executor';
import type { AgentDefinition } from './types';
/**
* Code-first agent registry (opinionated, not database-driven)
* Agents are defined in code and cannot be modified by users
*/
export class AgentRegistry {
// Deterministic agent definitions baked into code
private static readonly AGENTS: Record<string, AgentDefinition> = {
'ra-h': {
id: 1,
key: 'ra-h',
displayName: 'ra-h (hard)',
description: 'Opinionated orchestrator agent',
model: 'anthropic/claude-sonnet-4.5',
role: 'orchestrator',
systemPrompt: RAH_MAIN_SYSTEM_PROMPT,
availableTools: getDefaultToolNamesForRole('orchestrator'),
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
memory: null,
prompts: undefined
},
'ra-h-easy': {
id: 4,
key: 'ra-h-easy',
displayName: 'ra-h (easy)',
description: 'Fast, low-latency orchestrator',
model: 'openai/gpt-5-mini',
role: 'orchestrator',
systemPrompt: RAH_EASY_SYSTEM_PROMPT,
availableTools: getDefaultToolNamesForRole('orchestrator'),
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
memory: null,
prompts: undefined
},
'workflow': {
id: 3,
key: 'workflow',
displayName: 'workflow agent',
description: 'Workflow executor (uses same model as easy mode)',
model: 'openai/gpt-5-mini',
role: 'planner',
systemPrompt: WORKFLOW_EXECUTOR_SYSTEM_PROMPT,
availableTools: getDefaultToolNamesForRole('planner'),
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
memory: null,
prompts: undefined
},
// Alias for backwards compatibility
'wise-rah': {
id: 3,
key: 'workflow',
displayName: 'workflow agent',
description: 'Workflow executor (uses same model as easy mode)',
model: 'openai/gpt-5-mini',
role: 'planner',
systemPrompt: WORKFLOW_EXECUTOR_SYSTEM_PROMPT,
availableTools: getDefaultToolNamesForRole('planner'),
enabled: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
memory: null,
prompts: undefined
}
};
static async getAgentByKey(key: string): Promise<AgentDefinition | null> {
return this.AGENTS[key] || null;
}
static async getAgentById(id: number): Promise<AgentDefinition | null> {
return Object.values(this.AGENTS).find(a => a.id === id) || null;
}
static async getEnabledAgents(): Promise<AgentDefinition[]> {
return Object.values(this.AGENTS).filter(a => a.enabled);
}
static async orchestrator(): Promise<AgentDefinition> {
return this.AGENTS['ra-h'];
}
static async orchestratorForMode(mode: 'easy' | 'hard' = 'easy'): Promise<AgentDefinition> {
if (mode === 'hard') {
return this.AGENTS['ra-h'];
}
return this.AGENTS['ra-h-easy'];
}
static async planner(): Promise<AgentDefinition> {
return this.AGENTS['workflow'];
}
}
-246
View File
@@ -1,246 +0,0 @@
import OpenAI from 'openai';
import type { AgentDefinition } from './agents/types';
import { Node } from '@/types/database';
import { getToolSchemas, executeTool } from '../tools/infrastructure/registry';
import { getSQLiteClient } from './database/sqlite-client';
import { apiKeyService } from './storage/apiKeys';
// Initialize OpenAI client with dynamic API key support
function getOpenAiClient(): OpenAI {
const apiKey = apiKeyService.getOpenAiKey();
if (!apiKey) {
throw new Error('OpenAI API key required. Please:\n1. Click the Settings icon (⚙️) in the bottom left\n2. Go to API Keys tab\n3. Add your OpenAI API key\n\nGet your key at: https://platform.openai.com/api-keys');
}
return new OpenAI({ apiKey });
}
export interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface ToolCall {
id: string;
name: string;
params: any;
result: any;
}
export interface ChatResponse {
response: string;
toolCalls?: ToolCall[];
}
export class AIService {
/**
* Chat with a helper using GPT-4o-mini with function calling
*/
static async chatWithHelper(
helper: AgentDefinition,
message: string,
selectedNodeIds: number[] = [],
messageHistory: ChatMessage[] = []
): Promise<ChatResponse> {
try {
// Get selected nodes details
const selectedNodes = await this.getSelectedNodes(selectedNodeIds);
// Build context for tools
const toolContext = {
selectedNodes,
database: getSQLiteClient()
};
// Build messages array
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{
role: 'system',
content: this.buildSystemPrompt(helper, selectedNodes)
},
// Add message history
...messageHistory.map(msg => ({
role: msg.role as 'user' | 'assistant',
content: msg.content
})),
{
role: 'user',
content: message
}
];
// Get tool schemas for this helper
const toolSchemas = getToolSchemas(helper.availableTools);
// Make OpenAI API call
const openai = getOpenAiClient();
const completion = await openai.chat.completions.create({
model: helper.model,
messages,
tools: toolSchemas.length > 0 ? toolSchemas : undefined,
tool_choice: toolSchemas.length > 0 ? 'auto' : undefined,
temperature: 0.7,
max_tokens: 2000,
});
const assistantMessage = completion.choices[0]?.message;
if (!assistantMessage) {
throw new Error('No response from OpenAI');
}
let response = assistantMessage.content || '';
const toolCalls: ToolCall[] = [];
// Handle tool calls if present
if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
console.log(`Agent ${helper.key} is calling ${assistantMessage.tool_calls.length} tools`);
for (const toolCall of assistantMessage.tool_calls) {
try {
const toolName = toolCall.function.name;
const toolParams = JSON.parse(toolCall.function.arguments);
console.log(`Executing tool: ${toolName}`, toolParams);
// Execute the tool
const result = await executeTool(toolName, toolParams, toolContext);
toolCalls.push({
id: toolCall.id,
name: toolName,
params: toolParams,
result
});
console.log(`Tool ${toolName} completed:`, result.success ? 'success' : 'failed');
} catch (error) {
console.error(`Error executing tool ${toolCall.function.name}:`, error);
toolCalls.push({
id: toolCall.id,
name: toolCall.function.name,
params: {},
result: {
success: false,
error: error instanceof Error ? error.message : 'Tool execution failed'
}
});
}
}
// If we have tool results, make another call to get the final response
if (toolCalls.length > 0) {
const toolMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
...messages,
{
role: 'assistant',
content: assistantMessage.content,
tool_calls: assistantMessage.tool_calls
},
...toolCalls.map(toolCall => ({
role: 'tool' as const,
tool_call_id: toolCall.id,
content: JSON.stringify(toolCall.result)
}))
];
const finalCompletion = await getOpenAiClient().chat.completions.create({
model: 'gpt-5-mini',
messages: toolMessages,
temperature: 0.7,
max_tokens: 2000,
});
response = finalCompletion.choices[0]?.message?.content || response;
}
}
return {
response,
toolCalls: toolCalls.length > 0 ? toolCalls : undefined
};
} catch (error) {
console.error('Error in chatWithHelper:', error);
throw new Error(
error instanceof Error ? error.message : 'Failed to process chat message'
);
}
}
/**
* Get selected nodes with their details
*/
private static async getSelectedNodes(nodeIds: number[]): Promise<Node[]> {
if (nodeIds.length === 0) return [];
try {
const sqlite = getSQLiteClient();
const placeholders = nodeIds.map(() => '?').join(', ');
const result = sqlite.query<any>(
`SELECT n.id, n.title, n.content, n.link, n.metadata, n.chunk,
n.chunk_status, n.embedding_updated_at, n.embedding_text,
n.created_at, n.updated_at,
COALESCE((SELECT JSON_GROUP_ARRAY(d.dimension)
FROM node_dimensions d WHERE d.node_id = n.id), '[]') as dimensions_json
FROM nodes n
WHERE n.id IN (${placeholders})
ORDER BY n.created_at DESC`,
nodeIds
);
return result.rows.map((row: any) => ({
...row,
dimensions: JSON.parse(row.dimensions_json || '[]')
}));
} catch (error) {
console.error('Error fetching selected nodes:', error);
return [];
}
}
/**
* Build system prompt with context
*/
private static buildSystemPrompt(helper: AgentDefinition, selectedNodes: Node[]): string {
let systemPrompt = helper.systemPrompt;
// Universal rule: ensure clickable node labels in UI
systemPrompt += '\n\nNode references: Always format nodes as [NODE:id:"title"] so the UI renders clickable labels.';
// Add context about selected nodes
if (selectedNodes.length > 0) {
systemPrompt += '\n\n## Selected Nodes Context\n';
systemPrompt += `You have ${selectedNodes.length} node(s) selected:\n\n`;
selectedNodes.forEach((node, index) => {
systemPrompt += `${index + 1}. **${node.title || 'Untitled'}**\n`;
if (node.dimensions?.length > 0) {
systemPrompt += ` - Dimensions: ${node.dimensions.join(', ')}\n`;
}
if (node.link) {
systemPrompt += ` - URL: ${node.link}\n`;
}
systemPrompt += '\n';
});
} else {
systemPrompt += '\n\n## Context\nNo nodes are currently selected. You can still help with general queries and web searches.';
}
// Add available tools information
if (helper.availableTools.length > 0) {
systemPrompt += '\n\n## Available Tools\n';
systemPrompt += `You have access to the following tools: ${helper.availableTools.join(', ')}\n`;
systemPrompt += 'Use these tools when they would be helpful to answer the user\'s question.';
}
// Subtle, hard-coded intent capture across all helpers (non-intrusive)
systemPrompt += '\n\n## Subtle Intent Capture\n';
systemPrompt += [
'When it would clearly help you serve better, briefly ask for the user\'s intent (their "why").',
'Keep it lightweight and optional (one short sentence, once per topic).',
'Examples: "What are you hoping to do with this?", "What\'s the goal behind adding this?"',
'If the user answers, incorporate the rationale into your next steps and outputs.'
].join(' ');
return systemPrompt;
}
}
-301
View File
@@ -1,301 +0,0 @@
import { Node } from '@/types/database';
import { AgentRegistry } from '@/services/agents/registry';
import { WorkflowRegistry } from '@/services/workflows/registry';
import { getHelperTools, getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
import type { CacheableBlock, SystemPromptResult } from '@/types/prompts';
// buildAutoContextBlock removed - agent queries top nodes on-demand via sqliteQuery
export interface NodeContext {
nodes: Node[];
activeNodeId: number | null;
activeDimension?: string | null;
}
export interface DimensionContext {
name: string;
description: string | null;
isPriority: boolean;
nodeCount: number;
}
export interface ContextBuilderOptions {
maxPrimaryContent?: number; // Default: 500 chars
maxSecondaryContent?: number; // Default: 200 chars
includeMetadata?: boolean; // Include created/updated timestamps
}
const BASE_CONTEXT = `=== RA-H BASE CONTEXT ===
You have access to the RA-H knowledge graph via two approaches:
**sqliteQuery tool** — Use for flexible read operations:
- Ad-hoc queries, exploration, complex JOINs, aggregations
- Any query pattern not covered by structured tools
- Example: SELECT n.title, COUNT(e.id) FROM nodes n LEFT JOIN edges e ON n.id = e.from_node_id GROUP BY n.id
**Structured tools** — Use for these specific cases:
- Writes: createNode, updateNode, createEdge (need validation, embeddings, hooks)
- Semantic search: searchContentEmbeddings (needs vector DB)
- External APIs: webSearch, youtubeExtract, websiteExtract, paperExtract
**Schema Quick Reference:**
- nodes: id, title, content, chunk, dimensions (JSON array), url, created_at
- edges: id, from_node_id, to_node_id, context (JSON), source, explanation
- dimensions: id, name, description, is_locked
**Other Context:**
- Node references: use [NODE:id:"title"] format for clickable UI labels
- Focused nodes show truncated content; query for full detail
- "this conversation/paper/video" refers to the primary focused node
`;
function buildStaticBaseContext(): string {
return BASE_CONTEXT;
}
function buildAgentInstructionsBlock(helperKey: string, systemPrompt: string): string {
return `=== AGENT INSTRUCTIONS (${helperKey}) ===\n${systemPrompt}\n`;
}
function isPrimaryOrchestrator(helperKey: string): boolean {
return helperKey === 'ra-h' || helperKey === 'ra-h-easy';
}
function buildToolDefinitionsBlock(toolNames: string[]): string {
if (!Array.isArray(toolNames) || toolNames.length === 0) {
return '';
}
const tools = getHelperTools(toolNames);
const lines: string[] = ['=== AVAILABLE TOOLS ==='];
Object.entries(tools).forEach(([name, tool]) => {
if (tool?.description) {
lines.push(`\n## ${name}\n${tool.description.trim()}`);
}
});
return lines.join('\n');
}
async function buildWorkflowDefinitionsBlock(helperKey: string): Promise<string | null> {
// Only include for primary orchestrator variants
if (!isPrimaryOrchestrator(helperKey)) {
return null;
}
try {
const workflows = await WorkflowRegistry.getEnabledWorkflows();
if (workflows.length === 0) return null;
const lines: string[] = ['=== AVAILABLE WORKFLOWS ==='];
for (const workflow of workflows) {
lines.push(`\n## ${workflow.displayName} (${workflow.key})`);
lines.push(workflow.description);
}
return lines.join('\n');
} catch (error) {
console.warn('Workflow definitions load failed (contextBuilder):', error);
return null;
}
}
function truncateToWords(text: string, maxWords: number): string {
const words = text.trim().split(/\s+/);
if (words.length <= maxWords) return text;
return words.slice(0, maxWords).join(' ') + '…';
}
function parseMetadata(metadata: unknown): Record<string, any> {
if (!metadata) return {};
if (typeof metadata === 'string') {
try {
const parsed = JSON.parse(metadata);
if (parsed && typeof parsed === 'object') {
return parsed as Record<string, any>;
}
return {};
} catch (error) {
console.warn('Failed to parse node metadata JSON:', error);
return {};
}
}
if (typeof metadata === 'object') {
if (metadata === null) return {};
return metadata as Record<string, any>;
}
return {};
}
function describeChunkStatus(node: Node): string {
const status = node.chunk_status || 'unknown';
const chunkLength = typeof node.chunk === 'string' ? node.chunk.length : 0;
const approxChars = chunkLength > 0 ? ` (~${Math.max(1, Math.round(chunkLength / 1000))}k chars)` : '';
const metadata = parseMetadata(node.metadata);
const transcriptLength = typeof metadata.transcript_length === 'number'
? metadata.transcript_length
: undefined;
let transcriptLabel = '';
if (transcriptLength && transcriptLength > 0) {
transcriptLabel = `, transcript ≈${Math.max(1, Math.round(transcriptLength / 1000))}k chars`;
}
const embeddingsAvailable = status === 'chunked' || chunkLength > 0;
return `Chunks: ${status}${approxChars}${transcriptLabel}; Embeddings: ${embeddingsAvailable ? 'available' : 'missing'}`;
}
/**
* Builds the dynamic focused nodes context section
*/
export function buildFocusedNodesBlock(
context: NodeContext,
options: ContextBuilderOptions = {}
): string {
if (!context.nodes || context.nodes.length === 0) {
return '\n=== CURRENT FOCUS ===\nNo nodes currently in focus.';
}
let contextString = '=== CURRENT FOCUS ===';
contextString += '\n25-word previews; use queryNodes/searchContentEmbeddings for full detail\n';
const validNodes = context.nodes.filter(n => n != null);
const activeNode = validNodes.find(n => n.id === context.activeNodeId);
const otherNodes = validNodes.filter(n => n.id !== context.activeNodeId);
if (activeNode) {
contextString += `\n[PRIMARY - Tab 1]\nID: ${activeNode.id} | "${activeNode.title || 'Untitled'}"\n${truncateToWords(activeNode.content || 'No content', 25)}\nLink: ${activeNode.link || 'No link'}`;
contextString += `\n${describeChunkStatus(activeNode)}`;
}
if (otherNodes.length > 0) {
otherNodes.forEach((node, index) => {
contextString += `\n\n[Tab ${index + 2}]\nID: ${node.id} | "${node.title || 'Untitled'}"\n${truncateToWords(node.content || 'No content', 25)}\nLink: ${node.link || 'No link'}\n${describeChunkStatus(node)}`;
});
}
contextString += '\n===================================';
return contextString;
}
/**
* Fetches dimension context from API or database
*/
async function fetchDimensionContext(dimensionName: string): Promise<DimensionContext | null> {
try {
// In server context, we can call the API internally or query directly
const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/dimensions/${encodeURIComponent(dimensionName)}/context`);
if (!response.ok) return null;
const data = await response.json();
return data.success ? data.data : null;
} catch (error) {
console.warn('Failed to fetch dimension context:', error);
return null;
}
}
/**
* Builds the dimension context block for the system prompt
*/
function buildDimensionContextBlock(dimension: DimensionContext | null): string {
if (!dimension) return '';
return `
=== ACTIVE DIMENSION ===
Dimension: "${dimension.name}"
Description: ${dimension.description || 'No description'}
Node Count: ${dimension.nodeCount} nodes
Priority: ${dimension.isPriority ? 'Yes (locked)' : 'No'}
Note: Use queryDimensionNodes tool to retrieve actual nodes in this dimension.
===================================
`;
}
/**
* Builds system prompt as cacheable blocks (Anthropic prompt caching)
*/
export async function buildSystemPromptBlocks(
nodeContext: NodeContext,
helperComponentKey: string,
options?: ContextBuilderOptions
): Promise<SystemPromptResult> {
const helper = await AgentRegistry.getAgentByKey(helperComponentKey);
const isAnthropic = helper?.model?.startsWith('anthropic/');
const cacheControl = isAnthropic ? { type: 'ephemeral' as const } : undefined;
const blocks: CacheableBlock[] = [];
const baseContext = buildStaticBaseContext().trim();
const helperPrompt = helper?.systemPrompt || 'No instructions provided.';
const instructionsBlock = buildAgentInstructionsBlock(helperComponentKey, helperPrompt).trim();
const combinedInstructions = [baseContext, instructionsBlock]
.filter(section => section.length > 0)
.join('\n\n');
if (combinedInstructions.length > 0) {
blocks.push({
type: 'text',
text: combinedInstructions,
...(cacheControl ? { cache_control: cacheControl } : {})
});
}
const availableToolNames = helper?.availableTools?.length
? helper.availableTools
: getDefaultToolNamesForRole(helper?.role === 'executor' ? 'executor' : 'orchestrator');
const toolBlock = buildToolDefinitionsBlock(availableToolNames);
if (toolBlock.trim().length > 0) {
blocks.push({
type: 'text',
text: toolBlock,
...(cacheControl ? { cache_control: cacheControl } : {})
});
}
const workflowBlock = await buildWorkflowDefinitionsBlock(helperComponentKey);
if (workflowBlock && workflowBlock.trim().length > 0) {
blocks.push({
type: 'text',
text: workflowBlock,
...(cacheControl ? { cache_control: cacheControl } : {})
});
}
// REMOVED: autoContextBlock (top 10 nodes)
// Agent can now query this on-demand via sqliteQuery:
// SELECT id, title, COUNT(e.id) as edges FROM nodes n LEFT JOIN edges e ON n.id = e.from_node_id OR n.id = e.to_node_id GROUP BY n.id ORDER BY edges DESC LIMIT 10
// Add dimension context if an active dimension is provided
if (nodeContext.activeDimension) {
const dimensionContext = await fetchDimensionContext(nodeContext.activeDimension);
const dimensionBlock = buildDimensionContextBlock(dimensionContext);
if (dimensionBlock.trim().length > 0) {
blocks.push({ type: 'text', text: dimensionBlock });
}
}
const focusBlock = buildFocusedNodesBlock(nodeContext, options);
blocks.push({ type: 'text', text: focusBlock });
return { blocks, cacheHit: false };
}
/**
* Legacy string-based system prompt (for backward compatibility)
* @deprecated Use buildSystemPromptBlocks for Anthropic caching support
*/
export async function buildSystemPrompt(
nodeContext: NodeContext,
helperComponentKey: string,
options?: ContextBuilderOptions
): Promise<{ systemPrompt: string; cacheHit: boolean }> {
// Convert blocks to string for legacy callers
const result = await buildSystemPromptBlocks(nodeContext, helperComponentKey, options);
const systemPrompt = result.blocks.map(b => b.text).join('');
return { systemPrompt, cacheHit: result.cacheHit };
}
/**
* Loads helper instructions from JSON file
*/
// DB-backed; no-op placeholder kept for API stability if imported elsewhere
-126
View File
@@ -1,126 +0,0 @@
import fs from 'fs/promises';
import path from 'path';
interface LogEntry {
timestamp: string;
helper: string;
type: 'USER_MESSAGE' | 'SYSTEM_PROMPT' | 'TOOL_CALL' | 'TOOL_RESULT' | 'ASSISTANT_RESPONSE' | 'ERROR';
content: any;
sessionId: string;
}
const LOG_DIR = path.join(process.cwd(), 'logs');
const LOG_FILE = path.join(LOG_DIR, 'helper-interactions.log');
class HelperLogger {
private sessionId: string;
private logDirEnsured = false;
constructor() {
this.sessionId = Date.now().toString();
}
private async ensureLogDir() {
if (this.logDirEnsured) return;
try {
await fs.mkdir(LOG_DIR, { recursive: true });
this.logDirEnsured = true;
} catch (error) {
console.error('Failed to create log directory:', error);
}
}
private async writeLog(entry: LogEntry) {
try {
await this.ensureLogDir();
const logLine = JSON.stringify(entry) + '\n';
await fs.appendFile(LOG_FILE, logLine);
} catch (error) {
console.error('Failed to write log:', error);
}
}
logUserMessage(helper: string, messages: any[], openTabs: any[], activeTabId: any) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
helper,
type: 'USER_MESSAGE',
content: {
lastMessage: messages[messages.length - 1],
messageCount: messages.length,
openTabIds: openTabs.map(t => t.id),
activeTabId
},
sessionId: this.sessionId
};
this.writeLog(entry);
console.log(`✓ [${helper}] Request received`);
}
logSystemPrompt(helper: string, systemPrompt: string, cacheHit: boolean) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
helper,
type: 'SYSTEM_PROMPT',
content: {
systemPrompt,
cacheHit
},
sessionId: this.sessionId
};
this.writeLog(entry);
}
logToolCall(helper: string, toolName: string, parameters: any) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
helper,
type: 'TOOL_CALL',
content: { toolName, parameters },
sessionId: this.sessionId
};
this.writeLog(entry);
}
logToolResult(helper: string, toolName: string, result: any) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
helper,
type: 'TOOL_RESULT',
content: {
toolName,
result
},
sessionId: this.sessionId
};
this.writeLog(entry);
}
logAssistantResponse(helper: string, response: string) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
helper,
type: 'ASSISTANT_RESPONSE',
content: { response },
sessionId: this.sessionId
};
this.writeLog(entry);
}
logError(helper: string, error: Error | string) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
helper,
type: 'ERROR',
content: error instanceof Error ? {
message: error.message,
stack: error.stack
} : { message: error },
sessionId: this.sessionId
};
this.writeLog(entry);
console.error(`❌ [${helper}] Error occurred`);
}
}
export const helperLogger = new HelperLogger();