char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
fe235f434
char/aliceai-t5-moe
1/** 2* toolsStore - Tool registry and enablement 3* 4* Owns the server tool listing (with working-directory resolution), built-in 5* browser tools, MCP tools and per-tool enablement, exposed as a unified 6* tool set for the LLM and the tools UI. Consumed by the agentic loop and 7* the chat flows. 8*/ 9 10import { browser } from '$app/environment' ; 11import { 12buildBrowserInfoToolDefinition , 13buildGetDatetimeToolDefinition , 14buildReadMediaToolDefinition , 15DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY , 16DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY , 17HOME_TILDE , 18TOOL_GROUP_LABELS , 19TOOL_SERVER_LABELS 20} from '$lib/constants' ; 21import { 22BuiltInTool , 23GlobSearchType , 24HealthCheckStatus , 25JsonSchemaType , 26ToolCallType , 27ToolSource 28} from '$lib/enums' ; 29import { ToolsService } from '$lib/services/tools.service' ; 30// direct imports between stores, not via the barrel, to avoid circular deps 31import { mcpStore } from '$lib/stores/mcp/index.svelte' ; 32import { modelsStore } from '$lib/stores/models/index.svelte' ; 33import { settingsStore } from '$lib/stores/settings/index.svelte' ; 34import type { OpenAIToolDefinition , ToolEntry , ToolGroup } from '$lib/types' ; 35import { buildSandboxToolDefinition } from '$lib/utils' ; 36import { SvelteMap , SvelteSet } from 'svelte/reactivity' ; 37 38/** Stable selection identity for a tool, shared by the disabled set and the permission store */ 39 40class ToolsStore { 41// default disabled tool categories, seeded into newly created conversations; 42// the per-conversation policy lives on the conversation row 43private _disabledToolCategories = $state ( new SvelteSet < ToolSource >()); 44private _disabledTools = $state ( new SvelteSet < string >()); 45private _error = $state < string | null >( null ); 46private _loading = $state ( false ); 47private _serverHome = $state < string | null | undefined >( undefined ); 48private _serverTools = $state < OpenAIToolDefinition []>([]); 49private _toolsEndpointUnreachable = $state ( false ); 50// server tools that resolve their paths against the working directory, 51// as declared by the server in its `/tools` listing 52private cwdAwareTools = $state ( new SvelteSet < string >()); 53 54get allToolDefinitions () :OpenAIToolDefinition [] { 55return this . allTools . map (( t ) => t . definition ); 56} 57 58/** Canonical flat list of tool entries with source metadata and stable keys, deduped by key */ 59get allTools () :ToolEntry [] { 60const entries :ToolEntry [] = []; 61const seen = new SvelteSet < string >(); 62const push = ( entry :ToolEntry ) => { 63if ( seen . has ( entry . key )) return ; 64 65seen . add ( entry . key ); 66entries . push ( entry ); 67}; 68 69for ( const def of this . _serverTools ) { 70const name = def . function . name ; 71 72push ({ 73definition :def , 74key :this . toolKey ( ToolSource . SERVER , name ), 75source :ToolSource . SERVER 76}); 77} 78 79for ( const def of this . browserTools ) { 80const name = def . function . name ; 81 82push ({ 83definition :def , 84key :this . toolKey ( ToolSource . BROWSER , name ), 85source :ToolSource . BROWSER 86}); 87} 88 89for ( const { definition, serverId, serverName} of this . mcpEntries ()) { 90const name = definition . function . name ; 91 92push ({ 93 definition, 94key :this . toolKey ( ToolSource . MCP , name , serverId ), 95 serverId, 96 serverName, 97source :ToolSource . MCP 98}); 99} 100 101for ( const def of this . customTools ) { 102const name = def . function . name ; 103 104push ({ 105definition :def , 106key :this . toolKey ( ToolSource . CUSTOM , name ), 107source :ToolSource . CUSTOM 108}); 109} 110 111return entries ; 112} 113 114get browserTools () :OpenAIToolDefinition [] { 115const tools :OpenAIToolDefinition [] = [ buildGetDatetimeToolDefinition ()]; 116 117if ( settingsStore . config . jsSandboxEnabled ) { 118tools . push ( buildSandboxToolDefinition ( !! settingsStore . config . symbolicMathEnabled )); 119} 120 121const readMedia = this . readMediaTool (); 122 123if ( readMedia ) tools . push ( readMedia ); 124 125// provide browser's get_info tool if server doesn't provide one 126if ( ! this . hasServerTool ( BuiltInTool . SERVER_GET_INFO )) { 127tools . push ( buildBrowserInfoToolDefinition ()); 128} 129 130return tools ; 131} 132 133get customTools () :OpenAIToolDefinition [] { 134const raw = settingsStore . config . customJson ; 135 136if ( ! raw || typeof raw !== 'string' ) return []; 137 138try { 139const parsed = JSON . parse ( raw ); 140 141if ( ! Array . isArray ( parsed )) return []; 142 143return parsed . filter ( 144( t :unknown ) :t isOpenAIToolDefinition => 145typeof t === 'object' && 146t !== null && 147'type' in t && 148( t as OpenAIToolDefinition ). type === 'function' && 149'function' in t && 150typeof ( t as OpenAIToolDefinition ). function ?. name === 'string' 151); 152} catch { 153return []; 154} 155} 156 157get disabledToolCategories () :ReadonlySet < ToolSource > { 158return this . _disabledToolCategories ; 159} 160 161get disabledTools () :SvelteSet < string > { 162return this . _disabledTools ; 163} 164 165get error () :string | null { 166return this . _error ; 167} 168 169get isToolsEndpointUnreachable () :boolean { 170return this . _toolsEndpointUnreachable ; 171} 172 173get loading () :boolean { 174return this . _loading ; 175} 176 177get mcpTools () :OpenAIToolDefinition [] { 178return this . mcpEntries (). map (( e ) => e . definition ); 179} 180 181get serverHome () :string | null { 182return this . _serverHome ?? null ; 183} 184 185get serverTools () :OpenAIToolDefinition [] { 186return this . _serverTools ; 187} 188 189/** Tools grouped by category for tree display, derived from the canonical entries */ 190get toolGroups () :ToolGroup [] { 191const groups :ToolGroup [] = []; 192const byKey = new SvelteMap < string , ToolGroup >(); 193 194for ( const entry of this . allTools ) { 195const groupKey = 196entry . source === ToolSource . MCP ?`mcp: ${ entry . serverId ?? '' } ` :entry . source ; 197 198let group = byKey . get ( groupKey ); 199 200if ( ! group ) { 201group = { 202key :groupKey , 203label :this . groupLabel ( entry ), 204serverId :entry . serverId , 205source :entry . source , 206tools :[] 207}; 208byKey . set ( groupKey , group ); 209groups . push ( group ); 210} 211 212group . tools . push ( entry ); 213} 214 215return groups ; 216} 217 218/** Enable all tools belonging to a specific MCP server */ 219enableAllToolsForServer ( serverId :string ) :void { 220const connection = mcpStore . getConnections (). get ( serverId ); 221 222if ( ! connection ) return ; 223 224// the server-scoped group key disables every tool regardless of per-tool keys 225this . _disabledTools . delete ( this . getMcpServerToolsKey ( serverId )); 226 227for ( const tool of connection . tools ) { 228this . _disabledTools . delete ( this . toolKey ( ToolSource . MCP , tool . name , serverId )); 229} 230 231this . persistDisabledTools (); 232} 233 234async fetchServerTools () :Promise < void > { 235if ( this . _loading ) return ; 236 237this . _loading = true ; 238this . _error = null ; 239this . _toolsEndpointUnreachable = false ; 240 241try { 242const toolInfos = await ToolsService . list (); 243 244this . _serverTools = toolInfos . map (( info ) => info . definition ); 245this . cwdAwareTools = new SvelteSet ( 246toolInfos . filter (( info ) => info . uses_cwd ). map (( info ) => info . tool ) 247); 248} catch ( err ) { 249const errorMessage = err instanceof Error ?err . message :String ( err ); 250 251this . _error = errorMessage ; 252 253// 403 from /tools means the server was started without --tools 254// TODO: check status code instead of relying on message 255if ( errorMessage . includes ( 'this feature is disabled' )) { 256this . _toolsEndpointUnreachable = true ; 257console . info ( '[ToolsStore] Server tools are disabled on the server' ); 258} else { 259console . error ( '[ToolsStore] Failed to fetch server tools:' , err ); 260} 261} finally { 262this . _loading = false ; 263} 264} 265 266/** 267* Enabled tool definitions for sending to the LLM. Callers pass an 268* explicit policy (the active conversation's, resolved with global 269* defaults when absent); without arguments the store defaults apply. 270* MCP tool schemas are normalized here so the wire payload is consistent 271* across all four sources (server, browser/sandbox, MCP, custom JSON). 272* The API identifies tools by name, so a name is sent at most once. 273*/ 274getEnabledToolsForLLM ( 275disabledTools :ReadonlySet < string > = this . _disabledTools , 276disabledCategories :ReadonlySet < ToolSource > = this . _disabledToolCategories 277) :OpenAIToolDefinition [] { 278const enabledNames = new SvelteSet < string >(); 279 280for ( const entry of this . allTools ) { 281if ( this . isEntryEnabled ( entry , disabledTools , disabledCategories )) { 282enabledNames . add ( entry . definition . function . name ); 283} 284} 285 286const result :OpenAIToolDefinition [] = []; 287const seen = new SvelteSet < string >(); 288const take = ( def :OpenAIToolDefinition ) => { 289const name = def . function . name ; 290 291if ( ! enabledNames . has ( name ) || seen . has ( name )) return ; 292 293seen . add ( name ); 294result . push ( def ); 295}; 296 297for ( const def of this . _serverTools ) take ( def ); 298for ( const def of this . browserTools ) take ( def ); 299// mcpEntries() over mcpStore directly so wire shape stays normalized and aligned with the tools UI. 300for ( const entry of this . mcpEntries ()) take ( entry . definition ); 301for ( const def of this . customTools ) take ( def ); 302 303return result ; 304} 305 306/** Server-scoped tool key: disabling it disables all of that server's tools. */ 307getMcpServerToolsKey ( serverId :string ) :string { 308return `mcp: ${ serverId } ` ; 309} 310 311/** Permission key for a tool name, identical to the selection key */ 312getPermissionKey ( toolName :string ) :string | null { 313return this . findEntryByName ( toolName )?. key ?? null ; 314} 315 316/** Get the display label for the server that owns a given tool */ 317getToolServerLabel ( toolName :string ) :string { 318const entry = this . findEntryByName ( toolName ); 319 320if ( ! entry ) return '' ; 321 322if ( entry . serverName ) return mcpStore . getServerDisplayName ( entry . serverName ); 323 324if ( entry . source === ToolSource . SERVER ) return TOOL_SERVER_LABELS [ ToolSource . SERVER ]; 325 326if ( entry . source === ToolSource . CUSTOM ) return TOOL_SERVER_LABELS [ ToolSource . CUSTOM ]; 327 328if ( entry . source === ToolSource . BROWSER ) return TOOL_SERVER_LABELS [ ToolSource . BROWSER ]; 329 330return '' ; 331} 332 333/** Determine the source of a tool by its name */ 334getToolSource ( toolName :string ) :ToolSource | null { 335return this . findEntryByName ( toolName )?. source ?? null ; 336} 337 338/** 339* Check if a working directory is worth setting: at least one server tool 340* that reads it is both served and left enabled by the given policy 341* (defaults to the global defaults). 342*/ 343hasEnabledCwdTools ( 344disabledTools :ReadonlySet < string > = this . _disabledTools , 345disabledCategories :ReadonlySet < ToolSource > = this . _disabledToolCategories 346) :boolean { 347if ( disabledCategories . has ( ToolSource . SERVER )) return false ; 348 349return this . _serverTools . some (( def ) => { 350const name = def . function . name ; 351 352return ( 353this . cwdAwareTools . has ( name ) && ! disabledTools . has ( this . toolKey ( ToolSource . SERVER , name )) 354); 355}); 356} 357 358/** 359* Load persisted disabled tools and fetch the builtin tool list. 360* Called by initStores() after migrations have run. 361*/ 362initialize () :void { 363// browser-only init: skip on SSR to avoid localStorage/fetch side effects 364if ( ! browser ) return ; 365 366try { 367const stored = localStorage . getItem ( DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY ); 368 369if ( stored ) { 370const parsed = JSON . parse ( stored ); 371 372if ( Array . isArray ( parsed )) { 373for ( const key of parsed ) { 374if ( typeof key === 'string' ) this . _disabledTools . add ( key ); 375} 376} 377} 378} catch ( err ) { 379console . error ( '[ToolsStore] Failed to load disabled tools from localStorage:' , err ); 380} 381 382try { 383const stored = localStorage . getItem ( DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY ); 384 385if ( stored ) { 386const parsed = JSON . parse ( stored ); 387 388if ( Array . isArray ( parsed )) { 389for ( const key of parsed ) { 390if ( Object . values ( ToolSource ). includes ( key )) { 391this . _disabledToolCategories . add ( key as ToolSource ); 392} 393} 394} 395} 396} catch ( err ) { 397console . error ( '[ToolsStore] Failed to load disabled tool categories from localStorage:' , err ); 398} 399 400this . fetchServerTools (); 401} 402 403isCategoryEnabled ( source :ToolSource ) :boolean { 404return ! this . _disabledToolCategories . has ( source ); 405} 406 407isEntryEnabled ( 408entry :ToolEntry , 409disabledTools :ReadonlySet < string >, 410disabledCategories :ReadonlySet < ToolSource > 411) :boolean { 412if ( disabledCategories . has ( entry . source )) return false ; 413 414if ( disabledTools . has ( entry . key )) return false ; 415 416if ( entry . source === ToolSource . MCP && entry . serverId ) { 417return ! disabledTools . has ( this . getMcpServerToolsKey ( entry . serverId )); 418} 419 420return true ; 421} 422 423isToolEnabled ( key :string ) :boolean { 424return ! this . _disabledTools . has ( key ); 425} 426 427/** 428* Absolute home directory on the server, resolved once per session via 429* file_glob_search's `base` field (the server expands `~`). Anchors the 430* directory picker's search scope and the `~` abbreviation of cwd 431* displays. Returns null when tools are unavailable. 432*/ 433async resolveServerHome () :Promise < string | null > { 434if ( this . _serverHome !== undefined ) return this . _serverHome ; 435 436try { 437const res = await ToolsService . executeToolRaw ( BuiltInTool . SERVER_FILE_GLOB_SEARCH , { 438limit :1 , 439max_depth :1 , 440path :HOME_TILDE , 441type :GlobSearchType . DIR 442}); 443 444this . _serverHome = typeof res . base === 'string' ?res . base :null ; 445} catch { 446// searches still work via a literal `~`, only `~` abbreviation degrades 447this . _serverHome = null ; 448} 449 450return this . _serverHome ; 451} 452 453setCategoryEnabled ( source :ToolSource , enabled :boolean ) :void { 454if ( enabled ) { 455this . _disabledToolCategories . delete ( source ); 456} else { 457this . _disabledToolCategories . add ( source ); 458} 459 460this . persistDisabledToolCategories (); 461} 462 463setToolEnabled ( key :string , enabled :boolean ) :void { 464if ( enabled ) { 465this . _disabledTools . delete ( key ); 466} else { 467this . _disabledTools . add ( key ); 468} 469 470this . persistDisabledTools (); 471} 472 473toggleCategory ( source :ToolSource ) :void { 474this . setCategoryEnabled ( source , ! this . isCategoryEnabled ( source )); 475} 476 477toggleTool ( key :string ) :void { 478this . setToolEnabled ( key , ! this . isToolEnabled ( key )); 479} 480 481/** First canonical entry matching a tool name, runtime tool calls resolve by name */ 482private findEntryByName ( toolName :string ) :ToolEntry | null { 483for ( const entry of this . allTools ) { 484if ( entry . definition . function . name === toolName ) return entry ; 485} 486 487return null ; 488} 489 490/** Get MCP tools from health check data, used when live connections aren't established yet */ 491private getMcpToolsFromHealthChecks () :{ 492serverId :string ; 493serverName :string ; 494tools :{ name :string ; description ?:string }[]; 495}[] { 496const result :ReturnType < ToolsStore [ 'getMcpToolsFromHealthChecks' ]> = []; 497 498for ( const server of mcpStore . getServers ()) { 499if ( ! server . enabled ) continue ; 500 501const health = mcpStore . getHealthCheckState ( server . id ); 502 503if ( health . status === HealthCheckStatus . SUCCESS && health . tools . length > 0 ) { 504result . push ({ 505serverId :server . id , 506serverName :mcpStore . getServerLabel ( server ), 507tools :health . tools 508}); 509} 510} 511 512return result ; 513} 514 515private groupLabel ( entry :ToolEntry ) :string { 516switch ( entry . source ) { 517case ToolSource . MCP : 518return entry . serverName ?? '' ; 519case ToolSource . CUSTOM : 520return TOOL_GROUP_LABELS [ ToolSource . CUSTOM ]; 521case ToolSource . BROWSER : 522return TOOL_GROUP_LABELS [ ToolSource . BROWSER ]; 523default : 524return TOOL_GROUP_LABELS [ ToolSource . SERVER ]; 525} 526} 527 528private hasServerTool ( name :BuiltInTool ) :boolean { 529return this . _serverTools . some (( def ) => def . function . name === name ); 530} 531 532private inferTypeFromDefault ( value :unknown ) :string | undefined { 533if ( typeof value === 'string' ) return 'string' ; 534 535if ( typeof value === 'boolean' ) return 'boolean' ; 536 537if ( typeof value === 'number' ) return Number . isInteger ( value ) ?'integer' :'number' ; 538 539if ( Array . isArray ( value )) return 'array' ; 540 541if ( value !== null && typeof value === 'object' ) return 'object' ; 542 543return undefined ; 544} 545 546private mcpDefinition ( 547name :string , 548description :string | undefined , 549schema ?:Record < string , unknown > 550) :OpenAIToolDefinition { 551return { 552function :{ 553 description, 554 name, 555parameters :schema ?? { properties :{}, required :[], type :JsonSchemaType . OBJECT } 556}, 557type :ToolCallType . FUNCTION 558}; 559} 560 561/** Normalize MCP tools from live connections when available, fall back to health check data */ 562private mcpEntries () :{ 563serverId :string ; 564serverName :string ; 565definition :OpenAIToolDefinition ; 566}[] { 567const out :{ serverId :string ; serverName :string ; definition :OpenAIToolDefinition }[] = []; 568const connections = mcpStore . getConnections (); 569 570if ( connections . size > 0 ) { 571for ( const [ serverId , connection ] of connections ) { 572const serverName = mcpStore . getServerDisplayName ( serverId ); 573 574for ( const tool of connection . tools ) { 575const rawSchema = ( tool . inputSchema as Record < string , unknown >) ?? { 576properties :{}, 577required :[], 578type :JsonSchemaType . OBJECT 579}; 580 581out . push ({ 582definition :{ 583function :{ 584description :tool . description , 585name :tool . name , 586parameters :this . normalizeJsonSchema ( rawSchema ) 587}, 588type :ToolCallType . FUNCTION 589}, 590 serverId, 591 serverName 592}); 593} 594} 595} else { 596for ( const { serverId, serverName, tools} of this . getMcpToolsFromHealthChecks ()) { 597for ( const tool of tools ) { 598out . push ({ 599definition :this . mcpDefinition ( tool . name , tool . description ), 600 serverId, 601 serverName 602}); 603} 604} 605} 606 607return out ; 608} 609 610/** 611* Recursively normalize a JSON Schema object: infers `type` from `default` 612* for properties / items that omit it, and descends into nested `properties` 613* and `items`. Returns a new object -- does not mutate the input. 614*/ 615private normalizeJsonSchema ( schema :Record < string , unknown >) :Record < string , unknown > { 616if ( ! schema || typeof schema !== 'object' ) return schema ; 617 618const normalized :Record < string , unknown > = { ...schema }; 619 620if ( normalized . properties && typeof normalized . properties === 'object' ) { 621const props = normalized . properties as Record < string , Record < string , unknown >>; 622const normalizedProps :Record < string , Record < string , unknown >> = {}; 623 624for ( const [ key , prop ] of Object . entries ( props )) { 625if ( ! prop || typeof prop !== 'object' ) { 626normalizedProps [ key ] = prop ; 627 628continue ; 629} 630 631const normalizedProp :Record < string , unknown > = { ...prop }; 632 633if ( ! normalizedProp . type && normalizedProp . default !== undefined ) { 634const inferred = this . inferTypeFromDefault ( normalizedProp . default ); 635 636if ( inferred ) normalizedProp . type = inferred ; 637} 638 639if ( normalizedProp . properties ) { 640Object . assign ( 641normalizedProp , 642this . normalizeJsonSchema ( normalizedProp as Record < string , unknown >) 643); 644} 645 646if ( normalizedProp . items && typeof normalizedProp . items === 'object' ) { 647normalizedProp . items = this . normalizeJsonSchema ( 648normalizedProp . items as Record < string , unknown > 649); 650} 651 652normalizedProps [ key ] = normalizedProp ; 653} 654normalized . properties = normalizedProps ; 655} 656 657return normalized ; 658} 659 660private persistDisabledToolCategories () :void { 661try { 662localStorage . setItem ( 663DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY , 664JSON . stringify ([ ...this . _disabledToolCategories ]) 665); 666} catch { 667// ignore storage errors 668} 669} 670 671private persistDisabledTools () :void { 672try { 673localStorage . setItem ( 674DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY , 675JSON . stringify ([ ...this . _disabledTools ]) 676); 677} catch { 678// ignore storage errors 679} 680} 681 682/** 683* `read_media` runs in the browser on top of the server's `read_file`, so it 684* exists only when that tool is served and the active model can perceive the 685* bytes. The server cannot make this call - it does not know which model the 686* conversation uses. 687*/ 688private readMediaTool () :OpenAIToolDefinition | null { 689if ( ! this . hasServerTool ( BuiltInTool . SERVER_READ_FILE )) return null ; 690 691const model = modelsStore . selectedModelName ?? modelsStore . models [ 0 ]?. model ?? '' ; 692 693if ( ! model ) return null ; 694 695const vision = modelsStore . props . modelSupportsVision ( model ); 696const audio = modelsStore . props . modelSupportsAudio ( model ); 697 698if ( ! vision && ! audio ) return null ; 699 700return buildReadMediaToolDefinition ( vision , audio ); 701} 702 703private toolKey ( source :ToolSource , name :string , serverId ?:string ) :string { 704switch ( source ) { 705case ToolSource . MCP : 706// with a serverId this is a per-tool key; without one it hits the 707// server group key shape, which no MCP entry ever does 708return serverId ?`mcp- ${ serverId } : ${ name } ` :this . getMcpServerToolsKey ( name ); 709case ToolSource . CUSTOM : 710return `custom: ${ name } ` ; 711case ToolSource . BROWSER : 712return `browser: ${ name } ` ; 713default : 714return `server: ${ name } ` ; 715} 716} 717} 718 719export const toolsStore = new ToolsStore ();