The new loop's core: intake, restatement, go-ahead, threads (M2.1, PR B)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m44s

nisse_dev.loop runs alongside control.py, chosen with `loop: new`.
Issues assigned to the agent (by an operator, or filed by one) become work
items, each with a Matrix thread. A driver run is one model with tools
until it calls a waiting tool: restate puts rev-N up for a go-ahead, bound
to the revision on screen when the reply arrived; ask_human waits for an
answer; close_item closes the item and its issue. Before the go-ahead the
driver can look but not change or close anything. One item is active at a
time; replies in an item's thread go to it. On boot, an item cut off
mid-run waits on the operator, and waiting points are put up again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-02 23:46:42 +00:00
1 parent 6dff36f458
commit f84bb70839
14 files changed
+1205 -1

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
"""Assembling the new loop (M2.1): the same services as the old agent, with
LoopAgent in place of the Orchestrator. Chosen with `loop: new` in config."""
import threading
from dataclasses import dataclass
from nisse_dev.agent import (
SHUTDOWN_WAIT_S,
build_host,
discover_models,
make_llm_clients,
prepare_workspace,
push_credential,
remote_url,
)
from nisse_dev.cancel import Cancel
from nisse_dev.config import AgentConfig
from nisse_dev.gitea import GiteaClient
from nisse_dev.loop.agent import LoopAgent
from nisse_dev.loop.driver import Driver
from nisse_dev.matrix import MatrixTransport
from nisse_dev.oplog import OpLog
from nisse_dev.state import StateStore
from nisse_dev.tools import Workspace
from nisse_dev.worker import SerialWorker
@dataclass
class RunningLoop:
config: AgentConfig
account: str
loop: LoopAgent
store: StateStore
transport: MatrixTransport
gitea: GiteaClient
worker: SerialWorker
_stopped: threading.Event
def start(self) -> None:
self.loop.start()
self.transport.post(
f"nisse-dev up as {self.account} (new loop) on {self.config.gitea_repo}; "
f"{self.loop_status()}"
)
def loop_status(self) -> str:
return self.worker.call(self.loop.status)
def stop(self) -> None:
self.loop.stop()
self.transport.close()
self.gitea.close()
try:
self.worker.call(self.store.close, timeout=SHUTDOWN_WAIT_S)
except TimeoutError:
pass # still working; L8 covers exiting mid-work
self.worker.stop(timeout=SHUTDOWN_WAIT_S)
self._stopped.set()
def wait(self) -> None:
self._stopped.wait()
def build_loop_agent(config: AgentConfig) -> RunningLoop:
oplog = OpLog("nisse-dev")
worker = SerialWorker(
on_error=lambda exc: oplog.emit(
"worker.job_failed", cause="unhandled exception", error=repr(exc)
)
)
worker.start()
store = worker.call(lambda: StateStore(config.state_db, oplog=oplog))
catalog = discover_models(config, oplog)
gitea = GiteaClient(config.gitea_url, config.gitea_token)
account = gitea.whoami()
host = build_host(config)
credential = push_credential(config)
remote = remote_url(gitea, config.gitea_repo, credential)
prepare_workspace(host, config, account, remote, credential)
workspace = Workspace(
host,
config.workspace,
timeout_s=config.tuning.tools.command_timeout_s,
max_output_chars=config.tuning.tools.max_output_chars,
)
normalized, _passthrough = make_llm_clients(config, catalog)
cancel = Cancel()
# The driver is the planner's model for now (M2.6 brings the driver role).
driver = Driver(
normalized,
config.roles.resolve("planner"),
max_turns=config.tuning.executor.max_turns,
cancel=cancel,
)
transport = MatrixTransport(config.matrix)
loop = LoopAgent(
store,
transport,
gitea,
repo=config.gitea_repo,
project=config.project,
account=account,
operators=config.operators,
gitea_operators=config.gitea_operators,
driver=driver,
workspace=workspace,
worker=worker,
cancel=cancel,
oplog=oplog,
roles=config.roles.pinned(),
)
return RunningLoop(
config=config,
account=account,
loop=loop,
store=store,
transport=transport,
gitea=gitea,
worker=worker,
_stopped=threading.Event(),
)