machinaos 0.0.10 → 0.0.13

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 (72) hide show
  1. package/.env.template +16 -0
  2. package/client/package.json +1 -1
  3. package/client/src/Dashboard.tsx +3 -3
  4. package/client/src/components/AIAgentNode.tsx +24 -12
  5. package/client/src/components/OutputPanel.tsx +3 -2
  6. package/client/src/components/parameterPanel/InputSection.tsx +16 -3
  7. package/client/src/nodeDefinitions/aiAgentNodes.ts +12 -0
  8. package/client/src/nodeDefinitions/specializedAgentNodes.ts +68 -320
  9. package/client/src/nodeDefinitions/toolNodes.ts +87 -1
  10. package/client/src/nodeDefinitions/workflowNodes.ts +55 -1
  11. package/package.json +12 -3
  12. package/scripts/daemon.js +427 -0
  13. package/scripts/start.js +7 -1
  14. package/scripts/sync-version.js +108 -0
  15. package/server/Dockerfile +6 -7
  16. package/server/constants.py +2 -0
  17. package/server/core/cleanup.py +123 -0
  18. package/server/core/config.py +16 -0
  19. package/server/core/database.py +92 -1
  20. package/server/core/health.py +121 -0
  21. package/server/examples/__init__.py +1 -0
  22. package/server/gunicorn.conf.py +46 -0
  23. package/server/main.py +38 -3
  24. package/server/models/database.py +1 -0
  25. package/server/models/nodes.py +18 -2
  26. package/server/requirements-docker.txt +86 -0
  27. package/server/routers/database.py +16 -0
  28. package/server/routers/websocket.py +6 -5
  29. package/server/services/ai.py +115 -14
  30. package/server/services/auth.py +6 -1
  31. package/server/services/deployment/manager.py +14 -0
  32. package/server/services/event_waiter.py +55 -0
  33. package/server/services/example_loader.py +60 -0
  34. package/server/services/execution/executor.py +2 -0
  35. package/server/services/execution/models.py +8 -0
  36. package/server/services/handlers/__init__.py +2 -0
  37. package/server/services/handlers/ai.py +164 -11
  38. package/server/services/handlers/document.py +13 -4
  39. package/server/services/handlers/tools.py +445 -14
  40. package/server/services/node_executor.py +3 -0
  41. package/server/services/temporal/activities.py +3 -0
  42. package/server/services/workflow.py +2 -0
  43. package/server/skills/android_agent/app-launcher-skill/SKILL.md +137 -0
  44. package/server/skills/android_agent/app-list-skill/SKILL.md +148 -0
  45. package/server/skills/android_agent/audio-skill/SKILL.md +169 -0
  46. package/server/skills/android_agent/battery-skill/SKILL.md +114 -0
  47. package/server/skills/android_agent/bluetooth-skill/SKILL.md +151 -0
  48. package/server/skills/android_agent/camera-skill/SKILL.md +148 -0
  49. package/server/skills/android_agent/environmental-skill/SKILL.md +140 -0
  50. package/server/skills/android_agent/location-skill/SKILL.md +163 -0
  51. package/server/skills/android_agent/motion-skill/SKILL.md +141 -0
  52. package/server/skills/android_agent/screen-control-skill/SKILL.md +164 -0
  53. package/server/skills/android_agent/wifi-skill/SKILL.md +182 -0
  54. package/server/skills/assistant/subagent-skill/SKILL.md +205 -0
  55. package/server/skills/coding_agent/javascript-skill/SKILL.md +196 -0
  56. package/server/skills/coding_agent/python-skill/SKILL.md +165 -0
  57. package/server/skills/social_agent/whatsapp-db-skill/SKILL.md +284 -0
  58. package/server/skills/social_agent/whatsapp-send-skill/SKILL.md +180 -0
  59. package/server/skills/task_agent/cron-scheduler-skill/SKILL.md +215 -0
  60. package/server/skills/task_agent/task-manager-skill/SKILL.md +251 -0
  61. package/server/skills/task_agent/timer-skill/SKILL.md +168 -0
  62. package/server/skills/travel_agent/geocoding-skill/SKILL.md +186 -0
  63. package/server/skills/travel_agent/nearby-places-skill/SKILL.md +234 -0
  64. package/server/skills/web_agent/http-request-skill/SKILL.md +211 -0
  65. package/server/skills/android/skill/SKILL.md +0 -84
  66. package/server/skills/assistant/code-skill/SKILL.md +0 -176
  67. package/server/skills/assistant/http-skill/SKILL.md +0 -163
  68. package/server/skills/assistant/maps-skill/SKILL.md +0 -172
  69. package/server/skills/assistant/scheduler-skill/SKILL.md +0 -86
  70. package/server/skills/assistant/whatsapp-skill/SKILL.md +0 -285
  71. /package/server/skills/{android → android_agent}/personality/SKILL.md +0 -0
  72. /package/server/skills/{assistant → web_agent}/web-search-skill/SKILL.md +0 -0
