Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 19 additions & 40 deletions src/system/rag/builders/ChatRAGBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,37 +318,21 @@ export class ChatRAGBuilder extends RAGBuilder {
// 2.4. Inject RAG source context into system prompt — GENERIC LOOP
// Each RAGSource provides a systemPromptSection. We inject them all without
// knowing source names. Adding a new source requires ZERO changes here.
//
// Phase 1.5 (issue #918): the assembly order is enforced for stable
// byte-prefix prompts so llama-server / DMR can reuse KV cache. Order:
// 1. Identity systemPrompt (INVARIANT — already in finalIdentity)
// 2. Tool definitions (INVARIANT — moved here from end)
// 3. Loop iterates systemPromptSections in tier-sorted order
// (Phase 1's RAGComposer sort guarantees Map iteration order is
// INVARIANT → SEMI_STABLE → VOLATILE → alphabetical within tier)
// 4. Human presence (VOLATILE — moved here from start)
// Volatile content lives only in the suffix; the INVARIANT prefix is
// byte-identical across thousands of turns for the same persona+recipe.
const finalIdentity = { ...identity };

// 2.4.1. Inject INVARIANT tool definitions FIRST (after identity).
// Tool definitions are INVARIANT per the source classification — they
// change only when the tool catalog itself changes, not per request.
// Putting them at the top of the prefix maximizes the byte-stable
// region that DMR can reuse.
const toolDefinitionsPrompt = systemPromptSections.get('tool-definitions');
let injectedCount = 0;
if (!isSmallContext && toolDefinitionsPrompt) {
finalIdentity.systemPrompt += toolDefinitionsPrompt;
injectedCount++;
this.log(`🔧 ChatRAGBuilder: Injected tool definitions (INVARIANT, byte-stable prefix region)`);
// 2.4.1. Inject human presence awareness (which room each user is viewing)
// This is NOT a RAG source — it's lightweight synchronous state, always injected.
const allPresence = HumanPresenceTracker.allPresence;
if (allPresence.length > 0) {
const lines = allPresence.map(p => {
const viewingThis = p.roomId === contextId;
return `- ${p.displayName} is viewing: ${p.roomName}${viewingThis ? ' (this room — they can see your response in real-time)' : ''}`;
});
finalIdentity.systemPrompt = finalIdentity.systemPrompt +
`\n\n## HUMAN PRESENCE\n${lines.join('\n')}`;
}
Comment on lines +327 to 333
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

displayName and roomName come from presence events (ultimately user/room-provided strings) and are interpolated directly into the system prompt. This allows prompt-injection via crafted names (e.g., embedded newlines/markdown headers/instructions) at the highest-priority prompt level. Consider sanitizing/escaping these fields (at least strip newlines and other control characters, enforce a tight length cap) and/or clearly delimiting them as untrusted data (e.g., quoted/escaped) before inserting into systemPrompt.

Copilot uses AI. Check for mistakes.
Comment on lines +323 to 333
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The small-context guard above says tight context models should skip non-essential injections, but human presence is still injected unconditionally. With many users this can bloat the system prompt and defeat the small-context safeguard. Consider gating this block behind !isSmallContext, or truncating/condensing presence output (e.g., cap N users / shorten text) so small-context prompts remain within budget.

Copilot uses AI. Check for mistakes.

// 2.4.2. Inject all OTHER RAG source systemPromptSections in tier order.
//
// The Map iteration order matches the (tier, sourceName) sort that
// RAGComposer applied to result.sections in Phase 1 — Map preserves
// insertion order, and extractFromComposition inserts in that order.
// 2.4.2. Inject all RAG source systemPromptSections generically
//
// Sources with wrapper instructions — the section content gets wrapped with
// additional context instructions. Eventually these wrappers should move INTO
Expand All @@ -365,9 +349,10 @@ export class ChatRAGBuilder extends RAGBuilder {
// Codebase search is critical — if someone asks about code, they need the answer.
const ALWAYS_INJECT = new Set(['codebase-search']);

// Tool definitions already injected above; skip in the generic loop.
// Tool definitions are injected separately (native specs vs XML have different paths)
const SKIP_GENERIC = new Set(['tool-definitions']);

let injectedCount = 0;
for (const [sourceName, section] of systemPromptSections) {
if (SKIP_GENERIC.has(sourceName)) continue;
if (isSmallContext && !ALWAYS_INJECT.has(sourceName)) continue;
Expand All @@ -378,18 +363,12 @@ export class ChatRAGBuilder extends RAGBuilder {
this.log(`🔧 ChatRAGBuilder: Injected ${sourceName} into system prompt`);
}

// 2.4.3. Inject VOLATILE human presence LAST.
// HumanPresenceTracker is not a RAGSource but its content is volatile
// (changes when any user switches rooms). It must live in the suffix,
// never in the byte-stable prefix region.
const allPresence = HumanPresenceTracker.allPresence;
if (allPresence.length > 0) {
const lines = allPresence.map(p => {
const viewingThis = p.roomId === contextId;
return `- ${p.displayName} is viewing: ${p.roomName}${viewingThis ? ' (this room — they can see your response in real-time)' : ''}`;
});
finalIdentity.systemPrompt = finalIdentity.systemPrompt +
`\n\n## HUMAN PRESENCE\n${lines.join('\n')}`;
// 2.4.3. Inject XML tool definitions for text-based providers (budget-aware via ToolDefinitionsSource)
const toolDefinitionsPrompt = systemPromptSections.get('tool-definitions');
if (!isSmallContext && toolDefinitionsPrompt) {
finalIdentity.systemPrompt += toolDefinitionsPrompt;
injectedCount++;
this.log(`🔧 ChatRAGBuilder: Injected tool definitions into system prompt (XML format)`);
Comment on lines +366 to +371
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment/log claim the injected tool-definitions section is "XML format" and "for text-based providers", but ToolDefinitionsSource can also emit a non-XML behavioral nudge for native-tool providers. Please update the comment and log message to avoid mislabeling what’s being appended (or branch the log based on metadata.format if you need to distinguish XML vs native).

Suggested change
// 2.4.3. Inject XML tool definitions for text-based providers (budget-aware via ToolDefinitionsSource)
const toolDefinitionsPrompt = systemPromptSections.get('tool-definitions');
if (!isSmallContext && toolDefinitionsPrompt) {
finalIdentity.systemPrompt += toolDefinitionsPrompt;
injectedCount++;
this.log(`🔧 ChatRAGBuilder: Injected tool definitions into system prompt (XML format)`);
// 2.4.3. Inject tool-definitions content (budget-aware via ToolDefinitionsSource)
const toolDefinitionsPrompt = systemPromptSections.get('tool-definitions');
if (!isSmallContext && toolDefinitionsPrompt) {
finalIdentity.systemPrompt += toolDefinitionsPrompt;
injectedCount++;
this.log(`🔧 ChatRAGBuilder: Injected tool definitions/instructions into system prompt`);

Copilot uses AI. Check for mistakes.
}

if (isSmallContext) {
Expand Down
Loading