From 8a3b6df609c085066555dcf3d983f95265e29dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Thu, 29 Jan 2026 15:33:37 +1100 Subject: [PATCH] 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 --- src/services/agents/registry.ts | 103 --------- src/services/ai.ts | 246 -------------------- src/services/helpers/contextBuilder.ts | 301 ------------------------- src/services/helpers/logger.ts | 126 ----------- 4 files changed, 776 deletions(-) delete mode 100644 src/services/agents/registry.ts delete mode 100644 src/services/ai.ts delete mode 100644 src/services/helpers/contextBuilder.ts delete mode 100644 src/services/helpers/logger.ts diff --git a/src/services/agents/registry.ts b/src/services/agents/registry.ts deleted file mode 100644 index a0981f8..0000000 --- a/src/services/agents/registry.ts +++ /dev/null @@ -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 = { - '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 { - return this.AGENTS[key] || null; - } - - static async getAgentById(id: number): Promise { - return Object.values(this.AGENTS).find(a => a.id === id) || null; - } - - static async getEnabledAgents(): Promise { - return Object.values(this.AGENTS).filter(a => a.enabled); - } - - static async orchestrator(): Promise { - return this.AGENTS['ra-h']; - } - - static async orchestratorForMode(mode: 'easy' | 'hard' = 'easy'): Promise { - if (mode === 'hard') { - return this.AGENTS['ra-h']; - } - return this.AGENTS['ra-h-easy']; - } - - static async planner(): Promise { - return this.AGENTS['workflow']; - } -} diff --git a/src/services/ai.ts b/src/services/ai.ts deleted file mode 100644 index e3647d8..0000000 --- a/src/services/ai.ts +++ /dev/null @@ -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 { - 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 { - if (nodeIds.length === 0) return []; - - try { - const sqlite = getSQLiteClient(); - const placeholders = nodeIds.map(() => '?').join(', '); - const result = sqlite.query( - `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; - } -} diff --git a/src/services/helpers/contextBuilder.ts b/src/services/helpers/contextBuilder.ts deleted file mode 100644 index 24905d1..0000000 --- a/src/services/helpers/contextBuilder.ts +++ /dev/null @@ -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 { - // 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 { - if (!metadata) return {}; - if (typeof metadata === 'string') { - try { - const parsed = JSON.parse(metadata); - if (parsed && typeof parsed === 'object') { - return parsed as Record; - } - 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; - } - 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 { - 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 { - 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 diff --git a/src/services/helpers/logger.ts b/src/services/helpers/logger.ts deleted file mode 100644 index 3a163af..0000000 --- a/src/services/helpers/logger.ts +++ /dev/null @@ -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();