jarviscore-framework 0.1.0__py3-none-any.whl → 0.2.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 (99) hide show
  1. examples/autoagent_distributed_example.py +211 -0
  2. examples/custom_profile_decorator.py +134 -0
  3. examples/custom_profile_wrap.py +168 -0
  4. examples/customagent_distributed_example.py +362 -0
  5. examples/customagent_p2p_example.py +347 -0
  6. jarviscore/__init__.py +60 -15
  7. jarviscore/adapter/__init__.py +40 -0
  8. jarviscore/adapter/decorator.py +336 -0
  9. jarviscore/adapter/wrapper.py +303 -0
  10. jarviscore/cli/check.py +18 -13
  11. jarviscore/cli/scaffold.py +178 -0
  12. jarviscore/cli/smoketest.py +3 -2
  13. jarviscore/context/__init__.py +40 -0
  14. jarviscore/context/dependency.py +160 -0
  15. jarviscore/context/jarvis_context.py +207 -0
  16. jarviscore/context/memory.py +155 -0
  17. jarviscore/core/agent.py +44 -1
  18. jarviscore/core/mesh.py +196 -35
  19. jarviscore/data/.env.example +146 -0
  20. jarviscore/data/__init__.py +7 -0
  21. jarviscore/data/examples/autoagent_distributed_example.py +211 -0
  22. jarviscore/data/examples/calculator_agent_example.py +77 -0
  23. jarviscore/data/examples/customagent_distributed_example.py +362 -0
  24. jarviscore/data/examples/customagent_p2p_example.py +347 -0
  25. jarviscore/data/examples/multi_agent_workflow.py +132 -0
  26. jarviscore/data/examples/research_agent_example.py +76 -0
  27. jarviscore/docs/API_REFERENCE.md +264 -51
  28. jarviscore/docs/AUTOAGENT_GUIDE.md +198 -0
  29. jarviscore/docs/CONFIGURATION.md +41 -23
  30. jarviscore/docs/CUSTOMAGENT_GUIDE.md +415 -0
  31. jarviscore/docs/GETTING_STARTED.md +113 -17
  32. jarviscore/docs/TROUBLESHOOTING.md +155 -13
  33. jarviscore/docs/USER_GUIDE.md +144 -363
  34. jarviscore/execution/llm.py +23 -16
  35. jarviscore/orchestration/engine.py +20 -8
  36. jarviscore/p2p/__init__.py +10 -0
  37. jarviscore/p2p/coordinator.py +129 -0
  38. jarviscore/p2p/messages.py +87 -0
  39. jarviscore/p2p/peer_client.py +576 -0
  40. jarviscore/p2p/peer_tool.py +268 -0
  41. jarviscore_framework-0.2.0.dist-info/METADATA +143 -0
  42. jarviscore_framework-0.2.0.dist-info/RECORD +132 -0
  43. {jarviscore_framework-0.1.0.dist-info → jarviscore_framework-0.2.0.dist-info}/WHEEL +1 -1
  44. {jarviscore_framework-0.1.0.dist-info → jarviscore_framework-0.2.0.dist-info}/top_level.txt +1 -0
  45. test_logs/code_registry/functions/data_generator-558779ed_560ebc37.py +7 -0
  46. test_logs/code_registry/functions/data_generator-5ed3609e_560ebc37.py +7 -0
  47. test_logs/code_registry/functions/data_generator-66da0356_43970bb9.py +25 -0
  48. test_logs/code_registry/functions/data_generator-7a2fac83_583709d9.py +36 -0
  49. test_logs/code_registry/functions/data_generator-888b670f_aa235863.py +9 -0
  50. test_logs/code_registry/functions/data_generator-9ca5f642_aa235863.py +9 -0
  51. test_logs/code_registry/functions/data_generator-bfd90775_560ebc37.py +7 -0
  52. test_logs/code_registry/functions/data_generator-e95d2f7d_aa235863.py +9 -0
  53. test_logs/code_registry/functions/data_generator-f60ca8a2_327eb8c2.py +29 -0
  54. test_logs/code_registry/functions/mathematician-02adf9ee_958658d9.py +19 -0
  55. test_logs/code_registry/functions/mathematician-0706fb57_5df13441.py +23 -0
  56. test_logs/code_registry/functions/mathematician-153c9c4a_ba59c918.py +83 -0
  57. test_logs/code_registry/functions/mathematician-287e61c0_41daa793.py +18 -0
  58. test_logs/code_registry/functions/mathematician-2967af5a_863c2cc6.py +17 -0
  59. test_logs/code_registry/functions/mathematician-303ca6d6_5df13441.py +23 -0
  60. test_logs/code_registry/functions/mathematician-308a4afd_cbf5064d.py +73 -0
  61. test_logs/code_registry/functions/mathematician-353f16e2_0968bcf5.py +18 -0
  62. test_logs/code_registry/functions/mathematician-3c22475a_41daa793.py +17 -0
  63. test_logs/code_registry/functions/mathematician-5bac1029_0968bcf5.py +18 -0
  64. test_logs/code_registry/functions/mathematician-640f76b2_9198780b.py +19 -0
  65. test_logs/code_registry/functions/mathematician-752fa7ea_863c2cc6.py +17 -0
  66. test_logs/code_registry/functions/mathematician-baf9ef39_0968bcf5.py +18 -0
  67. test_logs/code_registry/functions/mathematician-bc8b2a2f_5df13441.py +23 -0
  68. test_logs/code_registry/functions/mathematician-c31e4686_41daa793.py +18 -0
  69. test_logs/code_registry/functions/mathematician-cc84c84c_863c2cc6.py +17 -0
  70. test_logs/code_registry/functions/mathematician-dd7c7144_9198780b.py +19 -0
  71. test_logs/code_registry/functions/mathematician-e671c256_41ea4487.py +74 -0
  72. test_logs/code_registry/functions/report_generator-1a878fcc_18d44bdc.py +47 -0
  73. test_logs/code_registry/functions/report_generator-25c1c331_cea57d0d.py +35 -0
  74. test_logs/code_registry/functions/report_generator-37552117_e711c2b9.py +35 -0
  75. test_logs/code_registry/functions/report_generator-bc662768_e711c2b9.py +35 -0
  76. test_logs/code_registry/functions/report_generator-d6c0e76b_5e7722ec.py +44 -0
  77. test_logs/code_registry/functions/report_generator-f270fb02_680529c3.py +44 -0
  78. test_logs/code_registry/functions/text_processor-11393b14_4370d3ed.py +40 -0
  79. test_logs/code_registry/functions/text_processor-7d02dfc3_d3b569be.py +37 -0
  80. test_logs/code_registry/functions/text_processor-8adb5e32_9168c5fe.py +13 -0
  81. test_logs/code_registry/functions/text_processor-c58ffc19_78b4ceac.py +42 -0
  82. test_logs/code_registry/functions/text_processor-cd5977b1_9168c5fe.py +13 -0
  83. test_logs/code_registry/functions/text_processor-ec1c8773_9168c5fe.py +13 -0
  84. tests/test_01_analyst_standalone.py +124 -0
  85. tests/test_02_assistant_standalone.py +164 -0
  86. tests/test_03_analyst_with_framework.py +945 -0
  87. tests/test_04_assistant_with_framework.py +1002 -0
  88. tests/test_05_integration.py +1301 -0
  89. tests/test_06_real_llm_integration.py +760 -0
  90. tests/test_07_distributed_single_node.py +578 -0
  91. tests/test_08_distributed_multi_node.py +454 -0
  92. tests/test_09_distributed_autoagent.py +509 -0
  93. tests/test_10_distributed_customagent.py +787 -0
  94. tests/test_context.py +467 -0
  95. tests/test_decorator.py +622 -0
  96. tests/test_mesh.py +35 -4
  97. jarviscore_framework-0.1.0.dist-info/METADATA +0 -136
  98. jarviscore_framework-0.1.0.dist-info/RECORD +0 -55
  99. {jarviscore_framework-0.1.0.dist-info → jarviscore_framework-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,576 @@
1
+ """
2
+ PeerClient - Direct peer-to-peer communication for agents.
3
+
4
+ Provides a simple API for agents to discover and communicate
5
+ with other agents in the mesh without going through workflow orchestration.
6
+
7
+ Example:
8
+ class MyAgent(JarvisAgent):
9
+ async def run(self):
10
+ # Discovery
11
+ analyst = self.peers.get_peer(role="analyst")
12
+
13
+ # Notify (fire-and-forget)
14
+ await self.peers.notify("analyst", {"event": "done", "data": result})
15
+
16
+ # Request-response
17
+ response = await self.peers.request("scout", {"need": "clarification"}, timeout=30)
18
+
19
+ # Receive incoming messages
20
+ message = await self.peers.receive(timeout=5)
21
+ """
22
+ import asyncio
23
+ import logging
24
+ from typing import List, Dict, Any, Optional, TYPE_CHECKING
25
+ from uuid import uuid4
26
+
27
+ from .messages import PeerInfo, IncomingMessage, OutgoingMessage, MessageType
28
+
29
+ if TYPE_CHECKING:
30
+ from .coordinator import P2PCoordinator
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class PeerClient:
36
+ """
37
+ Client for peer-to-peer agent communication.
38
+
39
+ Injected into agents during mesh startup, provides direct access
40
+ to peer discovery and messaging without workflow orchestration.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ coordinator: 'P2PCoordinator',
46
+ agent_id: str,
47
+ agent_role: str,
48
+ agent_registry: Dict[str, List],
49
+ node_id: str = ""
50
+ ):
51
+ """
52
+ Initialize PeerClient.
53
+
54
+ Args:
55
+ coordinator: P2P coordinator for message routing
56
+ agent_id: This agent's unique ID
57
+ agent_role: This agent's role
58
+ agent_registry: Registry mapping roles to agent lists
59
+ node_id: This node's P2P identifier (host:port)
60
+ """
61
+ self._coordinator = coordinator
62
+ self._agent_id = agent_id
63
+ self._agent_role = agent_role
64
+ self._agent_registry = agent_registry
65
+ self._node_id = node_id
66
+
67
+ # Message queue for incoming messages
68
+ self._message_queue: asyncio.Queue[IncomingMessage] = asyncio.Queue()
69
+
70
+ # Pending requests waiting for responses (correlation_id -> Future)
71
+ self._pending_requests: Dict[str, asyncio.Future] = {}
72
+
73
+ self._logger = logging.getLogger(f"jarviscore.peer_client.{agent_id}")
74
+
75
+ # ─────────────────────────────────────────────────────────────────
76
+ # DISCOVERY
77
+ # ─────────────────────────────────────────────────────────────────
78
+
79
+ def get_peer(self, role: str) -> Optional[PeerInfo]:
80
+ """
81
+ Get information about a peer by role.
82
+
83
+ Args:
84
+ role: The role to look for (e.g., "analyst", "scout")
85
+
86
+ Returns:
87
+ PeerInfo if found, None otherwise
88
+
89
+ Example:
90
+ analyst = self.peers.get_peer(role="analyst")
91
+ if analyst:
92
+ print(f"Found analyst: {analyst.agent_id}")
93
+ """
94
+ agents = self._agent_registry.get(role, [])
95
+ if not agents:
96
+ self._logger.debug(f"No peer found with role: {role}")
97
+ return None
98
+
99
+ # Return first agent with this role
100
+ agent = agents[0]
101
+ return PeerInfo(
102
+ agent_id=agent.agent_id,
103
+ role=agent.role,
104
+ capabilities=list(agent.capabilities),
105
+ node_id=self._node_id,
106
+ status="alive"
107
+ )
108
+
109
+ def discover(
110
+ self,
111
+ capability: str = None,
112
+ role: str = None
113
+ ) -> List[PeerInfo]:
114
+ """
115
+ Discover peers by capability or role.
116
+
117
+ Args:
118
+ capability: Filter by capability (e.g., "analysis")
119
+ role: Filter by role (e.g., "analyst")
120
+
121
+ Returns:
122
+ List of matching PeerInfo objects
123
+
124
+ Example:
125
+ analysts = self.peers.discover(capability="analysis")
126
+ for peer in analysts:
127
+ print(f"Found: {peer.role} - {peer.capabilities}")
128
+ """
129
+ results = []
130
+
131
+ if role:
132
+ agents = self._agent_registry.get(role, [])
133
+ for agent in agents:
134
+ if agent.agent_id != self._agent_id: # Exclude self
135
+ results.append(PeerInfo(
136
+ agent_id=agent.agent_id,
137
+ role=agent.role,
138
+ capabilities=list(agent.capabilities),
139
+ node_id=self._node_id,
140
+ status="alive"
141
+ ))
142
+
143
+ elif capability:
144
+ # Search all agents for capability
145
+ for role_name, agents in self._agent_registry.items():
146
+ for agent in agents:
147
+ if agent.agent_id != self._agent_id: # Exclude self
148
+ if capability in agent.capabilities:
149
+ results.append(PeerInfo(
150
+ agent_id=agent.agent_id,
151
+ role=agent.role,
152
+ capabilities=list(agent.capabilities),
153
+ node_id=self._node_id,
154
+ status="alive"
155
+ ))
156
+
157
+ else:
158
+ # Return all peers
159
+ for role_name, agents in self._agent_registry.items():
160
+ for agent in agents:
161
+ if agent.agent_id != self._agent_id: # Exclude self
162
+ results.append(PeerInfo(
163
+ agent_id=agent.agent_id,
164
+ role=agent.role,
165
+ capabilities=list(agent.capabilities),
166
+ node_id=self._node_id,
167
+ status="alive"
168
+ ))
169
+
170
+ return results
171
+
172
+ @property
173
+ def registry(self) -> Dict[str, PeerInfo]:
174
+ """
175
+ Read-only access to the full agent registry.
176
+
177
+ Returns:
178
+ Dictionary mapping agent_id to PeerInfo
179
+
180
+ Example:
181
+ for agent_id, info in self.peers.registry.items():
182
+ print(f"{agent_id}: {info.role}")
183
+ """
184
+ result = {}
185
+ for role_name, agents in self._agent_registry.items():
186
+ for agent in agents:
187
+ if agent.agent_id != self._agent_id: # Exclude self
188
+ result[agent.agent_id] = PeerInfo(
189
+ agent_id=agent.agent_id,
190
+ role=agent.role,
191
+ capabilities=list(agent.capabilities),
192
+ node_id=self._node_id,
193
+ status="alive"
194
+ )
195
+ return result
196
+
197
+ # ─────────────────────────────────────────────────────────────────
198
+ # IDENTITY
199
+ # ─────────────────────────────────────────────────────────────────
200
+
201
+ @property
202
+ def my_role(self) -> str:
203
+ """This agent's role."""
204
+ return self._agent_role
205
+
206
+ @property
207
+ def my_id(self) -> str:
208
+ """This agent's unique ID."""
209
+ return self._agent_id
210
+
211
+ # ─────────────────────────────────────────────────────────────────
212
+ # DISCOVERY (simplified for tool use)
213
+ # ─────────────────────────────────────────────────────────────────
214
+
215
+ def list_roles(self) -> List[str]:
216
+ """
217
+ Get list of available peer roles (excluding self).
218
+
219
+ Returns:
220
+ List of role strings like ["scout", "analyst"]
221
+
222
+ Example:
223
+ roles = self.peers.list_roles()
224
+ # ["scout", "analyst", "reporter"]
225
+ """
226
+ roles = set()
227
+ for role_name, agents in self._agent_registry.items():
228
+ for agent in agents:
229
+ if agent.agent_id != self._agent_id:
230
+ roles.add(role_name)
231
+ return sorted(list(roles))
232
+
233
+ def list_peers(self) -> List[Dict[str, Any]]:
234
+ """
235
+ Get detailed list of peers with capabilities.
236
+
237
+ Returns:
238
+ List of dicts with role, agent_id, capabilities, status
239
+
240
+ Example:
241
+ peers = self.peers.list_peers()
242
+ # [{"role": "scout", "capabilities": ["reasoning"], ...}]
243
+ """
244
+ seen = set()
245
+ peers = []
246
+ for role_name, agents in self._agent_registry.items():
247
+ for agent in agents:
248
+ if agent.agent_id != self._agent_id and agent.agent_id not in seen:
249
+ seen.add(agent.agent_id)
250
+ peers.append({
251
+ "role": agent.role,
252
+ "agent_id": agent.agent_id,
253
+ "capabilities": list(agent.capabilities),
254
+ "status": "online"
255
+ })
256
+ return peers
257
+
258
+ # ─────────────────────────────────────────────────────────────────
259
+ # MESSAGING - SEND
260
+ # ─────────────────────────────────────────────────────────────────
261
+
262
+ async def notify(self, target: str, message: Dict[str, Any]) -> bool:
263
+ """
264
+ Send a fire-and-forget notification to a peer.
265
+
266
+ Args:
267
+ target: Target agent role (e.g., "analyst") or agent_id
268
+ message: Message payload (any JSON-serializable dict)
269
+
270
+ Returns:
271
+ True if message was sent successfully
272
+
273
+ Example:
274
+ await self.peers.notify("analyst", {
275
+ "event": "scouting_complete",
276
+ "data": {"findings": 42}
277
+ })
278
+ """
279
+ target_agent = self._resolve_target(target)
280
+ if not target_agent:
281
+ self._logger.warning(f"Cannot notify: no peer found for '{target}'")
282
+ return False
283
+
284
+ outgoing = OutgoingMessage(
285
+ target=target,
286
+ type=MessageType.NOTIFY,
287
+ data=message,
288
+ sender=self._agent_id,
289
+ sender_node=self._node_id
290
+ )
291
+
292
+ return await self._send_message(target_agent, outgoing)
293
+
294
+ async def request(
295
+ self,
296
+ target: str,
297
+ message: Dict[str, Any],
298
+ timeout: float = 30.0
299
+ ) -> Optional[Dict[str, Any]]:
300
+ """
301
+ Send a request and wait for a response.
302
+
303
+ Args:
304
+ target: Target agent role (e.g., "scout") or agent_id
305
+ message: Request payload
306
+ timeout: Max seconds to wait for response (default: 30)
307
+
308
+ Returns:
309
+ Response data dict, or None if timeout/failure
310
+
311
+ Example:
312
+ response = await self.peers.request("scout", {
313
+ "need": "clarification",
314
+ "entity": "Entity_X"
315
+ }, timeout=10)
316
+
317
+ if response:
318
+ print(f"Got clarification: {response}")
319
+ """
320
+ target_agent = self._resolve_target(target)
321
+ if not target_agent:
322
+ self._logger.warning(f"Cannot request: no peer found for '{target}'")
323
+ return None
324
+
325
+ # Generate correlation ID for request-response matching
326
+ correlation_id = f"req-{uuid4().hex[:12]}"
327
+
328
+ outgoing = OutgoingMessage(
329
+ target=target,
330
+ type=MessageType.REQUEST,
331
+ data=message,
332
+ correlation_id=correlation_id,
333
+ sender=self._agent_id,
334
+ sender_node=self._node_id
335
+ )
336
+
337
+ # Create future to wait for response
338
+ response_future: asyncio.Future = asyncio.get_event_loop().create_future()
339
+ self._pending_requests[correlation_id] = response_future
340
+
341
+ try:
342
+ # Send request
343
+ sent = await self._send_message(target_agent, outgoing)
344
+ if not sent:
345
+ return None
346
+
347
+ # Wait for response with timeout
348
+ response = await asyncio.wait_for(response_future, timeout=timeout)
349
+ return response
350
+
351
+ except asyncio.TimeoutError:
352
+ self._logger.debug(f"Request to '{target}' timed out after {timeout}s")
353
+ return None
354
+
355
+ finally:
356
+ # Cleanup pending request
357
+ self._pending_requests.pop(correlation_id, None)
358
+
359
+ async def respond(self, message: IncomingMessage, response: Dict[str, Any]) -> bool:
360
+ """
361
+ Respond to an incoming request.
362
+
363
+ Args:
364
+ message: The incoming request message
365
+ response: Response data to send back
366
+
367
+ Returns:
368
+ True if response was sent successfully
369
+
370
+ Example:
371
+ message = await self.peers.receive()
372
+ if message and message.is_request:
373
+ await self.peers.respond(message, {"result": "done"})
374
+ """
375
+ if not message.correlation_id:
376
+ self._logger.warning("Cannot respond: message has no correlation_id")
377
+ return False
378
+
379
+ # Find target agent
380
+ target_agent = self._resolve_target(message.sender)
381
+ if not target_agent:
382
+ self._logger.warning(f"Cannot respond: sender '{message.sender}' not found")
383
+ return False
384
+
385
+ outgoing = OutgoingMessage(
386
+ target=message.sender,
387
+ type=MessageType.RESPONSE,
388
+ data=response,
389
+ correlation_id=message.correlation_id,
390
+ sender=self._agent_id,
391
+ sender_node=self._node_id
392
+ )
393
+
394
+ return await self._send_message(target_agent, outgoing)
395
+
396
+ async def broadcast(self, message: Dict[str, Any]) -> int:
397
+ """
398
+ Broadcast notification to ALL peers.
399
+
400
+ Args:
401
+ message: Message payload to broadcast
402
+
403
+ Returns:
404
+ Number of peers successfully notified
405
+
406
+ Example:
407
+ count = await self.peers.broadcast({
408
+ "event": "status_update",
409
+ "status": "completed"
410
+ })
411
+ print(f"Notified {count} peers")
412
+ """
413
+ count = 0
414
+ for peer in self.discover():
415
+ if await self.notify(peer.role, message):
416
+ count += 1
417
+ return count
418
+
419
+ # ─────────────────────────────────────────────────────────────────
420
+ # TOOL ADAPTER
421
+ # ─────────────────────────────────────────────────────────────────
422
+
423
+ def as_tool(self) -> 'PeerTool':
424
+ """
425
+ Get LLM tool adapter for this PeerClient.
426
+
427
+ Returns a PeerTool that wraps this client, providing:
428
+ - Tool definitions for LLM injection
429
+ - Tool execution dispatch
430
+
431
+ Returns:
432
+ PeerTool instance
433
+
434
+ Example:
435
+ # In your agent
436
+ tools = [SearchTool(), self.peers.as_tool()]
437
+ response = llm.chat(task, tools=[t.schema for t in tools])
438
+ """
439
+ from .peer_tool import PeerTool
440
+ return PeerTool(self)
441
+
442
+ # ─────────────────────────────────────────────────────────────────
443
+ # MESSAGING - RECEIVE
444
+ # ─────────────────────────────────────────────────────────────────
445
+
446
+ async def receive(self, timeout: float = None) -> Optional[IncomingMessage]:
447
+ """
448
+ Receive the next incoming message.
449
+
450
+ Args:
451
+ timeout: Max seconds to wait (None = wait forever)
452
+
453
+ Returns:
454
+ IncomingMessage if received, None if timeout
455
+
456
+ Example:
457
+ # Wait up to 5 seconds for a message
458
+ message = await self.peers.receive(timeout=5)
459
+ if message:
460
+ print(f"Got message from {message.sender}: {message.data}")
461
+ """
462
+ try:
463
+ if timeout is not None:
464
+ message = await asyncio.wait_for(
465
+ self._message_queue.get(),
466
+ timeout=timeout
467
+ )
468
+ else:
469
+ message = await self._message_queue.get()
470
+
471
+ return message
472
+
473
+ except asyncio.TimeoutError:
474
+ return None
475
+
476
+ def has_pending_messages(self) -> bool:
477
+ """Check if there are messages waiting to be received."""
478
+ return not self._message_queue.empty()
479
+
480
+ # ─────────────────────────────────────────────────────────────────
481
+ # INTERNAL METHODS
482
+ # ─────────────────────────────────────────────────────────────────
483
+
484
+ def _resolve_target(self, target: str):
485
+ """
486
+ Resolve target string to agent.
487
+
488
+ Args:
489
+ target: Role name or agent_id
490
+
491
+ Returns:
492
+ Agent instance or None
493
+ """
494
+ # First try as role
495
+ agents = self._agent_registry.get(target, [])
496
+ if agents:
497
+ return agents[0]
498
+
499
+ # Try as agent_id
500
+ for role_name, agents in self._agent_registry.items():
501
+ for agent in agents:
502
+ if agent.agent_id == target:
503
+ return agent
504
+
505
+ return None
506
+
507
+ async def _send_message(self, target_agent, message: OutgoingMessage) -> bool:
508
+ """
509
+ Send message to target agent via coordinator.
510
+
511
+ For local agents (same mesh), delivers directly to their queue.
512
+ For remote agents, sends via P2P coordinator.
513
+ """
514
+ try:
515
+ # Check if target has a peer client (local agent)
516
+ if hasattr(target_agent, 'peers') and target_agent.peers:
517
+ # Direct local delivery
518
+ incoming = IncomingMessage(
519
+ sender=message.sender,
520
+ sender_node=message.sender_node,
521
+ type=message.type,
522
+ data=message.data,
523
+ correlation_id=message.correlation_id,
524
+ timestamp=message.timestamp
525
+ )
526
+ await target_agent.peers._deliver_message(incoming)
527
+ self._logger.debug(
528
+ f"Delivered {message.type.value} to local agent {target_agent.agent_id}"
529
+ )
530
+ return True
531
+
532
+ # Remote delivery via P2P coordinator
533
+ if self._coordinator:
534
+ msg_type = f"PEER_{message.type.value.upper()}"
535
+ payload = {
536
+ 'sender': message.sender,
537
+ 'sender_node': message.sender_node,
538
+ 'target': message.target,
539
+ 'data': message.data,
540
+ 'correlation_id': message.correlation_id,
541
+ 'timestamp': message.timestamp
542
+ }
543
+ return await self._coordinator._send_p2p_message(
544
+ target_agent.node_id or self._node_id,
545
+ msg_type,
546
+ payload
547
+ )
548
+
549
+ self._logger.warning("No delivery mechanism available")
550
+ return False
551
+
552
+ except Exception as e:
553
+ self._logger.error(f"Failed to send message: {e}")
554
+ return False
555
+
556
+ async def _deliver_message(self, message: IncomingMessage):
557
+ """
558
+ Deliver an incoming message to this client.
559
+
560
+ Called by other PeerClients (local) or coordinator (remote).
561
+ """
562
+ # Check if this is a response to a pending request
563
+ if message.type == MessageType.RESPONSE and message.correlation_id:
564
+ future = self._pending_requests.get(message.correlation_id)
565
+ if future and not future.done():
566
+ future.set_result(message.data)
567
+ self._logger.debug(
568
+ f"Delivered response for {message.correlation_id}"
569
+ )
570
+ return
571
+
572
+ # Otherwise queue for receive()
573
+ await self._message_queue.put(message)
574
+ self._logger.debug(
575
+ f"Queued {message.type.value} from {message.sender}"
576
+ )