110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""Transport interface ABC + payload dataclasses (design §3.3.1).
|
|
|
|
The durable human-in-the-loop responder owns a notify+resume seam that is
|
|
transport-agnostic. This module defines the contract every adapter implements:
|
|
|
|
* :class:`Transport` — abstract base with ``post_question`` (deliver a
|
|
question-set, return a ``channel_ref`` that embeds the ``question_id``) and
|
|
``parse_answer`` (normalize an inbound raw payload to
|
|
``(question_id, answer, via)``).
|
|
* :class:`QuestionSet` — the question-set payload carried by a LangGraph
|
|
``interrupt()``.
|
|
* :class:`NormalizedAnswer` — the normalized inbound answer the responder
|
|
feeds into the §3.3.1 first-answer-wins compare-and-set.
|
|
|
|
Concrete Slack / GitHub / Claude-Code adapters subclass :class:`Transport` in
|
|
the leaves. The signatures here are CONTRACTS the leaf builders import
|
|
verbatim, so they are explicit and final.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
__all__ = [
|
|
"NormalizedAnswer",
|
|
"QuestionSet",
|
|
"Transport",
|
|
]
|
|
|
|
# Marker template embedded in transports without native callback metadata
|
|
# (e.g. a GitHub issue comment), so an inbound answer can be mapped back to its
|
|
# question. Slack embeds the question_id in ``callback_id`` instead.
|
|
GITHUB_MARKER_TEMPLATE = "<!-- shq:{question_id} -->"
|
|
|
|
|
|
@dataclass
|
|
class QuestionSet:
|
|
"""A set of questions delivered to Adam for one ``turn`` of a task (§3.3.1).
|
|
|
|
Carried in the ``interrupt()`` payload alongside ``thread_id``,
|
|
``question_id``, ``turn``, ``transport``, and ``deadline``. ``questions`` is
|
|
the ordered list of prompts; ``context`` is optional rendering metadata
|
|
(repo, summary) the adapter may surface.
|
|
"""
|
|
|
|
thread_id: str
|
|
question_id: str
|
|
turn: int
|
|
questions: list[str]
|
|
context: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class NormalizedAnswer:
|
|
"""A transport-normalized inbound answer (§3.3.1).
|
|
|
|
The responder maps this into the first-answer-wins compare-and-set:
|
|
``UPDATE ... SET status='answered' ... WHERE question_id=? AND
|
|
status='open'``. ``via`` records the answering channel/identity for the
|
|
audit trail (``answered_via``).
|
|
"""
|
|
|
|
question_id: str
|
|
answer: Any
|
|
via: str
|
|
|
|
|
|
class Transport(ABC):
|
|
"""Abstract transport adapter (§3.3.1).
|
|
|
|
Subclasses implement delivery and answer parsing for one channel. The
|
|
ledger and resume worker depend only on this interface, so an adapter can
|
|
ship first (Slack) and others follow without touching the durable core.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def post_question(
|
|
self,
|
|
*,
|
|
thread_id: str,
|
|
question_id: str,
|
|
turn: int,
|
|
question_set: QuestionSet,
|
|
deadline: str,
|
|
) -> str:
|
|
"""Deliver ``question_set`` and return its ``channel_ref``.
|
|
|
|
The posted message MUST embed ``question_id`` so an inbound answer can
|
|
be mapped back (Slack ``callback_id``; a
|
|
``<!-- shq:<question_id> -->`` marker in a GitHub comment). The returned
|
|
``channel_ref`` is the transport's locator for the post (Slack message
|
|
``ts`` / issue-comment id / Claude session id) and is stored on the
|
|
ledger row so reconcile/recovery can act on it (§3.3.1).
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def parse_answer(self, raw: Any) -> tuple[str, Any, str]:
|
|
"""Normalize an inbound ``raw`` payload to ``(question_id, answer, via)``.
|
|
|
|
Extracts the embedded ``question_id`` (from the Slack ``callback_id`` /
|
|
the GitHub marker / the Claude session), the answer value, and the
|
|
``via`` channel identity. The responder feeds the result into the
|
|
atomic compare-and-set. Implementations may build a
|
|
:class:`NormalizedAnswer` internally and return its fields as the tuple
|
|
the contract specifies.
|
|
"""
|
|
raise NotImplementedError
|