AYOUB.*
← EMERGING SYSTEMS ENGINEERINGREPOS ↗

Chapter 4 · Part II: AI-Native Systems, Agentic Workflows, and Platform Engineering

Agentic System Orchestration and Autonomous Workflows

An agent is a long-running workflow whose control flow is decided at run time by a model. Every property engineers rely on, from determinism to a bounded state space, has to be added back deliberately. This chapter covers the state graph, the sandbox, the memory layers, and the telemetry that a reasoning tree needs.

In this chapter
  1. 4.1 Non-Deterministic State Machine Engineering
  2. 4.2 Tool Use, Action Spaces, and Sandboxing
  3. 4.3 Long-Horizon Memory and Persistence Layers
  4. 4.4 Distributed Agent Observability
  5. Sources and further reading

4.1 Non-Deterministic State Machine Engineering

An agent loop is four steps: observe, decide, act, repeat. The model receives context, requests a tool, the system runs the tool, the result returns, and the loop continues until the model produces an answer or a limit stops it.

That loop has a property no other production system has. The control flow is chosen at run time by a component that gives a different answer to the same input. Every engineering technique that assumes deterministic control flow stops applying, and the response is to put the structure back explicitly.

Why an unconstrained loop fails

An agent that can call any tool from any point can reach any state from any state. That is a complete graph, and it has four consequences that appear in production within days.

It cannot be tested, because the set of paths is unbounded and a test exercises one arbitrary path. It cannot be reasoned about, because no invariant holds across an arbitrary transition. It cannot be constrained, because there is no place to attach a precondition. And it does not terminate reliably: the model calls a search tool, receives an unhelpful result, decides to search again with a slightly different query, and repeats until something external stops it.

That last failure is the one that produces an invoice. It is not a bug in the model. It is the predictable behavior of a loop with no termination condition other than the model's own judgment.

The explicit state graph

The fix is to name the states and the legal transitions. An agent moves through planning, retrieving, calling a tool, waiting for approval, summarizing, and then to one of a small number of terminal states.

                     +--------------------+
      start -------> |    PLAN            | <---------+
                     +---------+----------+           |
                               |                      | replan, max 2
              +----------------+----------------+     |
              v                v                v     |
        +-----------+   +-------------+   +----------+|
        | RETRIEVE  |   | CALL_TOOL   |   | ASK_USER |
        +-----+-----+   +------+------+   +-----+----+
              |                |                |
              |         +------+------+         |
              |         | risk == high|         |
              |         v             |         |
              |   +-----------+       |         |
              |   | AWAIT_    |       |         |
              |   | APPROVAL  |       |         |
              |   +-----+-----+       |         |
              |         | approved    | low     |
              +---------+-------------+---------+
                        v
                  +-----------+
                  | OBSERVE   |----> back to PLAN, or ->+
                  +-----------+                          |
                                                          v
                                                   +------------+
                                                   | SUMMARIZE  |
                                                   +-----+------+
                                                         v
                    +----------+   +----------+   +-----------+
                    | DONE     |   | FAILED   |   | ABORTED   |
                    +----------+   +----------+   +-----------+
                                                  budget exhausted

  Every transition has a guard. Every state has a timeout. Every path
  reaches a terminal state. Budgets live on the graph, not in the prompt.

Figure 4.1 A bounded agent state graph, with guards and budgets.

Four properties follow, and each one removes a class of production incident.

A stuck run names the state it is stuck in, rather than presenting an opaque loop with a rising token count. A state can require human approval, which is the only workable control for an action with irreversible consequences. A test can drive the machine through every transition with a stubbed model, giving a suite that does not depend on model output. And the budget has somewhere to live: a step limit, a wall-clock limit, and a token limit enforced by the graph rather than requested in a prompt.

The last point deserves emphasis because teams get it wrong in a specific way. A prompt that says "use at most five tool calls" is a request. A state machine that transitions to ABORTED on the sixth is a control. Only the second one holds when the model is confused, when the input is adversarial, or when a model upgrade changes the behavior.

Multi-agent structures

Once one agent works, the pressure is to add more. Three structures cover almost every real system and they differ in where the coordination lives.

A supervisor holds a graph and delegates sub-tasks to specialized workers, which return results and do not talk to each other. Coordination is in one place, the flow is inspectable, and the supervisor is a bottleneck and a single point of failure.

