[PR #29199] fix: fix workflow as tool log missing #32327

Closed
opened 2026-02-21 20:51:11 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/langgenius/dify/pull/29199

State: closed
Merged: No


Important

  1. Make sure you have read our contribution guidelines
  2. Ensure there is an associated issue and you have been assigned to it
  3. Use the correct syntax to link this PR: Fixes #<issue number>.

fix #31396

Summary

The original issue was that workflow as tool functionality was missing log
records during execution, while agent tool calls were properly logged.

Root Cause Analysis

After investigation, I found that the logging mechanism had a gap in the workflow
tool callback handler:

  1. Agent Tools: Used DifyAgentCallbackHandler.on_tool_end() with
    trace_manager.add_trace_task()
  2. Workflow Tools: Used DifyWorkflowCallbackHandler.on_tool_execution() without
    trace_manager support
  3. Missing Trace Recording: Workflow tool executions were not being logged to the
    database

Solution Implemented

  1. Enhanced Workflow Tool Callback Handler

File: api/core/callback_handler/workflow_tool_callback_handler.py

def on_tool_execution(
    self,
    tool_name: str,
    tool_inputs: Mapping[str, Any],
    tool_outputs: Iterable[ToolInvokeMessage],
    message_id: str | None = None,
    timer: Any | None = None,
    trace_manager: Union["TraceQueueManager", None] = None,
) -> Generator[ToolInvokeMessage, None, None]:
    # Convert tool_outputs to list for processing
    tool_outputs_list = list(tool_outputs)

    # Record trace for workflow tool execution
    if trace_manager:
        from core.ops.entities.trace_entity import TraceTaskName
        from core.ops.ops_trace_manager import TraceTask

        trace_manager.add_trace_task(
            TraceTask(
                TraceTaskName.TOOL_TRACE,
                message_id=message_id,
                tool_name=tool_name,
                tool_inputs=tool_inputs,
                tool_outputs=tool_outputs_list,
                timer=timer,
            )
        )

    for tool_output in tool_outputs_list:
        print_text("\n[on_tool_execution]\n", color=self.color)
        print_text("Tool: " + tool_name + "\n", color=self.color)
        print_text("Outputs: " + tool_output.model_dump_json()[:1000] + "\n",
color=self.color)
        print_text("\n")
        yield tool_output
  1. Updated Tool Engine for Trace Manager Support

File: api/core/tools/tool_engine.py

@staticmethod
def generic_invoke(
    tool: Tool,
    tool_parameters: dict[str, Any],
    user_id: str,
    workflow_tool_callback: DifyWorkflowCallbackHandler,
    workflow_call_depth: int,
    conversation_id: str | None = None,
    app_id: str | None = None,
    message_id: str | None = None,
    trace_manager: Union["TraceQueueManager", None] = None,
) -> Generator[ToolInvokeMessage, None, None]:
    # ... existing code ...

    # hit the callback handler
    response = workflow_tool_callback.on_tool_execution(
        tool_name=tool.entity.identity.name,
        tool_inputs=tool_parameters,
        tool_outputs=response,
        message_id=message_id,
        trace_manager=trace_manager,
    )

    return response

  1. Extended Graph Runtime State

File: api/core/workflow/runtime/graph_runtime_state.py

def __init__(
    self,
    *,
    variable_pool: VariablePool,
    start_at: float,
    # ... other parameters ...
    trace_manager: Union["TraceQueueManager", None] = None,
) -> None:
    # ... existing code ...
    self._trace_manager = trace_manager

@property
def trace_manager(self) -> Union["TraceQueueManager", None]:
    return self._trace_manager
  1. Updated Tool Node Integration

File: api/core/workflow/nodes/tool/tool_node.py

# Get trace_manager from the graph runtime state
trace_manager = self.graph_runtime_state.trace_manager

try:
    message_stream = ToolEngine.generic_invoke(
        tool=tool_runtime,
        tool_parameters=parameters,
        user_id=self.user_id,
        workflow_tool_callback=DifyWorkflowCallbackHandler(),
        workflow_call_depth=self.workflow_call_depth,
        app_id=self.app_id,
        conversation_id=conversation_id.text if conversation_id else None,
        trace_manager=trace_manager,
    )
  1. Updated App Runners

Files:

  • api/core/app/apps/workflow/app_runner.py
  • api/core/app/apps/advanced_chat/app_runner.py
  • api/core/app/apps/pipeline/pipeline_runner.py
graph_runtime_state = GraphRuntimeState(
    variable_pool=variable_pool,
    start_at=time.perf_counter(),
    trace_manager=self.application_generate_entity.trace_manager,
)

Additional Fix: Agent Call Detection

Why conversation_id and message_id are required

Dify distinguishes between two types of workflow calls:

  1. Debug Mode (Direct testing in debugger):
    - No conversation_id/message_id
    - Behavior: No logs saved to database
    - Purpose: Development and testing
  2. Agent/Production Mode (Called via applications):
    - Has conversation_id and message_id
    - Behavior: Logs saved permanently
    - Purpose: Real usage scenarios and analysis

Implementation

File: api/core/app/apps/workflow/app_generator.py
elif invoke_from == InvokeFrom.DEBUGGER:
# Check if this is called from an agent (should be treated as app-run, not
debugging)
is_agent_call = any(
key in args for key in ["conversation_id", "message_id"]
)
workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN
if is_agent_call else WorkflowRunTriggeredFrom.DEBUGGING

