31850c4a81
One tick polls in-flight runs, posts results, classifies the first line and moves state/assignee; failures get a structured note with transient retry and backoff, timeouts are killed and handed over, resume failures and long threads rebuild with a handoff summary. Then the lowest Todo items are dispatched within terminal concurrency and serialize groups, and a heartbeat is written. CLI: tick, new, show, comment, state, assign, smoke, check-config.
130 lines
5.6 KiB
Python
130 lines
5.6 KiB
Python
"""In-memory fakes for Plane and executors (no network, no processes)."""
|
|
import copy
|
|
import tempfile
|
|
|
|
from agentd.config import Config
|
|
from agentd.core import Agentd
|
|
from agentd.executors import ExecResult
|
|
from agentd.store import Store
|
|
|
|
STATES = {"Backlog": "s-bl", "Todo": "s-todo", "In Progress": "s-run", "Waiting": "s-wait", "Review": "s-rev", "Done": "s-done"}
|
|
MEMBERS = {"window10": "u-w10", "oregon": "u-or", "kappiee": "u-k"}
|
|
LABELS = {"model:astra": "l-astra", "needs-game": "l-game", "exec:claude": "l-claude", "budget:30m": "l-b30", "cap:gpu": "l-gpu"}
|
|
|
|
|
|
def config_data():
|
|
return {
|
|
"plane": {"base_url": "https://plane.example", "workspace": "ws"},
|
|
"agentd": {"max_rounds_per_thread": 3},
|
|
"terminals": {
|
|
"win10": {"transport": "warpgate-ssh", "target": "win10", "os": "windows", "launch": "schtasks", "repo": "C:\\repo",
|
|
"plane_user": "window10", "token_key": "TOK_W10", "concurrency": 1, "capabilities": ["game"],
|
|
"executors": ["codex"]},
|
|
"oregon": {"transport": "local", "os": "linux", "launch": "subprocess", "repo": "/repo", "plane_user": "oregon",
|
|
"token_key": "TOK_OR", "concurrency": 2, "executors": ["claude-code", "shell"]},
|
|
},
|
|
"executors": {
|
|
"codex": {"run": "codex exec -", "resume": "codex exec resume {thread} -", "models": ["sol", "astra"], "default_effort": "medium"},
|
|
"claude-code": {"run": "claude -p", "resume": "claude -p --resume {thread}", "models": ["sonnet"]},
|
|
"shell": {"run": "{script}", "resume": False},
|
|
},
|
|
"projects": {"GWENT": {
|
|
"id": "proj", "key_prefix": "GW", "reviewer": "oregon", "answerer": "kappiee",
|
|
"states": {"todo": "Todo", "running": "In Progress", "waiting": "Waiting", "review": "Review", "done": "Done"},
|
|
"routing": {"by_assignee": {"window10": "win10", "oregon": "oregon"},
|
|
"by_label": {"model:astra": {"model": "astra", "effort": "high"}, "needs-game": {"serialize_group": "game"},
|
|
"exec:claude": {"executor": "claude-code"}},
|
|
"default_executor": {"win10": "codex", "oregon": "claude-code"}},
|
|
"retry": {"transient_max": 2, "backoff_minutes": [2, 5]},
|
|
"timeout_hours": 4,
|
|
}},
|
|
}
|
|
|
|
|
|
class FakePlane:
|
|
def __init__(self):
|
|
self.issues, self.comments, self.patches, self.n = {}, {}, [], 0
|
|
|
|
def add_issue(self, seq, state="Todo", assignee="window10", labels=(), desc="<p>do <b>it</b></p>"):
|
|
iid = "i%d" % seq
|
|
self.issues[iid] = {"id": iid, "sequence_id": seq, "name": "T-%d" % seq, "description_html": desc,
|
|
"state": STATES[state], "assignees": [MEMBERS[assignee]], "labels": [LABELS[l] for l in labels]}
|
|
self.comments[iid] = []
|
|
return iid
|
|
|
|
def add_comment(self, iid, html):
|
|
self.n += 1
|
|
c = {"id": "c%d" % self.n, "created_at": "2026-10-21T00:00:%02d" % self.n, "comment_html": html}
|
|
self.comments[iid].append(c)
|
|
return c
|
|
|
|
def texts(self, iid):
|
|
from agentd.plane import html_to_text
|
|
return [html_to_text(c["comment_html"]) for c in self.comments[iid]]
|
|
|
|
def state_of(self, iid):
|
|
return {v: k for k, v in STATES.items()}[self.issues[iid]["state"]]
|
|
|
|
def assignee_of(self, iid):
|
|
return {v: k for k, v in MEMBERS.items()}[self.issues[iid]["assignees"][0]]
|
|
|
|
# client API used by core
|
|
def states(self, pid): return dict(STATES)
|
|
def labels(self, pid): return dict(LABELS)
|
|
def members(self): return dict(MEMBERS)
|
|
def list_issues(self, pid): return copy.deepcopy(list(self.issues.values()))
|
|
def list_comments(self, pid, iid): return copy.deepcopy(self.comments[iid])
|
|
def post_comment(self, pid, iid, html): return copy.deepcopy(self.add_comment(iid, html))
|
|
|
|
def set_state(self, pid, iid, state, assignees=None):
|
|
self.patches.append((iid, state, assignees))
|
|
self.issues[iid]["state"] = state
|
|
if assignees is not None:
|
|
self.issues[iid]["assignees"] = assignees
|
|
|
|
|
|
class FakeExecutor:
|
|
def __init__(self, resume=True):
|
|
self.resume = resume
|
|
self.started, self.killed, self.done = [], [], {}
|
|
|
|
def start(self, run):
|
|
self.started.append(run)
|
|
return {"run_dir": "/runs/" + run.run_id, "run_id": run.run_id, "thread_id": run.thread_id}
|
|
|
|
def poll(self, handle):
|
|
return "finished" if handle["run_id"] in self.done else "running"
|
|
|
|
def collect(self, handle):
|
|
return self.done[handle["run_id"]]
|
|
|
|
def kill(self, handle):
|
|
self.killed.append(handle["run_id"])
|
|
|
|
def finish(self, message, exit_code=0, thread="th-1", tail=""):
|
|
run = self.started[-1]
|
|
self.done[run.run_id] = ExecResult(message, exit_code, thread, ["/runs/%s/last.md" % run.run_id], tail)
|
|
|
|
|
|
class Harness:
|
|
def __init__(self, data=None):
|
|
self.cfg = Config(data or config_data())
|
|
self.plane = FakePlane()
|
|
self.execs = {}
|
|
self.clock = [1_000_000.0]
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.d = Agentd(self.cfg, store=Store(self.tmp.name, echo=False), plane_factory=lambda term: self.plane,
|
|
executor_factory=self.executor, now=lambda: self.clock[0])
|
|
|
|
def executor(self, term, name):
|
|
return self.execs.setdefault((term, name), FakeExecutor(resume=name != "shell"))
|
|
|
|
def tick(self):
|
|
return self.d.tick()
|
|
|
|
def rec(self, iid):
|
|
return self.d.store.load()["projects"]["GWENT"]["issues"][iid]
|
|
|
|
def close(self):
|
|
self.tmp.cleanup()
|