jarviscore-framework 0.3.1__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 (31) hide show
  1. examples/customagent_cognitive_discovery_example.py +49 -8
  2. examples/customagent_distributed_example.py +140 -1
  3. examples/fastapi_integration_example.py +70 -7
  4. jarviscore/__init__.py +1 -1
  5. jarviscore/core/mesh.py +149 -0
  6. jarviscore/data/examples/customagent_cognitive_discovery_example.py +49 -8
  7. jarviscore/data/examples/customagent_distributed_example.py +140 -1
  8. jarviscore/data/examples/fastapi_integration_example.py +70 -7
  9. jarviscore/docs/API_REFERENCE.md +547 -5
  10. jarviscore/docs/CHANGELOG.md +89 -0
  11. jarviscore/docs/CONFIGURATION.md +1 -1
  12. jarviscore/docs/CUSTOMAGENT_GUIDE.md +347 -2
  13. jarviscore/docs/TROUBLESHOOTING.md +1 -1
  14. jarviscore/docs/USER_GUIDE.md +286 -5
  15. jarviscore/p2p/coordinator.py +36 -7
  16. jarviscore/p2p/messages.py +13 -0
  17. jarviscore/p2p/peer_client.py +355 -23
  18. jarviscore/p2p/peer_tool.py +17 -11
  19. jarviscore/profiles/customagent.py +9 -2
  20. jarviscore/testing/__init__.py +35 -0
  21. jarviscore/testing/mocks.py +578 -0
  22. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +2 -2
  23. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +31 -24
  24. tests/test_17_session_context.py +489 -0
  25. tests/test_18_mesh_diagnostics.py +465 -0
  26. tests/test_19_async_requests.py +516 -0
  27. tests/test_20_load_balancing.py +546 -0
  28. tests/test_21_mock_testing.py +776 -0
  29. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
  30. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
  31. {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
@@ -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)"
@@ -181,15 +181,22 @@ class CustomAgent(Profile):
181
181
  Handles:
182
182
  - REQUEST messages: calls on_peer_request, sends response if auto_respond=True
183
183
  - NOTIFY messages: calls on_peer_notify
