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 RepairStrategyCore 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_controlmarkers to prefix messages - Gemini: Creates a
CachedContentresource 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). Noexec. - 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
loadSkilltool — entersMessageLog, markeddiscardable.
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:
generateText()— Full tool loop: model → tool execution → result → model (repeat until done)send()/submitToolResult()— Manual control: returns tool calls for you to executestreamText()— Streaming with tool loop: yields text deltas and tool calls as they arrivegenerateObject()— 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