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.