Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 6 additions & 20 deletions .github/workflows/stale-bot.yml
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
# .github/workflows/stale-issue-auditor.yml

# Best Practice: Always have a 'name' field at the top.
name: ADK Stale Issue Auditor

# The 'on' block defines the triggers.
on:
# The 'workflow_dispatch' trigger allows manual runs.
workflow_dispatch:

# The 'schedule' trigger runs the bot on a timer.
schedule:
# This runs at 6:00 AM UTC (e.g., 10 PM PST).
# This runs at 6:00 AM UTC (10 PM PST)
- cron: '0 6 * * *'

# The 'jobs' block contains the work to be done.
jobs:
# A unique ID for the job.
audit-stale-issues:
# The runner environment.
runs-on: ubuntu-latest
timeout-minutes: 60

# Permissions for the job's temporary GITHUB_TOKEN.
# These are standard and syntactically correct.
permissions:
issues: write
contents: read

# The sequence of steps for the job.
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.11'

- name: Install dependencies
# The '|' character allows for multi-line shell commands.
run: |
python -m pip install --upgrade pip
pip install requests google-adk
- name: Run Auditor Agent Script
# The 'env' block for setting environment variables.
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OWNER: google
OWNER: ${{ github.repository_owner }}
REPO: adk-python
ISSUES_PER_RUN: 100
CONCURRENCY_LIMIT: 3
LLM_MODEL_NAME: "gemini-2.5-flash"
PYTHONPATH: contributing/samples

# The final 'run' command.
run: python -m adk_stale_agent.main
100 changes: 64 additions & 36 deletions contributing/samples/adk_stale_agent/PROMPT_INSTRUCTION.txt
Original file line number Diff line number Diff line change
@@ -1,40 +1,68 @@
You are a highly intelligent and transparent repository auditor for '{OWNER}/{REPO}'.
Your job is to analyze all open issues and report on your findings before taking any action.
You are a highly intelligent repository auditor for '{OWNER}/{REPO}'.
Your job is to analyze a specific issue and report findings before taking action.

**Primary Directive:** Ignore any events from users ending in `[bot]`.
**Reporting Directive:** For EVERY issue you analyze, you MUST output a concise, human-readable summary, starting with "Analysis for Issue #[number]:".
**Reporting Directive:** Output a concise summary starting with "Analysis for Issue #[number]:".

**THRESHOLDS:**
- Stale Threshold: {stale_threshold_days} days.
- Close Threshold: {close_threshold_days} days.

**WORKFLOW:**
1. **Context Gathering**: Call `get_repository_maintainers` and `get_all_open_issues`.
2. **Per-Issue Analysis**: For each issue, call `get_issue_state`, passing in the maintainers list.
3. **Decision & Reporting**: Based on the summary from `get_issue_state`, follow this strict decision tree in order.

--- **DECISION TREE & REPORTING TEMPLATES** ---

**STEP 1: CHECK FOR ACTIVITY (IS THE ISSUE ACTIVE?)**
- **Condition**: Was the last human action NOT from a maintainer? (i.e., `last_human_commenter_is_maintainer` is `False`).
- **Action**: The author or a third party has acted. The issue is ACTIVE.
- **Report and Action**: If '{STALE_LABEL_NAME}' is present, report: "Analysis for Issue #[number]: Issue is ACTIVE. The last action was a [action type] by a non-maintainer. To get the [action type], you MUST use the value from the 'last_human_action_type' field in the summary you received from the tool." Action: Removing stale label and then call `remove_label_from_issue` with the label name '{STALE_LABEL_NAME}'. Otherwise, report: "Analysis for Issue #[number]: Issue is ACTIVE. No stale label to remove. Action: None."
- **If this condition is met, stop processing this issue.**

**STEP 2: IF PENDING, MANAGE THE STALE LIFECYCLE.**
- **Condition**: The last human action WAS from a maintainer (`last_human_commenter_is_maintainer` is `True`). The issue is PENDING.
- **Action**: You must now determine the correct state.

- **First, check if the issue is already STALE.**
- **Condition**: Is the `'{STALE_LABEL_NAME}'` label present in `current_labels`?
- **Action**: The issue is STALE. Your only job is to check if it should be closed.
- **Get Time Difference**: Call `calculate_time_difference` with the `stale_label_applied_at` timestamp.
- **Decision & Report**: If `hours_passed` > **{CLOSE_HOURS_AFTER_STALE_THRESHOLD}**: Report "Analysis for Issue #[number]: STALE. Close threshold met ({CLOSE_HOURS_AFTER_STALE_THRESHOLD} hours) with no author activity." Action: Closing issue and then call `close_as_stale`. Otherwise, report "Analysis for Issue #[number]: STALE. Close threshold not yet met. Action: None."

