MINARA

Architecture

Repo layout and the shape of the agent

Minara Agent is a TypeScript/Node.js AI agent. The turn loop runs on the Vercel AI SDK (ai, streamText) over multiple LLM providers (Anthropic, OpenAI, xAI, OpenRouter). It uses a custom in-house skill + tool registry for its own tools instead of MCP. Docker-deployable, CLI plus HTTP gateway, SQLite-backed state.

Stack: Node ≥ 24, TypeScript 5.7 (ESM), ai (Vercel AI SDK) + @ai-sdk/* provider adapters, better-sqlite3, ioredis, playwright, vitest. Package manager: [email protected].

Architecture diagram

architecture-overview diagram

Repo layout

src/
  app.ts                       # createApp() orchestrator, calls the app/ wiring builders
  app/                         # Private wiring builders (bootstrap, llm, mcp, messaging, ...)
  gateway/
    cli.ts                     # `pnpm dev` entry (REPL)
    server.ts                  # `pnpm serve` entry (HTTP)
    skills-cli.ts              # `minara skills add/list/upgrade/remove`
  core/
    tool-registry.ts           # ToolRegistry + PermissionTier + tool sets
    prompt-builder.ts          # Assembles system prompt (skill catalog + fragments)
  agent/
    runtime.ts                 # The LLM turn loop (streamText + stopWhen)
  tools/                       # Tool factories, createXxxTools() returns ToolEntry[]
    _shared/
      sandbox.ts               # resolveInSandbox, every file tool uses this
      result.ts                # ok() / err() / errFromThrow() envelope
  skills/
    types.ts                   # DomainSkill interface
    registry.ts                # SkillRegistry + requires_env gating
    builtin/<id>/SKILL.md      # Builtin skills, SKILL.md packages, auto-discovered
    external/                  # Vendored SKILL.md packages
  learning/                    # Methodology store + seeds
  memory/                      # Agent memory + audit log
  config/
    load-env.ts                # First-line `.env` loader via process.loadEnvFile()
docs-src/                      # Authoritative markdown source
docs/                          # This documentation site (Fumadocs)
.env.example                   # Canonical template, commit but never put real secrets

Core flow

  1. Entrypoint (gateway/cli.ts or gateway/server.ts) imports config/load-env.ts as its first line to populate process.env, then calls createApp() from app.ts.

  2. createApp() runs the wiring builders under app/ in order, which instantiate:

    • ToolRegistry: all tools, permission-tiered.
    • SkillRegistry: all builtin plus external domain skills, gated by requires_env.
    • The turn loop (agent/runtime.ts): orchestrates skill activation, tool calls, prompt assembly, and state persistence.
    • SQLite state (WAL + FTS5) for memory, sessions, audit log.
  3. Each turn, the runtime:

    • Pulls active skills from SkillRegistry (auto-activated or user-pinned).
    • Builds the system prompt via prompt-builder.ts, combining the skill catalog and prompt fragments.
    • Calls the LLM via streamText with the set of tools allowed by currently active skills and the current permission tier policy.
    • Routes tool calls through the permission hook pipeline (kill switch, daily spend cap, tier gating).
    • Persists the turn to SQLite (history plus audit log).

Why not MCP

MCP adds an extra process boundary, schema-transformation layer, and separate permission model. For an agent where we control every tool, the in-house registry:

  • Gives us typed tool handlers with direct access to shared state (SQLite, Minara client, sandbox).
  • Lets us enforce permission tiers from a single BeforeToolCallHook.
  • Keeps the audit log exact (tool name, args, reasoning, outcome, no cross-process translation).
  • Avoids serialization overhead on every call.

MCP is still supported for third-party servers. See MCP_SERVERS in Environment Variables.

On this page