elo-node 0.4.4__tar.gz → 0.4.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elo-node
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura.
5
5
  Author: Elo Contributors
6
6
  License: MIT
@@ -35,7 +35,7 @@ import asyncio
35
35
  from elo import Node
36
36
 
37
37
  async def main():
38
- node = Node("my-agent", port=7878)
38
+ node = Node("my-agent", port=7878, peers=["100.91.215.113:7878"])
39
39
  await node.connect()
40
40
  await node.register(agents=["analyst"], tools=["web-search"])
41
41
 
@@ -53,6 +53,7 @@ asyncio.run(main())
53
53
  - **Decentralized P2P** — discovery via public tracker or Kademlia DHT
54
54
  - **ed25519 signatures** — cryptographic identity, authenticated messages
55
55
  - **Capabilities** — publish/subscribe of agent skills across the mesh
56
+ - **Relay via tracker** — nodes behind NAT/Docker can communicate through a tracker
56
57
  - **Zero infra** — no Kafka, Redis, NATS, or central server
57
58
  - **Native CLI** — `python -m elo serve`, `status`, `init`, `id`
58
59
 
@@ -66,26 +67,21 @@ python -m elo init # Generate persistent identity
66
67
  python -m elo serve # Start an interactive node
67
68
  ```
68
69
 
69
- ## Architecture
70
+ ## Key Concepts
70
71
 
71
- ```
72
- ┌──────────────────┐ TCP/JSON ┌──────────────────┐
73
- │ Node A │◄──────────────►│ Node B │
74
- │ ed25519 key │ │ ed25519 key │
75
- │ Capabilities │ │ Capabilities │
76
- │ Interests │ │ Interests │
77
- └──────────────────┘ └──────────────────┘
78
- │ │
79
- │ Tracker (optional) │
80
- └────────────── DHT ───────────────┘
81
- ```
72
+ - **`peers=` is required** for outbound connections. Without it the node only listens.
73
+ - **`send_task()` auto-fallback:** direct → InterestTable → QUERY → **tracker relay** → NO_PEER
74
+ - **HELLO_ACK with known_peers:** tracker shares all peers on handshake (v0.4.4+)
75
+ - **`discover_peers_network()`** — QUERY broadcast across the mesh
76
+
77
+ ## Changelog
82
78
 
83
- Each node:
84
- 1. Generates an ed25519 identity on first run
85
- 2. Listens on a TCP port
86
- 3. Announces capabilities (e.g. "analyst", "web-search")
87
- 4. Discovers other nodes via shared tracker or manual peers
88
- 5. Exchanges signed messages (tasks, results, events)
79
+ | Version | Highlights |
80
+ |---------|------------|
81
+ | 0.4.5 | Multi-response discover, tracker returns all peers on query |
82
+ | 0.4.4 | HELLO_ACK with known_peers, send_task auto-fallback tracker |
83
+ | 0.4.3 | Relay via tracker, send_task_via_tracker() |
84
+ | 0.4.0 | Initial release |
89
85
 
90
86
  ## Compatibility
91
87
 
@@ -13,7 +13,7 @@ import asyncio
13
13
  from elo import Node
14
14
 
15
15
  async def main():
16
- node = Node("my-agent", port=7878)
16
+ node = Node("my-agent", port=7878, peers=["100.91.215.113:7878"])
17
17
  await node.connect()
18
18
  await node.register(agents=["analyst"], tools=["web-search"])
19
19
 
@@ -31,6 +31,7 @@ asyncio.run(main())
31
31
  - **Decentralized P2P** — discovery via public tracker or Kademlia DHT
32
32
  - **ed25519 signatures** — cryptographic identity, authenticated messages
33
33
  - **Capabilities** — publish/subscribe of agent skills across the mesh
34
+ - **Relay via tracker** — nodes behind NAT/Docker can communicate through a tracker
34
35
  - **Zero infra** — no Kafka, Redis, NATS, or central server
35
36
  - **Native CLI** — `python -m elo serve`, `status`, `init`, `id`
36
37
 
@@ -44,26 +45,21 @@ python -m elo init # Generate persistent identity
44
45
  python -m elo serve # Start an interactive node
45
46
  ```
