mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-26 14:32:22 +02:00
[eric] workflows: a stepless run rescues itself by committing the pending draft or synthesizing the build request as a step (ENG-335)
This commit is contained in:
@@ -8,6 +8,7 @@ routing, retries, and history all aligned with the rest of the app.
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from uuid import uuid4
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
@@ -86,6 +87,44 @@ def _resolve_allowed_tools(wf: Workflow) -> Optional[list[str]]:
|
||||
return list(wf.actions.configured_sets)
|
||||
|
||||
|
||||
def rescue_missing_steps(wf: Workflow) -> bool:
|
||||
"""A run on a stepless workflow used to be a guaranteed failure, and the builder agent could
|
||||
manufacture that state by answering the user's request without ever staging steps (ENG-335:
|
||||
"Ran 3 steps", saved zero, scheduled every 15 minutes into a wall). Prompt rules did not hold,
|
||||
so the seal is mechanical, at the one gate every run passes: promote an uncommitted draft
|
||||
(activating a workflow IS committing to it), else synthesize one step from the build chat's
|
||||
first real user message, which is a perfectly runnable workflow (the runner is a full agent).
|
||||
Returns whether live steps now exist."""
|
||||
from backend.apps.workflows.models import WorkflowStep
|
||||
try:
|
||||
if wf.draft_steps:
|
||||
live = [s for s in wf.draft_steps if s.enabled and s.text and s.text.strip()]
|
||||
if live:
|
||||
wf.steps = list(wf.draft_steps)
|
||||
wf.draft_steps = None
|
||||
storage.save_workflow(wf)
|
||||
logger.warning(f"[workflow {wf.id}] rescued stepless run by committing the pending draft ({len(live)} steps)")
|
||||
return True
|
||||
edit_sid = getattr(wf, "edit_agent_session_id", None)
|
||||
if edit_sid:
|
||||
from backend.apps.agents.manager.session.session_store import load_session_data
|
||||
data = load_session_data(edit_sid) or {}
|
||||
first = next(
|
||||
(m for m in data.get("messages", [])
|
||||
if m.get("role") == "user" and not m.get("hidden") and str(m.get("content") or "").strip()),
|
||||
None,
|
||||
)
|
||||
if first:
|
||||
text = str(first["content"]).strip()
|
||||
wf.steps = [WorkflowStep(id=uuid4().hex, text=text, label=text[:48], enabled=True)]
|
||||
storage.save_workflow(wf)
|
||||
logger.warning(f"[workflow {wf.id}] rescued stepless run by synthesizing a step from the build chat's request")
|
||||
return True
|
||||
except Exception:
|
||||
logger.exception(f"[workflow {wf.id}] stepless-rescue failed; the honest no-steps error stands")
|
||||
return False
|
||||
|
||||
|
||||
def resolve_workflow_dashboard_id(wf: Workflow) -> Optional[str]:
|
||||
"""Pick the dashboard this run's agent attaches to, so browser tools work like in chat.
|
||||
|
||||
@@ -323,6 +362,8 @@ async def execute(
|
||||
pass
|
||||
|
||||
steps = [s for s in wf.steps if s.enabled and s.text and s.text.strip()]
|
||||
if not steps and rescue_missing_steps(wf):
|
||||
steps = [s for s in wf.steps if s.enabled and s.text and s.text.strip()]
|
||||
if not steps:
|
||||
raise ValueError("Workflow has no steps")
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""ENG-335: the builder agent can save a workflow with ZERO steps after visibly doing the work
|
||||
("Ran 3 steps", saved none), turning every scheduled run into "Workflow has no steps". Prompt
|
||||
rules didn't hold, so the seal is mechanical at the run gate: promote an uncommitted draft, else
|
||||
synthesize one step from the build chat's first user message. These pin all three ladder rungs."""
|
||||
|
||||
from backend.apps.workflows import executor, storage
|
||||
from backend.apps.workflows.models import Workflow, WorkflowStep
|
||||
|
||||
|
||||
def p_wf(**kw) -> Workflow:
|
||||
wf = Workflow(title="T", description="", system_prompt="", steps=[], **kw)
|
||||
return wf
|
||||
|
||||
|
||||
def test_pending_draft_is_promoted(monkeypatch, tmp_path):
|
||||
saved = {}
|
||||
monkeypatch.setattr(storage, "save_workflow", lambda w: saved.setdefault("wf", w))
|
||||
wf = p_wf()
|
||||
wf.draft_steps = [WorkflowStep(id="d1", text="check example.com", label="Check", enabled=True)]
|
||||
assert executor.rescue_missing_steps(wf) is True
|
||||
assert [s.text for s in wf.steps] == ["check example.com"]
|
||||
assert wf.draft_steps is None, "activating the workflow IS the commit"
|
||||
assert saved["wf"] is wf
|
||||
|
||||
|
||||
def test_step_synthesized_from_build_chat_first_message(monkeypatch):
|
||||
monkeypatch.setattr(storage, "save_workflow", lambda w: None)
|
||||
from backend.apps.agents.manager.session import session_store
|
||||
monkeypatch.setattr(session_store, "load_session_data", lambda sid: {
|
||||
"messages": [
|
||||
{"role": "assistant", "content": "hello"},
|
||||
{"role": "user", "hidden": True, "content": "[Automated message] continue"},
|
||||
{"role": "user", "content": "plan my lunch near 501 folsom st"},
|
||||
],
|
||||
})
|
||||
wf = p_wf()
|
||||
wf.edit_agent_session_id = "sess-1"
|
||||
assert executor.rescue_missing_steps(wf) is True
|
||||
assert wf.steps[0].text == "plan my lunch near 501 folsom st"
|
||||
assert wf.steps[0].enabled is True
|
||||
|
||||
|
||||
def test_nothing_to_rescue_keeps_the_honest_error(monkeypatch):
|
||||
monkeypatch.setattr(storage, "save_workflow", lambda w: None)
|
||||
wf = p_wf()
|
||||
assert executor.rescue_missing_steps(wf) is False
|
||||
assert wf.steps == []
|
||||
Reference in New Issue
Block a user