developers.cogni.build

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.

142
Files
14
Layers
126
Public SDK
16
Private
483
Symbols
§1

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.

Dependencies point down. Never up, never sideways.
  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.

LayerHoldsAnswers
kernelValue objects, error hierarchy, idsWhat are the primitives everything shares?
engineAssistant, Thread, Message, RunLoop, Tool::Base, provider adaptersHow does a conversation with a tool-using model actually run?
channelBroadcaster, Marker, ReferenceStoreHow does a tool push a rendered widget to a screen and hand the model a token instead of the payload?
domainPrincipal, Grant, Scope, Profile, Proposal, policiesWho may do what, and to which content?
ports13 interfacesWhat does this framework need from the outside world?
adaptersConcrete implementationsWhich vendor, this time?
servicesTurnService and siblingsWhat sequence turns a request into an answer?
surfacesSurface::Base, chat, ambient, machineWhere does the assistant appear, and what must every appearance guarantee?
adminViewBase, fields, actions, auditHow is it configured and inspected?
evaluationSuites, judge, gate, reviewHow do we know a change helped?
testingHarness, doubles, assertionsHow is any of this tested without a network?
runtimeContainer, Bootstrap, MiddlewareHow is one deployment wired?
§2

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.

SuffixMeansExample
*BaseAbstract, meant to be subclassed by youTool::Base, Surface::Base, Admin::ViewBase, Adapters::Base
*PortAbstract outbound interface. Named for the capability, never the vendorVectorStorePort, ModelPort, ConnectorPort
*AdapterOne concrete implementation of exactly one port. Named for the vendor or technologyPgVectorStoreAdapter, OpenAIResponsesAdapter
*ServiceApplication service. Orchestrates; holds collaborators, not request stateTurnService, RetrievalService
*PolicyOne isolated decision, injectable and testable aloneScopePolicy, RoutingPolicy, ThrottlePolicy
*RegistryComposition and lookupTool::Registry, GrantCatalogue
*DefinitionDeclarative description of an extension, validated once at define timeTool::Definition
*Result / *OutcomeA return value; Outcome when there is more than one terminal stateTool::Result, RunOutcome
*RecordAppend-only, written for audit or later inspectionRunRecord, AuditEntry
*Store / *RepositoryPersistence for one entity familyReferenceStore, ProfileRepository
*ErrorA failure type. All descend from Kernel::ErrorNotPermittedError, DefinitionError

Method prefixes

PrefixContract
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
§3

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.

KindRulesExamples
Value objectImmutable, equality by value, no identity. Constructed, never mutatedGrant, Scope, Marker, Usage, Citation, Chunk
EntityHas identity and a lifecycle. Loaded and saved through a repositoryPrincipal, Conversation, AssistantProfile, Corpus, Proposal
DefinitionDeclarative registration data supplied by an author. Validated once, at define time, so errors surface at boot rather than inside a turnTool::Definition, Field, Action
DTOCrosses a boundary. No behaviour beyond shapingTurnRequest, Adapters::Response, Envelope, SurfaceConfig
RecordAppend-only then sealed. Written for audit, read under access controlRunRecord, 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.

§4

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 seenIt is here as
Hexagonal / ports & adapterspackages/ports and packages/adapters, with the container as the only binding site
A DI container / composition rootruntime/container and runtime/bootstrap. Self-hosted, hybrid and managed differ only in these bindings
Service objectsservices/*, all descending from Service::Base
RepositoriesProfileRepository, TenantRepository, and the *StorePort family
Rack / Express / servlet middlewareruntime/middleware — authentication, tenancy, admission, budget, telemetry, run record, in an asserted order
A plugin base class you subclassTool::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 streamspackages/channel — but carrying a Marker back to the model, which is the part that is not standard
§5

Reading the tree

ElementMeaning
Group headingsOne per layer, in dependency order top to bottom, with the layer number and role on the right
The dotProvenance, not visibility. Filled = observed in the originating implementation, half = partial, hollow = a design position with no counterpart
Header chipsVisibility, the data-class kind, and provenance again in words
SearchPaths, 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.