webui: Agentic Loop + MCP Client with support for Tools, Resources and Prompts (#18655)

This commit is contained in:
Aleksander Grygier
2026-03-06 10:00:39 +01:00
committed by GitHub
parent 2850bc6a13
commit f6235a41ef
147 changed files with 15285 additions and 366 deletions
@@ -1,3 +1,20 @@
import type { AgenticConfig } from '$lib/types/agentic';
export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
export const NEWLINE_SEPARATOR = '\n';
export const TURN_LIMIT_MESSAGE = '\n\n```\nTurn limit reached\n```\n';
export const LLM_ERROR_BLOCK_START = '\n\n```\nUpstream LLM error:\n';
export const LLM_ERROR_BLOCK_END = '\n```\n';
export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
enabled: true,
maxTurns: 100,
maxToolPreviewLines: 25
} as const;
// Agentic tool call tag markers
export const AGENTIC_TAGS = {
TOOL_CALL_START: '<<<AGENTIC_TOOL_CALL_START>>>',
@@ -13,6 +30,9 @@ export const REASONING_TAGS = {
END: '<<<reasoning_content_end>>>'
} as const;
// Regex for trimming leading/trailing newlines
export const TRIM_NEWLINES_REGEX = /^\n+|\n+$/g;
// Regex patterns for parsing agentic content
export const AGENTIC_REGEX = {
// Matches completed tool calls (with END marker)
@@ -32,6 +52,10 @@ export const AGENTIC_REGEX = {
REASONING_BLOCK: /<<<reasoning_content_start>>>[\s\S]*?<<<reasoning_content_end>>>/g,
// Matches an opening reasoning tag and any remaining content (unterminated)
REASONING_OPEN: /<<<reasoning_content_start>>>[\s\S]*$/,
// Matches a complete agentic tool call display block (start to end marker)
AGENTIC_TOOL_CALL_BLOCK: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*?<<<AGENTIC_TOOL_CALL_END>>>/g,
// Matches a pending/partial agentic tool call (start marker with no matching end)
AGENTIC_TOOL_CALL_OPEN: /\n*<<<AGENTIC_TOOL_CALL_START>>>[\s\S]*$/,
// Matches tool name inside content
TOOL_NAME_EXTRACT: /<<<TOOL_NAME:([^>]+)>>>/
} as const;
@@ -3,3 +3,6 @@ export const API_MODELS = {
LOAD: '/models/load',
UNLOAD: '/models/unload'
};
/** CORS proxy endpoint path */
export const CORS_PROXY_ENDPOINT = '/cors-proxy';
@@ -1,2 +1,4 @@
export const ATTACHMENT_LABEL_FILE = 'File';
export const ATTACHMENT_LABEL_PDF_FILE = 'PDF File';
export const ATTACHMENT_LABEL_MCP_PROMPT = 'MCP Prompt';
export const ATTACHMENT_LABEL_MCP_RESOURCE = 'MCP Resource';
@@ -27,6 +27,18 @@ export const MODEL_PROPS_CACHE_TTL_MS = 10 * 60 * 1000;
*/
export const MODEL_PROPS_CACHE_MAX_ENTRIES = 50;
/**
* Maximum number of MCP resources to cache
* @default 50
*/
export const MCP_RESOURCE_CACHE_MAX_ENTRIES = 50;
/**
* TTL for MCP resource cache entries in milliseconds
* @default 5 minutes
*/
export const MCP_RESOURCE_CACHE_TTL_MS = 5 * 60 * 1000;
/**
* Maximum number of inactive conversation states to keep in memory
* States for conversations beyond this limit will be cleaned up
@@ -1,3 +1,5 @@
export const INITIAL_FILE_SIZE = 0;
export const PROMPT_CONTENT_SEPARATOR = '\n\n';
export const CLIPBOARD_CONTENT_QUOTE_PREFIX = '"';
export const PROMPT_TRIGGER_PREFIX = '/';
export const RESOURCE_TRIGGER_PREFIX = '@';
@@ -8,3 +8,12 @@ export const INPUT_CLASSES = `
outline-none
text-foreground
`;
export const PANEL_CLASSES = `
bg-background
border border-border/30 dark:border-border/20
shadow-sm backdrop-blur-lg!
rounded-t-lg!
`;
export const CHAT_FORM_POPOVER_MAX_HEIGHT = 'max-h-80';
@@ -0,0 +1,4 @@
export const GOOGLE_FAVICON_BASE_URL = 'https://www.google.com/s2/favicons';
export const DEFAULT_FAVICON_SIZE = 32;
export const DOMAIN_SEPARATOR = '.';
export const ROOT_DOMAIN_MIN_PARTS = 2;
@@ -11,14 +11,19 @@ export * from './chat-form';
export * from './code-blocks';
export * from './code';
export * from './css-classes';
export * from './favicon';
export * from './floating-ui-constraints';
export * from './formatters';
export * from './key-value-pairs';
export * from './icons';
export * from './latex-protection';
export * from './literal-html';
export * from './localstorage-keys';
export * from './markdown';
export * from './max-bundle-size';
export * from './mcp';
export * from './mcp-form';
export * from './mcp-resource';
export * from './model-id';
export * from './precision';
export * from './processing-info';
@@ -30,4 +35,5 @@ export * from './supported-file-types';
export * from './table-html-restorer';
export * from './tooltip-config';
export * from './ui';
export * from './uri-template';
export * from './viewport';
@@ -0,0 +1,20 @@
/**
* Key-value pair form constraints and sanitization patterns.
*
* Both regexes target characters dangerous in HTTP-header / env-var contexts:
* \x00 null byte (injection)
* \x0A (\n) LF (HTTP header injection / response splitting)
* \x0D (\r) CR (HTTP header injection / response splitting)
* \x01\x08, \x0B\x0C, \x0E\x1F, \x7F other C0/DEL control chars
*
* KEY_UNSAFE_RE additionally strips TAB (\x09); values keep TAB because it is
* a valid header-value continuation character per RFC 7230.
*/
export const KEY_VALUE_PAIR_KEY_MAX_LENGTH = 256;
export const KEY_VALUE_PAIR_VALUE_MAX_LENGTH = 8192;
// eslint-disable-next-line no-control-regex
export const KEY_VALUE_PAIR_UNSAFE_KEY_RE = /[\x00-\x1F\x7F]/g;
// eslint-disable-next-line no-control-regex
export const KEY_VALUE_PAIR_UNSAFE_VALUE_RE = /[\x00-\x08\x0A-\x0D\x0E-\x1F\x7F]/g;
@@ -0,0 +1,2 @@
export const MCP_SERVER_URL_PLACEHOLDER = 'https://mcp.example.com/sse';
export const MIN_AUTOCOMPLETE_INPUT_LENGTH = 1;
@@ -0,0 +1,55 @@
import { MimeTypeImage } from '$lib/enums';
// File extension patterns for resource type detection
export const IMAGE_FILE_EXTENSION_REGEX = /\.(png|jpg|jpeg|gif|svg|webp)$/i;
export const CODE_FILE_EXTENSION_REGEX =
/\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i;
export const TEXT_FILE_EXTENSION_REGEX = /\.(txt|md|log)$/i;
// URI protocol prefix pattern
export const PROTOCOL_PREFIX_REGEX = /^[a-z]+:\/\//;
// File extension regex for display name extraction
export const FILE_EXTENSION_REGEX = /\.[^.]+$/;
// Separator regex for splitting display names (kebab-case/snake_case)
export const DISPLAY_NAME_SEPARATOR_REGEX = /[-_]/;
// Regex for matching base64-encoded data URIs
export const DATA_URI_BASE64_REGEX = /^data:([^;]+);base64,([A-Za-z0-9+/]+=*)$/;
// Prefix for MCP attachment filenames
export const MCP_ATTACHMENT_NAME_PREFIX = 'mcp-attachment';
// Prefix for MCP resource attachment IDs
export const MCP_RESOURCE_ATTACHMENT_ID_PREFIX = 'res';
// Default file extension for unknown image types
export const DEFAULT_IMAGE_EXTENSION = 'img';
// Default filename for resource content downloads
export const DEFAULT_RESOURCE_FILENAME = 'resource.txt';
// Path separator for resource URI parsing
export const PATH_SEPARATOR = '/';
// Separator for joining text content from multiple resource parts
export const RESOURCE_TEXT_CONTENT_SEPARATOR = '\n\n';
// Fallback text for unknown content types
export const RESOURCE_UNKNOWN_TYPE = 'unknown type';
// Label prefix for binary blob content
export const BINARY_CONTENT_LABEL = 'Binary content';
/**
* Mapping from image MIME types to file extensions.
* Used for generating attachment filenames from MIME types.
*/
export const IMAGE_MIME_TO_EXTENSION: Record<string, string> = {
[MimeTypeImage.JPEG]: 'jpg',
[MimeTypeImage.JPG]: 'jpg',
[MimeTypeImage.PNG]: 'png',
[MimeTypeImage.GIF]: 'gif',
[MimeTypeImage.WEBP]: 'webp'
} as const;
@@ -0,0 +1,63 @@
import { Zap, Globe, Radio } from '@lucide/svelte';
import { MCPTransportType } from '$lib/enums';
import type { ClientCapabilities, Implementation } from '$lib/types';
import type { Component } from 'svelte';
import { MimeTypeImage } from '$lib/enums/files';
export const DEFAULT_CLIENT_VERSION = '1.0.0';
export const DEFAULT_IMAGE_MIME_TYPE = MimeTypeImage.PNG;
/** MIME types considered safe for rendering MCP server icons */
export const MCP_ALLOWED_ICON_MIME_TYPES = new Set([
MimeTypeImage.PNG,
MimeTypeImage.JPEG,
MimeTypeImage.JPG,
MimeTypeImage.SVG,
MimeTypeImage.WEBP
]);
/**
* MCP specification version this client targets.
* Update when the upstream MCP spec introduces a new stable version:
* https://spec.modelcontextprotocol.io/
*/
export const MCP_PROTOCOL_VERSION = '2025-06-18';
export const DEFAULT_MCP_CONFIG = {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: { tools: { listChanged: true } } as ClientCapabilities,
clientInfo: { name: 'llama-webui-mcp', version: DEFAULT_CLIENT_VERSION } as Implementation,
requestTimeoutSeconds: 300, // 5 minutes for long-running tools
connectionTimeoutMs: 10_000 // 10 seconds for connection establishment
} as const;
export const MCP_SERVER_ID_PREFIX = 'LlamaCpp-WebUI-MCP-Server';
export const MCP_RECONNECT_INITIAL_DELAY = 1000;
export const MCP_RECONNECT_BACKOFF_MULTIPLIER = 2;
export const MCP_RECONNECT_MAX_DELAY = 30000;
/** Per-attempt timeout for a single reconnection attempt before giving up and backing off. */
export const MCP_RECONNECT_ATTEMPT_TIMEOUT_MS = 15_000;
/** Maximum number of MCP server avatars to display in the chat form */
export const MAX_DISPLAYED_MCP_AVATARS = 3;
/** Expected count when two theme-less icons represent a light/dark pair */
export const EXPECTED_THEMED_ICON_PAIR_COUNT = 2;
/** CORS proxy URL query parameter name */
export const CORS_PROXY_URL_PARAM = 'url';
/** Human-readable labels for MCP transport types */
export const MCP_TRANSPORT_LABELS: Record<MCPTransportType, string> = {
[MCPTransportType.WEBSOCKET]: 'WebSocket',
[MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
[MCPTransportType.SSE]: 'SSE'
};
/** Icon components for MCP transport types */
export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
[MCPTransportType.WEBSOCKET]: Zap,
[MCPTransportType.STREAMABLE_HTTP]: Globe,
[MCPTransportType.SSE]: Radio
};
@@ -24,6 +24,12 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
autoMicOnEmpty: false,
fullHeightCodeBlocks: false,
showRawModelNames: false,
mcpServers: '[]',
mcpServerUsageStats: '{}', // JSON object: { [serverId]: usageCount }
agenticMaxTurns: 10,
agenticMaxToolPreviewLines: 25,
showToolCallInProgress: false,
alwaysShowAgenticTurns: false,
// make sure these default values are in sync with `common.h`
samplers: 'top_k;typ_p;top_p;min_p;temperature',
backend_sampling: false,
@@ -119,6 +125,16 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
'Always display code blocks at their full natural height, overriding any height limits.',
showRawModelNames:
'Display full raw model identifiers (e.g. "unsloth/Qwen3.5-27B-GGUF:BF16") instead of parsed names with badges.',
mcpServers:
'Configure MCP servers as a JSON list. Use the form in the MCP Client settings section to edit.',
mcpServerUsageStats:
'Usage statistics for MCP servers. Tracks how many times tools from each server have been used.',
agenticMaxTurns:
'Maximum number of tool execution cycles before stopping (prevents infinite loops).',
agenticMaxToolPreviewLines:
'Number of lines shown in tool output previews (last N lines). Only these previews and the final LLM response persist after the agentic loop completes.',
showToolCallInProgress:
'Automatically expand tool call details while executing and keep them expanded after completion.',
pyInterpreterEnabled:
'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
enableContinueGeneration:
@@ -47,6 +47,11 @@ export const SETTINGS_KEYS = {
DRY_BASE: 'dry_base',
DRY_ALLOWED_LENGTH: 'dry_allowed_length',
DRY_PENALTY_LAST_N: 'dry_penalty_last_n',
// MCP
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
ALWAYS_SHOW_AGENTIC_TURNS: 'alwaysShowAgenticTurns',
AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines',
SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress',
// Developer
DISABLE_REASONING_PARSING: 'disableReasoningParsing',
SHOW_RAW_OUTPUT_SWITCH: 'showRawOutputSwitch',
@@ -1,5 +1,8 @@
/**
* Settings section titles constants for ChatSettings component.
*
* These titles define the navigation sections in the settings dialog.
* Used for both sidebar navigation and mobile horizontal scroll menu.
*/
export const SETTINGS_SECTION_TITLES = {
GENERAL: 'General',
@@ -7,8 +10,10 @@ export const SETTINGS_SECTION_TITLES = {
SAMPLING: 'Sampling',
PENALTIES: 'Penalties',
IMPORT_EXPORT: 'Import/Export',
MCP: 'MCP',
DEVELOPER: 'Developer'
} as const;
/** Type for settings section titles */
export type SettingsSectionTitle =
(typeof SETTINGS_SECTION_TITLES)[keyof typeof SETTINGS_SECTION_TITLES];
@@ -0,0 +1,57 @@
/**
* URI Template constants for RFC 6570 template processing.
*/
/** URI scheme separator */
export const URI_SCHEME_SEPARATOR = '://';
/** Regex to match template expressions like {var}, {+var}, {#var}, {/var} */
export const TEMPLATE_EXPRESSION_REGEX = /\{([+#./;?&]?)([^}]+)\}/g;
/** RFC 6570 URI template operators */
export const URI_TEMPLATE_OPERATORS = {
/** Simple string expansion (default) */
SIMPLE: '',
/** Reserved expansion */
RESERVED: '+',
/** Fragment expansion */
FRAGMENT: '#',
/** Path segment expansion */
PATH_SEGMENT: '/',
/** Label expansion */
LABEL: '.',
/** Path-style parameters */
PATH_PARAM: ';',
/** Form-style query */
FORM_QUERY: '?',
/** Form-style query continuation */
FORM_CONTINUATION: '&'
} as const;
/** URI template separators used in expansion */
export const URI_TEMPLATE_SEPARATORS = {
/** Comma separator for list expansion */
COMMA: ',',
/** Slash separator for path segments */
SLASH: '/',
/** Period separator for label expansion */
PERIOD: '.',
/** Semicolon separator for path parameters */
SEMICOLON: ';',
/** Question mark prefix for query string */
QUERY_PREFIX: '?',
/** Ampersand prefix for query continuation */
QUERY_CONTINUATION: '&'
} as const;
/** Maximum number of leading slashes to strip during URI normalization */
export const MAX_LEADING_SLASHES_TO_STRIP = 3;
/** Regex to strip explode modifier (*) from variable names */
export const VARIABLE_EXPLODE_MODIFIER_REGEX = /[*]$/;
/** Regex to strip prefix modifier (:N) from variable names */
export const VARIABLE_PREFIX_MODIFIER_REGEX = /:[\d]+$/;
/** Regex to strip one or more leading slashes */
export const LEADING_SLASHES_REGEX = /^\/+/;