bugfix; web tabs

This commit is contained in:
2026-04-13 20:58:40 -04:00
parent 6c82dce6f6
commit 5021138da6
11 changed files with 634 additions and 565 deletions

View File

@@ -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 [];

View File

@@ -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<void> {
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<string>;
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<string> {
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<string> {
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<HarnessEvent, string> {
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<HarnessEvent, string> {
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<HarnessEvent, string> {
const result = await this.execute(context, input);
return result;
async initialize(basePath: string): Promise<void> {
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<SubagentConfig> {
const configContent = await readFile(join(basePath, 'config.yaml'), 'utf-8');
return yaml.load(configContent) as SubagentConfig;
}
/**

View File

@@ -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<string> {
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<HarnessEvent, string> {
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<IndicatorSubagent> {
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;
}

View File

@@ -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<string> {
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<string> {
this.logger.info(
@@ -351,21 +245,11 @@ export async function createResearchSubagent(
tools?: any[],
imageCapture?: Array<{data: string; mimeType: string}>
): Promise<ResearchSubagent> {
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;
}

View File

@@ -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<string> {
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<HarnessEvent, string> {
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<StrategySubagent> {
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;
}

View File

@@ -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<string> {
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<HarnessEvent, string> {
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<WebExploreSubagent> {
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;
}