fix(session): record operator skill changes

Operator-driven /skill changes were only shown in the UI, leaving no trajectory marker for the model. Persist a system turn for named skill changes and clears, with regression coverage for both paths.
This commit is contained in:
Sanjay Santhanam
2026-07-16 21:28:33 -07:00
committed by Patrick Buckley
parent 8240d2c00d
commit a64cd25807
2 changed files with 49 additions and 0 deletions
+40
View File
@@ -213,6 +213,46 @@ def _run_exec_search(session, capture_return):
return output
class TestSkillCommand:
@pytest.mark.parametrize(
("command", "skill", "expected_name", "expected_target"),
[
("/skill beta", {"name": "beta"}, "beta", "beta"),
("/skill clear", None, None, "defaults"),
],
)
def test_operator_change_is_recorded_in_trajectory(
self, command, skill, expected_name, expected_target
):
session = ChatSession.__new__(ChatSession)
session._skill_name = "alpha"
session.messages = [turn_from_dict({"role": "user", "content": "prior work"})]
session._msg_tokens = [2]
session._chars_per_token = 4.0
session._ws_id = "test-workstream"
session.ui = MagicMock()
session.ui.on_system_turn.return_value = None
session._ui_event_id = MagicMock(return_value=None)
session.set_skill = MagicMock(
side_effect=lambda name: setattr(session, "_skill_name", name)
)
with (
patch("turnstone.core.session.get_skill_by_name", return_value=skill),
patch("turnstone.core.session.save_message") as save_message,
):
session.handle_command(command)
session.set_skill.assert_called_once_with(expected_name)
marker = turn_to_dict(session.messages[-1])
assert marker["role"] == "system"
assert marker["_source"] == "skill_hint"
assert marker["content"] == (
f"Operator changed the active skill from alpha to {expected_target}."
)
save_message.assert_called_once()
class TestChatSessionConstruction:
def test_system_messages_created(self, tmp_db):
session = _make_session()
+9
View File
@@ -16919,6 +16919,7 @@ class ChatSession:
self.ui.on_info("Instructions updated.")
elif cmd == "/skill":
previous_skill = self._skill_name or "defaults"
if not arg:
if self._skill_name:
self.ui.on_info(f"Active skill: {self._skill_name}")
@@ -16926,11 +16927,19 @@ class ChatSession:
self.ui.on_info("Using defaults. Usage: /skill <name> or /skill clear")
elif arg.strip().lower() == "clear":
self.set_skill(None)
self._append_system_turn(
"skill_hint",
f"Operator changed the active skill from {previous_skill} to defaults.",
)
self.ui.on_info("Skill cleared; using defaults.")
else:
tpl = get_skill_by_name(arg.strip())
if tpl:
self.set_skill(tpl["name"])
self._append_system_turn(
"skill_hint",
f"Operator changed the active skill from {previous_skill} to {tpl['name']}.",
)
self.ui.on_info(f"Skill set: {tpl['name']}")
else:
self.ui.on_error(f"Skill not found: {arg.strip()}")