Wrappers: Capabilities

Wrappers are how Happen maintains its core philosophy of radical simplicity while enabling powerful extensions. The core framework stays focused on just Nodes and Events, while wrappers provide specialized functionality built on top of these primitives.

Why Wrappers?

Happen's core is intentionally minimal - just two primitives (Nodes and Events) with causality tracking. This design choice enables:

  • Minimal dependencies in the core framework

  • Universal compatibility across environments

  • Predictable behavior regardless of extensions

  • Optional complexity - only pay for what you use

Wrappers let you add sophisticated functionality while preserving this simplicity.

How Wrappers Work

Wrappers are just specialized Node factories that create nodes with specific behaviors. They don't change how Happen works - they build on existing patterns.

Basic Wrapper Pattern

// A wrapper is just a function that creates configured nodes
function createDatabaseNode(config) {
  const node = createNode(config.name)
  
  // Add specialized event handlers
  node.on('db.query', async (event) => {
    const result = await database.query(event.payload.sql)
    node.broadcast({
      type: 'db.result',
      payload: result,
      correlationId: event.correlationId
    })
  })
  
  node.on('db.insert', async (event) => {
    // Handle inserts...
  })
  
  return node
}

// Usage - still just regular Happen patterns
const userDB = createDatabaseNode({ name: 'user-db' })

system.broadcast({
  type: 'db.query',
  payload: { sql: 'SELECT * FROM users WHERE active = true' }
})

The wrapper provides convenience, but underneath it's just Nodes handling Events.

The happen-agents Wrapper

The agents wrapper we discussed is a perfect example:

import { createAgentNode, createContextNode } from 'happen-agents'

// Creates a node that handles the agent event patterns
const researcher = createAgentNode({
  llm: openai,
  name: 'research-agent'
})

// Creates a node that composes context dynamically  
const context = createContextNode({
  compose: {
    expertise: () => system.state.get('domain-knowledge'),
    constraints: () => system.state.get('compliance-rules')
  }
})

// Still just regular event flow
system.broadcast({
  type: 'agent.research',
  payload: {
    goal: "analyze competitor pricing",
    contexts: [context.id]
  }
})

The wrapper handles the LLM integration complexity, but the interface remains pure Happen.

Building Your Own Wrapper

Creating a wrapper is straightforward. Here's a template:

export function createMyWrapper(config) {
  const node = createNode(config.name)
  
  // Initialize any external dependencies
  const externalService = new SomeAPI(config.apiKey)
  
  // Add specialized event handlers
  node.on('my-wrapper.action', async (event) => {
    try {
      // Do something with external service
      const result = await externalService.process(event.payload)
      
      // Emit results as events
      node.broadcast({
        type: 'my-wrapper.complete',
        payload: result,
        correlationId: event.correlationId
      })
      
    } catch (error) {
      // Error handling through events too
      node.broadcast({
        type: 'my-wrapper.error', 
        payload: { error: error.message },
        correlationId: event.correlationId
      })
    }
  })
  
  // Add helper methods if needed
  node.customMethod = () => {
    // But still emit events for actual work
    node.broadcast({ type: 'my-wrapper.custom' })
  }
  
  return node
}

Wrapper Best Practices

Keep the interface event-based: Don't expose complex APIs. Let everything flow through events.

Maintain causality: Always use correlationId to preserve causal chains.

Handle errors as events: Don't throw exceptions - emit error events.

Stay stateless when possible: Store state in the node, not in closure variables.

Document event types: Make it clear what events your wrapper handles and emits.

Wrapper Ecosystem

The wrapper pattern enables a rich ecosystem:

Data & Storage

  • happen-postgres - PostgreSQL integration

  • happen-redis - Redis pub/sub and caching

  • happen-s3 - AWS S3 file operations

  • happen-elasticsearch - Full-text search

AI & ML

  • happen-agents - LLM-based intelligent agents

  • happen-embeddings - Vector similarity search

  • happen-vision - Image analysis and generation

  • happen-speech - Speech-to-text and text-to-speech

Communication

  • happen-slack - Slack bot integration

  • happen-email - SMTP email sending

  • happen-websocket - Real-time client connections

  • happen-webhook - HTTP webhook handling

Monitoring

  • happen-metrics - Prometheus metrics collection

  • happen-logging - Structured logging

  • happen-tracing - Distributed tracing

  • happen-alerts - Alert management

Multiple Wrappers Together

Wrappers compose naturally:

import { createAgentNode } from 'happen-agents'
import { createDatabaseNode } from 'happen-postgres'  
import { createSlackNode } from 'happen-slack'

const analyst = createAgentNode({ llm: openai })
const database = createDatabaseNode({ connectionString: "..." })
const slack = createSlackNode({ botToken: "..." })

// Agent can request data
analyst.on('agent.complete', (event) => {
  database.broadcast({
    type: 'db.insert',
    payload: { table: 'reports', data: event.payload }
  })
})

// Database can trigger notifications
database.on('db.complete', (event) => {
  slack.broadcast({
    type: 'slack.message',
    payload: { 
      channel: '#reports',
      text: 'New analysis complete!'
    }
  })
})

Each wrapper handles its specialty, but they coordinate through Happen's event system.

Wrapper Development Guidelines

Installation

npm install happen happen-your-wrapper

Peer Dependencies

List happen as a peer dependency, not a regular dependency:

{
  "peerDependencies": {
    "happen": "^1.0.0"
  }
}

TypeScript Support

Provide types for your event payloads:

interface DatabaseQueryEvent {
  type: 'db.query'
  payload: {
    sql: string
    params?: any[]
  }
}

interface DatabaseResultEvent {
  type: 'db.result'
  payload: {
    rows: any[]
    rowCount: number
  }
}

Testing

Test your wrapper with regular Happen patterns:

test('handles query events', async () => {
  const db = createDatabaseNode({ name: 'test-db' })
  
  const resultPromise = new Promise(resolve => {
    db.on('db.result', resolve)
  })
  
  db.broadcast({
    type: 'db.query',
    payload: { sql: 'SELECT 1' }
  })
  
  const result = await resultPromise
  expect(result.payload.rows).toHaveLength(1)
})

The Wrapper Philosophy

Wrappers embody Happen's core philosophy:

Simple interface, powerful behavior: Complex functionality accessible through simple event patterns.

Composable by design: Any wrapper works with any other wrapper through events.

Causality preserved: All wrapper interactions maintain full causal tracking.

Environment agnostic: Same wrapper works in browser, server, worker, edge.

Optional complexity: Use only the wrappers you need, when you need them.

Wrappers let Happen grow without compromising its essential simplicity. They're how we achieve the goal of being infinitely extensible while remaining fundamentally understandable.

Last updated