ui: Linting & Formatting scripts (#26819)
This commit is contained in:
@@ -20,25 +20,14 @@
|
||||
* @see mcpStore in stores/mcp.svelte.ts for MCP operations
|
||||
*/
|
||||
|
||||
import { ChatService } from '$lib/services';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { permissionsStore } from '$lib/stores/permissions.svelte';
|
||||
import { BuiltInTool, ToolSource, ToolPermissionDecision } from '$lib/enums';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import { ToolsService } from '$lib/services/tools.service';
|
||||
import { SandboxService } from '$lib/services/sandbox.service';
|
||||
import { isAbortError } from '$lib/utils';
|
||||
import { DEFAULT_AGENTIC_CONFIG, NEWLINE } from '$lib/constants';
|
||||
import {
|
||||
IMAGE_MIME_TO_EXTENSION,
|
||||
DATA_URI_BASE64_REGEX,
|
||||
MCP_ATTACHMENT_NAME_PREFIX,
|
||||
DEFAULT_IMAGE_EXTENSION
|
||||
DEFAULT_IMAGE_EXTENSION,
|
||||
IMAGE_MIME_TO_EXTENSION,
|
||||
MCP_ATTACHMENT_NAME_PREFIX
|
||||
} from '$lib/constants';
|
||||
import { BuiltInTool, ToolPermissionDecision, ToolSource } from '$lib/enums';
|
||||
import {
|
||||
AttachmentType,
|
||||
ContentPartType,
|
||||
@@ -46,49 +35,60 @@ import {
|
||||
MimeTypePrefix,
|
||||
ToolCallType
|
||||
} from '$lib/enums';
|
||||
import { ChatService } from '$lib/services';
|
||||
import { SandboxService } from '$lib/services/sandbox.service';
|
||||
import { ToolsService } from '$lib/services/tools.service';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { permissionsStore } from '$lib/stores/permissions.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import type {
|
||||
AgenticConfig,
|
||||
AgenticFlowParams,
|
||||
AgenticFlowResult,
|
||||
AgenticSession,
|
||||
AgenticConfig,
|
||||
SettingsConfigType,
|
||||
McpServerOverride,
|
||||
MCPToolCall
|
||||
MCPToolCall,
|
||||
SettingsConfigType
|
||||
} from '$lib/types';
|
||||
import type {
|
||||
AgenticMessage,
|
||||
AgenticToolCallList,
|
||||
AgenticFlowCallbacks,
|
||||
AgenticFlowOptions,
|
||||
AgenticMessage,
|
||||
AgenticToolCallList,
|
||||
SteeringMessage
|
||||
} from '$lib/types/agentic';
|
||||
import type {
|
||||
ApiChatCompletionToolCall,
|
||||
ApiChatMessageData,
|
||||
ApiChatMessageContentPart
|
||||
ApiChatMessageContentPart,
|
||||
ApiChatMessageData
|
||||
} from '$lib/types/api';
|
||||
import type {
|
||||
ChatMessageAgenticTimings,
|
||||
ChatMessageAgenticTurnStats,
|
||||
ChatMessagePromptProgress,
|
||||
ChatMessageTimings,
|
||||
ChatMessageAgenticTimings,
|
||||
ChatMessageToolCallTiming,
|
||||
ChatMessageAgenticTurnStats
|
||||
ChatMessageToolCallTiming
|
||||
} from '$lib/types/chat';
|
||||
import type {
|
||||
DatabaseMessage,
|
||||
DatabaseMessageExtra,
|
||||
DatabaseMessageExtraImageFile
|
||||
} from '$lib/types/database';
|
||||
import { isAbortError } from '$lib/utils';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
function createDefaultSession(): AgenticSession {
|
||||
return {
|
||||
isRunning: false,
|
||||
currentTurn: 0,
|
||||
totalToolCalls: 0,
|
||||
executingToolCallId: null,
|
||||
isRunning: false,
|
||||
lastError: null,
|
||||
streamingToolCall: null,
|
||||
pendingPermissionRequest: null,
|
||||
executingToolCallId: null
|
||||
streamingToolCall: null,
|
||||
totalToolCalls: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,36 +100,39 @@ function toAgenticMessages(messages: ApiChatMessageData[]): AgenticMessage[] {
|
||||
message.tool_calls.length > 0
|
||||
) {
|
||||
return {
|
||||
role: MessageRole.ASSISTANT,
|
||||
content: message.content,
|
||||
reasoning_content: message.reasoning_content,
|
||||
role: MessageRole.ASSISTANT,
|
||||
tool_calls: message.tool_calls.map((call, index) => ({
|
||||
id: call.id ?? `call_${index}`,
|
||||
type: (call.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION,
|
||||
function: {
|
||||
name: call.function?.name ?? '',
|
||||
arguments: call.function?.arguments ?? ''
|
||||
}
|
||||
arguments: call.function?.arguments ?? '',
|
||||
name: call.function?.name ?? ''
|
||||
},
|
||||
id: call.id ?? `call_${index}`,
|
||||
type: (call.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION
|
||||
}))
|
||||
} satisfies AgenticMessage;
|
||||
}
|
||||
|
||||
if (message.role === MessageRole.ASSISTANT) {
|
||||
return {
|
||||
role: MessageRole.ASSISTANT,
|
||||
content: message.content,
|
||||
reasoning_content: message.reasoning_content
|
||||
reasoning_content: message.reasoning_content,
|
||||
role: MessageRole.ASSISTANT
|
||||
} satisfies AgenticMessage;
|
||||
}
|
||||
|
||||
if (message.role === MessageRole.TOOL && message.tool_call_id) {
|
||||
return {
|
||||
content: typeof message.content === 'string' ? message.content : '',
|
||||
role: MessageRole.TOOL,
|
||||
tool_call_id: message.tool_call_id,
|
||||
content: typeof message.content === 'string' ? message.content : ''
|
||||
tool_call_id: message.tool_call_id
|
||||
} satisfies AgenticMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
role: message.role as MessageRole.SYSTEM | MessageRole.USER,
|
||||
content: message.content
|
||||
content: message.content,
|
||||
role: message.role as MessageRole.SYSTEM | MessageRole.USER
|
||||
} satisfies AgenticMessage;
|
||||
});
|
||||
}
|
||||
@@ -159,20 +162,24 @@ class AgenticStore {
|
||||
for (const session of this._sessions.values()) {
|
||||
if (session.isRunning) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getSession(conversationId: string): AgenticSession {
|
||||
let session = this._sessions.get(conversationId);
|
||||
|
||||
if (!session) {
|
||||
session = createDefaultSession();
|
||||
this._sessions.set(conversationId, session);
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private updateSession(conversationId: string, update: Partial<AgenticSession>): void {
|
||||
const session = this.getSession(conversationId);
|
||||
|
||||
this._sessions.set(conversationId, { ...session, ...update });
|
||||
}
|
||||
|
||||
@@ -182,9 +189,11 @@ class AgenticStore {
|
||||
|
||||
getActiveSessions(): Array<{ conversationId: string; session: AgenticSession }> {
|
||||
const active: Array<{ conversationId: string; session: AgenticSession }> = [];
|
||||
|
||||
for (const [conversationId, session] of this._sessions.entries()) {
|
||||
if (session.isRunning) active.push({ conversationId, session });
|
||||
}
|
||||
|
||||
return active;
|
||||
}
|
||||
|
||||
@@ -224,6 +233,7 @@ class AgenticStore {
|
||||
|
||||
resolveContinue(conversationId: string, shouldContinue: boolean): void {
|
||||
const resolver = this._continueResolvers.get(conversationId);
|
||||
|
||||
if (resolver) {
|
||||
this._continueResolvers.delete(conversationId);
|
||||
resolver(shouldContinue);
|
||||
@@ -232,6 +242,7 @@ class AgenticStore {
|
||||
|
||||
resolvePermission(conversationId: string, decision: ToolPermissionDecision): void {
|
||||
const resolver = this._permissionResolvers.get(conversationId);
|
||||
|
||||
if (resolver) {
|
||||
this._permissionResolvers.delete(conversationId);
|
||||
resolver(decision);
|
||||
@@ -279,8 +290,11 @@ class AgenticStore {
|
||||
*/
|
||||
consumePendingSteeringMessage(conversationId: string): SteeringMessage | null {
|
||||
const msg = this._steeringMessages.get(conversationId);
|
||||
|
||||
if (!msg) return null;
|
||||
|
||||
this._steeringMessages.delete(conversationId);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -291,6 +305,7 @@ class AgenticStore {
|
||||
toolsStore.builtinTools.length > 0 ||
|
||||
toolsStore.frontendTools.length > 0 ||
|
||||
toolsStore.customTools.length > 0;
|
||||
|
||||
return {
|
||||
enabled: hasTools && DEFAULT_AGENTIC_CONFIG.enabled,
|
||||
maxTurns
|
||||
@@ -299,8 +314,11 @@ class AgenticStore {
|
||||
|
||||
private parseToolArguments(args: string | Record<string, unknown>): Record<string, unknown> {
|
||||
if (typeof args === 'object') return args;
|
||||
|
||||
const trimmed = args.trim();
|
||||
|
||||
if (trimmed === '') return {};
|
||||
|
||||
return JSON.parse(trimmed) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -311,21 +329,24 @@ class AgenticStore {
|
||||
signal?: AbortSignal
|
||||
): Promise<ToolPermissionDecision> {
|
||||
const permissionKey = toolsStore.getPermissionKey(toolName);
|
||||
|
||||
if (permissionKey && permissionsStore.hasTool(permissionKey)) {
|
||||
return ToolPermissionDecision.ONCE;
|
||||
}
|
||||
|
||||
this._pendingPermissions.set(conversationId, { toolName, serverLabel });
|
||||
this._pendingPermissions.set(conversationId, { serverLabel, toolName });
|
||||
|
||||
return new Promise<ToolPermissionDecision>((resolve) => {
|
||||
if (signal?.aborted) {
|
||||
this._pendingPermissions.set(conversationId, null);
|
||||
resolve(ToolPermissionDecision.DENY);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._permissionResolvers.set(conversationId, (decision) => {
|
||||
this._pendingPermissions.set(conversationId, null);
|
||||
|
||||
if (decision === ToolPermissionDecision.ALWAYS && permissionKey) {
|
||||
permissionsStore.allowTool(permissionKey);
|
||||
} else if (decision === ToolPermissionDecision.ALWAYS_SERVER) {
|
||||
@@ -337,8 +358,10 @@ class AgenticStore {
|
||||
)
|
||||
.map((t) => toolsStore.getPermissionKey(t.definition.function.name)!)
|
||||
.filter((k): k is string => k !== null);
|
||||
|
||||
permissionsStore.allowTools(serverToolKeys);
|
||||
}
|
||||
|
||||
resolve(decision);
|
||||
});
|
||||
|
||||
@@ -346,6 +369,7 @@ class AgenticStore {
|
||||
'abort',
|
||||
() => {
|
||||
const resolver = this._permissionResolvers.get(conversationId);
|
||||
|
||||
if (resolver) {
|
||||
this._permissionResolvers.delete(conversationId);
|
||||
this._pendingPermissions.set(conversationId, null);
|
||||
@@ -364,6 +388,7 @@ class AgenticStore {
|
||||
if (signal?.aborted) {
|
||||
this._pendingContinueRequests.set(conversationId, false);
|
||||
resolve(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -376,6 +401,7 @@ class AgenticStore {
|
||||
'abort',
|
||||
() => {
|
||||
const resolver = this._continueResolvers.get(conversationId);
|
||||
|
||||
if (resolver) {
|
||||
this._continueResolvers.delete(conversationId);
|
||||
this._pendingContinueRequests.set(conversationId, false);
|
||||
@@ -388,7 +414,7 @@ class AgenticStore {
|
||||
}
|
||||
|
||||
async runAgenticFlow(params: AgenticFlowParams): Promise<AgenticFlowResult> {
|
||||
const { conversationId, messages, options = {}, callbacks, signal, perChatOverrides } = params;
|
||||
const { callbacks, conversationId, messages, options = {}, perChatOverrides, signal } = params;
|
||||
|
||||
// Clear any pending permissions/continue requests for this conversation when starting a new flow
|
||||
this._pendingPermissions.set(conversationId, null);
|
||||
@@ -403,9 +429,11 @@ class AgenticStore {
|
||||
}
|
||||
|
||||
const agenticConfig = this.getConfig(config(), perChatOverrides);
|
||||
|
||||
if (!agenticConfig.enabled) return { handled: false };
|
||||
|
||||
const hasMcpServers = mcpStore.hasEnabledServers(perChatOverrides);
|
||||
|
||||
if (hasMcpServers) {
|
||||
const initialized = await mcpStore.ensureInitialized(perChatOverrides);
|
||||
|
||||
@@ -415,6 +443,7 @@ class AgenticStore {
|
||||
}
|
||||
|
||||
const tools = toolsStore.getEnabledToolsForLLM();
|
||||
|
||||
if (tools.length === 0) {
|
||||
return { handled: false };
|
||||
}
|
||||
@@ -428,42 +457,48 @@ class AgenticStore {
|
||||
return ChatService.convertDbMessageToApiChatMessageData(
|
||||
msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] }
|
||||
);
|
||||
|
||||
return msg as ApiChatMessageData;
|
||||
})
|
||||
)
|
||||
).filter((msg: { role: ChatRole; content: string | ApiChatMessageContentPart[] }) => {
|
||||
if (msg.role === MessageRole.SYSTEM) {
|
||||
const content = typeof msg.content === 'string' ? msg.content : '';
|
||||
|
||||
return content.trim().length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.updateSession(conversationId, {
|
||||
isRunning: true,
|
||||
currentTurn: 0,
|
||||
totalToolCalls: 0,
|
||||
lastError: null
|
||||
isRunning: true,
|
||||
lastError: null,
|
||||
totalToolCalls: 0
|
||||
});
|
||||
|
||||
if (hasMcpServers) mcpStore.acquireConnection();
|
||||
|
||||
try {
|
||||
await this.executeAgenticLoop({
|
||||
agenticConfig,
|
||||
callbacks,
|
||||
conversationId,
|
||||
messages: normalizedMessages,
|
||||
options,
|
||||
tools,
|
||||
agenticConfig,
|
||||
callbacks,
|
||||
signal
|
||||
signal,
|
||||
tools
|
||||
});
|
||||
|
||||
return { handled: true };
|
||||
} catch (error) {
|
||||
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
this.updateSession(conversationId, { lastError: normalizedError });
|
||||
callbacks.onError?.(normalizedError);
|
||||
return { handled: true, error: normalizedError };
|
||||
|
||||
return { error: normalizedError, handled: true };
|
||||
} finally {
|
||||
this.updateSession(conversationId, { isRunning: false });
|
||||
|
||||
@@ -486,40 +521,40 @@ class AgenticStore {
|
||||
callbacks: AgenticFlowCallbacks;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<void> {
|
||||
const { conversationId, messages, options, tools, agenticConfig, callbacks, signal } = params;
|
||||
const { agenticConfig, callbacks, conversationId, messages, options, signal, tools } = params;
|
||||
const {
|
||||
onChunk,
|
||||
onReasoningChunk,
|
||||
onToolCallsStreaming,
|
||||
onAttachments,
|
||||
onModel,
|
||||
onCompletionId,
|
||||
onAssistantTurnComplete,
|
||||
createToolResultMessage,
|
||||
updateToolResultMessage,
|
||||
createAssistantMessage,
|
||||
createToolResultMessage,
|
||||
onAssistantTurnComplete,
|
||||
onAttachments,
|
||||
onChunk,
|
||||
onCompletionId,
|
||||
onFlowComplete,
|
||||
onModel,
|
||||
onReasoningChunk,
|
||||
onTimings,
|
||||
onTurnComplete
|
||||
onToolCallsStreaming,
|
||||
onTurnComplete,
|
||||
updateToolResultMessage
|
||||
} = callbacks;
|
||||
|
||||
const sessionMessages: AgenticMessage[] = toAgenticMessages(messages);
|
||||
|
||||
let capturedTimings: ChatMessageTimings | undefined;
|
||||
let totalToolCallCount = 0;
|
||||
|
||||
const agenticTimings: ChatMessageAgenticTimings = {
|
||||
turns: 0,
|
||||
llm: { predicted_ms: 0, predicted_n: 0, prompt_ms: 0, prompt_n: 0 },
|
||||
perTurn: [],
|
||||
toolCalls: [],
|
||||
toolCallsCount: 0,
|
||||
toolsMs: 0,
|
||||
toolCalls: [],
|
||||
perTurn: [],
|
||||
llm: { predicted_n: 0, predicted_ms: 0, prompt_n: 0, prompt_ms: 0 }
|
||||
turns: 0
|
||||
};
|
||||
const maxTurns = agenticConfig.maxTurns;
|
||||
|
||||
const effectiveModel = options.model || modelsStore.models[0]?.model || '';
|
||||
|
||||
let turn = 0;
|
||||
|
||||
while (true) {
|
||||
if (turn >= maxTurns) {
|
||||
// Turn limit reached - ask user whether to continue
|
||||
@@ -530,6 +565,7 @@ class AgenticStore {
|
||||
|
||||
if (!shouldContinue || signal?.aborted) {
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -542,6 +578,7 @@ class AgenticStore {
|
||||
|
||||
if (signal?.aborted) {
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -558,10 +595,10 @@ class AgenticStore {
|
||||
let turnTimings: ChatMessageTimings | undefined;
|
||||
|
||||
const turnStats: ChatMessageAgenticTurnStats = {
|
||||
turn: turn + 1,
|
||||
llm: { predicted_n: 0, predicted_ms: 0, prompt_n: 0, prompt_ms: 0 },
|
||||
llm: { predicted_ms: 0, predicted_n: 0, prompt_ms: 0, prompt_n: 0 },
|
||||
toolCalls: [],
|
||||
toolsMs: 0
|
||||
toolsMs: 0,
|
||||
turn: turn + 1
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -569,16 +606,30 @@ class AgenticStore {
|
||||
sessionMessages as ApiChatMessageData[],
|
||||
{
|
||||
...options,
|
||||
stream: true,
|
||||
tools: tools.length > 0 ? tools : undefined,
|
||||
onChunk: (chunk: string) => {
|
||||
turnContent += chunk;
|
||||
onChunk?.(chunk);
|
||||
},
|
||||
onComplete: () => {
|
||||
/* Completion handled after sendMessage resolves */
|
||||
},
|
||||
onCompletionId,
|
||||
onError: (error: Error) => {
|
||||
throw error;
|
||||
},
|
||||
onModel,
|
||||
onReasoningChunk: (chunk: string) => {
|
||||
turnReasoningContent += chunk;
|
||||
onReasoningChunk?.(chunk);
|
||||
},
|
||||
onTimings: (timings?: ChatMessageTimings, progress?: ChatMessagePromptProgress) => {
|
||||
onTimings?.(timings, progress);
|
||||
|
||||
if (timings) {
|
||||
capturedTimings = timings;
|
||||
turnTimings = timings;
|
||||
}
|
||||
},
|
||||
onToolCallChunk: (serialized: string) => {
|
||||
try {
|
||||
turnToolCalls = JSON.parse(serialized) as ApiChatCompletionToolCall[];
|
||||
@@ -589,6 +640,7 @@ class AgenticStore {
|
||||
const name = turnToolCalls[0].function.name || '';
|
||||
const args = turnToolCalls[0].function.arguments || '';
|
||||
const argsLengthBucket = Math.floor(args.length / 100);
|
||||
|
||||
if (
|
||||
name !== lastStreamingToolCallName ||
|
||||
argsLengthBucket !== lastStreamingToolCallArgsLength
|
||||
@@ -596,7 +648,7 @@ class AgenticStore {
|
||||
lastStreamingToolCallName = name;
|
||||
lastStreamingToolCallArgsLength = argsLengthBucket;
|
||||
this.updateSession(conversationId, {
|
||||
streamingToolCall: { name, arguments: args }
|
||||
streamingToolCall: { arguments: args, name }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -604,21 +656,8 @@ class AgenticStore {
|
||||
/* Ignore parse errors during streaming */
|
||||
}
|
||||
},
|
||||
onModel,
|
||||
onCompletionId,
|
||||
onTimings: (timings?: ChatMessageTimings, progress?: ChatMessagePromptProgress) => {
|
||||
onTimings?.(timings, progress);
|
||||
if (timings) {
|
||||
capturedTimings = timings;
|
||||
turnTimings = timings;
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
/* Completion handled after sendMessage resolves */
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
throw error;
|
||||
}
|
||||
stream: true,
|
||||
tools: tools.length > 0 ? tools : undefined
|
||||
},
|
||||
conversationId,
|
||||
signal
|
||||
@@ -646,9 +685,12 @@ class AgenticStore {
|
||||
undefined
|
||||
);
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedError = error instanceof Error ? error : new Error('LLM stream error');
|
||||
|
||||
// preserve partial output as is, the outer error dialog informs the user separately
|
||||
await onAssistantTurnComplete?.(
|
||||
turnContent,
|
||||
@@ -657,6 +699,7 @@ class AgenticStore {
|
||||
undefined
|
||||
);
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
throw normalizedError;
|
||||
}
|
||||
|
||||
@@ -672,6 +715,7 @@ class AgenticStore {
|
||||
undefined
|
||||
);
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -686,6 +730,7 @@ class AgenticStore {
|
||||
turnToolCalls.length > 0 ? this.normalizeToolCalls(turnToolCalls) : undefined
|
||||
);
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -711,6 +756,7 @@ class AgenticStore {
|
||||
|
||||
// Normalize and save assistant turn with tool calls
|
||||
const normalizedCalls = this.normalizeToolCalls(turnToolCalls);
|
||||
|
||||
if (normalizedCalls.length === 0) {
|
||||
await onAssistantTurnComplete?.(
|
||||
turnContent,
|
||||
@@ -719,6 +765,7 @@ class AgenticStore {
|
||||
undefined
|
||||
);
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -735,9 +782,9 @@ class AgenticStore {
|
||||
|
||||
// Add assistant message to session history
|
||||
sessionMessages.push({
|
||||
role: MessageRole.ASSISTANT,
|
||||
content: turnContent || undefined,
|
||||
reasoning_content: turnReasoningContent || undefined,
|
||||
role: MessageRole.ASSISTANT,
|
||||
tool_calls: normalizedCalls
|
||||
});
|
||||
|
||||
@@ -747,6 +794,7 @@ class AgenticStore {
|
||||
|
||||
if (signal?.aborted) {
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -758,21 +806,23 @@ class AgenticStore {
|
||||
for (let j = i; j < normalizedCalls.length; j++) {
|
||||
const remainingCall = normalizedCalls[j];
|
||||
const interruptedContent = 'Tool execution was interrupted by a new user message.';
|
||||
|
||||
if (createToolResultMessage) {
|
||||
await createToolResultMessage(remainingCall.id, interruptedContent);
|
||||
}
|
||||
|
||||
sessionMessages.push({
|
||||
content: interruptedContent,
|
||||
role: MessageRole.TOOL,
|
||||
tool_call_id: remainingCall.id,
|
||||
content: interruptedContent
|
||||
tool_call_id: remainingCall.id
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const toolName = toolCall.function.name;
|
||||
const serverLabel = toolsStore.getToolServerLabel(toolName);
|
||||
|
||||
// Ask for permission before executing the tool
|
||||
const permission = await this.requestPermission(
|
||||
conversationId,
|
||||
@@ -786,6 +836,7 @@ class AgenticStore {
|
||||
|
||||
if (signal?.aborted) {
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -815,14 +866,17 @@ class AgenticStore {
|
||||
const args = this.parseToolArguments(toolCall.function.arguments);
|
||||
const cwd = conversationsStore.activeConversation?.cwd;
|
||||
const msg = await createToolResultMessage(toolCall.id, '', undefined, cwd);
|
||||
|
||||
createdToolResultMessageId = msg.id;
|
||||
|
||||
let accumulated = '';
|
||||
|
||||
for await (const ev of ToolsService.streamTool(toolName, args, signal, cwd)) {
|
||||
if (ev.chunk !== null) {
|
||||
accumulated += ev.chunk;
|
||||
await updateToolResultMessage(msg.id, accumulated);
|
||||
}
|
||||
|
||||
if (ev.done) {
|
||||
if (ev.error) {
|
||||
accumulated = accumulated
|
||||
@@ -831,6 +885,7 @@ class AgenticStore {
|
||||
await updateToolResultMessage(msg.id, accumulated);
|
||||
toolSuccess = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -852,8 +907,8 @@ class AgenticStore {
|
||||
if (executionResult.isError) toolSuccess = false;
|
||||
} else {
|
||||
const mcpCall: MCPToolCall = {
|
||||
id: toolCall.id,
|
||||
function: { name: toolName, arguments: toolCall.function.arguments }
|
||||
function: { arguments: toolCall.function.arguments, name: toolName },
|
||||
id: toolCall.id
|
||||
};
|
||||
const executionResult = await mcpStore.executeTool(mcpCall, signal);
|
||||
|
||||
@@ -863,14 +918,17 @@ class AgenticStore {
|
||||
if (isAbortError(error)) {
|
||||
this.updateSession(conversationId, { executingToolCallId: null });
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Carry the partial stream contents already mirrored to the UI -
|
||||
// they show up as live output even if the stream broke off mid-run.
|
||||
result = result
|
||||
? `${result}\nError: ${error instanceof Error ? error.message : String(error)}`
|
||||
: `Error: ${error instanceof Error ? error.message : String(error)}`;
|
||||
toolSuccess = false;
|
||||
|
||||
if (createdToolResultMessageId && updateToolResultMessage) {
|
||||
await updateToolResultMessage(createdToolResultMessageId, result);
|
||||
}
|
||||
@@ -881,8 +939,8 @@ class AgenticStore {
|
||||
|
||||
const toolDurationMs = performance.now() - toolStartTime;
|
||||
const toolTiming: ChatMessageToolCallTiming = {
|
||||
name: toolCall.function.name,
|
||||
duration_ms: Math.round(toolDurationMs),
|
||||
name: toolCall.function.name,
|
||||
success: toolSuccess
|
||||
};
|
||||
|
||||
@@ -894,10 +952,11 @@ class AgenticStore {
|
||||
|
||||
if (signal?.aborted) {
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { cleanedResult, attachments } = this.extractBase64Attachments(result);
|
||||
const { attachments, cleanedResult } = this.extractBase64Attachments(result);
|
||||
|
||||
// For streaming tools the result message was created empty
|
||||
// at the start of execution and updated in place as chunks
|
||||
@@ -906,8 +965,10 @@ class AgenticStore {
|
||||
// the final accumulator (rare, since chunks usually don't
|
||||
// carry image data URIs) and emit the attachments callback.
|
||||
let toolResultMessage: DatabaseMessage | undefined;
|
||||
|
||||
if (createdToolResultMessageId) {
|
||||
toolResultMessage = { id: createdToolResultMessageId } as DatabaseMessage;
|
||||
|
||||
if (attachments.length > 0 && updateToolResultMessage) {
|
||||
await updateToolResultMessage(createdToolResultMessageId, cleanedResult, attachments);
|
||||
}
|
||||
@@ -925,16 +986,17 @@ class AgenticStore {
|
||||
|
||||
// Build content parts for session history (including images for vision models)
|
||||
const contentParts: ApiChatMessageContentPart[] = [
|
||||
{ type: ContentPartType.TEXT, text: cleanedResult }
|
||||
{ text: cleanedResult, type: ContentPartType.TEXT }
|
||||
];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.type === AttachmentType.IMAGE) {
|
||||
if (modelsStore.modelSupportsVision(effectiveModel)) {
|
||||
contentParts.push({
|
||||
type: ContentPartType.IMAGE_URL,
|
||||
image_url: {
|
||||
url: (attachment as DatabaseMessageExtraImageFile).base64Url
|
||||
}
|
||||
},
|
||||
type: ContentPartType.IMAGE_URL
|
||||
});
|
||||
} else {
|
||||
console.info(
|
||||
@@ -945,9 +1007,9 @@ class AgenticStore {
|
||||
}
|
||||
|
||||
sessionMessages.push({
|
||||
content: contentParts.length === 1 ? cleanedResult : contentParts,
|
||||
role: MessageRole.TOOL,
|
||||
tool_call_id: toolCall.id,
|
||||
content: contentParts.length === 1 ? cleanedResult : contentParts
|
||||
tool_call_id: toolCall.id
|
||||
});
|
||||
}
|
||||
|
||||
@@ -955,6 +1017,7 @@ class AgenticStore {
|
||||
agenticTimings.perTurn!.push(turnStats);
|
||||
|
||||
const intermediateTimings = this.buildFinalTimings(capturedTimings, agenticTimings);
|
||||
|
||||
if (intermediateTimings) onTurnComplete?.(intermediateTimings);
|
||||
}
|
||||
|
||||
@@ -964,6 +1027,7 @@ class AgenticStore {
|
||||
'[AgenticStore] Steering message detected after tool execution, exiting agentic flow'
|
||||
);
|
||||
onFlowComplete?.(this.buildFinalTimings(capturedTimings, agenticTimings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -976,25 +1040,27 @@ class AgenticStore {
|
||||
agenticTimings: ChatMessageAgenticTimings
|
||||
): ChatMessageTimings | undefined {
|
||||
if (agenticTimings.toolCallsCount === 0) return capturedTimings;
|
||||
|
||||
return {
|
||||
predicted_n: capturedTimings?.predicted_n,
|
||||
predicted_ms: capturedTimings?.predicted_ms,
|
||||
prompt_n: capturedTimings?.prompt_n,
|
||||
prompt_ms: capturedTimings?.prompt_ms,
|
||||
agentic: agenticTimings,
|
||||
cache_n: capturedTimings?.cache_n,
|
||||
agentic: agenticTimings
|
||||
predicted_ms: capturedTimings?.predicted_ms,
|
||||
predicted_n: capturedTimings?.predicted_n,
|
||||
prompt_ms: capturedTimings?.prompt_ms,
|
||||
prompt_n: capturedTimings?.prompt_n
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeToolCalls(toolCalls: ApiChatCompletionToolCall[]): AgenticToolCallList {
|
||||
if (!toolCalls) return [];
|
||||
|
||||
return toolCalls.map((call, index) => ({
|
||||
id: call?.id ?? `tool_${index}`,
|
||||
type: (call?.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION,
|
||||
function: {
|
||||
name: call?.function?.name ?? '',
|
||||
arguments: call?.function?.arguments ?? ''
|
||||
}
|
||||
arguments: call?.function?.arguments ?? '',
|
||||
name: call?.function?.name ?? ''
|
||||
},
|
||||
id: call?.id ?? `tool_${index}`,
|
||||
type: (call?.type as ToolCallType.FUNCTION) ?? ToolCallType.FUNCTION
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1003,17 +1069,18 @@ class AgenticStore {
|
||||
attachments: DatabaseMessageExtra[];
|
||||
} {
|
||||
if (!result.trim()) {
|
||||
return { cleanedResult: result, attachments: [] };
|
||||
return { attachments: [], cleanedResult: result };
|
||||
}
|
||||
|
||||
const lines = result.split(NEWLINE);
|
||||
const attachments: DatabaseMessageExtra[] = [];
|
||||
|
||||
let attachmentIndex = 0;
|
||||
|
||||
const cleanedLines = lines.map((line) => {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
const match = trimmedLine.match(DATA_URI_BASE64_REGEX);
|
||||
|
||||
if (!match) {
|
||||
return line;
|
||||
}
|
||||
@@ -1029,7 +1096,7 @@ class AgenticStore {
|
||||
const name = this.buildAttachmentName(mimeType, attachmentIndex);
|
||||
|
||||
if (mimeType.startsWith(MimeTypePrefix.IMAGE)) {
|
||||
attachments.push({ type: AttachmentType.IMAGE, name, base64Url: trimmedLine });
|
||||
attachments.push({ base64Url: trimmedLine, name, type: AttachmentType.IMAGE });
|
||||
|
||||
return `[Attachment saved: ${name}]`;
|
||||
}
|
||||
@@ -1037,7 +1104,7 @@ class AgenticStore {
|
||||
return line;
|
||||
});
|
||||
|
||||
return { cleanedResult: cleanedLines.join(NEWLINE), attachments };
|
||||
return { attachments, cleanedResult: cleanedLines.join(NEWLINE) };
|
||||
}
|
||||
|
||||
private buildAttachmentName(mimeType: string, index: number): string {
|
||||
|
||||
Reference in New Issue
Block a user