> ## Documentation Index
> Fetch the complete documentation index at: https://docs.symbolica.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Human-in-the-Loop

> Maintain oversight and control in agentic applications

## Overview

Human-in-the-loop (HITL) means requiring human approval before agents perform certain actions. This is critical for:

* **High-stakes operations**: Deleting data, making purchases, sending communications
* **Compliance requirements**: Regulated industries requiring human oversight
* **Safety guardrails**: Preventing unintended consequences in production
* **Gradual trust building**: Starting with oversight, removing it as confidence grows

The key insight with the Agentica SDK is that **you don't need special framework features for HITL.**
It's just wrapping the functions you pass to agents with approval logic.
The Agentica SDK makes this trivial because agents just use regular functions -- wrap those functions, and you have human oversight.

<Info>
  **What's in this guide:**

  * <a href="#the-core-pattern">**The Core Pattern**</a> -- Basic HITL implementation (start here)
  * <a href="#decision-types">**Decision Types**</a> -- Approve, edit, or reject with feedback
  * <a href="#approval-mechanisms">**Approval Mechanisms**</a> -- CLI, web dashboard, Slack, conditional, and more
  * <a href="#practical-examples">**Practical Examples**</a> -- Real-world deployment, support, and data analysis use cases
  * <a href="#advanced-patterns">**Advanced Patterns**</a> -- Batch approvals, budgets, time-based rules, audit trails
  * <a href="#best-practices">**Best Practices**</a> -- Production-ready tips for scaling HITL systems
</Info>

## The Core Pattern

Instead of passing a function directly to an agent, wrap it with approval logic:

<CodeGroup>
  ```python Python theme={null}
  from agentica import spawn

  # Original function
  def delete_file(path: str) -> None:
      os.remove(path)

  # Wrapped version with approval
  def delete_file_with_approval(path: str) -> None:
      # Ask for approval however you want
      response = input(f"Delete {path}? (yes/no): ")
      if response.lower() != 'yes':
          raise PermissionError(f"User denied deletion of {path}")

      # If approved, call the original function
      os.remove(path)

  # Pass the wrapped version to the agent
  agent = await spawn(premise="You are a file organizer")
  await agent.call(None, "Clean up the temp directory", delete_file=delete_file_with_approval)
  ```

  ```typescript TypeScript theme={null}
  import { spawn } from '@symbolica/agentica';
  import * as readline from 'readline';

  // Original function
  function deleteFile(path: string): void {
    fs.unlinkSync(path);
  }

  // Wrapped version with approval
  async function deleteFileWithApproval(path: string): Promise<void> {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });

    const answer = await new Promise<string>(resolve => {
      rl.question(`Delete ${path}? (yes/no): `, resolve);
    });
    rl.close();

    if (answer.toLowerCase() !== 'yes') {
      throw new Error(`User denied deletion of ${path}`);
    }

    // If approved, call the original function
    fs.unlinkSync(path);
  }

  // Pass the wrapped version to the agent
  const agent = await spawn({ premise: "You are a file organizer" });
  await agent.call("Clean up the temp directory", { deleteFile: deleteFileWithApproval });
  ```
</CodeGroup>

No middleware, no special API, no framework hooks. Just wrap the function you're passing.

<Tip>
  The agent sees the approval prompt as part of the function's behavior. If you deny the action, the agent receives an exception and can adapt accordingly.
</Tip>

## Decision Types

When reviewing an action, you have three typical options:

| Decision    | Description                            | When to Use                               | Agent Response                         |
| ----------- | -------------------------------------- | ----------------------------------------- | -------------------------------------- |
| **Approve** | Execute the action exactly as proposed | Action is correct and safe                | Proceeds with execution                |
| **Edit**    | Modify arguments before executing      | Action is almost right but needs tweaking | Executes with your changes             |
| **Reject**  | Deny the action with feedback          | Action is wrong or unsafe                 | Sees your feedback and adapts strategy |

