Files
DemonGatanjieu-Anomalous_Mo…/api/ai_task_changes.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

60 lines
2.8 KiB
Python

"""The user's own changes in Anomalous while an AI works on a task: each activity log entry the
user makes (activity_log.py tells note_activity of every new entry) becomes a step of the task
the AI called within USER_STEP_SECONDS, and the entry names that task back. The AI is told of it
with its next call (ai_panels.attach_pending, a message of kind "change"), so it keeps the user's
change instead of working over it. The AI's own canvas changes (via mcp) are its steps already;
which actions count, and their stage, is action_catalog.USER_ACTIONS."""
import time
import uuid
from . import action_catalog, activity_log, ai_tasks
USER_STEP_SECONDS = 10 * 60
def _live_task(now):
"""The task an AI is working on now (it called within USER_STEP_SECONDS), or None."""
live = [task for task in ai_tasks._load().values() if not task.get("ended") and not task.get("waiting")
and now - task.get("updated", 0) < USER_STEP_SECONDS]
return max(live, key=lambda task: task.get("updated", 0)) if live else None
def _change_text(entry):
"""What the user changed, in words for the AI."""
if entry.get("source") != "canvas":
words = str(entry.get("action", "")).replace("_", " ")
return f"{words}: {entry['target']}" if entry.get("target") else words
lines = []
for change in (entry.get("detail") or {}).get("changes") or []:
if change.get("kind") == "changed":
lines.append(f"{change.get('node')} {change.get('widget')}: {change.get('before')} -> {change.get('after')}")
elif change.get("kind") == "opened":
lines.append(f"opened the workflow {change.get('node')}")
else:
lines.append(f"{change.get('kind')} {change.get('node')}")
return "Changed on the canvas: " + "; ".join(lines[:8])
def note_activity(entry):
"""A new activity log entry: a step of the AI's task when the user made it while the AI worked."""
stage = action_catalog.USER_ACTIONS.get(entry.get("action"))
if stage is None or (entry.get("detail") or {}).get("via") == "mcp":
return None
at = entry.get("time") or time.time()
with ai_tasks._lock:
task = _live_task(at)
if not task:
return None
step = {"id": uuid.uuid4().hex[:10], "at": at, "ms": 0, "tool": "user_activity", "kind": "user", "stage": stage,
"ok": True, "state": "done", "summary": entry.get("action", ""),
"text": ai_tasks._short(_change_text(entry), ai_tasks.MAX_NOTE), "activity": entry["id"],
"delivered": False, "results": []}
# Appended without moving "updated": the user's own changes do not keep the AI's task open.
task["steps"].append(step)
del task["steps"][:-ai_tasks.MAX_STEPS]
ai_tasks._save(task)
activity_log.tag(entry["id"], task["id"])
ai_tasks._notify(task["id"])
return step