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
jarviscore/core/mesh.py CHANGED
@@ -256,9 +256,18 @@ class Mesh:
256
256
  await self._p2p_coordinator.start()
257
257
  self._logger.info("✓ P2P coordinator started")
258
258
 
259
+ # Wait for mesh to stabilize before announcing
260
+ # Increased delay to ensure SWIM fully connects all nodes
261
+ await asyncio.sleep(5)
262
+ self._logger.info("Waited for mesh stabilization")
263
+
259
264
  # Announce capabilities to network
260
265
  await self._p2p_coordinator.announce_capabilities()
261
266
  self._logger.info("✓ Capabilities announced to mesh")
267
+
268
+ # Request capabilities from existing peers (for late-joiners)
269
+ await self._p2p_coordinator.request_peer_capabilities()
270
+ self._logger.info("✓ Requested capabilities from existing peers")
262
271
 
263
272
  # Inject PeerClients for p2p mode
264
273
  if self.mode == MeshMode.P2P:
@@ -615,6 +624,155 @@ class Mesh:
615
624
  """
616
625
  return self._capability_index.get(capability, [])
617
626
 
627
+ # ─────────────────────────────────────────────────────────────────
628
+ # DIAGNOSTICS
629
+ # ─────────────────────────────────────────────────────────────────
630
+
631
+ def get_diagnostics(self) -> Dict[str, Any]:
632
+ """
633
+ Get diagnostic information about the mesh and P2P connectivity.
634
+
635
+ Useful for debugging P2P issues, monitoring mesh health,
636
+ and understanding the current state of the distributed system.
637
+
638
+ Returns:
639
+ Dictionary containing:
640
+ - local_node: This node's configuration and status
641
+ - known_peers: List of discovered remote peers
642
+ - local_agents: List of local agents with capabilities
643
+ - connectivity_status: Overall health assessment
644
+ - keepalive_status: Keepalive manager health (if P2P enabled)
645
+ - swim_status: SWIM protocol status (if P2P enabled)
646
+ - capability_map: Mapping of capabilities to agent IDs
647
+
648
+ Example:
649
+ diagnostics = mesh.get_diagnostics()
650
+ print(f"Status: {diagnostics['connectivity_status']}")
651
+ for peer in diagnostics['known_peers']:
652
+ print(f" {peer['role']} at {peer['node_id']}: {peer['status']}")
653
+ """
654
+ result = {
655
+ "local_node": self._get_local_node_info(),
656
+ "known_peers": self._get_peer_list(),
657
+ "local_agents": self._get_local_agents_info(),
658
+ "connectivity_status": self._assess_connectivity_status()
659
+ }
660
+
661
+ # Add P2P-specific diagnostics if coordinator is available
662
+ if self._p2p_coordinator:
663
+ result["keepalive_status"] = self._get_keepalive_status()
664
+ result["swim_status"] = self._get_swim_status()
665
+ result["capability_map"] = self._get_capability_map()
666
+
667
+ return result
668
+
669
+ def _get_local_node_info(self) -> Dict[str, Any]:
670
+ """Get local node information."""
671
+ info = {
672
+ "mode": self.mode.value,
673
+ "started": self._started,
674
+ "agent_count": len(self.agents)
675
+ }
676
+
677
+ if self._p2p_coordinator and self._p2p_coordinator.swim_manager:
678
+ addr = self._p2p_coordinator.swim_manager.bind_addr
679
+ if addr:
680
+ info["bind_address"] = f"{addr[0]}:{addr[1]}"
681
+
682
+ return info
683
+
684
+ def _get_peer_list(self) -> List[Dict[str, Any]]:
685
+ """Get list of known remote peers."""
686
+ peers = []
687
+
688
+ if self._p2p_coordinator:
689
+ for agent in self._p2p_coordinator.list_remote_agents():
690
+ peers.append({
691
+ "role": agent.get("role", "unknown"),
692
+ "agent_id": agent.get("agent_id", "unknown"),
693
+ "node_id": agent.get("node_id", "unknown"),
694
+ "capabilities": agent.get("capabilities", []),
695
+ "status": "connected"
696
+ })
697
+
698
+ return peers
699
+
700
+ def _get_local_agents_info(self) -> List[Dict[str, Any]]:
701
+ """Get information about local agents."""
702
+ return [
703
+ {
704
+ "role": agent.role,
705
+ "agent_id": agent.agent_id,
706
+ "capabilities": list(agent.capabilities),
707
+ "description": getattr(agent, 'description', ''),
708
+ "has_peers": hasattr(agent, 'peers') and agent.peers is not None
709
+ }
710
+ for agent in self.agents
711
+ ]
712
+
713
+ def _assess_connectivity_status(self) -> str:
714
+ """
715
+ Assess overall connectivity status.
716
+
717
+ Returns:
718
+ "healthy" - P2P fully operational with peers
719
+ "isolated" - No peers connected
720
+ "degraded" - Some connectivity issues detected
721
+ "not_started" - Mesh not yet started
722
+ "local_only" - Not in distributed/p2p mode
723
+ """
724
+ if not self._started:
725
+ return "not_started"
726
+
727
+ if self.mode == MeshMode.AUTONOMOUS:
728
+ return "local_only"
729
+
730
+ if not self._p2p_coordinator:
731
+ return "local_only"
732
+
733
+ # Check SWIM health
734
+ if self._p2p_coordinator.swim_manager:
735
+ if not self._p2p_coordinator.swim_manager.is_healthy():
736
+ return "degraded"
737
+
738
+ # Check for connected peers
739
+ remote_agents = self._p2p_coordinator.list_remote_agents()
740
+ if not remote_agents:
741
+ return "isolated"
742
+
743
+ # Check keepalive health if available
744
+ if hasattr(self._p2p_coordinator, 'keepalive_manager') and self._p2p_coordinator.keepalive_manager:
745
+ health = self._p2p_coordinator.keepalive_manager.get_health_status()
746
+ if health.get('circuit_state') == 'OPEN':
747
+ return "degraded"
748
+
749
+ return "healthy"
750
+
751
+ def _get_keepalive_status(self) -> Optional[Dict[str, Any]]:
752
+ """Get keepalive manager status."""
753
+ if not self._p2p_coordinator:
754
+ return None
755
+
756
+ if hasattr(self._p2p_coordinator, 'keepalive_manager') and self._p2p_coordinator.keepalive_manager:
757
+ return self._p2p_coordinator.keepalive_manager.get_health_status()
758
+
759
+ return None
760
+
761
+ def _get_swim_status(self) -> Optional[Dict[str, Any]]:
762
+ """Get SWIM protocol status."""
763
+ if not self._p2p_coordinator or not self._p2p_coordinator.swim_manager:
764
+ return None
765
+
766
+ return self._p2p_coordinator.swim_manager.get_status()
767
+
768
+ def _get_capability_map(self) -> Dict[str, List[str]]:
769
+ """Get the capability to agent_id mapping."""
770
+ if not self._p2p_coordinator:
771
+ return {}
772
+
773
+ # Convert defaultdict to regular dict for serialization
774
+ return dict(self._p2p_coordinator._capability_map)
775
+
618
776
  def __repr__(self) -> str:
619
777
  """String representation of mesh."""
620
778
  return (
@@ -28,10 +28,10 @@ import sys
28
28
 
29
29
  sys.path.insert(0, '.')
30
30
 
31
- from jarviscore.profiles import ListenerAgent
31
+ from jarviscore.profiles import CustomAgent
32
32
 
33
33
 
34
- class StandaloneProcessor(ListenerAgent):
34
+ class StandaloneProcessor(CustomAgent):
35
35
  """
