@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.
195 lines (173 loc) • 7.54 kB
text/typescript
import { StructuredTool, ToolParams } from '@langchain/core/tools';
import { z } from 'zod';
import { HCS10Client, HCSMessageWithTimestamp } from '../hcs10/HCS10Client';
import { IStateManager } from '../state/state-types';
import { Logger } from '@hashgraphonline/standards-sdk'; // Assuming logger utility
export interface CheckMessagesToolParams extends ToolParams {
hcsClient: HCS10Client;
stateManager: IStateManager;
}
/**
* A tool to check for new messages on an active HCS-10 connection topic,
* or optionally fetch the latest messages regardless of timestamp.
*/
export class CheckMessagesTool extends StructuredTool {
name = 'check_messages';
description = `Checks for and retrieves messages from an active connection.
Identify the target agent using their account ID (e.g., 0.0.12345) or the connection number shown in 'list_connections'.
By default, it only retrieves messages newer than the last check.
Use 'fetchLatest: true' to get the most recent messages regardless of when they arrived.
Use 'lastMessagesCount' to specify how many latest messages to retrieve (default 1 when fetchLatest is true).`;
schema = z.object({
targetIdentifier: z
.string()
.describe(
"The account ID (e.g., 0.0.12345) of the target agent OR the connection number (e.g., '1', '2') from the 'list_connections' tool to check messages for."
),
fetchLatest: z
.boolean()
.optional()
.default(false)
.describe(
'Set to true to fetch the latest messages even if they have been seen before, ignoring the last checked timestamp. Defaults to false (fetching only new messages).'
),
lastMessagesCount: z
.number()
.int()
.positive()
.optional()
.describe(
'When fetchLatest is true, specifies how many of the most recent messages to retrieve. Defaults to 1.'
),
});
public hcsClient: HCS10Client;
private stateManager: IStateManager;
private logger: Logger;
constructor({ hcsClient, stateManager, ...rest }: CheckMessagesToolParams) {
super(rest);
this.hcsClient = hcsClient;
this.stateManager = stateManager;
this.logger = Logger.getInstance({ module: 'CheckMessagesTool' });
}
protected async _call({
targetIdentifier,
fetchLatest,
lastMessagesCount,
}: z.infer<this['schema']>): Promise<string> {
const connection =
this.stateManager.getConnectionByIdentifier(targetIdentifier);
if (!connection) {
return `Error: Could not find an active connection matching identifier "${targetIdentifier}". Use 'list_connections' to see active connections.`;
}
const connectionTopicId = connection.connectionTopicId;
const targetAgentName = connection.targetAgentName;
const lastProcessedTimestamp =
this.stateManager.getLastTimestamp(connectionTopicId);
this.logger.info(
`Checking messages for connection with ${targetAgentName} (${connection.targetAccountId}) on topic ${connectionTopicId} (fetchLatest: ${fetchLatest}, lastCount: ${lastMessagesCount}, since: ${lastProcessedTimestamp})`
);
try {
// 1. Get messages from the topic
const result = await this.hcsClient.getMessages(connectionTopicId);
const allMessages = result.messages;
if (!allMessages || allMessages.length === 0) {
return `No messages found on connection topic ${connectionTopicId}.`;
}
let messagesToProcess: HCSMessageWithTimestamp[] = [];
let latestTimestampNanos = lastProcessedTimestamp;
const isFetchingLatest = fetchLatest === true;
if (isFetchingLatest) {
this.logger.info('Fetching latest messages regardless of timestamp.');
const count = lastMessagesCount ?? 1;
messagesToProcess = allMessages.slice(-count);
} else {
this.logger.info(
`Filtering for messages newer than ${lastProcessedTimestamp}`
);
messagesToProcess = allMessages.filter((msg) => {
const msgTimestampNanos = msg.timestamp * 1_000_000;
return msgTimestampNanos > lastProcessedTimestamp;
});
if (messagesToProcess.length > 0) {
latestTimestampNanos = messagesToProcess.reduce(
(maxTs, msg) => Math.max(maxTs, msg.timestamp * 1_000_000),
lastProcessedTimestamp
);
}
}
if (messagesToProcess.length === 0) {
return isFetchingLatest
? `Could not retrieve the latest message(s). No messages found on topic ${connectionTopicId}.`
: `No new messages found for connection with ${targetAgentName} since last check.`;
}
this.logger.info(`Processing ${messagesToProcess.length} message(s).`);
// 3. Process messages (resolve inscriptions, format)
let outputString = isFetchingLatest
? `Latest message(s) from ${targetAgentName}:
`
: `New messages from ${targetAgentName}:
`;
for (const msg of messagesToProcess) {
let content = msg.data;
try {
// Check for inscription HRL
if (typeof content === 'string' && content.startsWith('hcs://')) {
this.logger.debug(`Resolving inscribed message: ${content}`);
content = await this.hcsClient.getMessageContent(content);
this.logger.debug(`Resolved content length: ${content?.length}`);
}
// Attempt to parse the content as the HCS-10 structure
let displayContent = content; // Default to raw content
try {
const parsed = JSON.parse(content || '{}');
if (
parsed.p === 'hcs-10' &&
parsed.op === 'message' &&
parsed.data
) {
// Extract sender and actual data from standard message format
const senderOpId = parsed.operator_id || 'unknown_sender';
displayContent = `[${senderOpId}]: ${parsed.data}`;
} else {
// If not standard format, maybe just show raw stringified version
displayContent = content; // Keep raw if parsing worked but not expected format
}
} catch (parseError) {
// Content wasn't JSON, keep raw content
displayContent = content;
}
const messageDate = new Date(msg.timestamp);
outputString += `\n[${messageDate.toLocaleString()}] (Seq: ${
msg.sequence_number
})
${displayContent}
`;
} catch (error) {
const errorMsg = `Error processing message (Seq: ${
msg.sequence_number
}): ${error instanceof Error ? error.message : String(error)}`;
this.logger.error(errorMsg);
outputString += `\n[Error processing message Seq: ${msg.sequence_number}]\n`;
}
}
// 4. Update the timestamp in demo state ONLY if fetching NEW messages
if (!isFetchingLatest && latestTimestampNanos > lastProcessedTimestamp) {
this.logger.debug(
`Updating timestamp for topic ${connectionTopicId} to ${latestTimestampNanos}`
);
this.stateManager.updateTimestamp(
connectionTopicId,
latestTimestampNanos
);
}
return outputString.trim();
} catch (error) {
this.logger.error(
`Failed to check messages for topic ${connectionTopicId}: ${error}`
);
return `Error checking messages for ${targetAgentName}: ${
error instanceof Error ? error.message : String(error)
}`;
}
}
}