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
@@ -148,6 +148,50 @@ await mesh.run_forever() # Blocks until SIGINT/SIGTERM
148
148
 
149
149
  ---
150
150
 
151
+ #### `get_diagnostics() -> dict` (v0.3.2)
152
+
153
+ Get diagnostic information about mesh health and P2P connectivity.
154
+
155
+ ```python
156
+ diag = mesh.get_diagnostics()
157
+ print(f"Status: {diag['connectivity_status']}")
158
+ print(f"Agents: {len(diag['local_agents'])}")
159
+
160
+ for peer in diag['known_peers']:
161
+ print(f" {peer['role']} at {peer['node_id']}: {peer['status']}")
162
+ ```
163
+
164
+ **Returns:**
165
+ ```python
166
+ {
167
+ "local_node": {
168
+ "mode": "p2p", # Current mesh mode
169
+ "started": True, # Whether mesh is started
170
+ "agent_count": 3, # Number of local agents
171
+ "bind_address": "127.0.0.1:7950" # P2P bind address (if P2P)
172
+ },
173
+ "known_peers": [ # Remote peers (if P2P enabled)
174
+ {"role": "analyst", "node_id": "10.0.0.2:7950", "status": "alive"}
175
+ ],
176
+ "local_agents": [ # Local agent info
177
+ {"role": "scout", "agent_id": "scout-abc", "capabilities": ["research"]}
178
+ ],
179
+ "connectivity_status": "healthy", # Overall health
180
+ "keepalive_status": {...}, # Keepalive manager status (P2P only)
181
+ "swim_status": {...}, # SWIM protocol status (P2P only)
182
+ "capability_map": {...} # Capability to agent mapping (P2P only)
183
+ }
184
+ ```
185
+
186
+ **Connectivity Status Values:**
187
+ - `"healthy"` - P2P active with connected peers
188
+ - `"isolated"` - P2P active but no peers found
189
+ - `"degraded"` - Some connectivity issues detected
190
+ - `"not_started"` - Mesh not yet started
191
+ - `"local_only"` - Autonomous mode (no P2P)
192
+
193
+ ---
194
+
151
195
  ### Agent
152
196
 
153
197
  Base class for all agents. Inherit from this to create custom agents.
@@ -664,6 +708,223 @@ Client for peer-to-peer communication, available as `self.peers` on agents.
664
708
 
665
709
  **Methods:**
666
710
 
