« Phase 01 · Track Overview

Warmup — The Agent Kernel, From Zero

Assumes Python and HTTP. Assumes nothing about agents, state machines, consistent hashing, memory architectures, or why any of this belongs in a "kernel." By the end you will be able to design an agent runtime that a bank can run and an auditor can read.


Table of Contents


1. What an agent actually is

1.1 The loop, stripped to its bones

Strip away every framework and an agent is eleven lines:

scratchpad = [goal]
while True:
    decision = model(render(scratchpad))     # (a) ask what to do next
    if decision.is_final:
        return decision.answer
    result = tools[decision.tool](decision.args)   # (b) do it
    scratchpad.append((decision, result))          # (c) feed it back

That is the whole idea. Three steps: reason, act, observe, repeated until the model says it is done. Everything else in this phase — states, budgets, memory tiers, checkpoints — is a consequence of the fact that this loop, written exactly as above, is unsafe to run in a bank.

1.2 Why the loop is dangerous

Read it again as an operator rather than a developer, and count the ways it fails:

LineWhat goes wrong
while Truenothing bounds it. A confused model loops until something else kills it — and the something else is usually your bill.
model(render(scratchpad))scratchpad grows every iteration, so input tokens grow quadratically (derived in §4.1).
tools[decision.tool]decision.tool came from a language model. It may not exist. A KeyError here kills a customer's conversation.
tools[...](args)the arguments also came from a language model. They may be the wrong types, the wrong account, the wrong amount.
the whole functionstate lives in a local variable. The pod restarts; the conversation is gone.
the whole functionnothing is recorded. When Audit asks what happened, you have nothing.
implicitlythere is no notion of pausing for a human. A payment release either happens silently or does not happen.

Every one of those is fixed by moving a responsibility out of the agent and into the runtime. That runtime is the kernel.

1.3 Why "kernel" is the right word

An operating-system kernel exists because you cannot trust a program. Not because programs are malicious — because they are arbitrary. So the kernel:

  • bounds what a process may consume (memory limits, CPU quotas, file descriptors);
  • mediates privileged access (syscalls, not direct hardware);
  • owns the process's metadata (the process table, page tables, scheduling state);
  • records what happened (accounting, audit);
  • survives the process misbehaving (an illegal instruction traps; it does not halt the machine).

Now substitute:

OS kernelAgent kernel
processagent run
memory limit, CPU quotatoken budget, step budget, cost ceiling, deadline
syscall interfacetool dispatch through a registry
process tablesession store
page tables held by the kernelsession state externalized, not in worker memory
SIGKILL on quota breachFAILED with a BudgetBreach
illegal instruction → trap, not crashhallucinated tool name → observation, not exception
process accountingexecution chain

The mapping is not a metaphor for teaching. It is the actual design, and it tells you where each responsibility belongs whenever you are unsure. If a process would not be trusted to decide it, an agent is not either.

2. Reasoning loops: ReAct, ReWOO, plan-execute-replan

The kernel hosts a loop; it does not have to host only one shape of loop. Three shapes matter.

2.1 ReAct

Reason + Act, interleaved. One step at a time: think, call one tool, see the result, think again. This is what the eleven-line loop above does, and it is the default in almost every framework.

  • Strength: maximally adaptive. Each decision sees every prior observation, so the agent can recover from surprises.
  • Weakness: maximally expensive. Every step re-sends the whole scratchpad (§4.1), and every step is a serial model round-trip, so latency is n × TTFT at best.
  • Reliability: \( p^n \) with a large n, because ReAct tends to produce many small steps.

2.2 ReWOO

Reasoning WithOut Observation. Plan the entire tool sequence up front, using variable references for results that do not exist yet:

Plan:
  #E1 = lookup_payment[reference="PMT-771"]
  #E2 = check_sanctions[name=#E1.beneficiary]
  #E3 = fetch_policy[topic="sanctions hold release"]
Solve: given #E1, #E2, #E3, answer the user's question.

