mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Rename remaining references to functioncall to toolcall
This commit is contained in:
parent
df424eee68
commit
89fa8b809c
31 changed files with 743 additions and 743 deletions
|
|
@ -16,7 +16,7 @@ export class AIToolCall {
|
|||
|
||||
public toConversationString() {
|
||||
return JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: this.name,
|
||||
args: this.arguments,
|
||||
id: this.toolId,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type { ConversationContent } from "Conversations/ConversationContent";
|
|||
import type { Attachment } from "Conversations/Attachment";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type { StreamingService } from "Services/StreamingService";
|
||||
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/Schemas/AIToolTypes";
|
||||
import type { StoredToolCall, StoredFunctionResponse } from "AIClasses/Schemas/AIToolTypes";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { ApiError, ApiErrorType } from "Types/ApiError";
|
||||
import type { AbortService } from "Services/AbortService";
|
||||
|
|
@ -109,24 +109,24 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
|
||||
protected filterConversationContents(conversationContent: ConversationContent[]): ConversationContent[] {
|
||||
return conversationContent.filter((content, index, array) => {
|
||||
if (!content.content && !content.functionCall && !content.functionResponse && (!content.attachments || content.attachments.length === 0)) {
|
||||
if (!content.content && !content.toolCall && !content.functionResponse && (!content.attachments || content.attachments.length === 0)) {
|
||||
return false; // Filter out empty content
|
||||
}
|
||||
|
||||
if (content.functionResponse) { // Filter out 'lone' function responses
|
||||
const previousItem = array[index - 1];
|
||||
const hasValidCall = previousItem && previousItem.functionCall && content.toolId === previousItem.toolId;
|
||||
const hasValidCall = previousItem && previousItem.toolCall && content.toolId === previousItem.toolId;
|
||||
if (!hasValidCall) {
|
||||
Exception.warn(`[Filter Debug] Filtered orphaned function response at index ${index}/${array.length}:\n` +
|
||||
` ToolId: ${content.toolId}\n` +
|
||||
` Previous item has functionCall: ${previousItem?.functionCall ? 'yes' : 'no'}\n` +
|
||||
` Previous item has toolCall: ${previousItem?.toolCall ? 'yes' : 'no'}\n` +
|
||||
` Previous item toolId: ${previousItem?.toolId}\n` +
|
||||
` Response: ${content.functionResponse}`);
|
||||
}
|
||||
return hasValidCall;
|
||||
}
|
||||
|
||||
if (!content.functionCall) {
|
||||
if (!content.toolCall) {
|
||||
return true; // Keep non-function-calls
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
` ToolId: ${content.toolId}\n` +
|
||||
` Next item has functionResponse: ${nextItem?.functionResponse ? 'yes' : 'no'}\n` +
|
||||
` Next item toolId: ${nextItem?.toolId}\n` +
|
||||
` Call: ${content.functionCall}`);
|
||||
` Call: ${content.toolCall}`);
|
||||
}
|
||||
return hasValidResponse;
|
||||
});
|
||||
|
|
@ -202,10 +202,10 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
* Converts a function call to legacy text format for cross-provider compatibility.
|
||||
* Used when a provider doesn't have the required ID field (e.g., Gemini → Claude/OpenAI).
|
||||
*/
|
||||
protected convertFunctionCallToText(parsedContent: StoredFunctionCall): string {
|
||||
protected convertToolCallToText(parsedContent: StoredToolCall): string {
|
||||
const formattedJson = JSON.stringify({
|
||||
name: parsedContent.functionCall.name,
|
||||
args: parsedContent.functionCall.args
|
||||
name: parsedContent.toolCall.name,
|
||||
args: parsedContent.toolCall.args
|
||||
}, null, 2);
|
||||
|
||||
return `<!-- Historical tool call. This action was ALREADY COMPLETED.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { MimeType, toMimeType } from "Enums/MimeType";
|
|||
import { isTextFile } from "Enums/FileType";
|
||||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { ApiError, ApiErrorType } from "Types/ApiError";
|
||||
import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
|
||||
export class Claude extends BaseAIClass {
|
||||
|
|
@ -103,7 +103,7 @@ export class Claude extends BaseAIClass {
|
|||
const data = JSON.parse(chunk) as RawMessageStreamEvent;
|
||||
|
||||
let text = "";
|
||||
let functionCall: AIToolCall | undefined = undefined;
|
||||
let toolCall: AIToolCall | undefined = undefined;
|
||||
let isComplete = false;
|
||||
let shouldContinue = false;
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ export class Claude extends BaseAIClass {
|
|||
if (this.accumulatedFunctionName && this.accumulatedFunctionArgs) {
|
||||
try {
|
||||
const args = JSON.parse(this.accumulatedFunctionArgs) as Record<string, unknown>;
|
||||
functionCall = new AIToolCall(
|
||||
toolCall = new AIToolCall(
|
||||
aiToolFromString(this.accumulatedFunctionName),
|
||||
args as Record<string, object>,
|
||||
this.accumulatedFunctionId || undefined,
|
||||
|
|
@ -169,7 +169,7 @@ export class Claude extends BaseAIClass {
|
|||
return {
|
||||
content: text,
|
||||
isComplete: isComplete,
|
||||
functionCall: functionCall,
|
||||
toolCall: toolCall,
|
||||
shouldContinue: shouldContinue,
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
@ -192,21 +192,21 @@ export class Claude extends BaseAIClass {
|
|||
}
|
||||
|
||||
// Add function call if present
|
||||
if (content.functionCall) {
|
||||
const parsedContent = parseFunctionCall(content.functionCall);
|
||||
if (content.toolCall) {
|
||||
const parsedContent = parseToolCall(content.toolCall);
|
||||
|
||||
if (parsedContent) {
|
||||
if (parsedContent.functionCall.id && parsedContent.functionCall.id.trim() !== "") {
|
||||
if (parsedContent.toolCall.id && parsedContent.toolCall.id.trim() !== "") {
|
||||
contentBlocks.push({
|
||||
type: "tool_use",
|
||||
id: parsedContent.functionCall.id,
|
||||
name: parsedContent.functionCall.name,
|
||||
input: parsedContent.functionCall.args
|
||||
id: parsedContent.toolCall.id,
|
||||
name: parsedContent.toolCall.name,
|
||||
input: parsedContent.toolCall.args
|
||||
});
|
||||
} else {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: this.convertFunctionCallToText(parsedContent)
|
||||
text: this.convertToolCallToText(parsedContent)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { isTextFile } from "Enums/FileType";
|
|||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
import { ApiError, ApiErrorType } from "Types/ApiError";
|
||||
import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import type { GeminiRetryInfo, GeminiErrorResponse } from "./GeminiTypes";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ export class Gemini extends BaseAIClass {
|
|||
const data = JSON.parse(chunk) as { candidates?: Candidate[] };
|
||||
|
||||
let text = "";
|
||||
let functionCall: AIToolCall | undefined = undefined;
|
||||
let toolCall: AIToolCall | undefined = undefined;
|
||||
const candidate = data.candidates?.[0];
|
||||
|
||||
if (candidate) {
|
||||
|
|
@ -199,7 +199,7 @@ export class Gemini extends BaseAIClass {
|
|||
|
||||
// If streaming is complete and we have accumulated a function call, return it
|
||||
if (isComplete && this.accumulatedFunctionName) {
|
||||
functionCall = new AIToolCall(
|
||||
toolCall = new AIToolCall(
|
||||
aiToolFromString(this.accumulatedFunctionName),
|
||||
this.accumulatedFunctionArgs as Record<string, object>,
|
||||
undefined, // toolId not used by Gemini
|
||||
|
|
@ -210,7 +210,7 @@ export class Gemini extends BaseAIClass {
|
|||
return {
|
||||
content: text,
|
||||
isComplete: isComplete,
|
||||
functionCall: functionCall,
|
||||
toolCall: toolCall,
|
||||
shouldContinue: shouldContinue,
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
@ -231,25 +231,25 @@ export class Gemini extends BaseAIClass {
|
|||
}
|
||||
|
||||
// Add function call if present
|
||||
if (content.functionCall) {
|
||||
const parsedContent = parseFunctionCall(content.functionCall);
|
||||
if (content.toolCall) {
|
||||
const parsedContent = parseToolCall(content.toolCall);
|
||||
|
||||
if (parsedContent) {
|
||||
// Check if this is a cross-provider function call (has toolId in the stored format)
|
||||
// Gemini never uses toolId, so presence of toolId indicates Claude/OpenAI origin
|
||||
const isCrossProvider = parsedContent.functionCall.id && parsedContent.functionCall.id.trim() !== "";
|
||||
const isCrossProvider = parsedContent.toolCall.id && parsedContent.toolCall.id.trim() !== "";
|
||||
|
||||
if (isCrossProvider) {
|
||||
// Cross-provider function call (from Claude/OpenAI) - use legacy text format
|
||||
parts.push({
|
||||
text: this.convertFunctionCallToText(parsedContent)
|
||||
text: this.convertToolCallToText(parsedContent)
|
||||
});
|
||||
} else {
|
||||
// Native Gemini function call - use proper function call format
|
||||
const part: Part = {
|
||||
functionCall: {
|
||||
name: parsedContent.functionCall.name,
|
||||
args: parsedContent.functionCall.args
|
||||
name: parsedContent.toolCall.name,
|
||||
args: parsedContent.toolCall.args
|
||||
}
|
||||
};
|
||||
// Include thoughtSignature if present (optional Gemini feature)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { ApiError, ApiErrorType } from "Types/ApiError";
|
|||
import { MimeType, toMimeType } from "Enums/MimeType";
|
||||
import { isTextFile } from "Enums/FileType";
|
||||
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
|
||||
import { parseFunctionCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
|
||||
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
|
||||
|
||||
export class OpenAI extends BaseAIClass {
|
||||
|
|
@ -78,7 +78,7 @@ export class OpenAI extends BaseAIClass {
|
|||
const event = JSON.parse(chunk) as ResponseEvent;
|
||||
|
||||
let text = "";
|
||||
let functionCall: AIToolCall | undefined = undefined;
|
||||
let toolCall: AIToolCall | undefined = undefined;
|
||||
let isComplete = false;
|
||||
let shouldContinue = false;
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ export class OpenAI extends BaseAIClass {
|
|||
itemDoneEvent.item.arguments) {
|
||||
try {
|
||||
const args = JSON.parse(itemDoneEvent.item.arguments) as Record<string, unknown>;
|
||||
functionCall = new AIToolCall(
|
||||
toolCall = new AIToolCall(
|
||||
aiToolFromString(itemDoneEvent.item.name),
|
||||
args as Record<string, object>,
|
||||
itemDoneEvent.item.call_id || itemDoneEvent.item_id,
|
||||
|
|
@ -185,7 +185,7 @@ export class OpenAI extends BaseAIClass {
|
|||
return {
|
||||
content: text,
|
||||
isComplete: isComplete,
|
||||
functionCall: functionCall,
|
||||
toolCall: toolCall,
|
||||
shouldContinue: shouldContinue,
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
@ -200,12 +200,12 @@ export class OpenAI extends BaseAIClass {
|
|||
const contentToExtract = content.content ?? "";
|
||||
|
||||
// Case 1: Assistant message with function call
|
||||
if (content.functionCall) {
|
||||
const parsedContent = parseFunctionCall(content.functionCall);
|
||||
if (content.toolCall) {
|
||||
const parsedContent = parseToolCall(content.toolCall);
|
||||
|
||||
if (parsedContent) {
|
||||
// Check if function call has required id field (for OpenAI Responses API)
|
||||
if (parsedContent.functionCall.id && parsedContent.functionCall.id.trim() !== "") {
|
||||
if (parsedContent.toolCall.id && parsedContent.toolCall.id.trim() !== "") {
|
||||
// Add assistant text message if present
|
||||
if (contentToExtract.trim() !== "") {
|
||||
results.push({
|
||||
|
|
@ -217,13 +217,13 @@ export class OpenAI extends BaseAIClass {
|
|||
// Add function call as separate input item
|
||||
results.push({
|
||||
type: "function_call",
|
||||
call_id: parsedContent.functionCall.id,
|
||||
name: parsedContent.functionCall.name,
|
||||
arguments: JSON.stringify(parsedContent.functionCall.args)
|
||||
call_id: parsedContent.toolCall.id,
|
||||
name: parsedContent.toolCall.name,
|
||||
arguments: JSON.stringify(parsedContent.toolCall.args)
|
||||
});
|
||||
} else {
|
||||
// No id (from other provider or legacy) - convert to text message
|
||||
const legacyText = this.convertFunctionCallToText(parsedContent);
|
||||
const legacyText = this.convertToolCallToText(parsedContent);
|
||||
const messageContent = contentToExtract.trim();
|
||||
const combinedContent = messageContent !== ""
|
||||
? `${messageContent}\n\n${legacyText}`
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface ResponseOutputTextDelta extends ResponseEvent {
|
|||
delta: string;
|
||||
}
|
||||
|
||||
export interface ResponseFunctionCallArgumentsDone extends ResponseEvent {
|
||||
export interface ResponseToolCallArgumentsDone extends ResponseEvent {
|
||||
type: "response.function_call_arguments.done";
|
||||
item_id: string;
|
||||
name: string;
|
||||
|
|
@ -92,7 +92,7 @@ export interface ResponsesAPIMessageInput {
|
|||
}
|
||||
|
||||
// Function call item (reconstructed from storage or appended from response.output)
|
||||
export interface ResponsesAPIFunctionCallInput {
|
||||
export interface ResponsesAPIToolCallInput {
|
||||
type: "function_call";
|
||||
call_id: string;
|
||||
name: string;
|
||||
|
|
@ -100,7 +100,7 @@ export interface ResponsesAPIFunctionCallInput {
|
|||
}
|
||||
|
||||
// Function call output (result of executing a function)
|
||||
export interface ResponsesAPIFunctionCallOutputInput {
|
||||
export interface ResponsesAPIToolCallOutputInput {
|
||||
type: "function_call_output";
|
||||
call_id: string;
|
||||
output: string; // JSON string
|
||||
|
|
@ -109,8 +109,8 @@ export interface ResponsesAPIFunctionCallOutputInput {
|
|||
// Union type for all possible input items
|
||||
export type ResponsesAPIInput =
|
||||
| ResponsesAPIMessageInput
|
||||
| ResponsesAPIFunctionCallInput
|
||||
| ResponsesAPIFunctionCallOutputInput;
|
||||
| ResponsesAPIToolCallInput
|
||||
| ResponsesAPIToolCallOutputInput;
|
||||
|
||||
/**
|
||||
* File API Types
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export type JSONSchemaProperty = {
|
|||
|
||||
//Stored function call format used across all AI providers
|
||||
// This is the format saved to conversation history when a function is called
|
||||
export interface StoredFunctionCall {
|
||||
functionCall: {
|
||||
export interface StoredToolCall {
|
||||
toolCall: {
|
||||
id: string;
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type ConversationContentInit = {
|
|||
timestamp?: Date;
|
||||
content?: string;
|
||||
displayContent?: string;
|
||||
functionCall?: string;
|
||||
toolCall?: string;
|
||||
functionResponse?: string;
|
||||
attachments?: Attachment[];
|
||||
references?: Reference[];
|
||||
|
|
@ -24,7 +24,7 @@ export class ConversationContent {
|
|||
public timestamp: Date;
|
||||
public content: string | undefined;
|
||||
public displayContent: string | undefined;
|
||||
public functionCall: string | undefined;
|
||||
public toolCall: string | undefined;
|
||||
public functionResponse: string | undefined;
|
||||
public attachments: Attachment[];
|
||||
public references: Reference[];
|
||||
|
|
@ -41,7 +41,7 @@ export class ConversationContent {
|
|||
* @param init.timestamp - Timestamp of the message (defaults to now)
|
||||
* @param init.content - The content to be displayed and/or sent to the AI provider
|
||||
* @param init.displayContent - Display content is used when content needs to be formatted differently when displayed versus as a prompt
|
||||
* @param init.functionCall - JSON string of the function call data (only set for function/tool calls)
|
||||
* @param init.toolCall - JSON string of the function call data (only set for function/tool calls)
|
||||
* @param init.functionResponse - JSON string of the function call response data (only set for function/tool responses)
|
||||
* @param init.attachments - Array of file attachments associated with this message (defaults to empty array)
|
||||
* @param init.references - Array of file references, used to display attachment's to the user associated with attachments
|
||||
|
|
@ -55,7 +55,7 @@ export class ConversationContent {
|
|||
this.timestamp = init.timestamp ?? new Date();
|
||||
this.content = init.content;
|
||||
this.displayContent = init.displayContent;
|
||||
this.functionCall = init.functionCall;
|
||||
this.toolCall = init.toolCall;
|
||||
this.functionResponse = init.functionResponse;
|
||||
this.attachments = init.attachments ?? [];
|
||||
this.references = init.references ?? [];
|
||||
|
|
@ -77,7 +77,7 @@ export class ConversationContent {
|
|||
timestamp: string;
|
||||
content?: string;
|
||||
displayContent?: string;
|
||||
functionCall?: string;
|
||||
toolCall?: string;
|
||||
functionResponse?: string;
|
||||
attachments?: unknown[];
|
||||
references?: unknown[];
|
||||
|
|
@ -96,7 +96,7 @@ export class ConversationContent {
|
|||
|
||||
(!("content" in data) || typeof data.content === "string") &&
|
||||
(!("displayContent" in data) || typeof data.displayContent === "string") &&
|
||||
(!("functionCall" in data) || typeof data.functionCall === "string") &&
|
||||
(!("toolCall" in data) || typeof data.toolCall === "string") &&
|
||||
(!("functionResponse" in data) || typeof data.functionResponse === "string") &&
|
||||
(!("attachments" in data) || Array.isArray(data.attachments)) &&
|
||||
(!("references" in data) || Array.isArray(data.references)) &&
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { AIToolCall } from "AIClasses/AIToolCall";
|
||||
import type { StoredFunctionCall, StoredFunctionResponse } from "AIClasses/Schemas/AIToolTypes";
|
||||
import type { StoredToolCall, StoredFunctionResponse } from "AIClasses/Schemas/AIToolTypes";
|
||||
import { StringTools } from "./StringTools";
|
||||
import { Exception } from "./Exception";
|
||||
|
||||
// handle the rare event where a function call is also included in content (gemini sometimes does this)
|
||||
export function sanitizeFunctionCallContent(content: string, functionCall: AIToolCall | null): string {
|
||||
export function sanitizeToolCallContent(content: string, toolCall: AIToolCall | null): string {
|
||||
// Early returns for simple cases
|
||||
if (!functionCall || !content.trim()) {
|
||||
if (!toolCall || !content.trim()) {
|
||||
return content;
|
||||
}
|
||||
|
||||
|
|
@ -15,14 +15,14 @@ export function sanitizeFunctionCallContent(content: string, functionCall: AIToo
|
|||
return content;
|
||||
}
|
||||
|
||||
const functionCallString = functionCall.toConversationString();
|
||||
const toolCallString = toolCall.toConversationString();
|
||||
let sanitized = content;
|
||||
|
||||
// Step 1: Remove markdown code blocks that might contain the function call
|
||||
// Pattern matches ```json\n...\n``` or ```\n...\n```
|
||||
sanitized = sanitized.replace(/```(?:json)?\s*\n?([\s\S]*?)\n?```/g, (match: string, codeContent: string) => {
|
||||
// If the code block contains our function call, remove it entirely
|
||||
if (codeContent.trim() === functionCallString.trim()) {
|
||||
if (codeContent.trim() === toolCallString.trim()) {
|
||||
return '';
|
||||
}
|
||||
// Otherwise keep the code block
|
||||
|
|
@ -30,12 +30,12 @@ export function sanitizeFunctionCallContent(content: string, functionCall: AIToo
|
|||
});
|
||||
|
||||
// Step 2: Remove exact JSON match (handles compact JSON)
|
||||
sanitized = sanitized.replace(functionCallString, '').trim();
|
||||
sanitized = sanitized.replace(toolCallString, '').trim();
|
||||
|
||||
// Step 3: Handle pretty-printed variations by normalizing both strings
|
||||
try {
|
||||
const functionCallObj: unknown = JSON.parse(functionCallString);
|
||||
const normalizedTarget = JSON.stringify(functionCallObj);
|
||||
const toolCallObj: unknown = JSON.parse(toolCallString);
|
||||
const normalizedTarget = JSON.stringify(toolCallObj);
|
||||
|
||||
// Find and remove any JSON that matches when normalized
|
||||
// This regex finds JSON objects/arrays in the text
|
||||
|
|
@ -62,13 +62,13 @@ export function sanitizeFunctionCallContent(content: string, functionCall: AIToo
|
|||
return sanitized;
|
||||
}
|
||||
|
||||
export function parseFunctionCall(functionCallJson: string): StoredFunctionCall | null {
|
||||
if (!StringTools.isValidJson(functionCallJson)) {
|
||||
Exception.log(`Invalid JSON in functionCall field:\n${functionCallJson}`);
|
||||
export function parseToolCall(toolCallJson: string): StoredToolCall | null {
|
||||
if (!StringTools.isValidJson(toolCallJson)) {
|
||||
Exception.log(`Invalid JSON in toolCall field:\n${toolCallJson}`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(functionCallJson) as StoredFunctionCall;
|
||||
return JSON.parse(toolCallJson) as StoredToolCall;
|
||||
} catch (error) {
|
||||
Exception.log(error);
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -29,96 +29,96 @@ export class AIToolService {
|
|||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
}
|
||||
|
||||
public async performAITool(functionCall: AIToolCall): Promise<AIToolResponse> {
|
||||
public async performAITool(toolCall: AIToolCall): Promise<AIToolResponse> {
|
||||
return await this.abortService.abortableOperation(async () => {
|
||||
switch (functionCall.name) {
|
||||
switch (toolCall.name) {
|
||||
case AITool.SearchVaultFiles: {
|
||||
const parseResult = SearchVaultFilesArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = SearchVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.SearchVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.searchVaultFiles(parseResult.data.search_terms), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.searchVaultFiles(parseResult.data.search_terms), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.ReadVaultFiles: {
|
||||
const parseResult = ReadVaultFilesArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = ReadVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.ReadVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.readVaultFiles(parseResult.data.file_paths), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.readVaultFiles(parseResult.data.file_paths), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.WriteVaultFile: {
|
||||
const parseResult = WriteVaultFileArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = WriteVaultFileArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.WriteVaultFile}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.writeVaultFile(parseResult.data.file_path, parseResult.data.content), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.writeVaultFile(parseResult.data.file_path, parseResult.data.content), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.PatchVaultFile: {
|
||||
const parseResult = PatchVaultFileArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = PatchVaultFileArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.PatchVaultFile}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.patchVaultFile(parseResult.data.file_path, parseResult.data.oldContent, parseResult.data.newContent), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.patchVaultFile(parseResult.data.file_path, parseResult.data.oldContent, parseResult.data.newContent), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.DeleteVaultFiles: {
|
||||
const parseResult = DeleteVaultFilesArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = DeleteVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.DeleteVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.deleteVaultFiles(parseResult.data.file_paths, parseResult.data.confirm_deletion), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.deleteVaultFiles(parseResult.data.file_paths, parseResult.data.confirm_deletion), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.MoveVaultFiles: {
|
||||
const parseResult = MoveVaultFilesArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = MoveVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.MoveVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.moveVaultFiles(parseResult.data.source_paths, parseResult.data.destination_paths), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.moveVaultFiles(parseResult.data.source_paths, parseResult.data.destination_paths), toolCall.toolId);
|
||||
}
|
||||
|
||||
case AITool.ListVaultFiles: {
|
||||
const parseResult = ListVaultFilesArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = ListVaultFilesArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
toolCall.name,
|
||||
{ error: `Invalid arguments for ${AITool.ListVaultFiles}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
return new AIToolResponse(functionCall.name, await this.ListVaultFiles(parseResult.data.path, parseResult.data.recursive), functionCall.toolId);
|
||||
return new AIToolResponse(toolCall.name, await this.ListVaultFiles(parseResult.data.path, parseResult.data.recursive), toolCall.toolId);
|
||||
}
|
||||
|
||||
// This is only used by gemini
|
||||
case AITool.RequestWebSearch:
|
||||
return new AIToolResponse(functionCall.name, {}, functionCall.toolId)
|
||||
return new AIToolResponse(toolCall.name, {}, toolCall.toolId)
|
||||
|
||||
// multi-agent functions are handled elsewhere - this shouldn't really ever get hit
|
||||
case AITool.ExecuteWorkflow:
|
||||
|
|
@ -131,23 +131,23 @@ export class AIToolService {
|
|||
case AITool.CompleteStep:
|
||||
case AITool.CompletePlan:
|
||||
case AITool.CancelPlan: {
|
||||
Exception.log(`Multi-agent function ${functionCall.name} should not be handled by AIToolService`);
|
||||
Exception.log(`Multi-agent function ${toolCall.name} should not be handled by AIToolService`);
|
||||
return new AIToolResponse(
|
||||
functionCall.name,
|
||||
{ error: `Failed to execute ${functionCall.name}.` },
|
||||
functionCall.toolId
|
||||
toolCall.name,
|
||||
{ error: `Failed to execute ${toolCall.name}.` },
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
|
||||
case AITool.Unknown:
|
||||
default: {
|
||||
const functionCallName = fromString(functionCall.name);
|
||||
const error = `Unknown function request ${functionCallName}`
|
||||
const toolCallName = fromString(toolCall.name);
|
||||
const error = `Unknown function request ${toolCallName}`
|
||||
Exception.log(error);
|
||||
return new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: error },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ConversationContent } from "Conversations/ConversationContent";
|
|||
import { AgentType } from "Enums/AgentType";
|
||||
import { Copy } from "Enums/Copy";
|
||||
import { Role } from "Enums/Role";
|
||||
import { sanitizeFunctionCallContent } from "Helpers/ResponseHelper";
|
||||
import { sanitizeToolCallContent } from "Helpers/ResponseHelper";
|
||||
import type { IChatServiceCallbacks } from "Services/ChatService";
|
||||
import { Resolve, TryResolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
|
|
@ -41,17 +41,17 @@ export abstract class BaseAgent {
|
|||
}
|
||||
|
||||
protected async runAgentLoop(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks,
|
||||
handleFunctionCall: (functionCall: AIToolCall) => Promise<{ shouldExit: boolean }>
|
||||
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.functionCall || response.shouldContinue) {
|
||||
if (response.functionCall) {
|
||||
this.debugService?.log("FunctionCall", `${agentType} received function call: ${response.functionCall.name}`);
|
||||
const result = await handleFunctionCall(response.functionCall);
|
||||
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);
|
||||
|
|
@ -86,8 +86,8 @@ export abstract class BaseAgent {
|
|||
});
|
||||
}
|
||||
|
||||
protected updateThought(functionCall: AIToolCall | null, callbacks: IChatServiceCallbacks) {
|
||||
const userMessage = functionCall?.arguments.user_message;
|
||||
protected updateThought(toolCall: AIToolCall | null, callbacks: IChatServiceCallbacks) {
|
||||
const userMessage = toolCall?.arguments.user_message;
|
||||
if (userMessage && typeof userMessage === "string") {
|
||||
callbacks.onThoughtUpdate(userMessage);
|
||||
}
|
||||
|
|
@ -117,16 +117,16 @@ export abstract class BaseAgent {
|
|||
}
|
||||
|
||||
private async streamRequestResponse(agentType: AgentType, conversation: Conversation, callbacks: IChatServiceCallbacks
|
||||
): Promise<{ functionCall: AIToolCall | null, shouldContinue: boolean }> {
|
||||
): Promise<{ toolCall: AIToolCall | null, shouldContinue: boolean }> {
|
||||
if (!this.ai) { // this should never happen
|
||||
return { functionCall: null, shouldContinue: false };
|
||||
return { toolCall: null, shouldContinue: false };
|
||||
}
|
||||
|
||||
const conversationContent = new ConversationContent({ role: Role.Assistant });
|
||||
conversation.contents.push(conversationContent);
|
||||
|
||||
let accumulatedContent = "";
|
||||
let capturedFunctionCall: AIToolCall | null = null;
|
||||
let capturedToolCall: AIToolCall | null = null;
|
||||
let capturedShouldContinue = false;
|
||||
|
||||
for await (const chunk of this.ai.streamRequest(conversation)) {
|
||||
|
|
@ -138,9 +138,9 @@ export abstract class BaseAgent {
|
|||
break;
|
||||
}
|
||||
|
||||
if (chunk.functionCall) {
|
||||
this.debugService?.log("FunctionCall", `Function call captured: ${chunk.functionCall.name}`);
|
||||
capturedFunctionCall = chunk.functionCall;
|
||||
if (chunk.toolCall) {
|
||||
this.debugService?.log("ToolCall", `Function call captured: ${chunk.toolCall.name}`);
|
||||
capturedToolCall = chunk.toolCall;
|
||||
}
|
||||
|
||||
if (chunk.shouldContinue) {
|
||||
|
|
@ -157,18 +157,18 @@ export abstract class BaseAgent {
|
|||
}
|
||||
|
||||
if (chunk.isComplete) {
|
||||
const sanitizedContent = sanitizeFunctionCallContent(accumulatedContent, capturedFunctionCall);
|
||||
const sanitizedContent = sanitizeToolCallContent(accumulatedContent, capturedToolCall);
|
||||
|
||||
if (sanitizedContent.trim() === "" && !capturedFunctionCall) {
|
||||
if (sanitizedContent.trim() === "" && !capturedToolCall) {
|
||||
conversation.contents.pop();
|
||||
} else {
|
||||
conversationContent.content = sanitizedContent;
|
||||
if (capturedFunctionCall) {
|
||||
conversationContent.functionCall = capturedFunctionCall.toConversationString();
|
||||
conversationContent.toolId = capturedFunctionCall.toolId;
|
||||
if (capturedToolCall) {
|
||||
conversationContent.toolCall = capturedToolCall.toConversationString();
|
||||
conversationContent.toolId = capturedToolCall.toolId;
|
||||
conversationContent.shouldDisplayContent = sanitizedContent.trim() !== "";
|
||||
if (capturedFunctionCall.thoughtSignature) {
|
||||
conversationContent.thoughtSignature = capturedFunctionCall.thoughtSignature;
|
||||
if (capturedToolCall.thoughtSignature) {
|
||||
conversationContent.thoughtSignature = capturedToolCall.thoughtSignature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@ export abstract class BaseAgent {
|
|||
|
||||
callbacks.onStreamingUpdate(null);
|
||||
|
||||
return { functionCall: capturedFunctionCall, shouldContinue: capturedShouldContinue };
|
||||
return { toolCall: capturedToolCall, shouldContinue: capturedShouldContinue };
|
||||
}
|
||||
|
||||
private async withToolCallingDisabled<T>(callback: () => Promise<T>): Promise<T> {
|
||||
|
|
|
|||
|
|
@ -45,28 +45,28 @@ export class ExecutionAgent extends BaseAgent {
|
|||
|
||||
let executionResult: CompleteTaskArgs | undefined = undefined;
|
||||
|
||||
await this.runAgentLoop(AgentType.Execution, this.conversation, callbacks, async functionCall => {
|
||||
const functionCallName = functionCall.name;
|
||||
await this.runAgentLoop(AgentType.Execution, this.conversation, callbacks, async toolCall => {
|
||||
const toolCallName = toolCall.name;
|
||||
|
||||
if (isAITool(functionCallName, AITool.CompleteTask)) {
|
||||
const parseResult = CompleteTaskArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.CompleteTask)) {
|
||||
const parseResult = CompleteTaskArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
this.conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CompleteTask}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
this.debugService?.log("ExecutionAgent", `Task completed (success: ${parseResult.data.success}): ${parseResult.data.description}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
executionResult = parseResult.data;
|
||||
return { shouldExit: true };
|
||||
}
|
||||
|
||||
this.debugService?.log("ExecutionAgent", `Executing function: ${functionCall.name}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
const functionResponse = await this.aiToolService.performAITool(functionCall);
|
||||
this.debugService?.log("ExecutionAgent", `Executing function: ${toolCall.name}`);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
const functionResponse = await this.aiToolService.performAITool(toolCall);
|
||||
this.conversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export class MainAgent extends BaseAgent {
|
|||
|
||||
let result = await this.runMainAgentLoop(conversation, callbacks);
|
||||
|
||||
while (result.planRequest && result.functionCall) {
|
||||
while (result.planRequest && result.toolCall) {
|
||||
this.debugService?.log("MainAgent", "Spawning OrchestrationAgent for planned workflow");
|
||||
const orchestrationAgent = new OrchestrationAgent();
|
||||
orchestrationAgent.resolveAIProvider();
|
||||
|
|
@ -39,9 +39,9 @@ export class MainAgent extends BaseAgent {
|
|||
this.debugService?.log("MainAgent", "OrchestrationAgent workflow completed");
|
||||
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
result.functionCall.name,
|
||||
result.toolCall.name,
|
||||
workflowResult,
|
||||
result.functionCall.toolId
|
||||
result.toolCall.toolId
|
||||
));
|
||||
|
||||
await this.setAgentPromptAndTools(planningMode, allowDestructiveActions);
|
||||
|
|
@ -51,37 +51,37 @@ export class MainAgent extends BaseAgent {
|
|||
|
||||
// the main agent loop - may return an execution plan if the agent has requested a planned workflow
|
||||
private async runMainAgentLoop(conversation: Conversation, callbacks: IChatServiceCallbacks
|
||||
): Promise<{ planRequest: ExecuteWorkflowArgs | undefined, functionCall: AIToolCall | undefined }> {
|
||||
): Promise<{ planRequest: ExecuteWorkflowArgs | undefined, toolCall: AIToolCall | undefined }> {
|
||||
|
||||
let planRequest: ExecuteWorkflowArgs | undefined;
|
||||
let planFunctionCall: AIToolCall | undefined;
|
||||
let planToolCall: AIToolCall | undefined;
|
||||
|
||||
await this.runAgentLoop(AgentType.Main, conversation, callbacks, async functionCall => {
|
||||
const functionCallName = functionCall.name;
|
||||
if (isAITool(functionCallName, AITool.ExecuteWorkflow)) {
|
||||
await this.runAgentLoop(AgentType.Main, conversation, callbacks, async toolCall => {
|
||||
const toolCallName = toolCall.name;
|
||||
if (isAITool(toolCallName, AITool.ExecuteWorkflow)) {
|
||||
this.debugService?.log("MainAgent", "ExecuteWorkflow function detected - transitioning to orchestration");
|
||||
const parseResult = ExecuteWorkflowArgsSchema.safeParse(functionCall.arguments);
|
||||
const parseResult = ExecuteWorkflowArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.ExecuteWorkflow}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
planRequest = parseResult.data;
|
||||
planFunctionCall = functionCall;
|
||||
this.updateThought(functionCall, callbacks);
|
||||
planToolCall = toolCall;
|
||||
this.updateThought(toolCall, callbacks);
|
||||
return { shouldExit: true };
|
||||
}
|
||||
|
||||
this.debugService?.log("MainAgent", `Executing function: ${functionCall.name}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
const functionResponse = await this.aiToolService.performAITool(functionCall);
|
||||
this.debugService?.log("MainAgent", `Executing function: ${toolCall.name}`);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
const functionResponse = await this.aiToolService.performAITool(toolCall);
|
||||
conversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
});
|
||||
return { planRequest: planRequest, functionCall: planFunctionCall };
|
||||
return { planRequest: planRequest, toolCall: planToolCall };
|
||||
}
|
||||
|
||||
private async setAgentPromptAndTools(planningMode: boolean, allowDestructiveActions: boolean): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -139,114 +139,114 @@ export class OrchestrationAgent extends BaseAgent {
|
|||
|
||||
let orchestrationResult: OrchestrationResult | undefined = undefined;
|
||||
|
||||
await this.runAgentLoop(AgentType.Orchestration, planningConversation, callbacks, async functionCall => {
|
||||
const functionCallName = functionCall.name;
|
||||
await this.runAgentLoop(AgentType.Orchestration, planningConversation, callbacks, async toolCall => {
|
||||
const toolCallName = toolCall.name;
|
||||
|
||||
if (!AIToolDefinitions.orchestrationAgentDefinitions().some(definition => isAITool(functionCallName, definition.name))) {
|
||||
this.debugService?.log("Orchestration", `Invalid tool call denied: ${functionCallName}`);
|
||||
if (!AIToolDefinitions.orchestrationAgentDefinitions().some(definition => isAITool(toolCallName, definition.name))) {
|
||||
this.debugService?.log("Orchestration", `Invalid tool call denied: ${toolCallName}`);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: Copy.OrchestrationToolDenial },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
|
||||
if (isAITool(functionCallName, AITool.CompleteStep)) {
|
||||
const parseResult = CompleteStepArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.CompleteStep)) {
|
||||
const parseResult = CompleteStepArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CompleteStep}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
if (!parseResult.data.confirm_completion) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: "Confirmation was false, no action taken" },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `CompleteStep called (confirmed: ${parseResult.data.confirm_completion})`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: "Step Completed" },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ continue: true, continueContext: parseResult.data.context_for_next_step });
|
||||
return Promise.resolve({ shouldExit: true });
|
||||
}
|
||||
|
||||
if (isAITool(functionCallName, AITool.CompletePlan)) {
|
||||
const parseResult = CompletePlanArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.CompletePlan)) {
|
||||
const parseResult = CompletePlanArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CompletePlan}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
if (!parseResult.data.confirm_completion) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: "Confirmation was false, no action taken" },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `CompletePlan called (confirmed: ${parseResult.data.confirm_completion})`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: "Plan Completed" },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ complete: true });
|
||||
return Promise.resolve({ shouldExit: true });
|
||||
}
|
||||
|
||||
if (isAITool(functionCallName, AITool.Replan)) {
|
||||
const parseResult = ReplanArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.Replan)) {
|
||||
const parseResult = ReplanArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.Replan}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `Replan requested: ${parseResult.data.context}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: "Replan Requested" },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ replan: true, replanContext: parseResult.data.context });
|
||||
return Promise.resolve({ shouldExit: true });
|
||||
}
|
||||
|
||||
if (isAITool(functionCallName, AITool.CancelPlan)) {
|
||||
const parseResult = CancelPlanArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.CancelPlan)) {
|
||||
const parseResult = CancelPlanArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.CancelPlan}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return Promise.resolve({ shouldExit: false });
|
||||
}
|
||||
this.debugService?.log("Orchestration", `Plan cancellation requested: ${parseResult.data.context}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
planningConversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: "Plan Cancelled" },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
orchestrationResult = new OrchestrationResult({ abort: true, abortContext: parseResult.data.context });
|
||||
return Promise.resolve({ shouldExit: true });
|
||||
|
|
|
|||
|
|
@ -32,63 +32,63 @@ export class PlanningAgent extends BaseAgent {
|
|||
this.planningDepth++;
|
||||
this.debugService?.log("PlanningAgent", `Starting PlanningAgent (isReplan: ${isReplan}, depth: ${this.planningDepth}/${PlanningAgent.MAX_AGENT_DEPTH})`);
|
||||
|
||||
await this.runAgentLoop(AgentType.Planning, conversation, callbacks, async (functionCall) => {
|
||||
const functionCallName = functionCall.name;
|
||||
await this.runAgentLoop(AgentType.Planning, conversation, callbacks, async (toolCall) => {
|
||||
const toolCallName = toolCall.name;
|
||||
|
||||
if (!AIToolDefinitions.planningAgentDefinitions().some(definition => isAITool(functionCallName, definition.name))) {
|
||||
this.debugService?.log("PlanningAgent", `Invalid tool call denied: ${functionCallName}`);
|
||||
if (!AIToolDefinitions.planningAgentDefinitions().some(definition => isAITool(toolCallName, definition.name))) {
|
||||
this.debugService?.log("PlanningAgent", `Invalid tool call denied: ${toolCallName}`);
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: Copy.PlanningToolDenial },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
|
||||
if (isAITool(functionCallName, AITool.AskUserQuestionPlanning)) {
|
||||
const parseResult = AskUserQuestionPlanningArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.AskUserQuestionPlanning)) {
|
||||
const parseResult = AskUserQuestionPlanningArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.AskUserQuestionPlanning}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
this.debugService?.log("PlanningAgent", `Asking user question: ${parseResult.data.question}`);
|
||||
this.updateThought(functionCall, callbacks);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
const answer = await callbacks.onUserQuestion(parseResult.data.question);
|
||||
this.debugService?.log("PlanningAgent", "User answer received");
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ answer: answer },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
|
||||
if (isAITool(functionCallName, AITool.SubmitPlan)) {
|
||||
const parseResult = SubmitPlanArgsSchema.safeParse(functionCall.arguments);
|
||||
if (isAITool(toolCallName, AITool.SubmitPlan)) {
|
||||
const parseResult = SubmitPlanArgsSchema.safeParse(toolCall.arguments);
|
||||
if (!parseResult.success) {
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ error: `Invalid arguments for ${AITool.SubmitPlan}: ${parseResult.error.message}` },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: false };
|
||||
}
|
||||
this.debugService?.log("PlanningAgent", `Plan submitted successfully with ${parseResult.data.steps.length} steps`);
|
||||
capturedPlan = new ExecutionPlan(parseResult.data, isReplan);
|
||||
conversation.addFunctionResponse(new AIToolResponse(
|
||||
functionCallName,
|
||||
toolCallName,
|
||||
{ message: Copy.PlanReceived },
|
||||
functionCall.toolId
|
||||
toolCall.toolId
|
||||
));
|
||||
return { shouldExit: true };
|
||||
}
|
||||
|
||||
this.updateThought(functionCall, callbacks);
|
||||
const functionResponse = await this.aiToolService.performAITool(functionCall);
|
||||
this.updateThought(toolCall, callbacks);
|
||||
const functionResponse = await this.aiToolService.performAITool(toolCall);
|
||||
conversation.addFunctionResponse(functionResponse);
|
||||
return { shouldExit: false };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class ConversationFileSystemService {
|
|||
timestamp: content.timestamp.toISOString(),
|
||||
content: content.content,
|
||||
displayContent: content.displayContent,
|
||||
functionCall: content.functionCall,
|
||||
toolCall: content.toolCall,
|
||||
functionResponse: content.functionResponse,
|
||||
attachments: content.attachments.map(att => ({
|
||||
fileName: att.fileName,
|
||||
|
|
@ -278,7 +278,7 @@ export class ConversationFileSystemService {
|
|||
timestamp: new Date(content.timestamp),
|
||||
content: content.content,
|
||||
displayContent: content.displayContent,
|
||||
functionCall: content.functionCall,
|
||||
toolCall: content.toolCall,
|
||||
functionResponse: content.functionResponse,
|
||||
attachments: attachments,
|
||||
references: references,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface IStreamChunk {
|
|||
isComplete: boolean;
|
||||
error?: string;
|
||||
errorType?: ApiErrorType;
|
||||
functionCall?: AIToolCall;
|
||||
toolCall?: AIToolCall;
|
||||
shouldContinue?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,68 +5,68 @@ import { AITool } from '../../Enums/AITool';
|
|||
describe('AIToolCall', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create instance with name and arguments only', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' }
|
||||
);
|
||||
|
||||
expect(functionCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(functionCall.arguments).toEqual({ query: 'test' });
|
||||
expect(functionCall.toolId).toBeUndefined();
|
||||
expect(functionCall.thoughtSignature).toBeUndefined();
|
||||
expect(toolCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(toolCall.arguments).toEqual({ query: 'test' });
|
||||
expect(toolCall.toolId).toBeUndefined();
|
||||
expect(toolCall.thoughtSignature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create instance with name, arguments, and toolId', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.ReadVaultFiles,
|
||||
{ path: 'test.md' },
|
||||
'tool-123'
|
||||
);
|
||||
|
||||
expect(functionCall.name).toBe(AITool.ReadVaultFiles);
|
||||
expect(functionCall.arguments).toEqual({ path: 'test.md' });
|
||||
expect(functionCall.toolId).toBe('tool-123');
|
||||
expect(functionCall.thoughtSignature).toBeUndefined();
|
||||
expect(toolCall.name).toBe(AITool.ReadVaultFiles);
|
||||
expect(toolCall.arguments).toEqual({ path: 'test.md' });
|
||||
expect(toolCall.toolId).toBe('tool-123');
|
||||
expect(toolCall.thoughtSignature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create instance with all four parameters including thoughtSignature', () => {
|
||||
const signature = 'base64EncodedSignature==';
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'notes' },
|
||||
undefined,
|
||||
signature
|
||||
);
|
||||
|
||||
expect(functionCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(functionCall.arguments).toEqual({ query: 'notes' });
|
||||
expect(functionCall.toolId).toBeUndefined();
|
||||
expect(functionCall.thoughtSignature).toBe(signature);
|
||||
expect(toolCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(toolCall.arguments).toEqual({ query: 'notes' });
|
||||
expect(toolCall.toolId).toBeUndefined();
|
||||
expect(toolCall.thoughtSignature).toBe(signature);
|
||||
});
|
||||
|
||||
it('should create instance with toolId and thoughtSignature', () => {
|
||||
const signature = 'aGVsbG8gd29ybGQ=';
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.WriteVaultFile,
|
||||
{ path: 'note.md', content: 'Hello' },
|
||||
'tool-456',
|
||||
signature
|
||||
);
|
||||
|
||||
expect(functionCall.name).toBe(AITool.WriteVaultFile);
|
||||
expect(functionCall.arguments).toEqual({ path: 'note.md', content: 'Hello' });
|
||||
expect(functionCall.toolId).toBe('tool-456');
|
||||
expect(functionCall.thoughtSignature).toBe(signature);
|
||||
expect(toolCall.name).toBe(AITool.WriteVaultFile);
|
||||
expect(toolCall.arguments).toEqual({ path: 'note.md', content: 'Hello' });
|
||||
expect(toolCall.toolId).toBe('tool-456');
|
||||
expect(toolCall.thoughtSignature).toBe(signature);
|
||||
});
|
||||
|
||||
it('should handle empty arguments object', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{}
|
||||
);
|
||||
|
||||
expect(functionCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(functionCall.arguments).toEqual({});
|
||||
expect(toolCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(toolCall.arguments).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle complex nested arguments', () => {
|
||||
|
|
@ -78,27 +78,27 @@ describe('AIToolCall', () => {
|
|||
limit: 10
|
||||
};
|
||||
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
complexArgs
|
||||
);
|
||||
|
||||
expect(functionCall.arguments).toEqual(complexArgs);
|
||||
expect(toolCall.arguments).toEqual(complexArgs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toConversationString', () => {
|
||||
it('should serialize to JSON with only name and args when no optional fields', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' }
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: AITool.SearchVaultFiles,
|
||||
args: { query: 'test' },
|
||||
id: undefined,
|
||||
|
|
@ -108,17 +108,17 @@ describe('AIToolCall', () => {
|
|||
});
|
||||
|
||||
it('should serialize with toolId when present', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.ReadVaultFiles,
|
||||
{ path: 'note.md' },
|
||||
'tool-789'
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: AITool.ReadVaultFiles,
|
||||
args: { path: 'note.md' },
|
||||
id: 'tool-789',
|
||||
|
|
@ -129,18 +129,18 @@ describe('AIToolCall', () => {
|
|||
|
||||
it('should serialize with thoughtSignature when present', () => {
|
||||
const signature = 'dGVzdFNpZ25hdHVyZQ==';
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'notes' },
|
||||
undefined,
|
||||
signature
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: AITool.SearchVaultFiles,
|
||||
args: { query: 'notes' },
|
||||
id: undefined,
|
||||
|
|
@ -151,18 +151,18 @@ describe('AIToolCall', () => {
|
|||
|
||||
it('should serialize with both toolId and thoughtSignature when present', () => {
|
||||
const signature = 'YW5vdGhlclNpZ25hdHVyZQ==';
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.WriteVaultFile,
|
||||
{ path: 'file.md', content: 'content' },
|
||||
'tool-999',
|
||||
signature
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed).toEqual({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: AITool.WriteVaultFile,
|
||||
args: { path: 'file.md', content: 'content' },
|
||||
id: 'tool-999',
|
||||
|
|
@ -172,136 +172,136 @@ describe('AIToolCall', () => {
|
|||
});
|
||||
|
||||
it('should produce valid JSON string', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
'tool-123',
|
||||
'c2lnbmF0dXJl'
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
|
||||
expect(() => JSON.parse(serialized)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle special characters in arguments', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test "quotes" and \'apostrophes\' and\nnewlines' }
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed.functionCall.args.query).toBe('test "quotes" and \'apostrophes\' and\nnewlines');
|
||||
expect(parsed.toolCall.args.query).toBe('test "quotes" and \'apostrophes\' and\nnewlines');
|
||||
});
|
||||
|
||||
it('should handle empty string thoughtSignature', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
undefined,
|
||||
''
|
||||
);
|
||||
|
||||
const serialized = functionCall.toConversationString();
|
||||
const serialized = toolCall.toConversationString();
|
||||
const parsed = JSON.parse(serialized);
|
||||
|
||||
expect(parsed.functionCall.thoughtSignature).toBe('');
|
||||
expect(parsed.toolCall.thoughtSignature).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('properties', () => {
|
||||
it('should have immutable name property', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' }
|
||||
);
|
||||
|
||||
// Readonly is enforced at TypeScript compile time
|
||||
// At runtime, the properties are accessible
|
||||
expect(functionCall.name).toBe(AITool.SearchVaultFiles);
|
||||
expect(toolCall.name).toBe(AITool.SearchVaultFiles);
|
||||
});
|
||||
|
||||
it('should have immutable arguments property', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' }
|
||||
);
|
||||
|
||||
expect(functionCall.arguments).toEqual({ query: 'test' });
|
||||
expect(toolCall.arguments).toEqual({ query: 'test' });
|
||||
});
|
||||
|
||||
it('should have immutable toolId property', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
'tool-123'
|
||||
);
|
||||
|
||||
expect(functionCall.toolId).toBe('tool-123');
|
||||
expect(toolCall.toolId).toBe('tool-123');
|
||||
});
|
||||
|
||||
it('should have immutable thoughtSignature property', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
undefined,
|
||||
'signature'
|
||||
);
|
||||
|
||||
expect(functionCall.thoughtSignature).toBe('signature');
|
||||
expect(toolCall.thoughtSignature).toBe('signature');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle very long thoughtSignature (realistic base64)', () => {
|
||||
const longSignature = 'A'.repeat(10000);
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
undefined,
|
||||
longSignature
|
||||
);
|
||||
|
||||
expect(functionCall.thoughtSignature).toBe(longSignature);
|
||||
expect(functionCall.thoughtSignature).toHaveLength(10000);
|
||||
expect(toolCall.thoughtSignature).toBe(longSignature);
|
||||
expect(toolCall.thoughtSignature).toHaveLength(10000);
|
||||
});
|
||||
|
||||
it('should handle thoughtSignature with base64 special characters', () => {
|
||||
const base64Signature = 'SGVsbG8gV29ybGQ+Pz8/Pz8+Pg==';
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
undefined,
|
||||
base64Signature
|
||||
);
|
||||
|
||||
expect(functionCall.thoughtSignature).toBe(base64Signature);
|
||||
expect(toolCall.thoughtSignature).toBe(base64Signature);
|
||||
});
|
||||
|
||||
it('should handle undefined toolId and defined thoughtSignature', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
undefined,
|
||||
'signature'
|
||||
);
|
||||
|
||||
expect(functionCall.toolId).toBeUndefined();
|
||||
expect(functionCall.thoughtSignature).toBe('signature');
|
||||
expect(toolCall.toolId).toBeUndefined();
|
||||
expect(toolCall.thoughtSignature).toBe('signature');
|
||||
});
|
||||
|
||||
it('should handle defined toolId and undefined thoughtSignature', () => {
|
||||
const functionCall = new AIToolCall(
|
||||
const toolCall = new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ query: 'test' },
|
||||
'tool-123',
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(functionCall.toolId).toBe('tool-123');
|
||||
expect(functionCall.thoughtSignature).toBeUndefined();
|
||||
expect(toolCall.toolId).toBe('tool-123');
|
||||
expect(toolCall.thoughtSignature).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { RegisterSingleton, DeregisterAllServices } from '../../Services/Depende
|
|||
import { Services } from '../../Services/Services';
|
||||
import { AbortService } from '../../Services/AbortService';
|
||||
import { AIProvider } from '../../Enums/ApiProvider';
|
||||
import { parseFunctionCall, parseFunctionResponse } from '../../Helpers/ResponseHelper';
|
||||
import { parseToolCall, parseFunctionResponse } from '../../Helpers/ResponseHelper';
|
||||
|
||||
/**
|
||||
* BaseAIClass Shared Method Tests
|
||||
|
|
@ -81,62 +81,62 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
DeregisterAllServices();
|
||||
});
|
||||
|
||||
describe('parseFunctionCall', () => {
|
||||
describe('parseToolCall', () => {
|
||||
it('should parse Claude-style function call (with id)', () => {
|
||||
const claudeCall = JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
id: 'toolu_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(claudeCall);
|
||||
const result = parseToolCall(claudeCall);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.functionCall.id).toBe('toolu_123');
|
||||
expect(result!.functionCall.name).toBe('search_vault_files');
|
||||
expect(result!.functionCall.args).toEqual({ query: 'test' });
|
||||
expect(result!.toolCall.id).toBe('toolu_123');
|
||||
expect(result!.toolCall.name).toBe('search_vault_files');
|
||||
expect(result!.toolCall.args).toEqual({ query: 'test' });
|
||||
});
|
||||
|
||||
it('should parse OpenAI-style function call (with id)', () => {
|
||||
const openaiCall = JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
id: 'call_abc',
|
||||
name: 'read_file',
|
||||
args: { path: 'note.md' }
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(openaiCall);
|
||||
const result = parseToolCall(openaiCall);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.functionCall.id).toBe('call_abc');
|
||||
expect(result!.functionCall.name).toBe('read_file');
|
||||
expect(result!.functionCall.args).toEqual({ path: 'note.md' });
|
||||
expect(result!.toolCall.id).toBe('call_abc');
|
||||
expect(result!.toolCall.name).toBe('read_file');
|
||||
expect(result!.toolCall.args).toEqual({ path: 'note.md' });
|
||||
});
|
||||
|
||||
it('should parse Gemini-style function call (no id)', () => {
|
||||
const geminiCall = JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'gemini test' }
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(geminiCall);
|
||||
const result = parseToolCall(geminiCall);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.functionCall.name).toBe('search_vault_files');
|
||||
expect(result!.functionCall.args).toEqual({ query: 'gemini test' });
|
||||
expect(result!.toolCall.name).toBe('search_vault_files');
|
||||
expect(result!.toolCall.args).toEqual({ query: 'gemini test' });
|
||||
// ID may be undefined for Gemini
|
||||
expect(result!.functionCall.id).toBeUndefined();
|
||||
expect(result!.toolCall.id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle invalid JSON gracefully', () => {
|
||||
const invalidJson = 'not valid json {';
|
||||
|
||||
const result = parseFunctionCall(invalidJson);
|
||||
const result = parseToolCall(invalidJson);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
|
@ -144,37 +144,37 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
it('should handle missing required fields', () => {
|
||||
// Missing 'name' field
|
||||
const missingName = JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
id: 'test-id',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(missingName);
|
||||
const result = parseToolCall(missingName);
|
||||
|
||||
// Should still parse, but may have undefined name
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.functionCall.id).toBe('test-id');
|
||||
expect(result!.toolCall.id).toBe('test-id');
|
||||
});
|
||||
|
||||
it('should handle empty args object', () => {
|
||||
const emptyArgs = JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
id: 'test-id',
|
||||
name: 'list_files',
|
||||
args: {}
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(emptyArgs);
|
||||
const result = parseToolCall(emptyArgs);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.functionCall.args).toEqual({});
|
||||
expect(result!.toolCall.args).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle complex nested args', () => {
|
||||
const complexArgs = JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
id: 'test-id',
|
||||
name: 'search',
|
||||
args: {
|
||||
|
|
@ -187,11 +187,11 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(complexArgs);
|
||||
const result = parseToolCall(complexArgs);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect((result!.functionCall.args as any).filters.tags).toHaveLength(2);
|
||||
expect((result!.functionCall.args as any).options.limit).toBe(10);
|
||||
expect((result!.toolCall.args as any).filters.tags).toHaveLength(2);
|
||||
expect((result!.toolCall.args as any).options.limit).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -300,8 +300,8 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_orphan',
|
||||
name: 'search',
|
||||
args: {}
|
||||
|
|
@ -330,8 +330,8 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_recent',
|
||||
name: 'search',
|
||||
args: {}
|
||||
|
|
@ -345,7 +345,7 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
|
||||
// Most recent call should be included
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].functionCall).toBeDefined();
|
||||
expect(result[1].toolCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle function call with response correctly', () => {
|
||||
|
|
@ -357,8 +357,8 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_with_response',
|
||||
name: 'search',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -397,8 +397,8 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: { id: 'orphan1', name: 'search', args: {} }
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: { id: 'orphan1', name: 'search', args: {} }
|
||||
}),
|
||||
toolId: 'orphan1'
|
||||
}),
|
||||
|
|
@ -410,8 +410,8 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: { id: 'complete1', name: 'read', args: {} }
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: { id: 'complete1', name: 'read', args: {} }
|
||||
}),
|
||||
toolId: 'complete1'
|
||||
}),
|
||||
|
|
@ -438,7 +438,7 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
expect(result).toHaveLength(5);
|
||||
expect(result[0].content).toBe('Start');
|
||||
expect(result[1].content).toBe('Middle');
|
||||
expect(result[2].functionCall).toBeDefined();
|
||||
expect(result[2].toolCall).toBeDefined();
|
||||
expect(result[3].functionResponse).toBeDefined();
|
||||
expect(result[4].content).toBe('End');
|
||||
});
|
||||
|
|
@ -446,20 +446,20 @@ describe('BaseAIClass Shared Methods', () => {
|
|||
|
||||
describe('Cross-Provider Consistency', () => {
|
||||
it('should parse same function call JSON consistently across providers', () => {
|
||||
const sharedFunctionCall = JSON.stringify({
|
||||
functionCall: {
|
||||
const sharedToolCall = JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'shared-123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'consistent test' }
|
||||
}
|
||||
});
|
||||
|
||||
const result = parseFunctionCall(sharedFunctionCall);
|
||||
const result = parseToolCall(sharedToolCall);
|
||||
|
||||
// All providers should parse to the same structure using the shared helper
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.functionCall.name).toBe('search_vault_files');
|
||||
expect(result!.functionCall.args).toEqual({ query: 'consistent test' });
|
||||
expect(result!.toolCall.name).toBe('search_vault_files');
|
||||
expect(result!.toolCall.args).toEqual({ query: 'consistent test' });
|
||||
});
|
||||
|
||||
it('should filter conversations consistently across providers', () => {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ describe('Claude', () => {
|
|||
|
||||
expect(result.content).toBe('Hello world');
|
||||
expect(result.isComplete).toBe(false);
|
||||
expect(result.functionCall).toBeUndefined();
|
||||
expect(result.toolCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detect tool_use start in content_block_start', () => {
|
||||
|
|
@ -189,10 +189,10 @@ describe('Claude', () => {
|
|||
type: 'content_block_stop'
|
||||
}));
|
||||
|
||||
expect(result.functionCall).toBeDefined();
|
||||
expect(result.functionCall?.name).toBe('search_vault_files');
|
||||
expect(result.functionCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.functionCall?.toolId).toBe('tool_123');
|
||||
expect(result.toolCall).toBeDefined();
|
||||
expect(result.toolCall?.name).toBe('search_vault_files');
|
||||
expect(result.toolCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.toolCall?.toolId).toBe('tool_123');
|
||||
});
|
||||
|
||||
it('should handle content_block_stop and finalize function call', () => {
|
||||
|
|
@ -207,10 +207,10 @@ describe('Claude', () => {
|
|||
|
||||
const result = (claude as any).parseStreamChunk(chunk);
|
||||
|
||||
expect(result.functionCall).toBeDefined();
|
||||
expect(result.functionCall?.name).toBe('search_vault_files');
|
||||
expect(result.functionCall?.arguments).toEqual({ param: 'value' });
|
||||
expect(result.functionCall?.toolId).toBe('func_456');
|
||||
expect(result.toolCall).toBeDefined();
|
||||
expect(result.toolCall?.name).toBe('search_vault_files');
|
||||
expect(result.toolCall?.arguments).toEqual({ param: 'value' });
|
||||
expect(result.toolCall?.toolId).toBe('func_456');
|
||||
|
||||
// Should reset accumulation
|
||||
expect((claude as any).accumulatedFunctionName).toBeNull();
|
||||
|
|
@ -266,7 +266,7 @@ describe('Claude', () => {
|
|||
|
||||
const result = (claude as any).parseStreamChunk(chunk);
|
||||
|
||||
expect(result.functionCall).toBeUndefined();
|
||||
expect(result.toolCall).toBeUndefined();
|
||||
expect(exceptionSpy).toHaveBeenCalled();
|
||||
|
||||
exceptionSpy.mockRestore();
|
||||
|
|
@ -302,12 +302,12 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function call to tool_use format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -317,7 +317,7 @@ describe('Claude', () => {
|
|||
shouldDisplayContent: false
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].content).toHaveLength(1);
|
||||
|
|
@ -330,12 +330,12 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function response to tool_result format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -358,7 +358,7 @@ describe('Claude', () => {
|
|||
toolId: 'call_123'
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent, functionResponseContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent, functionResponseContent]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].content).toHaveLength(1);
|
||||
|
|
@ -376,7 +376,7 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: 'invalid json {',
|
||||
toolCall: 'invalid json {',
|
||||
timestamp: new Date(),
|
||||
shouldDisplayContent: false
|
||||
});
|
||||
|
|
@ -395,12 +395,12 @@ describe('Claude', () => {
|
|||
it('should handle invalid JSON in function response gracefully', async () => {
|
||||
const exceptionSpy = vi.spyOn(Exception, 'log').mockImplementation(() => {});
|
||||
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_invalid',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -417,7 +417,7 @@ describe('Claude', () => {
|
|||
toolId: 'call_invalid'
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent, invalidContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent, invalidContent]);
|
||||
|
||||
// Should fallback to text
|
||||
expect(result).toHaveLength(2);
|
||||
|
|
@ -448,8 +448,8 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: 'Let me search for that',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_456',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -468,12 +468,12 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function call without ID to legacy text format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
// No ID field
|
||||
|
|
@ -483,7 +483,7 @@ describe('Claude', () => {
|
|||
shouldDisplayContent: false
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].content).toHaveLength(1);
|
||||
|
|
@ -500,12 +500,12 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function call with empty ID to legacy text format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: '', // Empty ID
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -515,7 +515,7 @@ describe('Claude', () => {
|
|||
shouldDisplayContent: false
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].content).toHaveLength(1);
|
||||
|
|
@ -532,12 +532,12 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function response without ID to legacy text format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_legacy',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -561,7 +561,7 @@ describe('Claude', () => {
|
|||
toolId: 'call_legacy'
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent, functionResponseContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent, functionResponseContent]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].content).toHaveLength(1);
|
||||
|
|
@ -578,12 +578,12 @@ describe('Claude', () => {
|
|||
});
|
||||
|
||||
it('should convert function response with empty ID to legacy text format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_empty',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -607,7 +607,7 @@ describe('Claude', () => {
|
|||
toolId: 'call_empty'
|
||||
});
|
||||
|
||||
const result = await (claude as any).extractContents([functionCallContent, functionResponseContent]);
|
||||
const result = await (claude as any).extractContents([toolCallContent, functionResponseContent]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].content).toHaveLength(1);
|
||||
|
|
@ -631,8 +631,8 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_orphaned',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -660,8 +660,8 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -704,8 +704,8 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_latest',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -732,8 +732,8 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_orphan1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test1' }
|
||||
|
|
@ -748,8 +748,8 @@ describe('Claude', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_orphan2',
|
||||
name: 'read_file',
|
||||
args: { path: 'test.md' }
|
||||
|
|
|
|||
|
|
@ -94,12 +94,12 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
describe('Claude/OpenAI → Gemini Switching', () => {
|
||||
it('should convert Claude function call (no thoughtSignature) to legacy text format', async () => {
|
||||
// Simulate a conversation started with Claude that made a function call
|
||||
const claudeFunctionCall = new ConversationContent({
|
||||
const claudeToolCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_abc123', // AIToolCall.toConversationString() includes id in JSON
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'meeting notes' }
|
||||
|
|
@ -110,7 +110,7 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
toolId: 'call_abc123'
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([claudeFunctionCall]);
|
||||
const result = await (gemini as any).extractContents([claudeToolCall]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].parts[0]).toHaveProperty('text');
|
||||
|
|
@ -126,8 +126,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_xyz789', // AIToolCall.toConversationString() includes id in JSON
|
||||
name: 'read_file',
|
||||
args: { path: 'project.md' }
|
||||
|
|
@ -148,12 +148,12 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
|
||||
it('should convert function response without id to legacy text format', async () => {
|
||||
// Function responses from Claude/OpenAI may not have the id field in content
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_cross1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -176,7 +176,7 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
functionResponse: responseContent,
|
||||
toolId: 'call_cross1'
|
||||
});
|
||||
const result = await (gemini as any).extractContents([functionCallContent, claudeResponse]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent, claudeResponse]);
|
||||
|
||||
expect(result[1].parts[0]).toHaveProperty('text');
|
||||
expect(result[1].parts[0].text).toContain('<!-- Historical tool result');
|
||||
|
|
@ -191,12 +191,12 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
it('should handle Gemini function call with thoughtSignature when switching providers', async () => {
|
||||
// This test verifies the data structure is preserved
|
||||
// In actual usage, Claude/OpenAI would ignore the thoughtSignature field
|
||||
const geminiFunctionCall = new ConversationContent({
|
||||
const geminiToolCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -207,13 +207,13 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
});
|
||||
|
||||
// Verify the signature is stored in ConversationContent
|
||||
expect(geminiFunctionCall.thoughtSignature).toBe('geminiThoughtSignature==');
|
||||
expect(geminiToolCall.thoughtSignature).toBe('geminiThoughtSignature==');
|
||||
|
||||
// When this conversation is sent to Claude/OpenAI, they'll see the function call
|
||||
// but ignore the thoughtSignature field (which is fine)
|
||||
const functionCallData = JSON.parse(geminiFunctionCall.functionCall!);
|
||||
expect(functionCallData.functionCall.name).toBe('search_vault_files');
|
||||
expect(functionCallData.functionCall.args).toEqual({ query: 'test' });
|
||||
const toolCallData = JSON.parse(geminiToolCall.toolCall!);
|
||||
expect(toolCallData.toolCall.name).toBe('search_vault_files');
|
||||
expect(toolCallData.toolCall.args).toEqual({ query: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -229,8 +229,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_claude_123', // AIToolCall.toConversationString() includes id
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'project' }
|
||||
|
|
@ -273,8 +273,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'read_file',
|
||||
args: { path: 'project.md' }
|
||||
}
|
||||
|
|
@ -313,18 +313,18 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// Find the Claude function call - should be legacy text
|
||||
const claudeFunctionCall = result.find((r: any) =>
|
||||
const claudeToolCall = result.find((r: any) =>
|
||||
r.parts[0]?.text?.includes('<!-- Historical tool call') &&
|
||||
r.parts[0]?.text?.includes('"name": "search_vault_files"')
|
||||
);
|
||||
expect(claudeFunctionCall).toBeDefined();
|
||||
expect(claudeToolCall).toBeDefined();
|
||||
|
||||
// Find the Gemini function call - should have proper format with signature
|
||||
const geminiFunctionCall = result.find((r: any) =>
|
||||
const geminiToolCall = result.find((r: any) =>
|
||||
r.parts[0]?.functionCall?.name === 'read_file' &&
|
||||
r.parts[0]?.thoughtSignature === 'geminiSignature123=='
|
||||
);
|
||||
expect(geminiFunctionCall).toBeDefined();
|
||||
expect(geminiToolCall).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle conversation switching from provider without signatures to Gemini', async () => {
|
||||
|
|
@ -338,8 +338,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_openai_456', // AIToolCall.toConversationString() includes id
|
||||
name: 'list_files',
|
||||
args: {}
|
||||
|
|
@ -392,8 +392,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call-1', // AIToolCall.toConversationString() includes id
|
||||
name: 'func1',
|
||||
args: { a: 1 }
|
||||
|
|
@ -428,8 +428,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: { name: 'func2', args: { b: 2 } }
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: { name: 'func2', args: { b: 2 } }
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
shouldDisplayContent: true,
|
||||
|
|
@ -485,12 +485,12 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
describe('Claude ↔ OpenAI Switching', () => {
|
||||
it('should handle Claude function call when switching to OpenAI', async () => {
|
||||
// Simulate a conversation started with Claude that made a function call
|
||||
const claudeFunctionCall = new ConversationContent({
|
||||
const claudeToolCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_abc123', // Claude's tool_use ID
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'meeting notes' }
|
||||
|
|
@ -521,7 +521,7 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
|
||||
// Now switch to OpenAI - it should read Claude's function call
|
||||
const openai = new OpenAI();
|
||||
const result = await (openai as any).extractContents([claudeFunctionCall, claudeResponse]);
|
||||
const result = await (openai as any).extractContents([claudeToolCall, claudeResponse]);
|
||||
|
||||
// OpenAI should convert Claude's function call to its Responses API format
|
||||
expect(result).toHaveLength(2);
|
||||
|
|
@ -544,8 +544,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_xyz789', // OpenAI's call_id
|
||||
name: 'read_file',
|
||||
args: { path: 'project.md' }
|
||||
|
|
@ -605,8 +605,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -647,8 +647,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_2',
|
||||
name: 'read_file',
|
||||
args: { path: 'file1.md' }
|
||||
|
|
@ -704,8 +704,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: '', // Empty string
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -735,8 +735,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: ' ', // Whitespace only
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -764,8 +764,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: 'Let me read that file for you',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_openai_123',
|
||||
name: 'read_file',
|
||||
args: { path: 'notes.md' }
|
||||
|
|
@ -798,8 +798,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_abc',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'project notes' }
|
||||
|
|
@ -827,8 +827,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -871,12 +871,12 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
// REGRESSION TEST: Bug discovered where Gemini → OpenAI switching failed
|
||||
// because OpenAI tried to use undefined call_id
|
||||
// Gemini function call with thoughtSignature but no id
|
||||
const geminiFunctionCall = new ConversationContent({
|
||||
const geminiToolCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -907,7 +907,7 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
|
||||
// OpenAI should convert to legacy text format (not try to use undefined call_id)
|
||||
const openai = new OpenAI();
|
||||
const result = await (openai as any).extractContents([geminiFunctionCall, geminiResponse]);
|
||||
const result = await (openai as any).extractContents([geminiToolCall, geminiResponse]);
|
||||
|
||||
// Should have 2 items (both converted to messages)
|
||||
expect(result).toHaveLength(2);
|
||||
|
|
@ -938,8 +938,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_xyz',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -987,9 +987,9 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
const openai = new OpenAI();
|
||||
const openaiResult = await (openai as any).extractContents(conversation);
|
||||
expect(openaiResult.length).toBeGreaterThan(0);
|
||||
const functionCall = openaiResult.find((r: any) => r.type === 'function_call');
|
||||
expect(functionCall).toBeDefined();
|
||||
expect(functionCall.call_id).toBe('call_xyz');
|
||||
const toolCall = openaiResult.find((r: any) => r.type === 'function_call');
|
||||
expect(toolCall).toBeDefined();
|
||||
expect(toolCall.call_id).toBe('call_xyz');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1005,8 +1005,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'notes' }
|
||||
|
|
@ -1043,8 +1043,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_2',
|
||||
name: 'read_file',
|
||||
args: { path: 'note1.md' }
|
||||
|
|
@ -1108,8 +1108,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'project' }
|
||||
}
|
||||
|
|
@ -1146,8 +1146,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_2',
|
||||
name: 'read_file',
|
||||
args: { path: 'project.md' }
|
||||
|
|
@ -1184,8 +1184,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_3',
|
||||
name: 'write_file',
|
||||
args: { path: 'summary.md', content: 'Summary here' }
|
||||
|
|
@ -1225,10 +1225,10 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
expect(openaiResult.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify all three function calls are present in each provider's view
|
||||
const geminiFunctionCalls = geminiResult.filter((r: any) =>
|
||||
const geminiToolCalls = geminiResult.filter((r: any) =>
|
||||
r.parts[0]?.functionCall || r.parts[0]?.text?.includes('<!-- Historical tool call')
|
||||
);
|
||||
expect(geminiFunctionCalls.length).toBe(3);
|
||||
expect(geminiToolCalls.length).toBe(3);
|
||||
|
||||
const claudeToolUses = claudeResult.filter((r: any) =>
|
||||
r.content.some((c: any) => c.type === 'tool_use' || c.text?.includes('<!-- Historical tool call'))
|
||||
|
|
@ -1246,8 +1246,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_round',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test', limit: 5 }
|
||||
|
|
@ -1315,8 +1315,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_round',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'openai test' }
|
||||
|
|
@ -1377,8 +1377,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: 'Searching...',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_step1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'workflow' }
|
||||
|
|
@ -1414,8 +1414,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: 'Reading file...',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_step2',
|
||||
name: 'read_file',
|
||||
args: { path: 'workflow.md' }
|
||||
|
|
@ -1451,8 +1451,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'list_files',
|
||||
args: { path: '/' }
|
||||
}
|
||||
|
|
@ -1525,8 +1525,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: { name: 'test_func', args: {} } // No id = native Gemini
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: { name: 'test_func', args: {} } // No id = native Gemini
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
shouldDisplayContent: true,
|
||||
|
|
@ -1546,8 +1546,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: { name: 'test_func', args: {} } // No id = native Gemini
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: { name: 'test_func', args: {} } // No id = native Gemini
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
shouldDisplayContent: true,
|
||||
|
|
@ -1571,8 +1571,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_123', // id field indicates Claude/OpenAI origin
|
||||
name: 'func1',
|
||||
args: {}
|
||||
|
|
@ -1620,8 +1620,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'tool-123', // ID in the JSON indicates cross-provider origin
|
||||
name: 'test_func',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -1667,8 +1667,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: { name: 'test_func', args: { query: 'test' } } // No id field!
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: { name: 'test_func', args: { query: 'test' } } // No id field!
|
||||
}),
|
||||
timestamp: new Date(),
|
||||
shouldDisplayContent: true,
|
||||
|
|
@ -1698,8 +1698,8 @@ describe('Cross-Provider Integration - Thought Signature Support', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call-123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
|
|||
|
|
@ -252,9 +252,9 @@ describe('Gemini', () => {
|
|||
|
||||
expect(result.isComplete).toBe(true);
|
||||
expect(result.shouldContinue).toBe(true);
|
||||
expect(result.functionCall).toBeDefined();
|
||||
expect(result.functionCall?.name).toBe('search_vault_files');
|
||||
expect(result.functionCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.toolCall).toBeDefined();
|
||||
expect(result.toolCall?.name).toBe('search_vault_files');
|
||||
expect(result.toolCall?.arguments).toEqual({ query: 'test' });
|
||||
});
|
||||
|
||||
it('should finalize function call with thoughtSignature on completion', () => {
|
||||
|
|
@ -274,10 +274,10 @@ describe('Gemini', () => {
|
|||
|
||||
expect(result.isComplete).toBe(true);
|
||||
expect(result.shouldContinue).toBe(true);
|
||||
expect(result.functionCall).toBeDefined();
|
||||
expect(result.functionCall?.name).toBe('search_vault_files');
|
||||
expect(result.functionCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.functionCall?.thoughtSignature).toBe(signature);
|
||||
expect(result.toolCall).toBeDefined();
|
||||
expect(result.toolCall?.name).toBe('search_vault_files');
|
||||
expect(result.toolCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.toolCall?.thoughtSignature).toBe(signature);
|
||||
});
|
||||
|
||||
it('should finalize function call without thoughtSignature when not accumulated', () => {
|
||||
|
|
@ -294,8 +294,8 @@ describe('Gemini', () => {
|
|||
|
||||
const result = (gemini as any).parseStreamChunk(chunk);
|
||||
|
||||
expect(result.functionCall).toBeDefined();
|
||||
expect(result.functionCall?.thoughtSignature).toBeUndefined();
|
||||
expect(result.toolCall).toBeDefined();
|
||||
expect(result.toolCall?.thoughtSignature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detect completion with STOP finish reason', () => {
|
||||
|
|
@ -430,12 +430,12 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should convert function call to Gemini format (with signature from Gemini)', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -445,7 +445,7 @@ describe('Gemini', () => {
|
|||
thoughtSignature: 'geminiSignatureFromAPI==' // Has signature from Gemini
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].role).toBe(Role.Model);
|
||||
|
|
@ -461,12 +461,12 @@ describe('Gemini', () => {
|
|||
|
||||
it('should convert function call with thoughtSignature to Gemini format with signature', async () => {
|
||||
const signature = 'geminiSignature==';
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -476,7 +476,7 @@ describe('Gemini', () => {
|
|||
thoughtSignature: signature
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].role).toBe(Role.Model);
|
||||
|
|
@ -491,12 +491,12 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should fall back to legacy text format for function call without thoughtSignature (cross-provider)', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'toolu_01234567', // toolId indicates this came from Claude/OpenAI
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -507,7 +507,7 @@ describe('Gemini', () => {
|
|||
// No thoughtSignature (came from Claude/OpenAI)
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].role).toBe(Role.Model);
|
||||
|
|
@ -520,12 +520,12 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should use native format for Gemini function call without thoughtSignature', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
// No id field - this is a native Gemini function call
|
||||
name: 'read_file',
|
||||
args: { path: 'note.md' }
|
||||
|
|
@ -536,7 +536,7 @@ describe('Gemini', () => {
|
|||
// No thoughtSignature (normal Gemini call without extended thinking)
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].parts[0]).toHaveProperty('functionCall');
|
||||
|
|
@ -546,12 +546,12 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should convert function response to Gemini format', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call-123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -575,7 +575,7 @@ describe('Gemini', () => {
|
|||
toolId: 'call-123'
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent, functionResponseContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent, functionResponseContent]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].parts).toHaveLength(1);
|
||||
|
|
@ -589,12 +589,12 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should fall back to legacy text format for function response without id (cross-provider)', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_legacy1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -617,7 +617,7 @@ describe('Gemini', () => {
|
|||
toolId: 'call_legacy1'
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent, functionResponseContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent, functionResponseContent]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].parts).toHaveLength(1);
|
||||
|
|
@ -630,12 +630,12 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
it('should fall back to legacy text format for function response with empty id', async () => {
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_legacy2',
|
||||
name: 'read_file',
|
||||
args: { path: 'test.md' }
|
||||
|
|
@ -659,7 +659,7 @@ describe('Gemini', () => {
|
|||
toolId: 'call_legacy2'
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent, functionResponseContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent, functionResponseContent]);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].parts[0]).toHaveProperty('text');
|
||||
|
|
@ -676,7 +676,7 @@ describe('Gemini', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: 'invalid json {',
|
||||
toolCall: 'invalid json {',
|
||||
timestamp: new Date(),
|
||||
shouldDisplayContent: false
|
||||
});
|
||||
|
|
@ -695,12 +695,12 @@ describe('Gemini', () => {
|
|||
it('should handle invalid JSON in function response gracefully', async () => {
|
||||
const exceptionSpy = vi.spyOn(Exception, 'log').mockImplementation(() => {});
|
||||
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_invalid',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -717,7 +717,7 @@ describe('Gemini', () => {
|
|||
toolId: 'call_invalid'
|
||||
});
|
||||
|
||||
const result = await (gemini as any).extractContents([functionCallContent, invalidContent]);
|
||||
const result = await (gemini as any).extractContents([toolCallContent, invalidContent]);
|
||||
|
||||
// Should fallback to text
|
||||
expect(result).toHaveLength(2);
|
||||
|
|
@ -750,8 +750,8 @@ describe('Gemini', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -778,8 +778,8 @@ describe('Gemini', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -822,8 +822,8 @@ describe('Gemini', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
|
|
@ -855,8 +855,8 @@ describe('Gemini', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test1' }
|
||||
}
|
||||
|
|
@ -870,8 +870,8 @@ describe('Gemini', () => {
|
|||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
name: 'read_file',
|
||||
args: { path: 'test.md' }
|
||||
}
|
||||
|
|
@ -894,16 +894,16 @@ describe('Gemini', () => {
|
|||
});
|
||||
|
||||
describe('Helper Methods', () => {
|
||||
describe('convertFunctionCallToText', () => {
|
||||
describe('convertToolCallToText', () => {
|
||||
it('should convert function call to legacy text format', async () => {
|
||||
const parsedContent = {
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test notes' }
|
||||
}
|
||||
};
|
||||
|
||||
const result = (gemini as any).convertFunctionCallToText(parsedContent);
|
||||
const result = (gemini as any).convertToolCallToText(parsedContent);
|
||||
|
||||
expect(result).toContain('<!-- Historical tool call');
|
||||
expect(result).toContain('This action was ALREADY COMPLETED');
|
||||
|
|
@ -914,7 +914,7 @@ describe('Gemini', () => {
|
|||
|
||||
it('should format complex arguments correctly', () => {
|
||||
const parsedContent = {
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: 'write_file',
|
||||
args: {
|
||||
path: 'note.md',
|
||||
|
|
@ -924,7 +924,7 @@ describe('Gemini', () => {
|
|||
}
|
||||
};
|
||||
|
||||
const result = (gemini as any).convertFunctionCallToText(parsedContent);
|
||||
const result = (gemini as any).convertToolCallToText(parsedContent);
|
||||
|
||||
expect(result).toContain('<!-- Historical tool call');
|
||||
expect(result).toContain('"name": "write_file"');
|
||||
|
|
@ -938,13 +938,13 @@ describe('Gemini', () => {
|
|||
|
||||
it('should handle function call with empty args', () => {
|
||||
const parsedContent = {
|
||||
functionCall: {
|
||||
toolCall: {
|
||||
name: 'list_files',
|
||||
args: {}
|
||||
}
|
||||
};
|
||||
|
||||
const result = (gemini as any).convertFunctionCallToText(parsedContent);
|
||||
const result = (gemini as any).convertToolCallToText(parsedContent);
|
||||
|
||||
const expected = `<!-- Historical tool call. This action was ALREADY COMPLETED.
|
||||
Use your native function calling for any NEW operations. -->
|
||||
|
|
|
|||
|
|
@ -146,10 +146,10 @@ describe('OpenAI', () => {
|
|||
|
||||
expect(result.isComplete).toBe(false);
|
||||
expect(result.shouldContinue).toBe(true);
|
||||
expect(result.functionCall).toBeDefined();
|
||||
expect(result.functionCall?.name).toBe('search_vault_files');
|
||||
expect(result.functionCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.functionCall?.toolId).toBe('call_123');
|
||||
expect(result.toolCall).toBeDefined();
|
||||
expect(result.toolCall?.name).toBe('search_vault_files');
|
||||
expect(result.toolCall?.arguments).toEqual({ query: 'test' });
|
||||
expect(result.toolCall?.toolId).toBe('call_123');
|
||||
});
|
||||
|
||||
it('should handle response.done event', () => {
|
||||
|
|
@ -231,7 +231,7 @@ describe('OpenAI', () => {
|
|||
|
||||
const result = (openai as any).parseStreamChunk(chunk);
|
||||
|
||||
expect(result.functionCall).toBeUndefined();
|
||||
expect(result.toolCall).toBeUndefined();
|
||||
expect(exceptionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ describe('OpenAI', () => {
|
|||
|
||||
expect(result.content).toBe('');
|
||||
expect(result.isComplete).toBe(false);
|
||||
expect(result.functionCall).toBeUndefined();
|
||||
expect(result.toolCall).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle response.refusal.delta events', () => {
|
||||
|
|
@ -334,18 +334,18 @@ describe('OpenAI', () => {
|
|||
|
||||
it('should convert function call to Responses API format', async () => {
|
||||
const conversation = new Conversation();
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: 'Let me search',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
})
|
||||
});
|
||||
conversation.contents.push(functionCallContent);
|
||||
conversation.contents.push(toolCallContent);
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
|
|
@ -378,12 +378,12 @@ describe('OpenAI', () => {
|
|||
it('should convert function response to function_call_output format', async () => {
|
||||
const conversation = new Conversation();
|
||||
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -391,7 +391,7 @@ describe('OpenAI', () => {
|
|||
}),
|
||||
toolId: 'call_123'
|
||||
});
|
||||
conversation.contents.push(functionCallContent);
|
||||
conversation.contents.push(toolCallContent);
|
||||
|
||||
const responseContent = JSON.stringify({
|
||||
id: 'call_123',
|
||||
|
|
@ -432,7 +432,7 @@ describe('OpenAI', () => {
|
|||
const conversation = new Conversation();
|
||||
const invalidContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: 'invalid json {'
|
||||
toolCall: 'invalid json {'
|
||||
});
|
||||
conversation.contents.push(invalidContent);
|
||||
|
||||
|
|
@ -457,12 +457,12 @@ describe('OpenAI', () => {
|
|||
|
||||
const conversation = new Conversation();
|
||||
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_invalid',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -470,7 +470,7 @@ describe('OpenAI', () => {
|
|||
}),
|
||||
toolId: 'call_invalid'
|
||||
});
|
||||
conversation.contents.push(functionCallContent);
|
||||
conversation.contents.push(toolCallContent);
|
||||
|
||||
const invalidContent = new ConversationContent({
|
||||
role: Role.User,
|
||||
|
|
@ -522,8 +522,8 @@ describe('OpenAI', () => {
|
|||
// Function call without response (orphaned)
|
||||
const orphanedCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_orphaned',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -553,17 +553,17 @@ describe('OpenAI', () => {
|
|||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent({ role: Role.User, content: 'Search for files' }));
|
||||
// Function call with response (not orphaned)
|
||||
const functionCall = new ConversationContent({
|
||||
const toolCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
})
|
||||
});
|
||||
conversation.contents.push(functionCall);
|
||||
conversation.contents.push(toolCall);
|
||||
// Corresponding function response
|
||||
const responseContent = JSON.stringify({
|
||||
id: 'call_123',
|
||||
|
|
@ -613,8 +613,8 @@ describe('OpenAI', () => {
|
|||
// Function call as most recent item (should be included)
|
||||
const latestCall = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_latest',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -653,8 +653,8 @@ describe('OpenAI', () => {
|
|||
// Orphaned function call #1
|
||||
const orphan1 = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_orphan1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test1' }
|
||||
|
|
@ -666,8 +666,8 @@ describe('OpenAI', () => {
|
|||
// Orphaned function call #2
|
||||
const orphan2 = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_orphan2',
|
||||
name: 'read_file',
|
||||
args: { path: 'test.md' }
|
||||
|
|
@ -697,18 +697,18 @@ describe('OpenAI', () => {
|
|||
describe('Responses API Format Edge Cases', () => {
|
||||
it('should handle assistant message with both text and function call', async () => {
|
||||
const conversation = new Conversation();
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: 'I will search for that.',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
})
|
||||
});
|
||||
conversation.contents.push(functionCallContent);
|
||||
conversation.contents.push(toolCallContent);
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
|
|
@ -731,17 +731,17 @@ describe('OpenAI', () => {
|
|||
|
||||
it('should handle function call with empty text content', async () => {
|
||||
const conversation = new Conversation();
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
}
|
||||
})
|
||||
});
|
||||
conversation.contents.push(functionCallContent);
|
||||
conversation.contents.push(toolCallContent);
|
||||
|
||||
mockStreamingService.streamRequest.mockImplementation(async function* () {
|
||||
yield { content: 'done', isComplete: true };
|
||||
|
|
@ -766,12 +766,12 @@ describe('OpenAI', () => {
|
|||
it('should handle complex function response objects', async () => {
|
||||
const conversation = new Conversation();
|
||||
|
||||
const functionCallContent = new ConversationContent({
|
||||
const toolCallContent = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_123',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -779,7 +779,7 @@ describe('OpenAI', () => {
|
|||
}),
|
||||
toolId: 'call_123'
|
||||
});
|
||||
conversation.contents.push(functionCallContent);
|
||||
conversation.contents.push(toolCallContent);
|
||||
|
||||
const complexResponse = {
|
||||
files: ['file1.txt', 'file2.md'],
|
||||
|
|
@ -824,8 +824,8 @@ describe('OpenAI', () => {
|
|||
// First function call
|
||||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_1',
|
||||
name: 'search_vault_files',
|
||||
args: { query: 'test' }
|
||||
|
|
@ -847,8 +847,8 @@ describe('OpenAI', () => {
|
|||
conversation.contents.push(new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: 'Let me read that file',
|
||||
functionCall: JSON.stringify({
|
||||
functionCall: {
|
||||
toolCall: JSON.stringify({
|
||||
toolCall: {
|
||||
id: 'call_2',
|
||||
name: 'read_file',
|
||||
args: { path: 'file1.txt' }
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ describe('Conversation', () => {
|
|||
role: 'user',
|
||||
content: 'Hello',
|
||||
promptContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
isFunctionCall: false,
|
||||
isFunctionCallResponse: false,
|
||||
isToolCall: false,
|
||||
isToolCallResponse: false,
|
||||
isProviderSpecificContent: false
|
||||
}
|
||||
]
|
||||
|
|
@ -196,10 +196,10 @@ describe('Conversation', () => {
|
|||
role: 'user',
|
||||
content: 'Hello',
|
||||
promptContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
isFunctionCall: false,
|
||||
isFunctionCallResponse: false,
|
||||
isToolCall: false,
|
||||
isToolCallResponse: false,
|
||||
isProviderSpecificContent: false
|
||||
},
|
||||
{ invalid: 'data' }
|
||||
|
|
@ -278,9 +278,9 @@ describe('Conversation', () => {
|
|||
conversation.contents.push(content);
|
||||
|
||||
const mostRecent = conversation.contents[conversation.contents.length - 1];
|
||||
mostRecent.functionCall = 'readFile';
|
||||
mostRecent.toolCall = 'readFile';
|
||||
|
||||
expect(conversation.contents[0].functionCall).toBe('readFile');
|
||||
expect(conversation.contents[0].toolCall).toBe('readFile');
|
||||
});
|
||||
|
||||
it('should mark most recent content as function call', () => {
|
||||
|
|
@ -289,9 +289,9 @@ describe('Conversation', () => {
|
|||
conversation.contents.push(content);
|
||||
|
||||
const mostRecent = conversation.contents[conversation.contents.length - 1];
|
||||
mostRecent.functionCall = 'readFile';
|
||||
mostRecent.toolCall = 'readFile';
|
||||
|
||||
expect(conversation.contents[0].functionCall).toBe('readFile');
|
||||
expect(conversation.contents[0].toolCall).toBe('readFile');
|
||||
});
|
||||
|
||||
it('should only update the last content when multiple contents exist', () => {
|
||||
|
|
@ -301,11 +301,11 @@ describe('Conversation', () => {
|
|||
conversation.contents.push(new ConversationContent({ role: Role.Assistant }));
|
||||
|
||||
const mostRecent = conversation.contents[conversation.contents.length - 1];
|
||||
mostRecent.functionCall = 'searchFiles';
|
||||
mostRecent.toolCall = 'searchFiles';
|
||||
|
||||
expect(conversation.contents[0].functionCall).toBeUndefined();
|
||||
expect(conversation.contents[1].functionCall).toBeUndefined();
|
||||
expect(conversation.contents[2].functionCall).toBe('searchFiles');
|
||||
expect(conversation.contents[0].toolCall).toBeUndefined();
|
||||
expect(conversation.contents[1].toolCall).toBeUndefined();
|
||||
expect(conversation.contents[2].toolCall).toBe('searchFiles');
|
||||
});
|
||||
|
||||
it('should do nothing when contents array is empty', () => {
|
||||
|
|
@ -315,7 +315,7 @@ describe('Conversation', () => {
|
|||
expect(() => {
|
||||
const mostRecent = conversation.contents[conversation.contents.length - 1];
|
||||
if (mostRecent) {
|
||||
mostRecent.functionCall = 'test';
|
||||
mostRecent.toolCall = 'test';
|
||||
}
|
||||
}).not.toThrow();
|
||||
expect(conversation.contents).toHaveLength(0);
|
||||
|
|
@ -326,20 +326,20 @@ describe('Conversation', () => {
|
|||
conversation.contents.push(new ConversationContent({ role: Role.Assistant }));
|
||||
|
||||
const mostRecent = conversation.contents[conversation.contents.length - 1];
|
||||
mostRecent.functionCall = '';
|
||||
mostRecent.toolCall = '';
|
||||
|
||||
expect(conversation.contents[0].functionCall).toBe('');
|
||||
expect(conversation.contents[0].toolCall).toBe('');
|
||||
});
|
||||
|
||||
it('should overwrite existing function call', () => {
|
||||
const conversation = new Conversation();
|
||||
const content = new ConversationContent({ role: Role.Assistant, functionCall: 'oldFunction' });
|
||||
const content = new ConversationContent({ role: Role.Assistant, toolCall: 'oldFunction' });
|
||||
conversation.contents.push(content);
|
||||
|
||||
const mostRecent = conversation.contents[conversation.contents.length - 1];
|
||||
mostRecent.functionCall = 'newFunction';
|
||||
mostRecent.toolCall = 'newFunction';
|
||||
|
||||
expect(conversation.contents[0].functionCall).toBe('newFunction');
|
||||
expect(conversation.contents[0].toolCall).toBe('newFunction');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -361,14 +361,14 @@ describe('Conversation', () => {
|
|||
mostRecent.content = 'Hi there, how can I help you?';
|
||||
|
||||
// Assistant makes a function call
|
||||
mostRecent.functionCall = 'readFile';
|
||||
mostRecent.toolCall = 'readFile';
|
||||
|
||||
expect(conversation.contents).toHaveLength(2);
|
||||
expect(conversation.contents[0].role).toBe(Role.User);
|
||||
expect(conversation.contents[0].content).toBe('Hello');
|
||||
expect(conversation.contents[1].role).toBe(Role.Assistant);
|
||||
expect(conversation.contents[1].content).toBe('Hi there, how can I help you?');
|
||||
expect(conversation.contents[1].functionCall).toBe('readFile');
|
||||
expect(conversation.contents[1].toolCall).toBe('readFile');
|
||||
});
|
||||
|
||||
it('should maintain conversation metadata', () => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('ConversationContent', () => {
|
|||
role: Role.User,
|
||||
content: 'Hello',
|
||||
displayContent: 'Hello Display',
|
||||
functionCall: 'functionCall',
|
||||
toolCall: 'toolCall',
|
||||
functionResponse: 'functionResponse',
|
||||
timestamp,
|
||||
attachments: [],
|
||||
|
|
@ -21,7 +21,7 @@ describe('ConversationContent', () => {
|
|||
expect(content.role).toBe(Role.User);
|
||||
expect(content.content).toBe('Hello');
|
||||
expect(content.displayContent).toBe('Hello Display');
|
||||
expect(content.functionCall).toBe('functionCall');
|
||||
expect(content.toolCall).toBe('toolCall');
|
||||
expect(content.functionResponse).toBe('functionResponse');
|
||||
expect(content.timestamp).toBe(timestamp);
|
||||
expect(content.attachments).toEqual([]);
|
||||
|
|
@ -36,7 +36,7 @@ describe('ConversationContent', () => {
|
|||
role: Role.User,
|
||||
content: 'Hello',
|
||||
displayContent: 'Hello Display',
|
||||
functionCall: 'functionCall',
|
||||
toolCall: 'toolCall',
|
||||
functionResponse: 'functionResponse',
|
||||
timestamp,
|
||||
attachments: [],
|
||||
|
|
@ -48,7 +48,7 @@ describe('ConversationContent', () => {
|
|||
expect(content.role).toBe(Role.User);
|
||||
expect(content.content).toBe('Hello');
|
||||
expect(content.displayContent).toBe('Hello Display');
|
||||
expect(content.functionCall).toBe('functionCall');
|
||||
expect(content.toolCall).toBe('toolCall');
|
||||
expect(content.functionResponse).toBe('functionResponse');
|
||||
expect(content.timestamp).toBe(timestamp);
|
||||
expect(content.attachments).toEqual([]);
|
||||
|
|
@ -63,7 +63,7 @@ describe('ConversationContent', () => {
|
|||
expect(content.role).toBe(Role.Assistant);
|
||||
expect(content.content).toBeUndefined();
|
||||
expect(content.displayContent).toBeUndefined();
|
||||
expect(content.functionCall).toBeUndefined();
|
||||
expect(content.toolCall).toBeUndefined();
|
||||
expect(content.functionResponse).toBeUndefined();
|
||||
expect(content.timestamp).toBeInstanceOf(Date);
|
||||
expect(content.attachments).toEqual([]);
|
||||
|
|
@ -85,13 +85,13 @@ describe('ConversationContent', () => {
|
|||
const content = new ConversationContent({
|
||||
role: Role.User,
|
||||
content: 'Hello',
|
||||
functionCall: 'someFunction'
|
||||
toolCall: 'someFunction'
|
||||
});
|
||||
|
||||
expect(content.role).toBe(Role.User);
|
||||
expect(content.content).toBe('Hello');
|
||||
expect(content.displayContent).toBeUndefined();
|
||||
expect(content.functionCall).toBe('someFunction');
|
||||
expect(content.toolCall).toBe('someFunction');
|
||||
expect(content.functionResponse).toBeUndefined();
|
||||
expect(content.timestamp).toBeInstanceOf(Date);
|
||||
expect(content.attachments).toEqual([]);
|
||||
|
|
@ -106,7 +106,7 @@ describe('ConversationContent', () => {
|
|||
|
||||
expect(content.role).toBe(Role.User);
|
||||
expect(content.content).toBe('What is the weather?');
|
||||
expect(content.functionCall).toBeUndefined();
|
||||
expect(content.toolCall).toBeUndefined();
|
||||
expect(content.functionResponse).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -123,13 +123,13 @@ describe('ConversationContent', () => {
|
|||
it('should create function call content', () => {
|
||||
const content = new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
functionCall: 'readFile',
|
||||
toolCall: 'readFile',
|
||||
timestamp: new Date(),
|
||||
toolId: 'call-1'
|
||||
});
|
||||
|
||||
expect(content.role).toBe(Role.Assistant);
|
||||
expect(content.functionCall).toBe('readFile');
|
||||
expect(content.toolCall).toBe('readFile');
|
||||
expect(content.functionResponse).toBeUndefined();
|
||||
expect(content.toolId).toBe('call-1');
|
||||
});
|
||||
|
|
@ -146,7 +146,7 @@ describe('ConversationContent', () => {
|
|||
expect(content.role).toBe(Role.User);
|
||||
expect(content.content).toBe('File contents here');
|
||||
expect(content.functionResponse).toBe('File contents here');
|
||||
expect(content.functionCall).toBeUndefined();
|
||||
expect(content.toolCall).toBeUndefined();
|
||||
expect(content.toolId).toBe('call-1');
|
||||
});
|
||||
|
||||
|
|
@ -213,7 +213,7 @@ describe('ConversationContent', () => {
|
|||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
content: 'Hello',
|
||||
displayContent: 'Hello Display',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
functionResponse: '',
|
||||
attachments: [],
|
||||
shouldDisplayContent: true
|
||||
|
|
@ -235,7 +235,7 @@ describe('ConversationContent', () => {
|
|||
const validData = {
|
||||
role: 'assistant',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
functionCall: 'readFile',
|
||||
toolCall: 'readFile',
|
||||
toolId: 'tool-123'
|
||||
};
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ describe('ConversationContent', () => {
|
|||
const validData = {
|
||||
role: 'assistant',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
functionCall: 'readFile',
|
||||
toolCall: 'readFile',
|
||||
thoughtSignature: 'base64Signature=='
|
||||
};
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ describe('ConversationContent', () => {
|
|||
const validData = {
|
||||
role: 'assistant',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
functionCall: 'readFile',
|
||||
toolCall: 'readFile',
|
||||
toolId: 'tool-123',
|
||||
thoughtSignature: 'base64Signature=='
|
||||
};
|
||||
|
|
@ -345,11 +345,11 @@ describe('ConversationContent', () => {
|
|||
expect(ConversationContent.isConversationContentData(invalidData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when functionCall is not a string', () => {
|
||||
it('should return false when toolCall is not a string', () => {
|
||||
const invalidData = {
|
||||
role: 'user',
|
||||
timestamp: '2024-01-01T00:00:00.000Z',
|
||||
functionCall: null
|
||||
toolCall: null
|
||||
};
|
||||
|
||||
expect(ConversationContent.isConversationContentData(invalidData)).toBe(false);
|
||||
|
|
@ -411,7 +411,7 @@ describe('ConversationContent', () => {
|
|||
timestamp: '',
|
||||
content: '',
|
||||
displayContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
functionResponse: ''
|
||||
};
|
||||
|
||||
|
|
@ -441,11 +441,11 @@ describe('ConversationContent', () => {
|
|||
expect(content.displayContent).toBe('modified');
|
||||
});
|
||||
|
||||
it('should allow functionCall to be modified', () => {
|
||||
it('should allow toolCall to be modified', () => {
|
||||
const content = new ConversationContent({ role: Role.Assistant });
|
||||
content.functionCall = 'readFile';
|
||||
content.toolCall = 'readFile';
|
||||
|
||||
expect(content.functionCall).toBe('readFile');
|
||||
expect(content.toolCall).toBe('readFile');
|
||||
});
|
||||
|
||||
it('should allow functionResponse to be modified', () => {
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ describe('AIControllerService - Integration Tests', () => {
|
|||
// First call returns a function call
|
||||
yield {
|
||||
content: 'Let me search',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ search_terms: ['test'], user_message: 'Searching' },
|
||||
'tool-1'
|
||||
|
|
@ -235,7 +235,7 @@ describe('AIControllerService - Integration Tests', () => {
|
|||
if (callCount === 1) {
|
||||
yield {
|
||||
content: '',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ search_terms: ['notes'], user_message: 'Searching for notes' },
|
||||
'tool-1'
|
||||
|
|
@ -349,7 +349,7 @@ describe('AIControllerService - Integration Tests', () => {
|
|||
if (callCount === 1) {
|
||||
yield {
|
||||
content: '',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{ search_terms: ['test'], user_message: 'Searching' },
|
||||
'tool-1'
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
|
||||
it('should serialize function calls correctly', async () => {
|
||||
const conversation = createTestConversation('With Function Call');
|
||||
const functionCall = {
|
||||
const toolCall = {
|
||||
name: 'test_function',
|
||||
arguments: { arg1: 'value1' }
|
||||
};
|
||||
|
|
@ -180,7 +180,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: 'Function call',
|
||||
functionCall: JSON.stringify(functionCall),
|
||||
toolCall: JSON.stringify(toolCall),
|
||||
timestamp: new Date('2024-01-01T10:02:00Z'),
|
||||
toolId: 'tool_123'
|
||||
})
|
||||
|
|
@ -189,12 +189,12 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
await service.saveConversation(conversation);
|
||||
|
||||
const savedData = mockFileSystemService.writeObjectToFile.mock.calls[0][1];
|
||||
const functionCallContent = savedData.contents[2];
|
||||
const toolCallContent = savedData.contents[2];
|
||||
|
||||
expect(functionCallContent).toMatchObject({
|
||||
expect(toolCallContent).toMatchObject({
|
||||
role: Role.Assistant,
|
||||
content: 'Function call',
|
||||
functionCall: JSON.stringify(functionCall),
|
||||
toolCall: JSON.stringify(toolCall),
|
||||
timestamp: '2024-01-01T10:02:00.000Z',
|
||||
toolId: 'tool_123'
|
||||
});
|
||||
|
|
@ -368,10 +368,10 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
role: Role.User,
|
||||
content: 'Message 1',
|
||||
promptContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
timestamp: '2024-01-01T10:00:00.000Z',
|
||||
isFunctionCall: false,
|
||||
isFunctionCallResponse: false,
|
||||
isToolCall: false,
|
||||
isToolCallResponse: false,
|
||||
isProviderSpecificContent: false
|
||||
}
|
||||
]
|
||||
|
|
@ -385,10 +385,10 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
role: Role.User,
|
||||
content: 'Message 2',
|
||||
promptContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
timestamp: '2024-01-02T10:00:00.000Z',
|
||||
isFunctionCall: false,
|
||||
isFunctionCallResponse: false,
|
||||
isToolCall: false,
|
||||
isToolCallResponse: false,
|
||||
isProviderSpecificContent: false
|
||||
}
|
||||
]
|
||||
|
|
@ -419,20 +419,20 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
role: Role.User,
|
||||
content: 'Hello',
|
||||
promptContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
timestamp: '2024-01-01T10:00:00.000Z',
|
||||
isFunctionCall: false,
|
||||
isFunctionCallResponse: false,
|
||||
isToolCall: false,
|
||||
isToolCallResponse: false,
|
||||
isProviderSpecificContent: false
|
||||
},
|
||||
{
|
||||
role: Role.Assistant,
|
||||
content: 'Hi!',
|
||||
promptContent: '',
|
||||
functionCall: '',
|
||||
toolCall: '',
|
||||
timestamp: '2024-01-01T10:01:00.000Z',
|
||||
isFunctionCall: false,
|
||||
isFunctionCallResponse: false,
|
||||
isToolCall: false,
|
||||
isToolCallResponse: false,
|
||||
isProviderSpecificContent: false
|
||||
}
|
||||
]
|
||||
|
|
@ -499,7 +499,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
{
|
||||
role: Role.Assistant,
|
||||
content: 'Calling function',
|
||||
functionCall: JSON.stringify({ name: 'test_func', arguments: {} }),
|
||||
toolCall: JSON.stringify({ name: 'test_func', arguments: {} }),
|
||||
timestamp: '2024-01-01T10:00:00.000Z',
|
||||
toolId: 'tool_1',
|
||||
attachments: [],
|
||||
|
|
@ -519,8 +519,8 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
|
||||
const conversations = await service.getAllConversations();
|
||||
|
||||
expect(conversations[0].contents[0].functionCall).toBeDefined();
|
||||
expect(JSON.parse(conversations[0].contents[0].functionCall!)).toEqual({
|
||||
expect(conversations[0].contents[0].toolCall).toBeDefined();
|
||||
expect(JSON.parse(conversations[0].contents[0].toolCall!)).toEqual({
|
||||
name: 'test_func',
|
||||
arguments: {}
|
||||
});
|
||||
|
|
@ -648,7 +648,7 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
new ConversationContent({
|
||||
role: Role.Assistant,
|
||||
content: 'Function',
|
||||
functionCall: JSON.stringify({ name: 'test', arguments: { arg: 'val' } }),
|
||||
toolCall: JSON.stringify({ name: 'test', arguments: { arg: 'val' } }),
|
||||
timestamp: new Date('2024-01-01T10:05:00Z'),
|
||||
toolId: 'tool_xyz'
|
||||
}),
|
||||
|
|
@ -678,8 +678,8 @@ describe('ConversationFileSystemService - Integration Tests', () => {
|
|||
// Verify all data preserved
|
||||
expect(reconstructed.title).toBe(original.title);
|
||||
expect(reconstructed.contents).toHaveLength(4);
|
||||
expect(reconstructed.contents[2].functionCall).toBeDefined();
|
||||
expect(JSON.parse(reconstructed.contents[2].functionCall!)).toEqual({
|
||||
expect(reconstructed.contents[2].toolCall).toBeDefined();
|
||||
expect(JSON.parse(reconstructed.contents[2].toolCall!)).toEqual({
|
||||
name: 'test',
|
||||
arguments: { arg: 'val' }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Task completed',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -115,7 +115,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Task completed',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -152,7 +152,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -190,7 +190,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Task failed',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: false,
|
||||
|
|
@ -220,7 +220,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Failed',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: false,
|
||||
|
|
@ -251,15 +251,15 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
instruction: 'Search for files with tag #important'
|
||||
};
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
// Call search function
|
||||
yield {
|
||||
content: 'Searching',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{
|
||||
search_terms: ['#important'],
|
||||
|
|
@ -273,7 +273,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Complete task
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -291,7 +291,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
|
||||
expect(mockAIToolService.performAITool).toHaveBeenCalled();
|
||||
expect(result?.success).toBe(true);
|
||||
expect(functionCallCount).toBe(2);
|
||||
expect(toolCallCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should call ReadVaultFiles during execution', async () => {
|
||||
|
|
@ -301,14 +301,14 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
instruction: 'Read contents of note.md'
|
||||
};
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
yield {
|
||||
content: 'Reading',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.ReadVaultFiles,
|
||||
{
|
||||
file_paths: ['note.md'],
|
||||
|
|
@ -321,7 +321,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -348,14 +348,14 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
instruction: 'Create new file with content'
|
||||
};
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
yield {
|
||||
content: 'Writing',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.WriteVaultFile,
|
||||
{
|
||||
file_path: 'new-note.md',
|
||||
|
|
@ -369,7 +369,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -396,14 +396,14 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
instruction: 'Search, read, and update files'
|
||||
};
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
yield {
|
||||
content: 'Searching',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{
|
||||
search_terms: ['TODO'],
|
||||
|
|
@ -413,10 +413,10 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
),
|
||||
isComplete: true
|
||||
};
|
||||
} else if (functionCallCount === 2) {
|
||||
} else if (toolCallCount === 2) {
|
||||
yield {
|
||||
content: 'Reading',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.ReadVaultFiles,
|
||||
{
|
||||
file_paths: ['found.md'],
|
||||
|
|
@ -426,10 +426,10 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
),
|
||||
isComplete: true
|
||||
};
|
||||
} else if (functionCallCount === 3) {
|
||||
} else if (toolCallCount === 3) {
|
||||
yield {
|
||||
content: 'Writing',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.PatchVaultFile,
|
||||
{
|
||||
file_path: 'found.md',
|
||||
|
|
@ -443,7 +443,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -486,7 +486,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Complete on retry
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -527,7 +527,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Complete on third attempt
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -574,7 +574,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -640,7 +640,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Complete on third attempt (max depth)
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -680,7 +680,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -710,7 +710,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -747,7 +747,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -783,7 +783,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Invalid: missing description
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true
|
||||
|
|
@ -797,7 +797,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Valid completion
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -832,7 +832,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Invalid: success is not boolean
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: 'yes',
|
||||
|
|
@ -846,7 +846,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
// Valid
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -875,14 +875,14 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
instruction: 'Do task with updates'
|
||||
};
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
yield {
|
||||
content: 'Searching',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{
|
||||
search_terms: ['test'],
|
||||
|
|
@ -895,7 +895,7 @@ describe('ExecutionAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -159,7 +159,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Replan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -201,7 +201,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -232,7 +232,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Failed',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: false,
|
||||
|
|
@ -264,7 +264,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -400,7 +400,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
searchCalled = true;
|
||||
yield {
|
||||
content: 'Searching',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{
|
||||
search_terms: ['test'],
|
||||
|
|
@ -413,7 +413,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -445,13 +445,13 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
};
|
||||
const callbacks = createMockCallbacks();
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
yield {
|
||||
content: 'Searching',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{
|
||||
search_terms: ['test'],
|
||||
|
|
@ -464,7 +464,7 @@ describe('Multi-Agent Integration Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Done',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Creating plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -135,7 +135,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -169,7 +169,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
yield {
|
||||
content: 'Replan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -210,7 +210,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Invalid: missing required fields
|
||||
yield {
|
||||
content: 'Invalid plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -228,7 +228,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Valid plan
|
||||
yield {
|
||||
content: 'Valid plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -274,7 +274,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Submit plan on retry
|
||||
yield {
|
||||
content: 'Plan ready',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -310,15 +310,15 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
content: 'Search for relevant notes'
|
||||
}));
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
// Ask user question
|
||||
yield {
|
||||
content: 'Question',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.AskUserQuestionPlanning,
|
||||
{
|
||||
question: 'What topic should I search for?',
|
||||
|
|
@ -332,7 +332,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Submit plan with user's answer incorporated
|
||||
yield {
|
||||
content: 'Plan with answer',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -354,7 +354,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
|
||||
expect(callbacks.onUserQuestion).toHaveBeenCalledWith('What topic should I search for?');
|
||||
expect(result).toBeDefined();
|
||||
expect(functionCallCount).toBe(2);
|
||||
expect(toolCallCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle multiple user questions in planning', async () => {
|
||||
|
|
@ -369,14 +369,14 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
content: 'Organize files'
|
||||
}));
|
||||
|
||||
let functionCallCount = 0;
|
||||
let toolCallCount = 0;
|
||||
|
||||
mockAI.streamRequest.mockImplementation(async function* () {
|
||||
functionCallCount++;
|
||||
if (functionCallCount === 1) {
|
||||
toolCallCount++;
|
||||
if (toolCallCount === 1) {
|
||||
yield {
|
||||
content: 'Q1',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.AskUserQuestionPlanning,
|
||||
{
|
||||
question: 'What file type?',
|
||||
|
|
@ -386,10 +386,10 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
),
|
||||
isComplete: true
|
||||
};
|
||||
} else if (functionCallCount === 2) {
|
||||
} else if (toolCallCount === 2) {
|
||||
yield {
|
||||
content: 'Q2',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.AskUserQuestionPlanning,
|
||||
{
|
||||
question: 'Which folder?',
|
||||
|
|
@ -402,7 +402,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -442,7 +442,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Invalid: missing question
|
||||
yield {
|
||||
content: 'Invalid',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.AskUserQuestionPlanning,
|
||||
{
|
||||
// Missing question field
|
||||
|
|
@ -456,7 +456,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Valid plan
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -496,7 +496,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
searchCalled = true;
|
||||
yield {
|
||||
content: 'Searching',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SearchVaultFiles,
|
||||
{
|
||||
search_terms: ['test'],
|
||||
|
|
@ -509,7 +509,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -548,7 +548,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
readCalled = true;
|
||||
yield {
|
||||
content: 'Reading',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.ReadVaultFiles,
|
||||
{
|
||||
file_paths: ['test.md'],
|
||||
|
|
@ -561,7 +561,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -601,7 +601,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Try to write file (not allowed in planning)
|
||||
yield {
|
||||
content: 'Writing',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.WriteVaultFile,
|
||||
{
|
||||
file_path: 'test.md',
|
||||
|
|
@ -615,7 +615,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Submit valid plan
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -659,7 +659,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Try to complete task (execution agent tool)
|
||||
yield {
|
||||
content: 'Completing',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteTask,
|
||||
{
|
||||
success: true,
|
||||
|
|
@ -672,7 +672,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -711,7 +711,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Try to complete step (orchestration agent tool)
|
||||
yield {
|
||||
content: 'Completing',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.CompleteStep,
|
||||
{
|
||||
confirm_completion: true
|
||||
|
|
@ -723,7 +723,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
} else {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -796,7 +796,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Submit plan on third attempt (just before max depth)
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
@ -834,7 +834,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
// Empty plan is valid according to schema
|
||||
yield {
|
||||
content: 'Empty plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: []
|
||||
|
|
@ -883,7 +883,7 @@ describe('PlanningAgent - Unit Tests', () => {
|
|||
if (hasRetryMessage) {
|
||||
yield {
|
||||
content: 'Plan',
|
||||
functionCall: new AIToolCall(
|
||||
toolCall: new AIToolCall(
|
||||
AITool.SubmitPlan,
|
||||
{
|
||||
steps: [
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ describe('StreamingService', () => {
|
|||
|
||||
it('should pass function call from parser', async () => {
|
||||
const chunks = [
|
||||
'data: {"content":"","done":false,"functionCall":{"name":"test_func","args":{}}}\n',
|
||||
'data: {"content":"","done":false,"toolCall":{"name":"test_func","args":{}}}\n',
|
||||
'data: {"content":"Done","done":true}\n'
|
||||
];
|
||||
|
||||
|
|
@ -416,12 +416,12 @@ describe('StreamingService', () => {
|
|||
body: createMockStream(chunks)
|
||||
});
|
||||
|
||||
const parserWithFunctionCall = (chunk: string): IStreamChunk => {
|
||||
const parserWithToolCall = (chunk: string): IStreamChunk => {
|
||||
const data = JSON.parse(chunk);
|
||||
return {
|
||||
content: data.content || '',
|
||||
isComplete: data.done || false,
|
||||
functionCall: data.functionCall
|
||||
toolCall: data.toolCall
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -429,12 +429,12 @@ describe('StreamingService', () => {
|
|||
for await (const chunk of service.streamRequest(
|
||||
'https://api.example.com/stream',
|
||||
{},
|
||||
parserWithFunctionCall
|
||||
parserWithToolCall
|
||||
)) {
|
||||
results.push(chunk);
|
||||
}
|
||||
|
||||
expect(results[0].functionCall).toEqual({ name: 'test_func', args: {} });
|
||||
expect(results[0].toolCall).toEqual({ name: 'test_func', args: {} });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue