feat(rah-light): remove agent delegation system

- Delete src/services/agents/delegation.ts (AgentDelegationService)
- Delete app/api/rah/delegations/ directory (all routes)
- Delete src/components/agents/DelegationIndicator.tsx
- Update workflowExecutor.ts to work without delegation streaming
- Update executeWorkflow.ts tool to execute workflows directly
- Update quickAdd.ts to execute directly without delegation tracking
- Add stub AgentDelegation types to UI components for compatibility
- Add stub voice hooks to RAHChat.tsx (will be removed in story 2)

Files deleted:
- src/services/agents/delegation.ts
- app/api/rah/delegations/route.ts
- app/api/rah/delegations/stream/route.ts
- app/api/rah/delegations/[sessionId]/route.ts
- app/api/rah/delegations/[sessionId]/summary/route.ts
- src/components/agents/DelegationIndicator.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-29 15:11:15 +11:00
co-authored by Claude Opus 4.5
parent 08012a8e5a
commit 6b7bb5a5f9
19 changed files with 234 additions and 819 deletions
@@ -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 });
}
}
@@ -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 });
}
}
-20
View File
@@ -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 });
}
}
-179
View File
@@ -1,179 +0,0 @@
import { NextRequest } from 'next/server';
export const runtime = 'nodejs';
export const maxDuration = 900;
class DelegationStreamBroadcaster {
private connections = new Map<string, Set<ReadableStreamDefaultController>>();
private pendingMessages = new Map<string, any[]>();
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',
},
});
}
+28 -28
View File
@@ -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(