[PR #426] fix(deepagents): hydrate serialized summaries and preserve latest user turn #455

Open
opened 2026-06-05 17:23:09 -04:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/langchain-ai/deepagentsjs/pull/426
Author: @hnustwjj
Created: 4/5/2026
Status: 🔄 Open

Base: mainHead: codex/fix-summarization-hydration


📝 Commits (2)

  • f1e9acc fix(deepagents): preserve latest user turn in summarization
  • d6f2aa7 Merge branch 'main' into codex/fix-summarization-hydration

📊 Changes

3 files changed (+124 additions, -3 deletions)

View changed files

.changeset/fair-pigs-punch.md (+5 -0)
📝 libs/deepagents/src/middleware/summarization.test.ts (+63 -0)
📝 libs/deepagents/src/middleware/summarization.ts (+56 -3)

📄 Description

Why

createSummarizationMiddleware() had two coupled failures in persisted / resumed conversations:

  1. _summarizationEvent.summaryMessage was validated with z.instanceof(HumanMessage).
    That works in-memory, but it breaks after checkpoint persistence because the event is restored as a plain serialized object instead of the original class instance.
    On the next turn, replaying the summarization event could fail schema validation even though the payload still logically represented a HumanMessage.

  2. When summarization triggered on a turn whose newest message was a fresh user request, the cutoff could summarize away that newest user message.
    That meant the model answered the generated summary prompt instead of the user's active instruction.

In practice this caused two visible regressions:

  • resumed threads could error on the first follow-up turn after summarization
  • the compression-triggering turn could lose instruction fidelity because the latest user request had already been summarized away

What changed

This patch makes two targeted changes:

  • Accept serialized summary messages in _summarizationEvent and hydrate them back into a HumanMessage before reconstructing effective messages.
  • Clamp the summarization cutoff so the latest HumanMessage is preserved when summarization runs.

These changes preserve the existing summarization flow, backend offloading, and tool compaction behavior while fixing the persisted-state and active-turn regressions.

Minimal repro

1. Persisted summary replay fails without hydration

import { HumanMessage } from "@langchain/core/messages";
import { createSummarizationMiddleware } from "deepagents";

const middleware = createSummarizationMiddleware({
  model: "gpt-4o-mini",
  backend: {
    async write(path: string) {
      return { path };
    },
  },
  trigger: { type: "messages", value: 99 },
  keep: { type: "messages", value: 1 },
});

await middleware.wrapModelCall!(
  {
    messages: [
      new HumanMessage({ content: "old request" }),
      new HumanMessage({ content: "new request" }),
    ],
    state: {
      _summarizationEvent: {
        cutoffIndex: 1,
        summaryMessage: {
          content: "serialized summary",
          additional_kwargs: { lc_source: "summarization" },
          type: "human",
        },
        filePath: "/conversation_history/session_test.md",
      },
    },
  } as never,
  async (request) => {
    console.log(request.messages[0]);
    return { ok: true } as never;
  },
);

Before this fix, request.messages[0] could remain a plain object instead of a hydrated HumanMessage.
After this fix, it is always reconstructed as a HumanMessage before replay.

2. Latest user turn gets summarized away

import { HumanMessage } from "@langchain/core/messages";
import { createSummarizationMiddleware } from "deepagents";

const middleware = createSummarizationMiddleware({
  model: "gpt-4o-mini",
  backend: {
    async write(path: string) {
      return { path };
    },
  },
  trigger: { type: "messages", value: 2 },
  keep: { type: "messages", value: 0 },
});

await middleware.wrapModelCall!(
  {
    messages: [
      new HumanMessage({ content: "older context" }),
      new HumanMessage({ content: "Reply with exactly: received-1" }),
    ],
    state: {},
  } as never,
  async (request) => {
    console.log(request.messages.map((message) => message.content));
    return { ok: true } as never;
  },
);

Before this fix, the handler could receive only the generated summary message.
After this fix, it receives:

  • the generated summary message for older context
  • the latest user request unchanged

Tests

  • pnpm exec vitest run src/middleware/summarization.test.ts --config vitest.config.ts --typecheck.enabled false
  • pnpm --filter deepagents typecheck
  • pnpm --filter deepagents test:unit

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/langchain-ai/deepagentsjs/pull/426 **Author:** [@hnustwjj](https://github.com/hnustwjj) **Created:** 4/5/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `codex/fix-summarization-hydration` --- ### 📝 Commits (2) - [`f1e9acc`](https://github.com/langchain-ai/deepagentsjs/commit/f1e9acc844ae8253db3a15bd173dd1aef482055f) fix(deepagents): preserve latest user turn in summarization - [`d6f2aa7`](https://github.com/langchain-ai/deepagentsjs/commit/d6f2aa723145e8e70d5a906022d4de5fbf32b7ca) Merge branch 'main' into codex/fix-summarization-hydration ### 📊 Changes **3 files changed** (+124 additions, -3 deletions) <details> <summary>View changed files</summary> ➕ `.changeset/fair-pigs-punch.md` (+5 -0) 📝 `libs/deepagents/src/middleware/summarization.test.ts` (+63 -0) 📝 `libs/deepagents/src/middleware/summarization.ts` (+56 -3) </details> ### 📄 Description ## Why `createSummarizationMiddleware()` had two coupled failures in persisted / resumed conversations: 1. `_summarizationEvent.summaryMessage` was validated with `z.instanceof(HumanMessage)`. That works in-memory, but it breaks after checkpoint persistence because the event is restored as a plain serialized object instead of the original class instance. On the next turn, replaying the summarization event could fail schema validation even though the payload still logically represented a `HumanMessage`. 2. When summarization triggered on a turn whose newest message was a fresh user request, the cutoff could summarize away that newest user message. That meant the model answered the generated summary prompt instead of the user's active instruction. In practice this caused two visible regressions: - resumed threads could error on the first follow-up turn after summarization - the compression-triggering turn could lose instruction fidelity because the latest user request had already been summarized away ## What changed This patch makes two targeted changes: - Accept serialized summary messages in `_summarizationEvent` and hydrate them back into a `HumanMessage` before reconstructing effective messages. - Clamp the summarization cutoff so the latest `HumanMessage` is preserved when summarization runs. These changes preserve the existing summarization flow, backend offloading, and tool compaction behavior while fixing the persisted-state and active-turn regressions. ## Minimal repro ### 1. Persisted summary replay fails without hydration ```ts import { HumanMessage } from "@langchain/core/messages"; import { createSummarizationMiddleware } from "deepagents"; const middleware = createSummarizationMiddleware({ model: "gpt-4o-mini", backend: { async write(path: string) { return { path }; }, }, trigger: { type: "messages", value: 99 }, keep: { type: "messages", value: 1 }, }); await middleware.wrapModelCall!( { messages: [ new HumanMessage({ content: "old request" }), new HumanMessage({ content: "new request" }), ], state: { _summarizationEvent: { cutoffIndex: 1, summaryMessage: { content: "serialized summary", additional_kwargs: { lc_source: "summarization" }, type: "human", }, filePath: "/conversation_history/session_test.md", }, }, } as never, async (request) => { console.log(request.messages[0]); return { ok: true } as never; }, ); ``` Before this fix, `request.messages[0]` could remain a plain object instead of a hydrated `HumanMessage`. After this fix, it is always reconstructed as a `HumanMessage` before replay. ### 2. Latest user turn gets summarized away ```ts import { HumanMessage } from "@langchain/core/messages"; import { createSummarizationMiddleware } from "deepagents"; const middleware = createSummarizationMiddleware({ model: "gpt-4o-mini", backend: { async write(path: string) { return { path }; }, }, trigger: { type: "messages", value: 2 }, keep: { type: "messages", value: 0 }, }); await middleware.wrapModelCall!( { messages: [ new HumanMessage({ content: "older context" }), new HumanMessage({ content: "Reply with exactly: received-1" }), ], state: {}, } as never, async (request) => { console.log(request.messages.map((message) => message.content)); return { ok: true } as never; }, ); ``` Before this fix, the handler could receive only the generated summary message. After this fix, it receives: - the generated summary message for older context - the latest user request unchanged ## Tests - `pnpm exec vitest run src/middleware/summarization.test.ts --config vitest.config.ts --typecheck.enabled false` - `pnpm --filter deepagents typecheck` - `pnpm --filter deepagents test:unit` --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-06-05 17:23:09 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/deepagentsjs#455