What isn't the dev agent's moves to nisse-core, unchanged in behaviour, with its tests: canonical, render, render_anthropic, llm, catalog, probe, context, views, state, reconcile, worker, cancel, dump, transport, matrix, execution, testing, vault, memory, oplog, envfile and gitea (whole). - nisse_core.state.StateStore holds the tables every agent has; an agent's subclass adds its tables (version, migrations), roles and views. nisse_dev.state.StateStore adds the build VM blocks (v5). - views.project takes what each role is shown, rather than a fixed table. - Vault takes the agent's name (agent=), for provenance. - A test that nisse_core never imports an agent. - The loop, config, git, delegation, expert, tools and evals stay in nisse-dev until nisse-ops has a loop to share. Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on the agent host and Terraform; Architecture Part L names the package. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
137 lines
5.6 KiB
Python
137 lines
5.6 KiB
Python
"""A work item's place in the loop (L1), kept in its row in the state store.
|
|
|
|
The row's ``position`` is the item's state; what the loop needs besides
|
|
(the thread its conversation lives in, the revision of what is put up for
|
|
agreement, what it is waiting for) is in ``data["loop"]``.
|
|
"""
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any
|
|
|
|
from nisse_core.state import WorkItem
|
|
from nisse_dev.state import StateStore
|
|
|
|
READY = "ready"
|
|
RUNNING = "running" # a driver run is under way; on boot it means interrupted
|
|
WAITING = "waiting"
|
|
BLOCKED = "blocked" # waits for its recorded dependencies to close (L1)
|
|
CLOSED = "closed"
|
|
|
|
# What a waiting item waits for.
|
|
GO_AHEAD = "go-ahead" # a go on the restatement up (L3, D1)
|
|
ANSWER = "answer" # an answer to a question
|
|
MERGE = "merge" # the PR's review and merge (D4, D5)
|
|
CHAT = "conversation" # a conversation waits for the operator to say more (L5)
|
|
PROVISION = "provisioning" # a new project waits for what it needs (D1, D8)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Loop:
|
|
thread: str = "" # the root of the item's Matrix thread (L5)
|
|
issue: int = 0 # its record issue (D1)
|
|
wait: str = "" # GO_AHEAD / ANSWER / MERGE, when waiting
|
|
rev: int = 0 # the latest proposal put up (L3)
|
|
started: bool = False # scope agreed: a go-ahead was given (D1)
|
|
note: str = "" # what a waiting item said it is waiting on
|
|
prepared: bool = False # its fresh workspace was made (D3)
|
|
milestone: int = 0 # the current milestone (D4)
|
|
branch: str = "" # the current milestone's branch, while one is open
|
|
pr: int = 0 # the current milestone's PR, once published
|
|
pr_url: str = ""
|
|
comments: int = 0 # the last PR conversation comment taken (L8)
|
|
notes: tuple[str, ...] = () # review notes taken (#69)
|
|
fixes: int = 0 # review fixes pushed, for follow-up branch names
|
|
blocked_by: tuple[str, ...] = () # open dependencies, "owner/repo#N" (L1)
|
|
first: int = 0 # the operator's "do #N next": higher runs sooner
|
|
labels: tuple[str, ...] = () # the issue's labels at intake: they steer (D1)
|
|
grace_until: float = 0.0 # a `bug` restatement proceeds then, unanswered
|
|
talked: bool = False # the operator has spoken in the item's thread
|
|
# A conversation (`general`, L1): when it last heard or said anything,
|
|
# the conversation it continues, the work it turned into, and the work
|
|
# proposed for a go-ahead.
|
|
idle_since: float = 0.0
|
|
follows: str = ""
|
|
became: tuple[str, ...] = ()
|
|
proposal_project: str = ""
|
|
proposal_title: str = ""
|
|
proposal_kind: str = "" # "work" on a project, or a new "project"
|
|
new_project: str = "" # a new project being provisioned (D1)
|
|
design_merged: bool = False # a `design` item's design PR has merged (D1, D2)
|
|
helps: int = 0 # delegations so far, for their branch names (L9)
|
|
helped_at: int = 0 # the milestone a helper was last called in for, unasked
|
|
# A question from the operator is answered without changing the item
|
|
# (L5): the wait it interrupted, to go back to.
|
|
question: bool = False
|
|
back_to: str = ""
|
|
back_note: str = ""
|
|
# Where the operator last spoke (D5: answer where asked): "" for the
|
|
# Matrix thread, or the PR it came from, with the inline location and a
|
|
# little of what they said, for quoting.
|
|
asked_on_pr: int = 0
|
|
asked_where: str = ""
|
|
asked_text: str = ""
|
|
# A dangerous action put to the operator (P3): what `allow` would let
|
|
# run, and what it did let run, once, in the next run (L3: bound to
|
|
# exactly that action, by its key).
|
|
pending_action: str = ""
|
|
allowed: str = ""
|
|
# The trust dial (M3.3): why an approved PR was last left unmerged, so
|
|
# it's said once per commit and reason.
|
|
merge_held: str = ""
|
|
# The toolchain as the harness last checked it (M4.1): name, version
|
|
# command, version.
|
|
toolchain: tuple[tuple[str, str, str], ...] = ()
|
|
# L6: the agent's memory index was put into the transcript (once).
|
|
memory_shown: bool = False
|
|
|
|
def to_data(self) -> dict[str, Any]:
|
|
return asdict(self)
|
|
|
|
|
|
def loop_of(item: WorkItem) -> Loop:
|
|
data = dict(item.data.get("loop", {}))
|
|
# Retired: a changed host key now blocks the build VM (#137).
|
|
data.pop("host_key_changed", None)
|
|
data["notes"] = tuple(data.get("notes", ()))
|
|
data["blocked_by"] = tuple(data.get("blocked_by", ()))
|
|
data["labels"] = tuple(data.get("labels", ()))
|
|
data["became"] = tuple(data.get("became", ()))
|
|
data["toolchain"] = tuple(tuple(t) for t in data.get("toolchain", ()))
|
|
return Loop(**data)
|
|
|
|
|
|
def save(
|
|
store: StateStore, item_id: str, state: str, loop: Loop, *, cause: str
|
|
) -> None:
|
|
item = store.work_item(item_id)
|
|
assert item is not None
|
|
store.update_work_item(
|
|
item_id,
|
|
position=state,
|
|
data={**item.data, "loop": loop.to_data()},
|
|
cause=cause,
|
|
)
|
|
|
|
|
|
def item_for_thread(
|
|
store: StateStore, thread: str, status: str | None = "open"
|
|
) -> WorkItem | None:
|
|
"""The item whose conversation is ``thread``; the latest, when a closed
|
|
conversation's thread was picked up again."""
|
|
found = [i for i in store.work_items(status) if loop_of(i).thread == thread]
|
|
return found[-1] if found else None
|
|
|
|
|
|
def item_for_issue(
|
|
store: StateStore, issue: int, project: str | None = None
|
|
) -> WorkItem | None:
|
|
"""The latest item, open or closed, recorded by this issue (in
|
|
``project``, when given): intake takes an issue once, and again only on a
|
|
fresh assignment after its item closed (D1)."""
|
|
found = [
|
|
item
|
|
for item in store.work_items()
|
|
if loop_of(item).issue == issue and project in (None, item.scope)
|
|
]
|
|
return found[-1] if found else None
|