/** * XML Directive Parser * * Parses an block at the start of agent text responses. * Extends the existing pattern to support richer actions * (reactions, file sends, etc.) without requiring tool calls. * * The block must appear at the start of the response: * * * * * Great idea! * * → cleanText: "Great idea!" * → directives: [{ type: 'react', emoji: 'thumbsup' }] */ export interface ReactDirective { type: 'react'; emoji: string; messageId?: string; } // Union type — extend with more directive types later export type Directive = ReactDirective; export interface ParseResult { cleanText: string; directives: Directive[]; } /** * Match the ... wrapper at the start of the response. * Captures the inner content of the block. */ const ACTIONS_BLOCK_REGEX = /^\s*([\s\S]*?)<\/actions>/; /** * Match self-closing child directive tags inside the actions block. * Captures the tag name and the full attributes string. */ const CHILD_DIRECTIVE_REGEX = /<(react)\s+((?:[a-zA-Z-]+="[^"]*"\s*)+)\s*\/>/g; /** * Parse a single attribute string like: emoji="eyes" message="123" */ function parseAttributes(attrString: string): Record { const attrs: Record = {}; const attrRegex = /([a-zA-Z-]+)="([^"]*)"/g; let match; while ((match = attrRegex.exec(attrString)) !== null) { attrs[match[1]] = match[2]; } return attrs; } /** * Parse child directives from the inner content of an block. */ function parseChildDirectives(block: string): Directive[] { const directives: Directive[] = []; let match; // Reset regex state (global flag) CHILD_DIRECTIVE_REGEX.lastIndex = 0; while ((match = CHILD_DIRECTIVE_REGEX.exec(block)) !== null) { const [, tagName, attrString] = match; if (tagName === 'react') { const attrs = parseAttributes(attrString); if (attrs.emoji) { directives.push({ type: 'react', emoji: attrs.emoji, ...(attrs.message ? { messageId: attrs.message } : {}), }); } } } return directives; } /** * Parse XML directives from agent response text. * * Looks for an ... block at the start of the response. * Returns the cleaned text (block stripped) and an array of parsed directives. * If no block is found, the text is returned unchanged. */ export function parseDirectives(text: string): ParseResult { const match = text.match(ACTIONS_BLOCK_REGEX); if (!match) { return { cleanText: text, directives: [] }; } const actionsContent = match[1]; const cleanText = text.slice(match[0].length).trim(); const directives = parseChildDirectives(actionsContent); return { cleanText, directives }; } /** * Strip a leading ... block from text for streaming display. * Returns the text after the block, or the original text if no complete block found. */ export function stripActionsBlock(text: string): string { return text.replace(ACTIONS_BLOCK_REGEX, '').trim(); }