Skip to content

Architecture Overview

This document explains the internal architecture of Helix Agents for contributors and advanced users who want to understand how the framework works.

Design Philosophy

Helix Agents follows these core principles:

  1. Interface-First Design - Core abstractions are interfaces, enabling multiple implementations
  2. Pure Functions for Logic - Orchestration logic is pure functions with no side effects
  3. Runtime Agnosticism - Agent definitions work across all runtimes unchanged
  4. Composability - Every component can be swapped or extended

Package Architecture

mermaid
graph TB
    subgraph Apps ["Applications"]
        App[" "]
    end

    subgraph SDK ["@helix-agents/sdk<br/>(Umbrella - core + memory + js)"]
        SDK_Content[" "]
    end

    Apps --> SDK

    subgraph Runtimes [" "]
        direction LR
        JS["<b>runtime-js</b><br/>In-process<br/>execution"]
        Temporal["<b>runtime-temporal</b><br/>Workflows +<br/>Activities"]
        CF["<b>runtime-cloudflare</b><br/>Workers +<br/>Workflows"]
    end

    SDK --> Runtimes

    subgraph Core ["@helix-agents/core"]
        direction TB
        subgraph CoreModules [" "]
            direction LR
            Types["<b>Types</b><br/>Agent, Tool<br/>State, Stream"]
            Orch["<b>Orchestration</b><br/>init, step<br/>messages, stop"]
            State["<b>State</b><br/>Immer, Tracker<br/>Patches"]
            Stream["<b>Stream</b><br/>Filters<br/>Projections<br/>Handler"]
        end
        subgraph CoreInterfaces [" "]
            direction LR
            Interfaces["<b>Interfaces</b><br/>StateStore<br/>StreamMgr<br/>LLMAdapter"]
            LLM["<b>LLM</b><br/>Adapter, Mock<br/>StopReason"]
        end
    end

    Runtimes --> Core

    subgraph Stores [" "]
        direction LR
        Memory["<b>store-memory</b><br/>InMemory<br/>State/Stream"]
        Redis["<b>store-redis</b><br/>Redis<br/>State/Stream"]
        CFStore["<b>store-cloudflare</b><br/>D1 + DO<br/>State/Stream"]
    end

    Core --> Stores

    subgraph Adapters [" "]
        direction LR
        Vercel["<b>llm-vercel</b><br/>Vercel AI<br/>SDK Adapter"]
        AISdk["<b>ai-sdk</b><br/>Frontend<br/>Integration"]
    end

    Core --> Adapters

Core Module Structure

Types (/types)

Pure type definitions with minimal runtime code:

  • agent.ts - Agent configuration, LLM config
  • tool.ts - Tool definition, sub-agent tools, finish tool
  • state.ts - Agent state, messages, thinking content
  • stream.ts - Stream chunks, all event types
  • runtime.ts - Step results, stop reasons, execution types
  • executor.ts - Executor interface
  • logger.ts - Logging interface

Orchestration (/orchestration)

Pure functions that implement agent logic:

orchestration/
├── state-operations.ts   # initializeAgentState, buildEffectiveTools
├── message-builder.ts    # createAssistantMessage, createToolResultMessage
├── step-processor.ts     # planStepProcessing
└── stop-checker.ts       # shouldStopExecution, determineFinalStatus

These functions are used by all runtimes to ensure consistent behavior.

State (/state)

State tracking with Immer:

state/
└── immer-tracker.ts      # ImmerStateTracker, RFC 6902 patches

LLM (/llm)

LLM adapter interface and utilities:

llm/
├── adapter.ts            # LLMAdapter interface
├── mock-adapter.ts       # MockLLMAdapter for testing
└── stop-reason.ts        # Stop reason mapping utilities

Store (/store)

Store interfaces (no implementations):

store/
├── state-store.ts        # StateStore interface
├── stream-manager.ts     # StreamManager, Writer, Reader interfaces
└── stream-events.ts      # Wire format for stream transport

Stream (/stream)

Stream utilities:

stream/
├── filters.ts            # filterByType, excludeTypes, etc.
├── state-streamer.ts     # CustomStateStreamer
├── state-projection.ts   # StateProjection, StreamProjector
└── resumable-handler.ts  # HTTP endpoint helpers

Data Flow

Agent Execution Flow

