[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:
ciregenz
2026-08-17 14:47:23 -07:00
parent 1179faaa57
commit 8e25440c95
2 changed files with 88 additions and 0 deletions
+41
View File
@@ -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 == []