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:

  1. Explicit > Implicit
  2. Simple > Feature-rich
  3. Hackable > Configurable
  4. Readable > Clever

If you need more features, you can build them. If you need less, you can delete code. That’s the point.