Kyno
Kyno is a coherence control plane for multi-agent systems. It holds one versioned source of truth for your system's mission and principles — its constitution — and serves it over MCP, so every agent acts on the direction in force right now, even when that direction changes mid-flight.
Why
When a multi-agent system's goals change, agents holding a stale copy of the old direction keep producing work for it. It gets worse: quality checks made against that stale copy push the work back toward the obsolete goal. Kyno removes the stale copy. Direction lives in one versioned store, agents pull the current version before each step, and subscribers are notified the moment it changes.
Quick start
pip install kyno # CLI: kyno
kyno init-db
kyno set --mission "Ship a lending product people trust" \
--note "initial constitution"
kyno current
kyno serve --transport stdio # or --transport http
A constitution is a mission plus ordered principles. The mission is the overarching purpose, and the tie-breaker when principles conflict. Every change appends a new immutable version with a plain-language change note. Nothing is edited in place, so the question "what was the direction when agent X acted" always has an answer.
Writing one
A one-line principle is a short name for an idea, not the full rule. Give a constitution as much as it needs and no more. Each of these is optional:
- a declaration: the long-form document that the mission is the headline of;
- a description under any principle: the paragraph that settles an argument about what the principle means.
Both are prose, and long prose doesn't fit in command-line flags, so a constitution is written in a file:
# constitution.yaml
mission: Ship a lending product people trust with their worst month
declaration: |
## What we are for
Lending is a promise about somebody's worst month. We would rather lose
the deal than make a promise we cannot keep.
## What that costs us
- We say no early and in plain words.
- We publish the number before the story that softens it.
principles:
- Say the hard number first
- title: Refuse clearly
description: |
If we cannot lend, we say so on the first screen, and we say why.
note: the constitution as written
by: camilo
kyno set --file constitution.yaml
kyno set --file constitution.yaml --constitution eu --note "the EU edit"
The declaration is markdown, and the published page renders it: headings,
lists, emphasis, quotes, links. Raw HTML inside it is escaped instead of
passed through, and javascript: links are refused. The page is served to
anonymous visitors, so text you typed must never reach them as markup that
runs. Images are not rendered either; that is what keeps the page one
self-contained response.
Everywhere else the declaration stays exactly the markdown you wrote. The
JSON endpoint, the MCP tools and kyno export all serve the source, not the
rendered document.
--note, --by and --constitution may override the file, because they
describe this edit rather than the constitution. The field flags
(--mission, --declaration, --principle) cannot be combined with
--file, because two sources for the same field would be ambiguous. Fields
the file leaves out are carried forward from the previous version; to clear
one, write it empty, like declaration: "".
The flags are still there for a quick edit:
kyno set --mission "Ship a lending product people trust" --note "sharpen the mission"
The contract
Over MCP or Python:
get_constitution— the direction in force now (mission, principles, version).get_changes_since(known_version)— the pull an agent makes before a step: the current direction plus the change notes since the version it last saw. Missing a notification causes no harm, because the next pull carries everything needed.get_mission,get_declaration,get_principles,get_principle(title)— one piece of the document each, for when a compact read left it out.set_direction(mission?, declaration?, principles?, change_note)— append the next version. Omitted fields carry forward;""clears one. On HTTP this requires the bearer token.
Every read returns as little as it can by default: the titles, not the long
text. An agent pulls before every step, and would otherwise pay for the
whole document each time. Ask for more when something actually needs it:
detail="full" on the two pulls, detail="full" on get_principles, or one
of the targeted reads. Every answer carries the version it came from, so a
client mixing them can tell when they have drifted apart.
Clients may also subscribe to the kyno://constitution/current resource and
receive a standard MCP resources/updated notification on every version
bump. It serves the compact form: a resource takes no parameters, and the
whole document is one tool call away.
Multiple constitutions
One Kyno can hold several constitutions side by side — for example one per
product line or per jurisdiction. Every operation takes an optional
constitution name, over MCP and on the CLI (--constitution eu), and
defaults to "default", so a single-constitution setup never has to mention
it. Each name has its own version sequence: bumping eu to v2 leaves
default at whatever version it was. A name you have never written to reads
as the same version-0 empty state an untouched store does. The subscribable
resource is the default constitution's; agents on another one pull it by name
with get_changes_since.
Adapters (CrewAI, LangGraph)
pip install "kyno[crewai]" # or: pip install "kyno[langgraph]"
On a different framework or language? The whole contract for building your own adapter is one page: docs/integrating.md.
An adapter binds a crew or a graph to one named constitution and re-binds every next step to the version in force right now:
import kyno
from kyno.adapters.crewai import CrewAiKyno
connection = kyno.connect() # reads KYNO_URL and KYNO_TOKEN
adapter = CrewAiKyno(connection.binder(), constitution="eu")
adapter.register() # injects the current direction before each model call
That is the whole integration. Every model call carries the version in force,
and a version published mid-run reaches the next step. The pieces behind
connect() — the binder, the sources, the policies — live in kyno.sdk for
anyone who needs to assemble them differently. Embedding Kyno in the same
process instead? Swap the source:
DirectionBinder(LocalDirectionSource(control_plane)).
- Pull before each step. The current mission and principle titles are
injected into the next model call, tagged with the constitution and version
they came from. That block rides on every model call, so it stays small by
default. Bind with
connection.binder(context="full")when you would rather spend the tokens: the declaration and the principle descriptions are injected too. When Kyno is unreachable, or answers with something unreadable, the pull degrades: the step runs on the last direction the binder holds, and the staleness is emitted as telemetry. Bind withconnection.binder(policy=PullPolicy(fail_closed=True))when your posture is "no direction, no work": the step raises instead of proceeding. - Push consumption.
BackgroundSubscriberturns an MCPresources/updatednotification into a re-pull by name. A step already running is never interrupted; the next one binds the new direction. - What changed. A pull carries the operator's change note and a computed delta: which principle moved, quoted both ways, whether the mission moved, what was added or dropped. The note says why, in the words of whoever wrote it; the delta says what, computed from the versions themselves. A consumer holding no version gets no delta, since the whole direction is already in front of it. The delta is what makes a small change visible: when one principle of four moves and the mission holds, the block otherwise reads the same as the last one.
- Planning. If your orchestrator plans before it executes,
binder.plan()returns a tracker:direction()pulls what to plan against and remembers the version, andchanged()returns the fresh direction when a newer version exists, so you know when to re-plan the remaining work. - Adapters are read-only. They pull and subscribe.
set_directionstays an operator or CLI action against Kyno, never something an adapter calls on a crew's or graph's behalf.
On LangGraph, inherit KynoState in your graph's state schema and put
direction_node ahead of the work. LangGraph carries only the keys a schema
declares, so without KynoState the direction a node pulls never reaches the
nodes after it:
from kyno.adapters.langgraph import KynoState, direction_node
class State(KynoState, total=False):
output: str
Acting on a change
Kyno carries the direction, the version, and what changed. What a system does when the version moves belongs to the system, and there are three answers:
- Carry on. The next step gets the new direction. The integration above already does this, and it costs nothing beyond the pull.
- Reassess. Re-derive the remaining work under the new direction. This
is a planning call, made when
binder.plan()reports a change. - Stop. Review finished work against the direction it was bound to, and halt on a bad verdict. This is the realignment gate, and it costs a judge call per finished task.
Kyno takes no position on which one is right.
The realignment gate reviews each finished task. It holds no judgment of
its own: it asks a VerdictSource you supply and acts on the answer, raising
on CrewAI and calling interrupt() on LangGraph when the verdict is
DRIFTED. Kyno ships no judge, so an adapter built without one has no gate.
Where a gate exists but its judge is unreachable, the work proceeds marked
unchecked and the event is emitted as telemetry;
GatePolicy(fail_closed=True) stops instead.
from kyno.sdk import RealignmentGate
from kyno.adapters.langgraph import gate_node # LangGraph
adapter = CrewAiKyno(binder, gate=RealignmentGate(source=your_judge))
crew = Crew(..., task_callback=adapter.task_callback) # CrewAI
Storage
SQLite out of the box; PostgreSQL for production via KYNO_DATABASE_URL.
Storage is pluggable: hand SqlConstitutionStore your own SQLAlchemy
Engine to live inside an existing database, or implement the small store
protocol to bring your own persistence entirely. Concurrent writers are safe:
versions are serialized by a unique index and a retry, never lost or
duplicated.
Reads never fail on an empty store. Before any direction is set, consumers get a version-0 empty state, so integrating Kyno ahead of adopting it costs nothing.
Publishing your constitution
If you want to show people the principles you say you operate by, Kyno can serve that page itself. The published page and the one your agents obey are then the same record, not two copies that drift apart.
kyno publish # the default constitution
kyno publish --constitution eu --with-history
kyno unpublish --constitution eu
While kyno serve --transport http is running, a published constitution is
readable by anyone at:
GET /constitutions/{name}— a self-contained HTML page (no scripts, no external assets, light and dark). The declaration is the body of it, rendered from markdown, and a described principle carries its paragraph.GET /constitutions/{name}.json— the same content, machine-readable.GET /constitutions/andGET /constitutions.json— an index of what you have published.
Three things worth knowing:
- A published name has to be a slug: lowercase letters, digits and
single hyphens (
acme,acme-eu). It is both the URL and the name your agents use, so Kyno refuses anything else rather than quietly rewriting it. Names you never publish are unrestricted. - Nothing is public until you publish it, and publication is per name. One Kyno can hold your internal constitution and your public one side by side; publishing the second does nothing to the first.
- Publishing shows the current direction only: mission, declaration,
principles, version, last-changed date. The version history stays private
unless you add
--with-history, because change notes are written for your operators and routinely explain why you changed course. A published history shows the 100 most recent versions; that is the page's contract. The full history stays available to authenticated callers over MCP andkyno export.
Anything you have not published answers 404, exactly as a name that does
not exist does. Nothing on the public side reveals which of the two it was.
Changing the colors
Six environment variables. Set the ones you care about and leave the rest:
| Variable | Default | What it colors |
|---|---|---|
KYNO_PAGE_ACCENT |
#6d6d66 |
link underlines, principle numbers |
KYNO_PAGE_BACKGROUND |
#fbfbf9 |
the page |
KYNO_PAGE_TEXT |
#1b1b19 |
body text |
KYNO_PAGE_MUTED |
#6d6d66 |
labels, dates, the version stamp |
KYNO_PAGE_RULE |
#e4e3de |
the hairlines between items |
KYNO_PAGE_FONT |
system sans | font-family for the page |
Unset, you get the built-in look, with its automatic dark mode. Set any color and Kyno stops swapping the palette for dark mode, because inverting colors you chose would give you a page you never approved. Past that point the palette is yours. Setting only the font keeps the dark swap.
Using your own templates
The pages Kyno serves are template files, and it will hand you the real ones:
kyno page export ./pages # constitution.html, index.html, page.css
Edit them, then point Kyno at your copies. It prints these two lines for you:
export KYNO_CONSTITUTION_TEMPLATE=/srv/pages/constitution.html
export KYNO_INDEX_TEMPLATE=/srv/pages/index.html # optional
That is the whole workflow. What you exported is what Kyno was already rendering — the same files, filled the same way — so you are editing a working page rather than reconstructing one, and anything you leave alone keeps working.
kyno page export refuses to overwrite files that are already there, and
writes nothing at all when it would have to.
The exported page.css is a starting point for your own styles: link it,
inline it, or throw it away. The $stylesheet placeholder below always
serves the styles built into Kyno, not your copy of them. A template that
keeps $stylesheet stays on the built-in look and follows the color
variables above; one that drops it is fully yours.
Placeholders
constitution.html
| Placeholder | What it is |
|---|---|
$stylesheet |
the whole <style> block: color variables + Kyno's page styles |
$name |
the constitution's name |
$mission |
the mission, or the name when there is no mission |
$declaration |
the declaration rendered from markdown, wrapped in its <div> — empty when there is none |
$principles |
the principles section, heading and list — empty when there are none |
$version |
the version number, e.g. 3 |
$updated |
the last-changed date, e.g. 2026-08-13 |
$history |
the version history block — empty unless you published history |
index.html
| Placeholder | What it is |
|---|---|
$stylesheet |
as above |
$items |
the list of published constitutions, or the "nothing published yet" line |
$count |
how many are published |
Each block placeholder brings its own wrapper and disappears entirely when it has nothing to say, so a template never has to ask "what if there is no declaration". That is deliberate. These are placeholders, not a template language — no loops, no conditions, no expressions — and the defaults are held to the same limit, which is why they are the same files you just exported.
This limit is also a safety feature. Kyno escapes your mission, principles and change notes before they reach your file, and renders your declaration's markdown with HTML disabled, so no template can turn text somebody typed into a constitution into markup that runs. A placeholder you misspell is left alone rather than breaking the page. And if your file is missing or unreadable when a request arrives, Kyno serves its own page and logs a warning, so a bad template never takes your public page down.
Auth
- stdio is open. A process that can spawn the server already owns the database file under it, so a token there would add a step without adding protection.
- HTTP is gated by a shared bearer token (
KYNO_TOKEN) gates every request to the MCP endpoint (/mcp). The server refuses to start tokenless over HTTP unless you explicitly opt in (KYNO_ALLOW_INSECURE_HTTP, for local experimentation only — it warns), and aKYNO_TOKENthat is set but blank is a configuration error rather than silently no auth. Embedders building the app in code opt in the same way:build_http_app(..., allow_insecure=True). The published constitution pages above sit outside that gate on purpose; they are the surface you chose to open.
The write token is direction control: whoever holds it steers the
instructions of every agent bound to this Kyno. Treat it like a system-prompt
credential — serve /mcp over TLS and keep the token out of logs and
checkpoints (Kyno's own reprs never print it). One related caution: the
[kyno:direction …] header on the injected block is a record for reading
transcripts, not a security check. Text arriving from tools or users can
imitate it, so nothing should trust a block just for looking like one. Kyno
refuses constitution text containing the marker, and the adapters only ever
replace the block they injected themselves.
Deploying
- Use an absolute
KYNO_DATABASE_URLin production. The default (sqlite:///kyno.sqlite3) is a dev convenience that resolves against whatever working directory the process starts in. - Run a hosted Kyno behind a reverse proxy that enforces rate limits. The public pages answer anonymous traffic, and rate limiting is the proxy's job, not Kyno's.
- Field sizes are part of the API contract: mission ≤ 4,000 characters,
declaration ≤ 200,000, change note ≤ 2,000, up to 100 principles with
titles ≤ 300 and descriptions ≤ 4,000, constitution names ≤ 200.
set_directionrefuses anything larger, and/mcprequest bodies are capped at 5 MB. - A pip-installed Kyno carries its own migration scripts:
kyno init-dbcreates a fresh schema stamped at the current head, andkyno upgrade-dbbrings an existing database up to date after an upgrade.
Testing
python -m pytest -q # SQLite, no network
KYNO_TEST_POSTGRES_URL=postgresql+psycopg://… python -m pytest -q # + Postgres
Licensing
The SDK, the adapters, the conformance kit, and the integration guide are MIT. The control plane is under the Elastic License 2.0. LICENSE explains how to tell which license a file carries.
Sibling project: Canon tests whether your system's outputs actually cohere with the constitution Kyno serves.
See CONTRIBUTING.md for style and test expectations.
Release files for kyno 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kyno-0.1.0.tar.gz | 158.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kyno-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 229.8 kB
Release files / kyno-0.1.0.tar.gz
| Download URL | kyno-0.1.0.tar.gz |
|---|---|
| Size | 158.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fa00901fdc40c7552326fcdfdd6ffba4973d8a9b04fdb5ae395960f3d388ba2a
|
|
BLAKE2b-256 checksum How to use checksums |
d8d49037e75f775143cb37ebae4ab18ac1a2bf783031e1df8af84aa41df38931
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.
Transparency logRelease files / kyno-0.1.0-py3-none-any.whl
| Download URL | kyno-0.1.0-py3-none-any.whl |
|---|---|
| Size | 71.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
62997b3a11dfba62925a576a107000ed2a57e411af53d4080ed308e9a30f67f4
|
|
BLAKE2b-256 checksum How to use checksums |
a3ffee20e13f95e8dfd377ebeed118babacf387707277709f87790c85517fbec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.
Transparency log