36
36
  Example standalone agent that joins mesh independently.
37
37
 
@@ -143,7 +143,7 @@ async def main():
143
143
  print("Listening for peer requests...")
144
144
  print("Press Ctrl+C to stop.\n")
145
145
 
146
- # Run agent (ListenerAgent's run() handles the message loop)
146
+ # Run agent (CustomAgent's run() handles the message loop)
147
147
  try:
148
148
  await agent.run()
149
149
  except asyncio.CancelledError:
@@ -0,0 +1,134 @@
1
+ """
2
+ Custom Profile Example: Using @jarvis_agent Decorator
3
+
4
+ This example shows how to use the @jarvis_agent decorator to convert
5
+ any Python class into a JarvisCore agent without modifying the class.
6
+
7
+ Use Case: You have existing Python classes/agents and want JarvisCore
8
+ to handle orchestration (data handoff, dependencies, shared memory).
9
+ """
10
+ import asyncio
11
+ from jarviscore import Mesh, jarvis_agent, JarvisContext
12
+
13
+
14
+ # Example 1: Simple decorator (no context needed)
15
+ @jarvis_agent(role="processor", capabilities=["data_processing"])
16
+ class DataProcessor:
17
+ """Simple data processor - doubles input values."""
18
+
19
+ def run(self, data):
20
+ """Process data by doubling values."""
21
+ if isinstance(data, list):
22
+ return {"processed": [x * 2 for x in data]}
23
+ return {"processed": data * 2}
24
+
25
+
26
+ # Example 2: Decorator with context access
27
+ @jarvis_agent(role="aggregator", capabilities=["aggregation"])
28
+ class Aggregator:
29
+ """Aggregates results from previous steps using JarvisContext."""
30
+
31
+ def run(self, task, ctx: JarvisContext):
32
+ """
33
+ Access previous step results via ctx.previous().
34
+
35
+ Args:
36
+ task: The task description
37
+ ctx: JarvisContext with memory and dependency access
38
+ """
39
+ # Get output from a specific previous step
40
+ processed = ctx.previous("step1")
41
+
42
+ if processed:
43
+ data = processed.get("processed", [])
44
+ return {
45
+ "sum": sum(data) if isinstance(data, list) else data,
46
+ "count": len(data) if isinstance(data, list) else 1,
47
+ "source_step": "step1"
48
+ }
49
+
50
+ return {"error": "No previous data found"}
51
+
52
+
53
+ # Example 3: Decorator with custom execute method
54
+ @jarvis_agent(role="validator", capabilities=["validation"], execute_method="validate")
55
+ class DataValidator:
56
+ """Validates data using a custom method name."""
57
+
58
+ def validate(self, data):
59
+ """Custom execute method - validates input data."""
60
+ if isinstance(data, list):
61
+ return {
62
+ "valid": all(isinstance(x, (int, float)) for x in data),
63
+ "count": len(data),
64
+ "type": "list"
65
+ }
66
+ return {
67
+ "valid": isinstance(data, (int, float)),
68
+ "type": type(data).__name__
69
+ }
70
+
71
+
72
+ async def main():
73
+ """Run a multi-step workflow with custom profile agents."""
74
+ print("=" * 60)
75
+ print(" Custom Profile Example: @jarvis_agent Decorator")
76
+ print("=" * 60)
77
+
78
+ # Create mesh in autonomous mode
79
+ mesh = Mesh(mode="autonomous")
80
+
81
+ # Add our decorated agents
82
+ mesh.add(DataProcessor)
83
+ mesh.add(Aggregator)
84
+ mesh.add(DataValidator)
85
+
86
+ # Start the mesh
87
+ await mesh.start()
88
+
89
+ try:
90
+ # Execute a multi-step workflow
91
+ print("\nExecuting workflow with 3 steps...\n")
92
+
93
+ results = await mesh.workflow("custom-profile-demo", [
94
+ {
95
+ "id": "step1",
96
+ "agent": "processor",
97
+ "task": "Process input data",
98
+ "params": {"data": [1, 2, 3, 4, 5]}
99
+ },
100
+ {
101
+ "id": "step2",
102
+ "agent": "aggregator",
103
+ "task": "Aggregate processed results",
104
+ "depends_on": ["step1"] # Wait for step1
105
+ },
106
+ {
107
+ "id": "step3",
108
+ "agent": "validator",
109
+ "task": "Validate original data",
110
+ "params": {"data": [1, 2, 3, 4, 5]}
111
+ }
112
+ ])
113
+
114
+ # Print results
115
+ print("Results:")
116
+ print("-" * 40)
117
+
118
+ for i, result in enumerate(results):
119
+ step_name = ["Processor", "Aggregator", "Validator"][i]
120
+ print(f"\n{step_name} (step{i+1}):")
121
+ print(f" Status: {result.get('status')}")
122
+ print(f" Output: {result.get('output')}")
123
+
124
+ print("\n" + "=" * 60)
125
+ print(" Workflow completed successfully!")
126
+ print("=" * 60)
127
+
128
+ finally:
129
+ # Stop the mesh
130
+ await mesh.stop()
131
+
132
+
133
+ if __name__ == "__main__":
134
+ asyncio.run(main())
@@ -0,0 +1,168 @@
1
+ """
2
+ Custom Profile Example: Using wrap() Function
3
+
4
+ This example shows how to use the wrap() function to convert
5
+ an existing instance into a JarvisCore agent.
6
+
7
+ Use Case: You have an already-instantiated object (like a LangChain
8
+ agent, CrewAI agent, or any configured instance) and want to use it
9
+ with JarvisCore orchestration.
10
+ """
11
+ import asyncio
12
+ from jarviscore import Mesh, wrap, JarvisContext
13
+
14
+
15
+ # Simulate an existing "LangChain-like" agent
16
+ class ExternalLLMAgent:
17
+ """
18
+ Simulates an external LLM agent (like LangChain).
19
+ In real usage, this would be your actual LangChain/CrewAI agent.
20
+ """
21
+
22
+ def __init__(self, model_name: str, temperature: float = 0.7):
23
+ self.model_name = model_name
24
+ self.temperature = temperature
25
+ print(f" Initialized ExternalLLMAgent with {model_name}")
26
+
27
+ def invoke(self, query: str) -> dict:
28
+ """LangChain-style invoke method."""
29
+ # Simulate LLM response
30
+ return {
31
+ "answer": f"Response to '{query}' from {self.model_name}",
32
+ "model": self.model_name,
33
+ "tokens_used": len(query.split()) * 10
34
+ }
35
+
36
+
37
+ # Simulate a data processing service
38
+ class DataService:
39
+ """Simulates an external data processing service."""
40
+
41
+ def __init__(self, api_url: str):
42
+ self.api_url = api_url
43
+ print(f" Initialized DataService with {api_url}")
44
+
45
+ def run(self, data):
46
+ """Process data through the service."""
47
+ if isinstance(data, list):
48
+ return {
49
+ "transformed": [x ** 2 for x in data],
50
+ "source": self.api_url
51
+ }
52
+ return {"transformed": data ** 2, "source": self.api_url}
53
+
54
+
55
+ # Simulate an agent that needs context
56
+ class ContextAwareProcessor:
57
+ """Agent that uses JarvisContext to access previous results."""
58
+
59
+ def run(self, task, ctx: JarvisContext):
60
+ """Process with context access."""
61
+ # Get all previous results
62
+ all_previous = ctx.all_previous()
63
+
64
+ summary = {
65
+ "task": task,
66
+ "previous_steps": list(all_previous.keys()),
67
+ "combined_data": {}
68
+ }
69
+
70
+ for step_id, output in all_previous.items():
71
+ if isinstance(output, dict):
72
+ summary["combined_data"][step_id] = output
73
+
74
+ return summary
75
+
76
+
77
+ async def main():
78
+ """Demonstrate wrapping existing instances."""
79
+ print("=" * 60)
80
+ print(" Custom Profile Example: wrap() Function")
81
+ print("=" * 60)
82
+
83
+ # Create instances of "external" agents
84
+ print("\nCreating external agent instances...")
85
+ llm_agent = ExternalLLMAgent(model_name="gpt-4-turbo", temperature=0.3)
86
+ data_service = DataService(api_url="https://api.example.com/process")
87
+ context_processor = ContextAwareProcessor()
88
+
89
+ # Wrap them for JarvisCore
90
+ print("\nWrapping instances for JarvisCore...")
91
+
92
+ wrapped_llm = wrap(
93
+ llm_agent,
94
+ role="llm_assistant",
95
+ capabilities=["chat", "qa"],
96
+ execute_method="invoke" # LangChain uses "invoke"
97
+ )
98
+
99
+ wrapped_data = wrap(
100
+ data_service,
101
+ role="data_processor",
102
+ capabilities=["data_processing", "transformation"]
103
+ # execute_method auto-detected as "run"
104
+ )
105
+
106
+ wrapped_context = wrap(
107
+ context_processor,
108
+ role="context_aggregator",
109
+ capabilities=["aggregation", "summary"]
110
+ )
111
+
112
+ # Create mesh and add wrapped agents
113
+ mesh = Mesh(mode="autonomous")
114
+ mesh.add(wrapped_llm)
115
+ mesh.add(wrapped_data)
116
+ mesh.add(wrapped_context)
117
+
118
+ await mesh.start()
119
+
120
+ try:
121
+ print("\nExecuting workflow with wrapped agents...\n")
122
+
123
+ results = await mesh.workflow("wrap-demo", [
124
+ {
125
+ "id": "llm_step",
126
+ "agent": "llm_assistant",
127
+ "task": "What is the capital of France?",
128
+ "params": {"query": "What is the capital of France?"}
129
+ },
130
+ {
131
+ "id": "data_step",
132
+ "agent": "data_processor",
133
+ "task": "Transform numbers",
134
+ "params": {"data": [1, 2, 3, 4, 5]}
135
+ },
136
+ {
137
+ "id": "summary_step",
138
+ "agent": "context_aggregator",
139
+ "task": "Summarize all results",
140
+ "depends_on": ["llm_step", "data_step"]
141
+ }
142
+ ])
143
+
144
+ # Print results
145
+ print("Results:")
146
+ print("-" * 40)
147
+
148
+ step_names = ["LLM Assistant", "Data Processor", "Context Aggregator"]
149
+ for i, result in enumerate(results):
150
+ print(f"\n{step_names[i]}:")
151
+ print(f" Status: {result.get('status')}")
152
+ output = result.get('output', {})
153
+ if isinstance(output, dict):
154
+ for key, value in output.items():
155
+ print(f" {key}: {value}")
156
+ else:
157
+ print(f" Output: {output}")
158
+
159
+ print("\n" + "=" * 60)
160
+ print(" Workflow with wrapped instances completed!")
161
+ print("=" * 60)
162
+
163
+ finally:
164
+ await mesh.stop()
165
+
166
+
167
+ if __name__ == "__main__":
168
+ asyncio.run(main())
@@ -1,42 +1,47 @@
1
1
  """
2
- ListenerAgent + Cognitive Discovery Example
2
+ CustomAgent + Cognitive Discovery Example
3
3
 
4
- Demonstrates two v0.3.0 features:
4
+ Demonstrates v0.3.0 and v0.3.2 features:
5
5
 
6
- 1. ListenerAgent - Handler-based P2P agents (no run() loop needed)
6
+ 1. CustomAgent - Handler-based P2P agents (no run() loop needed)
7
7
  - on_peer_request() handles incoming requests
8
8
  - on_peer_notify() handles broadcast notifications
9
9
 
10
- 2. Cognitive Discovery - Dynamic peer awareness for LLMs
10
+ 2. Cognitive Discovery (v0.3.0) - Dynamic peer awareness for LLMs
11
11
  - get_cognitive_context() generates LLM-ready peer descriptions
12
12
  - No hardcoded agent names in prompts
13
13
  - LLM autonomously decides when to delegate
14
14
 
15
+ 3. Session Context (v0.3.2) - Request tracking with metadata
16
+ - Pass context={mission_id, request_id} with peer requests
17
+ - Track requests across agent boundaries for debugging/tracing
18
+
15
19
  Usage:
16
- python examples/listeneragent_cognitive_discovery_example.py
20
+ python examples/customagent_cognitive_discovery_example.py
17
21
 
18
22
  Prerequisites:
19
23
  - .env file with CLAUDE_API_KEY (or other LLM provider)
20
24
  """