Then a worker executes the plan (no model involved — this is plain code), and a final solver call produces the answer.

  • Strength: two model calls instead of n. Enormously cheaper and lower-latency, and the scratchpad never accumulates because the planner sees only the goal.
  • Weakness: cannot adapt. If #E1 returns something unexpected, the rest of the plan is wrong, and the agent discovers this only at the solve step.
  • Where it wins: well-understood, repetitive workflows — which describes most banking investigations. This is why a bank platform should support ReWOO, not only ReAct.

2.3 Plan-execute-replan

The hybrid: plan the whole chain, execute it, and re-enter the planner only when an observation violates the plan's assumptions. In the best case you pay ReWOO's cost; in the worst case you degrade to ReAct.

The interesting engineering is the trigger. "Violated an assumption" must be detectable in code, not by asking the model — otherwise you have paid for a model call to decide whether to make model calls. Practical triggers: a tool returned an error; a returned value failed a schema or range check; a step produced an empty result where the plan assumed non-empty.

2.4 What the kernel supports vs imposes

The kernel imposes: the lifecycle, the budgets, the state model, the memory tiers, the execution chain. Those are platform invariants; an agent author who could opt out of them could opt out of your SLO and your audit trail.

The kernel supports: the loop shape. ReAct, ReWOO, and plan-execute-replan all reduce to "call a policy, get a decision, dispatch or finish," which is exactly the interface in the lab. Making the loop shape pluggable while the invariants are fixed is the central architectural decision of this phase, and it is what separates a platform kernel from a framework.

3. Lifecycle as a state machine

3.1 Why a table and not if statements

Most agent runtimes track state with booleans: is_running, waiting_for_input, done. With three booleans there are eight combinations, of which perhaps four are meaningful, and nothing prevents the other four. done=True, is_running=True is representable, and one day it will be represented.

A declared transition table makes illegal states unrepresentable in a way you can prove:

TRANSITIONS = {(RunState.PLANNING, Event.PROPOSE): RunState.ACTING, ...}

def transition(state, event):
    if state in TERMINAL_STATES:
        raise IllegalTransition(state, event)
    try:
        return TRANSITIONS[(state, event)]
    except KeyError:
        raise IllegalTransition(state, event) from None

Three properties fall out for free, and all three are testable:

  1. Every legal edge is enumerable — you can print the state machine, put it in a design doc, and hand it to a reviewer.
  2. Terminal states are absorbing — one check, not one check per call site.
  3. No trap states — you can assert that every non-terminal state has an edge to a terminal one, which is a real bug class (a waiting_input session that can never be cancelled leaks forever).

3.2 The eight states, justified one at a time

StateWhy it existsWhat it must not do
createda session exists before it runs — it has an owner, a tenant, a goal, and an audit identity from the moment it is createdrun
planningthe model is deciding. This is where cost is incurred and where finishing is legaldispatch a tool
actinga tool is executing. Separated from planning because the failure modes are entirely different — a tool timeout is not a model timeout, and only one of them is retryable in placefinish. acting → completed is deliberately illegal: the tool result must be observed and reasoned over before an answer exists. Skipping that is how agents "answer" with a tool result they never read
waiting_inputhuman-in-the-loop. The run is alive but not consuming, possibly for hoursconsume budget while parked
suspendedcheckpointed and evictable — the kernel reclaimed the worker. Distinct from waiting_input because the reason differs (kernel-initiated vs agent-initiated) and so does the resume pathbe resumed with an answer
completedthe goal was metanything
faileda budget breach, an unrecoverable error, or an illegal stateanything
cancelleda human or the control plane stopped it. Distinct from failed because "we killed it" and "it broke" are different rows in every incident report and every audit queryanything

The acting → completed prohibition is the one interviewers probe, because it looks like needless ceremony until you have seen the bug it prevents.

3.3 Absorbing states and the trap invariant

Absorbing: once in completed/failed/cancelled, no event applies. This is what makes "an agent cannot act after it has completed" a property rather than a hope, and it is what makes a replayed message safe — a duplicate finish on a completed session raises rather than producing a second answer.

No traps: for every non-terminal state there is at least one event leading to a terminal state. In the lab this is a test that iterates the table. Without it, you eventually ship a state whose only exits are back into itself — and you find out when sessions accumulate in a dashboard.

4. Memory architecture

