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:
Charles YaoandClaude Opus 4.8 committed 2026-08-23 05:42:18 -07:00
1 parent 7b41d09549
commit 47937f739f
4 files changed
+64 -2

No files matched your search

+6
View File
@@ -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:
+8 -2
View File
@@ -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