Skip to content

Architecture

Status: the ownership, layers and .maia scopes below are binding. The operation registry and maia help --json record what is implemented today; the roadmap records what is planned. Choices and their reasons are in the decision log.

Ownership

Maia is the durable control plane and product harness. The official Pi SDK supplies the agent loop, model, tool, provider and session primitives (decision).

Maia ownsPi SDK provides
Workspaces, Projects, execution environmentsmodel and provider integration
Sessions, activations, attempts and workersagent loop and model response streams
durable Session and turn identitiesconversation-session mechanics
operation schemas and authorizationtools, Skills, prompts and extensions
event order, receipts and recoverysteering, follow-up and compaction
A2A mapping and external identitymodel-facing context construction

The Maia worker runs outside maia-node and embeds the Pi SDK behind a narrow adapter. The Node uses only explicit Maia-owned state and never treats Pi as containment.

Product vocabulary

Workspace is reserved for the organizational scope in Maia → Workspace → Project. Execution resources are not organizational layers.

ConceptMeaning
WorkspaceGroups Projects; owns shared knowledge and coordinator context
ProjectOwns focused durable work, knowledge and workers; a repository is optional
Execution EnvironmentProject-owned checkout, Git worktree or admitted private working directory
Execution NodeComputer or server hosting execution capacity
Execution ProfilePermission and containment policy for execution

Do not use Container or Execution Workspace as new product labels. Today's container.* (organization) and workspace.* (execution) APIs keep their meaning until the versioned naming migration (R1b) moves them to workspace.* and environment.*. IDs, history and receipts are preserved.

Layers

  1. packages/protocol is the source of truth for operation IDs, Effect schemas, errors, events, risk and receipts.
  2. packages/sdk provides typed clients for the same registry.
  3. apps/cli is the first complete client and the reference JSON surface.
  4. services/node owns durable state, transports, ordering, leases and projections.
  5. packages/agent implements the Maia agent on official Pi SDK primitives.
  6. Voice, the Tauri-hosted Expo Desktop app, Expo iOS/Android, the presentation-only site and remote workers are replaceable clients or capacity (decision).

Maia Node remains TypeScript/Effect. Rust is limited to the terminal screen engine/client and the native process/PTY host; Desktop retains its Tauri shell (D-182). Native hosts are bounded adapters, and the terminal client uses the registry-generated binding. Authorization, durable state, orchestration and database migrations stay in the Node.

The external-brain flow today (Claude Code or Codex drives workers through the maia plugin):

  Claude Code or Codex (brain)            a person
  maia plugin: Skill + MCP tools          terminal or Desktop app
         |                                       |
     maia mcp           maia CLI         Desktop (Tauri) and Expo client
         +------------------+--------------------+
                       @maiaos/sdk   (operations from @maiaos/protocol)
                            |  authenticated loopback HTTP
                  Maia Node: one per machine and profile
                  validation, state, events, receipts, accounts
                            |  native process host
             +--------------+--------------+
     Claude Code worker             Codex worker
     (Agent SDK)                    (codex app-server)
     one Git worktree and one named account per Session

The Node alone validates, authorizes, stores, executes, orders events and writes receipts. A brain combines create, start, send, wait, observe, interrupt, stop and resume in Skills; Maia adds no hidden planner or agent loop.

Contracts are normative for cross-domain schemas and evolution; operations is normative for deployment, durability, identity, observability, retention and recovery. Domain documents may narrow these contracts but never redefine them.

A business mutation never originates in React, Tauri, Expo, a voice prompt, an A2A handler or a Pi extension; those surfaces submit a protocol operation. Consequential operations follow the mutation rules.

There is no standalone web control application. apps/client is the shared Expo renderer; its web export is local content in apps/desktop and a test artifact. apps/site is documentation and marketing only.

Shared Maia configuration

Maia has two .maia scopes. Neither belongs to one component.

ScopeLocationPurposeVersioned
user/machine~/.maia/machine defaults, Project inventory, reusable Agents and Routines, safe referencesno
Project<project-root>/.maia/portable versioned intent for one Projectyes

