char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
92d1bb0c9
char/aliceai-t5-moe
1import { ContinueIntentKind , MessageRole , MessageType } from '$lib/enums' ; 2import type { DatabaseMessage } from '$lib/types/database' ; 3import { classifyContinueIntent } from '$lib/utils/agentic' ; 4import { describe , expect , it } from 'vitest' ; 5 6/** 7* Tests for the Continue button intent classifier. 8* 9* The classifier walks the persisted message history to decide which of three 10* resume paths a Continue click should take: 11* 12* A. append_text -> plain text assistant turn, resume with 13* continue_final_message. 14* B. rerun_turn -> assistant turn with tool_calls but no tool results yet, 15* the stream was cut mid turn and the tool_calls are 16* unrecoverable as a token level continuation. Drop the 17* target and rerun from the previous history. 18* C. next_turn -> assistant turn with tool_calls that were already 19* resolved by trailing tool results. Hand the history 20* back to the agentic loop so it starts the next turn. 21*/ 22 23let nextId = 0 ; 24 25function makeMsg ( role :MessageRole , opts :Partial < DatabaseMessage > = {}) :DatabaseMessage { 26nextId ++ ; 27 28return { 29children :[], 30content :'' , 31convId :'conv-1' , 32id :`msg- ${ nextId } ` , 33parent :null , 34 role, 35timestamp :nextId , 36type :MessageType . TEXT , 37 ...opts 38}; 39} 40 41function toolCall ( id :string , name :string , args :string = '{}' ) :string { 42return JSON . stringify ([{ function :{ arguments :args , name}, id, type :'function' }]); 43} 44 45describe ( 'classifyContinueIntent' , () => { 46it ( 'returns append_text for a plain text assistant turn at the tail' , () => { 47const messages = [ 48makeMsg ( MessageRole . USER , { content :'hello' }), 49makeMsg ( MessageRole . ASSISTANT , { content :'hi there' }) 50]; 51const intent = classifyContinueIntent ( messages , 1 ); 52 53expect ( intent ). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 54}); 55 56it ( 'returns append_text for a plain text assistant turn in the middle' , () => { 57const messages = [ 58makeMsg ( MessageRole . USER , { content :'q1' }), 59makeMsg ( MessageRole . ASSISTANT , { content :'a1' }), 60makeMsg ( MessageRole . USER , { content :'q2' }), 61makeMsg ( MessageRole . ASSISTANT , { content :'a2' }) 62]; 63 64expect ( classifyContinueIntent ( messages , 1 )). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 65}); 66 67it ( 'returns rerun_turn when the assistant has tool_calls without results' , () => { 68const messages = [ 69makeMsg ( MessageRole . USER , { content :'list files' }), 70makeMsg ( MessageRole . ASSISTANT , { 71content :'' , 72toolCalls :toolCall ( 'call_1' , 'bash_tool' , '{"command":"ls"}' ) 73}) 74]; 75const intent = classifyContinueIntent ( messages , 1 ); 76 77expect ( intent ). toEqual ({ kind :ContinueIntentKind . RERUN_TURN , truncateAfter :0 }); 78}); 79 80it ( 'returns next_turn when trailing tool results resolve the tool_calls' , () => { 81const messages = [ 82makeMsg ( MessageRole . USER , { content :'list files' }), 83makeMsg ( MessageRole . ASSISTANT , { 84content :'' , 85toolCalls :toolCall ( 'call_1' , 'bash_tool' ) 86}), 87makeMsg ( MessageRole . TOOL , { content :'file1\nfile2' , toolCallId :'call_1' }) 88]; 89const intent = classifyContinueIntent ( messages , 1 ); 90 91expect ( intent ). toEqual ({ kind :ContinueIntentKind . NEXT_TURN , truncateAfter :2 }); 92}); 93 94it ( 'next_turn keeps all consecutive trailing tool results, not just one' , () => { 95const messages = [ 96makeMsg ( MessageRole . USER , { content :'do many things' }), 97makeMsg ( MessageRole . ASSISTANT , { 98content :'' , 99toolCalls :JSON . stringify ([ 100{ function :{ arguments :'{}' , name :'a' }, id :'call_1' , type :'function' }, 101{ function :{ arguments :'{}' , name :'b' }, id :'call_2' , type :'function' } 102]) 103}), 104makeMsg ( MessageRole . TOOL , { content :'r1' , toolCallId :'call_1' }), 105makeMsg ( MessageRole . TOOL , { content :'r2' , toolCallId :'call_2' }) 106]; 107const intent = classifyContinueIntent ( messages , 1 ); 108 109expect ( intent ). toEqual ({ kind :ContinueIntentKind . NEXT_TURN , truncateAfter :3 }); 110}); 111 112it ( 'next_turn stops at the first non tool message after the target' , () => { 113const messages = [ 114makeMsg ( MessageRole . USER , { content :'go' }), 115makeMsg ( MessageRole . ASSISTANT , { 116content :'' , 117toolCalls :toolCall ( 'call_1' , 'a' ) 118}), 119makeMsg ( MessageRole . TOOL , { content :'r1' , toolCallId :'call_1' }), 120makeMsg ( MessageRole . USER , { content :'wait' }), 121makeMsg ( MessageRole . TOOL , { content :'late' , toolCallId :'call_1' }) 122]; 123const intent = classifyContinueIntent ( messages , 1 ); 124 125// truncateAfter must point at the contiguous tool block, not jump over 126// the user message to grab the dangling late tool. 127expect ( intent ). toEqual ({ kind :ContinueIntentKind . NEXT_TURN , truncateAfter :2 }); 128}); 129 130it ( 'returns append_text when toolCalls is set but parses to empty array' , () => { 131const messages = [ 132makeMsg ( MessageRole . USER , { content :'q' }), 133makeMsg ( MessageRole . ASSISTANT , { content :'a' , toolCalls :'[]' }) 134]; 135 136expect ( classifyContinueIntent ( messages , 1 )). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 137}); 138 139it ( 'returns append_text when toolCalls is malformed JSON' , () => { 140const messages = [ 141makeMsg ( MessageRole . USER , { content :'q' }), 142makeMsg ( MessageRole . ASSISTANT , { content :'a' , toolCalls :'{not json' }) 143]; 144 145expect ( classifyContinueIntent ( messages , 1 )). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 146}); 147 148it ( 'returns append_text defensively when idx points at a non assistant message' , () => { 149const messages = [ 150makeMsg ( MessageRole . USER , { content :'q' }), 151makeMsg ( MessageRole . ASSISTANT , { content :'a' }) 152]; 153 154expect ( classifyContinueIntent ( messages , 0 )). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 155}); 156 157it ( 'returns append_text defensively when idx is out of bounds' , () => { 158const messages = [ makeMsg ( MessageRole . ASSISTANT , { content :'a' })]; 159 160expect ( classifyContinueIntent ( messages , 5 )). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 161expect ( classifyContinueIntent ([], 0 )). toEqual ({ kind :ContinueIntentKind . APPEND_TEXT }); 162}); 163});