data fixes, partial custom indicator support
This commit is contained in:
@@ -10,6 +10,7 @@ import type { SymbolIndexService } from '../services/symbol-index-service.js';
|
||||
import type { ContainerManager } from '../k8s/container-manager.js';
|
||||
import {
|
||||
WorkspaceManager,
|
||||
ContainerSync,
|
||||
DEFAULT_STORES,
|
||||
type ChannelAdapter,
|
||||
type ChannelCapabilities,
|
||||
@@ -120,15 +121,6 @@ export class WebSocketHandler {
|
||||
|
||||
sendStatus(socket, 'initializing', 'Starting your workspace...');
|
||||
|
||||
// Create workspace manager for this session
|
||||
const workspace = new WorkspaceManager({
|
||||
userId: authContext.userId,
|
||||
sessionId: authContext.sessionId,
|
||||
stores: DEFAULT_STORES,
|
||||
// containerSync will be added when MCP client is implemented
|
||||
logger,
|
||||
});
|
||||
|
||||
// Create WebSocket channel adapter
|
||||
const wsAdapter: ChannelAdapter = {
|
||||
sendSnapshot: (msg: SnapshotMessage) => {
|
||||
@@ -174,31 +166,47 @@ export class WebSocketHandler {
|
||||
}),
|
||||
};
|
||||
|
||||
// Declare harness outside try block so it's available in catch
|
||||
// Declare harness and workspace outside try block so they're available in catch
|
||||
let harness: AgentHarness | undefined;
|
||||
let workspace: WorkspaceManager | undefined;
|
||||
|
||||
try {
|
||||
// Initialize workspace first
|
||||
await workspace.initialize();
|
||||
workspace.setAdapter(wsAdapter);
|
||||
this.workspaces.set(authContext.sessionId, workspace);
|
||||
|
||||
// Create agent harness via factory (storage deps injected by factory)
|
||||
// Create and connect harness first so MCP client is available for ContainerSync
|
||||
harness = this.config.createHarness({
|
||||
userId: authContext.userId,
|
||||
sessionId: authContext.sessionId,
|
||||
license: authContext.license,
|
||||
mcpServerUrl: authContext.mcpServerUrl,
|
||||
logger,
|
||||
workspaceManager: workspace,
|
||||
channelAdapter: wsAdapter,
|
||||
channelType: authContext.channelType,
|
||||
channelUserId: authContext.channelUserId,
|
||||
});
|
||||
|
||||
await harness.initialize();
|
||||
|
||||
// Wire ContainerSync now that MCP client is connected, then initialize workspace
|
||||
const containerSync = new ContainerSync(harness.getMcpClient(), logger);
|
||||
workspace = new WorkspaceManager({
|
||||
userId: authContext.userId,
|
||||
sessionId: authContext.sessionId,
|
||||
stores: DEFAULT_STORES,
|
||||
containerSync,
|
||||
logger,
|
||||
});
|
||||
|
||||
await workspace.initialize();
|
||||
workspace.setAdapter(wsAdapter);
|
||||
harness.setWorkspaceManager(workspace);
|
||||
this.workspaces.set(authContext.sessionId, workspace);
|
||||
this.harnesses.set(authContext.sessionId, harness);
|
||||
|
||||
// Push all store snapshots to the client now, before 'connected'.
|
||||
// Empty seqs force full snapshots for every store, so the browser's
|
||||
// message queue has the current workspace state (including persistent
|
||||
// stores loaded from the container) before TradingView initializes.
|
||||
await workspace.handleHello({});
|
||||
|
||||
// Register session for event system
|
||||
// Container endpoint is derived from the MCP server URL (same container, different port)
|
||||
const containerEventEndpoint = this.getContainerEventEndpoint(authContext.mcpServerUrl);
|
||||
@@ -287,15 +295,18 @@ export class WebSocketHandler {
|
||||
} else if (payload.type === 'hello') {
|
||||
// Workspace sync: hello message
|
||||
logger.debug({ seqs: payload.seqs }, 'Handling workspace hello');
|
||||
await workspace.handleHello(payload.seqs || {});
|
||||
await workspace!.handleHello(payload.seqs || {});
|
||||
} else if (payload.type === 'patch') {
|
||||
// Workspace sync: patch message
|
||||
logger.debug({ store: payload.store, seq: payload.seq }, 'Handling workspace patch');
|
||||
await workspace.handlePatch(payload.store, payload.seq, payload.patch || []);
|
||||
await workspace!.handlePatch(payload.store, payload.seq, payload.patch || []);
|
||||
} else if (payload.type === 'agent_stop') {
|
||||
logger.info('Agent stop requested');
|
||||
harness?.interrupt();
|
||||
} else if (this.isDatafeedMessage(payload)) {
|
||||
// Historical data request - send to OHLC service
|
||||
logger.info({ type: payload.type }, 'Routing to datafeed handler');
|
||||
await this.handleDatafeedMessage(socket, payload, logger);
|
||||
await this.handleDatafeedMessage(socket, payload, logger, authContext);
|
||||
} else {
|
||||
logger.warn({ type: payload.type }, 'Unknown message type received');
|
||||
}
|
||||
@@ -322,7 +333,7 @@ export class WebSocketHandler {
|
||||
}
|
||||
|
||||
// Cleanup workspace
|
||||
await workspace.shutdown();
|
||||
await workspace!.shutdown();
|
||||
this.workspaces.delete(authContext.sessionId);
|
||||
|
||||
// Cleanup harness
|
||||
@@ -346,8 +357,10 @@ export class WebSocketHandler {
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to initialize session');
|
||||
socket.close(1011, 'Internal server error');
|
||||
await workspace.shutdown();
|
||||
this.workspaces.delete(authContext.sessionId);
|
||||
if (workspace) {
|
||||
await workspace.shutdown();
|
||||
this.workspaces.delete(authContext.sessionId);
|
||||
}
|
||||
if (harness) {
|
||||
await harness.cleanup();
|
||||
}
|
||||
@@ -382,6 +395,7 @@ export class WebSocketHandler {
|
||||
'get_bars',
|
||||
'subscribe_bars',
|
||||
'unsubscribe_bars',
|
||||
'evaluate_indicator',
|
||||
];
|
||||
return datafeedTypes.includes(payload.type);
|
||||
}
|
||||
@@ -392,7 +406,8 @@ export class WebSocketHandler {
|
||||
private async handleDatafeedMessage(
|
||||
socket: WebSocket,
|
||||
payload: any,
|
||||
logger: any
|
||||
logger: any,
|
||||
authContext?: any
|
||||
): Promise<void> {
|
||||
logger.info({ type: payload.type, payload }, 'handleDatafeedMessage called');
|
||||
const ohlcService = this.config.ohlcService;
|
||||
@@ -526,6 +541,69 @@ export class WebSocketHandler {
|
||||
);
|
||||
break;
|
||||
|
||||
case 'evaluate_indicator': {
|
||||
// Direct MCP call — bypasses the agent/LLM for performance
|
||||
const harness = this.harnesses.get(authContext.sessionId);
|
||||
if (!harness) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'evaluate_indicator_result',
|
||||
request_id: requestId,
|
||||
error: 'Session not initialized',
|
||||
}));
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const mcpResult = await harness.callMcpTool('evaluate_indicator', {
|
||||
symbol: payload.symbol,
|
||||
from_time: payload.from_time,
|
||||
to_time: payload.to_time,
|
||||
period_seconds: payload.period_seconds,
|
||||
pandas_ta_name: payload.pandas_ta_name,
|
||||
parameters: payload.parameters ?? {},
|
||||
}) as any;
|
||||
// MCP returns { content: [{type: 'text', text: '...json...'}] }
|
||||
// When the tool raises an exception, the MCP framework sets isError: true
|
||||
// and puts the raw exception text in content[0].text (not JSON-wrapped).
|
||||
const rawText = mcpResult?.content?.[0]?.text ?? mcpResult?.[0]?.text;
|
||||
if (mcpResult?.isError || rawText == null) {
|
||||
const errMsg = rawText ?? 'evaluate_indicator returned no content';
|
||||
logger.error({ pandas_ta_name: payload.pandas_ta_name, rawText }, 'evaluate_indicator sandbox error');
|
||||
socket.send(JSON.stringify({
|
||||
type: 'evaluate_indicator_result',
|
||||
request_id: requestId,
|
||||
error: errMsg,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
let data: any;
|
||||
try {
|
||||
data = JSON.parse(rawText);
|
||||
} catch {
|
||||
// Sandbox returned non-JSON (e.g. bare exception text)
|
||||
logger.error({ pandas_ta_name: payload.pandas_ta_name, rawText }, 'evaluate_indicator returned non-JSON');
|
||||
socket.send(JSON.stringify({
|
||||
type: 'evaluate_indicator_result',
|
||||
request_id: requestId,
|
||||
error: rawText,
|
||||
}));
|
||||
break;
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
type: 'evaluate_indicator_result',
|
||||
request_id: requestId,
|
||||
...data,
|
||||
}));
|
||||
} catch (err: any) {
|
||||
logger.error({ err: err?.message, pandas_ta_name: payload.pandas_ta_name }, 'evaluate_indicator handler error');
|
||||
socket.send(JSON.stringify({
|
||||
type: 'evaluate_indicator_result',
|
||||
request_id: requestId,
|
||||
error: err?.message ?? String(err),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
logger.warn({ type: payload.type }, 'Unknown datafeed message type');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user