Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/test/subagent-prompts.test.ts

typescript226 行查看原文
import { readFileSync } from "node:fs";
import { prepareTaskOutputArguments } from "@zpi-dev/builtin-plugins/tasks/controls";
import { SessionManager } from "@zpi-dev/coding-agent";
import { expect, test } from "vitest";
import { chunk, done, fakeModel, send } from "../../../tests/fake-server.ts";
import { SessionHost } from "../src/main/session-host.ts";
import { SettingsStore } from "../src/main/storage.ts";
import { cleanup, codec, fixture, run } from "./helpers/context-fixture.ts";

// Captured from the checked-out ZCode provider descriptions and subagent prompt builders.
const reference = JSON.parse(
  readFileSync(new URL("./fixtures/zcode-subagents.json", import.meta.url), "utf8"),
) as Record<string, string>;
const schemas = JSON.parse(
  readFileSync(new URL("./fixtures/zcode-subagent-schemas.json", import.meta.url), "utf8"),
) as Record<string, Record<string, unknown>>;
const adapt = (text: string) =>
  text
    .replaceAll("ZCode CLI", "ZPI")
    .replaceAll("ZCode Explore", "ZPI Explore")
    .replaceAll(".zcode/agents/", ".zpi/agents/")
    .replaceAll("Use Read", "Use read")
    .replaceAll("Use Bash ONLY", "Use bash ONLY")
    .replaceAll("Write report/", "write report/");

test.each(["general-purpose", "Explore"].flatMap((name) => [false, true].map((codex) => ({ name, codex }))))(
  "sends the $name child prompt with Codex guidance=$codex to the model",
  async ({ name, codex }) => {
    const f = await fixture((body, response) => {
      const tools = body.tools as { function: { name: string } }[];
      const rows = body.messages as { role: string; content: string }[];
      if (tools.some((tool) => tool.function.name === "Agent") && !rows.some((row) => row.role === "tool")) {
        send(
          response,
          chunk({
            tool_calls: [
              {
                index: 0,
                id: "launch",
                type: "function",
                function: {
                  name: "Agent",
                  arguments: JSON.stringify({
                    description: "Inspect source",
                    prompt: "Search",
                    subagent_type: name,
                  }),
                },
              },
            ],
          }),
        );
        done(response, "tool_calls");
      } else {
        send(response, chunk({ content: "Findings" }));
        done(response);
      }
    });
    f.settings.setPluginOptions("zpi.collaboration", { promptPreset: codex ? "codex" : "default" });
    await run(f, "Use a subagent to research");
    const parent = f.server.requests[0];
    const parentInstructions = (parent.messages as { role: string; content: string }[])
      .filter((row) => ["system", "developer"].includes(row.role))
      .map((row) => row.content)
      .join("\n");
    expect(parentInstructions).not.toContain(adapt(reference.agent));
    expect(parentInstructions).not.toContain("- Agent:");
    const task = f.host.plugins.collaboration.list(f.session.id)[0];
    const child = f.server.requests.find(
      (request) =>
        !(request.tools as { function: { name: string } }[]).some((tool) => tool.function.name === "Agent"),
    );
    if (!child) throw new Error("Missing child request");
    const system = (child.messages as { role: string; content: string }[])
      .filter((row) => ["system", "developer"].includes(row.role))
      .map((row) => row.content)
      .join("\n");
    expect(system).toContain(adapt(reference[name === "Explore" ? "explore" : "generalPurpose"]));
    expect(system).toContain(adapt(reference.commonNotes));
    const parentTools = (
      parent.tools as {
        function: { name: string; description: string; parameters: Record<string, unknown> };
      }[]
    ).map((entry) => entry.function);
    const description = parentTools.find((entry) => entry.name === "Agent")?.description ?? "";
    if (codex) {
      expect(description).toContain(
        "Do not launch subagents unless the user or applicable AGENTS.md/skill instructions explicitly ask",
      );
      expect(description).toContain(
        "Requests for depth, thoroughness, research, investigation, or detailed codebase analysis do not count as permission",
      );
      expect(description).toContain("Do not delegate urgent blocking work");
      expect(description).toContain("Do not duplicate work");
      expect(description).toContain("do meaningful non-overlapping work immediately");
      for (const capability of [
        "<system-reminder>",
        "run_in_background: true",
        "subagent_type",
        ".zpi/agents/*.md",
        "SendMessage",
        "TaskOutput",
        "agents share the workspace",
      ])
        expect(description).toContain(capability);
      expect(description).not.toContain("forked workspace");
      expect(parentInstructions).toContain("Handles self-contained implementation, debugging, and research");
      expect(parentInstructions).toContain("Read-only codebase exploration for a bounded question");
      expect(parentInstructions).not.toContain("not confident that you will find the right match");
    } else {
      expect(description).toBe(adapt(reference.agent));
      expect(parentInstructions).toContain("not confident that you will find the right match");
      expect(parentInstructions).toContain("when answering means sweeping many files");
    }
    for (const [tool, key] of [
      ["SendMessage", "sendMessage"],
      ["TaskStop", "taskStop"],
      ["TaskOutput", "taskOutput"],
    ])
      expect(parentTools.find((entry) => entry.name === tool)?.description).toBe(adapt(reference[key]));
    for (const [name, { $schema: _version, ...schema }] of Object.entries(schemas)) {
      if (name !== "RespondToCoordinator")
        expect(parentTools.find((entry) => entry.name === name)?.parameters).toEqual(schema);
    }
    const childTools = (
      child.tools as {
        function: { name: string; description: string; parameters: Record<string, unknown> };
      }[]
    ).map((entry) => entry.function);
    expect(childTools.find((entry) => entry.name === "RespondToCoordinator")?.description).toBe(
      reference.respondToCoordinator,
    );
    const { $schema: _version, ...responseSchema } = schemas.RespondToCoordinator;
    expect(childTools.find((entry) => entry.name === "RespondToCoordinator")?.parameters).toEqual(
      responseSchema,
    );
    expect(JSON.stringify(parent.messages)).toContain(reference.concurrency);
    expect(JSON.stringify(parent.messages)).toContain("<system-reminder>");
    expect(parentTools.find((entry) => entry.name === "TaskOutput")?.parameters.required).toEqual([
      "task_id",
      "block",
      "timeout",
    ]);
    const agentSchema = parentTools.find((entry) => entry.name === "Agent")?.parameters;
    expect(agentSchema?.required).toEqual(["description", "prompt"]);
    const rows = f.server.requests.at(-1)?.messages as { role: string; content: string }[];
    expect(rows.find((row) => row.role === "tool")?.content).toBe(
      `Findings\nagentId: ${task.agentId} (use SendMessage with to: '${task.agentId}' to continue this agent)\n<usage>tool_uses: ${task.totalToolUseCount}\nduration_ms: ${(task.endedAt as number) - task.startedAt}</usage>`,
    );
    const context = {
      cwd: f.workspace,
      model: fakeModel(""),
      sessionManager: SessionManager.inMemory(),
      isIdle: () => true,
      getSystemPrompt: () => "",
    };
    const reply = await f.host.plugins.collaboration
      .childTools(task.childSessionId)[0]
      .execute("reply", { summary: "Progress", message: "Done" }, undefined, undefined, context);
    expect(reply.content).toEqual([
      {
        type: "text",
        text: expect.stringMatching(
          /^Response response_[\w-]+ was queued for the coordinator\. Continue the current task unless the coordinator explicitly changed or ended it\.$/,
        ),
      },
    ]);
  },
);

