Skip to content

Latest commit

 

History

History
663 lines (508 loc) · 15.6 KB

File metadata and controls

663 lines (508 loc) · 15.6 KB

Migration Guide: Rust to Node.js

This guide helps you migrate from the Rust Harmony Protocol implementation to the Node.js version.

Overview

The Node.js implementation provides 100% API compatibility with the Rust version while offering TypeScript-native patterns and Node.js ecosystem integration.

Quick Comparison

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

Installation Migration

Rust (Before)

# Cargo.toml
[dependencies]
harmony-protocol-js = { git = "https://github.com/terraprompt/harmony-protocol-js" }

Node.js (After)

# 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"
  }
}

Import/Export Migration

Rust (Before)

use harmony_protocol::{
    load_harmony_encoding, HarmonyEncodingName,
    chat::{Role, Message, Conversation, SystemContent}
};

Node.js (After)

import {
  loadHarmonyEncoding,
  HarmonyEncodingName,
  Role,
  Message,
  Conversation,
  createSystemContent
} from 'harmony-protocol-js';

// Or CommonJS
const {
  loadHarmonyEncoding,
  Role,
  Message,
  Conversation
} = require('harmony-protocol-js');

Core API Migration

Loading Encodings

Rust (Before)

use harmony_protocol::{load_harmony_encoding, HarmonyEncodingName};

fn main() -> anyhow::Result<()> {
    let enc = load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)?;
    Ok(())
}

Node.js (After)

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

Message Creation

Rust (Before)

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);

Node.js (After)

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 (withRequiredChannels vs with_required_channels)
  • Builder pattern requires explicit .build() call
  • Convenience static methods available (Message.system(), Message.user(), etc.)

Conversation Handling

Rust (Before)

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!"),
]);

Node.js (After)

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

Rendering and Parsing

Rust (Before)

// 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))?;

Node.js (After)

// 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 undefined instead of Option<T>

Error Handling Migration

Rust (Before)

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),
}

Node.js (After)

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

Streaming Parser Migration

Rust (Before)

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();

Node.js (After)

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 new keyword
  • Method names in camelCase (getLastContentDelta, intoMessages)
  • No Result wrapping for simple operations
  • Optional parameters can be undefined

Tool Integration Migration

Rust (Before)

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);

Node.js (After)

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
  • undefined instead of None for optional parameters

Type System Migration

Rust (Before)

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(),
    }
}

Node.js (After)

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:

  • interface instead of struct
  • Optional properties use ? syntax
  • Enums are string-based in TypeScript
  • No explicit lifetime management

Async/Await Patterns

Rust (Before)

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(())
}

Node.js (After)

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

Testing Migration

Rust (Before)

#[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);
    }
}

Node.js (After)

// 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');
  });
});

Performance Considerations

Memory Usage

Rust: Manual memory management with ownership system

let encoding = load_harmony_encoding(HarmonyEncodingName::HarmonyGptOss)?; // Owned
let borrowed_encoding = &encoding; // Borrowed reference

Node.js: Garbage collected

const encoding = loadHarmonyEncoding(); // GC managed
// No explicit memory management needed

Token Processing

Rust: Zero-copy string processing where possible Node.js: V8 optimized string handling with some copying overhead

Concurrency

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 });

Best Practices for Migration

1. Gradual Migration

// Start with core functionality
const encoding = loadHarmonyEncoding();
const message = Message.user('Hello');

// Add complexity gradually
const systemContent = createSystemContent()
  .withRequiredChannels(['final'])
  .build();

2. Error Handling Strategy

// 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 };
  }
}

3. Type Safety

// Use TypeScript strict mode
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

4. Validation

// 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);
}

Common Migration Issues

1. Import/Export Confusion

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');  // CommonJS

2. Optional Parameters

Problem: 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); // undefined

3. Error Handling Patterns

Problem: 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);
  }
}

Migration Checklist

  • Update package dependencies (Cargo.toml → package.json)
  • Convert imports (useimport)
  • 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

Need Help?

If you encounter issues during migration:

  1. Check Examples: Review the examples for patterns
  2. API Reference: Consult the API documentation
  3. GitHub Issues: Search existing issues or create a new one
  4. 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.