Глава 3

Разложите задачу на граф до назначения агентов

Агент - исполнитель узла, а не способ декомпозиции. Сначала определите зависимости, артефакты и точки объединения, и только затем решайте, кто выполняет узлы. Фраза "пусть пять агентов работают параллельно" ничего не говорит о возможности параллелизма.

Нарисуйте направленный ациклический граф. Для каждого узла укажите входы, выходы, write set, проверку и зависимости - в companion-проекте это исполняемый контракт.

TypeScript
export type TaskKind = "plan" | "test" | "security" | "docs" | "review";
export type TaskStatus = "pending" | "running" | "completed" | "failed";

export interface WorkItem {
  readonly id: string;
  readonly title: string;
  readonly kind: TaskKind;
  readonly dependsOn: readonly string[];
  readonly writeSet: readonly string[];
  readonly maxAttempts: number;
}

export interface AgentContract {
  readonly id: AgentId;
  readonly accepts: readonly TaskKind[];
  readonly allowedWritePrefixes: readonly string[];
  readonly allowedHandoffs: readonly AgentId[];
}

export interface Usage {
  readonly calls: number;
  readonly tokens: number;
  readonly costUnits: number;

Проверить такой граф стоит по пяти вопросам к каждому узлу: какой точный input делает работу возможной; какой artifact подтверждает завершение; можно ли проверить output без чтения внутреннего reasoning; с какими узлами пересекается write set; может ли узел завершиться или повториться независимо. Часто после этого оказывается, что три придуманные роли стоят на одной критической цепочке и ускорить задачу не могут.

Граф проверяется кодом до запуска исполнителей - на дубли, неизвестные зависимости, self-dependency и циклы.

TypeScript
export function validateWorkGraph(tasks: readonly WorkItem[]): void {
  if (tasks.length === 0) {
    throw new Error("Work graph must contain at least one task");
  }

  const ids = new Set<string>();
  for (const task of tasks) {
    if (ids.has(task.id)) {
      throw new Error(`Duplicate task id: ${task.id}`);
    }
    if (task.maxAttempts < 1 || !Number.isInteger(task.maxAttempts)) {
      throw new Error(`Invalid maxAttempts for ${task.id}`);
    }
    ids.add(task.id);
  }

  for (const task of tasks) {
    for (const dependency of task.dependsOn) {
      if (!ids.has(dependency)) {
        throw new Error(`Unknown dependency ${dependency} for ${task.id}`);
      }
      if (dependency === task.id) {
        throw new Error(`Task ${task.id} depends on itself`);
      }
    }
  }

  const visiting = new Set<string>();
  const visited = new Set<string>();
  const byId = new Map(tasks.map((task) => [task.id, task]));

  const visit = (id: string): void => {
    if (visiting.has(id)) {
      throw new Error(`Cycle detected at ${id}`);
    }
    if (visited.has(id)) {
      return;
    }
    visiting.add(id);
    const task = byId.get(id);
    if (!task) {
      throw new Error(`Missing task ${id}`);
    }
    for (const dependency of task.dependsOn) {
      visit(dependency);
    }
    visiting.delete(id);
    visited.add(id);
  };
Не путайте роли с узлами. Один агент может выполнить несколько последовательных узлов. Один узел может породить несколько одинаковых workers по map/reduce. Число названий в prompt не равно числу полезных параллельных ветвей.

Граф есть. Теперь честный вопрос: а существует ли в нём полезный параллелизм вообще.