Agentic Loops vs. Spec-Driven Development

There are a lot of new terms being tossed around the AI developer world. Two new ones are Agentic Loops and Spec-Driven Development. This blog post will endeavor to clarify…

AI-driven software development today divides into two complementary but distinct patterns: agentic loop programming (autonomous, iterative agents) and Spec‑Driven Development (SDD, spec-as-source-of-truth). Both appear in modern toolchains: agentic loops power autonomous workflows and multi‑step reasoning, while SDD tames AI generation by anchoring code to machine‑readable contracts. This post compares the approaches, highlights tradeoffs, and provides compact, runnable C# examples you can adapt.

Short definitions

  • Agentic loop programming: goal → orchestrator →reason/plan → act (tools, etc.) → observe → orchestrator, loop where an LLM-driven agent runs multiple cycles, calls tools, observes results, and continues until the goal is met. Useful for autonomy, complex orchestration, and long‑horizon tasks.
  • Spec‑Driven Development (SDD): the machine‑readable spec is the primary artifact (single source of truth). Code (or tests) is generated/derived from the spec; at the end validates that implementations match the spec. SDD improves traceability, auditability, and reduces drift in AI-generated code.

When to choose which

  • Choose agentic loops when you need autonomy or multi‑step decision-making (automated repair, orchestration, continuous monitoring). Expect some non‑determinism but gain flexibility and problem-solving capability.
  • Choose SDD when determinism, correctness, and traceability matter (regulated domains, contracts, API-first development). SDD reduces drift and aids audit and compliance.

Agentic loop: risks and hardening
Main risks: goal drift, infinite loops, runaway cost/latency, security/tool misuse, auditability gaps. Mitigations: typed tool I/O, explicit stop conditions, iteration/cost caps, human checkpoints for high‑impact actions, rich logging/auditing, and canary testing. These patterns are essential before deploying agents to production

Spec‑Driven Development: risks and hardening
Main risks: over‑specification and maintenance burden; outdated or low‑quality specs yield brittle generation. Mitigations: invest in good spec discipline, machine‑readable formats (JSON, OpenAPI), auto‑derived tests, gates, and schema validation. SDD improves auditability because the spec itself is the contract.