@@ -9,7 +9,8 @@ import math
9
9
  import json
10
10
  import asyncio
11
11
  import uuid
12
- from typing import Dict, Any, Optional, TYPE_CHECKING
12
+ import hashlib
13
+ from typing import Dict, Any, Optional, List, Tuple, TYPE_CHECKING
13
14
 
14
15
  from core.logging import get_logger
15
16
  from constants import ANDROID_SERVICE_NODE_TYPES
@@ -23,6 +24,13 @@ logger = get_logger(__name__)
23
24
  # Track running delegated tasks for status checking
24
25
  _delegated_tasks: Dict[str, asyncio.Task] = {}
25
26
 
27
+ # In-memory cache of delegation results (fast path, survives task cleanup)
28
+ # Follows Celery AsyncResult / Ray ObjectRef pattern
29
+ _delegation_results: Dict[str, Dict[str, Any]] = {}
30
+
31
+ # Track active delegations to prevent duplicate calls: (parent_node_id, child_node_id, task_hash) -> task_id
32
+ _active_delegations: Dict[Tuple[str, str, str], str] = {}
33
+
26
34
 
27
35
  async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
28
36
  config: Dict[str, Any]) -> Dict[str, Any]:
@@ -115,6 +123,15 @@ async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
115
123
  if node_type == 'gmaps_nearby_places':
116
124
  return await _execute_nearby_places(tool_args, config.get('parameters', {}))
117
125
 
126
+ # Task Manager (dual-purpose: AI tool + workflow node)
127
+ if node_type == 'taskManager':
128
+ return await _execute_task_manager(tool_args, config)
129
+
130
+ # Built-in: Check delegated task results
131
+ # Auto-injected when parent has delegation tools
132
+ if node_type == '_builtin_check_delegated_tasks':
133
+ return await _execute_check_delegated_tasks(tool_args, config)
134
+
118
135
  # AI Agent delegation (fire-and-forget async delegation)
119
136
  # Includes specialized agents: android_agent, coding_agent, web_agent, task_agent, social_agent
120
137
  if node_type in ('aiAgent', 'chatAgent', 'android_agent', 'coding_agent', 'web_agent', 'task_agent', 'social_agent', 'travel_agent', 'tool_agent', 'productivity_agent', 'payments_agent', 'consumer_agent'):
@@ -1158,9 +1175,34 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1158
1175
  "hint": "Ensure nodes/edges are passed to tool config"
1159
1176
  }
1160
1177
 
1178
+ # Get parent node ID for duplicate tracking
1179
+ parent_node_id = config.get('parent_node_id', '')
1180
+
1181
+ # Generate hash of task to detect duplicate delegation attempts
1182
+ task_hash = hashlib.md5(f"{task_description}:{task_context}".encode()).hexdigest()[:16]
1183
+ delegation_key = (parent_node_id, node_id, task_hash)
1184
+
1185
+ # Check for duplicate delegation (prevents LLM from calling same delegation twice)
1186
+ existing_task_id = _active_delegations.get(delegation_key)
1187
+ if existing_task_id:
1188
+ logger.warning(f"[Delegated Agent] Duplicate delegation detected: task_hash={task_hash}, existing_task_id={existing_task_id}")
1189
+ return {
1190
+ "success": True,
1191
+ "status": "ALREADY_DELEGATED",
1192
+ "task_id": existing_task_id,
1193
+ "agent_name": config.get('parameters', {}).get('label', node_type),
1194
+ "result": (
1195
+ f"This task was ALREADY delegated (task_id: {existing_task_id}). "
1196
+ f"Do NOT call this tool again. Use 'check_delegated_tasks' to check status."
1197
+ ),
1198
+ }
1199
+
1161
1200
  # Generate unique task ID
