Z ZPI 文档 v0.3.8
介绍页

源码

packages/builtin-plugins/src/runtime.ts

typescript622 行查看原文
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { isJsonObject } from "@zpi-dev/ai";
import type { AgentSessionEvent, PluginSnapshot, ResourceLoader } from "@zpi-dev/coding-agent";
import {
  type BuiltinPluginBindings,
  collaborationEnabled,
  type PluginContext,
  pluginsForSubagent,
  resolveBuiltinPlugins,
  validateBuiltinPluginSnapshots,
} from "./assembly.ts";
import { BackgroundTasks } from "./background/tasks.ts";
import { CollaborationInbox } from "./collaboration/inbox.ts";
import { SubagentProfiles } from "./collaboration/profiles.ts";
import { SubagentPlugin } from "./collaboration/runtime.ts";
import type { SubagentProfile } from "./collaboration/types.ts";
import { desktopPluginIds, enabledBuiltinPlugins } from "./index.ts";
import { LongMemoryService, type LongMemoryState } from "./memory/long-memory.ts";
import { MemoryPolicy } from "./memory/policy.ts";
import { readShortMemory } from "./memory/short-memory.ts";
import { memorySources } from "./memory/sources.ts";
import type { MemoryStatus } from "./memory/types.ts";
import { NotificationsPlugin } from "./notifications.ts";
import type {
  BuiltinPluginPorts,
  PluginConfiguration,
  PluginInput,
  PluginRun,
  PluginRuntimeState,
} from "./ports.ts";
import { UserQuestions } from "./questions.ts";
import { SdkTaskControl } from "./tasks/sdk-control.ts";
import { TitlePolicy } from "./title/policy.ts";
import type { BackgroundTask, SubagentTask } from "./types.ts";
import { WebTools } from "./web/tools.ts";

export type { BuiltinPluginPorts, PluginConfiguration, PluginRun } from "./ports.ts";

interface Continuation {
  input: PluginInput;
  childAgentId?: string;
}

/** One owner for builtin policies, tool services, recovery and shutdown. */
export class BuiltinPluginRuntime {
  readonly memory: MemoryPolicy;
  readonly longMemory: LongMemoryService;
  readonly titles: TitlePolicy;
  readonly notifications: NotificationsPlugin;
  readonly questions: UserQuestions;
  readonly background: BackgroundTasks;
  readonly collaboration: SubagentPlugin;
  readonly inbox: CollaborationInbox;
  readonly taskControl: SdkTaskControl;
  private readonly parentInstructions = new Map<string, string[]>();
  private readonly web: WebTools;
  private readonly ports: BuiltinPluginPorts;

