Runtime
The runtime provides your agent's tools and behaviors with access to the current conversation context. It includes XMTP message data and can be extended with custom properties for your application.
What is Runtime?
Runtime is a context object passed to every tool execution and behavior. It contains:
interface AgentRuntime {
conversation: XmtpConversation // The XMTP conversation
message: XmtpMessage // The current message
}Accessing Runtime in Tools
Tools automatically receive the runtime context:
import { createTool } from "hybrid"
import { z } from "zod"
const myTool = createTool({
description: "Example tool that uses runtime",
inputSchema: z.object({
query: z.string()
}),
execute: async ({ input, runtime }) => {
// Access conversation context
const { conversation, message } = runtime
// Use XMTP data
console.log(`Sender: ${message.senderInboxId}`)
console.log(`Conversation ID: ${conversation.id}`)
// Send additional messages if needed
await conversation.send(`Processing: ${input.query}`)
return { success: true }
}
})Runtime in Behaviors
Behaviors also receive runtime through their context:
import type { BehaviorObject, BehaviorContext } from "hybrid/behaviors"
function customBehavior(): BehaviorObject {
return {
id: "custom-behavior",
config: { enabled: true },
async before(context: BehaviorContext) {
const { message, conversation, client } = context
// Access runtime data
const sender = message.senderInboxId
const isGroup = conversation.isGroup
console.log(`Message from ${sender} in ${isGroup ? 'group' : 'DM'}`)
}
}
}Extending Runtime
Extend the runtime with custom properties for your application using createRuntime:
Basic Extension
Add custom data to all tools and behaviors:
interface MyRuntimeExtension {
apiKey: string
userId: string
}
const agent = new Agent<MyRuntimeExtension>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: (baseRuntime) => ({
apiKey: process.env.MY_API_KEY!,
userId: "user-123"
})
})Using Extended Runtime in Tools
Access custom properties in your tools:
const myTool = createTool({
description: "Tool using custom runtime",
inputSchema: z.object({
action: z.string()
}),
execute: async ({ input, runtime }) => {
// Access base runtime
const { conversation, message } = runtime
// Access custom properties (with type safety)
const { apiKey, userId } = runtime as AgentRuntime & MyRuntimeExtension
// Use custom data
const response = await fetch("https://api.example.com/data", {
headers: {
"Authorization": `Bearer ${apiKey}`,
"X-User-ID": userId
}
})
return { success: true, data: await response.json() }
}
})Dynamic Runtime
Create runtime context dynamically based on the current message:
interface UserContext {
userId: string
preferences: Record<string, unknown>
metadata: Record<string, unknown>
}
const agent = new Agent<UserContext>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: async (baseRuntime) => {
// Extract user from message
const senderId = baseRuntime.message.senderInboxId
// Fetch user data from database
const userData = await db.users.findOne({ id: senderId })
return {
userId: senderId,
preferences: userData?.preferences || {},
metadata: userData?.metadata || {}
}
}
})Common Use Cases
Database Access
Provide database clients to all tools:
import { createClient } from "@supabase/supabase-js"
interface DatabaseRuntime {
db: ReturnType<typeof createClient>
}
const agent = new Agent<DatabaseRuntime>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: (runtime) => ({
db: createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY!
)
})
})
// Use in tools
const getUserTool = createTool({
description: "Get user data",
inputSchema: z.object({ userId: z.string() }),
execute: async ({ input, runtime }) => {
const { db } = runtime as AgentRuntime & DatabaseRuntime
const { data, error } = await db
.from("users")
.select("*")
.eq("id", input.userId)
.single()
return { success: !error, data }
}
})API Clients
Share API clients across tools:
interface APIRuntime {
stripe: Stripe
openai: OpenAI
}
const agent = new Agent<APIRuntime>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: (runtime) => ({
stripe: new Stripe(process.env.STRIPE_KEY!),
openai: new OpenAI({ apiKey: process.env.OPENAI_KEY! })
})
})User Preferences
Load and access user-specific settings:
interface PreferencesRuntime {
language: string
timezone: string
features: string[]
}
const agent = new Agent<PreferencesRuntime>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: async (runtime) => {
const userId = runtime.message.senderInboxId
const prefs = await loadUserPreferences(userId)
return {
language: prefs.language || "en",
timezone: prefs.timezone || "UTC",
features: prefs.enabledFeatures || []
}
}
})Session State
Maintain session-specific state:
interface SessionRuntime {
sessionId: string
startTime: number
cache: Map<string, unknown>
}
const sessionCache = new Map<string, Map<string, unknown>>()
const agent = new Agent<SessionRuntime>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: (runtime) => {
const sessionId = runtime.conversation.id
if (!sessionCache.has(sessionId)) {
sessionCache.set(sessionId, new Map())
}
return {
sessionId,
startTime: Date.now(),
cache: sessionCache.get(sessionId)!
}
}
})Type Safety
Use TypeScript generics for full type safety:
import type { AgentRuntime } from "hybrid"
// Define your extension interface
interface MyRuntime {
apiKey: string
userId: string
}
// Type the agent
const agent = new Agent<MyRuntime>({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: (runtime) => ({
apiKey: process.env.API_KEY!,
userId: "user-123"
})
})
// Create typed tools
const typedTool = createTool({
description: "Typed tool",
inputSchema: z.object({ action: z.string() }),
execute: async ({ input, runtime }) => {
// TypeScript knows about custom properties
const extended = runtime as AgentRuntime & MyRuntime
const apiKey = extended.apiKey // ✅ Type-safe
const userId = extended.userId // ✅ Type-safe
return { success: true }
}
})Best Practices
Initialize Once
Initialize expensive resources once in createRuntime:
const agent = new Agent({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: (runtime) => {
// ✅ Good: Initialize once
const db = createDatabaseClient()
return { db }
}
})
// ❌ Bad: Don't initialize in tools
const badTool = createTool({
description: "Bad example",
inputSchema: z.object({}),
execute: async ({ runtime }) => {
// ❌ This creates a new client every time
const db = createDatabaseClient()
return { success: true }
}
})Async Runtime Creation
Use async functions for runtime setup:
const agent = new Agent({
name: "My Agent",
model: yourModel,
instructions: "...",
createRuntime: async (runtime) => {
// Fetch data during setup
const config = await fetchRemoteConfig()
const cache = await initializeCache()
return {
config,
cache
}
}
})Avoid Side Effects
Keep createRuntime pure when possible:
// ✅ Good: Return data, no side effects
createRuntime: (runtime) => ({
apiKey: process.env.API_KEY,
baseUrl: "https://api.example.com"
})
// ⚠️ Acceptable: Necessary side effects for setup
createRuntime: async (runtime) => {
const logger = createLogger()
logger.info("Session started", { user: runtime.message.senderInboxId })
return { logger }
}
// ❌ Bad: Unnecessary side effects
createRuntime: (runtime) => {
console.log("Creating runtime...") // Don't log unnecessarily
globalState.incrementCounter() // Don't modify global state
return { apiKey: process.env.API_KEY }
}Next Steps
- Learn about Tools to create custom capabilities
- Explore Behaviors for message processing
- Check out Prompts for dynamic instructions
- See XMTP Tools for messaging capabilities