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.
- examples/cloud_deployment_example.py +3 -3
- examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
- examples/customagent_distributed_example.py +140 -1
- examples/fastapi_integration_example.py +74 -11
- jarviscore/__init__.py +8 -11
- jarviscore/cli/smoketest.py +1 -1
- jarviscore/core/mesh.py +158 -0
- jarviscore/data/examples/cloud_deployment_example.py +3 -3
- jarviscore/data/examples/custom_profile_decorator.py +134 -0
- jarviscore/data/examples/custom_profile_wrap.py +168 -0
- jarviscore/data/examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +55 -14
- jarviscore/data/examples/customagent_distributed_example.py +140 -1
- jarviscore/data/examples/fastapi_integration_example.py +74 -11
- jarviscore/docs/API_REFERENCE.md +576 -47
- jarviscore/docs/CHANGELOG.md +131 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +591 -153
- jarviscore/docs/GETTING_STARTED.md +186 -329
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +292 -12
- jarviscore/integrations/fastapi.py +4 -4
- jarviscore/p2p/coordinator.py +36 -7
- jarviscore/p2p/messages.py +13 -0
- jarviscore/p2p/peer_client.py +380 -21
- jarviscore/p2p/peer_tool.py +17 -11
- jarviscore/profiles/__init__.py +2 -4
- jarviscore/profiles/customagent.py +302 -74
- jarviscore/testing/__init__.py +35 -0
- jarviscore/testing/mocks.py +578 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +61 -46
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +42 -34
- tests/test_13_dx_improvements.py +37 -37
- tests/test_15_llm_cognitive_discovery.py +18 -18
- tests/test_16_unified_dx_flow.py +3 -3
- tests/test_17_session_context.py +489 -0
- tests/test_18_mesh_diagnostics.py +465 -0
- tests/test_19_async_requests.py +516 -0
- tests/test_20_load_balancing.py +546 -0
- tests/test_21_mock_testing.py +776 -0
- jarviscore/profiles/listeneragent.py +0 -292
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
jarviscore/p2p/peer_client.py
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
jarviscore/p2p/peer_tool.py
CHANGED
|
@@ -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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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=
|
|
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)"
|
jarviscore/profiles/__init__.py
CHANGED
|
@@ -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
|
|
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"
|
|
12
|
+
__all__ = ["AutoAgent", "CustomAgent"]
|