711
+ ---
712
+
713
+ ### Discovery Methods (v0.3.2)
714
+
715
+ #### `discover(capability=None, role=None, strategy="first") -> List[PeerInfo]`
716
+
717
+ Discover peers with optional load balancing strategy.
718
+
719
+ ```python
720
+ # Default: return in discovery order
721
+ peers = self.peers.discover(role="worker")
722
+
723
+ # Random: shuffle for load distribution
724
+ peers = self.peers.discover(role="worker", strategy="random")
725
+
726
+ # Round robin: rotate through peers on each call
727
+ peers = self.peers.discover(role="worker", strategy="round_robin")
728
+
729
+ # Least recent: return least recently used peers first
730
+ peers = self.peers.discover(role="worker", strategy="least_recent")
731
+ ```
732
+
733
+ **Parameters:**
734
+ - `capability` (str, optional): Filter by capability
735
+ - `role` (str, optional): Filter by role
736
+ - `strategy` (str): Selection strategy - `"first"`, `"random"`, `"round_robin"`, `"least_recent"`
737
+
738
+ **Returns:** List of PeerInfo objects ordered by strategy
739
+
740
+ ---
741
+
742
+ #### `discover_one(capability=None, role=None, strategy="first") -> Optional[PeerInfo]`
743
+
744
+ Discover a single peer (convenience wrapper for discover).
745
+
746
+ ```python
747
+ worker = self.peers.discover_one(role="worker", strategy="round_robin")
748
+ if worker:
749
+ await self.peers.request(worker.agent_id, {"task": "..."})
750
+ ```
751
+
752
+ **Returns:** Single PeerInfo or None if no match
753
+
754
+ ---
755
+
756
+ #### `record_peer_usage(peer_id: str)`
757
+
758
+ Record that a peer was used (for `least_recent` strategy tracking).
759
+
760
+ ```python
761
+ peer = self.peers.discover_one(role="worker", strategy="least_recent")
762
+ response = await self.peers.request(peer.agent_id, {"task": "..."})
763
+ self.peers.record_peer_usage(peer.agent_id) # Update usage timestamp
764
+ ```
765
+
766
+ ---
767
+
768
+ ### Messaging Methods (v0.3.2 - Context Support)
769
+
770
+ #### `async notify(target, message, context=None) -> bool`
771
+
772
+ Send a fire-and-forget notification with optional context.
773
+
774
+ ```python
775
+ await self.peers.notify("analyst", {
776
+ "event": "task_complete",
777
+ "data": result
778
+ }, context={"mission_id": "m-123", "priority": "high"})
779
+ ```
780
+
781
+ **Parameters:**
782
+ - `target` (str): Target agent role or agent_id
783
+ - `message` (dict): Message payload
784
+ - `context` (dict, optional): Metadata (mission_id, priority, trace_id, etc.)
785
+
786
+ ---
787
+
788
+ #### `async request(target, message, timeout=30.0, context=None) -> Optional[dict]`
789
+
790
+ Send request and wait for response with optional context.
791
+
792
+ ```python
793
+ response = await self.peers.request("analyst", {
794
+ "query": "analyze this data"
795
+ }, timeout=10, context={"mission_id": "m-123"})
796
+ ```
797
+
798
+ **Parameters:**
799
+ - `target` (str): Target agent role or agent_id
800
+ - `message` (dict): Request payload
801
+ - `timeout` (float): Max seconds to wait (default: 30)
802
+ - `context` (dict, optional): Metadata propagated with request
803
+
804
+ ---
805
+
806
+ #### `async respond(message, response, context=None) -> bool`
807
+
808
+ Respond to an incoming request. Auto-propagates context if not overridden.
809
+
810
+ ```python
811
+ async def on_peer_request(self, msg):
812
+ result = process(msg.data)
813
+ # Context auto-propagated from msg.context
814
+ await self.peers.respond(msg, {"result": result})
815
+
816
+ # Or override with custom context
817
+ await self.peers.respond(msg, {"result": result},
818
+ context={"status": "completed"})
819
+ ```
820
+
821
+ **Parameters:**
822
+ - `message` (IncomingMessage): The incoming request
823
+ - `response` (dict): Response data
824
+ - `context` (dict, optional): Override context (defaults to request's context)
825
+
826
+ ---
827
+
828
+ #### `async broadcast(message, context=None) -> int`
829
+
830
+ Broadcast notification to all peers with optional context.
831
+
832
+ ```python
833
+ count = await self.peers.broadcast({
834
+ "event": "status_update",
835
+ "status": "ready"
836
+ }, context={"broadcast_id": "bc-123"})
837
+ ```
838
+
839
+ **Returns:** Number of peers notified
840
+
841
+ ---
842
+
843
+ ### Async Request Pattern (v0.3.2)
844
+
845
+ #### `async ask_async(target, message, timeout=120.0, context=None) -> str`
846
+
847
+ Send request without blocking for response. Returns request_id immediately.
848
+
849
+ ```python
850
+ # Fire off multiple requests in parallel
851
+ request_ids = []
852
+ for analyst in analysts:
853
+ req_id = await self.peers.ask_async(analyst, {"task": "analyze"})
854
+ request_ids.append(req_id)
855
+
856
+ # Do other work while waiting...
857
+ await process_other_tasks()
858
+
859
+ # Collect responses later
860
+ for req_id in request_ids:
861
+ response = await self.peers.check_inbox(req_id, timeout=5)
862
+ ```
863
+
864
+ **Parameters:**
865
+ - `target` (str): Target agent role or agent_id
866
+ - `message` (dict): Request payload
867
+ - `timeout` (float): Max time to keep request active (default: 120s)
868
+ - `context` (dict, optional): Request context
869
+
870
+ **Returns:** Request ID string for use with `check_inbox()`
871
+
872
+ **Raises:** `ValueError` if target not found or send fails
873
+
874
+ ---
875
+
876
+ #### `async check_inbox(request_id, timeout=0.0, remove=True) -> Optional[dict]`
877
+
878
+ Check for response to an async request.
879
+
880
+ ```python
881
+ # Non-blocking check
882
+ response = await self.peers.check_inbox(req_id)
883
+
884
+ # Wait up to 5 seconds
885
+ response = await self.peers.check_inbox(req_id, timeout=5)
886
+
887
+ # Peek without removing
888
+ response = await self.peers.check_inbox(req_id, remove=False)
889
+ ```
890
+
891
+ **Parameters:**
892
+ - `request_id` (str): ID returned by `ask_async()`
893
+ - `timeout` (float): Seconds to wait (0 = immediate return)
894
+ - `remove` (bool): Remove from inbox after reading (default: True)
895
+
896
+ **Returns:** Response dict if available, None if not ready or timed out
897
+
898
+ ---
899
+
900
+ #### `get_pending_async_requests() -> List[dict]`
901
+
902
+ Get list of pending async requests.
903
+
904
+ ```python
905
+ pending = self.peers.get_pending_async_requests()
906
+ for req in pending:
907
+ print(f"Waiting for {req['target']} since {req['sent_at']}")
908
+ ```
909
+
910
+ **Returns:** List of dicts with `request_id`, `target`, `sent_at`, `timeout`
911
+
912
+ ---
913
+
914
+ #### `clear_inbox(request_id=None)`
915
+
916
+ Clear async request inbox.
917
+
918
+ ```python
919
+ # Clear specific request
920
+ self.peers.clear_inbox(req_id)
921
+
922
+ # Clear all
923
+ self.peers.clear_inbox()
924
+ ```
925
+
926
+ ---
927
+
667
928
  #### `get_cognitive_context() -> str`
668
929
 
669
930
  Generate LLM-ready text describing available peers.
@@ -742,16 +1003,28 @@ Message received from a peer.
742
1003
  #### Class: `IncomingMessage`
743
1004
 
744
1005
  **Attributes:**
1006
+ - `sender` (str): Agent ID of the sender
1007
+ - `sender_node` (str): P2P node ID of the sender
1008
+ - `type` (MessageType): Message type (NOTIFY, REQUEST, RESPONSE)
745
1009
  - `data` (dict): Message payload
746
- - `sender_role` (str): Role of sending agent
747
- - `sender_id` (str): ID of sending agent
1010
+ - `correlation_id` (str, optional): ID linking request to response
1011
+ - `timestamp` (float): When the message was sent
1012
+ - `context` (dict, optional): Metadata (mission_id, priority, trace_id, etc.) - *v0.3.2*
1013
+
1014
+ **Properties:**
748
1015
  - `is_request` (bool): True if this is a request expecting response
749
1016
  - `is_notify` (bool): True if this is a notification
750
1017
 
751
1018
  ```python
752
1019
  async def on_peer_request(self, msg):
753
- print(f"From: {msg.sender_role}")
1020
+ print(f"From: {msg.sender}")
754
1021
  print(f"Data: {msg.data}")
1022
+
1023
+ # Access context metadata (v0.3.2)
1024
+ if msg.context:
1025
+ mission_id = msg.context.get("mission_id")
1026
+ priority = msg.context.get("priority")
1027
+
755
1028
  return {"received": True}
756
1029
  ```
757
1030
 
@@ -1418,8 +1691,277 @@ async def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
1418
1691
 
1419
1692
  ---
1420
1693
 
1694
+ ## Testing Utilities (v0.3.2)
1695
+
1696
+ ### MockMesh
1697
+
1698
+ Simplified mesh for unit testing without real P2P infrastructure.
1699
+
1700
+ #### Class: `MockMesh`
1701
+
1702
+ ```python
1703
+ from jarviscore.testing import MockMesh
1704
+
1705
+ mesh = MockMesh(mode="p2p")
1706
+ mesh.add(MyAgent)
1707
+ await mesh.start()
1708
+
1709
+ agent = mesh.get_agent("my_role")
1710
+ # Test agent behavior...
1711
+
1712
+ await mesh.stop()
1713
+ ```
1714
+
1715
+ **Methods:**
1716
+
1717
+ #### `add(agent_class_or_instance, agent_id=None) -> Agent`
1718
+
1719
+ Register an agent with the mock mesh.
1720
+
1721
+ ```python
1722
+ mesh.add(MyAgent) # Add class
1723
+ mesh.add(my_instance) # Add instance
1724
+ ```
1725
+
1726
+ ---
1727
+
1728
+ #### `async start()`
1729
+
1730
+ Start the mock mesh. Runs agent setup and injects MockPeerClient into each agent.
1731
+
1732
+ ---
1733
+
1734
+ #### `async stop()`
1735
+
1736
+ Stop the mock mesh and run agent teardown.
1737
+
1738
+ ---
1739
+
1740
+ #### `get_agent(role) -> Optional[Agent]`
1741
+
1742
+ Get agent by role.
1743
+
1744
+ ```python
1745
+ agent = mesh.get_agent("analyst")
1746
+ ```
1747
+
1748
+ ---
1749
+
1750
+ #### `get_diagnostics() -> dict`
1751
+
1752
+ Get mock diagnostics (compatible structure with real Mesh).
1753
+
1754
+ ```python
1755
+ diag = mesh.get_diagnostics()
1756
+ assert diag["connectivity_status"] == "mock"
1757
+ ```
1758
+
1759
+ ---
1760
+
1761
+ ### MockPeerClient
1762
+
1763
+ Full mock replacement for PeerClient with test configuration and assertions.
1764
+
1765
+ #### Class: `MockPeerClient`
1766
+
1767
+ ```python
1768
+ from jarviscore.testing import MockPeerClient
1769
+
1770
+ client = MockPeerClient(
1771
+ agent_id="test-agent",
1772
+ agent_role="tester",
1773
+ mock_peers=[
1774
+ {"role": "analyst", "capabilities": ["analysis"]},
1775
+ {"role": "scout", "capabilities": ["research"]}
1776
+ ],
1777
+ auto_respond=True
1778
+ )
1779
+ ```
1780
+
1781
+ **Parameters:**
1782
+ - `agent_id` (str): ID for the mock agent
1783
+ - `agent_role` (str): Role for the mock agent
1784
+ - `mock_peers` (list): List of peer definitions with role, capabilities
1785
+ - `auto_respond` (bool): Auto-respond to requests with mock data (default: True)
1786
+
1787
+ ---
1788
+
1789
+ #### Configuration Methods
1790
+
1791
+ #### `set_mock_response(target, response)`
1792
+
1793
+ Configure response for a specific target.
1794
+
1795
+ ```python
1796
+ client.set_mock_response("analyst", {"result": "analysis complete", "score": 95})
1797
+ response = await client.request("analyst", {"query": "test"})
1798
+ assert response["score"] == 95
1799
+ ```
1800
+
1801
+ ---
1802
+
1803
+ #### `set_default_response(response)`
1804
+
1805
+ Set default response for unconfigured targets.
1806
+
1807
+ ```python
1808
+ client.set_default_response({"status": "success", "mock": True})
1809
+ ```
1810
+
1811
+ ---
1812
+
1813
+ #### `set_request_handler(handler)`
1814
+
1815
+ Set custom async handler for dynamic responses.
1816
+
1817
+ ```python
1818
+ async def custom_handler(target, message, context):
1819
+ return {"echo": message.get("query"), "target": target}
1820
+
1821
+ client.set_request_handler(custom_handler)
1822
+ ```
1823
+
1824
+ ---
1825
+
1826
+ #### `add_mock_peer(role, capabilities=None, **kwargs)`
1827
+
1828
+ Add a mock peer dynamically.
1829
+
1830
+ ```python
1831
+ client.add_mock_peer("reporter", capabilities=["reporting", "formatting"])
1832
+ ```
1833
+
1834
+ ---
1835
+
1836
+ #### `inject_message(sender, message_type, data, correlation_id=None, context=None)`
1837
+
1838
+ Inject a message into the receive queue for testing message handlers.
1839
+
1840
+ ```python
1841
+ from jarviscore.p2p.messages import MessageType
1842
+
1843
+ client.inject_message(
1844
+ sender="external_agent",
1845
+ message_type=MessageType.NOTIFY,
1846
+ data={"event": "test_event", "value": 42},
1847
+ context={"mission_id": "m-123"}
1848
+ )
1849
+
1850
+ msg = await client.receive(timeout=1)
1851
+ assert msg.data["value"] == 42
1852
+ ```
1853
+
1854
+ ---
1855
+
1856
+ #### Assertion Helpers
1857
+
1858
+ #### `assert_notified(target, message_contains=None)`
1859
+
1860
+ Assert a notification was sent to target.
1861
+
1862
+ ```python
1863
+ await client.notify("analyst", {"event": "done"})
1864
+ client.assert_notified("analyst")
1865
+ client.assert_notified("analyst", message_contains={"event": "done"})
1866
+ ```
1867
+
1868
+ ---
1869
+
1870
+ #### `assert_requested(target, message_contains=None)`
1871
+
1872
+ Assert a request was sent to target.
1873
+
1874
+ ```python
1875
+ await client.request("analyst", {"query": "test"})
1876
+ client.assert_requested("analyst")
1877
+ ```
1878
+
1879
+ ---
1880
+
1881
+ #### `assert_broadcasted(message_contains=None)`
1882
+
1883
+ Assert a broadcast was sent.
1884
+
1885
+ ```python
1886
+ await client.broadcast({"alert": "important"})
1887
+ client.assert_broadcasted()
1888
+ client.assert_broadcasted(message_contains={"alert": "important"})
1889
+ ```
1890
+
1891
+ ---
1892
+
1893
+ #### Tracking Methods
1894
+
1895
+ #### `get_sent_notifications() -> List[dict]`
1896
+
1897
+ Get all notifications sent during test.
1898
+
1899
+ ---
1900
+
1901
+ #### `get_sent_requests() -> List[dict]`
1902
+
1903
+ Get all requests sent during test.
1904
+
1905
+ ---
1906
+
1907
+ #### `get_sent_broadcasts() -> List[dict]`
1908
+
1909
+ Get all broadcasts sent during test.
1910
+
1911
+ ---
1912
+
1913
+ #### `reset()`
1914
+
1915
+ Clear all tracking state (notifications, requests, broadcasts, mock responses).
1916
+
1917
+ ```python
1918
+ client.reset()
1919
+ assert len(client.get_sent_notifications()) == 0
1920
+ ```
1921
+
1922
+ ---
1923
+
1924
+ ### Testing Pattern Example
1925
+
1926
+ ```python
1927
+ import pytest
1928
+ from jarviscore.testing import MockMesh
1929
+ from jarviscore.profiles import CustomAgent
1930
+
1931
+ class MyAgent(CustomAgent):
1932
+ role = "processor"
1933
+ capabilities = ["processing"]
1934
+
1935
+ async def on_peer_request(self, msg):
1936
+ # Ask analyst for help
1937
+ analysis = await self.peers.request("analyst", {"data": msg.data})
1938
+ return {"processed": True, "analysis": analysis}
1939
+
1940
+ @pytest.mark.asyncio
1941
+ async def test_processor_delegates_to_analyst():
1942
+ mesh = MockMesh()
1943
+ mesh.add(MyAgent)
1944
+ await mesh.start()
1945
+
1946
+ processor = mesh.get_agent("processor")
1947
+
1948
+ # Configure mock response
1949
+ processor.peers.set_mock_response("analyst", {"result": "analyzed"})
1950
+
1951
+ # Test the flow
1952
+ response = await processor.peers.request("analyst", {"test": "data"})
1953
+
1954
+ # Verify
1955
+ assert response["result"] == "analyzed"
1956
+ processor.peers.assert_requested("analyst")
1957
+
1958
+ await mesh.stop()
1959
+ ```
1960
+
1961
+ ---
1962
+
1421
1963
  ## Version
1422
1964
 
1423
- API Reference for JarvisCore v0.3.1
1965
+ API Reference for JarvisCore v0.3.2
1424
1966
 
1425
- Last Updated: 2026-01-29
1967
+ Last Updated: 2026-02-03
@@ -7,6 +7,95 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.3.2] - 2026-02-03
11
+
12
+ ### Added
13
+
14
+ #### Session Context Propagation
15
+ - Added `context` parameter to `notify()`, `request()`, `respond()`, and `broadcast()` methods
16
+ - Context carries metadata like mission_id, priority, trace_id across message flows
17
+ - `respond()` automatically propagates context from request if not overridden
18
+ - `IncomingMessage.context` accessible in all message handlers
19
+
20
+ ```python
21
+ # Send request with context
22
+ response = await peers.request("analyst", {"q": "..."}, context={"mission_id": "abc"})
23
+
24
+ # Access context in handler
25
+ async def on_peer_request(self, msg):
26
+ mission_id = msg.context.get("mission_id") # Available!
27
+ return {"result": "..."}
28
+ ```
29
+
30
+ #### Mesh Diagnostics
31
+ - Added `mesh.get_diagnostics()` method for mesh health monitoring
32
+ - Returns: `local_node`, `known_peers`, `local_agents`, `connectivity_status`
33
+ - Connectivity status values: `healthy`, `isolated`, `degraded`, `not_started`, `local_only`
34
+ - Includes SWIM and keepalive status when P2P is enabled
35
+
36
+ ```python
37
+ diag = mesh.get_diagnostics()
38
+ print(diag["connectivity_status"]) # "healthy", "isolated", etc.
39
+ ```
40
+
41
+ #### Async Request Pattern
42
+ - Added `ask_async(target, message, timeout, context)` - returns request_id immediately
43
+ - Added `check_inbox(request_id, timeout, remove)` - returns response or None
44
+ - Added `get_pending_async_requests()` - list pending async requests
45
+ - Added `clear_inbox(request_id)` - clear specific or all inbox entries
46
+
47
+ ```python
48
+ # Fire off multiple requests
49
+ req_ids = [await peers.ask_async(a, {"q": "..."}) for a in analysts]
50
+
51
+ # Do other work...
52
+ await process_other_tasks()
53
+
54
+ # Collect responses later
55
+ for req_id in req_ids:
56
+ response = await peers.check_inbox(req_id, timeout=5)
57
+ ```
58
+
59
+ #### Load Balancing Strategies
60
+ - Added `strategy` parameter to `discover()`: `"first"`, `"random"`, `"round_robin"`, `"least_recent"`
61
+ - Added `discover_one()` convenience method for single peer lookup
62
+ - Added `record_peer_usage(peer_id)` for least_recent tracking
63
+
64
+ ```python
65
+ # Round-robin across workers
66
+ worker = peers.discover_one(role="worker", strategy="round_robin")
67
+
68
+ # Least recently used analyst
69
+ analyst = peers.discover_one(role="analyst", strategy="least_recent")
70
+ ```
71
+
72
+ #### MockMesh Testing Utilities
73
+ - Created `jarviscore.testing` module
74
+ - `MockPeerClient`: Full mock with discovery, messaging, assertion helpers
75
+ - `MockMesh`: Simplified mesh without real P2P infrastructure
76
+ - Auto-injects MockPeerClient into agents during MockMesh.start()
77
+
78
+ ```python
79
+ from jarviscore.testing import MockMesh, MockPeerClient
80
+
81
+ mesh = MockMesh()
82
+ mesh.add(MyAgent)
83
+ await mesh.start()
84
+
85
+ agent = mesh.get_agent("my_role")
86
+ agent.peers.set_mock_response("analyst", {"result": "test"})
87
+ agent.peers.assert_requested("analyst")
88
+ ```
89
+
90
+ ### Testing
91
+ - Session context propagation through all messaging methods
92
+ - Mesh diagnostics structure and connectivity status values
93
+ - Async request/response flow with check_inbox
94
+ - Load balancing strategies (first, random, round_robin, least_recent)
95
+ - MockMesh and MockPeerClient functionality and assertion helpers
96
+
97
+ ---
98
+
10
99
  ## [0.3.1] - 2026-02-02
11
100
 
12
101
  ### Breaking Changes
@@ -766,6 +766,6 @@ LOG_DIRECTORY=/tmp/jarviscore-logs
766
766
 
767
767
  ## Version
768
768
 
769
- Configuration Guide for JarvisCore v0.3.1
769
+ Configuration Guide for JarvisCore v0.3.2
770
770
 
771
771
  Last Updated: 2026-01-23