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.
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.SupportDepsis a plain TypeScript interface. No framework types, no decorators. Define it wherever makes sense in your codebase.tool<SupportDeps>binds the tool to the deps type. TypeScript will catch any mismatch between the deps declared here and what you pass atagent.run()time.ctx.deps.dbis the injected database. The tool implementation never imports a global — it only uses what is given throughctx.new Agent<SupportDeps>(...)declares the agent’s dependency contract. The second type parameter (TOutput) is omitted and defaults tostring.systemPromptas a function receives the sameRunContext. You can build dynamic prompts from runtime state without any closures or globals.depsis passed inRunOptions. Swap in a test double here for testing — the agent code stays completely unchanged.
The RunContext interface
Every callback inside an agent — system prompts, toolexecute 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:
metadata alongside deps. Metadata is caller-controlled, per-run, and separate from deps:
Using deps in tools
Tools receiveRunContext<TDeps> as their first argument. Access anything you injected:
Using deps in dynamic system prompts
System prompts and instructions can be functions that receiveRunContext. This gives you per-run personalization without any closures:
Using deps in result validators
Result validators also receiveRunContext, so you can use runtime state to decide whether an output is acceptable:
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.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