@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
text/typescript
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.');
}
}
}