Getting Started
This guide walks you through running the Makaio Framework, understanding its core concepts, and writing your first extension.
Prerequisites
- Node.js 22+
- Yarn 4 (or npm — Yarn is used throughout this guide)
Starting the Server
The framework ships a headless CLI server and desktop host shells. The CLI server is the simplest way to start:
makaio serveThis boots the full runtime:
- Creates a Hono HTTP server with a
/healthendpoint - Attaches the WebSocket bus transport to the HTTP server
- Opens (or creates) the SQLite database and runs migrations
- Loads machine identity (keypair for device identification)
- Starts framework services (session, tools, capabilities)
- Discovers and loads extensions and their adapter/provider/tool contributions
- Prints
MAKAIO_PORT=6252to stdout when the bus is ready (default port; override with--port)
Verify it is running:
curl http://localhost:6252/health# { "ok": true, "auth": false }The bus is now accepting WebSocket connections at ws://localhost:6252/bus.
Server Options
makaio serve # Default port: 6252makaio serve --port 3000 # Fixed portmakaio serve --port 0 # Ask the OS to assign a free portmakaio serve --host 0.0.0.0 # Bind to all interfaces (default: loopback)For non-loopback access from the standalone CLI, set MAKAIO_BUS_SECRET and use
--host. The --lan-bind E2E pairing path is host-composition wiring and requires a
peer-signing-key resolver supplied by the embedding host.
Core Concepts in 2 Minutes
Bus — A typed pub/sub + RPC system. Every service, adapter, and tool talks through the bus. Subjects are defined with Zod schemas. Events are fire-and-forget; requests are RPC with typed responses.
Adapters — Three-layer bridges to AI providers (AIAdapter > AIAgent > AIAgentConnector). Each adapter registers on the bus and participates in the session lifecycle.
Sessions — Turn-based conversations. A session tracks messages, agents, and
context windows. Send session.sendMessage on the bus to start or continue a
conversation.
Tools — Typed function definitions with Zod input/output schemas. Tools
register on the bus and are available to all agents via tool.list and
tool.execute.
Extensions — Runtime extensions that contribute services, storage, CLI
commands, HTTP routes, windows, or browser bundles. Each extension is a
MakaioExtension loaded by the ExtensionCoordinator.
Storage — Bus-mediated and backend-agnostic. Storage handlers are namespace-specific
bus request handlers such as SessionStorageSubjects.get; the backend (memory or SQLite)
is invisible to callers.
Your First Bus Interaction
The bus is the foundation of everything. Here is a minimal example that registers a namespace, subscribes to events, and emits:
import { MakaioBus } from '@makaio/framework/bus';import { z } from 'zod';
// 1. Define schemasconst GreeterSchemas = { greeted: z.object({ name: z.string(), timestamp: z.number(), }), greet: { request: z.object({ name: z.string() }), response: z.object({ greeting: z.string() }), },};
// 2. Register a namespaceconst GreeterNamespace = MakaioBus.registerNamespace('greeter', GreeterSchemas);const GreeterSubjects = GreeterNamespace.subjects;
// 3. Subscribe to eventsMakaioBus.on(GreeterSubjects.greeted, (ctx) => { console.info(`${ctx.payload.name} was greeted at ${ctx.payload.timestamp}`);});
// 4. Handle requestsMakaioBus.on(GreeterSubjects.greet, (ctx) => { const greeting = `Hello, ${ctx.payload.name}!`; ctx.setResult({ greeting });});
// 5. Emit an eventawait MakaioBus.emit(GreeterSubjects.greeted, { name: 'World', timestamp: Date.now(),});
// 6. Wait for the next matching event (promise-based, with timeout)const ctx = await MakaioBus.once(GreeterSubjects.greeted, { filter: { name: 'World' }, timeoutMs: 5_000,});console.info(ctx.payload.timestamp);
// 7. Make a requestconst { greeting } = await MakaioBus.request( GreeterSubjects.greet, { name: 'Developer' },);console.info(greeting); // "Hello, Developer!"Everything is typed end-to-end. ctx.payload in the event handler is
{ name: string; timestamp: number }. The request() return type is
{ greeting: string }. No manual type annotations needed — they flow from the
Zod schemas. once() returns a promise that resolves on the next matching
event — use filter to narrow by payload fields and timeoutMs to avoid
hanging indefinitely.
See docs/bus/ for the full bus architecture guide.
Writing a Tool
Tools are the lowest-barrier contribution point. Define a tool with
defineTool(), group it into a toolset with defineToolset(), and register
it with the ToolRegistry:
import { defineTool, defineToolset, toolSuccess } from '@makaio/framework/tools';import { z } from 'zod';
// 1. Define the toolconst currentTimeTool = defineTool({ name: 'current_time', description: 'Returns the current UTC time as an ISO string.', inputSchema: z.object({}), outputSchema: z.object({ time: z.string() }), execute: async () => { return toolSuccess({ time: new Date().toISOString() }); },});
// 2. Group into a toolsetexport const clockToolset = defineToolset({ name: 'clock', description: 'Time-related tools', version: '0.1.0', tools: [currentTimeTool],});
// 3. Register (at runtime, after ToolRegistry is available)await toolRegistry.register(clockToolset);Once registered, the tool is discoverable by all agents via ToolSubjects.list
and executable via ToolSubjects.execute. Any code holding the bus can also
call tools directly:
import { ToolSubjects } from '@makaio/framework/contracts';
const result = await bus.request(ToolSubjects.execute, { toolName: 'current_time', input: {},});// result: { success: true, data: { time: '2026-04-12T...' } }For a real example, see ../tools/filesystem/src/tools/read-file.ts — the
framework’s built-in file read tool with path validation, encoding support,
and size constraints.
Writing an Extension
Extensions are the main way to add functionality to the framework. A
MakaioExtension declares what it contributes and the coordinator handles
lifecycle, dependency ordering, and failure isolation. Non-critical extension
failures are isolated; critical extension failures abort boot because the host
declared them mandatory.
Minimal Extension
import type { IMakaioBus } from '@makaio/framework/bus';import type { MakaioExtension } from '@makaio/framework/contracts';import { KernelSubjects } from '@makaio/framework/kernel';import { BaseService } from '@makaio/framework/service-base';
// 1. Implement the serviceclass HealthMonitorService extends BaseService { private interval: ReturnType<typeof setInterval> | undefined;
public constructor(bus: IMakaioBus) { super(bus); }
protected async onInit(): Promise<void> { this.interval = setInterval(async () => { const result = await this.bus.requestOptional( KernelSubjects.isReady, {}, ); if (result.handled) { console.info('Runtime ready:', result.data.ready); } }, 30_000); }
protected async onDestroy(): Promise<void> { clearInterval(this.interval); }}
// 2. Export the extension manifestexport const healthMonitorExtension: MakaioExtension = { name: 'health-monitor', displayName: 'Health Monitor', surface: 'any',
create(ctx) { return new HealthMonitorService(ctx.bus); },};The coordinator calls create(ctx) during boot, then service.init(). On
shutdown, it calls service.destroy() in reverse dependency order.
What Extensions Can Contribute
| Surface | Manifest field | Purpose |
|---|---|---|
| Background service | create(ctx) | Long-running service with bus handlers |
| Storage | storage.registerHandlers(bus, db, ctx) | Drizzle storage handler registration |
| CLI commands | cli: CliContribution | Commands runnable via makaio <command> |
| HTTP routes | http: { prefix, mount } | Hono sub-app mounted on the server |
| Windows | windows: WindowManifest[] | Desktop windows |
| Tray menu | tray: TrayManifest | System tray entry |
| Browser bundle | browser: { entrypoint } | Renderer-side code loaded in the shell |
For dependencies, surface gating, CLI authoring, scaffolding, and the build/publish workflow, see Creating Extensions.
Sessions and Agents
To start a conversation, send session.sendMessage on the bus:
import { AgentSubjects, SessionSubjects } from '@makaio/framework/contracts';
const sessionId = crypto.randomUUID();
// This auto-creates the session and attaches an agentconst { messageId, turnId } = await bus.request( SessionSubjects.sendMessage, { sessionId, agent: { kind: 'adapter', adapterName: 'openai-node', // Omit model to use the provider default, or pass an adapter-specific ID from the model registry. }, message: 'What files are in the current directory?', },);The session orchestrator handles the rest:
- Creates the session if
sessionIdis new - Attaches an agent via
adapter.startAgentif none is active - Routes the message to the agent’s connector
- The connector calls the provider API and streams events back through the bus
Listen for the response:
// Stream text deltasbus.on(AgentSubjects.message_delta, (ctx) => { process.stdout.write(ctx.payload.text);}, { filter: { sessionId } });
// Wait for completionconst completion = await bus.once(AgentSubjects.complete, { filter: { sessionId, messageId }, timeoutMs: 120_000,});
console.info('\nDone:', completion.payload.message);For lower-level control, resolve an adapter name to an adapter instance ID and
then call adapter.startAgent directly:
import { AdapterSubjects } from '@makaio/framework/contracts';import { AdapterRuntimeSubjects } from '@makaio/framework/services';
const { adapterId } = await bus.request(AdapterRuntimeSubjects.resolveId, { adapterName: 'openai-node',});
const result = await bus.request(AdapterSubjects.startAgent, { adapterId, role: 'lead', mode: 'create', initialMessage: 'Hello!',});
if (!result.success) { throw new Error(result.message);}
const { agentId } = result;Running Tests
The commands below assume the standalone framework repository root. In the larger development
monorepo, run them from framework/ or prefix paths with framework/ from the outer root.
# All framework testsyarn test
# Specific packageyarn test packages/bus-core
# Specific test fileyarn test packages/bus-core/src/__tests__/hierarchical-namespace.test.ts
# Full validation (types + lint + format)yarn validateThe test runner uses a token-efficient reporter optimized for AI consumption.
Do not override the reporter with --reporter.
Where to Go Next
| Topic | Document |
|---|---|
| Bus architecture (full guide) | docs/bus/ |
| Writing an adapter | docs/creating-adapters.md |
| Extension model (full guide) | docs/extensions/ |
| CLI commands and server | docs/cli.md |
Package-level READMEs:
| Package | README |
|---|---|
| Bus core | ../packages/bus-core/README.md |
| Contracts | ../packages/contracts/README.md |
| Kernel | ../packages/kernel/README.md |
| Adapter contracts | ../adapters/core/README.md |
| Tools core | ../tools/core/README.md |
| Storage core | ../packages/storage/core/README.md |