test("TaskOutput applies ZCode runtime defaults and semantic booleans without accepting invalid nulls", () => {
  expect(prepareTaskOutputArguments({ task_id: "id" })).toEqual({
    task_id: "id",
    block: true,
    timeout: 30000,
  });
  expect(prepareTaskOutputArguments({ task_id: "id", block: "false", timeout: 0 })).toEqual({
    task_id: "id",
    block: false,
    timeout: 0,
  });
  expect(prepareTaskOutputArguments({ task_id: "id", timeout: null })).toMatchObject({ timeout: null });
});

test("persists Codex guidance and snapshots its mode on first send, including restored sessions", async () => {
  const f = await fixture();
  const description = () => {
    const tools = f.server.requests.at(-1)?.tools as { function: { name: string; description: string } }[];
    return tools.find((tool) => tool.function.name === "Agent")?.function.description ?? "";
  };
  const codexRule = "Do not launch subagents unless";
  expect(f.settings.get().plugins.options["zpi.collaboration"]?.promptPreset === "codex").toBe(false);
  await run(f, "original chat");
  expect(description()).toBe(adapt(reference.agent));
  const draft = await f.host.createSession(null);
  f.settings.setPluginOptions("zpi.collaboration", { promptPreset: "codex" });
  expect(f.host.listTools().find((tool) => tool.name === "Agent")?.description).toContain(codexRule);
  expect((await f.host.previewPrompt()).prompt).toContain(
    "Read-only codebase exploration for a bounded question",
  );
  await f.host.startRun({ sessionId: draft.id, text: "first send after enabling" });
  await f.host.activeRuns.get(draft.id)?.done;
  expect(description()).toContain(codexRule);
  f.settings.setPluginOptions("zpi.collaboration", { promptPreset: "default" });
  await f.host.startRun({ sessionId: draft.id, text: "keep Codex guidance" });
  await f.host.activeRuns.get(draft.id)?.done;
  expect(description()).toContain(codexRule);
  await f.host.close();
  const settings = new SettingsStore(f.dir, codec);
  expect(settings.get().plugins.options["zpi.collaboration"]?.promptPreset === "codex").toBe(false);
  settings.setPluginOptions("zpi.collaboration", { promptPreset: "codex" });
  const host = new SessionHost(f.dir, settings, f.resources, f.workspace, undefined);
  await host.init();
  cleanup.push(() => host.close());
  for (const [sessionId, codex] of [
    [f.session.id, false],
    [draft.id, true],
  ] as const) {
    await host.startRun({ sessionId, text: "continue after restart" });
    await host.activeRuns.get(sessionId)?.done;
    expect(description().includes(codexRule)).toBe(codex);
    const messages = JSON.stringify(f.server.requests.at(-1)?.messages);
    expect(messages.includes("not confident that you will find the right match")).toBe(!codex);
  }
});