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
@@ -14,7 +14,6 @@ Complete API documentation for JarvisCore framework components.
14
14
  - [AutoAgent](#autoagent)
15
15
  - [Custom Profile](#custom-profile)
16
16
  - [CustomAgent](#customagent)
17
- - [ListenerAgent (v0.3.0)](#listeneragent-v030)
18
17
  3. [P2P Communication (v0.3.0)](#p2p-communication-v030)
19
18
  - [PeerClient](#peerclient)
20
19
  - [IncomingMessage](#incomingmessage)
@@ -149,6 +148,50 @@ await mesh.run_forever() # Blocks until SIGINT/SIGTERM
149
148
 
150
149
  ---
151
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
+
152
195
  ### Agent
153
196
 
154
197
  Base class for all agents. Inherit from this to create custom agents.
@@ -564,33 +607,9 @@ See [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for P2P and distributed mode detai
564
607
 
565
608
  ---
566
609
 
567
- ### ListenerAgent (v0.3.0)
568
-
569
- Handler-based agent for P2P communication without manual run loops.
570
-
571
- #### Class: `ListenerAgent(CustomAgent)`
572
-
573
- ```python
574
- from jarviscore.profiles import ListenerAgent
575
-
576
- class MyAgent(ListenerAgent):
577
- role = "processor"
578
- capabilities = ["processing"]
579
-
580
- async def on_peer_request(self, msg):
581
- """Handle incoming requests from peers."""
582
- return {"result": msg.data.get("task", "").upper()}
583
-
584
- async def on_peer_notify(self, msg):
585
- """Handle broadcast notifications."""
586
- print(f"Notification: {msg.data}")
587
- ```
588
-
589
- **Class Attributes:**
590
- - `role` (str): Agent role identifier (required)
591
- - `capabilities` (list): List of capability strings (required)
610
+ #### P2P Message Handlers
592
611
 
593
- **Handler Methods:**
612
+ CustomAgent includes built-in P2P message handlers:
594
613
 
595
614
  #### `async on_peer_request(msg) -> dict`
596
615
 
@@ -604,9 +623,9 @@ async def on_peer_request(self, msg):
604
623
  ```
605
624
 
606
625
  **Parameters:**
607
- - `msg` (IncomingMessage): Incoming message with `data`, `sender_role`, `sender_id`
626
+ - `msg` (IncomingMessage): Incoming message with `data`, `sender`, `correlation_id`
608
627
 
609
- **Returns:** dict - Response sent back to requester
628
+ **Returns:** dict - Response sent back to requester (if `auto_respond=True`)
610
629
 
611
630
  ---
612
631
 
@@ -628,7 +647,29 @@ async def on_peer_notify(self, msg):
628
647
 
629
648
  ---
630
649
 
631
- **Self-Registration Methods (v0.3.0):**
650
+ #### `async on_error(error, msg) -> None`
651
+
652
+ Handle errors during message processing.
653
+
654
+ ```python
655
+ async def on_error(self, error, msg):
656
+ self._logger.error(f"Error processing message: {error}")
657
+ ```
658
+
659
+ **Parameters:**
660
+ - `error` (Exception): The exception that occurred
661
+ - `msg` (IncomingMessage, optional): The message being processed
662
+
663
+ ---
664
+
665
+ #### Configuration Attributes
666
+
667
+ - `listen_timeout` (float): Seconds to wait for messages in run loop (default: 1.0)
668
+ - `auto_respond` (bool): Automatically send on_peer_request return value (default: True)
669
+
670
+ ---
671
+
672
+ #### Self-Registration Methods
632
673
 
633
674
  #### `async join_mesh(seed_nodes, advertise_endpoint=None)`
634
675
 
@@ -657,25 +698,232 @@ await agent.leave_mesh()
657
698
 
658
699
  ---
659
700
 
660
- #### `async serve_forever()`
701
+ ## P2P Communication (v0.3.0)
702
+
703
+ ### PeerClient
704
+
705
+ Client for peer-to-peer communication, available as `self.peers` on agents.
706
+
707
+ #### Class: `PeerClient`
708
+
709
+ **Methods:**
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]`
661
743
 
662
- Run the agent until shutdown signal.
744
+ Discover a single peer (convenience wrapper for discover).
663
745
 
664
746
  ```python
665
- await agent.serve_forever()
747
+ worker = self.peers.discover_one(role="worker", strategy="round_robin")
748
+ if worker:
749
+ await self.peers.request(worker.agent_id, {"task": "..."})
666
750
  ```
667
751
 
752
+ **Returns:** Single PeerInfo or None if no match
753
+
668
754
  ---
669
755
 
670
- ## P2P Communication (v0.3.0)
756
+ #### `record_peer_usage(peer_id: str)`
671
757
 
672
- ### PeerClient
758
+ Record that a peer was used (for `least_recent` strategy tracking).
673
759
 
674
- Client for peer-to-peer communication, available as `self.peers` on agents.
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
+ ```
675
765
 
676
- #### Class: `PeerClient`
766
+ ---
677
767
 
678
- **Methods:**
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
+ ---
679
927
 
680
928
  #### `get_cognitive_context() -> str`
681
929
 
@@ -755,16 +1003,28 @@ Message received from a peer.
755
1003
  #### Class: `IncomingMessage`
756
1004
 
757
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)
758
1009
  - `data` (dict): Message payload
759
- - `sender_role` (str): Role of sending agent
760
- - `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:**
761
1015
  - `is_request` (bool): True if this is a request expecting response
762
1016
  - `is_notify` (bool): True if this is a notification
763
1017
 
764
1018
  ```python
765
1019
  async def on_peer_request(self, msg):
766
- print(f"From: {msg.sender_role}")
1020
+ print(f"From: {msg.sender}")
767
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
+
768
1028
  return {"received": True}
769
1029
  ```
770
1030
 
@@ -811,7 +1071,7 @@ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p"))
811
1071
  ```
812
1072
 
813
1073
  **Parameters:**
814
- - `agent`: Agent instance (ListenerAgent or CustomAgent)
1074
+ - `agent`: CustomAgent instance (or list of agents)
815
1075
  - `mode` (str): "p2p" or "distributed"
816
1076
  - `bind_port` (int, optional): P2P port (default: 7950)
817
1077
  - `seed_nodes` (str, optional): Comma-separated seed node addresses
@@ -820,11 +1080,11 @@ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p"))
820
1080
 
821
1081
  ```python
822
1082
  from fastapi import FastAPI
823
- from jarviscore.profiles import ListenerAgent
1083
+ from jarviscore.profiles import CustomAgent
824
1084
  from jarviscore.integrations.fastapi import JarvisLifespan
825
1085
 
826
1086
 
827
- class ProcessorAgent(ListenerAgent):
1087
+ class ProcessorAgent(CustomAgent):
828
1088
  role = "processor"
829
1089
  capabilities = ["processing"]
830
1090
 
@@ -845,7 +1105,7 @@ async def process(data: dict):
845
1105
  **Handles:**
846
1106
  - Agent setup and teardown
847
1107
  - Mesh initialization
848
- - Background run loop (for ListenerAgent)
1108
+ - Background run loop (runs agent.run())
849
1109
  - Graceful shutdown
850
1110
 
851
1111
  ---
@@ -1431,8 +1691,277 @@ async def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
1431
1691
 
1432
1692
  ---
1433
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
+
1434
1963
  ## Version
1435
1964
 
1436
- API Reference for JarvisCore v0.3.0
1965
+ API Reference for JarvisCore v0.3.2
1437
1966
 
1438
- Last Updated: 2026-01-29
1967
+ Last Updated: 2026-02-03