[PR #32296] feat: Add Openai response tool call supper #33653

Open
opened 2026-02-21 20:53:40 -05:00 by yindo · 0 comments
Owner

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

State: open
Merged: No


Fixes #32027

Summary

This PR adds external tool callback support to Dify's LLM node in Advanced Chat (Chatflow) applications. When an LLM node is configured with external_tool_callback_enabled and the caller passes tool_call_mode: "structured", the workflow will pause when the LLM returns tool_calls, emit a workflow_paused SSE event with the tool call details, and resume when the caller sends back tool_results. This loop continues until the LLM stops calling tools or the max round limit is reached.

This enables Dify to act as an upstream LLM orchestrator for external tool execution systems (e.g., OpenClaw), following the OpenAI tool-calling protocol.

Motivation and Context

Dify workflows currently execute tools internally via plugins. However, many use cases require the LLM's tool call decisions to be executed by an external system that has access to local tools, sandboxes, or proprietary APIs. This PR bridges that gap by allowing the workflow to pause mid-execution, hand off tool calls to the caller, and resume with results — all within a single conversation turn.

Key Changes

1. Entity & Model Layer (feat(entities))

  • Add ToolCallPending variant to PauseReason discriminated union
  • Add QueueWorkflowPausedEvent and WorkflowPausedStreamResponse for pause SSE
  • Add WORKFLOW_PAUSED to QueueEvent and StreamEvent enums
  • Add tool_call_mode field to AdvancedChatAppGenerateEntity
  • Add external_tool_call_mode to SystemVariable
  • Add process_data to PauseRequestedEvent and propagate to NodeRunResult
  • Exclude trace_manager from serialization in AppGenerateEntity

2. LLM Node (feat(llm-node))

  • Add external_tool_callback_enabled and max_tool_call_rounds to LLMNodeData
  • Refactor _run into first-run vs resume paths with state serialization
  • On tool_calls from LLM, save prompt state and yield PauseRequestedEvent
  • On resume, restore prompt_messages, inject tool results, re-invoke LLM
  • Track tool_call_history and produce summary for downstream nodes
  • Filter external_tool_results to only relevant tool_call_ids per node
  • Add prompt message serialize/deserialize helpers

3. Graph Engine & Persistence (feat(engine))

  • Allow PAUSED status workflows to be re-paused in repository
  • Handle ToolCallPending reason type in WorkflowPauseReason creation
  • Remove update_outputs=False from paused node persistence call

4. App Generator / Runner / Pipeline (feat(advanced-chat))

  • Add _try_load_paused_state to find and restore paused workflow runs
  • Add _build_graph_engine_layers to wire PauseStatePersistenceLayer
  • Pass resumed_graph_runtime_state through runner for resume path
  • Forward tool_call_mode and GraphRunPausedEvent through pipeline
  • Handle QueueWorkflowPausedEvent to emit workflow_paused SSE stream

5. Web Frontend (feat(web))

  • Add "External Tool Callback" toggle and "Max rounds" input to LLM node panel
  • Add tool_call_history output variable when callback is enabled
  • Show paused status icon and tool callback round indicator in run panel
  • Add i18n strings for en-US and zh-Hans
  • Update API docs for tool_call_mode structured mode

Checklist

  • 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 ruff format + ruff check --fix (backend) and npx eslint (frontend) to appease the lint gods.
**Original Pull Request:** https://github.com/langgenius/dify/pull/32296 **State:** open **Merged:** No --- Fixes #32027 ## Summary This PR adds **external tool callback** support to Dify's LLM node in Advanced Chat (Chatflow) applications. When an LLM node is configured with `external_tool_callback_enabled` and the caller passes `tool_call_mode: "structured"`, the workflow will **pause** when the LLM returns `tool_calls`, emit a `workflow_paused` SSE event with the tool call details, and **resume** when the caller sends back `tool_results`. This loop continues until the LLM stops calling tools or the max round limit is reached. This enables Dify to act as an upstream LLM orchestrator for external tool execution systems (e.g., OpenClaw), following the OpenAI tool-calling protocol. ## Motivation and Context Dify workflows currently execute tools internally via plugins. However, many use cases require the LLM's tool call decisions to be executed by an **external system** that has access to local tools, sandboxes, or proprietary APIs. This PR bridges that gap by allowing the workflow to pause mid-execution, hand off tool calls to the caller, and resume with results — all within a single conversation turn. ## Key Changes ### 1. Entity & Model Layer (`feat(entities)`) - Add `ToolCallPending` variant to `PauseReason` discriminated union - Add `QueueWorkflowPausedEvent` and `WorkflowPausedStreamResponse` for pause SSE - Add `WORKFLOW_PAUSED` to `QueueEvent` and `StreamEvent` enums - Add `tool_call_mode` field to `AdvancedChatAppGenerateEntity` - Add `external_tool_call_mode` to `SystemVariable` - Add `process_data` to `PauseRequestedEvent` and propagate to `NodeRunResult` - Exclude `trace_manager` from serialization in `AppGenerateEntity` ### 2. LLM Node (`feat(llm-node)`) - Add `external_tool_callback_enabled` and `max_tool_call_rounds` to `LLMNodeData` - Refactor `_run` into first-run vs resume paths with state serialization - On `tool_calls` from LLM, save prompt state and yield `PauseRequestedEvent` - On resume, restore `prompt_messages`, inject tool results, re-invoke LLM - Track `tool_call_history` and produce summary for downstream nodes - Filter `external_tool_results` to only relevant `tool_call_ids` per node - Add prompt message serialize/deserialize helpers ### 3. Graph Engine & Persistence (`feat(engine)`) - Allow `PAUSED` status workflows to be re-paused in repository - Handle `ToolCallPending` reason type in `WorkflowPauseReason` creation - Remove `update_outputs=False` from paused node persistence call ### 4. App Generator / Runner / Pipeline (`feat(advanced-chat)`) - Add `_try_load_paused_state` to find and restore paused workflow runs - Add `_build_graph_engine_layers` to wire `PauseStatePersistenceLayer` - Pass `resumed_graph_runtime_state` through runner for resume path - Forward `tool_call_mode` and `GraphRunPausedEvent` through pipeline - Handle `QueueWorkflowPausedEvent` to emit `workflow_paused` SSE stream ### 5. Web Frontend (`feat(web)`) - Add "External Tool Callback" toggle and "Max rounds" input to LLM node panel - Add `tool_call_history` output variable when callback is enabled - Show paused status icon and tool callback round indicator in run panel - Add i18n strings for en-US and zh-Hans - Update API docs for `tool_call_mode` structured mode ## Checklist - [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 `ruff format` + `ruff check --fix` (backend) and `npx eslint` (frontend) to appease the lint gods.
yindo added the pull-request label 2026-02-21 20:53:40 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langgenius/dify#33653