Today no Node code resolves config.jsonc or profiles/<name>.jsonc: the unwired resolver and writer were removed in F7, and S13 owns the configurator that replaces them. The Node keeps only the filesystem helpers in services/node/src/configuration. The agent reads its resources from .maia/maia.json (see agent resources). The layout below is the target; there are no competing per-component configs such as .maia/agent, .maia/node or .maia/swarm. The ~/.maia home itself (installs, profiles, runtime files) is specified in release (decision).

User-level Swarm inventory

~/.maia/
  config.jsonc          machine-local defaults (Maia coordinator Agent, capacity, timezone, Connection refs)
  projects.jsonc        Project inventory: stable Project ID -> exact root on this machine
  agents/<agent-id>/agent.jsonc
  routines/<routine-id>/routine.jsonc

Skills are not in .maia: the skills CLI installs them where each harness reads them and pins them in its lock files (Skills, decision).

  • The inventory accepts absolute canonical paths only: no globs, environment interpolation, home expansion or duplicate active roots. Repository entries also record the normalized Git remote and object format; no path string alone changes Project identity.
  • It holds no credentials, leases, health, Session state or admission results. The file is owner-only; locator paths never enter Agent context, provider payloads or ordinary trace exports.
  • Both files are candidates, not hot-applied truth. Validation produces a resolved snapshot and diagnostics; admission applies one exact digest with revision compare-and-set and a receipt. Path, capacity, Maia coordinator, authority, Connection or provider changes need preview and confirmation. Missing entries never delete Projects. Relocation is blocked while a Project owns live assignments or unresolved effects. A restart reloads the last admitted snapshot.

Project-local versioned contract

AGENTS.md
skills-lock.json
.agents/skills/<skill-id>/SKILL.md   (and .claude/skills/<skill-id>/SKILL.md)
.maia/
  config.jsonc
  agents/<agent-id>/agent.jsonc, EVAL.md
  routines/<routine-id>/routine.jsonc, EVAL.md
InputConsumerContract
.maia/config.jsoncProtocol, Swarm, Node, Agent, ProvidersProject identity, defaults, limits, context, maintenance, allowed Connection refs
.maia/agents/*/agent.jsoncSwarm and AgentAgent definitions, tool grants, Skills, peer policy, limits, home-Session intent
skills-lock.json, Skill foldersevery harness and the Agentstandard operating procedures, pinned by folder hash (Skills)
.maia/routines/*/routine.jsoncSwarmmanual or cron start-or-wake intent and target Agent
Agent/Routine EVAL.mdNode evaluationevaluator-only rubric, never Agent context
root AGENTS.mdAgent context compilerminimal repository instructions, never authority or configuration
repository .maia candidate
  -> protocol parse and deterministic validation
  -> Swarm admission decision
  -> Node immutable admitted snapshot
  -> bounded Agent, Routine, evaluation and Provider views

packages/protocol owns the schemas for both scopes. No component rereads an ambient "latest" file during a managed activation; a source change creates a new candidate snapshot. Runtime state, tasks, goals, events, receipts, traces, transcripts, credentials, leases and generated artifacts never belong in .maia/. An optional ignored .maia/state/exports/ holds explicit redacted exports and is never read as authority. The prompt contract is one root AGENTS.md plus progressively loaded Skills, with no rules/ tree (decision).

Agent resources

The implemented maia agent keeps provider credentials in ~/.maia/auth.json, the default model in ~/.maia/config.toml and Pi's agent directory in ~/.maia/agent (MAIA_HOME moves all three; MAIA_AGENT_HOME moves only the agent directory). Pi resource discovery is off; Project extensions and Skills come only from digest-pinned paths in .maia/maia.json, and an extension loading error stops startup. The details are in the Agent architecture. The target replaces .maia/maia.json with the shared scopes above.

