mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] backend: leading-_ -> p_/public for class-private self-attributes (coordinated .attr + string-ref rename); promote cross-file attrs (per_session/idle_lru/payload)
This commit is contained in:
@@ -34,10 +34,10 @@ class SeqLogStore:
|
||||
"""Process-wide store. Per-session locks live inside `p_SessionSeqLog`."""
|
||||
|
||||
def __init__(self, persist_dir: Optional[str] = None) -> None:
|
||||
self._per_session: dict[str, p_SessionSeqLog] = {}
|
||||
self.per_session: dict[str, p_SessionSeqLog] = {}
|
||||
# Coarse lock guards only the setdefault path; never crosses an await.
|
||||
self._dict_lock = asyncio.Lock()
|
||||
self._persist_dir = persist_dir
|
||||
self.p_dict_lock = asyncio.Lock()
|
||||
self.p_persist_dir = persist_dir
|
||||
if persist_dir:
|
||||
try:
|
||||
os.makedirs(persist_dir, exist_ok=True)
|
||||
@@ -45,18 +45,18 @@ class SeqLogStore:
|
||||
logger.warning("seq_log: failed to create persist dir %s", persist_dir)
|
||||
|
||||
async def _get_or_create(self, session_id: str) -> p_SessionSeqLog:
|
||||
log = self._per_session.get(session_id)
|
||||
log = self.per_session.get(session_id)
|
||||
if log is not None:
|
||||
return log
|
||||
async with self._dict_lock:
|
||||
log = self._per_session.get(session_id)
|
||||
async with self.p_dict_lock:
|
||||
log = self.per_session.get(session_id)
|
||||
if log is None:
|
||||
log = p_SessionSeqLog()
|
||||
self._per_session[session_id] = log
|
||||
self.per_session[session_id] = log
|
||||
return log
|
||||
|
||||
def _peek(self, session_id: str) -> Optional[p_SessionSeqLog]:
|
||||
return self._per_session.get(session_id)
|
||||
return self.per_session.get(session_id)
|
||||
|
||||
@asynccontextmanager
|
||||
async def stamp(
|
||||
@@ -99,13 +99,13 @@ class SeqLogStore:
|
||||
return log.seq if log else 0
|
||||
|
||||
def _terminal_path(self, session_id: str) -> Optional[str]:
|
||||
if not self._persist_dir:
|
||||
if not self.p_persist_dir:
|
||||
return None
|
||||
# Session ids are uuid4 hex; sanitize anyway against path traversal.
|
||||
safe = "".join(c for c in session_id if c.isalnum() or c in ("-", "_"))
|
||||
if not safe:
|
||||
return None
|
||||
return os.path.join(self._persist_dir, f"{safe}.json")
|
||||
return os.path.join(self.p_persist_dir, f"{safe}.json")
|
||||
|
||||
def persist_terminal(self, session_id: str, payload_str: str) -> None:
|
||||
"""Atomic write of a terminal event for post-restart clients; best-effort, never blocks broadcast."""
|
||||
@@ -134,7 +134,7 @@ class SeqLogStore:
|
||||
|
||||
def clear(self, session_id: str) -> None:
|
||||
"""Drop in-memory log and persisted terminal; for full deletion only, closed-but-retained sessions keep it."""
|
||||
self._per_session.pop(session_id, None)
|
||||
self.per_session.pop(session_id, None)
|
||||
path = self._terminal_path(session_id)
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
|
||||
@@ -85,15 +85,15 @@ class AppRuntime:
|
||||
# _start_new_mode). frontend_url returns null until this flips,
|
||||
# so the preview pane doesn't try to navigate to an unbound port
|
||||
# and show a "Site can't be reached" error mid-npm-install.
|
||||
self._frontend_ready: bool = False
|
||||
self.p_frontend_ready: bool = False
|
||||
# True while the process tree is SIGSTOP'd in the idle pool. A frozen
|
||||
# vite still holds its port but can't answer it, so frontend_url must
|
||||
# stay null while suspended (else the webview loads a dead port = the
|
||||
# ERR_FAILED on fast app-switching).
|
||||
self._suspended: bool = False
|
||||
self.p_suspended: bool = False
|
||||
self.process: Optional[asyncio.subprocess.Process] = None
|
||||
self.log_buffer: deque[LogLine] = deque(maxlen=LOG_BUFFER_LINES)
|
||||
self._subscribers: set[LogSubscriber] = set()
|
||||
self.p_subscribers: set[LogSubscriber] = set()
|
||||
# Recent build/runtime errors scraped from stderr; drained by
|
||||
# the agent's post-tool hook after Write/Edit so the agent sees
|
||||
# vite/babel/uvicorn errors in its next turn and can self-fix
|
||||
@@ -101,10 +101,10 @@ class AppRuntime:
|
||||
self.recent_errors: deque[str] = deque(maxlen=RECENT_ERRORS_MAX)
|
||||
self.render_state: Optional[str] = None
|
||||
self.render_error_text: str = ""
|
||||
self._stdout_task: Optional[asyncio.Task] = None
|
||||
self._stderr_task: Optional[asyncio.Task] = None
|
||||
self._wait_task: Optional[asyncio.Task] = None
|
||||
self._frontend_ready_task: Optional[asyncio.Task] = None
|
||||
self.p_stdout_task: Optional[asyncio.Task] = None
|
||||
self.p_stderr_task: Optional[asyncio.Task] = None
|
||||
self.p_wait_task: Optional[asyncio.Task] = None
|
||||
self.p_frontend_ready_task: Optional[asyncio.Task] = None
|
||||
self.p_lock = asyncio.Lock()
|
||||
|
||||
def drain_errors(self) -> list[str]:
|
||||
@@ -151,7 +151,7 @@ class AppRuntime:
|
||||
# the ERR_FAILED you see on reopen. No live process, no URL.
|
||||
# And gated on `not _suspended`: a SIGSTOP'd idle runtime is "running"
|
||||
# (returncode is None) but frozen, so its port won't answer.
|
||||
if self.frontend_port and self._frontend_ready and self.running and not self._suspended:
|
||||
if self.frontend_port and self.p_frontend_ready and self.running and not self.p_suspended:
|
||||
return f"http://127.0.0.1:{self.frontend_port}/"
|
||||
return None
|
||||
|
||||
@@ -283,13 +283,13 @@ class AppRuntime:
|
||||
return False
|
||||
backend_note = f" + backend on {self.port}" if self.port else ""
|
||||
self._broadcast(LogLine("runtime", f"[runtime] {launch_desc} started; frontend on {self.frontend_port}{backend_note} (pid {self.process.pid})"))
|
||||
self._stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
|
||||
self._stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
|
||||
self._wait_task = asyncio.create_task(self._await_exit())
|
||||
self.p_stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
|
||||
self.p_stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
|
||||
self.p_wait_task = asyncio.create_task(self._await_exit())
|
||||
# Kick off the port-bind poller so frontend_url flips on once
|
||||
# Vite is actually accepting connections.
|
||||
self._frontend_ready = False
|
||||
self._frontend_ready_task = asyncio.create_task(self._await_frontend_bind())
|
||||
self.p_frontend_ready = False
|
||||
self.p_frontend_ready_task = asyncio.create_task(self._await_frontend_bind())
|
||||
return True
|
||||
|
||||
def _resolve_launch(self, env: dict) -> tuple[list[str], str, str]:
|
||||
@@ -368,7 +368,7 @@ class AppRuntime:
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
self._frontend_ready = True
|
||||
self.p_frontend_ready = True
|
||||
self._broadcast(LogLine(
|
||||
"runtime",
|
||||
f"[runtime] frontend ready at http://127.0.0.1:{port}/",
|
||||
@@ -424,9 +424,9 @@ class AppRuntime:
|
||||
self.process = None
|
||||
return False
|
||||
self._broadcast(LogLine("runtime", f"[runtime] backend started on port {self.port} (pid {self.process.pid})"))
|
||||
self._stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
|
||||
self._stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
|
||||
self._wait_task = asyncio.create_task(self._await_exit())
|
||||
self.p_stdout_task = asyncio.create_task(self._pipe_stream(self.process.stdout, "stdout"))
|
||||
self.p_stderr_task = asyncio.create_task(self._pipe_stream(self.process.stderr, "stderr"))
|
||||
self.p_wait_task = asyncio.create_task(self._await_exit())
|
||||
return True
|
||||
|
||||
def _spawn_env_base(self) -> dict[str, str]:
|
||||
@@ -448,8 +448,8 @@ class AppRuntime:
|
||||
if not self.process or self.process.returncode is not None:
|
||||
# Still cancel the bind poller in case stop() races a
|
||||
# never-launched runtime; defensive no-op otherwise.
|
||||
if self._frontend_ready_task and not self._frontend_ready_task.done():
|
||||
self._frontend_ready_task.cancel()
|
||||
if self.p_frontend_ready_task and not self.p_frontend_ready_task.done():
|
||||
self.p_frontend_ready_task.cancel()
|
||||
return
|
||||
try:
|
||||
# Walk the descendant tree first so vite/uvicorn grandchildren
|
||||
@@ -468,9 +468,9 @@ class AppRuntime:
|
||||
pass
|
||||
# Cancel the bind poller so it stops scanning a port that's
|
||||
# gone away, and reset the readiness flag.
|
||||
if self._frontend_ready_task and not self._frontend_ready_task.done():
|
||||
self._frontend_ready_task.cancel()
|
||||
self._frontend_ready = False
|
||||
if self.p_frontend_ready_task and not self.p_frontend_ready_task.done():
|
||||
self.p_frontend_ready_task.cancel()
|
||||
self.p_frontend_ready = False
|
||||
|
||||
async def restart(self) -> bool:
|
||||
await self.stop()
|
||||
@@ -480,7 +480,7 @@ class AppRuntime:
|
||||
"""Register a log subscriber. Immediately replays the ring buffer
|
||||
so a Terminal pane that opens mid-session shows context. Returns
|
||||
an unsubscribe function."""
|
||||
self._subscribers.add(cb)
|
||||
self.p_subscribers.add(cb)
|
||||
for line in list(self.log_buffer):
|
||||
try:
|
||||
cb(line)
|
||||
@@ -488,14 +488,14 @@ class AppRuntime:
|
||||
pass
|
||||
|
||||
def p_unsub() -> None:
|
||||
self._subscribers.discard(cb)
|
||||
self.p_subscribers.discard(cb)
|
||||
|
||||
return p_unsub
|
||||
|
||||
def _broadcast(self, line: LogLine) -> None:
|
||||
self.log_buffer.append(line)
|
||||
# Snapshot subscribers; they can self-remove during dispatch.
|
||||
for cb in list(self._subscribers):
|
||||
for cb in list(self.p_subscribers):
|
||||
try:
|
||||
cb(line)
|
||||
except Exception:
|
||||
@@ -536,7 +536,7 @@ class AppRuntime:
|
||||
# Unclean death (vite crash, OOM, orphaned parent) must drop readiness;
|
||||
# otherwise frontend_url keeps advertising a dead port and the preview
|
||||
# navigates into ERR_FAILED. stop() already does this for clean stops.
|
||||
self._frontend_ready = False
|
||||
self.p_frontend_ready = False
|
||||
self._broadcast(LogLine("runtime", f"[runtime] backend exited with code {rc}"))
|
||||
|
||||
|
||||
@@ -553,11 +553,11 @@ class AppRuntimeManager:
|
||||
def __init__(self) -> None:
|
||||
# workspace_id → AppRuntime, currently has >=1 subscriber.
|
||||
self.runtimes: dict[str, AppRuntime] = {}
|
||||
self._attached: dict[str, int] = {}
|
||||
self.p_attached: dict[str, int] = {}
|
||||
# workspace_id → AppRuntime with no subscribers but still
|
||||
# alive. OrderedDict gives O(1) move_to_end + popitem(last=False)
|
||||
# for LRU semantics.
|
||||
self._idle_lru: "OrderedDict[str, AppRuntime]" = OrderedDict()
|
||||
self.idle_lru: "OrderedDict[str, AppRuntime]" = OrderedDict()
|
||||
self.p_lock = asyncio.Lock()
|
||||
|
||||
async def attach(self, workspace_id: str, workspace_path: str) -> AppRuntime:
|
||||
@@ -571,7 +571,7 @@ class AppRuntimeManager:
|
||||
if rt is None:
|
||||
# Maybe the runtime is sitting idle in the LRU; revive
|
||||
# it without paying the spawn cost again.
|
||||
idle_rt = self._idle_lru.pop(workspace_id, None)
|
||||
idle_rt = self.idle_lru.pop(workspace_id, None)
|
||||
if idle_rt is not None and idle_rt.running:
|
||||
rt = idle_rt
|
||||
rt.workspace_path = workspace_path
|
||||
@@ -580,7 +580,7 @@ class AppRuntimeManager:
|
||||
# SIGCONT the process tree if A2 had it paused while
|
||||
# idle. Pair with the SIGSTOP in detach() below.
|
||||
resume_process_tree(rt.process)
|
||||
rt._suspended = False
|
||||
rt.p_suspended = False
|
||||
else:
|
||||
if idle_rt is not None:
|
||||
# Stale idle entry; process died while idling.
|
||||
@@ -595,7 +595,7 @@ class AppRuntimeManager:
|
||||
# folder), trust the latest caller; they have the
|
||||
# current truth.
|
||||
rt.workspace_path = workspace_path
|
||||
self._attached[workspace_id] = self._attached.get(workspace_id, 0) + 1
|
||||
self.p_attached[workspace_id] = self.p_attached.get(workspace_id, 0) + 1
|
||||
if not revived and not rt.running:
|
||||
await rt.start()
|
||||
# Stop any dead idle runtime outside the lock to avoid blocking.
|
||||
@@ -610,11 +610,11 @@ class AppRuntimeManager:
|
||||
to_idle: Optional[AppRuntime] = None
|
||||
to_reap: list[AppRuntime] = []
|
||||
async with self.p_lock:
|
||||
count = self._attached.get(workspace_id, 0) - 1
|
||||
count = self.p_attached.get(workspace_id, 0) - 1
|
||||
if count > 0:
|
||||
self._attached[workspace_id] = count
|
||||
self.p_attached[workspace_id] = count
|
||||
return
|
||||
self._attached.pop(workspace_id, None)
|
||||
self.p_attached.pop(workspace_id, None)
|
||||
rt = self.runtimes.pop(workspace_id, None)
|
||||
if rt is None:
|
||||
return
|
||||
@@ -625,12 +625,12 @@ class AppRuntimeManager:
|
||||
if not rt.running:
|
||||
to_reap.append(rt)
|
||||
else:
|
||||
self._idle_lru[workspace_id] = rt
|
||||
self._idle_lru.move_to_end(workspace_id)
|
||||
self.idle_lru[workspace_id] = rt
|
||||
self.idle_lru.move_to_end(workspace_id)
|
||||
suspend_process_tree(rt.process)
|
||||
rt._suspended = True
|
||||
while len(self._idle_lru) > MAX_IDLE_RUNTIMES:
|
||||
_, old_rt = self._idle_lru.popitem(last=False)
|
||||
rt.p_suspended = True
|
||||
while len(self.idle_lru) > MAX_IDLE_RUNTIMES:
|
||||
_, old_rt = self.idle_lru.popitem(last=False)
|
||||
# Reaping a stopped process: SIGCONT first so the
|
||||
# SIGTERM in stop() can be delivered cleanly (a
|
||||
# SIGSTOP'd process can't run its own shutdown).
|
||||
@@ -647,7 +647,7 @@ class AppRuntimeManager:
|
||||
except Exception:
|
||||
logger.exception("failed to reap idle runtime %s", workspace_id)
|
||||
if to_idle is not None:
|
||||
logger.debug("workspace %s idled (LRU size now %d)", workspace_id, len(self._idle_lru))
|
||||
logger.debug("workspace %s idled (LRU size now %d)", workspace_id, len(self.idle_lru))
|
||||
|
||||
def get(self, workspace_id: str) -> Optional[AppRuntime]:
|
||||
# Active subscribers see the live runtime; idle-pool members
|
||||
@@ -656,7 +656,7 @@ class AppRuntimeManager:
|
||||
rt = self.runtimes.get(workspace_id)
|
||||
if rt is not None:
|
||||
return rt
|
||||
return self._idle_lru.get(workspace_id)
|
||||
return self.idle_lru.get(workspace_id)
|
||||
|
||||
def drain_errors_for_path(self, file_path: str) -> list[str]:
|
||||
"""If `file_path` falls under one of the live workspace
|
||||
@@ -675,7 +675,7 @@ class AppRuntimeManager:
|
||||
# navigated away from the workspace mid-build, but the agent
|
||||
# could still be editing files; the LRU keeps the runtime alive
|
||||
# for ~3 idle slots.
|
||||
for rt in (*self.runtimes.values(), *self._idle_lru.values()):
|
||||
for rt in (*self.runtimes.values(), *self.idle_lru.values()):
|
||||
try:
|
||||
ws_root = os.path.abspath(rt.workspace_path)
|
||||
except Exception:
|
||||
@@ -685,18 +685,18 @@ class AppRuntimeManager:
|
||||
return []
|
||||
|
||||
def get_render_state_for_workspace(self, workspace_id: str) -> tuple[Optional[str], str]:
|
||||
rt = self.runtimes.get(workspace_id) or self._idle_lru.get(workspace_id)
|
||||
rt = self.runtimes.get(workspace_id) or self.idle_lru.get(workspace_id)
|
||||
if rt is None:
|
||||
return None, ""
|
||||
return rt.render_state, rt.render_error_text
|
||||
|
||||
def reset_render_state_for_workspace(self, workspace_id: str) -> None:
|
||||
rt = self.runtimes.get(workspace_id) or self._idle_lru.get(workspace_id)
|
||||
rt = self.runtimes.get(workspace_id) or self.idle_lru.get(workspace_id)
|
||||
if rt is not None:
|
||||
rt.reset_render_state()
|
||||
|
||||
async def restart(self, workspace_id: str, workspace_path: Optional[str] = None) -> Optional[AppRuntime]:
|
||||
rt = self.runtimes.get(workspace_id) or self._idle_lru.get(workspace_id)
|
||||
rt = self.runtimes.get(workspace_id) or self.idle_lru.get(workspace_id)
|
||||
if rt is None:
|
||||
return None
|
||||
if workspace_path:
|
||||
@@ -718,12 +718,12 @@ class AppRuntimeManager:
|
||||
victims: list[AppRuntime] = []
|
||||
for rt in list(self.runtimes.values()):
|
||||
victims.append(rt)
|
||||
for rt in list(self._idle_lru.values()):
|
||||
for rt in list(self.idle_lru.values()):
|
||||
resume_process_tree(rt.process)
|
||||
victims.append(rt)
|
||||
self.runtimes.clear()
|
||||
self._idle_lru.clear()
|
||||
self._attached.clear()
|
||||
self.idle_lru.clear()
|
||||
self.p_attached.clear()
|
||||
if not victims:
|
||||
return 0
|
||||
await asyncio.gather(
|
||||
|
||||
@@ -41,10 +41,10 @@ def p_created_with() -> str:
|
||||
|
||||
class p_Ctx:
|
||||
def __init__(self, local_to_bundle: dict[tuple, str]):
|
||||
self._m = local_to_bundle
|
||||
self.p_m = local_to_bundle
|
||||
|
||||
def bundle_id_for(self, etype: EntityType, local_id: str) -> str | None:
|
||||
return self._m.get((etype, local_id))
|
||||
return self.p_m.get((etype, local_id))
|
||||
|
||||
|
||||
# ---------- export ----------
|
||||
|
||||
@@ -19,7 +19,7 @@ class DashboardExportable:
|
||||
def __init__(self, did: str, name: str, data: dict):
|
||||
self.local_id = did
|
||||
self.name = name
|
||||
self._data = data
|
||||
self.p_data = data
|
||||
|
||||
@classmethod
|
||||
def load(cls, local_id: str) -> "DashboardExportable | None":
|
||||
@@ -29,7 +29,7 @@ class DashboardExportable:
|
||||
return cls(local_id, data.get("name") or "Dashboard", data)
|
||||
|
||||
def serialize(self, ctx: ExportContext) -> dict:
|
||||
layout = dict(self._data.get("layout") or {})
|
||||
layout = dict(self.p_data.get("layout") or {})
|
||||
cards = {}
|
||||
for sid, card in (layout.get("cards") or {}).items():
|
||||
bid = ctx.bundle_id_for(EntityType.session, sid)
|
||||
@@ -53,7 +53,7 @@ class DashboardExportable:
|
||||
c["spawned_by"] = ctx.bundle_id_for(EntityType.session, spawn) if spawn else None
|
||||
browser_cards[bkey] = c
|
||||
expanded = [b for b in (ctx.bundle_id_for(EntityType.session, s) for s in (layout.get("expanded_session_ids") or [])) if b]
|
||||
return {"name": self._data.get("name") or "Dashboard", "layout": {
|
||||
return {"name": self.p_data.get("name") or "Dashboard", "layout": {
|
||||
**layout, "cards": cards, "view_cards": view_cards,
|
||||
"browser_cards": browser_cards, "notes": layout.get("notes") or {},
|
||||
"expanded_session_ids": expanded,
|
||||
@@ -63,7 +63,7 @@ class DashboardExportable:
|
||||
return {}
|
||||
|
||||
def dependencies(self) -> list[DepRef]:
|
||||
layout = self._data.get("layout") or {}
|
||||
layout = self.p_data.get("layout") or {}
|
||||
deps = [DepRef(EntityType.session, sid, "has_agent") for sid in (layout.get("cards") or {})]
|
||||
deps += [DepRef(EntityType.app, oid, "has_app") for oid in (layout.get("view_cards") or {})]
|
||||
return deps
|
||||
|
||||
@@ -19,7 +19,7 @@ class ModeExportable:
|
||||
def __init__(self, mode_id: str, name: str, data: dict):
|
||||
self.local_id = mode_id
|
||||
self.name = name
|
||||
self._data = data
|
||||
self.p_data = data
|
||||
|
||||
@classmethod
|
||||
def load(cls, local_id: str) -> "ModeExportable | None":
|
||||
@@ -33,7 +33,7 @@ class ModeExportable:
|
||||
return cls(local_id, d.get("name") or local_id, d)
|
||||
|
||||
def serialize(self, ctx: ExportContext) -> dict:
|
||||
return {k: v for k, v in self._data.items() if k not in P_DROP}
|
||||
return {k: v for k, v in self.p_data.items() if k not in P_DROP}
|
||||
|
||||
def files(self) -> dict[str, bytes]:
|
||||
return {}
|
||||
|
||||
@@ -32,7 +32,7 @@ class SessionExportable:
|
||||
def __init__(self, sid: str, name: str, data: dict):
|
||||
self.local_id = sid
|
||||
self.name = name
|
||||
self._data = data
|
||||
self.p_data = data
|
||||
|
||||
@classmethod
|
||||
def load(cls, local_id: str) -> "SessionExportable | None":
|
||||
@@ -52,31 +52,31 @@ class SessionExportable:
|
||||
return cls(local_id, d.get("name") or "Agent", d)
|
||||
|
||||
def serialize(self, ctx: ExportContext) -> dict:
|
||||
return {k: self._data.get(k) for k in P_KEEP if k in self._data}
|
||||
return {k: self.p_data.get(k) for k in P_KEEP if k in self.p_data}
|
||||
|
||||
def files(self) -> dict[str, bytes]:
|
||||
return {}
|
||||
|
||||
def dependencies(self) -> list[DepRef]:
|
||||
mode = self._data.get("mode")
|
||||
mode = self.p_data.get("mode")
|
||||
if mode and mode not in P_BUILTIN_MODES:
|
||||
return [DepRef(EntityType.mode, mode, "uses_mode")]
|
||||
return []
|
||||
|
||||
def requirements(self) -> list[Requirement]:
|
||||
reqs: list[Requirement] = []
|
||||
for mcp in self._data.get("active_mcps") or []:
|
||||
for mcp in self.p_data.get("active_mcps") or []:
|
||||
reqs.append(Requirement(
|
||||
kind=RequirementKind.mcp_action, key=mcp, label=mcp,
|
||||
detail="An agent here uses this action.",
|
||||
))
|
||||
mode = self._data.get("mode") or "agent"
|
||||
mode = self.p_data.get("mode") or "agent"
|
||||
if mode in P_BUILTIN_MODES and mode != "agent":
|
||||
reqs.append(Requirement(
|
||||
kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode",
|
||||
detail="A built-in mode an agent runs in.",
|
||||
))
|
||||
provider = self._data.get("provider") or "anthropic"
|
||||
provider = self.p_data.get("provider") or "anthropic"
|
||||
reqs.append(Requirement(
|
||||
kind=RequirementKind.api_key, key=provider, label=f"A {provider} model",
|
||||
detail="Set up this provider so the agents can run.",
|
||||
|
||||
@@ -20,8 +20,8 @@ class SkillExportable:
|
||||
def __init__(self, local_id: str, name: str, payload: dict, files: dict[str, bytes] | None = None):
|
||||
self.local_id = local_id
|
||||
self.name = name
|
||||
self._payload = payload
|
||||
self._files = files or {}
|
||||
self.payload = payload
|
||||
self.p_files = files or {}
|
||||
|
||||
@classmethod
|
||||
def load(cls, local_id: str) -> "SkillExportable | None":
|
||||
@@ -46,10 +46,10 @@ class SkillExportable:
|
||||
return cls(local_id, name, payload, files)
|
||||
|
||||
def serialize(self, ctx: ExportContext) -> dict:
|
||||
return dict(self._payload)
|
||||
return dict(self.payload)
|
||||
|
||||
def files(self) -> dict[str, bytes]:
|
||||
return dict(self._files)
|
||||
return dict(self.p_files)
|
||||
|
||||
def dependencies(self) -> list[DepRef]:
|
||||
return []
|
||||
|
||||
@@ -48,7 +48,7 @@ class WorkflowExportable:
|
||||
def __init__(self, local_id: str, name: str, data: dict):
|
||||
self.local_id = local_id
|
||||
self.name = name
|
||||
self._data = data
|
||||
self.p_data = data
|
||||
|
||||
@classmethod
|
||||
def load(cls, local_id: str) -> "WorkflowExportable | None":
|
||||
@@ -62,7 +62,7 @@ class WorkflowExportable:
|
||||
return cls(local_id, data.get("title") or "Untitled workflow", data)
|
||||
|
||||
def serialize(self, ctx: ExportContext) -> dict:
|
||||
return sanitize_workflow(self._data)
|
||||
return sanitize_workflow(self.p_data)
|
||||
|
||||
def files(self) -> dict[str, bytes]:
|
||||
return {}
|
||||
@@ -72,18 +72,18 @@ class WorkflowExportable:
|
||||
|
||||
def requirements(self) -> list[Requirement]:
|
||||
reqs: list[Requirement] = []
|
||||
for name in (self._data.get("actions") or {}).get("configured_sets") or []:
|
||||
for name in (self.p_data.get("actions") or {}).get("configured_sets") or []:
|
||||
reqs.append(Requirement(
|
||||
kind=RequirementKind.mcp_action, key=name, label=name,
|
||||
detail="This workflow uses this action.",
|
||||
))
|
||||
mode = self._data.get("mode") or "agent"
|
||||
mode = self.p_data.get("mode") or "agent"
|
||||
if mode in P_BUILTIN_MODES and mode != "agent":
|
||||
reqs.append(Requirement(
|
||||
kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode",
|
||||
detail="A built-in mode this workflow runs in.",
|
||||
))
|
||||
provider = self._data.get("provider") or "anthropic"
|
||||
provider = self.p_data.get("provider") or "anthropic"
|
||||
reqs.append(Requirement(
|
||||
kind=RequirementKind.api_key, key=provider, label=f"A {provider} model",
|
||||
detail="Set up this provider to run the workflow.",
|
||||
|
||||
@@ -27,13 +27,13 @@ class RemapTable:
|
||||
"""bundle_id -> fresh local id, filled as import walks entities leaves-first."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._m: dict[str, str] = {}
|
||||
self.p_m: dict[str, str] = {}
|
||||
|
||||
def assign(self, bundle_id: str, local_id: str) -> None:
|
||||
self._m[bundle_id] = local_id
|
||||
self.p_m[bundle_id] = local_id
|
||||
|
||||
def local(self, bundle_id: str) -> str | None:
|
||||
return self._m.get(bundle_id)
|
||||
return self.p_m.get(bundle_id)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
|
||||
@@ -71,10 +71,10 @@ def test_audit_fires_every_n_finished_tasks(monkeypatch, tmp_path):
|
||||
|
||||
class p_SyncThread:
|
||||
def __init__(self, target=None, **kw):
|
||||
self._t = target
|
||||
self.p_t = target
|
||||
|
||||
def start(self):
|
||||
self._t()
|
||||
self.p_t()
|
||||
monkeypatch.setattr(m.threading, "Thread", p_SyncThread)
|
||||
|
||||
log = [{"tool": "BrowserClickIndex", "elapsed_ms": 5, "result_summary": "ok"}]
|
||||
|
||||
@@ -20,16 +20,16 @@ class p_FakeClient:
|
||||
"""Minimal Anthropic-shaped client: client.messages.create(...)."""
|
||||
|
||||
def __init__(self, resp=None, raise_exc=None):
|
||||
self._resp = resp
|
||||
self._raise = raise_exc
|
||||
self.p_resp = resp
|
||||
self.p_raise = raise_exc
|
||||
self.calls = []
|
||||
self.messages = self
|
||||
|
||||
async def create(self, **kwargs):
|
||||
self.calls.append(kwargs)
|
||||
if self._raise:
|
||||
raise self._raise
|
||||
return self._resp
|
||||
if self.p_raise:
|
||||
raise self.p_raise
|
||||
return self.p_resp
|
||||
|
||||
|
||||
def test_returns_extracted_guidance_and_assembles_prompt():
|
||||
|
||||
@@ -286,7 +286,7 @@ def test_terminal_event_visible_after_full_eviction(p_patch_persist_dir):
|
||||
|
||||
# Simulate a process restart: clear the in-memory ring buffer
|
||||
# but keep the persisted terminal file.
|
||||
seq_log._per_session.pop(sid, None)
|
||||
seq_log.per_session.pop(sid, None)
|
||||
|
||||
with client.websocket_connect(f"/ws/agents/{sid}") as ws:
|
||||
ws.send_text(json.dumps({"event": "client:hello", "data": {"last_seq": 0, "connection_uuid": "c1"}}))
|
||||
|
||||
@@ -146,7 +146,7 @@ async def test_stop_all_kills_active():
|
||||
await asyncio.sleep(0.05)
|
||||
else:
|
||||
raise AssertionError(f"port {port} not released after stop_all")
|
||||
assert not m.runtimes and not m._idle_lru, "manager should be empty after stop_all"
|
||||
assert not m.runtimes and not m.idle_lru, "manager should be empty after stop_all"
|
||||
print("PASS test_stop_all_kills_active")
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ async def test_stop_all_kills_idle():
|
||||
# Detach -> moves into LRU + SIGSTOP'd. If stop_all forgets to
|
||||
# SIGCONT before SIGTERM, the kill queues and the process hangs.
|
||||
await m.detach("ws-idle")
|
||||
assert "ws-idle" in m._idle_lru, "should be in idle LRU"
|
||||
assert "ws-idle" in m.idle_lru, "should be in idle LRU"
|
||||
# Confirm the process is suspended (T state on Linux, T on darwin).
|
||||
# Skip the OS check; just rely on the eventual kill working.
|
||||
killed = await m.stop_all()
|
||||
|
||||
@@ -168,7 +168,7 @@ def test_swarm_export_folder_skill_carries_supporting_files(skills_dir):
|
||||
files = exp.files()
|
||||
assert "scripts/go.py" in files
|
||||
assert files["scripts/go.py"] == b"print(1)"
|
||||
assert exp._payload["content"] == "render"
|
||||
assert exp.payload["content"] == "render"
|
||||
|
||||
|
||||
def test_swarm_import_writes_folder_when_files_present(skills_dir):
|
||||
|
||||
@@ -28,7 +28,7 @@ class p_FakeResp:
|
||||
class p_FakeClient:
|
||||
"""Stands in for httpx.AsyncClient; returns a canned response."""
|
||||
def __init__(self, resp: p_FakeResp):
|
||||
self._resp = resp
|
||||
self.p_resp = resp
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
@@ -37,7 +37,7 @@ class p_FakeClient:
|
||||
return False
|
||||
|
||||
async def post(self, *a, **k):
|
||||
return self._resp
|
||||
return self.p_resp
|
||||
|
||||
|
||||
def p_patch_client(monkeypatch, resp: p_FakeResp):
|
||||
|
||||
Reference in New Issue
Block a user