Skip to content

fix(agent): add Thinking field to synthetic post-summary assistant message#1101

Open
hoakhongmau98 wants to merge 2 commits intonextlevelbuilder:devfrom
hoakhongmau98:fix/deepseek-synthetic-thinking
Open

fix(agent): add Thinking field to synthetic post-summary assistant message#1101
hoakhongmau98 wants to merge 2 commits intonextlevelbuilder:devfrom
hoakhongmau98:fix/deepseek-synthetic-thinking

Conversation

@hoakhongmau98
Copy link
Copy Markdown

Summary

After session compaction, the agent loop injects a synthetic assistant ack ("I understand the context from our previous conversation. How can I help you?") into history but leaves Thinking empty. For thinking-mode providers (DeepSeek v4-pro, Kimi), the OpenAI-compat request builder requires reasoning_content on every assistant turn — the empty Thinking causes HTTP 400 reasoning_content in the thinking mode must be passed back to the API on the very next turn after compaction.

This PR populates Thinking on the synthetic message so the existing wire logic in internal/providers/openai_request.go emits a non-empty reasoning_content for DeepSeek/Kimi families.

Fixes #1047.

Root cause

  • internal/agent/loop_history.go:249-259 appends a synthetic assistant turn after the user's post-summary "Got it." message. Before this change, only Role and Content were set.
  • internal/providers/openai_request.go:59-61 writes reasoning_content = m.Thinking for assistant turns when openAIWireAssistantReasoningContent(model) returns true (DeepSeek, Kimi, OpenAI reasoning families). With Thinking == "", the field is omitted and DeepSeek rejects the request.

Change

Single 4-line diff in internal/agent/loop_history.go — add a Thinking value to the synthetic message. No provider-layer changes, no new helpers, no new tests required (existing internal/providers/openai_test.go:336 already asserts that DeepSeek wires reasoning_content from Thinking).

Relation to #1063

PR #1063 (nagaame:dev) addresses a related-but-distinct symptom: it ensures reasoning_content is preserved across tool-call turns and injects an empty string for DeepSeek when Thinking is absent in the provider layer. The two fixes are complementary:

Either PR alone leaves the other failure mode open. They can be merged independently; this one is the minimal, scoped fix for the post-summary case.

Test plan

  • go build ./... passes
  • go build -tags sqliteonly ./... passes
  • go vet ./... passes
  • go test -race ./internal/providers/... passes (existing DeepSeek wire test covers the path)
  • Manual verification on a session that triggered compaction with DeepSeek v4-pro

huyenchi11m7d4 and others added 2 commits May 5, 2026 04:50
…ssage — fixes DeepSeek 400 reasoning_content error

When a session has been compacted, buildMessages injects a synthetic
[user summary, assistant ack] pair. The assistant ack had Content but no
Thinking field, so for thinking-mode providers (DeepSeek v4-pro, Kimi)
the wired request was missing reasoning_content on that turn — DeepSeek
rejects the entire request with HTTP 400 ("The `reasoning_content` in
the thinking mode must be passed back to the API.").

Fix: set a non-empty Thinking value on the synthetic assistant message.
The existing wire path in internal/providers/openai_request.go is
already model-gated via openAIWireAssistantReasoningContent (DeepSeek /
Kimi / OpenAI reasoning families); other providers ignore the field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…epSeek reasoning_content

Without this, finalizeRun reads rs.finalThinking which is always "",
producing assistant messages with empty Thinking. DeepSeek/Kimi then
reject iteration 2 with "reasoning_content in the thinking mode must
be passed back to the API."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hoakhongmau98
Copy link
Copy Markdown
Author

Update: second fix appended

Trace 019df751-908e-70f3-b7d9-f3617e81e8ca confirms the original 4-line fix on loop_history.go is necessary but not sufficient — 3/3 sub-agents spawned via the batch-research skill still failed at iteration 2 with the same HTTP 400.

Root cause of the residual failure

The synthetic post-summary fix only covers history rebuilt after compaction. For sub-agents on a fresh session, the failure is on iteration 2 itself: the assistant's real (LLM-generated) thinking from iteration 1 never reaches the next request because runState.finalThinking is never populated from the v3 pipeline result.

Flow:

  1. internal/providers/openai_http.go:72result.Thinking = msg.ReasoningContent
  2. internal/pipeline/observe_stage.go:45state.Observe.FinalThinking = resp.Thinking
  3. internal/pipeline/run_state.go:59RunResult.Thinking: rs.Observe.FinalThinking
  4. internal/agent/loop_pipeline_adapter.go:254 (convertRunResult) — Thinking: pr.Thinking
  5. But bridgeRS.finalThinking is never assigned from the pipeline result ❌
  6. internal/agent/loop_finalize.go:106 reads rs.finalThinking → always ""reasoning_content missing → DeepSeek 400

Change in commit 2138a32

One-line bridge in runViaPipeline (internal/agent/loop_pipeline_adapter.go):

result := convertRunResult(pResult)
bridgeRS.finalThinking = result.Thinking
return result, nil

bridgeRS is the same runState instance that finalizeRun later reads when constructing the assistant message persisted to history. Backward compatible — non-thinking providers (Anthropic, OpenAI non-reasoning) ignore Thinking.

Verification

  • go build ./... passes
  • go build -tags sqliteonly ./... passes
  • go vet ./internal/agent/... passes

Manual reproduction on the cron-triggered sub-agent path is still pending.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] DeepSeek V4 Pro: Lỗi 400 do thiếu reasoning_content trên kênh Zalo (OpenAI-compatible)

3 participants