data fixes; indicator=>workspace sync
This commit is contained in:
@@ -8,8 +8,9 @@ import type { InboundMessage, OutboundMessage } from '../types/messages.js';
|
||||
import { MCPClientConnector } from './mcp-client.js';
|
||||
import { LLMProviderFactory, type ProviderConfig } from '../llm/provider.js';
|
||||
import { ModelRouter, RoutingStrategy } from '../llm/router.js';
|
||||
import type { ModelMiddleware } from '../llm/middleware.js';
|
||||
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
|
||||
import type { ChannelAdapter } from '../workspace/index.js';
|
||||
import type { ChannelAdapter, PathTriggerContext } from '../workspace/index.js';
|
||||
import type { ResearchSubagent } from './subagents/research/index.js';
|
||||
import type { DynamicStructuredTool } from '@langchain/core/tools';
|
||||
import { getToolRegistry } from '../tools/tool-registry.js';
|
||||
@@ -70,10 +71,10 @@ export class AgentHarness {
|
||||
private config: AgentHarnessConfig;
|
||||
private modelFactory: LLMProviderFactory;
|
||||
private modelRouter: ModelRouter;
|
||||
private middleware: ModelMiddleware | undefined;
|
||||
private mcpClient: MCPClientConnector;
|
||||
private workspaceManager?: WorkspaceManager;
|
||||
private channelAdapter?: ChannelAdapter;
|
||||
private isFirstMessage: boolean = true;
|
||||
private researchSubagent?: ResearchSubagent;
|
||||
private availableMCPTools: MCPToolInfo[] = [];
|
||||
private researchImageCapture: Array<{ data: string; mimeType: string }> = [];
|
||||
@@ -94,6 +95,8 @@ export class AgentHarness {
|
||||
mcpServerUrl: config.mcpServerUrl,
|
||||
logger: config.logger,
|
||||
});
|
||||
|
||||
this.registerWorkspaceTriggers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +196,7 @@ export class AgentHarness {
|
||||
const { createResearchSubagent } = await import('./subagents/research/index.js');
|
||||
|
||||
// Create a model for the research subagent
|
||||
const model = await this.modelRouter.route(
|
||||
const { model } = await this.modelRouter.route(
|
||||
'research analysis', // dummy query
|
||||
this.config.license,
|
||||
RoutingStrategy.COMPLEXITY,
|
||||
@@ -429,11 +432,25 @@ export class AgentHarness {
|
||||
|
||||
// 2. Load recent conversation history
|
||||
const channelKey = this.config.channelType ?? ChannelType.WEBSOCKET;
|
||||
const storedMessages = this.conversationStore
|
||||
let storedMessages = this.conversationStore
|
||||
? await this.conversationStore.getRecentMessages(
|
||||
this.config.userId, this.config.sessionId, this.config.historyLimit, channelKey
|
||||
)
|
||||
: [];
|
||||
|
||||
// First turn: seed conversation history with current workspace state
|
||||
if (storedMessages.length === 0 && this.workspaceManager && this.conversationStore) {
|
||||
const workspaceJSON = this.workspaceManager.serializeState();
|
||||
const content = `[Workspace State]\n\`\`\`json\n${workspaceJSON}\n\`\`\``;
|
||||
await this.conversationStore.saveMessage(
|
||||
this.config.userId, this.config.sessionId,
|
||||
'workspace', content, { isWorkspaceContext: true }, channelKey
|
||||
);
|
||||
storedMessages = await this.conversationStore.getRecentMessages(
|
||||
this.config.userId, this.config.sessionId, this.config.historyLimit, channelKey
|
||||
);
|
||||
}
|
||||
|
||||
const history = this.conversationStore
|
||||
? this.conversationStore.toLangChainMessages(storedMessages)
|
||||
: [];
|
||||
@@ -441,12 +458,13 @@ export class AgentHarness {
|
||||
|
||||
// 4. Get the configured model
|
||||
this.config.logger.debug('Routing to model');
|
||||
const model = await this.modelRouter.route(
|
||||
const { model, middleware } = await this.modelRouter.route(
|
||||
message.content,
|
||||
this.config.license,
|
||||
RoutingStrategy.COMPLEXITY,
|
||||
this.config.userId
|
||||
);
|
||||
this.middleware = middleware;
|
||||
this.config.logger.info({ modelName: model.constructor.name }, 'Model selected');
|
||||
|
||||
// 5. Build LangChain messages
|
||||
@@ -489,6 +507,11 @@ export class AgentHarness {
|
||||
'Tools loaded for main agent'
|
||||
);
|
||||
|
||||
// Apply middleware (e.g. Anthropic prompt caching)
|
||||
const processedMessages = this.middleware
|
||||
? this.middleware.processMessages(langchainMessages, tools)
|
||||
: langchainMessages;
|
||||
|
||||
// 7. Bind tools to model
|
||||
const modelWithTools = tools.length > 0 && model.bindTools ? model.bindTools(tools) : model;
|
||||
|
||||
@@ -501,7 +524,7 @@ 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, langchainMessages, tools);
|
||||
const assistantMessage = await this.executeWithToolCalling(modelWithTools, processedMessages, tools, 10);
|
||||
|
||||
this.config.logger.info(
|
||||
{ responseLength: assistantMessage.length },
|
||||
@@ -518,11 +541,6 @@ export class AgentHarness {
|
||||
);
|
||||
}
|
||||
|
||||
// Mark first message as processed
|
||||
if (this.isFirstMessage) {
|
||||
this.isFirstMessage = false;
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: `msg_${Date.now()}`,
|
||||
sessionId: message.sessionId,
|
||||
@@ -556,16 +574,10 @@ export class AgentHarness {
|
||||
private async buildSystemPrompt(): Promise<string> {
|
||||
// Load template and populate with license info
|
||||
const template = await AgentHarness.loadSystemPromptTemplate();
|
||||
let prompt = template
|
||||
const prompt = template
|
||||
.replace('{{licenseType}}', this.config.license.licenseType)
|
||||
.replace('{{features}}', JSON.stringify(this.config.license.features, null, 2));
|
||||
|
||||
// Add full workspace state from WorkspaceManager (first message only)
|
||||
if (this.isFirstMessage && this.workspaceManager) {
|
||||
const workspaceJSON = this.workspaceManager.serializeState();
|
||||
prompt += `\n\n# Current Workspace State\n\`\`\`json\n${workspaceJSON}\n\`\`\``;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
@@ -574,9 +586,14 @@ export class AgentHarness {
|
||||
*/
|
||||
private getToolLabel(toolName: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
research_agent: 'Researching...',
|
||||
research: 'Researching...',
|
||||
get_chart_data: 'Fetching chart data...',
|
||||
symbol_lookup: 'Looking up symbol...',
|
||||
category_list: 'Seeing what we have...',
|
||||
category_edit: 'Coding...',
|
||||
category_write: 'Coding...',
|
||||
category_read: 'Inspecting...',
|
||||
execute_research: 'Running script...',
|
||||
};
|
||||
return labels[toolName] ?? `Running ${toolName}...`;
|
||||
}
|
||||
@@ -685,6 +702,26 @@ export class AgentHarness {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register workspace path triggers to record state changes into conversation history.
|
||||
*/
|
||||
private registerWorkspaceTriggers(): void {
|
||||
if (!this.workspaceManager || !this.conversationStore) return;
|
||||
const channelKey = this.config.channelType ?? ChannelType.WEBSOCKET;
|
||||
|
||||
for (const store of ['shapes', 'indicators', 'chartState']) {
|
||||
this.workspaceManager.onPathChange(`/${store}/*`, async (_old: unknown, newVal: unknown, ctx: PathTriggerContext) => {
|
||||
const content = `[Workspace Update] ${ctx.store}${ctx.path}\n${JSON.stringify(newVal, null, 2)}`;
|
||||
await this.conversationStore!.saveMessage(
|
||||
this.config.userId, this.config.sessionId,
|
||||
'workspace', content,
|
||||
{ isWorkspaceUpdate: true, store: ctx.store, seq: ctx.seq },
|
||||
channelKey
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End the session: flush conversation to cold storage, then release resources.
|
||||
* Called by channel handlers on disconnect, session expiry, or graceful shutdown.
|
||||
|
||||
Reference in New Issue
Block a user