From 8f4c4447918dd376147c1f3e4368c5016aba410a Mon Sep 17 00:00:00 2001 From: Connor Adams Date: Sat, 3 Jan 2026 13:15:47 +0000 Subject: [PATCH] Preserve AGENTS.md in context --- lib/request/request-transformer.ts | 88 ++++++++++++++++++-- test/request-transformer.test.ts | 127 +++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 7 deletions(-) diff --git a/lib/request/request-transformer.ts b/lib/request/request-transformer.ts index 05705ed..4c28f61 100644 --- a/lib/request/request-transformer.ts +++ b/lib/request/request-transformer.ts @@ -324,11 +324,58 @@ export function isOpenCodeSystemPrompt( return contentText.startsWith("You are a coding agent running in"); } +/** + * Extract content text from an input item + * @param item - Input item + * @returns Content as string + */ +function getContentText(item: InputItem): string { + if (typeof item.content === "string") { + return item.content; + } + if (Array.isArray(item.content)) { + return item.content + .filter((c) => c.type === "input_text" && c.text) + .map((c) => c.text) + .join("\n"); + } + return ""; +} + +/** + * Extract AGENTS.md content from a concatenated OpenCode message + * + * OpenCode concatenates multiple pieces into a single developer message: + * 1. Base codex.txt prompt (starts with "You are a coding agent running in...") + * 2. Environment info + * 3. block + * 4. AGENTS.md content (prefixed with "Instructions from: /path/to/AGENTS.md") + * + * This function extracts the AGENTS.md portions so they can be preserved + * when filtering out the OpenCode base prompt. + * + * @param contentText - The full content text of the message + * @returns The AGENTS.md content if found, null otherwise + */ +function extractAgentsMdContent(contentText: string): string | null { + const marker = "Instructions from:"; + const idx = contentText.indexOf(marker); + if (idx > 0) { + return contentText.slice(idx).trimStart(); + } + return null; +} + /** * Filter out OpenCode system prompts from input * Used in CODEX_MODE to replace OpenCode prompts with Codex-OpenCode bridge + * + * When OpenCode sends a concatenated message containing both the base prompt + * AND AGENTS.md content, this function extracts and preserves the AGENTS.md + * portions while filtering out the OpenCode base prompt. + * * @param input - Input array - * @returns Input array without OpenCode system prompts + * @returns Input array without OpenCode system prompts (but with AGENTS.md preserved) */ export async function filterOpenCodeSystemPrompts( input: InputItem[] | undefined, @@ -344,12 +391,39 @@ export async function filterOpenCodeSystemPrompts( // This is safe because we still have the "starts with" check } - return input.filter((item) => { - // Keep user messages - if (item.role === "user") return true; - // Filter out OpenCode system prompts - return !isOpenCodeSystemPrompt(item, cachedPrompt); - }); + const result: InputItem[] = []; + + for (const item of input) { + // Keep user messages as-is + if (item.role === "user") { + result.push(item); + continue; + } + + // Check if this is an OpenCode system prompt + if (isOpenCodeSystemPrompt(item, cachedPrompt)) { + // OpenCode may concatenate AGENTS.md content with the base prompt + // Extract and preserve any AGENTS.md content + const contentText = getContentText(item); + const agentsMdContent = extractAgentsMdContent(contentText); + + if (agentsMdContent) { + // Create a new message with just the AGENTS.md content + result.push({ + type: "message", + role: "developer", + content: agentsMdContent, + }); + } + // Filter out the OpenCode base prompt (don't add original item) + continue; + } + + // Keep all other messages + result.push(item); + } + + return result; } /** diff --git a/test/request-transformer.test.ts b/test/request-transformer.test.ts index e0fced8..e540f50 100644 --- a/test/request-transformer.test.ts +++ b/test/request-transformer.test.ts @@ -544,6 +544,133 @@ describe('Request Transformer Module', () => { it('should return undefined for undefined input', async () => { expect(await filterOpenCodeSystemPrompts(undefined)).toBeUndefined(); }); + + // Tests for concatenated messages (single message containing both prompt AND AGENTS.md) + // This is how OpenCode actually sends content (as of v1.0.164+). + // The tests above cover separate messages pattern which may also occur. + describe('concatenated messages (OpenCode v1.0.164+ pattern)', () => { + it('should extract and preserve AGENTS.md when concatenated with OpenCode prompt', async () => { + // OpenCode sends a SINGLE message containing: + // 1. Base codex.txt prompt + // 2. Environment info + // 3. block + // 4. AGENTS.md content (prefixed with "Instructions from:") + const input: InputItem[] = [ + { + type: 'message', + role: 'developer', + content: `You are a coding agent running in the opencode, a terminal-based coding assistant. + +Here is some useful information about the environment you are running in: + + Working directory: /Users/test/project + Platform: darwin + + + src/ + index.ts + +Instructions from: /Users/test/project/AGENTS.md +# Project Guidelines + +Use TypeScript for all new code. +Follow existing patterns in the codebase. + +Instructions from: /Users/test/.config/opencode/AGENTS.md +# Global Settings + +Always use mise for tool management.`, + }, + { type: 'message', role: 'user', content: 'hello' }, + ]; + + const result = await filterOpenCodeSystemPrompts(input); + + // Should have 2 messages: extracted AGENTS.md content + user message + expect(result).toHaveLength(2); + expect(result![0].role).toBe('developer'); + expect(result![0].content).toContain('Instructions from:'); + expect(result![0].content).toContain('Project Guidelines'); + expect(result![0].content).toContain('Global Settings'); + // Should NOT contain the OpenCode base prompt + expect(result![0].content).not.toContain('You are a coding agent running in'); + expect(result![1].role).toBe('user'); + }); + + it('should preserve multiple AGENTS.md files in concatenated message', async () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'developer', + content: `You are a coding agent running in the opencode... + +Instructions from: /project/AGENTS.md +# Project AGENTS.md +Project-specific instructions here. + +Instructions from: /project/src/AGENTS.md +# Nested AGENTS.md +More specific instructions for src folder. + +Instructions from: ~/.config/opencode/AGENTS.md +# Global AGENTS.md +Global instructions here.`, + }, + { type: 'message', role: 'user', content: 'test' }, + ]; + + const result = await filterOpenCodeSystemPrompts(input); + + expect(result).toHaveLength(2); + // All AGENTS.md content should be preserved + expect(result![0].content).toContain('Project AGENTS.md'); + expect(result![0].content).toContain('Nested AGENTS.md'); + expect(result![0].content).toContain('Global AGENTS.md'); + }); + + it('should handle concatenated message with no AGENTS.md (just base prompt)', async () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'developer', + content: 'You are a coding agent running in the opencode, a terminal-based coding assistant.', + }, + { type: 'message', role: 'user', content: 'hello' }, + ]; + + const result = await filterOpenCodeSystemPrompts(input); + + // Should just have the user message (base prompt filtered, no AGENTS.md to preserve) + expect(result).toHaveLength(1); + expect(result![0].role).toBe('user'); + }); + + it('should handle array content format in concatenated message', async () => { + const input: InputItem[] = [ + { + type: 'message', + role: 'developer', + content: [ + { + type: 'input_text', + text: `You are a coding agent running in the opencode... + +Instructions from: /project/AGENTS.md +# My Custom Instructions +Do things this way.`, + }, + ], + }, + { type: 'message', role: 'user', content: 'hello' }, + ]; + + const result = await filterOpenCodeSystemPrompts(input); + + expect(result).toHaveLength(2); + expect(result![0].content).toContain('My Custom Instructions'); + expect(result![0].content).not.toContain('You are a coding agent'); + }); + }); }); describe('addCodexBridgeMessage', () => {