  constructor(ports: BuiltinPluginPorts) {
    this.ports = ports;
    const sessions = ports.sessions;
    this.longMemory = new LongMemoryService({
      directory: join(ports.directory, "memories_v2"),
      getState: () => ports.storage.getSetting<LongMemoryState>("memory.state"),
      setState: (value) => ports.storage.setSetting("memory.state", value),
      sources: async () =>
        memorySources({
          directory: ports.directory,
          records: () => sessions.records(),
          index: (id) => sessions.history(id),
          cwd: (record) => sessions.cwd(record.id),
          blocked: (id) => sessions.blocked(id),
          active: (id) => sessions.changing(id),
          exists: (id) => !!sessions.get(id),
          deleting: (id) => sessions.deleting(id),
        }),
      run: (request) => this.memory.run(request),
    });
    this.longMemory.configure(ports.memorySettings());
    this.memory = new MemoryPolicy({
      settings: ports.memorySettings,
      selection: (id) => ports.models.selection(id),
      model: (selection) => ports.models.create(selection),
      closing: ports.closing,
      error: (error) => ports.error("memory.check", error),
      homeDir: ports.homeDirectory,
      longMemory: this.longMemory,
      context: (id) => {
        const record = sessions.get(id);
        const config = sessions.configuration(id);
        return {
          cwd: sessions.cwd(id),
          workspace: record?.projectId ? sessions.cwd(id) : undefined,
          child: !!config.subagentProfile,
          parentId: record?.parentSessionId,
          memoryInstructions: config.memoryInstructions,
          memoryShortInstructions: config.memoryShortInstructions,
        };
      },
      updateInheritance: (id, value) => sessions.configure(id, value),
    });
    this.titles = new TitlePolicy({
      state: (id) => sessions.state(id),
      save: (id, patch) => sessions.update(id, patch),
      changed: ports.titleChanged,
      alive: (id) => !ports.closing() && !sessions.deleting(id) && !!sessions.get(id),
      error: (error, id, model) =>
        ports.error("session.title", error, { sessionId: id, provider: model.provider, model: model.id }),
    });
    this.notifications = new NotificationsPlugin(ports);
    this.questions = new UserQuestions((id, question) =>
      ports.publish(id, "questions", { type: "question_changed", question }),
    );
    this.web = new WebTools({ getApiKey: () => ports.credentials("webSearch"), requestFetch: ports.fetch });
    this.inbox = new CollaborationInbox(ports, (id) => !!this.collaboration.taskForChild(id));
    this.background = new BackgroundTasks(ports.directory, {
      publish: (task) => {
        if (!sessions.get(task.sessionId)) return;
        this.taskControl?.publish(task);
        ports.storage.setRuntime(task.sessionId, "background", task.taskId, task, task.status);
        ports.publish(task.sessionId, "background", { type: "background_task_changed", task });
        const parentId = sessions.get(task.sessionId)?.parentSessionId;
        if (parentId && this.collaboration.taskForChild(task.sessionId))
          ports.publish(parentId, "background", { type: "background_task_changed", task });
      },
      notify: async (task) => {
        const active = sessions.active(task.sessionId);
        if (active?.aborted) await active.done;
        if (ports.closing() || !sessions.get(task.sessionId) || sessions.closed(task.sessionId)) return;
        const child = this.collaboration.taskForChild(task.sessionId);
        if (child && child.status !== "running") return;
        await this.inbox.deliver(
          {
            parentSessionId: task.sessionId,
            agentId: task.taskId,
            agentType: "bash",
            description: task.description,
            status: task.status === "completed" ? "completed" : "error",
            result: `Background command ${task.status}. Exit code: ${task.exitCode ?? "unknown"}.\nOutput file: ${task.outputPath}\nUse TaskOutput with task_id ${task.taskId} if the output is needed.`,
          },
          undefined,
          true,
        );
      },
      error: (error) => ports.error("background.task", error),
    });
    this.collaboration = new SubagentPlugin(
      new SubagentProfiles(ports.directory, ports.storage),
      {
        cwd: (id) => sessions.cwd(id),
        selection: (id) => {
          const selection = ports.models.selection(id);
          if (!selection) throw new Error("configuration: 请先选择模型");
          return selection;
        },
        create: async (id, profile, selection, description) => {
          if (!ports.models.valid(selection))
            throw new Error("configuration: 子智能体模型不可用,请在设置中重新选择");
          if (sessions.closed(id) || sessions.active(id)?.aborted)
            throw new Error("busy: 父会话已停止或关闭");
          const child = await sessions.createChild(id);
          try {
            const parent = sessions.configuration(id);
            sessions.configure(child, {
              ...structuredClone(parent),
              plugins: pluginsForSubagent(parent.plugins),
              subagentProfile: structuredClone(profile),
              subagentInstructions: structuredClone(this.parentInstructions.get(id) ?? []),
              ...this.memory.inherit(child, id),
            });
            sessions.select(child, selection);
            sessions.update(child, { titleState: "manual", title: description, draft: false });
            return child;
          } catch (error) {
            await sessions.delete(child);
            throw error;
          }
        },
        run: async (id, prompt, profile, selection) => {
          if (profile && selection) {
            if (sessions.get(id)?.archivedAt != null) throw new Error("busy: 子智能体已归档");
            if (!ports.models.valid(selection)) throw new Error("configuration: 子智能体模型不可用");
            sessions.configure(id, {
              subagentProfile: structuredClone(profile),
              subagentInstructions:
                this.parentInstructions.get(sessions.get(id)?.parentSessionId ?? "") ?? [],
            });
            sessions.select(id, selection);
          }
          this.memory.refreshChild(id);
          await sessions.run({ sessionId: id, text: prompt }, true);
          while (sessions.active(id)) await sessions.active(id)?.done;
        },
        stop: async (id) => {
          if (sessions.active(id)) await sessions.stop(id);
          else await this.background.stopAll(id);
        },
        steer: (id, text) => this.inbox.guidance(id, text),
        publish: (task) => {
          if (!sessions.get(task.childSessionId) || !sessions.get(task.parentSessionId)) return;
          this.taskControl?.publish(task);
          ports.storage.setRuntime(task.childSessionId, "subagent", task.agentId, task, task.status);
          ports.publish(task.parentSessionId, "subagents", {
            type: "subagent_changed",
            task: structuredClone(task),
          });
        },
        notify: (task, message) => this.inbox.deliver(task, message),
      },
      join(ports.directory, "agent", "subagents"),
    );
    this.taskControl = new SdkTaskControl(
      [this.background.taskBackend(), this.collaboration.taskBackend()],
      (id) => [...this.background.list(id), ...this.collaboration.list(id)],
    );
  }

