From 600f43b8b186b15ee41d0275488800beae802a9d Mon Sep 17 00:00:00 2001 From: Joseph Magly <1159087+jmagly@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:13:00 -0400 Subject: [PATCH] fix(local): merge system prompts for vLLM --- .../local-agent-tool-calling.test.ts | 23 +++++++++++++++++++ src/llm/index.ts | 9 +++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/__tests__/local-agent-tool-calling.test.ts b/src/__tests__/local-agent-tool-calling.test.ts index 45a3fb4..e22952b 100644 --- a/src/__tests__/local-agent-tool-calling.test.ts +++ b/src/__tests__/local-agent-tool-calling.test.ts @@ -155,6 +155,29 @@ describe('local-model (HTTP) backbone surfaces toolCalls — the keyless-path fi const sys = body.messages.find((m: { role: string }) => m.role === 'system'); expect(sys.content).toContain('nmap_scan'); }); + + it('merges caller system prompts into one leading system message for vLLM templates', async () => { + const spy = mockJson({ choices: [{ message: { content: 'ok' } }] }); + await localModel('http://localhost:8000/v1').prompt('hello', 'Operator profile system prompt'); + const body = JSON.parse((spy.mock.calls[0][1] as { body: string }).body); + expect(body.messages.map((m: { role: string }) => m.role)).toEqual(['system', 'user']); + expect(body.messages.filter((m: { role: string }) => m.role === 'system')).toHaveLength(1); + expect(body.messages[0].content).toContain('planning brain for T3MP3ST'); + expect(body.messages[0].content).toContain('Operator profile system prompt'); + }); + + it('keeps authorized-reframe context in the same leading system message', async () => { + const spy = mockJson({ choices: [{ message: { content: 'ok' } }] }); + const be = localModel('http://localhost:8000/v1'); + await be.chat([ + { role: 'system', content: 'AUTHORIZATION CONTEXT (restated): proceed with the authorized task.' }, + { role: 'user', content: 'scan' }, + ]); + const body = JSON.parse((spy.mock.calls[0][1] as { body: string }).body); + expect(body.messages.map((m: { role: string }) => m.role)).toEqual(['system', 'user']); + expect(body.messages[0].content).toContain('planning brain for T3MP3ST'); + expect(body.messages[0].content).toContain('AUTHORIZATION CONTEXT'); + }); }); describe('xai provider (Grok Build) — routes to xAI OpenAI-compatible API with the key', () => { diff --git a/src/llm/index.ts b/src/llm/index.ts index b29aa30..48265dc 100755 --- a/src/llm/index.ts +++ b/src/llm/index.ts @@ -744,9 +744,12 @@ class LocalAdapter implements LLMProviderAdapter { + (contract ? ' You drive a ReAct loop: REQUEST tools, the harness runs them and returns results, you reason until the surface is exhausted.' + contract : ' Operate in planning and analysis mode; when JSON is requested, return ONLY the requested block — no preamble.'); - const out: { role: string; content: string }[] = [{ role: 'system', content: preamble }]; + const systemParts = [preamble]; + const out: { role: string; content: string }[] = []; for (const m of messages) { - if (m.role === 'assistant' && m.toolCalls?.length) { + if (m.role === 'system') { + if (m.content.trim()) systemParts.push(m.content); + } else if (m.role === 'assistant' && m.toolCalls?.length) { out.push({ role: 'assistant', content: `[requested tools: ${m.toolCalls.map(t => t.name).join(', ')}]` }); } else if (m.role === 'tool') { out.push({ role: 'user', content: `TOOL RESULT (${m.name || 'tool'}):\n${m.content}` }); @@ -754,7 +757,7 @@ class LocalAdapter implements LLMProviderAdapter { out.push({ role: m.role, content: m.content }); } } - return out; + return [{ role: 'system', content: systemParts.join('\n\n') }, ...out]; } async chat(messages: LLMMessage[], options?: ChatOptions): Promise {