Files
Kite c7dd11e03e fix(llm): preserve provider reasoning/thinking across assistant turns (#1070)
* fix(llm): preserve provider reasoning/thinking across assistant turns

Assistant tool-call history was rebuilt from Content()/ToolCalls() alone,
so provider reasoning state was dropped on replay across all three
supported protocols:

- OpenAI-compatible Chat Completions: reasoning_content was parsed but
  never sent back on the next request.
- Anthropic Messages: thinking/redacted_thinking blocks were flattened to
  a display string, losing the signature required to replay them.
- OpenAI Responses: reasoning items (encrypted_content) were dropped
  entirely from the next request's input.

Adds Message.Native (NativeTurn), an opaque per-protocol replay payload
built by reusing each SDK's own response->request converter
(anthropic.Message.ToParam(), each Responses output item's ToParam()) so
the wire format is never hand-rolled. Each adapter type-asserts its own
payload before reuse and falls back to the existing reconstruction
otherwise, so cross-provider or native-less turns are unaffected.

Also:
- Adds Message.ReasoningContent, a plain-text projection used by the
  compression summarizer so a reasoning-only turn isn't rendered blank.
- Fixes NativeTurn.EstimatedTokens() double-counting visible content
  already counted via ExtractText(), which could trigger compression
  earlier than necessary.
- Guards the Responses adapter against carrying a dangling reasoning
  item with no accompanying message/function_call into Native.
- Persists reasoning_content in the session JSONL transcript and surfaces
  it in the session viewer, for audit visibility into no-visible-answer
  turns.
- Makes Anthropic extended thinking (extra_body.thinking) safe to enable
  via the existing provider config: dropped per-request when it would
  conflict with a forced tool_choice or exceed that call's max_tokens,
  rather than failing the whole request.
- Removes GroupingTask's hardcoded 4096 max_tokens cap in favor of the
  template's own completion limit, which was the actual trigger for the
  max_tokens/budget_tokens conflict above.

Refs #805

* fix(llm): tighten follow-ups from reasoning/thinking replay fix

- Compare Anthropic extended thinking's budget_tokens against the
  effective max_tokens (falling back to the same 8192 default
  buildAnthropicParams uses) instead of the raw, possibly-zero
  ChatRequest.MaxTokens, so the guard doesn't get skipped when the
  caller leaves MaxTokens unset.
- Preserve a degenerate turn in history when it has reasoning text but
  neither visible content nor a replayable Native payload (e.g. an
  openai-responses reasoning item with no accompanying
  message/function_call), instead of dropping all trace of it.
- Unify token accounting between CountMessagesTokens and
  computeActiveZoneSize via a shared messageTokens() helper, so
  reasoning-heavy conversations don't get judged compressible by one
  and still-over-budget by the other.
- Only write reasoning_content to the session JSONL transcript when
  non-empty, matching the existing pattern for comments/error/
  sourceSessionId.

* fix(session): persist LLM native replay payload for training reflow

Session JSONL carried a human-readable ReasoningContent projection but
dropped the opaque replay state (llm.Message.Native) that holds an
Anthropic thinking block's signature or an OpenAI Responses reasoning
item's encrypted_content — the fields a full session export/training
reflow actually needs.

- ResponseRecord gains a Native field; SetResponse populates it from
  resp.Native() and persists it verbatim as native_payload on the
  llm_response JSONL record.
- copyMessagesForJSON now includes tool_calls (previously dropped) and
  native_payload per assistant message, so llm_request records are a
  complete, replayable projection of what was actually sent.
- copyMessages (the in-memory deep copy backing
  TaskRecord.RequestMessages) now also carries Native/ReasoningContent,
  closing a gap where its own "deep copy" contract silently dropped
  both fields.

Nothing parses or reshapes the payload on the way to disk — it is
written exactly as the provider adapter would replay it.

* refactor: trim verbose comments and add VisibleContent() unit tests

- Remove redundant 'what' comments and Go-basics explanations across
  the NativeTurn/reasoning replay implementation
- Keep only non-obvious 'why' comments (external API constraints,
  aliasing hazards, fallback semantics)
- Add 6 direct unit tests for ChatResponse.VisibleContent() covering:
  no choices, nil content, empty string, normal text, think-tag
  stripping, and whitespace-only (key difference from Content())

Net: -185 lines of comments, +66 lines of tests.

* docs: clarify json:"-" on ResponseMessage.Native

* style: fix gofmt alignment in client.go and history.go
2026-08-28 11:51:40 +08:00

7.0 KiB