  snapshots(settings: Parameters<typeof enabledBuiltinPlugins>[0]): PluginSnapshot[] {
    return enabledBuiltinPlugins(settings).filter((plugin) => {
      if (plugin.id !== desktopPluginIds.web) return true;
      try {
        return !!this.ports.credentials("webSearch");
      } catch {
        return false;
      }
    });
  }
  validate(snapshots: readonly PluginSnapshot[], external?: (snapshot: PluginSnapshot) => void): void {
    validateBuiltinPluginSnapshots(snapshots, external);
  }
  private questionTool(id: string) {
    const parentId = this.ports.sessions.configuration(id).subagentProfile
      ? this.ports.sessions.get(id)?.parentSessionId
      : undefined;
    const tool = this.questions.tool(parentId ?? id);
    if (!parentId) return tool;
    return {
      ...tool,
      execute: async (...args: Parameters<typeof tool.execute>) => {
        const task = this.collaboration.taskForChild(id);
        if (task) this.collaboration.update(task.agentId, { waitingForInput: true });
        try {
          return await tool.execute(...args);
        } finally {
          if (task) this.collaboration.update(task.agentId, { waitingForInput: false });
        }
      },
    };
  }

  contributions(
    snapshots: readonly PluginSnapshot[],
    context: PluginContext,
    external: NonNullable<BuiltinPluginBindings["external"]> & { agents(): SubagentProfile[] },
  ) {
    return resolveBuiltinPlugins(snapshots, context, {
      external,
      memory: {
        prepare: (id, mode, signal) => this.memory.prepare(id, mode, signal),
        instructions: (id) => this.memory.instructions(id),
        read: (id, path) => this.memory.recordRead(id, path),
        schedule: (id) => {
          if (!this.ports.sessions.get(id)?.parentSessionId) this.memory.schedule(id);
        },
        settled: (id) => this.memory.settled(id),
      },
      titles: {
        fallback: (id, text) => this.titles.fallback(id, text),
        start: (id) => {
          const input = this.ports.sessions.active(id)?.modelInput;
          if (input) this.titles.start(id, input);
        },
      },
      notifications: this.notifications,
      questionTool: (id) => (context.preview ? this.questions.tool(id) : this.questionTool(id)),
      webTools: () => this.web.tools(),
      background: this.background,
      collaboration: {
        tools: (id, codex) => this.collaboration.tools(id, codex),
        childTools: (id) => this.collaboration.childTools(id),
        taskBackend: () => this.collaboration.taskBackend(),
        prepare: (id, codex) => this.collaboration.prepare(id, codex, external.agents()),
        listing: (id) => this.collaboration.listing(id),
        previewListing: (cwd, codex) => this.collaboration.previewListing(cwd, codex, external.agents()),
      },
    });
  }