Agents, Swarm and coordinators

  • One Maia Agent kernel serves local use and every managed Session. Shared code is not a shared brain: Maia, Workspace, Project and worker Sessions each have their own principal, history, grants, credentials, budgets and resources (coordinator ownership, decision).
  • Maia Swarm is not another harness. It decides Project, Routine, Session-tree, queue, budget, wake-up and placement policy; the Node owns and enforces the durable rows, revisions, events, leases and receipts. Fleet, Workflow, Task-graph, Bot and Subagent are not public runtimes or CLI roots; task is a Session-local planning tool, and durable nested tasks (S53) are run ledger tasks (D-119).
  • Vendor executors (Codex CLI, Claude Code) are the v1 workers (vendor executors). An external brain drives Maia through the public CLI, MCP and Skills (coordination). Maia is the coordination hub where any agent or harness delegates to another through a Node (decision).
  • Worker accounts are named Connections; model providers are per-provider credentials in ~/.maia/auth.json; one maia login covers both (providers, decision). Voice is optional input and output for the exact selected Session (decision).
  • Semantic evaluation (Choice, Score, Boolean) is one planned Node service (R10a); coordinators use LLM + Jev, workers stay LLM-only (routing). Required AI failure blocks only the affected automation; manual controls keep working.
  • The terminal UI is a replaceable debugging client: local maia agent uses Pi InteractiveMode; planned maia session attach (S12) renders the Node Session timeline (decision).

Authority and state

Project intent lives in <project-root>/.maia; machine inventory and reusable user configuration live in ~/.maia. Authoritative runtime data (tasks, goals, events, receipts, traces, evaluations) lives in Node state under exact Project and Session IDs. Pi JSONL files and Pi Session IDs are never authoritative.

"All data possible" means all policy-permitted structured evidence, never secrets or indiscriminate content capture. Raw audio and private content stay excluded by default. Every authoritative transition has durable correlation evidence and a live Node projection; secrets, hidden reasoning, raw audio and forbidden content stay unobservable. Clients, Swarm, evaluators and exporters read shared Node projections and cursors, never worker-owned streams (decision).

Networking and process boundaries

  • The Node binds to loopback by default; tailnet reachability never replaces authentication, and Funnel is off (decision).
  • @maiaos/agent builds one official-Pi worker process with a bounded, versioned NDJSON protocol, explicit resource loading, in-memory Pi state, idempotent requests and interruption acknowledgement.
  • Node Sessions run two kinds of runtime: Pi (the official Pi RPC mode on macOS, that managed worker under hard containment on Linux and Windows), and vendor executor Sessions (claude-code, codex-cli) bound to one named account Connection. When the caller names only the executor kind, the Node picks the least-used account (decision). Other legacy Session runtimes fail closed.
  • Workers run under the Node's process manager and containment tiers (process tiers, Linux host).

Current architecture (audit, 2026-10-31)

Evidence: a read-only import-graph and grep audit of main at commit 612c8ba2 (F0). Counts come from regex parsing of static import, export from and import() specifiers in production TypeScript/TSX under workspace src/ (tests and testing/ folders excluded). Edge kinds are named with each count: type-only, static-value and lazy (import()).

  • Client layering holds. apps/cli, apps/client and packages/client-core import only @maiaos/sdk, client-core and (CLI only) @maiaos/agent; none imports @maiaos/protocol or the Node, and none calls fetch, WebSocket or HttpClient directly. Tauri invoke stays in apps/client/src/platform/desktop. The Desktop Rust bridge keeps an operation allowlist that scripts/quality/registry-parity.mjs checks against the registry. Package edges are acyclic:
