Skip to content

Commit 097ae8d

Browse files
committed
feat: wire agent advertisement into p2p capability gossip
Agents with identities now gossip their 0x addresses on the network via NodeCapability advertisements. The autonet service subscribes to agent lifecycle events and refreshes p2p capabilities on changes.
1 parent f087632 commit 097ae8d

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

atn/autonet_service.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import hashlib
1919
import json
2020
import logging
21+
import threading
2122
from dataclasses import dataclass, field
2223
from enum import Enum
2324
from typing import Any, TYPE_CHECKING
@@ -140,6 +141,17 @@ def __init__(self, config: AutonetConfig, event_bus: EventBus | None = None,
140141
self._on_execution_completed,
141142
)
142143

144+
# P2P agent advertisement
145+
self._p2p_host = None # AutonetHost (lazy, trio-based)
146+
self._p2p_thread: threading.Thread | None = None
147+
self._p2p_stop = threading.Event()
148+
self._agent_registry = None # Set by Runtime after init
149+
# Subscribe to agent lifecycle events for p2p advertisement refresh
150+
if self._events:
151+
for evt in (EventType.AGENT_REGISTERED, EventType.AGENT_UNREGISTERED,
152+
EventType.AGENT_ACTIVATED, EventType.AGENT_DEACTIVATED):
153+
self._events.subscribe(evt, self._on_agent_changed)
154+
143155
# Constitution CID loaded lazily from on-chain Registry
144156
self._constitution_loaded = False
145157
# Raw constitution text (loaded once, cached for prompt injection)
@@ -177,6 +189,107 @@ def _discover_jurisdiction(self) -> None:
177189
log.warning("Failed to discover jurisdiction from %s: %s",
178190
self.config.dao_address, e)
179191

192+
# ------------------------------------------------------------------
193+
# P2P agent advertisement
194+
# ------------------------------------------------------------------
195+
196+
def set_agent_registry(self, registry: Any) -> None:
197+
"""Called by Runtime after init to provide agent registry reference."""
198+
self._agent_registry = registry
199+
200+
async def _on_agent_changed(self, event: Any) -> None:
201+
"""Refresh p2p capability when agents are registered/unregistered."""
202+
self._refresh_p2p_agents()
203+
204+
def _refresh_p2p_agents(self) -> None:
205+
"""Rebuild agent advertisements from registry and push to p2p host."""
206+
if not self._p2p_host or not self._agent_registry:
207+
return
208+
try:
209+
ads = self._agent_registry.build_agent_advertisements()
210+
# Also include the connected wallet as a root agent if present
211+
if self.state.wallet_address and not any(
212+
a["address"].lower() == self.state.wallet_address.lower() for a in ads
213+
):
214+
ads.insert(0, {
215+
"address": self.state.wallet_address,
216+
"name": "root",
217+
"description": "",
218+
"agent_type": "orchestrator",
219+
"model": "",
220+
"is_root": True,
221+
"parent_address": "",
222+
"registered_on_chain": False,
223+
})
224+
self._p2p_host.update_capability(agents=ads)
225+
log.debug("P2P capability updated with %d agent(s)", len(ads))
226+
except Exception:
227+
log.debug("Failed to refresh p2p agents", exc_info=True)
228+
229+
def start_p2p(self) -> None:
230+
"""Start the p2p host in a background thread for agent advertisement."""
231+
if self._p2p_thread and self._p2p_thread.is_alive():
232+
return
233+
try:
234+
from nodes.common.p2p import AutonetHost, NodeCapability
235+
from nodes.common.config import load_config as load_autonet_config
236+
except Exception:
237+
log.debug("P2P not available (nodes package not installed or import error)")
238+
return
239+
240+
config_path = self.config.config_path or None
241+
try:
242+
cfg = load_autonet_config(config_path)
243+
except Exception:
244+
cfg = None
245+
246+
listen_port = cfg.p2p.listen_port if cfg else 0
247+
listen_host = cfg.p2p.listen_host if cfg else "0.0.0.0"
248+
bootstrap = cfg.p2p.bootstrap_peers if cfg else []
249+
advertise_interval = cfg.p2p.capability_advertise_interval if cfg else 60
250+
251+
node_id = f"atn-{self.state.wallet_address[:8]}" if self.state.wallet_address else "atn-daemon"
252+
cap = NodeCapability(peer_id="", node_id=node_id)
253+
254+
host = AutonetHost(
255+
node_id=node_id,
256+
listen_port=listen_port,
257+
listen_host=listen_host,
258+
bootstrap_peers=bootstrap,
259+
capability=cap,
260+
)
261+
self._p2p_host = host
262+
self._p2p_stop.clear()
263+
264+
def _run():
265+
import trio
266+
async def _main():
267+
async with host.run():
268+
self._refresh_p2p_agents()
269+
await host.advertise_capability()
270+
log.info("P2P host running, advertising %d agent(s)",
271+
len(host._capability.agents))
272+
while not self._p2p_stop.is_set():
273+
await trio.sleep(advertise_interval)
274+
self._refresh_p2p_agents()
275+
await host.advertise_capability()
276+
try:
277+
trio.run(_main)
278+
except Exception:
279+
log.debug("P2P host stopped", exc_info=True)
280+
281+
self._p2p_thread = threading.Thread(target=_run, name="p2p-host", daemon=True)
282+
self._p2p_thread.start()
283+
log.info("P2P agent advertisement started")
284+
285+
def stop_p2p(self) -> None:
286+
"""Stop the p2p background thread."""
287+
self._p2p_stop.set()
288+
if self._p2p_thread:
289+
self._p2p_thread.join(timeout=5)
290+
self._p2p_thread = None
291+
self._p2p_host = None
292+
180293
async def _emit(self, event_type_name: str, data: dict[str, Any] | None = None) -> None:
181294
"""Emit an event if the event bus is available."""
182295
if not self._events:
@@ -295,6 +408,8 @@ async def start(self) -> dict[str, Any]:
295408

