refactor: rename toolDefinitions to aiFunctionDefinitions and add AIFunctionUsageMode support

- Rename toolDefinitions to aiFunctionDefinitions across all AI classes
- Add aiFunctionUsageMode property to control function calling behavior
- Implement provider-specific tool_choice/tool_config based on usage mode
- Remove AIController and create BaseAgent base class
- Update ExecutionPrompt with scope of execution guidelines
- Simplify ChatArea layout update logic by removing debounce
- Add naming service completion wait in ChatService
- Replace console.warn with Exception.warn in InputService
- Delete unused AIController.ts file
- Update all tests to use new aiFunctionDefinitions property
This commit is contained in:
Andrew Beal 2026-01-30 19:36:52 +00:00
parent 9276d2d0cd
commit ac835d1346
19 changed files with 216 additions and 90 deletions

View file

@ -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<IStreamChunk, void, unknown>;
public abstract formatBinaryFiles(attachments: Attachment[]): string;

View file

@ -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;

View file

@ -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<string, unknown> = {
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;

View file

@ -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<IStreamChunk, void, unknown>;
formatBinaryFiles(attachments: Attachment[]): string;

View file

@ -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;
}

View file

@ -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

View file

@ -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<string, string> = new Map<string, string>();
let currentStreamFinalized: boolean = false;
let layoutUpdateTimeout: NodeJS.Timeout | null = null;
function getGreetingByTime(): string {
const hour = new Date().getHours();

View file

@ -0,0 +1,5 @@
export enum AIFunctionUsageMode {
Auto = "auto",
Enabled = "enabled",
Disabled = "disabled"
}

View file

@ -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<string> {
const response = await this.streamRequestResponse(AgentType.Main, this.ensureCorrectConversationStructure(conversation), callbacks);
protected async requestAgentResponse(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks): Promise<string> {
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<T>(callback: () => Promise<T>): Promise<T> {
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;
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -85,9 +85,10 @@ export class ChatService {
await this.saveConversation(conversation);
callbacks.onStreamingUpdate(conversationContent.timestamp.getTime().toString());
let namingPromise: Promise<void> | 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<void>(resolve => setTimeout(resolve, 5000));
await Promise.race([namingPromise, timeout]);
}
});
} catch (error) {
if (!AbortService.isAbortError(error)) {

View file

@ -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;
}

View file

@ -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);

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };