Skip to main content

Why dependency injection?

Consider a support agent that queries a database. Without dependency injection you have two bad options:
  • Global variable — the database is a module-level singleton. This makes tests fragile (they share state), makes the dependency invisible in the type signature, and creates ordering problems at startup.
  • Closure — you close over the database when building the tool. This ties every tool to one specific database instance and makes it impossible to swap for a test double.
Vibes takes a third path: declare what you need as a type, inject the concrete instance at agent.run() time, and every callback inside the agent receives it automatically via RunContext. The result is a fully self-contained, trivially testable agent where the type checker verifies the dependency contract at every boundary.

A motivating example

Let’s build a customer lookup agent that needs a database to work. We’ll see the dependency flow from declaration → injection → usage in tools and prompts.
Walk through the annotations:
  1. SupportDeps is a plain TypeScript interface. No framework types, no decorators. Define it wherever makes sense in your codebase.
  2. tool<SupportDeps> binds the tool to the deps type. TypeScript will catch any mismatch between the deps declared here and what you pass at agent.run() time.
  3. ctx.deps.db is the injected database. The tool implementation never imports a global — it only uses what is given through ctx.
  4. new Agent<SupportDeps>(...) declares the agent’s dependency contract. The second type parameter (TOutput) is omitted and defaults to string.
  5. systemPrompt as a function receives the same RunContext. You can build dynamic prompts from runtime state without any closures or globals.
  6. deps is passed in RunOptions. Swap in a test double here for testing — the agent code stays completely unchanged.

The RunContext interface

Every callback inside an agent — system prompts, tool execute functions, result validators, history processors — receives a RunContext<TDeps>. Its full interface is:
ctx.usage is a live snapshot. Read it inside a tool to check how many tokens have been consumed before making an expensive API call. Read it inside a result validator to enforce cost limits per run.

Passing deps to agent.run()

Every run method — run(), stream(), runStreamEvents() — accepts a RunOptions second argument where you pass deps:
You can also pass metadata alongside deps. Metadata is caller-controlled, per-run, and separate from deps:

Using deps in tools

Tools receive RunContext<TDeps> as their first argument. Access anything you injected:

Using deps in dynamic system prompts

System prompts and instructions can be functions that receive RunContext. This gives you per-run personalization without any closures:
Use systemPrompt for persistent identity and instructions for per-run rules that should NOT accumulate in message history across multi-turn conversations.

Using deps in result validators

Result validators also receive RunContext, so you can use runtime state to decide whether an output is acceptable:
If the validator throws, Vibes sends the error message back to the model as feedback and retries (up to maxRetries times). The model can then produce a revised answer that satisfies the constraint.

Full end-to-end example

Here is an agent that uses deps in all three places — system prompt, tool, and result validator — with a complete type-safe flow:

Testing with mock dependencies

The deps pattern makes testing straightforward: swap the real database for an in-memory mock. No patching, no module mocking — just pass different deps.
agent.override({ model: testModel }) bypasses the setAllowModelRequests(false) guard automatically, so your test suite can call override runs even when live model access is disabled.
The recommended testing pattern: inject test doubles via deps, script model behavior with TestModel, and assert on result.output. Never mock Vibes internals — the framework stays intact; only the external dependencies change.

Tools

Use deps inside tool execute functions

Agents

Full Agent constructor options and run methods