A pipeline chains agents where each output is the next input. It is simple and testable, and it cannot revisit an earlier stage when a later one discovers that the earlier one was wrong.

A blackboard gives agents a shared state store that they read and write, with a controller deciding who runs next. It is flexible, and its behavior emerges rather than being specified, which makes debugging hard.

Start with a supervisor. It is the structure that a team can reason about, and the coordination cost of the alternatives is rarely repaid.

The strongest available argument against adding agents is cost arithmetic. Each agent in a chain has an independent probability of a step that is good enough. At 95 percent per agent, a five-agent chain succeeds 77 percent of the time and a ten-agent chain 60 percent. Reliability falls geometrically with depth, so every added agent must earn its place, and the way it earns it is a validation gate that catches its failures rather than passing them downstream.

Fallback routing is where cost overruns hide

A failure path that returns to planning is correct and it is also a cycle. Without a bound, a model that cannot make progress will replan indefinitely, and each replan costs a full context window. Cap the replans at a small integer, make the cap a transition in the graph rather than an instruction in the prompt, and make the terminal failure state return something useful to the caller. An agent that fails cleanly after four attempts is a working system. One that retries until a budget alert fires is an incident.

4.2 Tool Use, Action Spaces, and Sandboxing

A tool is what turns a text generator into a system that acts. It is also the entire attack surface, because a tool is by definition a way for model output to cause an effect.

The action space is a security decision

Every tool a model can call is an action an attacker can induce it to take. The set of available tools is therefore not a capability decision, it is the system's permission set.

Two rules follow. Give an agent only the tools its task requires, scoped to the narrowest operation that works, rather than a general-purpose tool that can do anything. A tool that reads one customer's order history is safe. A tool that runs arbitrary SQL is a database credential with extra steps.

And separate tools by risk. A read-only tool with no side effects can be called freely. A tool that writes reversible state needs logging and a rate limit. A tool with irreversible consequences, which means money, communication with a third party, or deletion, needs an approval gate in the state graph.

Table 4.1: tool risk classes and the control each one needs

Class Examples Control Sandbox
Read, internal search an index, read a record rate limit, per-user scope none needed
Read, external fetch a URL, call a partner API allowlist, timeout, size cap egress policy
Write, reversible update a draft, add a tag audit log, idempotency key per-user credential
Write, irreversible charge, send, delete, deploy human approval, two-person rule per-user credential
Code execution interpreter, shell, notebook full isolation, no network, no persistence microVM or equivalent
Agent delegation call another agent depth limit, budget inheritance the callee's own controls

Sandboxing a code interpreter

A code execution tool runs model-generated code. The model is influenced by input the system does not control, so the code must be treated as hostile.

Volume II covered the isolation boundaries. The decision rule here is short: a shared kernel is not sufficient for model-generated code. A container with a seccomp profile stops a careless program and does not stop a deliberate kernel exploit, and the code came from a component that an attacker can influence.

Use a hypervisor boundary. A microVM starts in around 125 milliseconds, which is acceptable when the alternative is a cross-tenant breach. Inside it, five controls do the remaining work.

  • No network by default, and an explicit egress allowlist where a task genuinely needs one.
  • No persistent storage. The sandbox is created per session and destroyed after it.
  • A wall-clock limit and a memory limit, enforced by the hypervisor rather than by the guest.
  • No credentials inside. Data goes in as input and comes out as output, and the sandbox never holds a token that reaches anything else.
  • A fresh instance per session, because state that survives between users is a cross-tenant channel.

The last control is the one most often traded away for start-up latency, and it is the one that produces the worst incident. A pooled sandbox that carries a previous user's files is a data leak that no code review finds.

Tool invocation as an untrusted interface

The model produces the arguments to every tool call. Those arguments are untrusted input, and the tool must treat them the way a public HTTP endpoint treats a request body.

# The tool boundary is a trust boundary. Validate as if the caller
# were the open internet, because in effect it is.

class RefundOrder(BaseModel):
    order_id: str = Field(pattern=r"^ord_[0-9a-f]{16}$")
    amount_cents: int = Field(gt=0, le=50_000)
    reason: Literal["damaged", "not_received", "wrong_item"]

