melch

Run one syndicate file across every channel

The configuration file defines the complete agent team. Channels act as thin clients, passing incoming messages directly to the runtime and delivering responses back to the conversation.

An embedded Node dependency

When you install melchizedek-agents, the framework runs directly inside your application. You keep syndicate files alongside your application code in config/agents/. The built-in loader parses these definitions, and the model registry directs each agent turn to its configured provider using the API keys present in your local environment.

The package targets ESM on Node 22.12 or newer. Because @google/adk is a peer dependency, your application controls the shared model registry. You can set MELCHIZEDEK_AGENTS_DIR to direct the file loader to an alternate path.

npm install melchizedek-agents @google/adk

import { LlmAgent, Runner, InMemorySessionService } from '@google/adk';
import { loadSyndicate, registerAvailableProviders } from 'melchizedek-agents';

registerAvailableProviders();                      // every provider whose key is present
const config = loadSyndicate('tutor.yaml');        // <your-repo>/config/agents/, examples/ as fallback
const tutor = new LlmAgent({ name: config.orchestrator.name, model: config.orchestrator.model, instruction: config.orchestrator.instruction });
const runner = new Runner({ agent: tutor, appName: 'my-app', sessionService: new InMemorySessionService() });
await runner.sessionService.createSession({ appName: 'my-app', userId: 'u1', sessionId: 's1', state: {} });
const ask = { userId: 'u1', sessionId: 's1', newMessage: { role: 'user', parts: [{ text: 'In one sentence, what does an orchestrator do in a team of agents?' }] } };
for await (const ev of runner.runAsync(ask)) for (const p of ev.content?.parts ?? []) if (p.text) console.log(p.text);

We ran the example above against the engine on 2026-09-24; Tutor on ollama/qwen3:8b answered in 15.4 seconds, and the unedited reply appears below.

Tutor (ollama/qwen3:8b, 15.4 s) › An orchestrator in a team of agents coordinates their tasks to ensure efficient collaboration towards shared goals. What specific role do you think the orchestrator plays in resolving conflicts between agents?

HTTP services over A2A

The A2A server exposes any syndicate as a JSON-RPC endpoint speaking the Agent-to-Agent protocol. Because the runtime compiles the agent graph fresh for every incoming request, the server remains stateless and multi-tenant. External callers can supply their own model credentials directly through an X-API-Key request header.

An agent card at /.well-known/agent-card.json publishes endpoint capabilities, while A2A_SERVER_SECRET enforces bearer authentication. Every agent receives a dedicated route. You can query endpoints with a standard fetch call, and a zero-dependency demo client ships in the repository.

npx melchizedek-serve                      # serves config/agents/; each team at /<name>/a2a/…

curl -X POST http://localhost:4000/delegation/a2a/rest/v1/message:send \
  -H "Authorization: Bearer $A2A_SERVER_SECRET" \
  -H "X-API-Key: $GOOGLE_GENAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"message":{"messageId":"1","role":"ROLE_USER","content":[{"text":"Why does JavaScript sort put 10 before 9?"}]},"configuration":{"blocking":true}}'

On 2026-09-25, a locally served public starter pack with in-memory sessions returned HTTP 201 in 8.2 seconds with TASK_STATE_COMPLETED; this payload trims the answer text, and the full JSON is committed beside this page.

{
  "task": {
    "id": "c047772a-ec9d-42c1-ac7c-76c94eed60bf",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "message": {
        "role": "ROLE_AGENT",
        "content": [
          {
            "text": "### Why This Happens\n\nBy default, JavaScript’s `Array.prototype.sort()` converts elements into **strings** and sorts them **lexicographically** (alphabetical order based on UTF-16 code units) rather than numerically.\n\nWhen comparing `10` and `9`:\n1. They are c…"
          }
        ]
      }
    }
  }
}

MCP in both directions

A subagent that specifies an mcp_server_url property discovers remote tools over Server-Sent Events during startup. An internal network guard rejects connections to private addresses before the handshake completes. The syndicate file lists zero tools locally; the subagent receives its executable capabilities dynamically at runtime.

The framework also exposes its internal tool contracts through dedicated MCP servers. This design allows external clients such as Claude Code to inspect knowledge bundles or invoke custom application tools across process boundaries.

# in: a subagent discovers a server's tools at runtime
subagents:
  - name: "Librarian"
    model: "gemini-3.8-flash"
    mcp_server_url: "http://localhost:8931/sse"

# out: the framework serves tools; any MCP client registers the URL
npm run mcp:demo
claude mcp add --transport sse lyceum-library http://localhost:8931/sse

On 2026-09-24, an MCP client connected to the package’s demo scroll catalog for the Lyceum Librarian lesson, listed its four tools (search_catalog, read_scroll, borrow_scroll, annotate_scroll), and called search_catalog with query 'memory', as shown below.

tools/list → search_catalog, read_scroll, borrow_scroll, annotate_scroll
tools/call  search_catalog {"query":"memory"}
          → "scroll-003 · \"Records of the Hearth\" — anonymous [borrowed]"

The terminal REPL

The melchizedek-chat command provides an interactive multi-turn REPL for any syndicate. When you close an active session, the distillation pipeline extracts verified facts from the transcript, recording explicit dates, entities, and sources whenever you configure long-term Supabase storage.

You can bind template variables declared in the syndicate file directly from command-line flags. For automated pipelines, single-shot mode evaluates a single prompt and terminates immediately.

npx melchizedek-chat --syndicate tutor
npx melchizedek-chat --syndicate syndicate -- "What are the top AI stories today?"

Discord bots

Two operational bots connect to the HTTP surface today. The first runs as a financial analyst desk that answers community questions regarding portfolio holdings and broadcasts a daily advisory directive every morning. The second, Augustin, operates as a world-events arbiter that responds exclusively when mentioned, drawing conclusions solely from its own primary research.

The Discord bot forwards user messages to the A2A endpoint and posts the returning text. The syndicate file retains complete authority over agent behavior and tool execution.

# augustin_bot.py: an @-mention becomes one A2A call; the reply footer names who was consulted
async def generate_augustin_reply(query, context_id=None, status_msg=None, trace=None, surface=None):
    current_date_str = datetime.now(nyc_tz).strftime("%B %d, %Y")
    grounded_query = compose_augustin_query(query, current_date_str)
    return await a2a_chat(AGENT_ID, grounded_query, context_id=context_id,
                          status_cb=trace.wrap(_status_cb(status_msg)), poll_attempts=150, surface=surface)

@bot.event
async def on_message(message):
    if message.author.bot or bot.user not in message.mentions:
        return
    query = message.content.replace(f"<@{bot.user.id}>", "").strip()
    reply = await generate_augustin_reply(query, context_id=await resolve_augustin_context(message.channel.id))
    await send_long_response(message, reply, footer=trace.footer())

iMessage

This integration is not shipped. An upcoming messages relay will attach to the existing HTTP surface, mirroring the architecture of the Discord adapters. We will document the channel here once it becomes operational.