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       # resolveStopDecision, 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["resolveStopDecision(result, stepCount, config)"]
        S2["terminal → custom stopWhen → max steps"]
    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

The decision in step 6 is core resolveStopDecision on JS, the Cloudflare DO (which runs the JS loop) and Cloudflare Workflows: a stopWhen stop completes the run, and a maxSteps cut-off of an agent without outputSchema fails it with framework_max_steps_exhausted. Temporal and DBOS still check maxSteps alone, so they ignore stopWhen and complete a cut-off run (DI-29, DI-25 in the conformance findings).

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, terminal, fence?) { ... } // persist terminal.status as endStatus
  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.