agent-mcp 0.1.2__py3-none-any.whl → 0.1.4__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 (53) hide show
  1. agent_mcp/__init__.py +16 -0
  2. agent_mcp/camel_mcp_adapter.py +521 -0
  3. agent_mcp/cli.py +47 -0
  4. agent_mcp/crewai_mcp_adapter.py +281 -0
  5. agent_mcp/enhanced_mcp_agent.py +601 -0
  6. agent_mcp/heterogeneous_group_chat.py +798 -0
  7. agent_mcp/langchain_mcp_adapter.py +458 -0
  8. agent_mcp/langgraph_mcp_adapter.py +325 -0
  9. agent_mcp/mcp_agent.py +658 -0
  10. agent_mcp/mcp_decorator.py +257 -0
  11. agent_mcp/mcp_langgraph.py +733 -0
  12. agent_mcp/mcp_transaction.py +97 -0
  13. agent_mcp/mcp_transport.py +706 -0
  14. agent_mcp/mcp_transport_enhanced.py +46 -0
  15. agent_mcp/proxy_agent.py +24 -0
  16. agent_mcp-0.1.4.dist-info/METADATA +333 -0
  17. agent_mcp-0.1.4.dist-info/RECORD +49 -0
  18. {agent_mcp-0.1.2.dist-info → agent_mcp-0.1.4.dist-info}/WHEEL +1 -1
  19. agent_mcp-0.1.4.dist-info/entry_points.txt +2 -0
  20. agent_mcp-0.1.4.dist-info/top_level.txt +3 -0
  21. demos/__init__.py +1 -0
  22. demos/basic/__init__.py +1 -0
  23. demos/basic/framework_examples.py +108 -0
  24. demos/basic/langchain_camel_demo.py +272 -0
  25. demos/basic/simple_chat.py +355 -0
  26. demos/basic/simple_integration_example.py +51 -0
  27. demos/collaboration/collaborative_task_example.py +437 -0
  28. demos/collaboration/group_chat_example.py +130 -0
  29. demos/collaboration/simplified_crewai_example.py +39 -0
  30. demos/langgraph/autonomous_langgraph_network.py +808 -0
  31. demos/langgraph/langgraph_agent_network.py +415 -0
  32. demos/langgraph/langgraph_collaborative_task.py +619 -0
  33. demos/langgraph/langgraph_example.py +227 -0
  34. demos/langgraph/run_langgraph_examples.py +213 -0
  35. demos/network/agent_network_example.py +381 -0
  36. demos/network/email_agent.py +130 -0
  37. demos/network/email_agent_demo.py +46 -0
  38. demos/network/heterogeneous_network_example.py +216 -0
  39. demos/network/multi_framework_example.py +199 -0
  40. demos/utils/check_imports.py +49 -0
  41. demos/workflows/autonomous_agent_workflow.py +248 -0
  42. demos/workflows/mcp_features_demo.py +353 -0
  43. demos/workflows/run_agent_collaboration_demo.py +63 -0
  44. demos/workflows/run_agent_collaboration_with_logs.py +396 -0
  45. demos/workflows/show_agent_interactions.py +107 -0
  46. demos/workflows/simplified_autonomous_demo.py +74 -0
  47. functions/main.py +144 -0
  48. functions/mcp_network_server.py +513 -0
  49. functions/utils.py +47 -0
  50. agent_mcp-0.1.2.dist-info/METADATA +0 -475
  51. agent_mcp-0.1.2.dist-info/RECORD +0 -5
  52. agent_mcp-0.1.2.dist-info/entry_points.txt +0 -2
  53. agent_mcp-0.1.2.dist-info/top_level.txt +0 -1
