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. https://hol.org

1,576 lines (1,409 loc) 76.2 kB
import { BaseServiceBuilder } from 'hedera-agent-kit'; import type { HederaAgentKit } from 'hedera-agent-kit'; import { PrivateKey, TransactionReceipt } from '@hashgraph/sdk'; import { SignerProviderRegistry, type DAppSigner, type NetworkString } from '../../signing/signer-provider'; import { ActiveConnection, IStateManager } from '../../state/state-types'; import { ExecuteResult } from '../types'; import { FeeConfigBuilderInterface, FeeConfigBuilder, HCS10Client, AgentBuilder, InboundTopicType as StandardInboundTopicType, AIAgentCapability as StandardAIAgentCapability, AgentRegistrationResult, ProfileResponse as SDKProfileResponse, HCSMessage, LogLevel, Logger as SDKLogger, SocialPlatform, HandleConnectionRequestResponse, Connection, } from '@hashgraphonline/standards-sdk'; import fs from 'fs'; import path from 'path'; import axios from 'axios'; const NOT_INITIALIZED_ERROR = 'ConnectionsManager not initialized'; /** * Internal agent data for registration */ interface AgentRegistrationData { name: string; bio?: string; alias?: string; type?: 'autonomous' | 'manual'; model?: string; capabilities?: number[]; creator?: string; socials?: Record<string, string>; properties?: Record<string, unknown>; pfpBuffer?: Buffer; pfpFileName?: string; existingProfilePictureTopicId?: string; feeConfig?: FeeConfigBuilderInterface; } /** * Message response with timestamp */ export interface HCSMessageWithTimestamp extends Omit<HCSMessage, 'op'> { timestamp: number; data?: string; sequence_number: number; op?: HCSMessage['op'] | string; operator_id?: string; created?: Date; m?: string; } /** * Network type for HCS-10 */ export type StandardNetworkType = 'mainnet' | 'testnet'; /** * Parameters for agent registration */ export interface RegisterAgentParams { name: string; bio?: string; alias?: string; type?: 'autonomous' | 'manual'; model?: string; capabilities?: number[]; creator?: string; socials?: Record<string, string>; properties?: Record<string, unknown>; profilePicture?: | string | { url?: string; path?: string; filename?: string; }; existingProfilePictureTopicId?: string; initialBalance?: number; userAccountId?: string; hbarFee?: number; tokenFees?: Array<{ amount: number; tokenId: string; }>; exemptAccountIds?: string[]; } /** * Parameters for initiating a connection */ export interface InitiateConnectionParams { targetAccountId: string; disableMonitor?: boolean; memo?: string; } /** * Parameters for accepting a connection request */ export interface AcceptConnectionParams { requestKey: string; hbarFee?: number | undefined; exemptAccountIds?: string[] | undefined; } /** * Parameters for sending a message */ export interface SendMessageParams { topicId: string; message: string; disableMonitoring?: boolean; } /** * Parameters for sending a message to a connected account */ export interface SendMessageToConnectionParams { targetIdentifier: string; message: string; disableMonitoring?: boolean; } /** * HCS10Builder facilitates HCS-10 protocol operations for agent communication * This builder incorporates all HCS10Client functionality directly */ export class HCS10Builder extends BaseServiceBuilder { private standardClient: HCS10Client; private stateManager: IStateManager | undefined; private executeResult?: ExecuteResult & { rawResult?: unknown }; private network: StandardNetworkType; private sdkLogger: SDKLogger; constructor( hederaKit: HederaAgentKit, stateManager?: IStateManager, options?: { useEncryption?: boolean; registryUrl?: string; logLevel?: LogLevel; } ) { super(hederaKit); this.stateManager = stateManager; const network = this.hederaKit.client.network; this.network = network.toString().includes('mainnet') ? 'mainnet' : 'testnet'; const operatorId = this.hederaKit.signer.getAccountId().toString(); const operatorPrivateKey = this.hederaKit.signer?.getOperatorPrivateKey() ? this.hederaKit.signer.getOperatorPrivateKey().toStringRaw() : ''; this.sdkLogger = ((): SDKLogger => { try { if (typeof SDKLogger === 'function') { return new SDKLogger({ module: 'HCS10Builder', level: options?.logLevel || 'info', }); } } catch {} return { debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, trace: () => {}, setLogLevel: () => {}, getLevel: () => 'info', setSilent: () => {}, setModule: () => {}, } as unknown as SDKLogger; })(); this.standardClient = new HCS10Client({ network: this.network, operatorId: operatorId, operatorPrivateKey: operatorPrivateKey, logLevel: options?.logLevel || 'info', }); if (this.stateManager) { this.stateManager.initializeConnectionsManager(this.standardClient); } } /** * Get the operator account ID */ public getOperatorId(): string { const operator = this.standardClient.getClient().operatorAccountId; if (!operator) { throw new Error('Operator Account ID not configured in standard client.'); } return operator.toString(); } /** * Get the network type */ public getNetwork(): StandardNetworkType { return this.network; } /** * Get state manager instance */ public getStateManager(): IStateManager | undefined { return this.stateManager; } /** * Get account and signer information */ public getAccountAndSigner(): { accountId: string; signer: PrivateKey } { const result = this.standardClient.getAccountAndSigner(); return { accountId: result.accountId, signer: result.signer as unknown as PrivateKey, }; } /** * Get the inbound topic ID for the current operator */ public async getInboundTopicId(): Promise<string> { try { const operatorId = this.getOperatorId(); this.logger.info( `[HCS10Builder] Retrieving profile for operator ${operatorId} to find inbound topic...` ); const profileResponse = await this.getAgentProfile(operatorId); if (profileResponse.success && profileResponse.topicInfo?.inboundTopic) { this.logger.info( `[HCS10Builder] Found inbound topic for operator ${operatorId}: ${profileResponse.topicInfo.inboundTopic}` ); return profileResponse.topicInfo.inboundTopic; } else { throw new Error( `Could not retrieve inbound topic from profile for ${operatorId}. Profile success: ${profileResponse.success}, Error: ${profileResponse.error}` ); } } catch (error) { this.logger.error( `[HCS10Builder] Error fetching operator's inbound topic ID (${this.getOperatorId()}):`, error ); const operatorId = this.getOperatorId(); let detailedMessage = `Failed to get inbound topic ID for operator ${operatorId}.`; if ( error instanceof Error && error.message.includes('does not have a valid HCS-11 memo') ) { detailedMessage += ` The account profile may not exist or is invalid. Please ensure this operator account (${operatorId}) is registered as an HCS-10 agent. You might need to register it first (e.g., using the 'register_agent' tool or SDK function).`; } else if (error instanceof Error) { detailedMessage += ` Reason: ${error.message}`; } else { detailedMessage += ` Unexpected error: ${String(error)}`; } throw new Error(detailedMessage); } } /** * Get agent profile */ public async getAgentProfile(accountId: string): Promise<SDKProfileResponse> { try { return await this.standardClient.retrieveProfile(accountId); } catch (error) { this.logger.error( `[HCS10Builder] Error retrieving agent profile for account ${accountId}:`, error ); throw error; } } /** * Submit connection request */ public async submitConnectionRequest( inboundTopicId: string, memo: string ): Promise<TransactionReceipt> { const start = SignerProviderRegistry.startHCSDelegate; const exec = SignerProviderRegistry.walletExecutor; const preferWallet = SignerProviderRegistry.preferWalletOnly; const network: NetworkString = this.network; try { const { ByteBuildRegistry } = await import('../../signing/bytes-registry'); if (exec && ByteBuildRegistry.has('submitConnectionRequest')) { const built = await ByteBuildRegistry.build('submitConnectionRequest', this.hederaKit, { inboundTopicId, memo }); if (built && built.transactionBytes) { const { transactionId } = await exec(built.transactionBytes, network); return ({ transactionId } as unknown) as TransactionReceipt; } } } catch {} if (start && exec) { try { const request: Record<string, unknown> = { inboundTopicId, memo }; const { transactionBytes } = await start('submitConnectionRequest', request, network); const { transactionId } = await exec(transactionBytes, network); return ({ transactionId } as unknown) as TransactionReceipt; } catch (err) { if (preferWallet) { const e = new Error(`wallet_submit_failed: ${err instanceof Error ? err.message : String(err)}`); (e as unknown as { code: string }).code = 'wallet_submit_failed'; throw e; } } } else if (preferWallet) { const e = new Error('wallet_unavailable: connect a wallet or configure StartHCSDelegate and WalletExecutor'); (e as unknown as { code: string }).code = 'wallet_unavailable'; throw e; } return this.standardClient.submitConnectionRequest( inboundTopicId, memo ) as Promise<TransactionReceipt>; } /** * Handle connection request */ public async handleConnectionRequest( inboundTopicId: string, requestingAccountId: string, connectionRequestId: number, feeConfig?: FeeConfigBuilderInterface ): Promise<HandleConnectionRequestResponse> { try { const start = SignerProviderRegistry.startHCSDelegate; const exec = SignerProviderRegistry.walletExecutor; const preferWallet = SignerProviderRegistry.preferWalletOnly; const network: NetworkString = this.network; if (start && exec) { try { const request: Record<string, unknown> = { inboundTopicId, requestingAccountId, connectionRequestId, feeConfig: feeConfig ? 'configured' : undefined, }; const { transactionBytes } = await start('handleConnectionRequest', request, network); const { transactionId } = await exec(transactionBytes, network); const minimal = { success: true, transactionId, } as unknown as HandleConnectionRequestResponse; return minimal; } catch (err) { if (preferWallet) { const e = new Error(`wallet_submit_failed: ${err instanceof Error ? err.message : String(err)}`); (e as unknown as { code: string }).code = 'wallet_submit_failed'; throw e; } } } else if (preferWallet) { const e = new Error('wallet_unavailable: connect a wallet or configure StartHCSDelegate and WalletExecutor'); (e as unknown as { code: string }).code = 'wallet_unavailable'; throw e; } const result = await this.standardClient.handleConnectionRequest( inboundTopicId, requestingAccountId, connectionRequestId, feeConfig ); if ( result && result.connectionTopicId && typeof result.connectionTopicId === 'object' && 'toString' in result.connectionTopicId ) { result.connectionTopicId = ( result.connectionTopicId as { toString(): string } ).toString(); } return result; } catch (error) { this.logger.error( `Error handling connection request #${connectionRequestId} for topic ${inboundTopicId}:`, error ); throw new Error( `Failed to handle connection request: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Send message to a topic */ public async sendMessage( topicId: string, data: string, memo?: string ): Promise<{ sequenceNumber: number | undefined; receipt: TransactionReceipt; transactionId: string | undefined; }> { if (topicId && typeof topicId === 'object' && 'toString' in topicId) { topicId = (topicId as unknown as { toString: () => string })?.toString(); } if (!topicId || typeof topicId !== 'string') { throw new Error( `Invalid topic ID provided to sendMessage: ${JSON.stringify(topicId)}` ); } try { let prevMaxSeq = 0 try { const prev = await this.getMessages(topicId) prevMaxSeq = prev.messages.reduce((max, m) => Math.max(max, m.sequence_number || 0), 0) } catch {} const start = SignerProviderRegistry.startHCSDelegate; const exec = SignerProviderRegistry.walletExecutor; const preferWallet = SignerProviderRegistry.preferWalletOnly; const network: NetworkString = this.network; try { const { ByteBuildRegistry } = await import('../../signing/bytes-registry'); if (exec && ByteBuildRegistry.has('sendMessage')) { const built = await ByteBuildRegistry.build('sendMessage', this.hederaKit, { topicId, data, memo }); if (built && built.transactionBytes) { const { transactionId } = await exec(built.transactionBytes, network); const sequenceNumber = await this.pollForNewSequence(topicId, prevMaxSeq); return { sequenceNumber, receipt: ({ transactionId } as unknown) as TransactionReceipt, transactionId, }; } } } catch {} if (start && exec) { try { const request: Record<string, unknown> = { topicId, data, memo }; const { transactionBytes } = await start('sendMessage', request, network); const { transactionId } = await exec(transactionBytes, network); const sequenceNumber = await this.pollForNewSequence(topicId, prevMaxSeq); return { sequenceNumber, receipt: ({ transactionId } as unknown) as TransactionReceipt, transactionId, }; } catch (err) { if (preferWallet) { const e = new Error(`wallet_submit_failed: ${err instanceof Error ? err.message : String(err)}`); (e as unknown as { code: string }).code = 'wallet_submit_failed'; throw e; } } } else if (preferWallet) { const e = new Error('wallet_unavailable: connect a wallet or configure StartHCSDelegate and WalletExecutor'); (e as unknown as { code: string }).code = 'wallet_unavailable'; throw e; } const messageResponse = await this.standardClient.sendMessage( topicId, data, memo, undefined ); return { sequenceNumber: messageResponse.topicSequenceNumber?.toNumber(), receipt: messageResponse as unknown as TransactionReceipt, transactionId: 'transactionId' in messageResponse ? ( messageResponse as { transactionId?: { toString(): string } } ).transactionId?.toString() : undefined, }; } catch (error) { this.logger.error(`Error sending message to topic ${topicId}:`, error); throw new Error( `Failed to send message: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Get messages from a topic */ public async getMessages(topicId: string): Promise<{ messages: HCSMessageWithTimestamp[]; }> { if (topicId && typeof topicId === 'object' && 'toString' in topicId) { topicId = (topicId as unknown as { toString: () => string })?.toString(); } if (!topicId || typeof topicId !== 'string') { throw new Error( `Invalid topic ID provided to getMessages: ${JSON.stringify(topicId)}` ); } try { const result = await this.standardClient.getMessages(topicId); const mappedMessages: HCSMessageWithTimestamp[] = result.messages.map( (sdkMessage) => { const timestamp = sdkMessage?.created?.getTime() || 0; return { ...sdkMessage, timestamp: timestamp, data: sdkMessage.data || '', sequence_number: sdkMessage.sequence_number, p: 'hcs-10' as const, } as HCSMessageWithTimestamp; } ); mappedMessages.sort( (a: { timestamp: number }, b: { timestamp: number }) => a.timestamp - b.timestamp ); return { messages: mappedMessages }; } catch (error) { this.logger.error(`Error getting messages from topic ${topicId}:`, error); return { messages: [] }; } } /** * Get message stream from a topic */ public async getMessageStream(topicId: string): Promise<{ messages: HCSMessage[]; }> { if (topicId && typeof topicId === 'object' && 'toString' in topicId) { topicId = (topicId as unknown as { toString: () => string })?.toString(); } if (!topicId || typeof topicId !== 'string') { throw new Error( `Invalid topic ID provided to getMessageStream: ${JSON.stringify( topicId )}` ); } return this.standardClient.getMessageStream(topicId) as Promise<{ messages: HCSMessage[]; }>; } /** * Get message content */ public async getMessageContent(inscriptionIdOrData: string): Promise<string> { try { const content = await this.standardClient.getMessageContent( inscriptionIdOrData ); return content as string; } catch (error) { this.logger.error( `Error retrieving message content for: ${inscriptionIdOrData}`, error ); throw new Error( `Failed to retrieve message content: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Get the standard client instance (for compatibility) */ public getStandardClient(): HCS10Client { return this.standardClient; } /** * Load profile picture from URL or file path */ private async loadProfilePicture( profilePicture: string | { url?: string; path?: string; filename?: string } ): Promise<{ buffer: Buffer; filename: string } | null> { try { if (!profilePicture) { return null; } if (typeof profilePicture === 'string') { const isUrl = profilePicture.startsWith('http://') || profilePicture.startsWith('https://'); if (isUrl) { this.logger.info( `Loading profile picture from URL: ${profilePicture}` ); const response = await axios.get(profilePicture, { responseType: 'arraybuffer', }); const buffer = Buffer.from(response.data); const urlPathname = new URL(profilePicture).pathname; const filename = path.basename(urlPathname) || 'profile.png'; return { buffer, filename }; } else { if (!fs.existsSync(profilePicture)) { this.logger.warn( `Profile picture file not found: ${profilePicture}` ); return null; } this.logger.info( `Loading profile picture from file: ${profilePicture}` ); const buffer = fs.readFileSync(profilePicture); const filename = path.basename(profilePicture); return { buffer, filename }; } } else if (profilePicture.url) { this.logger.info( `Loading profile picture from URL: ${profilePicture.url}` ); const response = await axios.get(profilePicture.url, { responseType: 'arraybuffer', }); const buffer = Buffer.from(response.data); const filename = profilePicture.filename || 'profile.png'; return { buffer, filename }; } else if (profilePicture.path) { if (!fs.existsSync(profilePicture.path)) { this.logger.warn( `Profile picture file not found: ${profilePicture.path}` ); return null; } this.logger.info( `Loading profile picture from file: ${profilePicture.path}` ); const buffer = fs.readFileSync(profilePicture.path); const filename = profilePicture.filename || path.basename(profilePicture.path); return { buffer, filename }; } return null; } catch (error) { this.logger.error('Failed to load profile picture:', error); return null; } } private async pollForNewSequence(topicId: string, prevMax: number): Promise<number | undefined> { const maxAttempts = 10 const delayMs = 1000 for (let i = 0; i < maxAttempts; i++) { try { const res = await this.getMessages(topicId) const maxSeq = res.messages.reduce((m, msg) => Math.max(m, msg.sequence_number || 0), prevMax) if (maxSeq > prevMax) return maxSeq } catch {} await new Promise((r) => setTimeout(r, delayMs)) } return undefined } /** * Create and register an agent */ private async createAndRegisterAgent( data: AgentRegistrationData ): Promise<AgentRegistrationResult> { const builder = new AgentBuilder() .setName(data.name) .setBio(data.bio || '') .setCapabilities( data.capabilities || [StandardAIAgentCapability.TEXT_GENERATION] ) .setType(data.type || 'autonomous') .setModel(data.model || 'agent-model-2024') .setNetwork(this.getNetwork()) .setInboundTopicType(StandardInboundTopicType.PUBLIC); if (data.alias) { builder.setAlias(data.alias); } if (data.creator) { builder.setCreator(data.creator); } if (data?.feeConfig) { builder.setInboundTopicType(StandardInboundTopicType.FEE_BASED); builder.setFeeConfig(data.feeConfig); } if (data.existingProfilePictureTopicId) { builder.setExistingProfilePicture(data.existingProfilePictureTopicId); } else if (data.pfpBuffer && data.pfpFileName) { if (data.pfpBuffer.byteLength === 0) { this.logger.warn( 'Provided PFP buffer is empty. Skipping profile picture.' ); } else { this.logger.info( `Setting profile picture: ${data.pfpFileName} (${data.pfpBuffer.byteLength} bytes)` ); builder.setProfilePicture(data.pfpBuffer, data.pfpFileName); } } else { this.logger.warn( 'Profile picture not provided. Agent creation might fail if required by the underlying SDK builder.' ); } if (data.socials) { Object.entries(data.socials).forEach(([platform, handle]) => { builder.addSocial(platform as SocialPlatform, handle); }); } if (data.properties) { Object.entries(data.properties).forEach(([key, value]) => { builder.addProperty(key, value); }); } try { const hasFees = Boolean(data?.feeConfig); const result = await this.standardClient.createAndRegisterAgent(builder, { initialBalance: hasFees ? 50 : 10, }); return result; } catch (error) { this.logger.error('Error during agent creation/registration:', error); throw new Error( `Failed to create/register agent: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Register a new HCS-10 agent * Note: This performs multiple transactions and requires directExecution mode */ public async registerAgent(params: RegisterAgentParams): Promise<this> { this.clearNotes(); if (this.hederaKit.operationalMode === 'returnBytes') { throw new Error( 'Agent registration requires multiple transactions and cannot be performed in returnBytes mode. ' + 'Please use autonomous mode.' ); } try { let profilePictureData: { buffer: Buffer; filename: string } | null = null; if (params.profilePicture) { profilePictureData = await this.loadProfilePicture( params.profilePicture ); } const registrationData: AgentRegistrationData = { name: params.name, ...(params.bio !== undefined && { bio: params.bio }), ...(params.alias !== undefined && { alias: params.alias }), ...(params.type !== undefined && { type: params.type }), ...(params.model !== undefined && { model: params.model }), ...(params.capabilities !== undefined && { capabilities: params.capabilities, }), ...(params.creator !== undefined && { creator: params.creator }), ...(params.socials !== undefined && { socials: params.socials }), ...(params.properties !== undefined && { properties: params.properties, }), ...(params.existingProfilePictureTopicId !== undefined && { existingProfilePictureTopicId: params.existingProfilePictureTopicId, }), ...(profilePictureData?.buffer !== undefined && { pfpBuffer: profilePictureData.buffer, }), ...(profilePictureData?.filename !== undefined && { pfpFileName: profilePictureData.filename, }), }; if (params.hbarFee && params.hbarFee > 0) { const feeConfigBuilder = new FeeConfigBuilder({ network: this.network, logger: this.sdkLogger, }); const { accountId: collectorAccountId } = this.getAccountAndSigner(); if (!collectorAccountId) { throw new Error('Could not determine account ID for fee collection.'); } this.addNote( `Setting the operator account (${collectorAccountId}) as the fee collector since no specific collector was provided.` ); const effectiveExemptIds = params.exemptAccountIds?.filter( (id: string) => id !== collectorAccountId && id.startsWith('0.0') ) || []; registrationData.feeConfig = feeConfigBuilder.addHbarFee( params.hbarFee, collectorAccountId, effectiveExemptIds ); } const preferWallet = SignerProviderRegistry.preferWalletOnly; const browserClient = SignerProviderRegistry.getBrowserHCSClient(this.network as NetworkString) as | { create: (builder: unknown, opts?: Record<string, unknown>) => Promise<unknown> } | null; if (browserClient) { try { const aBuilder = new AgentBuilder() .setNetwork(this.network) .setName(registrationData.name) .setAlias(registrationData.alias || `${registrationData.name}-${Date.now()}`) .setBio(registrationData.bio || '') .setType(registrationData.type || 'autonomous') .setModel(registrationData.model || 'agent-model-2024'); if (registrationData.capabilities?.length) { aBuilder.setCapabilities(registrationData.capabilities as unknown as number[]); } if (registrationData.creator) aBuilder.setCreator(registrationData.creator); if (registrationData.existingProfilePictureTopicId) { aBuilder.setExistingProfilePicture(registrationData.existingProfilePictureTopicId); } if (registrationData.socials) { Object.entries(registrationData.socials).forEach(([platform, handle]) => { if (handle && typeof handle === 'string') { aBuilder.addSocial(platform as SocialPlatform, handle); } }); } if (registrationData.properties) { Object.entries(registrationData.properties).forEach(([key, value]) => { if (value != null) aBuilder.addProperty(key, value as unknown as string); }); } const resp = await browserClient.create(aBuilder, { progressCallback: (_: unknown) => {}, updateAccountMemo: true, }); this.executeResult = { success: true, transactionId: undefined, receipt: undefined, scheduleId: undefined, rawResult: { result: resp, name: registrationData.name }, } as unknown as ExecuteResult & { rawResult?: unknown }; return this; } catch (walletErr) { if (preferWallet) { throw new Error(`wallet_registration_failed: ${walletErr instanceof Error ? walletErr.message : String(walletErr)}`); } } } else if (preferWallet) { throw new Error('wallet_unavailable: BrowserHCSClient factory not provided'); } const result = await this.createAndRegisterAgent(registrationData); this.executeResult = { success: true, transactionId: result.transactionId, receipt: undefined, scheduleId: undefined, rawResult: { ...result, name: params.name, accountId: result?.metadata?.accountId || result.state?.agentMetadata?.accountId, }, } as unknown as ExecuteResult & { rawResult?: unknown }; } catch (error) { this.logger.error('Failed to register agent:', error); throw error; } return this; } /** * Initiate a connection to another agent */ public async initiateConnection( params: InitiateConnectionParams ): Promise<this> { this.clearNotes(); try { const targetProfile = await this.getAgentProfile(params.targetAccountId); if (!targetProfile.success || !targetProfile.topicInfo?.inboundTopic) { throw new Error( `Could not retrieve inbound topic for target account ${params.targetAccountId}` ); } const targetInboundTopicId = targetProfile.topicInfo.inboundTopic; let memo: string; if (params.memo !== undefined) { memo = params.memo; } else { memo = params.disableMonitor ? 'false' : 'true'; this.addNote( `No custom memo was provided. Using default memo '${memo}' based on monitoring preference.` ); } if (!params.disableMonitor) { this.addNote( `Monitoring will be enabled for this connection request as disableMonitor was not specified.` ); } const result = await this.submitConnectionRequest( targetInboundTopicId, memo ); this.executeResult = { success: true, transactionId: 'transactionId' in result ? ( result as { transactionId?: { toString(): string } } ).transactionId?.toString() : undefined, receipt: result, scheduleId: undefined, rawResult: { targetAccountId: params.targetAccountId, targetInboundTopicId, connectionRequestSent: true, monitoringEnabled: !params.disableMonitor, ...result, }, } as unknown as ExecuteResult & { rawResult?: unknown }; } catch (error) { this.logger.error('Failed to initiate connection:', error); throw error; } return this; } /** * Accept a connection request * Note: This performs multiple transactions and requires directExecution mode */ public async acceptConnection(params: AcceptConnectionParams): Promise<this> { this.clearNotes(); if (this.hederaKit.operationalMode === 'returnBytes') { throw new Error( 'Accepting connections requires multiple transactions and cannot be performed in returnBytes mode. ' + 'Please use autonomous mode.' ); } try { const currentAgent = this.stateManager?.getCurrentAgent(); if (!currentAgent) { throw new Error( 'Cannot accept connection request. No agent is currently active. Please register or select an agent first.' ); } const connectionsManager = this.stateManager?.getConnectionsManager(); if (!connectionsManager) { throw new Error(NOT_INITIALIZED_ERROR); } await connectionsManager.fetchConnectionData(currentAgent.accountId); const allRequests = [ ...connectionsManager.getPendingRequests(), ...connectionsManager.getConnectionsNeedingConfirmation(), ]; const request = allRequests.find( (r) => r.uniqueRequestKey === params.requestKey || r.connectionRequestId?.toString() === params.requestKey || r.inboundRequestId?.toString() === params.requestKey ); if (!request) { throw new Error( `Request with key ${params.requestKey} not found or no longer pending.` ); } if (!request.needsConfirmation || !request.inboundRequestId) { throw new Error( `Request with key ${params.requestKey} is not an inbound request that can be accepted.` ); } const targetAccountId = request.targetAccountId; const inboundRequestId = request.inboundRequestId; let feeConfig: FeeConfigBuilderInterface | undefined; if (params.hbarFee && params.hbarFee > 0) { const feeConfigBuilder = new FeeConfigBuilder({ network: this.network, logger: this.sdkLogger, }); const { accountId: collectorAccountId } = this.getAccountAndSigner(); if (!collectorAccountId) { throw new Error('Could not determine account ID for fee collection.'); } this.addNote( `Setting the operator account (${collectorAccountId}) as the fee collector since no specific collector was provided.` ); const effectiveExemptIds = params.exemptAccountIds?.filter( (id: string) => id !== collectorAccountId && id.startsWith('0.0') ) || []; feeConfig = feeConfigBuilder.addHbarFee( params.hbarFee, collectorAccountId, effectiveExemptIds ); } const inboundTopicId = await this.getInboundTopicId(); const confirmationResult = await this.handleConnectionRequest( inboundTopicId, targetAccountId, inboundRequestId, feeConfig ); let connectionTopicId = confirmationResult?.connectionTopicId; if ( connectionTopicId && typeof connectionTopicId === 'object' && 'toString' in connectionTopicId ) { connectionTopicId = ( connectionTopicId as unknown as { toString: () => string } )?.toString(); } if (!connectionTopicId || typeof connectionTopicId !== 'string') { try { const refreshed = await this.stateManager?.getConnectionsManager()?.fetchConnectionData(currentAgent.accountId); const established = (refreshed || []).find( (c: unknown) => (c as { targetAccountId?: string; status?: string }).targetAccountId === targetAccountId && (c as { status?: string }).status === 'established' ) as { connectionTopicId?: string } | undefined; if (established?.connectionTopicId) { connectionTopicId = established.connectionTopicId; } } catch (e) { this.logger.debug('Could not refresh connections after acceptance to derive topic id:', e); } } if (!connectionTopicId || typeof connectionTopicId !== 'string') { throw new Error( `Failed to create connection topic. Got: ${JSON.stringify( connectionTopicId )}` ); } if (this.stateManager) { const targetAgentName = request.targetAgentName || `Agent ${targetAccountId}`; if (!request.targetAgentName) { this.addNote( `No agent name was provided in the connection request, using default name 'Agent ${targetAccountId}'.` ); } let targetInboundTopicId = request.targetInboundTopicId || ''; if (!targetInboundTopicId) { try { const targetProfile = await this.getAgentProfile(targetAccountId); if ( targetProfile.success && targetProfile.topicInfo?.inboundTopic ) { targetInboundTopicId = targetProfile.topicInfo.inboundTopic; } } catch (profileError) { this.logger.warn( `Could not fetch profile for ${targetAccountId}:`, profileError ); } } const newConnection = { connectionId: `conn-${Date.now()}`, targetAccountId: targetAccountId, targetAgentName: targetAgentName, targetInboundTopicId: targetInboundTopicId, connectionTopicId: connectionTopicId, status: 'established' as const, created: new Date(), }; this.stateManager.addActiveConnection(newConnection); connectionsManager.markConnectionRequestProcessed( request.targetInboundTopicId || '', inboundRequestId ); } this.executeResult = { success: true, transactionId: undefined, receipt: undefined, scheduleId: undefined, rawResult: { targetAccountId, connectionTopicId, feeConfigured: !!params.hbarFee, hbarFee: params.hbarFee || 0, confirmationResult, }, } as unknown as ExecuteResult & { rawResult?: unknown }; } catch (error) { this.logger.error('Failed to accept connection:', error); throw error; } return this; } /** * Send a message using HCS (for operations that need direct topic access) */ public async sendHCS10Message(params: SendMessageParams): Promise<this> { this.clearNotes(); try { const result = await this.sendMessage(params.topicId, params.message); this.executeResult = { success: true, transactionId: result.transactionId, receipt: result.receipt, scheduleId: undefined, rawResult: result, } as unknown as ExecuteResult & { rawResult?: unknown }; this.addNote(`Message sent to topic ${params.topicId}.`); } catch (error) { this.logger.error('Failed to send message:', error); throw error; } return this; } /** * Send a message to a connected account with optional response monitoring */ public async sendMessageToConnection( params: SendMessageToConnectionParams ): Promise<this> { this.clearNotes(); if (!this.stateManager) { throw new Error( 'StateManager is required to send messages to connections' ); } try { const currentAgent = this.stateManager.getCurrentAgent(); if (!currentAgent) { throw new Error( 'Cannot send message. No agent is currently active. Please register or select an agent first.' ); } let connection: ActiveConnection | undefined; const identifier = params.targetIdentifier; if (identifier.includes('@')) { const parts = identifier.split('@'); if (parts.length === 2) { const accountId = parts[1]; connection = this.stateManager.getConnectionByIdentifier(accountId); if (!connection) { this.addNote( `Could not find connection using request key '${identifier}', extracted account ID '${accountId}'.` ); } } } if (!connection) { connection = this.stateManager.getConnectionByIdentifier(identifier); } if ( !connection && !identifier.startsWith('0.0.') && /^\d+$/.test(identifier) ) { const accountIdWithPrefix = `0.0.${identifier}`; connection = this.stateManager.getConnectionByIdentifier(accountIdWithPrefix); if (connection) { this.addNote( `Found connection using account ID with prefix: ${accountIdWithPrefix}` ); } } if (!connection && /^[1-9]\d*$/.test(identifier)) { const connections = this.stateManager.listConnections(); const index = parseInt(identifier) - 1; if (index >= 0 && index < connections.length) { connection = connections[index]; if (connection) { this.addNote( `Found connection by index ${identifier}: ${connection.targetAccountId}` ); } } } if (!connection) { const connections = this.stateManager.listConnections(); const availableIds = connections.map( (c, i) => `${i + 1}. ${c.targetAccountId} (Topic: ${c.connectionTopicId})` ); let errorMsg = `Connection not found for identifier: "${identifier}"\n`; errorMsg += `Available connections:\n${ availableIds.join('\n') || 'No active connections' }`; errorMsg += `\n\nYou can use:\n`; errorMsg += `- Connection number (e.g., "1", "2")\n`; errorMsg += `- Account ID (e.g., "0.0.6412936")\n`; errorMsg += `- Connection topic ID\n`; errorMsg += `Use 'list_connections' to see all active connections.`; throw new Error(errorMsg); } let connectionTopicId = connection.connectionTopicId; if ( connectionTopicId && typeof connectionTopicId === 'object' && 'toString' in connectionTopicId ) { connectionTopicId = ( connectionTopicId as unknown as { toString: () => string } )?.toString(); } if (!connectionTopicId || typeof connectionTopicId !== 'string') { throw new Error( `Invalid connection topic ID for ${ connection.targetAccountId }: ${JSON.stringify( connectionTopicId )} (type: ${typeof connectionTopicId})` ); } const targetAgentName = connection.targetAgentName; const operatorId = `${currentAgent.inboundTopicId}@${currentAgent.accountId}`; let baseSeq = 0 try { const prev = await this.getMessages(connectionTopicId) baseSeq = prev.messages.reduce((max, m) => Math.max(max, m.sequence_number || 0), 0) } catch {} const messageResult = await this.sendMessage( connectionTopicId, params.message, `Agent message from ${currentAgent.name}` ); const effectiveSeq = messageResult.sequenceNumber ?? baseSeq if (effectiveSeq === 0) { throw new Error('Failed to send message'); } let reply: string | null = null; if (!params.disableMonitoring) { reply = await this.monitorResponses( connectionTopicId, operatorId, effectiveSeq ); } else { this.addNote( `Message sent successfully. Response monitoring was disabled.` ); } this.executeResult = { success: true, transactionId: messageResult.transactionId, receipt: messageResult.receipt, scheduleId: undefined, rawResult: { targetAgentName, targetAccountId: connection.targetAccountId, connectionTopicId, sequenceNumber: messageResult.sequenceNumber, reply, monitoringEnabled: !params.disableMonitoring, message: params.message, messageResult, }, }; } catch (error) { this.logger.error('Failed to send message to connection:', error); throw error; } return this; } /** * Monitor responses on a topic after sending a message */ private async monitorResponses( topicId: string, operatorId: string, sequenceNumber: number ): Promise<string | null> { const maxAttempts = 30; let attempts = 0; while (attempts < maxAttempts) { try { const messages = await this.getMessageStream(topicId); for (const message of messages.messages) { if ( message.sequence_number < sequenceNumber || message.operator_id === operatorId ) { continue; } const content = await this.getMessageContent(message.data || ''); return content; } } catch (error) { this.logger.error(`Error monitoring responses: ${error}`); } await new Promise((resolve) => setTimeout(resolve, 4000)); attempts++; } return null; } /** * Start passive monitoring for incoming connection requests * This method monitors continuously in the background */ public async startPassiveConnectionMonitoring(): Promise<this> { this.clearNotes(); if (!this.stateManager) { throw new Error('StateManager is required for passive monitoring'); } const inboundTopicId = await this.getInboundTopicId(); this.logger.info( `Starting passive connection monitoring on topic ${inboundTopicId}...` ); this.executeResult = { success: true, transactionId: undefined, receipt: undefined, scheduleId: undefined, rawResult: { inboundTopicId, message: `Started monitoring inbound topic ${inboundTopicId} for connection requests in the background.`, }, } as unknown as ExecuteResult & { rawResult?: unknown }; return this; } /** * Monitor for incoming connection requests */ public async monitorConnections(params: { acceptAll?: boolean; targetAccountId?: string; monitorDurationSeconds?: number; hbarFees?: Array<{ amount: number; collectorAccount?: string }>; tokenFees?: Array<{ amount: number; tokenId: string; collectorAccount?: string; }>; exemptAccountIds?: string[]; defaultCollectorAccount?: string; }): Promise<this> { this.clearNotes(); const { acceptAll = false, targetAccountId, monitorDurationSeconds = 120, hbarFees = [], tokenFees = [], exemptAccountIds = [], defaultCollectorAccount, } = params; if (!this.stateManager) { throw new Error('StateManager is required for connection monitoring'); } const currentAgent = this.stateManager.getCurrentAgent(); if (!currentAgent) { throw new Error( 'Cannot monitor for connections. No agent is currently active.' ); } const inboundTopicId = await this.getInboundTopicId(); const endTime = Date.now() + monitorDurationSeconds * 1000; const pollIntervalMs = 3000; let connectionRequestsFound = 0; let acceptedConnections = 0; const processedRequestIds = new Set<number>(); while (Date.now() < endTime) { try { const messagesResult = await this.getMessages(inboundTopicId); const connectionRequests = messagesResult.messages.filter( (msg) => msg.op === 'connection_request' && typeof msg.sequence_number === 'number' ); for (const request of connectionRequests) { const connectionRequestId = request.sequence_number; if ( !connectionRequestId || processedRequestIds.has(connectionRequestId) ) { continue; } const requestingAccountId = request.operator_id?.split('@')[1]; if (!requestingAccountId) { continue; } connectionRequestsFound++; if (targetAccountId && requestingAccountId !== targetAccountId) { this.logger.info( `Skipping request from ${requestingAccountId} (not target account)` ); continue; } if (acceptAll || targetAccountId === requestingAccountId) { this.logger.info( `Accepting connection request from ${requestingAccountId}` ); let feeConfig; if (hbarFees.length > 0 || tokenFees.length > 0) { const builder = new FeeConfigBuilder({ network: this.network, logger: this.sdkLogger, }); for (const fee of hbarFees) { const collectorAccount = fee.collectorAccount || defaultCollectorAccount || this.getOperatorId(); builder.addHbarFee( fee.amount, collectorAccount, exemptAccountIds ); } for (const fee of tokenFees) { const collectorAccount = fee.collectorAccount || defaultCollectorAccount || this.getOperatorId(); builder.addTokenFee( fee.amount, fee.tokenId, collectorAccount, undefined, exemptAccountIds ); } feeConfig = builder; } await this.handleConnectionRequest( inboundTopicId, requestingAccountId, connectionRequestId, feeConfig ); processedRequestIds.add(connectionRequestId);