  async getMemoryStatus(id: string | null): Promise<MemoryStatus> {
    const record = id ? this.requiredRecord(id) : undefined;
    await this.longMemory.init();
    const short = await readShortMemory({
      homeDir: this.ports.homeDirectory,
      workspace: record?.projectId ? this.ports.sessions.cwd(record.id) : undefined,
    });
    return {
      shortFiles: short.files,
      long: this.longMemory.status(),
      usedSources: id ? this.memory.usedSources(id) : [],
    };
  }
  async checkMemoryNow(id: string | null): Promise<MemoryStatus> {
    if (id) this.requiredRecord(id);
    this.memory.schedule(id);
    return this.getMemoryStatus(id);
  }
  async clearLongMemory(): Promise<MemoryStatus> {
    await this.longMemory.clear();
    await this.memory.wait();
    // Withdraw inherited long-memory copies too. Active model requests stay stable; subsequent
    // child prompts read this owner again instead of a stale createAgentSession configuration closure.
    this.memory.withdraw(
      [...this.ports.sessions.records()]
        .filter((record) => record.parentSessionId && !this.ports.sessions.blocked(record.id))
        .map((record) => record.id),
    );
    return this.getMemoryStatus(null);
  }
  async openMemoryFile(
    id: string | null,
    scope: string,
    action: string,
  ): Promise<{ path: string; content: string; exists: boolean }> {
    if (!["user", "project", "long"].includes(scope) || !["view", "edit"].includes(action))
      throw new Error("invalid_input: 记忆文件操作无效");
    const record = id ? this.requiredRecord(id) : undefined;
    if (scope === "project" && !record?.projectId) throw new Error("invalid_input: 当前对话没有项目");
    if (scope === "long" && action === "edit") throw new Error("invalid_input: 长期记忆由后台维护");
    const path =
      scope === "user"
        ? join(this.ports.homeDirectory, ".zpi", "MEMORY.md")
        : scope === "project" && record
          ? join(this.ports.sessions.cwd(record.id), ".zpi", "MEMORY.md")
          : join(this.longMemory.status().path, "memory_summary.md");
    if (action === "edit") {
      await mkdir(join(path, ".."), { recursive: true });
      try {
        await writeFile(path, "# Memory\n", { flag: "wx", mode: 0o600 });
      } catch (error) {
        if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
      }
    }
    try {
      const info = await stat(path);
      if (!info.isFile()) throw new Error("invalid_input: 记忆路径必须是普通文件");
      if (scope === "long" || (action === "edit" && info.size > 2 * 1024 * 1024))
        return { path, content: "", exists: true };
      if (info.size > 2 * 1024 * 1024)
        throw new Error("invalid_input: 文件超过 2 MB 预览限制,请使用文件面板或外部编辑器");
      const preview = await this.ports.files.preview(id, path);
      if (preview.kind !== "text") throw new Error("invalid_input: 记忆文件必须是可读文本");
      return { path, content: preview.text ?? "", exists: true };
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === "ENOENT") return { path, content: "", exists: false };
      throw error;
    }
  }
  resourceLoader(id: string | null, config: PluginConfiguration, loader: Required<ResourceLoader>) {
    return {
      listSkills: () => loader.listSkills(),
      loadSkill: (name: string) => loader.loadSkill(name),
      getDiagnostics: () => [...loader.getDiagnostics(), ...(id ? this.memory.diagnostics(id) : [])],
      reload: async () => {
        await loader.reload();
        if (id && !config.subagentProfile)
          this.parentInstructions.set(
            id,
            loader.getInstructions().filter((part) => part.startsWith("<project_context")),
          );
      },
      getBaseInstructions: () => config.subagentProfile?.systemPrompt ?? loader.getBaseInstructions(),
      getInstructions: () =>
        loader
          .getInstructions()
          .filter((part) => !config.subagentProfile || !part.startsWith("<project_context")),
    };
  }
  customInstructions(config: PluginConfiguration): string[] {
    return config.subagentProfile?.injectAgentsMd ? [...(config.subagentInstructions ?? [])] : [];
  }
  observe(id: string, run: PluginRun, e: AgentSessionEvent): void {
    if (e.type === "agent_start") this.inbox.offer(id, run);
    if (e.type === "tool_execution_start" || e.type === "tool_execution_end") this.inbox.offer(id, run, true);
    if (e.type === "message_end" && e.message.role === "user")
      this.inbox.acknowledge(id, e.message.content, e.message.inputId);
    const childTask = this.collaboration.taskForChild(id);
    if (childTask && e.type === "tool_execution_start")
      this.collaboration.update(childTask.agentId, {
        latestActivity: `${e.toolName} · ${isJsonObject(e.args) ? String(e.args.path ?? e.args.command ?? "").slice(0, 500) : ""}`,
        totalToolUseCount: childTask.totalToolUseCount + 1,
      });
    if (childTask && e.type === "message_end" && e.message.role === "assistant")
      if (e.message.usageAvailable || e.message.usage.totalTokens > 0) {
        const usage = e.message.usage;
        const delta: Record<string, number> = {
          inputTokens: usage.input,
          outputTokens: usage.output,
          totalTokens: usage.totalTokens,
          cacheReadTokens: usage.cacheRead,
          cacheWriteTokens: usage.cacheWrite,
          ...(usage.reasoning === undefined ? {} : { reasoningTokens: usage.reasoning }),
        };
        this.collaboration.update(childTask.agentId, {
          totalTokens: childTask.totalTokens + usage.totalTokens,
          tokensReported: true,
          usage: Object.fromEntries(
            Object.entries(delta).map(([key, value]) => [key, (childTask.usage?.[key] ?? 0) + value]),
          ),
        });
      }
  }
  settle(
    id: string,
    run: PluginRun,
    output: { status: SubagentTask["status"]; endedAt: number; message?: string; result?: string },
    storageFailure: (error: unknown, source: string) => string,
  ): Continuation | undefined {
    const inbox = this.inbox.followUp(id);
    const continueInbox =
      inbox.length > 0 &&
      !run.aborted &&
      !this.ports.closing() &&
      !this.ports.sessions.deleting(id) &&
      !this.ports.sessions.blocked(id);
    const childTask = this.collaboration.taskForChild(id);
    if (childTask) {
      const result = output.result;
      try {
        this.collaboration.update(childTask.agentId, {
          status: continueInbox ? "running" : output.status,
          endedAt: continueInbox ? undefined : output.endedAt,
          error: output.message,
          result: continueInbox ? undefined : result,
          waitingForInput: false,
        });
      } catch (error) {
        const message = storageFailure(error, "subagent.persist");
        try {
          this.collaboration.update(childTask.agentId, {
            status: "error",
            error: message,
            endedAt: output.endedAt,
          });
        } catch {
          // The in-memory terminal owner was updated before publication failed.
        }
      }
    }
    return continueInbox ? { input: inbox[0], childAgentId: childTask?.agentId } : undefined;
  }
  continueRun(id: string, continuation?: Continuation): void {
    if (!continuation || this.ports.sessions.blocked(id)) return;
    const { input, childAgentId } = continuation;
    this.inbox.acknowledge(id, input.text);
    void this.ports.sessions.run(input, childAgentId !== undefined).catch((error) => {
      this.ports.error("subagent.delivery", error, { sessionId: id });
      if (childAgentId !== undefined)
        this.collaboration.update(childAgentId, {
          status: "error",
          error: String(error),
          endedAt: Date.now(),
        });
    });
  }
  async stopSession(id: string): Promise<void> {
    await Promise.all([
      this.collaboration.stopAll(id),
      ...(this.collaboration.taskForChild(id) ? [this.background.stopAll(id)] : []),
    ]);
  }
  async beforeHistoryChange(id: string): Promise<void> {
    this.titles.cancel(id);
    await this.titles.wait(id);
  }
  async historyChanged(id: string): Promise<void> {
    await this.longMemory.invalidateSource(id);
  }
  async archive(id: string): Promise<void> {
    await this.background.stopAll(id);
  }
  async configure(): Promise<void> {
    await this.memory.configure(this.ports.memorySettings());
  }
  async beforeDelete(id: string): Promise<void> {
    this.titles.cancel(id);
    await this.longMemory.invalidateSource(id);
    await this.background.stopAll(id);
  }
  forget(id: string): void {
    this.collaboration.forgetSession(id);
    this.background.forget(id);
    this.inbox.forget(id);
    this.parentInstructions.delete(id);
    this.memory.forget(id);
  }
  started(id: string, modelLabel: string): void {
    const task = this.collaboration.taskForChild(id);
    if (task) this.collaboration.update(task.agentId, { modelLabel });
  }
  view(id: string) {
    const subagents = this.collaboration.list(id);
    return {
      question: this.questions.get(id),
      subagents,
      backgroundTasks: [
        ...this.background.list(id),
        ...subagents.flatMap((task) => this.background.list(task.childSessionId)),
      ],
    };
  }
  listSubagents(id: string | null, imported: SubagentProfile[]) {
    const catalog = this.collaboration.profiles.list(id ? this.ports.sessions.cwd(id) : undefined);
    return { ...catalog, agents: [...catalog.agents, ...imported] };
  }
  saveSubagent(input: import("./collaboration/types.ts").SaveSubagentInput) {
    if (!input || (input.sessionId !== null && typeof input.sessionId !== "string"))
      throw new Error("invalid_input: 子智能体参数无效");
    if (input.config?.modelSelection && !this.ports.models.valid(input.config.modelSelection))
      throw new Error("configuration: 请为子智能体选择可用模型和推理档位");
    return this.collaboration.profiles.save(
      input.config,
      input.scope,
      input.id,
      input.sessionId ? this.ports.sessions.cwd(input.sessionId) : undefined,
    );
  }
  async closeChild(id: string): Promise<void> {
    const child = this.ports.sessions.configuration(id).subagentProfile;
    const task = child ? this.collaboration.taskForChild(id) : undefined;
    if (task?.status === "running") await this.collaboration.stop(task.parentSessionId, task.agentId);
    if (this.ports.sessions.active(id)) await this.ports.sessions.stop(id);
    if (child) await this.background.stopAll(id);
  }
  commandSubagents(id: string, imported: () => SubagentProfile[]) {
    const config = this.ports.sessions.configuration(id);
    if (!collaborationEnabled(config.plugins) || config.subagentProfile) return [];
    const enabled = this.listSubagents(id, imported()).agents.filter((agent) => agent.enabled);
    return [...new Map(enabled.map((agent) => [agent.name.toLowerCase(), agent])).values()];
  }
  agentRequest(
    id: string,
    text: string,
    imported: () => SubagentProfile[],
  ): { agent: string; prompt?: string } | undefined {
    const mention = /^@([^\s@/]+)(?:\s+([\s\S]*))?$/.exec(text.trim());
    if (!mention) return;
    const agents = imported();
    const declared = this.listSubagents(id, agents).agents.find(
      (agent) => agent.name.toLowerCase() === mention[1].toLowerCase(),
    );
    if (!declared) return;
    if (
      !this.commandSubagents(id, () => agents).some(
        (agent) => agent.name.toLowerCase() === declared.name.toLowerCase(),
      )
    )
      throw new Error("invalid_input: 此会话未启用该子智能体");
    return { agent: declared.name, prompt: mention[2]?.trim() };
  }
  agentPrompt(
    id: string,
    request: { agent: string; prompt?: string },
    imported: () => SubagentProfile[],
  ): string {
    if (
      !this.commandSubagents(id, imported).some(
        (agent) => agent.name.toLowerCase() === request.agent.toLowerCase(),
      )
    )
      throw new Error("invalid_input: 此会话未启用该子智能体");
    return `Use the Agent tool with subagent_type ${JSON.stringify(request.agent)} for the following user request. Preserve the requested agent's configured permissions and report its result.\n\n${request.prompt || `Use ${request.agent} for this task.`}`;
  }
  async deleteArtifacts(id: string): Promise<void> {
    await rm(join(this.ports.directory, "agent", "subagents", id), { recursive: true, force: true });
  }
  private requiredRecord(id: string) {
    const record = this.ports.sessions.get(id);
    if (!record) throw new Error("not_found: 会话不存在");
    return record;
  }
  restore(id: string, runtime: PluginRuntimeState): void {
    if (runtime.kind === "background") {
      const task = runtime.data as BackgroundTask;
      this.background.restore({ ...task, status: runtime.status === "interrupted" ? "lost" : task.status });
    } else if (runtime.kind === "subagent_inbox") this.inbox.restore(id, runtime.data as PluginInput[]);
    else if (runtime.kind === "subagent") {
      const task = runtime.data as SubagentTask;
      this.collaboration.restore({
        ...task,
        ...(runtime.status === "interrupted"
          ? { status: "interrupted" as const, waitingForInput: false }
          : {}),
      });
    }
  }

  async close(): Promise<void> {
    const errors: unknown[] = [];
    for (const close of [
      () => this.longMemory.close(),
      () => this.memory.wait(),
      () => this.collaboration.close(),
      () => this.background.close(),
      () => this.titles.close(),
      () => this.notifications.close(),
    ]) {
      try {
        await close();
      } catch (error) {
        errors.push(error);
      }
    }
    if (errors.length === 1) throw errors[0];
    if (errors.length > 1) throw new AggregateError(errors, "Failed to close builtin plugin runtimes");
  }
  async drain(): Promise<void> {
    this.memory.close();
    await this.longMemory.drain();
  }
}