fix(agent): 修复 skill action 卡片调用约定、memory 强类型校验和死代码清理

- 所有 calendar action .md: skill/action 替换为 module/method + mode 字段
- handler_memory: 新增 Pydantic extra=forbid 模型替代手工 dict 校验
- memory/SKILL.md: 补充 UserMemoryContent/WorkProfileContent 全字段文档
- 移除 handler_calendar 死代码 _batch_status 和 runner 旧别名 AgentScopeReActRunner
- PRD §5.2-5.6 和 sse-events 协议对齐实际 module/method 实现
This commit is contained in:
qzl
2026-04-24 14:10:57 +08:00
parent d060962a5f
commit d2d292a99e
16 changed files with 277 additions and 244 deletions
@@ -324,13 +324,14 @@ The worker still sees only:
## 5.2 New `project_cli` model-facing input contract ## 5.2 New `project_cli` model-facing input contract
The new canonical model-facing payload is: The canonical model-facing payload is:
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "get_event", "method": "read",
"input": { "input": {
"mode": "event",
"event_id": "<uuid>" "event_id": "<uuid>"
} }
} }
@@ -338,91 +339,79 @@ The new canonical model-facing payload is:
Field meanings: Field meanings:
- `skill`: enabled business skill namespace - `module`: enabled business module namespace (calendar, contacts, memory)
- `action`: concrete business operation inside the skill - `method`: concrete business operation inside the module
- `input`: strict action-specific payload - `input`: strict method-specific payload
This is still one tool call. The worker is not choosing among many tools. This is still one tool call. The worker is not choosing among many tools.
## 5.3 Calendar action protocol ## 5.3 Calendar method protocol
The calendar skill should be redesigned around real business actions derived from `schedule_items` and `schedule_subscriptions`. The calendar module exposes the following methods registered in the CLI router:
### Event actions | Module | Method | Handler | Input Shape |
|----------|----------------|----------------------------------|-------------|
| calendar | read | `handle_calendar_list_day` | discriminated by `mode` |
| calendar | create | `handle_calendar_create_event` | title, start_at, timezone, ... |
| calendar | update | `handle_calendar_update_event` | event_id + patch |
| calendar | delete | `handle_calendar_delete_event` | event_id |
| calendar | share | `handle_calendar_invite_subscriber` | event_id, invitee, permissions |
| calendar | accept_invite | `handle_calendar_accept_invite` | event_id |
| calendar | reject_invite | `handle_calendar_reject_invite` | event_id |
1. `list_day` The `read` method uses a discriminated union with `mode` field to dispatch to list_day, list_range, or get_event internally.
2. `list_range`
3. `get_event`
4. `create_event`
5. `update_event`
6. `delete_event`
### Subscription actions
1. `invite_subscriber`
2. `accept_invite`
3. `reject_invite`
### Why this action set
This set directly maps to current product behavior:
- user asks what is scheduled today -> `list_day`
- user asks what is scheduled this week -> `list_range`
- user asks for a known event's details -> `get_event`
- user creates or edits a schedule item -> `create_event` / `update_event`
- user removes a schedule item -> `delete_event`
- user invites another person -> `invite_subscriber`
- invite recipient responds -> `accept_invite` / `reject_invite`
This avoids overloading one label like `read` for two distinct business tasks. This avoids overloading one label like `read` for two distinct business tasks.
## 5.4 Canonical calendar action shapes ## 5.4 Canonical calendar method shapes
### `list_day` ### `read` with mode=day (list one day)
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "list_day", "method": "read",
"input": { "input": {
"mode": "day",
"date": "2026-04-23", "date": "2026-04-23",
"timezone": "Asia/Shanghai" "timezone": "Asia/Shanghai"
} }
} }
``` ```
### `list_range` ### `read` with mode=range (list time range)
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "list_range", "method": "read",
"input": { "input": {
"mode": "range",
"start_at": "2026-04-23T00:00:00+08:00", "start_at": "2026-04-23T00:00:00+08:00",
"end_at": "2026-04-24T00:00:00+08:00" "end_at": "2026-04-24T00:00:00+08:00"
} }
} }
``` ```
### `get_event` ### `read` with mode=event (get by ID)
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "get_event", "method": "read",
"input": { "input": {
"mode": "event",
"event_id": "<uuid>" "event_id": "<uuid>"
} }
} }
``` ```
### `create_event` ### `create`
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "create_event", "method": "create",
"input": { "input": {
"title": "Project sync", "title": "Project sync",
"start_at": "2026-04-23T16:00:00+08:00", "start_at": "2026-04-23T16:00:00+08:00",
@@ -439,12 +428,12 @@ This avoids overloading one label like `read` for two distinct business tasks.
} }
``` ```
### `update_event` ### `update`
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "update_event", "method": "update",
"input": { "input": {
"event_id": "<uuid>", "event_id": "<uuid>",
"patch": { "patch": {
@@ -457,24 +446,24 @@ This avoids overloading one label like `read` for two distinct business tasks.
} }
``` ```
### `delete_event` ### `delete`
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "delete_event", "method": "delete",
"input": { "input": {
"event_id": "<uuid>" "event_id": "<uuid>"
} }
} }
``` ```
### `invite_subscriber` ### `share`
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "invite_subscriber", "method": "share",
"input": { "input": {
"event_id": "<uuid>", "event_id": "<uuid>",
"invitee": { "invitee": {
@@ -493,8 +482,8 @@ This avoids overloading one label like `read` for two distinct business tasks.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "accept_invite", "method": "accept_invite",
"input": { "input": {
"event_id": "<uuid>" "event_id": "<uuid>"
} }
@@ -505,8 +494,8 @@ This avoids overloading one label like `read` for two distinct business tasks.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "reject_invite", "method": "reject_invite",
"input": { "input": {
"event_id": "<uuid>" "event_id": "<uuid>"
} }
@@ -556,21 +545,26 @@ This makes `view_skill_file` a real progressive-disclosure mechanism instead of
## 5.6 Error contract for self-correction ## 5.6 Error contract for self-correction
The redesigned CLI should return structured action-level validation feedback. The redesigned CLI returns structured validation feedback with field-level detail.
Canonical error example: Canonical error example:
```json ```json
{ {
"status": "failure", "ok": false,
"module": "calendar",
"method": "read",
"error": { "error": {
"code": "INVALID_ACTION_INPUT", "code": "INVALID_ACTION_INPUT",
"message": "action list_range requires start_at and end_at", "message": "input does not match method schema",
"skill": "calendar", "retryable": false,
"action": "list_range", "details": {
"missing_fields": ["start_at", "end_at"], "missing_fields": ["start_at", "end_at"],
"unexpected_fields": ["event_id"], "invalid_fields": [],
"suggested_alternative_actions": ["get_event"] "alias_corrections": {
"start_time": "start_at"
}
}
} }
} }
``` ```
@@ -1,7 +1,6 @@
__all__ = [ __all__ = [
"AgentScopeRuntimeOrchestrator", "AgentScopeRuntimeOrchestrator",
"AgentScopeRunner", "AgentScopeRunner",
"AgentScopeReActRunner",
] ]
@@ -14,8 +13,4 @@ def __getattr__(name: str):
from core.agentscope.runtime.runner import AgentScopeRunner from core.agentscope.runtime.runner import AgentScopeRunner
return AgentScopeRunner return AgentScopeRunner
if name == "AgentScopeReActRunner":
from core.agentscope.runtime.runner import AgentScopeReActRunner
return AgentScopeReActRunner
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -611,6 +611,3 @@ class SystemAgentRuntimeConfig:
api_key: str api_key: str
llm_config: SystemAgentLLMConfig llm_config: SystemAgentLLMConfig
extra_context: str | None = None extra_context: str | None = None
AgentScopeReActRunner = AgentScopeRunner
@@ -644,14 +644,6 @@ def _normalize_phone(raw: str) -> str:
return phone return phone
def _batch_status(success: int, failed: int) -> str:
if failed == 0:
return "success"
if success == 0:
return "failure"
return "partial"
def _day_input_to_range_input(input_payload: _CalendarReadDayInput) -> dict[str, str]: def _day_input_to_range_input(input_payload: _CalendarReadDayInput) -> dict[str, str]:
timezone_name = input_payload.timezone.strip() or "Asia/Shanghai" timezone_name = input_payload.timezone.strip() or "Asia/Shanghai"
try: try:
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from copy import deepcopy from copy import deepcopy
from typing import Any, cast from typing import Any, Literal, cast
from uuid import UUID from uuid import UUID
from core.agentscope.tools.cli.models import CliCommand, CliCommandResult from core.agentscope.tools.cli.models import CliCommand, CliCommandResult
@@ -9,24 +9,34 @@ from core.agentscope.tools.utils.memory_domain import (
create_memories_service, create_memories_service,
map_memory_exception, map_memory_exception,
) )
from pydantic import BaseModel, ConfigDict, Field
from schemas.agent.runtime_models import ErrorInfo from schemas.agent.runtime_models import ErrorInfo
from schemas.enums import MemoryType from schemas.enums import MemoryType
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
class _UpdateOperation(BaseModel):
model_config = ConfigDict(extra="forbid")
action: Literal["update", "delete"]
memory_type: MemoryType
user_content: dict[str, Any] | None = None
work_content: dict[str, Any] | None = None
forget_paths: list[str] | None = None
class _MemoryUpdateInput(BaseModel):
model_config = ConfigDict(extra="forbid")
operations: list[_UpdateOperation] = Field(..., min_length=1)
async def handle_memory_update(request: CliCommand) -> CliCommandResult: async def handle_memory_update(request: CliCommand) -> CliCommandResult:
from core.db.session import AsyncSessionLocal from core.db.session import AsyncSessionLocal
operations = request.input.get("operations") validated = _validate_input(request)
if not isinstance(operations, list) or not operations: if validated is None:
return _invalid_argument( return _validation_error(request)
request=request,
message="operations must be a non-empty list",
details={
"required_fields": ["operations"],
"field_types": {"operations": "array of objects"},
},
)
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
service = create_memories_service(session=session, owner_id=UUID(request.owner_id)) service = create_memories_service(session=session, owner_id=UUID(request.owner_id))
@@ -37,49 +47,9 @@ async def handle_memory_update(request: CliCommand) -> CliCommandResult:
failed_ops: list[dict[str, Any]] = [] failed_ops: list[dict[str, Any]] = []
result_items: list[dict[str, Any]] = [] result_items: list[dict[str, Any]] = []
for idx, op in enumerate(operations): for idx, op in enumerate(validated.operations):
if not isinstance(op, dict): memory_type = op.memory_type
failed_count += 1 action = op.action
failed_ops.append(
{
"code": "INVALID_ARGUMENT",
"message": "operation item must be object",
"retryable": False,
}
)
result_items.append(
{
"idx": idx,
"memoryType": "unknown",
"action": "invalid",
"status": "failure",
"code": "INVALID_ARGUMENT",
}
)
continue
action = str(op.get("action") or "").strip().lower()
if action not in {"update", "delete"}:
failed_count += 1
failed_ops.append(
{
"code": "INVALID_ARGUMENT",
"message": "action must be update or delete",
"retryable": False,
}
)
result_items.append(
{
"idx": idx,
"memoryType": str(op.get("memory_type") or "unknown"),
"action": action or "invalid",
"status": "failure",
"code": "INVALID_ARGUMENT",
}
)
continue
memory_type = MemoryType(str(op.get("memory_type") or "user"))
try: try:
if action == "update": if action == "update":
result = await _apply_update_operation( result = await _apply_update_operation(
@@ -153,15 +123,43 @@ async def handle_memory_update(request: CliCommand) -> CliCommandResult:
) )
def _validate_input(request: CliCommand) -> _MemoryUpdateInput | None:
try:
return _MemoryUpdateInput.model_validate(request.input)
except Exception:
return None
def _validation_error(request: CliCommand) -> CliCommandResult:
return CliCommandResult(
ok=False,
module=request.module,
method=request.method,
error=ErrorInfo(
code="INVALID_ARGUMENT",
message="operations must be a non-empty list of objects with action (update|delete) and memory_type (user|work)",
retryable=False,
details={
"required_fields": ["operations"],
"field_types": {
"operations": "array of objects",
"operations[].action": "update or delete",
"operations[].memory_type": "user or work",
},
},
),
)
async def _apply_update_operation( async def _apply_update_operation(
*, *,
service: Any, service: Any,
memory_type: MemoryType, memory_type: MemoryType,
op: dict[str, Any], op: _UpdateOperation,
) -> dict[str, Any]: ) -> dict[str, Any]:
existing = await service.get_memory_model(memory_type=memory_type) existing = await service.get_memory_model(memory_type=memory_type)
if memory_type == MemoryType.USER: if memory_type == MemoryType.USER:
content_data = op.get("user_content") content_data = op.user_content
if not isinstance(content_data, dict): if not isinstance(content_data, dict):
raise ValueError("update action for user memory requires user_content") raise ValueError("update action for user memory requires user_content")
base = ( base = (
@@ -177,7 +175,7 @@ async def _apply_update_operation(
validated = UserMemoryContent.model_validate(merged) validated = UserMemoryContent.model_validate(merged)
updated = await service.update_user_memory(content=validated) updated = await service.update_user_memory(content=validated)
else: else:
content_data = op.get("work_content") content_data = op.work_content
if not isinstance(content_data, dict): if not isinstance(content_data, dict):
raise ValueError("update action for work memory requires work_content") raise ValueError("update action for work memory requires work_content")
base = ( base = (
@@ -205,9 +203,9 @@ async def _apply_delete_operation(
*, *,
service: Any, service: Any,
memory_type: MemoryType, memory_type: MemoryType,
op: dict[str, Any], op: _UpdateOperation,
) -> dict[str, Any]: ) -> dict[str, Any]:
forget_paths_raw = op.get("forget_paths") forget_paths_raw = op.forget_paths
if not isinstance(forget_paths_raw, list) or not forget_paths_raw: if not isinstance(forget_paths_raw, list) or not forget_paths_raw:
raise ValueError("delete action requires non-empty forget_paths") raise ValueError("delete action requires non-empty forget_paths")
forget_paths = [ forget_paths = [
@@ -237,25 +235,6 @@ async def _apply_delete_operation(
} }
def _invalid_argument(
*,
request: CliCommand,
message: str,
details: dict[str, Any] | None,
) -> CliCommandResult:
return CliCommandResult(
ok=False,
module=request.module,
method=request.method,
error=ErrorInfo(
code="INVALID_ARGUMENT",
message=message,
retryable=False,
details=details,
),
)
def _deep_merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]: def _deep_merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
merged = deepcopy(base) merged = deepcopy(base)
for key, value in patch.items(): for key, value in patch.items():
@@ -1,5 +1,7 @@
# accept_invite # accept_invite
Use when accepting a shared event invitation.
## Input Schema ## Input Schema
- `input.event_id`: required, `string`, UUID - `input.event_id`: required, `string`, UUID
@@ -9,12 +11,10 @@
- success: subscription response object - success: subscription response object
- failure: `error.code`, `error.message`, `error.details` - failure: `error.code`, `error.message`, `error.details`
Use when accepting a shared event invitation.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "accept_invite", "method": "accept_invite",
"input": { "input": {
"event_id": "550e8400-e29b-41d4-a716-446655440000" "event_id": "550e8400-e29b-41d4-a716-446655440000"
} }
@@ -5,11 +5,11 @@ Use when creating a new event.
## Input Schema ## Input Schema
- `input.title`: required, `string` - `input.title`: required, `string`
- `input.start_at`: required, `string`, ISO 8601 datetime - `input.start_at`: required, `string`, ISO 8601 datetime with timezone
- `input.timezone`: required, `string`, IANA timezone - `input.timezone`: required, `string`, IANA timezone
- `input.end_at`: optional, `string | null`, ISO 8601 datetime - `input.end_at`: optional, `string`, ISO 8601 datetime with timezone
- `input.description`: optional, `string | null` - `input.description`: optional, `string`
- `input.metadata`: optional, `object | null` - `input.metadata`: optional, `object` with keys `location`, `reminder_minutes`, `color`, `notes`
## Output Shape ## Output Shape
@@ -18,8 +18,8 @@ Use when creating a new event.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "create_event", "method": "create",
"input": { "input": {
"title": "Project sync", "title": "Project sync",
"start_at": "2026-04-23T10:00:00+08:00", "start_at": "2026-04-23T10:00:00+08:00",
@@ -1,5 +1,7 @@
# delete_event # delete_event
Use when deleting one known event.
## Input Schema ## Input Schema
- `input.event_id`: required, `string`, UUID - `input.event_id`: required, `string`, UUID
@@ -9,12 +11,10 @@
- success: `data.status`, `data.success`, `data.failed`, `data.ids`, `data.results` - success: `data.status`, `data.success`, `data.failed`, `data.ids`, `data.results`
- failure: `error.code`, `error.message`, `error.details` - failure: `error.code`, `error.message`, `error.details`
Use when deleting one known event.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "delete_event", "method": "delete",
"input": { "input": {
"event_id": "550e8400-e29b-41d4-a716-446655440000" "event_id": "550e8400-e29b-41d4-a716-446655440000"
} }
@@ -4,6 +4,7 @@ Use when the user already knows the target event identity.
## Input Schema ## Input Schema
- `input.mode`: required, must be `"event"`
- `input.event_id`: required, `string`, UUID - `input.event_id`: required, `string`, UUID
## Output Shape ## Output Shape
@@ -13,9 +14,10 @@ Use when the user already knows the target event identity.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "get_event", "method": "read",
"input": { "input": {
"mode": "event",
"event_id": "550e8400-e29b-41d4-a716-446655440000" "event_id": "550e8400-e29b-41d4-a716-446655440000"
} }
} }
@@ -24,3 +26,4 @@ Use when the user already knows the target event identity.
## Rules ## Rules
- Prefer this over list actions when an `event_id` is already available. - Prefer this over list actions when an `event_id` is already available.
- Do not use old field names: `command`, `subcommand`, `args`.
@@ -6,11 +6,11 @@ Use when sharing an event with one phone number.
- `input.event_id`: required, `string`, UUID - `input.event_id`: required, `string`, UUID
- `input.invitee`: required, `object` - `input.invitee`: required, `object`
- `input.invitee.phone`: required, `string` - `input.invitee.phone`: required, `string`, phone number like `+8613800138000`
- `input.permissions`: optional, `object` - `input.permissions`: optional, `object`
- `input.permissions.view`: optional, `bool` - `input.permissions.view`: optional, `bool`, default `true`
- `input.permissions.edit`: optional, `bool` - `input.permissions.edit`: optional, `bool`, default `false`
- `input.permissions.invite`: optional, `bool` - `input.permissions.invite`: optional, `bool`, default `false`
## Output Shape ## Output Shape
@@ -19,8 +19,8 @@ Use when sharing an event with one phone number.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "invite_subscriber", "method": "share",
"input": { "input": {
"event_id": "550e8400-e29b-41d4-a716-446655440000", "event_id": "550e8400-e29b-41d4-a716-446655440000",
"invitee": { "invitee": {
@@ -4,6 +4,7 @@ Use when the user asks about one calendar day in a local timezone.
## Input Schema ## Input Schema
- `input.mode`: required, must be `"day"`
- `input.date`: required, `string`, format `YYYY-MM-DD` - `input.date`: required, `string`, format `YYYY-MM-DD`
- `input.timezone`: optional, `string`, IANA timezone like `Asia/Shanghai` - `input.timezone`: optional, `string`, IANA timezone like `Asia/Shanghai`
@@ -14,9 +15,10 @@ Use when the user asks about one calendar day in a local timezone.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "list_day", "method": "read",
"input": { "input": {
"mode": "day",
"date": "2026-04-23", "date": "2026-04-23",
"timezone": "Asia/Shanghai" "timezone": "Asia/Shanghai"
} }
@@ -28,4 +30,5 @@ Use when the user asks about one calendar day in a local timezone.
- `input` must not be empty. - `input` must not be empty.
- `date` must be a concrete date string, not an empty object. - `date` must be a concrete date string, not an empty object.
- For words like today or tomorrow, convert them to a concrete `YYYY-MM-DD` date from `system_time_local` before calling `project_cli`. - For words like today or tomorrow, convert them to a concrete `YYYY-MM-DD` date from `system_time_local` before calling `project_cli`.
- Use `get_event` instead if you already have an `event_id`. - Use `mode: "event"` instead if you already have an `event_id`.
- Do not use old field names: `start_time`, `end_time`, `event_timezone`.
@@ -4,8 +4,9 @@ Use when the user asks for a specific time range.
## Input Schema ## Input Schema
- `input.start_at`: required, `string`, ISO 8601 datetime - `input.mode`: required, must be `"range"`
- `input.end_at`: required, `string`, ISO 8601 datetime - `input.start_at`: required, `string`, ISO 8601 datetime with timezone
- `input.end_at`: required, `string`, ISO 8601 datetime with timezone
## Output Shape ## Output Shape
@@ -14,9 +15,10 @@ Use when the user asks for a specific time range.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "list_range", "method": "read",
"input": { "input": {
"mode": "range",
"start_at": "2026-04-23T09:00:00+08:00", "start_at": "2026-04-23T09:00:00+08:00",
"end_at": "2026-04-23T18:00:00+08:00" "end_at": "2026-04-23T18:00:00+08:00"
} }
@@ -25,5 +27,6 @@ Use when the user asks for a specific time range.
## Rules ## Rules
- `start_at` and `end_at` must both be present. - `start_at` and `end_at` must both be present with timezone offset.
- Do not send `event_id` to list actions. - Do not send `event_id` to list actions.
- Do not use old field names: `start_time`, `end_time`.
@@ -1,5 +1,7 @@
# reject_invite # reject_invite
Use when rejecting a shared event invitation.
## Input Schema ## Input Schema
- `input.event_id`: required, `string`, UUID - `input.event_id`: required, `string`, UUID
@@ -9,12 +11,10 @@
- success: subscription response object - success: subscription response object
- failure: `error.code`, `error.message`, `error.details` - failure: `error.code`, `error.message`, `error.details`
Use when rejecting a shared event invitation.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "reject_invite", "method": "reject_invite",
"input": { "input": {
"event_id": "550e8400-e29b-41d4-a716-446655440000" "event_id": "550e8400-e29b-41d4-a716-446655440000"
} }
@@ -7,11 +7,11 @@ Use when changing one known event.
- `input.event_id`: required, `string`, UUID - `input.event_id`: required, `string`, UUID
- `input.patch`: required, `object` - `input.patch`: required, `object`
- `input.patch.title`: optional, `string` - `input.patch.title`: optional, `string`
- `input.patch.description`: optional, `string | null` - `input.patch.description`: optional, `string`
- `input.patch.start_at`: optional, `string | null`, ISO 8601 datetime - `input.patch.start_at`: optional, `string`, ISO 8601 datetime with timezone
- `input.patch.end_at`: optional, `string | null`, ISO 8601 datetime - `input.patch.end_at`: optional, `string`, ISO 8601 datetime with timezone
- `input.patch.timezone`: optional, `string` - `input.patch.timezone`: optional, `string`, IANA timezone
- `input.patch.metadata`: optional, `object | null` - `input.patch.metadata`: optional, `object`
- `input.patch.status`: optional, `string` - `input.patch.status`: optional, `string`
## Output Shape ## Output Shape
@@ -21,8 +21,8 @@ Use when changing one known event.
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "update_event", "method": "update",
"input": { "input": {
"event_id": "550e8400-e29b-41d4-a716-446655440000", "event_id": "550e8400-e29b-41d4-a716-446655440000",
"patch": { "patch": {
@@ -37,3 +37,4 @@ Use when changing one known event.
- All mutable fields go inside `patch`. - All mutable fields go inside `patch`.
- Do not put mutable fields at the top level. - Do not put mutable fields at the top level.
- Use `start_at`/`end_at`, not `start_time`/`end_time`.
@@ -18,23 +18,16 @@ description: User memory management - store and forget personal facts and work p
- User wants to remove previously stored information - User wants to remove previously stored information
- Agent needs to recall user preferences for personalization - Agent needs to recall user preferences for personalization
## Available Tool
Use the single tool `project_cli`.
Read this file first with `view_skill_file` when memory is the relevant skill.
## Calling Contract ## Calling Contract
- `module`: required, must be `memory` - `module`: required, must be `memory`
- `method`: required, must be `update` - `method`: required, must be `update`
- `input.operations`: required, non-empty array - `input.operations`: required, non-empty array of operation objects
- Each operation: `action` (update|delete), `memory_type` (user|work), plus action-specific fields
- Output success fields: `data.status`, `data.success`, `data.failed`, `data.results` - Output success fields: `data.status`, `data.success`, `data.failed`, `data.results`
- Output failure fields: `error.code`, `error.message`, `error.details` - Output failure fields: `error.code`, `error.message`, `error.details`
### Update Memory ### Update Memory (user)
Call `project_cli` with:
```json ```json
{ {
@@ -45,39 +38,113 @@ Call `project_cli` with:
{ {
"action": "update", "action": "update",
"memory_type": "user", "memory_type": "user",
"user_content": {} "user_content": {
"occupation": "software engineer",
"timezone": "Asia/Shanghai",
"preferences": {
"communication_style": "concise"
},
"interests": ["reading", "hiking"]
}
} }
] ]
} }
} }
``` ```
Operation object fields: ### Update Memory (work)
- `action`: `update` or `delete`
- `memory_type`: `user` or `work`
- `update` requires matching content payload (`user_content` / `work_content`)
- `delete` requires `forget_paths`
Field requirements: ```json
- `operations[].action`: required, `string` {
- `operations[].memory_type`: required, `string` "module": "memory",
- `operations[].user_content`: required for `memory_type=user` and `action=update`, `object` "method": "update",
- `operations[].work_content`: required for `memory_type=work` and `action=update`, `object` "input": {
- `operations[].forget_paths`: required for `action=delete`, `array[string]` "operations": [
{
"action": "update",
"memory_type": "work",
"work_content": {
"occupation": "software engineer",
"expertise": ["backend", "distributed systems"],
"current_projects": [
{ "name": "Project X", "status": "active" }
]
}
}
]
}
}
```
## Composition Patterns ### Delete Memory Paths
1. When user says "remember that I prefer morning meetings": ```json
- Call `project_cli` with `module="memory"`, `method="update"`, and appropriate content {
"module": "memory",
"method": "update",
"input": {
"operations": [
{
"action": "delete",
"memory_type": "user",
"forget_paths": ["preferences.communication_style", "interests"]
}
]
}
}
```
2. When user says "forget my old address": ## Operation Fields
- Call `project_cli` with `module="memory"`, `method="update"`, `operations[0].action="delete"`, and the specific dot-path
## Protocol Reminder | Field | Required | Type | Description |
|-------|----------|------|-------------|
| `action` | yes | `"update"` or `"delete"` | Operation type |
| `memory_type` | yes | `"user"` or `"work"` | Which memory to modify |
| `user_content` | update+user | object | User memory content (see below) |
| `work_content` | update+work | object | Work memory content (see below) |
| `forget_paths` | delete | array of dot-path strings | Paths to remove, e.g. `["interests", "preferences.communication_style"]` |
- Never use old `command/subcommand/args` fields for memory writes. ## User Content Fields (`user_content`)
Top-level fields for `memory_type=user` updates. All fields optional; only include what changes.
| Field | Type | Description |
|-------|------|-------------|
| `occupation` | string | 职业 |
| `timezone` | string | 时区 |
| `primary_language` | string | 主要语言 |
| `people` | array | 重要人物: `{name, relationship?, role?, notes?}` |
| `places` | array | 常去地点: `{name, category?, address?, timezone?, notes?}` |
| `preferences` | object | 偏好: `{communication_style?, language_preference?, work_lifestyle?, notification_preference?}` |
| `scheduling_preferences` | object | 排程偏好: `{productive_windows?, preferred_meeting_windows?, no_meeting_windows?, meeting_buffer_minutes?, max_meetings_per_day?, notes?}` |
| `interests` | array of strings | 兴趣爱好 |
| `avoid_topics` | array of strings | 不想讨论的话题 |
| `custom_rules` | array of strings | 用户自定义规则 |
| `recurring_routines` | array | 周期性安排: `{name, description?, cadence?, time_windows?, importance?}` |
## Work Content Fields (`work_content`)
Top-level fields for `memory_type=work` updates. All fields optional; only include what changes.
| Field | Type | Description |
|-------|------|-------------|
| `occupation` | string | 职业身份 |
| `expertise` | array of strings | 专业领域 |
| `preferred_tools` | array of strings | 惯用工具 |
| `current_projects` | array | 项目: `{name, description?, status?, priority?, deadline?, collaborators?, notes?}` |
| `work_habits` | object | 工作习惯: `{available_hours?, deep_work_blocks?, preferred_meeting_windows?, no_meeting_windows?, notification_channel?, notes?}` |
| `team_members` | array | 团队成员: `{name, role?, relationship?, preferred_contact_channel?, notes?}` |
| `team_context` | string | 团队背景 |
| `work_rules` | array of strings | 工作规则 |
## Rules
- Never use old `command/subcommand/args` fields.
- For `update`, provide only the fields that changed. Existing fields are merged, not replaced.
- For `delete`, use dot-separated paths like `"preferences.communication_style"`.
- Extra fields not listed above will be rejected.
## Failure Recovery ## Failure Recovery
- If write fails, inspect `error.details` and retry with the documented field shape only - If write fails, inspect `error.details` and retry with the documented field shape only.
- If forget path is invalid, suggest checking the data structure - If forget path is invalid, suggest checking the data structure.
+10 -11
View File
@@ -216,9 +216,10 @@ data: <json>
```json ```json
{ {
"skill": "calendar", "module": "calendar",
"action": "get_event", "method": "read",
"input": { "input": {
"mode": "event",
"event_id": "evt_123" "event_id": "evt_123"
} }
} }
@@ -233,18 +234,16 @@ SSE 协议中的工具名字段保持后端原样,不做服务端翻译:
前端展示层统一通过工具名本地化映射进行中文渲染,要求兼容两类命名风格: 前端展示层统一通过工具名本地化映射进行中文渲染,要求兼容两类命名风格:
- dot 风格:`memory.update``calendar.get_event` - dot 风格:`memory.update``calendar.read`
- snake 风格:`memory_update``calendar_get_event` - snake 风格:`memory_update``calendar_read`
当前规范映射(canonical -> 中文)如下: 当前规范映射(canonical -> 中文)如下:
- `calendar.list_day` -> `读取当日日程` - `calendar.read` -> `读取日程`
- `calendar.list_range` -> `读取区间日程` - `calendar.create` -> `创建日程`
- `calendar.get_event` -> `读取日程详情` - `calendar.update` -> `更新日程`
- `calendar.create_event` -> `创建日程` - `calendar.delete` -> `删除日程`
- `calendar.update_event` -> `更新日程` - `calendar.share` -> `邀请参与者`
- `calendar.delete_event` -> `删除日程`
- `calendar.invite_subscriber` -> `邀请参与者`
- `calendar.accept_invite` -> `接受邀请` - `calendar.accept_invite` -> `接受邀请`
- `calendar.reject_invite` -> `拒绝邀请` - `calendar.reject_invite` -> `拒绝邀请`
- `contacts.read` -> `读取联系人` - `contacts.read` -> `读取联系人`