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
|
@@ -21,6 +21,11 @@ CustomAgent lets you integrate your **existing agent code** with JarvisCore's ne
|
|
|
21
21
|
10. [Multi-Node Deployment](#multi-node-deployment)
|
|
22
22
|
11. [Error Handling](#error-handling)
|
|
23
23
|
12. [Troubleshooting](#troubleshooting)
|
|
24
|
+
13. [Session Context Propagation (v0.3.2)](#session-context-propagation-v032) - Request tracking and metadata
|
|
25
|
+
14. [Async Request Pattern (v0.3.2)](#async-request-pattern-v032) - Non-blocking parallel requests
|
|
26
|
+
15. [Load Balancing Strategies (v0.3.2)](#load-balancing-strategies-v032) - Round-robin and random selection
|
|
27
|
+
16. [Mesh Diagnostics (v0.3.2)](#mesh-diagnostics-v032) - Health monitoring and debugging
|
|
28
|
+
17. [Testing with MockMesh (v0.3.2)](#testing-with-mockmesh-v032) - Unit testing patterns
|
|
24
29
|
|
|
25
30
|
---
|
|
26
31
|
|
|
@@ -1926,6 +1931,346 @@ if agent.peers:
|
|
|
1926
1931
|
|
|
1927
1932
|
---
|
|
1928
1933
|
|
|
1934
|
+
## Session Context Propagation (v0.3.2)
|
|
1935
|
+
|
|
1936
|
+
Pass metadata (mission IDs, trace IDs, priorities) through message flows:
|
|
1937
|
+
|
|
1938
|
+
### Sending Context
|
|
1939
|
+
|
|
1940
|
+
```python
|
|
1941
|
+
# All messaging methods accept context parameter
|
|
1942
|
+
await self.peers.notify("logger", {"event": "started"},
|
|
1943
|
+
context={"mission_id": "m-123", "trace_id": "t-abc"})
|
|
1944
|
+
|
|
1945
|
+
response = await self.peers.request("analyst", {"query": "..."},
|
|
1946
|
+
context={"priority": "high", "user_id": "u-456"})
|
|
1947
|
+
|
|
1948
|
+
await self.peers.broadcast({"alert": "ready"},
|
|
1949
|
+
context={"source": "coordinator"})
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
### Receiving Context
|
|
1953
|
+
|
|
1954
|
+
```python
|
|
1955
|
+
async def on_peer_request(self, msg):
|
|
1956
|
+
# Context is available on the message
|
|
1957
|
+
mission_id = msg.context.get("mission_id") if msg.context else None
|
|
1958
|
+
trace_id = msg.context.get("trace_id") if msg.context else None
|
|
1959
|
+
|
|
1960
|
+
self._logger.info(f"Request for mission {mission_id}, trace {trace_id}")
|
|
1961
|
+
|
|
1962
|
+
return {"result": "processed"}
|
|
1963
|
+
```
|
|
1964
|
+
|
|
1965
|
+
### Auto-Propagation in respond()
|
|
1966
|
+
|
|
1967
|
+
Context automatically propagates from request to response:
|
|
1968
|
+
|
|
1969
|
+
```python
|
|
1970
|
+
async def on_peer_request(self, msg):
|
|
1971
|
+
# msg.context = {"mission_id": "m-123", "trace_id": "t-abc"}
|
|
1972
|
+
result = await self.process(msg.data)
|
|
1973
|
+
|
|
1974
|
+
# Context auto-propagates - original sender receives same context
|
|
1975
|
+
await self.peers.respond(msg, {"result": result})
|
|
1976
|
+
|
|
1977
|
+
# Override if needed
|
|
1978
|
+
await self.peers.respond(msg, {"result": result},
|
|
1979
|
+
context={"status": "completed", "mission_id": msg.context.get("mission_id")})
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
---
|
|
1983
|
+
|
|
1984
|
+
## Async Request Pattern (v0.3.2)
|
|
1985
|
+
|
|
1986
|
+
Fire multiple requests without blocking, collect responses later:
|
|
1987
|
+
|
|
1988
|
+
### Fire-and-Collect Pattern
|
|
1989
|
+
|
|
1990
|
+
```python
|
|
1991
|
+
async def parallel_analysis(self, data_chunks):
|
|
1992
|
+
# Fire off requests to all available analysts
|
|
1993
|
+
analysts = self.peers.discover(role="analyst")
|
|
1994
|
+
request_ids = []
|
|
1995
|
+
|
|
1996
|
+
for i, (analyst, chunk) in enumerate(zip(analysts, data_chunks)):
|
|
1997
|
+
req_id = await self.peers.ask_async(
|
|
1998
|
+
analyst.agent_id,
|
|
1999
|
+
{"chunk_id": i, "data": chunk},
|
|
2000
|
+
context={"batch_id": "batch-001"}
|
|
2001
|
+
)
|
|
2002
|
+
request_ids.append((req_id, analyst.agent_id))
|
|
2003
|
+
|
|
2004
|
+
# Do other work while analysts process
|
|
2005
|
+
await self.update_status("processing")
|
|
2006
|
+
|
|
2007
|
+
# Collect results
|
|
2008
|
+
results = []
|
|
2009
|
+
for req_id, analyst_id in request_ids:
|
|
2010
|
+
response = await self.peers.check_inbox(req_id, timeout=30)
|
|
2011
|
+
if response:
|
|
2012
|
+
results.append(response)
|
|
2013
|
+
else:
|
|
2014
|
+
self._logger.warning(f"Timeout waiting for {analyst_id}")
|
|
2015
|
+
|
|
2016
|
+
return results
|
|
2017
|
+
```
|
|
2018
|
+
|
|
2019
|
+
### API Methods
|
|
2020
|
+
|
|
2021
|
+
```python
|
|
2022
|
+
# Fire async request - returns immediately with request_id
|
|
2023
|
+
req_id = await self.peers.ask_async(target, message, timeout=120, context=None)
|
|
2024
|
+
|
|
2025
|
+
# Check for response
|
|
2026
|
+
response = await self.peers.check_inbox(req_id, timeout=0) # Non-blocking
|
|
2027
|
+
response = await self.peers.check_inbox(req_id, timeout=10) # Wait up to 10s
|
|
2028
|
+
|
|
2029
|
+
# Manage pending requests
|
|
2030
|
+
pending = self.peers.get_pending_async_requests()
|
|
2031
|
+
self.peers.clear_inbox(req_id) # Clear specific
|
|
2032
|
+
self.peers.clear_inbox() # Clear all
|
|
2033
|
+
```
|
|
2034
|
+
|
|
2035
|
+
---
|
|
2036
|
+
|
|
2037
|
+
## Load Balancing Strategies (v0.3.2)
|
|
2038
|
+
|
|
2039
|
+
Distribute work across multiple peers:
|
|
2040
|
+
|
|
2041
|
+
### Discovery Strategies
|
|
2042
|
+
|
|
2043
|
+
```python
|
|
2044
|
+
# Default: first in discovery order (deterministic)
|
|
2045
|
+
workers = self.peers.discover(role="worker", strategy="first")
|
|
2046
|
+
|
|
2047
|
+
# Random: shuffle for basic distribution
|
|
2048
|
+
workers = self.peers.discover(role="worker", strategy="random")
|
|
2049
|
+
|
|
2050
|
+
# Round-robin: rotate through workers on each call
|
|
2051
|
+
workers = self.peers.discover(role="worker", strategy="round_robin")
|
|
2052
|
+
|
|
2053
|
+
# Least-recent: prefer workers not used recently
|
|
2054
|
+
workers = self.peers.discover(role="worker", strategy="least_recent")
|
|
2055
|
+
```
|
|
2056
|
+
|
|
2057
|
+
### discover_one() Convenience
|
|
2058
|
+
|
|
2059
|
+
```python
|
|
2060
|
+
# Get single peer with strategy applied
|
|
2061
|
+
worker = self.peers.discover_one(role="worker", strategy="round_robin")
|
|
2062
|
+
if worker:
|
|
2063
|
+
response = await self.peers.request(worker.agent_id, {"task": "..."})
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
### Tracking Usage for least_recent
|
|
2067
|
+
|
|
2068
|
+
```python
|
|
2069
|
+
# Track usage to influence least_recent ordering
|
|
2070
|
+
worker = self.peers.discover_one(role="worker", strategy="least_recent")
|
|
2071
|
+
response = await self.peers.request(worker.agent_id, {"task": "..."})
|
|
2072
|
+
self.peers.record_peer_usage(worker.agent_id) # Update timestamp
|
|
2073
|
+
```
|
|
2074
|
+
|
|
2075
|
+
### Example: Load-Balanced Task Distribution
|
|
2076
|
+
|
|
2077
|
+
```python
|
|
2078
|
+
class Coordinator(CustomAgent):
|
|
2079
|
+
role = "coordinator"
|
|
2080
|
+
capabilities = ["coordination"]
|
|
2081
|
+
|
|
2082
|
+
async def distribute_work(self, tasks):
|
|
2083
|
+
results = []
|
|
2084
|
+
for task in tasks:
|
|
2085
|
+
# Round-robin automatically rotates through workers
|
|
2086
|
+
worker = self.peers.discover_one(
|
|
2087
|
+
capability="processing",
|
|
2088
|
+
strategy="round_robin"
|
|
2089
|
+
)
|
|
2090
|
+
if worker:
|
|
2091
|
+
response = await self.peers.request(
|
|
2092
|
+
worker.agent_id,
|
|
2093
|
+
{"task": task}
|
|
2094
|
+
)
|
|
2095
|
+
results.append(response)
|
|
2096
|
+
return results
|
|
2097
|
+
```
|
|
2098
|
+
|
|
2099
|
+
---
|
|
2100
|
+
|
|
2101
|
+
## Mesh Diagnostics (v0.3.2)
|
|
2102
|
+
|
|
2103
|
+
Monitor mesh health for debugging and operations:
|
|
2104
|
+
|
|
2105
|
+
### Getting Diagnostics
|
|
2106
|
+
|
|
2107
|
+
```python
|
|
2108
|
+
# From mesh
|
|
2109
|
+
diag = mesh.get_diagnostics()
|
|
2110
|
+
|
|
2111
|
+
# Structure:
|
|
2112
|
+
# {
|
|
2113
|
+
# "local_node": {
|
|
2114
|
+
# "mode": "p2p",
|
|
2115
|
+
# "started": True,
|
|
2116
|
+
# "agent_count": 3,
|
|
2117
|
+
# "bind_address": "127.0.0.1:7950"
|
|
2118
|
+
# },
|
|
2119
|
+
# "known_peers": [
|
|
2120
|
+
# {"role": "analyst", "node_id": "10.0.0.2:7950", "status": "alive"}
|
|
2121
|
+
# ],
|
|
2122
|
+
# "local_agents": [
|
|
2123
|
+
# {"role": "coordinator", "agent_id": "...", "capabilities": [...]}
|
|
2124
|
+
# ],
|
|
2125
|
+
# "connectivity_status": "healthy"
|
|
2126
|
+
# }
|
|
2127
|
+
```
|
|
2128
|
+
|
|
2129
|
+
### Connectivity Status
|
|
2130
|
+
|
|
2131
|
+
| Status | Meaning |
|
|
2132
|
+
|--------|---------|
|
|
2133
|
+
| `healthy` | P2P active, peers connected |
|
|
2134
|
+
| `isolated` | P2P active, no peers found |
|
|
2135
|
+
| `degraded` | Some connectivity issues |
|
|
2136
|
+
| `not_started` | Mesh not started yet |
|
|
2137
|
+
| `local_only` | Autonomous mode (no P2P) |
|
|
2138
|
+
|
|
2139
|
+
### FastAPI Health Endpoint
|
|
2140
|
+
|
|
2141
|
+
```python
|
|
2142
|
+
from fastapi import FastAPI, Request
|
|
2143
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
2144
|
+
|
|
2145
|
+
app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p"))
|
|
2146
|
+
|
|
2147
|
+
@app.get("/health")
|
|
2148
|
+
async def health(request: Request):
|
|
2149
|
+
mesh = request.app.state.jarvis_mesh
|
|
2150
|
+
diag = mesh.get_diagnostics()
|
|
2151
|
+
return {
|
|
2152
|
+
"status": diag["connectivity_status"],
|
|
2153
|
+
"agents": diag["local_node"]["agent_count"],
|
|
2154
|
+
"peers": len(diag.get("known_peers", []))
|
|
2155
|
+
}
|
|
2156
|
+
```
|
|
2157
|
+
|
|
2158
|
+
---
|
|
2159
|
+
|
|
2160
|
+
## Testing with MockMesh (v0.3.2)
|
|
2161
|
+
|
|
2162
|
+
Unit test agents without real P2P infrastructure:
|
|
2163
|
+
|
|
2164
|
+
### Basic Test Setup
|
|
2165
|
+
|
|
2166
|
+
```python
|
|
2167
|
+
import pytest
|
|
2168
|
+
from jarviscore.testing import MockMesh, MockPeerClient
|
|
2169
|
+
from jarviscore.profiles import CustomAgent
|
|
2170
|
+
|
|
2171
|
+
class AnalystAgent(CustomAgent):
|
|
2172
|
+
role = "analyst"
|
|
2173
|
+
capabilities = ["analysis"]
|
|
2174
|
+
|
|
2175
|
+
async def on_peer_request(self, msg):
|
|
2176
|
+
return {"analysis": f"Analyzed: {msg.data.get('query')}"}
|
|
2177
|
+
|
|
2178
|
+
@pytest.mark.asyncio
|
|
2179
|
+
async def test_analyst_responds():
|
|
2180
|
+
mesh = MockMesh()
|
|
2181
|
+
mesh.add(AnalystAgent)
|
|
2182
|
+
await mesh.start()
|
|
2183
|
+
|
|
2184
|
+
analyst = mesh.get_agent("analyst")
|
|
2185
|
+
|
|
2186
|
+
# Inject a test message
|
|
2187
|
+
from jarviscore.p2p.messages import MessageType
|
|
2188
|
+
analyst.peers.inject_message(
|
|
2189
|
+
sender="tester",
|
|
2190
|
+
message_type=MessageType.REQUEST,
|
|
2191
|
+
data={"query": "test data"},
|
|
2192
|
+
correlation_id="test-123"
|
|
2193
|
+
)
|
|
2194
|
+
|
|
2195
|
+
# Receive and verify
|
|
2196
|
+
msg = await analyst.peers.receive(timeout=1)
|
|
2197
|
+
assert msg is not None
|
|
2198
|
+
assert msg.data["query"] == "test data"
|
|
2199
|
+
|
|
2200
|
+
await mesh.stop()
|
|
2201
|
+
```
|
|
2202
|
+
|
|
2203
|
+
### Mocking Peer Responses
|
|
2204
|
+
|
|
2205
|
+
```python
|
|
2206
|
+
@pytest.mark.asyncio
|
|
2207
|
+
async def test_coordinator_delegates():
|
|
2208
|
+
class CoordinatorAgent(CustomAgent):
|
|
2209
|
+
role = "coordinator"
|
|
2210
|
+
capabilities = ["coordination"]
|
|
2211
|
+
|
|
2212
|
+
async def on_peer_request(self, msg):
|
|
2213
|
+
# This agent delegates to analyst
|
|
2214
|
+
analysis = await self.peers.request("analyst", {"data": msg.data})
|
|
2215
|
+
return {"coordinated": True, "analysis": analysis}
|
|
2216
|
+
|
|
2217
|
+
mesh = MockMesh()
|
|
2218
|
+
mesh.add(CoordinatorAgent)
|
|
2219
|
+
await mesh.start()
|
|
2220
|
+
|
|
2221
|
+
coordinator = mesh.get_agent("coordinator")
|
|
2222
|
+
|
|
2223
|
+
# Mock the analyst response
|
|
2224
|
+
coordinator.peers.add_mock_peer("analyst", capabilities=["analysis"])
|
|
2225
|
+
coordinator.peers.set_mock_response("analyst", {"result": "mocked analysis"})
|
|
2226
|
+
|
|
2227
|
+
# Test the flow
|
|
2228
|
+
response = await coordinator.peers.request("analyst", {"test": "data"})
|
|
2229
|
+
|
|
2230
|
+
assert response["result"] == "mocked analysis"
|
|
2231
|
+
coordinator.peers.assert_requested("analyst")
|
|
2232
|
+
|
|
2233
|
+
await mesh.stop()
|
|
2234
|
+
```
|
|
2235
|
+
|
|
2236
|
+
### Assertion Helpers
|
|
2237
|
+
|
|
2238
|
+
```python
|
|
2239
|
+
# Verify notifications were sent
|
|
2240
|
+
agent.peers.assert_notified("target_role")
|
|
2241
|
+
agent.peers.assert_notified("target", message_contains={"event": "completed"})
|
|
2242
|
+
|
|
2243
|
+
# Verify requests were sent
|
|
2244
|
+
agent.peers.assert_requested("analyst")
|
|
2245
|
+
agent.peers.assert_requested("analyst", message_contains={"query": "test"})
|
|
2246
|
+
|
|
2247
|
+
# Verify broadcasts
|
|
2248
|
+
agent.peers.assert_broadcasted()
|
|
2249
|
+
agent.peers.assert_broadcasted(message_contains={"alert": "important"})
|
|
2250
|
+
|
|
2251
|
+
# Access sent messages for custom assertions
|
|
2252
|
+
notifications = agent.peers.get_sent_notifications()
|
|
2253
|
+
requests = agent.peers.get_sent_requests()
|
|
2254
|
+
broadcasts = agent.peers.get_sent_broadcasts()
|
|
2255
|
+
|
|
2256
|
+
# Reset between tests
|
|
2257
|
+
agent.peers.reset()
|
|
2258
|
+
```
|
|
2259
|
+
|
|
2260
|
+
### Custom Response Handler
|
|
2261
|
+
|
|
2262
|
+
```python
|
|
2263
|
+
async def dynamic_handler(target, message, context):
|
|
2264
|
+
"""Return different responses based on message content."""
|
|
2265
|
+
if "urgent" in message.get("query", ""):
|
|
2266
|
+
return {"priority": "high", "result": "fast response"}
|
|
2267
|
+
return {"priority": "normal", "result": "standard response"}
|
|
2268
|
+
|
|
2269
|
+
agent.peers.set_request_handler(dynamic_handler)
|
|
2270
|
+
```
|
|
2271
|
+
|
|
2272
|
+
---
|
|
2273
|
+
|
|
1929
2274
|
## API Reference
|
|
1930
2275
|
|
|
1931
2276
|
### CustomAgent Class Attributes
|
|
@@ -2265,10 +2610,10 @@ For complete, runnable examples, see:
|
|
|
2265
2610
|
|
|
2266
2611
|
- `examples/customagent_p2p_example.py` - P2P mode with LLM-driven peer communication
|
|
2267
2612
|
- `examples/customagent_distributed_example.py` - Distributed mode with workflows
|
|
2268
|
-
- `examples/customagent_cognitive_discovery_example.py` - CustomAgent + cognitive discovery (v0.
|
|
2613
|
+
- `examples/customagent_cognitive_discovery_example.py` - CustomAgent + cognitive discovery (v0.3.0)
|
|
2269
2614
|
- `examples/fastapi_integration_example.py` - FastAPI + JarvisLifespan (v0.3.0)
|
|
2270
2615
|
- `examples/cloud_deployment_example.py` - Self-registration with join_mesh (v0.3.0)
|
|
2271
2616
|
|
|
2272
2617
|
---
|
|
2273
2618
|
|
|
2274
|
-
*CustomAgent Guide - JarvisCore Framework v0.3.
|
|
2619
|
+
*CustomAgent Guide - JarvisCore Framework v0.3.2*
|
jarviscore/docs/USER_GUIDE.md
CHANGED
|
@@ -19,9 +19,14 @@ Practical guide to building agent systems with JarvisCore.
|
|
|
19
19
|
11. [FastAPI Integration (v0.3.0)](#fastapi-integration-v030)
|
|
20
20
|
12. [Cloud Deployment (v0.3.0)](#cloud-deployment-v030)
|
|
21
21
|
13. [Cognitive Discovery (v0.3.0)](#cognitive-discovery-v030)
|
|
22
|
-
14. [
|
|
23
|
-
15. [
|
|
24
|
-
16. [
|
|
22
|
+
14. [Session Context (v0.3.2)](#session-context-v032)
|
|
23
|
+
15. [Async Requests (v0.3.2)](#async-requests-v032)
|
|
24
|
+
16. [Load Balancing (v0.3.2)](#load-balancing-v032)
|
|
25
|
+
17. [Mesh Diagnostics (v0.3.2)](#mesh-diagnostics-v032)
|
|
26
|
+
18. [Testing with MockMesh (v0.3.2)](#testing-with-mockmesh-v032)
|
|
27
|
+
19. [Best Practices](#best-practices)
|
|
28
|
+
20. [Common Patterns](#common-patterns)
|
|
29
|
+
21. [Troubleshooting](#troubleshooting)
|
|
25
30
|
|
|
26
31
|
---
|
|
27
32
|
|
|
@@ -679,6 +684,282 @@ Use the `ask_peer` tool to delegate tasks to these specialists.
|
|
|
679
684
|
|
|
680
685
|
---
|
|
681
686
|
|
|
687
|
+
## Session Context (v0.3.2)
|
|
688
|
+
|
|
689
|
+
Pass metadata through your message flows for tracing, priority, and session tracking:
|
|
690
|
+
|
|
691
|
+
### Basic Usage
|
|
692
|
+
|
|
693
|
+
```python
|
|
694
|
+
# Send request with context
|
|
695
|
+
response = await self.peers.request(
|
|
696
|
+
"analyst",
|
|
697
|
+
{"query": "analyze sales data"},
|
|
698
|
+
context={"mission_id": "m-123", "priority": "high", "user_id": "u-456"}
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Context is available in the handler
|
|
702
|
+
async def on_peer_request(self, msg):
|
|
703
|
+
mission_id = msg.context.get("mission_id") # "m-123"
|
|
704
|
+
print(f"Processing request for mission: {mission_id}")
|
|
705
|
+
return {"result": "analysis complete"}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Auto-Propagation
|
|
709
|
+
|
|
710
|
+
When you `respond()`, context automatically propagates from the request:
|
|
711
|
+
|
|
712
|
+
```python
|
|
713
|
+
async def on_peer_request(self, msg):
|
|
714
|
+
# msg.context = {"mission_id": "m-123", ...}
|
|
715
|
+
result = process(msg.data)
|
|
716
|
+
|
|
717
|
+
# Context auto-propagates - no need to pass it!
|
|
718
|
+
await self.peers.respond(msg, {"result": result})
|
|
719
|
+
|
|
720
|
+
# Or override with custom context
|
|
721
|
+
await self.peers.respond(msg, {"result": result},
|
|
722
|
+
context={"status": "completed"})
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### All Methods Support Context
|
|
726
|
+
|
|
727
|
+
```python
|
|
728
|
+
# notify
|
|
729
|
+
await self.peers.notify("logger", {"event": "started"}, context={"trace_id": "t-1"})
|
|
730
|
+
|
|
731
|
+
# request
|
|
732
|
+
response = await self.peers.request("worker", {"task": "..."}, context={"priority": "low"})
|
|
733
|
+
|
|
734
|
+
# broadcast
|
|
735
|
+
await self.peers.broadcast({"alert": "system ready"}, context={"source": "coordinator"})
|
|
736
|
+
|
|
737
|
+
# ask_async
|
|
738
|
+
req_id = await self.peers.ask_async("analyst", {"q": "..."}, context={"batch_id": "b-1"})
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## Async Requests (v0.3.2)
|
|
744
|
+
|
|
745
|
+
Fire-and-collect pattern for parallel requests without blocking:
|
|
746
|
+
|
|
747
|
+
### Basic Pattern
|
|
748
|
+
|
|
749
|
+
```python
|
|
750
|
+
# Fire off multiple requests (non-blocking)
|
|
751
|
+
request_ids = []
|
|
752
|
+
for analyst in self.peers.discover(role="analyst"):
|
|
753
|
+
req_id = await self.peers.ask_async(analyst.agent_id, {"task": "analyze"})
|
|
754
|
+
request_ids.append(req_id)
|
|
755
|
+
|
|
756
|
+
# Do other work while analysts process...
|
|
757
|
+
await self.do_other_work()
|
|
758
|
+
|
|
759
|
+
# Collect responses
|
|
760
|
+
results = []
|
|
761
|
+
for req_id in request_ids:
|
|
762
|
+
response = await self.peers.check_inbox(req_id, timeout=10)
|
|
763
|
+
if response:
|
|
764
|
+
results.append(response)
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### API Reference
|
|
768
|
+
|
|
769
|
+
```python
|
|
770
|
+
# Send async request - returns immediately
|
|
771
|
+
req_id = await self.peers.ask_async(target, message, timeout=120, context=None)
|
|
772
|
+
|
|
773
|
+
# Check for response (non-blocking if timeout=0)
|
|
774
|
+
response = await self.peers.check_inbox(req_id, timeout=0)
|
|
775
|
+
|
|
776
|
+
# Check with wait
|
|
777
|
+
response = await self.peers.check_inbox(req_id, timeout=5)
|
|
778
|
+
|
|
779
|
+
# List pending requests
|
|
780
|
+
pending = self.peers.get_pending_async_requests()
|
|
781
|
+
# [{"request_id": "...", "target": "analyst", "sent_at": 1234567890.0}]
|
|
782
|
+
|
|
783
|
+
# Clear inbox
|
|
784
|
+
self.peers.clear_inbox(req_id) # Specific
|
|
785
|
+
self.peers.clear_inbox() # All
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## Load Balancing (v0.3.2)
|
|
791
|
+
|
|
792
|
+
Distribute requests across multiple peers with discovery strategies:
|
|
793
|
+
|
|
794
|
+
### Strategies
|
|
795
|
+
|
|
796
|
+
```python
|
|
797
|
+
# Default: first in discovery order
|
|
798
|
+
peers = self.peers.discover(role="worker", strategy="first")
|
|
799
|
+
|
|
800
|
+
# Random: shuffle for basic load distribution
|
|
801
|
+
peers = self.peers.discover(role="worker", strategy="random")
|
|
802
|
+
|
|
803
|
+
# Round-robin: rotate through peers on each call
|
|
804
|
+
peers = self.peers.discover(role="worker", strategy="round_robin")
|
|
805
|
+
|
|
806
|
+
# Least-recent: prefer peers not used recently
|
|
807
|
+
peers = self.peers.discover(role="worker", strategy="least_recent")
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### Convenience Method
|
|
811
|
+
|
|
812
|
+
```python
|
|
813
|
+
# Get single peer with strategy
|
|
814
|
+
worker = self.peers.discover_one(role="worker", strategy="round_robin")
|
|
815
|
+
if worker:
|
|
816
|
+
await self.peers.request(worker.agent_id, {"task": "..."})
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
### Track Usage for least_recent
|
|
820
|
+
|
|
821
|
+
```python
|
|
822
|
+
peer = self.peers.discover_one(role="worker", strategy="least_recent")
|
|
823
|
+
response = await self.peers.request(peer.agent_id, {"task": "..."})
|
|
824
|
+
|
|
825
|
+
# Update usage timestamp after successful communication
|
|
826
|
+
self.peers.record_peer_usage(peer.agent_id)
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### Example: Round-Robin Work Distribution
|
|
830
|
+
|
|
831
|
+
```python
|
|
832
|
+
async def distribute_tasks(self, tasks):
|
|
833
|
+
results = []
|
|
834
|
+
for task in tasks:
|
|
835
|
+
# Each call rotates to next worker
|
|
836
|
+
worker = self.peers.discover_one(role="worker", strategy="round_robin")
|
|
837
|
+
if worker:
|
|
838
|
+
response = await self.peers.request(worker.agent_id, {"task": task})
|
|
839
|
+
results.append(response)
|
|
840
|
+
return results
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## Mesh Diagnostics (v0.3.2)
|
|
846
|
+
|
|
847
|
+
Monitor mesh health and debug connectivity issues:
|
|
848
|
+
|
|
849
|
+
### Get Diagnostics
|
|
850
|
+
|
|
851
|
+
```python
|
|
852
|
+
diag = mesh.get_diagnostics()
|
|
853
|
+
|
|
854
|
+
print(f"Mode: {diag['local_node']['mode']}")
|
|
855
|
+
print(f"Status: {diag['connectivity_status']}")
|
|
856
|
+
print(f"Agents: {diag['local_node']['agent_count']}")
|
|
857
|
+
|
|
858
|
+
for agent in diag['local_agents']:
|
|
859
|
+
print(f" - {agent['role']}: {agent['capabilities']}")
|
|
860
|
+
|
|
861
|
+
for peer in diag['known_peers']:
|
|
862
|
+
print(f" - {peer['role']} @ {peer['node_id']}: {peer['status']}")
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### Connectivity Status Values
|
|
866
|
+
|
|
867
|
+
| Status | Meaning |
|
|
868
|
+
|--------|---------|
|
|
869
|
+
| `healthy` | P2P active with connected peers |
|
|
870
|
+
| `isolated` | P2P active but no peers found |
|
|
871
|
+
| `degraded` | Some connectivity issues |
|
|
872
|
+
| `not_started` | Mesh not yet started |
|
|
873
|
+
| `local_only` | Autonomous mode (no P2P) |
|
|
874
|
+
|
|
875
|
+
### FastAPI Health Endpoint
|
|
876
|
+
|
|
877
|
+
```python
|
|
878
|
+
@app.get("/health")
|
|
879
|
+
async def health(request: Request):
|
|
880
|
+
mesh = request.app.state.jarvis_mesh
|
|
881
|
+
diag = mesh.get_diagnostics()
|
|
882
|
+
return {
|
|
883
|
+
"status": diag["connectivity_status"],
|
|
884
|
+
"agents": diag["local_node"]["agent_count"],
|
|
885
|
+
"peers": len(diag["known_peers"])
|
|
886
|
+
}
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Testing with MockMesh (v0.3.2)
|
|
892
|
+
|
|
893
|
+
Unit test your agents without real P2P infrastructure:
|
|
894
|
+
|
|
895
|
+
### Basic Setup
|
|
896
|
+
|
|
897
|
+
```python
|
|
898
|
+
import pytest
|
|
899
|
+
from jarviscore.testing import MockMesh
|
|
900
|
+
from jarviscore.profiles import CustomAgent
|
|
901
|
+
|
|
902
|
+
class MyAgent(CustomAgent):
|
|
903
|
+
role = "processor"
|
|
904
|
+
capabilities = ["processing"]
|
|
905
|
+
|
|
906
|
+
async def on_peer_request(self, msg):
|
|
907
|
+
# Delegate to analyst
|
|
908
|
+
analysis = await self.peers.request("analyst", {"data": msg.data})
|
|
909
|
+
return {"processed": True, "analysis": analysis}
|
|
910
|
+
|
|
911
|
+
@pytest.mark.asyncio
|
|
912
|
+
async def test_processor_delegates():
|
|
913
|
+
mesh = MockMesh()
|
|
914
|
+
mesh.add(MyAgent)
|
|
915
|
+
await mesh.start()
|
|
916
|
+
|
|
917
|
+
agent = mesh.get_agent("processor")
|
|
918
|
+
|
|
919
|
+
# Configure mock response for analyst
|
|
920
|
+
agent.peers.set_mock_response("analyst", {"result": "analyzed"})
|
|
921
|
+
|
|
922
|
+
# Test the agent
|
|
923
|
+
response = await agent.peers.request("analyst", {"test": "data"})
|
|
924
|
+
|
|
925
|
+
# Verify
|
|
926
|
+
assert response["result"] == "analyzed"
|
|
927
|
+
agent.peers.assert_requested("analyst")
|
|
928
|
+
|
|
929
|
+
await mesh.stop()
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
### MockPeerClient Features
|
|
933
|
+
|
|
934
|
+
```python
|
|
935
|
+
# Configure responses
|
|
936
|
+
agent.peers.set_mock_response("analyst", {"result": "..."})
|
|
937
|
+
agent.peers.set_default_response({"status": "ok"})
|
|
938
|
+
|
|
939
|
+
# Custom handler for dynamic responses
|
|
940
|
+
async def handler(target, message, context):
|
|
941
|
+
return {"echo": message, "target": target}
|
|
942
|
+
agent.peers.set_request_handler(handler)
|
|
943
|
+
|
|
944
|
+
# Inject messages for handler testing
|
|
945
|
+
from jarviscore.p2p.messages import MessageType
|
|
946
|
+
agent.peers.inject_message("sender", MessageType.REQUEST, {"data": "test"})
|
|
947
|
+
|
|
948
|
+
# Assertions
|
|
949
|
+
agent.peers.assert_notified("target")
|
|
950
|
+
agent.peers.assert_requested("analyst", message_contains={"query": "test"})
|
|
951
|
+
agent.peers.assert_broadcasted()
|
|
952
|
+
|
|
953
|
+
# Track what was sent
|
|
954
|
+
notifications = agent.peers.get_sent_notifications()
|
|
955
|
+
requests = agent.peers.get_sent_requests()
|
|
956
|
+
|
|
957
|
+
# Reset between tests
|
|
958
|
+
agent.peers.reset()
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
682
963
|
## Best Practices
|
|
683
964
|
|
|
684
965
|
### 1. Always Use Context Managers
|
|
@@ -904,6 +1185,6 @@ mesh = Mesh(config=config)
|
|
|
904
1185
|
|
|
905
1186
|
## Version
|
|
906
1187
|
|
|
907
|
-
User Guide for JarvisCore v0.3.
|
|
1188
|
+
User Guide for JarvisCore v0.3.2
|
|
908
1189
|
|
|
909
|
-
Last Updated: 2026-02-
|
|
1190
|
+
Last Updated: 2026-02-03
|