Eilen Shin bd995a6a26
fix(agent): align unattended prompt with tool policy (#4919)
* fix(agent): align autonomous interaction guidance

* fix(agent): harden interaction policy selection

* fix(gateway): protect legacy interaction flags

* fix(channels): honor explicit interaction mode

* docs(agent): reduce inherited guidance size

* fix(agent): honor unattended policy across approval paths

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-20 18:57:49 +08:00

183 lines
8.2 KiB
Python

"""Shared interaction policy for lead-agent tools and prompt guidance."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from enum import StrEnum
from typing import Any
ASK_CLARIFICATION_TOOL_NAME = "ask_clarification"
class RunInteractionMode(StrEnum):
"""Interaction modes that can be selected by a trusted run entry point."""
INTERACTIVE = "interactive"
AUTONOMOUS = "autonomous"
WEBHOOK = "webhook"
SCHEDULED = "scheduled"
_INTERACTIVE_CLARIFICATION_SYSTEM = """<clarification_system>
**WORKFLOW PRIORITY: CLARIFY → PLAN → ACT**
1. **FIRST**: Analyze the request in your thinking - identify what's unclear, missing, or ambiguous
2. **SECOND**: If clarification is needed, call `ask_clarification` tool IMMEDIATELY - do NOT start working
3. **THIRD**: Only after all clarifications are resolved, proceed with planning and execution
**CRITICAL RULE: Clarification ALWAYS comes BEFORE action. Never start working and clarify mid-execution.**
**MANDATORY Clarification Scenarios - You MUST call ask_clarification BEFORE starting work when:**
1. **Missing Information** (`missing_info`): Required details not provided
- Example: User says "create a web scraper" but doesn't specify the target website
- Example: "Deploy the app" without specifying environment
- **REQUIRED ACTION**: Call ask_clarification to get the missing information
2. **Ambiguous Requirements** (`ambiguous_requirement`): Multiple valid interpretations exist
- Example: "Optimize the code" could mean performance, readability, or memory usage
- Example: "Make it better" is unclear what aspect to improve
- **REQUIRED ACTION**: Call ask_clarification to clarify the exact requirement
3. **Approach Choices** (`approach_choice`): Several valid approaches exist
- Example: "Add authentication" could use JWT, OAuth, session-based, or API keys
- Example: "Store data" could use database, files, cache, etc.
- **REQUIRED ACTION**: Call ask_clarification to let user choose the approach
4. **Risky Operations** (`risk_confirmation`): Destructive actions need confirmation
- Example: Deleting files, modifying production configs, database operations
- Example: Overwriting existing code or data
- **REQUIRED ACTION**: Call ask_clarification to get explicit confirmation
5. **Suggestions** (`suggestion`): You have a recommendation but want approval
- Example: "I recommend refactoring this code. Should I proceed?"
- **REQUIRED ACTION**: Call ask_clarification to get approval
**STRICT ENFORCEMENT:**
- ❌ DO NOT start working and then ask for clarification mid-execution - clarify FIRST
- ❌ DO NOT skip clarification for "efficiency" - accuracy matters more than speed
- ❌ DO NOT make assumptions when information is missing - ALWAYS ask
- ❌ DO NOT proceed with guesses - STOP and call ask_clarification first
- ❌ DO NOT call any other tool in the same turn as ask_clarification — sibling calls are dropped
- ✅ Analyze the request in thinking → Identify unclear aspects → Ask BEFORE any action
- ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY
- ✅ After calling ask_clarification, execution will be interrupted automatically
- ✅ Wait for user response - do NOT continue with assumptions
**How to Use:**
```python
ask_clarification(
question="Your specific question here?",
clarification_type="missing_info", # or other type
context="Why you need this information", # optional but recommended
options=["option1", "option2"] # optional, for choices
)
```
**Example:**
User: "Deploy the application"
You (thinking): Missing environment info - I MUST ask for clarification
You (action): ask_clarification(
question="Which environment should I deploy to?",
clarification_type="approach_choice",
context="I need to know the target environment for proper configuration",
options=["development", "staging", "production"]
)
[Execution stops - wait for user response]
User: "staging"
You: "Deploying to staging..." [proceed]
</clarification_system>"""
_AUTONOMOUS_CLARIFICATION_SYSTEM = """<clarification_system>
**WORKFLOW PRIORITY: ASSESS -> CHOOSE THE LOWEST-RISK PATH -> ACT**
There is no human available to answer a synchronous question during this run.
Do not wait for clarification or approval. Resolve ambiguity from the request,
{context_sources}.
- For low-risk and reversible work, make the smallest reasonable assumption and continue.
- State every material assumption in the final result.
- For high-risk or irreversible work without sufficient authorization, do not guess:
stop with a concise structured `BLOCKED` result that names the missing decision.
- Prefer inspection and read-only checks before changing state.
</clarification_system>"""
_AUTONOMOUS_CONTEXT_SOURCES = "the available run context and existing configuration"
_WEBHOOK_CONTEXT_SOURCES = "the issue, pull request, repository, event context, and existing configuration"
@dataclass(frozen=True, slots=True)
class RunInteractionPolicy:
"""The single source for interaction-sensitive tools and prompt guidance."""
mode: RunInteractionMode
@property
def allows_clarification(self) -> bool:
return self.mode is RunInteractionMode.INTERACTIVE
@property
def disabled_tool_names(self) -> frozenset[str]:
if self.allows_clarification:
return frozenset()
return frozenset({ASK_CLARIFICATION_TOOL_NAME})
@property
def thinking_guidance(self) -> str:
if self.allows_clarification:
return "- **PRIORITY CHECK: If anything is unclear, missing, or has multiple interpretations, you MUST ask for clarification FIRST - do NOT proceed with work**"
return "- **INTERACTION CHECK: This run has no synchronous human. Resolve ambiguity with the run context, choose the lowest-risk reversible path, and record material assumptions.**"
@property
def clarification_system(self) -> str:
if self.allows_clarification:
return _INTERACTIVE_CLARIFICATION_SYSTEM
context_sources = _WEBHOOK_CONTEXT_SOURCES if self.mode is RunInteractionMode.WEBHOOK else _AUTONOMOUS_CONTEXT_SOURCES
return _AUTONOMOUS_CLARIFICATION_SYSTEM.format(context_sources=context_sources)
@property
def clarification_reminder(self) -> str:
if self.allows_clarification:
return "- **Clarification First**: ALWAYS clarify unclear/missing/ambiguous requirements BEFORE starting work - never assume or guess"
return "- **Autonomous Interaction**: Do not wait for a human response; make minimal reversible assumptions, list them, or return a structured `BLOCKED` result for high-risk ambiguity"
@classmethod
def interactive(cls) -> RunInteractionPolicy:
return cls(RunInteractionMode.INTERACTIVE)
def resolve_run_interaction_policy(config: Mapping[str, Any] | None) -> RunInteractionPolicy:
"""Resolve one policy from legacy runtime flags and channel context.
``non_interactive`` is the scheduler's trusted legacy flag. GitHub and
other webhook channels currently use ``disable_clarification`` and/or
``channel_name``; both remain supported while callers migrate to an
explicit mode.
"""
merged: dict[str, Any] = {}
if config:
configurable = config.get("configurable")
context = config.get("context")
if isinstance(configurable, Mapping):
merged.update(configurable)
if isinstance(context, Mapping):
merged.update(context)
raw_mode = merged.get("interaction_mode")
if raw_mode is not None:
try:
return RunInteractionPolicy(RunInteractionMode(str(raw_mode)))
except ValueError as exc:
valid_modes = ", ".join(mode.value for mode in RunInteractionMode)
raise ValueError(f"Invalid interaction_mode {raw_mode!r}; expected one of: {valid_modes}") from exc
if merged.get("non_interactive"):
return RunInteractionPolicy(RunInteractionMode.SCHEDULED)
if merged.get("channel_name") == "github":
return RunInteractionPolicy(RunInteractionMode.WEBHOOK)
if merged.get("disable_clarification"):
return RunInteractionPolicy(RunInteractionMode.AUTONOMOUS)
return RunInteractionPolicy.interactive()