airbyte_ops_mcp.mcp.session_feedback

MCP tool for Devin session feedback reporting.

This module exposes the session feedback operation as an MCP tool for AI agents. It is a thin wrapper around the shared dispatch function in the human_in_the_loop module, with hardcoded channel, emoji, and formatting for the feedback use case.

Feedback is structured with categories and fields modeled after standard issue/bug report templates to ensure actionable, consistent reports.

MCP reference

MCP primitives registered by the session_feedback module of the airbyte-internal-ops server: 2 tool(s), 0 prompt(s), 0 resource(s).

Tools (2)

devin_session_feedback

Hints: open-world

Report structured feedback about a Devin session experience via Slack.

Posts a formatted feedback message to the #hydra-feedback Slack channel, tagging the reporting user and @AJ Steers. The message includes a clickable button for the Devin session link. For negative feedback, a triage workflow is automatically dispatched to launch a Devin session with v3 analyze mode that can inspect the original session's full conversation history.

IMPORTANT: This feedback will be logged publicly in Slack. Inform the user that their feedback is visible to the team and they may be contacted for additional details.

Use this tool when a user explicitly asks to report a positive or negative experience with their Devin session. Before calling this tool, let the user know:

  • Their feedback will be posted publicly in the #hydra-feedback Slack channel
  • They may be contacted by the team for more details
  • Both the reporting user and @AJ Steers will be tagged in the message
  • For negative feedback, a triage session will be automatically launched to inspect the reported session

The Slack message is sent by a GitHub Actions workflow so that Slack credentials are never exposed to the calling agent.

Parameters:

