Human-in-the-Loop
Enable interactive conversations where the agent can ask the user questions.
Quick Start
Section titled “Quick Start”import { LLMist, Gadget, HumanInputRequiredException, z } from 'llmist';
// Gadget that requests user inputclass AskUser extends Gadget({ description: 'Ask the user a question when you need more information', schema: z.object({ question: z.string().describe('The question to ask'), }),}) { execute(params: this['params']): string { throw new HumanInputRequiredException(params.question); }}
// Handle the input requestconst answer = await LLMist.createAgent() .withModel('sonnet') .withGadgets(AskUser) .onHumanInput(async (question) => { // Your input function (readline, prompt, etc.) return await promptUser(question); }) .askAndCollect('Help me plan a trip');How It Works
Section titled “How It Works”- LLM calls the
AskUsergadget with a question - Gadget throws
HumanInputRequiredException - Agent pauses and emits
human_input_requiredevent - Your
onHumanInputhandler is called - User’s response is sent back to the LLM
- Agent continues
Event Handling
Section titled “Event Handling”With askWith()
Section titled “With askWith()”await LLMist.createAgent() .withGadgets(AskUser) .onHumanInput(async (question) => { return await getUserInput(question); }) .askWith('Help me', { onText: (text) => console.log(text), onHumanInputRequired: (data) => { console.log(`Agent is asking: ${data.question}`); }, });With run()
Section titled “With run()”const agent = LLMist.createAgent() .withGadgets(AskUser) .onHumanInput(async (question) => { return await getUserInput(question); }) .ask('Help me plan');
for await (const event of agent.run()) { if (event.type === 'human_input_required') { console.log(`Agent needs input: ${event.question}`); // Input will be handled by onHumanInput }}Readline Example
Section titled “Readline Example”import * as readline from 'readline';
const rl = readline.createInterface({ input: process.stdin, output: process.stdout,});
function promptUser(question: string): Promise<string> { return new Promise((resolve) => { rl.question(`${question}\n> `, (answer) => { resolve(answer); }); });}
const answer = await LLMist.createAgent() .withModel('sonnet') .withGadgets(AskUser) .onHumanInput(promptUser) .askAndCollect('Interview me about my preferences');
rl.close();Multiple Questions
Section titled “Multiple Questions”The agent can ask multiple questions in sequence:
class AskUser extends Gadget({ description: 'Ask user a question. Use for: preferences, clarification, choices', schema: z.object({ question: z.string(), context: z.string().optional().describe('Why you need this info'), }),}) { execute(params: this['params']): string { const fullQuestion = params.context ? `${params.context}\n\n${params.question}` : params.question; throw new HumanInputRequiredException(fullQuestion); }}
// Agent might ask:// 1. "What's your budget?"// 2. "Do you prefer beach or mountains?"// 3. "How long is your trip?"Confirmation Pattern
Section titled “Confirmation Pattern”Ask for confirmation before actions:
class ConfirmAction extends Gadget({ description: 'Ask user to confirm before proceeding with an action', schema: z.object({ action: z.string().describe('What will be done'), consequences: z.string().optional(), }),}) { execute(params: this['params']): string { const message = params.consequences ? `${params.action}\n\nNote: ${params.consequences}\n\nProceed? (yes/no)` : `${params.action}\n\nProceed? (yes/no)`; throw new HumanInputRequiredException(message); }}
// Use with other gadgets.withGadgets(ConfirmAction, DeleteFile, SendEmail)Text-Only Handler
Section titled “Text-Only Handler”Control behavior when LLM responds without calling gadgets:
.withTextOnlyHandler('terminate') // Default: end loop.withTextOnlyHandler('acknowledge') // Continue for another iteration.withTextOnlyHandler('wait_for_input') // Wait for user input
// Custom handler.withTextOnlyHandler({ type: 'custom', handler: async (context) => { if (context.text.includes('?')) { return { action: 'wait_for_input', question: context.text }; } return { action: 'continue' }; },})Human Input in Subagents
Section titled “Human Input in Subagents”When building subagent gadgets (like browser automation), you may need to request user input from within nested agents. The requestHumanInput callback is automatically inherited through createSubagent():
- Parent’s
.onHumanInput()handler is passed viaExecutionContext - Subagents get the callback via
createSubagent(ctx, {...}) - Any
HumanInputRequiredExceptionin nested gadgets bubbles up to parent
This is useful for scenarios like:
- 2FA/SMS codes - Browser automation encounters login with 2FA
- CAPTCHAs - Automated flow needs human to solve CAPTCHA
- Confirmations - Nested agent needs user approval for sensitive actions
// In a browser automation subagentclass RequestUserAssistance extends Gadget({ name: 'RequestUserAssistance', schema: z.object({ reason: z.enum(['captcha', '2fa_code', 'sms_code']), message: z.string(), }),}) { execute(params: this['params']): string { // This bubbles up to the CLI's TUI throw new HumanInputRequiredException( `[${params.reason}] ${params.message}` ); }}
// Parent agent's onHumanInput handler receives the questionSee Subagents for implementation details.
See Also
Section titled “See Also”- Gadgets Guide - Creating custom gadgets
- Streaming Guide - Event handling
- Error Handling - Handling input errors