296409
self.state.status = AutonetStatus.RUNNING
297410
log.info("Autonet service started")
411+
# Start p2p agent advertisement alongside the training service
412+
self.start_p2p()
298413
await self._emit("AUTONET_STARTED")
299414
return {"status": "started"}
300415

@@ -338,6 +453,7 @@ async def stop(self) -> dict[str, Any]:
338453
except asyncio.CancelledError:
339454
pass
340455

456+
self.stop_p2p()
341457
self.state.status = AutonetStatus.STOPPED
342458
log.info("Autonet service stopped")
343459
await self._emit("AUTONET_STOPPED")

atn/runtime/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ def __init__(self, event_bus: EventBus, data_dir: Path | None = None, config: AT
203203
)
204204
# Give engine access to the autonet bridge for constitutional injection
205205
self.engine._autonet_bridge = self.autonet
206+
# Wire agent registry into autonet bridge for p2p advertisement
207+
self.autonet.set_agent_registry(self.registry)
206208

207209
# Trace logger (Phase B Step 1 — Agent Trace Collection)
208210
from ..trace_logger import TraceLogger, TraceLoggingConfig as _TLConfig
@@ -281,6 +283,8 @@ async def start(self) -> None:
281283
# Start autonet service if configured
282284
if self._config.autonet.enabled:
283285
await self.autonet.start()
286+
# Always start p2p agent advertisement (even without training service)
287+
self.autonet.start_p2p()
284288

285289
# Load constitution text (needed for prompt injection on registered agents)
286290
await self.autonet.load_constitution()

atn/runtime/agent_registry.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,30 @@ def list_agents(self) -> list[tuple[AgentDefinition, AgentStatus]]:
240240
for aid, defn in self._agents.items()
241241
]
242242

243+
def build_agent_advertisements(self) -> list[dict]:
244+
"""Build p2p AgentAdvertisement dicts for all agents with identity."""
245+
ads = []
246+
for aid, defn in self._agents.items():
247+
identity = defn.identity
248+
if not identity or not identity.address:
249+
continue
250+
parent_addr = ""
251+
if defn.parent_id:
252+
parent = self._agents.get(defn.parent_id)
253+
if parent and parent.identity:
254+
parent_addr = parent.identity.address
255+
ads.append({
256+
"address": identity.address,
257+
"name": defn.name,
258+
"description": defn.description or "",
259+
"agent_type": defn.agent_type or "",
260+
"model": defn.model or "",
261+
"is_root": defn.parent_id is None or defn.parent_id == "",
262+
"parent_address": parent_addr,
263+
"registered_on_chain": identity.registered_on_chain,
264+
})
265+
return ads
266+
243267
def get_agent_key(self, agent_id: str) -> str | None:
244268
"""Get the private key for an agent (parent holds child's key)."""
245269
return self._agent_keys.get(agent_id)

0 commit comments

Comments
 (0)