Chapter 3

Decompose the Task Into a Graph Before Assigning Agents

An agent is a node's executor, not a way to decompose. First define the dependencies, artifacts, and join points, and only then decide who executes the nodes. "Let five agents work in parallel" says nothing about the possibility of parallelism.

Draw a directed acyclic graph. For each node state the inputs, outputs, write set, check, and dependencies - in the companion project this is an executable contract.

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;

Check such a graph with five questions per node: what exact input makes the work possible; what artifact confirms completion; can you verify the output without reading the internal reasoning; which nodes does the write set overlap; can the node complete or retry independently. Often after this it turns out three invented roles sit on one critical chain and can't speed the task up.

The graph is checked by code before running the executors - for duplicates, unknown dependencies, self-dependency, and cycles.

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);
  };
Don't confuse roles with nodes. One agent can execute several sequential nodes. One node can spawn several identical workers via map/reduce. The number of names in the prompt isn't the number of useful parallel branches.

The graph exists. Now the honest question: does it contain any useful parallelism at all.