- 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>
60 lines
2.8 KiB
Python
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
|