Files
DemonGatanjieu-Anomalous_Mo…/api/ai_task_runs.py
T
DemonGatanjieuandClaude Opus 5.5 00958578fb AI workbench: one action catalog; your own changes join the AI's task; tick two pictures to compare
- api/action_catalog.py: every MCP tool's kind, stage of the work and gate in one place (a test
  keeps it and the tools in step); steps now carry their stage.
- api/ai_task_changes.py: changes the user makes in Anomalous while an AI works become steps of
  its task, reach the AI once with its next call (kind "change": keep it), and the activity
  entry names the task; the activity page links to it ("AI task ->").
- api/ai_task_runs.py: run watching moved out of ai_tasks.py (600-line rule); a step a restart
  left "running" is marked stopped when the record is read again.
- Pictures: inspector / compare with canvas / keep as tools on hover instead of three buttons
  under every picture; tick any two (any step or task) and compare from the bar at the foot of
  the page; the results strip shows thumbnails again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:38:37 +08:00

105 lines
4.1 KiB
Python

"""What a workflow run an AI started made: after run_workflow (mcp_server.py), the prompts
ComfyUI queued for it are watched in its history until they end, and the pictures and videos
they saved become the step's results (ai_tasks.py keeps the step). A thread per run, polling
every RUN_POLL_SECONDS, for at most ai_tasks.RUN_WATCH_SECONDS; nothing runs when no AI ran
anything."""
import threading
import time
from . import ai_tasks
RUN_POLL_SECONDS = 2.0
def _queue():
from server import PromptServer
return PromptServer.instance.prompt_queue
def prompt_ids():
"""Prompts queued, running or recently done: before and after run_workflow, the new ones are its."""
try:
queue = _queue()
running, pending = queue.get_current_queue()
ids = {item[1] for item in [*running, *pending]}
ids.update(queue.get_history(max_items=64).keys())
return ids
except Exception: # noqa: BLE001 - no server (tests): nothing to watch
return set()
def outputs_of(record_):
"""Saved pictures and videos in one history record."""
out = []
for output in (record_.get("outputs") or {}).values():
for key in ("images", "gifs", "videos"):
for item in output.get(key) or []:
if isinstance(item, dict) and item.get("type") == "output" and item.get("filename"):
out.append({"type": "image", "filename": item["filename"], "subfolder": item.get("subfolder") or "",
"folder": "output"})
return out
def _finish_run(task_id, step_id, prompts, history):
images, failed, done = [], False, 0
for prompt_id in prompts:
record_ = history.get(prompt_id)
if not record_:
continue
status = record_.get("status") or {}
if status.get("status_str") == "error":
failed = True
done += 1
elif status.get("completed"):
done += 1
for image in outputs_of(record_):
if image not in images: # a prompt ComfyUI answers from its cache repeats the same file
images.append(image)
with ai_tasks._lock:
task = ai_tasks._load().get(task_id)
step = next((item for item in (task or {}).get("steps", []) if item["id"] == step_id), None)
if not step:
return True
step["results"] = [item for item in step["results"] if item["type"] != "image"] + images[:ai_tasks.MAX_RESULTS]
step["state"] = "running" if done < len(prompts) else ("error" if failed else "done")
step["prompts"] = len(prompts)
if failed:
step["ok"] = False
step["error"] = "ComfyUI 运行出错 / The run failed"
if done >= len(prompts):
step["ms"] = int((time.time() - step["at"]) * 1000)
task["updated"] = max(task["updated"], time.time())
ai_tasks._save(task)
ai_tasks._notify(task_id)
return done >= len(prompts)
def watch(step_id, before):
"""Follows the run of step `step_id` (prompts not in `before`) until it ends."""
with ai_tasks._lock:
task_id = next((task["id"] for task in ai_tasks._load().values()
if any(step["id"] == step_id for step in task.get("steps") or [])), None)
if not task_id:
return
def follow():
start = time.monotonic()
prompts = []
while time.monotonic() - start < ai_tasks.RUN_WATCH_SECONDS:
if not prompts:
prompts = sorted(prompt_ids() - before)
if not prompts and time.monotonic() - start > 10:
_finish_run(task_id, step_id, [], {})
return # nothing was queued after all
if prompts:
try:
history = {pid: _queue().get_history(prompt_id=pid).get(pid) for pid in prompts}
except Exception: # noqa: BLE001
return
if _finish_run(task_id, step_id, prompts, {k: v for k, v in history.items() if v}):
return
time.sleep(RUN_POLL_SECONDS)
threading.Thread(target=follow, name="anomalous-ai-run", daemon=True).start()