- **ELSE (the issue is PENDING but not yet stale):**
- **Analyze Intent**: Semantically analyze the `last_maintainer_comment_text`. Is it either a question, a request for information, a suggestion, or a request for changes?
- **If YES (it is either a question, a request for information, a suggestion, or a request for changes)**:
- **CRITICAL CHECK**: Now, you must verify the author has not already responded. Compare the `last_author_event_time` and the `last_maintainer_comment_time`.
- **IF the author has NOT responded** (i.e., `last_author_event_time` is older than `last_maintainer_comment_time` or is null):
- **Get Time Difference**: Call `calculate_time_difference` with the `last_maintainer_comment_time`.
- **Decision & Report**: If `hours_passed` > **{STALE_HOURS_THRESHOLD}**: Report "Analysis for Issue #[number]: PENDING. Stale threshold met ({STALE_HOURS_THRESHOLD} hours)." Action: Marking as stale and then call `add_stale_label_and_comment` and if label name '{REQUEST_CLARIFICATION_LABEL}' is missing then call `add_label_to_issue` with the label name '{REQUEST_CLARIFICATION_LABEL}'. Otherwise, report: "Analysis for Issue #[number]: PENDING. Stale threshold not met. Action: None."
- **ELSE (the author HAS responded)**:
- **Report**: "Analysis for Issue #[number]: PENDING, but author has already responded to the last maintainer request. Action: None."
- **If NO (it is not a request):**
- **Report**: "Analysis for Issue #[number]: PENDING. Maintainer's last comment was not a request. Action: None."
1. **Context Gathering**: Call `get_issue_state`.
2. **Decision**: Follow this strict decision tree using the data returned by the tool.

--- **DECISION TREE** ---

**STEP 1: CHECK IF ALREADY STALE**
- **Condition**: Is `is_stale` (from tool) **True**?
- **Action**:
- **Check Role**: Look at `last_action_role`.

- **IF 'author' OR 'other_user'**:
- **Context**: The user has responded. The issue is now ACTIVE.
- **Action 1**: Call `remove_label_from_issue` with '{STALE_LABEL_NAME}'.
- **Action 2 (ALERT CHECK)**: Look at `maintainer_alert_needed`.
- **IF True**: User edited description silently.
-> **Action**: Call `alert_maintainer_of_edit`.
- **IF False**: User commented normally. No alert needed.
- **Report**: "Analysis for Issue #[number]: ACTIVE. User activity detected. Removed stale label."

- **IF 'maintainer'**:
- **Check Time**: Check `days_since_stale_label`.
- **If `days_since_stale_label` > {close_threshold_days}**:
- **Action**: Call `close_as_stale`.
- **Report**: "Analysis for Issue #[number]: STALE. Close threshold met. Closing."
- **Else**:
- **Report**: "Analysis for Issue #[number]: STALE. Waiting for close threshold. No action."

**STEP 2: CHECK IF ACTIVE (NOT STALE)**
- **Condition**: `is_stale` is **False**.
- **Action**:
- **Check Role**: If `last_action_role` is 'author' or 'other_user':
- **Context**: The issue is Active.
- **Action (ALERT CHECK)**: Look at `maintainer_alert_needed`.
- **IF True**: The user edited the description silently, and we haven't alerted yet.
-> **Action**: Call `alert_maintainer_of_edit`.
-> **Report**: "Analysis for Issue #[number]: ACTIVE. Silent update detected (Description Edit). Alerted maintainer."
- **IF False**:
-> **Report**: "Analysis for Issue #[number]: ACTIVE. Last action was by user. No action."

- **Check Role**: If `last_action_role` is 'maintainer':
- **Proceed to STEP 3.**

**STEP 3: ANALYZE MAINTAINER INTENT**
- **Context**: The last person to act was a Maintainer.
- **Action**: Read the text in `last_comment_text`.
- **Question Check**: Does the text ask a question, request clarification, ask for logs, or suggest trying a fix?
- **Time Check**: Is `days_since_activity` > {stale_threshold_days}?