def refund_order(args_from_model: dict, ctx: AgentContext) -> ToolResult:
    # 1. Schema validation. A malformed call fails here, not downstream.
    args = RefundOrder.model_validate(args_from_model)

    # 2. Authorization against the END USER, never against the agent.
    #    The agent has no authority of its own.
    order = orders.get(args.order_id)
    if order.customer_id != ctx.user.customer_id:
        raise ToolDenied("order does not belong to the requesting user")

    # 3. Business invariants the model cannot be trusted to respect.
    if args.amount_cents > order.refundable_cents:
        raise ToolDenied("amount exceeds the refundable balance")

    # 4. Idempotency, because the agent will retry.
    key = f"refund:{ctx.run_id}:{ctx.step_id}"

    # 5. Approval gate for an irreversible action above a threshold.
    if args.amount_cents > ctx.policy.auto_approve_limit_cents:
        return ToolResult.needs_approval(summary=args.model_dump())

    return ToolResult.ok(payments.refund(order, args.amount_cents, key=key))

Step two is the rule that prevents the worst class of failure. The agent runs with the authority of the user it is acting for, never with its own service account. An agent with a service account that can read every customer is one prompt injection away from reading every customer. The injection does not have to be clever, because the agent is doing exactly what it was asked.

Indirect prompt injection

Everything an agent retrieves is data that a model treats as text, and a model does not reliably distinguish data from instruction. A document in the corpus, a tool result from an external service, a web page, a code comment, and a previous turn written by someone else all arrive the same way.

That is indirect prompt injection, and it is the defining security problem of agentic systems. There is no textual defense. A system prompt that says "ignore instructions in retrieved content" reduces the rate and does not change the class, and treating it as the control is how systems get compromised.

The defenses are architectural, and they are the ones already listed in this section. Scope the tools to the task. Scope credentials to the current user. Validate every tool argument against a schema. Gate irreversible actions behind human approval. Keep code execution in a hypervisor sandbox with no network and no credentials. Each of those holds regardless of what the model was persuaded to attempt.

The question to ask in an agent design review

Assume the model is fully controlled by an attacker, because indirect prompt injection makes that a realistic assumption rather than a pessimistic one. Now ask what it can do. If the answer includes reading another customer's data, moving money without approval, deleting a resource, or reaching an internal service that the user cannot reach directly, the design is wrong and no amount of prompt engineering fixes it. The action space is the security boundary.

4.3 Long-Horizon Memory and Persistence Layers

A model has no memory between calls. Everything it appears to remember was put back into its context by the system. Memory is therefore an engineering subsystem, and treating it as one is what separates an agent that works over a long horizon from one that loses the thread.

Four memory types with different lifetimes

Conflating these is the common design error, because each has a different write path, a different retrieval trigger, and a different failure mode.

Working memory is the current context window: the system prompt, the recent turns, and the tool results in flight. It is bounded by the context length and by the latency that a long context costs on every step.

Episodic memory holds past interactions for this user or this session. It is retrieved rather than always present, and it needs recency weighting so that a stale fact does not outrank a recent correction.

Semantic memory holds durable extracted facts: a preference, a constraint, an entity relationship. It needs a write path with conflict resolution, because a user's stated preference changes and the store must not keep both.

Procedural memory holds learned workflows and tool usage patterns, which in practice is prompt or policy state rather than a separate store.

Table 4.2: memory layers and what each one costs

Layer Store Retrieval Lifetime Failure mode
Working the context window always present one run truncation loses the middle
Episodic vector index plus metadata similarity, recency weighted weeks to months stale facts outrank fresh ones
Semantic key-value or graph structured lookup by entity indefinite contradictory facts coexist
Procedural prompt or policy store always present until changed drifts from actual behavior
Scratch key-value, per run explicit read and write one run leaks between runs if not scoped

Compaction, and what it loses

Working memory fills. Two mechanisms keep a long run viable and they lose different things.

Summarization compresses older turns into a shorter form. It is the usual choice, it preserves the thread of the conversation, and it loses detail irreversibly. The specific loss that hurts is an exact identifier: an order number or an error code mentioned twenty turns ago disappears into "the user asked about a delayed order".

Offloading writes the detail to an external store and keeps a reference in context. It preserves everything and requires the agent to retrieve deliberately, which it will sometimes fail to do.

The practical answer combines them. Summarize the narrative and extract the facts. A compaction step that produces both a prose summary and a structured list of entities, identifiers, decisions, and open questions keeps the thread and keeps the details that a summary destroys.

