Design Decisions
Why AgentSilex is built the way it is.
Philosophy
“Read the entire codebase in one sitting. Understand exactly how your agents work. No magic, no hidden complexity.”
Simplicity Over Features
| We chose | Instead of |
|---|---|
| ~1000 lines | 10,000+ lines |
| Dataclasses | Complex class hierarchies |
| Functions | Abstract base classes |
| Explicit | Implicit/magical |
Transparency Over Convenience
# We prefer this (explicit)
runner = Runner(session)
result = runner.run(agent, prompt)
# Over this (hidden state)
agent.chat(prompt) # Where does session live?Key Decisions
1. Explicit Runner Initialization
Decision: Runner requires explicit initialization with session and optional context/callbacks.
Why:
- Clear dependency injection
- Easy to test
- Reusable for multiple agent runs
- Session and context are explicitly passed
2. Tools as Decorated Functions
Decision: Use @tool decorator on plain functions.
Why:
- Familiar Python pattern
- Automatic schema extraction
- No base classes needed
- Easy to test tools in isolation
3. Handoffs as Tools
Decision: Agent handoffs are implemented as special tools (transfer_to_*).
Why:
- Reuses existing tool infrastructure
- LLM decides when to hand off
- Simple implementation
- No special handling needed
4. Context Injection
Decision: Tools can opt-in to receive context dict.
Why:
- Shared state without globals
- Explicit parameter
- Hidden from LLM
- Easy to test
5. LiteLLM for Models
Decision: Use LiteLLM instead of direct SDK calls.
Why:
- One API for 100+ models
- Easy model switching
- Community maintained
- Handles quirks per provider
6. OpenTelemetry for Observability
Decision: Use OTEL instead of custom logging.
Why:
- Industry standard
- Works with existing tools
- Structured traces
- Extensible
What We Intentionally Don’t Have
| Feature | Why Not |
|---|---|
| Async by default | Complexity, sync is simpler to understand |
| Built-in RAG | Out of scope, use dedicated tools |
| Prompt templates | Just use f-strings |
| Agent memory/persistence | Use external storage |
| Rate limiting | Handle at infrastructure level |
| Retries | Use tenacity or similar |
Trade-offs We Accept
No Async
Trade-off: Can’t handle concurrent requests efficiently.
Why we accept it:
- Most use cases are single-user
- Sync code is easier to debug
- Can wrap in async if needed
No Built-in Error Handling
Trade-off: Tool errors can crash the agent.
Why we accept it:
- You know your error handling needs
- Can add with callbacks
- Keeps core simple
No Automatic Retries
Trade-off: Transient failures aren’t handled.
Why we accept it:
- Different apps need different strategies
- Easy to add externally
- Keeps core predictable
Extending AgentSilex
The simplicity enables extension:
# Add retries
from tenacity import retry
@retry(stop=stop_after_attempt(3))
def run_with_retry(runner, agent, prompt):
return runner.run(agent, prompt)
# Add async
async def run_async(runner, agent, prompt):
return await asyncio.to_thread(runner.run, agent, prompt)
# Add caching
cache = {}
def run_cached(runner, agent, prompt):
key = (agent.name, prompt)
if key not in cache:
cache[key] = runner.run(agent, prompt)
return cache[key]Inspiration
AgentSilex takes inspiration from:
- Flask — Simple, explicit, hackable
- SQLAlchemy Core — Power without ORM magic
- pytest — Plain functions over classes
- FastAPI — Type hints for schema generation
Summary
AgentSilex is opinionated:
- Explicit > Implicit
- Simple > Feature-rich
- Hackable > Configurable
- Readable > Clever
If you need more features, you can build them. If you need less, you can delete code. That’s the point.