machinaos 0.0.12 → 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 (41) hide show
  1. package/client/package.json +1 -1
  2. package/client/src/components/OutputPanel.tsx +3 -2
  3. package/client/src/components/parameterPanel/InputSection.tsx +4 -3
  4. package/package.json +1 -1
  5. package/server/routers/websocket.py +5 -5
  6. package/server/services/ai.py +38 -13
  7. package/server/services/deployment/manager.py +14 -0
  8. package/server/services/execution/executor.py +2 -0
  9. package/server/services/execution/models.py +8 -0
  10. package/server/services/handlers/ai.py +71 -23
  11. package/server/services/handlers/tools.py +103 -6
  12. package/server/skills/android_agent/app-launcher-skill/SKILL.md +137 -0
  13. package/server/skills/android_agent/app-list-skill/SKILL.md +148 -0
  14. package/server/skills/android_agent/audio-skill/SKILL.md +169 -0
  15. package/server/skills/android_agent/battery-skill/SKILL.md +114 -0
  16. package/server/skills/android_agent/bluetooth-skill/SKILL.md +151 -0
  17. package/server/skills/android_agent/camera-skill/SKILL.md +148 -0
  18. package/server/skills/android_agent/environmental-skill/SKILL.md +140 -0
  19. package/server/skills/android_agent/location-skill/SKILL.md +163 -0
  20. package/server/skills/android_agent/motion-skill/SKILL.md +141 -0
  21. package/server/skills/android_agent/screen-control-skill/SKILL.md +164 -0
  22. package/server/skills/android_agent/wifi-skill/SKILL.md +182 -0
  23. package/server/skills/assistant/subagent-skill/SKILL.md +62 -30
  24. package/server/skills/coding_agent/javascript-skill/SKILL.md +196 -0
  25. package/server/skills/coding_agent/python-skill/SKILL.md +165 -0
  26. package/server/skills/social_agent/whatsapp-db-skill/SKILL.md +284 -0
  27. package/server/skills/social_agent/whatsapp-send-skill/SKILL.md +180 -0
  28. package/server/skills/task_agent/cron-scheduler-skill/SKILL.md +215 -0
  29. package/server/skills/task_agent/task-manager-skill/SKILL.md +251 -0
  30. package/server/skills/task_agent/timer-skill/SKILL.md +168 -0
  31. package/server/skills/travel_agent/geocoding-skill/SKILL.md +186 -0
  32. package/server/skills/travel_agent/nearby-places-skill/SKILL.md +234 -0
  33. package/server/skills/web_agent/http-request-skill/SKILL.md +211 -0
  34. package/server/skills/android/skill/SKILL.md +0 -84
  35. package/server/skills/assistant/code-skill/SKILL.md +0 -176
  36. package/server/skills/assistant/http-skill/SKILL.md +0 -163
  37. package/server/skills/assistant/maps-skill/SKILL.md +0 -172
  38. package/server/skills/assistant/scheduler-skill/SKILL.md +0 -86
  39. package/server/skills/assistant/whatsapp-skill/SKILL.md +0 -285
  40. /package/server/skills/{android → android_agent}/personality/SKILL.md +0 -0
  41. /package/server/skills/{assistant → web_agent}/web-search-skill/SKILL.md +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-flow-client",
3
3
  "private": true,