The JD asks for "short-term, long-term, episodic." These are not three implementations of one idea; they are three different data structures with three different lifetimes and — this is the part that matters in a bank — three different partition keys.

4.1 The scratchpad, and its quadratic problem

The scratchpad is the accumulating thought/action/observation record fed back to the model each turn. It is short-term memory: it lives for one run.

Its cost is derived exactly as in Phase 00. With base prompt b and per-step addition a, step i sends \( b + a(i-1) \) input tokens, so a run of n steps sends

$$T_{\text{in}} = \sum_{i=1}^{n}\big[b + a(i-1)\big] = nb + a\frac{n(n-1)}{2}$$

At b=1 000, a=2 000: ten steps cost 100 000 input tokens, twenty steps cost 400 000. Doubling the steps quadrupled the cost. Nothing about the model changed.

There is a second, harder limit: the context window. At some n the scratchpad simply does not fit, and the run dies with a provider error that looks like a bug and is actually arithmetic.

4.2 Compaction, derived

The fix is to bound the scratchpad. Three strategies, and the kernel should own the choice:

  1. Truncation — drop the oldest steps. Cheap, and it silently loses the fact that made the whole investigation make sense.
  2. Compaction (summarization) — fold old steps into a summary, keep the recent window verbatim. Costs a model call, preserves the gist.
  3. Retrieval over the scratchpad — index every step and retrieve the relevant ones for each turn. Most faithful, most complex, and it turns every turn into a retrieval problem.

The lab implements (2) because it is what production runtimes actually do, and it encodes two rules that are easy to get wrong:

Rule 1 — never compact away the recent window. The model needs the last step or two verbatim to decide what to do next. The lab's compact_if_needed returns False rather than compacting when only the recent window remains — even if still over budget. Over budget with context is recoverable; under budget with amnesia is not.

Rule 2 — compaction is lossy for the model and never for the record. The summary replaces detail in the scratchpad, but every step is already checkpointed. execution_chain() reads from the store, so the audit artifact is complete even when the model's working set is not. Getting this backwards — compacting the persisted record — is a finding waiting to happen.

The economics: compaction converts a quadratic term into a piecewise-linear one. Once the pad is capped at M tokens, each step costs at most M, so a run costs \( O(nM) \) instead of \( O(n^2 a) \).

4.3 Semantic memory and the partition key

Semantic memory holds durable facts: "the relationship manager for Acme is Layla Al Mansouri," "this tenant's risk appetite is conservative," "this user prefers answers in Arabic."

The design question that matters is not storage — it is scope. Every fact belongs to exactly one of:

ScopeOwnerLifetimeExample
usera personas long as the person uses the platformlanguage preference, saved filters
tenanta business unitas long as the tenant existsthe RM for a client, the tenant's approval thresholds
appthe platformforeverthe ISO 20022 message catalogue

The lab keys facts on (scope, owner, key) and — critically — filters by visibility before ranking:

visible = [f for f in self._facts.values() if scopes.get(f.scope) == f.owner]
scored  = rank(visible, tags)

Do it the other way (rank everything, then filter) and you have built the same defect as a shared vector index with a post-hoc filter: the ranking leaks information about what exists, and one refactor away, the filter gets dropped. Authorization is a retrieval predicate, not a post-processing step — a rule you will meet again in Phase 06 and Phase 09.

4.4 Episodic memory

Episodic memory records what happened: a completed task, its goal, its outcome, how many steps it took, and what was learned. Recalled by similarity to the current task.

Why it is a separate tier: semantic memory answers "what is true?"; episodic memory answers "what happened last time I tried this?" An agent investigating a held payment benefits from "the last three times this vendor's payments were held, it was a name-matching false positive" — that is not a fact about the world, it is a fact about episodes, and indexing it as a fact loses the outcome, the step count, and the recency that make it useful.

Recall ranks by tag overlap, then recency. Recency matters more here than in semantic memory because episodes decay: a lesson from last week is worth more than one from last year, and the tie-break encodes that without needing a decay function.

4.5 Choosing a tier

