Skip to main content

What is an agent?

An agent is not a single model call. It is a loop — a stateful orchestrator that calls a model, executes tools based on the response, feeds results back into the next turn, and repeats until a validated output emerges or a limit is reached.

Model

The language model that generates responses. Anything from the Vercel AI SDK: anthropic(...), openai(...), google(...).

System Prompt

A static string or a dynamic function called each turn. Describes the agent’s role and persona.

Tools

Functions the model can call during a run. Each tool has a Zod schema, a description, and an execute function.

Output Schema

A Zod schema that shapes the agent’s return type. When set, Vibes validates and parses the model’s structured response.

Dependencies

Runtime values (databases, API clients, config) injected at agent.run() time and available everywhere inside the agent.

Result Validators

Functions that run after the output is parsed. Throw to reject and retry; return to accept (optionally modifying the output).
An agent is typed over two parameters: Agent<TDeps, TOutput>. Defaults are undefined (no deps) and string (plain text output). TypeScript infers these for you in almost all cases.

A motivating example

Let’s build something concrete: a roulette advisor that rolls a virtual wheel and recommends whether to bet on red or black.
Walk through each annotation:
  1. tool() creates a typed tool. The execute function receives RunContext as its first argument (unused here — _ctx) and the validated args as the second. It returns any serializable value.
  2. The Zod schema defines the shape of result.output. No schema → result.output is a plain string.
  3. new Agent(...) is cheap — it just stores config. Do it once at module level, not inside request handlers.
  4. name is optional but makes traces and logs much easier to read.
  5. Pick the smallest model that does the job well. claude-haiku-4-5-20251001 is 3× cheaper than Sonnet for simple tasks.
  6. systemPrompt can be a plain string (evaluated once) or a function (ctx) => string (evaluated every turn).
  7. tools is an array — Vibes does not use decorators. Pass all tools the model may need.
  8. outputSchema triggers structured output mode. Vibes injects a final_result tool under the hood; the model calls it to return the structured data.
  9. agent.run() is an ordinary async function. Await it.

Running an agent — three ways

Vibes gives you three run methods. They all execute the same loop; they differ only in how you receive the output.

1. agent.run() — await the result

The simplest option. Returns a Promise<RunResult<TOutput>> that resolves when the run is complete.
Pass result.messages back as messageHistory in the next call to continue a multi-turn conversation. The agent picks up exactly where it left off.

2. agent.stream() — stream text tokens

Returns a StreamResult<TOutput> immediately. Consume textStream for real-time tokens, then await the remaining promises after iteration.
For structured output being streamed progressively (only when outputMode: "tool"):

3. agent.runStreamEvents() — typed event stream

Returns an AsyncIterable<AgentStreamEvent<TOutput>>. Every turn, text token, tool call, and tool result is a distinct event. Use this when you need full observability or custom UI updates.
For the roulette agent, a typical trace looks like this:
The event discriminant is event.kind, not event.type. This is a common mistake when coming from other streaming libraries.

System prompts — static vs. dynamic

A system prompt can be a plain string (evaluated once at agent construction) or a function evaluated fresh every turn. Static string — simple, predictable, costs no extra computation:
Dynamic function — receives the full RunContext and can inspect deps, usage, or metadata:
instructions vs systemPrompt — Vibes distinguishes two prompt fields: instructions are injected into each turn alongside systemPrompt but are never stored in the message history. This matters for multi-turn conversations: when you pass result.messages back as messageHistory, the instructions are re-injected fresh rather than accumulating in the history.

Agent reuse

Instantiate agents once at module level. They are stateless — all run-specific state lives in RunContext and the returned RunResult, never on the agent object itself.
Because agents are stateless, they are safe to share across concurrent requests, workers, or background jobs without any locking.

agent.override() — creating variants

agent.override(overrides) returns a scoped runner that applies substituted options for a single call. The original agent is never mutated. This is the canonical pattern for testing, A/B model comparisons, and per-request overrides.
override() accepts the same fields as AgentOptions: model, systemPrompt, instructions, tools, toolsets, resultValidators, maxRetries, maxTurns, usageLimits, historyProcessors, modelSettings, endStrategy, telemetry.
Override runs bypass the setAllowModelRequests(false) guard, so they work cleanly in test environments that disable live model access.

Constructor options reference

All options passed to new Agent(opts: AgentOptions<TDeps, TOutput>):

Dependencies

Inject runtime values into tools and prompts via RunContext

Tools

Give agents the ability to take actions

Results

RunResult, StreamResult, and output validation