21
25
  import asyncio
22
26
  import sys
27
+ import uuid
23
28
  from pathlib import Path
24
29
 
25
30
  sys.path.insert(0, str(Path(__file__).parent.parent))
26
31
 
27
32
  from jarviscore import Mesh
28
- from jarviscore.profiles import ListenerAgent
33
+ from jarviscore.profiles import CustomAgent
29
34
 
30
35
 
31
36
  # ═══════════════════════════════════════════════════════════════════════════════
32
37
  # SPECIALIST AGENT - Responds to requests from other agents
33
38
  # ═══════════════════════════════════════════════════════════════════════════════
34
39
 
35
- class AnalystAgent(ListenerAgent):
40
+ class AnalystAgent(CustomAgent):
36
41
  """
37
42
  Specialist agent that handles analysis requests.
38
43
 
39
- Uses ListenerAgent profile - just implement handlers, no run() loop needed.
44
+ Uses CustomAgent profile - just implement handlers, no run() loop needed.
40
45
  """
41
46
  role = "analyst"
42
47
  capabilities = ["data_analysis", "statistics", "insights"]
@@ -45,13 +50,21 @@ class AnalystAgent(ListenerAgent):
45
50
  async def on_peer_request(self, msg):
46
51
  """Handle incoming analysis requests."""
