Skip to content

fix(langchain): preserve conversation text, roles and tool history - #6275

Merged
t0saki merged 11 commits into
volcengine:mainfrom
ehz0ah:fix/langchain-message-fidelity
Oct 19, 2026
Merged

t0saki merged 11 commits into
volcengine:mainfrom
ehz0ah:fix/langchain-message-fidelity

Conversation

@ehz0ah

@ehz0ah ehz0ah commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

Preserve conversation information across LangChain capture, recall-query selection and history restoration. This consolidates three contributed fixes and completes their shared message-conversion contracts:

  • Responses input_text / output_text blocks previously disappeared from recorded text, tool outputs and recall queries. They now retain their text.
  • Generic user messages were stored as assistant memory, while generic policy messages were persisted. Capture and recall now share role normalization: user/human are user content; system/developer are excluded from capture. Typed-message precedence and streamed message chunks are preserved.
  • Completed tool parts previously disappeared from restored history. They now restore as call/result pairs, including zero-argument calls. Results finish earlier pending calls before new assistant content. An explicit pending part starts another invocation after completion, including identical polling calls. Only repeated completed pairs without a new pending part are suppressed, and only when the call, output and status match.

The zero-argument fix includes the owning server and Python SDK serializers: explicit tool_input={} is retained, while missing/None inputs remain omitted. This uses the existing optional field; there is no schema migration, API signature, dependency or release-workflow change. Budgeted previews omit removed tool inputs on the wire, keeping them distinguishable from genuine {} arguments. Already-persisted records whose text, role or tool input was lost cannot be repaired.

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

Fixes #5806
Fixes #5853
Fixes #6242

Consolidates #5808, #5854 and #6247.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Changes Made

  • Reuse the existing text extractor and one role mapping for synchronous and asynchronous recording/history/recall paths.
  • Keep pending calls across intervening user messages so late results remain in restored history. A matching combined record completes the pending call once; a different call that reused the ID stays distinct. Recorded user-message order is preserved; this does not normalize malformed histories into provider-valid transcripts.
  • Preserve empty tool inputs through SDK normalization, server JSON/JSONL serialization and history reads.
  • Track pending invocations separately from completed pairs. Reset completed-pair deduplication at each user message while retaining unresolved calls. Deduplication requires matching call ID/name/arguments, output and result status, with no explicit new pending part. Split and coalesced polling calls remain separate even when their IDs, arguments and results are identical. Input records are not mutated.
  • Omit oversized tool inputs removed by active-message retention using None, preserving the distinction from real {} arguments and leaving durable messages unchanged.
  • Preserve error status and empty outputs. Reconstruct standalone calls only when their ID, name and input are recorded; incomplete orphan results retain their existing handling.
  • Keep adapter regressions in the adapter's existing recording and history tests; extend existing server and SDK serialization tests.

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows
python -m pytest -q -rs --noconftest -p no:cacheprovider -o addopts='' \
  integrations/frameworks/langchain/tests tests/unit/test_langchain_package_boundary.py \
  tests/unit/test_message.py tests/session/test_retention.py
python -m pytest -q --noconftest -p no:cacheprovider -o addopts='' tests/unit/test_message_format.py
python -m pytest -q -rs --confcutdir=sdk/python/tests -p no:cacheprovider -o addopts='' sdk/python/tests
ruff check integrations/frameworks/langchain/src integrations/frameworks/langchain/tests \
  openviking/message/message.py sdk/python/openviking_sdk/message.py \
  sdk/python/tests/test_async_client_behaviors.py tests/unit/test_message.py \
  openviking/session/retention.py tests/session/test_retention.py
ruff format --check integrations/frameworks/langchain/src integrations/frameworks/langchain/tests \
  openviking/message/message.py sdk/python/openviking_sdk/message.py \
  sdk/python/tests/test_async_client_behaviors.py tests/unit/test_message.py \
  openviking/session/retention.py tests/session/test_retention.py
