Human-in-the-Loop (HITL)
Human-in-the-Loop agents that can pause for approval, create checkpoints, and allow real-time modifications. Essential forhigh-stakes workflows.
Tool Approval
Security GateEnforce strict manual validation for sensitive operations like database writes or API calls.
State Checkpoints
ResilienceAtomic persistence of the agent's memory. Revert or resume from any specific step in the history.
Action Modification
SupervisionEmpower users to edit tool parameters mid-flight before the model commits to an execution.
# Basic Usage
The HITLAgent extends the base agent with human-in-the-loop capabilities. Configure which tools require approval and provide an interrupt handler to process human decisions.
import { HITLAgent, MemoryCheckpointStore } from '@orka-js/agent';import { OpenAIAdapter } from '@orka-js/openai';import type { Tool, InterruptRequest, InterruptResponse } from '@orka-js/agent'; const llm = new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY! }); // Define tools - some will require approvalconst searchTool: Tool = { name: 'web_search', description: 'Search the web for information', parameters: [{ name: 'query', type: 'string', description: 'Search query', required: true }], execute: async (input) => ({ output: `Results for: ${input.query}` })}; const sendEmailTool: Tool = { name: 'send_email', description: 'Send an email', parameters: [ { name: 'to', type: 'string', description: 'Recipient', required: true }, { name: 'subject', type: 'string', description: 'Subject', required: true }, { name: 'body', type: 'string', description: 'Email body', required: true } ], execute: async (input) => ({ output: `Email sent to ${input.to}` })}; const deleteFileTool: Tool = { name: 'delete_file', description: 'Delete a file', parameters: [{ name: 'path', type: 'string', description: 'File path', required: true }], execute: async (input) => ({ output: `Deleted: ${input.path}` })}; // Create HITL agentconst agent = new HITLAgent( { goal: 'Help users with tasks while requiring approval for sensitive operations', tools: [searchTool, sendEmailTool, deleteFileTool], verbose: true, hitl: { // Tools that require human approval requireApprovalFor: ['send_email', 'delete_file'], // Tools that are always auto-approved autoApproveTools: ['web_search'], // Create checkpoint every N steps checkpointEvery: 3, // Timeout for human response defaultTimeoutMs: 60000, // Handler for interrupt requests onInterrupt: async (request) => { // Your approval logic here (UI, CLI, API, etc.) console.log(`Approval needed: ${request.message}`); return { id: request.id, status: 'approved', respondedAt: new Date() }; } } }, llm); const result = await agent.run('Search for AI news and email a summary to team@example.com'); console.log(result.output);console.log(`Was interrupted: ${result.wasInterrupted}`);console.log(`Interrupts: ${result.interrupts.length}`);console.log(`Checkpoints: ${result.checkpoints.length}`);# Interrupt Handler
The interrupt handler is called whenever the agent needs human input. You can implement any approval workflow: CLI prompts, web UI, Slack integration, etc.
import type { InterruptRequest, InterruptResponse } from '@orka-js/agent'; // Interactive CLI handlerasync function cliInterruptHandler(request: InterruptRequest): Promise<InterruptResponse> { console.log('\n' + '='.repeat(50)); console.log('🔔 HUMAN APPROVAL REQUIRED'); console.log('='.repeat(50)); console.log(`Reason: ${request.reason}`); console.log(`Message: ${request.message}`); if (request.data.toolName) { console.log(`Tool: ${request.data.toolName}`); console.log(`Input: ${JSON.stringify(request.data.toolInput, null, 2)}`); } if (request.data.thought) { console.log(`Agent reasoning: ${request.data.thought}`); } // Get user input (use readline, inquirer, etc.) const answer = await promptUser('Approve? (y/n/m for modify): '); if (answer === 'y') { return { id: request.id, status: 'approved', respondedAt: new Date() }; } else if (answer === 'm') { const newInput = await promptUser('Enter modified input (JSON): '); return { id: request.id, status: 'modified', modifiedData: { toolName: request.data.toolName, toolInput: JSON.parse(newInput) }, respondedAt: new Date() }; } else { const feedback = await promptUser('Reason for rejection: '); return { id: request.id, status: 'rejected', feedback, respondedAt: new Date() }; }} // Web API handler (for async approval via webhooks)async function webhookInterruptHandler(request: InterruptRequest): Promise<InterruptResponse> { // Store pending request in database await db.pendingApprovals.create({ id: request.id, agentId: request.agentId, data: request.data, createdAt: request.createdAt }); // Send notification (Slack, email, etc.) await slack.send({ channel: '#agent-approvals', text: `🔔 Approval needed: ${request.message}`, actions: [ { type: 'button', text: 'Approve', value: 'approve' }, { type: 'button', text: 'Reject', value: 'reject' } ] }); // Wait for response (with timeout) const response = await waitForApproval(request.id, request.timeoutMs); return response;}📋 Interrupt Request Structure
interface InterruptRequest { id: string; // Unique request ID agentId: string; // Agent that triggered the interrupt reason: InterruptReason; // 'tool_approval' | 'checkpoint' | 'review' | 'confirmation' | 'custom' message: string; // Human-readable message data: { toolName?: string; // Tool being called toolInput?: Record<string, unknown>; // Tool parameters stepNumber?: number; // Current step thought?: string; // Agent's reasoning context?: Record<string, unknown>; // Additional context }; createdAt: Date; timeoutMs?: number; // How long to wait for response}Response Types
Immediate execution. The agent proceeds with original intent.
{ status: 'approved' }Execution halted. The agent receives feedback to replan.
{ status: 'rejected', feedback: '...' }Intercept and pivot. Execute tool with human-curated inputs.
{ status: 'modified', modifiedData: { ... } }Automatic suspension. Prevents zombie tasks in absence of human review.
{ status: 'timeout' }# Checkpoints & Recovery
Checkpoints save the agent's state at specific intervals, allowing you to resume execution from any saved point. This is crucial for long-running tasks and error recovery.
import { HITLAgent, MemoryCheckpointStore } from '@orka-js/agent'; // Use the built-in memory store (or implement your own)const checkpointStore = new MemoryCheckpointStore(); const agent = new HITLAgent( { goal: 'Process a large dataset', tools: [/* ... */], hitl: { checkpointEvery: 5, // Save state every 5 steps checkpointStore, onInterrupt: myHandler } }, llm); // Run the agentconst result = await agent.run('Analyze all customer data'); // List all checkpointsconst checkpoints = await agent.getCheckpoints();console.log('Saved checkpoints:', checkpoints.map(cp => cp.id)); // Resume from a specific checkpointconst resumedResult = await agent.run('Continue analysis', checkpoints[2].id);console.log(`Resumed from: ${resumedResult.resumedFromCheckpoint}`);Custom Checkpoint Store
Implement the CheckpointStore interface to persist checkpoints to Redis, PostgreSQL, or any storage backend:
import type { Checkpoint, CheckpointStore } from '@orka-js/agent'; class RedisCheckpointStore implements CheckpointStore { constructor(private redis: Redis) {} async save(checkpoint: Checkpoint): Promise<void> { await this.redis.hset( `checkpoints:${checkpoint.agentId}`, checkpoint.id, JSON.stringify(checkpoint) ); } async load(checkpointId: string): Promise<Checkpoint | null> { // Scan all agents for this checkpoint const keys = await this.redis.keys('checkpoints:*'); for (const key of keys) { const data = await this.redis.hget(key, checkpointId); if (data) return JSON.parse(data); } return null; } async loadLatest(agentId: string): Promise<Checkpoint | null> { const all = await this.redis.hgetall(`checkpoints:${agentId}`); const checkpoints = Object.values(all).map(v => JSON.parse(v) as Checkpoint); return checkpoints.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() )[0] ?? null; } async list(agentId: string): Promise<Checkpoint[]> { const all = await this.redis.hgetall(`checkpoints:${agentId}`); return Object.values(all).map(v => JSON.parse(v)); } async delete(checkpointId: string): Promise<void> { const keys = await this.redis.keys('checkpoints:*'); for (const key of keys) { await this.redis.hdel(key, checkpointId); } }}# Approval Patterns
Require Approval for All Tools
hitl: { requireApprovalFor: ['*'], // Wildcard: all tools need approval autoApproveTools: ['web_search'], // Except these}Approval for Specific Tools Only
hitl: { requireApprovalFor: ['send_email', 'delete_file', 'execute_code'], // All other tools run without approval}Manual Interrupts
You can also trigger interrupts programmatically for custom confirmation flows:
// Request confirmation before a critical actionconst response = await agent.requestConfirmation( 'About to process 10,000 records. Continue?', { recordCount: 10000, estimatedTime: '5 minutes' }); if (response.status === 'approved') { await processRecords();} // Request review of agent's reasoningconst reviewResponse = await agent.requestReview( 'Please review my analysis before I proceed', currentStep, 'I believe the data shows a 15% increase...');# Result Structure
interface HITLAgentResult extends AgentResult { // Standard agent result fields input: string; output: string; steps: AgentStepResult[]; totalLatencyMs: number; totalTokens: number; toolsUsed: string[]; metadata: Record<string, unknown>; // HITL-specific fields interrupts: InterruptResponse[]; // All interrupt responses checkpoints: string[]; // IDs of created checkpoints wasInterrupted: boolean; // True if any interrupts occurred resumedFromCheckpoint?: string; // Checkpoint ID if resumed} // Usageconst result = await agent.run('...'); if (result.wasInterrupted) { console.log(`Agent was interrupted ${result.interrupts.length} times`); for (const interrupt of result.interrupts) { console.log(`- ${interrupt.status}: ${interrupt.feedback ?? 'No feedback'}`); }} if (result.resumedFromCheckpoint) { console.log(`Resumed from checkpoint: ${result.resumedFromCheckpoint}`);}Best Practices
1. Classify Tools by Risk Level
Auto-approve read-only tools (search, fetch). Require approval for write operations (send, delete, modify). Always require approval for irreversible actions.
2. Set Appropriate Timeouts
Use short timeouts for interactive CLI (30-60s). Use longer timeouts for async approval (hours/days). Handle timeout gracefully — either retry or skip the action.
3. Persist Checkpoints for Production
MemoryCheckpointStore is for development only. Use Redis, PostgreSQL, or S3 for production. Include checkpoint cleanup in your maintenance routines.
4. Provide Context in Interrupt Messages
Include the agent's reasoning (thought) so humans understand why the action is being taken. Show the full tool input so humans can make informed decisions.
Tree-shaking Imports
// ✅ Import only HITL componentsimport { HITLAgent, MemoryCheckpointStore } from '@orka-js/agent';import type { InterruptRequest, InterruptResponse, Checkpoint, CheckpointStore, HITLConfig } from '@orka-js/agent'; // ✅ Or import from agent indeximport { type InterruptRequest, type InterruptResponse, type Checkpoint, type CheckpointStore, type HITLConfig, HITLAgent, MemoryCheckpointStore,} from '@orka-js/agent';