andy-stack_vaultkeeper-ai/Services/AIServices/BaseAgent.ts
Andrew Beal 10ddb1da28 feat: add artifact tracking system for agent file operations
Implement comprehensive artifact tracking to record all file modifications made by the AI agent during conversations. Add artifact persistence, garbage collection, and UI updates with new "Discuss" button styling.
2026-07-10 21:23:44 +01:00

238 lines
No EOL
10 KiB
TypeScript

import type { AIToolCall } from "AIClasses/AIToolCall";
import type { AIToolResponse } from "AIClasses/ToolDefinitions/AIToolResponse";
import type { IAIClass } from "AIClasses/IAIClass";
import type { IPrompt } from "AIPrompts/IPrompt";
import type { Conversation } from "Conversations/Conversation";
import { ConversationContent } from "Conversations/ConversationContent";
import { AgentType } from "Enums/AgentType";
import { Copy } from "Enums/Copy";
import { Role } from "Enums/Role";
import { sanitizeToolCallContent } from "Helpers/ResponseHelper";
import type { IChatServiceCallbacks } from "Services/ChatService";
import { Resolve, TryResolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import type { AIToolService } from "./AIToolService";
import type { DebugService } from "Services/DebugService";
import { DebugColor } from "Enums/DebugColor";
import { Exception } from "Helpers/Exception";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import type { SettingsService } from "Services/SettingsService";
export abstract class BaseAgent {
protected ai: IAIClass | undefined;
protected readonly aiPrompt: IPrompt;
protected readonly aiToolService: AIToolService;
protected readonly debugService: DebugService | undefined;
private readonly settingsService: SettingsService;
private onSaveConversation?: (conversation: Conversation) => Promise<void>;
public constructor() {
this.aiPrompt = Resolve<IPrompt>(Services.IPrompt);
this.aiToolService = Resolve<AIToolService>(Services.AIToolService);
this.settingsService = Resolve<SettingsService>(Services.SettingsService);
this.debugService = TryResolve<DebugService>(Services.DebugService);
this.setDebugColor();
}
public resolveAIProvider() {
this.ai = Resolve<IAIClass>(Services.IAIClass);
}
public setSaveCallback(callback: (conversation: Conversation) => Promise<void>) {
this.onSaveConversation = callback;
}
protected webViewerAccessEnabled(): boolean {
return this.settingsService.settings.enableWebViewer;
}
protected memoriesEnabled(): boolean {
return this.settingsService.settings.enableMemories;
}
protected updateMemoriesEnabled(): boolean {
return this.settingsService.settings.allowUpdatingMemories;
}
protected async runAgentLoop(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks,
handleToolCall: (toolCall: AIToolCall) => Promise<{ shouldExit: boolean }>
): Promise<void> {
this.debugService?.log("AgentLoop", `Starting ${agentType} agent loop`);
let response = await this.streamRequestResponse(agentType, this.ensureCorrectConversationStructure(conversation), callbacks);
await this.saveConversation(agentType, conversation);
while (response.toolCall || response.shouldContinue) {
if (response.toolCall) {
this.debugService?.log("ToolCall", `${agentType} received function call: ${response.toolCall.name}`);
const result = await handleToolCall(response.toolCall);
if (result.shouldExit) {
this.debugService?.log("AgentLoop", `${agentType} exiting loop (shouldExit: true)`);
await this.saveConversation(agentType, conversation);
return;
}
} else {
callbacks.onThoughtUpdate(Copy.AIThoughtMessage);
}
response = await this.streamRequestResponse(agentType, this.ensureCorrectConversationStructure(conversation), callbacks);
await this.saveConversation(agentType, conversation);
}
this.debugService?.log("AgentLoop", `${agentType} agent loop completed`);
}
protected async requestAgentResponse(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks): Promise<string> {
return await this.withToolCallingDisabled(async () => {
await this.streamRequestResponse(agentType, this.ensureCorrectConversationStructure(conversation), callbacks);
const lastContent = conversation.contents[conversation.contents.length - 1];
const textResponse = lastContent?.content?.trim() ?? "";
if (textResponse === "") {
conversation.contents.push(new ConversationContent({
role: Role.User,
content: Copy.TextResponseRequired
}));
return await this.requestAgentResponse(agentType, conversation, callbacks);
}
return textResponse;
});
}
protected updateThought(toolCall: AIToolCall | null, callbacks: IChatServiceCallbacks) {
const userMessage = toolCall?.arguments.user_message;
if (userMessage && typeof userMessage === "string") {
callbacks.onThoughtUpdate(userMessage);
}
}
protected setDebugColor() {
this.debugService?.setDebugColor(DebugColor.WHITE);
}
private async saveConversation(agentType: AgentType, conversation: Conversation) {
if (agentType === AgentType.Main) {
await this.onSaveConversation?.(conversation);
}
}
private ensureCorrectConversationStructure(conversation: Conversation): Conversation {
// Check if the last message is from the assistant to prevent assistant-to-assistant structure
// This can happen when the assistant's last message had no function call and the user sends a new request
if (conversation.contents.length > 0) {
const lastMessage = conversation.contents[conversation.contents.length - 1];
if (lastMessage.role === Role.Assistant) {
// Insert a hidden "Continue" message to maintain proper conversation structure
conversation.contents.push(ConversationContent.safeContinue());
}
}
return conversation;
}
private async streamRequestResponse(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks
): Promise<{ toolCall: AIToolCall | null, shouldContinue: boolean }> {
if (!this.ai) { // this should never happen
return { toolCall: null, shouldContinue: false };
}
const conversationContent = new ConversationContent({ role: Role.Assistant });
conversation.contents.push(conversationContent);
let accumulatedContent = "";
let capturedToolCall: AIToolCall | null = null;
let capturedShouldContinue = false;
for await (const chunk of this.ai.streamRequest(conversation)) {
if (chunk.error && chunk.errorType) {
this.debugService?.log("StreamError", `AI stream error for ${agentType}: ${chunk.errorType}`);
conversationContent.content = chunk.error;
conversationContent.errorType = chunk.errorType;
callbacks.onStreamingUpdate();
break;
}
if (chunk.toolCallStarted) {
this.debugService?.log("ToolCallStarted", `Tool call started: ${chunk.toolCallStarted}`);
callbacks.onToolCallStarted(chunk.toolCallStarted);
}
if (chunk.toolCall) {
this.debugService?.log("ToolCall", `Function call captured: ${chunk.toolCall.name}`);
capturedToolCall = chunk.toolCall;
}
if (chunk.shouldContinue) {
capturedShouldContinue = true;
}
if (chunk.content) {
accumulatedContent += chunk.content;
conversationContent.content = accumulatedContent;
if (accumulatedContent.trim() !== "" && agentType == AgentType.Main) {
callbacks.onThoughtUpdate(null);
}
}
if (chunk.isComplete) {
const sanitizedContent = sanitizeToolCallContent(accumulatedContent, capturedToolCall);
if (sanitizedContent.trim() === "" && !capturedToolCall) {
conversation.contents.pop();
} else {
conversationContent.content = sanitizedContent;
if (capturedToolCall) {
conversationContent.toolCall = capturedToolCall.toConversationString();
conversationContent.toolId = capturedToolCall.toolId;
conversationContent.shouldDisplayContent = sanitizedContent.trim() !== "";
if (capturedToolCall.thoughtSignature) {
conversationContent.thoughtSignature = capturedToolCall.thoughtSignature;
}
}
}
}
if (conversationContent.content?.trim() !== "") {
callbacks.onStreamingUpdate();
} else {
conversationContent.shouldDisplayContent = false;
}
}
callbacks.onStreamingUpdate();
return { toolCall: capturedToolCall, shouldContinue: capturedShouldContinue };
}
protected async performAITool(toolCall: AIToolCall, callbacks: IChatServiceCallbacks): Promise<AIToolResponse> {
const providerResult = await this.ai?.resolveToolCall?.(toolCall) ?? null;
if (providerResult !== null) {
return providerResult;
}
const result = await this.aiToolService.performAITool(toolCall);
for (const artifact of result.payload.artifacts) {
callbacks.onArtifactProduced(artifact);
}
return result;
}
private async withToolCallingDisabled<T>(callback: () => Promise<T>): Promise<T> {
if (!this.ai) { // this shouldn't ever happen
Exception.throw("Error: No AI provider has been set!");
}
const aiToolUsageMode = this.ai.aiToolUsageMode;
this.ai.aiToolUsageMode = AIToolUsageMode.Disabled;
const result = await callback();
this.ai.aiToolUsageMode = aiToolUsageMode;
return result;
}
}