jarviscore-framework 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

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.
Files changed (43) hide show
  1. examples/cloud_deployment_example.py +3 -3
  2. examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
  3. examples/customagent_distributed_example.py +140 -1
  4. examples/fastapi_integration_example.py +74 -11
  5. jarviscore/__init__.py +8 -11
  6. jarviscore/cli/smoketest.py +1 -1
  7. jarviscore/core/mesh.py +158 -0
  8. jarviscore/data/examples/cloud_deployment_example.py +3 -3
  9. jarviscore/data/examples/custom_profile_decorator.py +134 -0
  10. jarviscore/data/examples/custom_profile_wrap.py +168 -0
  11. jarviscore/data/examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
  12. jarviscore/data/examples/customagent_distributed_example.py +140 -1
  13. jarviscore/data/examples/fastapi_integration_example.py +74 -11
  14. jarviscore/docs/API_REFERENCE.md +576 -47
  15. jarviscore/docs/CHANGELOG.md +131 -0
  16. jarviscore/docs/CONFIGURATION.md +1 -1
  17. jarviscore/docs/CUSTOMAGENT_GUIDE.md +591 -153
  18. jarviscore/docs/GETTING_STARTED.md +186 -329
  19. jarviscore/docs/TROUBLESHOOTING.md +1 -1
  20. jarviscore/docs/USER_GUIDE.md +292 -12
  21. jarviscore/integrations/fastapi.py +4 -4
  22. jarviscore/p2p/coordinator.py +36 -7
  23. jarviscore/p2p/messages.py +13 -0
  24. jarviscore/p2p/peer_client.py +380 -21
  25. jarviscore/p2p/peer_tool.py +17 -11
  26. jarviscore/profiles/__init__.py +2 -4
  27. jarviscore/profiles/customagent.py +302 -74
  28. jarviscore/testing/__init__.py +35 -0
  29. jarviscore/testing/mocks.py +578 -0
  30. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +61 -46
  31. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +42 -34
  32. tests/test_13_dx_improvements.py +37 -37
  33. tests/test_15_llm_cognitive_discovery.py +18 -18
  34. tests/test_16_unified_dx_flow.py +3 -3
  35. tests/test_17_session_context.py +489 -0
  36. tests/test_18_mesh_diagnostics.py +465 -0
  37. tests/test_19_async_requests.py +516 -0
  38. tests/test_20_load_balancing.py +546 -0
  39. tests/test_21_mock_testing.py +776 -0
  40. jarviscore/profiles/listeneragent.py +0 -292
  41. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
  42. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
  43. {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,8 @@ Example:
21
21
  """
22
22
  import asyncio
23
23
  import logging
24
+ import random
25
+ import time
24
26
  from typing import List, Dict, Any, Optional, TYPE_CHECKING
25
27
  from uuid import uuid4
26
28
 
@@ -95,6 +97,16 @@ class PeerClient:
95
97
  # Pending requests waiting for responses (correlation_id -> Future)
96
98
  self._pending_requests: Dict[str, asyncio.Future] = {}
97
99
 
100
+ # Async request inbox (correlation_id -> response data or None)
101
+ self._async_inbox: Dict[str, Optional[Dict[str, Any]]] = {}
102
+
103
+ # Async request metadata (correlation_id -> metadata dict)
104
+ self._async_requests: Dict[str, Dict[str, Any]] = {}
105
+
106
+ # Load balancing state
107
+ self._round_robin_index: Dict[str, int] = {} # key -> current index
108
+ self._peer_last_used: Dict[str, float] = {} # agent_id -> timestamp
109
+
98
110
  self._logger = logging.getLogger(f"jarviscore.peer_client.{agent_id}")
99
111
 
100
112
  # ─────────────────────────────────────────────────────────────────
@@ -134,7 +146,8 @@ class PeerClient:
134
146
  def discover(
135
147
  self,
136
148
  capability: str = None,
137
- role: str = None
149
+ role: str = None,
150
+ strategy: str = "first"
138
151
  ) -> List[PeerInfo]:
139
152
  """
140
153
  Discover peers by capability or role.
@@ -142,17 +155,27 @@ class PeerClient:
142
155
  Args:
143
156
  capability: Filter by capability (e.g., "analysis")
144
157
  role: Filter by role (e.g., "analyst")
158
+ strategy: Selection strategy for ordering results:
159
+ - "first": Return in discovery order (default, current behavior)
160
+ - "random": Shuffle results randomly
161
+ - "round_robin": Rotate through peers on each call
162
+ - "least_recent": Return least recently used peers first
145
163
 
146
164
  Returns:
147
- List of matching PeerInfo objects
165
+ List of matching PeerInfo objects, ordered by strategy
148
166
 
149
167
  Example:
150
- analysts = self.peers.discover(capability="analysis")
151
- for peer in analysts:
152
- print(f"Found: {peer.role} - {peer.capabilities}")
168
+ # Get a random analyst for load balancing
169
+ analysts = self.peers.discover(role="analyst", strategy="random")
170
+ if analysts:
171
+ await self.peers.request(analysts[0].role, {"task": "..."})
172
+
173
+ # Round-robin across workers
174
+ workers = self.peers.discover(capability="processing", strategy="round_robin")
153
175
  """
154
176
  results = []
155
177
 
178
+ # Search LOCAL agents first
156
179
  if role:
157
180
  agents = self._agent_registry.get(role, [])
158
181
  for agent in agents:
@@ -166,7 +189,7 @@ class PeerClient:
166
189
  ))
167
190
 
168
191
  elif capability:
169
- # Search all agents for capability
192
+ # Search all local agents for capability
170
193
  for role_name, agents in self._agent_registry.items():
171
194
  for agent in agents:
172
195
  if agent.agent_id != self._agent_id: # Exclude self
@@ -180,7 +203,7 @@ class PeerClient:
180
203
  ))
181
204
 
182
205
  else:
183
- # Return all peers
206
+ # Return all local peers
184
207
  for role_name, agents in self._agent_registry.items():
185
208
  for agent in agents:
186
209
  if agent.agent_id != self._agent_id: # Exclude self
@@ -192,8 +215,112 @@ class PeerClient:
192
215
  status="alive"
193
216
  ))
194
217
 
218
+ # BUG FIX: Also search REMOTE agents from other nodes
219
+ # Access coordinator's _remote_agent_registry
220
+ if self._coordinator and hasattr(self._coordinator, '_remote_agent_registry'):
221
+ remote_registry = self._coordinator._remote_agent_registry
222
+
223
+ for agent_id, info in remote_registry.items():
224
+ if agent_id == self._agent_id: # Exclude self
225
+ continue
226
+
227
+ # Filter by role if specified
228
+ if role and info.get('role') != role:
229
+ continue
230
+
231
+ # Filter by capability if specified
232
+ if capability and capability not in info.get('capabilities', []):
233
+ continue
234
+
235
+ # Add remote peer
236
+ results.append(PeerInfo(
237
+ agent_id=info['agent_id'],
238
+ role=info['role'],
239
+ capabilities=info.get('capabilities', []),
240
+ node_id=info.get('node_id', 'unknown'),
241
+ status="alive"
242
+ ))
243
+
244
+ # Apply selection strategy
245
+ if results and strategy != "first":
246
+ key = capability or role or "all"
247
+ results = self._apply_strategy(results, key, strategy)
248
+
195
249
  return results
196
250
 
251
+ def _apply_strategy(
252
+ self,
253
+ peers: List[PeerInfo],
254
+ key: str,
255
+ strategy: str
256
+ ) -> List[PeerInfo]:
257
+ """Apply selection strategy to reorder peer list."""
258
+ if len(peers) <= 1:
259
+ return peers
260
+
261
+ if strategy == "random":
262
+ shuffled = peers.copy()
263
+ random.shuffle(shuffled)
264
+ return shuffled
265
+
266
+ elif strategy == "round_robin":
267
+ # Get current index for this key
268
+ idx = self._round_robin_index.get(key, 0)
269
+ # Rotate list to start from current index
270
+ rotated = peers[idx:] + peers[:idx]
271
+ # Increment index for next call
272
+ self._round_robin_index[key] = (idx + 1) % len(peers)
273
+ return rotated
274
+
275
+ elif strategy == "least_recent":
276
+ # Sort by last used time (oldest first)
277
+ def get_last_used(peer: PeerInfo) -> float:
278
+ return self._peer_last_used.get(peer.agent_id, 0.0)
279
+ return sorted(peers, key=get_last_used)
280
+
281
+ else: # "first" or unknown
282
+ return peers
283
+
284
+ def discover_one(
285
+ self,
286
+ capability: str = None,
287
+ role: str = None,
288
+ strategy: str = "first"
289
+ ) -> Optional[PeerInfo]:
290
+ """
291
+ Discover a single peer by capability or role.
292
+
293
+ Convenience method that returns just the first peer from discover().
294
+
295
+ Args:
296
+ capability: Filter by capability
297
+ role: Filter by role
298
+ strategy: Selection strategy ("first", "random", "round_robin", "least_recent")
299
+
300
+ Returns:
301
+ Single PeerInfo or None if no match found
302
+
303
+ Example:
304
+ # Get a random analyst
305
+ analyst = self.peers.discover_one(role="analyst", strategy="random")
306
+ if analyst:
307
+ response = await self.peers.request(analyst.agent_id, {...})
308
+ """
309
+ peers = self.discover(capability=capability, role=role, strategy=strategy)
310
+ return peers[0] if peers else None
311
+
312
+ def record_peer_usage(self, peer_id: str):
313
+ """
314
+ Record that a peer was used (for least_recent strategy).
315
+
316
+ Call this after successfully communicating with a peer
317
+ to update the usage timestamp for load balancing.
318
+
319
+ Args:
320
+ peer_id: The agent_id of the peer that was used
321
+ """
322
+ self._peer_last_used[peer_id] = time.time()
323
+
197
324
  @property
198
325
  def registry(self) -> Dict[str, PeerInfo]:
199
326
  """
@@ -310,13 +437,19 @@ class PeerClient:
310
437
  # MESSAGING - SEND
311
438
  # ─────────────────────────────────────────────────────────────────
312
439
 
313
- async def notify(self, target: str, message: Dict[str, Any]) -> bool:
440
+ async def notify(
441
+ self,
442
+ target: str,
443
+ message: Dict[str, Any],
444
+ context: Optional[Dict[str, Any]] = None
445
+ ) -> bool:
314
446
  """
315
447
  Send a fire-and-forget notification to a peer.
316
448
 
317
449
  Args:
318
450
  target: Target agent role (e.g., "analyst") or agent_id
319
451
  message: Message payload (any JSON-serializable dict)
452
+ context: Optional metadata (mission_id, priority, trace_id, etc.)
320
453
 
321
454
  Returns:
322
455
  True if message was sent successfully
@@ -325,7 +458,7 @@ class PeerClient:
325
458
  await self.peers.notify("analyst", {
326
459
  "event": "scouting_complete",
327
460
  "data": {"findings": 42}
328
- })
461
+ }, context={"mission_id": "abc123"})
329
462
  """
330
463
  target_agent = self._resolve_target(target)
331
464
  if not target_agent:
@@ -337,7 +470,8 @@ class PeerClient:
337
470
  type=MessageType.NOTIFY,
338
471
  data=message,
339
472
  sender=self._agent_id,
340
- sender_node=self._node_id
473
+ sender_node=self._node_id,
474
+ context=context
341
475
  )