47
52
  query = msg.data.get("question", msg.data.get("query", ""))
48
- print(f"\n[Analyst] Received request: {query[:50]}...")
53
+
54
+ # v0.3.2: Access session context for request tracking
55
+ context = msg.context or {}
56
+ mission_id = context.get("mission_id", "unknown")
57
+ request_id = context.get("request_id", "unknown")
58
+
59
+ print(f"\n[Analyst] Received request (mission={mission_id[:8]}..., req={request_id[:8]}...)")
60
+ print(f"[Analyst] Query: {query[:50]}...")
49
61
 
50
62
  # Simulate analysis (in real usage, this would use an LLM)
51
63
  result = {
52
64
  "analysis": f"Analysis of '{query}': The data shows positive trends.",
53
65
  "confidence": 0.85,
54
- "insights": ["Trend is upward", "Growth rate: 15%", "Recommendation: Continue"]
66
+ "insights": ["Trend is upward", "Growth rate: 15%", "Recommendation: Continue"],
67
+ "context": {"mission_id": mission_id, "request_id": request_id} # Echo back for tracing
55
68
  }
56
69
 
57
70
  print(f"[Analyst] Sending response with {len(result['insights'])} insights")
@@ -62,7 +75,7 @@ class AnalystAgent(ListenerAgent):
62
75
  # COORDINATOR AGENT - Uses LLM with cognitive discovery