- **DECISION**:
- **IF (Question == YES) AND (Time == YES)**:
- **Action**: Call `add_stale_label_and_comment`.
- **Check**: If '{REQUEST_CLARIFICATION_LABEL}' is not in `current_labels`, call `add_label_to_issue` for it.
- **Report**: "Analysis for Issue #[number]: STALE. Maintainer asked question [days_since_activity] days ago. Marking stale."
- **IF (Question == YES) BUT (Time == NO)**:
- **Report**: "Analysis for Issue #[number]: PENDING. Maintainer asked question, but threshold not met yet. No action."
- **IF (Question == NO)** (e.g., "I am working on this"):
- **Report**: "Analysis for Issue #[number]: ACTIVE. Maintainer gave status update (not a question). No action."
86 changes: 55 additions & 31 deletions contributing/samples/adk_stale_agent/README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,89 @@
# ADK Stale Issue Auditor Agent

This directory contains an autonomous agent designed to audit a GitHub repository for stale issues, helping to maintain repository hygiene and ensure that all open items are actionable.
This directory contains an autonomous, **GraphQL-powered** agent designed to audit a GitHub repository for stale issues. It maintains repository hygiene by ensuring all open items are actionable and responsive.

The agent operates as a "Repository Auditor," proactively scanning all open issues rather than waiting for a specific trigger. It uses a combination of deterministic Python tools and the semantic understanding of a Large Language Model (LLM) to make intelligent decisions about the state of a conversation.
Unlike traditional "Stale Bots" that only look at timestamps, this agent uses a **Unified History Trace** and an **LLM (Large Language Model)** to understand the *context* of a conversation. It distinguishes between a maintainer asking a question (stale candidate) vs. a maintainer providing a status update (active).

---

## Core Logic & Features

The agent's primary goal is to identify issues where a maintainer has requested information from the author, and to manage the lifecycle of that issue based on the author's response (or lack thereof).
The agent operates as a "Repository Auditor," proactively scanning open issues using a high-efficiency decision tree.

**The agent follows a precise decision tree:**
### 1. Smart State Verification (GraphQL)
Instead of making multiple expensive API calls, the agent uses a single **GraphQL** query per issue to reconstruct the entire history of the conversation. It combines:
* **Comments**
* **Description/Body Edits** ("Ghost Edits")
* **Title Renames**
* **State Changes** (Reopens)

1. **Audits All Open Issues:** On each run, the agent fetches a batch of the oldest open issues in the repository.
2. **Identifies Pending Issues:** It analyzes the full timeline of each issue to see if the last human action was a comment from a repository maintainer.
3. **Semantic Intent Analysis:** If the last comment was from a maintainer, the agent uses the LLM to determine if the comment was a **question or a request for clarification**.
4. **Marks as Stale:** If the maintainer's question has gone unanswered by the author for a configurable period (e.g., 7 days), the agent will:
* Apply a `stale` label to the issue.
* Post a comment notifying the author that the issue is now considered stale and will be closed if no further action is taken.
* Proactively add a `request clarification` label if it's missing, to make the issue's state clear.
5. **Handles Activity:** If any non-maintainer (the author or a third party) comments on an issue, the agent will automatically remove the `stale` label, marking the issue as active again.
6. **Closes Stale Issues:** If an issue remains in the `stale` state for another configurable period (e.g., 7 days) with no new activity, the agent will post a final comment and close the issue.
It sorts these events chronologically to determine the **Last Active Actor**.

### Self-Configuration
### 2. The "Last Actor" Rule
The agent follows a precise logic flow based on who acted last:

A key feature of this agent is its ability to self-configure. It does not require a hard-coded list of maintainer usernames. On each run, it uses the GitHub API to dynamically fetch the list of users with write access to the repository, ensuring its logic is always based on the current team.
* **If Author/User acted last:** The issue is **ACTIVE**.
* This includes comments, title changes, and *silent* description edits.
* **Action:** The agent immediately removes the `stale` label.
* **Silent Update Alert:** If the user edited the description but *did not* comment, the agent posts a specific alert: *"Notification: The author has updated the issue description..."* to ensure maintainers are notified (since GitHub does not trigger notifications for body edits).
* **Spam Prevention:** The agent checks if it has already alerted about a specific silent edit to avoid spamming the thread.

* **If Maintainer acted last:** The issue is **POTENTIALLY STALE**.
* The agent passes the text of the maintainer's last comment to the LLM.

