Chapter 8

Dependency Injection for Agents

From God Prompt to Composable Architecture

Part IV — The New Architecture 7 sections

Dependency Injection (DI) is a solution to tight coupling: a class should not construct its own dependencies. It should declare what it needs, and something external — a DI container — should provide those dependencies at runtime. The class gets what it needs; it does not know or care where it came from.

This pattern is not a best practice for agentic systems — it is a survival requirement. The alternative is the God Prompt.


8.1   The God Prompt Anti-Pattern

The God Prompt is what you get when you build an agent without dependency injection: a single, massive system prompt that tries to define everything the agent might ever need — 15,000 tokens of mixed-concern instructions, 12 different service categories, conditional logic scattered throughout. It exhibits every failure mode that DI was designed to prevent:

Context rot (Chapter 4): 15,000 tokens dilute attention. Untestable: how do you write an eval that covers 12 service categories? Unmaintainable: fixing refund logic requires editing a prompt that also contains shipping logic. Expensive: every call pays the token cost for all 15,000 tokens, even for a simple question.


8.2   The DI Translation

DI ConceptTraditional (Java/C#)Agentic Equivalent
InterfaceISearch — defines the capability contractTool Schema — JSON definition of what the tool does
ImplementationGoogleSearch — the concrete classTool Execution — the Python function / API call
Injection pointConstructor parameterRuntime context assembly: tools added to system prompt at call time
DI ContainerSpring IoC / .NET IServiceCollectionOrchestrator — assembles the agent's context for each invocation
Mockingnew MockSearch() in testsInjected fake JSON responses in eval context
Fig 8.1 — Agentic DI: From Intent to Injected Context
① User Request "Analyse our Q3 sales pipeline" ② Intent Resolver Classifies: domain, tools needed, user role ③ DI Container — assembles agent System prompt template User role: Sales VP Tools: CRM, SQL, chart RAG: Q3 deal docs Memory: prior queries Constraints: read-only ④ Assembled Agent Instance Fully wired at runtime — identity + tools + data + memory injected Zero hardcoded dependencies Comparison: Hardcoded vs DI ❌ agent.tools = [crm, sql] # hardcoded ❌ agent.role = "analyst" # hardcoded ✓ agent = container.build(intent) ✓ # tools resolved from intent # different user → different tools auto-injected

Agentic DI assembles an agent's identity, tools, data sources, and memory at runtime from a specification — never by hardcoding. The same role class can serve multiple personas by injecting different dependencies.


8.3   What Gets Injected?

A well-designed agentic DI system injects four categories of dependency:

Tools

The most visible injection. A tool is defined by a schema (the interface) and implemented by a function or API call (the implementation). The agent receives the schema in its context — it knows what the tool can do. The actual implementation is invisible to the agent.

Tool Schema — The Interface
{
  "name": "get_order_status",
  "description": "Retrieves the current status and estimated delivery 
                  date for a customer order.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": {
        "type": "string",
        "description": "The order ID from the customer's confirmation email"
      }
    },
    "required": ["order_id"]
  }
}

Context and Data

Injected context includes: user account information, conversation history, relevant documents retrieved from RAG, and any other data the agent needs that was not available at system prompt authoring time.

Dynamic Context Assembly
def build_agent_context(user_id: str, user_query: str) -> dict:
    user_profile = user_db.get(user_id)
    recent_orders = order_db.get_recent(user_id, limit=5)
    relevant_docs = rag.retrieve(user_query, top_k=3)
    
    return {
        "system": BASE_SYSTEM_PROMPT,
        "injected_context": {
            "user": user_profile,
            "recent_orders": recent_orders,
            "relevant_policy_docs": relevant_docs,
        },
        "user_message": user_query
    }

Persona and Constraints

Different use cases may require different agent personalities or authority levels. Rather than maintaining separate system prompts for each persona, a modular DI approach maintains a base system prompt and injects the persona module at runtime. Regulatory constraints, tone requirements, and policy restrictions are all injectable as modular blocks.


8.4   Dynamic vs. Static Injection

Static injection: The same tools and context are injected for every invocation. Simple to implement, but potentially wasteful — an agent that handles both financial and shipping queries carries both tool sets in every context.

Dynamic injection: The Orchestrator first classifies the intent (using a Semantic Router, Chapter 5), then injects only the tools and context relevant to that intent.

Fig 8.2 — Static vs. Dynamic Tool Injection
Static Tool Injection (avoid) tools = [ search, sql, crm, chart, file_read, file_write, email, ... ] # ALL tools loaded, always Problem: tokens wasted on unused tool descriptions Problem: model confused by too many choices Problem: write tools always present → unsafe for read-only agents Dynamic Tool Injection (prefer) tools = container.resolve( intent=user_intent, role=user.role ) # only needed tools loaded ✓ Token budget preserved — only 2–5 tools loaded per call ✓ Model decision-making improves with fewer choices ✓ Role-based security: read-only agents never see write tools

Dynamic tool injection resolves the tool list from the intent and role at runtime. The result is a smaller, safer, and more focused tool set — which directly improves the accuracy of the agent's tool-selection decisions.

The practical recommendation: start with static injection to validate your agent's behavior. Move to dynamic injection when you have more than 5–6 tools, or when token costs become a constraint.


8.5   Hot-Swapping Models: The Liskov Connection

One of the most powerful consequences of agentic DI is model substitution: the ability to swap the underlying LLM without changing any agent logic. This is the agentic realization of the Liskov Substitution Principle (Chapter 9): the prompt is the interface; any model that can process the prompt is a valid implementation.

Model Selection at Runtime
def create_agent(task_type: str, config: Config) -> Agent:
    if task_type == "classification":
        model = config.fast_cheap_model  # e.g., gemini-flash
    elif task_type == "reasoning":
        model = config.powerful_model    # e.g., gemini-pro
    elif task_type == "code_generation":
        model = config.code_model        # e.g., claude-3-5-sonnet
    
    return Agent(
        system_prompt=AGENT_PROMPTS[task_type],
        tools=AGENT_TOOLS[task_type],
        model=model  # injected — not hardcoded
    )

8.6   Security Implications of DI

Agentic DI is not just an architectural pattern — it is a security control. Credentials are never in the prompt. API keys, database connection strings, and OAuth tokens are injected by the Orchestrator at runtime — never hardcoded in the system prompt. Data access is controlled by the Orchestrator. The agent does not go directly to the database. It calls a tool. The tool implementation enforces authorization. Least privilege (Chapter 14) is implemented through selective tool injection — limiting the blast radius of a prompt injection attack.


8.7   Chapter Summary

Dependency Injection transforms agents from monolithic God Prompts into composable, testable, swappable components. The Orchestrator assembles the right context for each task; the agent reasons with what it receives. Tools are interfaces — implementations are invisible to the agent. Model substitution is natural when the prompt is the interface contract.

Core Principle — Chapter 8

The best agents do not know where their tools come from — they only know how to use them. The Orchestrator makes the decisions. The agent does the work.