A decision rule you can apply in a design review:

  • Does it matter only within this run? → scratchpad.
  • Is it a durable statement about the world, a user, or a tenant? → semantic, with an explicit scope.
  • Is it a record of an attempt and its outcome? → episodic.
  • Is it needed as evidence? → none of the above — it goes in the execution chain, which is persisted and immutable. Memory tiers are for usefulness; the chain is for truth.

That last bullet is the one people miss. Memory is a performance and quality feature. Evidence is a separate, non-negotiable artifact.

5. State, checkpoints and concurrency

5.1 Why state must leave the process

If a run's state lives in a worker's memory, then:

  • a deploy kills every in-flight conversation;
  • an autoscaler scale-in kills a subset, chosen arbitrarily;
  • a crash loses work with no way to resume;
  • human-in-the-loop is impossible beyond the process's lifetime — you cannot park a run for four hours waiting for an approver if the pod restarts hourly;
  • horizontal scaling requires sticky routing to work at all, rather than as an optimization.

Externalizing the state fixes all five at once. SessionSnapshot in the lab is deliberately immutable and complete: everything needed to reconstruct the run is in it (goal, state, steps, summary, counters, pending question, identity). Reconstruction is then a pure function of the snapshot, which is what makes resumption testable.

5.2 Optimistic concurrency, derived

Externalized state introduces a new problem: two workers may try to advance the same session. A retried message, a duplicated queue delivery, or a user double-clicking are all ordinary.

Two families of solution:

  • Pessimistic locking — take a lock before reading, release after writing. Correct, and it requires lock timeouts, lease renewal, and a story for a worker that dies holding the lock.
  • Optimistic concurrency control (OCC) — read with a version, write conditionally on that version, and reject the write if the version moved.

OCC wins here because conflicts are rare (the same session is usually advanced by one worker) and the cost of a conflict is low (retry the whole step). The lab implements the canonical compare-and-swap:

def save(self, snapshot, *, expected_version):
    current = self.load(snapshot.session_id)
    if current.version != expected_version:
        raise ConcurrentModification(...)
    return store(replace(snapshot, version=expected_version + 1))

The property this buys, and the sentence to say in an interview: exactly one writer wins, and the loser knows it lost. A last-write-wins store silently interleaves two runs' steps into one chain, which is both a correctness bug and an audit disaster — the record would show a sequence of actions that no single execution ever performed.

In production this is UPDATE ... WHERE version = ? in Postgres, a conditional write in DynamoDB, or an ETag precondition in blob storage. Same idea, same failure mode if you skip it.

5.3 Checkpointed is not durable

The lab checkpoints after each observation. So if the worker dies:

  • between steps → resume cleanly from the last checkpoint. Good.
  • during a tool call → the tool may have executed, but the observation was never recorded. On resume, the kernel re-dispatches. The call happens twice.

That is the honest limit, and it is the single most important sentence in this phase for a banking platform: a checkpointed kernel guarantees resumability, not exactly-once effects.

Two fixes, and you need both:

  1. Checkpoint before dispatch, recording the call as in_flight with an idempotency key. On resume, either re-dispatch with the same key or query the downstream for the key's outcome.
  2. Make the downstream idempotent so a duplicate dispatch is harmless. That is Phase 10, and it is why the action gateway exists as a separate layer rather than as kernel code.

Note what this means architecturally: the kernel cannot solve exactly-once by itself, no matter how clever its checkpointing, because the guarantee has to be enforced where the effect happens.

6. Session affinity and consistent hashing

6.1 What affinity buys once state is external

If state is external, why route a session to the same replica at all? Three real reasons:

  1. Warm caches — the rendered system prompt, tool schemas, retrieval results, and any provider-side prefix cache association.
  2. Open connections — to the model provider, to the vector store, to downstream systems.
  3. Fewer OCC conflicts — one replica handling a session serializes naturally.

And one non-reason: correctness. Once state is external, affinity is a performance optimization, and losing it costs a cache miss. Teams that treat affinity as a correctness requirement end up unable to deploy.

6.2 Modulo hashing and why it fails

The obvious mapping is replica = replicas[hash(session_id) % len(replicas)].

