mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
copilot: pause/resume (runner stop/resume short-circuit + RunStatus.PAUSED)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b41d09549
commit
47937f739f
4 files changed
+64
-2
No files matched your search
@@ -95,6 +95,12 @@ class Runner:
|
||||
f"copilot: stale base_version {turn.action.base_version} for session {session_id} (current {s.version})"
|
||||
)
|
||||
|
||||
action_kind = turn.action.kind if turn.action is not None else ""
|
||||
if action_kind in ("stop", "resume"):
|
||||
fc.paused = action_kind == "stop"
|
||||
s.version = self._env.repo.compare_and_advance(s.id, s.version, s.current_state, fc, [])
|
||||
return s
|
||||
|
||||
first = True
|
||||
while True:
|
||||
if is_waiting(s.current_state) and first and turn.action is None:
|
||||
|
||||
@@ -240,7 +240,7 @@ def resolve_action_kind(raw: str) -> str:
|
||||
return _ACTION_ID_TO_KIND.get(raw, raw)
|
||||
|
||||
|
||||
def _run_status(state: PcState) -> RunStatus:
|
||||
def _run_status(state: PcState, paused: bool = False) -> RunStatus:
|
||||
"""Port of Go ``runStatusFor``, widened to the ``RunStatus`` enum (spec
|
||||
§2). Deliberate wire-value change from the old string: ``waiting-input``
|
||||
(hyphen) -> ``RunStatus.WAITING_INPUT`` = ``"waiting_input"``
|
||||
@@ -251,11 +251,17 @@ def _run_status(state: PcState) -> RunStatus:
|
||||
complete``) but are not in ``_WORKING``/``_WAITING`` and are not
|
||||
``SUCCESS``/``FAILED`` -- without this ordering they'd wrongly fall
|
||||
through to EXECUTING.
|
||||
|
||||
``paused`` (Task 7, ``fc.paused``) only applies at a waiting state -- the
|
||||
canvas stays editable while paused -- and never overrides a terminal
|
||||
outcome (FAILED/COMPLETE win regardless of the flag).
|
||||
"""
|
||||
if state == PcState.FAILED:
|
||||
return RunStatus.FAILED
|
||||
if is_terminal(state): # SUCCESS, BUILD_COMPLETE, EDIT_PUBLISH
|
||||
return RunStatus.COMPLETE
|
||||
if paused and is_waiting(state):
|
||||
return RunStatus.PAUSED
|
||||
if is_waiting(state):
|
||||
return RunStatus.WAITING_INPUT
|
||||
if is_working(state):
|
||||
@@ -385,7 +391,7 @@ class WorkflowCopilotService:
|
||||
version=s.version,
|
||||
state=str(st),
|
||||
canvas_read_only=canvas_read_only(st),
|
||||
run_status=_run_status(st),
|
||||
run_status=_run_status(st, paused=fc.paused),
|
||||
interrupted=is_working(st) and not self._session_lock.exists(session_id),
|
||||
conversation=items,
|
||||
entry_mode=s.entry_mode,
|
||||
|
||||
@@ -223,3 +223,42 @@ def test_edit_apply_changes_undo_restores_pre_edit_graph():
|
||||
assert dify.read_graph("app", _actor())[0] == pre_edit # config edit undone
|
||||
assert res.context.checkpoint_id == ""
|
||||
assert {"event": "revert_checkpoint"} in events
|
||||
|
||||
|
||||
def test_runner_stop_sets_paused_same_state_and_bumps_version():
|
||||
from datetime import datetime
|
||||
|
||||
from core.workflow_copilot.models import Action, ConversationItem, CopilotContext, EntryMode, Session, Turn
|
||||
from core.workflow_copilot.placeholder_agent import PlaceholderAgent
|
||||
from core.workflow_copilot.runner import Env, Runner
|
||||
from core.workflow_copilot.state import PcState
|
||||
from tests.unit_tests.core.workflow_copilot.fakes import FakeDifyPort, InMemoryRepository
|
||||
|
||||
repo = InMemoryRepository()
|
||||
env = Env(dify=FakeDifyPort(), agent=PlaceholderAgent(), repo=repo, now=lambda: datetime.min)
|
||||
s = Session(app_id="app", tenant_id="t", owner_account_id="a",
|
||||
entry_mode=EntryMode.BUILD, current_state=PcState.BUILD_REVIEW)
|
||||
repo.create_session(s, CopilotContext(), [ConversationItem(kind="user", seq=0)])
|
||||
runner = Runner(env, {}) # empty registry: stop must not need a handler
|
||||
|
||||
out = runner.advance(s.id, Turn(action=Action(kind="stop", base_version=1), actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_REVIEW # unchanged
|
||||
assert out.version == 2 # committed
|
||||
_s, fc = repo.get_session(s.id)
|
||||
assert fc.paused is True
|
||||
|
||||
out = runner.advance(s.id, Turn(action=Action(kind="resume", base_version=2), actor=_actor()))
|
||||
assert out.current_state == PcState.BUILD_REVIEW
|
||||
_s, fc = repo.get_session(s.id)
|
||||
assert fc.paused is False
|
||||
|
||||
|
||||
def test_run_status_paused_when_flag_set():
|
||||
from core.workflow_copilot.contract import RunStatus
|
||||
from core.workflow_copilot.state import PcState
|
||||
from services.workflow_copilot.service import _run_status
|
||||
|
||||
assert _run_status(PcState.BUILD_REVIEW, paused=True) == RunStatus.PAUSED
|
||||
assert _run_status(PcState.BUILD_REVIEW, paused=False) == RunStatus.WAITING_INPUT
|
||||
# paused never overrides terminal/failed
|
||||
assert _run_status(PcState.BUILD_COMPLETE, paused=True) == RunStatus.COMPLETE
|
||||
@@ -633,3 +633,14 @@ def test_get_session_view_no_checkpoint_when_unset(
|
||||
repo.create_session(s, CopilotContext(), [ConversationItem(kind="user", seq=0)])
|
||||
view = service.get_session_view(s.id, _actor())
|
||||
assert view.checkpoint is None
|
||||
|
||||
|
||||
def test_get_session_view_run_status_paused(
|
||||
service: WorkflowCopilotService, repo: SqlCopilotRepository
|
||||
) -> None:
|
||||
s = Session(app_id=APP_ID, tenant_id=TENANT_ID, owner_account_id=ACCOUNT_ID,
|
||||
entry_mode=EntryMode.EDIT, current_state=PcState.EDIT_REVIEW)
|
||||
repo.create_session(s, CopilotContext(paused=True), [ConversationItem(kind="user", seq=0)])
|
||||
view = service.get_session_view(s.id, _actor())
|
||||
assert view.run_status == "paused"
|
||||
assert view.canvas_read_only is False # editable while paused
|
||||
Reference in new issue
Block a user