1162
1201
  task_id = f"delegated_{node_id}_{uuid.uuid4().hex[:8]}"
1163
1202
 
1203
+ # Register this delegation to prevent duplicates
1204
+ _active_delegations[delegation_key] = task_id
1205
+
1164
1206
  # Get child agent parameters from database
1165
1207
  child_params = await database.get_node_parameters(node_id) or {}
1166
1208
 
@@ -1233,8 +1275,69 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1233
1275
 
1234
1276
  logger.info(f"[Delegated Agent] Task {task_id} completed: success={result.get('success')}")
1235
1277
 
1278
+ # Check if child agent actually succeeded
1279
+ if not result.get('success'):
1280
+ # Child agent returned failure - treat as error
1281
+ error_msg = result.get('error', 'Child agent returned failure')
1282
+ logger.warning(f"[Delegated Agent] Task {task_id} returned success=False: {error_msg}")
1283
+
1284
+ await broadcaster.update_node_status(
1285
+ node_id,
1286
+ "error",
1287
+ {
1288
+ "phase": "delegated_error",
1289
+ "task_id": task_id,
1290
+ "error": error_msg
1291
+ },
1292
+ workflow_id=workflow_id
1293
+ )
1294
+
1295
+ # Cache error for parent retrieval
1296
+ _delegation_results[task_id] = {
1297
+ "task_id": task_id,
1298
+ "status": "error",
1299
+ "agent_name": agent_label,
1300
+ "agent_node_id": node_id,
1301
+ "result": None,
1302
+ "error": error_msg,
1303
+ }
1304
+
1305
+ # Persist error to DB
1306
+ if database:
1307
+ await database.save_node_output(
1308
+ node_id=node_id,
1309
+ session_id=f"delegation_{task_id}",
1310
+ output_name="delegation_result",
1311
+ data={
1312
+ "task_id": task_id,
1313
+ "parent_node_id": config.get('parent_node_id', ''),
1314
+ "agent_name": agent_label,
1315
+ "status": "error",
1316
+ "error": error_msg,
1317
+ }
1318
+ )
1319
+
1320
+ # Dispatch error event for trigger nodes
1321
+ await broadcaster.send_custom_event('task_completed', {
1322
+ 'task_id': task_id,
1323
+ 'status': 'error',
1324
+ 'agent_name': agent_label,
1325
+ 'agent_node_id': node_id,
1326
+ 'parent_node_id': config.get('parent_node_id', ''),
1327
+ 'error': error_msg,
1328
+ 'workflow_id': workflow_id,
1329
+ })
1330
+
1331
+ return result
1332
+
1333
+ # Success case - extract response properly
1334
+ response_text = result.get('result', {}).get('response', '')
1335
+ if not response_text:
1336
+ # Fallback: try to stringify the result dict
1337
+ response_text = str(result.get('result', '')) if result.get('result') else 'No response generated'
1338
+
1236
1339
  # Broadcast completion
1237
- response_preview = str(result.get('result', {}).get('response', ''))[:200]
1340
+ response_preview = response_text[:200] if response_text else ''
1238
1341
  await broadcaster.update_node_status(
1239
1342
  node_id,
1240
1343
  "success",
@@ -1246,6 +1349,42 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1246
1349
  workflow_id=workflow_id
1247
1350
  )
1248
1351
 
1352
+ # Cache result for parent retrieval (Layer 2: in-memory)
1353
+ _delegation_results[task_id] = {
1354
+ "task_id": task_id,
1355
+ "status": "completed",
1356
+ "agent_name": agent_label,
1357
+ "agent_node_id": node_id,
1358
+ "result": response_text,
1359
+ "error": None,
1360
+ }
1361
+
1362
+ # Persist to DB (Layer 3: cross-restart via existing NodeOutput)
1363
+ if database:
1364
+ await database.save_node_output(
1365
+ node_id=node_id,
1366
+ session_id=f"delegation_{task_id}",
1367
+ output_name="delegation_result",
1368
+ data={
1369
+ "task_id": task_id,
1370
+ "parent_node_id": config.get('parent_node_id', ''),
1371
+ "agent_name": agent_label,
1372
+ "status": "completed",
1373
+ "result": response_text,
1374
+ }
1375
+ )
1376
+
1377
+ # Dispatch task_completed event for trigger nodes
1378
+ await broadcaster.send_custom_event('task_completed', {
1379
+ 'task_id': task_id,
1380
+ 'status': 'completed',
1381
+ 'agent_name': agent_label,
1382
+ 'agent_node_id': node_id,
1383
+ 'parent_node_id': config.get('parent_node_id', ''),
1384
+ 'result': response_text,
1385
+ 'workflow_id': workflow_id,
1386
+ })
1387
+
1249
1388
  return result