46
47
 
47
- ## Architecture
48
+ ## Key Concepts
48
49
 
49
- ```
50
- ┌──────────────────┐ TCP/JSON ┌──────────────────┐
51
- │ Node A │◄──────────────►│ Node B │
52
- │ ed25519 key │ │ ed25519 key │
53
- │ Capabilities │ │ Capabilities │
54
- │ Interests │ │ Interests │
55
- └──────────────────┘ └──────────────────┘
56
- │ │
57
- │ Tracker (optional) │
58
- └────────────── DHT ───────────────┘
59
- ```
50
+ - **`peers=` is required** for outbound connections. Without it the node only listens.
51
+ - **`send_task()` auto-fallback:** direct → InterestTable → QUERY → **tracker relay** → NO_PEER
52
+ - **HELLO_ACK with known_peers:** tracker shares all peers on handshake (v0.4.4+)
53
+ - **`discover_peers_network()`** — QUERY broadcast across the mesh
54
+
55
+ ## Changelog
60
56
 
61
- Each node:
62
- 1. Generates an ed25519 identity on first run
63
- 2. Listens on a TCP port
64
- 3. Announces capabilities (e.g. "analyst", "web-search")
65
- 4. Discovers other nodes via shared tracker or manual peers
66
- 5. Exchanges signed messages (tasks, results, events)
57
+ | Version | Highlights |
58
+ |---------|------------|
59
+ | 0.4.5 | Multi-response discover, tracker returns all peers on query |
60
+ | 0.4.4 | HELLO_ACK with known_peers, send_task auto-fallback tracker |
61
+ | 0.4.3 | Relay via tracker, send_task_via_tracker() |
62
+ | 0.4.0 | Initial release |
67
63
 
68
64
  ## Compatibility
69
65
 
@@ -26,4 +26,4 @@ __all__ = [
26
26
  "Node", "Task", "Result", "Event", "NodeInfo", "Capabilities",
27
27
  "EphemeralIdentity", "generate_and_save_identity", "load_identity",
28
28
  ]
29
- __version__ = "0.4.4"
29
+ __version__ = "0.4.6"
@@ -78,7 +78,7 @@ class Node:
78
78
  peers: list[str] | None = None,
79
79
  tracker: str = "public",
80
80
  allowlist: list[str] | None = None,
81
- version: str = "0.4.4",
81
+ version: str = "0.4.6",
82
82
  identity: EphemeralIdentity | None = None,
83
83
  verify_peers: bool = True,
84
84
  heartbeat_interval_s: int = 30,
@@ -110,7 +110,7 @@ class Node:
110
110
  self._task_handler: Callable[[Task], Awaitable[dict[str, Any]]] | None = None
111
111
  self._shutdown_event = asyncio.Event()
112
112
  self._pending_results: dict[str, asyncio.Future] = {}
113
- self._pending_queries: dict[str, tuple[asyncio.Future, float]] = {}
113
+ self._pending_queries: dict[str, tuple[list[dict[str, Any]], asyncio.Event, float]] = {}
114
114
 
115
115
  # Cache de pubkeys para verificação de assinatura
116
116
  self._pubkey_cache: dict[str, tuple[Any, float]] = {}
@@ -330,34 +330,41 @@ class Node:
330
330
  return await self.get_known_peers_local()
331
331
 
332
332
  async def discover_peers_network(self, timeout: float = 5.0) -> list[dict[str, Any]]:
333
- """Discover peers on the network via QUERY broadcast with empty capability.
333
+ """Discover peers on the network via QUERY broadcast.
334
334
 
