Chapter 13: Creating Autonomous Agents with Loops
Theoretical Foundations
At the heart of autonomous AI behavior lies a fundamental concept: the ability to perceive, reason, act, and, most importantly, reflect upon the outcome to inform the next step. This cycle, known as the perception-reason-action loop, is the engine of agency. In the context of the Microsoft Semantic Kernel, we are not merely building stateless functions; we are architecting stateful, cognitive systems that can sustain a line of thought, adapt to new information, and pursue complex goals with minimal human intervention. This section lays the theoretical groundwork for understanding how these iterative, self-correcting loops are constructed, why they are essential for true autonomy, and how modern C# constructs provide the robust scaffolding necessary to manage their inherent complexity and potential for non-termination.
The Essence of the Loop: From Linear Chains to Cyclical Cognition
To grasp the significance of autonomous loops, we must first contrast them with their simpler, linear counterparts. A traditional function call or a simple prompt-response interaction is a straight line: input leads to output, and the process concludes. This is sufficient for tasks like summarization or translation, where the goal is a single, discrete transformation. However, complex problem-solving rarely follows a straight line. Consider the task of planning a multi-stage marketing campaign. A single prompt might generate a draft, but it would not incorporate feedback from a simulated focus group, adjust the messaging based on performance metrics, or re-allocate budget in response to competitor actions.
This is where the loop becomes indispensable. An autonomous loop transforms the AI from a passive tool into an active participant in its own reasoning process. It creates a feedback mechanism where the output of one cycle becomes the input for the next, allowing the agent to refine its understanding, correct its mistakes, and inch closer to a satisfactory solution with each iteration. This is the difference between giving someone a fish and teaching them how to fish; the loop provides the AI with the "fishing rod" of self-correction.
The Core Components of an Autonomous Loop
State Management: The agent must "remember" its previous actions, thoughts, and the results of those actions. Without persistent state, each iteration would begin from a blank slate, rendering the loop's iterative nature useless. This state is more than just a collection of variables; it is the agent's working memory, its "mental scratchpad" where it keeps track of the problem-solving trajectory. In the Semantic Kernel, this is often managed through KernelContext or custom state objects that are passed between functions or persist across invocations.
-
The Reasoning Engine (The Planner): This is the "brain" of the loop. In each cycle, the agent must decide what to do next. Should it call a plugin? Should it generate another thought? Should it terminate? The reasoning engine, often embodied by the Kernel's built-in planner or a custom prompt, analyzes the current state and formulates the next best action. It translates high-level goals into a sequence of executable steps. For example, if the goal is "Find the best flight to Paris," the planner might break this down into: 1. Call a
SearchFlightsplugin with origin and destination. 2. Analyze the results for price and duration. 3. If no good options, callSearchFlightsagain with a flexible date range. -
The Action Layer (Pluggable Functions): These are the agent's "hands." They are the concrete actions the agent can take in the world. In the Semantic Kernel, these are functions, which can be native C# functions or semantic functions (prompts). The action layer is where the agent interacts with external systems: calling APIs, querying databases, manipulating files, or even generating new prompts for the LLM. The power of the Semantic Kernel lies in its ability to seamlessly integrate these disparate actions under a common interface.
-
The Termination Condition (The "Stop" Signal): This is arguably the most critical component for safety and efficacy. A loop without a termination condition is a runaway process—an infinite loop that consumes resources and may produce nonsensical or even harmful outputs. The termination condition is the agent's self-awareness of completion or failure. It can be based on:
- Success Criteria: Has the goal been met? (e.g., "Has a viable flight been identified and booked?")
- Iteration Limits: Has the loop run for too long without progress? This is a crucial safety net.
- Error Detection: Has the agent encountered an unrecoverable error or a logical contradiction?
- Stagnation: Has the agent's state stopped changing, indicating it is stuck in a reasoning loop?
The Architectural Blueprint: A Visual Representation
The flow of an autonomous loop can be visualized as a cycle where state, reasoning, and action perpetually inform one another, all while being governed by a termination check.
This diagram illustrates the core cognitive rhythm. The agent begins with an initial state (the goal and any context). It then enters the main loop: reason, act, update. After updating its state with the new information, it performs a critical self-assessment. If the termination condition is not met, it loops back to reasoning, now armed with more context. If the condition is met, it exits the loop and finalizes its output.
The "Why": The Analogy of the Master Chess Player
State (Perception): The grandmaster assesses the board. They don't just see the pieces; they see patterns, threats, opportunities, and the history of the game so far. This is their state management. 2. Reasoning (Planning): They mentally simulate potential moves and their consequences. "If I move my knight here, my opponent might respond with their bishop, threatening my queen. So, that's a bad move. What if I move my pawn instead? This opens up my rook..." This is their reasoning engine. 3. Action (Execution): They physically move a piece on the board. This is the action layer. 4. Reflection (State Update): The board is now in a new configuration. The grandmaster immediately updates their mental model of the game state based on this new reality. 5. Evaluation (Termination Check): They ask: "Is my opponent in checkmate? Have I blundered and lost a key piece? Has the game gone on for too long?" If the game is over, they stop. If not, the loop begins again, but now with a richer, more informed state.
An autonomous AI agent in the Semantic Kernel aims to emulate this grandmaster-like process. It doesn't just generate a single answer; it plays a "game" against the problem, using the loop to strategically refine its position until a winning (or satisfactory) state is achieved.
C# Constructs: The Scaffolding for Modern Agentic Loops
While the logic of the loop is abstract, its implementation in a modern, resilient system requires robust software engineering. This is where modern C# features become not just conveniences, but essential architectural tools.
1. Asynchronous Streams (IAsyncEnumerable<T>) for Real-Time Thought
An autonomous loop can be a long-running process. If the agent is performing a complex task, such as researching a topic by browsing multiple websites, the user shouldn't be left waiting for a single, monolithic response. They should see the agent's thought process unfold in real-time.
IAsyncEnumerable<T> is the perfect construct for this. The agent can yield return its thoughts, actions, and intermediate results as they happen.
using System.Collections.Generic;
using Microsoft.SemanticKernel;
public class ResearchAgent
{
private readonly IKernel _kernel;
// ... constructor ...
public async IAsyncEnumerable<string> ResearchTopicAsync(string topic)
{
// Initial thought
yield return $"Beginning research on: {topic}";
// First iteration: Search for primary sources
yield return "Searching for academic papers...";
// ... perform search, get results ...
yield return "Found 5 relevant papers. Analyzing summaries...";
// Second iteration: Synthesize findings
yield return "Synthesizing key themes from papers...";
// ... perform synthesis ...
yield return "Key themes identified: X, Y, Z. Generating report...";
// Final iteration: Final report
yield return "Research complete. Final report generated.";
}
}
This provides a streaming, responsive user experience, which is critical for user trust and engagement with long-running agents.
2. Records and Immutability for Predictable State
State management in a multi-turn loop is a notorious source of bugs. If state is mutable and shared, a function might inadvertently alter it, causing the reasoning engine to make decisions based on corrupted data. Modern C# record types provide a solution through immutability.
A record is a reference type that provides value-based equality and, crucially, supports non-destructive mutation via the with keyword. When an agent updates its state, it doesn't change the existing state object; it creates a new one with the updated values. This makes the state transitions explicit, predictable, and thread-safe.
// Using a record to define the agent's immutable state
public record AgentState(
string CurrentGoal,
int IterationCount,
List<string> PastActions,
string? LastObservation
);
// How state is updated non-destructively
public AgentState UpdateState(AgentState currentState, string newObservation)
{
// The 'with' expression creates a NEW record, leaving the original untouched.
return currentState with
{
IterationCount = currentState.IterationCount + 1,
LastObservation = newObservation,
PastActions = new List<string>(currentState.PastActions) { newObservation }
};
}
This pattern is crucial for debugging (you can inspect the entire history of states) and for ensuring the integrity of the agent's "memory."
3. Interfaces for Swappable Cognitive Architectures
The components of our loop—the planner, the termination checker, the plugins—are not monolithic. We may want to swap them out. Perhaps we want to test a simple rule-based planner against a sophisticated LLM-based planner. Or we may want to use a different set of plugins depending on the user's subscription level.
This is where interfaces are paramount. By defining the agent's core components as interfaces, we adhere to the Dependency Inversion Principle, making the system flexible and testable.
// Defines the contract for any reasoning engine
public interface IPlanner
{
Task<Plan> CreatePlanAsync(KernelContext context);
}
// Defines the contract for any termination logic
public interface ITerminationCondition
{
bool ShouldTerminate(KernelContext context);
}
// The agent depends on the interfaces, not concrete implementations
public class AutonomousAgent
{
private readonly IPlanner _planner;
private readonly ITerminationCondition _terminationCondition;
public AutonomousAgent(IPlanner planner, ITerminationCondition termination)
{
_planner = planner;
_terminationCondition = termination;
}
// ... loop logic uses _planner and _terminationCondition ...
}
This design is crucial for building AI applications because it allows us to easily swap between different models (e.g., OpenAI vs. Local Llama) or different termination strategies without rewriting the core agent logic. This concept was foundational in Book 7, Chapter 11, "Pluggable Architectures," where we learned that decoupling components is the key to building scalable and maintainable systems.
4. IAsyncEnumerable<StreamingKernelContent> for Streaming LLM Responses
When the agent's "Action" is to call an LLM (a semantic function), the response can be slow. Modern applications demand streaming responses. The Semantic Kernel's functions can return IAsyncEnumerable<StreamingKernelContent>, which allows the agent to process the LLM's output token-by-token.
This is not just a UI improvement; it's an architectural advantage. An advanced agent could analyze the stream as it's being generated. For instance, if the agent asks the LLM to generate a plan, and the first few tokens indicate a flawed approach, the agent could potentially cancel the generation and re-prompt, saving time and tokens. This turns the LLM from a black box into a more interactive and controllable component within the loop.
The Perils of Freedom: Guarding Against Infinite Loops and Hallucinations
A flawed termination condition that is never met. * A reasoning engine that keeps proposing the same ineffective action. * An external dependency that fails silently, leaving the agent's state unchanged.
Strict Iteration Limits: The agent's loop should have a hard-coded maximum number of cycles. This is the ultimate safety brake. 2. Stagnation Detection: The agent should monitor its state for significant changes. If the state is not evolving meaningfully after several iterations, it should self-terminate. 3. Human-in-the-Loop (HITL) Escalation: For critical tasks, the agent should be designed to pause and request human approval when it reaches a certain uncertainty threshold or a decision point with significant consequences.
Another danger is goal drift or hallucination within the loop. The agent might misinterpret its own results, leading it to believe it has achieved the goal when it hasn't, or it might start pursuing a tangential objective that it "hallucinated" from ambiguous data. This is why robust state management and clear, explicit reasoning prompts are so vital. The agent's "mental scratchpad" must be a source of truth, not a canvas for creative fiction.
Conclusion: The Theory of the Self-Correcting System
The theoretical foundation of autonomous loops is the shift from viewing AI as a function to be called, to viewing it as a system to be designed. This system is a dynamic, cyclical process of state-driven reasoning and action, governed by strict termination rules. It is built upon the pillars of modern software engineering: asynchronous processing for responsiveness, immutability for predictability, and interfaces for flexibility.
By mastering these theoretical principles, you are not just learning to write a while loop. You are learning to architect cognitive engines that can navigate complexity, adapt to failure, and pursue goals with a level of sophistication that approaches true autonomy. The subsequent sections will translate this theory into practice, providing the concrete patterns and code needed to bring these powerful, self-correcting agents to life.
Basic Code Example
Imagine you are building a simple "Smart To-Do List Assistant" for a user. The user says, "I need to buy milk and eggs." A basic agent might just add those items. But an autonomous agent should be able to reason: "The user wants to buy these items. Do I have enough information? Where should they go? Should I remind them later?" In this "Hello World" example, we will build an agent that simulates a Self-Correcting Task Manager. It will take a raw user request, attempt to process it, realize it lacks context (e.g., "How many?"), ask for that context, and then finalize the task.
This demonstrates the core loop mechanism: Perceive -> Reason -> Act -> Evaluate -> Repeat.
The Code Example
This example uses Microsoft Semantic Kernel (SK) to create an agent that iteratively refines a shopping list entry until all necessary details (Item Name and Quantity) are present.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;
using System.Text.Json;
// 1. Setup: Define the data structure for our task
public class ShoppingTask
{
public string ItemName { get; set; } = string.Empty;
public int Quantity { get; set; }
public bool IsComplete => !string.IsNullOrEmpty(ItemName) && Quantity > 0;
public override string ToString() => $"Task: Buy {Quantity}x {ItemName} [{(IsComplete ? "Ready" : "Pending")}]";
}
// 2. The "Tools" (Skills) the Agent can use to interact with the world
public class TaskManagerPlugin
{
[KernelFunction("update_task")]
[Description("Updates the current shopping task with specific details.")]
public ShoppingTask UpdateTask(
[Description("The name of the item to buy")] string itemName,
[Description("The quantity of the item")] int quantity)
{
Console.WriteLine($"[System]: Updating task details...");
return new ShoppingTask { ItemName = itemName, Quantity = quantity };
}
[KernelFunction("finalize_task")]
[Description("Marks the task as complete and ready for execution.")]
public string FinalizeTask(ShoppingTask task)
{
if (!task.IsComplete)
throw new InvalidOperationException("Task is not ready to be finalized.");
return $"✅ Finalized: {task}";
}
}
// 3. The Main Execution Loop
public class Program
{
public static async Task Main()
{
// --- Configuration ---
// NOTE: In a real scenario, replace with your actual Azure OpenAI or OpenAI key.
// For this demo, we use a fake client to ensure the code runs without external keys.
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: "gpt-4",
apiKey: "fake-key-for-demo")
.Build();
// Register our plugin
var plugin = new TaskManagerPlugin();
kernel.Plugins.AddFromObject(plugin, "TaskManager");
// Initial User Request
string userRequest = "I need to buy milk.";
// The "Memory" of the agent (Current State)
ShoppingTask currentTask = new ShoppingTask();
// --- The Autonomous Loop ---
Console.WriteLine($"🤖 Agent Loop Started. User Request: \"{userRequest}\"\n");
int maxIterations = 5; // Safety break to prevent infinite loops
int iteration = 0;
while (iteration < maxIterations && !currentTask.IsComplete)
{
iteration++;
Console.WriteLine($"--- Iteration {iteration} ---");
// A. Construct the prompt dynamically based on current state
string prompt = $"""
Analyze the user request: "{userRequest}".
Current Task State:
{JsonSerializer.Serialize(currentTask)}
If the ItemName is missing, identify it from the request or ask the user.
2. If the Quantity is missing, ask the user for it.
3. Use the 'update_task' function ONLY when you have both name and quantity.
4. If the task is complete, use 'finalize_task'.
5. If you need to ask the user a question, output the question directly.
""";
// B. Invoke the Kernel (Reasoning Step)
var result = await kernel.InvokePromptAsync(prompt);
// C. Parse the result to update state or interact
// In a real app, the LLM might return a JSON object or a natural language string.
// Here, we simulate the LLM's behavior for the sake of the "Hello World" example.
// *Note: In a production environment, the Kernel handles function calling automatically.*
// Simulating the LLM's decision logic for this standalone example:
// (Since we don't have a real LLM connected to execute the function calls automatically
// in this specific code snippet structure without complex orchestration setup)
if (string.IsNullOrEmpty(currentTask.ItemName))
{
// Simulated LLM reasoning: "Item name is missing from state, extract it."
currentTask.ItemName = "milk"; // Extracted from userRequest
Console.WriteLine($"🤖 Agent: Detected item is '{currentTask.ItemName}'.");
}
else if (currentTask.Quantity == 0)
{
// Simulated LLM reasoning: "Quantity is missing. I need to ask the user."
Console.WriteLine($"🤖 Agent: Quantity is unknown. Asking user...");
// Simulate user response
Console.Write("👤 User (simulated input): ");
string simulatedUserResponse = "2 gallons"; // Simulating user typing
Console.WriteLine(simulatedUserResponse);
// Parse user response for quantity
if (int.TryParse(new string(simulatedUserResponse.Where(char.IsDigit).ToArray()), out int qty))
{
currentTask.Quantity = qty;
Console.WriteLine($"🤖 Agent: Updated quantity to {qty}.");
}
}
// D. Check Termination Condition
if (currentTask.IsComplete)
{
Console.WriteLine($"🤖 Agent: Task is complete. Finalizing...");
// Call the final function
var finalResult = plugin.FinalizeTask(currentTask);
Console.WriteLine(finalResult);
break;
}
else
{
Console.WriteLine($"🤖 Agent: State updated. Looping again...");
}
}
if (iteration >= maxIterations)
{
Console.WriteLine("⚠️ Loop terminated due to max iterations reached.");
}
}
}
using Directives: We import the core Semantic Kernel libraries (Microsoft.SemanticKernel), Chat Completion services, and standard system libraries for JSON serialization and LINQ.
2. ShoppingTask Class: This is our State Container. In agentic loops, the agent needs a structured way to remember progress. This simple class holds the ItemName and Quantity, and a computed property IsComplete to determine if the loop should stop.
3. TaskManagerPlugin Class: This represents the agent's Tools.
* [KernelFunction]: This attribute marks methods as callable by the LLM.
* UpdateTask: This function allows the agent to modify the state (e.g., setting the item name and quantity).
* FinalizeTask: This function acts as the "Exit" point, validating that the task is ready.
4. Kernel Builder: We initialize the Semantic Kernel. Even though we provide a "fake" API key here, in production, this is where you would plug in Azure OpenAI or OpenAI credentials to give the agent real reasoning power.
5. Plugin Registration: kernel.Plugins.AddFromObject makes the methods defined in TaskManagerPlugin available to the LLM. The LLM sees these as available tools it can choose to invoke.
6. The Loop (while): This is the heart of the autonomous agent.
* Termination Condition: !currentTask.IsComplete ensures the loop runs only until the goal is achieved.
* Max Iterations: maxIterations = 5 is a safety rail. Without this, a confused LLM might loop infinitely, burning API credits and hanging the application.
7. Dynamic Prompt Construction: Inside the loop, we rebuild the prompt every time. We inject the currentTask state as JSON. This is crucial: the LLM doesn't have persistent memory of the variables in your code; you must feed the current state back into the prompt every single iteration.
8. kernel.InvokePromptAsync: This sends the prompt to the AI model. The model processes the instructions and the available tools (functions).
* Note on Function Calling: In a full implementation, the LLM would return a request to call update_task. The Kernel SDK handles the serialization and execution of that function automatically. In this simplified "Hello World" code, we simulate that logic to make the code runnable without a real LLM connection, but the structure remains identical to how you would implement it with a real model.
9. State Update: We check the current state. If ItemName is empty, we extract it. If Quantity is 0, we simulate asking the user. This demonstrates the Perception-Action cycle.
10. Finalization: Once IsComplete is true, we break the loop and call the final function to output the result.
Visualizing the Loop
The following diagram illustrates the flow of control within the autonomous agent.
The "Infinite Loop" Trap:
* The Mistake: Forgetting to update the state variable that the termination condition checks. If currentTask is passed by reference incorrectly or not updated within the loop, the condition !currentTask.IsComplete will never change, causing the loop to run forever.
* The Fix: Always ensure the state object is mutable or re-instantiated correctly within the loop. Always include a maxIterations safety break.
-
Context Amnesia:
- The Mistake: Failing to inject the current state into the prompt in every iteration. Developers often assume the LLM remembers the previous turn. It does not (unless using specific conversation history patterns).
- The Fix: Every loop iteration must construct a fresh prompt that includes the full serialized state of the current task.
-
Over-Reliance on Natural Language Parsing:
- The Mistake: Trying to parse complex data (like quantities or dates) from natural language responses using Regex or string manipulation inside the loop.
- The Fix: Use Kernel Functions (Tools) to enforce structure. Instead of asking the LLM to "tell me the quantity," ask it to "call the
update_taskfunction with the quantity." This moves the validation logic to your code (strongly typed) rather than the LLM's text output.
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Loading knowledge check...
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.