Home / Articles / Practical notes: Agentic Loops & Design Patterns

This article is published in English.

Practical notes: Agentic Loops & Design Patterns

Operable walkthrough of Practical notes: Agentic Loops & Design Patterns: contracts, checks, and drop-in code slots for teams shipping this pattern.

1942 words

The following notes reconstruct a practical path around “Agentic Loops & Design Patterns”. Emphasis stays on contracts, checks, and drop-in code placeholders rather than motivational framing. When working through the Overview stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

1. Plan–Act–Verify loop

The 1 Plan Act Verify stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

// Pseudo-code: Node/TypeScript orchestrating Claude as an agent

async function planActVerifyLoop(ticket) {
  let iteration = 0;
  const maxIterations = 5;

  while (iteration < maxIterations) {
    iteration++;

    // 1. PLAN: ask Claude for the next step
    const plan = await claude.chat({
      model: "claude-3-opus",
      messages: [
        {
          role: "user",
          content: `You are a coding agent working on ticket ${ticket.id}.
Goal: Make tests pass for this ticket without changing public APIs.

Current context:
${ticket.description}
${ticket.latestFailureLog}

What is the single most useful next action?`
        }
      ]
    });

    // 2. ACT: execute the suggested action if it's in the allowed action space
    const action = parseAction(plan);
    const result = await executeAction(action); // run tests, edit file, etc.

    // 3. VERIFY: use tests as verification
    const verification = await runTests(ticket.testSuite);

    if (verification.allPassing) {
      return { status: "done", iterations: iteration };
    }

    // Attach the failure output back into ticket context
    ticket.latestFailureLog = verification.failureOutput;
  }

  return { status: "budget_exhausted" };
}

ReAct loop (Reason + Act)

The ReAct loop Reason Act stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.

# Pseudo-code: Python coordinator orchestrating Claude tool calls

def react_loop(incident):
    iteration = 0
    max_iterations = 6

    while iteration < max_iterations:
        iteration += 1

        # REASON: Claude decides what to inspect next
        reasoning = claude.chat(
            model="claude-3-opus",
            messages=[
                {
                    "role": "user",
                    "content": f"""
You are an SRE assistant triaging a payment incident.

Incident summary:
{incident.summary}

Recent metrics:
{incident.latest_metrics}

Logs snippet:
{incident.logs_snippet}

Decide one next diagnostic action from:
- CHECK_METRICS
- CHECK_LOGS
- CHECK_DB_HEALTH
- SUMMARIZE_FINDINGS_AND_RECOMMEND_ACTION

Explain your reasoning briefly and output JSON with 'action' and 'target'.
"""
                }
            ]
        )

        action = parse_json(reasoning)
        if action["action"] == "SUMMARIZE_FINDINGS_AND_RECOMMEND_ACTION":
            return claude.chat(... )  # final summary + recommended steps

        # ACT: run the selected diagnostic
        observation = run_diagnostic(action, incident)

        # Update incident state for the next reasoning step
        incident.update_with_observation(observation)

3. Reflect–Revise loop

The 3 Reflect Revise loop stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. The 3 Reflect Revise loop stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

async function reflectReviseLoop(draftInput: string) {
  // 1. GENERATE
  const draft = await claude.chat({
    model: "claude-3-opus",
    messages: [
      {
        role: "user",
        content: `
Write a customer-facing email explaining a declined payment due to suspected fraud.
Constraints:
- empathetic but clear
- no admission of fault
- no promises about future approvals

Context:
${draftInput}
`
      }
    ]
  });

  // 2. CRITIQUE (using a cheaper model as checker)
  const critique = await claude.chat({
    model: "claude-3-haiku",
    messages: [
      {
        role: "user",
        content: `
You are a compliance checker.

Review the following email for:
- policy violations
- misleading statements
- over-commitments

Output a JSON with:
- issues: list of strings
- safe: boolean

Email:
${draft.content}
`
      }
    ]
  });

  const review = JSON.parse(critique.content);

  if (review.safe) {
    return draft.content;
  }

  // 3. REVISE
  const revised = await claude.chat({
    model: "claude-3-opus",
    messages: [
      {
        role: "user",
        content: `
You wrote this email:
${draft.content}

Compliance issues:
${review.issues.join("\n")}

Rewrite the email to resolve all issues while preserving intent.
`
      }
    ]
  });

  return revised.content;
}

