|
| 1 | +"""Core domain models β pure data, no I/O, no LLM. |
| 2 | +
|
| 3 | +These types are the vocabulary the whole system shares. They deliberately carry no |
| 4 | +behavior beyond holding state; the rules live in :class:`panopticon.core.workflow.Workflow`, and |
| 5 | +the state classes live in :mod:`panopticon.core.state`. |
| 6 | +""" |
| 7 | + |
| 8 | +from __future__ import annotations |
| 9 | + |
| 10 | +from dataclasses import dataclass, field |
| 11 | +from enum import Enum |
| 12 | + |
| 13 | + |
| 14 | +class Turn(str, Enum): |
| 15 | + """Who currently holds the turn β who must act next before anything else happens.""" |
| 16 | + |
| 17 | + USER = "user" |
| 18 | + AGENT = "agent" |
| 19 | + |
| 20 | + |
| 21 | +class Mode(str, Enum): |
| 22 | + """Per-state classification a workflow supplies; the workflow derives the turn from it.""" |
| 23 | + |
| 24 | + FOREGROUND = "foreground" # user-driven β the turn goes to the user on entry |
| 25 | + BACKGROUND = "background" # agent-driven β the turn goes to the agent on entry |
| 26 | + |
| 27 | + |
| 28 | +class Status(str, Enum): |
| 29 | + """Resolution status of a single responsibility.""" |
| 30 | + |
| 31 | + PENDING = "pending" # not yet resolved β blocks handing the turn back |
| 32 | + MET = "met" |
| 33 | + FAILED = "failed" # could not be satisfied; requires a comment |
| 34 | + |
| 35 | + |
| 36 | +@dataclass(frozen=True) |
| 37 | +class Responsibility: |
| 38 | + """An agent obligation for a state. |
| 39 | +
|
| 40 | + The workflow supplies these as *definitions* (``status`` ``PENDING``, no comment). The |
| 41 | + agent resolves each to ``MET`` or ``FAILED`` before handing the turn back; a ``FAILED`` |
| 42 | + responsibility must carry a ``comment`` explaining why. They are agent-only β user |
| 43 | + actions drive transitions directly rather than being modelled as responsibilities. |
| 44 | + """ |
| 45 | + |
| 46 | + key: str |
| 47 | + description: str |
| 48 | + status: Status = Status.PENDING |
| 49 | + comment: str | None = None |
| 50 | + |
| 51 | + def resolve(self, status: Status, comment: str | None = None) -> Responsibility: |
| 52 | + """Return a resolved copy carrying the definition's ``key``/``description``.""" |
| 53 | + return Responsibility( |
| 54 | + key=self.key, description=self.description, status=status, comment=comment |
| 55 | + ) |
| 56 | + |
| 57 | + |
| 58 | +@dataclass |
| 59 | +class Repo: |
| 60 | + """A repository tasks operate on. Owns secret references (added in a later slice).""" |
| 61 | + |
| 62 | + id: str |
| 63 | + name: str |
| 64 | + git_url: str |
| 65 | + default_base: str = "main" |
| 66 | + |
| 67 | + |
| 68 | +@dataclass(frozen=True) |
| 69 | +class HistoryEntry: |
| 70 | + """One append-only entry in a task's transition log. |
| 71 | +
|
| 72 | + Timestamps are passed in by the caller (the task service stamps them); the core |
| 73 | + never reads the clock, which keeps the state machine deterministic and testable. |
| 74 | + ``responsibilities`` holds the set the agent resolved on this transition (empty when the |
| 75 | + state being left defines none). |
| 76 | + """ |
| 77 | + |
| 78 | + at: str # ISO-8601 timestamp, supplied by the caller |
| 79 | + from_state: str | None |
| 80 | + to_state: str |
| 81 | + via: str | None = None |
| 82 | + note: str | None = None |
| 83 | + responsibilities: tuple[Responsibility, ...] = () |
| 84 | + |
| 85 | + |
| 86 | +@dataclass |
| 87 | +class Task: |
| 88 | + """A unit of work. Identity is the internal ``id``; ``slug`` is a human label set later.""" |
| 89 | + |
| 90 | + id: str |
| 91 | + repo_id: str |
| 92 | + workflow: str |
| 93 | + state: str |
| 94 | + turn: Turn |
| 95 | + slug: str | None = None |
| 96 | + history: list[HistoryEntry] = field(default_factory=list) |
0 commit comments