mypy --config-file integrations/frameworks/langchain/pyproject.toml integrations/frameworks/langchain/src/langchain_openviking
  • Python 3.12; LangChain 1.4.6, core 1.6.12, LangGraph 1.2.16: adapter + repository boundary + server message and retention tests 475 passed, 1 skipped. Adjacent server message-format tests: 5 passed.
  • Python 3.10; LangChain/core/LangGraph 1.0.0: adapter + repository boundary 400 passed, 2 skipped.
  • Full Python SDK suite (SDK files unchanged in this re-review): 236 passed.
  • Skips: optional SQLite checkpointer; additionally asyncio.timeout on Python 3.10. Existing streaming, cancellation, recording and tool-history tests are included.
  • The 94 focused tool-history cases give 30 failures / 64 passes at aeaf57018, and 94 passes with this correction. They include identical split polling calls with unchanged/changed outputs, unfinished follow-up calls, coalesced polling, changed completed outputs/status, duplicate completed records, ID reuse and late results.
  • A separate differential run compares 432 generated histories against main, with zero differences. It covers split/coalesced/approval layouts, repeated/unique IDs, identical/different/empty arguments, unchanged/changing/empty outputs, success/error status, user boundaries and unfinished follow-up calls; stored inputs remain unchanged.
  • Ruff lint/format and adapter mypy: pass. SDK and adapter wheel/sdist builds and all four Twine checks: pass. Wheel Python modules match source. Base/full installed-package smoke tests and dependency check pass. Adapter tests bundled in the sdist against its installed wheel: 396 passed, 1 skipped.
  • A bounded localhost HTTP fixture exercises public sync/async recording and history through the actual SDK and real Message.from_dict(...).to_dict() server serializer. It covers Responses text, role aliases, split zero-argument calls, repeated combined calls, reused IDs across and within user turns, late results after approval messages, identical polling calls with unchanged and changed outputs, changed combined outputs, and error/empty outputs. It passes with the built SDK/adapter wheels, and with released SDK 0.1.15 using the adapter's dictionary payloads. Typed SDK parts need the serializer correction included here.

The HTTP fixture is a controlled server boundary, not a deployed OpenViking backend or external model run. Authentication enforcement and performance are not claimed. Test suites emit deprecation and mock-coroutine warnings.

API/CLI CI at f33464bde failed in filesystem embedding setup with a Volcengine 401 AuthenticationError (The API key format is incorrect, HAS_SECRETS=false). Its LangChain/SDK matrices and other executed PR checks passed. Updated-head CI is reported separately. The branch merges cleanly with main b15fa86f5; its two newer fixes do not alter any files changed by this PR.

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

Screenshots (if applicable)

Not applicable.

Additional Notes

A malformed history that reuses an unresolved call ID with different arguments can still leave an earlier call unresolved. This change preserves recorded data and does not invent results to normalize such histories.

Original authorship is preserved with git cherry-pick -x:

Test relocation, serializer/retention corrections, and the additional role/restoration fixes are separate commits by Hao Zhe. The original PRs have not been closed or modified.

ktz03 and others added 5 commits October 19, 2026 15:35
(cherry picked from commit 83702c7)
Signed-off-by: Hao Zhe <[email protected]>
(cherry picked from commit cecca47)
Signed-off-by: Hao Zhe <[email protected]>
Motivation: OpenVikingChatMessageHistory dropped a completed ToolPart that
stores call id, name, input and output in one part. The part became a
ToolMessage, and restore_openviking_messages() discarded it because no
pending call had registered its id. A session holding only that part
restored as [] (no exception).

Approach: when a completed part has a tool_id and a nonempty tool_input,
openviking_message_to_langchain() also emits the tool call on the AIMessage.
restore_openviking_messages() drops calls whose id is already active from an
earlier pending call, so no duplicate call is added. Result-only parts (no
input) keep the existing orphan handling. A stored empty tool_input ({}) is
treated as no input and is not restored.

Validation (ran in a local venv with the SDK and integration installed):
- pytest integrations/frameworks/langchain/tests/unit: 182 passed, 1 skipped.
- The new test for a combined part fails without the src change and passes
  with it.
- ruff check and ruff format --check pass on src and tests.
Not run: a live OpenViking server or Hermes host; only the in-memory client.

Report: volcengine#6242
Signed-off-by: Pujitha Paladugu <[email protected]>
Assisted-by: claude-sonnet-5-5 (via Claude Code)
(cherry picked from commit b468e19)
Signed-off-by: Hao Zhe <[email protected]>
Honor explicit empty tool inputs, deduplicate calls within a stored message, and finish existing call results before emitting another assistant turn. Require recorded call identity when reconstructing a completed part; leave result-only orphans unchanged.

Extend the public sync/async history contracts for combined, split, and mixed tool records without mutating stored messages.

Signed-off-by: Hao Zhe <[email protected]>
@t0saki
t0saki merged commit b1d0fa5 into volcengine:main Oct 19, 2026
18 of 19 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in OpenViking project Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

3 participants