<AccordionGroup>
  <Accordion title="Approve: Execute As-Is" icon="check">
    <CodeGroup>
      ```python Python theme={null}
      response = input(f"Approve action? (yes/no): ")
      if response.lower() == 'yes':
          return original_function(*args, **kwargs)
      else:
          raise PermissionError("Action denied")
      ```

      ```typescript TypeScript theme={null}
      const response = await getInput("Approve action? (yes/no): ");
      if (response.toLowerCase() === 'yes') {
        return originalFunction(...args);
      } else {
        throw new Error("Action denied");
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Edit: Modify Before Executing" icon="pen-to-square">
    Modify the agent's proposed arguments before executing. This is useful when the action is almost right but needs adjustment:

    <CodeGroup>
      ```python Python theme={null}
      def send_email_with_editable_approval(to: str, subject: str, body: str) -> str:
          print()
          print(f"📧 Agent wants to send email:")
          print(f"   To: {to}")
          print(f"   Subject: {subject}")
          print(f"   Body: {body[:100]}...")

          print()
          response = input("   Action? (approve/edit/reject): ")

          if response == 'approve':
              return send_email(to, subject, body)
          elif response == 'edit':
              # Let human modify the arguments
              new_to = input(f"   To [{to}]: ") or to
              new_subject = input(f"   Subject [{subject}]: ") or subject
              new_body = input(f"   Body (press enter to keep current): ") or body

              print(f"Executing with modified arguments")
              return send_email(new_to, new_subject, new_body)
          else:
              raise PermissionError("Email sending denied")
      ```

      ```typescript TypeScript theme={null}
      async function sendEmailWithEditableApproval(
        to: string,
        subject: string,
        body: string
      ): Promise<string> {
        console.log('\n📧 Agent wants to send email:');
        console.log(`   To: ${to}`);
        console.log(`   Subject: ${subject}`);
        console.log(`   Body: ${body.slice(0, 100)}...`);

        const action = await getInput('\n   Action? (approve/edit/reject): ');

        if (action === 'approve') {
          return sendEmail(to, subject, body);
        } else if (action === 'edit') {
          // Let human modify the arguments
          const newTo = await getInput(`   To [${to}]: `) || to;
          const newSubject = await getInput(`   Subject [${subject}]: `) || subject;
          const newBody = await getInput(`   Body (enter to keep): `) || body;

          console.log('Executing with modified arguments');
          return sendEmail(newTo, newSubject, newBody);
        } else {
          throw new Error('Email sending denied');
        }
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Reject with Feedback" icon="ban">
    Deny the action but provide feedback that helps the agent understand why and try a different approach:

    <CodeGroup>
      ```python Python theme={null}
      def execute_sql_with_feedback(query: str) -> list:
          print(f"\nAgent wants to execute SQL:")
          print(f"   Query: {query[:100]}...")

          response = input("\n   Approve? (yes/no): ")

          if response.lower() == 'yes':
              return run_sql_query(query)
          else:
              # Provide specific feedback to help the agent
              feedback = input("   Reason for rejection: ")
              raise PermissionError(
                  f"SQL execution denied. Feedback: {feedback}. "
                  f"Please revise the query and try again."
              )
              # The agent sees this error message and can adapt its approach
      ```

      ```typescript TypeScript theme={null}
      async function executeSqlWithFeedback(query: string): Promise<any[]> {
        console.log('\nAgent wants to execute SQL:');
        console.log(`   Query: ${query.slice(0, 100)}...`);

        const response = await getInput('\n   Approve? (yes/no): ');

        if (response.toLowerCase() === 'yes') {
          return runSqlQuery(query);
        } else {
          // Provide specific feedback to help the agent
          const feedback = await getInput('   Reason for rejection: ');
          throw new Error(
            `SQL execution denied. Feedback: ${feedback}. ` +
            `Please revise the query and try again.`
          );
          // The agent sees this error message and can adapt its approach
        }
      }
      ```
    </CodeGroup>

    <Note>
      When you raise an exception with a descriptive message, the agent sees that message and can adjust its strategy. For example, if you reject a query because "must use READ ONLY transaction," the agent might then add that to its next attempt.
    </Note>
  </Accordion>
</AccordionGroup>

## Approval Mechanisms

How you ask for approval depends on your application context. Here are practical patterns:

<AccordionGroup>
  <Accordion title="Terminal/CLI Approval" icon="terminal">
    Perfect for development, scripts, and command-line tools:

    <CodeGroup>
      ```python Python theme={null}
      def with_cli_approval(func, action_description: str):
          """Decorator that adds CLI approval to any function."""
          def wrapper(*args, **kwargs):
              print(f"\n🤖 Agent wants to: {action_description}")
              print(f"   Args: {args}, Kwargs: {kwargs}")
              response = input("   Approve? (yes/no): ")

              if response.lower() != 'yes':
                  raise PermissionError(f"User denied: {action_description}")

              return func(*args, **kwargs)
          return wrapper

      # Use it
      send_email = with_cli_approval(
          original_send_email,
          "send an email"
      )

      agent = await spawn(premise="You are an assistant")
      await agent.call(None, "Email the team about the meeting", send_email=send_email)
      ```

      ```typescript TypeScript theme={null}
      import * as readline from 'readline';

      function withCLIApproval<T extends (...args: any[]) => any>(
        func: T,
        actionDescription: string
      ): T {
        return (async (...args: any[]) => {
          console.log(`\n🤖 Agent wants to: ${actionDescription}`);
          console.log(`   Args: ${JSON.stringify(args)}`);

          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
          });

          const answer = await new Promise<string>(resolve => {
            rl.question('   Approve? (yes/no): ', resolve);
          });
          rl.close();

          if (answer.toLowerCase() !== 'yes') {
            throw new Error(`User denied: ${actionDescription}`);
          }

          return func(...args);
        }) as T;
      }

      // Use it
      const sendEmail = withCLIApproval(
        originalSendEmail,
        "send an email"
      );

      const agent = await spawn({ premise: "You are an assistant" });
      await agent.call("Email the team about the meeting", { sendEmail });
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Browser Alert/Confirm" icon="window">
    For web applications running in the browser:

    <CodeGroup>
      ```typescript TypeScript theme={null}
      function withBrowserApproval<T extends (...args: any[]) => any>(
        func: T,
        actionDescription: string
      ): T {
        return ((...args: any[]) => {
          const confirmed = window.confirm(
            `Agent wants to ${actionDescription}\n\n` +
            `Details: ${JSON.stringify(args, null, 2)}\n\n` +
            `Allow this action?`
          );

          if (!confirmed) {
            throw new Error(`User denied: ${actionDescription}`);
          }

          return func(...args);
        }) as T;
      }

      // Use it with Slack integration
      const postMessage = withBrowserApproval(
        slackClient.chat.postMessage.bind(slackClient.chat),
        "post a Slack message"
      );

      const agent = await spawn({ premise: "You are a Slack assistant" });
      await agent.call(
        "Send a message to #engineering about the deploy",
        { postMessage }
      );
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Web Dashboard Approval" icon="chart-mixed">
    For production systems with approval workflows, you may want approvals to go through a proper web interface where managers can review requests, see context, and make informed decisions. This is especially useful when:

    * The person approving isn't at their terminal
    * You need a record of who approved what and when
    * Multiple people might need to approve the same action
    * You want to batch approval requests for review

    The pattern: create an approval request via HTTP, poll until approved/denied, then proceed or raise an error.

    <CodeGroup>
      ```python Python theme={null}
      import requests
      import time

      def with_dashboard_approval(func, action_description: str, dashboard_url: str):
          """Request approval via web dashboard."""
          def wrapper(*args, **kwargs):
              # Create approval request
              response = requests.post(
                  f"{dashboard_url}/api/approvals",
                  json={"action": action_description, "args": list(args)}
              )
              request_id = response.json()["id"]
              print(f"⏳ Waiting for approval at {dashboard_url}/approvals/{request_id}")

              # Poll for approval
              while True:
                  status = requests.get(f"{dashboard_url}/api/approvals/{request_id}").json()

                  if status["approved"]:
                      print(f"✅ Action approved")
                      return func(*args, **kwargs)
                  elif status["denied"]:
                      raise PermissionError(f"Action denied: {status['reason']}")

                  time.sleep(2)  # Poll every 2 seconds
          return wrapper
      ```

      ```typescript TypeScript theme={null}
      function withDashboardApproval<T extends (...args: any[]) => any>(
        func: T,
        actionDescription: string,
        dashboardUrl: string
      ): T {
        return (async (...args: any[]) => {
          // Create approval request
          const response = await fetch(`${dashboardUrl}/api/approvals`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ action: actionDescription, args })
          });

          const { id: requestId } = await response.json();
          console.log(`⏳ Waiting for approval at ${dashboardUrl}/approvals/${requestId}`);

          // Poll for approval
          while (true) {
            const status = await (await fetch(`${dashboardUrl}/api/approvals/${requestId}`)).json();

            if (status.approved) {
              console.log('✅ Action approved');
              return func(...args);
            } else if (status.denied) {
              throw new Error(`Action denied: ${status.reason}`);
            }

            await new Promise(resolve => setTimeout(resolve, 2000)); // Poll every 2s
          }
        }) as T;
      }
      ```
    </CodeGroup>

    <Tip>
      See the [Customer Support example](#example%3A-customer-support-with-refund-approval) below for a complete production implementation using this pattern.
    </Tip>
  </Accordion>

  <Accordion title="Slack/Teams Approval" icon="slack">
    Request approval via chat platforms where your team already is:

    <CodeGroup>
      ````python Python theme={null}
      from slack_sdk import WebClient

      def with_slack_approval(func, action_description: str, slack_client: WebClient, channel: str):
          """Request approval via Slack message with buttons."""
          def wrapper(*args, **kwargs):
              # Send approval request with interactive buttons
              response = slack_client.chat_postMessage(
                  channel=channel,
                  text=f"🤖 Agent approval needed: {action_description}",
                  blocks=[
                      {
                          "type": "section",
                          "text": {
                              "type": "mrkdwn",
                              "text": f"*Agent wants to:* {action_description}\n*Details:* ```{args}```"
                          }
                      },
                      {
                          "type": "actions",
                          "elements": [
                              {
                                  "type": "button",
                                  "text": {"type": "plain_text", "text": "Approve"},
                                  "style": "primary",
                                  "action_id": "approve_action"
                              },
                              {
                                  "type": "button",
                                  "text": {"type": "plain_text", "text": "Deny"},
                                  "style": "danger",
                                  "action_id": "deny_action"
                              }
                          ]
                      }
                  ]
              )

              # Wait for button click (would need webhook handler in real implementation)
              # This is simplified - production needs proper Slack event handling
              approval_status = wait_for_slack_response(response['ts'])

              if approval_status != 'approved':
                  raise PermissionError(f"Action denied via Slack")

              return func(*args, **kwargs)

          return wrapper
      ````

      ```typescript TypeScript theme={null}
      import { WebClient } from '@slack/web-api';

      function withSlackApproval<T extends (...args: any[]) => any>(
        func: T,
        actionDescription: string,
        slackClient: WebClient,
        channel: string
      ): T {
        return (async (...args: any[]) => {
          // Send approval request with interactive buttons
          const response = await slackClient.chat.postMessage({
            channel,
            text: `🤖 Agent approval needed: ${actionDescription}`,
            blocks: [
              {
                type: 'section',
                text: {
                  type: 'mrkdwn',
                  text: `*Agent wants to:* ${actionDescription}\n*Details:* \`\`\`${JSON.stringify(args)}\`\`\``
                }
              },
              {
                type: 'actions',
                elements: [
                  {
                    type: 'button',
                    text: { type: 'plain_text', text: 'Approve' },
                    style: 'primary',
                    action_id: 'approve_action'
                  },
                  {
                    type: 'button',
                    text: { type: 'plain_text', text: 'Deny' },
                    style: 'danger',
                    action_id: 'deny_action'
                  }
                ]
              }
            ]
          });

          // Wait for button click (requires webhook handler in production)
          const approvalStatus = await waitForSlackResponse(response.ts!);

          if (approvalStatus !== 'approved') {
            throw new Error('Action denied via Slack');
          }

          return func(...args);
        }) as T;
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Conditional Approval" icon="filter">
    Only require approval for high-risk operations:

    <CodeGroup>
      ```python Python theme={null}
      def with_conditional_approval(func, should_require_approval):
          """Only require approval when condition is met."""
          def wrapper(*args, **kwargs):
              # Check if this specific call needs approval
              if should_require_approval(*args, **kwargs):
                  response = input(f"⚠️  High-risk operation detected. Approve? (yes/no): ")
                  if response.lower() != 'yes':
                      raise PermissionError("User denied high-risk operation")

              return func(*args, **kwargs)
          return wrapper

      # Only approve transfers over $10,000
      def needs_approval_for_large_transfer(amount: float, recipient: str) -> bool:
          return amount > 10000

      transfer_money = with_conditional_approval(
          bank.transfer,
          needs_approval_for_large_transfer
      )

      agent = await spawn(premise="Financial assistant")
      await agent.call(None, "Pay the invoices", transfer_money=transfer_money)
      ```

      ```typescript TypeScript theme={null}
      function withConditionalApproval<T extends (...args: any[]) => any>(
        func: T,
        shouldRequireApproval: (...args: any[]) => boolean
      ): T {
        return (async (...args: any[]) => {
          if (shouldRequireApproval(...args)) {
            const rl = readline.createInterface({
              input: process.stdin,
              output: process.stdout
            });

            const answer = await new Promise<string>(resolve => {
              rl.question('⚠️  High-risk operation detected. Approve? (yes/no): ', resolve);
            });
            rl.close();

            if (answer.toLowerCase() !== 'yes') {
              throw new Error('User denied high-risk operation');
            }
          }

          return func(...args);
        }) as T;
      }

      // Only approve transfers over $10,000
      function needsApprovalForLargeTransfer(amount: number, recipient: string): boolean {
        return amount > 10000;
      }

      const transferMoney = withConditionalApproval(
        bank.transfer.bind(bank),
        needsApprovalForLargeTransfer
      );

      const agent = await spawn({ premise: "Financial assistant" });
      await agent.call("Pay the invoices", { transferMoney });
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Practical Examples

<AccordionGroup>
  <Accordion title="Example: Deployment Pipeline with HITL" icon="rocket">
    An agent that can deploy code, but requires approval for production:

    <CodeGroup>
      ```python Python theme={null}
      from agentica import spawn
      from dataclasses import dataclass

      @dataclass
      class DeploymentConfig:
          environment: str
          branch: str
          services: list[str]

      def deploy_to_environment(config: DeploymentConfig) -> str:
          """Actually perform the deployment."""
          # Deployment logic here
          return f"Deployed {config.services} to {config.environment}"

      def deploy_with_approval(config: DeploymentConfig) -> str:
          """Wrapper that requires approval for production."""
          if config.environment == "production":
              print(f"\n⚠️  PRODUCTION DEPLOYMENT REQUEST")
              print(f"   Branch: {config.branch}")
              print(f"   Services: {', '.join(config.services)}")

              response = input("\n   Deploy to production? (yes/no): ")
              if response.lower() != 'yes':
                  raise PermissionError("Production deployment denied")

              print("\n✅ Production deployment approved")

          return deploy_to_environment(config)

      # Use with agent
      agent = await spawn(
          premise="You are a DevOps assistant. Deploy services when requested.",
          model="openai/gpt-5.2"
      )

      result = await agent.call(
          str,
          "Deploy the auth-service from main branch to production",
          deploy=deploy_with_approval
      )
      # Agent will call deploy_with_approval, which prompts for confirmation
      ```

      ```typescript TypeScript theme={null}
      import { spawn } from '@symbolica/agentica';
      import * as readline from 'readline';

      interface DeploymentConfig {
        environment: string;
        branch: string;
        services: string[];
      }

      function deployToEnvironment(config: DeploymentConfig): string {
        // Deployment logic here
        return `Deployed ${config.services.join(', ')} to ${config.environment}`;
      }

      async function deployWithApproval(config: DeploymentConfig): Promise<string> {
        if (config.environment === 'production') {
          console.log('\n⚠️  PRODUCTION DEPLOYMENT REQUEST');
          console.log(`   Branch: ${config.branch}`);
          console.log(`   Services: ${config.services.join(', ')}`);

          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
          });

          const answer = await new Promise<string>(resolve => {
            rl.question('\n   Deploy to production? (yes/no): ', resolve);
          });
          rl.close();

          if (answer.toLowerCase() !== 'yes') {
            throw new Error('Production deployment denied');
          }

          console.log('\n✅ Production deployment approved');
        }

        return deployToEnvironment(config);
      }

      // Use with agent
      const agent = await spawn({
        premise: "You are a DevOps assistant. Deploy services when requested.",
        model: "openai/gpt-5.2"
      });

      const result = await agent.call<string>(
        "Deploy the auth-service from main branch to production",
        { deploy: deployWithApproval }
      );
      // Agent will call deployWithApproval, which prompts for confirmation
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Example: Customer Support with Refund Approval" icon="headset">
    Agent handles support tickets but requires approval for refunds:

    <CodeGroup>
      ```python Python theme={null}
      from agentica import spawn

      class SupportTools:
          def __init__(self, approval_dashboard_url: str):
              self.dashboard_url = approval_dashboard_url

          def send_canned_response(self, ticket_id: str, response_type: str) -> str:
              """Automated responses don't need approval."""
              return f"Sent {response_type} response to ticket {ticket_id}"

          def issue_refund(self, ticket_id: str, amount: float, reason: str) -> str:
              """Refunds require approval via dashboard."""
              # Create approval request
              response = requests.post(
                  f"{self.dashboard_url}/api/refund-approvals",
                  json={
                      "ticket_id": ticket_id,
                      "amount": amount,
                      "reason": reason
                  }
              )
              approval_id = response.json()["id"]

              print(f"💰 Refund approval needed: ${amount} for ticket {ticket_id}")
              print(f"   Review at: {self.dashboard_url}/approvals/{approval_id}")

              # Wait for approval
              while True:
                  status = requests.get(
                      f"{self.dashboard_url}/api/refund-approvals/{approval_id}"
                  ).json()

                  if status["approved"]:
                      # Actually process the refund
                      return f"Refunded ${amount} to customer (ticket {ticket_id})"
                  elif status["denied"]:
                      raise PermissionError(f"Refund denied: {status['denial_reason']}")

                  time.sleep(3)

      # Use with support agent
      tools = SupportTools("https://support-dashboard.company.com")

      agent = await spawn(
          premise="""You are a customer support agent.
          You can send canned responses for common issues.
          For refund requests, use issue_refund - it will be reviewed by a manager."""
      )

      result = await agent.call(
          str,
          "Handle ticket #12345 - customer unhappy with product quality, requesting refund of $49.99",
          send_response=tools.send_canned_response,
          issue_refund=tools.issue_refund
      )
      ```

      ```typescript TypeScript theme={null}
      import { spawn } from '@symbolica/agentica';

      class SupportTools {
        constructor(private dashboardUrl: string) {}

        sendCannedResponse(ticketId: string, responseType: string): string {
          // Automated responses don't need approval
          return `Sent ${responseType} response to ticket ${ticketId}`;
        }

        async issueRefund(ticketId: string, amount: number, reason: string): Promise<string> {
          // Create approval request
          const response = await fetch(`${this.dashboardUrl}/api/refund-approvals`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ticketId, amount, reason })
          });

          const { id: approvalId } = await response.json();

          console.log(`💰 Refund approval needed: $${amount} for ticket ${ticketId}`);
          console.log(`   Review at: ${this.dashboardUrl}/approvals/${approvalId}`);

          // Wait for approval
          while (true) {
            const statusResponse = await fetch(
              `${this.dashboardUrl}/api/refund-approvals/${approvalId}`
            );
            const status = await statusResponse.json();

            if (status.approved) {
              return `Refunded $${amount} to customer (ticket ${ticketId})`;
            } else if (status.denied) {
              throw new Error(`Refund denied: ${status.denialReason}`);
            }

            await new Promise(resolve => setTimeout(resolve, 3000));
          }
        }
      }

      // Use with support agent
      const tools = new SupportTools('https://support-dashboard.company.com');

      const agent = await spawn({
        premise: `You are a customer support agent.
        You can send canned responses for common issues.
        For refund requests, use issueRefund - it will be reviewed by a manager.`
      });

      const result = await agent.call<string>(
        "Handle ticket #12345 - customer unhappy with product quality, requesting refund of $49.99",
        {
          sendResponse: tools.sendCannedResponse.bind(tools),
          issueRefund: tools.issueRefund.bind(tools)
        }
      );
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Example: Data Analysis with Destructive Action Gates" icon="database">
    Agent can query data freely, but needs approval for modifications:

    <CodeGroup>
      ```python Python theme={null}
      from agentica import spawn
      import pandas as pd

      class DatabaseTools:
          def __init__(self, db_connection):
              self.db = db_connection

          def query_data(self, sql: str) -> dict:
              """Read-only queries are always allowed."""
              if not sql.strip().upper().startswith('SELECT'):
                  raise ValueError("Only SELECT queries allowed in query_data")

              result = pd.read_sql(sql, self.db)
              return result.to_dict()

          def delete_records(self, table: str, condition: str) -> str:
              """Deletions require approval."""
              # Show what would be deleted
              preview_sql = f"SELECT * FROM {table} WHERE {condition}"
              preview = pd.read_sql(preview_sql, self.db)

              print(f"\n⚠️  DELETION REQUEST")
              print(f"   Table: {table}")
              print(f"   Condition: {condition}")
              print(f"   Rows to be deleted: {len(preview)}")
              print(f"\n   Preview:")
              print(preview.head().to_string())

              response = input(f"\n   Delete these {len(preview)} rows? (yes/no): ")
              if response.lower() != 'yes':
                  raise PermissionError("Deletion denied")

              # Perform deletion
              self.db.execute(f"DELETE FROM {table} WHERE {condition}")
              return f"Deleted {len(preview)} rows from {table}"

      # Use with data analysis agent
      db_tools = DatabaseTools(db_connection)

      agent = await spawn(
          premise="You are a data analyst. Query data to answer questions."
      )

      await agent.call(
          None,
          "Find and delete all test users created before 2024",
          query_data=db_tools.query_data,
          delete_records=db_tools.delete_records
      )
      # Agent can query freely, but deletions prompt for approval
      ```

      ```typescript TypeScript theme={null}
      import { spawn } from '@symbolica/agentica';
      import * as readline from 'readline';

      class DatabaseTools {
        constructor(private db: any) {}

        queryData(sql: string): object {
          if (!sql.trim().toUpperCase().startsWith('SELECT')) {
            throw new Error("Only SELECT queries allowed in queryData");
          }

          return this.db.query(sql);
        }

        async deleteRecords(table: string, condition: string): Promise<string> {
          // Show what would be deleted
          const preview = this.db.query(
            `SELECT * FROM ${table} WHERE ${condition}`
          );

          console.log('\n⚠️  DELETION REQUEST');
          console.log(`   Table: ${table}`);
          console.log(`   Condition: ${condition}`);
          console.log(`   Rows to be deleted: ${preview.length}`);
          console.log(`\n   Preview:`);
          console.log(preview.slice(0, 5));

          const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout
          });

          const answer = await new Promise<string>(resolve => {
            rl.question(`\n   Delete these ${preview.length} rows? (yes/no): `, resolve);
          });
          rl.close();

          if (answer.toLowerCase() !== 'yes') {
            throw new Error('Deletion denied');
          }

          // Perform deletion
          this.db.execute(`DELETE FROM ${table} WHERE ${condition}`);
          return `Deleted ${preview.length} rows from ${table}`;
        }
      }

      // Use with data analysis agent
      const dbTools = new DatabaseTools(dbConnection);

      const agent = await spawn({
        premise: "You are a data analyst. Query data to answer questions."
      });

      await agent.call(
        "Find and delete all test users created before 2024",
        {
          queryData: dbTools.queryData.bind(dbTools),
          deleteRecords: dbTools.deleteRecords.bind(dbTools)
        }
      );
      // Agent can query freely, but deletions prompt for approval
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Advanced Patterns

