Skip to content

Two agents talking to each other

Agent A asks agent B a question. B runs its own sealed mission — its framework, its tools, its model — and A gets B’s answer. Nothing is shared between them but a name and the rules below.

In a chat you just say so — “ask <B> what the weather in Chennai is” — and A uses its connect-to-agent memory (below). Every install has it (agent r30, harness engine r41).

A ──(1) who is B? ────────────► registry.fastagents.net by NAME, over DDE (a sealed query, not a GET)
A ──(2) open a session ───────► B /external/auth B's policy: may A talk to me?
A ──(3) the question ─────────► B /external/incoming B runs its ONE sealed mission
B ──(4) a model, please ──► B's router may B relay for A?
B ──(5) its tools ────────► other agents' APIs may B call them?
A ◄─(6) the answer ─────────── B

Verified 2026-09-27: from a workspace chat, an agent asked a PydanticAI agent by name through connect-to-agent; the PydanticAI agent answered with live weather and BBC headlines it fetched from two API agents. Asked to pass a question on to a LangGraph agent, it declined — no relay.

<A> and <B> below are the agents’ identity names — HEXAEIGHT_RESOURCENAME in each one’s env-file, the name in its boot log’s identity: line — not folder names.

Prerequisites — each agent works on its own

Section titled “Prerequisites — each agent works on its own”

Before two agents can coordinate, each one must be a working agent by itself. None of this is about the other agent; an agent missing any of it cannot answer anyone, its own owner included.

each agent needs
its own identitythe registry holds one address per name. Two agents under one identity cannot both be found by name — an agent others must reach by name needs an identity of its own
its own model (an LLM)its engine asks its router for a model, so its router must admit it: <agent> -> * inbound (it may relay turns) and <agent> -> <models> outbound. This is part of standing an agent up — the same as for any agent. The installer writes these rules for the agent it installs
a mission it servesthe agent being asked is a runner (add-runner) sealed to a mission. An API-only agent ("apiOnly": true) answers only named API routes and refuses a question: this agent serves API requests only
a way to be foundregistered ("register": true) with a public address — a tunnel, "reach": {"mode": "cloudflared", "bin": "<path to cloudflared>"} — or the caller is given its address explicitly

Giving an agent its model, on its router’s folder:

Terminal window
cp -p router-policy.he "router-policy.he.bak_$(date +%Y%m%d_%H%M%S)"
set -a; . ./env-file; set +a
printf 'N\n%s\n*\n\ny\n' "<agent>" | ./hexaeight-router-linux-x64 --init-policy # <agent> -> * both ways
heia restart router # the router reads its policy only at start

Replace * (the models) with a route pattern to limit which models the agent may use. A registered agent’s boot log shows it can be found:

[reach] mode=cloudflared tunnel up https://…trycloudflare.com
[register] OK -> https://registry.fastagents.net as <B> = https://…trycloudflare.com (http 200)

Measured: a newly stood-up agent was found by name and accepted the session, then could not answer: its router did not admit it (NOT authorized with router …: HTTP 400). That was a missing prerequisite — the agent had no model — not a coordination rule.

Coordination — the rules between the two

Section titled “Coordination — the rules between the two”

With both agents working, what lets A talk to B is each agent’s own policy, both sides, both directions. Policy is default-deny.

On agent B (the one being asked) — run in B’s folder:

Terminal window
./hexaeight-agent-linux-x64 hexaeight-agent.json --add-rule "<A>,*,inbound,allow" # A may open a session and ask
./hexaeight-agent-linux-x64 hexaeight-agent.json --add-rule "*,<A>,outbound,allow" # B may answer / reach A

The destination is *, not B’s own name. A session request is checked as sender = A, destination = self. A rule <A>,<B>,inbound looks right and does not match.

On agent A (the one asking) — in every folder that holds A’s identity:

Terminal window
./hexaeight-agent-linux-x64 hexaeight-agent.json --add-rule "*,<B>,outbound,allow" # A's engines may reach B
./hexaeight-agent-linux-x64 hexaeight-agent.json --add-rule "<B>,*,inbound,allow" # B's answers and calls may reach A

add-runner --peer <A> writes B’s outbound row for you when B is created.

On every API agent B’s tools call (a weather or news service, say) — the same pair, for B:

Terminal window
./hexaeight-agent-linux-x64 hexaeight-agent.json --add-rule "<B>,*,inbound,allow"
./hexaeight-agent-linux-x64 hexaeight-agent.json --add-rule "*,<B>,outbound,allow"

Agent policy changes are picked up without a restart.

Coordination needs no router change when each agent already has its model (the prerequisite above) and the answering agent’s router already knows the caller. Two things are worth knowing:

  • The answer’s model is always B’s — the route B’s engine is sealed to, on B’s router. A’s router is not involved in A asking B, and A cannot choose B’s model.

  • B’s router checks who each turn is for. A turn from A runs for A, so B’s router must allow A as the turn’s sender. When A and B share a router that already has <A> -> * rows (as it does for A’s own work), that is already true. When B’s router has never seen A — different routers, different operators — add A there:

    Terminal window
    printf 'N\n%s\n*\n%s\ny\n' "<A>" "<B>" | ./hexaeight-router-linux-x64 --init-policy # <A> -> <B> in, <A> -> * out
    heia restart router

    Measured on a shared router only; on separate routers this follows from the same check and has not been run end to end yet.

