This project is built with the Botpress Agent Development Kit (ADK) - a TypeScript-first framework for building AI agents.
- Quick Reference: Use the Botpress MCP Server
- What is the ADK?
- ADK CLI
- Core Concepts
- Project Structure
- Development Workflow
- Examples
- Best Practices
- Common APIs
- Advanced Autonomous Execution
- State and Metadata Management
- Advanced Table Operations
- Knowledge Base Operations
- Advanced Conversation Patterns
- Citations System
- When Making Changes
- Resources
IMPORTANT: When working on this project, always search the Botpress documentation using the mcp__botpress-docs__SearchBotpress tool before making changes. The ADK has specific patterns and APIs that are well-documented.
The ADK allows developers to build Botpress agents using code instead of the Studio interface. It provides:
- Project scaffolding with TypeScript
- Hot reloading development server (
adk dev) - Type-safe APIs and auto-generated types
- Build and deploy to Botpress Cloud
The ADK CLI is installed globally. You can run it using adk <command>.
Always use bash to run ADK. (Bash(adk))
To install an integration: adk install <integration>
To generate types without running in dev mode: adk build
The main configuration file defines:
- Agent name and description
- Default models for autonomous and zai operations
- State schemas (bot-level and user-level state using Zod)
- Configuration variables (encrypted, secure storage for API keys)
- Integration dependencies (webchat, chat, etc.)
export default defineConfig({
name: "my-agent",
defaultModels: {
autonomous: "cerebras:gpt-oss-120b",
zai: "cerebras:gpt-oss-120b",
},
bot: { state: z.object({}) },
user: { state: z.object({}) },
dependencies: {
integrations: {
webchat: { version: "webchat@0.3.0", enabled: true },
},
},
});Primary way agents handle user messages. Each conversation handler:
- Responds to messages from specific channels
- Uses
execute()to run autonomous AI logic - Can access conversation state, send messages, and call tools
Key Pattern: The execute() function runs the agent's AI loop:
export default new Conversation({
channel: "webchat.channel",
handler: async ({ execute, conversation, state }) => {
await execute({
instructions: "Your agent's instructions here",
tools: [myTool1, myTool2],
knowledge: [myKnowledgeBase],
});
},
});Long-running processes for complex, multi-step operations:
- Can run on schedules (cron syntax)
- Run independently or triggered by events
- NOT the same as Studio Workflows
- Use
step()for durable execution (survives restarts)
export default new Workflow({
name: "periodic-indexing",
schedule: "0 */6 * * *",
handler: async ({ step }) => {
await step("task-name", async () => {
// Your logic here
});
},
});Beyond basic step(), workflows have powerful methods for complex orchestration:
Parallel Processing:
step.map()- Process array items in parallel with concurrency controlstep.forEach()- Like map but for side effects (returns void)step.batch()- Process in sequential batches
// Process items in parallel
const results = await step.map(
'process-items',
items,
async (item, { i }) => processItem(item),
{ concurrency: 5, maxAttempts: 3 }
)
// Batch processing
await step.batch(
'bulk-insert',
records,
async (batch) => database.bulkInsert(batch),
{ batchSize: 100 }
)Workflow Coordination:
step.waitForWorkflow()- Wait for another workflow to completestep.executeWorkflow()- Start and wait in one call
const result = await step.executeWorkflow('run-child', ChildWorkflow, { input })Timing Control:
step.sleep()- Pause execution (< 10s in-memory, >= 10s uses listening mode)step.sleepUntil()- Sleep until specific timestep.listen()- Pause and wait for external event
await step.sleep('wait-5s', 5000)
await step.sleepUntil('wait-until-noon', new Date('2025-01-15T12:00:00Z'))Request Data from Conversation:
// In workflow
const { topic } = await step.request('topic', 'What topic should I research?')
// In conversation
if (isWorkflowDataRequest(event)) {
await workflow.provide(event, { topic: userInput })
}Execution Control:
step.fail()- Mark workflow as failedstep.abort()- Abort without failingstep.progress()- Record progress checkpoint
AI-callable functions that enable agents to perform actions:
- Must have clear name and description
- Use Zod schemas for input/output
- Can be passed to
execute()
export default new Autonomous.Tool({
name: "searchDatabase",
description: "Search the database",
input: z.object({ query: z.string() }),
output: z.object({ results: z.array(z.any()) }),
handler: async ({ query }) => {
// Tool logic
return { results: [] };
},
});RAG (Retrieval-Augmented Generation) for providing context:
- Website scraping
- Document ingestion
- Can be passed to
execute()viaknowledgeparameter
Reusable business logic that can:
- Be called from anywhere (import
actionsfrom@botpress/runtime) - Be converted to tools with
.asTool() - Encapsulate logic not tied to conversational flow
Zai is an LLM utility library that provides a clean, type-safe API for common AI operations. It's designed to work seamlessly with the ADK and SDK to process LLM inputs and outputs programmatically.
In the ADK, Zai is available from @botpress/runtime:
import { adk } from '@botpress/runtime'
// then adk.zai.<method_name>The default model for Zai operations is configured in agent.config.ts:
export default defineConfig({
defaultModels: {
autonomous: "cerebras:gpt-oss-120b",
zai: "cerebras:gpt-oss-120b", // Model used for Zai operations
},
})Use Zai when you need to:
- Extract structured data from unstructured text
- Answer questions from documents with source citations
- Verify Boolean conditions in content
- Summarize long text into concise summaries
- Generate text programmatically based on prompts
Use Zai instead of execute() when: You need deterministic, structured outputs for specific AI tasks (extraction, validation, summarization) rather than conversational interactions.
1. answer() - Answer Questions with Citations
Answers questions from documents with intelligent source citations.
const documents = [
'Botpress was founded in 2016.',
'The company is based in Quebec, Canada.',
]
const result = await zai.answer(documents, 'When was Botpress founded?')
if (result.type === 'answer') {
console.log(result.answer) // "Botpress was founded in 2016."
console.log(result.citations) // Array of citations with source references
}When to use: When you need to answer questions from a set of documents with traceable sources (e.g., custom RAG implementations, document Q&A).
2. extract() - Extract Structured Data
Extracts structured data from unstructured input using Zod schemas.
import { z, adk } from '@botpress/runtime'
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number()
})
const input = "My name is John Doe, I'm 30 years old and my email is john@example.com"
// zai.extract returns the extracted data DIRECTLY (not wrapped in { output: ... })
const result = await adk.zai.extract(input, userSchema)
console.log(result)
// { name: "John Doe", email: "john@example.com", age: 30 }When to use: When you need to parse unstructured user input into structured data (e.g., form extraction from natural language, parsing contact information).
3. check() - Verify Boolean Conditions
Verifies a condition against some input and returns a boolean with explanation.
const email = "Get rich quick! Click here now!!!"
const { output } = await zai.check(email, 'is spam').result()
console.log(output.value) // true
console.log(output.explanation) // "This email contains typical spam indicators..."When to use: When you need to validate content or make binary decisions (e.g., content moderation, intent verification, condition checking).
4. summarize() - Summarize Text
Creates concise summaries of lengthy text to a desired length.
const longArticle = "..." // Long article content
const summary = await zai.summarize(longArticle, {
length: 100, // tokens
prompt: 'key findings and main conclusions'
})When to use: When you need to condense long content (e.g., article summaries, transcript summaries, document overviews).
5. text() - Generate Text
Generates text of the desired length according to a prompt.
const generated = await zai.text('Write a welcome message for new users', {
length: 50 // tokens
})When to use: When you need to generate specific text content programmatically (e.g., dynamic content generation, templated responses).
All Zai operations return a Response object with promise-like behavior and additional functionality:
// Await the result directly
const result = await zai.extract(input, schema)
// Or use .result() for explicit promise handling
const { output } = await zai.check(content, 'is valid').result()agent.config.ts # Main configuration
src/
conversations/ # Message handlers (primary user interaction)
workflows/ # Long-running processes
tools/ # AI-callable functions
actions/ # Reusable business logic
knowledge/ # Knowledge bases for RAG
triggers/ # Event-based triggers
tables/ # Database tables
.botpress/ # Auto-generated types (DO NOT EDIT)
- Start dev server:
adk dev(http://localhost:3001 for console) - Add integrations:
adk add webchat@latest - Build:
adk build - Deploy:
adk deploy - Chat in CLI:
adk chat
Official examples: https://github.com/botpress/adk/tree/main/examples
What you'll learn: How to build a multi-agent system where an orchestrator delegates to specialists.
Shows the SubAgent pattern where each specialist (HR, IT, Sales, etc.) runs in its own context with mode: "worker", returns structured results via custom exits, and reports progress through onTrace hooks.
What you'll learn: How to build a RAG assistant with scheduled indexing, guardrails, and admin features.
Shows Autonomous.Object for dynamic tool grouping, onBeforeTool hooks to enforce knowledge search before answering, scheduled workflows for KB refresh, and ThinkSignal for interrupting execution.
What you'll learn: How to build complex, long-running workflows with progress tracking.
Shows step() and step.map() for workflow phases, Reference.Workflow for conversation-workflow linking, Tables for activity tracking, and extensive Zai usage (extract, answer, filter, text).
- Search Botpress docs first - Use the MCP tool before implementing
- Keep tools focused - Single responsibility per tool
- Use Zod schemas with
.describe()for clarity - State management - Minimize large variables in main workflow
- Type safety - Run
adk devoradk buildto regenerate types after config changes - Conversations vs Workflows:
- Conversations: User interactions, real-time responses
- Workflows: Background tasks, scheduled jobs, long-running processes
handler: async ({
execute, // Run autonomous AI loop
conversation, // Send messages, manage conversation
state, // Conversation state (persisted)
message, // Incoming message
client, // Botpress API client
}) => {};await execute({
instructions: "String or function returning instructions",
tools: [tool1, tool2], // Optional tools
knowledge: [kb1, kb2], // Optional knowledge bases
exits: [customExit], // Optional custom exits
hooks: { onTrace, onBeforeTool }, // Optional hooks
mode: "worker", // Optional: autonomous until exit
iterations: 10, // Max loops (default 10)
});The Autonomous namespace provides powerful primitives for controlling LLM behavior:
Define custom exits for autonomous execution loops:
import { Autonomous, z } from '@botpress/runtime'
const AnswerExit = new Autonomous.Exit({
name: 'answer',
description: 'Return when you have the final answer',
schema: z.object({
answer: z.string(),
confidence: z.number()
})
})
const NoAnswerExit = new Autonomous.Exit({
name: 'no_answer',
description: 'No answer could be found'
})
const result = await execute({
instructions: 'Research and answer the question',
exits: [AnswerExit, NoAnswerExit],
mode: 'worker' // Run until exit triggered
})
// ✅ CORRECT - Use result.is() and result.output
if (result.is(AnswerExit)) {
console.log(result.output.answer) // Type-safe access
console.log(result.output.confidence)
} else if (result.is(NoAnswerExit)) {
console.log('No answer found')
}
// ❌ WRONG - Don't use result.exit.name or result.exit.value
// if (result.exit?.name === 'answer') { ... }Provide context to the LLM without continuing execution:
const results = await fetchData()
if (!results.length) {
throw new ThinkSignal('error', 'No results found')
}
// Inject formatted results into LLM context
throw new ThinkSignal('results ready', formatResults(results))Group tools dynamically based on state:
const adminTools = new Autonomous.Object({
name: 'admin',
description: user.isAdmin ? 'Admin tools available' : 'Login required',
tools: user.isAdmin ? [refreshKB, manageBots] : [generateLoginCode]
})
await execute({
objects: [adminTools]
})Full control over the autonomous execution loop:
await execute({
instructions: '...',
hooks: {
// Before tool execution - can modify input
onBeforeTool: async ({ iteration, tool, input, controller }) => {
console.log(`About to call ${tool.name}`)
return { input: modifiedInput } // Optional: transform input
},
// After tool execution - can modify output
onAfterTool: async ({ iteration, tool, input, output, controller }) => {
console.log(`${tool.name} returned:`, output)
return { output: modifiedOutput } // Optional: transform output
},
// Before code execution in iteration
onBeforeExecution: async (iteration, controller) => {
return { code: modifiedCode } // Optional: transform generated code
},
// When exit is triggered
onExit: async (result) => {
console.log('Exited with:', result)
},
// After each iteration completes
onIterationEnd: async (iteration, controller) => {
if (iteration > 5) {
controller.abort() // Stop execution
}
},
// On trace events (synchronous, non-blocking)
onTrace: ({ trace, iteration }) => {
if (trace.type === 'comment') {
console.log('LLM thinking:', trace.comment)
}
if (trace.type === 'tool_call') {
console.log('Calling:', trace.tool_name)
}
}
}
})Hook use cases:
- Logging and debugging
- Input/output validation and transformation
- Rate limiting tool calls
- Custom abort conditions
- Injecting dynamic context
Track metadata for any entity (bot, user, conversation, workflow):
import { TrackedTags } from '@botpress/runtime'
// Create tags instance
const tags = TrackedTags.create({
type: 'bot', // or 'user' | 'conversation' | 'workflow'
id: entityId,
client: botClient,
initialTags: { status: 'active' }
})
// Load from server
await tags.load()
// Modify tags
tags.tags = {
...tags.tags,
lastSync: new Date().toISOString()
}
// Check if modified
if (tags.isDirty()) {
await tags.save()
}
// Batch operations
await TrackedTags.saveAllDirty()
await TrackedTags.loadAll()Access via workflow instance:
workflow.tags = { status: 'processing' }
await workflow.save()Serialize workflow references in state that auto-hydrate on access:
import { Reference, z } from '@botpress/runtime'
// In conversation state schema
state: z.object({
research: Reference.Workflow('deep_research').optional()
// or untyped: Reference.Workflow().optional()
})
// In handler - always a WorkflowInstance
handler: async ({ state }) => {
if (state.research) {
// state.research is typed WorkflowInstance
console.log(state.research.status) // 'running' | 'completed' | etc
console.log(state.research.output) // Typed output
if (state.research.status === 'completed') {
// Access completed workflow data
}
}
}Global context for accessing runtime information:
import { context } from '@botpress/runtime'
// Get specific context
const client = context.get('client')
const citations = context.get('citations')
const logger = context.get('logger')
// Get all context
const { client, cognitive, logger, operation } = context.getAll()Available context properties:
client- Botpress API clientcognitive- LLM accesslogger- Loggingoperation- Current operation infocitations- Citation trackingchat- Chat interfacebot- Bot tags and metadatauser- User informationconversation- Current conversationmessage- Incoming messageevent- Current eventworkflow- Current workflowworkflowControlContext- Workflow control (abort, fail, restart)
Access and modify tracked state:
import { bot, user } from '@botpress/runtime'
// Bot state
bot.state.lastIndexed = new Date().toISOString()
bot.state.config = { theme: 'dark' }
// User state
user.state.preferences = { notifications: true }
user.state.lastActive = Date.now()State persists automatically across executions.
IMPORTANT: Tables have strict naming requirements:
// ✅ CORRECT - Name must end with "Table"
export const MyDataTable = new Table({
name: "mydataTable", // Must end with "Table"
columns: { ... }
});
// ❌ WRONG - Missing "Table" suffix
name: "mydata"
name: "my_data"Reserved column names - Cannot use these as column names:
id(auto-generated)createdAt(auto-generated)updatedAt(auto-generated)computedstale
// ❌ WRONG - Using reserved column name
columns: {
createdAt: z.string() // Reserved!
}
// ✅ CORRECT - Use alternative name
columns: {
savedAt: z.string()
}Files in src/tables/ are auto-registered by the ADK. Do NOT re-export from index.ts:
// src/tables/index.ts
// ❌ WRONG - Causes duplicate registration errors
export { MyTable } from "./myTable";
// ✅ CORRECT - Leave empty or add comment
// Tables are auto-registered from src/tables/*.ts filesSame applies to src/conversations/, src/workflows/, src/triggers/, etc.
Beyond basic CRUD, Tables support powerful query and manipulation features:
Use logical operators and conditions:
await MyTable.findRows({
filter: {
$and: [
{ status: 'open' },
{ priority: { $in: ['high', 'urgent'] } }
],
$or: [
{ assignee: userId },
{ reporter: userId }
],
title: { $regex: 'bug|error', $options: 'i' }
}
})Filter operators:
$eq,$ne- Equal, not equal$gt,$gte,$lt,$lte- Comparisons$in,$nin- In array, not in array$exists- Field exists$regex- Regular expression match$options- Regex options (e.g., 'i' for case-insensitive)$and,$or- Logical operators
Search across searchable columns:
await MyTable.findRows({
search: 'query string',
filter: { status: 'active' }
})Mark columns as searchable in schema:
columns: {
title: z.string().searchable(),
description: z.string().searchable()
}Group and aggregate data:
await MyTable.findRows({
group: {
status: 'count',
priority: ['sum', 'avg'],
complexity: ['max', 'min']
}
})Aggregation operations: key, count, sum, avg, max, min, unique
Columns with values computed from row data:
columns: {
fullName: {
computed: true,
schema: z.string(),
dependencies: ['firstName', 'lastName'],
value: async (row) => `${row.firstName} ${row.lastName}`
},
age: {
computed: true,
schema: z.number(),
dependencies: ['birthDate'],
value: async (row) => {
const today = new Date()
const birth = new Date(row.birthDate)
return today.getFullYear() - birth.getFullYear()
}
}
}Insert or update based on key column:
await MyTable.upsertRows({
rows: [
{ externalId: '123', name: 'Item 1' },
{ externalId: '456', name: 'Item 2' }
],
keyColumn: 'externalId', // Update if exists, insert if not
waitComputed: true // Wait for computed columns to update
})Efficient batch operations:
// Delete by filter
await MyTable.deleteRows({
filter: { status: 'archived', createdAt: { $lt: '2024-01-01' } }
})
// Delete by IDs
await MyTable.deleteRowIds([1, 2, 3])
// Delete all
await MyTable.deleteAllRows()
// Update multiple
await MyTable.updateRows({
rows: [
{ id: 1, status: 'active' },
{ id: 2, status: 'inactive' }
],
waitComputed: true
})Collect errors and warnings from bulk operations:
const { errors, warnings } = await MyTable.createRows({
rows: data,
waitComputed: true
})
if (errors?.length) {
console.error('Failed rows:', errors)
}
if (warnings?.length) {
console.warn('Warnings:', warnings)
}Multiple source types for knowledge bases:
import { DataSource } from '@botpress/runtime'
const docs = DataSource.Directory.fromPath('src/knowledge', {
id: 'docs',
filter: (path) => path.endsWith('.md') || path.endsWith('.txt')
})const siteDocs = DataSource.Website.fromSitemap('https://example.com/sitemap.xml', {
id: 'website',
maxPages: 500,
fetch: 'node:fetch' // or custom fetch implementation
})import { Knowledge } from '@botpress/runtime'
export default new Knowledge({
name: 'docs',
description: 'Product documentation',
sources: [docsDirectory, websiteSource]
})Manually refresh knowledge base content:
// Refresh entire knowledge base
await DocsKB.refresh({ force: true })
// Refresh specific source
await DocsKB.refreshSource('website', { force: true })Options:
force: true- Force refresh even if recently updated- Automatic refresh via scheduled workflows recommended
await execute({
instructions: 'Answer using the documentation',
knowledge: [DocsKB, APIKB],
tools: [searchTool]
})Knowledge bases are automatically searchable via the search_knowledge tool.
Handle messages from multiple channels in one handler:
export default new Conversation({
channel: ['chat.channel', 'webchat.channel', 'slack.dm'],
handler: async ({ channel, execute }) => {
console.log(`Message from: ${channel}`)
await execute({ instructions: '...' })
}
})Subscribe to integration events:
export default new Conversation({
channel: 'webchat.channel',
events: ['webchat:conversationStarted', 'webchat:conversationEnded'],
handler: async ({ type, event, message }) => {
if (type === 'event' && event.type === 'webchat:conversationStarted') {
// Send welcome message
await conversation.send({
type: 'text',
payload: { text: 'Welcome!' }
})
}
if (type === 'message' && message?.type === 'text') {
// Handle regular messages
await execute({ instructions: '...' })
}
}
})Handle data requests from workflows:
import { isWorkflowDataRequest } from '@botpress/runtime'
handler: async ({ type, event, execute }) => {
// Check if this is a workflow requesting data
if (type === 'workflow_request' && isWorkflowDataRequest(event)) {
const userInput = await promptUser(event.payload.message)
// Provide data back to workflow
await workflow.provide(event, { topic: userInput })
return
}
// Regular message handling
await execute({ instructions: '...' })
}Work with typed workflow instances:
import { isWorkflow, ResearchWorkflow } from '@botpress/runtime'
handler: async ({ state }) => {
if (state.research && isWorkflow(state.research, 'research')) {
// state.research is now typed as ResearchWorkflow
console.log(state.research.status)
console.log(state.research.output) // Typed output
if (state.research.status === 'completed') {
await conversation.send({
type: 'text',
payload: { text: state.research.output.result }
})
}
}
}Provide different tools based on conversation state:
handler: async ({ state, execute }) => {
const tools = () => {
if (state.workflowRunning) {
return [cancelWorkflowTool, checkStatusTool]
} else {
return [startWorkflowTool, browseTool, searchTool]
}
}
await execute({
instructions: '...',
tools: tools()
})
}Send different message types:
// Text message
await conversation.send({
type: 'text',
payload: { text: 'Hello!' }
})
// Custom message type (integration-specific)
await conversation.send({
type: 'custom:messageType',
payload: { data: 'custom payload' }
})Track and manage source citations for LLM responses:
Access via context:
import { context } from '@botpress/runtime'
const citations = context.get('citations')Register sources that can be cited:
// Register with URL
const { tag } = citations.registerSource({
url: 'https://example.com/doc',
title: 'Documentation Page'
})
// Register with file reference
const { tag } = citations.registerSource({
file: fileKey,
title: 'Internal Document'
})Inject citation tags into LLM content:
const results = await searchKnowledgeBase(query)
for (const result of results) {
const { tag } = citations.registerSource({
file: result.file.key,
title: result.file.name
})
content += `${result.content} ${tag}\n`
}
// Return cited content
throw new ThinkSignal('results', content)Citations are automatically formatted with tags like [1], [2], etc., and tracked by the system for reference.
export default new Autonomous.Tool({
name: 'search_docs',
description: 'Search documentation',
handler: async ({ query }) => {
const citations = context.get('citations')
const results = await searchDocs(query)
let response = ''
for (const doc of results) {
const { tag } = citations.registerSource({
url: doc.url,
title: doc.title
})
response += `${doc.content} ${tag}\n\n`
}
return response
}
})// ❌ WRONG
import { zai } from '@botpress/runtime'
const result = await zai.extract(...)
// ✅ CORRECT
import { adk } from '@botpress/runtime'
const result = await adk.zai.extract(...)// ❌ WRONG - zai.extract returns data directly
const result = await adk.zai.extract(input, schema)
console.log(result.output) // undefined!
// ✅ CORRECT
const result = await adk.zai.extract(input, schema)
console.log(result) // { name: "John", age: 30 }// ❌ WRONG
if (result.exit?.name === 'my_exit') {
const data = result.exit.value
}
// ✅ CORRECT
if (result.is(MyExit)) {
const data = result.output // Type-safe!
}// ❌ WRONG - These are reserved
columns: {
id: z.string(),
createdAt: z.string(),
updatedAt: z.string()
}
// ✅ CORRECT - Use alternatives
columns: {
visibleId: z.string(),
savedAt: z.string(),
modifiedAt: z.string()
}// ❌ WRONG - src/tables/index.ts
export { MyTable } from "./myTable" // Causes duplicates!
// ✅ CORRECT - Leave index.ts empty
// Files in src/tables/, src/conversations/, etc. are auto-registered// ❌ WRONG
name: "users"
name: "user_data"
// ✅ CORRECT
name: "usersTable"
name: "userdataTable"- Always search Botpress docs using
mcp__botpress-docs__SearchBotpress - Check examples for patterns
- Regenerate types after changing
agent.config.ts(runadk dev) - Test in dev mode with hot reloading (
adk dev) - Follow TypeScript types - They're auto-generated from integrations
The ADK provides setupTestRuntime() to initialize the full ADK runtime within your test process. This sets up all environment variables, generates types, and imports the runtime so your tests can use actions, tools, workflows, etc.
# bunfig.toml
[test]
preload = ["./test-setup.ts"]// test-setup.ts
import { beforeAll } from "bun:test";
import { setupTestRuntime } from "@botpress/adk";
beforeAll(async () => {
const runtime = await setupTestRuntime();
await runtime.initialize();
});// vitest.setup.ts
import { beforeAll } from "vitest";
import { setupTestRuntime } from "@botpress/adk";
beforeAll(async () => {
const runtime = await setupTestRuntime();
await runtime.initialize();
});// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./vitest.setup.ts"],
},
});setupTestRuntime() auto-detects project path and credentials, but you can override:
const runtime = await setupTestRuntime({
projectPath: "/path/to/agent", // defaults to auto-detect from CWD
credentials: { token: "...", apiUrl: "..." }, // defaults to ~/.adk/credentials
prod: true, // use production bot instead of dev bot
forceRegenerate: true, // force regenerate bot project
env: { CUSTOM_VAR: "value" }, // additional env vars
});- Must have
@botpress/adkinstalled as a dev dependency (bun add -d @botpress/adk) - Must have run
adk devat least once (to create the dev bot) - Must be logged in (
adk login) or provide credentials explicitly