diff --git a/AIClasses/BaseAIClass.ts b/AIClasses/BaseAIClass.ts index af9bdfb..9b55f88 100644 --- a/AIClasses/BaseAIClass.ts +++ b/AIClasses/BaseAIClass.ts @@ -15,6 +15,7 @@ import { ApiError, ApiErrorType } from "Types/ApiError"; import type { AbortService } from "Services/AbortService"; import type { IAIFileService } from "./IAIFileService"; import { AgentType } from "Enums/AgentType"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; export abstract class BaseAIClass implements IAIClass { @@ -28,7 +29,8 @@ export abstract class BaseAIClass implements IAIClass { private _systemPrompt: string = ""; private _userInstruction: string = ""; private _agentType: AgentType = AgentType.Main; - private _toolDefinitions: IAIFunctionDefinition[] = []; + private _aiFunctionDefinitions: IAIFunctionDefinition[] = []; + private _aiFunctionUsageMode: AIFunctionUsageMode = AIFunctionUsageMode.Auto; protected constructor(provider: AIProvider) { this.provider = provider; @@ -60,12 +62,12 @@ export abstract class BaseAIClass implements IAIClass { return this._userInstruction; } - public get toolDefinitions(): IAIFunctionDefinition[] { - return this._toolDefinitions; + public get aiFunctionDefinitions(): IAIFunctionDefinition[] { + return this._aiFunctionDefinitions; } - public set toolDefinitions(toolDefinitions: IAIFunctionDefinition[]) { - this._toolDefinitions = toolDefinitions; + public set aiFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]) { + this._aiFunctionDefinitions = aiFunctionDefinitions; } public get agentType() { @@ -76,6 +78,14 @@ export abstract class BaseAIClass implements IAIClass { this._agentType = agentType; } + public get aiFunctionUsageMode(): AIFunctionUsageMode { + return this._aiFunctionUsageMode; + } + + public set aiFunctionUsageMode(mode: AIFunctionUsageMode) { + this._aiFunctionUsageMode = mode; + } + public abstract streamRequest(conversation: Conversation): AsyncGenerator; public abstract formatBinaryFiles(attachments: Attachment[]): string; diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 2701d6b..1116ab8 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -15,6 +15,7 @@ import { isTextFile } from "Enums/FileType"; import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping"; import { ApiError, ApiErrorType } from "Types/ApiError"; import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; export class Claude extends BaseAIClass { @@ -68,7 +69,7 @@ export class Claude extends BaseAIClass { }; const tools: ToolUnion[] = this.addCacheControlToTools([ - webSearchTool, ...this.mapFunctionDefinitions(this.toolDefinitions)]); + webSearchTool, ...this.mapFunctionDefinitions(this.aiFunctionDefinitions)]); const requestBody = { model: this.model(), @@ -76,6 +77,7 @@ export class Claude extends BaseAIClass { system: systemPrompt, messages: messages, tools: tools, + tool_choice: this.buildClaudeToolChoice(), stream: true }; @@ -365,6 +367,22 @@ export class Claude extends BaseAIClass { return cachedMessages; } + private buildClaudeToolChoice(): { type: string } { + // If no tools defined, fall back to auto + if (this.aiFunctionDefinitions.length === 0) { + return { type: "auto" }; + } + + switch (this.aiFunctionUsageMode) { + case AIFunctionUsageMode.Auto: + return { type: "auto" }; + case AIFunctionUsageMode.Enabled: + return { type: "any" }; + case AIFunctionUsageMode.Disabled: + return { type: "none" }; + } + } + private extractRetryDelay(error: ApiError): number | undefined { if (error.info.type !== ApiErrorType.RATE_LIMIT || !error.info.responseHeaders) { return undefined; diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index c44ac10..f14af9f 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -17,6 +17,7 @@ import { Exception } from "Helpers/Exception"; import { ApiError, ApiErrorType } from "Types/ApiError"; import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper"; import type { GeminiRetryInfo, GeminiErrorResponse } from "./GeminiTypes"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; export class Gemini extends BaseAIClass { @@ -103,11 +104,11 @@ export class Gemini extends BaseAIClass { information, recent events, news, or facts that may have changed. After calling this, you will be able to perform web searches.`, }, - ...this.mapFunctionDefinitions(this.toolDefinitions), + ...this.mapFunctionDefinitions(this.aiFunctionDefinitions), ] } - const requestBody = { + const requestBody: Record = { system_instruction: { parts: [ { @@ -137,6 +138,11 @@ export class Gemini extends BaseAIClass { tools: [tools] }; + // Only include tool_config when actually sending function declarations (not for google_search) + if (!requestWebSearch) { + requestBody.tool_config = this.buildGeminiToolConfig(); + } + yield* this.streamingService.streamRequest( `${AIProviderURL.Gemini}/${this.model()}:streamGenerateContent?key=${this.apiKey}&alt=sse`, requestBody, @@ -354,6 +360,22 @@ export class Gemini extends BaseAIClass { return JSON.stringify(parts); } + private buildGeminiToolConfig(): { function_calling_config: { mode: string } } { + // If no tools defined, fall back to auto + if (this.aiFunctionDefinitions.length === 0) { + return { function_calling_config: { mode: "AUTO" } }; + } + + switch (this.aiFunctionUsageMode) { + case AIFunctionUsageMode.Auto: + return { function_calling_config: { mode: "AUTO" } }; + case AIFunctionUsageMode.Enabled: + return { function_calling_config: { mode: "ANY" } }; + case AIFunctionUsageMode.Disabled: + return { function_calling_config: { mode: "NONE" } }; + } + } + private extractRetryDelay(error: ApiError): number | undefined { if (error.info.type !== ApiErrorType.RATE_LIMIT || !error.info.responseBody) { return undefined; diff --git a/AIClasses/IAIClass.ts b/AIClasses/IAIClass.ts index f334883..90550d5 100644 --- a/AIClasses/IAIClass.ts +++ b/AIClasses/IAIClass.ts @@ -4,13 +4,15 @@ import type { Attachment } from "Conversations/Attachment"; import type { IAIFunctionDefinition } from "./FunctionDefinitions/IAIFunctionDefinition"; import type { AIProvider } from "Enums/ApiProvider"; import type { AgentType } from "Enums/AgentType"; +import type { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; export interface IAIClass { get currentProvider(): AIProvider; set agentType(agentType: AgentType); set systemPrompt(systemPrompt: string); set userInstruction(userInstruction: string); - set toolDefinitions(toolDefinitions: IAIFunctionDefinition[]); + set aiFunctionDefinitions(aiFunctionDefinitions: IAIFunctionDefinition[]); + set aiFunctionUsageMode(mode: AIFunctionUsageMode); streamRequest(conversation: Conversation): AsyncGenerator; formatBinaryFiles(attachments: Attachment[]): string; diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index 38fd631..4a8ccd7 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -14,6 +14,7 @@ import { MimeType, toMimeType } from "Enums/MimeType"; import { isTextFile } from "Enums/FileType"; import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping"; import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; export class OpenAI extends BaseAIClass { @@ -42,13 +43,14 @@ export class OpenAI extends BaseAIClass { const tools = [{ type: "web_search" - }, ...this.mapFunctionDefinitions(this.toolDefinitions)]; + }, ...this.mapFunctionDefinitions(this.aiFunctionDefinitions)]; const requestBody = { model: this.model(), instructions: systemPrompt, input: input, tools: tools, + tool_choice: this.buildOpenAIToolChoice(), stream: true }; @@ -350,13 +352,29 @@ export class OpenAI extends BaseAIClass { }]); } + private buildOpenAIToolChoice(): string { + // If no tools defined, fall back to auto + if (this.aiFunctionDefinitions.length === 0) { + return "auto"; + } + + switch (this.aiFunctionUsageMode) { + case AIFunctionUsageMode.Auto: + return "auto"; + case AIFunctionUsageMode.Enabled: + return "required"; + case AIFunctionUsageMode.Disabled: + return "none"; + } + } + private extractRetryDelay(error: ApiError): number | undefined { if (error.info.type !== ApiErrorType.RATE_LIMIT || !error.info.responseHeaders) { return undefined; } - + const headers = error.info.responseHeaders; - + // 1. Prefer standard Retry-After header (seconds or HTTP-date) const retryAfter = headers.get('retry-after'); if (retryAfter) { @@ -365,16 +383,16 @@ export class OpenAI extends BaseAIClass { return Math.max(0, seconds); } } - + // 2. Fallback to provider-specific headers (e.g., OpenAI) - const resetHeader = + const resetHeader = headers.get('x-ratelimit-reset-requests') ?? headers.get('x-ratelimit-reset-tokens'); - + if (resetHeader) { return this.parseDurationToSeconds(resetHeader); } - + return undefined; } diff --git a/AIPrompts/ExecutionPrompt.ts b/AIPrompts/ExecutionPrompt.ts index b62abff..9be7ec3 100644 --- a/AIPrompts/ExecutionPrompt.ts +++ b/AIPrompts/ExecutionPrompt.ts @@ -29,6 +29,35 @@ You are a task execution agent. You execute assigned tasks and report outcomes. - Decide that an action is unnecessary - Interpret "if X exists" as permission to skip - always attempt the action - Speculate about future steps or broader plans +- **Perform actions beyond what the instruction explicitly states** - even if the context suggests more should be done + +--- + +## Scope of Execution + +**CRITICAL: Only perform the SPECIFIC action stated in the instruction. Nothing more.** + +The context is background information to help you understand the task - it is NOT additional instructions. Do not infer additional actions from the context. + +### Instruction vs Context + +| Instruction Says | Context Mentions | Correct Action | WRONG Action | +|-----------------|------------------|----------------|--------------| +| "Read the template file" | "We're creating an NPC note" | Read the file, report contents | Read AND create the note | +| "Search for #project files" | "User wants to summarize them" | Search, report results | Search AND summarize | +| "Check if config.json exists" | "We need to update settings" | Check existence, report | Check AND update settings | + +### Why This Matters + +The orchestration agent has broken the work into specific steps for a reason. Each step exists to: +- Gather specific information +- Perform a specific action +- Enable decisions about subsequent steps + +When you perform actions beyond your instruction, you: +- Skip the orchestrator's opportunity to make routing decisions +- May produce outcomes the user didn't approve +- Break the planned workflow's checkpoints --- @@ -148,6 +177,15 @@ The orchestration agent will evaluate your report and make routing decisions. Yo ❌ Wrong: "File hasn't changed recently, backup unnecessary" ✅ Right: Create the backup, report success +### Anti-pattern 4: Doing more than instructed + +**Instruction:** "Read the template file at templates/character.md" +**Context:** "We are creating a character named 'Bob' using this template" +❌ Wrong: Read the template, then create the character file +✅ Right: Read the template, report its contents, call CompleteTask + +The context tells you WHY you're reading the template, not that you should also create the character. A later step will handle creation. + --- ## Signaling Completion diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index e8745e6..77869dd 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -34,35 +34,21 @@ autoScroll = true; } - export function updateChatAreaLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean = false) { - if (layoutUpdateTimeout) { - clearTimeout(layoutUpdateTimeout); - layoutUpdateTimeout = null; + export async function updateChatAreaLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean = false) { + await tick(); + + if (!chatAreaPaddingElement) { + return; } - const executeLayout = async () => { - await tick(); - - if (!chatAreaPaddingElement) { - return; - } - - if (messageElements.length <= 0) { - chatAreaPaddingElement.style.paddingBottom = "0px"; - return; - } - - requestAnimationFrame(() => { - applyLayout(behavior, shouldSettle); - }); - }; - - // Instant/settle executes immediately; smooth updates are debounced - if (behavior === "instant" || shouldSettle) { - executeLayout(); - } else { - layoutUpdateTimeout = setTimeout(executeLayout, 50); + if (messageElements.length <= 0) { + chatAreaPaddingElement.style.paddingBottom = "0px"; + return; } + + requestAnimationFrame(() => { + applyLayout(behavior, shouldSettle); + }); } function applyLayout(behavior: ScrollBehavior | undefined, shouldSettle: boolean) { @@ -112,7 +98,6 @@ let messageElements: { index: number, element: HTMLElement }[] = []; let lastProcessedContent: Map = new Map(); let currentStreamFinalized: boolean = false; - let layoutUpdateTimeout: NodeJS.Timeout | null = null; function getGreetingByTime(): string { const hour = new Date().getHours(); diff --git a/Enums/AIFunctionUsageMode.ts b/Enums/AIFunctionUsageMode.ts new file mode 100644 index 0000000..890482c --- /dev/null +++ b/Enums/AIFunctionUsageMode.ts @@ -0,0 +1,5 @@ +export enum AIFunctionUsageMode { + Auto = "auto", + Enabled = "enabled", + Disabled = "disabled" +} \ No newline at end of file diff --git a/Services/AIServices/AIController.ts b/Services/AIServices/BaseAgent.ts similarity index 83% rename from Services/AIServices/AIController.ts rename to Services/AIServices/BaseAgent.ts index fcfae90..c51221e 100644 --- a/Services/AIServices/AIController.ts +++ b/Services/AIServices/BaseAgent.ts @@ -14,8 +14,10 @@ import { Services } from "Services/Services"; import type { AIFunctionService } from "./AIFunctionService"; import type { DebugService } from "Services/DebugService"; import { DebugColor } from "Enums/DebugColor"; +import { Exception } from "Helpers/Exception"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; -export class AIController { +export abstract class BaseAgent { protected ai: IAIClass | undefined; protected readonly aiPrompt: IPrompt; @@ -67,30 +69,22 @@ export class AIController { this.debugService?.log("AgentLoop", `${agentType} agent loop completed`); } - protected async requestAgentResponse(conversation: Conversation, callbacks: IChatServiceCallbacks): Promise { - const response = await this.streamRequestResponse(AgentType.Main, this.ensureCorrectConversationStructure(conversation), callbacks); + protected async requestAgentResponse(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks): Promise { + return await this.withToolCallingDisabled(async () => { + await this.streamRequestResponse(agentType, this.ensureCorrectConversationStructure(conversation), callbacks); - if (response.functionCall) { - conversation.addFunctionResponse(new AIFunctionResponse( - response.functionCall.name, - { error: Copy.TextResponseToolDenial }, - response.functionCall.toolId - )); - return await this.requestAgentResponse(conversation, callbacks); - } + const lastContent = conversation.contents[conversation.contents.length - 1]; + const textResponse = lastContent?.content?.trim() ?? ""; - 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(conversation, callbacks); - } - - return textResponse; + if (textResponse === "") { + conversation.contents.push(new ConversationContent({ + role: Role.User, + content: Copy.TextResponseRequired + })); + return await this.requestAgentResponse(agentType, conversation, callbacks); + } + return textResponse; + }); } protected updateThought(functionCall: AIFunctionCall | null, callbacks: IChatServiceCallbacks) { @@ -192,4 +186,18 @@ export class AIController { return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue }; } + + private async withToolCallingDisabled(callback: () => Promise): Promise { + if (!this.ai) { // this shouldn't ever happen + Exception.throw("Error: No AI provider has been set!"); + } + + const aiFunctionUsageMode = this.ai.aiFunctionUsageMode; + this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Disabled; + + const result = await callback(); + + this.ai.aiFunctionUsageMode = aiFunctionUsageMode; + return result; + } } \ No newline at end of file diff --git a/Services/AIServices/ExecutionAgent.ts b/Services/AIServices/ExecutionAgent.ts index 58c8fc4..4806ebf 100644 --- a/Services/AIServices/ExecutionAgent.ts +++ b/Services/AIServices/ExecutionAgent.ts @@ -1,5 +1,5 @@ import type { ExecutionStep } from "Types/ExecutionStep"; -import { AIController } from "./AIController"; +import { BaseAgent } from "./BaseAgent"; import { AgentType } from "Enums/AgentType"; import { Conversation } from "Conversations/Conversation"; import type { IChatServiceCallbacks } from "Services/ChatService"; @@ -12,8 +12,9 @@ import { Exception } from "Helpers/Exception"; import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions"; import { Copy, replaceCopy } from "Enums/Copy"; import { DebugColor } from "Enums/DebugColor"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; -export class ExecutionAgent extends AIController { +export class ExecutionAgent extends BaseAgent { private static readonly MAX_AGENT_DEPTH: number = 3; @@ -58,6 +59,7 @@ export class ExecutionAgent extends AIController { return { shouldExit: false }; } this.debugService?.log("ExecutionAgent", `Task completed (success: ${parseResult.data.success}): ${parseResult.data.description}`); + this.updateThought(functionCall, callbacks); executionResult = parseResult.data; return { shouldExit: true }; } @@ -85,9 +87,10 @@ export class ExecutionAgent extends AIController { Exception.throw("Error: No AI provider has been set!"); } this.ai.agentType = AgentType.Execution; + this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Enabled; this.ai.systemPrompt = this.aiPrompt.executionInstruction(); this.ai.userInstruction = ""; // do not include user instruction for execution agent - this.ai.toolDefinitions = AIFunctionDefinitions.executionAgentDefinitions(); + this.ai.aiFunctionDefinitions = AIFunctionDefinitions.executionAgentDefinitions(); } protected override setDebugColor(): void { diff --git a/Services/AIServices/MainAgent.ts b/Services/AIServices/MainAgent.ts index 35164df..51ca3a5 100644 --- a/Services/AIServices/MainAgent.ts +++ b/Services/AIServices/MainAgent.ts @@ -4,7 +4,7 @@ import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionD import { Exception } from "Helpers/Exception"; import { AIFunction, isAIFunction } from "Enums/AIFunction"; import { AgentType } from "Enums/AgentType"; -import { AIController } from "./AIController"; +import { BaseAgent } from "./BaseAgent"; import { ExecuteWorkflowArgsSchema, type ExecuteWorkflowArgs } from "AIClasses/Schemas/AIFunctionSchemas"; import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse"; import type { AIFunctionCall } from "AIClasses/AIFunctionCall"; @@ -12,8 +12,9 @@ import { OrchestrationAgent } from "./OrchestrationAgent"; import { ConversationContent } from "Conversations/ConversationContent"; import { Role } from "Enums/Role"; import { DebugColor } from "Enums/DebugColor"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; -export class MainAgent extends AIController { +export class MainAgent extends BaseAgent { public async runMainAgent(conversation: Conversation, allowDestructiveActions: boolean, planningMode: boolean, callbacks: IChatServiceCallbacks) { await this.setAgentPromptAndTools(planningMode, allowDestructiveActions); @@ -88,9 +89,10 @@ export class MainAgent extends AIController { Exception.throw("Error: No AI provider has been set!"); } this.ai.agentType = AgentType.Main; + this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Auto; this.ai.systemPrompt = this.aiPrompt.systemInstruction(); this.ai.userInstruction = await this.aiPrompt.userInstruction(); - this.ai.toolDefinitions = AIFunctionDefinitions.agentDefinitions(allowDestructiveActions, planningMode); + this.ai.aiFunctionDefinitions = AIFunctionDefinitions.agentDefinitions(allowDestructiveActions, planningMode); } protected override setDebugColor(): void { diff --git a/Services/AIServices/OrchestrationAgent.ts b/Services/AIServices/OrchestrationAgent.ts index 94eff5a..82f6d9e 100644 --- a/Services/AIServices/OrchestrationAgent.ts +++ b/Services/AIServices/OrchestrationAgent.ts @@ -1,5 +1,5 @@ import { CancelPlanArgsSchema, CompletePlanArgsSchema, CompleteStepArgsSchema, ReplanArgsSchema, type ExecuteWorkflowArgs } from "AIClasses/Schemas/AIFunctionSchemas"; -import { AIController } from "./AIController"; +import { BaseAgent } from "./BaseAgent"; import { ConversationContent } from "Conversations/ConversationContent"; import { Role } from "Enums/Role"; import type { IChatServiceCallbacks } from "Services/ChatService"; @@ -14,8 +14,9 @@ import { AgentType } from "Enums/AgentType"; import { AIFunction, isAIFunction } from "Enums/AIFunction"; import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse"; import { DebugColor } from "Enums/DebugColor"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; -export class OrchestrationAgent extends AIController { +export class OrchestrationAgent extends BaseAgent { private static readonly MAX_AGENT_DEPTH: number = 3; @@ -121,7 +122,7 @@ export class OrchestrationAgent extends AIController { role: Role.User, content: Copy.RequestPlanSummary })); - const orchestrationSummary = await this.requestAgentResponse(planningConversation, callbacks); + const orchestrationSummary = await this.requestAgentResponse(AgentType.Orchestration, planningConversation, callbacks); return { planExecutionSummary: orchestrationSummary }; } @@ -170,6 +171,7 @@ export class OrchestrationAgent extends AIController { return Promise.resolve({ shouldExit: false }); } this.debugService?.log("Orchestration", `CompleteStep called (confirmed: ${parseResult.data.confirm_completion})`); + this.updateThought(functionCall, callbacks); planningConversation.addFunctionResponse(new AIFunctionResponse( functionCallName, { message: "Step Completed" }, @@ -198,6 +200,7 @@ export class OrchestrationAgent extends AIController { return Promise.resolve({ shouldExit: false }); } this.debugService?.log("Orchestration", `CompletePlan called (confirmed: ${parseResult.data.confirm_completion})`); + this.updateThought(functionCall, callbacks); planningConversation.addFunctionResponse(new AIFunctionResponse( functionCallName, { message: "Plan Completed" }, @@ -218,6 +221,7 @@ export class OrchestrationAgent extends AIController { return Promise.resolve({ shouldExit: false }); } this.debugService?.log("Orchestration", `Replan requested: ${parseResult.data.context}`); + this.updateThought(functionCall, callbacks); planningConversation.addFunctionResponse(new AIFunctionResponse( functionCallName, { message: "Replan Requested" }, @@ -238,6 +242,7 @@ export class OrchestrationAgent extends AIController { return Promise.resolve({ shouldExit: false }); } this.debugService?.log("Orchestration", `Plan cancellation requested: ${parseResult.data.context}`); + this.updateThought(functionCall, callbacks); planningConversation.addFunctionResponse(new AIFunctionResponse( functionCallName, { message: "Plan Cancelled" }, @@ -271,9 +276,10 @@ export class OrchestrationAgent extends AIController { Exception.throw("Error: No AI provider has been set!"); } this.ai.agentType = AgentType.Orchestration; + this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Enabled; this.ai.systemPrompt = this.aiPrompt.orchestrationInstruction(); this.ai.userInstruction = ""; // do not include user instruction for orchestration agent - this.ai.toolDefinitions = AIFunctionDefinitions.orchestrationAgentDefinitions(); + this.ai.aiFunctionDefinitions = AIFunctionDefinitions.orchestrationAgentDefinitions(); } protected override setDebugColor(): void { diff --git a/Services/AIServices/PlanningAgent.ts b/Services/AIServices/PlanningAgent.ts index a47b5bf..c50f254 100644 --- a/Services/AIServices/PlanningAgent.ts +++ b/Services/AIServices/PlanningAgent.ts @@ -1,5 +1,5 @@ import { Conversation } from "Conversations/Conversation"; -import { AIController } from "./AIController"; +import { BaseAgent } from "./BaseAgent"; import { AskUserQuestionPlanningArgsSchema, SubmitPlanArgsSchema } from "AIClasses/Schemas/AIFunctionSchemas"; import { Exception } from "Helpers/Exception"; import type { IChatServiceCallbacks } from "Services/ChatService"; @@ -12,8 +12,9 @@ import { Copy } from "Enums/Copy"; import { ConversationContent } from "Conversations/ConversationContent"; import { Role } from "Enums/Role"; import { DebugColor } from "Enums/DebugColor"; +import { AIFunctionUsageMode } from "Enums/AIFunctionUsageMode"; -export class PlanningAgent extends AIController { +export class PlanningAgent extends BaseAgent { private static readonly MAX_AGENT_DEPTH = 3; @@ -110,9 +111,10 @@ export class PlanningAgent extends AIController { Exception.throw("Error: No AI provider has been set!"); } this.ai.agentType = AgentType.Planning; + this.ai.aiFunctionUsageMode = AIFunctionUsageMode.Enabled; this.ai.systemPrompt = this.aiPrompt.planningInstruction(); this.ai.userInstruction = await this.aiPrompt.userInstruction(); - this.ai.toolDefinitions = AIFunctionDefinitions.planningAgentDefinitions(); + this.ai.aiFunctionDefinitions = AIFunctionDefinitions.planningAgentDefinitions(); } protected override setDebugColor(): void { diff --git a/Services/ChatService.ts b/Services/ChatService.ts index 935df33..b1f6562 100644 --- a/Services/ChatService.ts +++ b/Services/ChatService.ts @@ -85,9 +85,10 @@ export class ChatService { await this.saveConversation(conversation); callbacks.onStreamingUpdate(conversationContent.timestamp.getTime().toString()); + let namingPromise: Promise | undefined; if (firstMessage) { this.onNameChanged?.(conversation.title); // on change for initial conversation name - void this.namingService.requestName(conversation, formattedRequest, this.onNameChanged) + namingPromise = this.namingService.requestName(conversation, formattedRequest, this.onNameChanged); } if (attachments.length > 0) { @@ -107,6 +108,11 @@ export class ChatService { callbacks.onStreamingUpdate(null); await this.mainAgent.runMainAgent(conversation, allowDestructiveActions, planningMode, callbacks); + + if (namingPromise) { + const timeout = new Promise(resolve => setTimeout(resolve, 5000)); + await Promise.race([namingPromise, timeout]); + } }); } catch (error) { if (!AbortService.isAbortError(error)) { diff --git a/Services/InputService.ts b/Services/InputService.ts index 3ed8603..8a4790a 100644 --- a/Services/InputService.ts +++ b/Services/InputService.ts @@ -164,7 +164,7 @@ export class InputService { public setCursorPosition(element: HTMLElement, position: number, fromEnd: boolean = false) { // Ensure element is focusable if (!element.isContentEditable) { - console.warn("Element must be contenteditable"); + Exception.warn("Element must be contenteditable"); return false; } @@ -213,7 +213,7 @@ export class InputService { public insertTextAtCursor(text: string, element?: HTMLElement) { if (element && !element.isContentEditable) { - console.warn("Element must be contenteditable"); + Exception.warn("Element must be contenteditable"); return; } @@ -236,7 +236,7 @@ export class InputService { public insertElementAtCursor(node: Node, element?: HTMLElement) { if (element && !element.isContentEditable) { - console.warn("Element must be contenteditable"); + Exception.warn("Element must be contenteditable"); return; } @@ -260,7 +260,7 @@ export class InputService { public deleteTextRange(startPos: number, endPos: number, element: HTMLElement) { if (!element.isContentEditable) { - console.warn("Element must be contenteditable"); + Exception.warn("Element must be contenteditable"); return; } @@ -277,7 +277,7 @@ export class InputService { const endResult = this.findTextNodeAndOffset(element, endPos); if (!startResult.node || !endResult.node) { - console.warn("Could not find text nodes for range deletion"); + Exception.warn("Could not find text nodes for range deletion"); return; } diff --git a/Services/StreamingMarkdownService.ts b/Services/StreamingMarkdownService.ts index 14c2ce5..1d9860b 100644 --- a/Services/StreamingMarkdownService.ts +++ b/Services/StreamingMarkdownService.ts @@ -15,6 +15,7 @@ import { Services } from "./Services"; import { Selector } from "Enums/Selector"; import type { HTMLService } from "./HTMLService"; import type { VaultCacheService } from "./VaultCacheService"; +import { Exception } from "Helpers/Exception"; interface IStreamingState { element: HTMLElement; @@ -70,7 +71,7 @@ export class StreamingMarkdownService { const result = this.processor.processSync(preprocessed); return String(result); } catch (error) { - console.warn("Markdown processing failed:", error); + Exception.warn(`Markdown processing failed:\n${error}`); return this.getFallbackHTML(text); } } @@ -117,7 +118,7 @@ export class StreamingMarkdownService { this.htmlService.setHTMLContent(state.element, html); state.lastProcessedLength = state.buffer.length; } catch (error) { - console.warn("Streaming render failed:", error); + Exception.warn(`Streaming render failed:\n${error}`); } this.renderTimeouts.delete(messageId); diff --git a/__tests__/AIClasses/Claude.test.ts b/__tests__/AIClasses/Claude.test.ts index 65e1e5c..e447f6d 100644 --- a/__tests__/AIClasses/Claude.test.ts +++ b/__tests__/AIClasses/Claude.test.ts @@ -1157,7 +1157,7 @@ describe('Claude', () => { // Set system prompts before calling streamRequest claude.systemPrompt = 'System instruction'; claude.userInstruction = 'User instruction'; - claude.toolDefinitions = []; + claude.aiFunctionDefinitions = []; mockStreamingService.streamRequest.mockImplementation(async function* () { yield { content: 'response', isComplete: true }; diff --git a/__tests__/AIClasses/Gemini.test.ts b/__tests__/AIClasses/Gemini.test.ts index 642a90a..e13071e 100644 --- a/__tests__/AIClasses/Gemini.test.ts +++ b/__tests__/AIClasses/Gemini.test.ts @@ -410,7 +410,7 @@ describe('Gemini', () => { // Set system prompts before calling streamRequest gemini.systemPrompt = 'System instruction'; gemini.userInstruction = 'User instruction'; - gemini.toolDefinitions = []; + gemini.aiFunctionDefinitions = []; mockStreamingService.streamRequest.mockImplementation(async function* () { yield { content: 'done', isComplete: true }; diff --git a/__tests__/AIClasses/OpenAI.test.ts b/__tests__/AIClasses/OpenAI.test.ts index 6fe96f0..328c91a 100644 --- a/__tests__/AIClasses/OpenAI.test.ts +++ b/__tests__/AIClasses/OpenAI.test.ts @@ -311,7 +311,7 @@ describe('OpenAI', () => { // Set system prompts before calling streamRequest openai.systemPrompt = 'System instruction'; openai.userInstruction = 'User instruction'; - openai.toolDefinitions = []; + openai.aiFunctionDefinitions = []; mockStreamingService.streamRequest.mockImplementation(async function* () { yield { content: 'response', isComplete: true };