Skip to content

Repository files navigation

Malkuth

한국어 | English

A modular multi-agent orchestration framework built on LangGraph.

Malkuth composes each goal as a graph of equal, Docker-isolated agents. Agents carry their own A2A endpoints and MCP servers, connect and disconnect through config-driven wiring, and share nothing except the graph state and declared, scoped memory.

Key Features

  • LangGraph orchestration — graph topology declared in YAML, built into a StateGraph with checkpointing, resume, and conditional routing
  • One agent = one Docker container — controlled through a standard Agent Control API, with resource limits, health checks, and graceful drain
  • Per-agent protocol isolation — every A2A endpoint and MCP server belongs to exactly one agent; no shared tool gateways
  • Equal peers, three access paths — orchestrated runs, interactive direct requests to any agent, and declared A2A peer calls; no hierarchy between agents
  • Two execution modes — goal-oriented mission graphs that run to completion, and perpetual service graphs that iterate indefinitely with idle backoff
  • Everything is a module — skillsets, promptsets, memorysets, and graphs are versioned, swappable deliverables; a solution is assembled, not written from scratch
  • Scoped resources — secrets, memory, artifacts, and quotas managed at three scopes: global / group / local, resolved nearest-first
  • Context memory — per-scope memory spaces with hybrid (vector + lexical) search indexes, token-budgeted recall, and declared retention
  • Assembled from a browser — one control plane serves a REST API and a Web UI for composing graphs, deploying them into containers, and running them (API, UI)

Status

Pre-v0.1.0. The framework layers described above are implemented and covered by the test suite, including the control plane REST API and the Web UI it serves. The remote module registry and the Kubernetes runtime backend remain future work.

Requirements

Python 3.12+ · uv · Docker Engine 24+ (for the runtime, integration, and E2E paths)

uv sync --frozen        # or: make install

Commands

Quality gates

make lint               # ruff check + ruff format --check
make typecheck          # mypy (strict on malkuth.core)
make test               # unit tests + coverage gate (>= 70%)
make check              # lint + typecheck + test — run this before pushing

Integration and E2E suites are opt-in because they need Docker:

make test-integration   # marked `integration` — Docker containers, real MCP sessions
make test-e2e           # marked `e2e` — full compose stack (nightly in CI)

Checkpointer integration tests skip unless their backends are addressable:

export MALKUTH_TEST_POSTGRES_URL=postgresql://malkuth:malkuth@127.0.0.1:15432/malkuth
export MALKUTH_TEST_REDIS_URL=redis://127.0.0.1:16379    # needs RediSearch (redis-stack)

Images and stacks

make build              # malkuth/agent-base + agent-echo images
make up                 # dev stack — one echo agent, control port on 18080
make down
make e2e-up             # E2E stack — fake provider, memory service, 4 reference agents
make e2e-down

Both up targets rebuild malkuth/agent-base and the services on top of it first, so a change under src/ reaches the containers. The framework code lives only in the base image — bringing a stack up without rebuilding it would validate stale code.

The E2E stack publishes agent control ports on 18081-18084, the Memory Service on 18090, the checkpoint Postgres on 15433, agent metrics on 19082-19084, and A2A ports on 19102-19104. Its agents talk to a fake model provider, so nothing reaches a real LLM.

Custom agents are not built by make. Their Dockerfile and src/ live in the control plane's material store, and a deploy refuses an agent whose image for that version is not built. The repository ships the claude-code agent's materials as a seed:

uv run malkuth agent-push claude-code examples/materials/claude-code
uv run malkuth agent-build claude-code --wait   # exits non-zero with the log tail if it fails

CLI

uv run malkuth <command>          # or `malkuth` inside an activated venv
Command What it does
malkuth validate validate every graph, agent manifest, and module ref in the repo
malkuth deploy <graph.yaml> validate one graph as a deploy gate (--a2a-port-range)
malkuth status summarize declared agents, graphs, groups, and modules
malkuth config [env] print the resolved configuration (dev / staging / prod)
malkuth check <state.yaml> report integrity discrepancies against observed state
malkuth run <graph.yaml> submit a mission or service run against agents you address yourself
malkuth run --deployment <id> submit a run to a deployment — the control plane resolves the addresses
malkuth run-list / run-drain <id> / run-resume <id> operate runs through a control plane

--json (before the subcommand) switches to machine-readable output; --root points at a repository other than the working directory.

Running a graph needs the address of each agent's Control API — the orchestrator never guesses where an agent lives. Against the E2E stack:

export MALKUTH_AGENT_TOKEN=e2e-token

# mission run — terminates at END and prints the final state
uv run malkuth run graphs/research-pipeline.yaml \
  --input '{"query": "malkuth architecture"}' \
  --agent planner=http://127.0.0.1:18082 \
  --agent researcher=http://127.0.0.1:18083 \
  --agent writer=http://127.0.0.1:18084

# service run — perpetual loop, bounded here so it terminates
uv run malkuth run graphs/feed-monitor.yaml --service --iterations 2 \
  --agent researcher=http://127.0.0.1:18083 \
  --agent planner=http://127.0.0.1:18082 \
  --agent writer=http://127.0.0.1:18084

A service run without --iterations runs until interrupted; Ctrl-C requests a drain, so it stops after finishing the current iteration rather than mid-flight.