1250
1389
 
1251
1390
  except Exception as e:
@@ -1260,11 +1399,50 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1260
1399
  },
1261
1400
  workflow_id=workflow_id
1262
1401
  )
1402
+
1403
+ # Cache error for parent retrieval (Layer 2: in-memory)
1404
+ _delegation_results[task_id] = {
1405
+ "task_id": task_id,
1406
+ "status": "error",
1407
+ "agent_name": agent_label,
1408
+ "agent_node_id": node_id,
1409
+ "result": None,
1410
+ "error": str(e),
1411
+ }
1412
+
1413
+ # Persist to DB (Layer 3: cross-restart)
1414
+ if database:
1415
+ await database.save_node_output(
1416
+ node_id=node_id,
1417
+ session_id=f"delegation_{task_id}",
1418
+ output_name="delegation_result",
1419
+ data={
1420
+ "task_id": task_id,
1421
+ "parent_node_id": config.get('parent_node_id', ''),
1422
+ "agent_name": agent_label,
1423
+ "status": "error",
1424
+ "error": str(e),
1425
+ }
1426
+ )
1427
+
1428
+ # Dispatch task_completed event for trigger nodes (error case)
1429
+ await broadcaster.send_custom_event('task_completed', {
1430
+ 'task_id': task_id,
1431
+ 'status': 'error',
1432
+ 'agent_name': agent_label,
1433
+ 'agent_node_id': node_id,
1434
+ 'parent_node_id': config.get('parent_node_id', ''),
1435
+ 'error': str(e),
1436
+ 'workflow_id': workflow_id,
1437
+ })
1438
+
1263
1439
  return {"success": False, "error": str(e)}
1264
1440
 
1265
1441
  finally:
1266
1442
  # Cleanup task reference
1267
1443
  _delegated_tasks.pop(task_id, None)
1444
+ # Cleanup delegation tracking (allows re-delegation after completion)
1445
+ _active_delegations.pop(delegation_key, None)
1268
1446
 
1269
1447
  # Spawn as background task (fire-and-forget)
1270
1448
  task = asyncio.create_task(run_child_agent())
@@ -1277,24 +1455,277 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1277
1455
  "task_id": task_id,
1278
1456
  "agent_node_id": node_id,
1279
1457
  "agent_name": agent_label,
1280
- "message": f"Task delegated to '{agent_label}'. Agent is now working independently on: {task_description[:100]}..."
1458
+ "message": (
1459
+ f"SUCCESS: Task delegated to '{agent_label}' (task_id: {task_id}). "
1460
+ f"Agent is now working INDEPENDENTLY in the background. "
1461
+ f"IMPORTANT: Delegation is COMPLETE. Do NOT call this tool again for this task. "
1462
+ f"To check results later, use 'check_delegated_tasks' with task_id='{task_id}'."
1463
+ ),
1281
1464
  }
1282
1465
 
1283
1466
 
1284
- def get_delegated_task_status(task_id: str) -> Dict[str, Any]:
1285
- """Check status of a delegated task (optional utility).
1467
+ async def get_delegated_task_status(task_ids: Optional[List[str]] = None,
1468
+ database=None) -> Dict[str, Any]:
1469
+ """Check status and retrieve results of delegated tasks.
1470
+
1471
+ 3-layer lookup: live tasks -> memory cache -> DB (NodeOutput).
1472
+ Follows Celery AsyncResult / Ray ObjectRef pattern.
1286
1473
 
1287
1474
  Args:
1288
- task_id: The task_id returned from delegation
1475
+ task_ids: Optional list of specific task IDs to check. If None, returns all known tasks.
1476
+ database: Database instance for Layer 3 (SQLite) lookup.
1289
1477
 
1290
1478
  Returns:
