jarviscore-framework 0.2.0__py3-none-any.whl → 0.3.0__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 (36) hide show
  1. examples/cloud_deployment_example.py +162 -0
  2. examples/customagent_p2p_example.py +566 -183
  3. examples/fastapi_integration_example.py +570 -0
  4. examples/listeneragent_cognitive_discovery_example.py +343 -0
  5. jarviscore/__init__.py +22 -5
  6. jarviscore/cli/smoketest.py +8 -4
  7. jarviscore/core/agent.py +227 -0
  8. jarviscore/data/examples/cloud_deployment_example.py +162 -0
  9. jarviscore/data/examples/customagent_p2p_example.py +566 -183
  10. jarviscore/data/examples/fastapi_integration_example.py +570 -0
  11. jarviscore/data/examples/listeneragent_cognitive_discovery_example.py +343 -0
  12. jarviscore/docs/API_REFERENCE.md +296 -3
  13. jarviscore/docs/CHANGELOG.md +97 -0
  14. jarviscore/docs/CONFIGURATION.md +2 -2
  15. jarviscore/docs/CUSTOMAGENT_GUIDE.md +2021 -255
  16. jarviscore/docs/GETTING_STARTED.md +112 -8
  17. jarviscore/docs/TROUBLESHOOTING.md +3 -3
  18. jarviscore/docs/USER_GUIDE.md +152 -6
  19. jarviscore/integrations/__init__.py +16 -0
  20. jarviscore/integrations/fastapi.py +247 -0
  21. jarviscore/p2p/broadcaster.py +10 -3
  22. jarviscore/p2p/coordinator.py +310 -14
  23. jarviscore/p2p/keepalive.py +45 -23
  24. jarviscore/p2p/peer_client.py +282 -10
  25. jarviscore/p2p/swim_manager.py +9 -4
  26. jarviscore/profiles/__init__.py +10 -2
  27. jarviscore/profiles/listeneragent.py +292 -0
  28. {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/METADATA +42 -8
  29. {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/RECORD +36 -22
  30. {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/WHEEL +1 -1
  31. tests/test_13_dx_improvements.py +554 -0
  32. tests/test_14_cloud_deployment.py +403 -0
  33. tests/test_15_llm_cognitive_discovery.py +684 -0
  34. tests/test_16_unified_dx_flow.py +947 -0
  35. {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/licenses/LICENSE +0 -0
  36. {jarviscore_framework-0.2.0.dist-info → jarviscore_framework-0.3.0.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,7 @@ Build your first AI agent in 5 minutes!
12
12
  |---------|----------|--------------|
13
13
  | **AutoAgent** | Rapid prototyping, LLM generates code from prompts | Yes |
14
14
  | **CustomAgent** | Existing code, full control (LangChain, CrewAI, etc.) | Optional |
15
+ | **ListenerAgent** | API-first (FastAPI), just implement handlers | Optional |
15
16
 
16
17
  ### Execution Modes (How agents are orchestrated)
17
18
 
@@ -19,9 +20,12 @@ Build your first AI agent in 5 minutes!
19
20
  |------|----------|------------|
20
21
  | **Autonomous** | Single machine, simple pipelines | ✅ This guide |
21
22
  | **P2P** | Direct agent communication, swarms | [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) |
22
- | **Distributed** | Multi-node production systems | [AutoAgent Guide](AUTOAGENT_GUIDE.md) |
23
+ | **Distributed** | Multi-node production systems | [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) |
23
24
 
24
- **Recommendation:** Start with **AutoAgent + Autonomous mode** below, then explore other modes.
25
+ **Recommendation:**
26
+ - **New to agents?** Start with **AutoAgent + Autonomous mode** below
27
+ - **Have existing code?** Jump to **CustomAgent** or **ListenerAgent** sections
28
+ - **Building APIs?** See **ListenerAgent + FastAPI** below
25
29
 
26
30
  ---
27
31
 
@@ -249,6 +253,90 @@ asyncio.run(main())
249
253
 
250
254
  ---
251
255
 
256
+ ## Step 6: ListenerAgent + FastAPI (API-First Path)
257
+
258
+ Building an API where agents run in the background? **ListenerAgent** eliminates the boilerplate.
259
+
260
+ ### The Problem
261
+
262
+ With CustomAgent, you write a `run()` loop to handle peer messages:
263
+
264
+ ```python
265
+ # CustomAgent - you write the loop
266
+ class MyAgent(CustomAgent):
267
+ async def run(self):
268
+ while not self.shutdown_requested:
269
+ msg = await self.peers.receive(timeout=1.0)
270
+ if msg is None:
271
+ continue
272
+ if msg.type == MessageType.REQUEST:
273
+ result = await self.process(msg.data)
274
+ await self.peers.respond(msg, result)
275
+ # ... error handling, logging, etc.
276
+ ```
277
+
278
+ ### The Solution
279
+
280
+ With ListenerAgent, just implement handlers:
281
+
282
+ ```python
283
+ from jarviscore.profiles import ListenerAgent
284
+
285
+ class MyAgent(ListenerAgent):
286
+ role = "processor"
287
+ capabilities = ["processing"]
288
+
289
+ async def on_peer_request(self, msg):
290
+ """Handle requests - return value is sent as response."""
291
+ return {"result": await self.process(msg.data)}
292
+
293
+ async def on_peer_notify(self, msg):
294
+ """Handle fire-and-forget notifications."""
295
+ await self.log_event(msg.data)
296
+ ```
297
+
298
+ ### FastAPI Integration (3 Lines)
299
+
300
+ ```python
301
+ from fastapi import FastAPI, Request
302
+ from jarviscore.profiles import ListenerAgent
303
+ from jarviscore.integrations.fastapi import JarvisLifespan
304
+
305
+ class ProcessorAgent(ListenerAgent):
306
+ role = "processor"
307
+ capabilities = ["data_processing"]
308
+
309
+ async def on_peer_request(self, msg):
310
+ return {"processed": msg.data.get("task", "").upper()}
311
+
312
+ # Create agent and integrate with FastAPI
313
+ agent = ProcessorAgent()
314
+ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
315
+
316
+ @app.post("/process")
317
+ async def process(data: dict, request: Request):
318
+ # Access your agent from the request
319
+ agent = request.app.state.jarvis_agents["processor"]
320
+ return await agent.process(data)
321
+
322
+ @app.get("/peers")
323
+ async def list_peers(request: Request):
324
+ agent = request.app.state.jarvis_agents["processor"]
325
+ return {"peers": agent.peers.list_peers()}
326
+ ```
327
+
328
+ Run with: `uvicorn myapp:app --host 0.0.0.0 --port 8000`
329
+
330
+ **What you get:**
331
+ - HTTP endpoints (FastAPI routes) as primary interface
332
+ - P2P mesh participation in background
333
+ - Auto message dispatch to handlers
334
+ - Graceful startup/shutdown handled by JarvisLifespan
335
+
336
+ **For more:** See [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for ListenerAgent details.
337
+
338
+ ---
339
+
252
340
  ## What Just Happened?
253
341
 
254
342
  Behind the scenes, JarvisCore:
@@ -394,7 +482,23 @@ class MyAgent(CustomAgent):
394
482
  ...
395
483
  ```
396
484
 
397
- ### 3. Mesh
485
+ ### 3. ListenerAgent Profile
486
+
487
+ The `ListenerAgent` profile is for API-first agents - just implement handlers:
488
+
489
+ ```python
490
+ class MyAgent(ListenerAgent):
491
+ role = "unique_name"
492
+ capabilities = ["skill1", "skill2"]
493
+
494
+ async def on_peer_request(self, msg): # Handle requests (return = response)
495
+ return {"result": ...}
496
+
497
+ async def on_peer_notify(self, msg): # Handle notifications (fire-and-forget)
498
+ await self.log(msg.data)
499
+ ```
500
+
501
+ ### 4. Mesh
398
502
 
399
503
  The `Mesh` is the orchestrator that manages agents and workflows:
400
504
 
@@ -408,10 +512,10 @@ await mesh.stop() # Cleanup
408
512
 
409
513
  **Modes:**
410
514
  - `autonomous`: Workflow engine only (AutoAgent)
411
- - `p2p`: P2P coordinator for agent-to-agent communication (CustomAgent)
412
- - `distributed`: Both workflow engine AND P2P (CustomAgent)
515
+ - `p2p`: P2P coordinator for agent-to-agent communication (CustomAgent, ListenerAgent)
516
+ - `distributed`: Both workflow engine AND P2P (CustomAgent, ListenerAgent)
413
517
 
414
- ### 4. Workflow
518
+ ### 5. Workflow
415
519
 
416
520
  A workflow is a list of tasks to execute:
417
521
 
@@ -425,7 +529,7 @@ results = await mesh.workflow("workflow-id", [
425
529
  ])
426
530
  ```
427
531
 
428
- ### 5. Results
532
+ ### 6. Results
429
533
 
430
534
  Each task returns a result dict:
431
535
 
@@ -689,7 +793,7 @@ Need help?
689
793
  python -m jarviscore.cli.smoketest --verbose
690
794
  ```
691
795
  3. **Check logs**: `cat logs/<agent>/<latest>.json`
692
- 4. **Report issues**: [GitHub Issues](https://github.com/yourusername/jarviscore/issues)
796
+ 4. **Report issues**: [GitHub Issues](https://github.com/Prescott-Data/jarviscore-framework/issues)
693
797
 
694
798
  ---
695
799
 
@@ -503,7 +503,7 @@ If issues persist:
503
503
  - Minimal code to reproduce issue
504
504
 
505
505
  4. **Create an issue:**
506
- - GitHub: https://github.com/yourusername/jarviscore/issues
506
+ - GitHub: https://github.com/Prescott-Data/jarviscore-framework/issues
507
507
  - Include diagnostics output above
508
508
 
509
509
  ---
@@ -557,10 +557,10 @@ If significantly slower:
557
557
 
558
558
  ---
559
559
 
560
- *Last updated: 2026-01-22*
560
+ *Last updated: 2026-01-23*
561
561
 
562
562
  ---
563
563
 
564
564
  ## Version
565
565
 
566
- Troubleshooting Guide for JarvisCore v0.2.0
566
+ Troubleshooting Guide for JarvisCore v0.2.1
@@ -16,9 +16,12 @@ Practical guide to building agent systems with JarvisCore.
16
16
  8. [Remote Sandbox](#remote-sandbox)
17
17
  9. [Result Storage](#result-storage)
18
18
  10. [Code Registry](#code-registry)
19
- 11. [Best Practices](#best-practices)
20
- 12. [Common Patterns](#common-patterns)
21
- 13. [Troubleshooting](#troubleshooting)
19
+ 11. [FastAPI Integration (v0.3.0)](#fastapi-integration-v030)
20
+ 12. [Cloud Deployment (v0.3.0)](#cloud-deployment-v030)
21
+ 13. [Cognitive Discovery (v0.3.0)](#cognitive-discovery-v030)
22
+ 14. [Best Practices](#best-practices)
23
+ 15. [Common Patterns](#common-patterns)
24
+ 16. [Troubleshooting](#troubleshooting)
22
25
 
23
26
  ---
24
27
 
@@ -138,12 +141,13 @@ mesh = Mesh(mode="distributed", config={'bind_port': 7950})
138
141
 
139
142
  ### Agents
140
143
 
141
- **Agents** are workers that execute tasks. JarvisCore offers two profiles:
144
+ **Agents** are workers that execute tasks. JarvisCore offers three profiles:
142
145
 
143
146
  | Profile | Best For | How It Works |
144
147
  |---------|----------|--------------|
145
148
  | **AutoAgent** | Rapid prototyping | LLM generates + executes code from prompts |
146
149
  | **CustomAgent** | Existing code | You provide `execute_task()` or `run()` |
150
+ | **ListenerAgent** | API-first agents | Just implement `on_peer_request()` handlers |
147
151
 
148
152
  See [AutoAgent Guide](AUTOAGENT_GUIDE.md) and [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for details.
149
153
 
@@ -534,6 +538,148 @@ print(f"Registered: {func['registered_at']}")
534
538
 
535
539
  ---
536
540
 
541
+ ## FastAPI Integration (v0.3.0)
542
+
543
+ Deploy agents as FastAPI services with minimal boilerplate:
544
+
545
+ ### JarvisLifespan
546
+
547
+ ```python
548
+ from fastapi import FastAPI, Request
549
+ from jarviscore.profiles import ListenerAgent
550
+ from jarviscore.integrations.fastapi import JarvisLifespan
551
+
552
+ class ProcessorAgent(ListenerAgent):
553
+ role = "processor"
554
+ capabilities = ["processing"]
555
+
556
+ async def on_peer_request(self, msg):
557
+ return {"result": msg.data.get("task", "").upper()}
558
+
559
+ # 3 lines to integrate
560
+ agent = ProcessorAgent()
561
+ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
562
+
563
+ @app.get("/peers")
564
+ async def list_peers(request: Request):
565
+ agent = request.app.state.jarvis_agents["processor"]
566
+ return {"peers": agent.peers.list_peers()}
567
+ ```
568
+
569
+ **What JarvisLifespan handles:**
570
+ - Mesh startup/shutdown
571
+ - Background task management for agent run() loops
572
+ - Graceful shutdown with timeouts
573
+ - State injection into FastAPI app
574
+
575
+ ---
576
+
577
+ ## Cloud Deployment (v0.3.0)
578
+
579
+ Deploy agents to containers without a central orchestrator:
580
+
581
+ ### Self-Registration Pattern
582
+
583
+ ```python
584
+ # In your container entrypoint
585
+ import asyncio
586
+ from jarviscore.profiles import ListenerAgent
587
+
588
+ class MyAgent(ListenerAgent):
589
+ role = "worker"
590
+ capabilities = ["processing"]
591
+
592
+ async def on_peer_request(self, msg):
593
+ return {"processed": msg.data}
594
+
595
+ async def main():
596
+ agent = MyAgent()
597
+
598
+ # Join existing mesh (uses JARVISCORE_SEED_NODES env var)
599
+ await agent.join_mesh()
600
+
601
+ print(f"Joined as {agent.role}, discovered: {agent.peers.list_peers()}")
602
+
603
+ # Run until shutdown, auto-leaves mesh on exit
604
+ await agent.run_standalone()
605
+
606
+ asyncio.run(main())
607
+ ```
608
+
609
+ ### Docker/Kubernetes
610
+
611
+ ```dockerfile
612
+ FROM python:3.11-slim
613
+ WORKDIR /app
614
+ COPY . .
615
+ RUN pip install jarviscore-framework
616
+
617
+ # Point to existing mesh
618
+ ENV JARVISCORE_SEED_NODES=mesh-service:7946
619
+
620
+ CMD ["python", "-m", "myapp.agent"]
621
+ ```
622
+
623
+ **Environment Variables:**
624
+ - `JARVISCORE_SEED_NODES` - Comma-separated list of seed nodes
625
+ - `JARVISCORE_MESH_ENDPOINT` - Single endpoint to join
626
+
627
+ ---
628
+
629
+ ## Cognitive Discovery (v0.3.0)
630
+
631
+ Let LLMs dynamically discover mesh peers instead of hardcoding agent names:
632
+
633
+ ### The Problem
634
+
635
+ ```python
636
+ # Before: Hardcoded peer names in prompts
637
+ system_prompt = """
638
+ You can delegate to:
639
+ - analyst for data analysis
640
+ - scout for research
641
+ """
642
+ # Breaks when mesh composition changes!
643
+ ```
644
+
645
+ ### The Solution
646
+
647
+ ```python
648
+ # After: Dynamic discovery
649
+ system_prompt = self.peers.build_system_prompt(
650
+ "You are a coordinator agent."
651
+ )
652
+ # Automatically includes all available peers!
653
+ ```
654
+
655
+ ### get_cognitive_context()
656
+
657
+ ```python
658
+ # Get prompt-ready peer descriptions
659
+ context = self.peers.get_cognitive_context(format="markdown")
660
+ ```
661
+
662
+ **Output:**
663
+ ```markdown
664
+ ## AVAILABLE MESH PEERS
665
+
666
+ You are part of a multi-agent mesh. The following peers are available:
667
+
668
+ - **analyst** (`agent-analyst-abc123`)
669
+ - Capabilities: analysis, charting, reporting
670
+ - Description: Analyzes data and generates insights
671
+
672
+ - **scout** (`agent-scout-def456`)
673
+ - Capabilities: research, reconnaissance
674
+ - Description: Gathers information
675
+
676
+ Use the `ask_peer` tool to delegate tasks to these specialists.
677
+ ```
678
+
679
+ **Formats:** `markdown`, `json`, `text`
680
+
681
+ ---
682
+
537
683
  ## Best Practices
538
684
 
539
685
  ### 1. Always Use Context Managers
@@ -759,6 +905,6 @@ mesh = Mesh(config=config)
759
905
 
760
906
  ## Version
761
907
 
762
- User Guide for JarvisCore v0.2.0
908
+ User Guide for JarvisCore v0.3.0
763
909
 
764
- Last Updated: 2026-01-22
910
+ Last Updated: 2026-01-29
@@ -0,0 +1,16 @@
1
+ """
2
+ Framework integrations for JarvisCore.
3
+
4
+ Provides first-class support for popular web frameworks,
5
+ reducing boilerplate for production deployments.
6
+
7
+ Available integrations:
8
+ - FastAPI: JarvisLifespan, create_jarvis_app
9
+ """
10
+
11
+ try:
12
+ from .fastapi import JarvisLifespan, create_jarvis_app
13
+ __all__ = ['JarvisLifespan', 'create_jarvis_app']
14
+ except ImportError:
15
+ # FastAPI not installed - integrations not available
16
+ __all__ = []
@@ -0,0 +1,247 @@
1
+ """
2
+ FastAPI integration for JarvisCore.
3
+
4
+ Reduces boilerplate from ~100 lines to 3 lines for integrating
5
+ JarvisCore agents with FastAPI applications.
6
+
7
+ Example:
8
+ from fastapi import FastAPI
9
+ from jarviscore.integrations.fastapi import JarvisLifespan
10
+
11
+ agent = MyAgent()
12
+ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
13
+
14
+ @app.get("/peers")
15
+ async def get_peers(request: Request):
16
+ agent = request.app.state.jarvis_agents.get("my_role")
17
+ return {"peers": agent.peers.list_peers()}
18
+ """
19
+ from contextlib import asynccontextmanager
20
+ from typing import Union, List, TYPE_CHECKING
21
+ import asyncio
22
+ import logging
23
+
24
+ if TYPE_CHECKING:
25
+ from jarviscore.core.agent import Agent
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class JarvisLifespan:
31
+ """
32
+ FastAPI lifespan manager for JarvisCore agents.
33
+
34
+ Handles the complete lifecycle of JarvisCore mesh integration:
35
+ - Mesh initialization on startup
36
+ - Background task management for agent run() loops
37
+ - Graceful shutdown with proper cleanup
38
+ - State injection into FastAPI app for handler access
39
+
40
+ Args:
41
+ agents: Single agent instance or list of agents to run
42
+ mode: Mesh mode - "p2p", "distributed", or "autonomous"
43
+ **mesh_config: Additional Mesh configuration options:
44
+ - bind_host: P2P bind address (default: "127.0.0.1")
45
+ - bind_port: P2P bind port (default: 7946)
46
+ - seed_nodes: Comma-separated seed nodes for joining cluster
47
+ - node_name: Node identifier for P2P network
48
+
49
+ Example - Single Agent:
50
+ from fastapi import FastAPI, Request
51
+ from jarviscore.integrations.fastapi import JarvisLifespan
52
+ from jarviscore.profiles import ListenerAgent
53
+
54
+ class MyAgent(ListenerAgent):
55
+ role = "processor"
56
+ capabilities = ["processing"]
57
+
58
+ async def on_peer_request(self, msg):
59
+ return {"processed": msg.data}
60
+
61
+ agent = MyAgent()
62
+ app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
63
+
64
+ @app.get("/health")
65
+ async def health(request: Request):
66
+ mesh = request.app.state.jarvis_mesh
67
+ return {"status": "ok", "mesh_started": mesh._started}
68
+
69
+ Example - Multiple Agents:
70
+ agents = [ProcessorAgent(), AnalyzerAgent()]
71
+ app = FastAPI(lifespan=JarvisLifespan(agents, mode="p2p"))
72
+
73
+ Example - Joining Existing Cluster:
74
+ app = FastAPI(lifespan=JarvisLifespan(
75
+ agent,
76
+ mode="p2p",
77
+ seed_nodes="192.168.1.10:7946,192.168.1.11:7946"
78
+ ))
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ agents: Union['Agent', List['Agent']],
84
+ mode: str = "p2p",
85
+ **mesh_config
86
+ ):
87
+ """
88
+ Initialize JarvisLifespan.
89
+
90
+ Args:
91
+ agents: Single agent or list of agents to run
92
+ mode: Mesh mode ("p2p", "distributed", "autonomous")
93
+ **mesh_config: Additional Mesh configuration
94
+ """
95
+ self.agents = agents if isinstance(agents, list) else [agents]
96
+ self.mode = mode
97
+ self.mesh_config = mesh_config
98
+ self.mesh = None
99
+ self._background_tasks: List[asyncio.Task] = []
100
+ self._nodes: List['Agent'] = []
101
+
102
+ @asynccontextmanager
103
+ async def __call__(self, app):
104
+ """
105
+ ASGI lifespan context manager.
106
+
107
+ Called by FastAPI/Starlette on app startup/shutdown.
108
+ Manages the complete mesh lifecycle.
109
+ """
110
+ from jarviscore import Mesh
111
+
112
+ # ─────────────────────────────────────────────────────────────
113
+ # STARTUP
114
+ # ─────────────────────────────────────────────────────────────
115
+ logger.info(f"JarvisLifespan: Starting mesh in {self.mode} mode...")
116
+
117
+ # 1. Create mesh with provided configuration
118
+ self.mesh = Mesh(mode=self.mode, config=self.mesh_config)
119
+
120
+ # 2. Register all agents with the mesh
121
+ self._nodes = []
122
+ for agent in self.agents:
123
+ node = self.mesh.add(agent)
124
+ self._nodes.append(node)
125
+ logger.debug(f"JarvisLifespan: Registered agent {node.role}")
126
+
127
+ # 3. Start mesh (initializes P2P coordinator, injects PeerClients)
128
+ await self.mesh.start()
129
+ logger.info(f"JarvisLifespan: Mesh started with {len(self._nodes)} agent(s)")
130
+
131
+ # 4. Launch agent run() loops as background tasks
132
+ # This is crucial - without backgrounding, the HTTP server would hang
133
+ for node in self._nodes:
134
+ if hasattr(node, 'run') and asyncio.iscoroutinefunction(node.run):
135
+ task = asyncio.create_task(
136
+ self._run_agent_with_error_handling(node),
137
+ name=f"jarvis-agent-{node.agent_id}"
138
+ )
139
+ self._background_tasks.append(task)
140
+ logger.info(f"JarvisLifespan: Started background loop for {node.role}")
141
+
142
+ # 5. Inject state into FastAPI app for handler access
143
+ app.state.jarvis_mesh = self.mesh
144
+ app.state.jarvis_agents = {node.role: node for node in self._nodes}
145
+
146
+ logger.info("JarvisLifespan: Startup complete")
147
+
148
+ # ─────────────────────────────────────────────────────────────
149
+ # APP RUNS HERE
150
+ # ─────────────────────────────────────────────────────────────
151
+ try:
152
+ yield
153
+ finally:
154
+ # ─────────────────────────────────────────────────────────────
155
+ # SHUTDOWN
156
+ # ─────────────────────────────────────────────────────────────
157
+ logger.info("JarvisLifespan: Shutting down...")
158
+
159
+ # 1. Request shutdown for all agents (signals run() loops to exit)
160
+ for node in self._nodes:
161
+ node.request_shutdown()
162
+
163
+ # 2. Cancel background tasks gracefully with timeout
164
+ for task in self._background_tasks:
165
+ if not task.done():
166
+ task.cancel()
167
+ try:
168
+ await asyncio.wait_for(task, timeout=5.0)
169
+ except (asyncio.CancelledError, asyncio.TimeoutError):
170
+ pass
171
+
172
+ # 3. Stop mesh (cleanup P2P coordinator, call agent teardown)
173
+ if self.mesh:
174
+ await self.mesh.stop()
175
+
176
+ logger.info("JarvisLifespan: Shutdown complete")
177
+
178
+ async def _run_agent_with_error_handling(self, agent: 'Agent'):
179
+ """
180
+ Run agent loop with error handling and logging.
181
+
182
+ Wraps the agent's run() method to catch and log errors
183
+ without crashing the entire application.
184
+ """
185
+ try:
186
+ await agent.run()
187
+ except asyncio.CancelledError:
188
+ logger.debug(f"Agent {agent.agent_id} loop cancelled")
189
+ raise
190
+ except Exception as e:
191
+ logger.error(f"Agent {agent.agent_id} loop error: {e}", exc_info=True)
192
+ # Re-raise to allow task to be marked as failed
193
+ raise
194
+
195
+
196
+ def create_jarvis_app(
197
+ agent: 'Agent',
198
+ mode: str = "p2p",
199
+ title: str = "JarvisCore Agent",
200
+ description: str = "API powered by JarvisCore",
201
+ version: str = "1.0.0",
202
+ **mesh_config
203
+ ) -> 'FastAPI':
204
+ """
205
+ Create a FastAPI app with JarvisCore integration pre-configured.
206
+
207
+ Convenience function for simple single-agent deployments.
208
+ For more control, use JarvisLifespan directly.
209
+
210
+ Args:
211
+ agent: Agent instance to run
212
+ mode: Mesh mode ("p2p", "distributed", "autonomous")
213
+ title: FastAPI app title
214
+ description: FastAPI app description
215
+ version: API version
216
+ **mesh_config: Mesh configuration options
217
+
218
+ Returns:
219
+ Configured FastAPI app with JarvisCore integration
220
+
221
+ Example:
222
+ from jarviscore.integrations.fastapi import create_jarvis_app
223
+ from jarviscore.profiles import ListenerAgent
224
+
225
+ class MyAgent(ListenerAgent):
226
+ role = "processor"
227
+ capabilities = ["processing"]
228
+
229
+ async def on_peer_request(self, msg):
230
+ return {"result": "processed"}
231
+
232
+ app = create_jarvis_app(MyAgent(), mode="p2p", bind_port=7950)
233
+
234
+ @app.get("/health")
235
+ async def health():
236
+ return {"status": "ok"}
237
+
238
+ # Run with: uvicorn myapp:app --host 0.0.0.0 --port 8000
239
+ """
240
+ from fastapi import FastAPI
241
+
242
+ return FastAPI(
243
+ title=title,
244
+ description=description,
245
+ version=version,
246
+ lifespan=JarvisLifespan(agent, mode=mode, **mesh_config)
247
+ )
@@ -143,12 +143,19 @@ class StepOutputBroadcaster:
143
143
  """
144
144
  try:
145
145
  message_id = message_data.get('id', str(uuid.uuid4()))
146
- step_result_data = message_data.get('step_result', {})
147
-
146
+
147
+ # The sender uses 'step_output_data' as the key (json string)
148
+ step_output_json = message_data.get('step_output_data', '')
149
+ if step_output_json:
150
+ step_result_data = json.loads(step_output_json)
151
+ else:
152
+ # Fallback for legacy format
153
+ step_result_data = message_data.get('step_result', {})
154
+
148
155
  # Send acknowledgment if callback provided
149
156
  if send_ack_callback:
150
157
  await send_ack_callback(sender_swim_id, message_id, "STEP_OUTPUT_BROADCAST")
151
-
158
+
152
159
  # Create StepExecutionResult from received data
153
160
  result = StepExecutionResult(**step_result_data)
154
161