Repository navigation
Description
Self Checks
- I have read the Contributing Guide and Language Policy.
- This is only for bug report, if you would like to ask a question, please head to Discussions.
- I have searched for existing issues search for existing issues, including closed ones.
- I confirm that I am using English to submit this report, otherwise it will be closed.
- 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
- Please do not modify this template :) and fill in all the required fields.
Delivery status for Dify 1.19.0 — 2026-11-02
This remains open for the complete Agent replay rollout.
The reproduction and root-cause sections below describe the original affected versions; implementation has progressed as follows.
| Component | Current status | Remaining delivery |
|---|---|---|
| SDK | langgenius/dify-plugin-sdks#390 merged; SDK public version remains 0.10.2 | Publish SDK 0.11.0, including the reviewed langgenius/dify-plugin-sdks#412 follow-up |
| Graphon | langgenius/graphon#261, langgenius/graphon#355 and langgenius/graphon#376 merged | Refresh the host candidate, incorporate the selected langgenius/graphon#344 container changes, and record the production dependency |
| Dify | #44502 replaces closed/unmerged #41095 and covers legacy Agents, workflow history and Agent v2 | Resolve current main conflicts and complete the release gates; keep the PR in draft |
| Official providers/Agents | langgenius/dify-official-plugins#4034 implements complete calls and signed replay; langgenius/dify-official-plugins#4156 adapts response hooks to the final SDK | Reconcile the candidates, publish the SDK-based plugin archives, and set minimum Dify/daemon versions |
| Daemon | langgenius/dify-plugin-daemon#828 remains open for lossless backwards JSON and history | Coordinate the selected deployment; no 1.19 milestone assignment in this update |
The Graphon, SDK, Official Plugins and Daemon numbers in this table refer to their respective repositories.
The existing host PR records controlled transport/history validation, and its current GitHub API, Agent, migration and web E2E checks have passed.
Live vendor signature acceptance and the production rollout remain unverified.
langgenius/dify-official-plugins#4034 is a prerequisite for the planned host consumer: the new collector rejects empty/repeated call IDs while existing providers such as Azure AI Studio emit argument fragments as separate calls.
Its OpenAI-compatible upgrade also needs langgenius/dify-official-plugins#4156's tools response-hook adaptation for the final SDK 0.11.
langgenius/dify-official-plugins#4033 and langgenius/dify-official-plugins#4035 remain separate OpenAI/endpoint delivery work with no independent Dify 1.19 release blocker established.
The target is the 1.19.0 milestone.
Dify version
1.16.1
Cloud or Self Hosted
- Cloud
- Self Hosted (Docker)
- Self Hosted (Source)
Expected to apply to all deployments running the plugin daemon (code-path inference; no per-deployment reproduction performed).
Steps to reproduce
Scope note: this report is based on code-path analysis rather than a live-deployment reproduction. It was verified against the cited sources — dify_plugin SDK 0.10.0, the official anthropic plugin v0.3.27, current Dify core / graphon sources, and the official Anthropic API documentation — and applies to releases built from the same code paths.
- Use an official Anthropic plugin model with thinking. No user configuration is needed to reach this state on the current model generation — per the official docs, "On Claude Opus 5, Claude Sonnet 5, Claude Fable 5, Claude Mythos 5, and Claude Mythos Preview, thinking is already on: no configuration needed" (the failure still only manifests on turns where the model actually emits a thinking block — adaptive thinking may skip it on simple requests).
- Create an Agent app using the function-calling strategy with at least one tool — or a workflow Agent node — such that a request can require more than one LLM round (assistant
tool_use→ tool result → next LLM call). - Run a query that forces a tool call.
- First LLM call: the API returns
thinking(and possiblyredacted_thinking) +tool_useblocks. This round succeeds. - The core re-issues the second LLM call with the tool result. The replayed assistant turn is rebuilt from aggregated text (see root cause, gap 3). Depending on the path, what "survives" as plain text differs — but in all cases the original signed
thinking/ encryptedredacted_thinkingcontent blocks (with theirsignature/datafields) are omitted, which the API requires:- streaming, summarized display: the plugin emits thinking deltas as ordinary assistant text wrapped in
<think>markers;redacted_thinkingblocks are dropped entirely (their encrypteddatapayload is discarded — see gap 2); the core concatenates any emitted strings into a plaintextblock; - streaming,
display: "omitted": the API returns a regularthinkingblock with emptythinkingtext and asignature; the stream emits the block-start event and asignature_delta, but no thinking text. The plugin captures the signature only in instance state (gap 1), so the core replays neither the block nor its signature; - non-streaming: the plugin copies only
textblocks into the returned message content; thinking/redacted blocks are not represented in that message — they are retained only in per-instance state, which the next invocation does not share.
- streaming, summarized display: the plugin emits thinking deltas as ordinary assistant text wrapped in
- The API rejects the request.
✔️ Expected Behavior
The agent loop completes. The Anthropic API requires that, within a tool-use turn, thinking blocks be passed back "complete and unmodified" alongside the tool_use blocks they accompanied — the official docs (Thinking — Preserving thinking blocks) state:
Required: within a tool-use turn, pass thinking blocks back.
Each thinking block carries a signature field — "an encrypted copy of the full reasoning that you pass back unchanged in multi-turn and tool-use conversations" — and redacted_thinking blocks carry an opaque encrypted data field with the same requirement.
❌ Actual Behavior
When (a) the preceding assistant turn emitted a thinking or redacted_thinking block and (b) the next request is the tool-result continuation of that turn, the second LLM call fails with a 400 invalid_request_error whose message contains (per the official troubleshooting page):
thinkingorredacted_thinkingblocks in the latest assistant message cannot be modified
Multi-round tool use with thinking enabled is effectively unusable (for adaptive-thinking models the failure is intermittent — it depends on whether the model emitted a thinking block that turn).
From the same docs: "Mid-turn conflicts degrade gracefully" applies only to explicitly toggling the top-level thinking parameter mid-turn — it does not apply to a tool-result request that replays an assistant turn with missing/modified signed blocks; that is the hard 400 above.
Historical root cause (code level)
Three separate loss points — two in the official Anthropic plugin, one in the core (schema, transport, and agent loop). The plugin already contains re-attachment logic, but it is structurally dead, and even a fixed plugin has nowhere to carry the blocks:
Gap 1 (plugin): per-turn reasoning state is kept on instance state of a class the SDK instantiates statelessly. The plugin SDK creates a fresh model instance for every LLM call — dify_plugin/core/model_factory.py (SDK 0.10.0):
class ModelFactory:
"""
Model factory
Given provider configurations and its model list, generate stateless
model instances.
"""
def get_instance(self, model_type: ModelType) -> AIModel:
return self.models[model_type](self.provider.models)PluginRegistration.get_model_instance() (same SDK, core/plugin_registration.py) simply delegates to this factory on every request. The official Anthropic plugin nevertheless stores the response's thinking blocks in instance state (models/llm/llm.py, v0.3.27):
__init__initializesself.previous_thinking_blocks = []/self.previous_redacted_thinking_blocks = [](lines 218–219);- the non-stream handler stores them from the response content (lines 945–954); the stream handler stores them at
message_stopwhen tool calls are present (lines 1225–1228); _process_assistant_messagere-attaches them to the assistant turn only when the prompt contains tool messages (lines 1486–1501):
has_tool_messages = any(isinstance(msg, ToolPromptMessage) for msg in all_messages)
if has_tool_messages:
content.extend(self.previous_thinking_blocks)
content.extend(self.previous_redacted_thinking_blocks)But the tool-result round (call N+1) executes on a different instance, so both lists are always [] there. The state is written on an instance that is discarded and read on a fresh one — the mechanism cannot fire. (Caching instances per conversation would not fix this: the SDK documents instances as stateless, and instances are not scoped per conversation.)
Gap 2 (plugin, latent until gap 1 is fixed): the redacted_thinking data payload is dropped in the stream handler. redacted_thinking blocks arrive on the content_block_start event (as RedactedThinkingBlock with an encrypted data field), but the handler records only the type (lines 1099–1101):
elif getattr(content_block, "type", None) == "redacted_thinking":
current_redacted_thinking_blocks.append({
"type": "redacted_thinking"
})The data field is never captured. (The plugin's separate redacted_thinking delta branch at lines 1169–1172, which would emit a human-facing placeholder string, is unreachable with the pinned anthropic SDK: RawContentBlockDelta is Union[TextDelta, InputJSONDelta, CitationsDelta, ThinkingDelta, SignatureDelta] — there is no redacted delta variant.) Even with working re-attachment, such blocks could not be round-tripped, because the docs require the original data value to be passed back unchanged.
Gap 3 (core schema, transport + agent loop): no end-to-end transport for signed reasoning blocks between LLM calls. The plugin SDK declares opaque_body: JsonValue | None on AssistantPromptMessage — a forward-compatible provider-state slot (dify-plugin-sdks PR #284, dify-plugin-daemon PR #585) — but the Dify core runtime does not carry it:
fc_agent_runner.pyimportsAssistantPromptMessagefromgraphon.model_runtime.entities(core pinsgraphon==0.7.0inapi/pyproject.toml), and the graphon v0.7.0 model has noopaque_bodyfield — so the field is dropped when plugin results are deserialized in core, before the agent loop ever sees it;- even if it arrived, the agent runner rebuilds each assistant turn from aggregated string + tool_calls only, and never propagates provider state (lines 212–218 / 244–248 aggregate; lines 265–278 rebuild):
# streamed / blocking deltas are aggregated into a plain string (lines 212–218 / 244–248)
response += str(chunk.delta.message.content)
...
# the assistant turn is then rebuilt from string + tool_calls only (lines 265–278)
assistant_message = AssistantPromptMessage(content=response, tool_calls=[])
if tool_calls:
assistant_message.tool_calls = [...]
self._current_thoughts.append(assistant_message)base_agent_runner.pyreconstructs historical assistant messages fromagent_thought.thought+ tool calls in the same way.
No agent-loop path propagates the field. Tellingly, the official moonshot plugin — which does populate opaque_body with reasoning_content — ships a regex fallback that re-parses reasoning from the flattened text content whenever opaque_body is unavailable — the same loss documented in this gap. Consequently, in the streaming (summarized) path the only "thinking" that survives into round N+1 is the summary text flattened into ordinary assistant text content; in the non-streaming path nothing survives. The signed thinking / encrypted redacted_thinking blocks are lost by construction in both paths.
Related work
- Host implementation: #44502, replacing closed/unmerged fix(agent): preserve provider state and signed tool-loop prefixes #41095.
- SDK complete messages: #390, merged; 0.11.0 publication #413 remains open.
- Graphon message/collector work: #261, #355, and #376, all merged.
- Provider and Agent replay: langgenius/dify-official-plugins#4032 / #4034, with #4156 required when adopting the final SDK response hooks.
- Backwards transport: langgenius/dify-plugin-daemon#827 / #828, both open and outside milestone assignment in this update.
- OpenAI-specific plugin adoption: langgenius/dify-official-plugins#4033 / #4035.
Requested change
Persist and pass through assistant-turn reasoning blocks across LLM calls.
Phase 1 — in-memory agent-loop bugfix (the 400 itself):
-
Cross-repo message/transport contract (dify-plugin-sdks + graphon + Dify core): preserve an optional provider-state field on assistant messages end-to-end. The plugin SDK already exposes
AssistantPromptMessage.opaque_body; graphon'sAssistantPromptMessage(and theLLMResult/LLMResultChunkDelta.messagemodels built on it) do not yet. Extend the runtime schema and the daemon→core deserialization path soopaque_bodysurvives both blocking results and the terminal stream chunk. Define the payload as the complete ordered sequence of provider assistant content blocks as returned (e.g.thinking→text→tool_usein their original positions) — not a bare list of reasoning blocks, since Anthropic supports interleaved thinking between tool calls and a list without ordering cannot round-trip interleaved turns:{ "assistant_blocks": [ {"type": "thinking", "thinking": "...", "signature": "..."}, {"type": "text", "text": "..."}, {"type": "tool_use", "id": "toolu_...", "name": "...", "input": {}}, {"type": "redacted_thinking", "data": "..."} ] } -
Core, in-memory agent loop:
fc_agent_runner.pymust preserve and propagate the provider state from the LLM result / terminal stream chunk into_current_thoughtsfor the next iteration.
Phase 2 — persistence for resumed conversations:
- Core, conversation memory: persist the provider state on assistant messages (the core prompt-save utility currently serializes role/text/files/tool_calls only) and reconstruct it when a conversation is resumed.
Plugin side (coordinated with dify-official-plugins#3715, not part of this repo's ask): the final plugin implementation should store the complete ordered block sequence (including redacted_thinking.data — see gap 2) rather than type-only entries.
Design considerations: stale blocks must be dropped when the user switches models mid-conversation — per the Anthropic docs, "Thinking blocks are tied to the model that produced them. Other models silently ignore them rather than rejecting the request, but ignored blocks still add input tokens." The behavior should also be specified for the display: "omitted" / summarized modes. Suggested test matrix: stream + non-stream, redacted blocks, omitted display, interleaved / multiple tool calls (ordering preservation), resumed history, model switch.
Workaround
For models that allow it, disabling thinking is the in-place workaround. For always-on model generations (e.g. Fable 5, Mythos 5) there is no configuration-only workaround while retaining that model/provider for multi-round tool use; switching models/providers or avoiding the multi-round tool loop are the only alternatives.
Impact
- Official Anthropic plugin: any affected Claude model/turn that emits
thinkingorredacted_thinkingblocks and enters a tool-result round — Agent (function-calling) or workflow Agent node. - Any future provider whose API requires signed reasoning blocks to be echoed back will hit the same wall; the passthrough in core/SDK benefits all such providers.
Activity
dosubot commentedon Aug 22, 2026
This is a well-researched and accurate report. Here's what I can confirm about the current state of the code and related work:
The three gaps you identified are confirmed in the current codebase:
Gap 1 (plugin instance state): The re-attachment logic in
_process_assistant_message(lines 1499–1501) reads fromself.previous_thinking_blocks/self.previous_redacted_thinking_blocks, but these are stored on a model instance that is discarded between RPC calls [1] [2]. This is being addressed by dify-official-plugins#3715, which persists the blocks viaAssistantPromptMessage.opaque_body— the same pattern the Moonshot plugin already uses [3].Gap 2 (redacted_thinking data dropped): Confirmed — the stream handler captures only
{"type": "redacted_thinking"}with nodatafield [4].Gap 3 (core has no opaque_body transport): This is the core-side blocker.
fc_agent_runner.pyimportsAssistantPromptMessagefromgraphon.model_runtime.entitiesand rebuilds each assistant turn from a plain string + tool calls only — no provider state is propagated [5].There is an open PR — dify#31566 — authored by @BeautyyuYanli titled "feat: support opaque_body from plugin LLM calls" (part of #31557). It proposes adding
opaque_bodyto the core message entity, capturing it during streaming, persisting it inmessage_metadataasassistant_opaque_body, and restoring it when rebuilding memory prompt history [6]. That PR is still open and unmerged, which is whyopaque_bodyis absent from the currentapi/coretree [7].What still needs to happen on the core side:
opaque_bodyonAssistantPromptMessage/LLMResult/LLMResultChunkDelta.messageso the field survives daemon→core deserialization.fc_agent_runner.pyneeds to readopaque_bodyfrom the LLM result and carry it into_current_thoughtsfor the next iteration (your Phase 1).base_agent_runner.pyneeds to persist and reconstructopaque_bodywhen rebuilding history fromagent_thoughtrecords (your Phase 2).PR #31566 covers most of this. You may want to coordinate with @BeautyyuYanli on that PR, since it's the direct core-side counterpart to the plugin-side #3715.
To reply, just mention @dosu.
Docs are dead. Just use Dosu.