Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PACL — Proactive Agent Coordination Layer

Keep a team of independent AI agents coherent. They report what they're working on over MCP, find out when they collide, and settle it between themselves — across sessions, providers, machines, and people.

Python MCP Gemini License: AGPL-3.0

Three agent CLIs from three vendors coordinating through PACL

Three terminals, three different products from three different vendors, no shared session between them, all pointed at one PACL. dev-alice takes the checkout refactor. dev-bob goes for the same file and is told before he touches it, then messages her directly and hands it back. dev-carol, told nothing by anyone, asks what the team is doing and describes it correctly.

Then the part worth watching: alice moves on to src/refunds.py, and bob's message arrives on the response to that. She never asked. (higher-quality mp4 — the editing is cutting the pauses while models think, and a camera move to whichever terminal is talking, because three panes of terminal text are unreadable at this size otherwise. Nothing inside the terminals is altered.)

Quickstart

uv sync --extra dev
cp .env.example .env      # add a Gemini AI Studio key
uv run pacl               # MCP server on http://127.0.0.1:8080/mcp

Point any MCP client at it:

{
  "mcpServers": {
    "pacl": {
      "url": "http://localhost:8080/mcp",
      "headers": { "X-PACL-Agent": "your-agent-id" }
    }
  }
}

Two agents on the same PACL now hear about each other on their next tool call.

Set the header. Identity comes from X-PACL-Agent, which is the only source that is both stable across reconnects and outside the model's control — nothing carried in request _meta is honoured, because the caller supplies it and could otherwise claim someone else's identity. Without the header an agent still gets a distinct id from its transport session, but a reconnect makes it a new agent.

The five tools

Tool When to call it
update_intent(intent, domain) the user states a new goal or changes direction
share_context(content) something substantive was discussed, decided, or found
report_activity(action, target) you're about to act on a file or resource
query(question) you want to know what the rest of the team is doing
send_to_agent(to_agent, message) you want to say something to another agent directly

Every call returns an alerts list. When it's non-empty, those are messages for you — from PACL, or from another agent. There is no push channel and no inbox to check: coordination rides on the next tool response you were making anyway.

That's a design constraint, not a shortcut. PACL doesn't depend on server-initiated push, because client support for it is inconsistent — the response you were already getting is the one channel every client honours.

sequenceDiagram
    participant A as dev-alice
    participant P as PACL
    participant B as dev-bob
    A->>P: update_intent('refactor the checkout payment flow', ['src/checkout.py'])
    B->>P: report_activity('edit', 'src/checkout.py')
    P-->>B: alerts: scope collision — dev-alice declared src/checkout.py
    Note over P: both sides are told, not just the newcomer
    Note over B: what to do about it is bob's call
    B->>P: send_to_agent('dev-alice', "I'll take the tests, it's yours")
    A->>P: query('anything for me?')
    P-->>A: alerts: scope collision — dev-bob just took src/checkout.py<br/>[from dev-bob] I'll take the tests, it's yours
    Note over P: sees the exchange, stays silent
Loading

Emergence is the design

Coordination lives in a prompt, not in code. There is no rule engine, no policy table, no if overlap: warn(), and no branch per coordination pattern — prompts.py is the whole policy, and you can read it in a minute.

What that prompt actually contains: one objective (keep the team coherent), four example situations marked explicitly as illustrations rather than a checklist, an instruction to generalise beyond them, and a bias toward silence. So overlap detection, escalation routing and context handoff are named as examples — what isn't specified is how to recognise any of them, when they apply, or what to do about each one.

Agents get the same treatment. PACL tells them a collision exists, never how to divide the work. They have send_to_agent and each other, and the resolution is theirs.

The honest test of whether that generalises is the eval suite below, which contains cases the prompt never anticipates — two agents doing identical work under completely different words, adjacent work that must not be flagged, a bystander who must be left out of someone else's collision. Those pass without a line of code per case.

The cost: coordination is only as good as the model's judgement, and nothing underneath guarantees a correct call. The failures are in the table below rather than hidden.

The one deterministic thing, and why it isn't a contradiction

Literal scope collisions — two agents naming the same file — are caught by a sensor, not a coordinator. Set intersection over the live scope map, delivered on the same tool response. Measured over loopback: ~1.3ms for a full collision-bearing report_activity round trip, of which the detection itself is ~0.003ms.