router change for A to talk to B
both agents have their model, one shared routernone
B’s router has never seen Aon B’s router: allow A as a turn’s sender (above)
they also talk the other waythe mirror, on A’s router, for B
A acts for a personthe person, as sha512(email), in B’s vouch, B’s inbound rule and B’s router — the BYOA stores

From a chat or a mission — connect-to-agent

Section titled “From a chat or a mission — connect-to-agent”

Every install has a memory named connect-to-agent. It holds no documents: searching it asks another agent.

memory_search("connect-to-agent", "<B> What is the weather in Chennai?")
memory_search("connect-to-agent", "<B>") # the name alone: B describes what it can do

The first word is the agent’s name, the rest is the question. The model does this itself when you ask it to talk to another agent; the answer comes back headed # Answer from agent '<B>', and the model reports it as B’s answer. Framework runners have the same memory built in.

  • One conversation per chat. Follow-ups in the same chat continue the conversation with B; a new chat starts a new one. The chat’s own id never leaves A’s machine.
  • It waits for the answer — up to 5 minutes — and returns it in the same turn.
  • APIs are not asked this way. Another agent’s API (weather, news, a search corpus) is reached through a memory that points at it, exactly as before — An API you already run.
  • The memory is kept exactly as shipped. An edited copy is put back at the next turn; the relay switch below is the only way to change it.

A turn that is serving someone else — another agent, or an external caller — does not ask a third agent for them. If B is answering A and B’s model tries connect-to-agent, it gets

NOT ASKED: this request came from someone else (an external caller), and connect-to-agent is set not to
contact other agents on anyone else's behalf. …

and tells A that it does not relay; A can ask that agent directly. Turns for the agent’s own user (your workspace chat) are never affected.

The check is in code, not in the prompt: a turn is “for someone else” when the agent opened it through its external door, which only external callers can reach. To let one agent relay:

Terminal window
# in that agent's folder — its harness root: ~/.hexaeight-harness for a workspace agent,
# <runner>-frontdoor/harness-root for a runner
~/.heia/runtime/harness/hexaeight-engine --connector-relay on --root <harness root>
~/.heia/runtime/harness/hexaeight-engine --connector-relay status --root <harness root>

Relaying is also subject to the policy of every agent in the chain: B needs the rules to reach the third agent, and the third agent the rules to accept B.

An agent that serves an API (external.api.enabled) registers that address separately, under the api capability. A name lookup returns the agent’s normal door; the API door is returned only when it is asked for — so an identity can hold a conversational agent and an API service at two addresses. An API-only agent ("apiOnly": true) registers only its API door.

From A’s folder:

Terminal window
set -a; . ./env-file; set +a
export HEIA_ASK_TIMEOUT_SECONDS=0 # wait for the work; B bounds its own turn
./hexaeight-agent-linux-x64 ask-agent --peer <B> --capability <label> --ask "What is the weather in Chennai?" --json

No URL: A finds B in the registry by name. (Pass --peer-url https://… to skip the registry.) A backend that holds a pipe open uses ask-daemon with the same fields, one JSON request per line.

What success looks like — on A:

[ask-agent] resolved <B> -> https://…trycloudflare.com (registry, by name)
[outbound] expecting a reply from <B> on s-…
{"sent":true, … "reply": … "result":"# Answer\n> Answered by PydanticAI\n\n…"}

and in B’s log:

[external] <A> created session s-…
[external] dde-auth request from <A> session 's-…'
[/v1/messages] session ext-s-… via DDE-map -> <A> (in B's router log)

Name lookup needs agent r30. Earlier agents look a named peer up by capability, and answer cannot resolve a url for an agent that publishes no tag. Upgrade, or pass --peer-url.

you seewhereit means
cannot resolve a url for '<B>'AB is not registered — or A’s agent predates r30; pass --peer-url
NOT ASKED: this request came from someone else …B’s answerB is answering someone else and does not relay — by design. Ask the third agent directly
agent '<B>' did not answer (denied)A’s chatB’s policy refused A — B lacks <A>,*,inbound,allow
this agent serves API requests onlyB’s answerB is an API agent (apiOnly); ask a runner instead
DENY session action from <A>: no matching allow ruleB’s logB lacks <A>,*,inbound,allow — note the *
NOT authorized with router …: HTTP 400, and on the router agent_jwt decrypt failed: 'r' is an invalid start of a valueB’s log / B’s routera prerequisite, not a coordination rule: B has no model — its router does not admit B. Give it one (prerequisites)
an answer that says a tool returned deniedB’s answeran API agent B called lacks <B>,*,inbound / *,<B>,outbound
{"ok":true,"accepted":"response"} and no answerAA and B share one identity and one store — give each its own HEIA_DIR
result
workspace chat → a PydanticAI runner, by nameanswered, with live weather and news from two API agents
workspace chat → a LangGraph runner, by nameanswered — when the runner’s policy allowed the caller; refused (denied) before
a new chat → the same agenta new conversation; the earlier chat’s history not carried over
PydanticAI runner, answering an agent, asked to ask a LangGraph runnerdeclined — no relay
connect-to-agent edited by handrestored at the next turn