@hashgraphonline/conversational-agent
Version:
Hashgraph Online conversational AI agent implementing HCS-10 communication, HCS-2 registries, and content inscription on Hedera. https://hol.org
1,236 lines (1,235 loc) • 44.5 kB
JavaScript
import { createOpenAIToolsAgent } from "langchain/agents";
import { FormAwareAgentExecutor } from "./index32.js";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { calculateTokenCostSync, TokenUsageCallbackHandler, getAllHederaCorePlugins, HederaAgentKit } from "hedera-agent-kit";
import { BaseAgent } from "./index8.js";
import { MCPClientManager } from "./index16.js";
import { convertMCPToolToLangChain } from "./index18.js";
import { SmartMemoryManager } from "./index19.js";
import { ResponseFormatter } from "./index36.js";
import { ERROR_MESSAGES } from "./index45.js";
import "./index43.js";
import { SystemMessage, AIMessage, HumanMessage } from "@langchain/core/messages";
import { ToolRegistry } from "./index46.js";
import { ExecutionPipeline } from "./index47.js";
import { FormEngine } from "./index12.js";
function hasHashLinkBlock(metadata) {
if (!metadata || typeof metadata !== "object") {
return false;
}
const meta = metadata;
if (!("hashLinkBlock" in meta) || !meta.hashLinkBlock || typeof meta.hashLinkBlock !== "object") {
return false;
}
const block = meta.hashLinkBlock;
return "blockId" in block && "hashLink" in block && "template" in block && "attributes" in block && typeof block.blockId === "string" && typeof block.hashLink === "string" && typeof block.template === "string" && typeof block.attributes === "object";
}
class LangChainAgent extends BaseAgent {
constructor() {
super(...arguments);
this.systemMessage = "";
this.mcpConnectionStatus = /* @__PURE__ */ new Map();
}
addToolRawToMemory(name, payload) {
try {
const content = `[tool-raw:${name}] ${payload}`;
this.smartMemory.addMessage(new SystemMessage(content));
} catch {
}
}
persistToolRaw(toolName, output) {
try {
let payload = "";
if (typeof output === "string") {
payload = this.isJSON(output) ? output : JSON.stringify({ output });
} else if (output !== void 0) {
try {
payload = JSON.stringify(output);
} catch {
payload = String(output);
}
} else {
payload = JSON.stringify({ observation: null });
}
this.addToolRawToMemory(toolName, payload);
} catch {
}
}
persistIntermediateSteps(steps) {
if (!steps || !Array.isArray(steps)) {
return;
}
try {
for (const step of steps) {
const name = step?.action?.tool || "unknown";
const obs = step?.observation;
this.persistToolRaw(name, obs);
}
} catch {
}
}
/**
* Get inscription tool by capability instead of hardcoded name
*/
getInscriptionTool() {
const criticalTools = this.toolRegistry.getToolsByCapability(
"priority",
"critical"
);
for (const entry of criticalTools) {
const tool = entry.tool;
const name = tool.name.toLowerCase();
const desc = tool.description?.toLowerCase() || "";
if (name.includes("inscribe") || name.includes("hashinal") || desc.includes("inscribe") || desc.includes("hashinal")) {
return tool;
}
}
const allTools = this.toolRegistry.getAllRegistryEntries();
for (const entry of allTools) {
const tool = entry.tool;
const name = tool.name.toLowerCase();
const desc = tool.description?.toLowerCase() || "";
if (name.includes("inscribe") || name.includes("hashinal") || desc.includes("inscribe") || desc.includes("hashinal")) {
return tool;
}
}
return null;
}
/**
* Execute a tool directly with parameters, optionally using ExecutionPipeline
*/
async executeToolDirect(toolName, parameters, useExecutionPipeline = false) {
if (useExecutionPipeline && this.executionPipeline && this.smartMemory) {
const sessionContext = {
sessionId: `session-${Date.now()}`,
timestamp: Date.now()
};
const result = await this.executionPipeline.execute(
toolName,
parameters,
sessionContext
);
if (!result.success) {
throw new Error(result.error || "Pipeline execution failed");
}
return result.output;
}
const entry = this.toolRegistry.getTool(toolName);
if (!entry) {
throw new Error(`Tool not found: ${toolName}`);
}
let processedParameters = { ...parameters };
if (this.pendingParameterPreprocessingCallback) {
this.logger.info(
"Applying parameter preprocessing in executeToolDirect",
{
toolName,
hasCallback: true,
parameterKeys: Object.keys(parameters)
}
);
try {
processedParameters = await this.pendingParameterPreprocessingCallback(
toolName,
parameters
);
if (JSON.stringify(processedParameters) !== JSON.stringify(parameters)) {
this.logger.info("Parameters preprocessed successfully", {
toolName,
originalKeys: Object.keys(parameters),
processedKeys: Object.keys(processedParameters),
changes: Object.keys(processedParameters).filter(
(key) => processedParameters[key] !== parameters[key]
)
});
}
} catch (error) {
this.logger.warn(
"Parameter preprocessing failed, using original parameters",
{
toolName,
error: error instanceof Error ? error.message : "Unknown error"
}
);
processedParameters = parameters;
}
}
const mergedArgs = { ...processedParameters, renderForm: false };
if (entry.wrapper) {
const maybeWrapper = entry.tool;
if (maybeWrapper.originalTool?.call) {
return await maybeWrapper.originalTool.call(mergedArgs);
}
}
return await entry.tool.call(mergedArgs);
}
/**
* Create a standard ChatResponse from tool output
*/
createToolResponse(toolOutput) {
return {
output: toolOutput,
message: toolOutput,
notes: []
};
}
/**
* Handle TOOL_EXECUTION format messages
*/
async handleToolExecution(message, context) {
let isToolExecution = false;
let toolExecutionData = null;
try {
if (message.includes("TOOL_EXECUTION")) {
const parsed = JSON.parse(message);
if (parsed.type === "TOOL_EXECUTION") {
isToolExecution = true;
toolExecutionData = parsed;
}
}
} catch {
}
if (!isToolExecution || !toolExecutionData?.formId) {
return null;
}
try {
const params = toolExecutionData.parameters || {};
const toolName = toolExecutionData.toolName;
if (toolName) {
const toolOutput = await this.executeToolDirect(toolName, params);
try {
const payload = this.isJSON(toolOutput) ? toolOutput : JSON.stringify({ output: toolOutput });
this.addToolRawToMemory(toolName, payload);
} catch {
}
return this.createToolResponse(toolOutput);
}
} catch {
}
const formSubmission = {
formId: toolExecutionData.formId,
toolName: toolExecutionData.toolName || "",
parameters: toolExecutionData.parameters || {},
timestamp: Date.now()
};
if (this.executor && "processFormSubmission" in this.executor && typeof this.executor.processFormSubmission === "function") {
return this.processFormSubmission(formSubmission, context);
}
return null;
}
/**
* Handle direct tool execution commands
*/
async handleDirectToolExecution(message) {
if (typeof message !== "string" || !message.includes("Please execute the following tool:")) {
return null;
}
try {
const toolLineMatch = message.match(/Tool:\s*(.+)/);
const argsLineIndex = message.indexOf("Arguments:");
if (toolLineMatch && argsLineIndex !== -1) {
const toolName = toolLineMatch[1].trim();
const argsText = message.slice(argsLineIndex + "Arguments:".length).trim();
let args = {};
try {
args = JSON.parse(argsText);
} catch {
}
const toolOutput = await this.executeToolDirect(toolName, args);
try {
const payload = this.isJSON(toolOutput) ? toolOutput : JSON.stringify({ output: toolOutput });
this.addToolRawToMemory(toolName, payload);
} catch {
}
return this.createToolResponse(toolOutput);
}
} catch {
}
return null;
}
/**
* Handle JSON format tool calls and form submissions
*/
async handleJsonToolCalls(message, context) {
if (typeof message !== "string") {
return null;
}
try {
const trimmed = message.trim();
if (!(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
return null;
}
const obj = JSON.parse(trimmed);
const formId = obj["formId"];
const toolName = obj["toolName"] || "";
const parameters = obj["parameters"] || {};
if (formId && this.executor && "processFormSubmission" in this.executor && typeof this.executor.processFormSubmission === "function") {
return this.processFormSubmission(
{ formId, toolName, parameters, timestamp: Date.now() },
context
);
}
if (toolName) {
const toolOutput = await this.executeToolDirect(toolName, parameters);
try {
const payload = this.isJSON(toolOutput) ? toolOutput : JSON.stringify({ output: toolOutput });
this.addToolRawToMemory(toolName, payload);
} catch {
}
return this.createToolResponse(toolOutput);
}
} catch {
}
return null;
}
/**
* Handle content-ref messages for inscription tools
*/
async handleContentRefMessages(message) {
if (typeof message !== "string" || !message.includes("content-ref:")) {
return null;
}
try {
const tool = this.getInscriptionTool();
if (!tool) {
return null;
}
const idMatch = message.match(/content-ref:([A-Za-z0-9_\-]+)/i) || message.match(/content-ref:([^\s)]+)/i);
const contentRef = idMatch && idMatch[1] ? `content-ref:${idMatch[1]}` : message.match(/content-ref:[^\s)]+/i)?.[0] || void 0;
const args = contentRef ? { contentRef, renderForm: true, withHashLinkBlocks: true } : { renderForm: true, withHashLinkBlocks: true };
const toolOutput = await tool.call(args);
let parsed;
try {
parsed = typeof toolOutput === "string" ? JSON.parse(toolOutput) : toolOutput;
} catch {
}
if (parsed && parsed["requiresForm"] && parsed["formMessage"]) {
const pending = /* @__PURE__ */ new Map();
const originalInput = {
input: message,
chat_history: this.smartMemory.getMessages()
};
const formMessage = parsed["formMessage"];
pending.set(formMessage.id, {
toolName: tool.name,
originalInput,
originalToolInput: args,
schema: null
});
const maybeRestore = this.executor;
if (typeof maybeRestore.restorePendingForms === "function") {
maybeRestore.restorePendingForms(pending);
}
const outputMsg = parsed["message"] || "Please complete the form to continue.";
return {
output: outputMsg,
message: outputMsg,
notes: [],
requiresForm: true,
formMessage
};
}
} catch {
}
return null;
}
/**
* Process executor result and format response
*/
async processExecutorResult(result) {
let outputStr = "";
if (typeof result.output === "string") {
outputStr = result.output;
} else if (result.output) {
try {
outputStr = JSON.stringify(result.output);
} catch {
outputStr = String(result.output);
}
}
let response = {
output: outputStr,
message: outputStr,
notes: [],
intermediateSteps: result.intermediateSteps
};
if (result.requiresForm && result.formMessage) {
response.formMessage = result.formMessage;
response.requiresForm = true;
}
if (result.intermediateSteps && Array.isArray(result.intermediateSteps)) {
const toolCalls = result.intermediateSteps.map(
(step, index) => ({
id: `call_${index}`,
name: step.action?.tool || "unknown",
args: step.action?.toolInput || {},
output: typeof step.observation === "string" ? step.observation : JSON.stringify(step.observation)
})
);
if (toolCalls.length > 0) {
response.tool_calls = toolCalls;
}
this.persistIntermediateSteps(
result.intermediateSteps
);
}
const steps = result?.intermediateSteps || [];
const lastJsonObservation = [...steps].reverse().find(
(s) => typeof s?.observation === "string" && this.isJSON(s.observation)
)?.observation;
if (lastJsonObservation) {
try {
const parsed = JSON.parse(lastJsonObservation);
if (ResponseFormatter.isInscriptionResponse(parsed)) {
const formattedMessage = ResponseFormatter.formatInscriptionResponse(parsed);
response.output = formattedMessage;
response.message = formattedMessage;
if (parsed.inscription) {
response.inscription = parsed.inscription;
}
if (parsed.metadata) {
response.metadata = { ...response.metadata, ...parsed.metadata };
}
} else {
if (typeof parsed.message === "string" && parsed.message.trim().length > 0) {
response.message = parsed.message;
response.output = parsed.message;
}
if (parsed.success === true) {
delete response.error;
}
if (typeof parsed.transactionBytes === "string") {
response.metadata = {
...response.metadata,
transactionBytes: parsed.transactionBytes
};
}
if (typeof parsed.scheduleId === "string") {
response.scheduleId = parsed.scheduleId;
}
}
const blockMetadata = this.processHashLinkBlocks(parsed);
if (blockMetadata.hashLinkBlock) {
response.metadata = { ...response.metadata, ...blockMetadata };
}
} catch (error) {
this.logger.error("Error parsing intermediate steps:", error);
}
}
if (!response.output || response.output.trim() === "") {
response.output = "Agent action complete.";
}
if (response.output) {
this.smartMemory.addMessage(new AIMessage(response.output));
}
if (this.tokenTracker) {
const tokenUsage = this.tokenTracker.getLatestTokenUsage();
if (tokenUsage) {
response.tokenUsage = tokenUsage;
response.cost = calculateTokenCostSync(tokenUsage);
}
}
const finalMemoryStats = this.smartMemory.getMemoryStats();
response.metadata = {
...response.metadata,
memoryStats: {
activeMessages: finalMemoryStats.totalActiveMessages,
tokenUsage: finalMemoryStats.currentTokenCount,
maxTokens: finalMemoryStats.maxTokens,
usagePercentage: finalMemoryStats.usagePercentage
}
};
this.logger.info("LangChainAgent.chat returning response:", response);
return response;
}
/**
* Normalize context messages into LangChain message instances and load into memory
*/
/**
* Loads context messages into memory, merging with existing messages
*/
loadContextMessages(context) {
if (!this.smartMemory || !context?.messages || context.messages.length === 0) {
return;
}
const existingMessages = this.smartMemory.getMessages();
const existingContent = new Set(
existingMessages.map((m) => `${m.constructor.name}:${m.content}`)
);
for (const msg of context.messages) {
let messageClass;
let content;
if (msg instanceof HumanMessage || msg instanceof AIMessage || msg instanceof SystemMessage) {
messageClass = msg.constructor;
content = msg.content;
} else if (msg && typeof msg === "object" && "content" in msg && "type" in msg) {
content = String(msg.content);
const type = String(msg.type);
if (type === "human") messageClass = HumanMessage;
else if (type === "ai") messageClass = AIMessage;
else if (type === "system") messageClass = SystemMessage;
else continue;
} else {
continue;
}
const key = `${messageClass.name}:${content}`;
if (!existingContent.has(key)) {
this.smartMemory.addMessage(new messageClass(content));
existingContent.add(key);
}
}
}
async boot() {
this.logger.info("🚨🚨🚨 LANGCHAIN AGENT BOOT METHOD CALLED 🚨🚨🚨");
if (this.initialized) {
this.logger.warn("Agent already initialized");
return;
}
try {
this.agentKit = await this.createAgentKit();
await this.agentKit.initialize();
const modelName = this.config.ai?.modelName || process.env.OPENAI_MODEL_NAME || "gpt-4o-mini";
try {
if (typeof TokenUsageCallbackHandler === "function") {
this.tokenTracker = new TokenUsageCallbackHandler(modelName);
} else {
this.logger.warn("TokenUsageCallbackHandler unavailable or not a constructor; skipping token tracking");
}
} catch {
this.logger.warn("TokenUsageCallbackHandler threw; skipping token tracking");
}
this.toolRegistry = new ToolRegistry(this.logger);
const allTools = this.agentKit.getAggregatedLangChainTools();
this.logger.info("=== TOOL REGISTRATION START ===");
this.logger.info(
"All tools from agentKit:",
allTools.map((t) => t.name)
);
const filteredTools = this.filterTools(allTools);
this.logger.info(
"Filtered tools for registration:",
filteredTools.map((t) => t.name)
);
for (const tool of filteredTools) {
this.logger.info(`🔧 Registering tool: ${tool.name}`);
const options = {};
const name = tool.name.toLowerCase();
const desc = tool.description?.toLowerCase() || "";
if (tool.name === "hedera-hts-mint-nft") {
const originalCall = tool.call.bind(tool);
tool.call = async (args) => {
if (args.metaOptions && typeof args.metaOptions === "object") {
const metaOptions = args.metaOptions;
if (metaOptions.transactionMemo) {
this.logger.warn(
"🚨 WORKAROUND: Stripping transactionMemo from hedera-hts-mint-nft to avoid bug",
{ originalMemo: metaOptions.transactionMemo }
);
delete metaOptions.transactionMemo;
}
}
return originalCall(args);
};
}
if (name.includes("inscribe") || name.includes("hashinal") || desc.includes("inscribe") || desc.includes("hashinal")) {
options.forceWrapper = true;
options.metadata = {
category: "core",
version: "1.0.0",
dependencies: []
};
this.logger.info(`🎯 CRITICAL TOOL DEBUG - ${tool.name} schema:`, {
hasSchema: !!tool.schema,
schemaType: tool.schema?.constructor?.name,
hasRenderConfig: !!tool.schema?._renderConfig,
renderConfig: tool.schema?._renderConfig
});
}
this.toolRegistry.registerTool(tool, options);
}
this.tools = this.toolRegistry.getAllTools();
this.logger.info(`🚀 TOOLS REGISTERED: ${this.tools.length} tools`);
const stats = this.toolRegistry.getStatistics();
this.logger.info("📊 Tool Registry Statistics:", {
total: stats.totalTools,
wrapped: stats.wrappedTools,
unwrapped: stats.unwrappedTools,
categories: stats.categoryCounts,
priorities: stats.priorityCounts
});
const inscriptionTool = this.getInscriptionTool();
if (inscriptionTool) {
const entry = this.toolRegistry.getTool(inscriptionTool.name);
if (entry) {
this.logger.info(
`✅ Inscription tool registered: ${inscriptionTool.name}`
);
}
}
const toolNames = this.toolRegistry.getToolNames();
const uniqueNames = new Set(toolNames);
if (toolNames.length !== uniqueNames.size) {
this.logger.error("DUPLICATE TOOL NAMES DETECTED in registry!");
const duplicates = toolNames.filter(
(name, index) => toolNames.indexOf(name) !== index
);
throw new Error(
`Duplicate tool names detected: ${duplicates.join(", ")}`
);
}
if (this.config.mcp?.servers && this.config.mcp.servers.length > 0) {
if (this.config.mcp.autoConnect !== false) {
await this.initializeMCP();
} else {
this.logger.info(
"MCP servers configured but autoConnect=false, skipping synchronous connection"
);
this.mcpManager = new MCPClientManager(this.logger);
}
}
this.smartMemory = new SmartMemoryManager({
modelName,
maxTokens: 9e4,
reserveTokens: 1e4,
storageLimit: 1e3
});
this.logger.info("SmartMemoryManager initialized:", {
modelName,
toolsCount: this.tools.length,
maxTokens: 9e4,
reserveTokens: 1e4
});
this.formEngine = new FormEngine(this.logger);
this.executionPipeline = new ExecutionPipeline(
this.toolRegistry,
this.formEngine,
this.smartMemory,
this.logger
);
this.systemMessage = this.buildSystemPrompt();
this.smartMemory.setSystemPrompt(this.systemMessage);
await this.createExecutor();
this.initialized = true;
this.logger.info("LangChain Hedera agent initialized with ToolRegistry");
} catch (error) {
this.logger.error("Failed to initialize agent:", error);
throw error;
}
}
async chat(message, context) {
if (!this.initialized || !this.executor || !this.smartMemory) {
throw new Error("Agent not initialized. Call boot() first.");
}
try {
const toolExecutionResult = await this.handleToolExecution(
message,
context
);
if (toolExecutionResult) {
return toolExecutionResult;
}
const directToolResult = await this.handleDirectToolExecution(message);
if (directToolResult) {
return directToolResult;
}
const jsonToolResult = await this.handleJsonToolCalls(message, context);
if (jsonToolResult) {
return jsonToolResult;
}
const contentRefResult = await this.handleContentRefMessages(message);
if (contentRefResult) {
return contentRefResult;
}
this.logger.info("LangChainAgent.chat called with:", {
message,
contextLength: context?.messages?.length || 0
});
this.loadContextMessages(context);
this.smartMemory.addMessage(new HumanMessage(message));
const memoryStats = this.smartMemory.getMemoryStats();
this.logger.info("Memory stats before execution:", {
totalMessages: memoryStats.totalActiveMessages,
currentTokens: memoryStats.currentTokenCount,
maxTokens: memoryStats.maxTokens,
usagePercentage: memoryStats.usagePercentage,
toolsCount: this.tools.length
});
const currentMessages = this.smartMemory.getMessages();
this.logger.info("Current messages in memory:", {
count: currentMessages.length
});
try {
const instr = currentMessages.map((m) => String(m.content || "")).filter(
(c) => typeof c === "string" && (c.includes("[instruction:") || c.includes("[tool-next-steps:"))
);
if (instr.length > 0) {
this.logger.info("Instruction/next-steps messages in memory:", {
messages: instr
});
}
} catch {
}
const result = await this.executor.invoke({
input: message,
chat_history: currentMessages
});
this.logger.info("LangChainAgent executor result:", result);
return this.processExecutorResult(result);
} catch (error) {
this.logger.error("LangChainAgent.chat error:", error);
return this.handleError(error);
}
}
async shutdown() {
if (this.mcpManager) {
await this.mcpManager.disconnectAll();
}
if (this.smartMemory) {
this.smartMemory.dispose();
this.smartMemory = void 0;
}
if (this.toolRegistry) {
this.toolRegistry.clear();
}
this.executor = void 0;
this.agentKit = void 0;
this.tools = [];
this.initialized = false;
this.logger.info("Agent cleaned up");
}
switchMode(mode) {
if (this.config.execution) {
this.config.execution.operationalMode = mode;
} else {
this.config.execution = { operationalMode: mode };
}
if (this.agentKit) {
this.agentKit.operationalMode = mode;
}
this.systemMessage = this.buildSystemPrompt();
this.logger.info(`Operational mode switched to: ${mode}`);
}
getUsageStats() {
if (!this.tokenTracker) {
return {
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
cost: { totalCost: 0 }
};
}
const usage = this.tokenTracker.getTotalTokenUsage();
const cost = calculateTokenCostSync(usage);
return { ...usage, cost };
}
getUsageLog() {
if (!this.tokenTracker) {
return [];
}
return this.tokenTracker.getTokenUsageHistory().map((usage) => ({
...usage,
cost: calculateTokenCostSync(usage)
}));
}
clearUsageStats() {
if (this.tokenTracker) {
this.tokenTracker.reset();
this.logger.info("Usage statistics cleared");
}
}
getMCPConnectionStatus() {
return new Map(this.mcpConnectionStatus);
}
/**
* Processes form submission and continues with tool execution
*/
async processFormSubmission(submission, context) {
if (!this.initialized || !this.executor || !this.smartMemory) {
throw new Error("Agent not initialized. Call boot() first.");
}
try {
if (!submission.parameters || typeof submission.parameters !== "object") {
this.logger.error("Invalid form submission parameters:", {
parameters: submission.parameters,
type: typeof submission.parameters
});
const errorInfo = JSON.stringify(submission, null, 2);
return this.handleError(
new Error(`Invalid form submission parameters: ${errorInfo}`)
);
}
this.loadContextMessages(context);
const safeSubmission = {
...submission,
parameters: submission.parameters || {}
};
const result = await this.executor.processFormSubmission(safeSubmission);
const preservedMetadata = result?.metadata ? { ...result.metadata } : {};
try {
const maybeRaw = result.rawToolOutput;
const toolName = result.toolName || "unknown";
if (typeof maybeRaw === "string" && maybeRaw.trim().length > 0) {
const payload = this.isJSON(maybeRaw) ? maybeRaw : JSON.stringify({ output: maybeRaw });
this.addToolRawToMemory(toolName, payload);
}
} catch {
}
let outputMessage = "Form processed successfully.";
if (typeof result.output === "string") {
outputMessage = result.output;
} else if (result.output) {
try {
outputMessage = JSON.stringify(result.output);
} catch {
outputMessage = String(result.output);
}
}
let response = {
output: outputMessage,
message: outputMessage,
notes: [],
intermediateSteps: result.intermediateSteps
};
if (result.metadata) {
response.metadata = {
...response.metadata,
...result.metadata
};
this.logger.info("🔍 DEBUG: Metadata after merge from result:", {
hasMetadata: !!response.metadata,
metadataKeys: response.metadata ? Object.keys(response.metadata) : [],
hasHashLinkBlock: hasHashLinkBlock(response.metadata),
hashLinkBlockContent: hasHashLinkBlock(response.metadata) ? response.metadata.hashLinkBlock : void 0
});
}
if (result.requiresForm && result.formMessage) {
response.formMessage = result.formMessage;
response.requiresForm = true;
}
if (result.intermediateSteps && Array.isArray(result.intermediateSteps)) {
const toolCalls = result.intermediateSteps.map(
(step, index) => {
const name = step?.action?.tool || "unknown";
const args = step?.action?.toolInput || {};
const obs = step?.observation;
let output = "";
if (typeof obs === "string") {
output = obs;
} else if (obs && typeof obs === "object") {
try {
output = JSON.stringify(obs);
} catch {
output = String(obs);
}
} else if (obs !== void 0) {
output = String(obs);
}
return { id: `call_${index}`, name, args, output };
}
);
if (toolCalls.length > 0) {
response.tool_calls = toolCalls;
}
this.persistIntermediateSteps(
result.intermediateSteps
);
}
const parsedSteps = result?.intermediateSteps?.[0]?.observation;
if (parsedSteps && typeof parsedSteps === "string" && this.isJSON(parsedSteps)) {
try {
const parsed = JSON.parse(parsedSteps);
response = { ...response, ...parsed };
const blockMetadata = this.processHashLinkBlocks(parsed);
if (blockMetadata.hashLinkBlock) {
response.metadata = {
...response.metadata,
...blockMetadata
};
}
} catch (error) {
this.logger.error("Error parsing intermediate steps:", error);
}
}
if (response.output) {
this.smartMemory.addMessage(new AIMessage(response.output));
}
if (this.tokenTracker) {
const tokenUsage = this.tokenTracker.getLatestTokenUsage();
if (tokenUsage) {
response.tokenUsage = tokenUsage;
response.cost = calculateTokenCostSync(tokenUsage);
}
}
const finalMemoryStats = this.smartMemory.getMemoryStats();
this.logger.info("🔍 DEBUG: Metadata before memoryStats merge:", {
hasMetadata: !!response.metadata,
metadataKeys: response.metadata ? Object.keys(response.metadata) : [],
hasHashLinkBlock: hasHashLinkBlock(response.metadata)
});
response.metadata = {
...preservedMetadata,
...response.metadata,
memoryStats: {
activeMessages: finalMemoryStats.totalActiveMessages,
tokenUsage: finalMemoryStats.currentTokenCount,
maxTokens: finalMemoryStats.maxTokens,
usagePercentage: finalMemoryStats.usagePercentage
}
};
this.logger.info("🔍 DEBUG: Final response metadata before return:", {
hasMetadata: !!response.metadata,
metadataKeys: response.metadata ? Object.keys(response.metadata) : [],
hasHashLinkBlock: hasHashLinkBlock(response.metadata),
fullMetadata: response.metadata
});
if (hasHashLinkBlock(preservedMetadata) && !hasHashLinkBlock(response.metadata)) {
this.logger.error(
"❌ CRITICAL: HashLink metadata was lost during processing!"
);
this.logger.error(
"Original metadata had hashLinkBlock:",
preservedMetadata.hashLinkBlock
);
this.logger.error("Final metadata missing hashLinkBlock");
}
return response;
} catch (error) {
this.logger.error("Form submission processing error:", error);
return this.handleError(error);
}
}
/**
* Check if the agent has pending forms that need to be completed
*/
hasPendingForms() {
return this.executor ? this.executor.hasPendingForms() : false;
}
/**
* Get information about pending forms
*/
getPendingFormsInfo() {
return this.executor ? this.executor.getPendingFormsInfo() : [];
}
async createAgentKit() {
const corePlugins = getAllHederaCorePlugins();
const extensionPlugins = this.config.extensions?.plugins || [];
const plugins = [...corePlugins, ...extensionPlugins];
const operationalMode = this.config.execution?.operationalMode || "returnBytes";
const modelName = this.config.ai?.modelName || "gpt-4o";
return new HederaAgentKit(
this.config.signer,
{ plugins },
operationalMode,
this.config.execution?.userAccountId,
this.config.execution?.scheduleUserTransactionsInBytesMode ?? false,
void 0,
modelName,
this.config.extensions?.mirrorConfig,
this.config.debug?.silent ?? false
);
}
async createExecutor() {
const existingPendingForms = this.executor?.getPendingForms() || /* @__PURE__ */ new Map();
let llm;
if (this.config.ai?.provider && this.config.ai.provider.getModel) {
llm = this.config.ai.provider.getModel();
} else if (this.config.ai?.llm) {
llm = this.config.ai.llm;
} else {
const apiKey = this.config.ai?.apiKey || process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OpenAI API key required");
}
const modelName = this.config.ai?.modelName || "gpt-4o-mini";
const isGPT5Model = modelName.toLowerCase().includes("gpt-5") || modelName.toLowerCase().includes("gpt5");
llm = new ChatOpenAI({
apiKey,
modelName,
callbacks: this.tokenTracker ? [this.tokenTracker] : [],
...isGPT5Model ? { temperature: 1 } : {}
});
}
const prompt = ChatPromptTemplate.fromMessages([
["system", this.systemMessage],
new MessagesPlaceholder("chat_history"),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad")
]);
const langchainTools = this.tools;
const inscriptionTool = this.getInscriptionTool();
if (inscriptionTool) {
const entry = this.toolRegistry.getTool(inscriptionTool.name);
if (entry) {
this.logger.info(
`✅ Inscription tool registered: ${inscriptionTool.name}`
);
}
}
const stats = this.toolRegistry.getStatistics();
this.logger.info("🛡️ TOOL SECURITY REPORT:", {
totalTools: stats.totalTools,
wrappedTools: stats.wrappedTools,
unwrappedTools: stats.unwrappedTools,
categories: stats.categoryCounts,
priorities: stats.priorityCounts
});
this.logger.info(
`📊 Tool Security Summary: ${stats.wrappedTools} wrapped, ${stats.unwrappedTools} unwrapped`
);
const agent = await createOpenAIToolsAgent({
llm,
tools: langchainTools,
prompt
});
this.executor = new FormAwareAgentExecutor({
agent,
tools: langchainTools,
verbose: this.config.debug?.verbose ?? false,
returnIntermediateSteps: true
});
if (this.pendingParameterPreprocessingCallback) {
this.executor.setParameterPreprocessingCallback(
this.pendingParameterPreprocessingCallback
);
this.logger.info(
"Parameter preprocessing callback re-applied to new executor",
{ hasCallback: true }
);
}
if (existingPendingForms.size > 0) {
this.logger.info(
`Restoring ${existingPendingForms.size} pending forms to new executor`
);
this.executor.restorePendingForms(existingPendingForms);
}
this.logger.info("FormAwareAgentExecutor initialization complete");
}
/**
* Set parameter preprocessing callback for tool parameter format conversion
*/
setParameterPreprocessingCallback(callback) {
this.pendingParameterPreprocessingCallback = callback;
if (this.executor) {
this.executor.setParameterPreprocessingCallback(callback);
this.logger.info("Parameter preprocessing callback configured", {
hasCallback: !!callback
});
} else {
this.logger.warn(
"Cannot set parameter preprocessing callback: executor not initialized"
);
}
}
handleError(error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
this.logger.error("Chat error:", error);
let tokenUsage;
let cost;
if (this.tokenTracker) {
tokenUsage = this.tokenTracker.getLatestTokenUsage();
if (tokenUsage) {
cost = calculateTokenCostSync(tokenUsage);
}
}
let userFriendlyMessage = errorMessage;
let userFriendlyOutput = errorMessage;
if (errorMessage.includes("429")) {
if (errorMessage.includes("quota")) {
userFriendlyMessage = "API quota exceeded. Please check your OpenAI billing and usage limits.";
userFriendlyOutput = "I'm currently unable to respond because the API quota has been exceeded. Please check your OpenAI account billing and usage limits, then try again.";
} else {
userFriendlyMessage = ERROR_MESSAGES.TOO_MANY_REQUESTS;
userFriendlyOutput = ERROR_MESSAGES.RATE_LIMITED;
}
} else if (errorMessage.includes("401") || errorMessage.includes("unauthorized")) {
userFriendlyMessage = "API authentication failed. Please check your API key configuration.";
userFriendlyOutput = "There's an issue with the API authentication. Please check your OpenAI API key configuration in settings.";
} else if (errorMessage.includes("timeout")) {
userFriendlyMessage = "Request timed out. Please try again.";
userFriendlyOutput = "The request took too long to process. Please try again.";
} else if (errorMessage.includes("network") || errorMessage.includes("fetch")) {
userFriendlyMessage = "Network error. Please check your internet connection and try again.";
userFriendlyOutput = "There was a network error. Please check your internet connection and try again.";
} else if (errorMessage.includes("400")) {
userFriendlyMessage = errorMessage;
userFriendlyOutput = errorMessage;
}
const errorResponse = {
output: userFriendlyOutput,
message: userFriendlyMessage,
error: errorMessage,
notes: []
};
if (tokenUsage) {
errorResponse.tokenUsage = tokenUsage;
}
if (cost) {
errorResponse.cost = cost;
}
return errorResponse;
}
async initializeMCP() {
this.mcpManager = new MCPClientManager(this.logger);
for (const serverConfig of this.config.mcp.servers) {
if (serverConfig.autoConnect === false) {
this.logger.info(
`Skipping MCP server ${serverConfig.name} (autoConnect=false)`
);
continue;
}
const status = await this.mcpManager.connectServer(serverConfig);
if (status.connected) {
this.logger.info(
`Connected to MCP server ${status.serverName} with ${status.tools.length} tools`
);
for (const mcpTool of status.tools) {
const langchainTool = convertMCPToolToLangChain(
mcpTool,
this.mcpManager,
serverConfig
);
this.toolRegistry.registerTool(langchainTool, {
metadata: {
category: "mcp",
version: "1.0.0",
dependencies: [serverConfig.name]
}
});
}
this.tools = this.toolRegistry.getAllTools();
} else {
this.logger.error(
`Failed to connect to MCP server ${status.serverName}: ${status.error}`
);
}
}
}
/**
* Connect to MCP servers asynchronously after agent boot with background timeout pattern
*/
async connectMCPServers() {
if (!this.config.mcp?.servers || this.config.mcp.servers.length === 0) {
return;
}
if (!this.mcpManager) {
this.mcpManager = new MCPClientManager(this.logger);
}
this.logger.info(
`Starting background MCP server connections for ${this.config.mcp.servers.length} servers...`
);
this.config.mcp.servers.forEach((serverConfig) => {
this.connectServerInBackground(serverConfig);
});
this.logger.info("MCP server connections initiated in background");
}
/**
* Connect to a single MCP server in background with timeout
*/
connectServerInBackground(serverConfig) {
const serverName = serverConfig.name;
setTimeout(async () => {
try {
this.logger.info(`Background connecting to MCP server: ${serverName}`);
const status = await this.mcpManager.connectServer(serverConfig);
this.mcpConnectionStatus.set(serverName, status);
if (status.connected) {
this.logger.info(
`Successfully connected to MCP server ${status.serverName} with ${status.tools.length} tools`
);
for (const mcpTool of status.tools) {
const langchainTool = convertMCPToolToLangChain(
mcpTool,
this.mcpManager,
serverConfig
);
this.toolRegistry.registerTool(langchainTool, {
metadata: {
category: "mcp",
version: "1.0.0",
dependencies: [serverConfig.name]
}
});
}
this.tools = this.toolRegistry.getAllTools();
if (this.initialized && this.executor) {
this.logger.info(
`Recreating executor with ${this.tools.length} total tools`
);
await this.createExecutor();
}
} else {
this.logger.error(
`Failed to connect to MCP server ${status.serverName}: ${status.error}`
);
}
} catch (error) {
this.logger.error(
`Background connection failed for MCP server ${serverName}:`,
error
);
this.mcpConnectionStatus.set(serverName, {
connected: false,
serverName,
tools: [],
error: error instanceof Error ? error.message : "Connection failed"
});
}
}, 1e3);
}
/**
* Detects and processes HashLink blocks from tool responses
* @param parsedResponse - The parsed JSON response from a tool
* @returns Metadata object containing hashLinkBlock if detected
*/
processHashLinkBlocks(parsedResponse) {
try {
const responseRecord = parsedResponse;
if (parsedResponse && typeof parsedResponse === "object" && responseRecord.hashLinkBlock && typeof responseRecord.hashLinkBlock === "object") {
const block = responseRecord.hashLinkBlock;
if (block.blockId && block.hashLink && block.template && block.attributes && typeof block.blockId === "string" && typeof block.hashLink === "string" && typeof block.template === "string" && typeof block.attributes === "object") {
this.logger.info("HashLink block detected:", {
blockId: block.blockId,
hashLink: block.hashLink,
template: block.template,
attributeKeys: Object.keys(block.attributes)
});
return {
hashLinkBlock: {
blockId: block.blockId,
hashLink: block.hashLink,
template: block.template,
attributes: block.attributes
}
};
} else {
this.logger.warn("Invalid HashLink block structure detected:", block);
}
}
} catch (error) {
this.logger.error("Error processing HashLink blocks:", error);
}
return {};
}
/**
* Check if a string is valid JSON
*/
isJSON(str) {
if (typeof str !== "string") return false;
const trimmed = str.trim();
if (!trimmed) return false;
if (!(trimmed.startsWith("{") && trimmed.endsWith("}")) && !(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
return false;
}
try {
JSON.parse(trimmed);
return true;
} catch {
return false;
}
}
}
export {
LangChainAgent
};
//# sourceMappingURL=index34.js.map