Draft–Test–Fix loop

For the Draft Test Fix loop stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

async function draftTestFixLoop(issue: Issue) {
  const maxIterations = 4;
  let iteration = 0;

  while (iteration < maxIterations) {
    iteration++;

    // DRAFT: Claude proposes code changes
    const patch = await claude.chat({
      model: "claude-3-opus",
      messages: [
        {
          role: "user",
          content: `
You are an autonomous coding agent.

Ticket:
${issue.title}
${issue.body}

Current failing tests:
${issue.failingTests}

Propose a minimal patch as a diff that makes tests pass
without changing public APIs.`
        }
      ]
    });

    applyPatchToGitRepo(patch.content);

    // TEST: run CI locally or via API
    const testResult = await runCi(issue.branchName);

    if (testResult.success) {
      // FIX_DONE: open a PR with the diff
      await openPullRequest(issue, patch.content);
      return { status: "done" };
    }

    // FEEDBACK: update failingTests for next iteration
    issue.failingTests = testResult.failureSummary;
  }

  return { status: "needs_human_review" };
}

Critic–Builder (maker–checker) loop

For the Critic Builder maker checker stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

Retry-with-memory loop

For the Retry-with-memory loop stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For the Retry-with-memory loop stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

def retry_with_memory_loop(task, max_attempts=3):
    failures = []

    for attempt in range(1, max_attempts + 1):
        # Ask Claude to consider past failures before deciding the next move
        decision = claude.chat(
            model="claude-3-opus",
            messages=[
                {
                    "role": "user",
                    "content": f"""
You are handling a task with a flaky external API.

Task:
{task.description}

Past failures:
{failures}

Decide whether to:
- RETRY_API
- FALLBACK_TO_CACHE
- ESCALATE_TO_HUMAN

Explain briefly and output JSON: {{ "choice": "...", "reason": "..." }}
"""
                }
            ]
        )

        choice = parse_json(decision)

        if choice["choice"] == "RETRY_API":
            result = call_api(task)
        elif choice["choice"] == "FALLBACK_TO_CACHE":
            result = use_cache(task)
        else:
            return {"status": "escalated", "failures": failures}

        if result.success:
            return {"status": "success", "attempts": attempt}

        failures.append(result.error_summary)

    return {"status": "max_attempts_exhausted", "failures": failures}

Human-in-the-loop escalation

When working through the Human-in-the-loop escalation stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

async function triageLoop(ticket: Ticket) {
  const autoActions = ["LABEL", "ROUTE_TO_QUEUE", "REQUEST_MORE_INFO"];

  const decision = await claude.chat({
    model: "claude-3-opus",
    messages: [
      {
        role: "user",
        content: `
You are a support triage agent in a payments company.

Ticket:
${ticket.body}

Decide one of:
- LABEL (low risk)
- ROUTE_TO_QUEUE (medium risk)
- ESCALATE_TO_HUMAN (high risk / unclear)

Return JSON with:
- choice
- risk_level
- rationale
`
      }
    ]
  });

  const choice = JSON.parse(decision.content);

  if (choice.choice === "ESCALATE_TO_HUMAN") {
    await createHumanTask(ticket, choice.rationale);
    return { status: "escalated" };
  }

  // LABEL or ROUTE_TO_QUEUE are automated but bounded
  await applyAutomatedTriage(ticket, choice);
  return { status: "auto_treated" };
}

How to choose patterns in practice

When working through the How to choose patterns stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.

Operational checklist

For the Operational checklist stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state.

Treat this stage as a contract between inputs and validated outputs. Name the artifacts, define success checks, and refuse silent partial completion.

Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

Write a short runbook: how to rotate keys, how to drain the queue, how to roll back the last ingest.

Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.

Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.

Before promoting the stack, freeze versions, capture a golden transcript for the critical path, and confirm rollback steps. Shared environments need rate limits, tenancy checks, and a clear owner for secret rotation. Prefer boring reliability over clever one-off demos.

Batch note for 54c3b06154e6: keep provider keys out of the repo, set a per-session token ceiling, and store transcripts next to the eval fixtures so later model swaps stay comparable.