342
476
 
343
477
  return await self._send_message(target_agent, outgoing)
@@ -346,7 +480,8 @@ class PeerClient:
346
480
  self,
347
481
  target: str,
348
482
  message: Dict[str, Any],
349
- timeout: float = 30.0
483
+ timeout: float = 30.0,
484
+ context: Optional[Dict[str, Any]] = None
350
485
  ) -> Optional[Dict[str, Any]]:
351
486
  """
352
487
  Send a request and wait for a response.
@@ -355,6 +490,7 @@ class PeerClient:
355
490
  target: Target agent role (e.g., "scout") or agent_id
356
491
  message: Request payload
357
492
  timeout: Max seconds to wait for response (default: 30)
493
+ context: Optional metadata (mission_id, priority, trace_id, etc.)
358
494
 
359
495
  Returns:
360
496
  Response data dict, or None if timeout/failure
@@ -363,7 +499,7 @@ class PeerClient:
363
499
  response = await self.peers.request("scout", {
364
500
  "need": "clarification",
365
501
  "entity": "Entity_X"
366
- }, timeout=10)
502
+ }, timeout=10, context={"mission_id": "abc123", "priority": "high"})
367
503
 
368
504
  if response:
369
505
  print(f"Got clarification: {response}")
@@ -382,7 +518,8 @@ class PeerClient:
382
518
  data=message,
383
519
  correlation_id=correlation_id,
384
520
  sender=self._agent_id,
385
- sender_node=self._node_id
521
+ sender_node=self._node_id,
522
+ context=context
386
523
  )
387
524
 
388
525
  # Create future to wait for response
@@ -407,13 +544,20 @@ class PeerClient:
407
544
  # Cleanup pending request
408
545
  self._pending_requests.pop(correlation_id, None)
409
546
 
410
- async def respond(self, message: IncomingMessage, response: Dict[str, Any]) -> bool:
547
+ async def respond(
548
+ self,
549
+ message: IncomingMessage,
550
+ response: Dict[str, Any],
551
+ context: Optional[Dict[str, Any]] = None
552
+ ) -> bool:
411
553
  """