4
- "version": "0.0.12",
4
+ "version": "0.0.13",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "vite --host 0.0.0.0",
@@ -177,9 +177,10 @@ const OutputPanel: React.FC<OutputPanelProps> = ({ nodeId }) => {
177
177
  // Helper to check if a handle is a config/auxiliary handle (not main data flow)
178
178
  const isConfigHandle = (handle: string | null | undefined): boolean => {
179
179
  if (!handle) return false;
180
- // Config handles follow pattern: input-<type> where type is not 'main'
180
+ // Config handles follow pattern: input-<type> where type is not 'main', 'chat', or 'task'
181
181
  // Examples: input-memory, input-tools, input-model
182
- if (handle.startsWith('input-') && handle !== 'input-main') {
182
+ // Non-config (data flow) handles: input-main, input-chat, input-task
183
+ if (handle.startsWith('input-') && handle !== 'input-main' && handle !== 'input-chat' && handle !== 'input-task') {
183
184
  return true;
184
185
  }
185
186
  return false;
@@ -60,10 +60,11 @@ const InputSection: React.FC<InputSectionProps> = ({ nodeId, visible = true }) =
60
60
  // Helper to check if a handle is a config/auxiliary handle (not main data flow)
61
61
  const isConfigHandle = (handle: string | null | undefined): boolean => {
62
62
  if (!handle) return false;
63
- // Config handles follow pattern: input-<type> where type is not 'main' or 'chat'
63
+ // Config handles follow pattern: input-<type> where type is not 'main', 'chat', or 'task'
64
64
  // Examples: input-memory, input-tools, input-model, input-skill
65
- // Non-config (primary data) handles: input-main, input-chat
66
- if (handle.startsWith('input-') && handle !== 'input-main' && handle !== 'input-chat') {
65
+ // Non-config (primary data) handles: input-main, input-chat, input-task
66
+ // Note: input-task is for taskTrigger node output which should be visible as draggable variables
67
+ if (handle.startsWith('input-') && handle !== 'input-main' && handle !== 'input-chat' && handle !== 'input-task') {
67
68
  return true;
68
69
  }
69
70
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machinaos",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Open source workflow automation platform with AI agents, React Flow, and n8n-inspired architecture",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -629,20 +629,20 @@ async def handle_deploy_workflow(data: Dict[str, Any], websocket: WebSocket) ->
629
629
  session_id = data.get("session_id", "default")
630
630
 
631
631
  # DEBUG: Log received edges to trace tool connection issues
632
- logger.info(f"[Deploy] Received {len(edges)} edges for workflow {workflow_id}")
632
+ logger.debug(f"[Deploy] Received {len(edges)} edges for workflow {workflow_id}")
633
633
  for e in edges:
634
634
  target_handle = e.get('targetHandle')
635
635
  if target_handle and target_handle.startswith('input-') and target_handle != 'input-main':
636
- logger.info(f"[Deploy] Config edge: {e.get('source')} -> {e.get('target')} (handle={target_handle})")
636
+ logger.debug(f"[Deploy] Config edge: {e.get('source')} -> {e.get('target')} (handle={target_handle})")
637
637
 
638
638
  # Check for tool connections to AI Agent
639
639
  tool_edges = [e for e in edges if e.get('targetHandle') == 'input-tools']
640
640
  if tool_edges:
641
- logger.info(f"[Deploy] Tool edges found: {len(tool_edges)}")
641
+ logger.debug(f"[Deploy] Tool edges found: {len(tool_edges)}")
642
642
  for te in tool_edges:
643
- logger.info(f"[Deploy] Tool edge: source={te.get('source')} -> target={te.get('target')}")
643
+ logger.debug(f"[Deploy] Tool edge: source={te.get('source')} -> target={te.get('target')}")
644
644
  else:
645
- logger.info(f"[Deploy] No input-tools edges found")
645
+ logger.debug(f"[Deploy] No input-tools edges found")
646
646
 
647
647
  if not nodes:
648
648
  return {"success": False, "error": "No nodes provided"}
@@ -2092,6 +2092,7 @@ class AIService:
2092
2092
  'whatsappDb': 'whatsapp_db',
2093
2093
  'gmaps_locations': 'geocode',
2094
2094
  'gmaps_nearby_places': 'nearby_places',
2095
+ 'taskManager': 'task_manager',
2095
2096
  'timer': 'timer',
2096
2097
  'cronScheduler': 'cron_scheduler',
2097
2098
  # Built-in check tool for delegation results
@@ -2138,23 +2139,24 @@ class AIService:
2138
2139
  'whatsappDb': 'Query WhatsApp database - list contacts, search groups, get contact/group info, retrieve chat history.',
2139
2140
  'gmaps_locations': 'Geocode addresses to coordinates or reverse geocode coordinates to addresses using Google Maps.',
2140
2141
  'gmaps_nearby_places': 'Search for nearby places (restaurants, hospitals, banks, etc.) using Google Maps Places API.',
2142
+ 'taskManager': 'Track delegated sub-agent tasks. Operations: list_tasks (see all tasks), get_task (check specific task status/result), mark_done (cleanup completed tasks).',
2141
2143
  'timer': 'Wait/sleep for a specified duration. Specify duration (1-3600) and unit (seconds, minutes, or hours). Returns timestamp and elapsed time after waiting.',
2142
2144
  'cronScheduler': 'Schedule a delayed or recurring execution. Supports seconds, minutes, hours, daily, weekly, monthly frequencies with timezone. Use frequency to set schedule type, then set the relevant interval/time parameters.',
2143
2145
  # Built-in check tool for delegation results
2144
2146
  '_builtin_check_delegated_tasks': 'Check status and retrieve results of previously delegated tasks.',
2145
- # Agent delegation tools
2146
- 'aiAgent': 'Delegate a task to another AI Agent. Returns task_id. Use check_delegated_tasks for results.',
2147
- 'chatAgent': 'Delegate a task to a Chat Agent. Returns task_id. Use check_delegated_tasks for results.',
2148
- 'android_agent': 'Delegate to Android Control Agent. Returns task_id. Use check_delegated_tasks for results.',
2149
- 'coding_agent': 'Delegate to Coding Agent. Returns task_id. Use check_delegated_tasks for results.',
2150
- 'web_agent': 'Delegate to Web Control Agent. Returns task_id. Use check_delegated_tasks for results.',
2151
- 'task_agent': 'Delegate to Task Management Agent. Returns task_id. Use check_delegated_tasks for results.',
2152
- 'social_agent': 'Delegate to Social Media Agent. Returns task_id. Use check_delegated_tasks for results.',
2153
- 'travel_agent': 'Delegate to Travel Agent. Returns task_id. Use check_delegated_tasks for results.',
2154
- 'tool_agent': 'Delegate to Tool Agent. Returns task_id. Use check_delegated_tasks for results.',
2155
- 'productivity_agent': 'Delegate to Productivity Agent. Returns task_id. Use check_delegated_tasks for results.',
2156
- 'payments_agent': 'Delegate to Payments Agent. Returns task_id. Use check_delegated_tasks for results.',
2157
- 'consumer_agent': 'Delegate to Consumer Agent. Returns task_id. Use check_delegated_tasks for results.',
2147
+ # Agent delegation tools - ONE-SHOT fire-and-forget pattern
2148
+ 'aiAgent': 'ONE-SHOT delegation to AI Agent. Call ONCE per task, returns task_id immediately. Agent works in background - do NOT re-call.',
2149
+ 'chatAgent': 'ONE-SHOT delegation to Chat Agent. Call ONCE per task, returns task_id immediately. Agent works in background - do NOT re-call.',
2150
+ 'android_agent': 'ONE-SHOT delegation to Android Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2151
+ 'coding_agent': 'ONE-SHOT delegation to Coding Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2152
+ 'web_agent': 'ONE-SHOT delegation to Web Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2153
+ 'task_agent': 'ONE-SHOT delegation to Task Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2154
+ 'social_agent': 'ONE-SHOT delegation to Social Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2155
+ 'travel_agent': 'ONE-SHOT delegation to Travel Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2156
+ 'tool_agent': 'ONE-SHOT delegation to Tool Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2157
+ 'productivity_agent': 'ONE-SHOT delegation to Productivity Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2158
+ 'payments_agent': 'ONE-SHOT delegation to Payments Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2159
+ 'consumer_agent': 'ONE-SHOT delegation to Consumer Agent. Call ONCE per task, returns task_id. Agent works in background - do NOT re-call.',
2158
2160
  # Android service nodes (direct tool usage)
2159
2161
  'batteryMonitor': 'Monitor Android battery status, level, charging state, temperature, and health.',
2160
2162
  'networkMonitor': 'Monitor Android network connectivity, type, and internet availability.',
@@ -2210,6 +2212,29 @@ class AIService:
2210
2212
  service_names = [s.get('label') or s.get('service_id', 'unknown') for s in connected_services]
2211
2213
  tool_description = f"{tool_description} Connected: {', '.join(service_names)}"
2212
2214
 
2215
+ # For AI Agent nodes, enhance description with child agent's tool capabilities
2216
+ # This allows parent agent to know what the child agent can do
2217
+ from constants import AI_AGENT_TYPES
2218
+ if node_type in AI_AGENT_TYPES:
2219
+ child_tools = tool_info.get('child_tools', [])
2220
+ if child_tools:
2221
+ # Build capability description from child's connected tools
2222
+ capability_descriptions = []
2223
+ for child_tool in child_tools:
2224
+ child_type = child_tool.get('node_type', '')
2225
+ child_label = child_tool.get('label', child_type)
2226
+ # Get the tool description from DEFAULT_TOOL_DESCRIPTIONS
2227
+ child_desc = DEFAULT_TOOL_DESCRIPTIONS.get(child_type, f"Use {child_label}")
2228
+ capability_descriptions.append(f"- {child_label}: {child_desc}")
2229
+
2230
+ capabilities_text = "\n".join(capability_descriptions)
2231
+ tool_description = (
2232
+ f"Delegate tasks to '{node_label}' agent. "
2233
+ f"This agent has the following capabilities:\n{capabilities_text}\n"
2234
+ f"Call ONCE per task, returns task_id. Agent works in background."
2235
+ )
2236
+ logger.info(f"[LangGraph] Enhanced tool description for {node_type} with {len(child_tools)} child tools")
2237
+
2213
2238
  # Clean tool name (LangChain requires alphanumeric + underscores)
2214
2239
  import re
2215
2240
  tool_name = re.sub(r'[^a-zA-Z0-9_]', '_', tool_name)
@@ -629,6 +629,20 @@ class DeploymentManager:
629
629
  downstream_ids.add(source)
630
630
  logger.debug(f"[Deployment] Including sub-node {source} connected to toolkit {target}")
631
631
 
632
+ # Include tool nodes connected to AI Agent nodes (for capability discovery)
633
+ # When a child agent is included, we need its connected tools so the parent
634
+ # can discover what capabilities the child has
635
+ from constants import AI_AGENT_TYPES
636
+ agent_node_ids = {n['id'] for n in nodes if n.get('type') in AI_AGENT_TYPES and n['id'] in downstream_ids}
637
+ for edge in edges:
638
+ target = edge.get('target')
639
+ source = edge.get('source')
640
+ target_handle = edge.get('targetHandle', '')
641
+ # Include tool nodes connected to agent's input-tools handle
642
+ if target in agent_node_ids and target_handle == 'input-tools' and source not in downstream_ids:
643
+ downstream_ids.add(source)
644
+ logger.debug(f"[Deployment] Including tool node {source} connected to agent {target}")
645
+
632
646
  return [n for n in nodes if n['id'] in downstream_ids]
633
647
 
634
648
  # =========================================================================
@@ -804,6 +804,7 @@ class WorkflowExecutor:
804
804
 
805
805
  # Build execution context for node handler
806
806
  # workflow_id is included for per-workflow status scoping (n8n pattern)
807
+ logger.info(f"[Executor] Building context for {node.node_id}, ctx.outputs keys: {list(ctx.outputs.keys())}")
807
808
  exec_context = {
808
809
  "nodes": ctx.nodes,
809
810
  "edges": ctx.edges,
@@ -813,6 +814,7 @@ class WorkflowExecutor:
813
814
  "start_time": node.started_at,
814
815
  "outputs": ctx.outputs, # Previous node outputs
815
816
  }
817
+ logger.info(f"[Executor] exec_context['outputs'] keys: {list(exec_context['outputs'].keys())}")
816
818
 
817
819
  # Call the actual node executor
818
820
  result = await self.node_executor(
@@ -13,6 +13,10 @@ from datetime import datetime
13
13
  from enum import Enum
14
14
  from typing import Dict, Any, List, Optional
15
15
 
16
+ from core.logging import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
16
20
 
17
21
  class TaskStatus(str, Enum):
18
22
  """Task execution states (Conductor-style lifecycle).
@@ -361,6 +365,8 @@ class ExecutionContext:
361
365
  if node.get("_pre_executed"):
362
366
  # Mark as COMPLETED with trigger output
363
367
  trigger_output = node.get("_trigger_output", {})
368
+ logger.info(f"[ExecutionContext] Pre-executed node found: {node_id} (type={node_type})")
369
+ logger.info(f"[ExecutionContext] Trigger output keys: {list(trigger_output.keys()) if trigger_output else 'empty'}")
364
370
  node_exec = NodeExecution(
365
371
  node_id=node_id,
366
372
  node_type=node_type,
@@ -369,6 +375,7 @@ class ExecutionContext:
369
375
  completed_at=time.time(),
370
376
  )
371
377
  ctx.outputs[node_id] = trigger_output
378
+ logger.info(f"[ExecutionContext] Set ctx.outputs[{node_id}] = trigger_output")
372
379
  ctx.checkpoints.append(node_id)
373
380
  else:
374
381
  node_exec = NodeExecution(
@@ -378,6 +385,7 @@ class ExecutionContext:
378
385
 
379
386
  ctx.node_executions[node_id] = node_exec
380
387
 
388
+ logger.info(f"[ExecutionContext] Created context with outputs: {list(ctx.outputs.keys())}")
381
389
  return ctx
382
390
 
383
391
  def get_node_status(self, node_id: str) -> Optional[TaskStatus]:
@@ -203,6 +203,45 @@ async def _collect_agent_connections(
203
203
  tool_entry['connected_services'] = connected_services
204
204
  logger.debug(f"{log_prefix} Android toolkit has {len(connected_services)} connected services")
205
205
 
206
+ # Special handling for AI Agent nodes - discover their connected tools
207
+ # This allows parent agent to know child agent's capabilities
208
+ if tool_type in AI_AGENT_TYPES:
209
+ child_tools = []
210
+
211
+ # Count edges targeting this child agent
212
+ child_incoming_edges = [e for e in edges if e.get('target') == source_node_id]
213
+ child_tool_edges = [e for e in child_incoming_edges if e.get('targetHandle') == 'input-tools']
214
+ logger.debug(f"{log_prefix} Child agent {source_node_id}: {len(child_incoming_edges)} incoming edges, {len(child_tool_edges)} input-tools edges")
215
+
216
+ # Log all incoming edge handles for debugging
217
+ if child_incoming_edges:
218
+ handles = [e.get('targetHandle', 'None') for e in child_incoming_edges]
219
+ logger.debug(f"{log_prefix} Child agent {source_node_id} incoming handles: {handles}")
220
+
221
+ # Scan edges for tools connected to this child agent's input-tools handle
222
+ for child_edge in edges:
223
+ if child_edge.get('target') != source_node_id:
224
+ continue
225
+ if child_edge.get('targetHandle') != 'input-tools':
226
+ continue
227
+
228
+ child_tool_id = child_edge.get('source')
229
+ child_tool_node = next((n for n in nodes if n.get('id') == child_tool_id), None)
230
+
231
+ logger.debug(f"{log_prefix} Child agent {source_node_id}: tool edge from {child_tool_id}, node found: {child_tool_node is not None}")
232
+
233
+ if child_tool_node:
234
+ child_tool_type = child_tool_node.get('type', '')
235
+ child_tool_label = child_tool_node.get('data', {}).get('label', child_tool_type)
236
+ child_tools.append({
237
+ 'node_type': child_tool_type,
238
+ 'label': child_tool_label
239
+ })
240
+
241
+ if child_tools:
242
+ tool_entry['child_tools'] = child_tools
243
+ logger.debug(f"{log_prefix} Child agent {source_node_id} has tools: {[t['label'] for t in child_tools]}")
244
+
206
245
  tool_data.append(tool_entry)
207
246
  logger.debug(f"{log_prefix} Connected tool: {tool_type}")
208
247
 
@@ -218,9 +257,24 @@ async def _collect_agent_connections(
218
257
  # Used to receive results from delegated child agents
219
258
  elif target_handle == 'input-task':
220
259
  logger.info(f"{log_prefix} Found input-task edge from {source_node_id} (type={source_node.get('type')})")
221
- all_outputs = context.get('outputs', {})
222
- logger.info(f"{log_prefix} Available outputs in context: {list(all_outputs.keys())}")
223
- source_output = all_outputs.get(source_node_id)
260
+
261
+ # Try context.outputs first (parallel executor), then database via get_output_fn
262
+ source_output = context.get('outputs', {}).get(source_node_id)
263
+ logger.info(f"{log_prefix} Context outputs check for {source_node_id}: {source_output is not None}")
264
+
265
+ if not source_output:
266
+ # Database is source of truth - use get_output_fn to retrieve stored output
267
+ get_output_fn = context.get('get_output_fn')
268
+ session_id = context.get('session_id', 'default')
269
+ if get_output_fn:
270
+ try:
271
+ source_output = await get_output_fn(session_id, source_node_id, 'output_0')
272
+ logger.info(f"{log_prefix} DB lookup for {source_node_id}: {source_output is not None}")
273
+ except Exception as e:
274
+ logger.warning(f"{log_prefix} Failed to get output from DB: {e}")
275
+ else:
276
+ logger.warning(f"{log_prefix} No get_output_fn in context, cannot retrieve task output")
277
+
224
278
  logger.info(f"{log_prefix} Source output for {source_node_id}: {source_output is not None}, type={type(source_output).__name__ if source_output else 'None'}")
225
279
  if source_output:
226
280
  # Handle nested result structure - taskTrigger may return {"result": {...}} or flat dict
@@ -320,19 +374,16 @@ async def handle_ai_agent(
320
374
  parameters = {**parameters, 'prompt': f"{task_context}\n\n{original_prompt}"}
321
375
  logger.info(f"[AI Agent] Task context injected for task_id={task_data.get('task_id')}")
322
376
 
323
- # CRITICAL FIX: Strip delegation tools when handling task completion
324
- # This prevents LLM confusion - tools bound but told not to use them
377
+ # CRITICAL FIX: Strip ALL tools when handling task completion
378
+ # When reporting a delegated task result, the agent should NOT use any tools.
379
+ # Binding tools while instructing "do not use tools" confuses Gemini (returns empty []).
380
+ # The agent's only job is to report the result naturally.
325
381
  task_status = task_data.get('status', '')
326
382
  if task_status in ('completed', 'error') and tool_data:
327
383
  original_tool_count = len(tool_data)
328
- # Filter out AI agent tools (delegation tools)
329
- tool_data = [
330
- t for t in tool_data
331
- if t.get('node_type') not in AI_AGENT_TYPES
332
- ]
333
- filtered_count = original_tool_count - len(tool_data)
334
- if filtered_count > 0:
335
- logger.info(f"[AI Agent] Stripped {filtered_count} delegation tools for task completion handling")
384
+ # Strip ALL tools - agent is just reporting result, not executing anything
385
+ tool_data = []
386
+ logger.info(f"[AI Agent] Stripped ALL {original_tool_count} tools for task completion handling")
336
387
 
337
388
  # Auto-use input data if prompt is empty (fallback for trigger nodes)
338
389
  if not parameters.get('prompt') and input_data:
@@ -402,19 +453,16 @@ async def handle_chat_agent(
402
453
  parameters = {**parameters, 'prompt': f"{task_context}\n\n{original_prompt}"}
403
454
  logger.info(f"[Chat Agent] Task context injected for task_id={task_data.get('task_id')}")
404
455
 
405
- # CRITICAL FIX: Strip delegation tools when handling task completion
406
- # This prevents LLM confusion - tools bound but told not to use them
456
+ # CRITICAL FIX: Strip ALL tools when handling task completion
457
+ # When reporting a delegated task result, the agent should NOT use any tools.
458
+ # Binding tools while instructing "do not use tools" confuses Gemini (returns empty []).
459
+ # The agent's only job is to report the result naturally.
407
460
  task_status = task_data.get('status', '')
408
461
  if task_status in ('completed', 'error') and tool_data:
409
462
  original_tool_count = len(tool_data)
410
- # Filter out AI agent tools (delegation tools)
411
- tool_data = [
412
- t for t in tool_data
413
- if t.get('node_type') not in AI_AGENT_TYPES
414
- ]
415
- filtered_count = original_tool_count - len(tool_data)
416
- if filtered_count > 0:
417
- logger.info(f"[Chat Agent] Stripped {filtered_count} delegation tools for task completion handling")
463
+ # Strip ALL tools - agent is just reporting result, not executing anything
464
+ tool_data = []
465
+ logger.info(f"[Chat Agent] Stripped ALL {original_tool_count} tools for task completion handling")
418
466
 
419
467
  # Auto-use input data if prompt is empty (fallback for trigger nodes)
420
468
  if not parameters.get('prompt') and input_data:
@@ -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, List, 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
@@ -27,6 +28,9 @@ _delegated_tasks: Dict[str, asyncio.Task] = {}
27
28
  # Follows Celery AsyncResult / Ray ObjectRef pattern
28
29
  _delegation_results: Dict[str, Dict[str, Any]] = {}
29
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
+
30
34
 
31
35
  async def execute_tool(tool_name: str, tool_args: Dict[str, Any],
32
36
  config: Dict[str, Any]) -> Dict[str, Any]:
@@ -1171,9 +1175,34 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1171
1175
  "hint": "Ensure nodes/edges are passed to tool config"
1172
1176
  }
1173
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
+
1174
1200
  # Generate unique task ID
1175
1201
  task_id = f"delegated_{node_id}_{uuid.uuid4().hex[:8]}"
1176
1202
 
1203
+ # Register this delegation to prevent duplicates
1204
+ _active_delegations[delegation_key] = task_id
1205
+
1177
1206
  # Get child agent parameters from database
1178
1207
  child_params = await database.get_node_parameters(node_id) or {}
1179
1208
 
@@ -1246,8 +1275,69 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1246
1275
 
1247
1276
  logger.info(f"[Delegated Agent] Task {task_id} completed: success={result.get('success')}")
1248
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
+
1249
1339
  # Broadcast completion
1250
- response_preview = str(result.get('result', {}).get('response', ''))[:200]
1340
+ response_preview = response_text[:200] if response_text else ''
1251
1341
  await broadcaster.update_node_status(
1252
1342
  node_id,
1253
1343
  "success",
@@ -1265,7 +1355,7 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1265
1355
  "status": "completed",
1266
1356
  "agent_name": agent_label,
1267
1357
  "agent_node_id": node_id,
1268
- "result": result.get('result', {}).get('response', str(result.get('result', ''))),
1358
+ "result": response_text,
1269
1359
  "error": None,
1270
1360
  }
1271
1361
 
@@ -1280,7 +1370,7 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1280
1370
  "parent_node_id": config.get('parent_node_id', ''),
1281
1371
  "agent_name": agent_label,
1282
1372
  "status": "completed",
1283
- "result": result.get('result', {}),
1373
+ "result": response_text,
1284
1374
  }
1285
1375
  )
1286
1376
 
@@ -1291,7 +1381,7 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1291
1381
  'agent_name': agent_label,
1292
1382
  'agent_node_id': node_id,
1293
1383
  'parent_node_id': config.get('parent_node_id', ''),
1294
- 'result': result.get('result', {}).get('response', str(result.get('result', ''))),
1384
+ 'result': response_text,
1295
1385
  'workflow_id': workflow_id,
1296
1386
  })
1297
1387
 
@@ -1351,6 +1441,8 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1351
1441
  finally:
1352
1442
  # Cleanup task reference
1353
1443
  _delegated_tasks.pop(task_id, None)
1444
+ # Cleanup delegation tracking (allows re-delegation after completion)
1445
+ _active_delegations.pop(delegation_key, None)
1354
1446
 
1355
1447
  # Spawn as background task (fire-and-forget)
1356
1448
  task = asyncio.create_task(run_child_agent())
@@ -1363,7 +1455,12 @@ async def _execute_delegated_agent(args: Dict[str, Any],
1363
1455
  "task_id": task_id,
1364
1456
  "agent_node_id": node_id,
1365
1457
  "agent_name": agent_label,
1366
- "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
+ ),
1367
1464
  }
1368
1465
 
1369
1466