extras = {
**extract_external_trace_id_from_args(args),
# Add conversation_id and message_id to extras for agent call detection
**{k: v for k, v in args.items() if k in ["conversation_id", "message_id"]},
}

File: api/core/app/apps/workflow/generate_task_pipeline.py
elif invoke_from == InvokeFrom.DEBUGGER:
agent_context = self._application_generate_entity.extras
if agent_context and any(key in str(agent_context) for key in
["conversation_id", "message_id"]):
created_from = WorkflowAppLogCreatedFrom.SERVICE_API
else:
# not save log for regular debugging
return

File: api/core/tools/workflow_as_tool/tool.py

Add conversation_id and message_id to args if available

args = {"inputs": tool_parameters, "files": files}
if conversation_id:
args["conversation_id"] = conversation_id
if message_id:
args["message_id"] = message_id

Fixed Circular Import Issues

Used TYPE_CHECKING and string type annotations to resolve circular import
problems:

if TYPE_CHECKING:
from core.ops.ops_trace_manager import TraceQueueManager

def method(self, trace_manager: Union["TraceQueueManager", None] = None):
# Method implementation

Results

Workflow as tool logging now works correctly

  • Tool calls are properly recorded to the database
  • Logs appear in the frontend interface
  • Consistent logging format with agent tools
  • Maintains backward compatibility

Distinguishes between debugging and production usage

  • Debug testing doesn't pollute production logs
  • Agent/tool calls are properly logged for analysis
  • Flexible system for different usage scenarios

No circular import issues

  • Uses TYPE_CHECKING for type annotations
  • Maintains clean architecture boundaries

The fix ensures that workflow as tool functionality provides complete execution
tracing and logging capabilities, matching the behavior of agent tool calls while
maintaining the flexibility to distinguish between development testing and
production usage.

Screenshots