335
- Sends a broadcast QUERY for an empty capability (catch-all) and aggregates
336
- responses for the given timeout period. Returns list of discovered peer dicts.
337
-
338
- Args:
339
- timeout: seconds to wait for responses (default 5.0)
340
-
341
- Returns:
342
- List of dicts with 'addr' and optionally 'node_id'
335
+ Envia QUERY com capability vazia e agrega todas as respostas
336
+ dentro do timeout. Modelo BitTorrent: o tracker retorna todos
337
+ os peers conhecidos, não só quem matcha a capability.
343
338
  """
344
339
  query_id = str(uuid.uuid4())[:8]
345
- future: asyncio.Future = asyncio.get_event_loop().create_future()
346
- self._pending_queries[query_id] = (future, time.time())
340
+ collected: list[dict[str, Any]] = []
341
+ event = asyncio.Event()
342
+ self._pending_queries[query_id] = (collected, event, time.time())
347
343
  discovered: dict[str, dict[str, Any]] = {}
348
344
 
349
345
  # Broadcast QUERY for any capability
350
346
  await self._tcp.broadcast(query_msg("", query_id, ttl=3))
351
347
 
352
348
  try:
353
- result_addr = await asyncio.wait_for(future, timeout=timeout)
354
- if result_addr:
355
- discovered[result_addr] = {"addr": result_addr, "connected": False, "caps": [], "via": "network"}
349
+ await asyncio.wait_for(event.wait(), timeout=timeout)
356
350
  except asyncio.TimeoutError:
357
351
  pass
358
352
  finally:
359
353
  self._pending_queries.pop(query_id, None)
360
354
 
355
+ # Merge collected peers from all responses
356
+ known_addrs: set[str] = set()
357
+ for entry in collected:
358
+ addr = entry.get("addr", "")
359
+ if addr and addr not in discovered:
360
+ discovered[addr] = {
361
+ "addr": addr,
362
+ "caps": entry.get("caps", []),
363
+ "connected": False,
364
+ "via": "network",
365
+ }
366
+ known_addrs.add(addr)
367
+
361
368
  # Merge with locally known peers
362
369
  local = await self.get_known_peers_local()
363
370
  for entry in local:
@@ -460,12 +467,23 @@ class Node:
460
467
  capability = msg.get("capability", "")
461
468
  query_id = msg.get("id", "")
462
469
  ttl = msg.get("ttl", 5)
463
- nodes = []
470
+ nodes: list[dict[str, Any]] = []
471
+
472
+ # Modelo BitTorrent: tracker retorna SEMPRE todos os peers conhecidos,
473
+ # não só quem matcha a capability. Isso permite que qualquer nó
474
+ # descubra a mesh completa com discover_peers_network().
475
+ for addr in self._routing.known_peers:
476
+ info = self._routing.get_peer_caps(addr)
477
+ if addr != peer_addr:
478
+ nodes.append({
479
+ "addr": addr,
480
+ "caps": list(info.get("caps", set())),
481
+ })
482
+
464
483
  if self._tracker.has_capability(capability):
465
484
  # Usa peer_addr (addr real do TCP) em vez de localhost — necessário para WAN/Tailscale
466
485
  nodes.append({"node_id": self._node_id[:12], "addr": peer_addr})
467
- for p in self._routing.find_all_peers_for(capability):
468
- nodes.append({"addr": p})
486
+
469
487
  if nodes:
470
488
  try:
471
489
  await self._tcp.send_to(peer_addr, query_resp_msg(query_id, nodes))
@@ -477,10 +495,11 @@ class Node:
477
495
  async def _on_query_resp(self, peer_addr: str, msg: dict) -> None:
478
496
  query_id = msg.get("id", "")
479
497
  if query_id in self._pending_queries:
480
- future, _ = self._pending_queries[query_id]
498
+ collected, event, _ts = self._pending_queries[query_id]
481
499
  nodes = msg.get("nodes", [])
482
- if nodes and not future.done():
483
- future.set_result(nodes[0].get("addr", ""))
500
+ if nodes:
501
+ collected.extend(nodes)
502
+ event.set()
484
503
 
485
504
  async def _on_interest_update(self, peer_addr: str, msg: dict) -> None:
486
505
  existing = self._routing.get_peer_caps(peer_addr)
@@ -609,15 +628,19 @@ class Node:
609
628
  async def _query_capability(self, capability: str, ttl: int = 5,
610
629
  timeout: float = 5.0) -> str | None:
611
630
  query_id = str(uuid.uuid4())[:8]
612
- future: asyncio.Future = asyncio.get_event_loop().create_future()
613
- self._pending_queries[query_id] = (future, time.time())
631
+ collected: list[dict[str, Any]] = []
632
+ event = asyncio.Event()
633
+ self._pending_queries[query_id] = (collected, event, time.time())
614
634
  await self._tcp.broadcast(query_msg(capability, query_id, ttl))
615
635
  try:
616
- return await asyncio.wait_for(future, timeout=timeout)
636
+ await asyncio.wait_for(event.wait(), timeout=timeout)
617
637
  except asyncio.TimeoutError:
618
- return None
638
+ pass
619
639
  finally:
620
640
  self._pending_queries.pop(query_id, None)
641
+ if collected:
642
+ return collected[0].get("addr", None)
643
+ return None
621
644
 
622
645
  async def _get_caller_pubkey(self, node_id: str) -> Any | None:
623
646
  now = time.time()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elo-node
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura.
5
5
  Author: Elo Contributors
6
6
  License: MIT
@@ -35,7 +35,7 @@ import asyncio
35
35
  from elo import Node
36
36
 
37
37
  async def main():
38
- node = Node("my-agent", port=7878)
38
+ node = Node("my-agent", port=7878, peers=["100.91.215.113:7878"])
39
39
  await node.connect()
40
40
  await node.register(agents=["analyst"], tools=["web-search"])
41
41
 
@@ -53,6 +53,7 @@ asyncio.run(main())
53
53
  - **Decentralized P2P** — discovery via public tracker or Kademlia DHT
54
54
  - **ed25519 signatures** — cryptographic identity, authenticated messages
55
55
  - **Capabilities** — publish/subscribe of agent skills across the mesh
56
+ - **Relay via tracker** — nodes behind NAT/Docker can communicate through a tracker
56
57
  - **Zero infra** — no Kafka, Redis, NATS, or central server
57
58
  - **Native CLI** — `python -m elo serve`, `status`, `init`, `id`
58
59
 
@@ -66,26 +67,21 @@ python -m elo init # Generate persistent identity
66
67
  python -m elo serve # Start an interactive node
67
68
  ```
68
69
 
69
- ## Architecture
70
+ ## Key Concepts
70
71
 
71
- ```
72
- ┌──────────────────┐ TCP/JSON ┌──────────────────┐
73
- │ Node A │◄──────────────►│ Node B │
74
- │ ed25519 key │ │ ed25519 key │
75
- │ Capabilities │ │ Capabilities │
76
- │ Interests │ │ Interests │
77
- └──────────────────┘ └──────────────────┘
78
- │ │
79
- │ Tracker (optional) │
80
- └────────────── DHT ───────────────┘
81
- ```
72
+ - **`peers=` is required** for outbound connections. Without it the node only listens.
73
+ - **`send_task()` auto-fallback:** direct → InterestTable → QUERY → **tracker relay** → NO_PEER
74
+ - **HELLO_ACK with known_peers:** tracker shares all peers on handshake (v0.4.4+)
75
+ - **`discover_peers_network()`** — QUERY broadcast across the mesh
76
+
77
+ ## Changelog
82
78
 
83
- Each node:
84
- 1. Generates an ed25519 identity on first run
85
- 2. Listens on a TCP port
86
- 3. Announces capabilities (e.g. "analyst", "web-search")
87
- 4. Discovers other nodes via shared tracker or manual peers
88
- 5. Exchanges signed messages (tasks, results, events)
79
+ | Version | Highlights |
80
+ |---------|------------|
81
+ | 0.4.5 | Multi-response discover, tracker returns all peers on query |
82
+ | 0.4.4 | HELLO_ACK with known_peers, send_task auto-fallback tracker |
83
+ | 0.4.3 | Relay via tracker, send_task_via_tracker() |
84
+ | 0.4.0 | Initial release |
89
85
 
90
86
  ## Compatibility
91
87
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "elo-node"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -136,7 +136,7 @@ class TestNodeConstruction:
136
136
  node = Node("test-node")
137
137
  assert node._name == "test-node"
138
138
  assert node._port == 7878
139
- assert node._version == "0.4.4"
139
+ assert node._version == "0.4.6"
140
140
  assert node.connected is False
141
141
  assert node.node_id is not None
142
142
  assert len(node.node_id) > 20
File without changes
File without changes
File without changes
File without changes
File without changes