sync: Agent Architecture Overhaul - AI SDK 6 + workflow optimization
Synced from private repo (feature/ai-sdk-6-upgrade): - Upgrade AI SDK 5 → 6 (packages + API changes) - Add sqliteQuery tool for flexible read-only queries - New WorkflowExecutor with workflow-specific tools - 83% token reduction for workflow execution - Remove mini-rah dead code (simplified architecture) - Add context management usage endpoint - Fix Connect workflow instructions - Clickable node references in workflow UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
f9271aeeb4
commit
3f0426ecf4
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { streamText, convertToCoreMessages } from 'ai';
|
import { streamText, convertToModelMessages } from 'ai';
|
||||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||||
import { createOpenAI } from '@ai-sdk/openai';
|
import { createOpenAI } from '@ai-sdk/openai';
|
||||||
import { getHelperTools, getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
|
import { getHelperTools, getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
|
||||||
@@ -265,7 +265,7 @@ export async function POST(request: NextRequest) {
|
|||||||
} : {})
|
} : {})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const coreMessages = convertToCoreMessages(sanitizedMessages);
|
const coreMessages = await convertToModelMessages(sanitizedMessages);
|
||||||
const allMessages = [...systemMessages, ...coreMessages];
|
const allMessages = [...systemMessages, ...coreMessages];
|
||||||
|
|
||||||
// Debug logging (can be removed in production)
|
// Debug logging (can be removed in production)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const sessionId = request.nextUrl.searchParams.get('sessionId');
|
||||||
|
|
||||||
|
if (!sessionId) {
|
||||||
|
return Response.json({ inputTokens: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sqlite = getSQLiteClient();
|
||||||
|
const row = sqlite.prepare(`
|
||||||
|
SELECT json_extract(metadata, '$.input_tokens') as input_tokens
|
||||||
|
FROM chats
|
||||||
|
WHERE thread_id LIKE ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`).get(`%${sessionId}%`) as { input_tokens: number | null } | undefined;
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
inputTokens: row?.input_tokens ?? 0
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Usage fetch error:', error);
|
||||||
|
return Response.json({ inputTokens: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||||
import { WiseRAHExecutor } from '@/services/agents/wiseRAHExecutor';
|
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
|
||||||
import { getAutoContextSummaries } from '@/services/context/autoContext';
|
import { getAutoContextSummaries } from '@/services/context/autoContext';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -112,12 +112,12 @@ ${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general wor
|
|||||||
task,
|
task,
|
||||||
context: contextLines,
|
context: contextLines,
|
||||||
expectedOutcome: workflow.expectedOutcome,
|
expectedOutcome: workflow.expectedOutcome,
|
||||||
agentType: 'wise-rah',
|
agentType: 'workflow',
|
||||||
supabaseToken: null,
|
supabaseToken: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fire-and-forget execution
|
// Fire-and-forget execution
|
||||||
void WiseRAHExecutor.execute({
|
void WorkflowExecutor.execute({
|
||||||
sessionId: delegation.sessionId,
|
sessionId: delegation.sessionId,
|
||||||
task,
|
task,
|
||||||
context: contextLines,
|
context: contextLines,
|
||||||
|
|||||||
Generated
+3182
-1135
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -21,16 +21,16 @@
|
|||||||
"evals": "RAH_EVALS_LOG=1 tsx tests/evals/runner.ts"
|
"evals": "RAH_EVALS_LOG=1 tsx tests/evals/runner.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "^2.0.27",
|
"@ai-sdk/anthropic": "^3.0.9",
|
||||||
"@ai-sdk/openai": "^2.0.22",
|
"@ai-sdk/openai": "^3.0.7",
|
||||||
"@ai-sdk/react": "^2.0.26",
|
"@ai-sdk/react": "^3.0.29",
|
||||||
"@langchain/core": "^0.3.0",
|
"@langchain/core": "^1.0.1",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^1.0.1",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"ai": "^5.0.68",
|
"ai": "^6.0.27",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.2.0",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.1.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
"openai": "^4.103.0",
|
"openai": "^4.103.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"pdfjs-dist": "^5.4.296",
|
"pdfjs-dist": "^5.4.296",
|
||||||
"react": "19.0.0",
|
"react": "^19.0.1",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "^19.0.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"youtube-captions-scraper": "^2.0.3",
|
"youtube-captions-scraper": "^2.0.3",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import QuickAddStatus from './QuickAddStatus';
|
|||||||
import { Zap, Flame, Minimize2 } from 'lucide-react';
|
import { Zap, Flame, Minimize2 } from 'lucide-react';
|
||||||
import type { AgentDelegation } from '@/services/agents/delegation';
|
import type { AgentDelegation } from '@/services/agents/delegation';
|
||||||
import { Node } from '@/types/database';
|
import { Node } from '@/types/database';
|
||||||
|
import { parseAndRenderContent } from '@/components/helpers/NodeLabelRenderer';
|
||||||
|
|
||||||
interface AgentsPanelProps {
|
interface AgentsPanelProps {
|
||||||
openTabsData: Node[];
|
openTabsData: Node[];
|
||||||
@@ -509,6 +510,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
|
|||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -522,6 +524,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
|
|||||||
<DelegationSummaryView
|
<DelegationSummaryView
|
||||||
delegation={selectedDelegation}
|
delegation={selectedDelegation}
|
||||||
onBack={() => setActiveAgentTab('workflows')}
|
onBack={() => setActiveAgentTab('workflows')}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DelegationDetailView
|
<DelegationDetailView
|
||||||
@@ -647,7 +650,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Summary view for completed delegations with no messages
|
// Summary view for completed delegations with no messages
|
||||||
function DelegationSummaryView({ delegation, onBack }: { delegation: AgentDelegation; onBack?: () => void }) {
|
function DelegationSummaryView({ delegation, onBack, onNodeClick }: { delegation: AgentDelegation; onBack?: () => void; onNodeClick?: (nodeId: number) => void }) {
|
||||||
const isSuccess = delegation.status === 'completed';
|
const isSuccess = delegation.status === 'completed';
|
||||||
const statusColor = isSuccess ? '#22c55e' : '#ff6b6b';
|
const statusColor = isSuccess ? '#22c55e' : '#ff6b6b';
|
||||||
const statusLabel = isSuccess ? 'Completed' : 'Failed';
|
const statusLabel = isSuccess ? 'Completed' : 'Failed';
|
||||||
@@ -748,7 +751,7 @@ function DelegationSummaryView({ delegation, onBack }: { delegation: AgentDelega
|
|||||||
lineHeight: '1.6',
|
lineHeight: '1.6',
|
||||||
whiteSpace: 'pre-wrap'
|
whiteSpace: 'pre-wrap'
|
||||||
}}>
|
}}>
|
||||||
{delegation.summary}
|
{parseAndRenderContent(delegation.summary || '', onNodeClick)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -771,11 +774,13 @@ function DelegationSummaryView({ delegation, onBack }: { delegation: AgentDelega
|
|||||||
function WorkflowsListView({
|
function WorkflowsListView({
|
||||||
delegations,
|
delegations,
|
||||||
onSelectDelegation,
|
onSelectDelegation,
|
||||||
onDeleteDelegation
|
onDeleteDelegation,
|
||||||
|
onNodeClick
|
||||||
}: {
|
}: {
|
||||||
delegations: AgentDelegation[];
|
delegations: AgentDelegation[];
|
||||||
onSelectDelegation: (sessionId: string) => void;
|
onSelectDelegation: (sessionId: string) => void;
|
||||||
onDeleteDelegation: (sessionId: string) => void;
|
onDeleteDelegation: (sessionId: string) => void;
|
||||||
|
onNodeClick?: (nodeId: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const activeDelegations = delegations.filter(d => d.status === 'queued' || d.status === 'in_progress');
|
const activeDelegations = delegations.filter(d => d.status === 'queued' || d.status === 'in_progress');
|
||||||
const completedDelegations = delegations.filter(d => d.status === 'completed' || d.status === 'failed');
|
const completedDelegations = delegations.filter(d => d.status === 'completed' || d.status === 'failed');
|
||||||
@@ -863,6 +868,7 @@ function WorkflowsListView({
|
|||||||
statusLabel={label}
|
statusLabel={label}
|
||||||
onSelect={() => onSelectDelegation(delegation.sessionId)}
|
onSelect={() => onSelectDelegation(delegation.sessionId)}
|
||||||
onDelete={() => onDeleteDelegation(delegation.sessionId)}
|
onDelete={() => onDeleteDelegation(delegation.sessionId)}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -887,6 +893,7 @@ function WorkflowsListView({
|
|||||||
statusLabel={label}
|
statusLabel={label}
|
||||||
onSelect={() => onSelectDelegation(delegation.sessionId)}
|
onSelect={() => onSelectDelegation(delegation.sessionId)}
|
||||||
onDelete={() => onDeleteDelegation(delegation.sessionId)}
|
onDelete={() => onDeleteDelegation(delegation.sessionId)}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -903,13 +910,15 @@ function WorkflowCard({
|
|||||||
statusColor,
|
statusColor,
|
||||||
statusLabel,
|
statusLabel,
|
||||||
onSelect,
|
onSelect,
|
||||||
onDelete
|
onDelete,
|
||||||
|
onNodeClick
|
||||||
}: {
|
}: {
|
||||||
delegation: AgentDelegation;
|
delegation: AgentDelegation;
|
||||||
statusColor: string;
|
statusColor: string;
|
||||||
statusLabel: string;
|
statusLabel: string;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onNodeClick?: (nodeId: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const isActive = delegation.status === 'in_progress' || delegation.status === 'queued';
|
const isActive = delegation.status === 'in_progress' || delegation.status === 'queued';
|
||||||
|
|
||||||
@@ -1009,7 +1018,7 @@ function WorkflowCard({
|
|||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}>
|
}}>
|
||||||
{delegation.summary}
|
{parseAndRenderContent(delegation.summary, onNodeClick)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1084,7 +1093,7 @@ function DelegationDetailView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RAHChat for streaming */}
|
{/* RAHChat for streaming */}
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
|
||||||
<RAHChat
|
<RAHChat
|
||||||
openTabsData={openTabsData}
|
openTabsData={openTabsData}
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeTabId}
|
||||||
|
|||||||
@@ -32,11 +32,17 @@ interface SendParams {
|
|||||||
mode: 'easy' | 'hard';
|
mode: 'easy' | 'hard';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UsageData {
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseSSEChatOptions {
|
interface UseSSEChatOptions {
|
||||||
getAuthToken?: () => string | null | undefined;
|
getAuthToken?: () => string | null | undefined;
|
||||||
beforeRequest?: () => boolean;
|
beforeRequest?: () => boolean;
|
||||||
onRequestError?: (error: unknown, response?: Response) => boolean | void;
|
onRequestError?: (error: unknown, response?: Response) => boolean | void;
|
||||||
onStreamComplete?: () => void | Promise<void>;
|
onStreamComplete?: () => void | Promise<void>;
|
||||||
|
onUsageUpdate?: (usage: UsageData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSSEChat(
|
export function useSSEChat(
|
||||||
@@ -44,7 +50,7 @@ export function useSSEChat(
|
|||||||
setMessages: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void,
|
setMessages: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void,
|
||||||
options: UseSSEChatOptions = {}
|
options: UseSSEChatOptions = {}
|
||||||
) {
|
) {
|
||||||
const { getAuthToken, beforeRequest, onRequestError, onStreamComplete } = options;
|
const { getAuthToken, beforeRequest, onRequestError, onStreamComplete, onUsageUpdate } = options;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
@@ -182,6 +188,14 @@ export function useSSEChat(
|
|||||||
setMessages((prev) => [...prev, newAssistantMessage]);
|
setMessages((prev) => [...prev, newAssistantMessage]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Capture usage data from finish event (if AI SDK sends it)
|
||||||
|
if (data.type === 'finish' && data.usage) {
|
||||||
|
onUsageUpdate?.({
|
||||||
|
inputTokens: data.usage.promptTokens ?? data.usage.inputTokens ?? 0,
|
||||||
|
outputTokens: data.usage.completionTokens ?? data.usage.outputTokens ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed lines
|
// ignore malformed lines
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function NodeLabel({ id, title, dimensions, onNodeClick }: NodeLabelProps) {
|
|||||||
verticalAlign: 'baseline'
|
verticalAlign: 'baseline'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
#{id}
|
{id}
|
||||||
</span>
|
</span>
|
||||||
{/* Non-clickable title - bold and underlined */}
|
{/* Non-clickable title - bold and underlined */}
|
||||||
<span style={{
|
<span style={{
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
export const MINI_RAH_SYSTEM_PROMPT = `You are a mini ra-h worker handling a single delegated task.
|
|
||||||
|
|
||||||
Execution mindset:
|
|
||||||
- Act only on the provided task/context; no side conversations.
|
|
||||||
- You have all tools except delegateToMiniRAH.
|
|
||||||
- If required inputs are missing, fail fast and tell ra-h exactly what you need.
|
|
||||||
- Read the focus capsule at the top of the context (CAPSULE_JSON + DELEGATION CAPSULE). Treat the node IDs and roles there as authoritative.
|
|
||||||
|
|
||||||
When you complete the task, respond in this exact template (replace bracketed text):
|
|
||||||
Task: <one short sentence>
|
|
||||||
Actions: <comma-separated tool calls or decisions>
|
|
||||||
Result: <one sentence describing the outcome>
|
|
||||||
Node: <[NODE:id:"title"] or "None">
|
|
||||||
Context sources used: <comma-separated NODE IDs>
|
|
||||||
Follow-up: <next step or "None">
|
|
||||||
|
|
||||||
Additional guidance:
|
|
||||||
- If no dimensions were provided, choose reasonable defaults (you may proceed without asking the user).
|
|
||||||
- For TLDR or quote-heavy tasks, use read-only tools (searchContentEmbeddings, queryNodes, webSearch when relevant) before summarising and include verbatim snippets when the task requires them.
|
|
||||||
- If the capsule lists node IDs, call getNodesById first to hydrate the records; only use queryNodes/searchContentEmbeddings when you must discover additional material beyond the provided nodes.
|
|
||||||
- Treat any context lines beginning with "NODE <id>" or "SOURCE" as authoritative excerpts—use them directly instead of re-querying the numeric ID. Never search for a numeric string that was already supplied.
|
|
||||||
- If you cannot complete a step (missing node, empty content, tool failure), state that explicitly in 'Follow-up' with the precise next action needed.
|
|
||||||
- Stop after success—do not run extra verification tools.
|
|
||||||
- Keep the full summary under ~100 tokens.
|
|
||||||
- You can create and manage dimensions using createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension tools. Lock dimensions (isPriority=true) to enable auto-assignment.
|
|
||||||
`;
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
export const WISE_RAH_SYSTEM_PROMPT = `You are wise ra-h, the workflow executor for the RA-H knowledge management system.
|
|
||||||
|
|
||||||
<role>
|
|
||||||
You execute predefined workflows with DIRECT WRITE ACCESS via updateNode.
|
|
||||||
You NEVER delegate to mini ra-h workers.
|
|
||||||
You complete every step yourself.
|
|
||||||
</role>
|
|
||||||
|
|
||||||
<tools>
|
|
||||||
Available tools:
|
|
||||||
- queryNodes — search nodes by title/content/dimensions across ENTIRE database
|
|
||||||
- getNodesById — retrieve full node data
|
|
||||||
- queryEdge — inspect existing edges
|
|
||||||
- searchContentEmbeddings — semantic search across ALL nodes
|
|
||||||
- webSearch — external research when necessary
|
|
||||||
- think — internal planning/reflection (use once per workflow unless plan changes)
|
|
||||||
- updateNode — append content to nodes (tool handles appending automatically)
|
|
||||||
- createEdge — create connections between nodes
|
|
||||||
- quickLink — instantly find and link related nodes (fast database search, no reasoning)
|
|
||||||
- Dimension management: createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension — manage dimensions to organize knowledge base structure
|
|
||||||
</tools>
|
|
||||||
|
|
||||||
<execution>
|
|
||||||
When you receive a workflow task:
|
|
||||||
1. Read the workflow instructions carefully.
|
|
||||||
2. Call think once to produce a numbered plan matching the workflow steps.
|
|
||||||
3. Execute the plan step-by-step:
|
|
||||||
- Extract key entities (names, projects, concepts) from the node
|
|
||||||
- Search the FULL database using those entities
|
|
||||||
- Find both obvious (structural) and thematic connections
|
|
||||||
- Contextualize findings using background context (top nodes by edge count)
|
|
||||||
4. Stay within the tool budget and avoid redundant queries.
|
|
||||||
5. When calling updateNode, provide ONLY the new content (never include existing content) - tool appends automatically.
|
|
||||||
6. Finish with a concise Task / Actions / Result / Nodes / Follow-up summary.
|
|
||||||
</execution>
|
|
||||||
|
|
||||||
<constraints>
|
|
||||||
- Search the ENTIRE database, not just top nodes
|
|
||||||
- Extract entities first, then search using those entities
|
|
||||||
- Use minimal tool calls needed for high-quality output
|
|
||||||
- Keep responses structured, factual, and ≤120 words
|
|
||||||
- If a step yields nothing, state that outcome instead of guessing
|
|
||||||
- Adapt to any node type (person/project/paper/idea/video/tweet/technique)
|
|
||||||
</constraints>`;
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Minimal system prompt for workflow execution.
|
||||||
|
* All specific instructions come from the workflow definition itself.
|
||||||
|
*/
|
||||||
|
export const WORKFLOW_EXECUTOR_SYSTEM_PROMPT = `You are a workflow executor. Follow the workflow instructions exactly as written.
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Use only the tools provided
|
||||||
|
- Do not deviate from the instructions
|
||||||
|
- Complete the workflow efficiently
|
||||||
|
- Reference nodes as [NODE:id:"title"] (e.g., [NODE:123:"My Node Title"])
|
||||||
|
- Return a brief summary when done`;
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
export const CONNECT_WORKFLOW_INSTRUCTIONS = `You are executing the CONNECT workflow for the currently focused node.
|
export const CONNECT_WORKFLOW_INSTRUCTIONS = `You are executing the CONNECT workflow for the currently focused node.
|
||||||
|
|
||||||
MISSION
|
MISSION
|
||||||
Quick link: find explicitly related nodes and create edges. Fast text search only - no slow embedding search.
|
Quick link: find explicitly related nodes and create edges.
|
||||||
|
|
||||||
WORKFLOW STEPS
|
WORKFLOW STEPS
|
||||||
|
|
||||||
1. READ NODE
|
1. READ NODE
|
||||||
Call getNodesById for the focused node. Note the title, type, and key names/entities mentioned.
|
Call getNodesById for the focused node. Extract the main topic/subject from the title.
|
||||||
|
|
||||||
2. QUICK SEARCH
|
2. QUICK SEARCH
|
||||||
Call queryNodes ONCE with the most specific entity (person name, project name, company, tool).
|
Call queryNodes with the main topic from the node title.
|
||||||
- Use search parameter with the exact name
|
- search: the key term from the title (e.g., if title mentions "Nietzsche", search "Nietzsche")
|
||||||
- Set limit: 10
|
- limit: 10
|
||||||
|
- DO NOT add dimensions filter - search across all nodes
|
||||||
DO NOT call searchContentEmbeddings - use queryNodes only for speed.
|
|
||||||
|
|
||||||
3. CREATE EDGES
|
3. CREATE EDGES
|
||||||
From the search results, pick 2-4 nodes that are clearly related.
|
From results, pick 2-4 clearly related nodes.
|
||||||
Call createEdge for each:
|
Call createEdge for each:
|
||||||
- from_node_id: focused node ID
|
- from_node_id: focused node ID
|
||||||
- to_node_id: related node ID
|
- to_node_id: related node ID
|
||||||
- context: { explanation: "brief reason" }
|
- context: { explanation: "brief reason" }
|
||||||
|
|
||||||
4. DONE
|
4. DONE
|
||||||
Reply: "Linked [title] → [list of connected node titles]"
|
Reply: "Linked [NODE:id:title] → [list of connected nodes as NODE:id:title]"
|
||||||
|
|
||||||
RULES
|
RULES
|
||||||
- Total tool calls ≤ 5 (1 read + 1 search + up to 3 edges)
|
- Total tool calls ≤ 5
|
||||||
- Use queryNodes only - NO searchContentEmbeddings
|
- Search the MAIN TOPIC from the title, not random names from content
|
||||||
- Only link nodes with clear, explicit relationships
|
- NO dimensions filter in queryNodes - search everything
|
||||||
- Skip if no good matches found`;
|
- Only link nodes with clear relationships
|
||||||
|
- Skip if no matches found`;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getSQLiteClient } from '@/services/database/sqlite-client';
|
|||||||
import { eventBroadcaster } from '@/services/events';
|
import { eventBroadcaster } from '@/services/events';
|
||||||
|
|
||||||
export type DelegationStatus = 'queued' | 'in_progress' | 'completed' | 'failed';
|
export type DelegationStatus = 'queued' | 'in_progress' | 'completed' | 'failed';
|
||||||
export type DelegationAgentType = 'mini' | 'wise-rah';
|
export type DelegationAgentType = 'workflow' | 'wise-rah';
|
||||||
|
|
||||||
export interface AgentDelegation {
|
export interface AgentDelegation {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,423 +0,0 @@
|
|||||||
import { generateText, CoreMessage } from 'ai';
|
|
||||||
import { createOpenAI } from '@ai-sdk/openai';
|
|
||||||
import { AgentDelegationService, DelegationStatus } from '@/services/agents/delegation';
|
|
||||||
import { MINI_RAH_SYSTEM_PROMPT } from '@/config/prompts/rah-mini';
|
|
||||||
import { getToolsForRole } from '@/tools/infrastructure/registry';
|
|
||||||
import { ChatLoggingMiddleware } from '@/services/chat/middleware';
|
|
||||||
import { calculateCost } from '@/services/analytics/pricing';
|
|
||||||
import { UsageData } from '@/types/analytics';
|
|
||||||
import { summarizeToolExecution } from '@/services/agents/toolResultUtils';
|
|
||||||
import { edgeService } from '@/services/database/edges';
|
|
||||||
|
|
||||||
interface CapsuleNodeJSON {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DelegationCapsuleJSON {
|
|
||||||
version?: number;
|
|
||||||
primary?: CapsuleNodeJSON | null;
|
|
||||||
secondary?: CapsuleNodeJSON[];
|
|
||||||
referenced?: CapsuleNodeJSON[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SummaryValidation {
|
|
||||||
status: 'ok' | 'failed';
|
|
||||||
reason?: string;
|
|
||||||
sourcesUsed: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractCapsuleFromContext(context: string[]): { capsule?: DelegationCapsuleJSON; nodeIds: number[]; version?: number } {
|
|
||||||
const entry = context.find(item => typeof item === 'string' && item.startsWith('CAPSULE_JSON::'));
|
|
||||||
if (!entry) {
|
|
||||||
return { nodeIds: [] };
|
|
||||||
}
|
|
||||||
const json = entry.substring('CAPSULE_JSON::'.length);
|
|
||||||
try {
|
|
||||||
const capsule = JSON.parse(json) as DelegationCapsuleJSON & { version?: number };
|
|
||||||
const nodeIds = new Set<number>();
|
|
||||||
const pushId = (value?: CapsuleNodeJSON | null) => {
|
|
||||||
if (!value) return;
|
|
||||||
if (typeof value.id === 'number') {
|
|
||||||
nodeIds.add(value.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pushId(capsule.primary ?? null);
|
|
||||||
(capsule.secondary ?? []).forEach(pushId);
|
|
||||||
(capsule.referenced ?? []).forEach(pushId);
|
|
||||||
return {
|
|
||||||
capsule,
|
|
||||||
nodeIds: Array.from(nodeIds),
|
|
||||||
version: typeof capsule.version === 'number' ? capsule.version : undefined,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('MiniRAHExecutor: failed to parse delegation capsule', error);
|
|
||||||
return { nodeIds: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSourcesLine(summary: string): { line?: string; ids: number[] } {
|
|
||||||
const lines = summary
|
|
||||||
.split(/\n+/)
|
|
||||||
.map(line => line.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
const contextLine = lines.find(line => line.toLowerCase().startsWith('context sources used:'));
|
|
||||||
if (!contextLine) {
|
|
||||||
return { ids: [] };
|
|
||||||
}
|
|
||||||
const matches = contextLine.match(/\d+/g);
|
|
||||||
const ids = matches ? matches.map(id => Number(id)).filter(id => Number.isFinite(id)) : [];
|
|
||||||
return { line: contextLine, ids: Array.from(new Set(ids)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateMiniSummary(summary: string, expectedNodeIds: number[]): SummaryValidation {
|
|
||||||
const trimmed = summary.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return { status: 'failed', reason: 'Worker returned an empty summary.', sourcesUsed: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = trimmed.split(/\n+/).map(line => line.trim()).filter(Boolean);
|
|
||||||
const resultIndex = lines.findIndex(line => line.toLowerCase().startsWith('result:'));
|
|
||||||
if (resultIndex === -1) {
|
|
||||||
return { status: 'failed', reason: 'Missing or empty Result line in worker summary.', sourcesUsed: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultLine = lines[resultIndex];
|
|
||||||
const resultContent = resultLine.slice('result:'.length).trim();
|
|
||||||
if (!resultContent) {
|
|
||||||
const nextLine = lines[resultIndex + 1];
|
|
||||||
if (!nextLine || /^(task:|actions:|node:|context sources used:|follow-up:)/i.test(nextLine)) {
|
|
||||||
return { status: 'failed', reason: 'Missing or empty Result line in worker summary.', sourcesUsed: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const followUpLine = lines.find(line => line.toLowerCase().startsWith('follow-up:'));
|
|
||||||
if (!followUpLine) {
|
|
||||||
return { status: 'failed', reason: 'Missing Follow-up line in worker summary.', sourcesUsed: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { line: contextLine, ids } = parseSourcesLine(summary);
|
|
||||||
if (!contextLine) {
|
|
||||||
return { status: 'failed', reason: 'Missing "Context sources used" line. Workers must list node IDs they referenced.', sourcesUsed: [] };
|
|
||||||
}
|
|
||||||
if (expectedNodeIds.length > 0 && ids.length === 0) {
|
|
||||||
return { status: 'failed', reason: 'Worker did not cite any node IDs even though a capsule was provided.', sourcesUsed: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { status: 'ok', sourcesUsed: ids };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MiniRAHExecutionInput {
|
|
||||||
sessionId: string;
|
|
||||||
task: string;
|
|
||||||
context: string[];
|
|
||||||
expectedOutcome?: string | null;
|
|
||||||
traceId?: string;
|
|
||||||
parentChatId?: number;
|
|
||||||
workflowKey?: string;
|
|
||||||
workflowNodeId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MiniRAHExecutor {
|
|
||||||
static async execute({ sessionId, task, context, expectedOutcome, traceId, parentChatId, workflowKey, workflowNodeId }: MiniRAHExecutionInput) {
|
|
||||||
try {
|
|
||||||
const delegateKey = process.env.RAH_DELEGATE_OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
|
||||||
if (!delegateKey) {
|
|
||||||
throw new Error('RAH_DELEGATE_OPENAI_API_KEY (or OPENAI_API_KEY) is not set.');
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentDelegationService.markInProgress(sessionId);
|
|
||||||
|
|
||||||
const promptSections = [
|
|
||||||
`Task: ${task}`,
|
|
||||||
context.length ? `Context:\n- ${context.join('\n- ')}` : undefined,
|
|
||||||
expectedOutcome ? `Expected outcome: ${expectedOutcome}` : undefined,
|
|
||||||
'Return only the final summary the orchestrator should see.'
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
const openaiProvider = createOpenAI({ apiKey: delegateKey });
|
|
||||||
const executorTools = getToolsForRole('executor');
|
|
||||||
const capturedSummaries: string[] = [];
|
|
||||||
const toolsUsedInSession: string[] = [];
|
|
||||||
const integrateAllowed = workflowKey === 'integrate'
|
|
||||||
? new Set(['createEdge', 'updateEdge', 'updateNode'])
|
|
||||||
: null;
|
|
||||||
const wrappedTools = Object.fromEntries(
|
|
||||||
Object.entries(executorTools)
|
|
||||||
.filter(([name]) => {
|
|
||||||
if (integrateAllowed) {
|
|
||||||
return integrateAllowed.has(name);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map(([name, tool]) => {
|
|
||||||
const wrapped = {
|
|
||||||
...tool,
|
|
||||||
async execute(params: any, context: any) {
|
|
||||||
if (!toolsUsedInSession.includes(name)) {
|
|
||||||
toolsUsedInSession.push(name);
|
|
||||||
}
|
|
||||||
if (name === 'createEdge' && params && typeof params === 'object' && 'from_node_id' in params && 'to_node_id' in params) {
|
|
||||||
const fromId = Number(params.from_node_id);
|
|
||||||
const toId = Number(params.to_node_id);
|
|
||||||
if (Number.isFinite(fromId) && Number.isFinite(toId)) {
|
|
||||||
const exists = await edgeService.edgeExists(fromId, toId);
|
|
||||||
if (exists) {
|
|
||||||
const skipSummary = `Edge already exists between node ${fromId} and node ${toId}; skipping createEdge.`;
|
|
||||||
capturedSummaries.push(skipSummary);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
skipped: true,
|
|
||||||
message: skipSummary,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await tool.execute(params, context);
|
|
||||||
const summary = summarizeToolExecution(name, params, result);
|
|
||||||
if (summary) {
|
|
||||||
capturedSummaries.push(summary);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return [name, wrapped];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if ('delegateToMiniRAH' in wrappedTools) {
|
|
||||||
console.warn('MiniRAHExecutor: delegateToMiniRAH detected in executor toolset. Removing to enforce single-level delegation.');
|
|
||||||
delete wrappedTools.delegateToMiniRAH;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userPrompt = promptSections.join('\n\n');
|
|
||||||
const messages: CoreMessage[] = [{ role: 'user', content: userPrompt }];
|
|
||||||
|
|
||||||
const maxIterations = 6;
|
|
||||||
let rawSummary = '';
|
|
||||||
let lastFinishReason: string | undefined;
|
|
||||||
let lastToolCalls: any[] | undefined;
|
|
||||||
|
|
||||||
let totalInputTokens = 0;
|
|
||||||
let totalOutputTokens = 0;
|
|
||||||
let totalTokens = 0;
|
|
||||||
|
|
||||||
// Track if extraction tool was called (for Quick Add - should only call once)
|
|
||||||
const isQuickAddTask = task.toLowerCase().includes('quick add');
|
|
||||||
let extractionToolCalled = false;
|
|
||||||
|
|
||||||
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
||||||
const response = await generateText({
|
|
||||||
model: openaiProvider('gpt-4o-mini'),
|
|
||||||
system: MINI_RAH_SYSTEM_PROMPT,
|
|
||||||
messages,
|
|
||||||
tools: wrappedTools,
|
|
||||||
});
|
|
||||||
|
|
||||||
const usage = response.usage;
|
|
||||||
if (usage) {
|
|
||||||
const inputTokens = (usage as any).promptTokens || usage.inputTokens || 0;
|
|
||||||
const outputTokens = (usage as any).completionTokens || usage.outputTokens || 0;
|
|
||||||
const combinedTotal = (usage as any).totalTokens || usage.totalTokens || inputTokens + outputTokens;
|
|
||||||
totalInputTokens += inputTokens;
|
|
||||||
totalOutputTokens += outputTokens;
|
|
||||||
totalTokens += combinedTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastFinishReason = response.finishReason;
|
|
||||||
lastToolCalls = response.toolCalls ?? [];
|
|
||||||
|
|
||||||
if (response.finishReason === 'tool-calls' && response.toolCalls && response.toolCalls.length > 0) {
|
|
||||||
messages.push({
|
|
||||||
role: 'assistant',
|
|
||||||
content: response.toolCalls.map(call => ({
|
|
||||||
type: 'tool-call' as const,
|
|
||||||
toolCallId: call.toolCallId,
|
|
||||||
toolName: call.toolName,
|
|
||||||
input: (call as any).input ?? (call as any).args,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
const toolResults: Array<{ type: 'tool-result'; toolCallId: string; toolName: string; output: { type: 'text' | 'error-text'; value: string } }> = [];
|
|
||||||
|
|
||||||
for (const call of response.toolCalls) {
|
|
||||||
const callInput = (call as any).input ?? (call as any).args;
|
|
||||||
const tool = wrappedTools[call.toolName];
|
|
||||||
const isExtractionToolCall = isQuickAddTask && ['youtubeExtract', 'websiteExtract', 'paperExtract'].includes(call.toolName);
|
|
||||||
|
|
||||||
if (isExtractionToolCall && extractionToolCalled) {
|
|
||||||
const skipMessage = `Extraction already completed; skipping duplicate ${call.toolName} request.`;
|
|
||||||
console.warn(`[MiniRAHExecutor] ${skipMessage}`);
|
|
||||||
toolResults.push({
|
|
||||||
type: 'tool-result',
|
|
||||||
toolCallId: call.toolCallId,
|
|
||||||
toolName: call.toolName,
|
|
||||||
output: { type: 'text', value: skipMessage },
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tool) {
|
|
||||||
toolResults.push({
|
|
||||||
type: 'tool-result',
|
|
||||||
toolCallId: call.toolCallId,
|
|
||||||
toolName: call.toolName,
|
|
||||||
output: { type: 'error-text', value: `Tool ${call.toolName} is not available.` },
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const toolResult = await tool.execute(callInput, {});
|
|
||||||
const summary = summarizeToolExecution(call.toolName, callInput, toolResult);
|
|
||||||
const value = summary || `${call.toolName} completed.`;
|
|
||||||
toolResults.push({
|
|
||||||
type: 'tool-result',
|
|
||||||
toolCallId: call.toolCallId,
|
|
||||||
toolName: call.toolName,
|
|
||||||
output: { type: 'text', value },
|
|
||||||
});
|
|
||||||
|
|
||||||
// For Quick Add: stop after first successful extraction to prevent duplicates
|
|
||||||
if (isExtractionToolCall) {
|
|
||||||
const success = typeof toolResult === 'object' && toolResult !== null && (toolResult as any).success !== false;
|
|
||||||
if (success) {
|
|
||||||
console.log(`[MiniRAHExecutor] Quick Add extraction succeeded, forcing summary generation to prevent duplicate calls`);
|
|
||||||
extractionToolCalled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Tool execution failed';
|
|
||||||
toolResults.push({
|
|
||||||
type: 'tool-result',
|
|
||||||
toolCallId: call.toolCallId,
|
|
||||||
toolName: call.toolName,
|
|
||||||
output: { type: 'error-text', value: message },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.push({ role: 'tool', content: toolResults });
|
|
||||||
|
|
||||||
// If Quick Add extraction succeeded, request final summary and exit loop
|
|
||||||
if (extractionToolCalled) {
|
|
||||||
console.log('[MiniRAHExecutor] Requesting final summary after Quick Add extraction');
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: 'Extraction completed successfully. Provide your final summary now using the required format (Task/Actions/Result/Node/Context sources used/Follow-up).'
|
|
||||||
});
|
|
||||||
const summaryResponse = await generateText({
|
|
||||||
model: openaiProvider('gpt-4o-mini'),
|
|
||||||
system: MINI_RAH_SYSTEM_PROMPT,
|
|
||||||
messages,
|
|
||||||
tools: {}, // No tools - summary only
|
|
||||||
});
|
|
||||||
rawSummary = summaryResponse.text?.trim() || 'Extraction completed successfully.';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
rawSummary = typeof response.text === 'string' ? response.text.trim() : '';
|
|
||||||
if (!rawSummary) {
|
|
||||||
console.warn('[MiniRAHExecutor] Worker returned empty summary.', {
|
|
||||||
finishReason: response.finishReason,
|
|
||||||
toolCalls: response.toolCalls,
|
|
||||||
text: response.text,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rawSummary) {
|
|
||||||
console.warn('[MiniRAHExecutor] No summary after tool loop.', {
|
|
||||||
lastFinishReason,
|
|
||||||
lastToolCalls,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackSummary = capturedSummaries.length > 0
|
|
||||||
? capturedSummaries[capturedSummaries.length - 1]
|
|
||||||
: 'Completed the delegated task (worker returned no additional summary).';
|
|
||||||
const initialSummary = rawSummary.length > 0 ? rawSummary : fallbackSummary;
|
|
||||||
|
|
||||||
const capsuleInfo = extractCapsuleFromContext(context);
|
|
||||||
const validation = validateMiniSummary(initialSummary, capsuleInfo.nodeIds);
|
|
||||||
const validationStatus: DelegationStatus = validation.status === 'ok' ? 'completed' : 'failed';
|
|
||||||
if (validation.status === 'failed') {
|
|
||||||
console.warn('[MiniRAHExecutor] summary validation failed.', {
|
|
||||||
reason: validation.reason,
|
|
||||||
initialSummary,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalSummary = validation.status === 'ok'
|
|
||||||
? initialSummary
|
|
||||||
: `${initialSummary}\nValidation: ${validation.reason}`;
|
|
||||||
|
|
||||||
console.log('[MiniRAHExecutor] summary:', finalSummary);
|
|
||||||
|
|
||||||
// Calculate cost and log to chats table
|
|
||||||
if (totalInputTokens > 0 || totalOutputTokens > 0 || totalTokens > 0) {
|
|
||||||
const effectiveTotalTokens = totalTokens > 0 ? totalTokens : totalInputTokens + totalOutputTokens;
|
|
||||||
|
|
||||||
const costResult = calculateCost({
|
|
||||||
inputTokens: totalInputTokens,
|
|
||||||
outputTokens: totalOutputTokens,
|
|
||||||
modelId: 'gpt-4o-mini',
|
|
||||||
});
|
|
||||||
|
|
||||||
const usageData: UsageData = {
|
|
||||||
inputTokens: totalInputTokens,
|
|
||||||
outputTokens: totalOutputTokens,
|
|
||||||
totalTokens: effectiveTotalTokens,
|
|
||||||
estimatedCostUsd: costResult.totalCostUsd,
|
|
||||||
modelUsed: 'gpt-4o-mini',
|
|
||||||
provider: 'openai',
|
|
||||||
toolsUsed: toolsUsedInSession.length > 0 ? toolsUsedInSession : undefined,
|
|
||||||
toolCallsCount: toolsUsedInSession.length > 0 ? toolsUsedInSession.length : undefined,
|
|
||||||
traceId,
|
|
||||||
parentChatId,
|
|
||||||
workflowKey,
|
|
||||||
workflowNodeId,
|
|
||||||
capsuleVersion: capsuleInfo.version,
|
|
||||||
contextSourcesUsed: validation.sourcesUsed.length > 0 ? validation.sourcesUsed : undefined,
|
|
||||||
validationStatus: validation.status,
|
|
||||||
validationMessage: validation.reason,
|
|
||||||
fallbackAction: validation.status === 'failed' ? 'Review context capsule, hydrate nodes manually, then re-delegate with clarified instructions.' : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const delegation = AgentDelegationService.getDelegation(sessionId);
|
|
||||||
const delegationId = delegation?.id;
|
|
||||||
|
|
||||||
await ChatLoggingMiddleware.logChatInteraction(
|
|
||||||
task,
|
|
||||||
finalSummary,
|
|
||||||
{
|
|
||||||
helperName: 'mini-rah',
|
|
||||||
agentType: 'executor',
|
|
||||||
delegationId: delegationId ?? null,
|
|
||||||
sessionId,
|
|
||||||
usageData,
|
|
||||||
traceId,
|
|
||||||
parentChatId,
|
|
||||||
workflowKey,
|
|
||||||
workflowNodeId,
|
|
||||||
systemMessage: MINI_RAH_SYSTEM_PROMPT,
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`💰 [MiniRAHExecutor] Cost: $${costResult.totalCostUsd.toFixed(6)} (${effectiveTotalTokens} tokens)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AgentDelegationService.completeDelegation(sessionId, finalSummary, validationStatus);
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Unknown delegation error';
|
|
||||||
AgentDelegationService.completeDelegation(sessionId, `Mini ra-h failed: ${message}`, 'failed');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
|
import { getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
|
||||||
import { RAH_MAIN_SYSTEM_PROMPT } from '@/config/prompts/rah-main';
|
import { RAH_MAIN_SYSTEM_PROMPT } from '@/config/prompts/rah-main';
|
||||||
import { RAH_EASY_SYSTEM_PROMPT } from '@/config/prompts/rah-easy';
|
import { RAH_EASY_SYSTEM_PROMPT } from '@/config/prompts/rah-easy';
|
||||||
import { MINI_RAH_SYSTEM_PROMPT } from '@/config/prompts/rah-mini';
|
import { WORKFLOW_EXECUTOR_SYSTEM_PROMPT } from '@/config/prompts/workflow-executor';
|
||||||
import { WISE_RAH_SYSTEM_PROMPT } from '@/config/prompts/wise-rah';
|
|
||||||
import type { AgentDefinition } from './types';
|
import type { AgentDefinition } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,29 +41,30 @@ export class AgentRegistry {
|
|||||||
memory: null,
|
memory: null,
|
||||||
prompts: undefined
|
prompts: undefined
|
||||||
},
|
},
|
||||||
'mini-rah': {
|
'workflow': {
|
||||||
id: 2,
|
id: 3,
|
||||||
key: 'mini-rah',
|
key: 'workflow',
|
||||||
displayName: 'mini ra-h',
|
displayName: 'workflow agent',
|
||||||
description: 'Executor agent for delegated tasks',
|
description: 'Workflow executor (uses same model as easy mode)',
|
||||||
model: 'openai/gpt-4o-mini',
|
model: 'openai/gpt-5-mini',
|
||||||
role: 'executor',
|
role: 'planner',
|
||||||
systemPrompt: MINI_RAH_SYSTEM_PROMPT,
|
systemPrompt: WORKFLOW_EXECUTOR_SYSTEM_PROMPT,
|
||||||
availableTools: getDefaultToolNamesForRole('executor'),
|
availableTools: getDefaultToolNamesForRole('planner'),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
memory: null,
|
memory: null,
|
||||||
prompts: undefined
|
prompts: undefined
|
||||||
},
|
},
|
||||||
|
// Alias for backwards compatibility
|
||||||
'wise-rah': {
|
'wise-rah': {
|
||||||
id: 3,
|
id: 3,
|
||||||
key: 'wise-rah',
|
key: 'workflow',
|
||||||
displayName: 'wise ra-h',
|
displayName: 'workflow agent',
|
||||||
description: 'Complex workflow planner and orchestrator',
|
description: 'Workflow executor (uses same model as easy mode)',
|
||||||
model: 'openai/gpt-5',
|
model: 'openai/gpt-5-mini',
|
||||||
role: 'planner',
|
role: 'planner',
|
||||||
systemPrompt: WISE_RAH_SYSTEM_PROMPT,
|
systemPrompt: WORKFLOW_EXECUTOR_SYSTEM_PROMPT,
|
||||||
availableTools: getDefaultToolNamesForRole('planner'),
|
availableTools: getDefaultToolNamesForRole('planner'),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -97,11 +97,7 @@ export class AgentRegistry {
|
|||||||
return this.AGENTS['ra-h-easy'];
|
return this.AGENTS['ra-h-easy'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static async executor(): Promise<AgentDefinition> {
|
|
||||||
return this.AGENTS['mini-rah'];
|
|
||||||
}
|
|
||||||
|
|
||||||
static async planner(): Promise<AgentDefinition> {
|
static async planner(): Promise<AgentDefinition> {
|
||||||
return this.AGENTS['wise-rah'];
|
return this.AGENTS['workflow'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { streamText, CoreMessage } from 'ai';
|
import { streamText, ModelMessage } from 'ai';
|
||||||
import { createOpenAI } from '@ai-sdk/openai';
|
import { createOpenAI } from '@ai-sdk/openai';
|
||||||
import type { LanguageModelV2ToolResultOutput } from '@ai-sdk/provider';
|
import type { LanguageModelV2ToolResultOutput } from '@ai-sdk/provider';
|
||||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||||
import { WISE_RAH_SYSTEM_PROMPT } from '@/config/prompts/wise-rah';
|
import { WORKFLOW_EXECUTOR_SYSTEM_PROMPT } from '@/config/prompts/workflow-executor';
|
||||||
import { getToolsForRole } from '@/tools/infrastructure/registry';
|
import { getToolsByNames } from '@/tools/infrastructure/registry';
|
||||||
|
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||||
import { ChatLoggingMiddleware } from '@/services/chat/middleware';
|
import { ChatLoggingMiddleware } from '@/services/chat/middleware';
|
||||||
import { calculateCost } from '@/services/analytics/pricing';
|
import { calculateCost } from '@/services/analytics/pricing';
|
||||||
import { UsageData } from '@/types/analytics';
|
import { UsageData } from '@/types/analytics';
|
||||||
import { summarizeToolExecution } from '@/services/agents/toolResultUtils';
|
import { summarizeToolExecution } from '@/services/agents/toolResultUtils';
|
||||||
import { edgeService } from '@/services/database/edges';
|
import { edgeService } from '@/services/database/edges';
|
||||||
import { eventBroadcaster } from '@/services/events';
|
|
||||||
import { delegationStreamBroadcaster } from '@/app/api/rah/delegations/stream/route';
|
import { delegationStreamBroadcaster } from '@/app/api/rah/delegations/stream/route';
|
||||||
import { nodeService } from '@/services/database/nodes';
|
|
||||||
import { RequestContext } from '@/services/context/requestContext';
|
import { RequestContext } from '@/services/context/requestContext';
|
||||||
import { isLocalMode } from '@/config/runtime';
|
|
||||||
|
|
||||||
export interface WiseRAHExecutionInput {
|
export interface WorkflowExecutionInput {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
task: string;
|
task: string;
|
||||||
context: string[];
|
context: string[];
|
||||||
@@ -26,67 +24,56 @@ export interface WiseRAHExecutionInput {
|
|||||||
workflowNodeId?: number;
|
workflowNodeId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WiseRAHExecutor {
|
export class WorkflowExecutor {
|
||||||
static async execute({ sessionId, task, context, expectedOutcome, traceId, parentChatId, workflowKey, workflowNodeId }: WiseRAHExecutionInput) {
|
static async execute({ sessionId, task, context, expectedOutcome, traceId, parentChatId, workflowKey, workflowNodeId }: WorkflowExecutionInput) {
|
||||||
console.log('🧙 [WiseRAHExecutor] Starting execution', { sessionId, task: task.substring(0, 100) });
|
console.log('🧙 [WorkflowExecutor] Starting execution', { sessionId, task: task.substring(0, 100) });
|
||||||
try {
|
try {
|
||||||
const requestContext = RequestContext.get();
|
const requestContext = RequestContext.get();
|
||||||
const wiseRahKey =
|
const workflowApiKey =
|
||||||
requestContext.apiKeys?.openai ||
|
requestContext.apiKeys?.openai ||
|
||||||
process.env.RAH_WISE_RAH_OPENAI_API_KEY ||
|
process.env.RAH_WISE_RAH_OPENAI_API_KEY ||
|
||||||
process.env.OPENAI_API_KEY;
|
process.env.OPENAI_API_KEY;
|
||||||
if (!wiseRahKey) {
|
|
||||||
throw new Error('RAH_WISE_RAH_OPENAI_API_KEY (or OPENAI_API_KEY) is not set.');
|
if (!workflowApiKey) {
|
||||||
|
throw new Error('OPENAI_API_KEY is not set for workflow execution.');
|
||||||
}
|
}
|
||||||
|
|
||||||
AgentDelegationService.markInProgress(sessionId);
|
AgentDelegationService.markInProgress(sessionId);
|
||||||
console.log('✅ [WiseRAHExecutor] Delegation marked in progress');
|
console.log('✅ [WorkflowExecutor] Delegation marked in progress');
|
||||||
|
|
||||||
const normalizedTask = task.toLowerCase();
|
// Get workflow definition if available
|
||||||
const normalizedOutcome = (expectedOutcome || '').toLowerCase();
|
const workflow = workflowKey ? await WorkflowRegistry.getWorkflowByKey(workflowKey) : null;
|
||||||
const isWorkflow = Boolean(workflowKey) || normalizedTask.startsWith('execute workflow');
|
const maxIterationsLimit = workflow?.maxIterations ?? 10;
|
||||||
const explicitWriteRequest = /\b(create|update|edge|append|workflow|integrate|summarize into node|link node|embed|ingest|synchronize)\b/.test(normalizedTask) ||
|
|
||||||
/\b(create|update|edge|append|workflow|integrate|summarize into node|link node|embed|ingest|synchronize)\b/.test(normalizedOutcome);
|
|
||||||
const allowWrites = isWorkflow || explicitWriteRequest;
|
|
||||||
const analysisOnly = !allowWrites;
|
|
||||||
const maxIterationsLimit = isWorkflow ? 20 : 5;
|
|
||||||
const maxDelegationsAllowed = allowWrites ? (isWorkflow ? 12 : 2) : 0;
|
|
||||||
const maxDistinctWebSearches = isWorkflow ? 6 : 4;
|
|
||||||
const maxDistinctEmbeddingSearches = isWorkflow ? 5 : 3;
|
|
||||||
|
|
||||||
|
// Build the user prompt - just the task (which includes workflow instructions)
|
||||||
const promptSections = [
|
const promptSections = [
|
||||||
`Task: ${task}`,
|
task,
|
||||||
context.length ? `Context:\n- ${context.join('\n- ')}` : undefined,
|
context.length ? `Context:\n- ${context.join('\n- ')}` : undefined,
|
||||||
expectedOutcome ? `Expected outcome: ${expectedOutcome}` : undefined,
|
expectedOutcome ? `Expected outcome: ${expectedOutcome}` : undefined,
|
||||||
analysisOnly ? 'Constraint: This is an analysis-only request. Stay strictly read-only: do not call delegateToMiniRAH and do not request new extractions. Work only with existing knowledge.' : undefined,
|
|
||||||
'Return a structured summary following the format in your system prompt (Task/Actions/Result/Nodes/Follow-up).'
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const openaiProvider = createOpenAI({ apiKey: wiseRahKey });
|
const openaiProvider = createOpenAI({ apiKey: workflowApiKey });
|
||||||
console.log('🔧 [WiseRAHExecutor] OpenAI provider created');
|
console.log('🔧 [WorkflowExecutor] OpenAI provider created');
|
||||||
|
|
||||||
const plannerTools = getToolsForRole('planner');
|
// Use workflow-specified tools if available, otherwise fall back to safe default set
|
||||||
|
// IMPORTANT: Workflows should NEVER have access to delegateToMiniRAH - they are one-shot executors
|
||||||
// Remove delegateToMiniRAH for integrate workflow (wise-rah does updates directly)
|
const workflowTools = workflow?.tools;
|
||||||
if (workflowKey === 'integrate' && plannerTools.delegateToMiniRAH) {
|
const SAFE_WORKFLOW_DEFAULT_TOOLS = [
|
||||||
delete plannerTools.delegateToMiniRAH;
|
'getNodesById', 'queryNodes', 'queryDimensionNodes', 'searchContentEmbeddings',
|
||||||
console.log('🚫 [WiseRAHExecutor] Removed delegateToMiniRAH for integrate workflow (direct updates only)');
|
'webSearch', 'updateNode', 'createEdge'
|
||||||
}
|
];
|
||||||
|
const tools = workflowTools?.length
|
||||||
// For analysis-only tasks, also remove delegation
|
? getToolsByNames(workflowTools)
|
||||||
if (analysisOnly && plannerTools.delegateToMiniRAH) {
|
: getToolsByNames(SAFE_WORKFLOW_DEFAULT_TOOLS);
|
||||||
delete plannerTools.delegateToMiniRAH;
|
|
||||||
}
|
console.log('🛠️ [WorkflowExecutor] Tools for workflow:', Object.keys(tools));
|
||||||
|
|
||||||
console.log('🛠️ [WiseRAHExecutor] Planner tools retrieved:', Object.keys(plannerTools));
|
|
||||||
|
|
||||||
const toolsUsedInSession: string[] = [];
|
const toolsUsedInSession: string[] = [];
|
||||||
const delegatedEdgeKeys = new Set<string>();
|
const delegatedEdgeKeys = new Set<string>();
|
||||||
|
|
||||||
// Workflow progress is now streamed directly to delegation tabs via delegationStreamBroadcaster
|
// Workflow progress is now streamed directly to delegation tabs via delegationStreamBroadcaster
|
||||||
// No need to broadcast WORKFLOW_PROGRESS events to main chat anymore
|
|
||||||
const wrappedTools = Object.fromEntries(
|
const wrappedTools = Object.fromEntries(
|
||||||
Object.entries(plannerTools).map(([name, tool]) => {
|
Object.entries(tools).map(([name, tool]) => {
|
||||||
const wrapped = {
|
const wrapped = {
|
||||||
...tool,
|
...tool,
|
||||||
async execute(params: any, context: any) {
|
async execute(params: any, context: any) {
|
||||||
@@ -162,31 +149,10 @@ export class WiseRAHExecutor {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enforce read-only constraint - remove write tools EXCEPT for workflows
|
console.log('📝 [WorkflowExecutor] Starting execution loop...');
|
||||||
// Workflows need updateNode for content updates and createEdge for Quick Link workflow
|
|
||||||
const writeToolsToRemove = isWorkflow
|
|
||||||
? ['createNode', 'updateEdge', 'embedContent', 'youtubeExtract', 'websiteExtract', 'paperExtract', 'delegateToWiseRAH']
|
|
||||||
: ['createNode', 'updateNode', 'createEdge', 'updateEdge', 'embedContent', 'youtubeExtract', 'websiteExtract', 'paperExtract', 'delegateToWiseRAH'];
|
|
||||||
|
|
||||||
writeToolsToRemove.forEach(toolName => {
|
const messages: ModelMessage[] = [
|
||||||
if (toolName in wrappedTools) {
|
{ role: 'system', content: WORKFLOW_EXECUTOR_SYSTEM_PROMPT },
|
||||||
console.warn(`WiseRAHExecutor: ${toolName} detected in planner toolset. Removing to enforce read-only constraint.`);
|
|
||||||
delete wrappedTools[toolName];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isWorkflow && 'updateNode' in wrappedTools) {
|
|
||||||
console.log('✅ [WiseRAHExecutor] updateNode preserved for workflow execution');
|
|
||||||
}
|
|
||||||
if (isWorkflow && 'createEdge' in wrappedTools) {
|
|
||||||
console.log('✅ [WiseRAHExecutor] createEdge preserved for workflow execution');
|
|
||||||
}
|
|
||||||
console.log('🔒 [WiseRAHExecutor] Final tools after read-only enforcement:', Object.keys(wrappedTools));
|
|
||||||
|
|
||||||
console.log('📝 [WiseRAHExecutor] Starting manual agentic loop...');
|
|
||||||
|
|
||||||
const messages: CoreMessage[] = [
|
|
||||||
{ role: 'system', content: WISE_RAH_SYSTEM_PROMPT },
|
|
||||||
{ role: 'user', content: promptSections.join('\n\n') }
|
{ role: 'user', content: promptSections.join('\n\n') }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -196,21 +162,6 @@ export class WiseRAHExecutor {
|
|||||||
|
|
||||||
const seenToolResults = new Map<string, { output: LanguageModelV2ToolResultOutput; summary: string }>();
|
const seenToolResults = new Map<string, { output: LanguageModelV2ToolResultOutput; summary: string }>();
|
||||||
const workerSummaries: string[] = [];
|
const workerSummaries: string[] = [];
|
||||||
const workerSummarySet = new Set<string>();
|
|
||||||
let hasPlan = false;
|
|
||||||
let planReminderAdded = false;
|
|
||||||
let planIncludesDelegation = false;
|
|
||||||
let planRevisionNoticeSent = false;
|
|
||||||
let delegationNudgeSent = false;
|
|
||||||
let iterationsSincePlan = 0;
|
|
||||||
let lastPlanSummary = '';
|
|
||||||
let totalDelegations = 0;
|
|
||||||
let didCreateEdge = false;
|
|
||||||
let didUpdateNode = false;
|
|
||||||
const uniqueWebQueries = new Set<string>();
|
|
||||||
const uniqueEmbeddingQueries = new Set<string>();
|
|
||||||
let finalSummaryRequested = false;
|
|
||||||
let iterationsWithoutDelegation = 0;
|
|
||||||
|
|
||||||
const ensureString = (value: unknown) => (typeof value === 'string' ? value.trim() : '');
|
const ensureString = (value: unknown) => (typeof value === 'string' ? value.trim() : '');
|
||||||
|
|
||||||
@@ -219,7 +170,7 @@ export class WiseRAHExecutor {
|
|||||||
try {
|
try {
|
||||||
return JSON.parse(JSON.stringify(value));
|
return JSON.parse(JSON.stringify(value));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[WiseRAHExecutor] Failed to serialize delegation payload', error);
|
console.warn('[WorkflowExecutor] Failed to serialize delegation payload', error);
|
||||||
if (typeof value === 'string') return value;
|
if (typeof value === 'string') return value;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -284,18 +235,18 @@ export class WiseRAHExecutor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const finalStreamResult = await streamText({
|
const finalStreamResult = await streamText({
|
||||||
model: openaiProvider('gpt-5'),
|
model: openaiProvider('gpt-5-mini'),
|
||||||
messages,
|
messages,
|
||||||
tools: {},
|
tools: {},
|
||||||
maxOutputTokens: 500,
|
maxOutputTokens: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collect the complete response
|
// Collect the complete response
|
||||||
const finalChunks: string[] = [];
|
const finalChunks: string[] = [];
|
||||||
for await (const chunk of finalStreamResult.textStream) {
|
for await (const chunk of finalStreamResult.textStream) {
|
||||||
finalChunks.push(chunk);
|
finalChunks.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalResponse = {
|
const finalResponse = {
|
||||||
text: finalChunks.join(''),
|
text: finalChunks.join(''),
|
||||||
usage: await finalStreamResult.usage,
|
usage: await finalStreamResult.usage,
|
||||||
@@ -327,23 +278,23 @@ export class WiseRAHExecutor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < maxIterations; i++) {
|
for (let i = 0; i < maxIterations; i++) {
|
||||||
console.log(`🔄 [WiseRAHExecutor] Iteration ${i + 1}/${maxIterations}`);
|
console.log(`🔄 [WorkflowExecutor] Iteration ${i + 1}/${maxIterations}`);
|
||||||
|
|
||||||
// Touch delegation every iteration to prevent cleanup from killing it
|
// Touch delegation every iteration to prevent cleanup from killing it
|
||||||
AgentDelegationService.touchDelegation(sessionId);
|
AgentDelegationService.touchDelegation(sessionId);
|
||||||
|
|
||||||
const streamResult = await streamText({
|
const streamResult = await streamText({
|
||||||
model: openaiProvider('gpt-5'),
|
model: openaiProvider('gpt-5-mini'),
|
||||||
messages,
|
messages,
|
||||||
tools: wrappedTools,
|
tools: wrappedTools,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collect the complete response
|
// Collect the complete response
|
||||||
const chunks: string[] = [];
|
const chunks: string[] = [];
|
||||||
for await (const chunk of streamResult.textStream) {
|
for await (const chunk of streamResult.textStream) {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
text: chunks.join(''),
|
text: chunks.join(''),
|
||||||
finishReason: await streamResult.finishReason,
|
finishReason: await streamResult.finishReason,
|
||||||
@@ -355,7 +306,7 @@ export class WiseRAHExecutor {
|
|||||||
totalUsage.outputTokens += response.usage?.outputTokens || 0;
|
totalUsage.outputTokens += response.usage?.outputTokens || 0;
|
||||||
totalUsage.totalTokens += response.usage?.totalTokens || 0;
|
totalUsage.totalTokens += response.usage?.totalTokens || 0;
|
||||||
|
|
||||||
console.log(`📊 [WiseRAHExecutor] Step ${i + 1} finishReason:`, response.finishReason);
|
console.log(`📊 [WorkflowExecutor] Step ${i + 1} finishReason:`, response.finishReason);
|
||||||
|
|
||||||
// Stream text response to delegation chat
|
// Stream text response to delegation chat
|
||||||
if (response.text && response.text.trim()) {
|
if (response.text && response.text.trim()) {
|
||||||
@@ -367,13 +318,13 @@ export class WiseRAHExecutor {
|
|||||||
|
|
||||||
if (response.finishReason !== 'tool-calls') {
|
if (response.finishReason !== 'tool-calls') {
|
||||||
finalText = response.text;
|
finalText = response.text;
|
||||||
console.log('✅ [WiseRAHExecutor] Got final text');
|
console.log('✅ [WorkflowExecutor] Got final text');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolCalls = response.toolCalls || [];
|
const toolCalls = response.toolCalls || [];
|
||||||
console.log(`🔧 [WiseRAHExecutor] Executing ${toolCalls.length} tool calls`);
|
console.log(`🔧 [WorkflowExecutor] Executing ${toolCalls.length} tool calls`);
|
||||||
|
|
||||||
// Broadcast new assistant message for next iteration
|
// Broadcast new assistant message for next iteration
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
emitDelegationEvent({ type: 'assistant-message' });
|
emitDelegationEvent({ type: 'assistant-message' });
|
||||||
@@ -396,39 +347,16 @@ export class WiseRAHExecutor {
|
|||||||
output: LanguageModelV2ToolResultOutput;
|
output: LanguageModelV2ToolResultOutput;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
let executedTool = false;
|
|
||||||
|
|
||||||
for (const call of toolCalls) {
|
for (const call of toolCalls) {
|
||||||
let callInputRaw = (call as any).input ?? (call as any).args;
|
let callInputRaw = (call as any).input ?? (call as any).args;
|
||||||
|
|
||||||
// Append logic now handled in updateNode tool itself (lines 27-44)
|
|
||||||
|
|
||||||
const signatureInput = normaliseForSignature(call.toolName, callInputRaw);
|
const signatureInput = normaliseForSignature(call.toolName, callInputRaw);
|
||||||
const signature = JSON.stringify({ tool: call.toolName, input: signatureInput });
|
const signature = JSON.stringify({ tool: call.toolName, input: signatureInput });
|
||||||
|
|
||||||
// Broadcast tool call to delegation stream
|
// Broadcast tool call to delegation stream
|
||||||
emitToolStart(call.toolCallId, call.toolName, callInputRaw);
|
emitToolStart(call.toolCallId, call.toolName, callInputRaw);
|
||||||
|
|
||||||
if (!hasPlan && call.toolName !== 'think') {
|
// Skip duplicate tool calls (except think which can be called multiple times)
|
||||||
const warning = 'Planning required: use the think tool to outline a numbered plan (purpose, thoughts, next action) before other tools.';
|
|
||||||
toolResults.push({
|
|
||||||
type: 'tool-result',
|
|
||||||
toolCallId: call.toolCallId,
|
|
||||||
toolName: call.toolName,
|
|
||||||
output: { type: 'error-text', value: warning },
|
|
||||||
});
|
|
||||||
emitToolCompletion(call.toolCallId, call.toolName, { success: false }, warning, 'error', warning);
|
|
||||||
|
|
||||||
if (!planReminderAdded) {
|
|
||||||
planReminderAdded = true;
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: 'Before calling other tools, use the think tool to draft a numbered plan and specify your next action.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.toolName !== 'think' && seenToolResults.has(signature)) {
|
if (call.toolName !== 'think' && seenToolResults.has(signature)) {
|
||||||
const cached = seenToolResults.get(signature)!;
|
const cached = seenToolResults.get(signature)!;
|
||||||
toolResults.push({
|
toolResults.push({
|
||||||
@@ -437,22 +365,15 @@ export class WiseRAHExecutor {
|
|||||||
toolName: call.toolName,
|
toolName: call.toolName,
|
||||||
output: cached.output,
|
output: cached.output,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Broadcast cached result
|
// Broadcast cached result
|
||||||
emitToolCompletion(call.toolCallId, call.toolName, cached.summary || 'Cached result', cached.summary || 'Cached result');
|
emitToolCompletion(call.toolCallId, call.toolName, cached.summary || 'Cached result', cached.summary || 'Cached result');
|
||||||
|
|
||||||
if (call.toolName === 'delegateToMiniRAH' && cached.summary) {
|
|
||||||
if (!workerSummarySet.has(cached.summary)) {
|
|
||||||
workerSummarySet.add(cached.summary);
|
|
||||||
workerSummaries.push(`[reused] ${cached.summary}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tool = wrappedTools[call.toolName];
|
const tool = wrappedTools[call.toolName];
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
const warning = `Tool ${call.toolName} is not available to wise ra-h.`;
|
const warning = `Tool ${call.toolName} is not available for this workflow.`;
|
||||||
toolResults.push({
|
toolResults.push({
|
||||||
type: 'tool-result',
|
type: 'tool-result',
|
||||||
toolCallId: call.toolCallId,
|
toolCallId: call.toolCallId,
|
||||||
@@ -477,46 +398,10 @@ export class WiseRAHExecutor {
|
|||||||
});
|
});
|
||||||
emitToolCompletion(call.toolCallId, call.toolName, rawResult, summary, 'complete');
|
emitToolCompletion(call.toolCallId, call.toolName, rawResult, summary, 'complete');
|
||||||
|
|
||||||
if (call.toolName === 'think') {
|
// Cache result (except think which can be called multiple times)
|
||||||
hasPlan = true;
|
if (call.toolName !== 'think') {
|
||||||
lastPlanSummary = summary;
|
|
||||||
if (allowWrites) {
|
|
||||||
planIncludesDelegation = /delegate\b|delegateToMiniRAH|mini ra-h/i.test(summary);
|
|
||||||
} else {
|
|
||||||
planIncludesDelegation = false;
|
|
||||||
}
|
|
||||||
planRevisionNoticeSent = false;
|
|
||||||
delegationNudgeSent = false;
|
|
||||||
iterationsSincePlan = 0;
|
|
||||||
} else {
|
|
||||||
seenToolResults.set(signature, { output, summary });
|
seenToolResults.set(signature, { output, summary });
|
||||||
if (call.toolName === 'delegateToMiniRAH' && summary && !workerSummarySet.has(summary)) {
|
|
||||||
workerSummarySet.add(summary);
|
|
||||||
workerSummaries.push(summary);
|
|
||||||
totalDelegations += 1;
|
|
||||||
|
|
||||||
if (/Created edge/i.test(summary) || /Edge created/i.test(summary)) {
|
|
||||||
didCreateEdge = true;
|
|
||||||
}
|
|
||||||
if (/Updated node/i.test(summary) || /Appended/i.test(summary) || /content updated/i.test(summary)) {
|
|
||||||
didUpdateNode = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (call.toolName === 'webSearch') {
|
|
||||||
const query = ensureString(signatureInput?.query ?? callInputRaw?.query);
|
|
||||||
if (query) {
|
|
||||||
uniqueWebQueries.add(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (call.toolName === 'searchContentEmbeddings') {
|
|
||||||
const query = ensureString(signatureInput?.query ?? callInputRaw?.query);
|
|
||||||
if (query) {
|
|
||||||
uniqueEmbeddingQueries.add(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
executedTool = true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Tool execution failed';
|
const message = error instanceof Error ? error.message : 'Tool execution failed';
|
||||||
toolResults.push({
|
toolResults.push({
|
||||||
@@ -533,109 +418,47 @@ export class WiseRAHExecutor {
|
|||||||
role: 'tool',
|
role: 'tool',
|
||||||
content: toolResults,
|
content: toolResults,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasPlan) {
|
|
||||||
iterationsSincePlan += 1;
|
|
||||||
// Legacy delegation nudges removed - wise-rah completes workflows independently
|
|
||||||
}
|
|
||||||
|
|
||||||
const enoughMaterial = hasPlan && (allowWrites ? workerSummaries.length >= 2 : executedTool);
|
|
||||||
const tooManyDelegations = allowWrites && totalDelegations >= maxDelegationsAllowed && maxDelegationsAllowed > 0;
|
|
||||||
const webSearchCapReached = uniqueWebQueries.size >= maxDistinctWebSearches;
|
|
||||||
const embeddingCapReached = uniqueEmbeddingQueries.size >= maxDistinctEmbeddingSearches;
|
|
||||||
|
|
||||||
if (allowWrites && workflowKey === 'integrate' && totalDelegations === 0) {
|
|
||||||
iterationsWithoutDelegation += 1;
|
|
||||||
if (!delegationNudgeSent && iterationsWithoutDelegation >= 4) {
|
|
||||||
delegationNudgeSent = true;
|
|
||||||
const targetInstruction = workflowNodeId
|
|
||||||
? `Focus on node [NODE:${workflowNodeId}]. Delegate to mini ra-h now to (a) append integration insights to its content and (b) create edges to the highest-value related nodes you identified.`
|
|
||||||
: 'Delegate to mini ra-h now to (a) append integration insights to the focused node and (b) create edges to the highest-value related nodes you identified.';
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: `${targetInstruction} Do not continue researching until those delegations are complete.`,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
iterationsWithoutDelegation = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowWrites && workflowKey === 'integrate' && didCreateEdge && !didUpdateNode) {
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: 'Edges are in place. Delegate to mini ra-h to append the Integration Insights section to the focused node before moving forward.',
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const minIterationsBeforeSummary = allowWrites ? 2 : 1;
|
|
||||||
if (!finalSummaryRequested && (tooManyDelegations || webSearchCapReached || embeddingCapReached || (enoughMaterial && executedTool && i >= minIterationsBeforeSummary))) {
|
|
||||||
if (allowWrites && workflowKey === 'integrate' && !didUpdateNode) {
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: 'Before summarizing, delegate to mini ra-h to append the Integration Insights content to the focused node.',
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowWrites && totalDelegations === 0) {
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: 'You have not delegated any execution yet. Identify the concrete write actions required and call delegateToMiniRAH to perform them before summarising.',
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let instruction = 'You have enough evidence from the workers. Provide the final Task/Actions/Result/Nodes/Follow-up summary now without further tool calls.';
|
|
||||||
if (tooManyDelegations) {
|
|
||||||
instruction = `You have already delegated the maximum allowed (${maxDelegationsAllowed}). Synthesize the findings now using the Task/Actions/Result/Nodes/Follow-up format. Do not call additional tools.`;
|
|
||||||
} else if (webSearchCapReached) {
|
|
||||||
instruction = `You have already issued about ${maxDistinctWebSearches} distinct web searches. Consolidate what you found into the final Task/Actions/Result/Nodes/Follow-up summary now—no additional tool calls.`;
|
|
||||||
} else if (embeddingCapReached) {
|
|
||||||
instruction = `Embedding searches have covered the knowledge base (limit ${maxDistinctEmbeddingSearches}). Switch to producing the Task/Actions/Result/Nodes/Follow-up summary—do not call further tools.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
finalText = await requestFinalSummary(instruction);
|
|
||||||
finalSummaryRequested = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we hit max iterations without a final response, request one
|
||||||
if (!finalText) {
|
if (!finalText) {
|
||||||
if (allowWrites && totalDelegations === 0) {
|
console.warn('⚠️ [WorkflowExecutor] Max iterations hit with no summary. Requesting final response without tools.');
|
||||||
throw new Error('Wise ra-h attempted to summarize without delegating any execution to mini ra-h.');
|
finalText = await requestFinalSummary('Provide a brief summary of what was accomplished. Do not call any tools.');
|
||||||
}
|
console.log('✅ [WorkflowExecutor] Final summary obtained after tool cutoff.');
|
||||||
console.warn('⚠️ [WiseRAHExecutor] Max iterations hit with no summary. Requesting final response without tools.');
|
|
||||||
finalText = await requestFinalSummary('You have gathered everything needed. Provide the final Task/Actions/Result/Nodes/Follow-up summary now. Do not call any tools.');
|
|
||||||
console.log('✅ [WiseRAHExecutor] Final summary obtained after tool cutoff.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage = totalUsage;
|
const usage = totalUsage;
|
||||||
let summary = typeof finalText === 'string' ? finalText.trim() : '';
|
let summary = typeof finalText === 'string' ? finalText.trim() : '';
|
||||||
|
|
||||||
if (summary.length > 2000) {
|
if (summary.length > 2000) {
|
||||||
console.log('⚠️ [WiseRAHExecutor] Summary too long, requesting concise version.');
|
console.log('⚠️ [WorkflowExecutor] Summary too long, requesting concise version.');
|
||||||
summary = (await requestFinalSummary('Condense the findings into ≤300 tokens using the Task/Actions/Result/Nodes/Follow-up format. Focus on the most salient insights and reference key nodes. Do not call any tools.')).trim();
|
summary = (await requestFinalSummary('Condense the findings into ≤300 tokens using the Task/Actions/Result/Nodes/Follow-up format. Focus on the most salient insights and reference key nodes. Do not call any tools.')).trim();
|
||||||
}
|
}
|
||||||
if (summary.length > 1000) {
|
if (summary.length > 1000) {
|
||||||
summary = `${summary.slice(0, 997)}…`;
|
summary = `${summary.slice(0, 997)}…`;
|
||||||
}
|
}
|
||||||
console.log('📄 [WiseRAHExecutor] Summary after trim:', summary);
|
console.log('📄 [WorkflowExecutor] Summary after trim:', summary);
|
||||||
console.log('📏 [WiseRAHExecutor] Summary length:', summary.length);
|
console.log('📏 [WorkflowExecutor] Summary length:', summary.length);
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
emitDelegationEvent({
|
emitDelegationEvent({
|
||||||
type: 'assistant-message',
|
type: 'assistant-message',
|
||||||
});
|
});
|
||||||
emitDelegationEvent({
|
emitDelegationEvent({
|
||||||
type: 'text-delta',
|
type: 'text-delta',
|
||||||
delta: 'Wise ra-h attempted to summarise but the response was empty. Check tool logs above for context.',
|
delta: 'Workflow executor attempted to summarise but the response was empty. Check tool logs above for context.',
|
||||||
});
|
});
|
||||||
throw new Error('Wise ra-h returned empty summary');
|
throw new Error('Workflow executor returned empty summary');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[WiseRAHExecutor] summary:', 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
|
// Calculate cost and log to chats table
|
||||||
if (usage) {
|
if (usage) {
|
||||||
@@ -646,7 +469,7 @@ export class WiseRAHExecutor {
|
|||||||
const costResult = calculateCost({
|
const costResult = calculateCost({
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
modelId: 'gpt-5',
|
modelId: 'gpt-5-mini',
|
||||||
});
|
});
|
||||||
|
|
||||||
const usageData: UsageData = {
|
const usageData: UsageData = {
|
||||||
@@ -654,7 +477,7 @@ export class WiseRAHExecutor {
|
|||||||
outputTokens,
|
outputTokens,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
estimatedCostUsd: costResult.totalCostUsd,
|
estimatedCostUsd: costResult.totalCostUsd,
|
||||||
modelUsed: 'gpt-5',
|
modelUsed: 'gpt-5-mini',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
toolsUsed: toolsUsedInSession.length > 0 ? toolsUsedInSession : undefined,
|
toolsUsed: toolsUsedInSession.length > 0 ? toolsUsedInSession : undefined,
|
||||||
toolCallsCount: toolsUsedInSession.length > 0 ? toolsUsedInSession.length : undefined,
|
toolCallsCount: toolsUsedInSession.length > 0 ? toolsUsedInSession.length : undefined,
|
||||||
@@ -671,7 +494,7 @@ export class WiseRAHExecutor {
|
|||||||
task,
|
task,
|
||||||
summary,
|
summary,
|
||||||
{
|
{
|
||||||
helperName: 'wise-rah',
|
helperName: 'workflow-agent',
|
||||||
agentType: 'planner',
|
agentType: 'planner',
|
||||||
delegationId: delegationId ?? null,
|
delegationId: delegationId ?? null,
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -680,32 +503,31 @@ export class WiseRAHExecutor {
|
|||||||
parentChatId,
|
parentChatId,
|
||||||
workflowKey,
|
workflowKey,
|
||||||
workflowNodeId,
|
workflowNodeId,
|
||||||
systemMessage: WISE_RAH_SYSTEM_PROMPT,
|
systemMessage: WORKFLOW_EXECUTOR_SYSTEM_PROMPT,
|
||||||
backendUsage: [],
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`💰 [WiseRAHExecutor] Cost: $${costResult.totalCostUsd.toFixed(6)} (${totalTokens} tokens)`);
|
console.log(`💰 [WorkflowExecutor] Cost: $${costResult.totalCostUsd.toFixed(6)} (${totalTokens} tokens)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ [WiseRAHExecutor] Completing delegation with summary');
|
console.log('✅ [WorkflowExecutor] Completing delegation with summary');
|
||||||
return AgentDelegationService.completeDelegation(sessionId, summary);
|
return AgentDelegationService.completeDelegation(sessionId, summary);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [WiseRAHExecutor] Error during execution:', error);
|
console.error('❌ [WorkflowExecutor] Error during execution:', error);
|
||||||
console.error('❌ [WiseRAHExecutor] Error stack:', error instanceof Error ? error.stack : 'No stack');
|
console.error('❌ [WorkflowExecutor] Error stack:', error instanceof Error ? error.stack : 'No stack');
|
||||||
const message = error instanceof Error ? error.message : 'Unknown delegation error';
|
const message = error instanceof Error ? error.message : 'Unknown delegation error';
|
||||||
|
|
||||||
// Broadcast error to delegation stream
|
// Broadcast error to delegation stream
|
||||||
delegationStreamBroadcaster.broadcast(sessionId, {
|
delegationStreamBroadcaster.broadcast(sessionId, {
|
||||||
type: 'assistant-message',
|
type: 'assistant-message',
|
||||||
});
|
});
|
||||||
delegationStreamBroadcaster.broadcast(sessionId, {
|
delegationStreamBroadcaster.broadcast(sessionId, {
|
||||||
type: 'text-delta',
|
type: 'text-delta',
|
||||||
delta: `Wise ra-h failed: ${message}`,
|
delta: `Workflow executor failed: ${message}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
AgentDelegationService.completeDelegation(sessionId, `Wise ra-h failed: ${message}`, 'failed');
|
AgentDelegationService.completeDelegation(sessionId, `Workflow executor failed: ${message}`, 'failed');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ import { AgentRegistry } from '@/services/agents/registry';
|
|||||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||||
import { getHelperTools, getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
|
import { getHelperTools, getDefaultToolNamesForRole } from '@/tools/infrastructure/registry';
|
||||||
import type { CacheableBlock, SystemPromptResult } from '@/types/prompts';
|
import type { CacheableBlock, SystemPromptResult } from '@/types/prompts';
|
||||||
import { buildAutoContextBlock } from '@/services/context/autoContext';
|
// buildAutoContextBlock removed - agent queries top nodes on-demand via sqliteQuery
|
||||||
|
|
||||||
export interface NodeContext {
|
export interface NodeContext {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
@@ -25,14 +25,27 @@ export interface ContextBuilderOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BASE_CONTEXT = `=== RA-H BASE CONTEXT ===
|
const BASE_CONTEXT = `=== RA-H BASE CONTEXT ===
|
||||||
- Nodes store content (title, content, dimensions, metadata, link, chunk)
|
You have access to the RA-H knowledge graph via two approaches:
|
||||||
- Edges capture directed relationships between nodes
|
|
||||||
- Dimensions organize nodes; locked dimensions (isPriority=true) auto-assign to new nodes
|
**sqliteQuery tool** — Use for flexible read operations:
|
||||||
- You can create and manage dimensions using dimension tools (createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension)
|
- Ad-hoc queries, exploration, complex JOINs, aggregations
|
||||||
- When auto-context is enabled you'll see BACKGROUND CONTEXT with the 10 most-connected nodes (ID + title only)
|
- Any query pattern not covered by structured tools
|
||||||
- Focused nodes show truncated content; use queryNodes, searchContentEmbeddings, or queryEdge when you need full detail
|
- 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
|
||||||
- Node references must use [NODE:id:"title"] so the UI renders clickable labels
|
|
||||||
- Pronouns or phrases like "this conversation/paper/video" refer to the primary focused node unless clarified
|
**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 {
|
function buildStaticBaseContext(): string {
|
||||||
@@ -243,16 +256,9 @@ export async function buildSystemPromptBlocks(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoContextBlock = isPrimaryOrchestrator(helperComponentKey)
|
// REMOVED: autoContextBlock (top 10 nodes)
|
||||||
? buildAutoContextBlock()
|
// Agent can now query this on-demand via sqliteQuery:
|
||||||
: null;
|
// 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
|
||||||
if (autoContextBlock && autoContextBlock.trim().length > 0) {
|
|
||||||
blocks.push({
|
|
||||||
type: 'text',
|
|
||||||
text: autoContextBlock,
|
|
||||||
...(cacheControl ? { cache_control: cacheControl } : {})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add dimension context if an active dimension is provided
|
// Add dimension context if an active dimension is provided
|
||||||
if (nodeContext.activeDimension) {
|
if (nodeContext.activeDimension) {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
|||||||
requiresFocusedNode: true,
|
requiresFocusedNode: true,
|
||||||
primaryActor: 'oracle',
|
primaryActor: 'oracle',
|
||||||
expectedOutcome: 'Brief section appended with what/gist/why it matters',
|
expectedOutcome: 'Brief section appended with what/gist/why it matters',
|
||||||
|
tools: ['getNodesById', 'updateNode'],
|
||||||
|
maxIterations: 3,
|
||||||
},
|
},
|
||||||
'research': {
|
'research': {
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -29,6 +31,8 @@ const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
|||||||
requiresFocusedNode: true,
|
requiresFocusedNode: true,
|
||||||
primaryActor: 'oracle',
|
primaryActor: 'oracle',
|
||||||
expectedOutcome: 'Research notes appended with background and key findings',
|
expectedOutcome: 'Research notes appended with background and key findings',
|
||||||
|
tools: ['getNodesById', 'webSearch', 'updateNode'],
|
||||||
|
maxIterations: 5,
|
||||||
},
|
},
|
||||||
'connect': {
|
'connect': {
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -40,6 +44,8 @@ const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
|||||||
requiresFocusedNode: true,
|
requiresFocusedNode: true,
|
||||||
primaryActor: 'oracle',
|
primaryActor: 'oracle',
|
||||||
expectedOutcome: '3-5 edges created to related nodes',
|
expectedOutcome: '3-5 edges created to related nodes',
|
||||||
|
tools: ['getNodesById', 'queryNodes', 'createEdge'],
|
||||||
|
maxIterations: 6,
|
||||||
},
|
},
|
||||||
'integrate': {
|
'integrate': {
|
||||||
id: 4,
|
id: 4,
|
||||||
@@ -51,6 +57,8 @@ const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
|||||||
requiresFocusedNode: true,
|
requiresFocusedNode: true,
|
||||||
primaryActor: 'oracle',
|
primaryActor: 'oracle',
|
||||||
expectedOutcome: 'Integration analysis appended; 3-5 edges created',
|
expectedOutcome: 'Integration analysis appended; 3-5 edges created',
|
||||||
|
tools: ['getNodesById', 'queryNodes', 'searchContentEmbeddings', 'createEdge', 'updateNode'],
|
||||||
|
maxIterations: 12,
|
||||||
},
|
},
|
||||||
'survey': {
|
'survey': {
|
||||||
id: 5,
|
id: 5,
|
||||||
@@ -62,6 +70,8 @@ const BUNDLED_WORKFLOWS: Record<string, WorkflowDefinition> = {
|
|||||||
requiresFocusedNode: false, // Requires active dimension, not focused node
|
requiresFocusedNode: false, // Requires active dimension, not focused node
|
||||||
primaryActor: 'oracle',
|
primaryActor: 'oracle',
|
||||||
expectedOutcome: 'Dimension description updated with survey findings',
|
expectedOutcome: 'Dimension description updated with survey findings',
|
||||||
|
tools: ['queryDimensionNodes', 'searchContentEmbeddings', 'updateDimension'],
|
||||||
|
maxIterations: 5,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,6 +90,8 @@ function userWorkflowToDefinition(uw: ReturnType<typeof loadUserWorkflow>, id: n
|
|||||||
requiresFocusedNode: uw.requiresFocusedNode,
|
requiresFocusedNode: uw.requiresFocusedNode,
|
||||||
primaryActor: 'oracle',
|
primaryActor: 'oracle',
|
||||||
expectedOutcome: undefined,
|
expectedOutcome: undefined,
|
||||||
|
tools: uw.tools,
|
||||||
|
maxIterations: uw.maxIterations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,8 @@ export interface WorkflowDefinition {
|
|||||||
requiresFocusedNode: boolean;
|
requiresFocusedNode: boolean;
|
||||||
primaryActor: 'oracle' | 'main';
|
primaryActor: 'oracle' | 'main';
|
||||||
expectedOutcome?: string;
|
expectedOutcome?: string;
|
||||||
|
/** Tools this workflow is allowed to use. If not specified, uses default set. */
|
||||||
|
tools?: string[];
|
||||||
|
/** Maximum iterations for this workflow. Defaults to 10. */
|
||||||
|
maxIterations?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export interface UserWorkflow {
|
|||||||
instructions: string;
|
instructions: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
requiresFocusedNode: boolean;
|
requiresFocusedNode: boolean;
|
||||||
|
tools?: string[];
|
||||||
|
maxIterations?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBaseConfigDir(): string {
|
function resolveBaseConfigDir(): string {
|
||||||
@@ -66,6 +68,8 @@ export function listUserWorkflows(): UserWorkflow[] {
|
|||||||
instructions: parsed.instructions,
|
instructions: parsed.instructions,
|
||||||
enabled: parsed.enabled !== false,
|
enabled: parsed.enabled !== false,
|
||||||
requiresFocusedNode: parsed.requiresFocusedNode !== false,
|
requiresFocusedNode: parsed.requiresFocusedNode !== false,
|
||||||
|
tools: Array.isArray(parsed.tools) ? parsed.tools : undefined,
|
||||||
|
maxIterations: typeof parsed.maxIterations === 'number' ? parsed.maxIterations : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -99,6 +103,8 @@ export function loadUserWorkflow(key: string): UserWorkflow | null {
|
|||||||
instructions: parsed.instructions,
|
instructions: parsed.instructions,
|
||||||
enabled: parsed.enabled !== false,
|
enabled: parsed.enabled !== false,
|
||||||
requiresFocusedNode: parsed.requiresFocusedNode !== false,
|
requiresFocusedNode: parsed.requiresFocusedNode !== false,
|
||||||
|
tools: Array.isArray(parsed.tools) ? parsed.tools : undefined,
|
||||||
|
maxIterations: typeof parsed.maxIterations === 'number' ? parsed.maxIterations : undefined,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to load workflow ${key}:`, error);
|
console.warn(`Failed to load workflow ${key}:`, error);
|
||||||
@@ -110,7 +116,7 @@ export function saveWorkflow(workflow: UserWorkflow): void {
|
|||||||
ensureWorkflowsDirExists();
|
ensureWorkflowsDirExists();
|
||||||
|
|
||||||
const filePath = path.join(getWorkflowsDir(), `${workflow.key}.json`);
|
const filePath = path.join(getWorkflowsDir(), `${workflow.key}.json`);
|
||||||
const data = {
|
const data: Record<string, unknown> = {
|
||||||
key: workflow.key,
|
key: workflow.key,
|
||||||
displayName: workflow.displayName,
|
displayName: workflow.displayName,
|
||||||
description: workflow.description,
|
description: workflow.description,
|
||||||
@@ -119,6 +125,10 @@ export function saveWorkflow(workflow: UserWorkflow): void {
|
|||||||
requiresFocusedNode: workflow.requiresFocusedNode,
|
requiresFocusedNode: workflow.requiresFocusedNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only include tools/maxIterations if defined
|
||||||
|
if (workflow.tools) data.tools = workflow.tools;
|
||||||
|
if (workflow.maxIterations) data.maxIterations = workflow.maxIterations;
|
||||||
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,9 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
|||||||
queryDimensionNodes: 'core',
|
queryDimensionNodes: 'core',
|
||||||
searchContentEmbeddings: 'core',
|
searchContentEmbeddings: 'core',
|
||||||
|
|
||||||
// Orchestration: Delegation and reasoning (orchestrator only)
|
// Orchestration: Workflows and reasoning (orchestrator only)
|
||||||
webSearch: 'orchestration',
|
webSearch: 'orchestration',
|
||||||
think: 'orchestration',
|
think: 'orchestration',
|
||||||
delegateToMiniRAH: 'orchestration',
|
|
||||||
delegateNodeQuotes: 'orchestration',
|
|
||||||
delegateNodeComparison: 'orchestration',
|
|
||||||
delegateToWiseRAH: 'orchestration',
|
delegateToWiseRAH: 'orchestration',
|
||||||
executeWorkflow: 'orchestration',
|
executeWorkflow: 'orchestration',
|
||||||
listWorkflows: 'orchestration',
|
listWorkflows: 'orchestration',
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import { updateEdgeTool } from '../database/updateEdge';
|
|||||||
import { quickLinkTool } from '../database/quickLink';
|
import { quickLinkTool } from '../database/quickLink';
|
||||||
import { createDimensionTool } from '../database/createDimension';
|
import { createDimensionTool } from '../database/createDimension';
|
||||||
import { updateDimensionTool } from '../database/updateDimension';
|
import { updateDimensionTool } from '../database/updateDimension';
|
||||||
import { lockDimensionTool } from '../database/lockDimension';
|
// lockDimension and unlockDimension consolidated into updateDimension (use isPriority param)
|
||||||
import { unlockDimensionTool } from '../database/unlockDimension';
|
|
||||||
import { deleteDimensionTool } from '../database/deleteDimension';
|
import { deleteDimensionTool } from '../database/deleteDimension';
|
||||||
import { queryDimensionsTool } from '../database/queryDimensions';
|
import { queryDimensionsTool } from '../database/queryDimensions';
|
||||||
import { getDimensionTool } from '../database/getDimension';
|
import { getDimensionTool } from '../database/getDimension';
|
||||||
@@ -18,8 +17,6 @@ import { queryDimensionNodesTool } from '../database/queryDimensionNodes';
|
|||||||
import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings';
|
import { searchContentEmbeddingsTool } from '../other/searchContentEmbeddings';
|
||||||
import { webSearchTool } from '../other/webSearch';
|
import { webSearchTool } from '../other/webSearch';
|
||||||
import { thinkTool } from '../other/think';
|
import { thinkTool } from '../other/think';
|
||||||
import { delegateToMiniRAHTool } from '../orchestration/delegateToMiniRAH';
|
|
||||||
import { delegateNodeQuotesTool, delegateNodeComparisonTool } from '../orchestration/delegationHelpers';
|
|
||||||
import { delegateToWiseRAHTool } from '../orchestration/delegateToWiseRAH';
|
import { delegateToWiseRAHTool } from '../orchestration/delegateToWiseRAH';
|
||||||
import { executeWorkflowTool } from '../orchestration/executeWorkflow';
|
import { executeWorkflowTool } from '../orchestration/executeWorkflow';
|
||||||
import { listWorkflowsTool } from '../orchestration/listWorkflows';
|
import { listWorkflowsTool } from '../orchestration/listWorkflows';
|
||||||
@@ -28,10 +25,12 @@ import { editWorkflowTool } from '../orchestration/editWorkflow';
|
|||||||
import { youtubeExtractTool } from '../other/youtubeExtract';
|
import { youtubeExtractTool } from '../other/youtubeExtract';
|
||||||
import { websiteExtractTool } from '../other/websiteExtract';
|
import { websiteExtractTool } from '../other/websiteExtract';
|
||||||
import { paperExtractTool } from '../other/paperExtract';
|
import { paperExtractTool } from '../other/paperExtract';
|
||||||
|
import { sqliteQueryTool } from '../other/sqliteQuery';
|
||||||
import { logEvalToolCall } from '@/services/evals/evalsLogger';
|
import { logEvalToolCall } from '@/services/evals/evalsLogger';
|
||||||
|
|
||||||
// Core tools available to all agents (read-only graph operations)
|
// Core tools available to all agents (read-only graph operations)
|
||||||
const CORE_TOOLS: Record<string, any> = {
|
const CORE_TOOLS: Record<string, any> = {
|
||||||
|
sqliteQuery: sqliteQueryTool,
|
||||||
queryNodes: queryNodesTool,
|
queryNodes: queryNodesTool,
|
||||||
getNodesById: getNodesByIdTool,
|
getNodesById: getNodesByIdTool,
|
||||||
queryEdge: queryEdgeTool,
|
queryEdge: queryEdgeTool,
|
||||||
@@ -44,9 +43,6 @@ const CORE_TOOLS: Record<string, any> = {
|
|||||||
const ORCHESTRATION_TOOLS: Record<string, any> = {
|
const ORCHESTRATION_TOOLS: Record<string, any> = {
|
||||||
webSearch: webSearchTool,
|
webSearch: webSearchTool,
|
||||||
think: thinkTool,
|
think: thinkTool,
|
||||||
delegateToMiniRAH: delegateToMiniRAHTool,
|
|
||||||
delegateNodeQuotes: delegateNodeQuotesTool,
|
|
||||||
delegateNodeComparison: delegateNodeComparisonTool,
|
|
||||||
delegateToWiseRAH: delegateToWiseRAHTool,
|
delegateToWiseRAH: delegateToWiseRAHTool,
|
||||||
executeWorkflow: executeWorkflowTool,
|
executeWorkflow: executeWorkflowTool,
|
||||||
listWorkflows: listWorkflowsTool,
|
listWorkflows: listWorkflowsTool,
|
||||||
@@ -63,8 +59,6 @@ const EXECUTION_TOOLS: Record<string, any> = {
|
|||||||
quickLink: quickLinkTool,
|
quickLink: quickLinkTool,
|
||||||
createDimension: createDimensionTool,
|
createDimension: createDimensionTool,
|
||||||
updateDimension: updateDimensionTool,
|
updateDimension: updateDimensionTool,
|
||||||
lockDimension: lockDimensionTool,
|
|
||||||
unlockDimension: unlockDimensionTool,
|
|
||||||
deleteDimension: deleteDimensionTool,
|
deleteDimension: deleteDimensionTool,
|
||||||
youtubeExtract: youtubeExtractTool,
|
youtubeExtract: youtubeExtractTool,
|
||||||
websiteExtract: websiteExtractTool,
|
websiteExtract: websiteExtractTool,
|
||||||
@@ -98,8 +92,6 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
|
|||||||
'quickLink',
|
'quickLink',
|
||||||
'createDimension',
|
'createDimension',
|
||||||
'updateDimension',
|
'updateDimension',
|
||||||
'lockDimension',
|
|
||||||
'unlockDimension',
|
|
||||||
'deleteDimension',
|
'deleteDimension',
|
||||||
'youtubeExtract',
|
'youtubeExtract',
|
||||||
'websiteExtract',
|
'websiteExtract',
|
||||||
@@ -108,25 +100,22 @@ const ORCHESTRATOR_TOOL_NAMES = Array.from(new Set([
|
|||||||
|
|
||||||
const EXECUTOR_TOOL_NAMES = [
|
const EXECUTOR_TOOL_NAMES = [
|
||||||
...Object.keys(CORE_TOOLS),
|
...Object.keys(CORE_TOOLS),
|
||||||
...Object.keys(ORCHESTRATION_TOOLS).filter(name =>
|
...Object.keys(ORCHESTRATION_TOOLS).filter(name =>
|
||||||
name !== 'delegateToMiniRAH' &&
|
|
||||||
name !== 'delegateToWiseRAH' &&
|
name !== 'delegateToWiseRAH' &&
|
||||||
name !== 'executeWorkflow' &&
|
name !== 'executeWorkflow'
|
||||||
name !== 'delegateNodeQuotes' &&
|
|
||||||
name !== 'delegateNodeComparison'
|
|
||||||
),
|
),
|
||||||
...Object.keys(EXECUTION_TOOLS),
|
...Object.keys(EXECUTION_TOOLS),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Note: PLANNER_TOOL_NAMES kept for backwards compatibility but workflows now use specific tool sets
|
||||||
const PLANNER_TOOL_NAMES = [
|
const PLANNER_TOOL_NAMES = [
|
||||||
...Object.keys(CORE_TOOLS),
|
...Object.keys(CORE_TOOLS),
|
||||||
'webSearch',
|
'webSearch',
|
||||||
'think',
|
'think',
|
||||||
'delegateToMiniRAH',
|
'updateNode',
|
||||||
'updateNode', // For workflow execution (integrate workflow needs direct write access)
|
'createEdge',
|
||||||
'createEdge', // For edge creation in workflows
|
'quickLink',
|
||||||
'quickLink', // Fast edge creation via text search
|
'updateDimension',
|
||||||
'updateDimension', // For survey workflow (dimension analysis)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,6 +185,17 @@ export function getToolsForRole(role: 'orchestrator' | 'executor' | 'planner'):
|
|||||||
return getHelperTools(names);
|
return getHelperTools(names);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tools by their names (for workflow execution with specific tool sets)
|
||||||
|
*/
|
||||||
|
export function getToolsByNames(toolNames: string[]): Record<string, any> {
|
||||||
|
if (!Array.isArray(toolNames) || toolNames.length === 0) {
|
||||||
|
console.warn('[getToolsByNames] No tool names provided');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return getHelperTools(toolNames);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a tool with given parameters and context
|
* Execute a tool with given parameters and context
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
import { tool } from 'ai';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
|
||||||
import { MiniRAHExecutor } from '@/services/agents/executor';
|
|
||||||
import { RequestContext } from '@/services/context/requestContext';
|
|
||||||
import type { Node } from '@/types/database';
|
|
||||||
import { nodeService } from '@/services/database/nodes';
|
|
||||||
|
|
||||||
type CapsuleNodeRole = 'primary' | 'secondary' | 'referenced';
|
|
||||||
|
|
||||||
interface CapsuleNodeSnapshot {
|
|
||||||
id: number;
|
|
||||||
title: string | null;
|
|
||||||
link: string | null;
|
|
||||||
dimensions: string[];
|
|
||||||
chunkStatus: string;
|
|
||||||
hasChunks: boolean;
|
|
||||||
excerpt: string | null;
|
|
||||||
role: CapsuleNodeRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DelegationCapsule {
|
|
||||||
version: number;
|
|
||||||
generatedAt: string;
|
|
||||||
primary: CapsuleNodeSnapshot | null;
|
|
||||||
secondary: CapsuleNodeSnapshot[];
|
|
||||||
referenced: CapsuleNodeSnapshot[];
|
|
||||||
focusCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateWords(text: string, limit: number): string {
|
|
||||||
if (!text) return '';
|
|
||||||
const words = text.trim().split(/\s+/);
|
|
||||||
if (words.length <= limit) return text.trim();
|
|
||||||
return `${words.slice(0, limit).join(' ')}…`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function describeChunk(node: Node): { status: string; hasChunks: boolean } {
|
|
||||||
const status = node.chunk_status || 'unknown';
|
|
||||||
const hasChunks = status === 'chunked' || (typeof node.chunk === 'string' && node.chunk.length > 0);
|
|
||||||
return { status, hasChunks };
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSnapshot(node: Node, role: CapsuleNodeRole): CapsuleNodeSnapshot {
|
|
||||||
const primaryText = (node.content || node.description || '').trim();
|
|
||||||
const linkFallback = node.link || '';
|
|
||||||
const excerptSource = primaryText.length > 0 ? primaryText : linkFallback;
|
|
||||||
const { status, hasChunks } = describeChunk(node);
|
|
||||||
return {
|
|
||||||
id: node.id,
|
|
||||||
title: node.title || null,
|
|
||||||
link: node.link || null,
|
|
||||||
dimensions: node.dimensions || [],
|
|
||||||
chunkStatus: status,
|
|
||||||
hasChunks,
|
|
||||||
excerpt: excerptSource ? truncateWords(excerptSource, 80) : null,
|
|
||||||
role,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCapsuleForLLM(capsule: DelegationCapsule): string {
|
|
||||||
const lines: string[] = ['=== DELEGATION CAPSULE ==='];
|
|
||||||
if (capsule.primary) {
|
|
||||||
lines.push('Primary focus:', formatSnapshotLine(capsule.primary));
|
|
||||||
} else {
|
|
||||||
lines.push('Primary focus: None');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (capsule.secondary.length > 0) {
|
|
||||||
lines.push('Secondary focus nodes:');
|
|
||||||
capsule.secondary.forEach(snapshot => {
|
|
||||||
lines.push(`- ${formatSnapshotLine(snapshot)}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
lines.push('Secondary focus nodes: None');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (capsule.referenced.length > 0) {
|
|
||||||
lines.push('Referenced nodes provided in this task:');
|
|
||||||
capsule.referenced.forEach(snapshot => {
|
|
||||||
lines.push(`- ${formatSnapshotLine(snapshot)}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push('Instructions:',
|
|
||||||
'- Use the capsule snapshots as ground truth for node IDs and titles.',
|
|
||||||
'- Call getNodesById if you need the full record beyond the provided excerpt.',
|
|
||||||
'- List the node IDs you used in the "Context sources used" line of your final summary.',
|
|
||||||
'=== END CAPSULE ===');
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSnapshotLine(snapshot: CapsuleNodeSnapshot): string {
|
|
||||||
const dimensionLabel = snapshot.dimensions.length > 0 ? snapshot.dimensions.join(', ') : 'none';
|
|
||||||
const excerpt = snapshot.excerpt ? `Excerpt: ${snapshot.excerpt}` : 'Excerpt: (no stored content; hydrate via getNodesById if required)';
|
|
||||||
return `[NODE:${snapshot.id}:"${snapshot.title || 'Untitled'}"] | role=${snapshot.role} | dimensions=${dimensionLabel} | chunk_status=${snapshot.chunkStatus} | ${excerpt}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSourceBlock(snapshot: CapsuleNodeSnapshot): string {
|
|
||||||
return [
|
|
||||||
`=== SOURCE: NODE ${snapshot.id} ===`,
|
|
||||||
`Title: "${snapshot.title || 'Untitled'}"`,
|
|
||||||
`Role: ${snapshot.role}`,
|
|
||||||
`Chunk status: ${snapshot.chunkStatus} (has_chunks=${snapshot.hasChunks})`,
|
|
||||||
snapshot.link ? `Link: ${snapshot.link}` : 'Link: None',
|
|
||||||
snapshot.dimensions.length > 0 ? `Dimensions: ${snapshot.dimensions.join(', ')}` : 'Dimensions: None',
|
|
||||||
snapshot.excerpt ? `Excerpt: ${snapshot.excerpt}` : 'Excerpt: (not available) — call getNodesById if you need more context.',
|
|
||||||
'====================='
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hydrateReferencedNodes(nodeIds: number[]): Promise<Node[]> {
|
|
||||||
const nodes: Node[] = [];
|
|
||||||
for (const id of nodeIds) {
|
|
||||||
try {
|
|
||||||
const node = await nodeService.getNodeById(id);
|
|
||||||
if (node) {
|
|
||||||
nodes.push(node);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`delegateToMiniRAH: failed to load node ${id}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const delegateToMiniRAHTool = tool({
|
|
||||||
description: 'Delegate task to mini worker',
|
|
||||||
inputSchema: z.object({
|
|
||||||
task: z.string().describe('Clear, actionable description of what the mini ra-h should do'),
|
|
||||||
context: z.array(z.string()).max(16).default([]).describe('Optional context: URLs, node IDs, or key information the worker needs'),
|
|
||||||
expectedOutcome: z.string().optional().describe('Optional: what format or structure you expect in the summary'),
|
|
||||||
}),
|
|
||||||
execute: async ({ task, context = [], expectedOutcome }) => {
|
|
||||||
const requestContext = RequestContext.get();
|
|
||||||
const openTabs = (requestContext.openTabs ?? []) as Node[];
|
|
||||||
const activeTabId = requestContext.activeTabId ?? null;
|
|
||||||
|
|
||||||
const providedEntries = Array.isArray(context) ? context.filter(entry => typeof entry === 'string') as string[] : [];
|
|
||||||
const referencedNodeIds = new Set<number>();
|
|
||||||
const passthroughEntries: string[] = [];
|
|
||||||
|
|
||||||
providedEntries.forEach(entry => {
|
|
||||||
const trimmed = entry.trim();
|
|
||||||
const numericMatch = trimmed.match(/^(?:node_id:)?(\d+)$/i);
|
|
||||||
if (numericMatch) {
|
|
||||||
const id = Number(numericMatch[1]);
|
|
||||||
if (Number.isFinite(id) && id > 0) {
|
|
||||||
referencedNodeIds.add(id);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
passthroughEntries.push(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
const focusMap = new Map<number, Node>();
|
|
||||||
openTabs.forEach(node => {
|
|
||||||
if (node && typeof node.id === 'number') {
|
|
||||||
focusMap.set(node.id, node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const additionalIds = Array.from(referencedNodeIds).filter(id => !focusMap.has(id));
|
|
||||||
const referencedNodes = await hydrateReferencedNodes(additionalIds);
|
|
||||||
|
|
||||||
referencedNodes.forEach(node => focusMap.set(node.id, node));
|
|
||||||
|
|
||||||
const focusNodes = Array.from(focusMap.values());
|
|
||||||
const primaryNode = focusNodes.find(node => node.id === activeTabId) || focusNodes[0] || null;
|
|
||||||
const secondaryNodes = focusNodes.filter(node => primaryNode && node.id !== primaryNode.id && openTabs.some(tab => tab.id === node.id));
|
|
||||||
const referencedOnlyNodes = focusNodes.filter(node => !openTabs.some(tab => tab.id === node.id));
|
|
||||||
|
|
||||||
const capsule: DelegationCapsule = {
|
|
||||||
version: 1,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
primary: primaryNode ? buildSnapshot(primaryNode, 'primary') : null,
|
|
||||||
secondary: secondaryNodes.map(node => buildSnapshot(node, 'secondary')),
|
|
||||||
referenced: referencedOnlyNodes.map(node => buildSnapshot(node, 'referenced')),
|
|
||||||
focusCount: openTabs.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
const sourceBlocks: string[] = [];
|
|
||||||
const snapshots: CapsuleNodeSnapshot[] = [];
|
|
||||||
if (capsule.primary) snapshots.push(capsule.primary);
|
|
||||||
snapshots.push(...capsule.secondary, ...capsule.referenced);
|
|
||||||
snapshots.forEach(snapshot => {
|
|
||||||
sourceBlocks.push(buildSourceBlock(snapshot));
|
|
||||||
});
|
|
||||||
|
|
||||||
const enrichedContext: string[] = [
|
|
||||||
`CAPSULE_JSON::${JSON.stringify(capsule)}`,
|
|
||||||
formatCapsuleForLLM(capsule),
|
|
||||||
...sourceBlocks,
|
|
||||||
...passthroughEntries,
|
|
||||||
].slice(0, 16);
|
|
||||||
|
|
||||||
const delegation = AgentDelegationService.createDelegation({
|
|
||||||
task,
|
|
||||||
context: enrichedContext,
|
|
||||||
expectedOutcome,
|
|
||||||
supabaseToken: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const execution = await MiniRAHExecutor.execute({
|
|
||||||
sessionId: delegation.sessionId,
|
|
||||||
task,
|
|
||||||
context: enrichedContext,
|
|
||||||
expectedOutcome,
|
|
||||||
traceId: requestContext.traceId,
|
|
||||||
parentChatId: requestContext.parentChatId,
|
|
||||||
workflowKey: requestContext.workflowKey,
|
|
||||||
workflowNodeId: requestContext.workflowNodeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const summary = execution?.summary || 'Task delegated but no summary returned.';
|
|
||||||
const status = execution?.status || 'completed';
|
|
||||||
const sessionSuffix = delegation.sessionId.split('_').pop();
|
|
||||||
const statusLabel = status === 'completed' ? 'completed the task' : 'flagged an issue';
|
|
||||||
return `Mini ra-h (session ${sessionSuffix}) ${statusLabel}:\n\n${summary}`;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { tool } from 'ai';
|
import { tool } from 'ai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||||
import { WiseRAHExecutor } from '@/services/agents/wiseRAHExecutor';
|
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
|
||||||
import { RequestContext } from '@/services/context/requestContext';
|
import { RequestContext } from '@/services/context/requestContext';
|
||||||
|
|
||||||
export const delegateToWiseRAHTool = tool({
|
export const delegateToWiseRAHTool = tool({
|
||||||
@@ -25,7 +25,7 @@ export const delegateToWiseRAHTool = tool({
|
|||||||
supabaseToken: null,
|
supabaseToken: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const execution = await WiseRAHExecutor.execute({
|
const execution = await WorkflowExecutor.execute({
|
||||||
sessionId: delegation.sessionId,
|
sessionId: delegation.sessionId,
|
||||||
task,
|
task,
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { tool } from 'ai';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { delegateToMiniRAHTool } from './delegateToMiniRAH';
|
|
||||||
|
|
||||||
export const delegateNodeQuotesTool = tool({
|
|
||||||
description: 'Extract quotes from single node',
|
|
||||||
inputSchema: z.object({
|
|
||||||
nodeId: z.number().int().positive().describe('The node to read from'),
|
|
||||||
question: z.string().min(3).describe('The question or prompt the quotes should answer'),
|
|
||||||
maxQuotes: z.number().int().min(1).max(6).default(3).describe('Maximum number of quotes to return'),
|
|
||||||
requireSummary: z.boolean().default(false).describe('Whether to include a short synthesis after the quotes'),
|
|
||||||
}),
|
|
||||||
execute: async ({ nodeId, question, maxQuotes, requireSummary }) => {
|
|
||||||
const task = `Extract up to ${maxQuotes} ready-to-use verbatim quotes (include speaker/source if present) from [NODE:${nodeId}] that answer: "${question}". Provide each quote with a one-line takeaway.`;
|
|
||||||
const context = [String(nodeId), `REQUIRE_SYNTHESIS:${requireSummary ? 'yes' : 'no'}`];
|
|
||||||
const expectedOutcome = requireSummary
|
|
||||||
? 'Use the required Task/Actions/Result/Node/Context sources used/Follow-up template. In the Result line, list the quotes (prefixed with takeaways) first, then add a 2-3 sentence comparison summary. Always finish with "Context sources used" listing every NODE ID referenced.'
|
|
||||||
: 'Use the required Task/Actions/Result/Node/Context sources used/Follow-up template. In the Result line, list only the quotes (each prefixed with a takeaway) and end the summary with "Context sources used" covering every NODE ID referenced.';
|
|
||||||
|
|
||||||
if (typeof delegateToMiniRAHTool.execute !== 'function') {
|
|
||||||
throw new Error('delegateToMiniRAH tool is unavailable.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return delegateToMiniRAHTool.execute({
|
|
||||||
task,
|
|
||||||
context,
|
|
||||||
expectedOutcome,
|
|
||||||
}, undefined as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const delegateNodeComparisonTool = tool({
|
|
||||||
description: 'Create synthesis from gathered evidence',
|
|
||||||
inputSchema: z.object({
|
|
||||||
title: z.string().min(3).describe('Title for the synthesis node'),
|
|
||||||
comparisonPrompt: z.string().min(5).describe('Short description of what to compare or conclude'),
|
|
||||||
sourceNodeIds: z.array(z.number().int().positive()).min(1).max(8).describe('Source nodes that must be cited'),
|
|
||||||
includeOutline: z.boolean().default(false).describe('If true, worker should draft with numbered sections matching current outline in context'),
|
|
||||||
}),
|
|
||||||
execute: async ({ title, comparisonPrompt, sourceNodeIds, includeOutline }) => {
|
|
||||||
const task = `Using the gathered evidence, draft the final synthesis titled "${title}". Compare or conclude on: ${comparisonPrompt}.`;
|
|
||||||
const outlineHint = includeOutline
|
|
||||||
? 'Follow the outline provided in the context exactly (use the same headings).'
|
|
||||||
: 'Structure the answer with clear sections (Intro, Comparison, Takeaways).';
|
|
||||||
const contextEntries = sourceNodeIds.map(id => String(id));
|
|
||||||
contextEntries.push(outlineHint);
|
|
||||||
|
|
||||||
const expectedOutcome = 'Use the required Task/Actions/Result/Node/Context sources used/Follow-up template. In the Result line, deliver polished prose ready for createNode/updateNode, cite specific quotes inline where relevant, and finish with "Context sources used" listing every NODE ID you relied on.';
|
|
||||||
|
|
||||||
if (typeof delegateToMiniRAHTool.execute !== 'function') {
|
|
||||||
throw new Error('delegateToMiniRAH tool is unavailable.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return delegateToMiniRAHTool.execute({
|
|
||||||
task,
|
|
||||||
context: contextEntries,
|
|
||||||
expectedOutcome,
|
|
||||||
}, undefined as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|||||||
import { WorkflowRegistry } from '@/services/workflows/registry';
|
import { WorkflowRegistry } from '@/services/workflows/registry';
|
||||||
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
import { getSQLiteClient } from '@/services/database/sqlite-client';
|
||||||
import { AgentDelegationService } from '@/services/agents/delegation';
|
import { AgentDelegationService } from '@/services/agents/delegation';
|
||||||
import { WiseRAHExecutor } from '@/services/agents/wiseRAHExecutor';
|
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
|
||||||
import { RequestContext } from '@/services/context/requestContext';
|
import { RequestContext } from '@/services/context/requestContext';
|
||||||
import { getAutoContextSummaries } from '@/services/context/autoContext';
|
import { getAutoContextSummaries } from '@/services/context/autoContext';
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ ${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general wor
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fire-and-forget execution so main ra-h stays responsive while workflow runs
|
// Fire-and-forget execution so main ra-h stays responsive while workflow runs
|
||||||
void WiseRAHExecutor.execute({
|
void WorkflowExecutor.execute({
|
||||||
sessionId: delegation.sessionId,
|
sessionId: delegation.sessionId,
|
||||||
task,
|
task,
|
||||||
context: contextLines,
|
context: contextLines,
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { tool } from 'ai';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
// Get database path (same logic as sqlite-client.ts)
|
||||||
|
function getDatabasePath(): string {
|
||||||
|
return process.env.SQLITE_DB_PATH || path.join(
|
||||||
|
process.env.HOME || '~',
|
||||||
|
'Library/Application Support/RA-H/db/rah.sqlite'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: Only allow SELECT statements
|
||||||
|
function isReadOnlyQuery(sql: string): boolean {
|
||||||
|
const normalized = sql.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Must start with SELECT, WITH (for CTEs), or PRAGMA (for schema inspection)
|
||||||
|
const allowedPrefixes = ['select', 'with', 'pragma'];
|
||||||
|
const startsWithAllowed = allowedPrefixes.some(prefix =>
|
||||||
|
normalized.startsWith(prefix)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!startsWithAllowed) return false;
|
||||||
|
|
||||||
|
// Block dangerous patterns even in subqueries
|
||||||
|
const dangerousPatterns = [
|
||||||
|
/\binsert\b/i,
|
||||||
|
/\bupdate\b/i,
|
||||||
|
/\bdelete\b/i,
|
||||||
|
/\bdrop\b/i,
|
||||||
|
/\bcreate\b/i,
|
||||||
|
/\balter\b/i,
|
||||||
|
/\battach\b/i,
|
||||||
|
/\bdetach\b/i,
|
||||||
|
/\breindex\b/i,
|
||||||
|
/\bvacuum\b/i,
|
||||||
|
/\banalyze\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
return !dangerousPatterns.some(pattern => pattern.test(sql));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sqliteQueryTool = tool({
|
||||||
|
description: 'Execute read-only SQL queries (SELECT/WITH/PRAGMA). Tables: nodes, edges, dimensions, chunks. Use PRAGMA table_info(tablename) for schema.',
|
||||||
|
|
||||||
|
inputSchema: z.object({
|
||||||
|
sql: z.string().describe('The SQL query to execute. Must be a SELECT, WITH, or PRAGMA statement.'),
|
||||||
|
format: z.enum(['table', 'json', 'csv']).default('table').describe('Output format: table (default), json, or csv'),
|
||||||
|
}),
|
||||||
|
|
||||||
|
execute: async ({ sql, format = 'table' }) => {
|
||||||
|
console.log('🔍 SQLite Query tool called:', sql.substring(0, 100));
|
||||||
|
|
||||||
|
// Security check
|
||||||
|
if (!isReadOnlyQuery(sql)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Only SELECT, WITH, and PRAGMA statements are allowed. Write operations must use dedicated tools (createNode, updateNode, etc.).',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbPath = getDatabasePath();
|
||||||
|
|
||||||
|
// Build sqlite3 command with appropriate output mode
|
||||||
|
let modeFlag = '';
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
modeFlag = '-json';
|
||||||
|
break;
|
||||||
|
case 'csv':
|
||||||
|
modeFlag = '-csv -header';
|
||||||
|
break;
|
||||||
|
case 'table':
|
||||||
|
default:
|
||||||
|
modeFlag = '-header -column';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape the SQL for shell (replace single quotes)
|
||||||
|
const escapedSql = sql.replace(/'/g, "'\"'\"'");
|
||||||
|
const command = `sqlite3 ${modeFlag} "${dbPath}" '${escapedSql}'`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(command, {
|
||||||
|
timeout: 5000, // 5 second timeout
|
||||||
|
maxBuffer: 1024 * 1024, // 1MB max output
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stderr && !stdout) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: stderr,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON output if requested
|
||||||
|
let data: any = stdout.trim();
|
||||||
|
if (format === 'json' && data) {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
// Keep as string if parse fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count rows for message
|
||||||
|
let rowCount = 0;
|
||||||
|
if (format === 'json' && Array.isArray(data)) {
|
||||||
|
rowCount = data.length;
|
||||||
|
} else if (typeof data === 'string') {
|
||||||
|
// Count non-empty lines minus header
|
||||||
|
const lines = data.split('\n').filter(line => line.trim());
|
||||||
|
rowCount = Math.max(0, lines.length - 1); // Subtract header row
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
message: `Query returned ${rowCount} row${rowCount !== 1 ? 's' : ''}.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle timeout
|
||||||
|
if (error.killed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Query timed out after 5 seconds. Try a simpler query or add LIMIT.',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Query execution failed',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user