63
76
  # ═══════════════════════════════════════════════════════════════════════════════
64
77
 
65
- class CoordinatorAgent(ListenerAgent):
78
+ class CoordinatorAgent(CustomAgent):
66
79
  """
67
80
  Coordinator agent that uses LLM with dynamic peer discovery.
68
81
 
@@ -78,6 +91,9 @@ class CoordinatorAgent(ListenerAgent):
78
91
  async def setup(self):
79
92
  await super().setup()
80
93
  self.llm = self._create_llm_client()
94
+ # v0.3.2: Track missions for context propagation
95
+ self.mission_id = str(uuid.uuid4())
96
+ self.request_counter = 0
81
97
 
82
98
  def _create_llm_client(self):
83
99
  """Create LLM client with fallback to mock."""
@@ -167,10 +183,22 @@ Never try to do analysis yourself - always delegate to the analyst."""
167
183
  # Mock: simulate LLM deciding to delegate
168
184
  if any(word in user_query.lower() for word in ["analyze", "analysis", "statistics", "data"]):
169
185
  print("[Coordinator] Mock LLM decides to delegate to analyst")
186
+
187
+ # v0.3.2: Generate request context for tracking
188
+ self.request_counter += 1
189
+ request_context = {
190
+ "mission_id": self.mission_id,
191
+ "request_id": str(uuid.uuid4()),
192
+ "request_num": self.request_counter,
193
+ "source": "coordinator"
194
+ }
195
+ print(f"[Coordinator] Sending with context: mission={self.mission_id[:8]}...")
196
+
170
197
  response = await self.peers.request(
171
198
  "analyst",
172
199
  {"question": user_query},
173
- timeout=30
200
+ timeout=30,
201
+ context=request_context # v0.3.2: Pass context
174
202
  )
