Human-in-the-Loop Example
Complete example of HITL agent with interactive approval, checkpoints, and resume.
This example demonstrates a financial assistant that requires human approval for sensitive operations like money transfers, creates checkpoints for recovery, and allows resuming from saved states.
ORKA — HUMAN-IN-THE-LOOP WORKFLOW
👤 User Request
HITL Agent
Analyze request
Plan actions
Plan actions
⚠️ Sensitive Action?
Money transfer, deletion, etc.
No
Execute
Auto-run
Yes
🛑 Pause
Wait for approval
👤 Human Review
Approve/Reject/Modify
✅ Execute
If approved
✨ Result + Checkpoint
1. Tool Definitions
tools.ts
import { Tool } from '@orka-js/agent'; // Tool pour vérifier le solde d'un compteconst checkBalanceTool: Tool = { name: 'check_balance', description: 'Vérifie le solde d'un compte bancaire', parameters: [ { name: 'accountId', type: 'string', description: 'ID du compte', required: true }, ], async execute(input) { const accountId = input.accountId as string; const balances: Record<string, number> = { 'ACC-001': 5420.50, 'ACC-002': 12890.00, 'ACC-003': 850.25, }; const balance = balances[accountId] ?? 0; return { output: `Solde du compte ${accountId}: ${balance}€` }; },}; // Tool pour transférer de l'argent (opération sensible)const transferMoneyTool: Tool = { name: 'transfer_money', description: 'Transfère de l'argent entre deux comptes', parameters: [ { name: 'fromAccount', type: 'string', description: 'Compte source', required: true }, { name: 'toAccount', type: 'string', description: 'Compte destination', required: true }, { name: 'amount', type: 'number', description: 'Montant à transférer', required: true }, ], async execute(input) { const { fromAccount, toAccount, amount } = input; // Simulation du transfert const transactionId = `TXN-${Date.now()}`; return { output: `Transfert de ${amount}€ effectué de ${fromAccount} vers ${toAccount}. Transaction ID: ${transactionId}` }; },}; // Tool pour consulter l'historiqueconst getTransactionHistoryTool: Tool = { name: 'get_transaction_history', description: 'Récupère l'historique des transactions d'un compte', parameters: [ { name: 'accountId', type: 'string', description: 'ID du compte', required: true }, { name: 'limit', type: 'number', description: 'Nombre de transactions', required: false }, ], async execute(input) { const accountId = input.accountId as string; const limit = (input.limit as number) ?? 5; return { output: `Dernières ${limit} transactions du compte ${accountId}: [Achat -45€, Virement +200€, Retrait -100€]` }; },};2. Interactive Approval Handler
This handler prompts the user in the terminal for approval decisions. In production, you would integrate with your UI, Slack, email, or webhook system.
approval-handler.ts
import * as readline from 'readline';import type { InterruptRequest, InterruptResponse } from '@orka-js/agent'; // Gestionnaire d'interruption interactifasync function interactiveApprovalHandler( request: InterruptRequest): Promise<InterruptResponse> { console.log(`\n⚠️ INTERRUPTION REQUISE [${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)}`); } const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question( '\n👤 Votre décision? (a=approve, r=reject, m=modify): ', (answer) => { rl.close(); const status = answer.toLowerCase(); if (status === 'a') { resolve({ id: request.id, status: 'approved', respondedAt: new Date(), }); } else if (status === 'r') { resolve({ id: request.id, status: 'rejected', feedback: 'Opération refusée par l\'utilisateur', respondedAt: new Date(), }); } else if (status === 'm') { // Exemple: réduire le montant du transfert const modifiedInput = { ...request.data.toolInput }; if (modifiedInput.amount) { modifiedInput.amount = (modifiedInput.amount as number) / 2; console.log(`✏️ Montant modifié: ${modifiedInput.amount}€`); } resolve({ id: request.id, status: 'modified', modifiedData: { toolInput: modifiedInput }, feedback: 'Montant réduit de moitié', respondedAt: new Date(), }); } else { resolve({ id: request.id, status: 'rejected', feedback: 'Réponse invalide', respondedAt: new Date(), }); } } ); });}3. HITL Agent Setup
agent-setup.ts
import { OpenAIAdapter } from '@orka-js/openai';import { HITLAgent, MemoryCheckpointStore } from '@orka-js/agent'; // Créer le checkpoint storeconst checkpointStore = new MemoryCheckpointStore(); // Créer l'agent HITLconst financialAgent = new HITLAgent( { name: 'financial-assistant', goal: 'Aider l\'utilisateur avec ses opérations bancaires', tools: [checkBalanceTool, transferMoneyTool, getTransactionHistoryTool], maxSteps: 10, temperature: 0.2, verbose: true, hitl: { // Nécessite approbation pour les transferts d'argent requireApprovalFor: ['transfer_money'], // Auto-approuve les consultations (non sensibles) autoApproveTools: ['check_balance', 'get_transaction_history'], // Crée un checkpoint tous les 2 steps checkpointEvery: 2, // Timeout de 5 minutes pour les approbations defaultTimeoutMs: 5 * 60 * 1000, // Gestionnaire d'interruption onInterrupt: interactiveApprovalHandler, // Store de checkpoints checkpointStore, }, }, new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY! }));4. Event Listeners
event-listeners.ts
// Écouter les événements de l'agentfinancialAgent.on('step:start', (event) => { console.log(`\n🔄 Step ${event.stepNumber} démarré`);}); financialAgent.on('step:complete', (event) => { console.log(`✅ Step ${event.stepNumber} terminé`); if (event.toolUsed) { console.log(` Tool utilisé: ${event.toolUsed}`); }}); financialAgent.on('checkpoint:created', (event) => { console.log(`💾 Checkpoint créé: ${event.checkpointId}`);}); financialAgent.on('interrupt:requested', (event) => { console.log(`⏸️ Interruption demandée: ${event.reason}`);}); financialAgent.on('interrupt:resolved', (event) => { console.log(`▶️ Interruption résolue: ${event.status}`);}); financialAgent.on('complete', (event) => { console.log(`\n🎉 Agent terminé!`); console.log(` Steps: ${event.totalSteps}`); console.log(` Interruptions: ${event.interruptCount}`);});5. Running the Agent
main.ts
async function main() { console.log('🤖 Assistant Financier HITL\n'); try { // Scénario 1: Transfert d'argent (nécessite approbation) console.log('=== Scénario 1: Transfert d\'argent ==='); const result1 = await financialAgent.run( 'Transfère 500€ de mon compte ACC-001 vers ACC-002' ); console.log('\n📊 Résultat:'); console.log('Réponse:', result1.output); console.log('Steps:', result1.steps.length); console.log('Interruptions:', result1.interrupts.length); console.log('Interrompu?', result1.wasInterrupted); // Afficher les détails des interruptions if (result1.interrupts.length > 0) { console.log('\nDétails des interruptions:'); result1.interrupts.forEach((interrupt, i) => { console.log(` ${i + 1}. Status: ${interrupt.status}`); if (interrupt.feedback) { console.log(` Feedback: ${interrupt.feedback}`); } }); } // Afficher les checkpoints créés if (result1.checkpoints.length > 0) { console.log('\n💾 Checkpoints créés:', result1.checkpoints); } } catch (error) { console.error('❌ Erreur:', error); }} main().catch(console.error);6. Resume from Checkpoint
If the agent is interrupted or crashes, you can resume from the last checkpoint:
resume.ts
async function resumeFromCheckpoint() { // Récupérer tous les checkpoints de l'agent const checkpoints = await financialAgent.getCheckpoints(); if (checkpoints.length === 0) { console.log('Aucun checkpoint disponible'); return; } // Afficher les checkpoints disponibles console.log('\n💾 Checkpoints disponibles:'); checkpoints.forEach((cp, i) => { console.log(` ${i + 1}. ${cp.id} - Step ${cp.stepNumber} - ${cp.createdAt}`); }); // Reprendre depuis le dernier checkpoint const latestCheckpoint = checkpoints[checkpoints.length - 1]; console.log(`\n▶️ Reprise depuis: ${latestCheckpoint.id}`); const result = await financialAgent.run( 'Continue l\'opération précédente', latestCheckpoint.id ); console.log('\n✅ Opération reprise avec succès'); console.log('Repris depuis checkpoint:', result.resumedFromCheckpoint); console.log('Résultat:', result.output);} // UtilisationresumeFromCheckpoint().catch(console.error);7. Manual Interrupts
You can also request manual confirmations or reviews during agent execution:
manual-interrupts.ts
// Dans votre tool ou logique métierconst complexOperationTool: Tool = { name: 'complex_operation', description: 'Effectue une opération complexe', parameters: [ { name: 'data', type: 'object', description: 'Données', required: true }, ], async execute(input, context) { // Demander une confirmation avant de continuer const confirmation = await context.agent.requestConfirmation( 'Cette opération est irréversible. Continuer?', { operation: 'complex_operation', data: input.data } ); if (confirmation.status !== 'approved') { return { output: 'Opération annulée par l\'utilisateur' }; } // Effectuer l'opération const result = performComplexOperation(input.data); // Demander une revue du résultat const review = await context.agent.requestReview( 'Veuillez vérifier le résultat', context.stepNumber, 'Opération complexe terminée' ); if (review.status === 'rejected') { return { output: 'Résultat rejeté, opération annulée' }; } return { output: `Opération réussie: ${result}` }; },};8. Complete Example Output
output.txt
🤖 Assistant Financier HITL === Scénario 1: Transfert d'argent === 🔄 Step 1 démarré✅ Step 1 terminé Tool utilisé: check_balance 💾 Checkpoint créé: cp-1234567890 🔄 Step 2 démarré ⚠️ INTERRUPTION REQUISE [tool_approval]📋 Message: Approbation requise pour: transfer_money🔧 Tool: transfer_money📥 Input: { "fromAccount": "ACC-001", "toAccount": "ACC-002", "amount": 500} 👤 Votre décision? (a=approve, r=reject, m=modify): a ▶️ Interruption résolue: approved✅ Step 2 terminé Tool utilisé: transfer_money 💾 Checkpoint créé: cp-1234567891 🔄 Step 3 démarré✅ Step 3 terminé 🎉 Agent terminé! Steps: 3 Interruptions: 1 📊 Résultat:Réponse: J'ai effectué le transfert de 500€ de votre compte ACC-001 vers ACC-002. Transaction ID: TXN-1234567890Steps: 3Interruptions: 1Interrompu? true Détails des interruptions: 1. Status: approved 💾 Checkpoints créés: ['cp-1234567890', 'cp-1234567891']💡 Best Practices
- Security: Always require approval for sensitive operations (money transfers, data deletion, external API calls)
- Checkpoints: Create checkpoints frequently for long-running operations to enable recovery
- Timeouts: Set appropriate timeouts for approvals to avoid blocking indefinitely
- Production: In production, integrate with your UI, Slack, webhooks, or notification system instead of terminal prompts
- Persistence: Use RedisCheckpointStore or PostgresCheckpointStore for production instead of MemoryCheckpointStore
🚀 Next Steps
- Explore the HITL documentation for advanced patterns
- Implement custom CheckpointStore for your database
- Integrate approval workflows with your existing systems
- Add monitoring and observability for HITL events