AI Agents · Ai

AI Agent Orchestration Patterns

AI Agent Orchestration Patterns is the work that defines the next phase of enterprise software. ERP systems hold the most valuable business data in the company — customers,.

John Kihiu12 min read

Orchestration is the part of an agent system that decides what happens next: which tool to call, whether to retry, when to stop and ask a human. It sounds abstract until the agent gets stuck in a loop calling the same failing tool five times, or stops one step short of finishing a task, and you realize there was no explicit control flow — just an LLM improvising the next step every time with no bounds on how long it could keep going.

The loop underneath every agent framework

Strip away the framework branding and every agent orchestration pattern is a loop: call the model, inspect whether it wants to call a tool, execute the tool if so, feed the result back, repeat until the model produces a final answer or you hit a stop condition. That loop, written directly, is maybe 30 lines of code. Frameworks add retry policies, parallel tool execution, and state persistence around that loop — genuinely useful once you need them, but the loop itself doesn't require a dependency to understand or debug.

PYTHON · THE LOOP, EXPLICIT
def run_agent(user_message: str, max_steps: int = 8) -> str:
    messages = [{"role": "user", "content": user_message}]
    for step in range(max_steps):
        response = call_model(messages, tools=TOOL_SCHEMAS)
        if not response.tool_calls:
            return response.content  # model is done, no more tools needed
        for call in response.tool_calls:
            result = execute_tool(call.name, call.arguments)  # validated, logged
            messages.append(tool_result_message(call.id, result))
    return "Reached max steps without a final answer — escalating to a human."

Bounding the loop is not optional

Every production agent needs a hard step limit and a hard time limit, enforced in code, not requested in the prompt. An LLM that decides to double-check the same fact three different ways will happily burn your entire latency and cost budget doing it if nothing stops it. When the limit is hit, the fallback should be a clear "I couldn't complete this, here's what I tried" handed to a human — not a silent failure and not an infinite retry.

Sequential vs parallel steps

Not every tool call needs to wait for the previous one. If a task needs the customer's credit status and their open order count, and neither depends on the other, fetch both concurrently rather than serially — it's a straightforward asyncio.gather, not a reason to adopt a graph-based orchestration framework. Reserve genuine sequential dependency (fetch the order, then validate against its actual line items) for cases where step 2 truly needs step 1's output.

Retries need a cap and a different strategy per failure type

A tool call that failed because of a transient network error should retry with backoff. A tool call that failed because the model passed an invalid parameter should not retry blindly — it should go back to the model with the error message so it can correct the argument, capped at one or two correction attempts before giving up. Treating both failure types the same way either wastes retries or gives up too early.

State that survives a restart

For anything that runs longer than a single request-response cycle — a multi-step approval workflow, a batch job processing hundreds of records — persist the loop's state (which step it's on, what's been done, what's pending) somewhere durable, not just in memory. If the process restarts mid-task, you want to resume from the last completed step, not silently lose a half-finished workflow or, worse, restart it and duplicate the tool calls that already ran.

When a framework earns its place

Reach for LangGraph, a durable-execution engine, or similar once you need: state that survives process restarts across long-running workflows, complex branching that a plain loop makes hard to read, or a team standard so multiple engineers write orchestration the same way. None of that is required for the common case of "user asks something, agent calls a couple of tools, agent answers" — that case is the 30-line loop above with a step cap and a timeout, and adding a framework for it just adds a debugging layer between you and what actually happened.

SituationRight tool
Single request, a few tool calls, donePlain loop with step/time limits
Independent lookupsConcurrent calls (asyncio.gather), not sequential
Multi-day workflow, needs to survive restartsDurable execution engine or persisted state machine
Complex conditional branching, multiple engineersGraph-based framework, for the shared vocabulary

Wrapping up

Orchestration is control flow with an LLM in the loop — treat it with the same discipline you'd apply to any retry logic: hard bounds, explicit failure paths, and state that survives a crash when the task is long-running. Start with the plain loop, add a framework only when you can name the specific capability (durable state, complex branching, team standardization) it gives you that the loop doesn't.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.