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.
- examples/customagent_cognitive_discovery_example.py +49 -8
- examples/customagent_distributed_example.py +140 -1
- examples/fastapi_integration_example.py +70 -7
- jarviscore/__init__.py +1 -1
- jarviscore/core/mesh.py +149 -0
- jarviscore/data/examples/customagent_cognitive_discovery_example.py +49 -8
- jarviscore/data/examples/customagent_distributed_example.py +140 -1
- jarviscore/data/examples/fastapi_integration_example.py +70 -7
- jarviscore/docs/API_REFERENCE.md +547 -5
- jarviscore/docs/CHANGELOG.md +89 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +347 -2
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +286 -5
- jarviscore/p2p/coordinator.py +36 -7
- jarviscore/p2p/messages.py +13 -0
- jarviscore/p2p/peer_client.py +355 -23
- jarviscore/p2p/peer_tool.py +17 -11
- jarviscore/profiles/customagent.py +9 -2
- jarviscore/testing/__init__.py +35 -0
- jarviscore/testing/mocks.py +578 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +2 -2
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +31 -24
- 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_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
jarviscore/docs/API_REFERENCE.md
CHANGED
|
@@ -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
|
-
- `
|
|
747
|
-
- `
|
|
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.
|
|
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.
|
|
1965
|
+
API Reference for JarvisCore v0.3.2
|
|
1424
1966
|
|
|
1425
|
-
Last Updated: 2026-
|
|
1967
|
+
Last Updated: 2026-02-03
|
jarviscore/docs/CHANGELOG.md
CHANGED
|
@@ -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
|