Z ZPI 文档 v0.3.8
介绍页

源码

packages/agent-sdk/src/tasks.ts

typescript142 行查看原文
import { randomUUID } from "node:crypto";
import { abortable } from "@zpi-dev/ai";
import type { ToolDefinition } from "@zpi-dev/coding-agent";
import { type TObject, Type } from "typebox";
import { defineTool } from "./define-tool.ts";

export interface TaskMetadata {
  description?: string;
  toolUseId?: string;
  taskType?: string;
  subagentType?: string;
}
export interface TaskEvent extends TaskMetadata {
  id: string;
  phase: "started" | "progress" | "terminal";
  status: "running" | "completed" | "failed" | "stopped";
  output: string;
  startedAt: number;
  totalTokens: number;
  toolUses: number;
}
interface Task extends TaskEvent {
  controller: AbortController;
  done: Promise<string>;
}
export class Tasks {
  private tasks = new Map<string, Task>();
  private closed = false;
  private listener?: (event: TaskEvent) => void;
  constructor(listener?: (event: TaskEvent) => void) {
    this.listener = listener;
  }
  start(
    run: (
      signal: AbortSignal,
      progress: (output: string, totalTokens?: number, toolUses?: number) => void,
    ) => Promise<string>,
    metadata: TaskMetadata = {},
  ): string {
    if (this.closed) throw new Error("Tasks are closed");
    const task: Task = {
      ...metadata,
      phase: "started",
      startedAt: Date.now(),
      totalTokens: 0,
      toolUses: 0,
      id: randomUUID(),
      controller: new AbortController(),
      status: "running",
      output: "",
      done: Promise.resolve(""),
    };
    this.tasks.set(task.id, task);
    this.emit(task);
    task.done = Promise.resolve()
      .then(() =>
        run(task.controller.signal, (output, totalTokens, toolUses) => {
          if (task.status !== "running") return;
          task.output = output;
          task.totalTokens = totalTokens ?? task.totalTokens;
          task.toolUses = toolUses ?? task.toolUses;
          task.phase = "progress";
          this.emit(task);
        }),
      )
      .then(
        (output) => {
          task.output = output;
          task.status = task.controller.signal.aborted ? "stopped" : "completed";
          task.phase = "terminal";
          this.emit(task);
          return output;
        },
        (error) => {
          task.status = task.controller.signal.aborted ? "stopped" : "failed";
          task.output = String(error);
          task.phase = "terminal";
          this.emit(task);
          return task.output;
        },
      );
    return task.id;
  }
  private emit(task: Task): void {
    const { controller: _, done: __, ...event } = task;
    this.listener?.(event);
  }
  async stop(id: string, signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    if (this.closed) throw new Error("Tasks are closed");
    const task = this.tasks.get(id);
    if (!task) throw new Error("Unknown task_id");
    if (task.status !== "running") return;
    task.controller.abort();
    await (signal ? abortable(task.done, signal) : task.done);
  }
  tools(): ToolDefinition<TObject>[] {
    return [
      defineTool({
        name: "TaskOutput",
        description: "Read a background task's output. Optionally wait for completion.",
        parameters: Type.Object({
          task_id: Type.String(),
          block: Type.Optional(Type.Boolean()),
          timeout: Type.Optional(Type.Integer({ minimum: 0, maximum: 600000 })),
        }),
        execute: async (input, { signal }) => {
          const task = this.tasks.get(input.task_id);
          if (!task) throw new Error("Unknown task_id");
          if (input.block !== false && task.status === "running") {
            const timer = AbortSignal.timeout(input.timeout ?? 30000);
            try {
              await abortable(task.done, signal ? AbortSignal.any([timer, signal]) : timer);
            } catch (error) {
              if (signal?.aborted) throw error;
            }
          }
          return { task_id: task.id, status: task.status, output: task.output };
        },
      }),
      defineTool({
        name: "TaskStop",
        description: "Stop a background task owned by this query.",
        parameters: Type.Object({ task_id: Type.String() }),
        execute: async (input, { signal }) => {
          await this.stop(input.task_id, signal);
          return `Stopped ${input.task_id}`;
        },
      }),
    ];
  }
  async close(): Promise<void> {
    this.closed = true;
    for (const task of this.tasks.values()) task.controller.abort();
    try {
      await Promise.all([...this.tasks.values()].map((task) => task.done));
    } finally {
      this.tasks.clear();
    }
  }
}