It is already the latest posts!
It is already the oldest posts!

agent runtime lab en · 2026 · Technical Research Note

Running an Agent
Fault Exercise

A successful happy path proves that the system can run. A controlled failure shows whether it knows where to stop and how to recover.

Inject tool timeouts, corrupted results, and process crashes to test an Agent's retries, state recovery, and side-effect boundaries.

Research edition · 9 chapters · 6 minute read · Updated 2026-08-23

Edition A · 中文 B · English
SCROLL

An order-processing Agent called a payment tool. The provider charged the customer, but the response timed out before it reached the runtime. The runtime saw no result and submitted the charge again.

A normal integration test would not expose this bug. When the request succeeds and its acknowledgement returns, both the state machine and retry policy appear correct. The duplicate charge appears only when execution stops in the narrow window after the side effect but before its confirmation.

Fault injection deliberately creates timeouts, malformed data, dependency failures, or process interruption in a controlled environment. The purpose is not to make the Agent fail. It is to observe whether the system preserves enough evidence to stop safely and recover without repeating work.

Inject faults at exact boundaries

“Make the tool fail at random” exercises generic error handling, but it does not identify which state transition is unsafe. A tool call with a side effect can be divided into five observable points:

validate arguments
-> persist call intent
-> execute the side effect
-> persist the result
-> return the result to the model

If the process stops before the intent record, the runtime may treat the call as never started. If it stops after the external side effect, local state alone cannot determine whether the operation completed.

Give each boundary its own injection switch. Otherwise, a test that “caused a timeout” proves only that something failed somewhere. It does not prove that recovery covers the dangerous uncertainty window.

Reproduce a lost acknowledgement with a small program

A side effect changes external state, such as editing a file, charging a card, or sending a message. An idempotency key identifies one logical operation. A service that supports idempotency executes repeated submissions with the same key only once.

from dataclasses import dataclass, field


class LostAcknowledgement(Exception):
    """The external action completed, but its acknowledgement was lost."""


@dataclass
class PaymentService:
    """An in-memory payment service with idempotency support."""

    charges: dict[str, int] = field(default_factory=dict)

    def charge(
        self,
        idempotency_key: str,
        amount: int,
        lose_ack: bool = False,
    ) -> int:
        if idempotency_key not in self.charges:
            self.charges[idempotency_key] = amount

        if lose_ack:
            raise LostAcknowledgement(
                "charge completed, acknowledgement lost"
            )

        return self.charges[idempotency_key]


service = PaymentService()
key = "order-20260823-001"

try:
    service.charge(key, 5000, lose_ack=True)
except LostAcknowledgement:
    # Query or retry with the original key. Never invent a new one here.
    charged = service.charge(key, 5000)

assert charged == 5000
assert service.charges == {key: 5000}

If recovery generates a new key, the simulated provider records two charges. The runtime must therefore generate and persist the idempotency key when it accepts the logical call, not before each attempt.

Build a fault matrix before writing injection code

I start by drawing the critical path and asking one question at each boundary: after this failure, which facts does the runtime actually know?

Injection pointRuntime evidenceExternal stateCorrect response
Before validationNo accepted callUnchangedCorrect input or stop
After intent, before executionPending call recordUnchangedContinue with the original call ID
After execution, before acknowledgementPending call recordPossibly changedQuery external state; do not retry blindly
After result persistence, before model returnCompleted recordChangedReplay the saved result
After model return, before the next model stepModel receipt uncertainChangedRestore the conversation; do not rerun the tool

The third row is the critical one. The operation is unknown, not failed. Duplicate payments, duplicate emails, and duplicate tickets often begin when a runtime collapses those two states into one.

Give every exercise explicit acceptance criteria

“The process did not crash” is not a pass condition. Check both final state and recovery behavior.

experiment: lost-payment-ack
inject_at: after_external_commit_before_local_result
expected:
  external_charge_count: 1
  final_run_state: completed
  reused_idempotency_key: true
forbidden:
  - second_charge
  - fabricate_success_message
  - discard_pending_record
recovery_deadline_seconds: 30

The Agent's final sentence “Payment succeeded” proves nothing by itself. The provider must contain exactly one charge, the pending record must converge to a known state, and recovery must use the original idempotency key.

Test different failure classes separately

A tool error, response timeout, and runtime crash may all appear as “task failed” in the interface. Their recovery rules differ:

  • A validation error requires corrected arguments or termination. Repeating the same request has no value.
  • A timed-out read may be retried within an attempt and elapsed-time budget.
  • A write interrupted before acknowledgement requires an idempotent status query, not an assumption that execution never happened.
  • A permission rejection or safety-policy block should normally stop, not enter a retry loop.

Record the injection point, state before failure, attempts, external receipts, and final recovery path. A screenshot labeled “exercise successful” cannot show whether the runtime recovered automatically or an operator repaired the database by hand.

Persist the retry budget across process restarts

Many systems keep retry_count only in memory. A restart resets the counter, so a nominal three-attempt policy can run through several sets of three. Elapsed-time budgets have the same problem.

Persist the budget for the logical operation: first-started time, attempt count, last error, next allowed attempt, and idempotency key. Store those fields with the call intent.

Backoff also needs error semantics. Reads may use automatic exponential backoff. Writes in an unknown state must query first. Validation, authorization, and safety failures generally should not retry. Treating every error as an unstable network teaches the Agent to repeat the wrong action more patiently.

Recover from durable state, not a model's guess

After a runtime restart, it may be tempting to tell the model, “The payment tool might have run; please continue.” That asks a language model to infer transaction state from prose.

The recovery process should read structured records first. It must distinguish completed, not-started, and unknown calls, then reconcile unknown calls with the external service. Only after the state converges should it return the necessary facts to the model.

The model may decide how to continue the user's task. It must not decide whether yesterday's side effect actually occurred. That fact belongs to durable logs and the external system.

Run a reviewable game day

Once isolated tests are repeatable, schedule a small game day: a coordinated exercise in which the team operates the system while controlled failures occur. Participants should know the stop conditions and recovery owner, even if they do not know the exact injection time.

Keep a timeline: when the fault began, how long monitoring took to detect it, which Agent state appeared, what the on-call engineer could see, whether recovery ran automatically, and which manual commands were required. The resulting fixes often involve alert text, state dashboards, runbooks, and permission switches as much as retry code.

If only the original author can recognize and recover a failure, the service is not yet operable.

Keep early exercises controlled

Fault injection does not mean killing random production processes. Begin with isolated accounts, simulated dependencies, and dedicated data. Trigger injections with explicit test identifiers rather than random conditions that might affect real users.

After the recovery path works repeatedly in isolation, a team may consider a narrowly scoped production exercise. It still needs an operator, stop condition, and rollback procedure. An Agent should never choose the exercise boundary for itself.

The happy path answers whether the system can complete a task when every dependency cooperates. A fault exercise answers the harder question: when those conditions disappear, does the system know what happened, where it stopped, and which action is safe next?

Statement: If no specific statement in the content, the copyright belongs to sshipanoo . Reprint please indicate the link of this article.

(The content is authorized with CC BY-NC-SA 4.0 protocol)

Title:17. Running a Fault-Injection Exercise for an Agent

Link:https://www.sshipanoo.com/en/blog/ai/agent-runtime-lab/agent-fault-injection-exercise/