gaunt-sloth-assistant
Version:
[](https://github.com/Galvanized-Pukeko/gaunt-sloth-assistant/actions/workflows/unit-tests.yml) [ {
this.statusUpdate = (level, message) => {
statusUpdate(level, message);
};
}
async init(command, configIn, checkpointSaver) {
debugLog(`GthLangChainAgent.init called with command: ${command || 'default'}`);
// Merge command-specific filesystem config if provided
this.config = this.getEffectiveConfig(configIn, command);
debugLogObject('Effective Config', {
filesystem: this.config.filesystem,
builtInTools: this.config.builtInTools,
streamOutput: this.config.streamOutput,
debugLog: this.config.debugLog,
});
if (this.config.modelDisplayName) {
this.statusUpdate('info', `Model: ${this.config.modelDisplayName}`);
}
this.mcpClient = await this.getMcpClient(this.config);
// Get default filesystem tools (filtered based on config)
debugLog('Loading default tools...');
const defaultTools = await getDefaultTools(this.config, command);
debugLog(`Default tools loaded: ${defaultTools.length}`);
// Get user config tools
const flattenedConfigTools = this.extractAndFlattenTools(this.config.tools || []);
debugLog(`User config tools loaded: ${flattenedConfigTools.length}`);
// Get MCP tools
const mcpTools = (await this.mcpClient?.getTools()) ?? [];
debugLog(`MCP tools loaded: ${mcpTools.length}`);
// Combine all tools
const tools = [...defaultTools, ...flattenedConfigTools, ...mcpTools];
if (tools.length > 0) {
const toolNames = tools
.map((tool) => tool.name)
.filter((name) => name)
.join(', ');
this.statusUpdate('info', `Loaded tools: ${toolNames}`);
debugLog(`Total tools available: ${tools.length}`);
debugLogObject('All Tools', toolNames.split(', '));
}
// Create the React agent
debugLog('Creating React agent...');
this.agent = createReactAgent({
llm: this.config.llm,
tools,
checkpointSaver,
postModelHook: (state) => {
debugLogObject('postModel state', state);
const lastMessage = state.messages[state.messages.length - 1];
if (isAIMessage(lastMessage) &&
lastMessage.tool_calls &&
lastMessage.tool_calls?.length > 0) {
this.statusUpdate('info', `\nRequested tools: ${formatToolCalls(lastMessage.tool_calls)}\n`);
}
if (configIn.hooks?.postModelHook) {
return configIn.hooks.postModelHook(state);
}
return state;
},
preModelHook: configIn.hooks?.preModelHook,
});
debugLog('React agent created successfully');
}
/**
* Invoke LLM with a message and runnable config.
* For streaming use {@link #stream} method, streaming is preferred if model API supports it.
* Please note that this when tools are involved, this method will anyway do multiple LLM
* calls within LangChain dependency.
*/
async invoke(messages, runConfig) {
if (!this.agent || !this.config) {
throw new Error('Agent not initialized. Call init() first.');
}
debugLog('=== Starting non-streaming invoke ===');
debugLogObject('LLM Input Messages', messages);
debugLogObject('Invoke RunConfig', runConfig);
try {
const progress = new ProgressIndicator('Thinking.');
try {
debugLog('Calling agent.invoke...');
const response = await this.agent.invoke({ messages }, runConfig);
const aiMessage = response.messages[response.messages.length - 1].content;
this.statusUpdate('display', aiMessage);
return aiMessage;
}
catch (e) {
debugLogError('invoke inner', e);
if (e instanceof Error && e?.name === 'ToolException') {
throw e; // Re-throw ToolException to be handled by outer catch
}
this.statusUpdate('warning', `Something went wrong ${e.message}`);
return '';
}
finally {
progress.stop();
}
}
catch (error) {
debugLogError('invoke outer', error);
if (error instanceof Error) {
if (error?.name === 'ToolException') {
this.statusUpdate('error', `Tool execution failed: ${error?.message}`);
return `Tool execution failed: ${error?.message}`;
}
}
throw error;
}
}
/**
* Induce LLM to stream AI messages with a user message and runnable config.
* When stream is not appropriate use {@link invoke}.
*/
async stream(messages, runConfig) {
if (!this.agent || !this.config) {
throw new Error('Agent not initialized. Call init() first.');
}
debugLog('=== Starting streaming invoke ===');
debugLogObject('LLM Input Messages', messages);
debugLogObject('Stream RunConfig', runConfig);
this.statusUpdate('info', '\nThinking...\n');
const stream = await this.agent.stream({ messages }, { ...runConfig, streamMode: 'messages' });
const statusUpdate = this.statusUpdate;
const interruptState = { escape: false };
waitForEscape(() => (interruptState.escape = true), this.config.canInterruptInferenceWithEsc);
return new IterableReadableStream({
async start(controller) {
try {
debugLog('Starting stream processing...');
let totalChunks = 0;
for await (const [chunk, _metadata] of stream) {
debugLogObject('Stream chunk', { chunk, _metadata });
if (isAIMessage(chunk)) {
const text = chunk.text;
totalChunks++;
statusUpdate('stream', text);
controller.enqueue(text);
}
if (interruptState.escape) {
statusUpdate('warning', '\n\nInterrupted by user, exiting\n\n');
break;
}
}
stopWaitingForEscape();
debugLog(`Stream completed. Total chunks: ${totalChunks}`);
controller.close();
}
catch (error) {
stopWaitingForEscape();
debugLogError('stream processing', error);
if (error instanceof Error) {
if (error?.name === 'ToolException') {
statusUpdate('error', `Tool execution failed: ${error?.message}`);
}
}
controller.error(error);
}
},
async cancel() {
stopWaitingForEscape();
// Clean up the underlying stream if it has a cancel method
if (stream && typeof stream.cancel === 'function') {
await stream.cancel();
}
},
});
}
// noinspection JSUnusedGlobalSymbols
getMCPClient() {
return this.mcpClient;
}
async cleanup() {
debugLog('Cleaning up GthLangChainAgent...');
if (this.mcpClient) {
debugLog('Closing MCP client...');
await this.mcpClient.close();
this.mcpClient = null;
}
this.agent = null;
this.config = null;
debugLog('GthLangChainAgent cleanup complete');
}
getEffectiveConfig(config, command) {
debugLog(`Getting effective config for command: ${command || 'default'}`);
const supportsTools = !!config.llm.bindTools;
if (!supportsTools) {
this.statusUpdate('warning', 'Model does not seem to support tools.');
debugLog('Warning: Model does not support tools');
}
return {
...config,
filesystem: command && config.commands?.[command]?.filesystem !== undefined
? config.commands[command].filesystem
: config.filesystem,
builtInTools: command && config.commands?.[command]?.builtInTools !== undefined
? config.commands[command].builtInTools
: config.builtInTools,
};
}
/**
* Extract and flatten tools from toolkits
*/
extractAndFlattenTools(tools) {
const flattenedTools = [];
for (const toolOrToolkit of tools) {
// eslint-disable-next-line
if (toolOrToolkit['getTools'] instanceof Function) {
// This is a toolkit
flattenedTools.push(...toolOrToolkit.getTools());
}
else {
// This is a regular tool
flattenedTools.push(toolOrToolkit);
}
}
return flattenedTools;
}
getDefaultMcpServers() {
return {};
}
async getMcpClient(config) {
debugLog('Setting up MCP client...');
const defaultServers = this.getDefaultMcpServers();
// Merge with user's mcpServers
const rawMcpServers = { ...defaultServers, ...(config.mcpServers || {}) };
debugLog(`MCP servers count: ${Object.keys(rawMcpServers).length}`);
const mcpServers = {};
for (const serverName of Object.keys(rawMcpServers)) {
const server = rawMcpServers[serverName];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (server.url && server && server.authProvider === 'OAuth') {
displayInfo(`Starting OAuth for for ${server.url}`);
const authProvider = await createAuthProviderAndAuthenticate(server);
mcpServers[serverName] = {
...server,
authProvider,
};
}
else {
// Add non-OAuth servers as-is
mcpServers[serverName] = server;
}
}
if (Object.keys(mcpServers).length > 0) {
debugLog('Creating MultiServerMCPClient...');
return new MultiServerMCPClient({
throwOnLoadError: true,
prefixToolNameWithServerName: true,
additionalToolNamePrefix: 'mcp',
mcpServers: mcpServers,
});
}
else {
debugLog('No MCP servers configured');
return null;
}
}
}
//# sourceMappingURL=GthLangChainAgent.js.map