mirror of
https://github.com/open-webui/open-webui.git
synced 2026-05-07 19:38:46 -05:00
[GH-ISSUE #18612] feat: Refactor and Improve Tools System #18651
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Davixk on GitHub (Oct 25, 2025).
Original GitHub issue: https://github.com/open-webui/open-webui/issues/18612
Problem Statement
Currently, tool calls are embedded inline as
<details>XML within assistant message content during streaming. After the tool completes and the model continues generating text, this XML is stripped from the message before it's stored.What happens:
"sure! let me create that for you""done! I generated your cottage image"When the user sends the next message, the model receives:
The model has no context that it called a tool, what arguments it passed, or what the tool returned. It only sees its own output text with no explanation for why it generated those specific words. This is misleading and degrades conversation quality.
Additional issues:
Solution
Elevate Tool Execution to First-Class Messages
Transform tool calls from embedded XML into proper messages. What is currently ONE assistant message becomes THREE separate messages:
Before (1 message):
After (3 messages):
Message Flow
Three message types in sequence:
Each tool call now has its own message and event timeline, just like regular messages do.
Streaming Flow
Step-by-step:
tool_callsfield populatedrole: "tool"andtool_call_idtool:statusevents withtool_call_idtool_call_id→ updates UI in real-timetool_outputfield in tool messageKey characteristics:
tool_call_id, not message contentEvent-Driven Status Updates
Each tool call gets its own event stream, identified by
tool_call_id:Frontend matches events to tool messages and maintains a chronological list of status updates per tool call. This list persists across page reloads.
Timeline Persistence
The frontend now maintains a proper, persistent timeline:
This timeline is preserved in the database and reconstructed on reload. Users can see the full history of what happened, when, and why.
Implementation Requirements
Database Schema
Add fields to Message model:
role(String, enum: "user" | "assistant" | "tool")tool_calls(JSON, nullable) - for assistant messages that call toolstool_call_id(String, nullable) - for tool messagestool_output(JSON, nullable) - for tool messagesAll new fields are nullable for backwards compatibility with existing messages.
Backend Changes
Message Creation:
rolefor each message typetool_callsfield when model calls toolstool_call_id(before tool executes)tool_outputafter tool returns dataStreaming:
role: "tool"as separate messagestool:statusevents withtool_call_idfor matchingContext Management:
tool_output)Frontend Changes
Rendering:
role: "tool"messages → render tool execution UI inline (not as separate chat bubble)tool:statusevents to tool messages bytool_call_idPersistence:
Why This Works
Complete Context Preservation:
Event-Driven Architecture:
tool_call_idClean Separation of Concerns:
Benefits:
@Davixk commented on GitHub (Oct 25, 2025):
as for how the tool can emit tool events, I can see two ways:
__tool_call_id__argument to the tool call, just like others like__event_emitter__are already injected. the tool can then use it as needed with the event emitter to emit tool events__tool_event_emitter__which is already associated with the current tool call ID, and can easily emit events directly displayed for that tool callversion 2 sounds better to me. it limits the tool from potentially guessing and hijacking other tool call status events, while also streamlining usage.