From 3ff9643091e460087efb29daf7c24db4141d1837 Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:15:05 +0000 Subject: [PATCH] fix instructions template modality rendering --- .changeset/instructions-template-collapse.md | 5 +++ agents/src/llm/chat_context.test.ts | 39 ++++++++++++++++++++ agents/src/llm/chat_context.ts | 9 ++--- 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 .changeset/instructions-template-collapse.md diff --git a/.changeset/instructions-template-collapse.md b/.changeset/instructions-template-collapse.md new file mode 100644 index 000000000..5698d35e6 --- /dev/null +++ b/.changeset/instructions-template-collapse.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents': patch +--- + +Fix modality-aware instruction templates to collapse identical variants and avoid duplicate rendered output. diff --git a/agents/src/llm/chat_context.test.ts b/agents/src/llm/chat_context.test.ts index 101a9fe46..c20ad249e 100644 --- a/agents/src/llm/chat_context.test.ts +++ b/agents/src/llm/chat_context.test.ts @@ -1321,6 +1321,45 @@ extra`; expect(instr.text).toBe('null=null undefined=undefined'); }); + it('tpl renders each modality variant exactly once', () => { + const instr = Instructions.tpl`${'You are a helpful assistant.'} + +${new Instructions({ audio: 'Handle noisy voice input.', text: 'Handle typed input.' })}`; + + expect(renderInstructions(instr, 'audio')).toBe( + 'You are a helpful assistant.\n\nHandle noisy voice input.', + ); + expect(renderInstructions(instr, 'text')).toBe( + 'You are a helpful assistant.\n\nHandle typed input.', + ); + expect(renderInstructions(instr, 'audio').split('You are a helpful assistant.')).toHaveLength( + 2, + ); + }); + + it('tpl without Instructions interpolations is an audio-only render', () => { + const instr = Instructions.tpl`Hello ${'Alex'}`; + + expect(instr.toJSON()).toEqual({ type: 'instructions', audio: 'Hello Alex' }); + expect(instr.audio).toBe('Hello Alex'); + expect(instr.text).toBe('Hello Alex'); + expect(renderInstructions(instr)).toBe('Hello Alex'); + expect(renderInstructions(instr, 'audio')).toBe('Hello Alex'); + }); + + it('tpl collapses identical modality variants', () => { + const instr = Instructions.tpl`${'You are a helpful assistant.'} + +${new Instructions({ audio: 'shared note', text: 'shared note' })}`; + + expect(instr.toJSON()).toEqual({ + type: 'instructions', + audio: 'You are a helpful assistant.\n\nshared note', + }); + expect(renderInstructions(instr)).toBe('You are a helpful assistant.\n\nshared note'); + expect(renderInstructions(instr, 'audio')).toBe('You are a helpful assistant.\n\nshared note'); + }); + it('serializes to a dict with both variants and round-trips through toJSON', () => { const instr = new Instructions({ audio: 'audio variant', text: 'text variant' }); diff --git a/agents/src/llm/chat_context.ts b/agents/src/llm/chat_context.ts index 34cf39200..dbb733a94 100644 --- a/agents/src/llm/chat_context.ts +++ b/agents/src/llm/chat_context.ts @@ -104,13 +104,12 @@ export class Instructions { return result; }; - const hasTextVariant = values.some( - (value) => isInstructions(value) && value._textVariant !== undefined, - ); + const audio = render('audio'); + const text = render('text'); return new Instructions({ - audio: render('audio'), - text: hasTextVariant ? render('text') : undefined, + audio, + text: audio === text ? undefined : text, represent: render('value'), }); }