diff --git a/app/api/rah/delegations/[sessionId]/route.ts b/app/api/rah/delegations/[sessionId]/route.ts deleted file mode 100644 index 2fcd186..0000000 --- a/app/api/rah/delegations/[sessionId]/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { AgentDelegationService } from '@/services/agents/delegation'; - -export async function GET( - _request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const { sessionId } = await params; - const delegation = AgentDelegationService.getBySessionId(sessionId); - if (!delegation) { - return NextResponse.json({ error: 'Delegation not found' }, { status: 404 }); - } - return NextResponse.json({ delegation }); - } catch (error) { - console.error('Failed to fetch delegation:', error); - return NextResponse.json({ error: 'Failed to fetch delegation' }, { status: 500 }); - } -} - -export async function PATCH( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const { sessionId } = await params; - const body = await request.json(); - const summary: string | undefined = body?.summary; - const status: string | undefined = body?.status; - - if (!summary && !status) { - return NextResponse.json({ error: 'Nothing to update' }, { status: 400 }); - } - - const normalizedStatus = status && ['queued', 'in_progress', 'completed', 'failed'].includes(status) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ? (status as any) - : undefined; - - const delegation = summary - ? AgentDelegationService.completeDelegation(sessionId, summary, normalizedStatus ?? 'completed') - : AgentDelegationService.markInProgress(sessionId); - - if (!delegation) { - return NextResponse.json({ error: 'Delegation not found' }, { status: 404 }); - } - - return NextResponse.json({ delegation }); - } catch (error) { - console.error('Failed to update delegation:', error); - return NextResponse.json({ error: 'Failed to update delegation' }, { status: 500 }); - } -} - -export async function DELETE( - _request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const { sessionId } = await params; - const deleted = AgentDelegationService.deleteDelegation(sessionId); - if (!deleted) { - return NextResponse.json({ error: 'Delegation not found' }, { status: 404 }); - } - return NextResponse.json({ success: true }); - } catch (error) { - console.error('Failed to delete delegation:', error); - return NextResponse.json({ error: 'Failed to delete delegation' }, { status: 500 }); - } -} diff --git a/app/api/rah/delegations/[sessionId]/summary/route.ts b/app/api/rah/delegations/[sessionId]/summary/route.ts deleted file mode 100644 index e79b98e..0000000 --- a/app/api/rah/delegations/[sessionId]/summary/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { AgentDelegationService } from '@/services/agents/delegation'; - -export async function POST( - request: NextRequest, - { params }: { params: Promise<{ sessionId: string }> } -) { - try { - const { sessionId } = await params; - const body = await request.json(); - const summary: string | undefined = body?.summary; - const status: string | undefined = body?.status; - - if (!summary) { - return NextResponse.json({ error: 'Summary is required' }, { status: 400 }); - } - - const normalizedStatus = status && ['queued', 'in_progress', 'completed', 'failed'].includes(status) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ? (status as any) - : 'completed'; - - const delegation = AgentDelegationService.completeDelegation(sessionId, summary, normalizedStatus); - if (!delegation) { - return NextResponse.json({ error: 'Delegation not found' }, { status: 404 }); - } - - return NextResponse.json({ delegation }); - } catch (error) { - console.error('Failed to store delegation summary:', error); - return NextResponse.json({ error: 'Failed to store delegation summary' }, { status: 500 }); - } -} diff --git a/app/api/rah/delegations/route.ts b/app/api/rah/delegations/route.ts deleted file mode 100644 index daeaa54..0000000 --- a/app/api/rah/delegations/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { AgentDelegationService } from '@/services/agents/delegation'; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const statusFilter = searchParams.get('status'); - - const includeCompleted = searchParams.get('includeCompleted') === 'true' - || statusFilter !== 'active'; - const delegations = statusFilter === 'active' - ? AgentDelegationService.listActive({ includeCompleted }) - : AgentDelegationService.listRecent(); - - return NextResponse.json({ delegations }); - } catch (error) { - console.error('Failed to list delegations:', error); - return NextResponse.json({ error: 'Failed to load delegations' }, { status: 500 }); - } -} diff --git a/app/api/rah/delegations/stream/route.ts b/app/api/rah/delegations/stream/route.ts deleted file mode 100644 index 1e04e1a..0000000 --- a/app/api/rah/delegations/stream/route.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { NextRequest } from 'next/server'; - -export const runtime = 'nodejs'; -export const maxDuration = 900; - -class DelegationStreamBroadcaster { - private connections = new Map>(); - private pendingMessages = new Map(); - private encoder = new TextEncoder(); - - private encode(message: any) { - return `data: ${JSON.stringify({ ...message, timestamp: Date.now() })}\n\n`; - } - - private send(controller: ReadableStreamDefaultController, encoded: string) { - try { - controller.enqueue(this.encoder.encode(encoded)); - return true; - } catch (error) { - console.log('[DelegationStream] Removing dead connection', error); - return false; - } - } - - addConnection(sessionId: string, controller: ReadableStreamDefaultController) { - if (!this.connections.has(sessionId)) { - this.connections.set(sessionId, new Set()); - } - this.connections.get(sessionId)!.add(controller); - console.log(`[DelegationStream] Connection added for ${sessionId}, total: ${this.connections.get(sessionId)!.size}`); - - const backlog = this.pendingMessages.get(sessionId); - if (backlog && backlog.length > 0) { - console.log(`[DelegationStream] Flushing ${backlog.length} queued events for ${sessionId}`); - for (const message of backlog) { - const encoded = this.encode(message); - const delivered = this.send(controller, encoded); - if (!delivered) { - this.removeConnection(sessionId, controller); - break; - } - } - if ((this.connections.get(sessionId)?.size || 0) > 0) { - this.pendingMessages.delete(sessionId); - } - } - } - - removeConnection(sessionId: string, controller: ReadableStreamDefaultController) { - const sessionConns = this.connections.get(sessionId); - if (sessionConns) { - sessionConns.delete(controller); - console.log(`[DelegationStream] Connection removed from ${sessionId}, remaining: ${sessionConns.size}`); - if (sessionConns.size === 0) { - this.connections.delete(sessionId); - } - } - } - - broadcast(sessionId: string, message: any) { - const sessionConns = this.connections.get(sessionId); - if (!sessionConns || sessionConns.size === 0) { - const queue = this.pendingMessages.get(sessionId) ?? []; - queue.push(message); - // Prevent unbounded growth by keeping the latest 200 events - if (queue.length > 200) { - queue.splice(0, queue.length - 200); - } - this.pendingMessages.set(sessionId, queue); - console.log(`[DelegationStream] Queued event for ${sessionId}, pending=${queue.length}`); - return; - } - - const encoded = this.encode(message); - - let successCount = 0; - const staleControllers: ReadableStreamDefaultController[] = []; - for (const controller of sessionConns) { - if (this.send(controller, encoded)) { - successCount++; - } else { - staleControllers.push(controller); - } - } - - if (staleControllers.length > 0) { - staleControllers.forEach((controller) => sessionConns.delete(controller)); - } - - console.log(`[DelegationStream] Broadcasted to ${successCount}/${sessionConns.size} connections for ${sessionId}`); - } - - sendKeepAlive(sessionId: string) { - const sessionConns = this.connections.get(sessionId); - if (!sessionConns) return; - - const ping = this.encoder.encode(`: keep-alive\n\n`); - - for (const controller of sessionConns) { - try { - controller.enqueue(ping); - } catch { - sessionConns.delete(controller); - } - } - } -} - -declare global { - // eslint-disable-next-line no-var - var delegationStreamBroadcaster: DelegationStreamBroadcaster | undefined; -} - -export const delegationStreamBroadcaster = - globalThis.delegationStreamBroadcaster ?? new DelegationStreamBroadcaster(); - -if (typeof window === 'undefined') { - globalThis.delegationStreamBroadcaster = delegationStreamBroadcaster; -} - -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const sessionId = searchParams.get('sessionId'); - - if (!sessionId) { - return new Response('Missing sessionId', { status: 400 }); - } - - const stream = new ReadableStream({ - start(controller) { - const state = this as unknown as { cleanup?: () => void; abortHandler?: () => void }; - delegationStreamBroadcaster.addConnection(sessionId, controller); - - const encoder = new TextEncoder(); - controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'CONNECTION_ESTABLISHED', timestamp: Date.now() })}\n\n`)); - - const keepAliveInterval = setInterval(() => { - delegationStreamBroadcaster.sendKeepAlive(sessionId); - }, 30000); - - const cleanup = () => { - clearInterval(keepAliveInterval); - delegationStreamBroadcaster.removeConnection(sessionId, controller); - state.cleanup = undefined; - }; - - const abortHandler = () => { - cleanup(); - request.signal.removeEventListener('abort', abortHandler); - state.abortHandler = undefined; - }; - - request.signal.addEventListener('abort', abortHandler); - - state.cleanup = cleanup; - state.abortHandler = abortHandler; - }, - cancel() { - console.log(`[DelegationStream] Stream cancelled for ${sessionId}`); - const state = this as unknown as { cleanup?: () => void; abortHandler?: () => void }; - if (state.abortHandler) { - request.signal.removeEventListener('abort', state.abortHandler); - state.abortHandler = undefined; - } - if (state.cleanup) { - state.cleanup(); - } - } - }); - - return new Response(stream, { - headers: { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache, no-transform', - 'Connection': 'keep-alive', - 'X-Accel-Buffering': 'no', - }, - }); -} diff --git a/app/api/workflows/execute/route.ts b/app/api/workflows/execute/route.ts index a4346a9..0bd54c3 100644 --- a/app/api/workflows/execute/route.ts +++ b/app/api/workflows/execute/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { WorkflowRegistry } from '@/services/workflows/registry'; import { getSQLiteClient } from '@/services/database/sqlite-client'; -import { AgentDelegationService } from '@/services/agents/delegation'; import { WorkflowExecutor } from '@/services/agents/workflowExecutor'; import { getAutoContextSummaries } from '@/services/context/autoContext'; @@ -107,35 +106,36 @@ ${workflow.instructions} ${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general workflow)'}`; - // Create delegation - const delegation = AgentDelegationService.createDelegation({ - task, - context: contextLines, - expectedOutcome: workflow.expectedOutcome, - agentType: 'workflow', - supabaseToken: null, - }); + const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - // Fire-and-forget execution - void WorkflowExecutor.execute({ - sessionId: delegation.sessionId, - task, - context: contextLines, - expectedOutcome: workflow.expectedOutcome, - workflowKey, - workflowNodeId: nodeId, - }).catch((error) => { + try { + // Execute workflow synchronously + const result = await WorkflowExecutor.execute({ + sessionId, + task, + context: contextLines, + expectedOutcome: workflow.expectedOutcome, + workflowKey, + workflowNodeId: nodeId, + }); + + return NextResponse.json({ + success: true, + sessionId, + workflowKey, + nodeId: nodeId || null, + status: 'completed', + summary: result.summary, + message: `Workflow '${workflow.displayName}' completed`, + }); + } catch (error) { console.error('[/api/workflows/execute] Execution failed:', error); - }); - - return NextResponse.json({ - success: true, - delegationId: delegation.sessionId, - workflowKey, - nodeId: nodeId || null, - status: 'executing', - message: `Workflow '${workflow.displayName}' started`, - }); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + return NextResponse.json( + { success: false, error: `Workflow execution failed: ${errorMessage}` }, + { status: 500 } + ); + } } catch (error) { console.error('Error executing workflow:', error); return NextResponse.json( diff --git a/src/components/agents/AgentsPanel.tsx b/src/components/agents/AgentsPanel.tsx index 5c9ddb3..61924b6 100644 --- a/src/components/agents/AgentsPanel.tsx +++ b/src/components/agents/AgentsPanel.tsx @@ -5,10 +5,21 @@ import RAHChat from './RAHChat'; import QuickAddInput from './QuickAddInput'; import QuickAddStatus from './QuickAddStatus'; import { Zap, Flame, Minimize2 } from 'lucide-react'; -import type { AgentDelegation } from '@/services/agents/delegation'; import { Node } from '@/types/database'; import { parseAndRenderContent } from '@/components/helpers/NodeLabelRenderer'; +// Stub type for delegation (delegation system removed in rah-light) +type AgentDelegation = { + id: number; + sessionId: string; + task: string; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + agentType: string; + createdAt: string; + updatedAt: string; +}; + interface AgentsPanelProps { openTabsData: Node[]; activeTabId: number | null; diff --git a/src/components/agents/DelegationIndicator.tsx b/src/components/agents/DelegationIndicator.tsx deleted file mode 100644 index ab976ed..0000000 --- a/src/components/agents/DelegationIndicator.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { AgentDelegation } from '@/services/agents/delegation'; - -interface DelegationIndicatorProps { - delegations: AgentDelegation[]; -} - -export default function DelegationIndicator({ delegations }: DelegationIndicatorProps) { - if (!delegations.length) return null; - - const activeCount = delegations.filter((d) => d.status === 'queued' || d.status === 'in_progress').length; - - if (activeCount === 0) return null; - - return ( -
- - {activeCount} active -
- ); -} diff --git a/src/components/agents/MiniRAHPanel.tsx b/src/components/agents/MiniRAHPanel.tsx index 590b8a0..359661e 100644 --- a/src/components/agents/MiniRAHPanel.tsx +++ b/src/components/agents/MiniRAHPanel.tsx @@ -1,5 +1,17 @@ import { ReactNode } from 'react'; -import type { AgentDelegation } from '@/services/agents/delegation'; + +// Stub type for delegation (delegation system removed in rah-light) +type AgentDelegation = { + id: number; + sessionId: string; + task: string; + context: string[]; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + agentType: string; + createdAt: string; + updatedAt: string; +}; interface MiniRAHPanelProps { delegation: AgentDelegation; diff --git a/src/components/agents/QuickAddStatus.tsx b/src/components/agents/QuickAddStatus.tsx index e6e7ba4..679f279 100644 --- a/src/components/agents/QuickAddStatus.tsx +++ b/src/components/agents/QuickAddStatus.tsx @@ -1,6 +1,15 @@ "use client"; -import type { AgentDelegation } from '@/services/agents/delegation'; +// Stub type for delegation (delegation system removed in rah-light) +type AgentDelegation = { + id: number; + sessionId: string; + task: string; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + createdAt: string; + updatedAt: string; +}; interface QuickAddStatusProps { delegations: AgentDelegation[]; diff --git a/src/components/agents/RAHChat.tsx b/src/components/agents/RAHChat.tsx index 6e421e3..f7c173e 100644 --- a/src/components/agents/RAHChat.tsx +++ b/src/components/agents/RAHChat.tsx @@ -6,15 +6,60 @@ import AsciiBanner from './AsciiBanner'; import TerminalMessage from './TerminalMessage'; import TerminalInput from './TerminalInput'; import { Zap, Flame } from 'lucide-react'; -import DelegationIndicator from './DelegationIndicator'; -import type { AgentDelegation } from '@/services/agents/delegation'; import { useSSEChat, ChatMessage, MessageRole } from './hooks/useSSEChat'; import { useQuotaHandler } from '@/hooks/useQuotaHandler'; import { apiKeyService } from '@/services/storage/apiKeys'; -import { useVoiceSession } from './hooks/useVoiceSession'; -import { useAssistantTTS } from './hooks/useAssistantTTS'; -import { useRealtimeVoiceClient } from './hooks/useRealtimeVoiceClient'; -import { useVoiceInterruption } from './hooks/useVoiceInterruption'; + +// Stub type for delegation (delegation system removed in rah-light) +type AgentDelegation = { + id: number; + sessionId: string; + task: string; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + agentType: string; + createdAt: string; + updatedAt: string; +}; + +// Stub DelegationIndicator component (delegation system removed) +function DelegationIndicator({ delegations }: { delegations: AgentDelegation[] }) { + return null; +} + +// Stub voice hooks (voice system will be removed in story 2) +function useVoiceSession() { + return { + isActive: false, + amplitude: 0, + startSession: () => {}, + stopSession: () => {}, + resetTranscript: () => {}, + setStatus: (_status: string) => {}, + setAmplitude: (_amp: number) => {}, + setInterimTranscript: (_text: string) => {}, + appendFinalTranscript: (_text: string) => {}, + }; +} + +function useAssistantTTS(_options: { onSpeechStart?: () => void; onSpeechComplete?: () => void; onError?: (e: Error) => void }) { + return { + speak: (_text: string, _options?: { flush?: boolean; metadata?: Record }) => {}, + stop: () => {}, + status: 'idle' as const, + }; +} + +function useVoiceInterruption(_options: { amplitude: number; isVoiceActive: boolean; ttsStatus: string; onInterruption: () => void }) {} + +function useRealtimeVoiceClient(_handlers: { onStatusChange?: (status: string) => void; onInterimTranscript?: (text: string) => void; onFinalTranscript?: (text: string) => void; onAmplitude?: (amp: number) => void; onError?: (e: Error) => void }, _options: { getAuthToken: () => string | null }) { + return { + connect: () => {}, + disconnect: () => {}, + start: () => {}, + stop: () => {}, + }; +} const createSessionId = () => `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const createVoiceRequestId = () => diff --git a/src/components/agents/WiseRAHPanel.tsx b/src/components/agents/WiseRAHPanel.tsx index 26676f3..b6cd884 100644 --- a/src/components/agents/WiseRAHPanel.tsx +++ b/src/components/agents/WiseRAHPanel.tsx @@ -1,5 +1,17 @@ import { Fragment, ReactNode, useMemo } from 'react'; -import type { AgentDelegation } from '@/services/agents/delegation'; + +// Stub type for delegation (delegation system removed in rah-light) +type AgentDelegation = { + id: number; + sessionId: string; + task: string; + context: string[]; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + agentType: string; + createdAt: string; + updatedAt: string; +}; const statusPalette: Record = { queued: { border: '#3a2f5f', badge: '#a78bfa' }, diff --git a/src/components/layout/ThreePanelLayout.tsx b/src/components/layout/ThreePanelLayout.tsx index d3d078e..9cc765c 100644 --- a/src/components/layout/ThreePanelLayout.tsx +++ b/src/components/layout/ThreePanelLayout.tsx @@ -6,9 +6,21 @@ import SearchModal from '../nodes/SearchModal'; import { Node } from '@/types/database'; import { DatabaseEvent } from '@/services/events'; import { usePersistentState } from '@/hooks/usePersistentState'; -import type { AgentDelegation } from '@/services/agents/delegation'; import type { ChatMessage } from '@/components/agents/hooks/useSSEChat'; +// Stub type for delegation (delegation system removed in rah-light) +type AgentDelegation = { + id: number; + sessionId: string; + task: string; + context: string[]; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + agentType: string; + createdAt: string; + updatedAt: string; +}; + // Layout components import LeftToolbar from './LeftToolbar'; import SplitHandle from './SplitHandle'; @@ -66,8 +78,8 @@ export default function ThreePanelLayout() { // Active dimension tracking (for chat context) const [activeDimension, setActiveDimension] = usePersistentState('ui.focus.activeDimension', null); - // Delegations state (shared between chat and workflows panes) - const [delegationsMap, setDelegationsMap] = useState>({}); + // Delegations state (deprecated - kept for component compatibility) + const [delegationsMap] = useState>({}); const delegations = useMemo(() => Object.values(delegationsMap), [delegationsMap]); // Chat state (lifted to persist across pane type changes) @@ -161,32 +173,7 @@ export default function ThreePanelLayout() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [openTabsKey]); - // Load delegations on mount - useEffect(() => { - let cancelled = false; - const loadExisting = async () => { - try { - const response = await fetch('/api/rah/delegations?status=active&includeCompleted=true'); - if (!response.ok) return; - const data = await response.json(); - if (!Array.isArray(data.delegations)) return; - - setDelegationsMap((prev) => { - if (cancelled) return prev; - const next = { ...prev }; - for (const delegation of data.delegations as AgentDelegation[]) { - next[delegation.sessionId] = delegation; - } - return next; - }); - } catch (error) { - console.error('[ThreePanelLayout] Failed to load delegations:', error); - } - }; - - loadExisting(); - return () => { cancelled = true; }; - }, []); + // Delegations loading removed (delegation system removed in rah-light) // Keyboard shortcut handler useEffect(() => { @@ -278,27 +265,8 @@ export default function ThreePanelLayout() { break; case 'AGENT_DELEGATION_CREATED': - if (data.data?.delegation) { - setDelegationsMap(prev => ({ - ...prev, - [data.data.delegation.sessionId]: data.data.delegation - })); - } - if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('delegations:created', { detail: data.data })); - } - break; - case 'AGENT_DELEGATION_UPDATED': - if (data.data?.delegation) { - setDelegationsMap(prev => ({ - ...prev, - [data.data.delegation.sessionId]: data.data.delegation - })); - } - if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('delegations:updated', { detail: data.data })); - } + // Delegation events ignored (delegation system removed in rah-light) break; case 'WORKFLOW_PROGRESS': diff --git a/src/components/panes/WorkflowsPane.tsx b/src/components/panes/WorkflowsPane.tsx index 5a10062..2bf2a89 100644 --- a/src/components/panes/WorkflowsPane.tsx +++ b/src/components/panes/WorkflowsPane.tsx @@ -3,8 +3,7 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import RAHChat from '@/components/agents/RAHChat'; import PaneHeader from './PaneHeader'; -import { WorkflowsPaneProps, PaneType } from './types'; -import type { AgentDelegation } from '@/services/agents/delegation'; +import { WorkflowsPaneProps, PaneType, AgentDelegation } from './types'; import { parseAndRenderContent } from '@/components/helpers/NodeLabelRenderer'; import type { ChatMessage } from '@/components/agents/hooks/useSSEChat'; diff --git a/src/components/panes/types.ts b/src/components/panes/types.ts index 9d8c1b2..1336b02 100644 --- a/src/components/panes/types.ts +++ b/src/components/panes/types.ts @@ -1,6 +1,18 @@ import React from 'react'; import { Node } from '@/types/database'; -import type { AgentDelegation } from '@/services/agents/delegation'; + +// Stub type for delegation (delegation system removed in rah-light) +export type AgentDelegation = { + id: number; + sessionId: string; + task: string; + context: string[]; + status: 'queued' | 'in_progress' | 'completed' | 'failed'; + summary?: string | null; + agentType: string; + createdAt: string; + updatedAt: string; +}; // The six pane types export type PaneType = 'node' | 'chat' | 'workflows' | 'dimensions' | 'map' | 'views'; diff --git a/src/services/agents/delegation.ts b/src/services/agents/delegation.ts deleted file mode 100644 index b40a4ca..0000000 --- a/src/services/agents/delegation.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { getSQLiteClient } from '@/services/database/sqlite-client'; -import { eventBroadcaster } from '@/services/events'; - -export type DelegationStatus = 'queued' | 'in_progress' | 'completed' | 'failed'; -export type DelegationAgentType = 'workflow' | 'wise-rah'; - -export interface AgentDelegation { - id: number; - sessionId: string; - task: string; - context: string[]; - expectedOutcome?: string | null; - status: DelegationStatus; - summary?: string | null; - agentType: DelegationAgentType; - supabaseToken?: string | null; - createdAt: string; - updatedAt: string; -} - -function rowToDelegation(row: any): AgentDelegation { - return { - id: row.id, - sessionId: row.session_id, - task: row.task, - context: (() => { - try { - return row.context ? JSON.parse(row.context) : []; - } catch { - return []; - } - })(), - expectedOutcome: row.expected_outcome, - status: row.status as DelegationStatus, - summary: row.summary, - agentType: (row.agent_type || 'mini') as DelegationAgentType, - supabaseToken: row.supabase_token ?? null, - // SQLite CURRENT_TIMESTAMP is UTC, append 'Z' to parse correctly as UTC - createdAt: row.created_at ? row.created_at.replace(' ', 'T') + 'Z' : row.created_at, - updatedAt: row.updated_at ? row.updated_at.replace(' ', 'T') + 'Z' : row.updated_at, - }; -} - -function ensureTable() { - const db = getSQLiteClient(); - db.query(` - CREATE TABLE IF NOT EXISTS agent_delegations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT UNIQUE NOT NULL, - task TEXT NOT NULL, - context TEXT, - expected_outcome TEXT, - status TEXT NOT NULL DEFAULT 'queued', - summary TEXT, - agent_type TEXT NOT NULL DEFAULT 'mini', - supabase_token TEXT, - created_at TEXT DEFAULT CURRENT_TIMESTAMP, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP - ) - `); - - // Add agent_type column if it doesn't exist (migration) - try { - const stmt = db.prepare('SELECT 1 FROM agent_delegations LIMIT 0'); - const tableExists = stmt !== null; - - if (tableExists) { - // Try to add the column, ignore if it already exists - try { - db.prepare(`ALTER TABLE agent_delegations ADD COLUMN agent_type TEXT NOT NULL DEFAULT 'mini'`).run(); - console.log('βœ… Added agent_type column to agent_delegations table'); - } catch (alterError: any) { - // Column already exists, ignore - if (!alterError.message?.includes('duplicate column')) { - console.warn('Migration warning:', alterError.message); - } - } - - try { - db.prepare(`ALTER TABLE agent_delegations ADD COLUMN supabase_token TEXT`).run(); - console.log('βœ… Added supabase_token column to agent_delegations table'); - } catch (alterError: any) { - if (!alterError.message?.includes('duplicate column')) { - console.warn('Migration warning:', alterError.message); - } - } - } - } catch (error: any) { - // Table doesn't exist yet or other error - it will be created with the columns - console.log('Table creation will include agent_type and supabase_token columns'); - } -} - -export class AgentDelegationService { - static createDelegation(input: { - task: string; - context?: string[]; - expectedOutcome?: string | null; - agentType?: DelegationAgentType; - supabaseToken?: string | null; - }): AgentDelegation { - ensureTable(); - const db = getSQLiteClient(); - const sessionId = `delegation_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - const contextJson = JSON.stringify(input.context ?? []); - const agentType = input.agentType || 'mini'; - db.prepare( - `INSERT INTO agent_delegations (session_id, task, context, expected_outcome, status, agent_type, supabase_token) - VALUES (?, ?, ?, ?, 'queued', ?, ?)` - ).run( - sessionId, - input.task, - contextJson, - input.expectedOutcome ?? null, - agentType, - input.supabaseToken ?? null - ); - - const row = db - .prepare('SELECT * FROM agent_delegations WHERE session_id = ?') - .get(sessionId); - - const delegation = rowToDelegation(row); - eventBroadcaster.broadcast({ type: 'AGENT_DELEGATION_CREATED', data: { delegation } }); - return delegation; - } - - static markInProgress(sessionId: string): AgentDelegation | null { - ensureTable(); - const db = getSQLiteClient(); - db.prepare( - `UPDATE agent_delegations - SET status = 'in_progress', updated_at = CURRENT_TIMESTAMP - WHERE session_id = ? AND status = 'queued'` - ).run(sessionId); - const row = db.prepare('SELECT * FROM agent_delegations WHERE session_id = ?').get(sessionId); - if (!row) return null; - const delegation = rowToDelegation(row); - eventBroadcaster.broadcast({ type: 'AGENT_DELEGATION_UPDATED', data: { delegation } }); - return delegation; - } - - static touchDelegation(sessionId: string): void { - // Update the timestamp to prevent cleanup from killing active delegations - ensureTable(); - const db = getSQLiteClient(); - db.prepare( - `UPDATE agent_delegations SET updated_at = CURRENT_TIMESTAMP WHERE session_id = ? AND status = 'in_progress'` - ).run(sessionId); - } - - static completeDelegation(sessionId: string, summary: string, status: DelegationStatus = 'completed'): AgentDelegation | null { - ensureTable(); - const db = getSQLiteClient(); - db.prepare( - `UPDATE agent_delegations - SET status = ?, summary = ?, updated_at = CURRENT_TIMESTAMP, supabase_token = NULL - WHERE session_id = ?` - ).run(status, summary, sessionId); - const row = db.prepare('SELECT * FROM agent_delegations WHERE session_id = ?').get(sessionId); - if (!row) return null; - const delegation = rowToDelegation(row); - eventBroadcaster.broadcast({ type: 'AGENT_DELEGATION_UPDATED', data: { delegation } }); - return delegation; - } - - static getBySessionId(sessionId: string): AgentDelegation | null { - ensureTable(); - const db = getSQLiteClient(); - const row = db.prepare('SELECT * FROM agent_delegations WHERE session_id = ?').get(sessionId); - return row ? rowToDelegation(row) : null; - } - - static getDelegation(sessionId: string): AgentDelegation | null { - return this.getBySessionId(sessionId); - } - - static listActive({ includeCompleted = true, limit = 100 }: { includeCompleted?: boolean; limit?: number } = {}): AgentDelegation[] { - ensureTable(); - const db = getSQLiteClient(); - // Load all delegations - user closes them manually from UI - const rows = includeCompleted - ? db.prepare( - `SELECT * FROM agent_delegations - ORDER BY updated_at DESC - LIMIT ?` - ).all(limit) - : db.prepare( - `SELECT * FROM agent_delegations - WHERE status IN ('queued','in_progress') - ORDER BY updated_at DESC - LIMIT ?` - ).all(limit); - return rows.map(rowToDelegation); - } - - static listRecent(limit = 20): AgentDelegation[] { - ensureTable(); - const db = getSQLiteClient(); - const rows = db.prepare( - `SELECT * FROM agent_delegations - ORDER BY created_at DESC - LIMIT ?` - ).all(limit); - return rows.map(rowToDelegation); - } - - static deleteDelegation(sessionId: string): boolean { - ensureTable(); - const db = getSQLiteClient(); - const result = db.prepare('DELETE FROM agent_delegations WHERE session_id = ?').run(sessionId); - return result.changes > 0; - } - - static cleanupStaleDelegations(timeoutMinutes = 15): number { - ensureTable(); - const db = getSQLiteClient(); - const cutoffTime = new Date(Date.now() - timeoutMinutes * 60 * 1000).toISOString(); - - const result = db.prepare(` - UPDATE agent_delegations - SET status = 'failed', - summary = 'Task timed out (exceeded ${timeoutMinutes} minutes)', - updated_at = CURRENT_TIMESTAMP - WHERE status = 'in_progress' - AND updated_at < ? - `).run(cutoffTime); - - const affectedCount = result.changes || 0; - - if (affectedCount > 0) { - const rows = db.prepare( - `SELECT * FROM agent_delegations WHERE status = 'failed' AND summary LIKE 'Task timed out%'` - ).all(); - rows.forEach(row => { - const delegation = rowToDelegation(row); - eventBroadcaster.broadcast({ type: 'AGENT_DELEGATION_UPDATED', data: { delegation } }); - }); - } - - return affectedCount; - } -} diff --git a/src/services/agents/quickAdd.ts b/src/services/agents/quickAdd.ts index 080244c..a35d6bb 100644 --- a/src/services/agents/quickAdd.ts +++ b/src/services/agents/quickAdd.ts @@ -1,10 +1,10 @@ -import { AgentDelegationService } from './delegation'; import { summarizeToolExecution } from './toolResultUtils'; import { youtubeExtractTool } from '@/tools/other/youtubeExtract'; import { websiteExtractTool } from '@/tools/other/websiteExtract'; import { paperExtractTool } from '@/tools/other/paperExtract'; import { formatNodeForChat } from '@/tools/infrastructure/nodeFormatter'; import { summarizeTranscript } from './transcriptSummarizer'; +import { eventBroadcaster } from '@/services/events'; export type QuickAddMode = 'link' | 'note' | 'chat'; @@ -340,41 +340,42 @@ async function handleChatTranscriptQuickAdd(rawInput: string, task: string): Pro }); } -export async function enqueueQuickAdd({ rawInput, mode, description }: QuickAddInput) { +export interface QuickAddResult { + success: boolean; + summary?: string; + error?: string; + nodeId?: number; +} + +export async function enqueueQuickAdd({ rawInput, mode, description }: QuickAddInput): Promise { const inputType = detectInputType(rawInput, mode); - const context: string[] = (inputType === 'note' || inputType === 'chat') ? [] : [rawInput]; const task = buildTaskPrompt(inputType, rawInput); - const expectedOutcome = buildExpectedOutcome(inputType); - const delegation = AgentDelegationService.createDelegation({ - task, - context, - expectedOutcome, - }); - - setImmediate(async () => { - try { - AgentDelegationService.markInProgress(delegation.sessionId); + try { + let summary: string; + if (inputType === 'note') { + summary = await handleNoteQuickAdd(rawInput, task, description); + } else if (inputType === 'chat') { + summary = await handleChatTranscriptQuickAdd(rawInput, task); + } else { + summary = await handleExtractionQuickAdd(inputType as ExtractionQuickAddType, rawInput, task); + } - let summary: string; - if (inputType === 'note') { - summary = await handleNoteQuickAdd(rawInput, task, description); - } else if (inputType === 'chat') { - summary = await handleChatTranscriptQuickAdd(rawInput, task); - } else { - summary = await handleExtractionQuickAdd(inputType as ExtractionQuickAddType, rawInput, task); - } + // Extract node ID from summary if present + const nodeIdMatch = summary.match(/\[NODE:(\d+)/); + const nodeId = nodeIdMatch ? parseInt(nodeIdMatch[1], 10) : undefined; - AgentDelegationService.completeDelegation(delegation.sessionId, summary, 'completed'); - } catch (error) { - const message = error instanceof Error ? error.message : 'unknown error'; - AgentDelegationService.completeDelegation( - delegation.sessionId, - `Quick Add failed: ${message}`, - 'failed' - ); + // Broadcast node created event + if (nodeId) { + eventBroadcaster.broadcast({ + type: 'NODE_CREATED', + data: { node: { id: nodeId, title: 'Quick Add' } } + }); } - }); - return delegation; + return { success: true, summary, nodeId }; + } catch (error) { + const message = error instanceof Error ? error.message : 'unknown error'; + return { success: false, error: `Quick Add failed: ${message}` }; + } } diff --git a/src/services/agents/workflowExecutor.ts b/src/services/agents/workflowExecutor.ts index 5a67905..ef037ff 100644 --- a/src/services/agents/workflowExecutor.ts +++ b/src/services/agents/workflowExecutor.ts @@ -1,7 +1,6 @@ import { streamText, ModelMessage } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import type { LanguageModelV2ToolResultOutput } from '@ai-sdk/provider'; -import { AgentDelegationService } from '@/services/agents/delegation'; import { WORKFLOW_EXECUTOR_SYSTEM_PROMPT } from '@/config/prompts/workflow-executor'; import { getToolsByNames } from '@/tools/infrastructure/registry'; import { WorkflowRegistry } from '@/services/workflows/registry'; @@ -10,7 +9,6 @@ import { calculateCost } from '@/services/analytics/pricing'; import { UsageData } from '@/types/analytics'; import { summarizeToolExecution } from '@/services/agents/toolResultUtils'; import { edgeService } from '@/services/database/edges'; -import { delegationStreamBroadcaster } from '@/app/api/rah/delegations/stream/route'; import { RequestContext } from '@/services/context/requestContext'; export interface WorkflowExecutionInput { @@ -38,8 +36,7 @@ export class WorkflowExecutor { throw new Error('OPENAI_API_KEY is not set for workflow execution.'); } - AgentDelegationService.markInProgress(sessionId); - console.log('βœ… [WorkflowExecutor] Delegation marked in progress'); + console.log('βœ… [WorkflowExecutor] Starting workflow execution'); // Get workflow definition if available const workflow = workflowKey ? await WorkflowRegistry.getWorkflowByKey(workflowKey) : null; @@ -165,28 +162,9 @@ export class WorkflowExecutor { const ensureString = (value: unknown) => (typeof value === 'string' ? value.trim() : ''); - const sanitizeForBroadcast = (value: unknown) => { - if (value === undefined) return undefined; - try { - return JSON.parse(JSON.stringify(value)); - } catch (error) { - console.warn('[WorkflowExecutor] Failed to serialize delegation payload', error); - if (typeof value === 'string') return value; - return undefined; - } - }; - - const emitDelegationEvent = (payload: Record) => { - delegationStreamBroadcaster.broadcast(sessionId, payload); - }; - + // Logging helpers (no-op for lite version without delegation streaming) const emitToolStart = (toolCallId: string, toolName: string, input: unknown) => { - emitDelegationEvent({ - type: 'tool-input-start', - toolCallId, - toolName, - input: sanitizeForBroadcast(input), - }); + console.log(`πŸ”§ [WorkflowExecutor] Tool start: ${toolName}`); }; const emitToolCompletion = ( @@ -197,15 +175,7 @@ export class WorkflowExecutor { status: 'complete' | 'error' = 'complete', errorMessage?: string ) => { - emitDelegationEvent({ - type: 'tool-output-available', - toolCallId, - toolName, - result: sanitizeForBroadcast(rawResult), - summary, - status, - error: errorMessage, - }); + console.log(`βœ… [WorkflowExecutor] Tool complete: ${toolName} - ${status}`); }; const buildToolOutput = (toolName: string, summary: string, rawResult: any): LanguageModelV2ToolResultOutput => { @@ -280,9 +250,6 @@ export class WorkflowExecutor { for (let i = 0; i < maxIterations; i++) { console.log(`πŸ”„ [WorkflowExecutor] Iteration ${i + 1}/${maxIterations}`); - // Touch delegation every iteration to prevent cleanup from killing it - AgentDelegationService.touchDelegation(sessionId); - const streamResult = await streamText({ model: openaiProvider('gpt-5-mini'), messages, @@ -308,12 +275,9 @@ export class WorkflowExecutor { console.log(`πŸ“Š [WorkflowExecutor] Step ${i + 1} finishReason:`, response.finishReason); - // Stream text response to delegation chat + // Log text response if (response.text && response.text.trim()) { - emitDelegationEvent({ - type: 'text-delta', - delta: response.text, - }); + console.log(`πŸ“ [WorkflowExecutor] Response text: ${response.text.substring(0, 100)}...`); } if (response.finishReason !== 'tool-calls') { @@ -325,9 +289,9 @@ export class WorkflowExecutor { const toolCalls = response.toolCalls || []; console.log(`πŸ”§ [WorkflowExecutor] Executing ${toolCalls.length} tool calls`); - // Broadcast new assistant message for next iteration + // Log tool calls if (toolCalls.length > 0) { - emitDelegationEvent({ type: 'assistant-message' }); + console.log(`πŸ”§ [WorkflowExecutor] Processing ${toolCalls.length} tool calls`); } messages.push({ @@ -441,25 +405,12 @@ export class WorkflowExecutor { console.log('πŸ“ [WorkflowExecutor] Summary length:', summary.length); if (!summary) { - emitDelegationEvent({ - type: 'assistant-message', - }); - emitDelegationEvent({ - type: 'text-delta', - delta: 'Workflow executor attempted to summarise but the response was empty. Check tool logs above for context.', - }); + console.warn('[WorkflowExecutor] Empty summary received'); throw new Error('Workflow executor returned empty summary'); } console.log('[WorkflowExecutor] summary:', summary); - // Emit final summary to the stream so it appears in the UI - emitDelegationEvent({ type: 'assistant-message' }); - emitDelegationEvent({ - type: 'text-delta', - delta: summary, - }); - // Calculate cost and log to chats table if (usage) { const inputTokens = (usage as any).promptTokens || usage.inputTokens || 0; @@ -487,16 +438,13 @@ export class WorkflowExecutor { workflowNodeId, }; - const delegation = AgentDelegationService.getDelegation(sessionId); - const delegationId = delegation?.id; - await ChatLoggingMiddleware.logChatInteraction( task, summary, { helperName: 'workflow-agent', agentType: 'planner', - delegationId: delegationId ?? null, + delegationId: null, sessionId, usageData, traceId, @@ -511,23 +459,12 @@ export class WorkflowExecutor { console.log(`πŸ’° [WorkflowExecutor] Cost: $${costResult.totalCostUsd.toFixed(6)} (${totalTokens} tokens)`); } - console.log('βœ… [WorkflowExecutor] Completing delegation with summary'); - return AgentDelegationService.completeDelegation(sessionId, summary); + console.log('βœ… [WorkflowExecutor] Workflow execution complete'); + return { sessionId, summary, status: 'completed' as const }; } catch (error) { console.error('❌ [WorkflowExecutor] Error during execution:', error); console.error('❌ [WorkflowExecutor] Error stack:', error instanceof Error ? error.stack : 'No stack'); - const message = error instanceof Error ? error.message : 'Unknown delegation error'; - - // Broadcast error to delegation stream - delegationStreamBroadcaster.broadcast(sessionId, { - type: 'assistant-message', - }); - delegationStreamBroadcaster.broadcast(sessionId, { - type: 'text-delta', - delta: `Workflow executor failed: ${message}`, - }); - - AgentDelegationService.completeDelegation(sessionId, `Workflow executor failed: ${message}`, 'failed'); + const message = error instanceof Error ? error.message : 'Unknown workflow error'; throw error; } } diff --git a/src/tools/orchestration/delegateToWiseRAH.ts b/src/tools/orchestration/delegateToWiseRAH.ts index 0f7862e..6e424cb 100644 --- a/src/tools/orchestration/delegateToWiseRAH.ts +++ b/src/tools/orchestration/delegateToWiseRAH.ts @@ -1,11 +1,10 @@ import { tool } from 'ai'; import { z } from 'zod'; -import { AgentDelegationService } from '@/services/agents/delegation'; import { WorkflowExecutor } from '@/services/agents/workflowExecutor'; import { RequestContext } from '@/services/context/requestContext'; export const delegateToWiseRAHTool = tool({ - description: 'Delegate complex workflows to wise ra-h (GPT-5)', + description: 'Delegate complex workflows to workflow executor', inputSchema: z.object({ task: z.string().describe('Complex workflow description: what needs to be planned and executed'), context: z.array(z.string()).max(8).default([]).describe('Optional context: node IDs, URLs, or key information the planner needs'), @@ -17,16 +16,10 @@ export const delegateToWiseRAHTool = tool({ const requestContext = RequestContext.get(); console.log('[delegateToWiseRAH] Current traceId:', requestContext.traceId); - const delegation = AgentDelegationService.createDelegation({ - task, - context, - expectedOutcome, - agentType: 'wise-rah', - supabaseToken: null, - }); + const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; const execution = await WorkflowExecutor.execute({ - sessionId: delegation.sessionId, + sessionId, task, context, expectedOutcome, @@ -37,7 +30,7 @@ export const delegateToWiseRAHTool = tool({ }); // Return a simple string that Claude can directly use in conversation - const summary = execution?.summary || 'Wise ra-h delegated but no summary returned.'; - return `Wise ra-h (session ${delegation.sessionId.split('_').pop()}) completed the workflow:\n\n${summary}`; + const summary = execution?.summary || 'Workflow completed but no summary returned.'; + return `Workflow (session ${sessionId.split('_').pop()}) completed:\n\n${summary}`; }, }); diff --git a/src/tools/orchestration/executeWorkflow.ts b/src/tools/orchestration/executeWorkflow.ts index ef21f6e..41568c3 100644 --- a/src/tools/orchestration/executeWorkflow.ts +++ b/src/tools/orchestration/executeWorkflow.ts @@ -2,7 +2,6 @@ import { tool } from 'ai'; import { z } from 'zod'; import { WorkflowRegistry } from '@/services/workflows/registry'; import { getSQLiteClient } from '@/services/database/sqlite-client'; -import { AgentDelegationService } from '@/services/agents/delegation'; import { WorkflowExecutor } from '@/services/agents/workflowExecutor'; import { RequestContext } from '@/services/context/requestContext'; import { getAutoContextSummaries } from '@/services/context/autoContext'; @@ -92,39 +91,37 @@ ${workflow.instructions} ${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general workflow)'}`; - // 5. Delegate to wise ra-h oracle with workflow metadata + // 5. Execute workflow directly const requestContext = RequestContext.get(); console.log('[executeWorkflowTool] Current traceId:', requestContext.traceId); - const delegation = AgentDelegationService.createDelegation({ - task, - context: contextLines, - expectedOutcome: workflow.expectedOutcome, - agentType: 'wise-rah', - supabaseToken: null, - }); + const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - // Fire-and-forget execution so main ra-h stays responsive while workflow runs - void WorkflowExecutor.execute({ - sessionId: delegation.sessionId, - task, - context: contextLines, - expectedOutcome: workflow.expectedOutcome, - traceId: requestContext.traceId, - parentChatId: requestContext.parentChatId, - workflowKey, - workflowNodeId: nodeId, - }).catch((error) => { - console.error('[executeWorkflowTool] Wise ra-h delegation failed', error); - }); + 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 }); + RequestContext.set({ workflowKey: undefined, workflowNodeId: undefined }); - const shortSessionId = delegation.sessionId.split('_').pop(); - const workflowLabel = workflow.displayName || workflowKey; - return [ - `Delegated **${workflowLabel}** to wise ra-h (session ${shortSessionId}).`, - 'Keep working hereβ€”wise ra-h will stream every step inside its tab and post the final summary when complete.', - ].join('\n'); + 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}` }; + } }, });