cli -> sdk, client-core, agent     client -> client-core, sdk
client-core -> sdk                 sdk -> protocol
node -> protocol, agent            (sdk: a devDependency, used by the bridge)
  • The Node areas are tangled. 31 of the 41 top-level areas of services/node/src form one strongly connected component (SCC), counting value edges. storage and the domain areas depend on each other: 24 mutual value dependencies, 26 with type edges. runtime imports harness, session and storage. File level, the Node has one 11-file static-value SCC (connection/harness). A separate 178-file SCC, which includes type edges, hangs on storage/database/node-database.ts and the type-only edge integration-authorization-operations.ts to NodeDatabaseResource; removing that edge leaves 177 files.
  • CLI, client and SDK cycles. The CLI has no static-value cycle: its 4-file anchor passes through a type-only import, and its 57-file component exists only with lazy value edges. The SDK has 24 cyclic components: 21 Effect/facade pairs, one voice triple and two other pairs; F1 classifies their kinds, and node-changes is the one named value cycle. The client has one component whose kinds F1 classifies. Report type, static-value and lazy edges separately.
  • Node reaches into the agent root. provider/pool-store.ts and command-line/release-command.ts import home resolution and credential parsers from @maiaos/agent, a wider coupling than the worker protocol adapter.
  • Repeated primitives. Prefixed UUID helpers (newId(prefix) and named variants, about a dozen copies) and at least 12 immediate-transaction helpers (examples: agents/preview.ts, hold/preview.ts, routine/preview.ts, run-ledger/journal.ts, voice/storage.ts, terminal/storage/live/*, session/storage/record/transition.ts, orchestration/storage.ts:1827, provider/lease-authority.ts:227, integration-authorization replay-store.ts:74, session-credential/store.ts:43). Native-host spawns are documented exceptions.
  • Unsafe SQL boundary. 14 as unknown as row assertions on SQLite results (for example terminal/storage/live/controller-policy.ts:119) in place of schema decodes.
  • Unwired configuration. services/node/src/configuration/resolver.ts has no production importer; only the filesystem helpers are used. (Removed in F7a, 31 October 2026, with its writer, parser and jsonc-parser; the last commit that held them is named in that pull request.)
  • Size. 169 production TypeScript/TSX files under workspace src/ exceed 400 lines (116 in the Node); the largest, node-database.ts, has 2,273. Native code is outside that count: the Desktop Rust bridge is about 3.8k lines and the Linux process host C about 3.4k.
  • No cycle rule. Nothing enforces the "keep dependencies acyclic" rule of source organization.

Target architecture

  • Every primitive has one owner module with a small documented API. Telemetry, errors, transport and extension admission already have owners; their rules and tests get stronger. No new logging, result, RPC or retry framework.
  • Dependency direction is enforced by a graph ratchet in CI that baselines exact edges and membership by kind, so an equal-sized swap fails.
  • Package dependencies and adapter ownership are checked against declared package.json entries and allowed subpaths.
  • Storage contracts belong to their domain, not to NodeDatabaseResource.
  • Source files trend under about 400 lines.
  • One event core (D-188) is the only way to produce or consume an event.

Decision: D-189.

Foundations sequence

Each PR is independently green, runs pnpm check:changed plus its boundary tests, and keeps the baseline shrinking. Acceptance for every PR also preserves telemetry redaction, bounds and flushing; error-to-wire mapping; transport authentication, deadlines and cancellation; and static registry and extension admission. The event core keeps its own order, EV0a → EV0b → {EV0c, EV0d} → EV0e (slices).

#Bounded changeEnforcing gateEvidence
F1Dependency graph ratchetExact edges inside recorded cycles and membership by kind; new acyclic edges are free; F2 owns package edgesPackage-exports resolution, barrel, self-import and equal-size-swap tests
F2Package and adapter ownershipDeclared dependencies and allowed subpaths, bundled SDK use recognizedRejection fixtures; built-package and bridge behavior
F3SDK node-changes cycleF1 baseline shrinksStream cancellation and public declarations
F4Node connection/harness cycle (11 files)F1 baseline shrinksConnection and launch failure and cleanup tests
F5One unsafe SQL boundary (controller-policy first)Baseline the existing SQL assertions as a shrinking exception list; ban new onesMalformed-row and error-mapping tests
F6Immediate-transaction consolidation, in batchesRatchet transaction entry sitesCommit and rollback failures; time sampled after lock acquisition
F7Unwired configuration cleanupProduction-entry reachability checkUsed filesystem helpers kept; provider behavior; built test paths
F8Storage contract ownershipF1 baseline shrinksIntegration-authorization contract out of NodeDatabaseResource; authorization and idempotency tests
F9Client cycles; CLI lazy dependencies apartScope-entry rules; F1 baselineClient startup; CLI help and import cost
F10Node-local ID consolidationRatchet ambient randomUUID callsPrefixes and entropy kept; deterministic tests

Imported from Maia OS docs at 3dc3e07