import debug from 'debug';
import { BaseLastUserContentProvider } from '../base/BaseLastUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
topicReferenceCount?: number;
topicReferenceInjected?: boolean;
}
}
const log = debug('context-engine:provider:TopicReferenceContextInjector');
/**
* Topic reference item
*/
export interface TopicReferenceItem {
/** Recent messages as fallback when no summary available */
recentMessages?: Array<{ content: string; role: string }>;
/** The topic's history summary (undefined if no summary available) */
summary?: string;
/** The topic ID */
topicId: string;
/** The topic title */
topicTitle?: string;
}
export interface TopicReferenceContextInjectorConfig {
/** Whether topic reference context injection is enabled */
enabled?: boolean;
/** Referenced topics (with or without summaries) */
topicReferences?: TopicReferenceItem[];
}
/**
* Format topic references for injection
* Topics with summaries are inlined; topics without summaries prompt the agent to use the tool
*/
function formatRecentMessages(messages: Array<{ content: string; role: string }>): string {
return messages
.map((msg) => {
const role = msg.role === 'user' ? 'User' : msg.role === 'assistant' ? 'Assistant' : msg.role;
return `**${role}**: ${msg.content}`;
})
.join('\n\n');
}
function formatTopicReferences(items: TopicReferenceItem[]): string | null {
if (items.length === 0) return null;
const withContext = items.filter((item) => item.summary || item.recentMessages?.length);
const withoutContext = items.filter((item) => !item.summary && !item.recentMessages?.length);
const lines: string[] = [
'',
];
for (const item of withContext) {
const title = item.topicTitle ? ` title="${item.topicTitle}"` : '';
const type = item.summary ? 'summary' : 'recent_messages';
lines.push(``);
if (item.summary) {
lines.push(item.summary);
} else {
lines.push(formatRecentMessages(item.recentMessages!));
}
lines.push('');
}
if (withoutContext.length > 0) {
lines.push(
'',
);
for (const item of withoutContext) {
const title = item.topicTitle ? ` title="${item.topicTitle}"` : '';
lines.push(``);
}
lines.push('');
}
lines.push('');
return lines.join('\n');
}
/**
* Topic Reference Context Injector
*
* When user messages contain tags referencing other topics,
* this injector appends the referenced topics' context to the last user message.
* - Topics with summaries: summary is inlined directly
* - Topics without summaries: a hint is added prompting the agent to call the getTopicContext tool
*/
export class TopicReferenceContextInjector extends BaseLastUserContentProvider {
readonly name = 'TopicReferenceContextInjector';
constructor(
private config: TopicReferenceContextInjectorConfig,
options: ProcessorOptions = {},
) {
super(options);
}
protected async doProcess(context: PipelineContext): Promise {
log('doProcess called');
log('config.enabled:', this.config.enabled);
const clonedContext = this.cloneContext(context);
if (
!this.config.enabled ||
!this.config.topicReferences ||
this.config.topicReferences.length === 0
) {
log('Topic reference not enabled or no references, skipping injection');
return this.markAsExecuted(clonedContext);
}
const formattedContent = formatTopicReferences(this.config.topicReferences);
if (!formattedContent) {
log('No topic reference content to inject');
return this.markAsExecuted(clonedContext);
}
log('Formatted content length:', formattedContent.length);
const lastUserIndex = this.findLastUserMessageIndex(clonedContext.messages);
log('Last user message index:', lastUserIndex);
if (lastUserIndex === -1) {
log('No user messages found, skipping injection');
return this.markAsExecuted(clonedContext);
}
const hasExistingWrapper = this.hasExistingSystemContext(clonedContext);
const contentToAppend = hasExistingWrapper
? this.createContextBlock(formattedContent, 'topic_reference_context')
: this.wrapWithSystemContext(formattedContent, 'topic_reference_context');
this.appendToLastUserMessage(clonedContext, contentToAppend);
clonedContext.metadata.topicReferenceInjected = true;
clonedContext.metadata.topicReferenceCount = this.config.topicReferences.length;
log('Topic reference context appended to last user message');
return this.markAsExecuted(clonedContext);
}
}