UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

742 lines (590 loc) 23 kB
# Autonomous Execution The `execute()` function is the core of AI-powered behavior in ADK bots. It runs a code-generating LLM in a sandboxed loop: the model writes TypeScript, the runtime executes it, and the loop continues until an exit is reached or iterations are exhausted. This reference covers **Objects, Exits, execution hooks, and configuration options**. For basic tool definition and handler patterns, see [tools.md](./tools.md). ## execute() Full API `execute()` is provided as a parameter in conversation handlers and is also available in workflows. It accepts these properties: ```typescript type Props = { /** System prompt guiding the LLM. Can be static or dynamic per iteration. */ instructions: string | ((ctx: Context) => string | Promise<string>) /** AI-callable tools. See tools.md for creation patterns. */ tools?: Tool[] | ((ctx: Context) => Tool[] | Promise<Tool[]>) /** Namespaced objects with properties and scoped tools. */ objects?: Object[] | ((ctx: Context) => Object[] | Promise<Object[]>) /** Structured termination points. Controls how execution ends. */ exits?: Exit[] | ((ctx: Context) => Exit[] | Promise<Exit[]>) /** Execution lifecycle hooks. */ hooks?: Hooks /** LLM temperature (0-2). Default: 0.7 */ temperature?: number | ((ctx: Context) => number | Promise<number>) /** Model or fallback chain. Uses agent config default if omitted. */ model?: Model | Model[] | ((ctx: Context) => Model | Model[] | Promise<Model | Model[]>) /** Reasoning effort for models that support it. */ reasoningEffort?: 'low' | 'medium' | 'high' | 'dynamic' | 'none' /** Knowledge bases for RAG. Adds a search_knowledge tool automatically. */ knowledge?: BaseKnowledge[] /** Maximum iteration loops. Default: 10. Clamped to 1-100. */ iterations?: number /** AbortSignal to cancel execution externally. */ signal?: AbortSignal } ``` Every property except `instructions` is optional. Most properties accept a **ValueOrGetter** either a static value or a function receiving the current execution context that returns the value (optionally async). This lets you change tools, objects, or instructions between iterations based on what happened previously. ## Exits Exits define the structured ways an execution can terminate. The LLM ends execution by writing `return { action: 'exit_name', ...data }` in its generated code. ### Creating Exits ```typescript import { Autonomous, z } from '@botpress/runtime' // Simple exit (no data) const Done = new Autonomous.Exit({ name: 'done', description: 'Task is complete', }) // Exit with typed data const TriageComplete = new Autonomous.Exit({ name: 'triage_complete', description: 'Classification is done and the user has been notified.', schema: z.object({ category: z.string().describe('The category the request was classified as'), }), }) // Exit with aliases const Escalate = new Autonomous.Exit({ name: 'escalate', aliases: ['transfer', 'handoff'], description: 'Escalate to a human agent', schema: z.object({ reason: z.enum(['frustrated', 'technical', 'sensitive', 'other']), priority: z.enum(['low', 'medium', 'high']).default('medium'), }), }) // Exit with metadata for orchestration const Handoff = new Autonomous.Exit({ name: 'handoff_sales', description: 'Transfer to sales team', metadata: { department: 'sales', type: 'handoff' }, schema: z.object({ reason: z.string() }), }) ``` ### Exit Constructor ```typescript new Autonomous.Exit<T>({ name: string // Required. Valid TypeScript identifier. description: string // Required. Tells the LLM when to use this exit. schema?: ZuiType<T> // Optional. Zod schema for validated return data. aliases?: string[] // Optional. Alternative names the LLM can use. metadata?: Record<string, unknown> // Optional. Custom data for orchestration. }) ``` ### Using Exits in execute() ```typescript export const SlackDM = new Conversation({ channel: 'slack.dm', async handler({ execute }) { const result = await execute({ instructions: `Classify the user's request, tell them the result, then exit.`, tools: [classifyRequest.asTool()], exits: [TriageComplete], }) // Type-safe result checking if (result.is(TriageComplete)) { console.log(`Triaged as: ${result.output.category}`) } }, }) ``` ### Multiple Exits for Branching ```typescript const Approved = new Autonomous.Exit({ name: 'approved', description: 'Loan application approved', schema: z.object({ amount: z.number(), reference: z.string() }), }) const Rejected = new Autonomous.Exit({ name: 'rejected', description: 'Loan application rejected', schema: z.object({ reason: z.string() }), }) const result = await execute({ instructions: 'Review and decide on the loan application.', tools: [creditCheckTool, reviewTool], exits: [Approved, Rejected], }) if (result.is(Approved)) { console.log(`Approved: $${result.output.amount} (ref: ${result.output.reference})`) } else if (result.is(Rejected)) { console.log(`Rejected: ${result.output.reason}`) } ``` ### Built-in Exits Three exits are built into the runtime. Do not redefine them. | Exit | When Available | Action Name | Purpose | | --------------- | ------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------- | | **ListenExit** | Chat mode (conversation handlers) | `listen` | Pauses execution, returns control to the user. Auto-added when `execute()` runs in a conversation. | | **ThinkExit** | Always | `think` | LLM pauses to reflect. Triggers another iteration (does not end execution). | | **DefaultExit** | Worker mode, when no custom exits provided | `done` | Returns `{ success: true, result? }` or `{ success: false, error }`. | ```typescript import { Autonomous } from '@botpress/runtime' // Check for built-in exits if (result.is(Autonomous.ListenExit)) { // Agent is waiting for user input } if (result.is(Autonomous.DefaultExit)) { if (result.output.success) { console.log('Result:', result.output.result) } else { console.error('Error:', result.output.error) } } ``` **Chat mode behavior**: In conversation handlers, `ListenExit` is added automatically. When no custom exits are provided the LLM will use `ListenExit` to hand control back after responding. When you provide custom exits, both your exits and `ListenExit` are available. **Worker mode behavior**: When `execute()` runs without a chat context (e.g. in a workflow or standalone), `ListenExit` is not available. If no custom exits are provided, `DefaultExit` is added automatically. ### Exit Utilities ```typescript // Clone and rename an exit const customExit = baseExit.clone().rename('custom') // Deduplicate exit names const uniqueExits = Autonomous.Exit.withUniqueNames([exit1, exit2]) // Match an exit result if (someExit.match(exitResult)) { // exitResult is typed as ExitResult<T> } ``` ## Objects Objects group related properties and tools into a namespace. The LLM sees them as TypeScript namespaces with typed constants and methods. ### Creating Objects ```typescript import { Autonomous, z } from '@botpress/runtime' const userProfile = new Autonomous.Object({ name: 'user', description: 'Current user profile data', properties: [ { name: 'name', value: 'John Doe', type: z.string().min(1), description: 'User full name', writable: true, }, { name: 'email', value: null, type: z.string().email().nullable(), description: 'User email address', writable: true, }, { name: 'id', value: 'user_123', writable: false, // Read-only LLM cannot modify }, ], }) ``` The LLM sees this as: ```typescript export namespace user { const name: Writable<string> = 'John Doe' const email: Writable<string | null> = null const id: Readonly<string> = 'user_123' } ``` ### Object Constructor ```typescript new Autonomous.Object({ name: string // Required. Valid TypeScript identifier. description?: string // What this object represents. properties?: ObjectProperty[] // Stateful variables (max 100). tools?: Tool[] // Scoped tools (called as obj.toolName()). metadata?: Record<string, unknown> }) ``` ### ObjectProperty ```typescript type ObjectProperty = { name: string // Valid TypeScript identifier. value: any // Current value. type?: ZuiType // Zod schema for validation on write. description?: string // Helps the LLM understand the property. writable?: boolean // Default: false. If true, LLM can assign new values. } ``` ### Objects with Scoped Tools Tools on an object are called as `objectName.toolName()` by the LLM: ```typescript const fileSystem = new Autonomous.Object({ name: 'fs', description: 'File system operations', tools: [ new Autonomous.Tool({ name: 'readFile', input: z.object({ path: z.string() }), output: z.string(), handler: async ({ path }) => readFileSync(path, 'utf8'), }), new Autonomous.Tool({ name: 'writeFile', input: z.object({ path: z.string(), content: z.string() }), handler: async ({ path, content }) => writeFileSync(path, content), }), ], }) // LLM can call: fs.readFile({ path: '/tmp/data.txt' }) // LLM can call: fs.writeFile({ path: '/tmp/out.txt', content: '...' }) ``` ### Dynamic Objects Pass a function instead of an array to rebuild objects each iteration, reflecting current state: ```typescript const memory: Record<string, any> = {} await execute({ instructions: 'Collect user information.', objects: () => [ new Autonomous.Object({ name: 'form', properties: [ { name: 'name', value: memory.name ?? null, type: z.string().nullable(), writable: true, }, { name: 'age', value: memory.age ?? null, type: z.number().min(0).max(150).nullable(), writable: true, }, ], }), ], hooks: { onTrace: ({ trace }) => { if (trace.type === 'property') { memory[trace.property] = trace.value } }, }, }) ``` ### Using Objects in execute() ```typescript await execute({ instructions: 'You have access to the user profile. Update fields as needed.', objects: [userProfile], tools: [saveProfileTool], }) ``` ## Execution Hooks Hooks let you observe and modify the execution loop. They are passed in the `hooks` property of `execute()`. ### Hook Reference ```typescript type Hooks = { /** NON-BLOCKING. Called for each trace (log, tool call, LLM call, etc). */ onTrace?: (props: { trace: Trace; iteration: number }) => void /** BLOCKING. Called before each iteration. Can modify iteration parameters. */ onIterationStart?: ( iteration: Iteration, controller: AbortController, context: Context ) => Promise<void | Partial<Iteration>> | void | Partial<Iteration> /** BLOCKING. Called after each iteration. Good for logging and cleanup. */ onIterationEnd?: (iteration: Iteration, controller: IterationController) => void | Promise<void> /** BLOCKING. Called when an exit is reached. Throw to reject the exit. */ onExit?: <T = unknown>(result: ExitResult<T>) => Promise<void> | void /** BLOCKING, MUTATION. Called after LLM generates code, before execution. */ onBeforeExecution?: (iteration: Iteration, controller: AbortController) => Promise<{ code?: string } | void> /** BLOCKING, MUTATION. Called before any tool executes. Can modify input. */ onBeforeTool?: (event: { iteration: Iteration tool: Tool input: unknown controller: AbortController }) => Promise<{ input?: unknown } | void> /** BLOCKING, MUTATION. Called after a tool executes. Can modify output. */ onAfterTool?: (event: { iteration: Iteration tool: Tool input: unknown output: unknown controller: AbortController }) => Promise<{ output?: unknown } | void> } ``` ### Hook Categories | Category | Hooks | Behavior | | -------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------- | | **Non-blocking** | `onTrace` | Fire-and-forget. Cannot delay execution. | | **Blocking, mutation** | `onIterationStart`, `onBeforeTool`, `onAfterTool`, `onBeforeExecution` | Block until resolved. Can return modified values. | | **Blocking, non-mutation** | `onIterationEnd`, `onExit` | Block until resolved. Cannot change tool I/O or code. | ### Common Hook Patterns **Logging tool calls:** ```typescript await execute({ instructions: 'Help the user', tools: [searchTool, ticketTool], hooks: { onBeforeTool: async ({ tool, input }) => { console.log(`[tool:start] ${tool.name}`, JSON.stringify(input)) }, onAfterTool: async ({ tool, input, output }) => { console.log(`[tool:end] ${tool.name}`, JSON.stringify(output)) }, }, }) ``` **Modifying tool input (e.g. appending search scope):** ```typescript hooks: { onBeforeTool: async ({ tool, input }) => { if (tool.name === 'search') { return { input: { ...input, query: `${input.query} site:docs.example.com` } } } }, } ``` **Rejecting an exit (forces the LLM to keep iterating):** ```typescript hooks: { onExit: async (result) => { if (result.exit.name === 'done' && !result.result?.confirmed) { throw new Error('Must confirm before exiting') } }, } ``` **Aborting execution from a hook:** ```typescript hooks: { onBeforeTool: async ({ controller }) => { if (shouldStop()) { controller.abort() } }, } ``` **Modifying generated code before execution:** ```typescript hooks: { onBeforeExecution: async (iteration) => { // Inject a safety wrapper or strip dangerous patterns const modified = iteration.code?.replace(/dangerousCall\(\)/g, '/* blocked */') return { code: modified } }, } ``` **Tracking property mutations via onTrace:** ```typescript hooks: { onTrace: ({ trace }) => { if (trace.type === 'property') { console.log(`${trace.object}.${trace.property} = ${trace.value}`) } }, } ``` ## Execution Result `execute()` returns an `ExecutionResult` with three possible statuses: ```typescript const result = await execute({ instructions: '...' }) // Status checks result.isSuccess() // Completed with an exit result.isError() // Failed (aborted, iterations exhausted) result.isInterrupted() // Paused with a snapshot (SnapshotSignal) // Exit type-narrowing result.is(MyExit) // true if exited via MyExit, narrows result.output type // Access execution data result.iteration // Last iteration (or null) result.iterations // All iterations result.output // Exit data (if success), null otherwise result.context // Full execution context ``` ### Success Result ```typescript if (result.isSuccess()) { console.log('Exit:', result.result.exit.name) console.log('Output:', result.output) console.log('Code:', result.iteration.code) } // Typed exit checking (preferred) if (result.is(TriageComplete)) { // result.output is typed as { category: string } console.log(result.output.category) } ``` ### Error Result ```typescript if (result.isError()) { console.error('Failed:', result.error) // Inspect the last iteration for details const last = result.iteration if (last?.status.type === 'execution_error') { console.error(last.status.execution_error.message) } } ``` ### Interrupted Result (Snapshots) When a tool throws `SnapshotSignal`, execution pauses and can be resumed later: ```typescript if (result.isInterrupted()) { const snapshot = result.snapshot.toJSON() // Persist snapshot, resume later with: execute({ snapshot, ... }) } ``` ## Configuration Options ### Model Selection ```typescript await execute({ instructions: '...', model: 'openai:gpt-4o', // Single model }) await execute({ instructions: '...', model: ['openai:gpt-4o', 'anthropic:claude-3-5-sonnet'], // Fallback chain }) // Dynamic model per iteration await execute({ instructions: '...', model: (ctx) => (ctx.iteration > 2 ? 'openai:gpt-4o' : 'openai:gpt-4o-mini'), }) ``` If `model` is omitted, the `defaultModels.autonomous` value from `agent.config.ts` is used. ### Temperature ```typescript await execute({ instructions: '...', temperature: 0.1, // Low = deterministic. Default: 0.7. Range: 0-2. }) ``` ### Reasoning Effort ```typescript await execute({ instructions: '...', reasoningEffort: 'high', // 'none' | 'low' | 'medium' | 'high' | 'dynamic' }) ``` - `'none'` disables reasoning for models with optional reasoning. - `'dynamic'` lets the provider decide. - Omitting this field disables reasoning for optional-reasoning models. ### Iteration Limit ```typescript await execute({ instructions: '...', iterations: 20, // Default: 10. Clamped to 1-100. }) ``` Each "iteration" is one LLM generation + code execution cycle. The LLM can call multiple tools in a single iteration. `ThinkExit` consumes one iteration. If iterations are exhausted without an exit, the result is an error. ### AbortSignal ```typescript const controller = new AbortController() setTimeout(() => controller.abort(), 30_000) await execute({ instructions: '...', signal: controller.signal, }) ``` When aborted, the current LLM generation and sandbox execution are killed immediately. The result status will be `error` with an aborted iteration. ## Dynamic Properties (ValueOrGetter) Most `execute()` properties accept either a static value or a function that receives the execution context: ```typescript // Static tools: [searchTool, ticketTool] // Dynamic re-evaluated each iteration tools: (ctx) => { if (ctx.iteration > 0) { return [searchTool, ticketTool, escalateTool] } return [searchTool] } // Async dynamic instructions: async (ctx) => { const rules = await fetchRules() return `Follow these rules: ${rules}` } ``` This pattern applies to `instructions`, `tools`, `objects`, `exits`, `temperature`, and `model`. ## Chat Mode vs Worker Mode `execute()` behaves differently depending on where it runs: | Aspect | Chat Mode (Conversations) | Worker Mode (Workflows, Actions) | | --------------- | ---------------------------------- | ------------------------------------ | | Transcript | LLM sees conversation history | No transcript | | ListenExit | Auto-added | Not available | | DefaultExit | Not auto-added when exits provided | Auto-added when no exits provided | | Components | Can yield UI components | No component rendering | | Typical pattern | Open-ended conversation | Task completion with structured exit | In conversation handlers, `execute()` is provided as a parameter and runs in chat mode by default: ```typescript export const Chat = new Conversation({ channel: 'chat.channel', async handler({ execute }) { await execute({ instructions: 'You are a helpful assistant.', tools: [searchTool], }) // After execute(), the LLM used ListenExit to return control }, }) ``` For lifecycle event handling (nudge/expire) in conversations, see [conversation-lifecycle.md](./conversation-lifecycle.md). ## Common Patterns ### Classify-and-exit (single-shot) ```typescript const Category = new Autonomous.Exit({ name: 'categorized', description: 'Message has been categorized', schema: z.object({ category: z.enum(['billing', 'technical', 'general']), confidence: z.number().min(0).max(1), }), }) const result = await execute({ instructions: 'Classify the user message into a category.', exits: [Category], iterations: 3, }) if (result.is(Category)) { await routeToTeam(result.output.category) } ``` ### Chaining execute() calls ```typescript async handler({ execute }) { // First pass: classify const result = await execute({ instructions: 'Classify the request and respond.', tools: [classifyTool], exits: [TriageComplete], }) // Second pass: follow-up if (result.is(TriageComplete) && result.output.category === 'technical') { await execute({ instructions: 'Ask for technical details and create a ticket.', tools: [createTicketTool], }) } } ``` ### Open-ended conversation (no custom exits) ```typescript await execute({ instructions: 'You are an IT help desk assistant.', tools: [createTicket, lookupTicket, updateTicket, deleteTicket], knowledge: [DocsKB], }) // LLM uses ListenExit to return control after each response ``` ## Pitfalls - **Do not redefine built-in exits.** Creating an exit named `listen`, `think`, or `done` will conflict with the built-in ListenExit, ThinkExit, and DefaultExit. - **Exits require good descriptions.** The LLM decides which exit to use based on the `description`. Vague descriptions lead to wrong exit selection. - **Writable properties need a schema.** If `writable: true` without `type`, the sandbox cannot validate writes and they will be unchecked. - **Object property limit is 100.** More than 100 properties on a single object will throw. - **ThinkExit consumes an iteration.** If your iteration limit is low and the LLM thinks frequently, it may exhaust iterations before finishing. Increase `iterations` for complex tasks. - **Blocking hooks delay execution.** All hooks except `onTrace` are async and block the loop. Keep hook logic fast. - **onExit throw = retry.** Throwing in `onExit` rejects the exit and the LLM gets the error as context for the next iteration. Use this deliberately. ## See Also - [Tools](./tools.md) Tool creation, handler syntax, ThinkSignal, advanced properties - [Conversations](./conversations.md) Conversation handlers, message routing, channel IDs - [Workflows](./workflows.md) Long-running processes, step-based execution - [Context API](./context-api.md) Accessing runtime services and state - [Model Configuration](./model-configuration.md) Model selection and defaults - [Workflow Steps](./workflow-steps.md) Step API reference for using execute() within workflow handlers - [Conversation Lifecycle](./conversation-lifecycle.md) Lifecycle events (nudge/expire) that also receive execute as a handler prop