412
554
  Respond to an incoming request.
413
555
 
414
556
  Args:
415
557
  message: The incoming request message
416
558
  response: Response data to send back
559
+ context: Optional metadata to include in response.
560
+ If None, automatically propagates the original request's context.
417
561
 
418
562
  Returns:
419
563
  True if response was sent successfully
@@ -421,7 +565,12 @@ class PeerClient:
421
565
  Example:
422
566
  message = await self.peers.receive()
423
567
  if message and message.is_request:
568
+ # Context is auto-propagated from the request
424
569
  await self.peers.respond(message, {"result": "done"})
570
+
571
+ # Or override with custom context
572
+ await self.peers.respond(message, {"result": "done"},
573
+ context={"status": "completed"})
425
574
  """
426
575
  if not message.correlation_id:
427
576
  self._logger.warning("Cannot respond: message has no correlation_id")
@@ -433,23 +582,32 @@ class PeerClient:
433
582
  self._logger.warning(f"Cannot respond: sender '{message.sender}' not found")
434
583
  return False
435
584
 
585
+ # Auto-propagate context from request if not overridden
586
+ response_context = context if context is not None else message.context
587
+
436
588
  outgoing = OutgoingMessage(
437
589
  target=message.sender,
438
590
  type=MessageType.RESPONSE,
439
591
  data=response,
440
592
  correlation_id=message.correlation_id,
441
593
  sender=self._agent_id,
442
- sender_node=self._node_id
594
+ sender_node=self._node_id,
595
+ context=response_context
443
596
  )
444
597
 
445
598
  return await self._send_message(target_agent, outgoing)
446
599
 
447
- async def broadcast(self, message: Dict[str, Any]) -> int:
600
+ async def broadcast(
601
+ self,
602
+ message: Dict[str, Any],
603
+ context: Optional[Dict[str, Any]] = None
604
+ ) -> int:
448
605
  """