184
+ - RESPONSE messages: ignored (handled by _deliver_message resolving futures)
184
185
  """
185
186
  from jarviscore.p2p.messages import MessageType
186
187
 
187
188
  try:
189
+ # Skip RESPONSE messages - they should be handled by pending request futures
190
+ if msg.type == MessageType.RESPONSE:
191
+ self._logger.debug(
192
+ f"[{self.role}] Ignoring orphaned RESPONSE from {msg.sender} (no pending request)"
193
+ )
194
+ return
195
+
188
196
  # Check if this is a request (expects response)
189
197
  is_request = (
190
198
  msg.type == MessageType.REQUEST or
191
- getattr(msg, 'is_request', False) or
192
- msg.correlation_id is not None
199
+ getattr(msg, 'is_request', False)
193
200
  )
194
201
 
195
202
  if is_request:
@@ -0,0 +1,35 @@
1
+ """
2
+ Testing utilities for JarvisCore.
3
+
4
+ Provides mock implementations for unit testing agents without
5
+ requiring real P2P infrastructure or network connections.
6
+
7
+ Example:
8
+ from jarviscore.testing import MockMesh, MockPeerClient
9
+
10
+ # Using MockMesh for full integration testing
11
+ mesh = MockMesh()
12
+ mesh.add(MyAgent)
13
+ await mesh.start()
14
+
15
+ agent = mesh.get_agent("my_role")
16
+ agent.peers.set_mock_response("analyst", {"result": "test"})
17
+
18
+ # Test and verify
19
+ await agent.process_request(...)
20
+ agent.peers.assert_requested("analyst")
21
+
22
+ # Using MockPeerClient for unit testing
23
+ agent = MyAgent()
24
+ agent.peers = MockPeerClient(mock_peers=[
25
+ {"role": "analyst", "capabilities": ["analysis"]}
26
+ ])
27
+ """
28
+
29
+ from .mocks import MockMesh, MockPeerClient, MockPeerInfo
30
+
31
+ __all__ = [
32
+ 'MockMesh',
33
+ 'MockPeerClient',
34
+ 'MockPeerInfo',
35
+ ]
@@ -0,0 +1,578 @@
1
+ """
2
+ Mock implementations for testing JarvisCore agents.
3
+
4
+ These mocks allow testing agent logic without:
5
+ - Real ZMQ connections
6
+ - SWIM protocol
7
+ - Network operations
8
+ - Multiple processes
9
+
10
+ Example:
11
+ from jarviscore.testing import MockMesh, MockPeerClient
12
+
13
+ # Create mock mesh with simulated peers
14
+ mesh = MockMesh()
15
+ mesh.add(MyAgent)
16
+ await mesh.start()
17
+
18
+ # Inject mock peer client for testing
19
+ agent = mesh.get_agent("my_role")
20
+ agent.peers.set_mock_response("analyst", {"result": "test"})
21
+
22
+ # Test agent behavior
23
+ response = await agent.peers.request("analyst", {"question": "test"})
24
+ assert response == {"result": "test"}
25
+
26
+ # Verify interactions
27
+ agent.peers.assert_requested("analyst")
28
+ """
29
+ import asyncio
30
+ import time
31
+ from typing import List, Dict, Any, Optional, Callable
32
+ from dataclasses import dataclass, field
33
+ from uuid import uuid4
34
+
35
+ from jarviscore.core.agent import Agent
36
+ from jarviscore.p2p.messages import PeerInfo, IncomingMessage, MessageType
37
+
38
+
39
+ @dataclass
40
+ class MockPeerInfo:
41
+ """Mock peer for testing discovery."""
42
+ role: str
43
+ capabilities: List[str] = field(default_factory=list)
44
+ agent_id: str = ""
45
+ node_id: str = "mock-node"
46
+ status: str = "alive"
47
+ description: str = ""
48
+
49
+ def __post_init__(self):
50
+ if not self.agent_id:
51
+ self.agent_id = f"{self.role}-{uuid4().hex[:8]}"
52
+
53
+
54
+ class MockPeerClient:
55
+ """
56
+ Mock PeerClient for unit testing.
57
+
58
+ Simulates peer discovery and messaging without real P2P.
59
+ Can be configured with mock responses for testing specific scenarios.
60
+
61
+ Example:
62
+ client = MockPeerClient(
63
+ mock_peers=[
64
+ {"role": "analyst", "capabilities": ["analysis"]},
65
+ {"role": "scout", "capabilities": ["research"]}
66
+ ]
67
+ )
68
+
69
+ # Configure mock response
70
+ client.set_mock_response("analyst", {"result": "test data"})
71
+
72
+ # Now test your agent
73
+ response = await agent.peers.request("analyst", {"question": "..."})
74
+ assert response == {"result": "test data"}
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ agent_id: str = "mock-agent",
80
+ agent_role: str = "mock",
81
+ mock_peers: List[Dict[str, Any]] = None,
82
+ auto_respond: bool = True
83
+ ):
84
+ """
85
+ Initialize MockPeerClient.
86
+
87
+ Args:
88
+ agent_id: ID for the mock agent
89
+ agent_role: Role for the mock agent
90
+ mock_peers: List of peer definitions with role, capabilities
91
+ auto_respond: If True, automatically respond to requests with mock data
92
+ """
93
+ self._agent_id = agent_id
94
+ self._agent_role = agent_role
95
+ self._auto_respond = auto_respond
96
+
97
+ # Build mock peer registry
98
+ self._mock_peers: List[MockPeerInfo] = []
99
+ if mock_peers:
100
+ for peer_def in mock_peers:
101
+ self._mock_peers.append(MockPeerInfo(
102
+ role=peer_def.get("role", "unknown"),
103
+ capabilities=peer_def.get("capabilities", []),
104
+ agent_id=peer_def.get("agent_id", ""),
105
+ node_id=peer_def.get("node_id", "mock-node"),
106
+ description=peer_def.get("description", "")
107
+ ))
108
+
109
+ # Mock responses for request()
110
+ self._mock_responses: Dict[str, Dict[str, Any]] = {}
111
+ self._default_response: Dict[str, Any] = {"status": "success", "mock": True}
112
+
113
+ # Message tracking for assertions
114
+ self._sent_notifications: List[Dict[str, Any]] = []
115
+ self._sent_requests: List[Dict[str, Any]] = []
116
+ self._sent_broadcasts: List[Dict[str, Any]] = []
117
+
118
+ # Message queue for receive()
119
+ self._message_queue: asyncio.Queue = asyncio.Queue()
120
+
121
+ # Request handler for custom responses
122
+ self._request_handler: Optional[Callable] = None
123
+
124
+ # Load balancing state (for strategy support)
125
+ self._round_robin_index: Dict[str, int] = {}
126
+ self._peer_last_used: Dict[str, float] = {}
127
+
128
+ # Identity properties
129
+ @property
130
+ def my_role(self) -> str:
131
+ return self._agent_role
132
+
133
+ @property
134
+ def my_id(self) -> str:
135
+ return self._agent_id
136
+
137
+ # Discovery methods
138
+ def get_peer(self, role: str) -> Optional[PeerInfo]:
139
+ """Get mock peer by role."""
140
+ for peer in self._mock_peers:
141
+ if peer.role == role:
142
+ return PeerInfo(
143
+ agent_id=peer.agent_id,
144
+ role=peer.role,
145
+ capabilities=peer.capabilities,
146
+ node_id=peer.node_id,
147
+ status=peer.status
148
+ )
149
+ return None
150
+
151
+ def discover(
152
+ self,
153
+ capability: str = None,
154
+ role: str = None,
155
+ strategy: str = "first"
156
+ ) -> List[PeerInfo]:
157
+ """Discover mock peers with strategy support."""
158
+ results = []
159
+ for peer in self._mock_peers:
160
+ if role and peer.role != role:
161
+ continue
162
+ if capability and capability not in peer.capabilities:
163
+ continue
164
+ results.append(PeerInfo(
165
+ agent_id=peer.agent_id,
166
+ role=peer.role,
167
+ capabilities=peer.capabilities,
168
+ node_id=peer.node_id,
169
+ status=peer.status
170
+ ))
171
+
172
+ # Apply strategy if needed
173
+ if results and strategy != "first":
174
+ import random
175
+ key = capability or role or "all"
176
+
177
+ if strategy == "random":
178
+ random.shuffle(results)
179
+ elif strategy == "round_robin":
180
+ idx = self._round_robin_index.get(key, 0)
181
+ results = results[idx:] + results[:idx]
182
+ self._round_robin_index[key] = (idx + 1) % len(results) if results else 0
183
+ elif strategy == "least_recent":
184
+ results.sort(key=lambda p: self._peer_last_used.get(p.agent_id, 0.0))
185
+
186
+ return results
187
+
188
+ def discover_one(
189
+ self,
190
+ capability: str = None,
191
+ role: str = None,
192
+ strategy: str = "first"
193
+ ) -> Optional[PeerInfo]:
194
+ """Discover single mock peer."""
195
+ peers = self.discover(capability=capability, role=role, strategy=strategy)
196
+ return peers[0] if peers else None
197
+
198
+ def record_peer_usage(self, peer_id: str):
199
+ """Record peer usage for least_recent strategy."""
200
+ self._peer_last_used[peer_id] = time.time()
201
+
202
+ def list_roles(self) -> List[str]:
203
+ """List available mock roles."""
204
+ return list(set(p.role for p in self._mock_peers))
205
+
206
+ def list_peers(self) -> List[Dict[str, Any]]:
207
+ """List all mock peers."""
208
+ return [
209
+ {
210
+ "role": p.role,
211
+ "agent_id": p.agent_id,
212
+ "capabilities": p.capabilities,
213
+ "status": p.status,
214
+ "location": "mock"
215
+ }
216
+ for p in self._mock_peers
217
+ ]
218
+
219
+ # Messaging methods
220
+ async def notify(
221
+ self,
222
+ target: str,
223
+ message: Dict[str, Any],
224
+ context: Optional[Dict[str, Any]] = None
225
+ ) -> bool:
226
+ """Send mock notification (tracked for assertions)."""
227
+ self._sent_notifications.append({
228
+ "target": target,
229
+ "message": message,
230
+ "context": context,
231
+ "timestamp": time.time()
232
+ })
233
+ return True
234
+
235
+ async def request(
236
+ self,
237
+ target: str,
238
+ message: Dict[str, Any],
239
+ timeout: float = 30.0,
240
+ context: Optional[Dict[str, Any]] = None
241
+ ) -> Optional[Dict[str, Any]]:
242
+ """Send mock request and return configured response."""
243
+ self._sent_requests.append({
244
+ "target": target,
245
+ "message": message,
246
+ "context": context,
247
+ "timeout": timeout,
248
+ "timestamp": time.time()
249
+ })
250
+
251
+ # Use custom handler if set
252
+ if self._request_handler:
253
+ return await self._request_handler(target, message, context)
254
+
255
+ # Return configured mock response
256
+ if target in self._mock_responses:
257
+ return self._mock_responses[target]
258
+
259
+ if self._auto_respond:
260
+ return self._default_response
261
+
262
+ return None
263
+
264
+ async def respond(
265
+ self,
266
+ message: IncomingMessage,
267
+ response: Dict[str, Any],
268
+ context: Optional[Dict[str, Any]] = None
269
+ ) -> bool:
270
+ """Mock respond (no-op, returns True)."""
271
+ return True
272
+
273
+ async def broadcast(
274
+ self,
275
+ message: Dict[str, Any],
276
+ context: Optional[Dict[str, Any]] = None
277
+ ) -> int:
278
+ """Send mock broadcast (tracked for assertions)."""
279
+ self._sent_broadcasts.append({
280
+ "message": message,
281
+ "context": context,
282
+ "timestamp": time.time()
283
+ })
284
+ return len(self._mock_peers)
285
+
286
+ async def receive(self, timeout: float = None) -> Optional[IncomingMessage]:
287
+ """Receive from mock message queue."""
288
+ try:
289
+ if timeout:
290
+ return await asyncio.wait_for(
291
+ self._message_queue.get(),
292
+ timeout=timeout
293
+ )
294
+ return self._message_queue.get_nowait()
295
+ except (asyncio.TimeoutError, asyncio.QueueEmpty):
296
+ return None
297
+
298
+ def has_pending_messages(self) -> bool:
299
+ """Check mock message queue."""
300
+ return not self._message_queue.empty()
301
+
302
+ # Async request methods (Feature 2 compatibility)
303
+ async def ask_async(
304
+ self,
305
+ target: str,
306
+ message: Dict[str, Any],
307
+ timeout: float = 120.0,
308
+ context: Optional[Dict[str, Any]] = None
309
+ ) -> str:
310
+ """Mock async request."""
311
+ correlation_id = f"mock-{uuid4().hex[:12]}"
312
+ self._sent_requests.append({
313
+ "target": target,
314
+ "message": message,
315
+ "context": context,
316
+ "correlation_id": correlation_id,
317
+ "async": True,
318
+ "timestamp": time.time()
319
+ })
320
+ return correlation_id
321
+
322
+ async def check_inbox(
323
+ self,
324
+ request_id: str,
325
+ timeout: float = 0.0,
326
+ remove: bool = True
327
+ ) -> Optional[Dict[str, Any]]:
328
+ """Mock inbox check - returns default response."""
329
+ if self._auto_respond:
330
+ return self._default_response
331
+ return None
332
+
333
+ def get_pending_async_requests(self) -> List[Dict[str, Any]]:
334
+ """Get pending async requests (always empty for mock)."""
335
+ return []
336
+
337
+ def clear_inbox(self, request_id: Optional[str] = None):
338
+ """Clear inbox (no-op for mock)."""
339
+ pass
340
+
341
+ # Cognitive context (Feature 4 compatibility)
342
+ def get_cognitive_context(
343
+ self,
344
+ format: str = "markdown",
345
+ include_capabilities: bool = True,
346
+ include_description: bool = True,
347
+ tool_name: str = "ask_peer"
348
+ ) -> str:
349
+ """Generate mock cognitive context."""
350
+ if not self._mock_peers:
351
+ return ""
352
+
353
+ lines = ["## AVAILABLE MESH PEERS (Mock)", ""]
354
+ for peer in self._mock_peers:
355
+ lines.append(f"- **{peer.role}** (`{peer.agent_id}`)")
356
+ if include_capabilities and peer.capabilities:
357
+ lines.append(f" - Capabilities: {', '.join(peer.capabilities)}")
358
+ if include_description and peer.description:
359
+ lines.append(f" - {peer.description}")
360
+ lines.append("")
361
+ lines.append(f"Use the `{tool_name}` tool to communicate with these peers.")
362
+
363
+ return "\n".join(lines)
364
+
365
+ # Test configuration methods
366
+ def set_mock_response(self, target: str, response: Dict[str, Any]):
367
+ """Configure response for a specific target."""
368
+ self._mock_responses[target] = response
369
+
370
+ def set_default_response(self, response: Dict[str, Any]):
371
+ """Set default response for all requests."""
372
+ self._default_response = response
373
+
374
+ def set_request_handler(self, handler: Callable):
375
+ """
376
+ Set custom request handler.
377
+
378
+ Args:
379
+ handler: Async function(target, message, context) -> response
380
+ """
381
+ self._request_handler = handler
382
+
383
+ def add_mock_peer(self, role: str, capabilities: List[str] = None, **kwargs):
384
+ """Add a mock peer dynamically."""
385
+ self._mock_peers.append(MockPeerInfo(
386
+ role=role,
387
+ capabilities=capabilities or [],
388
+ **kwargs
389
+ ))
390
+
391
+ def inject_message(
392
+ self,
393
+ sender: str,
394
+ message_type: MessageType,
395
+ data: Dict[str, Any],
396
+ correlation_id: str = None,
397
+ context: Optional[Dict[str, Any]] = None
398
+ ):
399
+ """
400
+ Inject a message into the receive queue for testing.
401
+
402
+ Example:
403
+ client.inject_message(
404
+ sender="analyst",
405
+ message_type=MessageType.NOTIFY,
406
+ data={"event": "analysis_complete", "result": {...}}
407
+ )
408
+
409
+ # Agent receives the injected message
410
+ msg = await agent.peers.receive()
411
+ """
412
+ incoming = IncomingMessage(
413
+ sender=sender,
414
+ sender_node="mock-node",
415
+ type=message_type,
416
+ data=data,
417
+ correlation_id=correlation_id,
418
+ context=context
419
+ )
420
+ self._message_queue.put_nowait(incoming)
421
+
422
+ # Assertion helpers
423
+ def get_sent_notifications(self) -> List[Dict[str, Any]]:
424
+ """Get all notifications sent during test."""
425
+ return self._sent_notifications.copy()
426
+
427
+ def get_sent_requests(self) -> List[Dict[str, Any]]:
428
+ """Get all requests sent during test."""
429
+ return self._sent_requests.copy()
430
+
431
+ def get_sent_broadcasts(self) -> List[Dict[str, Any]]:
432
+ """Get all broadcasts sent during test."""
433
+ return self._sent_broadcasts.copy()
434
+
435
+ def assert_notified(self, target: str, message_contains: Dict[str, Any] = None):
436
+ """Assert that a notification was sent to target."""
437
+ for notif in self._sent_notifications:
438
+ if notif["target"] == target:
439
+ if message_contains:
440
+ for key, value in message_contains.items():
441
+ if notif["message"].get(key) != value:
442
+ continue
443
+ return True
444
+ raise AssertionError(f"Expected notification to {target} not found")
445
+
446
+ def assert_requested(self, target: str, message_contains: Dict[str, Any] = None):
447
+ """Assert that a request was sent to target."""
448
+ for req in self._sent_requests:
449
+ if req["target"] == target:
450
+ if message_contains:
451
+ for key, value in message_contains.items():
452
+ if req["message"].get(key) != value:
453
+ continue
454
+ return True
455
+ raise AssertionError(f"Expected request to {target} not found")
456
+
457
+ def assert_broadcasted(self, message_contains: Dict[str, Any] = None):
458
+ """Assert that a broadcast was sent."""
459
+ if not self._sent_broadcasts:
460
+ raise AssertionError("No broadcasts sent")
461
+ if message_contains:
462
+ for broadcast in self._sent_broadcasts:
463
+ match = all(
464
+ broadcast["message"].get(k) == v
465
+ for k, v in message_contains.items()
466
+ )
467
+ if match:
468
+ return True
469
+ raise AssertionError(f"Broadcast with {message_contains} not found")
470
+ return True
471
+
472
+ def reset(self):
473
+ """Clear all tracking state."""
474
+ self._sent_notifications.clear()
475
+ self._sent_requests.clear()
476
+ self._sent_broadcasts.clear()
477
+ self._mock_responses.clear()
478
+ self._round_robin_index.clear()
479
+ self._peer_last_used.clear()
480
+ while not self._message_queue.empty():
481
+ try:
482
+ self._message_queue.get_nowait()
483
+ except asyncio.QueueEmpty:
484
+ break
485
+
486
+
487
+ class MockMesh:
488
+ """
489
+ Mock Mesh for unit testing.
490
+
491
+ Provides agent registration and setup without P2P infrastructure.
492
+
493
+ Example:
494
+ mesh = MockMesh()
495
+ mesh.add(MyAgent)
496
+ await mesh.start()
497
+
498
+ agent = mesh.get_agent("my_role")
499
+ # Test agent behavior...
500
+ """
501
+
502
+ def __init__(self, mode: str = "p2p"):
503
+ self.mode = mode
504
+ self.agents: List[Agent] = []
505
+ self._agent_registry: Dict[str, List[Agent]] = {}
506
+ self._started = False
507
+
508
+ def add(self, agent_class_or_instance, agent_id: str = None, **kwargs) -> Agent:
509
+ """Register agent with mock mesh."""
510
+ if isinstance(agent_class_or_instance, Agent):
511
+ agent = agent_class_or_instance
512
+ else:
513
+ agent = agent_class_or_instance(agent_id=agent_id, **kwargs)
514
+
515
+ agent._mesh = self
516
+ self.agents.append(agent)
517
+
518
+ if agent.role not in self._agent_registry:
519
+ self._agent_registry[agent.role] = []
520
+ self._agent_registry[agent.role].append(agent)
521
+
522
+ return agent
523
+
524
+ async def start(self):
525
+ """Start mock mesh (runs agent setup)."""
526
+ for agent in self.agents:
527
+ await agent.setup()
528
+ # Inject mock peer client
529
+ agent.peers = MockPeerClient(
530
+ agent_id=agent.agent_id,
531
+ agent_role=agent.role,
532
+ mock_peers=self._build_peer_list(agent)
533
+ )
534
+ self._started = True
535
+
536
+ async def stop(self):
537
+ """Stop mock mesh."""
538
+ for agent in self.agents:
539
+ await agent.teardown()
540
+ self._started = False
541
+
542
+ def get_agent(self, role: str) -> Optional[Agent]:
543
+ """Get agent by role."""
544
+ agents = self._agent_registry.get(role, [])
545
+ return agents[0] if agents else None
546
+
547
+ def _build_peer_list(self, exclude_agent: Agent) -> List[Dict[str, Any]]:
548
+ """Build peer list excluding the specified agent."""
549
+ peers = []
550
+ for agent in self.agents:
551
+ if agent.agent_id != exclude_agent.agent_id:
552
+ peers.append({
553
+ "role": agent.role,
554
+ "agent_id": agent.agent_id,
555
+ "capabilities": list(agent.capabilities),
556
+ "description": getattr(agent, 'description', '')
557
+ })
558
+ return peers
559
+
560
+ def get_diagnostics(self) -> Dict[str, Any]:
561
+ """Get mock diagnostics."""
562
+ return {
563
+ "local_node": {
564
+ "mode": self.mode,
565
+ "started": self._started,
566
+ "agent_count": len(self.agents)
567
+ },
568
+ "known_peers": [],
569
+ "local_agents": [
570
+ {
571
+ "role": a.role,
572
+ "agent_id": a.agent_id,
573
+ "capabilities": list(a.capabilities)
574
+ }
575
+ for a in self.agents
576
+ ],
577
+ "connectivity_status": "mock"
578
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarviscore-framework
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Build autonomous AI agents in 3 lines of code. Production-ready orchestration with P2P mesh networking.
5
5
  Author-email: Ruth Mutua <mutuandinda82@gmail.com>, Muyukani Kizito <muyukani@prescottdata.io>
6
6
  Maintainer-email: Prescott Data <info@prescottdata.io>
@@ -185,7 +185,7 @@ python -c "import jarviscore; print(jarviscore.__path__[0] + '/docs')"
185
185
 
186
186
  ## Version
187
187
 
188
- **0.4.0**
188
+ **0.3.2**
189
189
 
190
190
  ## License
191
191