175
203
  return f"Based on the analyst's findings: {response.get('analysis', 'No response')}"
176
204
  return f"I can help with: {user_query}"
@@ -261,12 +289,24 @@ Never try to do analysis yourself - always delegate to the analyst."""
261
289
  role = args.get("role", "")
262
290
  question = args.get("question", "")
263
291
 
292
+ # v0.3.2: Generate request context for tracking
293
+ self.request_counter += 1
294
+ request_context = {
295
+ "mission_id": self.mission_id,
296
+ "request_id": str(uuid.uuid4()),
297
+ "request_num": self.request_counter,
298
+ "source": "coordinator",
299
+ "tool": "ask_peer"
300
+ }
301
+
264
302
  print(f"[Coordinator] Asking {role}: {question[:50]}...")
303
+ print(f"[Coordinator] Context: mission={self.mission_id[:8]}..., req_num={self.request_counter}")
265
304
 
266
305
  response = await self.peers.request(
267
306
  role,
268
307
  {"question": question},
269
- timeout=30
308
+ timeout=30,
309
+ context=request_context # v0.3.2: Pass context
270
310
  )
271
311
 
272
312
  return response
@@ -284,7 +324,8 @@ Never try to do analysis yourself - always delegate to the analyst."""
284
324
 
285
325
  async def main():
286
326
  print("=" * 60)
287
- print("LLM Cognitive Discovery Example")
327
+ print("CustomAgent + Cognitive Discovery + Session Context")
328
+ print("Features: v0.3.0 Cognitive Discovery, v0.3.2 Session Context")
288
329
  print("=" * 60)
289
330
 
290
331
  # Create mesh with both agents