Name Type Required Default Description
feedback_type enum("positive", "negative") yes Type of feedback: 'positive' for a good experience or 'negative' for a bad experience. Use 'positive' when the user expresses satisfaction, praise, or a success story. Use 'negative' when the user reports a problem, frustration, or failure.
category enum("tool_failure", "missing_guidance", "suspected_hallucination", "bad_approach", "excessive_iteration", "poor_quality", "other_concern", "great_results", "exceeded_expectations", "fast_completion", "good_communication", "other_positive_feedback") yes Feedback category. For NEGATIVE feedback, use one of: 'tool_failure' (a specific tool/integration broke), 'missing_guidance' (Devin lacked instructions or context), 'suspected_hallucination' (Devin fabricated information or made incorrect claims), 'bad_approach' (Devin took a fundamentally wrong strategy), 'excessive_iteration' (too many loops/retries before success), 'poor_quality' (output quality below expectations), 'other_concern'. For POSITIVE feedback, use one of: 'great_results' (task completed with high quality), 'exceeded_expectations' (went above and beyond), 'fast_completion' (completed quickly and efficiently), 'good_communication' (kept user well-informed), 'other_positive_feedback'.
task_description string yes Brief description of what the user asked Devin to do. This sets the context for the feedback.
agent_session_url string yes Your agent session URL so the team can view the full context. Use the session URL from your system prompt.
reporting_user string yes The person providing the feedback. Accepts an email address (e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ (e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9').
expected_behavior string | null no null What should have happened. REQUIRED for negative feedback. Describe the expected outcome clearly.
observed_behavior string | null no null What actually happened. REQUIRED for negative feedback. Describe the actual outcome, including any error messages or unexpected results.
what_went_well string | null no null What specifically was good about the experience. REQUIRED for positive feedback. Be specific about what Devin did well.
severity enum("low", "medium", "high", "critical") | null no null Severity of the issue. Recommended for negative feedback. 'low' = minor inconvenience, 'medium' = notable impact, 'high' = significant blocker, 'critical' = complete failure.
steps_to_reproduce string | null no null Optional steps to reproduce the issue. Helpful for negative feedback to enable the team to investigate.
session_to_evaluate string | null no null Optional Devin session URL to evaluate/triage. Use this when reporting feedback about a different session (not your own). If omitted, agent_session_url is used as the session to triage (i.e., the reporter is reporting on itself).

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "feedback_type": {
      "description": "Type of feedback: 'positive' for a good experience or 'negative' for a bad experience. Use 'positive' when the user expresses satisfaction, praise, or a success story. Use 'negative' when the user reports a problem, frustration, or failure.",
      "enum": [
        "positive",
        "negative"
      ],
      "type": "string"
    },
    "category": {
      "description": "Feedback category. For NEGATIVE feedback, use one of: 'tool_failure' (a specific tool/integration broke), 'missing_guidance' (Devin lacked instructions or context), 'suspected_hallucination' (Devin fabricated information or made incorrect claims), 'bad_approach' (Devin took a fundamentally wrong strategy), 'excessive_iteration' (too many loops/retries before success), 'poor_quality' (output quality below expectations), 'other_concern'. For POSITIVE feedback, use one of: 'great_results' (task completed with high quality), 'exceeded_expectations' (went above and beyond), 'fast_completion' (completed quickly and efficiently), 'good_communication' (kept user well-informed), 'other_positive_feedback'.",
      "enum": [
        "tool_failure",
        "missing_guidance",
        "suspected_hallucination",
        "bad_approach",
        "excessive_iteration",
        "poor_quality",
        "other_concern",
        "great_results",
        "exceeded_expectations",
        "fast_completion",
        "good_communication",
        "other_positive_feedback"
      ],
      "type": "string"
    },
    "task_description": {
      "description": "Brief description of what the user asked Devin to do. This sets the context for the feedback.",
      "type": "string"
    },
    "agent_session_url": {
      "description": "Your agent session URL so the team can view the full context. Use the session URL from your system prompt.",
      "type": "string"
    },
    "reporting_user": {
      "description": "The person providing the feedback. Accepts an email address (e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ (e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9').",
      "type": "string"
    },
    "expected_behavior": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "What should have happened. REQUIRED for negative feedback. Describe the expected outcome clearly."
    },
    "observed_behavior": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "What actually happened. REQUIRED for negative feedback. Describe the actual outcome, including any error messages or unexpected results."
    },
    "what_went_well": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "What specifically was good about the experience. REQUIRED for positive feedback. Be specific about what Devin did well."
    },
    "severity": {
      "anyOf": [
        {
          "enum": [
            "low",
            "medium",
            "high",
            "critical"
          ],
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Severity of the issue. Recommended for negative feedback. 'low' = minor inconvenience, 'medium' = notable impact, 'high' = significant blocker, 'critical' = complete failure."
    },
    "steps_to_reproduce": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional steps to reproduce the issue. Helpful for negative feedback to enable the team to investigate."
    },
    "session_to_evaluate": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional Devin session URL to evaluate/triage. Use this when reporting feedback about a *different* session (not your own). If omitted, agent_session_url is used as the session to triage (i.e., the reporter is reporting on itself)."
    }
  },
  "required": [
    "feedback_type",
    "category",
    "task_description",
    "agent_session_url",
    "reporting_user"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Response from the session feedback tool.",
  "properties": {
    "success": {
      "description": "Whether the workflow was triggered successfully",
      "type": "boolean"
    },
    "message": {
      "description": "Human-readable status message",
      "type": "string"
    },
    "workflow_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "URL to view the GitHub Actions workflow file"
    },
    "run_id": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "GitHub Actions workflow run ID"
    },
    "run_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Direct URL to the GitHub Actions workflow run"
    },
    "triage_run_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "URL to the auto-triage workflow run"
    }
  },
  "required": [
    "success",
    "message"
  ],
  "type": "object"
}

devin_session_feedback_followup

Hints: open-world

Post a follow-up to an existing feedback thread in #hydra-feedback.

This is the "second call" in the feedback workflow: after devin_session_feedback creates the initial report, this tool appends triage findings or additional context as a threaded reply.

Each reply is wrapped with a disclaimer clarifying that the thread is non-interactive and not monitored by any agent.

Workspace validation ensures only URLs from the expected Slack workspace are accepted.

Parameters:

