UNPKG

@hashgraphonline/standards-agent-kit

Version:

A modular SDK for building on-chain autonomous agents using Hashgraph Online Standards, including HCS-10 for agent discovery and communication.

232 lines (202 loc) 9.04 kB
import { HCS10Client } from '../hcs10/HCS10Client'; import { StructuredTool, ToolParams } from '@langchain/core/tools'; import { z } from 'zod'; // Import FeeConfigBuilder if needed for explicit fee handling // import { FeeConfigBuilder } from '@hashgraphonline/standards-sdk'; import { Logger } from '@hashgraphonline/standards-sdk'; import { IStateManager, ActiveConnection, } from '../state/state-types'; // Corrected import path/name export interface ConnectionToolParams extends ToolParams { client: HCS10Client; stateManager: IStateManager; } /** * ConnectionTool monitors the *current* agent's inbound topic for connection requests * and automatically handles them using the HCS-10 standard SDK flow. * Use this ONLY to passively LISTEN for other agents trying to connect TO YOU. * This tool takes NO arguments and does NOT start outgoing connections. */ export class ConnectionTool extends StructuredTool { name = 'monitor_connections'; description = "Starts passively LISTENING on the current agent's own inbound topic for INCOMING HCS-10 connection requests. Handles received requests automatically. Takes NO arguments. DO NOT use this to start a new connection TO someone else."; public client: HCS10Client; public logger: Logger; private stateManager: IStateManager; // Renamed property private isMonitoring: boolean = false; // Flag to prevent multiple monitors private monitoringTopic: string | null = null; // Schema now takes NO arguments schema = z.object({}); /** * @param client - Instance of HCS10Client. * @param stateManager - Instance of StateManager for shared state management. */ constructor({ client, stateManager, ...rest }: ConnectionToolParams) { super(rest); this.client = client; this.stateManager = stateManager; // Renamed assignment this.logger = Logger.getInstance({ module: 'ConnectionTool', level: 'info', }); } /** * Initiates the connection request monitoring process in the background. * Gets the inbound topic ID from the configured client. */ async _call(/* _input: z.infer<typeof this.schema> */): Promise<string> { // Get inboundTopicId from the client let inboundTopicId: string; try { // Assuming getInboundTopicId() is implemented and available inboundTopicId = await this.client.getInboundTopicId(); } catch (error) { const errorMsg = `Error getting inbound topic ID for monitoring: ${ error instanceof Error ? error.message : String(error) }`; this.logger.error(errorMsg); return errorMsg; } if (!inboundTopicId) { return 'Error: Could not determine the inbound topic ID for the current agent.'; } if (this.isMonitoring) { if (this.monitoringTopic === inboundTopicId) { return `Already monitoring topic ${inboundTopicId}.`; } else { return `Error: Already monitoring a different topic (${this.monitoringTopic}). Stop the current monitor first.`; // TODO: Add a mechanism to stop the monitor if needed. } } this.isMonitoring = true; this.monitoringTopic = inboundTopicId; this.logger.info( `Initiating connection request monitoring for topic ${inboundTopicId}...` ); // Start the monitoring process asynchronously without awaiting it // This allows the tool call to return quickly. this.monitorIncomingRequests(inboundTopicId).catch((error) => { this.logger.error( `Monitoring loop for ${inboundTopicId} encountered an unrecoverable error:`, error ); this.isMonitoring = false; // Reset flag on loop failure this.monitoringTopic = null; }); return `Started monitoring inbound topic ${inboundTopicId} for connection requests in the background.`; } /** * The core monitoring loop. */ private async monitorIncomingRequests(inboundTopicId: string): Promise<void> { this.logger.info(`Monitoring inbound topic ${inboundTopicId}...`); let lastProcessedMessageSequence = 0; const processedRequestIds = new Set<number>(); // Track processed requests within this monitoring session // Main monitoring loop while (this.isMonitoring && this.monitoringTopic === inboundTopicId) { try { const messagesResult = await this.client.getMessages(inboundTopicId); const allMessages = messagesResult.messages; const connectionRequests = allMessages.filter( (msg) => msg.op === 'connection_request' && typeof msg.sequence_number === 'number' // Keep filtering by sequence number if needed, or remove if checking existing confirmations is sufficient // msg.sequence_number > lastProcessedMessageSequence // Temporarily remove or adjust this if checking confirmations is the primary method ); for (const message of connectionRequests) { // Update lastProcessedMessageSequence regardless of handling outcome to avoid re-checking handled/skipped messages in future loops lastProcessedMessageSequence = Math.max( lastProcessedMessageSequence, message.sequence_number || 0 // Use 0 if sequence_number is undefined (though filter should prevent this) ); const connectionRequestId = message.sequence_number; if (!connectionRequestId) { continue; // Skip if sequence number is missing } // --- Check if already handled --- const alreadyHandled = allMessages.some( (m) => m.op === 'connection_created' && m.connection_id === connectionRequestId ); if (alreadyHandled) { this.logger.debug( `Connection request #${connectionRequestId} already handled (found connection_created). Skipping.` ); continue; // Skip to the next request } // --- End Check --- // Extract requesting account ID from the message's operator_id field (topic@account) const senderOperatorId = message.operator_id || ''; const requestingAccountId = senderOperatorId.split('@')[1] || null; if (!requestingAccountId) { this.logger.warn( `Could not determine requesting account ID from operator_id '${senderOperatorId}' for request #${connectionRequestId}. Skipping.` ); continue; } if (processedRequestIds.has(connectionRequestId)) { this.logger.info( `Connection request #${connectionRequestId} already processed in this session. Skipping.` ); continue; } this.logger.info( `Processing connection request #${connectionRequestId} from account ${requestingAccountId}...` ); try { // Handle the connection request using the HCS10Client wrapper const confirmation = await this.client.handleConnectionRequest( inboundTopicId, requestingAccountId, connectionRequestId ); processedRequestIds.add(connectionRequestId); this.logger.info( `Connection confirmed for request #${connectionRequestId}. New connection topic: ${confirmation.connectionTopicId}` ); // Use stateManager to add connection const newConnection: ActiveConnection = { targetAccountId: requestingAccountId, targetAgentName: `Agent ${requestingAccountId}`, targetInboundTopicId: 'N/A', connectionTopicId: confirmation.connectionTopicId, }; this.stateManager.addActiveConnection(newConnection); this.logger.info( `Added new active connection to ${requestingAccountId} state.` ); } catch (handleError) { this.logger.error( `Error handling connection request #${connectionRequestId} from ${requestingAccountId}:`, handleError ); } } } catch (error) { this.logger.error( `Error fetching or processing messages for topic ${inboundTopicId}:`, error ); // Implement backoff or error threshold if needed } // Wait before the next poll await new Promise((resolve) => setTimeout(resolve, 5000)); // Poll every 5 seconds (adjust as needed) } this.logger.info(`Monitoring loop stopped for topic ${inboundTopicId}.`); this.isMonitoring = false; // Ensure flag is reset when loop exits this.monitoringTopic = null; } // Optional: Method to explicitly stop monitoring public stopMonitoring(): void { if (this.isMonitoring) { this.logger.info( `Stopping monitoring for topic ${this.monitoringTopic}...` ); this.isMonitoring = false; this.monitoringTopic = null; } else { this.logger.info('Monitor is not currently running.'); } } }