An AI agent's basic control loop asks for the next action, runs a permitted tool, adds the result to state and repeats until it finishes or stops. Reading that sentence is easy. Explaining what happened during one actual run takes a different kind of evidence: the source branch, the emitted event and the resulting artifact.
Inside an AI agent loop: follow one execution
This walkthrough inspects a Python agent loop in Minimal Agent Framework, or MAF, at commit 15293b0da02e2a831278103c5be8d789018b9b4e. MAF is a small Python runtime built to make the loop inspectable. We will read a local file, write a summary, inspect the state and deliberately hit three halt paths.
The model responses are scripted with MockAdapter. The runtime, schema checks, filesystem operations and trace persistence are real; no live model is choosing the actions. That makes the exercise repeatable without an API key, while keeping the limit clear: it tests execution mechanics, not an LLM's ability to understand the task.
Start in AgentRuntime.run. At this commit, the action branches are final, continue and tool_call. The runtime decides what to execute after the adapter proposes an action.
A final action appends the answer and exits. A continue action may update scratch state and advances to another iteration. A tool_call enters the registry, permission and schema checks before reaching a handler.
What is the smallest useful AI agent loop to inspect?
Choose one invocation that produces an observable result, then include the decision, tool boundary, state update and exit. A file read followed by a write is enough for this exercise. Add a rejected action so you can explain both paths. You do not need to inventory every class or background task before understanding this run.
Run one local file task and predict the next event
Use a clone pinned to the inspected commit. The repository's installation metadata supplies the local package; this exercise uses its Python API rather than the default CLI mock, which simply returns an echo when no actions are supplied.
git clone https://github.com/fedewedreamlabsio/minimal-agent-framework.git
cd minimal-agent-framework
git checkout 15293b0da02e2a831278103c5be8d789018b9b4e
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
Save the accompanying walkthrough.py in the repository and run python walkthrough.py. Its important setup is below; the downloadable file contains the complete runnable example.
from pathlib import Path
from maf import (
Action, AgentRuntime, MockAdapter,
RuntimeConfig, build_power_tools,
)
root = Path("maf-demo").resolve()
task = root / "files"
task.mkdir(parents=True, exist_ok=True)
(task / "input.txt").write_text(
"A small local test fixture.\n", encoding="utf-8"
)
config = RuntimeConfig(
trace_dir=str(root / "runs"),
fs_root_path=str(task),
kv_store_path=str(root / "kv.json"),
tool_allowlist=["fs"],
max_steps=6,
max_run_seconds=2,
)
actions = [
Action(type="tool_call", tool_name="fs",
tool_input={"op": "read", "path": "input.txt"}),
Action(type="tool_call", tool_name="fs",
tool_input={"op": "write", "path": "summary.txt",
"content": "Read one local fixture.\n"}),
Action(type="final", final_output="summary.txt written"),
]
runtime = AgentRuntime(
config=config,
llm_adapter=MockAdapter(scripted_actions=actions),
tools=build_power_tools(config),
)
result = runtime.run("Read input.txt and write summary.txt")
Before running it, predict the events. Each tool step should produce model_called, model_output, tool_called and tool_result. The final step has no tool events because its action takes the completion branch.
The captured run matched that prediction. Two tool calls were followed by final, run_finished reported completed, and summary.txt contained “Read one local fixture.” The summary text is supplied by the scripted action; this is not a demonstration of model-generated summarization.
If you first need an agent to inspect, use the Build AI Agent route, then bring one concrete execution back to this worksheet.
Before you start
Python 3.10+, a terminal, and the MAF checkout pinned to commit 15293b0da02e2a831278103c5be8d789018b9b4e.
Pin the repository
Clone MAF and check out commit 15293b0da02e2a831278103c5be8d789018b9b4e.
Expected result
The checkout is at the inspected commit.
Run the local task
Use walkthrough.py with the explicit fs-only allowlist and scripted read/write/final actions.
Expected result
Two filesystem calls read input.txt and write summary.txt, followed by the scripted final action.
Inspect the evidence
Match model/tool events to the file and initial/final state, then run rejection and replay probes.
Expected result
The trace records the branch taken; state and the actual file establish its outcome.
Verify the result
Compare the predicted events with the retained local capture: completed status, two tool calls, user/tool/tool/assistant message roles and the inspected summary.txt content. The adapter actions are scripted; no model-generated summarization is demonstrated.
Troubleshooting
A missing fs path should halt before tool_called with tool_input_validation_failed. A disallowed kv.set should halt with tool_not_allowed. Consult the stop-conditions section for the separate step-limit probe.
Command
PYTHONPATH=/tmp/starkslab-maf-inspection-15293b0 python3 maf-article-evidence/capture_walkthrough.py /tmp/starkslab-maf-schema-retry-captureActual output
{"case": "success", "run_id": "2999c7568e444a38a3f655f8a5a59d4c", "halt_reason": null, "status": "completed", "events": ["run_started", "model_called", "model_output", "tool_called", "tool_result", "model_called", "model_output", "tool_called", "tool_result", "model_called", "model_output", "run_finished"], "tool_called_count": 2, "messages": ["user", "tool", "tool", "assistant"], "artifact": "Read one local fixture.\n"}Trace tool calls from proposed action to file write
Follow the tool_call branch forward instead of stopping at the model response. The runtime looks up the tool, checks the configured allowlist and validates the input schema. Only then does it emit tool_called and enter execution.
For fs, the actual write occurs inside _make_fs_tool, when its handler calls target.write_text. A proposed action and a tool_called event therefore describe different points from a completed write. Check tool_result and the file itself to establish the outcome.
The runtime records the tool result and then validates its output schema. That ordering matters: output validation can reject a result after a side effect has happened. It cannot turn an already-written file back into an unexecuted action.
Now request fs with {"op": "write"}, leaving out the required path. The captured run stopped with tool_input_validation_failed; it emitted no tool_called event. This is a pre-execution gate you can locate in both source and trace.
Next request kv.set while keeping tool_allowlist=["fs"]. That run stopped with tool_not_allowed, also before tool_called, and no key-value file appeared. Use an explicit allowlist: at this commit, an empty list is not a deny-all policy because the check is conditional on a truthy list.
The filesystem handler resolves paths and checks that they remain under its configured root. The shell handler's working-directory restriction is a different boundary; setting cwd does not confine arbitrary shell commands to that directory. The control-surface guide is the broader next step when deciding which component should enforce a missing control.
Where does a proposed tool call become an executed action?
Find the dispatcher and follow it into the handler that performs the effect. In this example, lookup, allowlist and input validation come first; the filesystem write happens inside the handler. A tool name in model output proves only a proposal. Confirm execution using the matching result and artifact, and remember that output validation runs after the handler.
Command
PYTHONPATH=/tmp/starkslab-maf-inspection-15293b0 python3 maf-article-evidence/capture_walkthrough.py /tmp/starkslab-maf-schema-retry-captureActual output
{"case": "invalid-input", "run_id": "2fdf03f8248b4edf966e1215f8855869", "halt_reason": "tool_input_validation_failed", "status": "halted", "events": ["run_started", "model_called", "model_output", "error", "run_finished"], "tool_called_count": 0, "messages": ["user"]}Follow agent state between steps and after the run
Open the successful run's state.initial.json and state.final.json. The initial snapshot already contains the user message, because run appends the input before saving that snapshot. It is the start of this invocation, not a pristine state before any input was received.
In the final snapshot, the message roles were user → tool → tool → assistant. The read result includes the fixture's contents; the write result includes the path and byte count. These recorded results are the state available to a later decision.
Inspect AgentState and JsonlRunStore beside the snapshots. Messages, scratch, artifacts, vars and budgets are separate fields. In this run, the summary file existed while state.artifacts remained empty: creating a file does not automatically register it there.
Persistence is another explicit step. The store writes trace, metadata and initial/final state records, but loading a later session requires a caller to load a snapshot, reconstruct the state and pass it to run. A saved JSON file is not evidence that automatic resume is wired into an application.
Is saved conversation history the same as automatic session resume?
No. Saving messages establishes that a record exists; resuming requires a component to load that record and supply it to the next run. Inspect the store, the state-construction code and the caller together. MAF exposes those pieces, but the snapshot alone does not establish an application-level resume policy, retention period or recovery guarantee.
Read stop conditions and the actual halt path
Use max_steps=2 with two scripted continue actions followed by final. The captured run made two adapter calls and stopped with max_steps. It never reached the third action, so a final answer waiting in the script did not override the runtime's iteration budget.
Read the halt reason together with the event sequence:
| Probe | Observed halt | Tool handler reached? |
|---|---|---|
Missing fs path |
tool_input_validation_failed |
No |
Disallowed kv.set |
tool_not_allowed |
No |
| Two-step budget | max_steps |
No tool requested |
| File read/write/final | No halt; completed | Twice |
There is no human approve/reject branch in the inspected AgentRuntime.run path. Validation and permission checks can prevent execution; trace events make execution visible. Neither should be relabelled as a human approval workflow.
Time controls need the same close reading. The overall run-time budget is checked between iterations, so it is not a mechanism that forcibly interrupts an in-flight model call or tool. The tool wrapper waits on a worker future with a timeout, but its executor context waits for that worker when exiting.
A bounded local probe used a 0.02-second timeout around a handler that slept for 0.12 seconds. It reported tool_failed and returned after about 0.127 seconds. That single observation supports a narrow conclusion: this wrapper reports the timeout without forcibly terminating the running handler; it is not a performance benchmark.
Complete the source-and-trace worksheet
Use the same worksheet for every stage. Record the condition that selects a branch, the event that exposes it and the artifact that confirms an effect. Keep “documented,” “inspected in code” and “observed in this run” separate when filling it.
| Stage and source | Branch or input | Event/state evidence | Effect or stop |
|---|---|---|---|
AgentRuntime.run initializes |
Task text supplied | run_started; initial user message |
Run directory created |
MockAdapter.complete proposes |
fs read action |
model_output contains action |
No file effect yet |
| Runtime accepts the call | Registered, allowed, valid input | tool_called |
Handler can execute |
| Filesystem read returns | input.txt |
tool_result; tool message appended |
Fixture contents observed |
| Filesystem write returns | summary.txt plus content |
Result reports 24 bytes; file inspected | Summary exists |
Runtime handles final |
Scripted final action | Assistant message; run_finished |
Completed |
| Input check rejects | Write action missing path | Error; no tool_called |
Validation halt |
| Allowlist rejects | kv.set under fs-only policy |
Error; no tool_called |
Permission halt |
For another repository, replace these source symbols and fixture details rather than copying the conclusions. A registered tool may have different credentials, a result may update different state, and an error may continue instead of halting. The source-reading guide explains how to extend this narrow exercise into a larger operating map.
Replay recorded decisions and verify what happened
MAF's replay combines recorded model actions with recorded tool results. ReplayAdapter.from_trace supplies the actions, and extract_tool_results supplies the result payloads passed into run. The runtime follows those records without asking a live model to choose again.
The capture exercise removed summary.txt before replay and replaced the filesystem handler with a function that would fail if called. Replay still completed, the handler-call count remained zero, both tool results were marked replayed: true, and the deleted summary file stayed absent. Recorded execution reproduced the control path without reproducing its external write.
Verification was captured on October 14, 2026 with Python 3.14.3 on macOS ARM64. Nine runtime files were checked against the Git blob hashes at the pinned commit; six bounded probes and ten selected regression tests passed. The earlier HTTP integration check was blocked by the environment's local-server restriction and was not counted as an HTTP success.
The evidence bundle includes the fixture, runnable scripts, trace events, initial/final snapshots, test output and source manifest. Its scope is local execution with scripted model responses: no live LLM, provider API, customer workflow or production outcome was tested.
Does replay mean a live model will make the same decisions?
No. This replay reads previously recorded actions and tool results, so it avoids fresh model sampling and external tool execution. It is useful for inspecting a known trajectory, not for demonstrating identical live-model behaviour. Runtime or schema changes can still alter validation, and replaying a write result does not prove that the destination artifact currently exists.
Command
PYTHONPATH=/tmp/starkslab-maf-inspection-15293b0 python3 maf-article-evidence/capture_walkthrough.py /tmp/starkslab-maf-schema-retry-captureActual output
{"case": "replay", "run_id": "20762b8434c346469c88936411fa8dee", "halt_reason": null, "status": "completed", "events": ["run_started", "model_called", "model_output", "tool_called", "tool_result", "model_called", "model_output", "tool_called", "tool_result", "model_called", "model_output", "run_finished"], "tool_called_count": 2, "messages": ["user", "tool", "tool", "assistant"], "handler_calls": 0, "artifact_recreated": false, "replayed_flags": [true, true]}