Глава 18

Делайте конфликты и повторы явными состояниями

Retry полезен для временного сбоя, но опасен для semantic conflict и side effect. Система должна классифицировать отказ до повторения - иначе повтор либо жжёт бюджет, либо дублирует необратимое действие.

Отказ · Пример · Действие

  • Transient - Timeout, временный rate limit, worker crash; Bounded retry с backoff и тем же idempotency key
  • Permanent input - Неверная schema, отсутствующий artifact; Stop или исправление input
  • Policy - Запрещенный tool или write path; Fail closed, без model retry
  • Conflict - expectedVersion устарел; Новый read, semantic merge или escalation
  • Quality - Acceptance grader не пройден; Ограниченный revision loop с findings

В companion-проекте transient failure повторяется в пределах maxAttempts, но BudgetExceeded останавливает branch сразу. Policy error и расход бюджета не исправляются просьбой "попробуй ещё раз".

TypeScript
          for (let attempt = 1; attempt <= task.maxAttempts; attempt += 1) {
            eventLog.append({ type: "task.started", taskId: task.id, agentId, attempt });
            const openSpan = trace.start(task.id, agentId, attempt);
            try {
              const result = await handler.run({
                runId,
                task,
                inputArtifacts: store.list(),
                attempt
              });
              budget.consume(result.usage);

              for (const draft of result.artifacts) {
                if (!task.writeSet.includes(draft.key)) {
                  throw new Error(`Task ${task.id} produced undeclared artifact ${draft.key}`);
                }
                const artifact = store.write(agentId, draft);
                eventLog.append({
                  type: "artifact.written",
                  taskId: task.id,
                  key: artifact.key,
                  version: artifact.version
                });
              }
              evidence.push(...result.evidence);
              trace.end(openSpan, "ok", result.summary);
              eventLog.append({ type: "task.completed", taskId: task.id, agentId });
              return { task };
            } catch (error) {
              const reason = errorMessage(error);
              trace.end(openSpan, "error", reason);
              const terminal = attempt === task.maxAttempts || error instanceof BudgetExceededError;
              if (terminal) {
                eventLog.append({ type: "task.failed", taskId: task.id, agentId, reason });
                return { task, reason };
              }
            }
          }
          return { task, reason: `Task ${task.id} exhausted attempts` };
        })
      );

      for (const outcome of outcomes) {
        if (outcome.reason) {
          status[outcome.task.id] = "failed";
          failureReason ??= `${outcome.task.id}: ${outcome.reason}`;
        } else {
          status[outcome.task.id] = "completed";
        }
      }
    }

    if (failureReason) {
      eventLog.append({ type: "run.failed", runId, reason: failureReason });
      return {
        runId,
        status: "failed",
        taskStatus: { ...status },
        artifacts: store.list(),

Отдельная дисциплина - идемпотентность side effects: сформируйте stable operation key из run, task и action; запишите intent до внешнего call; перед retry проверьте, не был ли effect уже принят provider; сохраните provider receipt как artifact; не позволяйте другому агенту повторить операцию под новым key.

Не скрывайте конфликт synthesis. Если два workers предлагают несовместимые архитектуры, reducer не должен механически склеивать тексты. Верните alternatives, assumptions и evidence тому, кто имеет право принять решение.

Отказы классифицированы. Но чтобы run не ушёл в бесконечность, нужны лимиты - на каждую ветвь и на весь run.

Ссылки