@@ -0,0 +1,601 @@
1
+ """
2
+ Enhanced MCP Agent with client/server capabilities.
3
+ """
4
+
5
+ import asyncio
6
+ import collections
7
+ from typing import Optional, Dict, Any, List
8
+ from .mcp_agent import MCPAgent
9
+ from .mcp_transport import MCPTransport
10
+
11
+ class EnhancedMCPAgent(MCPAgent):
12
+ """MCPAgent with client/server capabilities"""
13
+
14
+ def __init__(self,
15
+ name: str,
16
+ transport: Optional[MCPTransport] = None,
17
+ server_mode: bool = False,
18
+ client_mode: bool = False,
19
+ **kwargs):
20
+ super().__init__(name=name, **kwargs)
21
+
22
+ self.transport = transport
23
+ self.server_mode = server_mode
24
+ self.client_mode = client_mode
25
+ self.connected_agents = {}
26
+ self.task_queue = asyncio.Queue()
27
+ self.task_results = {}
28
+ self.task_dependencies = {}
29
+ self._pending_tasks = {}
30
+ self._task_processor = None
31
+ self._message_processor = None
32
+
33
+ def start_server(self):
34
+ """Start agent in server mode"""
35
+ if not self.server_mode or not self.transport:
36
+ raise ValueError("Agent not configured for server mode")
37
+
38
+ # Start the transport server
39
+ self.transport.start()
40
+
41
+ async def connect_to_server(self, server_url: str):
42
+ """Connect to another agent's server"""
43
+ if not self.client_mode or not self.transport:
44
+ raise ValueError("Agent not configured for client mode")
45
+
46
+ # Register with the server
47
+ registration = {
48
+ "type": "registration",
49
+ "agent_id": self.mcp_id,
50
+ "name": self.name,
51
+ "capabilities": self.list_available_tools()
52
+ }
53
+
54
+ response = await self.transport.send_message(server_url, registration)
55
+ if response.get("status") == "ok":
56
+ self.connected_agents[server_url] = response.get("server_id")
57
+ print(f"Successfully connected to server at {server_url}")
58
+
59
+ async def handle_incoming_message(self, message: Dict[str, Any]):
60
+ """Handle incoming messages from other agents"""
61
+ # First check if type is directly in the message
62
+ msg_type = message.get("type")
63
+
64
+ # If not, check if it's inside the content field
65
+ if not msg_type and "content" in message and isinstance(message["content"], dict):
66
+ msg_type = message["content"].get("type")
67
+
68
+ print(f"[DEBUG] {self.name}: Received message of type: {msg_type}")
69
+
70
+ if msg_type == "registration":
71
+ # Handle new agent registration
72
+ await self._handle_registration(message)
73
+ elif msg_type == "tool_call":
74
+ # Handle tool execution request
75
+ await self._handle_tool_call(message)
76
+ elif msg_type == "task":
77
+ # Handle new task assignment
78
+ await self._handle_task(message)
79
+ elif msg_type == "task_result":
80
+ # Handle task result
81
+ print(f"[DEBUG] {self.name}: Processing task_result message: {message}")
82
+ await self._handle_task_result(message)
83
+ elif msg_type == "get_result":
84
+ # Handle get result request
85
+ await self._handle_get_result(message)
86
+ else:
87
+ print(f"[WARN] {self.name}: Received unknown message type: {msg_type}")
88
+
89
+ async def _handle_registration(self, message: Dict[str, Any]):
90
+ """Handle agent registration"""
91
+ agent_id = message.get("agent_id")
92
+ agent_name = message.get("name")
93
+ capabilities = message.get("capabilities", [])
94
+
95
+ self.connected_agents[agent_id] = {
96
+ "name": agent_name,
97
+ "capabilities": capabilities
98
+ }
99
+
100
+ print(f"New agent registered: {agent_name} ({agent_id})")
101
+ return {"status": "ok", "server_id": self.mcp_id}
102
+
103
+ async def _handle_tool_call(self, message: Dict[str, Any]):
104
+ """Handle tool execution request"""
105
+ tool_name = message.get("tool")
106
+ arguments = message.get("arguments", {})
107
+
108
+ if tool_name in self.mcp_tools:
109
+ result = await self.execute_tool(tool_name, **arguments)
110
+ return {"status": "ok", "result": result}
111
+ else:
112
+ return {"status": "error", "message": f"Tool {tool_name} not found"}
113
+
114
+ async def _handle_task(self, message: Dict[str, Any]):
115
+ """Handle incoming task"""
116
+ print(f"[DEBUG] {self.name}: Received task message: {message}")
117
+
118
+ # --- Idempotency Check ---
119
+ # Check if we should process this message based on task_id
120
+ if not self._should_process_message(message):
121
+ # If not, acknowledge it immediately if possible and stop
122
+ message_id = message.get("message_id")
123
+ if message_id and self.transport:
124
+ # Run acknowledge in background, don't wait
125
+ asyncio.create_task(self.transport.acknowledge_message(self.name, message_id))
126
+ print(f"[DEBUG] {self.name}: Acknowledged duplicate task {message.get('task_id')} (msg_id: {message_id})")
127
+ return {"status": "skipped", "message": "Task already completed"}
128
+ # --- End Idempotency Check ---
129
+
130
+ task_id = message.get("task_id") or message.get("content", {}).get("task_id")
131
+ depends_on = message.get("depends_on", [])
132
+ message_id = message.get("message_id") # Get the message ID for acknowledgment
133
+
134
+ if not task_id:
135
+ print(f"[ERROR] {self.name}: Received task without task_id: {message}")
136
+ return {"status": "error", "message": "No task_id provided"}
137
+
138
+ # If this task has dependencies, check if they're met
139
+ if depends_on:
140
+ print(f"[DEBUG] {self.name}: Task {task_id} depends on: {depends_on}")
141
+ # Check if all dependencies are in task_results
142
+ for dep_id in depends_on:
143
+ if dep_id not in self.task_results:
144
+ print(f"[DEBUG] {self.name}: Dependency {dep_id} not met for task {task_id}, waiting...")
145
+ return {"status": "waiting", "message": f"Waiting for dependency {dep_id}"}
146
+ print(f"[DEBUG] {self.name}: All dependencies met for task {task_id}")
147
+
148
+ # Store task info if we're the coordinator
149
+ if self.server_mode:
150
+ print(f"[DEBUG] {self.name}: Storing task {task_id} in coordinator")
151
+ self.task_results[task_id] = None
152
+
153
+ print(f"[DEBUG] {self.name}: Queueing task {task_id} for processing")
154
+ await self.task_queue.put(message)
155
+ print(f"[DEBUG] {self.name}: Successfully queued task {task_id}")
156
+
157
+ return {"status": "ok"}
158
+
159
+ async def _handle_task_result(self, message: Dict[str, Any]):
160
+ """Handle task result from an agent"""
161
+ content = message.get("content", {})
162
+ if isinstance(content, dict) and "text" in content:
163
+ # Handle case where content has a text field containing JSON
164
+ try:
165
+ import json
166
+ text_content = json.loads(content["text"])
167
+ task_id = text_content.get("task_id")
168
+ result = text_content.get("result")
169
+ except (json.JSONDecodeError, AttributeError) as e:
170
+ print(f"[ERROR] {self.name}: Failed to parse text content: {e}")
171
+ task_id = content.get("task_id")
172
+ result = content.get("result")
173
+ else:
174
+ # Normal case - extract directly from content or root
175
+ task_id = content.get("task_id") if isinstance(content, dict) else message.get("task_id")
176
+ result = content.get("result") if isinstance(content, dict) else message.get("result")
177
+ original_message_id = message.get('id')
178
+ sender_name = message.get('from', 'Unknown Sender')
179
+
180
+ print(f"[DEBUG] {self.name}: Handling task result for task_id: {task_id}")
181
+
182
+ if not task_id:
183
+ print(f"[ERROR] {self.name}: Received task result without task_id: {message}")
184
+ return
185
+
186
+ print(f"[DEBUG] {self.name}: Received result for task {task_id} from {sender_name} (original_message_id: {original_message_id})")
187
+
188
+ # Store result
189
+ self.task_results[task_id] = result
190
+
191
+ # Check for dependent tasks
192
+ if task_id in self.task_dependencies:
193
+ print(f"[DEBUG] {self.name}: Found dependent tasks for {task_id}")
194
+
195
+ # Get tasks that depend on this one
196
+ task_ids = self.task_dependencies[task_id]
197
+ dependent_tasks = [self._pending_tasks[tid] for tid in task_ids if tid in self._pending_tasks]
198
+
199
+ # Remove this task from dependencies
200
+ del self.task_dependencies[task_id]
201
+
202
+ # Process each dependent task
203
+ for dependent_task in dependent_tasks:
204
+ print(f"[DEBUG] {self.name}: Processing dependent task {dependent_task}")
205
+ # Ensure task_info is a dictionary
206
+ if not isinstance(dependent_task, dict):
207
+ print(f"[WARNING] {self.name}: Skipping invalid task_info (not a dictionary): {dependent_task}")
208
+ continue
209
+
210
+ # Check if all dependencies are met
211
+ dependencies = dependent_task.get("depends_on", [])
212
+ all_deps_met = True
213
+
214
+ for dep in dependencies:
215
+ if dep not in self.task_results:
216
+ all_deps_met = False
217
+ # Re-add to dependencies since not all deps are met
218
+ task_id = dependent_task.get('task_id')
219
+ if not task_id:
220
+ print(f"[WARNING] {self.name}: Skipping task without task_id: {dependent_task}")
221
+ continue
222
+ # Add task_id to dependencies and store full task info
223
+ if dep not in self.task_dependencies:
224
+ self.task_dependencies[dep] = set()
225
+ self.task_dependencies[dep].add(task_id)
226
+ self._pending_tasks[task_id] = dependent_task
227
+ break
228
+
229
+ if all_deps_met:
230
+ task_id = dependent_task.get('task_id')
231
+ if task_id in self._pending_tasks:
232
+ full_task = self._pending_tasks[task_id]
233
+ print(f"[DEBUG] {self.name}: All dependencies met for task {task_id}")
234
+ # Forward task to agent
235
+ # Ensure task has proper structure
236
+ task_to_assign = {
237
+ "task_id": task_id,
238
+ "description": full_task.get("description"),
239
+ "type": "task",
240
+ "depends_on": dependencies,
241
+ "content": full_task.get("content", {})
242
+ }
243
+ await self.assign_task(full_task["agent"], task_to_assign)
244
+ # Clean up
245
+ del self._pending_tasks[task_id]
246
+ for dep in dependencies:
247
+ if dep not in self.task_results:
248
+ # Extract fields from content if present
249
+ content = dependent_task.get("content", {})
250
+ task_id = content.get("task_id") or dependent_task.get("task_id")
251
+ description = content.get("description") or dependent_task.get("description")
252
+ depends_on = content.get("depends_on") or dependent_task.get("depends_on", [])
253
+ agent = dependent_task.get("agent")
254
+
255
+ if not task_id or not agent or not description:
256
+ print(f"[ERROR] {self.name}: Missing required fields in dependent task: {dependent_task}")
257
+ continue
258
+
259
+ # Maintain consistent message structure
260
+ validated_task_info = {
261
+ "type": "task",
262
+ "content": {
263
+ "task_id": task_id,
264
+ "description": description,
265
+ "depends_on": depends_on,
266
+ "type": "task"
267
+ },
268
+ "agent": agent
269
+ }
270
+ task_id = validated_task_info.get('task_id')
271
+ if task_id:
272
+ if dep not in self.task_dependencies:
273
+ self.task_dependencies[dep] = set()
274
+ self.task_dependencies[dep].add(task_id)
275
+ # Store full task info
276
+ self._pending_tasks[task_id] = validated_task_info
277
+
278
+ # Acknowledge the task result if we have the original message ID
279
+ if original_message_id and self.transport:
280
+ try:
281
+ await self.transport.acknowledge_message(self.name, original_message_id)
282
+ print(f"[DEBUG] {self.name}: Acknowledged task result for {task_id} with message_id {original_message_id}")
283
+ except Exception as e:
284
+ print(f"[ERROR] {self.name}: Error acknowledging task result: {e}")
285
+ traceback.print_exc()
286
+
287
+ return {"status": "ok"}
288
+
289
+ async def _handle_get_result(self, message: Dict[str, Any]):
290
+ """Handle get result request"""
291
+ task_id = message.get("task_id")
292
+ if task_id in self.task_results:
293
+ return {"status": "ok", "result": self.task_results[task_id]}
294
+ else:
295
+ return {"status": "error", "message": f"Result for task {task_id} not found"}
296
+
297
+ async def assign_task(self, target_url: str, task: Dict[str, Any]):
298
+ """Assign a task to another agent"""
299
+ print(f"{self.name}: Assigning task {task} to {target_url}")
300
+
301
+ # Extract task details from either content or root level
302
+ task_id = task.get("task_id")
303
+ description = task.get("description")
304
+ depends_on = task.get("depends_on", [])
305
+
306
+ # If task details are in content, use those instead
307
+ if "content" in task and isinstance(task["content"], dict):
308
+ content = task["content"]
309
+ task_id = content.get("task_id", task_id)
310
+ description = content.get("description", description)
311
+ depends_on = content.get("depends_on", depends_on)
312
+
313
+ message = {
314
+ "type": "task",
315
+ "content": {
316
+ "task_id": task_id,
317
+ "description": description,
318
+ "depends_on": depends_on,
319
+ "type": "task"
320
+ },
321
+ "from": self.mcp_id
322
+ }
323
+
324
+ # Only include reply_to if it exists in the task
325
+ if "reply_to" in task:
326
+ message["content"]["reply_to"] = task["reply_to"]
327
+
328
+ return await self.transport.send_message(target_url, message)
329
+
330
+ async def process_messages(self):
331
+ """Process incoming messages from transport"""
332
+ print(f"{self.name}: Starting message processor...")
333
+ while True:
334
+ try:
335
+ # Get message with timeout (transport now handles this)
336
+ message, message_id = await self.transport.receive_message()
337
+
338
+ # Handle timeout case
339
+ if message is None:
340
+ await asyncio.sleep(0.1) # Prevent tight loop
341
+ continue
342
+
343
+ # Skip invalid messages
344
+ if not isinstance(message, dict):
345
+ print(f"{self.name}: Skipping invalid message format: {message}")
346
+ if message_id: # Still acknowledge to avoid retries
347
+ await self.transport.acknowledge_message(self.name, message_id)
348
+ continue
349
+
350
+ print(f"{self.name}: Processing message ID: {message_id}, Type: {message.get('type', 'unknown')}")
351
+
352
+ # Add message_id for tracking
353
+ message['message_id'] = message_id
354
+
355
+ try:
356
+ # Process the message
357
+ await self.handle_incoming_message(message)
358
+
359
+ # Only acknowledge after successful processing
360
+ if message_id:
361
+ await self.transport.acknowledge_message(self.name, message_id)
362
+ print(f"{self.name}: Acknowledged message {message_id}")
363
+ except Exception as e:
364
+ print(f"{self.name}: Error handling message {message_id}: {e}")
365
+ import traceback
366
+ traceback.print_exc()
367
+ # Don't acknowledge on error so it can be retried
368
+
369
+ except asyncio.CancelledError:
370
+ print(f"{self.name}: Message processor cancelled")
371
+ break
372
+ except Exception as e:
373
+ print(f"{self.name}: Error in message processor: {e}")
374
+ import traceback
375
+ traceback.print_exc()
376
+ await asyncio.sleep(1) # Brief pause on unexpected error
377
+
378
+ async def process_tasks(self):
379
+ """Process tasks from the queue"""
380
+ print(f"{self.name}: Starting task processor...")
381
+ while True:
382
+ try:
383
+ task = await self.task_queue.get()
384
+ print(f"{self.name}: Processing task: {task}")
385
+
386
+ # Get task description and task_id
387
+ task_desc = task.get("description") or task.get("content", {}).get("description") if isinstance(task.get("content"), dict) else task.get("description", "")
388
+ task_id = task.get("task_id") or task.get("content", {}).get("task_id") if isinstance(task.get("content"), dict) else task.get("task_id")
389
+ message_id = task.get('message_id')
390
+
391
+ if not task_desc or not task_id:
392
+ print(f"{self.name}: Error: Task is missing description or task_id")
393
+ continue
394
+
395
+ # Check if this task has dependencies
396
+ depends_on = task.get("depends_on", [])
397
+ if depends_on:
398
+ print(f"{self.name}: Task {task_id} depends on: {depends_on}")
399
+ # Check if we have all dependencies
400
+ missing_deps = []
401
+ for dep_id in depends_on:
402
+ if dep_id not in self.task_results:
403
+ # Try to get dependency result from coordinator
404
+ if self.transport and self.transport.remote_url:
405
+ try:
406
+ result = await self.transport.send_message(
407
+ f"{self.transport.remote_url}/message/{self.name}",
408
+ {
409
+ "type": "get_result",
410
+ "task_id": dep_id,
411
+ "result": "",
412
+ "sender": self.name,
413
+ "original_message_id": message_id,
414
+ }
415
+ )
416
+ if result and result.get("result"):
417
+ self.task_results[dep_id] = result["result"]
418
+ continue
419
+ except Exception as e:
420
+ print(f"{self.name}: Error getting dependency result: {e}")
421
+
422
+ missing_deps.append(dep_id)
423
+
424
+ if missing_deps:
425
+ print(f"{self.name}: Task {task_id} is missing dependencies: {missing_deps}. Putting back in queue.")
426
+ # Put task back in queue and wait
427
+ await self.task_queue.put(task)
428
+ await asyncio.sleep(1)
429
+ continue
430
+
431
+ # Add dependency results as context
432
+ task_context = "\nBased on the following findings:\n"
433
+ for dep_id in depends_on:
434
+ task_context += f"\nFrom {dep_id}:\n{self.task_results[dep_id]}"
435
+ else:
436
+ task_context = ""
437
+
438
+ # Generate response using LLM if configured
439
+ if hasattr(self, 'llm_config') and self.llm_config:
440
+ print(f"{self.name}: Generating response for task {task_id}...")
441
+ try:
442
+ response = self.generate_reply(
443
+ messages=[{
444
+ "role": "user",
445
+ "content": f"Please help with this task: {task_desc}{task_context}"
446
+ }]
447
+ )
448
+ print(f"{self.name}: Generated response for task {task_id}: {response}")
449
+
450
+ # Store result locally
451
+ self.task_results[task_id] = response
452
+
453
+ # --- Mark task completed for idempotency ---
454
+ self._mark_task_completed(task_id)
455
+ # --- End mark task completed ---
456
+
457
+ # Send result back if there's a reply_to
458
+ if "reply_to" in task and self.transport:
459
+ try:
460
+ await self.transport.send_message(
461
+ task["reply_to"],
462
+ {
463
+ "type": "task_result",
464
+ "task_id": task_id,
465
+ "result": response,
466
+ "sender": self.name,
467
+ "original_message_id": message_id
468
+ }
469
+ )
470
+ print(f"{self.name}: Result sent successfully")
471
+
472
+ # Try to acknowledge task completion
473
+ if "message_id" in task:
474
+ await self.transport.acknowledge_message(self.name, task["message_id"])
475
+ print(f"{self.name}: Task {task_id} acknowledged with message_id {task['message_id']}")
476
+ except Exception as send_error:
477
+ print(f"{self.name}: Error sending result: {send_error}")
478
+ traceback.print_exc()
479
+ except Exception as e:
480
+ print(f"{self.name}: Error generating response: {e}")
481
+ traceback.print_exc()
482
+ response = f"Error generating response: {e}"
483
+
484
+ # Send result back if there's a reply_to
485
+ if "reply_to" in task and self.transport:
486
+ reply_url = task["reply_to"]
487
+ print(f"{self.name}: Sending result back to {reply_url}")
488
+ try:
489
+ result = await self.transport.send_message(
490
+ reply_url,
491
+ {
492
+ "type": "task_result",
493
+ "task_id": task_id,
494
+ "result": response,
495
+ "sender": self.name,
496
+ "original_message_id": message_id
497
+ }
498
+ )
499
+ print(f"{self.name}: Result sent successfully: {result}")
500
+
501
+ # Store result locally too
502
+ self.task_results[task_id] = response
503
+
504
+ # Try to acknowledge task completion
505
+ try:
506
+ if "message_id" in task:
507
+ await self.transport.acknowledge_message(self.name, task["message_id"])
508
+ print(f"{self.name}: Acknowledged completion of task {task_id} with message_id {task['message_id']}")
509
+ else:
510
+ print(f"{self.name}: No message_id found for task {task_id}, cannot acknowledge")
511
+ except Exception as e:
512
+ print(f"{self.name}: Error acknowledging task completion: {e}")
513
+ traceback.print_exc()
514
+ except Exception as e:
515
+ print(f"{self.name}: Error sending result: {e}")
516
+ traceback.print_exc()
517
+ # Store result locally if no reply_to or transport info is available
518
+ print(f"{self.name}: No reply_to or transport info, storing result locally for task {task_id}")
519
+ self.task_results[task_id] = response
520
+
521
+ self.task_queue.task_done()
522
+
523
+ except Exception as e:
524
+ print(f"{self.name}: Error processing task: {str(e)}")
525
+
526
+ async def run(self):
527
+ """Run the agent's main loop"""
528
+ if not self.transport:
529
+ raise ValueError("Transport not configured for agent")
530
+
531
+ # For remote connections, we need to connect() instead of start()
532
+ if hasattr(self.transport, 'is_remote') and self.transport.is_remote:
533
+ # Connect to remote server
534
+ await self.transport.connect(agent_name=self.name)
535
+ # Brief pause to ensure connection is ready
536
+ await asyncio.sleep(1)
537
+ else:
538
+ # For local server, just start it
539
+ self.transport.start()
540
+
541
+ # Start message and task processing in new tasks
542
+ self._message_processor = asyncio.create_task(
543
+ self.process_messages(),
544
+ name=f"messages_{self.name}"
545
+ )
546
+ self._task_processor = asyncio.create_task(
547
+ self.process_tasks(),
548
+ name=f"tasks_{self.name}"
549
+ )
550
+
551
+ # Brief pause to let tasks start
552
+ await asyncio.sleep(0.1)
553
+
554
+ # Create a task to monitor the processors
555
+ monitor_task = asyncio.create_task(
556
+ self._monitor_processors(),
557
+ name=f"monitor_{self.name}"
558
+ )
559
+
560
+ # Return the monitor task so it can be awaited
561
+ return monitor_task
562
+
563
+ async def _monitor_processors(self):
564
+ """Monitor the message and task processors"""
565
+ try:
566
+ # Wait for both processors to complete or error
567
+ await asyncio.gather(
568
+ self._message_processor,
569
+ self._task_processor
570
+ )
571
+ except asyncio.CancelledError:
572
+ # Handle cancellation gracefully
573
+ await self.shutdown()
574
+ raise
575
+ except Exception as e:
576
+ print(f"{self.name}: Error in processors: {e}")
577
+ await self.shutdown()
578
+ raise
579
+
580
+ async def shutdown(self):
581
+ """Shutdown the agent's tasks and disconnect transport"""
582
+ if hasattr(self, '_message_processor'):
583
+ self._message_processor.cancel()
584
+ try:
585
+ await self._message_processor
586
+ except asyncio.CancelledError:
587
+ pass
588
+
589
+ if hasattr(self, '_task_processor'):
590
+ self._task_processor.cancel()
591
+ try:
592
+ await self._task_processor
593
+ except asyncio.CancelledError:
594
+ pass
595
+
596
+ # Disconnect transport if remote
597
+ if hasattr(self.transport, 'is_remote') and self.transport.is_remote:
598
+ try:
599
+ await self.transport.disconnect()
600
+ except Exception as e:
601
+ print(f"{self.name}: Error during disconnect: {e}")