automagik-genie
Version:
Self-evolving AI agent orchestration framework with Model Context Protocol support
256 lines (255 loc) • 9.27 kB
JavaScript
;
/**
* Markdown Formatter for Token-Efficient AI-to-AI Orchestration Output
*
* Replaces verbose Ink rendering (16k tokens) with compact markdown (~300-500 tokens).
* Provides 3 output modes optimized for different orchestration scenarios.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatSessionList = void 0;
exports.formatTranscriptMarkdown = formatTranscriptMarkdown;
exports.formatTaskList = formatTaskList;
// ============================================================================
// Constants
// ============================================================================
const TOKEN_BUDGET = {
final: 500, // ~2000 chars
recent: 300, // ~1200 chars
overview: 400, // ~1600 chars
full: Infinity // No truncation - complete transcript
};
const CHARS_PER_TOKEN = 4; // Rough estimate for token counting
// ============================================================================
// Main Formatting Functions
// ============================================================================
/**
* Format transcript messages into markdown with specified mode
*
* @param messages - Array of chat messages in temporal order
* @param meta - Session metadata (id, agent, status, metrics)
* @param mode - Output mode: 'final' | 'recent' | 'overview'
* @returns Formatted markdown string optimized for token efficiency
*/
function formatTranscriptMarkdown(messages, meta, mode) {
const parts = [];
// Session header
const taskId = meta.taskId || 'pending';
parts.push(`## Session: ${taskId}`);
switch (mode) {
case 'final':
return formatFinalMode(messages, meta, parts);
case 'recent':
return formatRecentMode(messages, meta, parts);
case 'overview':
return formatOverviewMode(messages, meta, parts);
case 'full':
return formatFullMode(messages, meta, parts);
default:
return formatRecentMode(messages, meta, parts); // Default to recent
}
}
/**
* Format task list into compact markdown table
*
* @param sessions - Array of task entries
* @returns Markdown table with task info
*/
function formatTaskList(sessions) {
if (sessions.length === 0) {
return '**No tasks found**\n';
}
const parts = [];
parts.push('## Active Tasks\n');
parts.push('| Task ID | Agent | Status | Executor | Model |');
parts.push('|---------|-------|--------|----------|-------|');
for (const task of sessions) {
const id = trimTaskId(task.taskId);
const model = task.model ? task.model : '';
parts.push(`| ${id} | ${task.agent} | ${task.status} | ${task.executor} | ${model} |`);
}
return parts.join('\n') + '\n';
}
/** @deprecated Use formatTaskList instead. */
exports.formatSessionList = formatTaskList;
// ============================================================================
// Mode-Specific Formatters
// ============================================================================
/**
* Final mode: Last message only, mini-report format
* Target: ~500 tokens (2000 chars)
* Use case: Completed tasks, final status reports
*/
function formatFinalMode(messages, meta, parts) {
parts.push(`**Agent:** ${meta.agent}`);
parts.push(`**Status:** ${meta.status || 'unknown'}`);
if (messages.length === 0) {
parts.push('\n*No messages available*\n');
return enforceTokenBudget(parts.join('\n'), 'final');
}
// Get last message
const lastMsg = messages[messages.length - 1];
parts.push(`**Last message:** ${lastMsg.title}\n`);
// Format body with truncation if needed
const body = formatMessageBody(lastMsg.body);
parts.push(body);
// Add metrics if available
if (meta.tokens) {
parts.push(`\n**Tokens:** ${meta.tokens.total}`);
}
return enforceTokenBudget(parts.join('\n'), 'final');
}
/**
* Recent mode: Latest 5 messages, compact
* Target: ~300 tokens (1200 chars)
* Use case: In-progress work, recent context (DEFAULT)
*/
function formatRecentMode(messages, meta, parts) {
parts.push(`**Agent:** ${meta.agent}`);
parts.push(`**Status:** ${meta.status || 'running'}\n`);
if (messages.length === 0) {
parts.push('*No messages yet*\n');
return enforceTokenBudget(parts.join('\n'), 'recent');
}
// Get last 5 messages
const recentMessages = messages.slice(-5);
for (let i = 0; i < recentMessages.length; i++) {
const msg = recentMessages[i];
const msgNum = messages.length - recentMessages.length + i + 1;
parts.push(`### Message ${msgNum}: ${msg.title}`);
// Compact body (truncate per message if needed)
const body = formatMessageBody(msg.body, 200); // Max 200 chars per message
parts.push(body + '\n');
}
return enforceTokenBudget(parts.join('\n'), 'recent');
}
/**
* Overview mode: Session metadata + checkpoints
* Target: ~400 tokens (1600 chars)
* Use case: High-level status, orchestration dashboards
*/
function formatOverviewMode(messages, meta, parts) {
parts.push(`**Agent:** ${meta.agent}`);
parts.push(`**Status:** ${meta.status || 'unknown'}`);
if (meta.executor) {
parts.push(`**Executor:** ${meta.executor}`);
}
if (meta.model) {
parts.push(`**Model:** ${meta.model}`);
}
// Token metrics
if (meta.tokens) {
const { input, output, total } = meta.tokens;
parts.push(`**Tokens:** ${total} (in: ${input}, out: ${output})`);
}
// Tool usage
if (meta.toolCalls && meta.toolCalls.length > 0) {
const toolSummary = meta.toolCalls
.map(tc => `${tc.name}:${tc.count}`)
.join(', ');
parts.push(`**Tools:** ${toolSummary}`);
}
// Message count
parts.push(`**Messages:** ${messages.length}\n`);
// Checkpoints (every 5th message or key milestones)
if (messages.length > 0) {
parts.push('### Key Checkpoints\n');
const checkpoints = [];
// Always include first and last
checkpoints.push(messages[0]);
// Add every 5th message
for (let i = 4; i < messages.length - 1; i += 5) {
checkpoints.push(messages[i]);
}
// Add last if different from first
if (messages.length > 1) {
checkpoints.push(messages[messages.length - 1]);
}
for (const msg of checkpoints) {
parts.push(`- **${msg.title}**: ${truncateText(msg.body.join(' '), 80)}`);
}
}
return enforceTokenBudget(parts.join('\n'), 'overview');
}
/**
* Full mode: Complete transcript with all messages
* Target: No limit (Infinity)
* Use case: Debugging, full context review, --full flag
*/
function formatFullMode(messages, meta, parts) {
parts.push(`**Agent:** ${meta.agent}`);
parts.push(`**Status:** ${meta.status || 'unknown'}`);
if (meta.executor) {
parts.push(`**Executor:** ${meta.executor}`);
}
if (meta.model) {
parts.push(`**Model:** ${meta.model}`);
}
// Message count
parts.push(`**Messages:** ${messages.length}\n`);
if (messages.length === 0) {
parts.push('*No messages available*\n');
return parts.join('\n');
}
// Show all messages with full content
parts.push('### Full Transcript\n');
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
parts.push(`#### Message ${i + 1}: ${msg.title}`);
// Show complete message body without truncation
const body = formatMessageBody(msg.body);
parts.push(body + '\n');
}
// Add metrics if available (no token budget enforcement)
if (meta.tokens) {
const { input, output, total } = meta.tokens;
parts.push(`\n**Tokens:** ${total} (in: ${input}, out: ${output})`);
}
return parts.join('\n'); // No budget enforcement
}
// ============================================================================
// Utility Functions
// ============================================================================
/**
* Format message body with optional truncation
*/
function formatMessageBody(body, maxChars) {
if (body.length === 0) {
return '*(empty)*';
}
const combined = body.join('\n');
if (maxChars && combined.length > maxChars) {
return truncateText(combined, maxChars);
}
return combined;
}
/**
* Truncate text to specified length with ellipsis
*/
function truncateText(text, maxLength) {
if (text.length <= maxLength) {
return text;
}
return text.slice(0, maxLength - 3) + '...';
}
/**
* Trim task ID for display (keep first 8 chars)
*/
function trimTaskId(taskId) {
if (taskId.length <= 10) {
return taskId;
}
return `${taskId.slice(0, 8)}...`;
}
/**
* Enforce token budget by truncating if necessary
*/
function enforceTokenBudget(content, mode) {
const budget = TOKEN_BUDGET[mode];
const maxChars = budget * CHARS_PER_TOKEN;
if (content.length <= maxChars) {
return content;
}
// Truncate to budget with warning
const truncated = content.slice(0, maxChars - 50);
return truncated + '\n\n*[Output truncated to meet token budget]*\n';
}