From 5021138da691b6625f9f2f68132e00ea451e72e0 Mon Sep 17 00:00:00 2001 From: Tim Olson Date: Mon, 13 Apr 2026 20:58:40 -0400 Subject: [PATCH] bugfix; web tabs --- doc/test_prompt.md | 4 + gateway/src/clients/duckdb-client.ts | 10 +- .../src/harness/subagents/base-subagent.ts | 229 +++++++++++--- .../src/harness/subagents/indicator/index.ts | 136 +-------- .../src/harness/subagents/research/index.ts | 170 ++--------- .../src/harness/subagents/strategy/index.ts | 136 +-------- .../harness/subagents/web-explore/index.ts | 107 +------ web/src/App.vue | 39 ++- web/src/components/BottomTray.vue | 279 ++++++++++++++++++ web/src/components/tabs/OrdersTab.vue | 69 +++++ web/src/components/tabs/PlaceholderTab.vue | 20 ++ 11 files changed, 634 insertions(+), 565 deletions(-) create mode 100644 doc/test_prompt.md create mode 100644 web/src/components/BottomTray.vue create mode 100644 web/src/components/tabs/OrdersTab.vue create mode 100644 web/src/components/tabs/PlaceholderTab.vue diff --git a/doc/test_prompt.md b/doc/test_prompt.md new file mode 100644 index 00000000..87689530 --- /dev/null +++ b/doc/test_prompt.md @@ -0,0 +1,4 @@ +what conclusions can you make by analyzing historical data on ETH price direction changes near market session overlaps and market sessions changes on monday and tuesday? + +--- + diff --git a/gateway/src/clients/duckdb-client.ts b/gateway/src/clients/duckdb-client.ts index 866d4347..804b490f 100644 --- a/gateway/src/clients/duckdb-client.ts +++ b/gateway/src/clients/duckdb-client.ts @@ -196,7 +196,7 @@ export class DuckDBClient { // Fallback: scan Parquet files written directly to conversations bucket if (this.conversationsBucket) { this.logger.debug({ userId, sessionId }, 'REST catalog miss, scanning Parquet cold storage'); - const parquetPath = `s3://${this.conversationsBucket}/gateway/conversations/**/user_id=${userId}/${sessionId}.parquet`; + const parquetPath = `s3://${this.conversationsBucket}/gateway/conversations/year=*/month=*/user_id=${userId}/${sessionId}.parquet`; const fallbackSql = ` SELECT id, user_id, session_id, role, content, metadata, timestamp FROM read_parquet('${parquetPath}') @@ -709,15 +709,15 @@ export class DuckDBClient { if (!tablePath) { // Fallback: scan per-turn Parquet files written directly to S3 if (this.conversationsBucket) { - this.logger.debug({ userId, sessionId }, 'REST catalog miss, scanning blob Parquet files'); - const parquetPath = `s3://${this.conversationsBucket}/gateway/blobs/**/user_id=${userId}/${sessionId}_*.parquet`; + this.logger.info({ userId, sessionId }, 'REST catalog miss, scanning blob Parquet files'); + const parquetPath = `s3://${this.conversationsBucket}/gateway/blobs/year=*/month=*/user_id=${userId}/${sessionId}_*.parquet`; const idClause = blobIds?.length ? `WHERE id IN (${blobIds.map(id => `'${id.replace(/'/g, "''")}'`).join(', ')})` : ''; try { return await this.query(`SELECT * FROM read_parquet('${parquetPath}') ${idClause} ORDER BY timestamp ASC`); - } catch { - // No blobs yet for this session + } catch (err: any) { + this.logger.debug({ err: err.message, userId, sessionId }, 'No blob Parquet files found for session'); } } return []; diff --git a/gateway/src/harness/subagents/base-subagent.ts b/gateway/src/harness/subagents/base-subagent.ts index 4101285e..5ffb7fae 100644 --- a/gateway/src/harness/subagents/base-subagent.ts +++ b/gateway/src/harness/subagents/base-subagent.ts @@ -8,6 +8,8 @@ import type { DynamicStructuredTool } from '@langchain/core/tools'; import { readFile } from 'fs/promises'; import { join } from 'path'; import type { HarnessEvent, SubagentChunkEvent, SubagentThinkingEvent } from '../harness-events.js'; +import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import yaml from 'js-yaml'; /** * Subagent configuration (loaded from config.yaml) @@ -73,44 +75,164 @@ export abstract class BaseSubagent { this.tools = tools || []; } - /** - * Initialize subagent: load system prompt and memory files - */ - async initialize(basePath: string): Promise { - this.logger.info({ subagent: this.config.name }, 'Initializing subagent'); + /** Per-subagent recursion limit for the LangGraph agent loop */ + protected abstract getRecursionLimit(): number; - // Load system prompt - if (this.config.systemPromptFile) { - const promptPath = join(basePath, this.config.systemPromptFile); - this.systemPrompt = await this.loadFile(promptPath); - } + /** Fallback text returned when the agent produces no output */ + protected abstract getFallbackText(): string; - // Load memory files - for (const memoryFile of this.config.memoryFiles) { - const memoryPath = join(basePath, 'memory', memoryFile); - const content = await this.loadFile(memoryPath); - if (content) { - this.memoryContext.push(`# ${memoryFile}\n\n${content}`); - } - } - - this.logger.info( - { - subagent: this.config.name, - memoryFiles: this.config.memoryFiles.length, - systemPromptLoaded: !!this.systemPrompt, - }, - 'Subagent initialized' - ); + /** Whether an MCP client is required; defaults true. Override to false for tool-only subagents. */ + protected requiresMCPClient(): boolean { + return true; } /** - * Execute subagent with given input + * Build the system message and final human message for agent invocation. + * Subclasses may override to augment the system message (e.g. injecting dynamic context). */ - abstract execute( + protected async buildSystemMessage( context: SubagentContext, - input: string - ): Promise; + instruction: string + ): Promise<{ systemMessage: SystemMessage; humanMessage: BaseMessage }> { + const msgs = this.buildMessages(context, instruction); + return { + systemMessage: msgs[0] as SystemMessage, + humanMessage: msgs[msgs.length - 1], + }; + } + + /** + * Shared execute body. Subclasses that need pre/post hooks (e.g. image capture) + * override execute() and call this method internally. + */ + protected async executeAgent(context: SubagentContext, instruction: string): Promise { + this.logger.info( + { + subagent: this.getName(), + userId: context.userContext.userId, + instruction: instruction.substring(0, 200), + toolCount: this.tools.length, + toolNames: this.tools.map(t => t.name), + }, + `${this.config.name} subagent starting` + ); + + if (this.requiresMCPClient() && !this.hasMCPClient()) { + throw new Error(`MCP client not available for ${this.config.name} subagent`); + } + + if (this.requiresMCPClient() && this.tools.length === 0) { + this.logger.warn(`${this.config.name} subagent has no tools`); + } + + const { systemMessage, humanMessage } = await this.buildSystemMessage(context, instruction); + + const agent = createReactAgent({ + llm: this.model, + tools: this.tools, + prompt: systemMessage, + }); + + const result = await agent.invoke( + { messages: [humanMessage] }, + { recursionLimit: this.getRecursionLimit() } + ); + + const allMessages: any[] = result.messages ?? []; + + this.logger.info( + { messageCount: allMessages.length }, + `${this.config.name} subagent graph completed` + ); + + const lastAI = [...allMessages].reverse().find( + (m: any) => m.constructor?.name === 'AIMessage' || m._getType?.() === 'ai' + ); + + const finalText = lastAI + ? (typeof lastAI.content === 'string' ? lastAI.content : JSON.stringify(lastAI.content)) + : this.getFallbackText(); + + this.logger.info({ textLength: finalText.length }, `${this.config.name} subagent finished`); + + return finalText; + } + + /** + * Execute subagent. Delegates to executeAgent by default; + * subclasses with side-effects (image capture, etc.) override this. + */ + async execute(context: SubagentContext, instruction: string): Promise { + return this.executeAgent(context, instruction); + } + + /** + * Shared streamEvents loop. Subclasses that need pre/post hooks override streamEvents() + * and delegate here via `yield* this.streamEventsCore(...)`. + */ + protected async *streamEventsCore( + context: SubagentContext, + instruction: string, + signal?: AbortSignal, + ): AsyncGenerator { + this.logger.info({ subagent: this.getName() }, 'streamEvents starting'); + + if (this.requiresMCPClient() && !this.hasMCPClient()) { + throw new Error(`MCP client not available for ${this.config.name} subagent`); + } + + const { systemMessage, humanMessage } = await this.buildSystemMessage(context, instruction); + + const agent = createReactAgent({ + llm: this.model, + tools: this.tools, + prompt: systemMessage, + }); + + const stream = agent.stream( + { messages: [humanMessage] }, + { streamMode: ['messages', 'updates'], recursionLimit: this.getRecursionLimit(), signal } + ); + + let finalText = ''; + + for await (const [mode, data] of await stream) { + if (signal?.aborted) break; + if (mode === 'messages') { + for (const chunk of BaseSubagent.extractStreamChunks(data, this.config.name)) { + yield chunk; + } + } else if (mode === 'updates') { + if ((data as any).agent?.messages) { + for (const msg of (data as any).agent.messages as any[]) { + if (msg.tool_calls?.length) { + for (const tc of msg.tool_calls) { + yield { type: 'subagent_tool_call', agentName: this.config.name, toolName: tc.name, label: tc.name }; + } + } else { + const content = BaseSubagent.extractFinalText(msg); + if (content) finalText = content; + } + } + } + } + } + + this.logger.info({ textLength: finalText.length }, 'streamEvents finished'); + return finalText; + } + + /** + * Stream typed HarnessEvents during execution. Delegates to streamEventsCore by default. + * Subclasses with pre/post logic override this and use `yield* this.streamEventsCore(...)`. + */ + async *streamEvents( + context: SubagentContext, + input: string, + signal?: AbortSignal, + ): AsyncGenerator { + return yield* this.streamEventsCore(context, input, signal); + } /** * Stream execution (optional, default to non-streaming) @@ -169,17 +291,42 @@ export abstract class BaseSubagent { } /** - * Stream typed HarnessEvents during execution. - * Subclasses override this to emit subagent_chunk / subagent_tool_call events - * using agent.stream() from LangGraph. Default falls back to execute(). + * Initialize subagent: load system prompt and memory files */ - async *streamEvents( - context: SubagentContext, - input: string, - _signal?: AbortSignal, - ): AsyncGenerator { - const result = await this.execute(context, input); - return result; + async initialize(basePath: string): Promise { + this.logger.info({ subagent: this.config.name }, 'Initializing subagent'); + + // Load system prompt + if (this.config.systemPromptFile) { + const promptPath = join(basePath, this.config.systemPromptFile); + this.systemPrompt = await this.loadFile(promptPath); + } + + // Load memory files + for (const memoryFile of this.config.memoryFiles) { + const memoryPath = join(basePath, 'memory', memoryFile); + const content = await this.loadFile(memoryPath); + if (content) { + this.memoryContext.push(`# ${memoryFile}\n\n${content}`); + } + } + + this.logger.info( + { + subagent: this.config.name, + memoryFiles: this.config.memoryFiles.length, + systemPromptLoaded: !!this.systemPrompt, + }, + 'Subagent initialized' + ); + } + + /** + * Load config.yaml from basePath and parse it. + */ + static async loadConfig(basePath: string): Promise { + const configContent = await readFile(join(basePath, 'config.yaml'), 'utf-8'); + return yaml.load(configContent) as SubagentConfig; } /** diff --git a/gateway/src/harness/subagents/indicator/index.ts b/gateway/src/harness/subagents/indicator/index.ts index f546fb19..2857fb4e 100644 --- a/gateway/src/harness/subagents/indicator/index.ts +++ b/gateway/src/harness/subagents/indicator/index.ts @@ -1,10 +1,7 @@ -import { BaseSubagent, type SubagentConfig, type SubagentContext } from '../base-subagent.js'; +import { BaseSubagent } from '../base-subagent.js'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { SystemMessage } from '@langchain/core/messages'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; import type { FastifyBaseLogger } from 'fastify'; import type { MCPClientConnector } from '../../mcp-client.js'; -import type { HarnessEvent } from '../../harness-events.js'; /** * Indicator Subagent @@ -14,127 +11,10 @@ import type { HarnessEvent } from '../../harness-events.js'; * - Read, add, modify, and remove indicators from the indicators store * - Create custom indicator scripts via python_* tools * - Validate indicators using the evaluate_indicator tool - * - * Simpler than ResearchSubagent — no image capture needed. */ export class IndicatorSubagent extends BaseSubagent { - constructor( - config: SubagentConfig, - model: BaseChatModel, - logger: FastifyBaseLogger, - mcpClient?: MCPClientConnector, - tools?: any[] - ) { - super(config, model, logger, mcpClient, tools); - } - - /** - * Execute indicator request using LangGraph's createReactAgent. - */ - async execute(context: SubagentContext, instruction: string): Promise { - this.logger.info( - { - subagent: this.getName(), - userId: context.userContext.userId, - instruction: instruction.substring(0, 200), - toolCount: this.tools.length, - toolNames: this.tools.map(t => t.name), - }, - 'Indicator subagent starting' - ); - - if (!this.hasMCPClient()) { - throw new Error('MCP client not available for indicator subagent'); - } - - if (this.tools.length === 0) { - this.logger.warn('Indicator subagent has no tools — cannot read or patch workspace'); - } - - const initialMessages = this.buildMessages(context, instruction); - const systemMessage = initialMessages[0]; - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage as SystemMessage, - }); - - const result = await agent.invoke( - { messages: [humanMessage] }, - { recursionLimit: 25 } - ); - - const allMessages: any[] = result.messages ?? []; - - this.logger.info( - { messageCount: allMessages.length }, - 'Indicator subagent graph completed' - ); - - const lastAI = [...allMessages].reverse().find( - (m: any) => m.constructor?.name === 'AIMessage' || m._getType?.() === 'ai' - ); - - const finalText = lastAI - ? (typeof lastAI.content === 'string' ? lastAI.content : JSON.stringify(lastAI.content)) - : 'Indicator update completed.'; - - this.logger.info({ textLength: finalText.length }, 'Indicator subagent finished'); - - return finalText; - } - - async *streamEvents(context: SubagentContext, instruction: string, signal?: AbortSignal): AsyncGenerator { - this.logger.info({ subagent: this.getName() }, 'streamEvents starting'); - - if (!this.hasMCPClient()) { - throw new Error('MCP client not available for indicator subagent'); - } - - const initialMessages = this.buildMessages(context, instruction); - const systemMessage = initialMessages[0]; - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage as SystemMessage, - }); - - const stream = agent.stream( - { messages: [humanMessage] }, - { streamMode: ['messages', 'updates'], recursionLimit: 25, signal } - ); - - let finalText = ''; - - for await (const [mode, data] of await stream) { - if (signal?.aborted) break; - if (mode === 'messages') { - for (const chunk of IndicatorSubagent.extractStreamChunks(data, this.config.name)) { - yield chunk; - } - } else if (mode === 'updates') { - if ((data as any).agent?.messages) { - for (const msg of (data as any).agent.messages as any[]) { - if (msg.tool_calls?.length) { - for (const tc of msg.tool_calls) { - yield { type: 'subagent_tool_call', agentName: this.config.name, toolName: tc.name, label: tc.name }; - } - } else { - const content = IndicatorSubagent.extractFinalText(msg); - if (content) finalText = content; - } - } - } - } - } - - this.logger.info({ textLength: finalText.length }, 'streamEvents finished'); - return finalText; - } + protected getRecursionLimit() { return 25; } + protected getFallbackText() { return 'Indicator update completed.'; } } /** @@ -147,16 +27,8 @@ export async function createIndicatorSubagent( mcpClient?: MCPClientConnector, tools?: any[] ): Promise { - const { readFile } = await import('fs/promises'); - const { join } = await import('path'); - const yaml = await import('js-yaml'); - - const configPath = join(basePath, 'config.yaml'); - const configContent = await readFile(configPath, 'utf-8'); - const config = yaml.load(configContent) as SubagentConfig; - + const config = await BaseSubagent.loadConfig(basePath); const subagent = new IndicatorSubagent(config, model, logger, mcpClient, tools); await subagent.initialize(basePath); - return subagent; } diff --git a/gateway/src/harness/subagents/research/index.ts b/gateway/src/harness/subagents/research/index.ts index 23b97e84..45c83cbf 100644 --- a/gateway/src/harness/subagents/research/index.ts +++ b/gateway/src/harness/subagents/research/index.ts @@ -1,7 +1,7 @@ import { BaseSubagent, type SubagentConfig, type SubagentContext } from '../base-subagent.js'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { SystemMessage } from '@langchain/core/messages'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; +import type { BaseMessage } from '@langchain/core/messages'; import type { FastifyBaseLogger } from 'fastify'; import type { MCPClientConnector } from '../../mcp-client.js'; import type { HarnessEvent } from '../../harness-events.js'; @@ -47,6 +47,9 @@ export class ResearchSubagent extends BaseSubagent { super(config, model, logger, mcpClient, tools); } + protected getRecursionLimit() { return 40; } + protected getFallbackText() { return 'Research completed.'; } + setImageCapture(capture: Array<{data: string; mimeType: string}>): void { this.imageCapture = capture; } @@ -103,81 +106,38 @@ export class ResearchSubagent extends BaseSubagent { } } + /** + * Augment system message with custom indicators section. + */ + protected async buildSystemMessage( + context: SubagentContext, + instruction: string + ): Promise<{ systemMessage: SystemMessage; humanMessage: BaseMessage }> { + const { systemMessage, humanMessage } = await super.buildSystemMessage(context, instruction); + const customIndicatorsSection = await this.fetchCustomIndicatorsSection(); + if (customIndicatorsSection) { + const base = typeof systemMessage.content === 'string' + ? systemMessage.content + : JSON.stringify(systemMessage.content); + return { systemMessage: new SystemMessage(base + customIndicatorsSection), humanMessage }; + } + return { systemMessage, humanMessage }; + } + /** * Execute research request using LangGraph's createReactAgent. - * This is the standard LangChain pattern for agents with tool access — - * createReactAgent handles the tool calling loop automatically. + * Wraps executeAgent to manage image capture state. */ async execute(context: SubagentContext, instruction: string): Promise { - this.logger.info( - { - subagent: this.getName(), - userId: context.userContext.userId, - instruction: instruction.substring(0, 200), - toolCount: this.tools.length, - toolNames: this.tools.map(t => t.name), - }, - 'Research subagent starting' - ); - - if (!this.hasMCPClient()) { - throw new Error('MCP client not available for research subagent'); - } - - if (this.tools.length === 0) { - this.logger.warn('Research subagent has no tools — cannot write or execute scripts'); - } - // Clear previous images (in-place so tool wrappers keep the same array reference) this.imageCapture.length = 0; this.lastImages = []; - const customIndicatorsSection = await this.fetchCustomIndicatorsSection(); - - // Build system prompt (with memory context appended) - const initialMessages = this.buildMessages(context, instruction); - // buildMessages returns [SystemMessage, ...history, HumanMessage] - // Extract system content for createReactAgent's prompt parameter - let systemMessage = initialMessages[0] as SystemMessage; - if (customIndicatorsSection) { - const base = typeof systemMessage.content === 'string' ? systemMessage.content : JSON.stringify(systemMessage.content); - systemMessage = new SystemMessage(base + customIndicatorsSection); - } - const humanMessage = initialMessages[initialMessages.length - 1]; - - // createReactAgent is the standard LangChain/LangGraph pattern for tool-using agents. - // It manages the tool calling loop, message accumulation, and termination automatically. - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage, - }); - - const result = await agent.invoke( - { messages: [humanMessage] }, - { recursionLimit: 40 } - ); - - // The final message in the graph output is the agent's last AIMessage - const allMessages: any[] = result.messages ?? []; - - this.logger.info( - { messageCount: allMessages.length }, - 'Research subagent graph completed' - ); + const finalText = await this.executeAgent(context, instruction); // Images were captured in real-time by the MCP tool wrappers into this.imageCapture this.lastImages = [...this.imageCapture]; - // Return the final AI response - const lastAI = [...allMessages].reverse().find( - (m: any) => m.constructor?.name === 'AIMessage' || m._getType?.() === 'ai' - ); - - const finalText = lastAI - ? (typeof lastAI.content === 'string' ? lastAI.content : JSON.stringify(lastAI.content)) - : 'Research completed.'; - this.logger.info( { textLength: finalText.length, imageCount: this.lastImages.length }, 'Research subagent finished' @@ -225,73 +185,7 @@ export class ResearchSubagent extends BaseSubagent { // the first `updates` event fires (after the LLM finishes its first response). yield { type: 'subagent_tool_call', agentName: this.config.name, toolName: 'Thinking...', label: 'Thinking...' }; - const customIndicatorsSection = await this.fetchCustomIndicatorsSection(); - - const initialMessages = this.buildMessages(context, instruction); - let systemMessage = initialMessages[0] as SystemMessage; - if (customIndicatorsSection) { - const base = typeof systemMessage.content === 'string' ? systemMessage.content : JSON.stringify(systemMessage.content); - systemMessage = new SystemMessage(base + customIndicatorsSection); - } - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage, - }); - - this.logger.debug( - { toolCount: this.tools.length, toolNames: this.tools.map(t => t.name) }, - 'Research subagent: starting stream with tools' - ); - - const systemChars = typeof systemMessage.content === 'string' - ? systemMessage.content.length - : JSON.stringify(systemMessage.content).length; - const humanChars = typeof humanMessage.content === 'string' - ? humanMessage.content.length - : JSON.stringify(humanMessage.content).length; - this.logger.info( - { systemChars, humanChars, approxInputKB: Math.round((systemChars + humanChars) / 1024) }, - 'Research subagent: input context size' - ); - - const stream = agent.stream( - { messages: [humanMessage] }, - { streamMode: ['messages', 'updates'], recursionLimit: 40, signal } - ); - - let finalText = ''; - let updateCount = 0; - - for await (const [mode, data] of await stream) { - if (signal?.aborted) break; - if (mode === 'messages') { - // Real-time token streaming from the LLM — data is [BaseMessage, metadata] - for (const chunk of ResearchSubagent.extractStreamChunks(data, this.config.name)) { - yield chunk; - } - } else if (mode === 'updates') { - updateCount++; - const updateKeys = Object.keys(data as any); - this.logger.debug({ updateCount, updateKeys }, 'Research subagent: graph update'); - // Agent node fired — yield tool call decisions before tools run - if ((data as any).agent?.messages) { - for (const msg of (data as any).agent.messages as any[]) { - if (msg.tool_calls?.length) { - for (const tc of msg.tool_calls) { - yield { type: 'subagent_tool_call', agentName: this.config.name, toolName: tc.name, label: tc.name }; - } - } else { - // Capture final text for return value (already streamed via messages above) - const content = ResearchSubagent.extractFinalText(msg); - if (content) finalText = content; - } - } - } - } - } + const finalText = yield* this.streamEventsCore(context, instruction, signal); this.lastImages = [...this.imageCapture]; if (!finalText) { @@ -309,7 +203,7 @@ export class ResearchSubagent extends BaseSubagent { } /** - * Stream research execution + * Stream research execution (raw model streaming, no agent loop) */ async *stream(context: SubagentContext, instruction: string): AsyncGenerator { this.logger.info( @@ -351,21 +245,11 @@ export async function createResearchSubagent( tools?: any[], imageCapture?: Array<{data: string; mimeType: string}> ): Promise { - const { readFile } = await import('fs/promises'); - const { join } = await import('path'); - const yaml = await import('js-yaml'); - - // Load config - const configPath = join(basePath, 'config.yaml'); - const configContent = await readFile(configPath, 'utf-8'); - const config = yaml.load(configContent) as SubagentConfig; - - // Create and initialize subagent + const config = await BaseSubagent.loadConfig(basePath); const subagent = new ResearchSubagent(config, model, logger, mcpClient, tools); if (imageCapture !== undefined) { subagent.setImageCapture(imageCapture); } await subagent.initialize(basePath); - return subagent; } diff --git a/gateway/src/harness/subagents/strategy/index.ts b/gateway/src/harness/subagents/strategy/index.ts index 926b8f71..ff018da6 100644 --- a/gateway/src/harness/subagents/strategy/index.ts +++ b/gateway/src/harness/subagents/strategy/index.ts @@ -1,137 +1,17 @@ -import { BaseSubagent, type SubagentConfig, type SubagentContext } from '../base-subagent.js'; +import { BaseSubagent } from '../base-subagent.js'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { SystemMessage } from '@langchain/core/messages'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; import type { FastifyBaseLogger } from 'fastify'; import type { MCPClientConnector } from '../../mcp-client.js'; -import type { HarnessEvent } from '../../harness-events.js'; /** * Strategy Subagent * * Specialized agent for writing PandasStrategy classes, running backtests, * and managing strategy activation/deactivation. - * - * Mirrors the pattern of IndicatorSubagent in indicator/index.ts. */ export class StrategySubagent extends BaseSubagent { - constructor( - config: SubagentConfig, - model: BaseChatModel, - logger: FastifyBaseLogger, - mcpClient?: MCPClientConnector, - tools?: any[] - ) { - super(config, model, logger, mcpClient, tools); - } - - /** - * Execute a strategy request using LangGraph's createReactAgent. - */ - async execute(context: SubagentContext, instruction: string): Promise { - this.logger.info( - { - subagent: this.getName(), - userId: context.userContext.userId, - instruction: instruction.substring(0, 200), - toolCount: this.tools.length, - toolNames: this.tools.map(t => t.name), - }, - 'Strategy subagent starting' - ); - - if (!this.hasMCPClient()) { - throw new Error('MCP client not available for strategy subagent'); - } - - if (this.tools.length === 0) { - this.logger.warn('Strategy subagent has no tools'); - } - - const initialMessages = this.buildMessages(context, instruction); - const systemMessage = initialMessages[0]; - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage as SystemMessage, - }); - - const result = await agent.invoke( - { messages: [humanMessage] }, - { recursionLimit: 30 } - ); - - const allMessages: any[] = result.messages ?? []; - - this.logger.info( - { messageCount: allMessages.length }, - 'Strategy subagent graph completed' - ); - - const lastAI = [...allMessages].reverse().find( - (m: any) => m.constructor?.name === 'AIMessage' || m._getType?.() === 'ai' - ); - - const finalText = lastAI - ? (typeof lastAI.content === 'string' ? lastAI.content : JSON.stringify(lastAI.content)) - : 'Strategy task completed.'; - - this.logger.info({ textLength: finalText.length }, 'Strategy subagent finished'); - - return finalText; - } - - async *streamEvents(context: SubagentContext, instruction: string, signal?: AbortSignal): AsyncGenerator { - this.logger.info({ subagent: this.getName() }, 'streamEvents starting'); - - if (!this.hasMCPClient()) { - throw new Error('MCP client not available for strategy subagent'); - } - - const initialMessages = this.buildMessages(context, instruction); - const systemMessage = initialMessages[0]; - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage as SystemMessage, - }); - - const stream = agent.stream( - { messages: [humanMessage] }, - { streamMode: ['messages', 'updates'], recursionLimit: 30, signal } - ); - - let finalText = ''; - - for await (const [mode, data] of await stream) { - if (signal?.aborted) break; - if (mode === 'messages') { - for (const chunk of StrategySubagent.extractStreamChunks(data, this.config.name)) { - yield chunk; - } - } else if (mode === 'updates') { - if ((data as any).agent?.messages) { - for (const msg of (data as any).agent.messages as any[]) { - if (msg.tool_calls?.length) { - for (const tc of msg.tool_calls) { - yield { type: 'subagent_tool_call', agentName: this.config.name, toolName: tc.name, label: tc.name }; - } - } else { - const content = StrategySubagent.extractFinalText(msg); - if (content) finalText = content; - } - } - } - } - } - - this.logger.info({ textLength: finalText.length }, 'streamEvents finished'); - return finalText; - } + protected getRecursionLimit() { return 30; } + protected getFallbackText() { return 'Strategy task completed.'; } } /** @@ -144,16 +24,8 @@ export async function createStrategySubagent( mcpClient?: MCPClientConnector, tools?: any[] ): Promise { - const { readFile } = await import('fs/promises'); - const { join } = await import('path'); - const yaml = await import('js-yaml'); - - const configPath = join(basePath, 'config.yaml'); - const configContent = await readFile(configPath, 'utf-8'); - const config = yaml.load(configContent) as SubagentConfig; - + const config = await BaseSubagent.loadConfig(basePath); const subagent = new StrategySubagent(config, model, logger, mcpClient, tools); await subagent.initialize(basePath); - return subagent; } diff --git a/gateway/src/harness/subagents/web-explore/index.ts b/gateway/src/harness/subagents/web-explore/index.ts index 0815bf6f..8e5d498d 100644 --- a/gateway/src/harness/subagents/web-explore/index.ts +++ b/gateway/src/harness/subagents/web-explore/index.ts @@ -1,9 +1,6 @@ -import { BaseSubagent, type SubagentConfig, type SubagentContext } from '../base-subagent.js'; +import { BaseSubagent, type SubagentConfig } from '../base-subagent.js'; import type { BaseChatModel } from '@langchain/core/language_models/chat_models'; -import { SystemMessage } from '@langchain/core/messages'; -import { createReactAgent } from '@langchain/langgraph/prebuilt'; import type { FastifyBaseLogger } from 'fastify'; -import type { HarnessEvent } from '../../harness-events.js'; /** * Web Explore Subagent @@ -24,95 +21,9 @@ export class WebExploreSubagent extends BaseSubagent { super(config, model, logger, undefined, tools); } - async execute(context: SubagentContext, instruction: string): Promise { - this.logger.info( - { - subagent: this.getName(), - userId: context.userContext.userId, - instruction: instruction.substring(0, 200), - toolCount: this.tools.length, - toolNames: this.tools.map(t => t.name), - }, - 'Web explore subagent starting' - ); - - const initialMessages = this.buildMessages(context, instruction); - const systemMessage = initialMessages[0]; - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage as SystemMessage, - }); - - const result = await agent.invoke( - { messages: [humanMessage] }, - { recursionLimit: 15 } - ); - - const allMessages: any[] = result.messages ?? []; - - this.logger.info({ messageCount: allMessages.length }, 'Web explore subagent graph completed'); - - const lastAI = [...allMessages].reverse().find( - (m: any) => m.constructor?.name === 'AIMessage' || m._getType?.() === 'ai' - ); - - const finalText = lastAI - ? (typeof lastAI.content === 'string' ? lastAI.content : JSON.stringify(lastAI.content)) - : 'No results found.'; - - this.logger.info({ textLength: finalText.length }, 'Web explore subagent finished'); - - return finalText; - } - - async *streamEvents(context: SubagentContext, instruction: string, signal?: AbortSignal): AsyncGenerator { - this.logger.info({ subagent: this.getName() }, 'streamEvents starting'); - - const initialMessages = this.buildMessages(context, instruction); - const systemMessage = initialMessages[0]; - const humanMessage = initialMessages[initialMessages.length - 1]; - - const agent = createReactAgent({ - llm: this.model, - tools: this.tools, - prompt: systemMessage as SystemMessage, - }); - - const stream = agent.stream( - { messages: [humanMessage] }, - { streamMode: ['messages', 'updates'], recursionLimit: 15, signal } - ); - - let finalText = ''; - - for await (const [mode, data] of await stream) { - if (signal?.aborted) break; - if (mode === 'messages') { - for (const chunk of WebExploreSubagent.extractStreamChunks(data, this.config.name)) { - yield chunk; - } - } else if (mode === 'updates') { - if ((data as any).agent?.messages) { - for (const msg of (data as any).agent.messages as any[]) { - if (msg.tool_calls?.length) { - for (const tc of msg.tool_calls) { - yield { type: 'subagent_tool_call', agentName: this.config.name, toolName: tc.name, label: tc.name }; - } - } else { - const content = WebExploreSubagent.extractFinalText(msg); - if (content) finalText = content; - } - } - } - } - } - - this.logger.info({ textLength: finalText.length }, 'streamEvents finished'); - return finalText; - } + protected getRecursionLimit() { return 15; } + protected getFallbackText() { return 'No results found.'; } + protected requiresMCPClient() { return false; } } /** @@ -124,16 +35,8 @@ export async function createWebExploreSubagent( basePath: string, tools?: any[] ): Promise { - const { readFile } = await import('fs/promises'); - const { join } = await import('path'); - const yaml = await import('js-yaml'); - - const configPath = join(basePath, 'config.yaml'); - const configContent = await readFile(configPath, 'utf-8'); - const config = yaml.load(configContent) as SubagentConfig; - + const config = await BaseSubagent.loadConfig(basePath); const subagent = new WebExploreSubagent(config, model, logger, tools); await subagent.initialize(basePath); - return subagent; } diff --git a/web/src/App.vue b/web/src/App.vue index d2a750f4..e488af44 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -5,6 +5,7 @@ import SplitterPanel from 'primevue/splitterpanel' import ChartView from './components/ChartView.vue' import ChatPanel from './components/ChatPanel.vue' import LoginScreen from './components/LoginScreen.vue' +import BottomTray from './components/BottomTray.vue' import { useChartStore } from './stores/chart' import { useShapeStore } from './stores/shapes' import { useIndicatorStore } from './stores/indicators' @@ -137,14 +138,19 @@ onBeforeUnmount(() => { :error-message="authError" @authenticate="handleAuthenticate" /> - - - - - - - - +
+
+ + + + + + + + +
+ +
@@ -165,8 +171,21 @@ onBeforeUnmount(() => { background: #0f0f0f !important; } +.desktop-layout { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.top-area { + flex: 1; + min-height: 0; + overflow: hidden; +} + .main-splitter { - height: 100vh !important; + height: 100% !important; background: #0f0f0f !important; } @@ -200,7 +219,7 @@ onBeforeUnmount(() => { right: 0; bottom: 0; z-index: 9999; - cursor: col-resize; + cursor: auto; background: transparent; } diff --git a/web/src/components/BottomTray.vue b/web/src/components/BottomTray.vue new file mode 100644 index 00000000..9f80b4dc --- /dev/null +++ b/web/src/components/BottomTray.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/web/src/components/tabs/OrdersTab.vue b/web/src/components/tabs/OrdersTab.vue new file mode 100644 index 00000000..8c46df58 --- /dev/null +++ b/web/src/components/tabs/OrdersTab.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/web/src/components/tabs/PlaceholderTab.vue b/web/src/components/tabs/PlaceholderTab.vue new file mode 100644 index 00000000..376ac6cb --- /dev/null +++ b/web/src/components/tabs/PlaceholderTab.vue @@ -0,0 +1,20 @@ + + + + +