Name Type Required Default Description
thread_url string yes Slack thread URL from the original feedback post in #hydra-feedback. This is the thread where follow-up context will be appended. Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019
message string yes Follow-up message text in Slack mrkdwn format. Typically a triage report or additional context about the feedback being investigated. Supports bold, _italic_, code, code blocks, > blockquotes, and links.
agent_session_url string yes Your agent session URL for audit trail. Use the session URL from your system prompt.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "thread_url": {
      "description": "Slack thread URL from the original feedback post in #hydra-feedback. This is the thread where follow-up context will be appended. Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019",
      "type": "string"
    },
    "message": {
      "description": "Follow-up message text in Slack mrkdwn format. Typically a triage report or additional context about the feedback being investigated. Supports *bold*, _italic_, `code`, ```code blocks```, > blockquotes, and <url|label> links.",
      "type": "string"
    },
    "agent_session_url": {
      "description": "Your agent session URL for audit trail. Use the session URL from your system prompt.",
      "type": "string"
    }
  },
  "required": [
    "thread_url",
    "message",
    "agent_session_url"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Response from the session feedback follow-up tool.",
  "properties": {
    "success": {
      "description": "Whether the follow-up was posted successfully",
      "type": "boolean"
    },
    "message": {
      "description": "Human-readable status message",
      "type": "string"
    },
    "reply_ts": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Timestamp of the posted reply (Slack ts format)"
    }
  },
  "required": [
    "success",
    "message"
  ],
  "type": "object"
}

  1# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
  2"""MCP tool for Devin session feedback reporting.
  3
  4This module exposes the session feedback operation as an MCP tool for AI agents.
  5It is a thin wrapper around the shared dispatch function in the human_in_the_loop
  6module, with hardcoded channel, emoji, and formatting for the feedback use case.
  7
  8Feedback is structured with categories and fields modeled after standard issue/bug
  9report templates to ensure actionable, consistent reports.
 10
 11## MCP reference
 12
 13.. include:: ../../../docs/mcp-generated/session_feedback.md
 14    :start-line: 2
 15"""
 16
 17from __future__ import annotations
 18
 19__all__: list[str] = []
 20
 21import logging
 22from enum import StrEnum
 23from typing import Annotated, Literal
 24
 25import requests
 26from fastmcp import FastMCP
 27from fastmcp_extensions import mcp_tool, register_mcp_tools
 28from pydantic import BaseModel, Field
 29
 30from airbyte_ops_mcp.github_actions import (
 31    WorkflowDispatchResult,
 32    trigger_workflow_dispatch,
 33)
 34from airbyte_ops_mcp.github_api import resolve_ci_trigger_github_token
 35from airbyte_ops_mcp.human_in_the_loop import dispatch_escalation
 36from airbyte_ops_mcp.slack_api import SlackAPIError, SlackURLParseError
 37from airbyte_ops_mcp.slack_posting import parse_slack_thread_url, post_thread_reply
 38
 39logger = logging.getLogger(__name__)
 40
 41# Hardcoded settings for the feedback tool — not agent-controllable.
 42_FEEDBACK_CHANNEL = "C0ACUHRP6B1"
 43_AJ_STEERS_IDENTIFIER = "U05AKF1BCC9"
 44
 45# Triage workflow settings
 46_TRIAGE_REPO_OWNER = "airbytehq"
 47_TRIAGE_REPO_NAME = "airbyte-ops-mcp"
 48_TRIAGE_WORKFLOW_FILE = "devin-session-triage.yml"
 49_TRIAGE_DEFAULT_BRANCH = "main"
 50
 51# --- Category definitions ---
 52
 53_CATEGORY_DISPLAY: dict[str, str] = {
 54    "tool_failure": "Tool Failure",
 55    "missing_guidance": "Missing Guidance",
 56    "suspected_hallucination": "Suspected Hallucination",
 57    "bad_approach": "Bad Approach",
 58    "excessive_iteration": "Excessive Iteration",
 59    "poor_quality": "Poor Quality",
 60    "other_concern": "Other Concern",
 61    "great_results": "Great Results",
 62    "exceeded_expectations": "Exceeded Expectations",
 63    "fast_completion": "Fast Completion",
 64    "good_communication": "Good Communication",
 65    "other_positive_feedback": "Other Positive Feedback",
 66}
 67
 68
 69class FeedbackCategory(StrEnum):
 70    """Feedback categories for Devin session reports."""
 71
 72    # Negative categories
 73    TOOL_FAILURE = "tool_failure"
 74    MISSING_GUIDANCE = "missing_guidance"
 75    SUSPECTED_HALLUCINATION = "suspected_hallucination"
 76    BAD_APPROACH = "bad_approach"
 77    EXCESSIVE_ITERATION = "excessive_iteration"
 78    POOR_QUALITY = "poor_quality"
 79    OTHER_CONCERN = "other_concern"
 80
 81    # Positive categories
 82    GREAT_RESULTS = "great_results"
 83    EXCEEDED_EXPECTATIONS = "exceeded_expectations"
 84    FAST_COMPLETION = "fast_completion"
 85    GOOD_COMMUNICATION = "good_communication"
 86    OTHER_POSITIVE_FEEDBACK = "other_positive_feedback"
 87
 88    def is_negative(self) -> bool:
 89        """Return True if this is a negative feedback category."""
 90        return self in _NEGATIVE_MEMBERS
 91
 92    def display_name(self) -> str:
 93        """Return the human-readable display name for this category."""
 94        return _CATEGORY_DISPLAY.get(self.value, self.value)
 95
 96
 97_NEGATIVE_MEMBERS = frozenset(
 98    {
 99        FeedbackCategory.TOOL_FAILURE,
100        FeedbackCategory.MISSING_GUIDANCE,
101        FeedbackCategory.SUSPECTED_HALLUCINATION,
102        FeedbackCategory.BAD_APPROACH,
103        FeedbackCategory.EXCESSIVE_ITERATION,
104        FeedbackCategory.POOR_QUALITY,
105        FeedbackCategory.OTHER_CONCERN,
106    }
107)
108
109_SEVERITY_DISPLAY: dict[str, str] = {
110    "low": "Low",
111    "medium": "Medium",
112    "high": "High",
113    "critical": "Critical",
114}
115
116
117def _feedback_emoji(feedback_type: str) -> str:
118    """Return the header emoji for the given feedback type."""
119    return ":tada:" if feedback_type == "positive" else ":warning:"
120
121
122def _feedback_label(feedback_type: str) -> str:
123    """Return the header label for the given feedback type."""
124    type_display = "Positive" if feedback_type == "positive" else "Negative"
125    return f"Devin Session Feedback ({type_display})"
126
127
128def _build_feedback_body(
129    *,
130    feedback_type: str,
131    category: str,
132    task_description: str,
133    expected_behavior: str | None,
134    observed_behavior: str | None,
135    what_went_well: str | None,
136    severity: str | None,
137    steps_to_reproduce: str | None,
138) -> str:
139    """Build a Slack mrkdwn message body from structured feedback fields."""
140    lines: list[str] = []
141
142    cat = FeedbackCategory(category)
143    lines.append(f"*Category:* {cat.display_name()}")
144
145    if severity:
146        sev_display = _SEVERITY_DISPLAY.get(severity, severity)
147        lines.append(f"*Severity:* {sev_display}")
148
149    lines.append("")
150    lines.append(f"*Task:* {task_description}")
151
152    if feedback_type == "negative":
153        if expected_behavior:
154            lines.append("")
155            lines.append(f"*Expected Behavior:* {expected_behavior}")
156        if observed_behavior:
157            lines.append("")
158            lines.append(f"*Observed Behavior:* {observed_behavior}")
159        if steps_to_reproduce:
160            lines.append("")
161            lines.append(f"*Steps to Reproduce:* {steps_to_reproduce}")
162    else:
163        if what_went_well:
164            lines.append("")
165            lines.append(f"*What Went Well:* {what_went_well}")
166
167    if feedback_type == "negative":
168        lines.append("")
169        lines.append(
170            "_Auto-triage: a Devin session with v3 analyze mode will inspect this session._"
171        )
172
173    return "\n".join(lines)
174
175
176def _validate_negative_fields(
177    expected_behavior: str | None,
178    observed_behavior: str | None,
179) -> str | None:
180    """Return an error message if required negative feedback fields are missing."""
181    missing: list[str] = []
182    if not expected_behavior:
183        missing.append("expected_behavior")
184    if not observed_behavior:
185        missing.append("observed_behavior")
186    if missing:
187        return f"Negative feedback requires: {', '.join(missing)}."
188    return None
189
190
191def _validate_positive_fields(
192    what_went_well: str | None,
193) -> str | None:
194    """Return an error message if required positive feedback fields are missing."""
195    if not what_went_well:
196        return "Positive feedback requires: what_went_well."
197    return None
198
199
200def _dispatch_triage_workflow(
201    session_url: str,
202    feedback_context: str,
203    reporting_user: str,
204    cc_persons: str = "",
205    header_emoji: str = "",
206    header_label: str = "",
207) -> WorkflowDispatchResult | None:
208    """Dispatch the v3 session triage workflow.
209
210    The triage workflow launches a Devin session with v3 analyze mode and
211    posts a single Slack notification via the HITL reusable workflow.
212    Formatting params (emoji, header, cc) are passed through to the HITL
213    notification so the caller doesn't need to post separately.
214
215    Returns the dispatch result, or None if dispatch fails.
216    """
217    token = resolve_ci_trigger_github_token()
218    inputs: dict[str, str] = {
219        "session_url": session_url,
220        "feedback_context": feedback_context,
221        "reporting_user": reporting_user,
222    }
223    if cc_persons:
224        inputs["cc_persons"] = cc_persons
225    if header_emoji:
226        inputs["header_emoji"] = header_emoji
227    if header_label:
228        inputs["header_label"] = header_label
229    try:
230        return trigger_workflow_dispatch(
231            owner=_TRIAGE_REPO_OWNER,
232            repo=_TRIAGE_REPO_NAME,
233            workflow_file=_TRIAGE_WORKFLOW_FILE,
234            ref=_TRIAGE_DEFAULT_BRANCH,
235            inputs=inputs,
236            token=token,
237        )
238    except requests.HTTPError:
239        logger.exception("Failed to dispatch triage workflow")
240        return None
241
242
243class SessionFeedbackResponse(BaseModel):
244    """Response from the session feedback tool."""
245
246    success: bool = Field(description="Whether the workflow was triggered successfully")
247    message: str = Field(description="Human-readable status message")
248    workflow_url: str | None = Field(
249        default=None,
250        description="URL to view the GitHub Actions workflow file",
251    )
252    run_id: int | None = Field(
253        default=None,
254        description="GitHub Actions workflow run ID",
255    )
256    run_url: str | None = Field(
257        default=None,
258        description="Direct URL to the GitHub Actions workflow run",
259    )
260    triage_run_url: str | None = Field(
261        default=None,
262        description="URL to the auto-triage workflow run",
263    )
264
265
266@mcp_tool(
267    read_only=False,
268    idempotent=False,
269    open_world=True,
270)
271def devin_session_feedback(
272    feedback_type: Annotated[
273        Literal["positive", "negative"],
274        Field(
275            description=(
276                "Type of feedback: 'positive' for a good experience or 'negative' for a "
277                "bad experience. Use 'positive' when the user expresses satisfaction, "
278                "praise, or a success story. Use 'negative' when the user reports a problem, "
279                "frustration, or failure."
280            ),
281        ),
282    ],
283    category: Annotated[
284        FeedbackCategory,
285        Field(
286            description=(
287                "Feedback category. "
288                "For NEGATIVE feedback, use one of: "
289                "'tool_failure' (a specific tool/integration broke), "
290                "'missing_guidance' (Devin lacked instructions or context), "
291                "'suspected_hallucination' (Devin fabricated information or made incorrect claims), "
292                "'bad_approach' (Devin took a fundamentally wrong strategy), "
293                "'excessive_iteration' (too many loops/retries before success), "
294                "'poor_quality' (output quality below expectations), "
295                "'other_concern'. "
296                "For POSITIVE feedback, use one of: "
297                "'great_results' (task completed with high quality), "
298                "'exceeded_expectations' (went above and beyond), "
299                "'fast_completion' (completed quickly and efficiently), "
300                "'good_communication' (kept user well-informed), "
301                "'other_positive_feedback'."
302            ),
303        ),
304    ],
305    task_description: Annotated[
306        str,
307        Field(
308            description=(
309                "Brief description of what the user asked Devin to do. "
310                "This sets the context for the feedback."
311            ),
312        ),
313    ],
314    agent_session_url: Annotated[
315        str,
316        Field(
317            description=(
318                "Your agent session URL so the team can view the full context. "
319                "Use the session URL from your system prompt."
320            ),
321        ),
322    ],
323    reporting_user: Annotated[
324        str,
325        Field(
326            description=(
327                "The person providing the feedback. Accepts an email address "
328                "(e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ "
329                "(e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9')."
330            ),
331        ),
332    ],
333    expected_behavior: Annotated[
334        str | None,
335        Field(
336            default=None,
337            description=(
338                "What should have happened. REQUIRED for negative feedback. "
339                "Describe the expected outcome clearly."
340            ),
341        ),
342    ],
343    observed_behavior: Annotated[
344        str | None,
345        Field(
346            default=None,
347            description=(
348                "What actually happened. REQUIRED for negative feedback. "
349                "Describe the actual outcome, including any error messages or unexpected results."
350            ),
351        ),
352    ],
353    what_went_well: Annotated[
354        str | None,
355        Field(
356            default=None,
357            description=(
358                "What specifically was good about the experience. REQUIRED for positive feedback. "
359                "Be specific about what Devin did well."
360            ),
361        ),
362    ],
363    severity: Annotated[
364        Literal["low", "medium", "high", "critical"] | None,
365        Field(
366            default=None,
367            description=(
368                "Severity of the issue. Recommended for negative feedback. "
369                "'low' = minor inconvenience, 'medium' = notable impact, "
370                "'high' = significant blocker, 'critical' = complete failure."
371            ),
372        ),
373    ],
374    steps_to_reproduce: Annotated[
375        str | None,
376        Field(
377            default=None,
378            description=(
379                "Optional steps to reproduce the issue. Helpful for negative feedback "
380                "to enable the team to investigate."
381            ),
382        ),
383    ],
384    session_to_evaluate: Annotated[
385        str | None,
386        Field(
387            default=None,
388            description=(
389                "Optional Devin session URL to evaluate/triage. Use this when reporting "
390                "feedback about a *different* session (not your own). If omitted, "
391                "agent_session_url is used as the session to triage (i.e., the reporter "
392                "is reporting on itself)."
393            ),
394        ),
395    ],
396) -> SessionFeedbackResponse:
397    """Report structured feedback about a Devin session experience via Slack.
398
399    Posts a formatted feedback message to the #hydra-feedback Slack channel,
400    tagging the reporting user and @AJ Steers. The message includes a clickable
401    button for the Devin session link. For negative feedback, a triage workflow
402    is automatically dispatched to launch a Devin session with v3 analyze mode
403    that can inspect the original session's full conversation history.
404
405    IMPORTANT: This feedback will be logged publicly in Slack. Inform the user
406    that their feedback is visible to the team and they may be contacted for
407    additional details.
408
409    Use this tool when a user explicitly asks to report a positive or negative
410    experience with their Devin session. Before calling this tool, let the user
411    know:
412    - Their feedback will be posted publicly in the #hydra-feedback Slack channel
413    - They may be contacted by the team for more details
414    - Both the reporting user and @AJ Steers will be tagged in the message
415    - For negative feedback, a triage session will be automatically launched to inspect the reported session
416
417    The Slack message is sent by a GitHub Actions workflow so that Slack
418    credentials are never exposed to the calling agent.
419    """
420    # Validate category matches feedback type.
421    cat = FeedbackCategory(category)
422    is_negative_feedback = feedback_type == "negative"
423    if cat.is_negative() != is_negative_feedback:
424        expected_kind = "negative" if is_negative_feedback else "positive"
425        valid = [
426            c.value for c in FeedbackCategory if c.is_negative() == is_negative_feedback
427        ]
428        return SessionFeedbackResponse(
429            success=False,
430            message=(
431                f"Invalid category '{category}' for {feedback_type} feedback. "
432                f"Valid {expected_kind} categories: {', '.join(valid)}."
433            ),
434        )
435
436    # Validate required fields based on feedback type.
437    if feedback_type == "negative":
438        validation_error = _validate_negative_fields(
439            expected_behavior=expected_behavior,
440            observed_behavior=observed_behavior,
441        )
442    else:
443        validation_error = _validate_positive_fields(
444            what_went_well=what_went_well,
445        )
446
447    if validation_error:
448        return SessionFeedbackResponse(
449            success=False,
450            message=validation_error,
451        )
452
453    message_body = _build_feedback_body(
454        feedback_type=feedback_type,
455        category=category,
456        task_description=task_description,
457        expected_behavior=expected_behavior,
458        observed_behavior=observed_behavior,
459        what_went_well=what_went_well,
460        severity=severity,
461        steps_to_reproduce=steps_to_reproduce,
462    )
463
464    # For negative feedback, dispatch triage workflow (which also posts to Slack
465    # via the HITL reusable workflow — single message with triage button).
466    # For positive feedback, dispatch HITL directly (no triage needed).
467    if is_negative_feedback:
468        triage_session_url = session_to_evaluate or agent_session_url
469        # _dispatch_triage_workflow catches exceptions internally and returns None
470        triage_result = _dispatch_triage_workflow(
471            session_url=triage_session_url,
472            feedback_context=message_body,
473            reporting_user=reporting_user,
474            cc_persons=_AJ_STEERS_IDENTIFIER,
475            header_emoji=_feedback_emoji(feedback_type),
476            header_label=_feedback_label(feedback_type),
477        )
478        if triage_result is not None:
479            view_url = triage_result.run_url or triage_result.workflow_url
480            return SessionFeedbackResponse(
481                success=True,
482                message=(
483                    "Feedback submitted. Auto-triage workflow launched. "
484                    "A Slack notification will be posted to #hydra-feedback "
485                    "once the triage session starts. "
486                    f"View workflow progress at: {view_url}"
487                ),
488                workflow_url=triage_result.workflow_url,
489                run_id=triage_result.run_id,
490                run_url=triage_result.run_url,
491                triage_run_url=view_url,
492            )
493        # Triage dispatch failed — fall back to direct HITL notification
494        # so negative feedback is still recorded in Slack.
495        logger.warning(
496            "Triage workflow dispatch failed; falling back to direct HITL dispatch."
497        )
498
499    # Positive feedback (or negative feedback fallback): dispatch HITL directly
500    result = dispatch_escalation(
501        target_person=reporting_user,
502        message=message_body,
503        agent_session_url=agent_session_url,
504        cc=[_AJ_STEERS_IDENTIFIER],
505        channel_override=_FEEDBACK_CHANNEL,
506        header_emoji=_feedback_emoji(feedback_type),
507        header_label=_feedback_label(feedback_type),
508    )
509
510    view_url = result.run_url or result.workflow_url
511    return SessionFeedbackResponse(
512        success=True,
513        message=(
514            f"Feedback submitted and posted to #hydra-feedback. "
515            f"The reporting user and @AJ Steers have been tagged. "
516            f"View progress at: {view_url}"
517        ),
518        workflow_url=result.workflow_url,
519        run_id=result.run_id,
520        run_url=result.run_url,
521    )
522
523
524# ---------------------------------------------------------------------------
525# Non-interactive thread disclaimer
526# ---------------------------------------------------------------------------
527
528_FOLLOWUP_HEADER = "🤖 *Automated Triage Update*"
529_FOLLOWUP_FOOTER_TEMPLATE = (
530    "_ℹ️ This thread is not monitored by Devin. "
531    "Replies here will not be seen by any agent. "
532    "For follow-up, use the <{agent_session_url}|linked session> or create a new task._"
533)
534
535
536def _wrap_followup_message(message: str, *, agent_session_url: str) -> str:
537    """Wrap a follow-up message with session link and non-interactive disclaimer."""
538    footer = _FOLLOWUP_FOOTER_TEMPLATE.format(agent_session_url=agent_session_url)
539    return f"{_FOLLOWUP_HEADER}\n\n{message}\n\n{footer}"
540
541
542# ---------------------------------------------------------------------------
543# Feedback follow-up tool
544# ---------------------------------------------------------------------------
545
546
547class SessionFeedbackFollowupResponse(BaseModel):
548    """Response from the session feedback follow-up tool."""
549
550    success: bool = Field(description="Whether the follow-up was posted successfully")
551    message: str = Field(description="Human-readable status message")
552    reply_ts: str | None = Field(
553        default=None,
554        description="Timestamp of the posted reply (Slack ts format)",
555    )
556
557
558@mcp_tool(
559    read_only=False,
560    idempotent=False,
561    open_world=True,
562)
563def devin_session_feedback_followup(
564    thread_url: Annotated[
565        str,
566        Field(
567            description=(
568                "Slack thread URL from the original feedback post in #hydra-feedback. "
569                "This is the thread where follow-up context will be appended. "
570                "Example: https://airbytehq-team.slack.com/archives/C0ACUHRP6B1/p1773062711122019"
571            ),
572        ),
573    ],
574    message: Annotated[
575        str,
576        Field(
577            description=(
578                "Follow-up message text in Slack mrkdwn format. "
579                "Typically a triage report or additional context about the "
580                "feedback being investigated. "
581                "Supports *bold*, _italic_, `code`, ```code blocks```, "
582                "> blockquotes, and <url|label> links."
583            ),
584        ),
585    ],
586    agent_session_url: Annotated[
587        str,
588        Field(
589            description=(
590                "Your agent session URL for audit trail. "
591                "Use the session URL from your system prompt."
592            ),
593        ),
594    ],
595) -> SessionFeedbackFollowupResponse:
596    """Post a follow-up to an existing feedback thread in #hydra-feedback.
597
598    This is the "second call" in the feedback workflow: after
599    `devin_session_feedback` creates the initial report, this tool appends
600    triage findings or additional context as a threaded reply.
601
602    Each reply is wrapped with a disclaimer clarifying that the thread is
603    non-interactive and not monitored by any agent.
604
605    Workspace validation ensures only URLs from the expected Slack
606    workspace are accepted.
607    """
608    try:
609        channel_id, thread_ts = parse_slack_thread_url(thread_url)
610    except SlackURLParseError as exc:
611        return SessionFeedbackFollowupResponse(
612            success=False,
613            message=str(exc),
614        )
615
616    wrapped_message = _wrap_followup_message(
617        message, agent_session_url=agent_session_url
618    )
619
620    try:
621        reply_ts = post_thread_reply(
622            channel_id=channel_id,
623            thread_ts=thread_ts,
624            message=wrapped_message,
625        )
626    except SlackAPIError as exc:
627        return SessionFeedbackFollowupResponse(
628            success=False,
629            message=f"Slack API error: {exc}",
630        )
631
632    logger.info(
633        "Feedback follow-up posted: channel=%s thread_ts=%s agent=%s",
634        channel_id,
635        thread_ts,
636        agent_session_url,
637    )
638    return SessionFeedbackFollowupResponse(
639        success=True,
640        message=f"Follow-up posted to feedback thread in channel {channel_id}.",
641        reply_ts=reply_ts,
642    )
643
644
645def register_session_feedback_tools(app: FastMCP) -> None:
646    """Register session feedback tools with the FastMCP app."""
647    register_mcp_tools(app, mcp_module=__name__)