Scope collision: dev-alice already declared src/checkout.py (intent: "refactoring the checkout payment flow", since …).

That is the whole message. It states what is true and stops. A test asserts it never grows directive language.

It exists for latency, not judgement. The model path runs several sequential calls and lands in the tens of seconds, and "told before it touches anything" isn't true at that speed for an agent working quickly. Two rules keep it from eroding the design:

  1. It never enters the intermediary's prompt. The model still gets no precomputed overlap and must notice it by reasoning, which is the only way the semantic cases get caught — two agents doing the same work under completely different words. The sensor is blind to those.
  2. It never decides anything. The moment it says what an agent should do, coordination stops being emergent and becomes a lookup table with a model bolted on the side.

Does it actually work?

Emergent behaviour is easy to claim and easy to fake with one good demo, so it's measured. 38 scenarios, 152 live runs against real models, fresh substrate per run, every run graded by one ruleset. 140 of 152 pass. The suite drives the intermediary directly rather than over MCP — it grades the coordination reasoning, not the transport. The transport is what the video exercises.

92% overall across 152 live runs, 99% detection, 79% restraint

Roughly a third of the suite asserts silence, because a coordinator that alerts on everything is worse than none — agents learn to ignore it. Precision is graded as hard as detection.

result reading
Detection 91/92 one miss in 152 runs, and it was a context handoff rather than a collision — every semantic case passed, including the ones where the two agents shared no vocabulary at all
Restraint 38/48 it over-alerts. The problem is chattiness, not blindness

Of the twelve failures, ten are the coordinator speaking when it should have stayed quiet, one is a missed durable record, and one is the handoff case above. Nothing was tuned to make any of it look better, and the per-scenario breakdown is in the full report.

The headline blends two models, and they are not equal — the faster one scores better on restraint than gemini-2.5-pro, which is the default. The by-model table splits it rather than leaving you to assume the default is the best case.

# exactly what produced the published numbers: 38 x 3 on flash, 38 x 1 on pro
EVAL_K=3 EVAL_MODELS=gemini-2.5-flash uv run python -m pacl.evals.run_matrix
EVAL_K=1 EVAL_MODELS=gemini-2.5-pro   uv run python -m pacl.evals.run_matrix
uv run python scripts/eval_report.py --in .eval_runs/runs.jsonl

Full report: docs/eval-report.html. Every raw run, including the exact messages produced and the assertions it was graded against: docs/eval-runs.jsonl — the numbers above recompute from that file alone, without needing this repo's scenario definitions.

Two details that make the numbers mean something. Deterministic sensor output is stripped before grading, so a literal tag match can never inflate a result — without that split, an "expect alert" scenario with matching tags would pass with a no-op model. And the same suite runs unchanged across models, because coordination that only works on one model is falling out of that model's habits rather than the objective.

Architecture

Layer Choice
Agent interface MCP (Streamable HTTP), mounted at /mcp
Reasoning Gemini via the Google Agent Development Kit
Egress per-agent in-memory queue, drained onto tool responses
Substrate local-disk markdown (shared/durable storage is future work)
Hosting Cloud Run

Can you swap the models? The agents, yes — entirely. PACL only needs an MCP endpoint, so any MCP-capable client from any vendor works, which is the whole point. The intermediary is currently Gemini via ADK; ADK supports other providers through LiteLlm, so swapping it means google-adk[extensions] and passing a model object instead of a string. Small change, untested.

Configuration

Variable Default Purpose
GEMINI_API_KEY Gemini AI Studio key
GEMINI_MODEL gemini-2.5-pro the intermediary's model
HOST 127.0.0.1 bind address — set 0.0.0.0 only when you mean to expose it
PORT 8080 listen port
PACL_INTENT_TTL 3600 seconds before a stale intent ages out of overlap detection
SUBSTRATE_LOCAL_ROOT ./substrate substrate directory
PACL_BATCH_WINDOW 2.0 seconds spent collecting more events before the intermediary runs
LOG_LEVEL INFO uvicorn log level
uv sync --extra dev
uv run python -m pytest      # 67 tests

License

AGPL-3.0-or-later — Copyright (C) 2026 Dylan Porter. See LICENSE.

About

Proactive Agent Coordination Layer - a coordination layer for AI agents

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages