Skip to content

Architecture ​

Halo is organized as a monorepo with six packages. Here's how they fit together.

Package Dependency Graph ​

Halo (factory)
  └─► HaloAgent
        ├─► StablePrefix          (stable: system + tools + few-shots)
        ├─► MessageLog            (dynamic: conversation turns)
        ├─► ModelAdapter          (provider API calls)
        ├─► Sandbox?              (optional: filesystem + command execution)
        ├─► Skills?               (optional: progressive disclosure)
        ├─► ContextStrategy?      (optional: truncation / summarization)
        ├─► RepairStrategy?       (optional: JSON fix / storm suppression)
        └─► Tool Executors        (Map<name, execute>)

DeepSeekAdapter implements ModelAdapter
  └─► fetch → DeepSeek API /chat/completions

/packages/sandbox
  ├─ VirtualSandbox     (in-memory, cross-platform)
  └─ ContainerSandbox   (real fs + child_process, Node only)

/packages/mcp
  └─ createMCPServer()  (bridge external MCP tools)

/packages/stream (independent)
  ├─ toDataStream()
  └─ createHaloStream()

/packages/strategies (independent)
  ├─ TruncateStrategy    implements ContextStrategy
  └─ BasicRepair         implements RepairStrategy

Core Layers ​

StablePrefix ​

The stable prefix is the cacheable foundation of every request. It contains:

  • System prompt — the agent's personality and instructions
  • Tool definitions — JSON Schema for each tool the agent can call
  • Few-shot examples — optional example exchanges

Any mutation to the prefix triggers a fingerprint change, which causes the next API call to miss the cache — exactly once. After that, the new prefix is cached again.

MessageLog ​

The message log holds the dynamic conversation: user messages, assistant responses, and tool results. It's a simple ring buffer — when the storage limit is reached, the oldest messages are dropped.

clearLog() resets the log without touching the prefix, preserving the cache.

ModelAdapter ​

The adapter is the single interface between Halo and any model provider. It receives separated prefix and history, letting each adapter decide how to achieve caching:

  • DeepSeek: [...prefix, ...history] — prefix at position 0 is auto-cached
  • Anthropic: Adds cache_control markers to prefix messages
  • Gemini: Creates a CachedContent resource from the prefix
  • OpenAI: [...prefix, ...history] — automatic caching

Sandbox ​

The sandbox provides filesystem access and command execution for agents. Two implementations:

  • VirtualSandbox — Pure in-memory Map. Cross-platform (browser, Edge, tests). No exec.
  • ContainerSandbox — Real filesystem + child_process. Node.js only. Path-isolated with security checks.

Sandboxes never enter StablePrefix — they're cache-transparent. Tools generated by sandbox.tools() enter the prefix, but the sandbox instance itself does not.

Skills ​

Agent Skills follow agentskills.io progressive disclosure:

  • Metadata (name + description) enters StablePrefix — always visible, cached.
  • Body loaded at runtime via the auto-registered loadSkill tool — enters MessageLog, marked discardable.

This keeps the prefix lean while giving agents deep domain knowledge on demand.

HaloAgent ​

The agent is the user-facing API. It coordinates prefix, log, adapter, sandbox, skills, and strategies into three interaction modes:

  1. generateText() — Full tool loop: model → tool execution → result → model (repeat until done)
  2. send() / submitToolResult() — Manual control: returns tool calls for you to execute
  3. streamText() — Streaming with tool loop: yields text deltas and tool calls as they arrive
  4. generateObject() — Structured output: model returns typed JSON, tools suppressed

Request Flow ​

agent.generateText("What's the weather?")
  │
  ├─► _resolveHistory()
  │     ├─► prefix.toMessages()    → [system, tools, ...fewShots, skill metadata]
  │     ├─► log.toFullHistory()    → [user:"What's..."]
  │     └─► context.prepare(prefix, history)  → (optional truncation)
  │
  ├─► adapter.chat(prefix, history, tools)
  │     └─► POST /chat/completions
  │           body: { model, messages: [...prefix, ...history], tools }
  │
  ├─► Model returns: { content, toolCalls, usage }
  │
  ├─► repair.repair(toolCalls, content)  → (optional fix)
  ├─► log.append({ role: "assistant", content, tool_calls })
  ├─► stats updated (turns, tokens, cache hits)
  │
  └─► For each tool call:
        ├─► If execute exists → run it (inject sandbox if needed)
        ├─► If loadSkill → read SKILL.md from disk/sandbox
        └─► Append tool result → loop back to model call

Released under the MIT License.