Team Ai
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 21d agoView on Hugging Face
0likes1.2kdownloads
agentic-sections.test.ts404 linesDownload Raw Back to unit
1import { AgenticSectionType, MessageRole } from '$lib/enums';2import type { ApiChatCompletionToolCall } from '$lib/types/api';3import type { DatabaseMessage } from '$lib/types/database';4import { deriveAgenticSections, hasAgenticContent } from '$lib/utils/agentic';5import { describe, expect, it } from 'vitest';6 7function makeAssistant(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {8	return {9		children: [],10		content: overrides.content ?? '',11		convId: 'conv-1',12		id: overrides.id ?? 'ast-1',13		parent: null,14		role: MessageRole.ASSISTANT,15		timestamp: Date.now(),16		type: 'text',17		...overrides18	} as DatabaseMessage;19}20 21function makeToolMsg(overrides: Partial<DatabaseMessage> = {}): DatabaseMessage {22	return {23		children: [],24		content: overrides.content ?? 'tool result',25		convId: 'conv-1',26		id: overrides.id ?? 'tool-1',27		parent: null,28		role: MessageRole.TOOL,29		timestamp: Date.now(),30		toolCallId: overrides.toolCallId ?? 'call_1',31		type: 'text',32		...overrides33	} as DatabaseMessage;34}35 36describe('deriveAgenticSections', () => {37	it('returns empty array for assistant with no content', () => {38		const msg = makeAssistant({ content: '' });39		const sections = deriveAgenticSections(msg);40 41		expect(sections).toEqual([]);42	});43 44	it('returns text section for simple assistant message', () => {45		const msg = makeAssistant({ content: 'Hello world' });46		const sections = deriveAgenticSections(msg);47 48		expect(sections).toHaveLength(1);49		expect(sections[0].type).toBe(AgenticSectionType.TEXT);50		expect(sections[0].content).toBe('Hello world');51	});52 53	it('returns reasoning + text for message with reasoning', () => {54		const msg = makeAssistant({55			content: 'Answer is 4.',56			reasoningContent: 'Let me think...'57		});58		const sections = deriveAgenticSections(msg);59 60		expect(sections).toHaveLength(2);61		expect(sections[0].type).toBe(AgenticSectionType.REASONING);62		expect(sections[0].content).toBe('Let me think...');63		expect(sections[1].type).toBe(AgenticSectionType.TEXT);64	});65 66	it('single turn: assistant with tool calls and results', () => {67		const msg = makeAssistant({68			content: 'Let me check.',69			toolCalls: JSON.stringify([70				{71					function: { arguments: '{"q":"test"}', name: 'search' },72					id: 'call_1',73					type: 'function'74				}75			])76		});77		const toolResult = makeToolMsg({78			content: 'Found 3 results',79			toolCallId: 'call_1'80		});81		const sections = deriveAgenticSections(msg, [toolResult]);82 83		expect(sections).toHaveLength(2);84		expect(sections[0].type).toBe(AgenticSectionType.TEXT);85		expect(sections[1].type).toBe(AgenticSectionType.TOOL_CALL);86		expect(sections[1].toolName).toBe('search');87		expect(sections[1].toolResult).toBe('Found 3 results');88	});89 90	it('single turn: pending tool call without result', () => {91		const msg = makeAssistant({92			toolCalls: JSON.stringify([93				{ function: { arguments: '{}', name: 'bash' }, id: 'call_1', type: 'function' }94			])95		});96		const sections = deriveAgenticSections(msg, [], [], true);97 98		expect(sections).toHaveLength(1);99		expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL_PENDING);100		expect(sections[0].toolName).toBe('bash');101	});102 103	it('chat-streaming write_file surfaces as TOOL_CALL_PENDING with partial toolArgs (not TOOL_CALL_STREAMING)', () => {104		// Regression: while the LLM is emitting a write_file tool call's105		// args, `chat.svelte.ts` JSON-encodes the partial tool-call array on106		// every chunk, so `parseToolCalls` succeeds and the section is107		// classified TOOL_CALL_PENDING - not TOOL_CALL_STREAMING (which is108		// only produced from the `streamingToolCalls` parameter, never set109		// by current UI callers). Streaming-only UI like auto-scroll in the110		// code block must still trigger, driven by `isStreaming && (isPending111		// || isStreamingCall)`, not `isStreamingCall` alone.112		const partialArgs = '{"path":"/Users/fifa2026.html","content":"<!DOCTYPE h';113		const msg = makeAssistant({114			toolCalls: JSON.stringify([115				{ function: { arguments: partialArgs, name: 'write_file' }, id: 'call_1', type: 'function' }116			])117		});118		const sections = deriveAgenticSections(msg, [], [], true);119 120		expect(sections).toHaveLength(1);121		expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL_PENDING);122		expect(sections[0].type).not.toBe(AgenticSectionType.TOOL_CALL_STREAMING);123		expect(sections[0].toolName).toBe('write_file');124		expect(sections[0].toolArgs).toBe(partialArgs);125	});126 127	it('multi-turn: two assistant turns grouped as one session', () => {128		const assistant1 = makeAssistant({129			content: 'Turn 1 text',130			id: 'ast-1',131			toolCalls: JSON.stringify([132				{133					function: { arguments: '{"q":"foo"}', name: 'search' },134					id: 'call_1',135					type: 'function'136				}137			])138		});139		const tool1 = makeToolMsg({ content: 'result 1', id: 'tool-1', toolCallId: 'call_1' });140		const assistant2 = makeAssistant({141			content: 'Final answer based on results.',142			id: 'ast-2'143		});144		// toolMessages contains both tool result and continuation assistant145		const sections = deriveAgenticSections(assistant1, [tool1, assistant2]);146 147		expect(sections).toHaveLength(3);148		// Turn 1149		expect(sections[0].type).toBe(AgenticSectionType.TEXT);150		expect(sections[0].content).toBe('Turn 1 text');151		expect(sections[1].type).toBe(AgenticSectionType.TOOL_CALL);152		expect(sections[1].toolName).toBe('search');153		expect(sections[1].toolResult).toBe('result 1');154		// Turn 2 (final)155		expect(sections[2].type).toBe(AgenticSectionType.TEXT);156		expect(sections[2].content).toBe('Final answer based on results.');157	});158 159	it('multi-turn: three turns with tool calls', () => {160		const assistant1 = makeAssistant({161			content: '',162			id: 'ast-1',163			toolCalls: JSON.stringify([164				{165					function: { arguments: '{}', name: 'list_files' },166					id: 'call_1',167					type: 'function'168				}169			])170		});171		const tool1 = makeToolMsg({ content: 'file1 file2', id: 'tool-1', toolCallId: 'call_1' });172		const assistant2 = makeAssistant({173			content: 'Reading file1...',174			id: 'ast-2',175			toolCalls: JSON.stringify([176				{177					function: { arguments: '{"path":"file1"}', name: 'read_file' },178					id: 'call_2',179					type: 'function'180				}181			])182		});183		const tool2 = makeToolMsg({184			content: 'contents of file1',185			id: 'tool-2',186			toolCallId: 'call_2'187		});188		const assistant3 = makeAssistant({189			content: 'Here is the analysis.',190			id: 'ast-3',191			reasoningContent: 'The file contains...'192		});193		const sections = deriveAgenticSections(assistant1, [tool1, assistant2, tool2, assistant3]);194 195		// Turn 1: tool_call (no text since content is empty)196		// Turn 2: text + tool_call197		// Turn 3: reasoning + text198		expect(sections).toHaveLength(5);199		expect(sections[0].type).toBe(AgenticSectionType.TOOL_CALL);200		expect(sections[0].toolName).toBe('list_files');201		expect(sections[1].type).toBe(AgenticSectionType.TEXT);202		expect(sections[1].content).toBe('Reading file1...');203		expect(sections[2].type).toBe(AgenticSectionType.TOOL_CALL);204		expect(sections[2].toolName).toBe('read_file');205		expect(sections[3].type).toBe(AgenticSectionType.REASONING);206		expect(sections[4].type).toBe(AgenticSectionType.TEXT);207		expect(sections[4].content).toBe('Here is the analysis.');208	});209 210	it('returns REASONING_PENDING when streaming with only reasoning content', () => {211		const msg = makeAssistant({212			reasoningContent: 'Let me think about this...'213		});214		const sections = deriveAgenticSections(msg, [], [], true);215 216		expect(sections).toHaveLength(1);217		expect(sections[0].type).toBe(AgenticSectionType.REASONING_PENDING);218		expect(sections[0].content).toBe('Let me think about this...');219	});220 221	it('returns REASONING (not pending) when streaming but text content has appeared', () => {222		const msg = makeAssistant({223			content: 'The answer is',224			reasoningContent: 'Let me think...'225		});226		const sections = deriveAgenticSections(msg, [], [], true);227 228		expect(sections).toHaveLength(2);229		expect(sections[0].type).toBe(AgenticSectionType.REASONING);230		expect(sections[1].type).toBe(AgenticSectionType.TEXT);231	});232 233	it('returns REASONING (not pending) when not streaming', () => {234		const msg = makeAssistant({235			reasoningContent: 'Let me think...'236		});237		const sections = deriveAgenticSections(msg, [], [], false);238 239		expect(sections).toHaveLength(1);240		expect(sections[0].type).toBe(AgenticSectionType.REASONING);241	});242 243	it('multi-turn: streaming tool calls on last turn', () => {244		const assistant1 = makeAssistant({245			toolCalls: JSON.stringify([246				{ function: { arguments: '{}', name: 'search' }, id: 'call_1', type: 'function' }247			])248		});249		const tool1 = makeToolMsg({ content: 'result', toolCallId: 'call_1' });250		const assistant2 = makeAssistant({ content: '', id: 'ast-2' });251		const streamingToolCalls: ApiChatCompletionToolCall[] = [252			{ function: { arguments: '{"pa', name: 'write_file' }, id: 'call_2', type: 'function' }253		];254		const sections = deriveAgenticSections(assistant1, [tool1, assistant2], streamingToolCalls);255 256		// Turn 1: tool_call257		// Turn 2 (streaming): streaming tool call258		expect(sections.some((s) => s.type === AgenticSectionType.TOOL_CALL)).toBe(true);259		expect(sections.some((s) => s.type === AgenticSectionType.TOOL_CALL_STREAMING)).toBe(true);260	});261});262 263describe('hasAgenticContent', () => {264	it('returns false for plain assistant', () => {265		const msg = makeAssistant({ content: 'Just text' });266 267		expect(hasAgenticContent(msg)).toBe(false);268	});269 270	it('returns true when message has toolCalls', () => {271		const msg = makeAssistant({272			toolCalls: JSON.stringify([273				{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }274			])275		});276 277		expect(hasAgenticContent(msg)).toBe(true);278	});279 280	it('returns true when toolMessages are provided', () => {281		const msg = makeAssistant();282		const tool = makeToolMsg();283 284		expect(hasAgenticContent(msg, [tool])).toBe(true);285	});286 287	it('returns false for empty toolCalls JSON', () => {288		const msg = makeAssistant({ toolCalls: '[]' });289 290		expect(hasAgenticContent(msg)).toBe(false);291	});292});293 294// The turn-section cache: completed turns are immutable, so repeated295// derivations return the same section objects - which is what keeps tool296// block props stable while another turn streams. Every field the cache297// compares must invalidate it; a miss here renders stale content.298 299describe('completed turn section reuse', () => {300	const toolCallsJson = JSON.stringify([301		{ function: { arguments: '{"path":"/a"}', name: 'test' }, id: 'call_1', type: 'function' }302	]);303 304	function makeSession() {305		return {306			anchor: makeAssistant({307				content: 'answer',308				reasoningContent: 'thinking',309				toolCalls: toolCallsJson310			}),311			tools: [makeToolMsg({ content: 'tool result', extra: [{ type: 'file' } as never] })]312		};313	}314 315	it('returns the same section objects for unchanged inputs', () => {316		const { anchor, tools } = makeSession();317		const first = deriveAgenticSections(anchor, tools, [], false);318		const second = deriveAgenticSections(anchor, tools, [], false);319 320		expect(second[0]).toBe(first[0]);321		expect(second[1]).toBe(first[1]);322	});323 324	it('recomputes when the assistant content changes', () => {325		const { anchor, tools } = makeSession();326		const first = deriveAgenticSections(anchor, tools, [], false);327 328		anchor.content = 'edited';329		const second = deriveAgenticSections(anchor, tools, [], false);330 331		expect(second).not.toBe(first);332		expect(second.some((s) => s.type === AgenticSectionType.TEXT && s.content === 'edited')).toBe(333			true334		);335	});336 337	it('recomputes when reasoning content changes', () => {338		const { anchor, tools } = makeSession();339		const first = deriveAgenticSections(anchor, tools, [], false);340 341		anchor.reasoningContent = 'new thinking';342		const second = deriveAgenticSections(anchor, tools, [], false);343 344		expect(second).not.toBe(first);345	});346 347	it('recomputes when toolCalls change', () => {348		const { anchor, tools } = makeSession();349		const first = deriveAgenticSections(anchor, tools, [], false);350 351		anchor.toolCalls = '[]';352		const second = deriveAgenticSections(anchor, tools, [], false);353 354		expect(second).not.toBe(first);355	});356 357	it('recomputes when a tool result or its extras change', () => {358		const { anchor, tools } = makeSession();359		const first = deriveAgenticSections(anchor, tools, [], false);360 361		tools[0].content = 'new tool result';362		expect(deriveAgenticSections(anchor, tools, [], false)).not.toBe(first);363 364		const firstAfterContent = deriveAgenticSections(anchor, tools, [], false);365 366		tools[0].extra = [{ type: 'image' } as never];367		expect(deriveAgenticSections(anchor, tools, [], false)).not.toBe(firstAfterContent);368	});369 370	it('never reuses the streaming turn', () => {371		const { anchor, tools } = makeSession();372		const first = deriveAgenticSections(anchor, tools, [], true);373		const second = deriveAgenticSections(anchor, tools, [], true);374 375		expect(second).not.toBe(first);376	});377 378	it('keeps completed turns stable while the last turn streams', () => {379		const anchor = makeAssistant({380			content: 'turn one',381			id: 'ast-1',382			toolCalls: JSON.stringify([383				{ function: { arguments: '{}', name: 'test' }, id: 'call_1', type: 'function' }384			])385		});386		const continuation = makeAssistant({ content: 'turn two', id: 'ast-2' });387		const tools = [388			makeToolMsg({ content: 'r1', id: 'tool-1', toolCallId: 'call_1' }),389			continuation,390			makeToolMsg({ content: 'r2', id: 'tool-2', toolCallId: 'call_2' })391		];392		const first = deriveAgenticSections(anchor, tools, [], true);393		const second = deriveAgenticSections(anchor, tools, [], true);394 395		// turn one is complete: identical section objects across derivations396		expect(second.slice(0, 2)).toEqual(first.slice(0, 2));397		expect(second[0]).toBe(first[0]);398		expect(second[1]).toBe(first[1]);399 400		// the streaming last turn recomputed: fresh section objects401		expect(second[second.length - 1]).not.toBe(first[first.length - 1]);402	});403});404