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

# Turn-Based Interaction with Agent

> Create a turn-based workflow where the agent updates a GitHub PR with progress and responds to PR comments as prompts.

<Tip>This is an optional extension of the [Running Agents on Sandboxes](/docs/tutorials/running-agents-on-sandboxes) tutorial. Complete the main tutorial first.</Tip>

<Note title="Starter image">
  The starter image is used when you start a devbox without specifying an image, and as the default base when you build a blueprint without providing Dockerfile content. It includes:

  * **Core tools:** jq, sudo
  * **Extras:** dnsutils, iputils-ping, less, vim, rsync,
    gh
  * **Python stack:** Python 3.12, pip, uv
  * **Node stack:** Node 22.15.0, npm, Yarn 1.22.22 via
    corepack
</Note>

## Overview

Create a turn-based system where an AI agent posts status updates to a GitHub pull request and executes tasks based on PR review comments. This enables collaborative workflows where reviewers guide the agent's work just by leaving comments.

<Steps>
  <Step title="Set up the initial PR and post agent status">
    Create a pull request and post an initial status comment from the agent. This establishes the communication channel for turn-based interaction.

    <CodeGroup>
      ```python Python theme={null}
      import os
      from github import Github  # Requires PyGithub library

      # Initialize GitHub client
      g = Github(os.environ["GH_TOKEN"])
      repo = g.get_repo("runloopai/sample-todo-nextjs")

      # Push the branch and create a PR (or use existing PR)
      # For this example, assume we have a PR number
      pr_number = 123  # Replace with your actual PR number
      pr = repo.get_pull(pr_number)

      # Post initial agent status
      initial_comment = """🤖 **Agent Status: Ready**

      I'm ready to help with this PR. I'll monitor this thread for instructions and update you on my progress.

      You can comment with tasks like:
      - "Update the color scheme to use purple accents"
      - "Add error handling to the API routes"
      - "Refactor the component structure"

      I'll respond with my progress and results.
      """
      pr.create_issue_comment(initial_comment)
      print(f"Posted initial status to PR #{pr_number}")
      ```

      ```typescript TypeScript theme={null}
      import { Octokit } from '@octokit/rest';

      // Initialize GitHub client
      const octokit = new Octokit({ auth: process.env.GH_TOKEN });

      // Push the branch and create a PR (or use existing PR)
      // For this example, assume we have a PR number
      const prNumber = 123; // Replace with your actual PR number

      // Post initial agent status
      const initialComment = `🤖 **Agent Status: Ready**

      I'm ready to help with this PR. I'll monitor this thread for instructions and update you on my progress.

      You can comment with tasks like:
      - "Update the color scheme to use purple accents"
      - "Add error handling to the API routes"
      - "Refactor the component structure"

      I'll respond with my progress and results.
      `;

      await octokit.rest.issues.createComment({
        owner: 'runloopai',
        repo: 'sample-todo-nextjs',
        issue_number: prNumber,
        body: initialComment,
      });
      console.log(`Posted initial status to PR #${prNumber}`);
      ```
    </CodeGroup>
  </Step>

  <Step title="Monitor PR comments and process agent tasks">
    Set up a loop that monitors the PR for new comments, processes them as agent prompts, and posts progress updates. Use the `-r` flag to resume the most recent Claude Code conversation across multiple PR comments.

    <CodeGroup>
      ```python Python theme={null}
      import os
      import time
      from github import Github

      g = Github(os.environ["GH_TOKEN"])
      repo = g.get_repo("runloopai/sample-todo-nextjs")
      pr = repo.get_pull(pr_number)

      # Track processed comments to avoid duplicates
      processed_comment_ids = set()

      # Create a named shell and navigate to the todo app directory
      shell = devbox.shell("agent-shell")
      await shell.exec("cd ~/sample-todo-nextjs")

      print("Monitoring PR for comments...")
      # Note: This loop runs indefinitely; in production, add your own exit criteria or lifecycle controls.
      while True:
          # Get all comments on the PR
          comments = pr.get_issue_comments()
          
          for comment in comments:
              # Skip if we've already processed this comment
              if comment.id in processed_comment_ids:
                  continue
              
              # Skip bot comments (including our own)
              if comment.user.type == "Bot":
                  continue
              
              # Mark as processed
              processed_comment_ids.add(comment.id)
              
              # Extract the task from the comment
              task = comment.body.strip()
              commenter = comment.user.login
              
              print(f"New task from @{commenter}: {task}")
              
              # Post "working" status
              working_comment = f"""🤖 **Agent Status: Working**

      Processing task from @{commenter}:
      > {task}

      Starting work now...
      """
              status_comment = pr.create_issue_comment(working_comment)
              
              # Execute the task using Claude Code
              # Use -r flag to resume the most recent session (or start new if none exists)
              result = await shell.exec(f'claude -r -p "{task}"')
              
              output = await result.stdout()
              
              # Post results
              result_comment = f"""✅ **Agent Status: Complete**

      Task from @{commenter}:
      > {task}

      **Results:**
      \`\`\`
      {output[:1000]}
      \`\`\`

      **Next Steps:**
      - Review the changes in the PR
      - Comment with additional tasks or feedback
      - The agent will continue monitoring for new instructions
      """
              pr.create_issue_comment(result_comment)
              
              # Stage and commit the changes (shell maintains working directory)
              await shell.exec("git add .")
              await shell.exec(f'git commit -m "Agent: {task[:50]}"')
              await shell.exec("git push")
              
              print(f"Completed task and updated PR")
          
          # Wait before checking again
          time.sleep(10)  # Check every 10 seconds
      ```

      ```typescript TypeScript theme={null}
      import { Octokit } from '@octokit/rest';

      const octokit = new Octokit({ auth: process.env.GH_TOKEN });

      // Track processed comments to avoid duplicates
      const processedCommentIds = new Set<number>();

      // Create a named shell and navigate to the todo app directory
      const shell = devbox.shell('agent-shell');
      await shell.exec('cd ~/sample-todo-nextjs');

      console.log('Monitoring PR for comments...');
      // Note: This loop runs indefinitely; in production, add your own exit criteria or lifecycle controls.
      while (true) {
        // Get all comments on the PR
        const { data: comments } = await octokit.rest.issues.listComments({
          owner: 'runloopai',
          repo: 'sample-todo-nextjs',
          issue_number: prNumber,
        });
        
        for (const comment of comments) {
          // Skip if we've already processed this comment
          if (processedCommentIds.has(comment.id)) {
            continue;
          }
          
          // Skip bot comments (including our own)
          if (comment.user?.type === 'Bot') {
            continue;
          }
          
          // Mark as processed
          processedCommentIds.add(comment.id);
          
          // Extract the task from the comment
          const task = comment.body?.trim() || '';
          const commenter = comment.user?.login || 'unknown';
          
          console.log(`New task from @${commenter}: ${task}`);
          
          // Post "working" status
          const workingComment = `🤖 **Agent Status: Working**

      Processing task from @${commenter}:
      > ${task}

      Starting work now...
      `;
          await octokit.rest.issues.createComment({
            owner: 'runloopai',
            repo: 'sample-todo-nextjs',
            issue_number: prNumber,
            body: workingComment,
          });
          
          // Execute the task using Claude Code
          // Use -r flag to resume the most recent session (or start new if none exists)
          const result = await shell.exec(`claude -r -p "${task}"`);
          
          const output = await result.stdout();
          
          // Post results
          const resultComment = `✅ **Agent Status: Complete**

      Task from @${commenter}:
      > ${task}

      **Results:**
      \`\`\`
      ${output.substring(0, 1000)}
      \`\`\`

      **Next Steps:**
      - Review the changes in the PR
      - Comment with additional tasks or feedback
      - The agent will continue monitoring for new instructions
      `;
          await octokit.rest.issues.createComment({
            owner: 'runloopai',
            repo: 'sample-todo-nextjs',
            issue_number: prNumber,
            body: resultComment,
          });
          
          // Stage and commit the changes (shell maintains working directory)
          await shell.exec('git add .');
          await shell.exec(`git commit -m "Agent: ${task.substring(0, 50)}"`);
          await shell.exec('git push');
          
          console.log('Completed task and updated PR');
        }
        
        // Wait before checking again
        await new Promise(resolve => setTimeout(resolve, 10000)); // Check every 10 seconds
      }
      ```
    </CodeGroup>

    <Note>
      **Named Shells**: We use a named shell (`devbox.shell("agent-shell")`) to maintain the working directory state across commands. After the initial `cd ~/sample-todo-nextjs`, all subsequent commands run in that directory without needing to `cd` each time. Learn more about [named shells](/docs/devboxes/named-shells).

      **Session Resumption**: The `-r` flag without a session ID will resume the most recent Claude Code conversation. On the first call, it starts a new session, and on subsequent calls, it automatically resumes the previous conversation, maintaining context across multiple PR comments.

      **Production Webhooks**: In production, you should use GitHub webhooks to receive real-time notifications when comments are added, rather than polling. This polling approach is shown for simplicity, but webhooks are more efficient and responsive.
    </Note>
  </Step>

  <Step title="Add error handling and status updates">
    Enhance the workflow with better error handling and more detailed status updates to keep reviewers informed.

    <CodeGroup>
      ```python Python theme={null}
      import os
      import time
      from github import Github

      g = Github(os.environ["GH_TOKEN"])
      repo = g.get_repo("runloopai/sample-todo-nextjs")
      pr = repo.get_pull(pr_number)

      processed_comment_ids = set()

      # Create a named shell and navigate to the todo app directory
      shell = devbox.shell("agent-shell")
      await shell.exec("cd ~/sample-todo-nextjs")

      while True:
          comments = pr.get_issue_comments()
          
          for comment in comments:
              if comment.id in processed_comment_ids or comment.user.type == "Bot":
                  continue
              
              processed_comment_ids.add(comment.id)
              task = comment.body.strip()
              commenter = comment.user.login
              
              # Post working status
              working_comment = f"""🤖 **Agent Status: Working**

      Processing task from @{commenter}:
      > {task}

      Starting work now...
      """
              pr.create_issue_comment(working_comment)
              
              try:
                  # Execute the task using Claude Code
                  # Use -r flag to resume the most recent session
                  result = await shell.exec(f'claude -r -p "{task}"')
                  
                  output = await result.stdout()
                  exit_code = result.exit_code
                  
                  if exit_code == 0:
                      # Success - post results
                      result_comment = f"""✅ **Agent Status: Complete**

      Task from @{commenter}:
      > {task}

      **Results:**
      \`\`\`
      {output[:1500]}
      \`\`\`

      Changes have been committed and pushed to this PR.
      """
                      pr.create_issue_comment(result_comment)
                      
                      # Commit changes (shell maintains working directory)
                      await shell.exec("git add .")
                      await shell.exec(f'git commit -m "Agent: {task[:50]}"')
                      await shell.exec("git push")
                  else:
                      # Error - post failure message
                      error_comment = f"""❌ **Agent Status: Error**

      Task from @{commenter}:
      > {task}

      **Error:**
      The agent encountered an error while processing this task.

      **Output:**
      \`\`\`
      {output[:1500]}
      \`\`\`

      Please review the error and provide additional guidance or clarification.
      """
                      pr.create_issue_comment(error_comment)
                      
              except Exception as e:
                  # Exception handling
                  error_comment = f"""❌ **Agent Status: Exception**

      Task from @{commenter}:
      > {task}

      **Error:**
      An exception occurred: {str(e)}

      Please check the agent logs for more details.
      """
                  pr.create_issue_comment(error_comment)
          
          time.sleep(10)
      ```

      ```typescript TypeScript theme={null}
      import { Octokit } from '@octokit/rest';

      const octokit = new Octokit({ auth: process.env.GH_TOKEN });
      const processedCommentIds = new Set<number>();

      // Create a named shell and navigate to the todo app directory
      const shell = devbox.shell('agent-shell');
      await shell.exec('cd ~/sample-todo-nextjs');

      while (true) {
        const { data: comments } = await octokit.rest.issues.listComments({
          owner: 'runloopai',
          repo: 'sample-todo-nextjs',
          issue_number: prNumber,
        });
        
        for (const comment of comments) {
          if (processedCommentIds.has(comment.id) || comment.user?.type === 'Bot') {
            continue;
          }
          
          processedCommentIds.add(comment.id);
          const task = comment.body?.trim() || '';
          const commenter = comment.user?.login || 'unknown';
          
          // Post working status
          const workingComment = `🤖 **Agent Status: Working**

      Processing task from @${commenter}:
      > ${task}

      Starting work now...
      `;
          await octokit.rest.issues.createComment({
            owner: 'runloopai',
            repo: 'sample-todo-nextjs',
            issue_number: prNumber,
            body: workingComment,
          });
          
          try {
            // Execute the task using Claude Code
            // Use -r flag to resume the most recent session
            const result = await shell.exec(`claude -r -p "${task}"`);
            
            const output = await result.stdout();
            const exitCode = result.exitCode;
            
            if (exitCode === 0) {
              // Success - post results
              const resultComment = `✅ **Agent Status: Complete**

      Task from @${commenter}:
      > ${task}

      **Results:**
      \`\`\`
      ${output.substring(0, 1500)}
      \`\`\`

      Changes have been committed and pushed to this PR.
      `;
              await octokit.rest.issues.createComment({
                owner: 'runloopai',
                repo: 'sample-todo-nextjs',
                issue_number: prNumber,
                body: resultComment,
              });
              
              // Commit changes (shell maintains working directory)
              await shell.exec('git add .');
              await shell.exec(`git commit -m "Agent: ${task.substring(0, 50)}"`);
              await shell.exec('git push');
            } else {
              // Error - post failure message
              const errorComment = `❌ **Agent Status: Error**

      Task from @${commenter}:
      > ${task}

      **Error:**
      The agent encountered an error while processing this task.

      **Output:**
      \`\`\`
      ${output.substring(0, 1500)}
      \`\`\`

      Please review the error and provide additional guidance or clarification.
      `;
              await octokit.rest.issues.createComment({
                owner: 'runloopai',
                repo: 'sample-todo-nextjs',
                issue_number: prNumber,
                body: errorComment,
              });
            }
          } catch (error) {
            // Exception handling
            const errorComment = `❌ **Agent Status: Exception**

      Task from @${commenter}:
      > ${task}

      **Error:**
      An exception occurred: ${error instanceof Error ? error.message : String(error)}

      Please check the agent logs for more details.
      `;
            await octokit.rest.issues.createComment({
              owner: 'runloopai',
              repo: 'sample-todo-nextjs',
              issue_number: prNumber,
              body: errorComment,
            });
          }
        }
        
        await new Promise(resolve => setTimeout(resolve, 10000));
      }
      ```
    </CodeGroup>
  </Step>

  <Step title="Add command filtering and special instructions">
    Add support for special commands and filtering to make the interaction more controlled and useful.

    <CodeGroup>
      ```python Python theme={null}
      # Helper function to check if comment is a command
      def is_command(comment_body):
          commands = ["/status", "/stop", "/help"]
          return any(comment_body.strip().startswith(cmd) for cmd in commands)

      async def handle_command(command, pr, shell):
          if command == "/status":
              # Get git status (shell maintains working directory)
              status_result = await shell.exec("git status")
              status_output = await status_result.stdout()
              
              status_comment = f"""📊 **Agent Status Report**

      **Git Status:**
      \`\`\`
      {status_output}
      \`\`\`
      """
              pr.create_issue_comment(status_comment)
              return True
          
          elif command == "/stop":
              stop_comment = """🛑 **Agent Status: Stopped**

      The agent has stopped monitoring this PR. To resume, post a new comment with a task.
      """
              pr.create_issue_comment(stop_comment)
              return False  # Signal to stop monitoring
          
          elif command == "/help":
              help_comment = """ℹ️ **Agent Commands**

      Available commands:
      - `/status` - Show current git status
      - `/stop` - Stop the agent from monitoring this PR
      - `/help` - Show this help message

      To give the agent a task, simply comment with your instruction (no command prefix needed).
      """
              pr.create_issue_comment(help_comment)
              return True
          
          return True

      # In the main loop, check for commands first
      for comment in comments:
          if comment.id in processed_comment_ids or comment.user.type == "Bot":
              continue
          
          processed_comment_ids.add(comment.id)
          comment_body = comment.body.strip()
          
          if is_command(comment_body):
              should_continue = await handle_command(comment_body, pr, shell)
              if not should_continue:
                  break  # Stop monitoring
              continue
          
          # Process as regular task...
          # (rest of the task processing code)
      ```

      ```typescript TypeScript theme={null}
      // Helper function to check if comment is a command
      function isCommand(commentBody: string): boolean {
        const commands = ['/status', '/stop', '/help'];
        return commands.some(cmd => commentBody.trim().startsWith(cmd));
      }

      async function handleCommand(
        command: string,
        prNumber: number,
        shell: any
      ): Promise<boolean> {
        // Returns true to keep monitoring, or false to stop (for example, when handling `/stop`).
        if (command === '/status') {
          // Get git status (shell maintains working directory)
          const statusResult = await shell.exec('git status');
          const statusOutput = await statusResult.stdout();
          
          const statusComment = `📊 **Agent Status Report**

      **Git Status:**
      \`\`\`
      ${statusOutput}
      \`\`\`
      `;
          await octokit.rest.issues.createComment({
            owner: 'runloopai',
            repo: 'sample-todo-nextjs',
            issue_number: prNumber,
            body: statusComment,
          });
          return true;
        } else if (command === '/stop') {
          const stopComment = `🛑 **Agent Status: Stopped**

      The agent has stopped monitoring this PR. To resume, post a new comment with a task.
      `;
          await octokit.rest.issues.createComment({
            owner: 'runloopai',
            repo: 'sample-todo-nextjs',
            issue_number: prNumber,
            body: stopComment,
          });
          return false; // Signal to stop monitoring
        } else if (command === '/help') {
          const helpComment = `ℹ️ **Agent Commands**

      Available commands:
      - \`/status\` - Show current git status
      - \`/stop\` - Stop the agent from monitoring this PR
      - \`/help\` - Show this help message

      To give the agent a task, simply comment with your instruction (no command prefix needed).
      `;
          await octokit.rest.issues.createComment({
            owner: 'runloopai',
            repo: 'sample-todo-nextjs',
            issue_number: prNumber,
            body: helpComment,
          });
          return true;
        }
        return true;
      }

      // In the main loop, check for commands first
      for (const comment of comments) {
        if (processedCommentIds.has(comment.id) || comment.user?.type === 'Bot') {
          continue;
        }
        
        processedCommentIds.add(comment.id);
        const commentBody = comment.body?.trim() || '';
        
        if (isCommand(commentBody)) {
          const shouldContinue = await handleCommand(commentBody, prNumber, shell);
          if (!shouldContinue) {
            break; // Stop monitoring
          }
          continue;
        }
        
        // Process as regular task...
        // (rest of the task processing code)
      }
      ```
    </CodeGroup>
  </Step>
</Steps>

## Best Practices

* **Use webhooks in production**: Replace polling with GitHub webhooks for real-time notifications
* **Rate limiting**: Be mindful of GitHub API rate limits when polling frequently; authenticating with a GitHub token or GitHub App increases your available quota
* **Error recovery**: Implement retry logic for transient failures
* **Security**: Validate and sanitize user input and secrets before passing them to the agent
* **Logging**: Keep detailed logs of agent actions for debugging and auditing
* **Session management**: Use `claude -r -p "query"` to resume the most recent conversation. The `-r` flag without a session ID automatically resumes the most recent session, maintaining context across multiple PR comments
* **Claude Code flags**:
  * Use `-p` flag for print mode (non-interactive, SDK usage)
  * Use `-r` or `--resume` without a session ID to resume the most recent session
  * Use `-r "<session-id>"` or `--resume "<session-id>"` to resume a specific session by ID
  * Use `-c` to continue the most recent conversation in the current directory
  * For more production examples of webhook-based workflows and secret management, see the [Runloop tutorials](/docs/tutorials/quickstart) and [Account Secrets](/docs/devboxes/configuration/account-secrets) documentation.

## Next Steps

* Learn how to [share a live preview](/docs/tutorials/running-agents-on-sandboxes/share-live-preview) of your changes
* Explore [suspend and resume workflows](/docs/tutorials/running-agents-on-sandboxes/suspend-resume-workflow) for long-running interactions
* Check out [GitHub webhooks documentation](https://docs.github.com/en/webhooks) for production-ready implementations
