Layered architecture
Zhin.js is made of dozens of small packages, but to write bots and plugins you almost only install and import zhin.js. This page explains how the packages are layered and why — for readers who want to understand the internals or contribute.
Three things to remember
- There is one public entry.
zhin.jsre-exports what plugin authors need:definePluginfromzhin.js,defineCommandand friends from subpaths such aszhin.js/commandandzhin.js/middleware. - AI is optional. The default install is the IM core only (under 10MB);
@zhin.js/agentand@zhin.js/aiare optional and used throughzhin.js/agent/zhin.js/ai. - Dependencies only point down. Upper packages may depend on lower ones; lower packages never know upper ones exist.
pnpm check:architectureenforces this in CI — it isn't just a convention.
| You are… | Depend on |
|---|---|
| Writing a bot or a regular plugin | zhin.js only |
| Writing AI tools | plus @zhin.js/tool (npx zhin setup --ai handles it) |
| Writing a new kind of capability (a Feature) | only @zhin.js/feature-kit / @zhin.js/plugin-runtime — not @zhin.js/core |
| Contributing to the framework itself | the layering rules below |
Layer overview
Dependencies are defined by each package's package.json. Read the diagram from the bottom up:
The foundation consists of three independent zero-dependency contracts: @zhin.js/plugin-runtime owns generation, snapshots, disposal, and tokens; @zhin.js/im-contract owns transport-neutral identities, segments, conversation events, and delivery semantics; @zhin.js/interaction owns typed user-interaction requests and conclusions. One layer up, @zhin.js/feature-kit depends only on the runtime base and provides the registration, discovery, and projection mechanism for Feature providers. Feature-layer packages (adapter / command / component / middleware / handler / tool / skill / ...) depend only on these foundation contracts and do not depend on each other.
Moving further up, @zhin.js/core assembles the adapter / command / component / middleware Features together with the kernel into the IM layer (Plugin, Adapter, Endpoint, message send/receive). The facade package zhin.js re-exports core packages as a unified entry -- plugin authors only need import { ... } from 'zhin.js'. @zhin.js/agent, @zhin.js/ai, etc. are optional peer dependencies, so the default installation contains only the IM core, with AI added on demand.
Layer responsibilities
| Layer | Package | Responsibility |
|---|---|---|
| Foundation | basic/logger schema schedule database | Logging, config validation, scheduling, database -- zero/near-zero dependencies |
| Runtime base | packages/im/plugin-runtime (users import from zhin.js) | Generation transactions, snapshot leases, handoff, dispose (see Generation & lifecycle) |
| IM contract | @zhin.js/im-contract | Endpoint, conversation, segment, and delivery contracts with no Core or platform SDK dependency |
| Feature mechanism | @zhin.js/feature-kit | Declare Feature providers, discover capabilities by convention, project into runtime indexes |
| Kernel | @zhin.js/kernel | Plugin system and error hierarchy, no IM concepts |
| Feature layer | @zhin.js/adapter command component middleware ... | Contract for a capability type (e.g. defineAdapter) + projection index (e.g. AdapterIndex) |
| AI engine | @zhin.js/ai | Provider, agentLoop, sessions, memory -- no IM concepts |
| IM layer | @zhin.js/core | Plugin, message pipeline, command dispatch (see Message flow) |
| Agent | @zhin.js/agent | Multi-model orchestration, tool security policies, MCP client |
| Facade | zhin.js | Re-exports the core authoring surface; zhin.js/agent, zhin.js/ai sub-paths exposed on demand |
| Runtime | @zhin.js/runtime | RootRuntime: scans the project graph, composes configuration, executes generation transactions, HMR |
| Assembly | @zhin.js/cli | Composition root -- zhin runtime start wires everything together |
Composition root: @zhin.js/cli
The framework itself does not "start" anything -- each layer only provides mechanisms. The code that assembles these mechanisms into a runnable process is concentrated in @zhin.js/cli, the only place in the entire repo allowed to import across all layers (the sole exception to the layering rules).
zhin runtime start (defined in basic/cli/src/commands/runtime.ts, assembly logic in basic/cli/src/plugin-runtime/) does the following:
- Wraps the single YAML/JSON Root config with
createConfigDocument(@zhin.js/config-file) into a transactionalConfigDocumentPort; - Creates
RootRuntime(@zhin.js/runtime), injecting the module loader (in dev mode:NativeDevelopmentModuleRuntime), config port, and Root resource installer; - Installs Host-level resources via
installResources: HTTP Host, database, Agent Host (including AI fallback handler), Console API, etc.; - After startup, attaches
HmrCoordinator-- file changes trigger generation reloads or process restarts.
A user project's entry point can therefore be minimal. examples/minimal-bot/package.json:
{
"name": "minimal-bot",
"type": "module",
"scripts": {
"dev": "zhin runtime start",
"start": "zhin runtime start --mode production --no-watch"
},
"dependencies": {
"zhin.js": "workspace:*"
},
"zhin": {
"protocol": 1,
"type": "plugin",
"entry": "./plugin.ts",
"engine": "^1.0.0",
"runtime": "trusted",
"features": [],
"plugins": []
}
}The project itself is a plugin (Root Plugin), and the CLI is responsible for discovering and starting it. For a full explanation of manifest fields, see Plugin model; for config declaration and validation, see Config as data.
Corollaries of the layering rules
When writing a Feature (a new capability type), depend only on feature-kit / plugin-runtime -- do not import core. kernel and ai know nothing about IM concepts like "group" or "direct message"; IM concepts only appear in core and above. Host packages (packages/host/http, mcp, a2a) sit above core and are assembled by the CLI; plugins do not depend on Host processes directly.
JSX and visual components
@zhin.js/jsx owns lazy JSX trees and an HTML serializer, depending only on the canonical IM contract types. Core lays out JSX as ordered Segments; only <html> / <Html> children become segment.html. Native lowercase tags are typed without imports, and registered components can be used as named tags; zhin.js/jsx is the authoring facade. Optional @zhin.js/components uses this base plus Markdown parsing and syntax highlighting dependencies, accepts JSXRenderable display props and does not depend on Core, React or image engines. Render tools consume HTML; adapters declare platform policies.