It works until len(replicas) changes. Then almost every session moves. Going from 3 to 4 replicas, a session stays put only when \( h \bmod 3 = h \bmod 4 \), which happens for roughly 1 in 4 of them — so ~75% move. Every warm cache is cold, every connection re-established, at exactly the moment you were adding capacity because you were under load.

6.3 The ring, derived

Consistent hashing solves this. The idea, from the 1997 Karger et al. paper that also gave us distributed caches and later Dynamo:

  1. Map both replicas and keys into the same circular space (here, 64-bit integers, wrapping at \( 2^{64} \)).
  2. A key belongs to the first replica clockwise from the key's position.
        0 ──────────────────────────────────── 2^64
        │    ▲pod-b        ▲pod-a      ▲pod-c   │
        │        ●s-17          ●s-3            │
             s-17 → pod-a    s-3 → pod-c    (first replica clockwise)

Now add pod-d. It lands at one point on the ring and takes only the keys between its predecessor and itself. Every other key is untouched. Removing a replica is the mirror image: its keys go to its clockwise successor, and nothing else moves.

The movement property: adding the \( n \)-th replica moves about \( 1/n \) of keys. Going from 3 to 4 moves ~25%, versus ~75% for modulo. The lab's test asserts both this bound and the stronger invariant that every moved session goes to the new replica — no churn between existing ones, which is the property that actually protects your caches.

6.4 Virtual nodes

With one point per replica, the ring is lumpy: three random points do not divide a circle into three equal arcs. One replica ends up with 55% of traffic.

