@cloudbase/agent-examples-claude-agent-human-in-the-loop
v0.0.24
Published
Claude Agent SDK human-in-the-loop example
Readme
Claude Agent Human-in-the-Loop Example
This example demonstrates how to implement human-in-the-loop (HITL) workflows with Claude Agent SDK.
Overview
When a user asks Claude to perform a task, the agent:
- Calls the
plan_execution_stepstool to generate 10 imperative steps - Returns an interrupt with the steps for user review
- Waits for user to enable/disable steps
- Continues execution with the approved steps
- Generates a creative description of how it will perform the task
Architecture
User: "Help me bake a cake"
↓
Claude: Calls plan_execution_steps
↓
Tool: Returns { __interrupt__: true, steps: [...] }
↓
Agent: Detects __interrupt__, emits RUN_FINISHED (interrupt)
↓
Frontend: Shows steps UI (enable/disable checkboxes)
↓
User: Disables "Add frosting", clicks Continue
↓
Frontend: Sends resume({ steps: [...modified...] })
↓
Agent: Uses session ID to resume
↓
Claude: Generates creative response based on enabled stepsKey Features
- Decoupled Design: Agent doesn't know about specific tools, only detects
__interrupt__marker - Session Management: Uses Claude SDK's native session mechanism for context preservation
- Generic Protocol: Any tool can trigger interrupts by returning
__interrupt__: true - Flexible: Easy to add new HITL scenarios without modifying the agent
Usage
Environment Variables
ANTHROPIC_API_KEY=your-api-key
CLAUDE_MODEL=claude-3-5-sonnet-20241022 # optionalRunning the Example
# Install dependencies
pnpm install
# Build
pnpm run build
# Run the server (from examples/agents/server)
cd ../server
pnpm run devTesting
- Open the UI:
http://localhost:5173/?agent=ts-claude-agent-human-in-the-loop - Send a message: "Help me bake a cake"
- Claude will generate 10 steps and pause
- Review the steps, disable any you don't want
- Click "Continue"
- Claude will generate a creative response based on your choices
Implementation Details
Tool Definition
const planExecutionStepsTool = tool(
"plan_execution_steps",
"Generate 10 imperative steps for a task",
{ steps: z.array(z.object({ description: z.string(), status: z.literal("enabled") })) },
async (args) => {
// Return __interrupt__ marker
return {
content: [{
type: "text",
text: JSON.stringify({
__interrupt__: true,
reason: "Task planning requires user confirmation",
steps: args.steps,
})
}],
isError: false,
};
}
);Agent Configuration
const agent = new ClaudeAgent({
name: "claude-hitl-agent",
mcpServers: { hitl: createSdkMcpServer({ tools: [planExecutionStepsTool] }) },
systemPrompt: "When user asks to perform a task, MUST call plan_execution_steps...",
includePartialMessages: true,
});Interrupt Detection (in ClaudeAgent)
private detectInterrupt(sdkMessage: SDKMessage) {
if (sdkMessage.type === "assistant") {
for (const content of message.content) {
if (content.type === "text") {
const parsed = JSON.parse(content.text);
if (parsed.__interrupt__) {
return { id, reason, payload: parsed };
}
}
}
}
return null;
}Resume (in ClaudeAgent)
private async _resume(subscriber, input) {
const sessionId = this.sessionStore.get(threadId);
const resumePrompt = this.buildResumePrompt(payload);
const agentQuery = query({
prompt: resumePrompt,
options: { resume: sessionId } // SDK auto-loads history
});
// Continue processing...
}Extending with New HITL Scenarios
To add a new HITL scenario (e.g., payment approval):
const approvePaymentTool = tool(
"approve_payment",
"Request approval for a payment",
{ amount: z.number(), recipient: z.string() },
async (args) => {
return {
content: [{
type: "text",
text: JSON.stringify({
__interrupt__: true,
type: "approve_payment",
amount: args.amount,
recipient: args.recipient,
})
}],
isError: false,
};
}
);No changes needed to ClaudeAgent! It automatically detects any __interrupt__ marker.
