This guide will walk you through everything you need to know to start using the Harmony Protocol Node.js library effectively.
- Installation & Setup
- Core Concepts
- Your First Harmony Conversation
- Understanding Channels
- Working with Tools
- Streaming Responses
- Rendering and Output Formatting
- Integration with AI Providers
- Real-World Examples
- Best Practices
- Common Pitfalls
- Troubleshooting
- Node.js ≥ 18.0.0
- TypeScript ≥ 5.0.0 (for TypeScript projects)
- OpenAI-compatible model that supports Harmony formatting
npm install harmony-protocol-jsCreate a simple test file to verify the installation:
// test-harmony.ts
import { loadHarmonyEncoding, Message, Role } from 'harmony-protocol-js';
const encoding = loadHarmonyEncoding();
const message = Message.user('Hello, Harmony!');
console.log('✅ Harmony Protocol is working!');
console.log('Message:', message.getTextContent());
console.log('Tokens:', encoding.encode(message.getTextContent()).length);Run it:
npx tsx test-harmony.tsHarmony Protocol supports five distinct roles:
enum Role {
SYSTEM = 'system', // Instructions and configuration
DEVELOPER = 'developer', // Development-specific instructions
USER = 'user', // End-user messages
ASSISTANT = 'assistant', // AI model responses
TOOL = 'tool' // Tool execution results
}Channels organize assistant responses into different types:
enum Channel {
ANALYSIS = 'analysis', // Internal reasoning
COMMENTARY = 'commentary', // Meta-commentary and explanations
FINAL = 'final' // User-facing final response
}Harmony uses special tokens to structure conversations:
| Token | ID | Purpose |
|---|---|---|
| `< | start | >` |
| `< | message | >` |
| `< | end | >` |
| `< | channel | >` |
Let's build a complete example step by step:
import { Message, Role, createSystemContent } from 'harmony-protocol-js';
// System message with configuration
const systemContent = createSystemContent()
.withIdentity('Helpful AI Assistant')
.withRequiredChannels(['analysis', 'final'])
.withReasoningEffort('medium')
.build();
const systemMessage = Message.system(systemContent);
const userMessage = Message.user('What is the capital of France?');import { Conversation } from 'harmony-protocol-js';
const conversation = Conversation.fromMessages([
systemMessage,
userMessage
]);
// Validate the conversation
const validation = conversation.validate();
if (!validation.isValid) {
console.error('Validation errors:', validation.errors);
}import { loadHarmonyEncoding } from 'harmony-protocol-js';
const encoding = loadHarmonyEncoding();
// Get tokens ready for your OpenAI model
const tokens = encoding.renderConversationForCompletion(conversation, Role.ASSISTANT);
console.log(`Ready to send ${tokens.length} tokens to model`);// After getting response from your model
function parseModelResponse(responseTokens: number[]) {
const messages = encoding.parseMessagesFromCompletionTokens(
responseTokens,
Role.ASSISTANT
);
// Extract different channels
const analysis = messages.find(m => m.channel === Channel.ANALYSIS);
const final = messages.find(m => m.channel === Channel.FINAL);
return {
reasoning: analysis?.getTextContent(),
answer: final?.getTextContent()
};
}Channels allow the AI model to provide structured, multi-layered responses:
Internal reasoning and problem breakdown:
// Model might output:
// <|start|>assistant<|channel|>analysis<|message|>
// The user is asking about France's capital. This is a straightforward
// geographical question. The capital of France is Paris.
// <|end|>Meta-information and context:
// <|start|>assistant<|channel|>commentary<|message|>
// This is a basic geography question that most people learn in school.
// France has had Paris as its capital since 987 AD.
// <|end|>User-facing response:
// <|start|>assistant<|channel|>final<|message|>
// The capital of France is Paris.
// <|end|>// Require specific channels
const systemContent = createSystemContent()
.withRequiredChannels(['analysis', 'final']) // Analysis + Final only
.build();
// Or allow all channels
const systemContent = createSystemContent()
.withRequiredChannels(['analysis', 'commentary', 'final'])
.build();Tools extend the model's capabilities with external functions:
import {
createSystemContent,
createBrowserToolNamespace,
createPythonToolNamespace
} from 'harmony-protocol-js';
const systemContent = createSystemContent()
.withBrowserTool() // Web browsing and search
.withPythonTool() // Python code execution
.withCommonFunctionTools() // Math, date/time, file operations
.build();import { createToolDescription, createToolNamespace } from 'harmony-protocol-js';
// Define a custom tool
const weatherTool = createToolDescription(
'get_weather',
'Get current weather for a location',
{
type: 'object',
properties: {
location: {
type: 'string',
description: 'City name or coordinates'
},
units: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
default: 'celsius'
}
},
required: ['location']
}
);
// Create namespace
const weatherNamespace = createToolNamespace(
'weather',
'Weather information tools',
[weatherTool]
);
// Add to system
const systemContent = createSystemContent()
.withTools(weatherNamespace)
.build();For real-time user interfaces, use the streaming parser:
import { StreamableParser, loadHarmonyEncoding, Role } from 'harmony-protocol-js';
const encoding = loadHarmonyEncoding();
const parser = new StreamableParser(encoding, Role.ASSISTANT);// From OpenAI streaming response
async function handleStreamingResponse(stream: ReadableStream) {
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process each chunk
parser.processText(new TextDecoder().decode(value));
// Get real-time content delta
const delta = parser.getLastContentDelta();
if (delta) {
// Update UI immediately
appendToChat(delta);
}
// Check for complete messages
const stats = parser.getStats();
if (stats.totalMessages > 0) {
console.log(`Received ${stats.totalMessages} complete messages`);
}
}
// Get final structured messages
const messages = parser.intoMessages();
finalizeChat(messages);
}// Robust streaming with error recovery
function processStreamChunk(chunk: string) {
try {
parser.processText(chunk);
const delta = parser.getLastContentDelta();
if (delta) {
updateUI(delta);
}
} catch (error) {
console.warn('Stream parsing error:', error);
// Parser continues working despite errors
}
}Harmony Protocol provides comprehensive rendering helpers for different output formats and use cases:
import {
renderToText,
renderToMarkdown,
renderToHTML,
renderFinalOnly,
renderSummary
} from 'harmony-protocol-js';
// Simple text output for logs or debugging
const text = renderToText(conversation, {
showChannels: true,
showRoles: true
});
// Markdown for documentation
const markdown = renderToMarkdown(conversation);
// HTML for web interfaces
const html = renderToHTML(conversation);
// Only final responses for clean chat UI
const chatOutput = renderFinalOnly(conversation);import { HarmonyRenderer } from 'harmony-protocol-js';
const renderer = new HarmonyRenderer({
showChannels: true,
showRoles: true,
channelLabels: {
analysis: '🤔 Analysis',
commentary: '💭 Notes',
final: '💬 Response'
},
roleLabels: {
user: 'You',
assistant: 'AI Assistant'
},
maxContentLength: 200, // Truncate long messages
includeTimestamps: true
});
const output = renderer.renderConversation(conversation);
console.log(output.text); // Plain text
console.log(output.markdown); // Markdown format
console.log(output.html); // HTML format
console.log(output.structured); // Structured data for analysis// Get content from specific channels
const analysisOnly = renderChannel(conversation, Channel.ANALYSIS);
const finalOnly = renderChannel(conversation, Channel.FINAL);
// Render by channel for detailed processing
const channelOutputs = renderer.renderByChannel(conversation);
Object.entries(channelOutputs).forEach(([channel, output]) => {
console.log(`${channel}: ${output.messageCount} messages`);
console.log(output.combinedContent);
});// JSON export for data persistence
const jsonExport = {
conversation: conversation.toJSON(),
rendered: renderer.renderConversation(conversation),
exportedAt: new Date().toISOString()
};
// CSV export for analytics
const structured = renderer.renderConversation(conversation).structured;
const csvData = structured.messages.map(msg =>
`"${msg.role}","${msg.channel || 'none'}","${msg.content}",${msg.originalLength}`
);import OpenAI from 'openai';
const openai = new OpenAI();
async function callHarmonyModel(conversation: Conversation) {
const encoding = loadHarmonyEncoding();
const tokens = encoding.renderConversationForCompletion(conversation, Role.ASSISTANT);
// Convert tokens to text for OpenAI
const harmonyText = encoding.decode(tokens);
const response = await openai.completions.create({
model: 'your-harmony-compatible-model',
prompt: harmonyText,
max_tokens: 2000,
stream: false
});
const responseText = response.choices[0].text || '';
const responseTokens = encoding.encode(responseText);
return encoding.parseMessagesFromCompletionTokens(responseTokens, Role.ASSISTANT);
}async function callOllamaHarmony(conversation: Conversation) {
const encoding = loadHarmonyEncoding();
const harmonyText = encoding.decode(
encoding.renderConversationForCompletion(conversation, Role.ASSISTANT)
);
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'your-harmony-model',
prompt: harmonyText,
stream: false
})
});
const result = await response.json();
const responseTokens = encoding.encode(result.response);
return encoding.parseMessagesFromCompletionTokens(responseTokens, Role.ASSISTANT);
}Here are practical patterns you'll commonly use:
import {
Conversation,
Message,
Role,
Channel,
createSystemContent,
HarmonyRenderer,
loadHarmonyEncoding
} from 'harmony-protocol-js';
class HarmonyChatApp {
private conversation: Conversation;
private encoding = loadHarmonyEncoding();
private renderer = new HarmonyRenderer({
showChannels: false, // Clean chat UI
showRoles: true,
roleLabels: {
user: 'You',
assistant: 'Assistant'
}
});
constructor() {
// Initialize with system message
const systemContent = createSystemContent()
.withIdentity('Helpful Chat Assistant')
.withRequiredChannels(['final']) // Only need final responses
.withReasoningEffort('medium')
.build();
this.conversation = Conversation.fromMessages([
Message.system(systemContent)
]);
}
addUserMessage(text: string) {
this.conversation.addMessage(Message.user(text));
}
async getResponse(): Promise<string> {
// Call your AI model here
const response = await this.callAIModel();
// Add response to conversation
this.conversation.addMessage(response);
// Return only the final response for UI
return response.getTextContent();
}
getChatHistory(): string {
// Filter to show only user and final assistant messages
const chatMessages = this.conversation.messages.filter(msg =>
msg.role === Role.USER ||
(msg.role === Role.ASSISTANT && msg.channel === Channel.FINAL)
);
return this.renderer.renderConversation(
new Conversation(chatMessages)
).text;
}
private async callAIModel(): Promise<Message> {
// Implementation depends on your AI provider
// This is a simplified example
const tokens = this.encoding.renderConversationForCompletion(
this.conversation,
Role.ASSISTANT
);
// Call your model and get response tokens
const responseTokens = await yourModelCall(tokens);
// Parse the response
const messages = this.encoding.parseMessagesFromCompletionTokens(
responseTokens,
Role.ASSISTANT
);
return messages.find(m => m.channel === Channel.FINAL) || messages[0];
}
}async function analyzeDocument(document: string): Promise<{
analysis: string;
summary: string;
recommendations: string;
}> {
const systemContent = createSystemContent()
.withIdentity('Document Analysis Expert')
.withRequiredChannels(['analysis', 'commentary', 'final'])
.withReasoningEffort('high')
.build();
const conversation = Conversation.fromMessages([
Message.system(systemContent),
Message.user(`Please analyze this document and provide recommendations:\n\n${document}`)
]);
// Get AI response
const responseMessages = await callAIModel(conversation);
// Extract different types of content
const analysis = responseMessages
.find(m => m.channel === Channel.ANALYSIS)
?.getTextContent() || '';
const commentary = responseMessages
.find(m => m.channel === Channel.COMMENTARY)
?.getTextContent() || '';
const summary = responseMessages
.find(m => m.channel === Channel.FINAL)
?.getTextContent() || '';
return {
analysis,
summary,
recommendations: commentary
};
}import { StreamingRenderer } from 'harmony-protocol-js';
class StreamingChatUI {
private parser = new StreamableParser(loadHarmonyEncoding(), Role.ASSISTANT);
private streamRenderer = new StreamingRenderer({
showChannels: false,
showRoles: false
});
async handleStreamingResponse(stream: ReadableStream, onUpdate: (text: string) => void) {
const reader = stream.getReader();
let accumulated = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process the chunk
const chunk = new TextDecoder().decode(value);
this.parser.processText(chunk);
// Get incremental content
const delta = this.parser.getLastContentDelta();
if (delta) {
accumulated += delta;
// Update UI immediately for responsive feel
onUpdate(accumulated);
}
}
// Finalize with complete messages
const messages = this.parser.intoMessages();
const finalMessage = messages.find(m => m.channel === Channel.FINAL);
if (finalMessage) {
onUpdate(finalMessage.getTextContent());
}
} catch (error) {
console.error('Streaming error:', error);
onUpdate('Sorry, there was an error processing the response.');
}
}
}// ✅ Good: Use builder pattern for system content
const systemContent = createSystemContent()
.withIdentity('Expert Assistant')
.withRequiredChannels(['analysis', 'final'])
.build();
// ❌ Avoid: Manual construction
const systemContent = {
identity: 'Expert Assistant',
required_channels: ['analysis', 'final']
};// ✅ Good: Validate before processing
const validation = conversation.validate();
if (!validation.isValid) {
throw new Error(`Invalid conversation: ${validation.errors.join(', ')}`);
}
// ✅ Good: Handle encoding errors
try {
const tokens = encoding.renderConversation(conversation);
} catch (error) {
if (error instanceof RenderError) {
console.error('Rendering failed:', error.message);
}
}// ✅ Good: Reuse encoding instances
const encoding = loadHarmonyEncoding(); // Create once
// Use encoding multiple times
// ✅ Good: Use streaming for real-time UIs
const parser = new StreamableParser(encoding, Role.ASSISTANT);
// ❌ Avoid: Creating new instances repeatedly
// const encoding = loadHarmonyEncoding(); // Don't do this in loops// ✅ Good: Filter channels based on use case
const finalOnly = conversation.filter(msg =>
msg.role !== Role.ASSISTANT || msg.channel === Channel.FINAL
);
// ✅ Good: Drop analysis when not needed
const tokens = encoding.renderConversation(conversation, {
drop_analysis_when_final_present: true
});// ❌ Wrong: Assuming tokens are strings
const tokens = encoding.encode(text);
console.log(tokens.join('')); // This won't work
// ✅ Correct: Tokens are numbers, decode to get text
const text = encoding.decode(tokens);
console.log(text);// ❌ Wrong: Forgetting to specify channels in system message
const system = Message.system('You are a helpful assistant.');
// ✅ Correct: Always specify required channels
const system = Message.system(
createSystemContent()
.withRequiredChannels(['final'])
.build()
);// ❌ Wrong: Not handling parser state
parser.processText(chunk);
// Continue without checking state
// ✅ Correct: Check parsing state
parser.processText(chunk);
const stats = parser.getStats();
if (stats.totalMessages > previousMessageCount) {
handleNewMessages(parser.getMessages());
}// ❌ Wrong: Creating tools without validation
const tool = { name: 'bad-name!', description: '' };
// ✅ Correct: Use validation
const validation = validateToolDescription(tool);
if (!validation.isValid) {
console.error('Tool validation failed:', validation.errors);
}Problem: Getting TypeError: tokens.join is not a function
// ❌ This will fail
const tokens = encoding.encode('Hello');
const text = tokens.join(''); // tokens is number[], not string[]Solution: Use the decode method
// ✅ Correct approach
const tokens = encoding.encode('Hello');
const text = encoding.decode(tokens); // Properly decode tokens to textProblem: Model doesn't output expected channels
// ❌ Generic system message
const system = Message.system('You are helpful.');Solution: Always specify required channels
// ✅ Explicit channel requirements
const system = Message.system(
createSystemContent()
.withRequiredChannels(['analysis', 'final'])
.build()
);Problem: No content appears during streaming
// ❌ Not checking for deltas
parser.processText(chunk);
// Missing delta extractionSolution: Always check for content deltas
// ✅ Proper streaming handling
parser.processText(chunk);
const delta = parser.getLastContentDelta();
if (delta) {
updateUI(delta);
}Problem: Cannot resolve module 'harmony-protocol-js'
- Check that the package is installed:
npm list harmony-protocol-js - Verify Node.js version ≥ 18.0.0
- For TypeScript projects, ensure
moduleResolution: "node"in tsconfig.json
Problem: Slow rendering with many messages
// ❌ Re-creating encoding repeatedly
function processMessage() {
const encoding = loadHarmonyEncoding(); // Expensive!
// ... processing
}Solution: Reuse encoding instances
// ✅ Create once, use many times
const globalEncoding = loadHarmonyEncoding();
function processMessage() {
// Use globalEncoding
}Problem: ValidationError: Tool name contains invalid characters
// ❌ Invalid tool name
const tool = createToolDescription('get-weather!', ...);Solution: Use valid tool names (alphanumeric + underscores only)
// ✅ Valid tool name
const tool = createToolDescription('get_weather', ...);// Add this to see detailed parsing information
process.env.HARMONY_DEBUG = 'true';
const encoding = loadHarmonyEncoding();
// Will now show detailed token information// Check conversation validity
const validation = conversation.validate();
console.log('Validation:', validation);
// Examine message structure
conversation.messages.forEach((msg, i) => {
console.log(`Message ${i}:`, {
role: msg.role,
channel: msg.channel,
contentLength: msg.getTextContent().length
});
});// Verify encoding/decoding works correctly
const originalText = 'Test message';
const tokens = encoding.encode(originalText);
const decodedText = encoding.decode(tokens);
console.log('Roundtrip successful:', originalText === decodedText);If you encounter issues not covered here:
- Check Examples: Look at the working examples in the
/examplesdirectory - Enable Debug Mode: Set
HARMONY_DEBUG=truefor detailed logging - Validate Inputs: Always validate conversations and tools before processing
- Read Error Messages: Harmony Protocol provides detailed error messages with suggestions
- Report Issues: Open a GitHub issue with minimal reproduction code
Now that you understand the basics, explore:
- Advanced Examples - See complex usage patterns
- API Reference - Complete method documentation
- Integration Guides - Provider-specific setup
- Migration Guide - Coming from Rust implementation
- 📖 Documentation: Browse the complete docs
- 💬 Issues: Report bugs on GitHub
- 🔍 Examples: Check the examples directory
- 📧 Contact: Reach out to the maintainers