Two further rules matter. Never compact the system prompt or the most recent turns, because the model needs both intact. And make compaction observable, recording what was compacted and when, because a run that goes wrong after compaction is otherwise impossible to diagnose.

Structured retrieval beats similarity alone

Vector similarity finds semantically related text and misses exact terms. A query about order ord_4471 retrieves plausible neighbors rather than that order, which is precisely the wrong behavior for an agent that needs a fact.

Three additions fix it, and each targets a different failure.

Hybrid retrieval runs lexical search alongside vector search and fuses the ranked lists, which recovers exact identifiers, error codes, and rare proper nouns.

A graph layer holds entities and relationships explicitly. "Which orders belong to this customer" is a traversal, not a similarity search, and traversal gives a complete and correct answer where similarity gives a plausible one.

Metadata filtering scopes retrieval to the current user, the current session, and the relevant time range. It is also the authorization boundary, and it must be applied during retrieval rather than after it. Filtering after the fact both returns too few results and leaks the existence of what it removed.

Memory is an injection surface and a privacy surface at once

Anything written to long-term memory is retrieved later and placed in a context window, so a malicious instruction written into memory persists across sessions. Treat every memory write as untrusted content: extract structured facts rather than storing raw text where possible, and never store retrieved third-party content as though the user had said it. Separately, memory accumulates personal data that nobody designed a retention policy for. Give every memory record a subject, a purpose, and an expiry, and build the deletion path before the store has a million records in it.

4.4 Distributed Agent Observability

An agent run is a trace whose shape is decided at run time. Standard application telemetry answers none of the questions that matter, because the failures are semantic rather than technical: a run returns HTTP 200, costs four dollars, and produces a wrong answer.

The unit is the step

A run is a trace and each step is a span. Every step needs the model identifier and version, the token counts in both directions, the latency, and the computed cost. It also needs the tool name, the tool arguments, the result size, the state the machine was in, and the outcome.

That last field is what makes the data useful. Record the semantic outcome alongside the technical one: the tool succeeded and returned nothing relevant, the model chose a tool the task did not need, the retrieval returned documents that did not contain the answer. None of those is an error at the transport level and all of them explain a bad run.

  run 9f3a2c  user=u_88  policy=support-agent-v4     total 11.2s  $0.41
  |
  +- PLAN                    gpt-class  in 2,140  out 180   1.4s  $0.03
  |    outcome: chose retrieve + call_tool
  |
  +- RETRIEVE                                             0.4s  $0.00
  |    +- vector search    k=50  filter=customer:c_412    0.2s
  |    +- lexical search   k=50                           0.1s
  |    +- rerank           50 -> 8                        0.1s  $0.01
  |    outcome: 8 documents, top score 0.31   <-- LOW, worth an alert
  |
  +- CALL_TOOL  get_order_status                          0.3s  $0.00
  |    args: {order_id: "ord_4471"}   result: 412 bytes
  |    outcome: ok
  |
  +- PLAN  (replan 1 of 2)   in 6,890  out 240            2.1s  $0.09
  |    outcome: chose call_tool again    <-- context grew 3x
  |
  +- CALL_TOOL  get_order_status                          0.3s  $0.00
  |    args: {order_id: "ord_4471"}   <-- IDENTICAL ARGUMENTS
  |    outcome: ok                          a loop is forming here
  |
  +- PLAN  (replan 2 of 2)   in 9,410  out 310            2.6s  $0.13
  +- SUMMARIZE               in 9,900  out 420            4.1s  $0.15
       outcome: answered

Figure 4.2 One agent run as a trace, with the cost attached.

The picture shows three things that no aggregate metric shows. Retrieval returned a low top score, which predicts a poor answer. A tool was called twice with identical arguments, which is the beginning of a loop. And the context grew by a factor of three across replans, which is where the cost went.

The failures that only show at this granularity

Four classes appear nowhere else.

A wrong tool choice looks like a successful run with an unhelpful answer. Find it by recording the tool chosen alongside the tool that a labelled example says was correct.

An empty or irrelevant retrieval looks identical to a successful one at the transport layer. Record the top relevance score and the count, and alert when the score distribution shifts, because that is what a corpus change or an embedding model change looks like.

Plan drift accumulates across steps and is invisible in any single one. Record the stated objective at the start and compare it against the actions taken.