<AccordionGroup>
  <Accordion title="Batch Approvals" icon="list-check">
    When an agent wants to perform multiple actions at once, you can collect them all, review them together, then decide on each one.

    **How it works:**

    1. When the agent calls a wrapped function, instead of executing immediately, the call is queued and a placeholder like `"PENDING_APPROVAL_1"` is returned to the agent
    2. The agent continues executing and may queue multiple actions
    3. After the agent finishes, you call `review_and_execute()` to see all pending actions at once
    4. You can approve all, deny all, or handle each individually
    5. The actual functions execute based on your decisions

    This pattern is useful when you want to see the full scope of what the agent plans to do before committing to any individual action.

    <CodeGroup>
      ```python expandable Python theme={null}
      def create_batch_approver(*funcs_with_names):
          """
          Create wrapper that shows all pending actions for review.
          funcs_with_names: list of (name, function) tuples
          """
          pending_actions = []

          def make_wrapper(name, func):
              def wrapper(*args, **kwargs):
                  # Collect action for later approval
                  action_id = len(pending_actions) + 1
                  pending_actions.append({
                      'name': name,
                      'func': func,
                      'args': args,
                      'kwargs': kwargs,
                      'id': action_id
                  })
                  # Return marker that agent can see
                  return f"PENDING_APPROVAL_{action_id}"
              return wrapper

          # Create wrapped versions
          wrapped_funcs = {
              name: make_wrapper(name, func)
              for name, func in funcs_with_names
          }

          # Store results for wait_for_approvals to return
          approval_results = {}
          approval_complete = {'done': False}

          def wait_for_approvals():
              """
              Agent calls this to block until human reviews all pending actions.
              Returns dict mapping action IDs to their results.
              """
              import time
              print("\n[Agent is waiting for approval of pending actions...]")

              # Block until review_and_execute is called
              while not approval_complete['done']:
                  time.sleep(0.1)

              # Return results to agent
              approval_complete['done'] = False  # Reset for next batch
              return approval_results.copy()

          def review_and_execute():
              """Review all pending actions and execute based on decisions."""
              if not pending_actions:
                  return []

              print(f"\nAgent wants to perform {len(pending_actions)} actions:")

              # Show all actions
              for i, action in enumerate(pending_actions, 1):
                  print(f"\n{i}. {action['name']}")
                  print(f"   Args: {action['args']}")
                  print(f"   Kwargs: {action['kwargs']}")

              # Get batch decision
              print(f"\nOptions: 'all' (approve all), 'none' (deny all), 'review' (individual), 'edit N' (edit action N)")
              batch_decision = input("Decision: ").strip().lower()

              results = []

              if batch_decision == 'all':
                  # Approve all
                  for action in pending_actions:
                      result = action['func'](*action['args'], **action['kwargs'])
                      results.append(result)
                      approval_results[action['id']] = result
                      print(f"✓ Executed {action['name']}: {result}")

              elif batch_decision == 'none':
                  # Deny all
                  feedback = input("Reason for denying all: ")
                  for action in pending_actions:
                      rejection = f"REJECTED: {feedback}"
                      results.append(rejection)
                      approval_results[action['id']] = rejection
                  print(f"✗ Denied all {len(pending_actions)} actions")

              elif batch_decision.startswith('edit '):
                  # Edit specific action
                  try:
                      idx = int(batch_decision.split()[1]) - 1
                      if 0 <= idx < len(pending_actions):
                          action = pending_actions[idx]
                          print(f"\nEditing action {idx + 1}: {action['name']}")
                          print(f"   Current args: {action['args']}")

                          # Simple example: modify first positional arg
                          if action['args']:
                              new_arg = input(f"   {action['args'][0]} -> ")
                              if new_arg:
                                  modified_args = (new_arg,) + action['args'][1:]
                                  action['args'] = modified_args
                                  print(f"   Updated. Run review_and_execute() again to approve.")
                          return []  # Return empty, user needs to review again
                      else:
                          print(f"Invalid index. Use 1-{len(pending_actions)}")
                          return []
                  except (ValueError, IndexError):
                      print("Invalid edit command. Use 'edit N' where N is the action number.")
                      return []

              else:  # 'review' or default - individual review
                  for i, action in enumerate(pending_actions, 1):
                      print(f"\n--- Action {i}/{len(pending_actions)}: {action['name']} ---")
                      print(f"    Args: {action['args']}")
                      decision = input("    Decision (approve/reject/skip): ").strip().lower()

                      if decision == 'approve':
                          result = action['func'](*action['args'], **action['kwargs'])
                          results.append(result)
                          approval_results[action['id']] = result
                          print(f"    ✓ Executed: {result}")
                      elif decision == 'reject':
                          feedback = input("    Reason: ")
                          rejection = f"REJECTED: {feedback}"
                          results.append(rejection)
                          approval_results[action['id']] = rejection
                          print(f"    ✗ Rejected")
                      else:  # skip
                          skipped = "SKIPPED"
                          results.append(skipped)
                          approval_results[action['id']] = skipped
                          print(f"    - Skipped")

              pending_actions.clear()
              approval_complete['done'] = True  # Signal to wait_for_approvals
              return results

          wrapped_funcs['_review_and_execute'] = review_and_execute
          wrapped_funcs['wait_for_approvals'] = wait_for_approvals
          return wrapped_funcs

      # Usage example - Agent waits for approval
      from agentica import spawn

      def delete_user(user_id: str) -> str:
          return f"Deleted user {user_id}"

      def send_notification(user_id: str, message: str) -> str:
          return f"Sent '{message}' to {user_id}"

      # Create batch-approved versions
      tools = create_batch_approver(
          ('delete_user', delete_user),
          ('send_notification', send_notification)
      )

      agent = await spawn(premise="You are a user admin")

      # Agent queues actions then waits for approval
      await agent.call(
          None,
          """Delete user 123 and notify user 456 about it.
          After queuing these actions, call wait_for_approvals() to get the actual results.""",
          delete_user=tools['delete_user'],
          send_notification=tools['send_notification'],
          wait_for_approvals=tools['wait_for_approvals']
      )
      # What happens:
      # 1. Agent calls delete_user(123) → gets "PENDING_APPROVAL_1"
      # 2. Agent calls send_notification(456, "...") → gets "PENDING_APPROVAL_2"
      # 3. Agent calls wait_for_approvals() → BLOCKS waiting for human
      # 4. Human reviews in another terminal/thread with tools['_review_and_execute']()
      # 5. wait_for_approvals() returns {1: "Deleted user 123", 2: "Sent '...' to 456"}
      # 6. Agent can now see which actions succeeded vs were rejected
      ```

      ```typescript expandable TypeScript theme={null}
      function createBatchApprover(
        funcsWithNames: Array<[string, Function]>
      ): Record<string, Function> {
        const pendingActions: Array<{
          name: string;
          func: Function;
          args: any[];
          id: number;
        }> = [];

        function makeWrapper(name: string, func: Function) {
          return (...args: any[]) => {
            // Collect action for later approval
            const actionId = pendingActions.length + 1;
            pendingActions.push({ name, func, args, id: actionId });
            return `PENDING_APPROVAL_${actionId}`;
          };
        }

        // Create wrapped versions
        const wrappedFuncs: Record<string, Function> = {};
        for (const [name, func] of funcsWithNames) {
          wrappedFuncs[name] = makeWrapper(name, func);
        }

        // Store results for waitForApprovals to return
        const approvalResults: Record<number, any> = {};
        let approvalComplete = false;

        async function waitForApprovals(): Promise<Record<number, any>> {
          console.log('\n[Agent is waiting for approval of pending actions...]');

          // Block until reviewAndExecute is called
          while (!approvalComplete) {
            await new Promise(resolve => setTimeout(resolve, 100));
          }

          // Return results to agent
          approvalComplete = false;  // Reset for next batch
          return { ...approvalResults };
        }

        async function reviewAndExecute(): Promise<any[]> {
          if (pendingActions.length === 0) return [];

          console.log(`\nAgent wants to perform ${pendingActions.length} actions:`);

          // Show all actions
          pendingActions.forEach((action, i) => {
            console.log(`\n${i + 1}. ${action.name}`);
            console.log(`   Args: ${action.args}`);
          });

          // Get batch decision
          console.log(`\nOptions: 'all' (approve all), 'none' (deny all), 'review' (individual), 'edit N' (edit action N)`);
          const batchDecision = (await getInput('Decision: ')).trim().toLowerCase();

          const results: any[] = [];

          if (batchDecision === 'all') {
            // Approve all
            for (const action of pendingActions) {
              const result = await action.func(...action.args);
              results.push(result);
              approvalResults[action.id] = result;
              console.log(`✓ Executed ${action.name}: ${result}`);
            }

          } else if (batchDecision === 'none') {
            // Deny all
            const feedback = await getInput('Reason for denying all: ');
            for (const action of pendingActions) {
              const rejection = `REJECTED: ${feedback}`;
              results.push(rejection);
              approvalResults[action.id] = rejection;
            }
            console.log(`✗ Denied all ${pendingActions.length} actions`);

          } else if (batchDecision.startsWith('edit ')) {
            // Edit specific action
            try {
              const idx = parseInt(batchDecision.split(' ')[1]) - 1;
              if (idx >= 0 && idx < pendingActions.length) {
                const action = pendingActions[idx];
                console.log(`\nEditing action ${idx + 1}: ${action.name}`);
                console.log(`   Current args: ${action.args}`);

                if (action.args.length > 0) {
                  const newArg = await getInput(`   ${action.args[0]} -> `);
                  if (newArg) {
                    action.args = [newArg, ...action.args.slice(1)];
                    console.log('   Updated. Run reviewAndExecute() again to approve.');
                  }
                }
                return [];  // Return empty, user needs to review again
              } else {
                console.log(`Invalid index. Use 1-${pendingActions.length}`);
                return [];
              }
            } catch (e) {
              console.log("Invalid edit command. Use 'edit N' where N is the action number.");
              return [];
            }

          } else {  // 'review' or default - individual review
            for (let i = 0; i < pendingActions.length; i++) {
              const action = pendingActions[i];
              console.log(`\n--- Action ${i + 1}/${pendingActions.length}: ${action.name} ---`);
              console.log(`    Args: ${action.args}`);
              const decision = (await getInput('    Decision (approve/reject/skip): ')).trim().toLowerCase();

              if (decision === 'approve') {
                const result = await action.func(...action.args);
                results.push(result);
                approvalResults[action.id] = result;
                console.log(`    ✓ Executed: ${result}`);
              } else if (decision === 'reject') {
                const feedback = await getInput('    Reason: ');
                const rejection = `REJECTED: ${feedback}`;
                results.push(rejection);
                approvalResults[action.id] = rejection;
                console.log('    ✗ Rejected');
              } else {  // skip
                const skipped = 'SKIPPED';
                results.push(skipped);
                approvalResults[action.id] = skipped;
                console.log('    - Skipped');
              }
            }
          }

          pendingActions.length = 0;  // Clear
          approvalComplete = true;  // Signal to waitForApprovals
          return results;
        }

        wrappedFuncs['_reviewAndExecute'] = reviewAndExecute;
        wrappedFuncs['waitForApprovals'] = waitForApprovals;
        return wrappedFuncs;
      }

      // Usage example - Agent waits for approval
      import { spawn } from '@symbolica/agentica';

      function deleteUser(userId: string): string {
        return `Deleted user ${userId}`;
      }

      function sendNotification(userId: string, message: string): string {
        return `Sent '${message}' to ${userId}`;
      }

      // Create batch-approved versions
      const tools = createBatchApprover([
        ['deleteUser', deleteUser],
        ['sendNotification', sendNotification]
      ]);

      const agent = await spawn({ premise: "You are a user admin" });

      // Agent queues actions then waits for approval
      await agent.call(
        `Delete user 123 and notify user 456 about it.
         After queuing these actions, call waitForApprovals() to get the actual results.`,
        {
          deleteUser: tools['deleteUser'],
          sendNotification: tools['sendNotification'],
          waitForApprovals: tools['waitForApprovals']
        }
      );
      // What happens:
      // 1. Agent calls deleteUser(123) → gets "PENDING_APPROVAL_1"
      // 2. Agent calls sendNotification(456, "...") → gets "PENDING_APPROVAL_2"
      // 3. Agent calls waitForApprovals() → BLOCKS waiting for human
      // 4. Human reviews in another terminal/thread with tools['_reviewAndExecute']()
      // 5. waitForApprovals() returns {1: "Deleted user 123", 2: "Sent '...' to 456"}
      // 6. Agent can now see which actions succeeded vs were rejected
      ```
    </CodeGroup>

    <Note>
      **Key insight**: By providing `wait_for_approvals()` to the agent, the agent can:

      1. Queue up actions (getting placeholders back like `"PENDING_APPROVAL_1"`)
      2. Call `wait_for_approvals()` to block until human review
      3. Receive a dict mapping action IDs to actual results: `{1: "Deleted user 123", 2: "REJECTED: ..."}`
      4. Adapt based on what was approved vs rejected

      The agent can check results like: `if "REJECTED" in results[1]` to handle denied actions differently.

      **Alternative**: If you don't give the agent `wait_for_approvals()`, it will receive placeholders and complete without knowing the real outcomes. Useful when the agent doesn't need to adapt based on approval decisions.

      You can customize this pattern however you want: web UI for review, role-based approvals, risk-level grouping, timeouts -- just code it.
    </Note>

    **Example: Agent adapts based on approval results**

    <CodeGroup>
      ```python Python theme={null}
      # Give agent ability to check what was approved
      agent = await spawn(premise="You are a deployment manager")

      await agent.call(
          str,
          """Deploy services A, B, and C to production.
          After queuing deployments, call wait_for_approvals() to see which were approved.
          For any rejected deployments, log the reason and continue with approved ones.""",
          deploy_service=tools['deploy_service'],
          log_message=tools['log_message'],
          wait_for_approvals=tools['wait_for_approvals']
      )

      # Agent might do:
      # 1. deploy_service("A") → "PENDING_APPROVAL_1"
      # 2. deploy_service("B") → "PENDING_APPROVAL_2"
      # 3. deploy_service("C") → "PENDING_APPROVAL_3"
      # 4. results = wait_for_approvals() → {1: "Deployed A", 2: "REJECTED: staging failed", 3: "Deployed C"}
      # 5. log_message("Services A and C deployed. Service B rejected: staging failed")
      #
      # Agent adapts: it sees B was rejected and logs accordingly!
      ```

      ```typescript TypeScript theme={null}
      // Give agent ability to check what was approved
      const agent = await spawn({ premise: "You are a deployment manager" });

      await agent.call<string>(
        `Deploy services A, B, and C to production.
         After queuing deployments, call waitForApprovals() to see which were approved.
         For any rejected deployments, log the reason and continue with approved ones.`,
        {
          deployService: tools['deployService'],
          logMessage: tools['logMessage'],
          waitForApprovals: tools['waitForApprovals']
        }
      );

      // Agent might do:
      // 1. deployService("A") → "PENDING_APPROVAL_1"
      // 2. deployService("B") → "PENDING_APPROVAL_2"
      // 3. deployService("C") → "PENDING_APPROVAL_3"
      // 4. results = waitForApprovals() → {1: "Deployed A", 2: "REJECTED: staging failed", 3: "Deployed C"}
      // 5. logMessage("Services A and C deployed. Service B rejected: staging failed")
      //
      // Agent adapts: it sees B was rejected and logs accordingly!
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Approval Budget" icon="coins">
    Allow a certain number of operations without approval, then require oversight:

    <CodeGroup>
      ```python Python theme={null}
      class ApprovalBudget:
          def __init__(self, free_operations: int):
              self.remaining = free_operations

          def with_budget(self, func, action_description: str):
              def wrapper(*args, **kwargs):
                  if self.remaining > 0:
                      self.remaining -= 1
                      print(f"✓ Auto-approved ({self.remaining} remaining in budget)")
                      return func(*args, **kwargs)

                  # Budget exhausted, require approval
                  response = input(f"Budget exhausted. Approve '{action_description}'? (yes/no): ")
                  if response.lower() != 'yes':
                      raise PermissionError(f"Denied: {action_description}")

                  return func(*args, **kwargs)
              return wrapper

      # Allow 5 free API calls, then require approval
      budget = ApprovalBudget(free_operations=5)

      make_api_call = budget.with_budget(
          external_api.call,
          "make external API call"
      )
      ```

      ```typescript TypeScript theme={null}
      class ApprovalBudget {
        private remaining: number;

        constructor(freeOperations: number) {
          this.remaining = freeOperations;
        }

        withBudget<T extends (...args: any[]) => any>(
          func: T,
          actionDescription: string
        ): T {
          return (async (...args: any[]) => {
            if (this.remaining > 0) {
              this.remaining--;
              console.log(`✓ Auto-approved (${this.remaining} remaining in budget)`);
              return func(...args);
            }

            // Budget exhausted, require approval
            const rl = readline.createInterface({
              input: process.stdin,
              output: process.stdout
            });

            const answer = await new Promise<string>(resolve => {
              rl.question(`Budget exhausted. Approve '${actionDescription}'? (yes/no): `, resolve);
            });
            rl.close();

            if (answer.toLowerCase() !== 'yes') {
              throw new Error(`Denied: ${actionDescription}`);
            }

            return func(...args);
          }) as T;
        }
      }

      // Allow 5 free API calls, then require approval
      const budget = new ApprovalBudget(5);

      const makeApiCall = budget.withBudget(
        externalApi.call.bind(externalApi),
        "make external API call"
      );
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Time-Based Approval" icon="clock">
    Require approval during business hours, allow automation off-hours:

    <CodeGroup>
      ```python Python theme={null}
      from datetime import datetime

      def with_business_hours_approval(func, action_description: str):
          def wrapper(*args, **kwargs):
              hour = datetime.now().hour
              is_business_hours = 9 <= hour < 17

              if is_business_hours:
                  # During work hours, require approval
                  response = input(f"[Business hours] Approve '{action_description}'? (yes/no): ")
                  if response.lower() != 'yes':
                      raise PermissionError(f"Denied: {action_description}")
              else:
                  # Off hours, auto-approve but log
                  print(f"✓ Auto-approved (off-hours): {action_description}")

              return func(*args, **kwargs)
          return wrapper
      ```

      ```typescript TypeScript theme={null}
      function withBusinessHoursApproval<T extends (...args: any[]) => any>(
        func: T,
        actionDescription: string
      ): T {
        return (async (...args: any[]) => {
          const hour = new Date().getHours();
          const isBusinessHours = hour >= 9 && hour < 17;

          if (isBusinessHours) {
            const rl = readline.createInterface({
              input: process.stdin,
              output: process.stdout
            });

            const answer = await new Promise<string>(resolve => {
              rl.question(`[Business hours] Approve '${actionDescription}'? (yes/no): `, resolve);
            });
            rl.close();

            if (answer.toLowerCase() !== 'yes') {
              throw new Error(`Denied: ${actionDescription}`);
            }
          } else {
            console.log(`✓ Auto-approved (off-hours): ${actionDescription}`);
          }

          return func(...args);
        }) as T;
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Audit Trail" icon="file-lines">
    Log all approval decisions for compliance:

    <CodeGroup>
      ```python Python theme={null}
      import json
      from datetime import datetime

      class AuditedApproval:
          def __init__(self, audit_log_path: str):
              self.audit_log = audit_log_path

          def with_audit(self, func, action_description: str):
              def wrapper(*args, **kwargs):
                  response = input(f"Approve '{action_description}'? (yes/no): ")
                  approved = response.lower() == 'yes'

                  # Log the decision
                  audit_entry = {
                      "timestamp": datetime.now().isoformat(),
                      "action": action_description,
                      "args": str(args),
                      "approved": approved,
                      "approver": os.getenv("USER", "unknown")
                  }

                  with open(self.audit_log, 'a') as f:
                      f.write(json.dumps(audit_entry) + '\n')

                  if not approved:
                      raise PermissionError(f"Denied: {action_description}")

                  result = func(*args, **kwargs)

                  # Log the result
                  result_entry = {
                      **audit_entry,
                      "completed": True,
                      "result": str(result)[:200]  # Truncate long results
                  }
                  with open(self.audit_log, 'a') as f:
                      f.write(json.dumps(result_entry) + '\n')

                  return result
              return wrapper
      ```

      ```typescript TypeScript theme={null}
      import * as fs from 'fs';

      class AuditedApproval {
        constructor(private auditLogPath: string) {}

        withAudit<T extends (...args: any[]) => any>(
          func: T,
          actionDescription: string
        ): T {
          return (async (...args: any[]) => {
            const rl = readline.createInterface({
              input: process.stdin,
              output: process.stdout
            });

            const response = await new Promise<string>(resolve => {
              rl.question(`Approve '${actionDescription}'? (yes/no): `, resolve);
            });
            rl.close();

            const approved = response.toLowerCase() === 'yes';

            // Log the decision
            const auditEntry = {
              timestamp: new Date().toISOString(),
              action: actionDescription,
              args: JSON.stringify(args),
              approved,
              approver: process.env.USER || 'unknown'
            };

            fs.appendFileSync(this.auditLogPath, JSON.stringify(auditEntry) + '\n');

            if (!approved) {
              throw new Error(`Denied: ${actionDescription}`);
            }

            const result = await func(...args);

            // Log the result
            const resultEntry = {
              ...auditEntry,
              completed: true,
              result: String(result).slice(0, 200) // Truncate long results
            };
            fs.appendFileSync(this.auditLogPath, JSON.stringify(resultEntry) + '\n');

            return result;
          }) as T;
        }
      }
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Why This Approach Works

