This guide helps you migrate from the Rust Harmony Protocol implementation to the Node.js version.
The Node.js implementation provides 100% API compatibility with the Rust version while offering TypeScript-native patterns and Node.js ecosystem integration.
| Aspect | Rust | Node.js |
|---|---|---|
| Language | Rust | TypeScript/JavaScript |
| Package Manager | Cargo | npm/yarn/pnpm |
| Type System | Native Rust types | TypeScript definitions |
| Error Handling | Result<T, E> |
Exceptions + validation |
| Memory Management | Manual with ownership | Garbage collected |
| Performance | Highest | High (V8 optimized) |
| Ecosystem | Cargo crates | npm packages |
# Cargo.toml
[dependencies]
harmony-protocol-js = { git = "https://github.com/terraprompt/harmony-protocol-js" }# Package installation
npm install harmony-protocol-js
# or with specific version
npm install harmony-protocol-js@^0.1.0// package.json
{
"dependencies": {
"harmony-protocol-js": "^0.1.0"
}
}use harmony_protocol::{
load_harmony_encoding, HarmonyEncodingName,
chat::{Role, Message, Conversation, SystemContent}
};import {
loadHarmonyEncoding,
HarmonyEncodingName,
Role,
Message,
Conversation,
createSystemContent
} from 'harmony-protocol-js';
// Or CommonJS
const {
loadHarmonyEncoding,
Role,
Message,
Conversation
} = require('harmony-protocol-js');use harmony_protocol::{load_harmony_encoding, HarmonyEncodingName};
fn main() -> anyhow::Result<()> {
let enc = load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)?;
Ok(())
}import { loadHarmonyEncoding, HarmonyEncodingName } from 'harmony-protocol-js';
// Using string encoding name
const encoding = loadHarmonyEncoding('o200k_base');
// Or using enum (if defined)
// const encoding = loadHarmonyEncoding(HarmonyEncodingName.HARMONY_GPT_OSS);Key Differences:
- No
Result<T, E>return type - throws exceptions instead - Default encoding parameter supported
- Built-in error handling with try/catch
use harmony_protocol::chat::{Role, Message, SystemContent};
let system_content = SystemContent::new()
.with_required_channels(&["analysis", "commentary", "final"]);
let message = Message::from_role_and_content(Role::System, system_content);import { Role, Message, createSystemContent } from 'harmony-protocol-js';
const systemContent = createSystemContent()
.withRequiredChannels(['analysis', 'commentary', 'final'])
.build();
const message = Message.fromRoleAndContent(Role.SYSTEM, systemContent);
// Or using convenience method
const message = Message.system(systemContent);Key Differences:
- Method names use camelCase (
withRequiredChannelsvswith_required_channels) - Builder pattern requires explicit
.build()call - Convenience static methods available (
Message.system(),Message.user(), etc.)
use harmony_protocol::chat::Conversation;
let convo = Conversation::from_messages([
Message::from_role_and_content(Role::System, system_content),
Message::from_role_and_content(Role::User, "Hello, world!"),
]);import { Conversation, Message, Role } from 'harmony-protocol-js';
const conversation = Conversation.fromMessages([
Message.fromRoleAndContent(Role.SYSTEM, systemContent),
Message.fromRoleAndContent(Role.USER, 'Hello, world!')
]);
// Or using convenience methods
const conversation = Conversation.fromMessages([
Message.system(systemContent),
Message.user('Hello, world!')
]);Key Differences:
- Array syntax uses
[]instead of slice notation - Fluent builder methods available for convenience
// Render for completion
let input_tokens = enc.render_conversation_for_completion(&convo, Role::Assistant, None)?;
// Parse response
let messages = enc.parse_messages_from_completion_tokens(response_tokens, Some(Role::Assistant))?;// Render for completion
const inputTokens = encoding.renderConversationForCompletion(conversation, Role.ASSISTANT);
// Parse response
const messages = encoding.parseMessagesFromCompletionTokens(responseTokens, Role.ASSISTANT);Key Differences:
- Method names in camelCase
- No
Result<T, E>wrapper - throws on error - Optional parameters can be
undefinedinstead ofOption<T>
use anyhow::Result;
fn process_conversation() -> Result<Vec<Message>> {
let enc = load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)?;
let tokens = enc.render_conversation(&convo, None)?;
let messages = enc.parse_messages_from_completion_tokens(tokens, Some(Role::Assistant))?;
Ok(messages)
}
// Usage
match process_conversation() {
Ok(messages) => println!("Success: {} messages", messages.len()),
Err(e) => eprintln!("Error: {}", e),
}import { HarmonyError, ParseError, RenderError } from 'harmony-protocol-js';
function processConversation(): Message[] {
try {
const encoding = loadHarmonyEncoding();
const tokens = encoding.renderConversation(conversation);
const messages = encoding.parseMessagesFromCompletionTokens(tokens, Role.ASSISTANT);
return messages;
} catch (error) {
if (error instanceof ParseError) {
console.error('Parse error:', error.message);
} else if (error instanceof RenderError) {
console.error('Render error:', error.message);
} else {
console.error('Unexpected error:', error);
}
throw error;
}
}
// Usage
try {
const messages = processConversation();
console.log(`Success: ${messages.length} messages`);
} catch (error) {
console.error('Failed to process conversation');
}Key Differences:
- Use try/catch instead of
Result<T, E> - Specific error types available for granular handling
- TypeScript provides compile-time error checking
use harmony_protocol::{StreamableParser, Role};
let mut parser = StreamableParser::new(encoding.clone(), Some(Role::Assistant))?;
for token in response_tokens {
parser.process(token)?;
if let Ok(Some(delta)) = parser.last_content_delta() {
print!("{}", delta);
}
}
let messages = parser.into_messages();import { StreamableParser, Role } from 'harmony-protocol-js';
const parser = new StreamableParser(encoding, Role.ASSISTANT);
for (const token of responseTokens) {
parser.process(token);
const delta = parser.getLastContentDelta();
if (delta) {
process.stdout.write(delta);
}
}
const messages = parser.intoMessages();Key Differences:
- Constructor uses
newkeyword - Method names in camelCase (
getLastContentDelta,intoMessages) - No
Resultwrapping for simple operations - Optional parameters can be
undefined
use harmony_protocol::chat::{ToolDescription, ToolNamespaceConfig, SystemContent};
let tools = vec![
ToolDescription::new(
"calculate",
"Performs mathematical calculations",
Some(serde_json::json!({
"type": "object",
"properties": {
"expression": {"type": "string"}
},
"required": ["expression"]
}))
)
];
let function_namespace = ToolNamespaceConfig::new("functions", None, tools);
let system_content = SystemContent::new()
.with_browser_tool()
.with_tools(function_namespace);import {
createToolDescription,
createToolNamespace,
createSystemContent
} from 'harmony-protocol-js';
const tools = [
createToolDescription(
'calculate',
'Performs mathematical calculations',
{
type: 'object',
properties: {
expression: { type: 'string' }
},
required: ['expression']
}
)
];
const functionNamespace = createToolNamespace('functions', undefined, tools);
const systemContent = createSystemContent()
.withBrowserTool()
.withTools(functionNamespace)
.build();Key Differences:
- Factory functions instead of constructors (
createToolDescription) - JSON objects instead of
serde_json::json!macro - Explicit
.build()call required for system content undefinedinstead ofNonefor optional parameters
use harmony_protocol::chat::{Role, Channel, Content};
#[derive(Debug)]
struct MyMessage {
role: Role,
content: Content,
channel: Option<Channel>,
}
fn process_message(msg: &MyMessage) -> String {
match msg.channel {
Some(Channel::Final) => "Final response".to_string(),
Some(Channel::Analysis) => "Analysis content".to_string(),
None => "No channel".to_string(),
}
}import { Role, Channel, Content } from 'harmony-protocol-js';
interface MyMessage {
role: Role;
content: Content;
channel?: Channel;
}
function processMessage(msg: MyMessage): string {
switch (msg.channel) {
case Channel.FINAL:
return 'Final response';
case Channel.ANALYSIS:
return 'Analysis content';
default:
return 'No channel';
}
}Key Differences:
interfaceinstead ofstruct- Optional properties use
?syntax - Enums are string-based in TypeScript
- No explicit lifetime management
use tokio;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let response = call_openai_api().await?;
let tokens = encoding.encode(&response);
let messages = encoding.parse_messages_from_completion_tokens(tokens, Some(Role::Assistant))?;
Ok(())
}async function main(): Promise<void> {
try {
const response = await callOpenAIAPI();
const tokens = encoding.encode(response);
const messages = encoding.parseMessagesFromCompletionTokens(tokens, Role.ASSISTANT);
} catch (error) {
console.error('Error:', error);
throw error;
}
}
main().catch(console.error);Key Differences:
- Native async/await support in JavaScript/TypeScript
- Promise-based instead of Future-based
- No need for tokio runtime
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation() {
let msg = Message::from_role_and_content(Role::User, "test");
assert_eq!(msg.role, Role::User);
}
}// Using Jest
describe('Message Creation', () => {
test('creates user message correctly', () => {
const message = Message.fromRoleAndContent(Role.USER, 'test');
expect(message.role).toBe(Role.USER);
expect(message.getTextContent()).toBe('test');
});
});Rust: Manual memory management with ownership system
let encoding = load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)?; // Owned
let borrowed_encoding = &encoding; // Borrowed referenceNode.js: Garbage collected
const encoding = loadHarmonyEncoding(); // GC managed
// No explicit memory management neededRust: Zero-copy string processing where possible Node.js: V8 optimized string handling with some copying overhead
Rust: Fearless concurrency with ownership system
use tokio::task;
let handle = task::spawn(async move {
process_conversation(encoding).await
});Node.js: Event loop based with Worker threads for CPU-intensive tasks
import { Worker } from 'worker_threads';
const worker = new Worker('./harmony-worker.js');
worker.postMessage({ conversation, tokens });// Start with core functionality
const encoding = loadHarmonyEncoding();
const message = Message.user('Hello');
// Add complexity gradually
const systemContent = createSystemContent()
.withRequiredChannels(['final'])
.build();// Create wrapper for Rust-like error handling
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function safeEncode(text: string): Result<number[], Error> {
try {
const tokens = encoding.encode(text);
return { ok: true, value: tokens };
} catch (error) {
return { ok: false, error: error as Error };
}
}// Use TypeScript strict mode
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}// Validate conversations before processing
function safeProcessConversation(conversation: Conversation): Message[] {
const validation = conversation.validate();
if (!validation.isValid) {
throw new ValidationError(`Invalid conversation: ${validation.errors.join(', ')}`);
}
return encoding.parseMessagesFromCompletionTokens(tokens, Role.ASSISTANT);
}Problem: Mixing CommonJS and ES modules Solution: Be consistent with module system
// Choose one consistently
import { Message } from 'harmony-protocol-js'; // ES modules
// OR
const { Message } = require('harmony-protocol-js'); // CommonJSProblem: Rust Option<T> vs TypeScript T | undefined
Solution: Use TypeScript optional parameters correctly
// Rust: Some(Role::Assistant) or None
// TypeScript: Role.ASSISTANT or undefined
const messages = encoding.parseMessagesFromCompletionTokens(tokens, Role.ASSISTANT);
const messages2 = encoding.parseMessagesFromCompletionTokens(tokens); // undefinedProblem: Missing try/catch blocks Solution: Wrap operations that may fail
try {
const encoding = loadHarmonyEncoding();
const tokens = encoding.renderConversation(conversation);
} catch (error) {
if (error instanceof HarmonyError) {
// Handle Harmony-specific errors
console.error('Harmony error:', error.message);
} else {
// Handle other errors
console.error('Unexpected error:', error);
}
}- Update package dependencies (Cargo.toml → package.json)
- Convert imports (
use→import) - Update method names (snake_case → camelCase)
- Replace
Result<T, E>with try/catch - Convert
Option<T>to optional parameters - Update enum usage (Rust enums → string enums)
- Add
.build()calls to builders - Update test framework (rust test → Jest/Mocha)
- Review async patterns (tokio → native async/await)
- Update error handling strategy
If you encounter issues during migration:
- Check Examples: Review the examples for patterns
- API Reference: Consult the API documentation
- GitHub Issues: Search existing issues or create a new one
- Community: Join discussions in the GitHub repository
The Node.js implementation is designed to be a drop-in replacement for most Rust use cases while providing a more JavaScript/TypeScript-native experience.