Architecture Decisions Basics
8 examples to get you started with agent architecture decisions - 5 basic and 3 intermediate.
You will draft a tiny ADR for a framework choice, encode the decision in config, list consequences, mark status changes, and sketch a revisit trigger.
Prerequisites
- Python 3.11+ recommended
- Comfort editing Markdown and a small config module
- No paid APIs required for these sketches
python -m venv .venv && source .venv/bin/activate
# stdlib only for the examples belowBasic Examples
1. Name the Decision Before You Write Code
Start with a one-line decision title and status.
from dataclasses import dataclass
from datetime import date
from enum import Enum
class AdrStatus(str, Enum):
PROPOSED = "proposed"
ACCEPTED = "accepted"
DEPRECATED = "deprecated"
SUPERSEDED = "superseded"
@dataclass
class AdrHeader:
id: str
title: str
status: AdrStatus
date: date
owner: str
header = AdrHeader(
id="ADR-001",
title="Use a thin custom loop for the support triage agent",
status=AdrStatus.PROPOSED,
date=date(2026, 7, 1),
owner="platform-agents",
)
print(header)- One decision per ADR keeps supersession clean.
- Status is first-class, not a comment in a PR.
- Owner answers "who revisits this?"
2. Capture Context and Forces
Document why a choice is forced now.
from dataclasses import dataclass
@dataclass
class AdrContext:
problem: str
forces: list[str]
non_goals: list[str]
ctx = AdrContext(
problem="Ship a single-tool support triage agent in two weeks.",
forces=[
"Team knows FastAPI and stdlib; limited graph experience",
"One read-only knowledge tool; no multi-day HITL yet",
"Must keep tools portable if we adopt LangGraph later",
],
non_goals=[
"Multi-agent research pipelines",
"Durable multi-day resume",
],
)
for f in ctx.forces:
print("-", f)- Forces are constraints, not preferences.
- Non-goals stop scope creep inside the ADR.
- Future readers need the world as of the decision date.
3. List Options and Pick One Explicitly
Rejected options are part of the value.
from dataclasses import dataclass
@dataclass
class Option:
name: str
pros: list[str]
cons: list[str]
options = [
Option(
name="Thin custom loop",
pros=["Fast to ship", "Few dependencies", "Easy to reason about"],
cons=["We own resume/HITL later", "Less community graph tooling"],
),
Option(
name="LangGraph",
pros=["Checkpoints and graphs ready", "Ecosystem tracing"],
cons=["Learning curve this sprint", "Heavier than one tool needs"],
),
Option(
name="CrewAI multi-agent",
pros=["Role metaphors for demos"],
cons=["Overkill for one triage tool", "Coordination cost now"],
),
]
decision = "Thin custom loop"
print("DECISION:", decision)
for o in options:
mark = "← chosen" if o.name == decision else ""
print(f"* {o.name} {mark}")- Always keep at least two real options.
- "Do nothing" can be an option for rewrites.
- The decision line should be unambiguous.
4. Record Consequences (Good and Bad)
Accepted pain is still architecture.
from dataclasses import dataclass
@dataclass
class Consequences:
positive: list[str]
negative: list[str]
follow_ups: list[str]
cons = Consequences(
positive=[
"Triage agent ships without framework upgrade risk",
"Tool function stays a pure Python callable",
],
negative=[
"No built-in durable resume if we add long HITL later",
"Team must enforce max turns and timeouts ourselves",
],
follow_ups=[
"Add max_turns=8 and wall_clock_s=60 in host policy",
"Write ADR-00N if we need graph resume",
],
)
print("Watch:", cons.negative[0])- Negative consequences prevent surprise blame later.
- Follow-ups become tickets, not forgotten bullets.
- Pair with stop conditions in code, not only in prose.
5. Encode the Decision as Config Pins
ADRs should point at runnable pins.
# agent_config.py - pins referenced by ADR-001
AGENT_RUNTIME = "custom_thin_loop"
FRAMEWORK = None # explicit: no agent framework yet
MAX_TURNS = 8
WALL_CLOCK_S = 60
MODEL_ROLE = "mid_default" # see model ADR; do not hardcode vendor id here
TOOLS_ALLOWLIST = ["knowledge_search"]
def describe() -> str:
return (
f"runtime={AGENT_RUNTIME} framework={FRAMEWORK} "
f"max_turns={MAX_TURNS} tools={TOOLS_ALLOWLIST}"
)
print(describe())- Config is the live projection of the ADR.
- Prefer roles (
mid_default) over forever-hardcoded model marketing names in app code. - Allowlists make privilege decisions reviewable.
Related: Documenting Model and Provider Decisions for Future Migrations
Intermediate Examples
6. Render a Minimal ADR Markdown Blob
Generate the doc your team will commit.
from textwrap import dedent
def render_adr() -> str:
return dedent(
f"""
# ADR-001: Thin custom loop for support triage
- Status: {AdrStatus.ACCEPTED.value}
- Date: 2026-07-01
- Owner: platform-agents
## Context
{ctx.problem}
## Decision
Use a thin custom agent loop (no LangGraph/CrewAI) for the support triage agent.
## Options considered
- Thin custom loop (chosen)
- LangGraph
- CrewAI multi-agent
## Consequences
Positive: fast ship, portable tools.
Negative: we own bounds and future resume.
## Revisit when
- We need durable mid-flight resume or multi-agent privilege split
- Or 2026-10-01 review, whichever comes first
"""
).strip()
print(render_adr()[:200], "...")- Keep the file in git next to the service.
- Status updates happen via PR, not silent edits without history.
- Revisit triggers make the ADR alive.
7. Supersede Without Losing History
Mark the old decision and point at the new one.
from dataclasses import dataclass
@dataclass
class AdrLink:
id: str
status: AdrStatus
supersedes: str | None = None
superseded_by: str | None = None
adr001 = AdrLink(
id="ADR-001",
status=AdrStatus.SUPERSEDED,
superseded_by="ADR-014",
)
adr014 = AdrLink(
id="ADR-014",
status=AdrStatus.ACCEPTED,
supersedes="ADR-001",
)
assert adr001.superseded_by == adr014.id
assert adr014.supersedes == adr001.id
print(adr001.status, "->", adr014.id)- Never delete accepted ADRs; supersede them.
- Bidirectional links help audits and onboarding.
- New ADR should state what stayed portable (tools, evals).
8. Attach a Revisit Checklist to the Decision
Turn "review later" into a testable list.
from dataclasses import dataclass
@dataclass
class RevisitSignal:
name: str
fired: bool
signals = [
RevisitSignal("Need durable HITL resume", False),
RevisitSignal("More than one specialist privilege tier", False),
RevisitSignal("Custom loop code > 1.5k lines of orchestration", True),
RevisitSignal("Quarterly review date reached", False),
]
if any(s.fired for s in signals):
print("Schedule ADR review / spike")
for s in signals:
if s.fired:
print(" fired:", s.name)
else:
print("No revisit required")- Orchestration size and new requirements beat vibes.
- See Revisiting Old Agent Architecture Decisions as the Field Moves.
- Pair with When a Custom Runtime Stops Being Worth Maintaining.
What You Should Have Now
- A named ADR with status, owner, and date.
- Context, options, decision, and consequences.
- Config pins that match the written decision.
- A supersession pattern and revisit signals.
Next: use the full framework template and trade-off guides when the choice is larger than a single triage agent.
Related
- Why Agent Systems Need Their Own ADR Discipline - why this habit exists
- An ADR Template for Agent Framework Selection - full template
- Trade-off Guide: Single Agent vs Multi-Agent Architecture - topology
- Trade-off Guide: Managed Gateway vs Direct Provider Integration - access path
- Architecture Decisions Best Practices - team checklist
- Framework-Free vs Framework-Based: When to Roll Your Own - custom vs framework
- What Actually Differs Between Agent Frameworks - comparison axes
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.