The checkpoint backend comes from configuration — --checkpointer only overrides it for one run. postgres and redis additionally need a connection URL, which is best supplied out of band so credentials never land in a file:

MALKUTH_ENV=prod \
MALKUTH_ORCHESTRATOR__CHECKPOINTER_URL=postgresql://user:pass@host:5432/malkuth \
  uv run malkuth run graphs/research-pipeline.yaml --agent ...

With a durable backend, re-running the same --run-id resumes that run from its last checkpoint — including from a different process. The default memory backend disappears with the process, so runs on it cannot be resumed.

run-list / run-drain / run-resume talk to a control plane over --control-url, and they only see runs that were recorded — set orchestrator.run_store so the run and the control plane share one store. run-drain leaves a request and returns immediately; the process driving the run stops at its next iteration boundary.

--deployment is the other way to run a graph, and it needs no --agent flags at all: the control plane already knows where the deployed agents are.

uv run malkuth run --deployment dep-2fc94a0b5f24 \
  --input '{"query": "malkuth architecture"}' \
  --control-url http://127.0.0.1:8700

It returns once the run finishes (--no-wait to return at submission). run-resume continues a run that a service graph halted after repeated failures; a run you drained on purpose is submitted again rather than resumed. A control plane without a deployment surface drives no runs at all and answers run-resume with 501 instead of reporting a resume that never happened. See the Control Plane API for the full surface.

Long-running processes

python -m malkuth.agentd        # in-container agent daemon — Control API on 8080
python -m malkuth.memory        # Memory Service — HTTP surface plus the async indexing loop
python -m malkuth.orchestrator  # Control Plane — REST API + Web UI on /ui

agentd is what the runtime layer starts inside every agent container; it reads MALKUTH_MANIFEST, MALKUTH_AGENT_TOKEN, MALKUTH_ROOT, and — when memory is wired — MALKUTH_MEMORY_URL with MALKUTH_MEMORY_TOKEN or MALKUTH_MEMORY_TOKEN_FILE.

The Memory Service reads MALKUTH_REPO_ROOT, MALKUTH_MEMORY_PORT, and MALKUTH_MEMORY_TOKENS_PATH. It must run as its own process: appends commit immediately but indexing is asynchronous, so without the loop nothing becomes searchable.

The Control Plane reads orchestrator.run_store, control_host, and control_port from configuration and refuses to start without a store — serving an empty list would read as "there are no runs". Set orchestrator.control_token and send it as a bearer token; every /v1/* route requires it, and binding a non-loopback address without one is refused (CFG_001). GET /v1/health and the UI's static files stay unauthenticated.

Setting orchestrator.deployment_store additionally opens the deployment surface: the process then starts agent containers itself, drives runs submitted against them, and can resume a halted one. Without it those routes stay closed and the process only reports runs that other processes drive.

Setting orchestrator.material_store lets the process store custom agents' build materials, and setting orchestrator.build_store as well opens image builds. With both set, the process bakes images on request, and a deploy refuses any custom agent whose image for its version is not built. Without a material store the material routes answer CFG_001; without both stores the image routes do not exist, and deploys use the image each manifest names.

Open http://127.0.0.1:8700/ for the Web UI — catalog, graph and agent editors, deployment, and runs. It is served from the same process and calls only the documented REST API (API reference, UI guide).

All three honour MALKUTH_ENV, MALKUTH_CONFIG_DIR, MALKUTH_LOG_LEVEL, MALKUTH_LOG_FORMAT, and MALKUTH_METRICS_PORT.

Configuration

Configuration lives in configs/{env}.yaml (dev, staging, prod). The CLI selects it positionally; the long-running processes read MALKUTH_ENV:

uv run malkuth config prod            # print the resolved prod configuration

Overrides use a double underscore to separate section from key:

MALKUTH_ORCHESTRATOR__NODE_TIMEOUT_S=600 uv run malkuth config

Single-underscore MALKUTH_* variables are process settings (MALKUTH_AGENT_TOKEN and the like), never configuration — the loader ignores them on purpose, so that injecting agent env into a container cannot corrupt that container's configuration.

For a walkthrough that assembles a solution from scratch, see Getting Started.

Documentation

Document Description
Architecture Layers, interaction model, execution modes, resource scoping
Getting Started Environment setup and first solution
Module System Skillsets, promptsets, memorysets, graphs, groups
Control Plane API REST reference — catalog, authoring, deployments, runs
Web UI Assembling, deploying, and running a system in a browser
Testing Test strategy and quality gates
CI Conventions Merge gates, workflow design rules
Required Status Checks Single source of truth for check names

Development rules (the authoritative ruleset) live in .claude/rules/.

Technology Stack

Python 3.12+ · uv · LangGraph · pydantic v2 · Docker · a2a-sdk · mcp · FastAPI · structlog · Prometheus · pytest

Conventions (Summary)

  • Commit messages: [FEAT]: ... / [FIX]: ... / [REFAC]: ... / [DOCS]: ... / [CHORE]: ... — written in Korean
  • PR titles: [FEAT#N] title (category + issue number)
  • Branches: {category}/#{issue}/{summary}
  • Documentation: English first (docs/en/), Korean mirror (docs/ko/)

See .claude/rules/07-code-style.md and .claude/rules/08-workflow.md for the full conventions.

About

Multi-agent Framework

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages