← Back to Home

Hexagonal architecture. Not accidental spaghetti.

13 packages, 19 port interfaces, 30+ adapters, strict dependency layering. Every component is testable in isolation, swappable without touching core, and type-safe from config to transport.

13

Packages

19

Port interfaces

30+

Adapters

0

Thrown exceptions

The idea

Ports define what. Adapters define how.

The core defines port interfaces - what the system needs (send a message, store memory, embed text). Adapters implement those interfaces for specific technologies (Discord, SQLite, OpenAI). The core never knows which adapter is running.

A single composition root (bootstrap.ts) wires everything together at startup. Returns a typed AppContainer with all services. No singletons, no global state, no service locator.

Port interfaces

19 ports, 30+ adapters.

ChannelPort 9 adapters

Methods

start, stop, sendMessage, editMessage, reactToMessage, deleteMessage, fetchMessages, sendAttachment

Adapters

Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE, Email

MemoryPort 1 adapter

Methods

store, retrieve, search, update, delete, clear

Adapters

SqliteMemoryAdapter (FTS5 + vector search)

SkillPort 2 adapters

Methods

validate, execute, manifest

Adapters

SkillRegistry (prompt-only), MCP client

EmbeddingPort 2 adapters

Methods

embed, embedBatch, dispose

Adapters

Local GGUF (nomic), OpenAI

MediaResolverPort 8 adapters

Methods

resolve

Adapters

7 per-platform resolvers, CompositeResolver

TranscriptionPort 3 adapters

Methods

transcribe

Adapters

OpenAI Whisper, Groq, Deepgram

TTSPort 3 adapters

Methods

synthesize

Adapters

OpenAI, ElevenLabs, Edge TTS

ImageAnalysisPort 1 adapter

Methods

analyze

Adapters

Vision analysis providers

VisionProvider 1 adapter

Methods

analyzeImage, analyzeVideo

Adapters

Gemini Vision (multi-capability)

FileExtractionPort 3 adapters

Methods

extract

Adapters

PDF (pdfjs-dist), CSV, document text

ImageGenerationPort 2 adapters

Methods

generate

Adapters

FAL, OpenAI

OutputGuardPort 1 adapter

Methods

scan

Adapters

LLM output secret-leak scanner (15 patterns)

SecretStorePort 1 adapter

Methods

get, set, delete, list

Adapters

SQLite-backed AES-256-GCM encrypted store

DeviceIdentityPort 1 adapter

Methods

getIdentity, sign, verify

Adapters

Ed25519 keypair (filesystem-persisted)

PluginPort 1 adapter

Methods

register, unregister, list

Adapters

Plugin registry with lifecycle management

DeliveryQueuePort 1 adapter

Methods

enqueue, dequeue, ack, nack

Adapters

SQLite-backed delivery queue

DeliveryMirrorPort 1 adapter

Methods

record, check, prune

Adapters

SQLite-backed deduplication mirror

CredentialMappingPort 1 adapter

Methods

get, set, delete, list

Adapters

Credential mapping CRUD

Package graph

13 packages in strict layers.

Package Layer Purpose Depends on
shared Foundation Result<T,E> type, utilities zero
core Foundation Ports, domain types, config, security, event bus, hooks shared
infra Foundation Pino structured logging core
memory Service SQLite + FTS5 + vector search core
scheduler Service Cron, heartbeat, task extraction core
skills Service Manifest, MCP, media processing, STT/TTS core
agent Service Executor, RAG, budget, circuit breaker, sessions core, memory, scheduler
gateway Adapter Hono HTTP, JSON-RPC, WebSocket, mTLS core
channels Adapter 9 platform adapters core, agent, infra
daemon Composition Orchestrator, observability, systemd all packages
cli Adapter Commander.js, JSON-RPC client core, agent
comis Composition Namespace re-exports all packages
web Adapter Lit + Vite + Tailwind SPA standalone

Key patterns

6 patterns that keep it clean.

Result<T, E>

Discriminated union for error handling

Eliminates exception-driven control flow. Every function explicitly declares success and failure types. The compiler enforces handling of both paths.

ok(value) | err(error) - narrows on result.ok discriminant

Factory Functions

createXxx() returns typed interface, not class

Decouples construction from usage. Dependencies injected as plain objects. Tests can substitute any compatible implementation without class hierarchies.

createDiscordAdapter(deps) -> ChannelPort

Typed Event Bus

Type-safe EventEmitter with EventMap interface

Decouples modules without direct imports. Compile-time checking of event names and payload shapes. Wrong payload -> type error.

eventBus.emit('context:compacted', { agentId, ... })

AsyncLocalStorage Context

Request-scoped identity via runWithContext()

Flows tenant, user, session, and trace IDs through entire async call chain without parameter drilling. Enables cross-cutting authorization and logging.

runWithContext(ctx, async () => { ... getContext().traceId })

Zod Schema -> Type

Define schema once, infer TypeScript type

Single source of truth for validation and types. Runtime validation + compile-time safety. No drift between what you validate and what you use.

const Schema = z.object({...}); type T = z.infer<typeof Schema>;

Composition Root

bootstrap.ts wires the entire application

One place creates all dependencies in the right order. Returns AppContainer with typed services. No hidden singletons, no global state, no service locator.

bootstrap(paths) -> Result<AppContainer, ConfigError>

Why it matters

Architecture pays dividends at scale.

Swap any adapter without touching core

Want to add Matrix support? Implement ChannelPort. Want to switch from SQLite to Postgres? Implement MemoryPort. The core never changes.

Test anything in isolation

Every port can be mocked with a plain object. No real Discord API needed to test message routing. No real database needed to test memory search. The chaos echo adapter injects faults for integration testing.

Reason about dependencies

The package dependency graph is a strict DAG. shared has zero deps. core depends only on shared. No circular imports. TypeScript project references enforce build order.

Extend without modifying

The plugin system lets you add hooks, tools, HTTP routes, and config schemas without touching core code. Priority ordering controls execution order. Plugin lifecycle is centralized.

Read the code. It speaks for itself.

Apache-2.0-licensed TypeScript monorepo. Every function returns a typed Result. Every port has a test suite. Every package has a clear responsibility.