Skip to content

Commit 40e2137

Browse files
committed
Handle commands the update the coder class in prompt queue feature
1 parent 64d00b8 commit 40e2137

3 files changed

Lines changed: 120 additions & 2 deletions

File tree

‎cecli/coders/base_coder.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2044,6 +2044,8 @@ def warn_paused():
20442044
" queued; use /run-queue to resume."
20452045
)
20462046

2047+
from cecli.helpers.agents.service import AgentService
2048+
20472049
self._processing_queue = True
20482050
try:
20492051
while True:
@@ -2063,7 +2065,18 @@ def warn_paused():
20632065
command_queue.requeue_active_prompt(self)
20642066
warn_paused()
20652067
raise
2066-
except (SwitchCoderSignal, SystemExit):
2068+
except SwitchCoderSignal as switch:
2069+
if not getattr(self, "parent_uuid", ""):
2070+
raise
2071+
2072+
service = AgentService.get_instance(self)
2073+
info = service.sub_agents.get(self.uuid)
2074+
if info is None:
2075+
raise
2076+
2077+
await service.switch_sub_agent_coder(info, switch, preproc)
2078+
return
2079+
except SystemExit:
20672080
raise
20682081
except Exception as e:
20692082
command_queue.clear_active_prompt(self)

‎cecli/helpers/agents/service.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,40 @@ async def _create_sub_agent_coder(
806806
# Public API
807807
# ------------------------------------------------------------------ #
808808

809+
async def switch_sub_agent_coder(self, info: SubAgentInfo, switch, preproc=True) -> Any:
810+
"""Atomically hand a sub-agent's remaining queue to a replacement coder.
811+
812+
Commands such as /add request a coder rebuild by raising SwitchCoderSignal.
813+
A sub-agent must absorb that signal locally: letting it escape recreates the
814+
primary coder from the sub-agent and leaves the main session in sub-agent
815+
mode. The active prompt has already run, so only the remaining queue moves.
816+
"""
817+
from cecli.coders import Coder
818+
from cecli.helpers import command_queue
819+
820+
old_coder = info.coder
821+
kwargs = dict(io=old_coder.io, from_coder=old_coder)
822+
kwargs.update(switch.kwargs)
823+
kwargs.pop("show_announcements", None)
824+
kwargs["from_coder"] = old_coder
825+
kwargs["args"] = old_coder.args
826+
kwargs["summarize_from_coder"] = False
827+
828+
new_coder = await Coder.create(**kwargs)
829+
830+
new_coder.prompt_queue = list(old_coder.prompt_queue)
831+
new_coder._queue_counter = old_coder._queue_counter
832+
old_coder.prompt_queue.clear()
833+
834+
command_queue.clear_active_prompt(old_coder)
835+
836+
info.coder = new_coder
837+
self.sub_agents[new_coder.uuid] = info
838+
self._uuid_coder_map[new_coder.uuid] = weakref.ref(new_coder)
839+
840+
await new_coder.process_prompt_queue(preproc)
841+
return new_coder
842+
809843
def start_generate_task(self, info: SubAgentInfo, user_message: str) -> asyncio.Task:
810844
"""Start a sub-agent's generate task in the background with status management.
811845

‎tests/commands/test_queue_commands.py‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import threading
44
import time
5-
from unittest.mock import MagicMock
5+
from unittest.mock import AsyncMock, MagicMock, patch
66

77
import pytest
88

@@ -813,6 +813,77 @@ async def swallowed_interrupt_generate(text, preproc=True):
813813
mock_io.tool_warning.assert_called_once()
814814

815815

816+
class TestSubAgentCoderSwitch:
817+
"""A sub-agent's queued command signal swaps coders instead of escaping."""
818+
819+
@pytest.mark.asyncio
820+
async def test_process_prompt_queue_moves_queue_to_replacement_coder(self, mock_io):
821+
"""A SwitchCoderSignal from a queued command switches only the sub-agent."""
822+
from cecli.coders.base_coder import Coder
823+
824+
coder = _make_coder_with_commands(("raises-switch", "second", "third"))
825+
coder.uuid = "sub-uuid"
826+
coder.parent_uuid = "primary-uuid"
827+
coder.io = mock_io
828+
coder.args = MagicMock()
829+
830+
async def signal_generate(text, preproc=True):
831+
raise SwitchCoderSignal(edit_format="subagent", from_coder=coder)
832+
833+
coder.generate = AsyncMock(side_effect=signal_generate)
834+
835+
new_coder = MagicMock()
836+
new_coder.uuid = "sub-uuid"
837+
new_coder.process_prompt_queue = AsyncMock()
838+
839+
from cecli.helpers.agents.service import AgentService, SubAgentInfo
840+
841+
parent = MagicMock()
842+
parent.uuid = "primary-uuid"
843+
service = AgentService(parent)
844+
info = SubAgentInfo(name="wf:storytime", coder=coder, parent_uuid="primary-uuid")
845+
service.sub_agents["sub-uuid"] = info
846+
service._sub_agent_order.append("sub-uuid")
847+
848+
with (
849+
patch("cecli.coders.Coder.create", AsyncMock(return_value=new_coder)) as create,
850+
patch("cecli.helpers.agents.service.AgentService.get_instance", return_value=service),
851+
):
852+
await Coder.process_prompt_queue(coder)
853+
854+
assert [item["text"] for item in new_coder.prompt_queue] == ["second", "third"]
855+
assert coder.prompt_queue == []
856+
assert info.coder is new_coder
857+
assert service.sub_agents["sub-uuid"] is info
858+
assert coder._processing_queue is False
859+
assert command_queue.get_active_prompt(coder) is None
860+
new_coder.process_prompt_queue.assert_awaited_once_with(True)
861+
862+
create_kwargs = create.call_args.kwargs
863+
assert create_kwargs["from_coder"] is coder
864+
865+
@pytest.mark.asyncio
866+
async def test_process_prompt_queue_reraises_switch_for_primary(self, mock_io):
867+
"""A primary coder still propagates its own SwitchCoderSignal."""
868+
from cecli.coders.base_coder import Coder
869+
870+
coder = _make_coder_with_commands(("raises-switch",))
871+
coder.uuid = "primary-uuid"
872+
coder.parent_uuid = ""
873+
coder.io = mock_io
874+
coder.args = MagicMock()
875+
876+
async def signal_generate(text, preproc=True):
877+
raise SwitchCoderSignal(edit_format="agent", from_coder=coder)
878+
879+
coder.generate = AsyncMock(side_effect=signal_generate)
880+
881+
with pytest.raises(SwitchCoderSignal):
882+
await Coder.process_prompt_queue(coder)
883+
884+
assert coder._processing_queue is False
885+
886+
816887
class TestTUICompletionApplication:
817888
"""Applying nested completions must preserve the host and inner command."""
818889

0 commit comments

Comments
 (0)