### 3. Semantic Intent Analysis (LLM)
If the maintainer was the last person to speak, the LLM analyzes the comment text to determine intent:
* **Question/Request:** "Can you provide logs?" / "Please try v2.0."
* **Verdict:** **STALE** (Waiting on Author).
* **Action:** If the time threshold is met, the agent adds the `stale` label. It also checks for the `request clarification` label and adds it if missing.
* **Status Update:** "We are working on a fix." / "Added to backlog."
* **Verdict:** **ACTIVE** (Waiting on Maintainer).
* **Action:** No action taken. The issue remains open without stale labels.

### 4. Lifecycle Management
* **Marking Stale:** After `STALE_HOURS_THRESHOLD` (default: 7 days) of inactivity following a maintainer's question.
* **Closing:** After `CLOSE_HOURS_AFTER_STALE_THRESHOLD` (default: 7 days) of continued inactivity while marked stale.

---

## Performance & Safety

* **GraphQL Optimized:** Fetches comments, edits, labels, and timeline events in a single network request to minimize latency and API quota usage.
* **Search API Filtering:** Uses the GitHub Search API to pre-filter issues created recently, ensuring the bot doesn't waste cycles analyzing brand-new issues.
* **Rate Limit Aware:** Includes intelligent sleeping and retry logic (exponential backoff) to handle GitHub API rate limits (HTTP 429) gracefully.
* **Execution Metrics:** Logs the time taken and API calls consumed for every issue processed.

---

## Configuration

The agent is configured entirely via environment variables, which should be set as secrets in the GitHub Actions workflow environment.
The agent is configured via environment variables, typically set as secrets in GitHub Actions.

### Required Secrets

| Secret Name | Description |
| :--- | :--- |
| `GITHUB_TOKEN` | A GitHub Personal Access Token (PAT) with the required permissions. It's recommended to use a PAT from a dedicated "bot" account.
| `GOOGLE_API_KEY` | An API key for the Google AI (Gemini) model used for the agent's reasoning.

### Required PAT Permissions

The `GITHUB_TOKEN` requires the following **Repository Permissions**:
* **Issues**: `Read & write` (to read issues, add labels, comment, and close)
* **Administration**: `Read-only` (to read the list of repository collaborators/maintainers)
| `GITHUB_TOKEN` | A GitHub Personal Access Token (PAT) or Service Account Token with `repo` scope. |
| `GOOGLE_API_KEY` | An API key for the Google AI (Gemini) model used for reasoning. |

### Optional Configuration

These environment variables can be set in the workflow file to override the defaults in `settings.py`.
These variables control the timing thresholds and model selection.

| Variable Name | Description | Default |
| :--- | :--- | :--- |
| `STALE_HOURS_THRESHOLD` | The number of hours of inactivity after a maintainer's question before an issue is marked as `stale`. | `168` (7 days) |
| `CLOSE_HOURS_AFTER_STALE_THRESHOLD` | The number of hours after being marked `stale` before an issue is closed. | `168` (7 days) |
| `ISSUES_PER_RUN` | The maximum number of oldest open issues to process in a single workflow run. | `100` |
| `LLM_MODEL_NAME`| LLM model to use. | `gemini-2.5-flash` |
| `STALE_HOURS_THRESHOLD` | Hours of inactivity after a maintainer's question before marking as `stale`. | `168` (7 days) |
| `CLOSE_HOURS_AFTER_STALE_THRESHOLD` | Hours after being marked `stale` before the issue is closed. | `168` (7 days) |
| `LLM_MODEL_NAME`| The specific Gemini model version to use. | `gemini-2.5-flash` |
| `OWNER` | Repository owner (auto-detected in Actions). | (Environment dependent) |
| `REPO` | Repository name (auto-detected in Actions). | (Environment dependent) |

---

## Deployment

To deploy this agent, a GitHub Actions workflow file (`.github/workflows/stale-bot.yml`) is included. This workflow runs on a daily schedule and executes the agent's main script.
To deploy this agent, a GitHub Actions workflow file (`.github/workflows/stale-bot.yml`) is recommended.

### Directory Structure Note
Because this agent resides within the `adk-python` package structure, the workflow must ensure the script is executed correctly to handle imports.

Ensure the necessary repository secrets are configured and the `stale` and `request clarification` labels exist in the repository.
Loading