char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
92d1bb0c9
char/aliceai-t5-moe
1import { MessageRole } from '$lib/enums' ; 2import type { DatabaseMessage } from '$lib/types/database' ; 3import { deriveAgenticSections } from '$lib/utils/agentic' ; 4import { describe , expect , it } from 'vitest' ; 5 6function makeAssistant ( overrides :Partial < DatabaseMessage > = {}) :DatabaseMessage { 7return { 8children :[], 9content :overrides . content ?? '' , 10convId :'conv-1' , 11id :overrides . id ?? 'ast-1' , 12parent :null , 13role :MessageRole . ASSISTANT , 14timestamp :Date . now (), 15type :'text' , 16 ...overrides 17} as DatabaseMessage ; 18} 19 20// Mirrors the filter inside ChatService.convertDbMessageToApiChatMessageData: 21// a partial tool call captured mid-stream must not survive into the next request 22// payload. The fix in chatStore.savePartialResponseIfNeeded clears toolCalls to '' 23// on Stop/Send immediately, mirroring what the agentic flow already does in 24// onAssistantTurnComplete(...undefined). 25function buildApiToolCalls ( message :DatabaseMessage ) :unknown [] | undefined { 26if ( ! message . toolCalls ) return undefined ; 27 28try { 29const parsed = JSON . parse ( message . toolCalls ); 30 31return Array . isArray ( parsed ) && parsed . length > 0 ?parsed :undefined ; 32} catch { 33return undefined ; 34} 35} 36 37describe ( 'partial tool call cleanup' , () => { 38// Reproduces the broken payload from the user's screenshot: model was 39// streaming a tool call whose arguments JSON was cut mid-string. The outer 40// envelope still parses, but the arguments themselves are invalid JSON and 41// the server rejects the request. 42it ( 'marks a partial tool call payload as unsafe to re-send' , () => { 43const message = makeAssistant ({ 44content :'partial reasoning' , 45toolCalls :JSON . stringify ([ 46{ 47function :{ 48arguments :'{"command":`grep -n \\"read_to\\" ` /Users' , 49name :'exec_shell_command' 50}, 51id :'call_1' , 52type :'function' 53} 54]) 55}); 56const apiToolCalls = buildApiToolCalls ( message ); 57 58// The bug: even though arguments are invalid, the outer array parses and 59// the request gets sent. Function arguments must be parseable JSON on their 60// own for the server to execute the tool. 61expect ( apiToolCalls ). toBeDefined (); 62const args = ( apiToolCalls ! [ 0 ] as { function :{ arguments :string } }). function . arguments ; 63 64expect (() => JSON . parse ( args )). toThrow (); 65}); 66 67// After Stop, savePartialResponseIfNeeded clears toolCalls and the agentic 68// flow does the same in its silent-return detection. The next request reads 69// toolCalls = '' and the conversion drops the field entirely so the server 70// never sees the half-streamed call. 71it ( 'drops tool_calls from the API request after toolCalls is cleared' , () => { 72const clearedMessage = makeAssistant ({ 73content :'partial reasoning' , 74toolCalls :'' 75}); 76const apiToolCalls = buildApiToolCalls ( clearedMessage ); 77 78expect ( apiToolCalls ). toBeUndefined (); 79}); 80 81// The cleanup path keeps the partial reasoning content visible in the UI; 82// only the tool_calls field is reset. deriveAgenticSections should still 83// surface the reasoning as interrupted (no content / no tool calls behind 84// it) without resurrecting the dead tool call block. 85it ( 'keeps reasoning content visible after cleanup, without a tool call block' , () => { 86const cleared = makeAssistant ({ 87content :'' , 88reasoningContent :'thinking about read_to' , 89toolCalls :'' 90}); 91const sections = deriveAgenticSections ( cleared ); 92 93expect ( sections ). toHaveLength ( 1 ); 94expect ( sections [ 0 ]. type ). toBe ( 'reasoning' ); 95expect ( sections . some (( s ) => s . type . includes ( 'tool_call' ))). toBe ( false ); 96}); 97});