Skip to content

Vouch request: fix repeated inner compaction in chat.agent #4976

Description

@ggmathur

Why do you want to contribute?

I'd like to contribute a focused SDK fix and regression tests for repeated inner compaction in chat.agent.

In clean installs of @trigger.dev/sdk@4.6.3 and 4.6.4, shouldCompact is called at the first completed step, but not at any later inner boundary after the first successful summary. A long tool-using turn can therefore exceed its context budget even though compaction is configured.

The same early-return branch is present in the current main source: packages/trigger-sdk/src/v3/ai.ts, chatCompact. It rebuilds summary + response tail and returns before the shouldCompact evaluation.

I checked the contribution guide and am requesting vouching before opening a draft PR. Is this a fix you'd welcome, or is someone already working on it?

Reproduction

This is a standalone offline test using only the public SDK, its test harness and AI SDK mock model. No hosted project, API keys, network model calls, private application code, or customer data are required.

Environment: Node 24.12.0. In an empty directory, install:

npm init -y
npm install --save-exact @trigger.dev/sdk@4.6.4 ai@5.0.197 zod@3.25.76 vitest@4.1.11 msw@2.14.1
# Save the test below as repeat-compaction.test.ts
npx vitest run repeat-compaction.test.ts

The same test also fails with SDK 4.6.3. All five tool steps finish without errors, but the final assertion returns:

Expected: { innerChecks: [0, 1, 2, 3], summaries: 2 }
Received: { innerChecks: [0], summaries: 1 }

Reported input usage alternates between 90,000 and 100 solely to exercise the decision at multiple step boundaries; it does not represent real token consumption. Outer compaction is disabled by the callback so it cannot obscure the result.

Complete synthetic regression test
import { chat } from "@trigger.dev/sdk/ai";
import { mockChatAgent } from "@trigger.dev/sdk/ai/test";
import { MockLanguageModelV2, simulateReadableStream } from "ai/test";
import { tool } from "ai";
import { z } from "zod";
import { expect, it } from "vitest";

it("checks compaction again after the first inner summary", async () => {
  let steps = 0;
  let summaries = 0;
  const innerChecks: number[] = [];
  const model = new MockLanguageModelV2({
    doStream: async () => {
      const step = ++steps;
      const inputTokens = step % 2 ? 90_000 : 100;
      return {
        stream: simulateReadableStream({ chunks: [
          { type: "stream-start", warnings: [] },
          { type: "tool-call", toolCallId: "read-" + step, toolName: "read", input: "{}" },
          { type: "finish", finishReason: "tool-calls",
            usage: { inputTokens, outputTokens: 10, totalTokens: inputTokens + 10 } },
        ] }),
      };
    },
  });
  const agent = chat.agent({
    id: "repeat-compaction-repro",
    compaction: {
      shouldCompact: ({ source, stepNumber, inputTokens }) => {
        if (source !== "inner") return false;
        innerChecks.push(stepNumber!);
        return (inputTokens ?? 0) > 80_000;
      },
      summarize: async () => "Synthetic summary " + ++summaries,
    },
    tools: {
      read: tool({ inputSchema: z.object({}), execute: async () => ({ value: "synthetic" }) }),
    },
    run: ({ messages, tools, streamText, signal }) => streamText({
      model, messages, tools, abortSignal: signal, maxRetries: 0,
      stopWhen: () => steps >= 5,
    }),
    onBeforeTurnComplete: () => { chat.endRun(); },
  });
  const runtime = await mockChatAgent(agent);
  try {
    const turn = await runtime.sendMessage({
      id: "start", role: "user", parts: [{ type: "text", text: "Read five synthetic records." }],
    });
    expect(turn.chunks.filter(chunk => chunk.type === "error")).toEqual([]);
    expect(steps).toBe(5);
    expect({ innerChecks, summaries }).toEqual({ innerChecks: [0, 1, 2, 3], summaries: 2 });
  } finally {
    await runtime.close();
  }
});

Proposed contribution / relevant experience

I use long-running TypeScript agent sessions with tool execution and mid-turn steering. I have an AI-assisted candidate SDK workaround that passes this exact isolated test on 4.6.3, plus local regression coverage for ordered steering/background injections, repeated summaries, tool-call/result pairing, failed summaries and cold resume.

Before submitting an upstream implementation, I'd adapt it to the current TypeScript source and existing upstream tests, keeping this PR focused on the compaction lifecycle. In particular, the fix must preserve injected messages at their original boundaries, avoid repeated summarization using the same stale usage boundary, and not rehydrate the full visible transcript into compacted model history. Simply removing the early return is not sufficient to prove those invariants.

I'd follow the draft-PR, changeset, CodeRabbit and CI requirements. No application-specific prompts, tools, credentials or data would be included.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions