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