Z ZPI 文档 v0.3.8
介绍页

源码

packages/coding-agent/test/compaction-boundaries.test.ts

typescript215 行查看原文
import {
  type AssistantMessage,
  createAssistantMessageEventStream,
  emptyAssistant,
  getCurrentSystemPrompt,
} from "@zpi-dev/ai";
import {
  createAgentSession,
  ModelRuntime,
  SessionManager,
  SettingsManager,
  StaticResourceLoader,
} from "@zpi-dev/coding-agent";
import { Type } from "typebox";
import { expect, it } from "vitest";
import { deferred, fakeConfig, fakeModel } from "../../../tests/fake-server.ts";

it.each([
  { kind: "empty", split: false, content: [], stopReason: "stop" },
  { kind: "whitespace", split: false, content: [{ type: "text", text: " \n\t " }], stopReason: "stop" },
  {
    kind: "thinking-only",
    split: false,
    content: [{ type: "thinking", thinking: "Need to summarize" }],
    stopReason: "stop",
  },
  {
    kind: "aborted",
    split: false,
    content: [{ type: "text", text: "Incomplete checkpoint" }],
    stopReason: "aborted",
  },
  { kind: "empty split-turn", split: true, content: [], stopReason: "stop" },
  {
    kind: "aborted split-turn",
    split: true,
    content: [{ type: "text", text: "Incomplete turn prefix" }],
    stopReason: "aborted",
  },
] satisfies {
  kind: string;
  split: boolean;
  content: AssistantMessage["content"];
  stopReason: AssistantMessage["stopReason"];
}[])(
  "rejects $kind summaries without replacing the active history",
  async ({ split, content, stopReason }) => {
    const model = fakeModel("");
    const manager = SessionManager.inMemory();
    manager.appendMessage({ role: "user", content: "Keep this original requirement", timestamp: 1 });
    manager.appendMessage({
      ...emptyAssistant(model),
      content: [{ type: "text", text: "Original answer" }],
      stopReason: "stop",
    });
    manager.appendMessage({ role: "user", content: "Keep this recent requirement", timestamp: 2 });
    if (split)
      manager.appendMessage({
        ...emptyAssistant(model),
        content: [{ type: "text", text: "Recent answer" }],
        stopReason: "stop",
      });
    let calls = 0;
    const runtime = await ModelRuntime.create();
    runtime.registerProvider("fake", {
      models: [fakeConfig("")],
      streamSimple(model) {
        calls++;
        const stream = createAssistantMessageEventStream();
        const badSummary = !split || calls === 2;
        stream.end({
          ...emptyAssistant(model),
          content: badSummary ? content : [{ type: "text", text: "Valid history checkpoint" }],
          stopReason: badSummary ? stopReason : "stop",
        });
        return stream;
      },
    });
    const { session } = await createAgentSession({
      modelRuntime: runtime,
      sessionManager: manager,
      settingsManager: SettingsManager.inMemory({
        compaction: { enabled: false, keepRecentTokens: 1 },
        retry: { enabled: false },
      }),
      resourceLoader: new StaticResourceLoader(),
      noTools: "all",
    });
    try {
      const before = session.messages.slice();
      await expect(session.compact()).rejects.toThrow(/empty summary|aborted/);
      expect(calls).toBe(split ? 2 : 1);
      expect(manager.getEntries().filter((entry) => entry.type === "compaction")).toHaveLength(0);
      expect(session.messages).toEqual(before);
      expect(manager.buildSessionContext().messages).toEqual(before);
      expect(session.isIdle).toBe(true);
    } finally {
      session.dispose();
    }
  },
);

it("uses transcript order for compaction when new replies have older timestamps", async () => {
  const runtime = await ModelRuntime.create();
  let turns = 0;
  runtime.registerProvider("fake", {
    models: [{ ...fakeConfig(""), contextWindow: 10000 }],
    streamSimple(model, context) {
      const summary = getCurrentSystemPrompt(context.messages).startsWith(
        "You are a context summarization assistant.",
      );
      const output = emptyAssistant(model);
      if (summary) output.content = [{ type: "text", text: "Completed tool turns" }];
      else {
        turns++;
        // Provider timestamps may be equal or move backwards; only transcript order defines the boundary.
        output.timestamp = 1;
        output.usage.input = output.usage.totalTokens = 100;
        output.content =
          turns <= 3
            ? [{ type: "toolCall", id: `large${turns}`, name: "large", arguments: {} }]
            : [{ type: "text", text: "done" }];
        output.stopReason = turns <= 3 ? "toolUse" : "stop";
      }
      const stream = createAssistantMessageEventStream();
      stream.end(output);
      return stream;
    },
  });
  const manager = SessionManager.inMemory();
  const { session } = await createAgentSession({
    modelRuntime: runtime,
    sessionManager: manager,
    settingsManager: SettingsManager.inMemory({
      compaction: { reserveTokens: 7000, keepRecentTokens: 10 },
      retry: { enabled: false },
    }),
    resourceLoader: new StaticResourceLoader(),
    noTools: "builtin",
    customTools: [
      {
        name: "large",
        label: "Large result",
        description: "Return a large local test result",
        parameters: Type.Object({}),
        execute: async () => ({ content: [{ type: "text", text: "x".repeat(20000) }], details: undefined }),
      },
    ],
  });
  try {
    await session.prompt("Collect three large results then answer");
    expect(turns).toBe(4);
    expect(manager.getEntries().filter((entry) => entry.type === "compaction")).toHaveLength(3);
    expect(session.messages).toEqual(manager.buildSessionContext().messages);
  } finally {
    session.dispose();
  }
});

it("declines steering during manual compaction instead of leaking it into the next prompt", async () => {
  const started = deferred();
  const release = deferred();
  const contexts: string[] = [];
  const runtime = await ModelRuntime.create();
  runtime.registerProvider("fake", {
    models: [fakeConfig("")],
    streamSimple(model, context) {
      const stream = createAssistantMessageEventStream();
      const output = {
        ...emptyAssistant(model),
        content: [{ type: "text" as const, text: "Complete summary or answer" }],
      };
      if (getCurrentSystemPrompt(context.messages).startsWith("You are a context summarization assistant.")) {
        started.resolve();
        void release.promise.then(() => stream.end(output));
      } else {
        contexts.push(JSON.stringify(context));
        stream.end(output);
      }
      return stream;
    },
  });
  const manager = SessionManager.inMemory();
  manager.appendMessage({ role: "user", content: "Original requirement", timestamp: 1 });
  manager.appendMessage({
    ...emptyAssistant(fakeModel("")),
    content: [{ type: "text", text: "Original answer" }],
  });
  manager.appendMessage({ role: "user", content: "Recent requirement", timestamp: 2 });
  const { session } = await createAgentSession({
    modelRuntime: runtime,
    sessionManager: manager,
    settingsManager: SettingsManager.inMemory({ compaction: { enabled: false, keepRecentTokens: 1 } }),
    resourceLoader: new StaticResourceLoader(),
    noTools: "all",
  });
  try {
    const compacting = session.compact();
    await started.promise;
    const accepted = session.steer("Stale guidance during manual compaction");
    release.resolve();
    await compacting;
    const prompting = session.prompt("Next task");
    expect(session.steer("Current prompt guidance")).toBe(true);
    await prompting;
    expect(accepted).toBe(false);
    expect(contexts).toHaveLength(2);
    expect(contexts[1]).toContain("Current prompt guidance");
    expect(contexts.join()).not.toContain("Stale guidance during manual compaction");
  } finally {
    release.resolve();
    session.dispose();
  }
});