OrkaJS
Orka.JS

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 Gate

Enforce strict manual validation for sensitive operations like database writes or API calls.

State Checkpoints

Resilience

Atomic persistence of the agent's memory. Revert or resume from any specific step in the history.

Action Modification

Supervision

Empower 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 approval
const 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 agent
const 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 handler
async 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

approved
Full Authorization

Immediate execution. The agent proceeds with original intent.

{ status: 'approved' }
rejected
Manual Override

Execution halted. The agent receives feedback to replan.

{ status: 'rejected', feedback: '...' }
modified
Guided Correction

Intercept and pivot. Execute tool with human-curated inputs.

{ status: 'modified', modifiedData: { ... } }
timeout
Safe Fallback

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 agent
const result = await agent.run('Analyze all customer data');
 
// List all checkpoints
const checkpoints = await agent.getCheckpoints();
console.log('Saved checkpoints:', checkpoints.map(cp => cp.id));
 
// Resume from a specific checkpoint
const 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 action
const 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 reasoning
const 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
}
 
// Usage
const 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 components
import { HITLAgent, MemoryCheckpointStore } from '@orka-js/agent';
import type {
InterruptRequest,
InterruptResponse,
Checkpoint,
CheckpointStore,
HITLConfig
} from '@orka-js/agent';
 
// ✅ Or import from agent index
import {
type InterruptRequest,
type InterruptResponse,
type Checkpoint,
type CheckpointStore,
type HITLConfig,
HITLAgent,
MemoryCheckpointStore,
} from '@orka-js/agent';