449
606
  Broadcast notification to ALL peers.
450
607
 
451
608
  Args:
452
609
  message: Message payload to broadcast
610
+ context: Optional metadata (mission_id, priority, trace_id, etc.)
453
611
 
454
612
  Returns:
455
613
  Number of peers successfully notified
@@ -458,15 +616,213 @@ class PeerClient:
458
616
  count = await self.peers.broadcast({
459
617
  "event": "status_update",
460
618
  "status": "completed"
461
- })
619
+ }, context={"mission_id": "abc123"})
462
620
  print(f"Notified {count} peers")
463
621
  """
464
622
  count = 0
465
623
  for peer in self.discover():
466
- if await self.notify(peer.role, message):
624
+ if await self.notify(peer.role, message, context=context):
467
625
  count += 1
468
626
  return count
469
627
 
628
+ # ─────────────────────────────────────────────────────────────────
629
+ # ASYNC REQUEST PATTERN
630
+ # ─────────────────────────────────────────────────────────────────
631
+
632
+ async def ask_async(
633
+ self,
634
+ target: str,
635
+ message: Dict[str, Any],
636
+ timeout: float = 120.0,
637
+ context: Optional[Dict[str, Any]] = None
638
+ ) -> str:
639
+ """
640
+ Send a request asynchronously without blocking for response.
641
+
642
+ Unlike request(), this method returns immediately with a request_id.
643
+ Use check_inbox() to retrieve the response later.
644
+
645
+ Args:
646
+ target: Target agent role or agent_id
647
+ message: Request payload
648
+ timeout: Max time to keep request active (default: 120s)
649
+ context: Optional context metadata
650
+
651
+ Returns:
652
+ Request ID to use with check_inbox()
653
+
654
+ Raises:
655
+ ValueError: If target not found or send fails
656
+
657
+ Example:
658
+ # Fire off multiple requests in parallel
659
+ request_ids = []
660
+ for analyst in analysts:
661
+ req_id = await self.peers.ask_async(analyst, {"question": "..."})
662
+ request_ids.append(req_id)
663
+
664
+ # Do other work...
665
+ await process_other_tasks()
666
+
667
+ # Collect responses later
668
+ for req_id in request_ids:
669
+ response = await self.peers.check_inbox(req_id, timeout=5)
670
+ if response:
671
+ results.append(response)
672
+ """
673
+ target_agent = self._resolve_target(target)
674
+ if not target_agent:
675
+ raise ValueError(f"No peer found for target: {target}")
676
+
677
+ # Generate correlation ID
678
+ correlation_id = f"async-{uuid4().hex[:12]}"
679
+
680
+ # Create outgoing message
681
+ outgoing = OutgoingMessage(
682
+ target=target,
683
+ type=MessageType.REQUEST,
684
+ data=message,
685
+ correlation_id=correlation_id,
686
+ sender=self._agent_id,
687
+ sender_node=self._node_id,
688
+ context=context
689
+ )
690
+
691
+ # Initialize inbox slot
692
+ self._async_inbox[correlation_id] = None
693
+ self._async_requests[correlation_id] = {
694
+ 'target': target,
695
+ 'sent_at': time.time(),
696
+ 'timeout': timeout
697
+ }
698
+
699
+ # Create future for async response handling
700
+ response_future: asyncio.Future = asyncio.get_event_loop().create_future()
701
+ self._pending_requests[correlation_id] = response_future
702
+
703
+ # Setup background task to move response to inbox
704
+ asyncio.create_task(
705
+ self._async_response_handler(correlation_id, response_future, timeout)
706
+ )
707
+
708
+ # Send request
709
+ sent = await self._send_message(target_agent, outgoing)
710
+ if not sent:
711
+ # Cleanup on send failure
712
+ self._async_inbox.pop(correlation_id, None)
713
+ self._async_requests.pop(correlation_id, None)
714
+ self._pending_requests.pop(correlation_id, None)
715
+ raise ValueError(f"Failed to send async request to {target}")
716
+
717
+ self._logger.debug(f"Sent async request {correlation_id} to {target}")
718
+ return correlation_id
719
+
720
+ async def _async_response_handler(
721
+ self,
722
+ correlation_id: str,
723
+ future: asyncio.Future,
724
+ timeout: float
725
+ ):
726
+ """Background handler that moves response to inbox when received."""
727
+ try:
728
+ response = await asyncio.wait_for(future, timeout=timeout)
729
+ self._async_inbox[correlation_id] = response
730
+ self._logger.debug(f"Async response received for {correlation_id}")
731
+ except asyncio.TimeoutError:
732
+ self._async_inbox[correlation_id] = None # Mark as timed out
733
+ self._logger.debug(f"Async request {correlation_id} timed out")
734
+ except asyncio.CancelledError:
735
+ self._async_inbox.pop(correlation_id, None)
736
+ finally:
737
+ self._pending_requests.pop(correlation_id, None)
738
+
739
+ async def check_inbox(
740
+ self,
741
+ request_id: str,
742
+ timeout: float = 0.0,
743
+ remove: bool = True
744
+ ) -> Optional[Dict[str, Any]]:
745
+ """
746
+ Check for a response to an async request.
747
+
748
+ Args:
749
+ request_id: The ID returned by ask_async()
750
+ timeout: How long to wait for response (0 = don't wait, return immediately)
751
+ remove: Remove from inbox after reading (default: True)
752
+
753
+ Returns:
754
+ Response data dict if available, None if not ready or timed out
755
+
756
+ Example:
757
+ # Non-blocking check
758
+ response = await self.peers.check_inbox(request_id)
759
+
760
+ # Wait up to 5 seconds
761
+ response = await self.peers.check_inbox(request_id, timeout=5)
762
+ """
763
+ if request_id not in self._async_inbox:
764
+ self._logger.debug(f"Request {request_id} not found in inbox")
765
+ return None
766
+
767
+ # If response already available
768
+ response = self._async_inbox.get(request_id)
769
+ if response is not None:
770
+ if remove:
771
+ self._async_inbox.pop(request_id, None)
772
+ self._async_requests.pop(request_id, None)
773
+ return response
774
+
775
+ # If no timeout, return None immediately
776
+ if timeout <= 0:
777
+ return None
778
+
779
+ # Wait for response with timeout
780
+ start_time = time.time()
781
+ while time.time() - start_time < timeout:
782
+ await asyncio.sleep(0.1) # Check every 100ms
783
+ response = self._async_inbox.get(request_id)
784
+ if response is not None:
785
+ if remove:
786
+ self._async_inbox.pop(request_id, None)
787
+ self._async_requests.pop(request_id, None)
788
+ return response
789
+
790
+ return None
791
+
792
+ def get_pending_async_requests(self) -> List[Dict[str, Any]]:
793
+ """
794
+ Get list of pending async requests.
795
+
796
+ Returns:
797
+ List of dicts with request_id, target, sent_at, timeout
798
+
799
+ Example:
800
+ pending = self.peers.get_pending_async_requests()
801
+ for req in pending:
802
+ print(f"Waiting for {req['target']} since {req['sent_at']}")
803
+ """
804
+ return [
805
+ {
806
+ 'request_id': req_id,
807
+ **metadata
808
+ }
809
+ for req_id, metadata in self._async_requests.items()
810
+ ]
811
+
812
+ def clear_inbox(self, request_id: Optional[str] = None):
813
+ """
814
+ Clear async request inbox.
815
+
816
+ Args:
817
+ request_id: Specific request to clear, or None to clear all
818
+ """
819
+ if request_id:
820
+ self._async_inbox.pop(request_id, None)
821
+ self._async_requests.pop(request_id, None)
822
+ else:
823
+ self._async_inbox.clear()
824
+ self._async_requests.clear()
825
+
470
826
  # ─────────────────────────────────────────────────────────────────
471
827
  # TOOL ADAPTER
472
828
  # ─────────────────────────────────────────────────────────────────
@@ -767,7 +1123,8 @@ class PeerClient:
767
1123
  'target_role': target_agent.role,
768
1124
  'data': message.data,
769
1125
  'correlation_id': message.correlation_id,
770
- 'timestamp': message.timestamp
1126
+ 'timestamp': message.timestamp,
1127
+ 'context': message.context
771
1128
  }
772
1129
  result = await self._coordinator._send_p2p_message(
773
1130
  target_agent.node_id,
@@ -792,7 +1149,8 @@ class PeerClient:
792
1149
  type=message.type,
793
1150
  data=message.data,
794
1151
  correlation_id=message.correlation_id,
795
- timestamp=message.timestamp
1152
+ timestamp=message.timestamp,
1153
+ context=message.context
796
1154
  )
797
1155
  await target_agent.peers._deliver_message(incoming)
798
1156
  self._logger.debug(
@@ -809,7 +1167,8 @@ class PeerClient:
809
1167
  'target': message.target,
810
1168
  'data': message.data,
811
1169
  'correlation_id': message.correlation_id,
812
- 'timestamp': message.timestamp
1170
+ 'timestamp': message.timestamp,
1171
+ 'context': message.context
813
1172
  }
814
1173
  node_id = getattr(target_agent, 'node_id', None) or self._node_id
815
1174
  return await self._coordinator._send_p2p_message(
@@ -185,21 +185,27 @@ class PeerTool:
185
185
  if not role or not question:
186
186
  return "Error: 'role' and 'question' are required"
187
187
 
188
- # Check peer exists
189
- peer = self._peers.get_peer(role=role)
190
- if not peer:
191
- available = self._peers.list_roles()
192
- return (
193
- f"Error: '{role}' is not online. "
194
- f"Available: {', '.join(available) if available else 'none'}"
195
- )
196
-
197
- # Send request
188
+ self._logger.info(f"ask_peer: Attempting to contact role='{role}'")
189
+
190
+ # Send request (request() will handle resolution of local/remote peers)
191
+ # Use 10min timeout to allow analyst time to query database and analyze
198
192
  response = await self._peers.request(
199
193
  role,
200
194
  {"query": question, "from": self._peers.my_role},
201
- timeout=30.0
195
+ timeout=600.0
202
196
  )
197
+
198
+ self._logger.info(f"ask_peer: Got response: {response}")
199
+
200
+ # If request() returns None, peer wasn't found or didn't respond
201
+ if response is None:
202
+ peers = self._peers.list_peers()
203
+ available_roles = [p['role'] for p in peers]
204
+ self._logger.warning(f"ask_peer: request() returned None. Available peers: {available_roles}")
205
+ return (
206
+ f"Error: '{role}' not found or did not respond. "
207
+ f"Available: {', '.join(available_roles) if available_roles else 'none'}"
208
+ )
203
209
 
204
210
  if response is None:
205
211
  return f"Error: {role} did not respond (timeout)"
@@ -3,12 +3,10 @@ Execution profiles for agents.
3
3
 
4
4
  Profiles define HOW agents execute tasks:
5
5
  - AutoAgent: LLM-powered code generation + sandboxed execution
6
- - CustomAgent: User-defined logic (LangChain, MCP, raw Python)
7
- - ListenerAgent: API-first agents with background P2P listening
6
+ - CustomAgent: User-defined logic with P2P message handling
8
7
  """
9
8
 
10
9
  from .autoagent import AutoAgent
11
10
  from .customagent import CustomAgent
12
- from .listeneragent import ListenerAgent
13
11
 
14
- __all__ = ["AutoAgent", "CustomAgent", "ListenerAgent"]
12
+ __all__ = ["AutoAgent", "CustomAgent"]