A loop shows as a rising step count rather than as an error. Detect it directly by hashing the tool name and arguments and counting repeats within a run.

Cost as a first-class signal

A single agent run can cost a thousand times an ordinary request, and the cost is decided by a component that makes different decisions on different days.

Track cost per run, per user, and per tenant. Alert on the rate of change rather than the absolute, because the absolute is budgeted and a sudden change is a regression. Enforce a per-run budget in the state machine. And record cost per successful outcome rather than cost per run, because a cheap run that fails and is retried by the user is not cheap.

Token growth across steps is the specific thing to watch. Context grows as results accumulate, and cost grows with context, so a run that takes twice as many steps costs more than twice as much. The trace above shows the effect: the final planning step cost four times the first one for the same work.

Evaluation closes the loop

Telemetry describes what happened. Evaluation says whether it was any good, and three layers are needed.

Unit tests with a stubbed model cover the state machine: every transition, every guard, every budget. They are fast, deterministic, and they test the engineering rather than the model.

Offline evaluation runs a fixed set of cases through the real system and scores the outputs against a rubric. It catches regression when a prompt changes, a model version changes, or a tool changes. The case set is the asset, and it grows by adding every production failure to it.

Online evaluation samples production runs and scores them, which is the only mechanism that catches a distribution shift in real traffic. It needs a sampling strategy that over-samples the interesting cases: every run that hit a budget limit, every run with low retrieval scores, and every run where the user rephrased immediately afterward. That last one is the strongest available implicit signal that an answer was wrong.

A team with only the first layer believes the system works until a user says otherwise.

Sources and further reading

The OWASP Top 10 for Large Language Model Applications, and its agentic security guidance, for the threat model and for prompt injection in particular. Simon Willison's writing on prompt injection and on the lethal trifecta of private data, untrusted content, and external communication. The OpenTelemetry generative AI semantic conventions, noting that they were still marked development through mid-2026. The Temporal documentation on durable execution and workflow determinism, which Volume II covers and which this chapter assumes. The published architecture notes for graph-based agent frameworks, for state machine patterns. The NIST AI Risk Management Framework for the governance vocabulary.

What this chapter established

  • An agent's control flow is chosen at run time by a component that gives different answers to the same input. Every property that assumes determinism must be added back explicitly.
  • An unconstrained loop can reach any state from any state, which makes it untestable, unconstrainable, and non-terminating.
  • An explicit state graph gives named states, guarded transitions, per-state timeouts, approval gates, and somewhere for budgets to live.
  • A prompt that requests a limit is a request. A transition that enforces it is a control. Only the second holds under adversarial input.
  • Reliability falls geometrically with agent count. At 95 percent per agent, a ten-agent chain succeeds 60 percent of the time.
  • Bound the replan cycle with a small integer, or a model that cannot make progress replans until a budget alert fires.
  • The set of available tools is the system's permission set, not a capability list. A tool that runs arbitrary SQL is a database credential with extra steps.
  • Model-generated code needs a hypervisor boundary, no network, no credentials, no persistence, and a fresh instance per session.
  • Tool arguments are untrusted input. Validate against a schema, and authorize against the end user rather than against the agent.
  • Indirect prompt injection has no textual defense. The controls are architectural: scoped tools, scoped credentials, schema validation, and approval gates.
  • Assume in design review that an attacker fully controls the model, then ask what it can reach.
  • Memory is four subsystems with different lifetimes and different failure modes. Conflating them is the common design error.
  • Summarization loses exact identifiers. Compact into a prose summary plus a structured fact list, and never compact the system prompt or the recent turns.
  • Similarity search misses exact identifiers. Add lexical retrieval, a graph layer for relationships, and metadata filtering applied during retrieval rather than after it.
  • A memory write is an injection surface that persists across sessions and a privacy surface that needs a retention policy from the start.
  • Record the semantic outcome of each step, not only the technical one. A run that returns 200, costs four dollars, and answers wrongly is the normal failure.
  • Watch token growth across steps. Cost grows faster than step count, because context accumulates.
  • Three evaluation layers are needed: stubbed unit tests for the machine, offline scoring for regression, and online sampling for distribution shift.

Chapter 4 of Emerging Systems Engineering, revision 1.0. Attribution and sole author credit: Ayoub Zulfiqar.