Fix: give each replica V points (pod-a#0, pod-a#1, …, pod-a#63). With V = 64–256 the arcs average out and the distribution tightens toward uniform (the standard deviation of a replica's share shrinks like \( 1/\sqrt{V} \)).

Virtual nodes also enable weighting: a replica with twice the capacity gets twice the points. That is how you run a heterogeneous fleet without a separate scheduler.

The cost is memory and lookup time: the ring has R × V entries and lookup is a binary search, \( O(\log(RV)) \). At R=20, V=128 that is 2 560 entries — nothing.

6.5 Draining

Removing a replica from the ring immediately reassigns its sessions. During a rolling deploy that is exactly wrong: you want no new sessions on the pod that is about to go away, while existing ones finish.

So drain() marks a replica ineligible for new routing without removing its ring points. route() walks clockwise and skips draining replicas. The pod finishes its work and is removed when idle. This distinction — drain then remove, never remove alone — is what makes a zero-disruption deploy possible, and its absence is a common cause of "why did we lose sessions during a deploy?"

6.6 The hash function matters

Python's built-in hash() for strings is salted per process (since 3.3, as a hash-flooding defence). Two pods computing hash("s-42") get different numbers. A ring built on it means every pod routes differently and every restart reshuffles — a bug that is invisible in a single-process test and catastrophic in production.

The lab uses blake2b truncated to 8 bytes: stable across processes, across restarts, and across machines. Any stable digest works (md5, sha1, xxhash, murmur3); cryptographic strength is irrelevant here, stability is not. The lab has a test for exactly this, because it is the kind of defect you only find in production.

7. Budgets

7.1 The four budgets and what each prevents

BudgetPreventsTypical value
max_stepsinfinite loops; also caps \( p^n \) degradation8–25
max_tokenscontext explosion and the associated bill20k–200k per run
max_cost_microsthe case where few steps are individually expensive (a long document, an expensive model)a per-agent ceiling from the tenant's quota
deadline_secondsa run that is not looping but is stuck behind a slow dependencytied to the channel's tolerance

They are not redundant. A run can breach any one without the others: 3 steps over a 200-page document breaches tokens and cost but not steps; 30 fast cache hits breach steps but not cost; one call to a hung dependency breaches the deadline alone.

Two more that belong in a production kernel and are left as extensions: max concurrent tool calls (a fan-out bomb) and max scratchpad tokens (the lab has this as a compaction trigger rather than a hard fail).

7.2 Check before you pay

The loop checks budgets at the top, before calling the policy:

while True:
    if step_no > max_steps:  breach("steps");  break
    ...
    decision = policy(pad.render())     # the expensive call

Check afterwards and you have already paid for the model call whose result you are about to throw away. Over a fleet, at one wasted call per breached run, this is a real number — and worse, the wasted call also consumed provider rate-limit budget that other tenants needed.

The lab tests this directly: with max_steps=2, the policy is called exactly twice. A kernel that calls it three times fails that test, and it should.

8. The error taxonomy

The kernel must classify every failure into exactly one of two buckets, and the classification is a platform decision, not an agent-author decision.

Recoverable — feed the error back as an observation and let the model correct itself:

CaseWhy recoverable
Unknown tool namethe model hallucinated; telling it so is usually enough
Schema violation in argumentsa repair loop fixes most of these in one turn
Tool returned a business error ("account not found")that is information; the agent should reason about it
Tool 5xx / timeout (within retry budget)transient

Fatal — stop the run:

CaseWhy fatal
Budget breachby definition; the whole point of the budget
Illegal state transitionthe kernel's invariant is broken; continuing is undefined behaviour
Policy denial from the control planenot the agent's to retry
Store unavailablecannot checkpoint, therefore cannot guarantee resumability

The failure mode of getting this wrong is symmetric and both directions are bad. Treat everything as fatal → brittle agents that die on a typo'd tool name, and a flood of user-visible errors that are really self-correcting. Treat everything as recoverable → an agent that retries a budget breach forever, and a runaway that the kernel was supposed to stop.

There is a third bucket that is easy to miss: recoverable but rate-limited. The same recoverable error repeating (the model calling the same nonexistent tool five times) should become fatal. The cheap implementation is a per-error-kind counter in the scratchpad; a good extension exercise.

9. The execution chain

The chain is the per-step record: index, tool, arguments, outcome, error, tokens, duration, plus the session's identity (tenant, user). It is written as a by-product of running, not as a separate logging concern.

Three properties it must have:

  1. Complete — every step, including the ones compaction removed from the scratchpad. Hence execution_chain() reads from the store.
  2. Identified — every row carries the tenant and user. A chain without identity cannot answer "who authorized this," which is the only question anyone will ever ask it.
  3. Joinable — the same session_id appears on the trace, the audit record, and the cost record, so an investigation is a join and not an archaeology project.

What it is not: a log. Logs are lines optimized for humans grepping. The chain is a structure optimized for reconstruction. In production it is emitted as OpenTelemetry spans (one per step, child of a run span) with GenAI semantic-convention attributes, so the same data serves debugging, cost attribution, and evidence.

The design test to apply: can you reconstruct the run from the chain alone, with the code deleted? If not, something is missing.

10. Lab walkthrough

Work Lab 01 in this order — each section is used by the next.

  1. TRANSITIONS and transition (§3). Fill the table from the comment. Then run the terminal and trap tests first: they are the cheapest proof the machine is right.
  2. estimate_tokens, Scratchpad (§4.1–4.2). token_count sums the summary and every rendered step. compact_if_needed has three early exits worth writing explicitly: under budget, nothing to fold, and only the recent window remains.
  3. SemanticMemory (§4.3). Filter by visibility before ranking. The test test_semantic_memory_cannot_be_widened_by_asking_nicely fails loudly if you filter after.
  4. EpisodicMemory.recall (§4.4). Sort by (-overlap, -index); drop zero-overlap entirely.
  5. SessionStore (§5.2). create sets version 1; save is a compare-and-swap. Return the stored snapshot, not the caller's — the caller's has a stale version.
  6. AffinityRouter (§6). _rebuild sorts (hash, replica); route walks clockwise with a wrap by concatenating the ring to itself; add keeps _replicas sorted so construction order cannot change routing. Check the two ring tests (deterministic, stable_across_processes) before the distribution ones.
  7. Budgets.__post_init__, Decision.__post_init__ (§7). Small, but the validation tests are free correctness.
  8. AgentKernel.create_session, _checkpoint, execution_chain (§5, §9). Do these before runrun calls all three.
  9. AgentKernel.run (§7.2, §8). The order inside the loop is the lesson: budget-check → policy → branch on kind → dispatch → observe → compact → checkpoint. Two details the tests pin down: an unknown tool is OBSERVE, not FAIL; and the policy is called exactly max_steps times when an agent loops.
  10. default_summarizer — trivial, but keep it deterministic; the whole test suite depends on it.

Then run python solution.py and read the eight sections against §§3–7 above.

11. Success criteria

Without the guide open, you can:

  • Draw the lifecycle and justify acting → completed being illegal.
  • Name the three memory tiers, their lifetimes, their partition keys, and what belongs in none of them.
  • Derive the quadratic scratchpad term and state what compaction changes it to.
  • Explain why compaction must not touch the persisted chain.
  • Explain OCC, why it beats locking here, and what a last-write-wins store would do to an audit trail.
  • State why a checkpointed kernel does not give exactly-once effects, and name the two fixes.
  • Derive consistent hashing's \( 1/n \) movement property and contrast it with modulo's \( (n-1)/n \).
  • Explain why hash() is the wrong hash for a ring.
  • Explain drain-then-remove.
  • Name four budgets, what each catches that the others do not, and why they are checked first.
  • Classify five failures as recoverable or fatal and defend each.

12. Common mistakes

Booleans instead of a state machine. is_running and is_done will be True one day.

Letting acting finish directly. The agent "answers" with a tool result nobody reasoned over.

One "memory" abstraction. You get either a leaking store or an exploding context, usually both.

Compacting the audit record. Convenient, and a finding.

Ranking then filtering in memory search. The same defect as a shared vector index with a post-hoc filter.

Session state in process memory. Every deploy is a customer-visible event.

Last-write-wins on session state. Two runs' steps interleave into one chain, which no execution ever performed.

Believing checkpointing gives exactly-once. It gives resumability. Effects are the action gateway's problem.

Modulo hashing for affinity. ~75% of sessions move when you scale.

hash() in the ring. Invisible in tests; catastrophic across processes.

Removing instead of draining. Sessions die on every deploy.

Checking budgets after the model call. You pay for the step you reject, and you burn a rate-limit slot another tenant needed.

Treating every error as fatal. A hallucinated tool name becomes a user-visible failure instead of a self-correction.

13. Interview Q&A

Q: How would you design the agent runtime for a bank platform?

A: "As a kernel, and I mean that structurally rather than as a metaphor. The runtime owns five things the agent author does not get to touch: the lifecycle, the budgets, the session state, the memory tiers, and the execution chain. Lifecycle is an explicit transition table so illegal states are provably unrepresentable and terminal states are absorbing — a replayed 'finish' on a completed session raises rather than producing a second answer. Budgets are steps, tokens, cost, and wall-clock deadline, checked before the model call so a breached run doesn't pay for the step it's about to discard. State is externalized into a session store with optimistic concurrency, so a deploy is a cache miss instead of forty lost conversations, and so a human-in-the-loop pause can outlive the pod. Memory is three tiers with three partition keys. And every step is checkpointed, which gives me the execution chain — the same object that serves debugging and audit. What I don't impose is the loop shape: ReAct, ReWOO and plan-execute-replan all reduce to 'call a policy, get a decision, dispatch or finish,' so that's pluggable while the invariants are fixed."

Q: Short-term, long-term, episodic — what's the actual difference?

A: "Different lifetimes and, more importantly, different partition keys. Short-term is the scratchpad: one run, bounded, compacted, and it's the thing whose growth is quadratic in step count — ten steps at two thousand tokens each is a hundred thousand input tokens, not twenty thousand. Long-term semantic memory is durable facts keyed by scope and owner: user, tenant, or app. That partition is not optional; a memory store that can return another tenant's fact is the same defect class as an un-namespaced vector index, and the filter has to be a retrieval predicate, not a post-processing step. Episodic memory is what happened — prior tasks, outcomes, step counts, lessons — recalled by similarity and recency, and it's separate because 'the last three times this vendor's payments were held it was a name-match false positive' is a fact about episodes, not about the world. And there's a fourth thing that isn't memory at all: evidence. Memory is for usefulness and can be lossy; the execution chain is for truth and cannot."

Q: What's your session-affinity strategy?

A: "Externalize the state first, so affinity is an optimization rather than a correctness requirement — that single decision is what makes deploys boring. Then consistent hashing with virtual nodes for the optimization itself: adding a replica moves about 1/n of sessions instead of modulo's (n−1)/n, and every session that moves goes to the new replica, so there's no churn between existing ones and warm caches survive. A hundred-plus virtual nodes per replica to smooth the distribution, and they double as a weighting mechanism for a heterogeneous fleet. Two details that bite people: the hash must be stable across processes — Python's hash() is salted per process, so a ring built on it reshuffles every restart — and deploys need drain, not remove. Draining stops new sessions from landing while in-flight ones finish; removing reassigns them immediately, which is the thing you were trying to avoid."

Q: Your kernel checkpoints every step. Does that give you exactly-once execution?

A: "No, and it's important to be precise about that. Checkpointing after the observation gives me resumability: if the worker dies between steps I resume cleanly. But if it dies during a tool call, the call may have executed and the observation was never recorded, so on resume I re-dispatch and the effect happens twice. That's fine for a read and unacceptable for a payment. The fix is two-sided: checkpoint before dispatch with the call recorded as in-flight and an idempotency key attached, and make the downstream idempotent so a duplicate dispatch is harmless. The second half is the action gateway's job, not the kernel's — the guarantee has to be enforced where the effect happens, which is exactly why they're separate layers."

Q: An agent calls a tool that doesn't exist. What happens?

A: "It's recoverable, so the kernel records the error on the step and feeds it back as an observation — the model usually corrects itself on the next turn. That's a deliberate taxonomy: recoverable failures are unknown tools, schema violations, business errors from a tool, and transient 5xx; fatal ones are budget breaches, illegal state transitions, policy denials, and a store I can't checkpoint to. Getting this wrong is bad in both directions — treat everything as fatal and you get brittle agents dying on typos; treat everything as recoverable and the runaway your budget was supposed to stop retries forever. The bucket people miss is 'recoverable but rate-limited': the same recoverable error five times in a row should become fatal, because the model isn't correcting, it's stuck."

Q: Why is acting → completed illegal in your state machine?

A: "Because the tool result has to be observed and reasoned over before an answer exists. Allowing that edge lets an agent return a raw tool result as its answer without a model step in between — which sounds like an optimization and is actually how you ship an answer nobody validated, with no thought recorded in the chain explaining why that result answered the question. It's a one-line prohibition in the table that removes a whole class of 'the agent said something weird' incidents, and it costs nothing."

14. References

Agent loops

  • Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, ICLR 2023 — arXiv:2210.03629.
  • Xu et al., ReWOO: Decoupling Reasoning from Observations for Efficient Augmented Language Models, 2023 — arXiv:2305.18323.
  • Wang et al., Plan-and-Solve Prompting, ACL 2023 — arXiv:2305.04091.
  • Shinn et al., Reflexion: Language Agents with Verbal Reinforcement Learning, NeurIPS 2023 — the origin of most episodic-memory-for-agents thinking.

Distributed systems

  • Karger et al., Consistent Hashing and Random Trees, STOC 1997 — the original ring.
  • DeCandia et al., Dynamo: Amazon's Highly Available Key-value Store, SOSP 2007 — consistent hashing with virtual nodes, in production.
  • Kleppmann, Designing Data-Intensive Applications, Ch. 6 (partitioning) and Ch. 7 (concurrency control, including OCC).
  • Google, Maglev: A Fast and Reliable Software Network Load Balancer, NSDI 2016 — the other consistent-hash scheme you will meet, in Envoy.

Runtimes and frameworks (for the "how the real thing does it" comparison)

  • LangGraph documentation — checkpointers, thread_id, interrupt(), recursion_limit, time travel.
  • Google ADK — session services, state scopes (user:, app:, temp:), the event-driven runner.
  • AWS Bedrock AgentCore — runtime session isolation and memory strategies.
  • OpenAI Agents SDK — Runner, sessions, handoffs, guardrails.
  • Temporal — durable execution, and the clearest available statement of the difference between checkpointing and determinism-based replay.

Operating systems (the analogy, taken seriously)

  • Arpaci-Dusseau & Arpaci-Dusseau, Operating Systems: Three Easy Pieces — processes, scheduling, and limits; free online.