<Note>
  **Key insight:** Agentica SDK agents call real functions, not schema definitions. This means HITL is just wrapping functions -- no special framework features needed.
</Note>

Traditional agent frameworks require special middleware or plugins for human-in-the-loop because they treat tools as schema definitions, not actual code. You configure tools through JSON or decorators, and the framework manages execution.

The Agentica SDK is different: **agents call real functions**. When you pass `delete_file` to an agent, the agent literally calls your `delete_file` function through RPC via Warp. There's no schema layer, no tool registry, no execution middleware.

**This means:**

* **You control execution** -- wrap functions however you want
* **Use any approval mechanism** -- terminal, web, Slack, custom systems
* **Standard programming patterns** -- decorators, higher-order functions, classes
* **No framework lock-in** -- approval logic is in your code, not the framework

You don't need the Agentica SDK to provide **HITL** because you already have everything you need: functions and control flow.

## Best Practices

<AccordionGroup>
  <Accordion title="Make Denial Clear to the Agent" icon="message">
    When denying an action, raise an exception with a clear message. The agent sees this and can adapt:

    <CodeGroup>
      ```python Python theme={null}
      # Good - agent understands why
      raise PermissionError("Deployment denied: must deploy to staging first")

      # Bad - agent gets generic error
      raise Exception("No")
      ```

      ```typescript TypeScript theme={null}
      // Good - agent understands why
      throw new Error("Deployment denied: must deploy to staging first");

      // Bad - agent gets generic error
      throw new Error("No");
      ```
    </CodeGroup>

    The agent might then try an alternative approach based on the error message.
  </Accordion>

  <Accordion title="Different Approval Levels for Different Risks" icon="layer-group">
    Not everything needs the same oversight:

    * **Low risk**: Auto-approve, just log
    * **Medium risk**: CLI approval for development
    * **High risk**: Dashboard with multiple approvers
    * **Critical**: Require manager approval + audit trail
  </Accordion>

  <Accordion title="Make Approval Optional per Environment" icon="code-branch">
    Use environment variables to control approval requirements:

    <CodeGroup>
      ```python Python theme={null}
      def deploy_with_approval(config: DeploymentConfig) -> str:
          # Production always requires approval
          # Other environments only if REQUIRE_APPROVAL=true
          needs_approval = (
              config.environment == "production" or
              os.getenv("REQUIRE_APPROVAL") == "true"
          )

          if needs_approval:
              # ... approval logic
              pass

          return deploy(config)
      ```

      ```typescript TypeScript theme={null}
      function deployWithApproval(config: DeploymentConfig): string {
        const needsApproval = (
          config.environment === 'production' ||
          process.env.REQUIRE_APPROVAL === 'true'
        );

        if (needsApproval) {
          // ... approval logic
        }

        return deploy(config);
      }
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Timeout Approval Requests" icon="hourglass">
    Don't wait forever for approval:

    <CodeGroup>
      ```python Python theme={null}
      import asyncio

      async def with_timeout_approval(func, action_description: str, timeout_seconds: int = 300):
          async def wrapper(*args, **kwargs):
              # Start approval request in background
              approval_task = asyncio.create_task(
                  get_approval_async(action_description, args)
              )

              try:
                  # Wait for approval with timeout
                  approved = await asyncio.wait_for(approval_task, timeout=timeout_seconds)
                  if not approved:
                      raise PermissionError(f"Denied: {action_description}")
              except asyncio.TimeoutError:
                  raise TimeoutError(f"Approval timeout for: {action_description}")

              return func(*args, **kwargs)
          return wrapper
      ```

      ```typescript TypeScript theme={null}
      function withTimeoutApproval<T extends (...args: any[]) => any>(
        func: T,
        actionDescription: string,
        timeoutSeconds: number = 300
      ): T {
        return (async (...args: any[]) => {
          const approvalPromise = getApprovalAsync(actionDescription, args);
          const timeoutPromise = new Promise((_, reject) =>
            setTimeout(() => reject(new Error('Approval timeout')), timeoutSeconds * 1000)
          );

          try {
            const approved = await Promise.race([approvalPromise, timeoutPromise]);
            if (!approved) {
              throw new Error(`Denied: ${actionDescription}`);
            }
          } catch (e) {
            throw new Error(`Approval timeout for: ${actionDescription}`);
          }

          return func(...args);
        }) as T;
      }
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Best Practices" icon="star" href="/guides/best-practices">
    Production-ready patterns and security
  </Card>

  <Card title="Multi-Agent Systems" icon="users" href="/guides/multi-agent-systems">
    Apply HITL in multi-agent workflows
  </Card>

  <Card title="Error Handling" icon="shield" href="/guides/agent-errors">
    Handle approval denials and failures
  </Card>

  <Card title="Examples" icon="code" href="/guides/examples">
    Explore examples & get inspired
  </Card>
</CardGroup>