Before After
... ...
image

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran dev/reformat(backend) and cd web && npx lint-staged(frontend) to appease the lint gods
**Original Pull Request:** https://github.com/langgenius/dify/pull/29199 **State:** closed **Merged:** No --- > [!IMPORTANT] > > 1. Make sure you have read our [contribution guidelines](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) > 1. Ensure there is an associated issue and you have been assigned to it > 1. Use the correct syntax to link this PR: `Fixes #<issue number>`. fix #31396 ## Summary The original issue was that workflow as tool functionality was missing log records during execution, while agent tool calls were properly logged. Root Cause Analysis After investigation, I found that the logging mechanism had a gap in the workflow tool callback handler: 1. Agent Tools: Used DifyAgentCallbackHandler.on_tool_end() with trace_manager.add_trace_task() 2. Workflow Tools: Used DifyWorkflowCallbackHandler.on_tool_execution() without trace_manager support 3. Missing Trace Recording: Workflow tool executions were not being logged to the database Solution Implemented 1. Enhanced Workflow Tool Callback Handler File: api/core/callback_handler/workflow_tool_callback_handler.py ```python def on_tool_execution( self, tool_name: str, tool_inputs: Mapping[str, Any], tool_outputs: Iterable[ToolInvokeMessage], message_id: str | None = None, timer: Any | None = None, trace_manager: Union["TraceQueueManager", None] = None, ) -> Generator[ToolInvokeMessage, None, None]: # Convert tool_outputs to list for processing tool_outputs_list = list(tool_outputs) # Record trace for workflow tool execution if trace_manager: from core.ops.entities.trace_entity import TraceTaskName from core.ops.ops_trace_manager import TraceTask trace_manager.add_trace_task( TraceTask( TraceTaskName.TOOL_TRACE, message_id=message_id, tool_name=tool_name, tool_inputs=tool_inputs, tool_outputs=tool_outputs_list, timer=timer, ) ) for tool_output in tool_outputs_list: print_text("\n[on_tool_execution]\n", color=self.color) print_text("Tool: " + tool_name + "\n", color=self.color) print_text("Outputs: " + tool_output.model_dump_json()[:1000] + "\n", color=self.color) print_text("\n") yield tool_output ``` 2. Updated Tool Engine for Trace Manager Support File: api/core/tools/tool_engine.py ```python @staticmethod def generic_invoke( tool: Tool, tool_parameters: dict[str, Any], user_id: str, workflow_tool_callback: DifyWorkflowCallbackHandler, workflow_call_depth: int, conversation_id: str | None = None, app_id: str | None = None, message_id: str | None = None, trace_manager: Union["TraceQueueManager", None] = None, ) -> Generator[ToolInvokeMessage, None, None]: # ... existing code ... # hit the callback handler response = workflow_tool_callback.on_tool_execution( tool_name=tool.entity.identity.name, tool_inputs=tool_parameters, tool_outputs=response, message_id=message_id, trace_manager=trace_manager, ) return response ``` 3. Extended Graph Runtime State File: api/core/workflow/runtime/graph_runtime_state.py ```python def __init__( self, *, variable_pool: VariablePool, start_at: float, # ... other parameters ... trace_manager: Union["TraceQueueManager", None] = None, ) -> None: # ... existing code ... self._trace_manager = trace_manager @property def trace_manager(self) -> Union["TraceQueueManager", None]: return self._trace_manager ``` 4. Updated Tool Node Integration File: api/core/workflow/nodes/tool/tool_node.py ```python # Get trace_manager from the graph runtime state trace_manager = self.graph_runtime_state.trace_manager try: message_stream = ToolEngine.generic_invoke( tool=tool_runtime, tool_parameters=parameters, user_id=self.user_id, workflow_tool_callback=DifyWorkflowCallbackHandler(), workflow_call_depth=self.workflow_call_depth, app_id=self.app_id, conversation_id=conversation_id.text if conversation_id else None, trace_manager=trace_manager, ) ``` 5. Updated App Runners Files: - api/core/app/apps/workflow/app_runner.py - api/core/app/apps/advanced_chat/app_runner.py - api/core/app/apps/pipeline/pipeline_runner.py ```python graph_runtime_state = GraphRuntimeState( variable_pool=variable_pool, start_at=time.perf_counter(), trace_manager=self.application_generate_entity.trace_manager, ) ``` Additional Fix: Agent Call Detection Why conversation_id and message_id are required Dify distinguishes between two types of workflow calls: 1. Debug Mode (Direct testing in debugger): - No conversation_id/message_id - Behavior: No logs saved to database - Purpose: Development and testing 2. Agent/Production Mode (Called via applications): - Has conversation_id and message_id - Behavior: Logs saved permanently - Purpose: Real usage scenarios and analysis Implementation File: api/core/app/apps/workflow/app_generator.py elif invoke_from == InvokeFrom.DEBUGGER: # Check if this is called from an agent (should be treated as app-run, not debugging) is_agent_call = any( key in args for key in ["conversation_id", "message_id"] ) workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN \ if is_agent_call else WorkflowRunTriggeredFrom.DEBUGGING extras = { **extract_external_trace_id_from_args(args), # Add conversation_id and message_id to extras for agent call detection **{k: v for k, v in args.items() if k in ["conversation_id", "message_id"]}, } File: api/core/app/apps/workflow/generate_task_pipeline.py elif invoke_from == InvokeFrom.DEBUGGER: agent_context = self._application_generate_entity.extras if agent_context and any(key in str(agent_context) for key in ["conversation_id", "message_id"]): created_from = WorkflowAppLogCreatedFrom.SERVICE_API else: # not save log for regular debugging return File: api/core/tools/workflow_as_tool/tool.py # Add conversation_id and message_id to args if available args = {"inputs": tool_parameters, "files": files} if conversation_id: args["conversation_id"] = conversation_id if message_id: args["message_id"] = message_id Fixed Circular Import Issues Used TYPE_CHECKING and string type annotations to resolve circular import problems: if TYPE_CHECKING: from core.ops.ops_trace_manager import TraceQueueManager def method(self, trace_manager: Union["TraceQueueManager", None] = None): # Method implementation Results ✅ Workflow as tool logging now works correctly - Tool calls are properly recorded to the database - Logs appear in the frontend interface - Consistent logging format with agent tools - Maintains backward compatibility ✅ Distinguishes between debugging and production usage - Debug testing doesn't pollute production logs - Agent/tool calls are properly logged for analysis - Flexible system for different usage scenarios ✅ No circular import issues - Uses TYPE_CHECKING for type annotations - Maintains clean architecture boundaries The fix ensures that workflow as tool functionality provides complete execution tracing and logging capabilities, matching the behavior of agent tool calls while maintaining the flexibility to distinguish between development testing and production usage. ## Screenshots | Before | After | |--------|-------| | ... | ... | <img width="2932" height="746" alt="image" src="https://github.com/user-attachments/assets/4a2be279-08c7-44c4-b840-41e9d9cf5f9b" /> ## Checklist - [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs) - [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!) - [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change. - [x] I've updated the documentation accordingly. - [x] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods
yindo added the pull-request label 2026-02-21 20:51:11 -05:00
yindo closed this issue 2026-02-21 20:51:11 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#32327