Framework source · pseudocode
COGNI.BUILD
A layered framework for putting a tool-using assistant inside a product that already has users, content and permissions. Read this page first: it explains the layer stack, the naming rules, and the five kinds of data class — after which every filename in the tree should predict its own contents.
The layer stack
Ports and adapters over a strict downward dependency rule. If you have worked in a hexagonal, clean or onion architecture, this is that — the vocabulary below is deliberately the conventional one rather than anything invented here.
runtime container, configuration, bootstrap, middleware
│ the ONLY layer that knows which adapter satisfies which port
surfaces admin evaluation delivery, screens, measurement
│
services TurnService, RetrievalService, IndexingService …
│ orchestration. Composes domain decisions with the engine
adapters PgVectorStoreAdapter, OpenAIResponsesAdapter …
│ the only place a vendor name may appear
ports VectorStorePort, ModelPort, ConnectorPort …
│ abstract outbound interfaces — the hexagon edge
domain Principal, Grant, Scope, Profile, Proposal, Citation
│ entities and policies. Knows the business, not the wiring
engine channel Assistant, Thread, RunLoop, Tool::Base │ Broadcaster, Marker
│ the LLM + tool engine, and server-driven UI
kernel Identifier, Result, Error, Usage, Text
primitives. Depends on nothing
The rule that earns its keep:
engine may not import domain. The engine takes an
already-composed tool registry and never asks who the principal is, because by the time it runs
the question is settled. That single constraint is what makes it extractable as a library,
testable without a database, and reusable by a product with a completely different permission
model. CI fails the build on an upward import.
| Layer | Holds | Answers |
|---|---|---|
kernel | Value objects, error hierarchy, ids | What are the primitives everything shares? |
engine | Assistant, Thread, Message, RunLoop, Tool::Base, provider adapters | How does a conversation with a tool-using model actually run? |
channel | Broadcaster, Marker, ReferenceStore | How does a tool push a rendered widget to a screen and hand the model a token instead of the payload? |
domain | Principal, Grant, Scope, Profile, Proposal, policies | Who may do what, and to which content? |
ports | 13 interfaces | What does this framework need from the outside world? |
adapters | Concrete implementations | Which vendor, this time? |
services | TurnService and siblings | What sequence turns a request into an answer? |
surfaces | Surface::Base, chat, ambient, machine | Where does the assistant appear, and what must every appearance guarantee? |
admin | ViewBase, fields, actions, audit | How is it configured and inspected? |
evaluation | Suites, judge, gate, review | How do we know a change helped? |
testing | Harness, doubles, assertions | How is any of this tested without a network? |
runtime | Container, Bootstrap, Middleware | How is one deployment wired? |
Naming conventions
Applied without exception, so a filename predicts its contents and a class name predicts its role. If you can name the suffix, you know where it lives and what it may depend on.
| Suffix | Means | Example |
|---|---|---|
*Base | Abstract, meant to be subclassed by you | Tool::Base, Surface::Base, Admin::ViewBase, Adapters::Base |
*Port | Abstract outbound interface. Named for the capability, never the vendor | VectorStorePort, ModelPort, ConnectorPort |
*Adapter | One concrete implementation of exactly one port. Named for the vendor or technology | PgVectorStoreAdapter, OpenAIResponsesAdapter |
*Service | Application service. Orchestrates; holds collaborators, not request state | TurnService, RetrievalService |
*Policy | One isolated decision, injectable and testable alone | ScopePolicy, RoutingPolicy, ThrottlePolicy |
*Registry | Composition and lookup | Tool::Registry, GrantCatalogue |
*Definition | Declarative description of an extension, validated once at define time | Tool::Definition |
*Result / *Outcome | A return value; Outcome when there is more than one terminal state | Tool::Result, RunOutcome |
*Record | Append-only, written for audit or later inspection | RunRecord, AuditEntry |
*Store / *Repository | Persistence for one entity family | ReferenceStore, ProfileRepository |
*Error | A failure type. All descend from Kernel::Error | NotPermittedError, DefinitionError |
Method prefixes
| Prefix | Contract |
|---|---|
build_* | Pure construction, no side effects |
resolve_* | Look something up, and fail closed if it cannot be found |
compose_* | Assemble from parts, upstream of any content being read |
emit_* | Push to a channel; returns a Marker, never the payload |
propose_* | Create something a person must accept before it takes effect |
on_* | An event handler |
*? | A predicate returning a boolean |
The five kinds of data class
Every data-carrying class is exactly one of these, and each file says which in its header chip. If you cannot decide which of the five a new class is, it is probably two classes.
| Kind | Rules | Examples |
|---|---|---|
| Value object | Immutable, equality by value, no identity. Constructed, never mutated | Grant, Scope, Marker, Usage, Citation, Chunk |
| Entity | Has identity and a lifecycle. Loaded and saved through a repository | Principal, Conversation, AssistantProfile, Corpus, Proposal |
| Definition | Declarative registration data supplied by an author. Validated once, at define time, so errors surface at boot rather than inside a turn | Tool::Definition, Field, Action |
| DTO | Crosses a boundary. No behaviour beyond shaping | TurnRequest, Adapters::Response, Envelope, SurfaceConfig |
| Record | Append-only then sealed. Written for audit, read under access control | RunRecord, AuditEntry, ReviewRecord |
Why ids are classes and not strings.
Kernel::Identifier has one subclass per id kind — TenantId,
RunRef, CorpusId — so a tenant id can never be passed where a run
reference belongs. That is the mistake which produces cross-tenant bugs no test catches, because
both values are strings and both look plausible in a log.
If you already know…
Nothing here is novel. Each piece is a pattern you have used somewhere else, named the way that ecosystem names it.
| You have seen | It is here as |
|---|---|
| Hexagonal / ports & adapters | packages/ports and packages/adapters, with the container as the only binding site |
| A DI container / composition root | runtime/container and runtime/bootstrap. Self-hosted, hybrid and managed differ only in these bindings |
| Service objects | services/*, all descending from Service::Base |
| Repositories | ProfileRepository, TenantRepository, and the *StorePort family |
| Rack / Express / servlet middleware | runtime/middleware — authentication, tenancy, admission, budget, telemetry, run record, in an asserted order |
| A plugin base class you subclass | Tool::Base, Surface::Base, Admin::ViewBase. All validate in self.inherited, so a bad extension fails at boot |
| A scaffolded admin (Django admin, ActiveAdmin) | packages/admin — declare a resource, columns, fields, actions, audit |
| Pub/sub or Turbo/Hotwire streams | packages/channel — but carrying a Marker back to the model, which is the part that is not standard |
Reading the tree
| Element | Meaning |
|---|---|
| Group headings | One per layer, in dependency order top to bottom, with the layer number and role on the right |
| The dot | Provenance, not visibility. Filled = observed in the originating implementation, half = partial, hollow = a design position with no counterpart |
| Header chips | Visibility, the data-class kind, and provenance again in words |
| Search | Paths, guide titles and all 483 declared symbols. Press / |
77 of 142 files are marked observed, 59 are marked design. The first group describes mechanisms that existed and were working before they were written up. The second is argued — informed by one implementation, validated by none. Presenting them at equal confidence would be the easiest way to misuse this, which is why the distinction is in the tree rather than in a footnote. See What is observed and what is argued.