data fixes, partial custom indicator support
This commit is contained in:
@@ -12,10 +12,14 @@ import type { ModelMiddleware } from '../llm/middleware.js';
|
||||
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
|
||||
import type { ChannelAdapter, PathTriggerContext } from '../workspace/index.js';
|
||||
import type { ResearchSubagent } from './subagents/research/index.js';
|
||||
import type { IndicatorSubagent } from './subagents/indicator/index.js';
|
||||
import type { WebExploreSubagent } from './subagents/web-explore/index.js';
|
||||
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { getToolRegistry } from '../tools/tool-registry.js';
|
||||
import type { MCPToolInfo } from '../tools/mcp/mcp-tool-wrapper.js';
|
||||
import { createResearchAgentTool } from '../tools/platform/research-agent.tool.js';
|
||||
import { createIndicatorAgentTool } from '../tools/platform/indicator-agent.tool.js';
|
||||
import { createWebExploreAgentTool } from '../tools/platform/web-explore-agent.tool.js';
|
||||
import { createUserContext } from './memory/session-context.js';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join, dirname } from 'path';
|
||||
@@ -52,6 +56,8 @@ export interface AgentHarnessConfig extends HarnessSessionConfig {
|
||||
conversationStore?: ConversationStore;
|
||||
historyLimit: number;
|
||||
researchSubagent?: ResearchSubagent;
|
||||
indicatorSubagent?: IndicatorSubagent;
|
||||
webExploreSubagent?: WebExploreSubagent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,12 +85,17 @@ export class AgentHarness {
|
||||
private availableMCPTools: MCPToolInfo[] = [];
|
||||
private researchImageCapture: Array<{ data: string; mimeType: string }> = [];
|
||||
private conversationStore?: ConversationStore;
|
||||
private indicatorSubagent?: IndicatorSubagent;
|
||||
private webExploreSubagent?: WebExploreSubagent;
|
||||
private abortController: AbortController | null = null;
|
||||
|
||||
constructor(config: AgentHarnessConfig) {
|
||||
this.config = config;
|
||||
this.workspaceManager = config.workspaceManager;
|
||||
this.channelAdapter = config.channelAdapter;
|
||||
this.researchSubagent = config.researchSubagent;
|
||||
this.indicatorSubagent = config.indicatorSubagent;
|
||||
this.webExploreSubagent = config.webExploreSubagent;
|
||||
|
||||
this.modelFactory = new LLMProviderFactory(config.providerConfig, config.logger);
|
||||
this.modelRouter = new ModelRouter(this.modelFactory, config.logger);
|
||||
@@ -117,6 +128,10 @@ export class AgentHarness {
|
||||
this.channelAdapter = adapter;
|
||||
}
|
||||
|
||||
interrupt(): void {
|
||||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize harness and connect to user's MCP server
|
||||
*/
|
||||
@@ -132,9 +147,15 @@ export class AgentHarness {
|
||||
// Discover available MCP tools from user's server
|
||||
await this.discoverMCPTools();
|
||||
|
||||
// Initialize web explore subagent first — research and indicator subagents inject it as a tool
|
||||
await this.initializeWebExploreSubagent();
|
||||
|
||||
// Initialize research subagent if not provided
|
||||
await this.initializeResearchSubagent();
|
||||
|
||||
// Initialize indicator subagent if not provided
|
||||
await this.initializeIndicatorSubagent();
|
||||
|
||||
this.config.logger.info('Agent harness initialized');
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error }, 'Failed to initialize agent harness');
|
||||
@@ -214,6 +235,24 @@ export class AgentHarness {
|
||||
(img) => this.researchImageCapture.push(img)
|
||||
);
|
||||
|
||||
// Inject web_explore tool if the web-explore subagent is ready
|
||||
if (this.webExploreSubagent) {
|
||||
const webExploreContext = {
|
||||
userContext: createUserContext({
|
||||
userId: this.config.userId,
|
||||
sessionId: this.config.sessionId,
|
||||
license: this.config.license,
|
||||
channelType: this.config.channelType ?? ChannelType.WEBSOCKET,
|
||||
channelUserId: this.config.channelUserId ?? this.config.userId,
|
||||
}),
|
||||
};
|
||||
researchTools.push(createWebExploreAgentTool({
|
||||
webExploreSubagent: this.webExploreSubagent,
|
||||
context: webExploreContext,
|
||||
logger: this.config.logger,
|
||||
}));
|
||||
}
|
||||
|
||||
// Path resolution: use the compiled output path
|
||||
const researchSubagentPath = join(__dirname, 'subagents', 'research');
|
||||
this.config.logger.debug({ researchSubagentPath }, 'Using research subagent path');
|
||||
@@ -243,6 +282,143 @@ export class AgentHarness {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize indicator subagent
|
||||
*/
|
||||
private async initializeIndicatorSubagent(): Promise<void> {
|
||||
if (this.indicatorSubagent) {
|
||||
this.config.logger.debug('Indicator subagent already provided');
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.logger.debug('Creating indicator subagent for session');
|
||||
|
||||
try {
|
||||
const { createIndicatorSubagent } = await import('./subagents/indicator/index.js');
|
||||
|
||||
const { model } = await this.modelRouter.route(
|
||||
'indicator management',
|
||||
this.config.license,
|
||||
RoutingStrategy.COMPLEXITY,
|
||||
this.config.userId
|
||||
);
|
||||
|
||||
const toolRegistry = getToolRegistry();
|
||||
const indicatorTools = await toolRegistry.getToolsForAgent(
|
||||
'indicator',
|
||||
this.mcpClient,
|
||||
this.availableMCPTools,
|
||||
this.workspaceManager,
|
||||
undefined, // no image callback
|
||||
(storeName, newState) => {
|
||||
// After a workspace_patch succeeds in the container, update the gateway's
|
||||
// WorkspaceManager so it pushes a WebSocket patch to the web client.
|
||||
this.workspaceManager?.setState(storeName, newState).catch((err) =>
|
||||
this.config.logger.error({ err, storeName }, 'Failed to sync workspace after indicator mutation')
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Inject web_explore tool if the web-explore subagent is ready
|
||||
if (this.webExploreSubagent) {
|
||||
const webExploreContext = {
|
||||
userContext: createUserContext({
|
||||
userId: this.config.userId,
|
||||
sessionId: this.config.sessionId,
|
||||
license: this.config.license,
|
||||
channelType: this.config.channelType ?? ChannelType.WEBSOCKET,
|
||||
channelUserId: this.config.channelUserId ?? this.config.userId,
|
||||
}),
|
||||
};
|
||||
indicatorTools.push(createWebExploreAgentTool({
|
||||
webExploreSubagent: this.webExploreSubagent,
|
||||
context: webExploreContext,
|
||||
logger: this.config.logger,
|
||||
}));
|
||||
}
|
||||
|
||||
const indicatorSubagentPath = join(__dirname, 'subagents', 'indicator');
|
||||
this.config.logger.debug({ indicatorSubagentPath }, 'Using indicator subagent path');
|
||||
|
||||
this.indicatorSubagent = await createIndicatorSubagent(
|
||||
model,
|
||||
this.config.logger,
|
||||
indicatorSubagentPath,
|
||||
this.mcpClient,
|
||||
indicatorTools
|
||||
);
|
||||
|
||||
this.config.logger.info(
|
||||
{
|
||||
toolCount: indicatorTools.length,
|
||||
toolNames: indicatorTools.map(t => t.name),
|
||||
},
|
||||
'Indicator subagent created successfully'
|
||||
);
|
||||
} catch (error) {
|
||||
this.config.logger.error(
|
||||
{ error, errorMessage: (error as Error).message, stack: (error as Error).stack },
|
||||
'Failed to create indicator subagent'
|
||||
);
|
||||
// Don't throw — indicator subagent is optional
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize web explore subagent
|
||||
*/
|
||||
private async initializeWebExploreSubagent(): Promise<void> {
|
||||
if (this.webExploreSubagent) {
|
||||
this.config.logger.debug('Web explore subagent already provided');
|
||||
return;
|
||||
}
|
||||
|
||||
this.config.logger.debug('Creating web explore subagent for session');
|
||||
|
||||
try {
|
||||
const { createWebExploreSubagent } = await import('./subagents/web-explore/index.js');
|
||||
|
||||
const { model } = await this.modelRouter.route(
|
||||
'web research and summarization',
|
||||
this.config.license,
|
||||
RoutingStrategy.COMPLEXITY,
|
||||
this.config.userId
|
||||
);
|
||||
|
||||
const toolRegistry = getToolRegistry();
|
||||
const webExploreTools = await toolRegistry.getToolsForAgent(
|
||||
'web-explore',
|
||||
undefined, // no MCP client needed
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
const webExploreSubagentPath = join(__dirname, 'subagents', 'web-explore');
|
||||
this.config.logger.debug({ webExploreSubagentPath }, 'Using web explore subagent path');
|
||||
|
||||
this.webExploreSubagent = await createWebExploreSubagent(
|
||||
model,
|
||||
this.config.logger,
|
||||
webExploreSubagentPath,
|
||||
webExploreTools
|
||||
);
|
||||
|
||||
this.config.logger.info(
|
||||
{
|
||||
toolCount: webExploreTools.length,
|
||||
toolNames: webExploreTools.map(t => t.name),
|
||||
},
|
||||
'Web explore subagent created successfully'
|
||||
);
|
||||
} catch (error) {
|
||||
this.config.logger.error(
|
||||
{ error, errorMessage: (error as Error).message, stack: (error as Error).stack },
|
||||
'Failed to create web explore subagent'
|
||||
);
|
||||
// Don't throw — web explore subagent is optional
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute model with tool calling loop
|
||||
* Handles multi-turn tool calls until the model produces a final text response
|
||||
@@ -251,7 +427,8 @@ export class AgentHarness {
|
||||
model: any,
|
||||
messages: BaseMessage[],
|
||||
tools: DynamicStructuredTool[],
|
||||
maxIterations: number = 2
|
||||
maxIterations: number = 2,
|
||||
signal?: AbortSignal
|
||||
): Promise<string> {
|
||||
this.config.logger.info(
|
||||
{ toolCount: tools.length, maxIterations },
|
||||
@@ -262,6 +439,7 @@ export class AgentHarness {
|
||||
let iterations = 0;
|
||||
|
||||
while (iterations < maxIterations) {
|
||||
if (signal?.aborted) break;
|
||||
iterations++;
|
||||
this.config.logger.info(
|
||||
{
|
||||
@@ -275,7 +453,7 @@ export class AgentHarness {
|
||||
this.config.logger.debug('Streaming model response...');
|
||||
let response: any = null;
|
||||
try {
|
||||
const stream = await model.stream(messagesCopy);
|
||||
const stream = await model.stream(messagesCopy, { signal });
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.content === 'string' && chunk.content.length > 0) {
|
||||
this.channelAdapter?.sendChunk(chunk.content);
|
||||
@@ -415,6 +593,29 @@ export class AgentHarness {
|
||||
return 'I apologize, but I encountered an issue processing your request. Please try rephrasing your question.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a tool on the user's MCP server directly (bypasses the agent/LLM).
|
||||
* Used by channel handlers for direct data requests (e.g. evaluate_indicator).
|
||||
*/
|
||||
async callMcpTool(name: string, args: Record<string, unknown>): Promise<unknown> {
|
||||
return this.mcpClient.callTool(name, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose MCP client so channel handlers can wire ContainerSync after harness init.
|
||||
*/
|
||||
getMcpClient(): MCPClientConnector {
|
||||
return this.mcpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set workspace manager after construction (used when ContainerSync requires MCP to be connected first).
|
||||
*/
|
||||
setWorkspaceManager(workspace: WorkspaceManager): void {
|
||||
this.workspaceManager = workspace;
|
||||
this.registerWorkspaceTriggers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming message from user
|
||||
*/
|
||||
@@ -480,18 +681,19 @@ export class AgentHarness {
|
||||
this.workspaceManager // Pass session workspace manager
|
||||
);
|
||||
|
||||
// Build shared subagent context
|
||||
const subagentContext = {
|
||||
userContext: createUserContext({
|
||||
userId: this.config.userId,
|
||||
sessionId: this.config.sessionId,
|
||||
license: this.config.license,
|
||||
channelType: this.config.channelType ?? ChannelType.WEBSOCKET,
|
||||
channelUserId: this.config.channelUserId ?? this.config.userId,
|
||||
}),
|
||||
};
|
||||
|
||||
// Add research subagent as a tool if available
|
||||
if (this.researchSubagent) {
|
||||
const subagentContext = {
|
||||
userContext: createUserContext({
|
||||
userId: this.config.userId,
|
||||
sessionId: this.config.sessionId,
|
||||
license: this.config.license,
|
||||
channelType: this.config.channelType ?? ChannelType.WEBSOCKET,
|
||||
channelUserId: this.config.channelUserId ?? this.config.userId,
|
||||
}),
|
||||
};
|
||||
|
||||
tools.push(createResearchAgentTool({
|
||||
researchSubagent: this.researchSubagent,
|
||||
context: subagentContext,
|
||||
@@ -499,6 +701,24 @@ export class AgentHarness {
|
||||
}));
|
||||
}
|
||||
|
||||
// Add indicator subagent as a tool if available
|
||||
if (this.indicatorSubagent) {
|
||||
tools.push(createIndicatorAgentTool({
|
||||
indicatorSubagent: this.indicatorSubagent,
|
||||
context: subagentContext,
|
||||
logger: this.config.logger,
|
||||
}));
|
||||
}
|
||||
|
||||
// Add web explore subagent as a tool if available
|
||||
if (this.webExploreSubagent) {
|
||||
tools.push(createWebExploreAgentTool({
|
||||
webExploreSubagent: this.webExploreSubagent,
|
||||
context: subagentContext,
|
||||
logger: this.config.logger,
|
||||
}));
|
||||
}
|
||||
|
||||
this.config.logger.info(
|
||||
{
|
||||
toolCount: tools.length,
|
||||
@@ -524,7 +744,9 @@ export class AgentHarness {
|
||||
|
||||
// 8. Call LLM with tool calling loop
|
||||
this.config.logger.info('Invoking LLM with tool support');
|
||||
const assistantMessage = await this.executeWithToolCalling(modelWithTools, processedMessages, tools, 10);
|
||||
this.abortController = new AbortController();
|
||||
const assistantMessage = await this.executeWithToolCalling(modelWithTools, processedMessages, tools, 10, this.abortController.signal);
|
||||
this.abortController = null;
|
||||
|
||||
this.config.logger.info(
|
||||
{ responseLength: assistantMessage.length },
|
||||
@@ -587,13 +809,17 @@ export class AgentHarness {
|
||||
private getToolLabel(toolName: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
research: 'Researching...',
|
||||
indicator: 'Adjusting indicators...',
|
||||
get_chart_data: 'Fetching chart data...',
|
||||
symbol_lookup: 'Searching symbol...',
|
||||
category_list: 'Seeing what we have...',
|
||||
category_edit: 'Coding...',
|
||||
category_write: 'Coding...',
|
||||
category_read: 'Inspecting...',
|
||||
python_list: 'Seeing what we have...',
|
||||
python_edit: 'Coding...',
|
||||
python_write: 'Coding...',
|
||||
python_read: 'Inspecting...',
|
||||
execute_research: 'Running script...',
|
||||
backtest_strategy: 'Running backtest...',
|
||||
list_active_strategies: 'Checking active strategies...',
|
||||
web_explore: 'Searching the web...',
|
||||
};
|
||||
return labels[toolName] ?? `Running ${toolName}...`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user