1291
- Status dict with task state
1479
+ Dict with 'tasks' list containing status and results for each task.
1292
1480
  """
1293
- task = _delegated_tasks.get(task_id)
1481
+ if not task_ids:
1482
+ # Return all known from memory
1483
+ task_ids = list(set(
1484
+ list(_delegated_tasks.keys()) + list(_delegation_results.keys())
1485
+ ))
1486
+
1487
+ tasks = []
1488
+ db_lookup_ids = []
1489
+
1490
+ for tid in task_ids:
1491
+ # Layer 1: Live asyncio.Task (still running or just finished)
1492
+ live_task = _delegated_tasks.get(tid)
1493
+ if live_task is not None:
1494
+ if not live_task.done():
1495
+ tasks.append({"task_id": tid, "status": "running"})
1496
+ else:
1497
+ # Task finished -- extract result via task.result()
1498
+ try:
1499
+ result = live_task.result()
1500
+ response = result.get('result', {}).get('response', str(result.get('result', '')))
1501
+ tasks.append({"task_id": tid, "status": "completed", "result": response})
1502
+ except Exception as e:
1503
+ tasks.append({"task_id": tid, "status": "error", "error": str(e)})
1504
+ continue
1505
+
1506
+ # Layer 2: In-memory result cache
1507
+ cached = _delegation_results.get(tid)
1508
+ if cached:
1509
+ tasks.append(cached)
1510
+ continue
1511
+
1512
+ # Layer 3: Need DB lookup
1513
+ db_lookup_ids.append(tid)
1514
+
1515
+ # DB fallback for results not in memory (cross-restart)
1516
+ if db_lookup_ids and database:
1517
+ for tid in list(db_lookup_ids):
1518
+ db_result = await database.get_node_output_by_session(
1519
+ session_id=f"delegation_{tid}",
1520
+ output_name="delegation_result"
1521
+ )
1522
+ if db_result:
1523
+ data = db_result.get('data', {})
1524
+ result_data = data.get("result", {})
1525
+ response_text = result_data.get("response", str(result_data)) if isinstance(result_data, dict) else str(result_data)
1526
+ tasks.append({
1527
+ "task_id": tid,
1528
+ "status": data.get("status", "completed"),
1529
+ "agent_name": data.get("agent_name", ""),
1530
+ "result": response_text,
1531
+ "error": data.get("error"),
1532
+ })
1533
+ db_lookup_ids.remove(tid)
1534
+
1535
+ # Remaining IDs not found anywhere
1536
+ for tid in db_lookup_ids:
1537
+ tasks.append({"task_id": tid, "status": "not_found"})
1538
+
1539
+ return {"tasks": tasks}
1540
+
1541
+
1542
+ async def _execute_check_delegated_tasks(args: Dict[str, Any],
1543
+ config: Dict[str, Any]) -> Dict[str, Any]:
1544
+ """LLM-callable tool: check on delegated child agents.
1545
+
1546
+ Returns status and results for previously delegated tasks.
1547
+ Follows Celery AsyncResult / Ray ObjectRef patterns.
1548
+ """
1549
+ task_ids = args.get('task_ids')
1550
+ database = config.get('database')
1551
+ result = await get_delegated_task_status(task_ids=task_ids, database=database)
1552
+
1553
+ formatted = []
1554
+ for task in result.get("tasks", []):
1555
+ entry = {
1556
+ "task_id": task.get("task_id"),
1557
+ "status": task.get("status"),
1558
+ "agent_name": task.get("agent_name"),
1559
+ }
1560
+ if task.get("status") == "completed":
1561
+ text = str(task.get("result", ""))
1562
+ entry["result"] = text[:4000] + "... [truncated]" if len(text) > 4000 else text
1563
+ elif task.get("status") == "error":
1564
+ entry["error"] = task.get("error")
1565
+ elif task.get("status") == "running":
1566
+ entry["message"] = f"Agent '{task.get('agent_name', 'unknown')}' is still working"
1567
+ formatted.append(entry)
1294
1568
 
1295
- if task is None:
1296
- return {"status": "not_found_or_completed", "task_id": task_id}
1297
- elif task.done():
1298
- return {"status": "completed", "task_id": task_id}
1299
- else:
1300
- return {"status": "running", "task_id": task_id}
1569
+ return {
1570
+ "total_tasks": len(formatted),
1571
+ "completed": sum(1 for t in formatted if t.get("status") == "completed"),
1572
+ "running": sum(1 for t in formatted if t.get("status") == "running"),
1573
+ "errors": sum(1 for t in formatted if t.get("status") == "error"),
1574
+ "tasks": formatted,
1575
+ }
1576
+
1577
+
1578
+ async def _execute_task_manager(
1579
+ tool_args: Dict[str, Any],
1580
+ config: Dict[str, Any]
1581
+ ) -> Dict[str, Any]:
1582
+ """Execute task manager operations.
1583
+
1584
+ Dual-purpose: works as AI tool (LLM fills args) or workflow node (uses params).
1585
+
1586
+ Operations:
1587
+ - list_tasks: List all active and completed delegated tasks
1588
+ - get_task: Get status details for a specific task
1589
+ - mark_done: Remove a task from active tracking
1590
+
1591
+ Args:
1592
+ tool_args: Arguments from LLM tool call (operation, task_id, status_filter)
1593
+ config: Tool configuration with node parameters
1594
+
1595
+ Returns:
1596
+ Dict with operation results
1597
+ """
1598
+ # Merge tool_args with node parameters (tool_args takes precedence)
1599
+ params = config.get('parameters', {})
1600
+ operation = tool_args.get('operation') or params.get('operation', 'list_tasks')
1601
+ task_id = tool_args.get('task_id') or params.get('task_id')
1602
+ status_filter = tool_args.get('status_filter') or params.get('status_filter')
1603
+ database = config.get('database')
1604
+
1605
+ logger.debug(f"[TaskManager] Operation: {operation}, task_id: {task_id}, filter: {status_filter}")
1606
+
1607
+ if operation == 'list_tasks':
1608
+ # Collect all tasks from _delegated_tasks and _delegation_results
1609
+ tasks = []
1610
+
1611
+ # Active tasks from asyncio.Task tracking
1612
+ for tid, task in _delegated_tasks.items():
1613
+ if task.done():
1614
+ try:
1615
+ if task.cancelled():
1616
+ status = 'cancelled'
1617
+ elif task.exception():
1618
+ status = 'error'
1619
+ else:
1620
+ status = 'completed'
1621
+ except Exception:
1622
+ status = 'completed'
1623
+ else:
1624
+ status = 'running'
1625
+
1626
+ tasks.append({
1627
+ 'task_id': tid,
1628
+ 'status': status,
1629
+ 'active': True
1630
+ })
1631
+
1632
+ # Completed tasks from in-memory cache
1633
+ for tid, result in _delegation_results.items():
1634
+ if tid not in [t['task_id'] for t in tasks]:
1635
+ tasks.append({
1636
+ 'task_id': tid,
1637
+ 'status': result.get('status', 'completed'),
1638
+ 'agent_name': result.get('agent_name'),
1639
+ 'result_summary': str(result.get('result', ''))[:200],
1640
+ 'active': False
1641
+ })
1642
+
1643
+ # Apply status filter
1644
+ if status_filter:
1645
+ tasks = [t for t in tasks if t.get('status') == status_filter]
1646
+
1647
+ return {
1648
+ 'success': True,
1649
+ 'operation': 'list_tasks',
1650
+ 'tasks': tasks,
1651
+ 'count': len(tasks),
1652
+ 'running': sum(1 for t in tasks if t.get('status') == 'running'),
1653
+ 'completed': sum(1 for t in tasks if t.get('status') == 'completed'),
1654
+ 'errors': sum(1 for t in tasks if t.get('status') == 'error')
1655
+ }
1656
+
1657
+ elif operation == 'get_task':
1658
+ if not task_id:
1659
+ return {'success': False, 'error': 'task_id is required for get_task operation'}
1660
+
1661
+ # Use existing get_delegated_task_status for detailed lookup
1662
+ result = await get_delegated_task_status(task_ids=[task_id], database=database)
1663
+ tasks = result.get('tasks', [])
1664
+
1665
+ if not tasks:
1666
+ return {
1667
+ 'success': False,
1668
+ 'error': f'Task {task_id} not found',
1669
+ 'task_id': task_id
1670
+ }
1671
+
1672
+ task_info = tasks[0]
1673
+ return {
1674
+ 'success': True,
1675
+ 'operation': 'get_task',
1676
+ 'task_id': task_id,
1677
+ 'status': task_info.get('status'),
1678
+ 'agent_name': task_info.get('agent_name'),
1679
+ 'result': task_info.get('result'),
1680
+ 'error': task_info.get('error')
1681
+ }
1682
+
1683
+ elif operation == 'mark_done':
1684
+ if not task_id:
1685
+ return {'success': False, 'error': 'task_id is required for mark_done operation'}
1686
+
1687
+ # Remove from active tracking
1688
+ removed = False
1689
+ if task_id in _delegated_tasks:
1690
+ del _delegated_tasks[task_id]
1691
+ removed = True
1692
+ if task_id in _delegation_results:
1693
+ del _delegation_results[task_id]
1694
+ removed = True
1695
+
1696
+ return {
1697
+ 'success': True,
1698
+ 'operation': 'mark_done',
1699
+ 'task_id': task_id,
1700
+ 'removed': removed,
1701
+ 'message': f'Task {task_id} marked as done and removed from tracking' if removed else f'Task {task_id} was not in active tracking'
1702
+ }
1703
+
1704
+ return {'success': False, 'error': f'Unknown operation: {operation}'}
1705
+
1706
+
1707
+ async def handle_task_manager(
1708
+ node_id: str,
1709
+ node_type: str,
1710
+ parameters: Dict[str, Any],
1711
+ context: Dict[str, Any]
1712
+ ) -> Dict[str, Any]:
1713
+ """Handle taskManager as workflow node.
1714
+
1715
+ Wrapper for _execute_task_manager that conforms to the standard
1716
+ workflow node handler signature.
1717
+
1718
+ Args:
1719
+ node_id: The node ID
1720
+ node_type: Should be 'taskManager'
1721
+ parameters: Node parameters (operation, task_id, status_filter)
1722
+ context: Execution context
1723
+
1724
+ Returns:
1725
+ Task manager operation results
1726
+ """
1727
+ config = {
1728
+ 'parameters': parameters,
1729
+ 'database': context.get('database')
1730
+ }
1731
+ return await _execute_task_manager({}, config)
@@ -35,6 +35,7 @@ from services.handlers import (
35
35
  handle_social_receive, handle_social_send,
36
36
  handle_http_scraper, handle_file_downloader, handle_document_parser,
37
37
  handle_text_chunker, handle_embedding_generator, handle_vector_store,
38
+ handle_task_manager,
38
39
  )
39
40
 
40
41
  if TYPE_CHECKING:
@@ -145,6 +146,8 @@ class NodeExecutor:
145
146
  'textChunker': handle_text_chunker,
146
147
  'embeddingGenerator': handle_embedding_generator,
147
148
  'vectorStore': handle_vector_store,
149
+ # Task management
150
+ 'taskManager': handle_task_manager,
148
151
  # Note: 'console' handled in _dispatch with connected_outputs
149
152
  }
150
153
 
@@ -208,6 +208,9 @@ class NodeExecutionActivities:
208
208
  "edges": context.get("edges", []),
209
209
  "session_id": context.get("session_id", "default"),
210
210
  "workflow_id": context.get("workflow_id"),
211
+ # CRITICAL: Pass upstream node outputs for downstream nodes to access
212
+ # This enables taskTrigger -> chatAgent data flow via input-task handle
213
+ "outputs": context.get("inputs", {}),
211
214
  }
212
215
 
213
216
  print(f"[Activity] WebSocket execute for {node_id}")
@@ -142,6 +142,7 @@ class WorkflowService:
142
142
  session_id: str = "default",
143
143
  execution_id: str = None,
144
144
  workflow_id: str = None,
145
+ outputs: Dict[str, Any] = None,
145
146
  ) -> Dict[str, Any]:
146
147
  """Execute a single workflow node."""
147
148
  context = {
@@ -151,6 +152,7 @@ class WorkflowService:
151
152
  "execution_id": execution_id,
152
153
  "workflow_id": workflow_id, # For per-workflow status scoping (n8n pattern)
153
154
  "get_output_fn": self.get_node_output,
155
+ "outputs": outputs or {}, # Upstream node outputs for data flow (e.g., taskTrigger -> chatAgent)
154
156
  }
155
157
  return await self._node_executor.execute(
156
158
  node_id=node_id,