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.
- package/client/package.json +1 -1
- package/client/src/components/OutputPanel.tsx +3 -2
- package/client/src/components/parameterPanel/InputSection.tsx +4 -3
- package/package.json +1 -1
- package/server/routers/websocket.py +5 -5
- package/server/services/ai.py +38 -13
- package/server/services/deployment/manager.py +14 -0
- package/server/services/execution/executor.py +2 -0
- package/server/services/execution/models.py +8 -0
- package/server/services/handlers/ai.py +71 -23
- package/server/services/handlers/tools.py +103 -6
- package/server/skills/android_agent/app-launcher-skill/SKILL.md +137 -0
- package/server/skills/android_agent/app-list-skill/SKILL.md +148 -0
- package/server/skills/android_agent/audio-skill/SKILL.md +169 -0
- package/server/skills/android_agent/battery-skill/SKILL.md +114 -0
- package/server/skills/android_agent/bluetooth-skill/SKILL.md +151 -0
- package/server/skills/android_agent/camera-skill/SKILL.md +148 -0
- package/server/skills/android_agent/environmental-skill/SKILL.md +140 -0
- package/server/skills/android_agent/location-skill/SKILL.md +163 -0
- package/server/skills/android_agent/motion-skill/SKILL.md +141 -0
- package/server/skills/android_agent/screen-control-skill/SKILL.md +164 -0
- package/server/skills/android_agent/wifi-skill/SKILL.md +182 -0
- package/server/skills/assistant/subagent-skill/SKILL.md +62 -30
- package/server/skills/coding_agent/javascript-skill/SKILL.md +196 -0
- package/server/skills/coding_agent/python-skill/SKILL.md +165 -0
- package/server/skills/social_agent/whatsapp-db-skill/SKILL.md +284 -0
- package/server/skills/social_agent/whatsapp-send-skill/SKILL.md +180 -0
- package/server/skills/task_agent/cron-scheduler-skill/SKILL.md +215 -0
- package/server/skills/task_agent/task-manager-skill/SKILL.md +251 -0
- package/server/skills/task_agent/timer-skill/SKILL.md +168 -0
- package/server/skills/travel_agent/geocoding-skill/SKILL.md +186 -0
- package/server/skills/travel_agent/nearby-places-skill/SKILL.md +234 -0
- package/server/skills/web_agent/http-request-skill/SKILL.md +211 -0
- package/server/skills/android/skill/SKILL.md +0 -84
- package/server/skills/assistant/code-skill/SKILL.md +0 -176
- package/server/skills/assistant/http-skill/SKILL.md +0 -163
- package/server/skills/assistant/maps-skill/SKILL.md +0 -172
- package/server/skills/assistant/scheduler-skill/SKILL.md +0 -86
- package/server/skills/assistant/whatsapp-skill/SKILL.md +0 -285
- /package/server/skills/{android → android_agent}/personality/SKILL.md +0 -0
- /package/server/skills/{assistant → web_agent}/web-search-skill/SKILL.md +0 -0
package/client/package.json
CHANGED
|
@@ -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
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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.
|
|
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.
|
|
641
|
+
logger.debug(f"[Deploy] Tool edges found: {len(tool_edges)}")
|
|
642
642
|
for te in tool_edges:
|
|
643
|
-
logger.
|
|
643
|
+
logger.debug(f"[Deploy] Tool edge: source={te.get('source')} -> target={te.get('target')}")
|
|
644
644
|
else:
|
|
645
|
-
logger.
|
|
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"}
|
package/server/services/ai.py
CHANGED
|
@@ -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': '
|
|
2147
|
-
'chatAgent': '
|
|
2148
|
-
'android_agent': '
|
|
2149
|
-
'coding_agent': '
|
|
2150
|
-
'web_agent': '
|
|
2151
|
-
'task_agent': '
|
|
2152
|
-
'social_agent': '
|
|
2153
|
-
'travel_agent': '
|
|
2154
|
-
'tool_agent': '
|
|
2155
|
-
'productivity_agent': '
|
|
2156
|
-
'payments_agent': '
|
|
2157
|
-
'consumer_agent': '
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
source_output =
|
|
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
|
|
324
|
-
#
|
|
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
|
-
#
|
|
329
|
-
tool_data = [
|
|
330
|
-
|
|
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
|
|
406
|
-
#
|
|
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
|
-
#
|
|
411
|
-
tool_data = [
|
|
412
|
-
|
|
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
|
-
|
|
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 =
|
|
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":
|
|
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":
|
|
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':
|
|
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":
|
|
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
|
|