Skip to content

Why I built QuadKit — consistency for agentic coding

I built QuadKit alone. No team, no funding, no timeline. Just a problem I kept hitting — and then a much worse version of it.

Availability note (0.0.50): Eighteen distributions are published, including quadkit-ai-llm, quadkit-ai-agents, quadkit-ai-mcp, quadkit-workflow, and quadkit-notification. The runnable examples are in the framework workspace.

Every Python web app I built ended up the same way. Services importing from each other. Domain types leaking into the HTTP layer. No way to swap an implementation without touching three files. Not cosmetic differences — structural ones, and they compound:

  • Onboarding takes longer, because the conventions are local trivia that don’t transfer.
  • Code review turns subjective, because “that’s not how we do it here” replaces an objective rule.
  • Refactoring gets risky, because the dependency graph is implicit and nobody can verify it.

Then coding agents arrived, and made it worse.

Not because they are bad at Python. Inconsistent conventions create a familiar failure mode: a correction in one place may not carry to the next file or session. A model can fall back to patterns common in its training data, which may not match the conventions in a particular codebase.

A change can pass type-checking and still violate a package boundary or an integration rule. Those issues may only be caught by the relevant linter, tests, or review—often later than is ideal. Coding agents can make the same inconsistency appear faster.

A style guide alone does not solve this. A more reliable pattern is one an agent can follow repeatedly, with important boundaries represented in types, generators, and CI checks. QuadKit is my attempt to make those rules easier to apply and harder to violate.

The rules are the product. Everything else in the framework exists to make these four enforceable rather than aspirational.

1. Contracts stay free of implementation imports

Section titled “1. Contracts stay free of implementation imports”
contracts Shared types; no QuadKit implementation imports.
↑
core Contracts are its intended internal boundary.
↑
extensions Peer imports are checked by policy.

quadkit-contracts has no QuadKit implementation imports and depends at runtime on typing-extensions. When packages share a protocol, type, or exception, its intended home is contracts; that avoids coupling peers to each other’s implementations. Regular extensions may also use the permitted foundational packages, while composites and tracked exceptions are listed in the lint configuration.

This is not left to convention. The root import-linter configuration has separate checks for contracts, core and foundational layers, regular extension independence, and the AI and multimedia sub-package seams; CI runs them through the namespace-aware wrapper. Composite packages are excluded where they intentionally compose extensions, and specific tracked allowances are documented. The result is a defined, reviewable boundary policy—not a claim that every cross-package import is forbidden.

Two phases, two different types, never mixed. register() takes a ContainerRegistrarProtocol, which has no resolve() method — not by convention, but because the type doesn’t have one. A model that tries to resolve a dependency during registration gets a type error, not a runtime surprise.

The module compiler then sorts providers into parallel boot levels and unwinds them in reverse if one fails. It is a small static analysis pass over your dependency graph, and it runs before your code does.

Not an exception, not None, not a tuple. A value. The caller has to branch, because the type forces it.

unwrap() raises on Err, so unwrap() without is_ok() is a bug the runtime finds rather than a review finding it might miss.

There is one layout. quadkit new project my-app --template api writes the canonical starter; quadkit add adds installed packages and the generators grow the app with gen controller, gen provider, gen module, and gen migration.

Scaffolding gives an agent a consistent starting structure and reduces the number of conventions it has to infer.

How a pattern reaches an unsupervised agent

Section titled “How a pattern reaches an unsupervised agent”

This is the part that most frameworks skip. A pattern that only lives in a human onboarding document is not a pattern an agent can follow.

Machine-readable docs meet agents where they work. Four surfaces—/llms.txt, /agents.md, /SKILL.md, and /llms-full.txt—are assembled by site generators from shared catalog data and their own maintained content. They complement the human pages; generation does not by itself guarantee that every piece of prose stays synchronized:

SurfaceWhat it is
/llms.txtIdentity, install, rules, every documentation URL
/agents.mdMachine-readable agent guidance — hierarchy, Result vs exceptions, provider lifecycle
/SKILL.mdOne-file skill, fetchable without another clone
/llms-full.txtThe full catalog plus architecture notes

When a packed skill and the site disagree about layout, the site wins. That is written down, because an agent that guesses will guess differently next time.

The CLI generates repeatable structure. Scaffolding, migrations, controllers, and providers have generators, which reduces the amount of layout and boilerplate an agent has to reconstruct from prose.

Exemplars, not prose. The runnable examples catalog spans the framework; useful patterns include sql-repository for repository-backed SQL, auth-web for cookie sessions and JWT claims, auth-rbac for role-based access, and support-agent for an agent loop with container-provided tools. Copying a working example is more concrete than prose alone.

Ten failure modes are documented with fail/fix pairs in Common mistakes. They cluster into four causes; the table samples representative failures:

CauseFailureWhy a model reaches for it
Boundary blindnessCross-extension importExtensions look like sibling packages; nothing in the names says no
Service locatorDependency injection from training data often resolves globals
Convention driftmodels/ instead of domains/Both are real Python conventions; the codebase’s choice is invisible
Relative importsUniversally idiomatic, wrong across package boundaries
Control-flow collapseunwrap() without is_ok()Idiomatic Rust-like error handling, applied where the type forbids it
Any, print(), eager fallbacks”It works anyway” is a pattern the model has seen pass review
Lifecycle confusionregister() resolvingRegistration and construction look identical in most frameworks
Depends() as the composition rootThe FastAPI pattern is the strongest prior for a Python web service

Many of these mistakes can type-check in isolation: a type checker validates shapes, not every package boundary or architectural rule. The register()/boot() mismatch is an exception that the distinct protocols can catch statically; configured import boundaries need their own checks.

  1. Does it cross a prohibited package boundary? The repository checks configured boundaries in CI, with documented exclusions and allowances.
  2. Does every domain failure branch? No unwrap() without is_ok(), no None standing in for Err.
  3. Is the layout from the CLI or from memory? If a directory appeared that no generator wrote, it is wrong regardless of how it looks.

Consistency is not something you maintain by asking. It is something you install.

That is true for people, and especially useful for agents: they may not retain a convention from one prompt or file to the next. Give them explicit boundaries, types that expose expected branches, generators for repeatable structure, and documentation in the formats they can consume, and you reduce the number of inconsistencies that need to be caught in review.

The framework is still alpha. The four rules above are the part that will not move. See what is stable in 0.0.x.

If you are building an AI backend: start with the examples, or read the agent playbook.