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:
- Interface-First Design - Core abstractions are interfaces, enabling multiple implementations
- Pure Functions for Logic - Orchestration logic is pure functions with no side effects
- Runtime Agnosticism - Agent definitions work across all runtimes unchanged
- 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 --> AdaptersCore Module Structure
Types (/types)
Pure type definitions with minimal runtime code:
agent.ts- Agent configuration, LLM configtool.ts- Tool definition, sub-agent tools, finish toolstate.ts- Agent state, messages, thinking contentstream.ts- Stream chunks, all event typesruntime.ts- Step results, stop reasons, execution typesexecutor.ts- Executor interfacelogger.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, determineFinalStatusThese functions are used by all runtimes to ensure consistent behavior.
State (/state)
State tracking with Immer:
state/
└── immer-tracker.ts # ImmerStateTracker, RFC 6902 patchesLLM (/llm)
LLM adapter interface and utilities:
llm/
├── adapter.ts # LLMAdapter interface
├── mock-adapter.ts # MockLLMAdapter for testing
└── stop-reason.ts # Stop reason mapping utilitiesStore (/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 transportStream (/stream)
Stream utilities:
stream/
├── filters.ts # filterByType, excludeTypes, etc.
├── state-streamer.ts # CustomStateStreamer
├── state-projection.ts # StateProjection, StreamProjector
└── resumable-handler.ts # HTTP endpoint helpersData 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| FinalStream 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:
- Testability - Easy to unit test without mocking I/O
- Reusability - Same logic works across all runtimes
- Determinism - Required for Temporal workflow replay
- Clarity - Clear separation of logic and effects
Why Interfaces?
Store and adapter interfaces enable:
- Swappable Implementations - Dev vs production stores
- Testing - In-memory mocks for unit tests
- Platform Adaptation - Different stores for different platforms
- Future Extensibility - New implementations without core changes
Why Immer for State?
Immer provides:
- Immutable Updates - Draft mutations become immutable updates
- RFC 6902 Patches - Standard format for change tracking
- Type Safety - Full TypeScript support
- Familiarity - Common pattern in React ecosystem