MCPcopy Create free account
hub / github.com/adcontextprotocol/adcp / buildMessageTurnsWithMetadata

Function buildMessageTurnsWithMetadata

server/src/addie/prompts.ts:588–711  ·  view source on GitHub ↗
(
  userMessage: string,
  threadContext?: ThreadContextEntry[],
  options?: BuildMessageTurnsOptions
)

Source from the content-addressed store, hash-verified

586 * Use this when you need visibility into whether conversation was trimmed.
587 */
588export function buildMessageTurnsWithMetadata(
589 userMessage: string,
590 threadContext?: ThreadContextEntry[],
591 options?: BuildMessageTurnsOptions
592): BuildMessageTurnsResult {
593 const maxMessages = options?.maxMessages ?? 50;
594 // Pass toolCount for more accurate token budget when available
595 const tokenLimit = options?.tokenLimit ?? getConversationTokenLimit(options?.model, options?.toolCount);
596
597 let messages: MessageTurn[] = [];
598
599 // Detect whether the thread has multiple distinct human speakers. When it
600 // does, prefix every user-role turn with the speaker's name so the model
601 // can tell when the speaker switches mid-thread (e.g. an admin replying to
602 // a non-member's question). 'User' is the unknown-speaker sentinel and is
603 // not counted. Names are re-sanitized here as defense in depth — callers
604 // are expected to sanitize at ingest, but stored rows or hand-built
605 // entries shouldn't be able to break out of the `[name] text` envelope.
606 const sanitizedCurrent = sanitizeSpeakerName(options?.currentSpeakerName);
607 const distinctSpeakers = new Set<string>();
608 if (threadContext) {
609 for (const e of threadContext) {
610 if (e.user && e.user !== 'Addie' && e.user !== 'User') {
611 const clean = sanitizeSpeakerName(e.user);
612 if (clean) distinctSpeakers.add(clean);
613 }
614 }
615 }
616 if (sanitizedCurrent) distinctSpeakers.add(sanitizedCurrent);
617 const isMultiSpeaker = distinctSpeakers.size > 1;
618
619 if (threadContext && threadContext.length > 0) {
620 // First pass: apply message count limit if specified
621 let recentHistory = maxMessages > 0
622 ? threadContext.slice(-maxMessages)
623 : threadContext;
624
625 // Convert each entry to proper message turn
626 // Skip empty messages defensively
627 for (const entry of recentHistory) {
628 const trimmedText = entry.text?.trim();
629 if (!trimmedText) continue;
630 const role: 'user' | 'assistant' = entry.user === 'Addie' ? 'assistant' : 'user';
631 const cleanSpeaker = role === 'user' && entry.user !== 'User'
632 ? sanitizeSpeakerName(entry.user)
633 : undefined;
634 // Skip the prefix when content already starts with `[` to avoid
635 // double-bracketed turns like `[Brian] [User reacted with ...]`.
636 const content = (isMultiSpeaker && role === 'user' && cleanSpeaker && !trimmedText.startsWith('['))
637 ? `[${cleanSpeaker}] ${trimmedText}`
638 : trimmedText;
639 // Pass through tool calls so claude-client can reconstruct proper API blocks
640 const toolCalls = (role === 'assistant' && entry.toolCalls && entry.toolCalls.length > 0)
641 ? entry.toolCalls.map(tc => ({
642 name: tc.name,
643 input: tc.input,
644 result: typeof tc.result === 'string' ? tc.result : tc.result != null ? JSON.stringify(tc.result) : '',
645 is_error: tc.is_error,

Calls 6

sanitizeSpeakerNameFunction · 0.85
compactOldToolResultsFunction · 0.85
trimConversationHistoryFunction · 0.85
addMethod · 0.80
trimMethod · 0.65

Tested by

no test coverage detected