Files
Rohit Yanamadala cb19d0714c fix: Optimize Stale Agent with GraphQL and Search API to resolve 429 Quota errors
Merge https://github.com/google/adk-python/pull/3700

### Description
This PR refactors the `adk_stale_agent` to address `429 RESOURCE_EXHAUSTED` errors encountered during workflow execution. The previous implementation was inefficient in fetching issue history (using pagination over the REST API) and lacked server-side filtering, causing excessive API calls and huge token consumption that breached Gemini API quotas.

The new implementation switches to a **GraphQL-first approach**, implements server-side filtering via the Search API, adds robust concurrency controls, and significantly improves code maintainability through modular refactoring.

### Root Cause of Failure
The previous workflow failed with the following error due to passing too much context to the LLM and processing too many irrelevant issues:
```text
google.genai.errors.ClientError: 429 RESOURCE_EXHAUSTED.
Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_paid_tier_input_token_count
```
### Key Changes

#### 1. Optimization: REST → GraphQL (`agent.py`)
*   **Old:** Fetched issue comments and timeline events using multiple paginated REST API calls (`/timeline`).
*   **New:** Implemented `get_issue_state` using a single **GraphQL** query. This fetches comments, `userContentEdits`, and specific timeline events (Labels, Renames) in one network request.
*   **Refactoring:** The complex analysis logic has been decomposed into focused helper functions (_fetch_graphql_data, _build_history_timeline, _replay_history_to_find_state) for better readability and testing.
*   **Configurable:** Added GRAPHQL_COMMENT_LIMIT and GRAPHQL_TIMELINE_LIMIT settings to tune context depth
*   **Impact:** Drastically reduces the data payload size and eliminates multiple API round-trips, significantly lowering the token count sent to the LLM.

#### 2. Optimization: Server-Side Filtering (`utils.py`)
*   **Old:** Fetched *all* open issues via REST and filtered them in Python memory.
*   **New:** Uses the GitHub Search API (`get_old_open_issue_numbers`) with `created:<DATE` syntax.
*   **Impact:** Only fetches issue numbers that actually meet the age threshold, preventing the agent from wasting cycles and tokens on brand-new issues.

#### 3. Concurrency & Rate Limiting (`main.py` & `settings.py`)
*   **Old:** Sequential execution loop.
*   **New:** Implemented `asyncio.gather` with a configurable `CONCURRENCY_LIMIT` (set to 3).
*   **New:** Added `urllib3` retry strategies (exponential backoff) in `utils.py` to handle GitHub API rate limits (HTTP 429) gracefully.

#### 4. Logic Improvements ("Ghost Edits")
*   **New Feature:** The agent now detects "Ghost Edits" (where an author updates the issue description without posting a new comment).
*   **Action:** If a silent edit is detected on a stale candidate, the agent now alerts maintainers instead of marking it stale, preventing false positives.

### File Comparison Summary

| File | Change |
| :--- | :--- |
| `main.py` | Switched from `InMemoryRunner` loop to `asyncio` chunked processing. Added execution timing and API usage logging. |
| `agent.py` | Replaced REST logic with GraphQL query. Added logic to handle silent body edits. Decomposed giant get_issue_state into helper functions with docstrings. Added _format_days helper. |
| `utils.py` | Added `HTTPAdapter` with Retries. Added `get_old_open_issue_numbers` using Search API. |
| `settings.py` | Removed `ISSUES_PER_RUN`; added configuration for CONCURRENCY_LIMIT, SLEEP_BETWEEN_CHUNKS, and GraphQL limits. |
| `PROMPT_INSTRUCTIONS.txt` | Simplified decision tree; removed date calculation responsibility from LLM. |

### Verification
The new logic minimizes token usage by offloading date calculations to Python and strictly limiting the context passed to the LLM to semantic intent analysis (e.g., "Is this a question?").

*   **Metric Check:** The workflow now tracks API calls per issue to ensure we stay within limits.
*   **Safety:** Silent edits by users now correctly reset the "Stale" timer.
*   **Maintainability:** All complex logic is now isolated in typed helper functions with comprehensive docstrings.

Co-authored-by: Xuan Yang <xygoogle@google.com>
COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/3700 from ryanaiagent:feat/improve-stale-agent 888064eff125ae74f7c3a9ad6c74f98de80243a2
PiperOrigin-RevId: 838885530
2025-12-01 12:25:51 -08:00

4.7 KiB

ADK Stale Issue Auditor Agent

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.

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 operates as a "Repository Auditor," proactively scanning open issues using a high-efficiency 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)

It sorts these events chronologically to determine the Last Active Actor.

2. The "Last Actor" Rule

The agent follows a precise logic flow based on who acted last:

  • 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 via environment variables, typically set as secrets in GitHub Actions.

Required Secrets

Secret Name Description
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 variables control the timing thresholds and model selection.

Variable Name Description Default
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 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.