Z ZPI 文档 v0.3.8
介绍页

源码

packages/coding-agent/test/cancelled-tool-durability.test.ts

typescript85 行查看原文
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { Agent } from "@zpi-dev/agent";
import { createAssistantMessageEventStream, emptyAssistant } from "@zpi-dev/ai";
import { projectStreamedToolJournal, SessionManager } from "@zpi-dev/coding-agent";
import { Type } from "typebox";
import { expect, it } from "vitest";
import { fakeModel } from "../../../tests/fake-server.ts";
import { StreamingToolJournal } from "../src/core/streaming-tool-journal.ts";
import { directory } from "./helpers/session-fixture.ts";

for (const mode of ["sequential", "parallel"] as const) {
  it(`durably closes every declared call after cancellation during ${mode} execution`, async () => {
    const cwd = await directory();
    const manager = SessionManager.create(cwd, join(cwd, "sessions"));
    const journal = new StreamingToolJournal(manager, () => {});
    const model = fakeModel("http://localhost");
    const executed: string[] = [];
    const preflighted: string[] = [];
    const after: string[] = [];
    let agent: Agent;
    agent = new Agent({
      initialState: {
        model,
        tools: [
          {
            name: "inert",
            label: "Inert test tool",
            description: "Records calls without external side effects",
            parameters: Type.Object({}),
            async execute(id) {
              executed.push(id);
              // Stop while the first sequential call settles, as with a dismissed question.
              agent.abort();
              return { content: [{ type: "text", text: "first completed" }], details: undefined };
            },
          },
        ],
      },
      toolExecution: mode,
      beforeToolCall: async ({ toolCall }) => {
        preflighted.push(toolCall.id);
        // In parallel mode, stop during ordered preparation, before any handler starts.
        if (mode === "parallel") agent.abort();
      },
      afterToolCall: async ({ toolCall }) => {
        after.push(toolCall.id);
      },
      streamFn: () => {
        const stream = createAssistantMessageEventStream();
        const message = emptyAssistant(model);
        message.content = ["first", "second", "third"].map((id) => ({
          type: "toolCall",
          id,
          name: "inert",
          arguments: {},
        }));
        message.stopReason = "toolUse";
        stream.push({ type: "start", partial: message });
        stream.push({ type: "done", reason: "toolUse", message });
        return stream;
      },
    });
    // Exercise the same journal-before-canonical writer order as AgentSession.
    agent.subscribe(async (event) => {
      await journal.observe(event);
      if (event.type === "message_end") manager.appendMessage(event.message);
    });
    await agent.prompt("run the inert batch");
    expect(executed).toEqual(mode === "sequential" ? ["first"] : []);
    expect(preflighted).toEqual(["first"]);
    expect(after).toEqual(mode === "sequential" ? ["first"] : []);
    const messages = manager.buildSessionContext().messages;
    const results = messages.filter((message) => message.role === "toolResult");
    expect(results.map((result) => result.toolCallId)).toEqual(["first", "second", "third"]);
    expect(results.slice(mode === "sequential" ? 1 : 0).every((result) => result.isError)).toBe(true);
    expect(manager.getLastEntry()).toMatchObject({ data: { committed: true } });
    expect(projectStreamedToolJournal(manager.getEntries())).toEqual(manager.getEntries());
    const path = manager.getSessionFile() as string;
    const before = await readFile(path, "utf8");
    expect(SessionManager.open(path).buildSessionContext().messages).toEqual(messages);
    expect(await readFile(path, "utf8")).toBe(before);
  });
}