mermaid
flowchart TB
    Input["User Input"]

    subgraph Init ["1. Initialize"]
        I1["initializeAgentState(agent, input, runId, streamId)"]
        I2["Parse input · Apply schema defaults<br/>Create AgentState · Add user message"]
    end

    subgraph Build ["2. Build Messages"]
        B1["buildMessagesForLLM(messages, systemPrompt, state)"]
        B2["Resolve dynamic system prompt · Prepend system message"]
    end

    subgraph LLM ["3. Call LLM"]
        L1["llmAdapter.generateStep(input, callbacks)"]
        L2["Convert messages · Call API with streaming<br/>Emit events · Return StepResult"]
    end

    subgraph Process ["4. Process Step"]
        P1["planStepProcessing(stepResult, options)"]
        P2["Check __finish__ · Plan assistant message<br/>Identify tool calls · Return StepProcessingPlan"]
    end

    subgraph Tools ["5. Execute Tools (if any)"]
        T1["For each pendingToolCall:"]
        T2["Execute tool · Collect patches<br/>Create result message · Emit tool_end"]
    end

    subgraph Stop ["6. Check Stop Condition"]
        S1["shouldStopExecution(result, stepCount, config)"]
        S2["Check terminal types · Check max steps<br/>Check custom stopWhen"]
    end

    subgraph Final ["7. Finalize"]
        F1["Apply final status · End stream · Return result"]
    end

    Input --> Init
    Init --> Build
    Build --> LLM
    LLM --> Process
    Process --> Tools
    Tools --> Stop
    Stop -->|Continue| Build
    Stop -->|Stop| Final

Stream Event Flow

mermaid
graph TB
    LLM["LLM Response"]

    LLM --> TextChunk["Text chunk"]
    TextChunk --> TextDelta["text_delta { content }"]

    LLM --> ThinkingChunk["Thinking chunk"]
    ThinkingChunk --> Thinking["thinking { content, isComplete }"]

    LLM --> ToolStart["Tool call start"]
    ToolStart --> ToolStartEvent["tool_start { toolCallId, toolName, arguments }"]
    ToolStartEvent --> ToolExec["Tool Execution"]
    ToolExec --> StateMutation["State mutation"]
    StateMutation --> StatePatch["state_patch { patches }"]
    ToolExec --> CustomEvent["Custom event"]
    CustomEvent --> Custom["custom { eventName, data }"]
    ToolExec --> ToolEnd["tool_end { toolCallId, result }"]

    LLM --> SubAgentCall["Sub-agent call"]
    SubAgentCall --> SubStart["subagent_start { subAgentId, agentType }"]
    SubStart --> SubExec["Sub-Agent Execution<br/>(recursive)"]
    SubExec --> SubEnd["subagent_end { subAgentId, result }"]

    LLM --> ErrorEvent["Error"]
    ErrorEvent --> ErrorChunk["error { error, recoverable }"]

    LLM --> OutputEvent["Structured output"]
    OutputEvent --> Output["output { output }"]

Extension Points

Custom Runtime

Implement the execution loop using core orchestration functions:

typescript
// Use pure functions for logic
import {
  initializeAgentState,
  buildMessagesForLLM,
  buildEffectiveTools,
  planStepProcessing,
  shouldStopExecution,
} from '@helix-agents/core';

// Implement AgentExecutor interface
class MyCustomRuntime implements AgentExecutor {
  async execute(agent, input) {
    // Use orchestration functions
    const state = initializeAgentState({ agent, input, runId, streamId });
    // ... execution loop
  }
}

Custom Store

Implement the store interfaces:

typescript
import type { StateStore, StreamManager } from '@helix-agents/core';

class MyStateStore implements SessionStateStore {
  async saveState(sessionId, state) { ... }
  async loadState(sessionId) { ... }
  async exists(sessionId) { ... }
  async updateStatus(sessionId, status) { ... }
  async getMessages(sessionId, options) { ... }
}

class MyStreamManager implements StreamManager {
  async createWriter(streamId, agentId, agentType) { ... }
  async createReader(streamId) { ... }
  async endStream(streamId, output?) { ... }
  async failStream(streamId, error) { ... }
  async getInfo(streamId) { ... }
}

Custom LLM Adapter

Implement the LLM adapter interface:

typescript
import type { LLMAdapter, LLMGenerateInput, StepResult } from '@helix-agents/core';

class MyLLMAdapter implements LLMAdapter {
  async generateStep(input: LLMGenerateInput): Promise<StepResult> {
    // Call your LLM provider
    // Stream events via callbacks in input
    // Return StepResult
  }
}

Key Design Decisions

Why Pure Functions?

Orchestration uses pure functions because:

  1. Testability - Easy to unit test without mocking I/O
  2. Reusability - Same logic works across all runtimes
  3. Determinism - Required for Temporal workflow replay
  4. Clarity - Clear separation of logic and effects

Why Interfaces?

Store and adapter interfaces enable:

  1. Swappable Implementations - Dev vs production stores
  2. Testing - In-memory mocks for unit tests
  3. Platform Adaptation - Different stores for different platforms
  4. Future Extensibility - New implementations without core changes

Why Immer for State?

Immer provides:

  1. Immutable Updates - Draft mutations become immutable updates
  2. RFC 6902 Patches - Standard format for change tracking
  3. Type Safety - Full TypeScript support
  4. Familiarity - Common pattern in React ecosystem

See Also

Released under the MIT License.