client-py connected

This commit is contained in:
2026-03-27 16:33:40 -04:00
parent c76887ab92
commit c3a8fae132
55 changed files with 1598 additions and 426 deletions

View File

@@ -101,9 +101,15 @@ See individual workflow READMEs for details.
YAML-based configuration:
- `models.yaml`: LLM providers, routing, rate limits
- `subagent-routing.yaml`: When to use which subagent
Model configuration is centralized in the main gateway config (`/config/config.yaml`), including:
- Default models and providers
- License tier model assignments (free, pro, enterprise)
- Model routing rules (complexity, cost-optimized)
Resource limits (token limits, rate limits) are stored in the database per user.
## User Context
Enhanced session context with channel awareness for multi-channel support:

View File

@@ -8,6 +8,7 @@ import { MCPClientConnector } from './mcp-client.js';
import { CONTEXT_URIS, type ResourceContent } from '../types/resources.js';
import { LLMProviderFactory, type ProviderConfig } from '../llm/provider.js';
import { ModelRouter, RoutingStrategy } from '../llm/router.js';
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
export interface AgentHarnessConfig {
userId: string;
@@ -15,6 +16,7 @@ export interface AgentHarnessConfig {
license: UserLicense;
providerConfig: ProviderConfig;
logger: FastifyBaseLogger;
workspaceManager?: WorkspaceManager;
}
/**
@@ -33,9 +35,13 @@ export class AgentHarness {
private modelFactory: LLMProviderFactory;
private modelRouter: ModelRouter;
private mcpClient: MCPClientConnector;
private workspaceManager?: WorkspaceManager;
private lastWorkspaceSeq: number = 0;
private isFirstMessage: boolean = true;
constructor(config: AgentHarnessConfig) {
this.config = config;
this.workspaceManager = config.workspaceManager;
this.modelFactory = new LLMProviderFactory(config.providerConfig, config.logger);
this.modelRouter = new ModelRouter(this.modelFactory, config.logger);
@@ -102,18 +108,14 @@ export class AgentHarness {
// 7. Extract text response (tool handling TODO)
const assistantMessage = response.content as string;
// 8. Save messages to user's MCP server
this.config.logger.debug('Saving messages to MCP');
await this.mcpClient.callTool('save_message', {
role: 'user',
content: message.content,
timestamp: message.timestamp.toISOString(),
});
await this.mcpClient.callTool('save_message', {
role: 'assistant',
content: assistantMessage,
timestamp: new Date().toISOString(),
});
// TODO: Save messages to Iceberg conversation table instead of MCP
// Should batch-insert periodically or on session end to avoid many small Parquet files
// await icebergConversationStore.appendMessages([...]);
// Mark first message as processed
if (this.isFirstMessage) {
this.isFirstMessage = false;
}
return {
messageId: `msg_${Date.now()}`,
@@ -157,17 +159,17 @@ export class AgentHarness {
yield content;
}
// Save after streaming completes
await this.mcpClient.callTool('save_message', {
role: 'user',
content: message.content,
timestamp: message.timestamp.toISOString(),
});
await this.mcpClient.callTool('save_message', {
role: 'assistant',
content: fullResponse,
timestamp: new Date().toISOString(),
});
// TODO: Save messages to Iceberg conversation table instead of MCP
// Should batch-insert periodically or on session end to avoid many small Parquet files
// await icebergConversationStore.appendMessages([
// { role: 'user', content: message.content, timestamp: message.timestamp },
// { role: 'assistant', content: fullResponse, timestamp: new Date() }
// ]);
// Mark first message as processed
if (this.isFirstMessage) {
this.isFirstMessage = false;
}
} catch (error) {
this.config.logger.error({ error }, 'Error streaming message');
throw error;
@@ -224,6 +226,15 @@ export class AgentHarness {
});
}
// Add workspace delta (for subsequent turns)
const workspaceDelta = this.buildWorkspaceDelta();
if (workspaceDelta) {
messages.push({
role: 'user',
content: workspaceDelta,
});
}
// Add current user message
messages.push({
role: 'user',
@@ -273,9 +284,18 @@ Available features: ${JSON.stringify(this.config.license.features, null, 2)}`;
prompt += `\n\n# User Profile\n${userProfile.text}`;
}
// Add workspace context
// Add workspace context from MCP resource (if available)
if (workspaceState?.text) {
prompt += `\n\n# Current Workspace\n${workspaceState.text}`;
prompt += `\n\n# Current Workspace (from MCP)\n${workspaceState.text}`;
}
// Add full workspace state from WorkspaceManager (first message only)
if (this.isFirstMessage && this.workspaceManager) {
const workspaceJSON = this.workspaceManager.serializeState();
prompt += `\n\n# Workspace State (JSON)\n\`\`\`json\n${workspaceJSON}\n\`\`\``;
// Record current workspace sequence for delta tracking
this.lastWorkspaceSeq = this.workspaceManager.getCurrentSeq();
}
// Add user's custom instructions (highest priority)
@@ -286,6 +306,30 @@ Available features: ${JSON.stringify(this.config.license.features, null, 2)}`;
return prompt;
}
/**
* Build workspace delta message for subsequent turns.
* Returns null if no changes since last message.
*/
private buildWorkspaceDelta(): string | null {
if (!this.workspaceManager || this.isFirstMessage) {
return null;
}
const changes = this.workspaceManager.getChangesSince(this.lastWorkspaceSeq);
if (Object.keys(changes).length === 0) {
return null;
}
// Format changes as JSON
const deltaJSON = JSON.stringify(changes, null, 2);
// Update sequence marker
this.lastWorkspaceSeq = this.workspaceManager.getCurrentSeq();
return `[Workspace Changes Since Last Turn]\n\`\`\`json\n${deltaJSON}\n\`\`\``;
}
/**

View File

@@ -1,110 +0,0 @@
# Default LLM Model Configuration
# Default model for general agent tasks
default:
provider: anthropic
model: claude-3-5-sonnet-20241022
temperature: 0.7
maxTokens: 4096
# Model overrides for specific use cases
models:
# Fast model for simple tasks (routing, classification)
fast:
provider: anthropic
model: claude-3-haiku-20240307
temperature: 0.3
maxTokens: 1024
# Reasoning model for complex analysis
reasoning:
provider: anthropic
model: claude-3-5-sonnet-20241022
temperature: 0.5
maxTokens: 8192
# Precise model for code generation/review
code:
provider: anthropic
model: claude-3-5-sonnet-20241022
temperature: 0.2
maxTokens: 8192
# Creative model for strategy brainstorming
creative:
provider: anthropic
model: claude-3-5-sonnet-20241022
temperature: 0.9
maxTokens: 4096
# Embedding model configuration
embeddings:
provider: openai
model: text-embedding-3-small
dimensions: 1536
# Model routing rules (complexity-based)
routing:
# Simple queries → fast model
simple:
keywords:
- "what is"
- "define"
- "list"
- "show me"
maxTokens: 100
model: fast
# Code-related → code model
code:
keywords:
- "code"
- "function"
- "implement"
- "debug"
- "review"
model: code
# Analysis tasks → reasoning model
analysis:
keywords:
- "analyze"
- "compare"
- "evaluate"
- "assess"
model: reasoning
# Everything else → default
default:
model: default
# Cost optimization settings
costControl:
# Cache system prompts (Anthropic prompt caching)
cacheSystemPrompts: true
# Token limits per license type
tokenLimits:
free:
maxTokensPerMessage: 2048
maxTokensPerDay: 50000
pro:
maxTokensPerMessage: 8192
maxTokensPerDay: 500000
enterprise:
maxTokensPerMessage: 16384
maxTokensPerDay: -1 # unlimited
# Rate limiting
rateLimits:
# Requests per minute by license
requestsPerMinute:
free: 10
pro: 60
enterprise: 120
# Concurrent requests
concurrentRequests:
free: 1
pro: 3
enterprise: 10

View File

@@ -1,4 +1,5 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import type { FastifyBaseLogger } from 'fastify';
@@ -11,7 +12,7 @@ export interface MCPClientConfig {
/**
* MCP client connector for user's container
* Manages connection to user-specific MCP server
* Manages connection to user-specific MCP server via SSE transport
*/
export class MCPClientConnector {
private client: Client | null = null;
@@ -23,8 +24,7 @@ export class MCPClientConnector {
}
/**
* Connect to user's MCP server
* TODO: Implement HTTP/SSE transport instead of stdio for container communication
* Connect to user's MCP server via SSE transport
*/
async connect(): Promise<void> {
if (this.connected) {
@@ -34,7 +34,7 @@ export class MCPClientConnector {
try {
this.config.logger.info(
{ userId: this.config.userId, url: this.config.mcpServerUrl },
'Connecting to user MCP server'
'Connecting to user MCP server via SSE'
);
this.client = new Client(
@@ -46,24 +46,18 @@ export class MCPClientConnector {
capabilities: {
sampling: {},
},
} as any
}
);
// TODO: Replace with HTTP transport when user containers are ready
// For now, this is a placeholder structure
// const transport = new HTTPTransport(this.config.mcpServerUrl, {
// headers: {
// 'Authorization': `Bearer ${this.config.platformJWT}`
// }
// });
// Placeholder: will be replaced with actual container transport
this.config.logger.warn(
'MCP transport not yet implemented - using placeholder'
// Create SSE transport for HTTP connection to user container
const transport = new SSEClientTransport(
new URL(`${this.config.mcpServerUrl}/sse`)
);
await this.client.connect(transport);
this.connected = true;
this.config.logger.info('Connected to user MCP server');
this.config.logger.info('Connected to user MCP server via SSE');
} catch (error) {
this.config.logger.error(
{ error, userId: this.config.userId },
@@ -84,12 +78,8 @@ export class MCPClientConnector {
try {
this.config.logger.debug({ tool: name, args }, 'Calling MCP tool');
// TODO: Implement when MCP client is connected
// const result = await this.client.callTool({ name, arguments: args });
// return result;
// Placeholder response
return { success: true, message: 'MCP tool call placeholder' };
const result = await this.client.callTool({ name, arguments: args });
return result;
} catch (error) {
this.config.logger.error({ error, tool: name }, 'MCP tool call failed');
throw error;
@@ -98,27 +88,35 @@ export class MCPClientConnector {
/**
* List available tools from user's MCP server
* Filters to only return tools marked as agent_accessible
*/
async listTools(): Promise<Array<{ name: string; description?: string }>> {
async listTools(): Promise<Array<{ name: string; description?: string; inputSchema?: any }>> {
if (!this.client || !this.connected) {
throw new Error('MCP client not connected');
}
try {
// TODO: Implement when MCP client is connected
// const tools = await this.client.listTools();
// return tools;
const response = await this.client.listTools();
// Placeholder tools (actions only, not context)
return [
{ name: 'save_message', description: 'Save message to conversation history' },
{ name: 'list_strategies', description: 'List user strategies' },
{ name: 'read_strategy', description: 'Read strategy code' },
{ name: 'write_strategy', description: 'Write strategy code' },
{ name: 'run_backtest', description: 'Run backtest on strategy' },
{ name: 'get_watchlist', description: 'Get user watchlist' },
{ name: 'execute_trade', description: 'Execute trade' },
];
// Filter tools to only include agent-accessible ones
const tools = response.tools
.filter((tool: any) => {
// Check if tool has agent_accessible annotation
const annotations = tool.annotations || {};
return annotations.agent_accessible === true;
})
.map((tool: any) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
this.config.logger.debug(
{ totalTools: response.tools.length, agentAccessibleTools: tools.length },
'Listed MCP tools with filtering'
);
return tools;
} catch (error) {
this.config.logger.error({ error }, 'Failed to list MCP tools');
throw error;
@@ -127,6 +125,7 @@ export class MCPClientConnector {
/**
* List available resources from user's MCP server
* Filters to only return resources marked as agent_accessible
*/
async listResources(): Promise<Array<{ uri: string; name: string; description?: string; mimeType?: string }>> {
if (!this.client || !this.connected) {
@@ -134,37 +133,28 @@ export class MCPClientConnector {
}
try {
// TODO: Implement when MCP client is connected
// const resources = await this.client.listResources();
// return resources;
const response = await this.client.listResources();
// Placeholder resources for user context
return [
{
uri: 'context://user-profile',
name: 'User Profile',
description: 'User trading style, preferences, and background',
mimeType: 'text/plain',
},
{
uri: 'context://conversation-summary',
name: 'Conversation Summary',
description: 'Semantic summary of recent conversation history with RAG',
mimeType: 'text/plain',
},
{
uri: 'context://workspace-state',
name: 'Workspace State',
description: 'Current chart, watchlist, and open positions',
mimeType: 'application/json',
},
{
uri: 'context://system-prompt',
name: 'Custom System Prompt',
description: 'User custom instructions for the assistant',
mimeType: 'text/plain',
},
];
// Filter resources to only include agent-accessible ones
const resources = response.resources
.filter((resource: any) => {
// Check if resource has agent_accessible annotation
const annotations = resource.annotations || {};
return annotations.agent_accessible === true;
})
.map((resource: any) => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType,
}));
this.config.logger.debug(
{ totalResources: response.resources.length, agentAccessibleResources: resources.length },
'Listed MCP resources with filtering'
);
return resources;
} catch (error) {
this.config.logger.error({ error }, 'Failed to list MCP resources');
throw error;
@@ -182,55 +172,18 @@ export class MCPClientConnector {
try {
this.config.logger.debug({ uri }, 'Reading MCP resource');
// TODO: Implement when MCP client is connected
// const resource = await this.client.readResource({ uri });
// return resource;
const response = await this.client.readResource({ uri });
// Placeholder resource content
if (uri === 'context://user-profile') {
return {
uri,
mimeType: 'text/plain',
text: `User Profile:
- Trading experience: Intermediate
- Preferred timeframes: 1h, 4h, 1d
- Risk tolerance: Medium
- Focus: Swing trading with technical indicators`,
};
} else if (uri === 'context://conversation-summary') {
return {
uri,
mimeType: 'text/plain',
text: `Recent Conversation Summary:
[RAG-generated summary would go here]
// Extract the first content item (MCP returns array of contents)
const content = response.contents[0];
User recently discussed:
- Moving average crossover strategies
- Backtesting on BTC/USDT
- Risk management techniques`,
};
} else if (uri === 'context://workspace-state') {
return {
uri,
mimeType: 'application/json',
text: JSON.stringify({
currentChart: { ticker: 'BINANCE:BTC/USDT', timeframe: '1h' },
watchlist: ['BTC/USDT', 'ETH/USDT', 'SOL/USDT'],
openPositions: [],
}, null, 2),
};
} else if (uri === 'context://system-prompt') {
return {
uri,
mimeType: 'text/plain',
text: `Custom Instructions:
- Be concise and data-driven
- Always show risk/reward ratios
- Prefer simple strategies over complex ones`,
};
}
return { uri, text: '' };
// Handle union type: content is either TextContent or BlobContent
return {
uri: content.uri,
mimeType: content.mimeType,
text: 'text' in content ? content.text : undefined,
blob: 'blob' in content ? content.blob : undefined,
};
} catch (error) {
this.config.logger.error({ error, uri }, 'MCP resource read failed');
throw error;

View File

@@ -42,7 +42,7 @@ name: my-subagent
description: What it does
# Model override (optional)
model: claude-3-5-sonnet-20241022
model: claude-sonnet-4-6
temperature: 0.3
maxTokens: 4096

View File

@@ -4,7 +4,7 @@ name: code-reviewer
description: Reviews trading strategy code for bugs, performance issues, and best practices
# Model configuration (optional override)
model: claude-3-5-sonnet-20241022
model: claude-sonnet-4-6
temperature: 0.3
maxTokens: 4096

View File

@@ -334,7 +334,7 @@ timeout: 60000
maxRetries: 3
requiresApproval: false
model: claude-3-5-sonnet-20241022
model: claude-sonnet-4-6
```
### 6. Add Factory Function

View File

@@ -15,5 +15,5 @@ maxValidationRetries: 3 # Max times to retry fixing errors
minBacktestScore: 0.5 # Minimum Sharpe ratio to pass
# Model override (optional)
model: claude-3-5-sonnet-20241022
model: claude-sonnet-4-6
temperature: 0.3

View File

@@ -15,5 +15,5 @@ maxPositionPercent: 0.05 # 5% of portfolio max
minRiskRewardRatio: 2.0 # Minimum 2:1 risk/reward
# Model override (optional)
model: claude-3-5-sonnet-20241022
model: claude-sonnet-4-6
temperature: 0.2