Foundations
•6 min read
Running Agents
Run a single AI agent in three lines.
This is the minimal path. No orchestrator, no guardrails, no memory – just a runner function, an agent, and an input. When you need more, layer in the orchestrator.
What Is a Runner?
A runner is an async function that sends a prompt to an LLM provider and returns a standardized result. It handles the HTTP call, authentication, and response parsing for a specific provider (OpenAI, Anthropic, Ollama, etc.) so your application code stays provider-agnostic. Think of it as a thin adapter: (agent, input) => RunResult.
Directive ships pre-built runners (createOpenAIRunner, createAnthropicRunner, createOllamaRunner) and a createRunner helper for custom providers. Every runner returns the same RunResult shape – swap providers by changing one line.
Quick Start
import { createOpenAIRunner } from '@directive-run/ai/openai';
// Create a runner for OpenAI (just needs an API key)
const runner = createOpenAIRunner({
apiKey: process.env.OPENAI_API_KEY!,
});
// Define an agent
const agent = {
name: 'assistant',
instructions: 'You are helpful.',
model: 'gpt-4o',
};
// Run the agent with user input
const result = await runner(agent, 'What is WebAssembly?');
console.log(result.output); // "WebAssembly is..."
console.log(result.totalTokens); // 142
console.log(result.tokenUsage); // { inputTokens: 42, outputTokens: 100 }
That's it. runner is a plain async function – no framework, no state, no setup.
Choose a Provider
Directive ships pre-built runners for common providers. Each returns a standard AgentRunner:
OpenAI
import { createOpenAIRunner } from '@directive-run/ai/openai';
const runner = createOpenAIRunner({
apiKey: process.env.OPENAI_API_KEY!,
model: 'gpt-4o', // Default model (agent can override)
baseURL: 'https://api.openai.com/v1', // Works with Azure, Together, etc.
});
Anthropic (Claude)
import { createAnthropicRunner } from '@directive-run/ai/anthropic';
const runner = createAnthropicRunner({
apiKey: process.env.ANTHROPIC_API_KEY!,
model: 'claude-sonnet-4-5-20250929', // Default Claude model
maxTokens: 4096, // Max output tokens per request
});
Google Gemini
import { createGeminiRunner } from '@directive-run/ai/gemini';
const runner = createGeminiRunner({
apiKey: process.env.GEMINI_API_KEY!,
model: 'gemini-2.0-flash', // Default Gemini model
maxOutputTokens: 4096, // Max output tokens per request
});
Ollama (Local)
import { createOllamaRunner } from '@directive-run/ai/ollama';
// Connect to a locally running Ollama instance – no API key needed
const runner = createOllamaRunner({
model: 'llama3',
baseURL: 'http://localhost:11434', // Default Ollama address
});
Define an Agent
An agent is a plain object with name, instructions, and model:
import type { AgentLike } from '@directive-run/ai';
// An agent is a plain object – name, instructions, and optional model
const agent: AgentLike = {
name: 'code-reviewer',
instructions: 'You review code for bugs, security issues, and style.',
model: 'gpt-4o', // Optional – falls back to the runner's default model
};
The model field is optional – if omitted, the runner's default model is used.
Model names are not validated
The model field is an untyped string – Directive passes it directly to the provider's API without validation. If you use a model name the provider doesn't recognize, you'll get an error from the provider (e.g., OpenAI returns a 404). This is intentional: providers add models frequently, and runners like the OpenAI adapter also work with Azure, Together, and other compatible APIs that have their own model names.
Run Result
Every runner() call returns a RunResult:
const result = await runner(agent, 'Review this function: function add(a, b) { return a + b; }');
// Every RunResult includes these fields
result.output; // string – the agent's response
result.messages; // Message[] – full conversation (user + assistant turns)
result.toolCalls; // ToolCall[] – any tool calls made (empty for basic runs)
result.totalTokens; // number – total tokens consumed
result.tokenUsage; // { inputTokens, outputTokens } – breakdown by direction
Cost Tracking
Every adapter returns a tokenUsage breakdown alongside totalTokens. Pair it with the pricing constants each adapter exports:
import { estimateCost } from '@directive-run/ai';
import { createOpenAIRunner, OPENAI_PRICING } from '@directive-run/ai/openai';
const runner = createOpenAIRunner({ apiKey: process.env.OPENAI_API_KEY! });
const result = await runner(agent, 'Summarize this document...');
const { inputTokens, outputTokens } = result.tokenUsage!;
const cost =
estimateCost(inputTokens, OPENAI_PRICING['gpt-4o'].input) +
estimateCost(outputTokens, OPENAI_PRICING['gpt-4o'].output);
console.log(`$${cost.toFixed(6)}`); // "$0.001025"
Available pricing constants:
| Import | Models |
|---|---|
OPENAI_PRICING from @directive-run/ai/openai | gpt-4o, gpt-4o-mini, gpt-4-turbo, o3-mini |
ANTHROPIC_PRICING from @directive-run/ai/anthropic | claude-sonnet-4-5-20250929, claude-haiku-3-5-20241022, claude-opus-4-20250514 |
GEMINI_PRICING from @directive-run/ai/gemini | gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash, gemini-2.0-flash-lite |
Pricing disclaimer
Pricing changes over time. The constants are provided as a convenience and may not reflect the latest rates. Always verify at your provider's pricing page.
Lifecycle Hooks
Attach hooks to any adapter for tracing, logging, and metrics without modifying application code:
import { createAnthropicRunner } from '@directive-run/ai/anthropic';
const runner = createAnthropicRunner({
apiKey: process.env.ANTHROPIC_API_KEY!,
hooks: {
onBeforeCall: ({ agent, input }) => {
console.log(`→ ${agent.name}`, input.slice(0, 50));
},
onAfterCall: ({ durationMs, tokenUsage, totalTokens }) => {
metrics.track('llm_call', {
durationMs,
inputTokens: tokenUsage.inputTokens,
outputTokens: tokenUsage.outputTokens,
totalTokens,
});
},
onError: ({ error, durationMs }) => {
Sentry.captureException(error, { extra: { durationMs } });
},
},
});
| Hook | Fires | Payload |
|---|---|---|
onBeforeCall | Before each LLM API call | agent, input, timestamp |
onAfterCall | After a successful response | agent, input, output, totalTokens, tokenUsage, durationMs, timestamp |
onError | When a call fails | agent, input, error, durationMs, timestamp |
Hooks work on all standard runners (createOpenAIRunner, createAnthropicRunner, createGeminiRunner, createOllamaRunner) and streaming runners (createOpenAIStreamingRunner, createAnthropicStreamingRunner, createGeminiStreamingRunner).
Custom Runner
For providers without a pre-built helper, use createRunner:
import { createRunner } from '@directive-run/ai';
const runner = createRunner({
// Build the HTTP request from the agent definition and user input
buildRequest: (agent, input) => ({
url: 'https://my-llm.example.com/chat',
init: {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ...' },
body: JSON.stringify({
model: agent.model ?? 'default-model',
system: agent.instructions ?? '',
messages: [{ role: 'user', content: input }],
}),
},
}),
// Extract the text and token count from the raw HTTP response
parseResponse: async (res) => {
const data = await res.json();
return {
text: data.output ?? '',
totalTokens: data.usage?.total ?? 0,
};
},
});
Or write an AgentRunner from scratch:
import type { AgentRunner } from '@directive-run/ai';
// Implement the AgentRunner interface from scratch
const runner: AgentRunner = async (agent, input, options) => {
const response = await fetch('/api/chat', {
method: 'POST',
signal: options?.signal, // Support cancellation via AbortSignal
body: JSON.stringify({ model: agent.model, prompt: input }),
});
const data = await response.json();
// Return a standard RunResult so it works with the orchestrator and stack
return {
output: data.text,
messages: [
{ role: 'user', content: input },
{ role: 'assistant', content: data.text },
],
toolCalls: [],
totalTokens: data.tokens ?? 0,
};
};
When to Add More
The raw runner is perfect for scripts, one-off calls, and simple integrations. Layer in more features as your needs grow:
| Need | Solution |
|---|---|
| Retry with backoff, fallback providers | Resilience & Routing |
| Cost budget limits, model routing | Resilience & Routing |
| Typed JSON output from LLMs | Resilience & Routing |
| Guardrails (input/output validation) | Orchestrator |
| Approval workflows | Orchestrator |
| Token budgets | Orchestrator |
| Reactive UI state | Orchestrator + Framework hooks |
| Memory / conversation context | Orchestrator |
| Caching, circuit breakers, observability | Resilience & Routing |
| Parallel / sequential / supervisor patterns | Multi-Agent |
Next Steps
- Resilience & Routing – retry, fallback, budgets, model selection, structured outputs
- Orchestrator – add guardrails, approvals, and state tracking
- Streaming – real-time token streaming
- Guardrails – input validation and output safety

