refactor: clean CLI taxonomy — canonical subcommands, merged memory.update, no aliases

- calendar: split write → create/read/update/delete/share
- contacts: rename lookup → read
- memory: merge write+forget → update (unified action field in operations)
- Remove all alias/normalization logic from adapter and handlers
- Update tool_postprocessor ui_hints builders to canonical keys
- Remove frontend legacy TOOL_CALL_START/ARGS/END events and ToolCallItem
- Update SKILL.md files and protocol docs
- Update tests and settings screens
This commit is contained in:
qzl
2026-04-23 12:12:41 +08:00
parent 91077a933d
commit 19e273a9e6
48 changed files with 1578 additions and 811 deletions
@@ -43,7 +43,7 @@ def test_parse_tool_agent_output_uses_side_channel_payload() -> None:
store_tool_agent_output(
tool_call_id=tool_call_id,
payload={
"tool_name": "calendar.write",
"tool_name": "calendar.update",
"tool_call_id": tool_call_id,
"tool_call_args": {"title": "Sync"},
"status": "success",
@@ -60,12 +60,12 @@ def test_parse_tool_agent_output_uses_side_channel_payload() -> None:
parsed = parse_tool_agent_output(
output,
tool_call_id=tool_call_id,
tool_name="calendar.write",
tool_name="calendar.update",
tool_call_args={"title": "Sync"},
)
assert parsed is not None
assert parsed.tool_name == "calendar.write"
assert parsed.tool_name == "calendar.update"
assert parsed.tool_call_id == tool_call_id
assert parsed.result == {"status": "success", "event": {"id": "evt_1"}}
assert parsed.ui_hints == {"view": "calendar_event_created"}
@@ -0,0 +1,38 @@
from __future__ import annotations
from core.agentscope.tools.cli.handler_calendar import (
_resolve_read_range,
)
from core.agentscope.tools.cli.models import CliCommand
def test_resolve_read_range_supports_date_timezone_fallback() -> None:
request = CliCommand(
command="calendar",
subcommand="read",
owner_id="u1",
args={"date": "2026-04-23", "timezone": "Asia/Shanghai"},
)
start_at, end_at, error = _resolve_read_range(request)
assert error is None
assert start_at is not None
assert end_at is not None
assert start_at.isoformat() == "2026-04-22T16:00:00+00:00"
assert end_at.isoformat() == "2026-04-23T16:00:00+00:00"
def test_resolve_read_range_rejects_bad_date() -> None:
request = CliCommand(
command="calendar",
subcommand="read",
owner_id="u1",
args={"date": "2026/04/23", "timezone": "Asia/Shanghai"},
)
start_at, end_at, error = _resolve_read_range(request)
assert start_at is None
assert end_at is None
assert error == "date must be YYYY-MM-DD"
@@ -0,0 +1,20 @@
from __future__ import annotations
from core.agentscope.tools.cli.handlers import build_router
def test_router_registers_only_new_canonical_subcommands() -> None:
router = build_router()
assert ("calendar", "create") in router.command_pairs
assert ("calendar", "read") in router.command_pairs
assert ("calendar", "update") in router.command_pairs
assert ("calendar", "delete") in router.command_pairs
assert ("calendar", "share") in router.command_pairs
assert ("contacts", "read") in router.command_pairs
assert ("memory", "update") in router.command_pairs
assert ("calendar", "write") not in router.command_pairs
assert ("contacts", "lookup") not in router.command_pairs
assert ("memory", "write") not in router.command_pairs
assert ("memory", "forget") not in router.command_pairs
@@ -22,35 +22,54 @@ def _make_tool_output(
)
def test_postprocess_calendar_read_success() -> None:
def test_postprocess_calendar_read_has_ui_hints() -> None:
output = _make_tool_output(command="calendar", subcommand="read", status=ToolStatus.SUCCESS, data={"total": 5, "items": []})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["view"] == "calendar_event_list"
assert processed.ui_hints["total"] == 5
assert processed.ui_hints["intent"] == "list"
def test_postprocess_calendar_write_partial() -> None:
output = _make_tool_output(command="calendar", subcommand="write", status=ToolStatus.PARTIAL, data={"status": "partial", "results": []})
def test_postprocess_calendar_create_partial() -> None:
output = _make_tool_output(command="calendar", subcommand="create", status=ToolStatus.PARTIAL, data={"status": "partial", "success": 1, "failed": 1, "results": []})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["view"] == "calendar_batch_result"
assert processed.ui_hints["status"] == "partial"
assert processed.ui_hints["intent"] == "status"
assert processed.ui_hints["status"] == "warning"
def test_postprocess_contacts_lookup_success() -> None:
output = _make_tool_output(command="contacts", subcommand="lookup", status=ToolStatus.SUCCESS, data={"friends_count": 3, "friends": []})
def test_postprocess_contacts_read_has_ui_hints() -> None:
output = _make_tool_output(command="contacts", subcommand="read", status=ToolStatus.SUCCESS, data={"friends_count": 3, "friends": []})
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["view"] == "contact_list"
assert processed.ui_hints["friends_count"] == 3
assert processed.ui_hints["intent"] == "list"
assert processed.ui_hints["status"] == "success"
def test_postprocess_memory_forget_success() -> None:
output = _make_tool_output(command="memory", subcommand="forget", status=ToolStatus.SUCCESS, data={"status": "success", "forgotten": 5})
def test_postprocess_memory_update_has_ui_hints() -> None:
output = _make_tool_output(
command="memory",
subcommand="update",
status=ToolStatus.SUCCESS,
data={
"status": "success",
"success": 1,
"failed": 0,
"forgotten": 5,
"results": [
{
"memoryType": "user",
"action": "delete",
"status": "success",
"forgotten": 5,
"memoryId": "mem_1",
}
],
},
)
processed = postprocess_tool_output(output)
assert processed.ui_hints is not None
assert processed.ui_hints["forgotten"] == 5
assert processed.ui_hints["intent"] == "status"
assert processed.ui_hints["status"] == "success"
def test_postprocess_failure_no_ui_hints() -> None:
@@ -29,6 +29,16 @@ def test_validate_accepts_known_skills() -> None:
assert result == {"calendar", "contacts"}
def test_validate_rejects_unknown_allowed_command() -> None:
from core.agentscope.tools.toolkit import _validate_allowed_commands
try:
_validate_allowed_commands({"calendar", "unknown_command"})
assert False, "should have raised"
except ValueError as exc:
assert "unknown_command" in str(exc)
def test_build_toolkit_registers_project_cli() -> None:
toolkit = build_toolkit()
schemas = toolkit.get_json_schemas()
+21 -3
View File
@@ -413,7 +413,7 @@ async def test_enqueue_run_rejects_too_many_attachments(monkeypatch) -> None:
@pytest.mark.asyncio
async def test_get_history_snapshot_filters_out_tool_messages() -> None:
async def test_get_history_snapshot_keeps_tool_messages_for_ui_replay() -> None:
class _HistoryRepository(_FakeRepository):
async def get_history_day(
self,
@@ -446,7 +446,20 @@ async def test_get_history_snapshot_filters_out_tool_messages() -> None:
"tool_name": "calendar_read",
"tool_call_id": "call-1",
"status": "success",
"result": "status=success total=3 returned=3",
"result": {
"command": "calendar",
"subcommand": "read",
"data": {"total": 3, "items": []},
},
"ui_hints": {
"intent": "status",
"status": "success",
"title": "完成",
"items": [],
"listItems": [],
"sections": [],
"actions": [],
},
},
},
"timestamp": "2026-03-17T09:00:01Z",
@@ -482,7 +495,12 @@ async def test_get_history_snapshot_filters_out_tool_messages() -> None:
current_user=_user(),
)
assert [message.role for message in snapshot.messages] == ["user", "assistant"]
assert [message.role for message in snapshot.messages] == [
"user",
"tool",
"assistant",
]
assert snapshot.messages[1].ui_schema is not None
@pytest.mark.asyncio
+29 -18
View File
@@ -16,32 +16,43 @@ class _FakeMessage:
self.timestamp = datetime.now(timezone.utc)
def test_convert_message_to_history_does_not_attach_ui_schema_for_tool_message() -> (
None
):
def test_convert_message_to_history_attaches_ui_schema_for_tool_message() -> None:
message = _FakeMessage(
role="tool",
metadata={"tool_agent_output": {"result": "done"}},
)
result = convert_message_to_history(message) # type: ignore[arg-type]
assert "ui_schema" not in result
assert "uiSchema" not in result
def test_convert_message_to_history_does_not_attach_ui_schema_for_assistant_message() -> None:
message = _FakeMessage(
role="assistant",
metadata={
"agent_output": {"ui_schema": {"version": "2.0", "root": {"type": "stack"}}}
"tool_agent_output": {
"result": {"status": "success"},
"ui_hints": {
"intent": "status",
"status": "success",
"title": "完成",
"items": [],
"listItems": [],
"sections": [],
"actions": [],
},
}
},
)
result = convert_message_to_history(message) # type: ignore[arg-type]
assert "ui_schema" not in result
assert "uiSchema" not in result
assert "ui_schema" in result
def test_convert_message_to_history_adds_suggested_actions_for_assistant_message() -> None:
message = _FakeMessage(
role="assistant",
metadata={
"agent_output": {
"suggested_actions": ["查今天日程", "创建会议"]
}
},
)
result = convert_message_to_history(message) # type: ignore[arg-type]
assert result["suggestedActions"] == ["查今天日程", "创建会议"]
def test_convert_message_to_history_returns_multiple_user_attachments() -> None: