Add the composition root and a runnable entry point
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m14s
Nothing had ever assembled the agent. main.py was still the hello-world placeholder from the scaffold, and no code outside the test suite constructed an Orchestrator — which is how two fully built, fully tested components came to be reachable from nothing: the delegation harness, and the Gitea comment poller. Unit tests that construct a component directly cannot catch that; only assembly can. - config.py validates every variable before anything is built and names all of them at once, rather than failing at first use twenty minutes into a milestone on the call that mattered. Whitespace-only counts as missing, since a token of spaces passes a truthiness check and fails at the first request instead - agent.py builds the whole graph and owns startup and shutdown order: reconcile before accepting commands, and on stop, stop watching, then stop talking, then close the store last - The Gitea account is read via whoami rather than configured: it names the identity every permission is evaluated against, and proves the token works before anything depends on it - Poller lifecycle is now a state-machine concern: watch the plan issue while a gate stands, the PR while it is open, and stop on leaving. The orchestrator owns when to watch; an injected factory owns how to build one - The executor reports each turn, because it runs synchronously inside the transport callback: without it the agent is silent for a whole milestone and a working run looks exactly like a hung one - main.py loads .env, builds, and shuts down cleanly on SIGINT/SIGTERM The entry-point tests changed shape: nisse-dev no longer greets, it refuses to start unconfigured and says everything that is missing. Both tests now run with a deliberately empty environment and a .env path that does not exist, because the working directory's .env holds live credentials and a test must never be one cwd away from starting a real agent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6331fcafd9
commit
2fb25b8195
11 files changed
+694
-15
No files matched your search
@@ -10,6 +10,7 @@ or finished-enough. Until delegation exists (step 7), an escalation gates to
|
||||
the human with the failures attached rather than engaging a frontier model.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -60,6 +61,7 @@ class LLMExecutor:
|
||||
failure_limit: int = DEFAULT_FAILURE_LIMIT,
|
||||
expert: ExpertDesk | None = None,
|
||||
delegate: Delegation | None = None,
|
||||
notify: Callable[[str], None] | None = None,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._store = store
|
||||
@@ -69,6 +71,21 @@ class LLMExecutor:
|
||||
self._failure_limit = failure_limit
|
||||
self._expert = expert
|
||||
self._delegate = delegate
|
||||
# The loop runs synchronously inside the transport callback, so
|
||||
# without this the agent is silent for the whole milestone and a
|
||||
# working run looks exactly like a hung one.
|
||||
self._notify = notify
|
||||
|
||||
def _report(self, turn: int, calls: list[ToolCall]) -> None:
|
||||
if self._notify is None:
|
||||
return
|
||||
summary = ", ".join(
|
||||
f"{c.tool}({c.arguments.get('path') or c.arguments.get('command', '')[:40]})"
|
||||
if c.arguments
|
||||
else c.tool
|
||||
for c in calls
|
||||
)
|
||||
self._notify(f"turn {turn + 1}: {summary}")
|
||||
|
||||
def _escalate(
|
||||
self, milestone: int, failures: int, last_failure: str
|
||||
@@ -133,7 +150,7 @@ class LLMExecutor:
|
||||
did_work = False
|
||||
challenged = False
|
||||
|
||||
for _turn in range(self._max_turns):
|
||||
for turn in range(self._max_turns):
|
||||
try:
|
||||
produced = self._client.complete(
|
||||
self._store.read_events(),
|
||||
@@ -166,6 +183,7 @@ class LLMExecutor:
|
||||
"executor claimed the milestone was done without making any change",
|
||||
)
|
||||
|
||||
self._report(turn, calls)
|
||||
for call in calls:
|
||||
if call.tool in MUTATING_TOOLS:
|
||||
did_work = True
|
||||
|
||||
Reference in new issue
Block a user