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
- 2. Reasoning loops: ReAct, ReWOO, plan-execute-replan
- 3. Lifecycle as a state machine
- 4. Memory architecture
- 5. State, checkpoints and concurrency
- 6. Session affinity and consistent hashing
- 7. Budgets
- 8. The error taxonomy
- 9. The execution chain
- 10. Lab walkthrough
- 11. Success criteria
- 12. Common mistakes
- 13. Interview Q&A
- 14. References
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:
| Line | What goes wrong |
|---|---|
while True | nothing 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 function | state lives in a local variable. The pod restarts; the conversation is gone. |
| the whole function | nothing is recorded. When Audit asks what happened, you have nothing. |
| implicitly | there 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 kernel | Agent kernel |
|---|---|
| process | agent run |
| memory limit, CPU quota | token budget, step budget, cost ceiling, deadline |
| syscall interface | tool dispatch through a registry |
| process table | session store |
| page tables held by the kernel | session state externalized, not in worker memory |
| SIGKILL on quota breach | FAILED with a BudgetBreach |
| illegal instruction → trap, not crash | hallucinated tool name → observation, not exception |
| process accounting | execution 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 × TTFTat 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
#E1returns 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:
- Every legal edge is enumerable — you can print the state machine, put it in a design doc, and hand it to a reviewer.
- Terminal states are absorbing — one check, not one check per call site.
- 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_inputsession that can never be cancelled leaks forever).
3.2 The eight states, justified one at a time
| State | Why it exists | What it must not do |
|---|---|---|
created | a session exists before it runs — it has an owner, a tenant, a goal, and an audit identity from the moment it is created | run |
planning | the model is deciding. This is where cost is incurred and where finishing is legal | dispatch a tool |
acting | a 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 place | finish. 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_input | human-in-the-loop. The run is alive but not consuming, possibly for hours | consume budget while parked |
suspended | checkpointed 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 path | be resumed with an answer |
completed | the goal was met | anything |
failed | a budget breach, an unrecoverable error, or an illegal state | anything |
cancelled | a 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 query | anything |
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:
- Truncation — drop the oldest steps. Cheap, and it silently loses the fact that made the whole investigation make sense.
- Compaction (summarization) — fold old steps into a summary, keep the recent window verbatim. Costs a model call, preserves the gist.
- 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:
| Scope | Owner | Lifetime | Example |
|---|---|---|---|
user | a person | as long as the person uses the platform | language preference, saved filters |
tenant | a business unit | as long as the tenant exists | the RM for a client, the tenant's approval thresholds |
app | the platform | forever | the 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:
- Checkpoint before dispatch, recording the call as
in_flightwith an idempotency key. On resume, either re-dispatch with the same key or query the downstream for the key's outcome. - 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:
- Warm caches — the rendered system prompt, tool schemas, retrieval results, and any provider-side prefix cache association.
- Open connections — to the model provider, to the vector store, to downstream systems.
- 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:
- Map both replicas and keys into the same circular space (here, 64-bit integers, wrapping at \( 2^{64} \)).
- 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
| Budget | Prevents | Typical value |
|---|---|---|
max_steps | infinite loops; also caps \( p^n \) degradation | 8–25 |
max_tokens | context explosion and the associated bill | 20k–200k per run |
max_cost_micros | the case where few steps are individually expensive (a long document, an expensive model) | a per-agent ceiling from the tenant's quota |
deadline_seconds | a run that is not looping but is stuck behind a slow dependency | tied 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:
| Case | Why recoverable |
|---|---|
| Unknown tool name | the model hallucinated; telling it so is usually enough |
| Schema violation in arguments | a 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:
| Case | Why fatal |
|---|---|
| Budget breach | by definition; the whole point of the budget |
| Illegal state transition | the kernel's invariant is broken; continuing is undefined behaviour |
| Policy denial from the control plane | not the agent's to retry |
| Store unavailable | cannot 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:
- Complete — every step, including the ones compaction removed from the scratchpad. Hence
execution_chain()reads from the store. - 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.
- Joinable — the same
session_idappears 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.
TRANSITIONSandtransition(§3). Fill the table from the comment. Then run the terminal and trap tests first: they are the cheapest proof the machine is right.estimate_tokens,Scratchpad(§4.1–4.2).token_countsums the summary and every rendered step.compact_if_neededhas three early exits worth writing explicitly: under budget, nothing to fold, and only the recent window remains.SemanticMemory(§4.3). Filter by visibility before ranking. The testtest_semantic_memory_cannot_be_widened_by_asking_nicelyfails loudly if you filter after.EpisodicMemory.recall(§4.4). Sort by(-overlap, -index); drop zero-overlap entirely.SessionStore(§5.2).createsets version 1;saveis a compare-and-swap. Return the stored snapshot, not the caller's — the caller's has a stale version.AffinityRouter(§6)._rebuildsorts(hash, replica);routewalks clockwise with a wrap by concatenating the ring to itself;addkeeps_replicassorted so construction order cannot change routing. Check the two ring tests (deterministic,stable_across_processes) before the distribution ones.Budgets.__post_init__,Decision.__post_init__(§7). Small, but the validation tests are free correctness.AgentKernel.create_session,_checkpoint,execution_chain(§5, §9). Do these beforerun—runcalls all three.AgentKernel.run(§7.2, §8). The order inside the loop is the lesson: budget-check → policy → branch onkind→ dispatch → observe → compact → checkpoint. Two details the tests pin down: an unknown tool isOBSERVE, notFAIL; and the policy is called exactlymax_stepstimes when an agent loops.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 → completedbeing 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.