Concrete C# examples
Below are compact patterns you can drop into a sandbox. The LLM calls are mocked — replace them with your LLM client (OpenAI/Anthropic) in real projects.

  1. Agentic loop pattern (C#)
    Purpose: show a simple agent loop that (a) reasons via LLM placeholder, (b) executes typed tools, (c) observes and updates memory, and (d) uses explicit stop conditions.

Key elements: ITool contract, AgentContext (memory), clear stop conditions, max iterations, and typed tool outputs for safer parsing.

Code (runnable pattern)

using System;
using System.Threading.Tasks;

public interface ITool<TInput, TOutput>
{
    Task<TOutput> ExecuteAsync(TInput input);
}

// Example tool: shell-like task
public class ShellTool : ITool<string, string>
{
    public Task<string> ExecuteAsync(string input)
    {
        // Simulate work
        return Task.FromResult($"Shell executed: {input}");
    }
}

public class AgentContext
{
    public string Goal { get; }
    public int Iteration { get; set; }
    public string Memory { get; set; }
    public decimal CostSoFar { get; set; }

    public AgentContext(string goal)
    {
        Goal = goal; Iteration = 0; Memory = ""; CostSoFar = 0m;
    }
}

public class Agent
{
    private readonly ITool<string, string> _tool;
    private readonly int _maxIterations;
    private readonly decimal _maxCost;

    public Agent(ITool<string, string> tool, int maxIterations = 10, decimal maxCost = 10.0m)
    {
        _tool = tool; _maxIterations = maxIterations; _maxCost = maxCost;
    }

    // Placeholder for LLM call; replace with actual client.
    private Task<string> CallLLMAsync(string prompt)
    {
        // Very simple mock that alternates between calling tool and finishing.
        if (prompt.Contains("satisfied", StringComparison.OrdinalIgnoreCase))
            return Task.FromResult("DONE");
        return Task.FromResult("ACTION:call_tool INPUT:run-step");
    }

    public async Task RunAsync(AgentContext ctx)
    {
        while (true)
        {
            if (ctx.Iteration >= _maxIterations)
            {
                Console.WriteLine("Stopping: reached max iterations.");
                return;
            }

            if (ctx.CostSoFar >= _maxCost)
            {
                Console.WriteLine("Stopping: reached cost budget.");
                return;
            }

            ctx.Iteration++;
            var observation = $"Goal:{ctx.Goal}; Memory:{ctx.Memory}; Iteration:{ctx.Iteration}";
            var plan = await CallLLMAsync($"Observe: {observation}\nDecide: is goal satisfied?");
            Console.WriteLine($"[LLM] plan={plan}");

            if (plan.StartsWith("DONE", StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("Agent: Goal satisfied.");
                return;
            }

            if (plan.Contains("ACTION:call_tool", StringComparison.OrdinalIgnoreCase))
            {
                // Parse input token (naive here)
                var toolInput = "run-step";
                var toolResult = await _tool.ExecuteAsync(toolInput);
                Console.WriteLine($"[Tool] {toolResult}");

                // Update memory and cost (mock cost per tool call)
                ctx.Memory += $" | iter{ctx.Iteration}:{toolResult}";
                ctx.CostSoFar += 1.0m;

                // Ask LLM to evaluate the observation
                var eval = await CallLLMAsync("Was result satisfactory?");
                if (eval.StartsWith("DONE", StringComparison.OrdinalIgnoreCase))
                {
                    Console.WriteLine("Agent: confirmed success after tool execution.");
                    return;
                }
            }
            else
            {
                Console.WriteLine("Agent: no actionable plan, stopping to avoid stray behavior.");
                return;
            }
        }
    }
}

// Example run
public static class Program
{
    public static async Task Main()
    {
        var agent = new Agent(new ShellTool(), maxIterations: 5, maxCost: 3.0m);
        var ctx = new AgentContext("Add feature X and pass unit tests");
        await agent.RunAsync(ctx);
    }
}

Notes on hardening:

  • Replace CallLLMAsync with structured prompts and parse JSON responses. Use typed JSON schemas for actions.
  • Add RBAC for tools that access network or shell. Log each step with timestamps and full inputs/outputs for audit.
  • Implement deciders that prevent repeated cycles (dedup memory) and ephemeral human checkpoints when necessary.
  1. Spec‑Driven Development pattern (C#)
    Purpose: show a minimal SDD flow: a machine‑readable spec (JSON) describes endpoints; a generator creates a service skeleton and a test scaffold. In production, an AI can implement methods from the spec and CI runs auto‑derived tests to enforce the contract.

Example Spec (spec.json)

{
  "service": "UserService",
  "description": "Create and get users",
  "endpoints": [
    { "name": "CreateUser", "input": { "name":"string","email":"string" }, "output": { "id":"guid" } },
    { "name": "GetUser", "input": { "id":"guid" }, "output": { "name":"string","email":"string" } }
  ],
  "constraints": {
    "email": "must be valid format",
    "name": "non-empty"
  }
}

Generator and test scaffold (simplified)

using System;
using System.IO;
using System.Text.Json;
using System.Text;

public class SpecGenerator
{
    public static void Generate(string specPath, string outDir)
    {
        var json = File.ReadAllText(specPath);
        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        var serviceName = root.GetProperty("service").GetString();

        var sb = new StringBuilder();
        sb.AppendLine("using System;");
        sb.AppendLine("namespace Generated {");
        sb.AppendLine($"  public class {serviceName} {{");

        foreach (var ep in root.GetProperty("endpoints").EnumerateArray())
        {
            var name = ep.GetProperty("name").GetString();
            sb.AppendLine($"    // Auto-generated: {name} - implement per spec");
            sb.AppendLine($"    public object {name}(object input) => throw new NotImplementedException();");
        }

        sb.AppendLine("  } }");

        Directory.CreateDirectory(outDir);
        File.WriteAllText(Path.Combine(outDir, $"{serviceName}.cs"), sb.ToString());

        // Test scaffold example
        var test = new StringBuilder();
        test.AppendLine("using Xunit;");
        test.AppendLine($"public class {serviceName}Tests {{");
        test.AppendLine("  [Fact]");
        test.AppendLine($"  public void CreateUser_ShouldReturnId() => throw new NotImplementedException();");
        test.AppendLine("}");
        File.WriteAllText(Path.Combine(outDir, $"{serviceName}.Tests.cs"), test.ToString());
        Console.WriteLine(\"Generated files in: \" + outDir);
    }

    public static void Main(string[] args)
    {
        Generate(\"spec.json\", \"out\");
    }
}

How AI fits in SDD:

  • The spec is authoritative. An AI (or human) generates code that implements the spec. Tests are generated from the spec (asserting types, constraints, and examples). CI runs tests; failures trigger regeneration or human review. This establishes traceability and reduces drift

Making SDD production-quality

  • Use a formal schema (OpenAPI, JSON Schema, protobuf). Generate strong typed models (C# records/classes) from the spec. Use auto‑derived contract tests that validate constraints (email format, non-empty name). For more advanced workflows, integrate Roslyn-based analyzers that flag divergences at compile time, and store specs in a versioned repository for audit.

Practical comparisons and recommendations

  • Observability & Auditability
    • Agentic loop: requires explicit structured logging, typed tool outputs, and a chain-of-thought or step tracing. Without this, auditing is difficult.
    • SDD: natural audit trail because the spec is the contract; tests and commit history show intent and evidence.
  • Speed vs Overhead
    • Agentic loops can rapidly iterate and adapt but add runtime complexity (guardrails, tool contracts).
    • SDD requires upfront work on a machine‑readable spec but reduces downstream rework and improves compliance.
  • Typical real-world use-cases
    • Agentic loops: automated bug-fix agents, cloud orchestration that runs multi-step operations, autonomous data pipelines, or customer support agents that call external APIs.
    • SDD: API-first microservices in finance/healthcare, contract-based integrations, teams generating implementations from an agreed, audited spec.
  • Combining the two (hybrid patterns)
    • Use SDD to define critical contracts and expose typed tool interfaces to agents. Agents operate inside the guardrails defined by the spec. For example, an agent that patches code should only call tools whose inputs/outputs are specified in SDD artifacts. This approach balances autonomy with control and auditability.

Conclusion
Agentic loop programming and Spec‑Driven Development serve different needs. Agentic loops buy autonomy and multi-step problem solving. SDD buys determinism, traceability, and compliance. In practice, the strongest architectures blend them: use SDD to define and enforce the boundaries and contracts, and deploy agentic loops inside those boundaries to perform autonomous tasks. The C# patterns above give you a starting point: a simple agent loop pattern hardened with stop conditions and typed tools, and an SDD generator that shows how specs map to code and tests

Note: Much of the code was written by AI and is offered as is (AI makes mistakes, so proceed with caution). The text was also written by AI but significantly edited by me.

Unknown's avatar

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. Liberty is a Senior AI Engineer at the University of Pittsburgh Medical Center, and was a Team Lead and Senior Software Engineer for various corporations, a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a 21 year Microsoft MVP.
This entry was posted in AI, Essentials. Bookmark the permalink.