fischer-agentkit/docs/plans/2026-06-23-003-feat-calenda...

55 KiB
Raw Blame History

title status date origin type
Calendar & Schedule Feature completed 2026-06-23 docs/brainstorms/2026-06-23-calendar-schedule-requirements.md feature

Summary

在 AgentKit 客户端嵌入行事历通过混合模式ReAct 工具调用 + 时间关键词触发的后处理提取)自动捕获对话中的日程,支持完整的手动管理——循环事件、自定义类型、标签、三种视图(日历/卡片/列表),双向同步 Apple Calendar (CalDAV)、Outlook (Graph API) 和 iCal/ICS配套独立的多渠道提醒子系统。

Problem Frame

Agent 对话中频繁产生可执行的时间安排,但这些信息当前无处记录。痛点不在"缺少一个日历应用",而在"Agent 产生的日程与日历之间没有桥梁"。本计划实现这架桥梁Agent 工具调用 + 后处理提取 + 手动管理 + 外部同步 + 提醒分发,形成完整的日程闭环。

Scope: Deep — 跨前端 UI、后端 API、Agent 工具、外部同步、提醒子系统五大领域。

Requirements Traceability

Requirement Group R-IDs Covered by Units
事件数据模型 R1-R5 U1, U2
视图模式 R6-R9 U10 (KTD-12: R9 = summary + drawer)
Agent 自动识别 R10-R15 U3, U4 (R15 source distinction: U10)
手动管理 R16-R19 U10, U11
外部日历同步 R20-R24 U6, U7, U8 (SyncManager: U6)
提醒子系统 R25-R29 U5, U12
共享与邀请 R30-R33 U2, U11 (R30 user search: U2, R33 invited styling: U10)
UI/UX R34-R37 U10, U11

Key Technical Decisions

KTD-1. Mirror documents subsystem for backend structure. 日历后端镜像 src/agentkit/documents/ 的结构——calendar/db.py (aiosqlite bare-connection)、calendar/models.py (dataclass DTO)、calendar/service.py (business logic)、calendar/tool.py (agent tool)。这是代码库中最近验证过的模式,与现有架构一致,避免引入 SQLAlchemy engine/session 的复杂性。Rationale: aiosqlite bare-connection 已在 auth 和 documents 子系统中验证,轻量且与异步架构兼容。

KTD-2. asyncio loop for reminder scheduler, not APScheduler. 提醒调度器跟随 src/agentkit/server/task_store.pystart()/stop() + asyncio.create_task 循环模式,在 app.py lifespan 中启停。Rationale: 不引入新依赖与现有后台任务模式一致task_store cleanup loop、session writer loopponytail: asyncio.sleep 轮询的精度是秒级,如果未来需要亚秒级调度可升级到 APScheduler。 Note: 需求文档提到"新增依赖:后台调度器(如 APScheduler",本决策选择不引入 APScheduler 而复用 asyncio loop 模式——这是有意识的偏离,因为 task_store 已验证此模式可行,且避免新依赖。

KTD-3. python-dateutil.rrule for recurrence expansion. 使用 dateutil.rrule 处理 RRULE 规则展开RFC 5545 兼容),支持 FREQ/INTERVAL/COUNT/UNTIL/BYDAY。Rationale: 标准库 datetime 不支持循环规则dateutil 是 Python 生态中唯一成熟的 RRULE 实现,且是 icalendar 库的依赖。

KTD-4. caldav library for Apple Calendar, httpx for Outlook Graph API. Apple Calendar 同步使用 caldavCalDAV 协议Outlook 同步直接用 httpx 调用 Microsoft Graph REST API不引入 msgraph SDK。Rationale: caldav 库是 Python 生态中唯一成熟的 CalDAV 客户端msgraph SDK 过重(数百 MBhttpx 已在依赖中Graph REST API 简单直接。

KTD-5. icalendar library for ICS import/export. 使用 icalendar 库生成和解析 .ics 文件。Rationale: 手写 iCalendar 格式容易出错编码、转义、时区icalendar 库是事实标准。

KTD-6. FullCalendar Vue3 for calendar grid views. 前端日历网格视图(月/周/日)使用 @fullcalendar/vue3,样式定制为 Notion Calendar 风格。Rationale: 从零实现带拖拽的月/周/日网格是数千行代码FullCalendar 是成熟标准库,支持拖拽创建/移动、循环事件展开、视图切换。ponytail: FullCalendar 是本计划中唯一的大型前端依赖,如果后续需要移除,日历网格组件是独立的可替换层。

KTD-7. Post-processing hook in chat.py after assistant reply persistence. 后处理提取钩子插入 src/agentkit/server/routes/chat.py_handle_chat_message() 中,在 assistant 回复持久化之后REACT 路径约 line 1158DIRECT_CHAT 路径约 line 971触发。先做零 LLM 的正则关键词检测,命中后才调用 LLM 提取。Rationale: 这是对话轮次结束的自然插入点,不改变现有流程,仅在回复完成后追加一个异步后台任务。

KTD-8. aiosmtplib for email reminder channel. 邮件提醒渠道使用 aiosmtplib(异步 SMTP 客户端。Rationale: smtplib 是同步的会阻塞事件循环aiosmtplib 是标准异步替代品。

KTD-9. Reuse existing LLM gateway for post-processing extraction. 后处理提取的 LLM 调用复用 agentkit.llm 网关,不新建 LLM 客户端。Rationale: LLM 网关已有 fallback、缓存、用量追踪直接复用。

KTD-10. WebSocket piggyback for real-time calendar push. 日历实时更新Agent 创建事件、提醒触发、邀请通知、同步冲突)复用现有 chat WebSocket 连接,新增 calendar_event_createdcalendar_remindercalendar_invitationcalendar_sync_conflict 消息类型。Rationale: 避免新建 WS 连接,前端 chat store 已有消息分发机制。

KTD-11. UTC storage + local display timezone strategy. 所有时间字段(start_timeend_timescheduled_timelast_modifiedcreated_at)以 ISO 8601 UTC 存储在数据库中前端显示时由浏览器自动转换为本地时区。RRULE 展开基于 UTC 计算显示时再转本地。Rationale: 统一存储避免时区歧义,浏览器 Intl.DateTimeFormat 或 dayjs UTC 插件处理显示转换。ponytail: 不支持用户手动选择时区——浏览器本地时区即显示时区,跨时区用户需自行调整设备时区。

KTD-12. Right panel = summary view, drawer = full management. 右面板日历 tab 仅展示摘要视图(今日 + 未来 3 条事件),点击"展开"按钮打开 80% 宽度的大抽屉,抽屉内包含完整的三视图管理(日历网格/卡片/列表)+ 事件编辑器 + 邀请管理。Rationale: 右面板空间有限,摘要视图满足"快速查看"需求;完整管理功能需要大画布,抽屉模式不离开当前页面上下文。

Architecture

Module Map

src/agentkit/calendar/                    # New subsystem (mirrors documents/)
  __init__.py
  models.py                               # @dataclass: CalendarEvent, EventType, Tag, Reminder, ReminderRule, ExternalCalendarConfig, Invitation
  db.py                                   # aiosqlite: init_calendar_db(), CRUD functions
  service.py                              # CalendarService: business logic, event CRUD, type/tag management
  recurrence.py                           # RRULE expansion wrapper (dateutil.rrule)
  scheduler.py                            # ReminderScheduler: start/stop asyncio loop, scan + dispatch
  reminders.py                            # ReminderDispatcher: client push, email, webhook channels
  extraction.py                           # PostProcessingExtractor: keyword gate + LLM extraction
  sync/
    __init__.py
    base.py                               # AbstractSyncProvider interface
    caldav_provider.py                    # Apple Calendar sync via caldav library
    outlook_provider.py                   # Outlook sync via httpx + Graph API
    ics_provider.py                       # ICS import/export via icalendar library
    manager.py                            # SyncManager: orchestrate providers, conflict resolution

src/agentkit/tools/calendar_tool.py       # CalendarTool(Tool): create_event, query_events, update_event, delete_event

src/agentkit/server/routes/calendar.py    # APIRouter(prefix="/calendar"): REST endpoints

src/agentkit/server/frontend/src/
  api/calendar.ts                         # CalendarApiClient extends BaseApiClient
  stores/calendar.ts                      # useCalendarStore (Pinia Composition API)
  components/calendar/
    CalendarPanel.vue                     # Right panel tab content (summary view only — KTD-12)
    CalendarDrawer.vue                    # Large drawer wrapper (full management — KTD-12)
    CalendarGrid.vue                      # FullCalendar wrapper (month/week/day)
    CardView.vue                          # Kanban-style card view
    ListView.vue                          # Sorted list view
    EventBadge.vue                        # Source distinction badge (manual/agent/post_extract — G3)
    EventEditor.vue                       # Event create/edit form (a-drawer)
    ReminderConfig.vue                    # Reminder rule configuration
    SyncSettings.vue                      # External calendar sync config
    InvitationManager.vue                 # Invitation response UI
  views/CalendarView.vue                  # Top-level route page (optional, for drawer mode)

Wiring Points

Integration File Location
DB init + service src/agentkit/server/app.py lifespan startup (~line 263, next to init_documents_db())
Route registration src/agentkit/server/app.py ~line 954, app.include_router(calendar.router, prefix="/api/v1")
Tool registration src/agentkit/server/app.py ~line 268, agent._tool_registry.register(CalendarTool(...))
Scheduler start src/agentkit/server/app.py lifespan startup (~line 151, next to task_store.start_cleanup())
Scheduler stop src/agentkit/server/app.py lifespan shutdown (~line 430)
SyncManager start src/agentkit/server/app.py lifespan startup (next to reminder scheduler)
SyncManager stop src/agentkit/server/app.py lifespan shutdown (next to reminder scheduler stop)
Post-processing hook src/agentkit/server/routes/chat.py ~line 1158 (REACT), ~line 971 (DIRECT_CHAT)
WebSocket push src/agentkit/server/routes/chat.py chat_manager.send_json(session_id, {...})
Frontend right panel tab src/agentkit/server/frontend/src/components/layout/AgentLayout.vue bottomRightTabs array (~line 153)
Frontend WS handler src/agentkit/server/frontend/src/stores/chat.ts handleWsMessage() switch (~line 586)
Frontend WS types src/agentkit/server/frontend/src/api/types.ts WsServerMessage union (~line 111)

Data Model

# src/agentkit/calendar/models.py (dataclasses, mirrors documents/models.py)

@dataclass
class EventType:
    id: str               # UUID
    user_id: str          # FK to users
    name: str             # "会议", "截止", "个人"
    color: str            # "#4A90D9"
    is_default: bool      # system-provided types

@dataclass
class Tag:
    id: str
    user_id: str
    name: str

@dataclass
class CalendarEvent:
    id: str
    user_id: str
    title: str
    description: str
    start_time: str       # ISO 8601 UTC (see KTD-11)
    end_time: str         # ISO 8601 UTC
    is_all_day: bool
    location: str
    event_type_id: str | None
    rrule: str | None     # RFC 5545 RRULE string, e.g. "FREQ=WEEKLY;BYDAY=MO;COUNT=10"
    source: str           # "manual" | "agent" | "post_extract"
    is_invited: bool      # True if this event arrived via invitation (R33 — special styling)
    conversation_id: str | None  # origin traceability for agent/extracted events
    external_id: str | None      # ID in external calendar (for sync)
    external_provider: str | None  # "caldav" | "outlook"
    last_modified: str    # ISO 8601 UTC, for conflict resolution (last-write-wins)
    created_at: str       # ISO 8601 UTC

@dataclass
class EventTag:           # many-to-many junction
    event_id: str
    tag_id: str

@dataclass
class ReminderRule:
    id: str
    event_id: str         # FK to events (nullable for type-level defaults)
    event_type_id: str    # FK to event_types (for default reminders)
    offset_minutes: int   # -15 = 15 min before, -1440 = 1 day before
    channels: list[str]   # ["client", "email", "webhook"]

@dataclass
class ReminderDelivery:
    id: str
    reminder_rule_id: str
    event_id: str
    scheduled_time: str
    status: str           # "pending" | "sent" | "failed" | "read"
    channel: str
    attempts: int
    last_error: str | None

@dataclass
class ExternalCalendarConfig:
    id: str
    user_id: str
    provider: str         # "caldav" | "outlook"
    credentials: str      # encrypted JSON (CalDAV URL+user+app_password, or OAuth refresh_token)
    sync_frequency: int   # minutes
    sync_scope: list[str] # event type IDs to sync, empty = all
    last_sync: str | None
    sync_token: str | None  # delta token for incremental sync

@dataclass
class Invitation:
    id: str
    event_id: str
    inviter_user_id: str
    invitee_email: str
    status: str           # "pending" | "accepted" | "declined" | "tentative"
    responded_at: str | None

Implementation Units

U1. Backend data model & storage

  • Goal: Define calendar dataclasses, aiosqlite DB schema with CRUD functions, and RRULE recurrence expansion wrapper.
  • Files:
    • src/agentkit/calendar/__init__.py (new)
    • src/agentkit/calendar/models.py (new — dataclasses above)
    • src/agentkit/calendar/db.py (new — init_calendar_db(), insert_event(), get_event(), list_events(), update_event(), delete_event(), same for types/tags/reminders/configs/invitations)
    • src/agentkit/calendar/recurrence.py (new — expand_rrule(rrule_str, start_time, range_start, range_end) -> list[str] wrapper around dateutil.rrule)
  • Patterns: Mirror src/agentkit/documents/db.py (aiosqlite bare-connection, PRAGMA journal_mode=WAL, module-level async functions). Mirror src/agentkit/documents/models.py for dataclass DTO pattern. recurrence.py wraps dateutil.rrule.rrulestr() + between() for date-range expansion.
  • DB schema: 7 tables — calendar_events, calendar_event_types, calendar_tags, calendar_event_tags, calendar_reminder_rules, calendar_reminder_deliveries, calendar_external_configs, calendar_invitations. All with String(36) UUIDs, ISO 8601 UTC timestamps (see KTD-11), indexes on user_id and start_time.
  • RRULE expansion (G1, G9): recurrence.py provides expand_rrule(rrule_str, dtstart, range_start, range_end) returning a list of ISO 8601 UTC occurrence strings within the range. Used by: (a) CalendarService.list_events() to expand recurring events for display, (b) ReminderScheduler to find occurrences entering reminder window, (c) sync providers to push individual occurrences if external calendar requires expanded events. All times in UTC per KTD-11.
  • Test file: tests/unit/calendar/test_db.py, tests/unit/calendar/test_recurrence.py
  • Test scenarios:
    • test_init_calendar_db_creates_all_tables — call init_calendar_db(), verify tables exist
    • test_insert_and_get_event_roundtrip — insert event, fetch by id, all fields preserved (including is_invited)
    • test_list_events_by_user_filtered_by_date_range — insert 3 events, query range covering 2
    • test_update_event_modifies_fields — insert, update title, verify
    • test_delete_event_removes_record — insert, delete, verify gone
    • test_event_type_crud — create/list/update/delete event types
    • test_tag_many_to_many — event with 3 tags, query by tag returns event
    • test_reminder_rule_crud — create rule for event, create default rule for type
    • test_external_config_stores_encrypted_credentials — insert config, verify credentials field is opaque
    • test_expand_rrule_weekly_countFREQ=WEEKLY;BYDAY=MO;COUNT=4 from Monday → 4 occurrences
    • test_expand_rrule_daily_range_filterFREQ=DAILY starting Jan 1, range Jan 3Jan 5 → 3 occurrences
    • test_expand_rrule_until_clauseFREQ=DAILY;UNTIL=20260131 → occurrences stop at Jan 31
    • test_expand_rrule_no_rrule_returns_singlerrule=None → returns [start_time] only
    • test_expand_rrule_all_day_event — all-day event with RRULE, verify date (not datetime) expansion
    • test_expand_rrule_dst_boundary — event crossing DST transition, verify UTC consistency
  • Dependencies: None (foundation unit)

U2. Backend calendar service & REST API

  • Goal: Business logic layer + REST endpoints for full calendar CRUD, event types, tags, invitations, and non-admin user search for invitations.
  • Files:
    • src/agentkit/calendar/service.py (new — CalendarService class)
    • src/agentkit/server/routes/calendar.py (new — APIRouter(prefix="/calendar", tags=["calendar"]))
  • Patterns: Mirror src/agentkit/documents/service.py (service class with injected dependencies) and src/agentkit/server/routes/documents.py (route module with _verify_api_key or require_authenticated dependency, Pydantic request/response models, request.app.state.calendar_service access).
  • Service methods: create_event(), get_event(), list_events() (with RRULE expansion via recurrence.expand_rrule), update_event(), delete_event(), list_event_types(), create_event_type(), update_event_type(), list_tags(), create_tag(), create_invitation(), respond_to_invitation(), list_invitations(), search_users(q) (returns [{username, email}] for invitation picker — G5/A3).
  • REST endpoints:
    • POST /api/v1/calendar/events — create event
    • GET /api/v1/calendar/events?start=...&end=...&type_id=...&tag_id=... — list with filters (tag_id filter covers G2)
    • GET /api/v1/calendar/events/{id} — get single
    • PATCH /api/v1/calendar/events/{id} — update
    • DELETE /api/v1/calendar/events/{id} — delete
    • POST /api/v1/calendar/events/{id}/invitations — invite user by email (creates Invitation + pushes calendar_invitation WS to invitee — G6)
    • POST /api/v1/calendar/invitations/{id}/respond — accept/decline/tentative
    • GET /api/v1/calendar/invitations — list pending invitations for current user
    • GET /api/v1/calendar/users/search?q=xxx — non-admin user search by username/email, returns [{username, email}] only (no user_id leak) — G5/A3. Auth: any authenticated user.
    • GET /api/v1/calendar/event-types — list types
    • POST /api/v1/calendar/event-types — create type
    • PATCH /api/v1/calendar/event-types/{id} — update type
    • GET /api/v1/calendar/tags — list tags
    • POST /api/v1/calendar/tags — create tag
  • User search implementation (G5/A3): search_users(q) queries users table (from src/agentkit/server/auth/models.py UserModel) by username ILIKE '%q%' OR email ILIKE '%q%', returns top 10 results as [{username, email}]. Does NOT require admin permission — uses standard authenticated user dependency. Existing GET /api/v1/admin/users requires admin and has no username/email search, so this new endpoint is necessary.
  • Invitation notification (G6): When create_invitation() is called, after persisting the Invitation record, push calendar_invitation WS message to the invitee's active session(s) via chat_manager.send_json(). Invitee frontend shows a notification + updates invitation list.
  • Wiring: app.py lifespan — await init_calendar_db(), app.state.calendar_service = CalendarService(), app.include_router(calendar.router, prefix="/api/v1").
  • Test file: tests/unit/calendar/test_service.py, tests/unit/calendar/test_routes.py
  • Test scenarios:
    • test_create_event_with_type_and_tags — create event with type_id and 2 tags, verify all linked
    • test_list_events_filters_by_date_range — 3 events across days, filter returns correct subset
    • test_list_events_filters_by_type_and_tag — filter combination (G2)
    • test_list_events_filters_by_tag_only — 5 events, 2 with tag X, filter tag_id=X returns only 2 (G2)
    • test_list_events_expands_recurring — event with FREQ=DAILY;COUNT=3, list range covering 2 days → returns 2 expanded occurrences
    • test_update_event_partial_fields — PATCH only title, other fields unchanged
    • test_delete_event_cascades_reminders_and_tags — delete event, verify reminder rules and junction rows removed
    • test_create_invitation_pushes_ws_to_invitee — create invitation, verify chat_manager.send_json() called with calendar_invitation type (G6)
    • test_respond_to_invitation_updates_status — invite, respond "accepted", verify status + responded_at set
    • test_search_users_by_username — seed 3 users, search "alice" returns matching user with username+email only (G5)
    • test_search_users_by_email — search by email domain returns matches (G5)
    • test_search_users_no_match_returns_empty — search "zzz" returns []
    • test_search_users_requires_auth — no auth header → 401
    • test_search_users_returns_max_10 — seed 15 matching users, verify only 10 returned
    • test_event_type_default_color_persistence — create type with color, verify roundtrip
    • test_route_create_event_requires_auth — no auth header → 401
    • test_route_list_events_returns_paginated — large result set with limit/offset
  • Dependencies: U1

U3. Agent calendar tool (ReAct integration)

  • Goal: CalendarTool for ReAct loop — Agent autonomously calls create_event, query_events, update_event, delete_event.
  • Files:
    • src/agentkit/tools/calendar_tool.py (new — CalendarTool(Tool))
  • Patterns: Mirror src/agentkit/tools/document_tool.py — extend Tool ABC, define input_schema as JSON Schema dict, execute(**kwargs) dispatches by action parameter, returns {"success": True/False, ...} dict, errors caught not raised.
  • Tool actions:
    • create_event — params: title, start_time, end_time, description, location, event_type_name, tag_names, rrule, is_all_day
    • query_events — params: start_date, end_date, limit
    • update_event — params: event_id, fields to update
    • delete_event — params: event_id
  • Source marking: All events created via tool set source="agent" and conversation_id from execution context.
  • Wiring: app.py ~line 268 — agent._tool_registry.register(CalendarTool(service=calendar_service)).
  • Test file: tests/unit/tools/test_calendar_tool.py
  • Test scenarios:
    • test_create_event_action_returns_success — call execute with create_event action, verify event in DB with source="agent"
    • test_create_event_with_recurrence_sets_rrule — rrule param stored correctly
    • test_query_events_returns_list — create 2 events, query, verify both returned
    • test_update_event_action_modifies_fields — create then update title
    • test_delete_event_action_removes_record — create then delete
    • test_invalid_action_returns_error — unknown action → {"success": False, "error": "..."}
    • test_missing_required_field_returns_error — create_event without title → error
    • test_created_event_has_conversation_id — verify conversation_id is set from context
  • Dependencies: U2

U4. Post-processing extraction (keyword gating + LLM)

  • Goal: After conversation turn, detect time keywords (zero-LLM regex), if hit then call LLM to extract schedule info, create events with source="post_extract".
  • Files:
    • src/agentkit/calendar/extraction.py (new — PostProcessingExtractor class)
  • Patterns: Keyword regex follows the _TOOL_CONTEXT_RE / greeting regex pattern in src/agentkit/chat/request_preprocessor.py. LLM call via agentkit.llm gateway (reuse existing LLMGateway.complete() or acomplete()).
  • Keyword regex: 明天|后天|下周|本周|今天下午|今天上午|上午|下午|晚上|\d+点|\d+月\d+日|\d+号|开会|截止|deadline|schedule|reminder|提醒|预约|约定|安排
  • LLM extraction prompt: System prompt instructs LLM to extract events as JSON array [{title, start_time, end_time, description, event_type}] from conversation text. Empty array if no events.
  • Flow:
    1. Hook fires after assistant reply persisted in chat.py (~line 1158 REACT, ~line 971 DIRECT_CHAT)
    2. asyncio.create_task(extractor.extract(conversation_text, conversation_id, user_id))
    3. Regex keyword scan — if no match, return immediately (zero LLM cost)
    4. If match, call LLM gateway with extraction prompt
    5. Parse JSON response, create events with source="post_extract"
    6. Push calendar_event_created WS message to client
  • Wiring: chat.py — after sm.append_message() for assistant reply, call asyncio.create_task(post_extractor.extract(...)). Extractor instance on app.state.post_extractor.
  • Test file: tests/unit/calendar/test_extraction.py
  • Test scenarios:
    • test_keyword_regex_matches_chinese_time_words — "明天下午3点" matches, "继续优化吧" doesn't
    • test_keyword_regex_matches_english_time_words — "deadline tomorrow" matches
    • test_no_keyword_skips_llm_call — mock LLM gateway, verify .complete() never called when no keyword
    • test_keyword_hit_triggers_llm_extraction — mock LLM returns [{title: "会议", start_time: "..."}], verify event created with source="post_extract"
    • test_llm_returns_empty_array_creates_nothing — LLM returns [], no events created
    • test_malformed_llm_response_handled_gracefully — LLM returns invalid JSON, no crash, logged
    • test_extracted_events_have_conversation_id — verify conversation_id set
    • test_extraction_does_not_block_chat_response — verify extract is async, chat reply returns before extraction completes
  • Dependencies: U2, U3 (for source marking pattern)

U5. Reminder subsystem (scheduler + multi-channel dispatch)

  • Goal: Background scheduler scans upcoming events, matches reminder rules, dispatches via client push / email / webhook, tracks delivery status with retry.
  • Files:
    • src/agentkit/calendar/scheduler.py (new — ReminderScheduler class)
    • src/agentkit/calendar/reminders.py (new — ReminderDispatcher class)
  • Patterns: Scheduler follows src/agentkit/server/task_store.py start()/stop() + asyncio.create_task loop pattern. Dispatcher uses strategy pattern — one method per channel.
  • Scheduler loop:
    1. Every 60 seconds: query events where start_time + offset_minutes is within next 60s window
    2. For each match, check if ReminderDelivery already exists (idempotency)
    3. If not, create ReminderDelivery records (one per channel) and dispatch
    4. Failed deliveries retried up to 3 times with exponential backoff
  • Dispatcher channels:
    • clientchat_manager.send_json(session_id, {"type": "calendar_reminder", "data": {...}})
    • emailaiosmtplib.send() with SMTP config from agentkit.yaml
    • webhookhttpx.post() to user-configured webhook URL
  • Default reminders: When event created, if its event_type_id has default ReminderRules, clone them to the event.
  • Wiring: app.py lifespan startup — reminder_scheduler = ReminderScheduler(calendar_service, dispatcher); app.state.reminder_scheduler = reminder_scheduler; await reminder_scheduler.start(). Shutdown — await reminder_scheduler.stop().
  • Test file: tests/unit/calendar/test_scheduler.py, tests/unit/calendar/test_reminders.py
  • Test scenarios:
    • test_scheduler_finds_event_within_reminder_window — event 10 min away, rule offset -15min, scheduler finds it
    • test_scheduler_skips_event_outside_window — event 2 hours away, rule offset -15min, not found
    • test_idempotent_delivery_no_duplicate — scheduler runs twice, only one delivery record created
    • test_client_channel_sends_ws_message — mock chat_manager, verify send_json called
    • test_email_channel_sends_smtp — mock aiosmtplib, verify send called with correct params
    • test_webhook_channel_posts_to_url — mock httpx, verify POST called
    • test_failed_delivery_retries_up_to_3_times — mock channel to fail, verify 3 attempts
    • test_default_reminders_inherited_from_event_type — create event with type that has default rules, verify rules cloned
    • test_scheduler_start_stop_lifecycle — start(), verify task running, stop(), verify cancelled
  • Dependencies: U1, U2

U6. External sync — CalDAV (Apple Calendar)

  • Goal: Bidirectional sync with Apple Calendar via CalDAV protocol using caldav library, plus SyncManager orchestration.
  • Files:
    • src/agentkit/calendar/sync/base.py (new — AbstractSyncProvider interface: pull_changes(), push_changes(), test_connection())
    • src/agentkit/calendar/sync/caldav_provider.py (new — CalDAVSyncProvider)
    • src/agentkit/calendar/sync/manager.py (new — SyncManager class — G8)
  • Patterns: Provider interface allows future providers (Google Calendar) without changing SyncManager. CalDAV connection uses caldav.DAVClient(url) with user Apple ID + app-specific password.
  • SyncManager (G8): SyncManager orchestrates all providers. Methods: sync_all(user_id) (iterates user's ExternalCalendarConfigs, calls provider pull/push), sync_provider(config_id) (single provider), resolve_conflict(local_event, remote_event) (last-write-wins + WS notification). Called by: (a) scheduled sync (asyncio loop, same pattern as reminder scheduler), (b) manual "Sync Now" button, (c) after local event creation/update (push to external). SyncManager is registered on app.state.sync_manager and started/stopped in app.py lifespan.
  • Sync flow:
    1. pull_changes()calendar.date_search(start, end) to fetch events in range, match by external_id (CalDAV UID), create/update local events
    2. push_changes() — for local events modified since last_sync, calendar.save_event(ical_data) to push to CalDAV
    3. Conflict: last_modified comparison, last-write-wins, log conflict + push calendar_sync_conflict WS notification to user (G4)
  • ICS generation: Use icalendar library to convert CalendarEvent → iCalendar VEVENT (with RRULE if present).
  • Config: ExternalCalendarConfig stores CalDAV URL, username, app-specific password (encrypted). User configures via settings UI.
  • Test file: tests/unit/calendar/test_sync_caldav.py, tests/unit/calendar/test_sync_manager.py
  • Test scenarios:
    • test_caldav_provider_pull_creates_local_events — mock caldav client returning 2 events, verify local events created with external_id set
    • test_caldav_provider_pull_updates_existing_event — event already exists (matched by external_id), remote modified, local updated
    • test_caldav_provider_push_creates_remote_event — local event with no external_id, push creates remote, external_id stored
    • test_caldav_provider_push_updates_remote_event — local event modified, push updates remote
    • test_caldav_conflict_last_write_wins — both sides modified, newer last_modified wins, conflict logged
    • test_caldav_conflict_sends_ws_notification — conflict detected, verify chat_manager.send_json() called with calendar_sync_conflict type (G4)
    • test_caldav_rrule_roundtrip — event with RRULE synced, verify RRULE preserved in both directions
    • test_caldav_test_connection_success — mock returns calendars, test_connection() returns True
    • test_caldav_test_connection_failure — mock raises, test_connection() returns False with error message
    • test_sync_manager_sync_all_iterates_providers — 2 configs, verify both providers called (G8)
    • test_sync_manager_sync_provider_updates_last_sync — sync completes, verify config.last_sync updated (G8)
    • test_sync_manager_resolve_conflict_notifies_user — conflict, verify WS push (G8/G4)
  • Dependencies: U2, U8 (ICS generation)

U7. External sync — Outlook (Microsoft Graph API)

  • Goal: Bidirectional sync with Outlook Calendar via Microsoft Graph REST API using httpx.
  • Files:
    • src/agentkit/calendar/sync/outlook_provider.py (new — OutlookSyncProvider)
  • Patterns: Same AbstractSyncProvider interface. OAuth2 flow — user authorizes via browser, app stores refresh token, auto-refreshes access token. Graph API endpoints: GET /me/calendarView/delta for incremental pull, POST /me/events for create, PATCH /me/events/{id} for update.
  • Delta sync: Use Graph API delta query ($deltaToken / $skipToken) for incremental changes — first call is full sync, subsequent calls are incremental.
  • OAuth flow:
    1. User clicks "Connect Outlook" in settings UI
    2. Redirect to Microsoft login with Calendars.ReadWrite scope
    3. Callback receives authorization code, exchanges for access + refresh token
    4. Tokens stored in ExternalCalendarConfig.credentials (encrypted JSON)
    5. On token expiry (401), auto-refresh using refresh token
  • Config: ExternalCalendarConfig stores OAuth client_id, refresh_token, access_token (with expiry). Client ID configured in agentkit.yaml.
  • Test file: tests/unit/calendar/test_sync_outlook.py
  • Test scenarios:
    • test_outlook_provider_pull_delta_initial_sync — mock httpx returning 3 events, verify local events created
    • test_outlook_provider_pull_delta_incremental — mock returning 1 new + 1 updated, verify only those processed
    • test_outlook_provider_push_creates_remote_event — local event pushed, remote ID stored
    • test_outlook_provider_push_updates_remote_event — local modification pushed
    • test_outlook_token_refresh_on_401 — mock 401 response, verify refresh token used, request retried
    • test_outlook_conflict_last_write_wins — both sides modified, newer wins
    • test_outlook_rrule_roundtrip — recurring event synced, pattern preserved
    • test_outlook_test_connection_success — mock Graph /me returns user, True
  • Dependencies: U2, U8 (ICS for RRULE conversion)

U8. iCal/ICS import/export

  • Goal: Import .ics files to local calendar, export local calendar to .ics file, using icalendar library.
  • Files:
    • src/agentkit/calendar/sync/ics_provider.py (new — ICSProvider)
  • Patterns: icalendar.Calendar.from_ical(ics_bytes) for import, icalendar.Calendar() + add_component() for export. Map VEVENT fields to CalendarEvent dataclass.
  • Import flow: Parse ICS → for each VEVENT, create CalendarEvent (map SUMMARY→title, DTSTART→start_time, RRULE→rrule, etc.) → skip events with duplicate UID already imported.
  • Export flow: Query events in date range → for each, create VEVENT component → assemble into Calendar → serialize to .ics bytes → return as downloadable file.
  • REST endpoints (in routes/calendar.py):
    • POST /api/v1/calendar/import-ics — upload .ics file, parse, create events
    • GET /api/v1/calendar/export-ics?start=...&end=... — download .ics file
  • Test file: tests/unit/calendar/test_ics_provider.py
  • Test scenarios:
    • test_import_simple_ics_creates_event — single VEVENT ICS, verify event created with correct fields
    • test_import_recurring_ics_preserves_rrule — VEVENT with RRULE, verify rrule field set
    • test_import_all_day_event — DTSTART is date not datetime, verify is_all_day=True
    • test_import_skips_duplicate_uid — import same ICS twice, second import creates no new events
    • test_export_produces_valid_ics — create event, export, parse result with icalendar, verify roundtrip
    • test_export_includes_recurrence — event with rrule, export, verify RRULE in ICS
    • test_export_date_range_filter — 3 events, export range covering 2, verify only 2 in ICS
    • test_import_malformed_ics_raises_error — invalid ICS bytes, verify graceful error
  • Dependencies: U2

U9. Frontend store, API client & types

  • Goal: Pinia store, API client, and TypeScript types for calendar feature.
  • Files:
    • src/agentkit/server/frontend/src/api/calendar.ts (new — CalendarApiClient extends BaseApiClient, types co-located)
    • src/agentkit/server/frontend/src/stores/calendar.ts (new — useCalendarStore Composition API)
    • src/agentkit/server/frontend/src/api/types.ts (modify — add calendar_event_created, calendar_reminder, calendar_invitation, calendar_sync_conflict to WsServerMessage union)
  • Patterns: API client mirrors src/agentkit/server/frontend/src/api/documents.ts (extend BaseApiClient, singleton export, types co-located, runtime type guard isCalendarEvent). Store mirrors src/agentkit/server/frontend/src/stores/settings.ts (Composition API, ref() state, computed() getters, async actions with try/catch + isLoading).
  • API client methods: listEvents(), createEvent(), updateEvent(), deleteEvent(), listEventTypes(), createEventType(), listTags(), createTag(), importIcs(), exportIcs(), createInvitation(), respondToInvitation(), listInvitations(), searchUsers(q) (G5/A3), listExternalConfigs(), createExternalConfig(), testExternalConnection(), syncNow(configId).
  • Store state: events, eventTypes, tags, isLoading, error, selectedEvent, viewMode ('calendar'|'card'|'list'), dateRange, pendingInvitations, syncConflicts.
  • Store actions: loadEvents(), createEvent(), updateEvent(), deleteEvent(), loadEventTypes(), loadTags(), setViewMode(), loadInvitations(), respondToInvitation(), searchUsers(q), handleWsEvent() (dispatch for calendar_event_created, calendar_reminder, calendar_invitation, calendar_sync_conflict WS messages).
  • WS integration: stores/chat.ts handleWsMessage() — add case 'calendar_event_created', case 'calendar_reminder', case 'calendar_invitation', case 'calendar_sync_conflict' branches that call into calendar store (lazy import pattern to avoid circular deps).
  • Test file: tests/unit/frontend/calendar/store.test.ts (if frontend test infra exists; otherwise manual verification via npm run typecheck)
  • Test scenarios:
    • Type check: npm run typecheck passes with new types
    • test_loadEvents_populates_state — mock API, call loadEvents, verify events ref populated
    • test_createEvent_adds_to_state — call createEvent, verify events array updated
    • test_handleWsEvent_calendar_event_created — call handleWsEvent with calendar_event_created payload, verify event added to state
    • test_handleWsEvent_calendar_reminder — call handleWsEvent with reminder payload, verify notification shown
    • test_handleWsEvent_calendar_invitation — call handleWsEvent with invitation payload, verify pendingInvitations updated + notification shown (G6)
    • test_handleWsEvent_calendar_sync_conflict — call handleWsEvent with conflict payload, verify syncConflicts updated + alert shown (G4)
    • test_searchUsers_returns_results — call searchUsers("ali"), verify results populated (G5)
    • test_viewMode_switch — setViewMode('card'), verify state
  • Dependencies: U2 (API contract must exist)

U10. Frontend calendar views (3 views + right panel tab + drawer)

  • Goal: Three view modes (calendar grid / card / list), right panel tab integration (summary only), expandable drawer (full management), with event source distinction and invited-event styling.
  • Files:
    • src/agentkit/server/frontend/src/components/calendar/CalendarPanel.vue (new — right panel tab, summary view only per KTD-12)
    • src/agentkit/server/frontend/src/components/calendar/CalendarDrawer.vue (new — large drawer wrapper, full management per KTD-12)
    • src/agentkit/server/frontend/src/components/calendar/CalendarGrid.vue (new — FullCalendar wrapper)
    • src/agentkit/server/frontend/src/components/calendar/CardView.vue (new — kanban card view)
    • src/agentkit/server/frontend/src/components/calendar/ListView.vue (new — sorted list view)
    • src/agentkit/server/frontend/src/components/calendar/EventBadge.vue (new — source distinction badge/icon component — G3)
    • src/agentkit/server/frontend/src/components/layout/AgentLayout.vue (modify — add calendar to bottomRightTabs)
    • src/agentkit/server/frontend/src/components/layout/tabs/CalendarTab.vue (new — tab content wrapper)
    • package.json (modify — add @fullcalendar/vue3, @fullcalendar/daygrid, @fullcalendar/timegrid, @fullcalendar/interaction deps)
  • Patterns: Right panel tab follows existing QuadrantPanel tab pattern (add to bottomRightTabs, add named slot). Drawer follows a-drawer pattern from SkillDetail.vue (but wider, ~80% viewport). FullCalendar initialized in CalendarGrid.vue with Vue3 wrapper.
  • Event source distinction (G3, R15): Each event displays a visual indicator of its origin:
    • manual — no badge (default)
    • agent — small robot icon (🤖 or RobotOutlined) in event card/grid item
    • post_extract — small sparkle icon ( or ThunderboltOutlined) in event card/grid item
    • EventBadge.vue component renders the appropriate icon based on event.source field, used in CalendarGrid/CardView/ListView
  • Invited event styling (G7, R33): Events with is_invited=True render with a distinct visual treatment:
    • Dashed border instead of solid
    • Subtle background tint (e.g., light blue overlay)
    • "受邀" tag displayed next to title
    • Clicking an invited event opens InvitationManager (respond UI) instead of EventEditor
  • Timezone display (KTD-11): All times received from API are UTC ISO 8601. Frontend converts to local timezone for display using dayjs(utcTime).local() or native Intl.DateTimeFormat. No user-facing timezone selector — browser local timezone is used.
  • CalendarPanel (right panel tab — summary only per KTD-12):
    • Shows today's events + next 3 upcoming events as compact list
    • Each item shows time + title + source badge (G3)
    • "Expand" button opens CalendarDrawer
    • Visual highlight animation when new event created (via WS event trigger)
    • Does NOT include view mode switcher, event editor, or full calendar grid — those live in the drawer
  • CalendarDrawer (large drawer — full management per KTD-12):
    • a-drawer with width="80%" and placement="right"
    • View mode switcher (calendar/card/list) at top
    • Contains CalendarGrid, CardView, or ListView based on selected mode
    • "New Event" button opens EventEditor (U11)
    • Invitation management accessible via tab/section (U11)
  • CalendarGrid (FullCalendar):
    • Month/Week/Day views via FullCalendar
    • Drag-to-create (select time range → open EventEditor)
    • Drag-to-move (drag event to new time → call updateEvent)
    • Events colored by event_type.color
    • Recurring events expanded by backend (U2 list_events with RRULE expansion) — FullCalendar receives pre-expanded occurrences
    • Invited events (is_invited=True) styled with dashed border class (G7)
    • Styled with Notion Calendar aesthetic: clean borders, ample whitespace, soft colors
  • CardView:
    • Kanban-style, grouped by date or event type (toggle)
    • Cards draggable between groups (changes date or type)
    • Each card shows source badge (G3) and invited styling (G7)
  • ListView:
    • Sorted by start_time, supports inline edit of title, batch select with checkbox
    • Source column with badge (G3)
  • Wiring: AgentLayout.vue — add { key: 'calendar', label: '日历', icon: CalendarOutlined } to bottomRightTabs, add <template #calendar><CalendarTab /></template> slot.
  • Test file: Manual verification + npm run typecheck
  • Test scenarios:
    • Type check passes
    • Right panel shows calendar tab with today's events (summary only — no grid)
    • Each event in panel shows correct source badge (manual=none, agent=robot, post_extract=sparkle) (G3)
    • Clicking "Expand" opens drawer with 80% width
    • Drawer contains view mode switcher + full calendar grid
    • Calendar grid shows month view by default, can switch to week/day
    • Drag-select time range opens event editor
    • Drag event to new time updates event
    • Invited events show dashed border + "受邀" tag in all views (G7)
    • Clicking invited event opens invitation response UI, not editor (G7)
    • Card view groups by date, cards draggable, source badge visible (G3)
    • List view sorted by time, inline edit works, source column visible (G3)
    • New event from Agent triggers highlight animation in panel
    • Times displayed in browser local timezone (KTD-11)
  • Dependencies: U9

U11. Frontend event editor & management UI

  • Goal: Event create/edit form, drag-and-drop rescheduling, batch operations, invitation management.
  • Files:
    • src/agentkit/server/frontend/src/components/calendar/EventEditor.vue (new — a-drawer width 560)
    • src/agentkit/server/frontend/src/components/calendar/InvitationManager.vue (new — invitation list + respond buttons)
  • Patterns: Event editor follows a-drawer + form pattern from SkillDetail.vue. Form uses Ant Design Vue form components (a-form, a-input, a-date-picker, a-time-picker, a-select, a-tag).
  • EventEditor fields:
    • Title (a-input, required)
    • Description (a-textarea)
    • Date range (a-range-picker) + time (a-time-picker, hidden if all-day)
    • All-day toggle (a-switch)
    • Location (a-input)
    • Event type (a-select, colored options)
    • Tags (a-select mode="tags", user can create new)
    • Recurrence (a-select: none/daily/weekly/monthly/custom + custom RRULE builder)
    • Reminders (list of offset + channel selectors, add/remove)
  • Batch operations: In ListView, checkbox multi-select → toolbar with "Delete", "Change Type", "Add Tag" buttons.
  • InvitationManager: Shows list of invitees with status icons, "Invite" button opens user search dialog (calls GET /api/v1/calendar/users/search?q=xxx from U2 — G5/A3), invitee view shows accept/decline/tentative buttons. Also displays incoming invitations (received via calendar_invitation WS push from U2 — G6) with response buttons.
  • Test file: Manual verification + npm run typecheck
  • Test scenarios:
    • Type check passes
    • Create event with all fields, verify saved
    • Edit existing event, verify changes persisted
    • Set recurrence weekly, verify RRULE generated
    • Add 2 reminders (15min client + 1day email), verify saved
    • Batch select 3 events, delete, verify removed
    • Batch select, change type, verify all updated
    • Invite user by email, verify invitation created (G5)
    • User search dialog returns results from /api/v1/calendar/users/search (G5)
    • Invitation response (accept) updates status
    • Incoming invitation notification displays when calendar_invitation WS received (G6)
  • Dependencies: U9, U10

U12. Frontend reminder & external sync settings UI

  • Goal: Reminder notification display, external calendar connection settings, sync status.
  • Files:
    • src/agentkit/server/frontend/src/components/calendar/ReminderConfig.vue (new — default reminder rules per event type)
    • src/agentkit/server/frontend/src/components/calendar/SyncSettings.vue (new — external calendar config)
  • Patterns: Settings panels follow existing settings component patterns. Notification display uses Ant Design Vue a-notification or a-message component.
  • ReminderConfig:
    • Per event type: list of default reminder rules (offset + channels)
    • Add/remove rules, save to backend
    • When new event created with this type, rules auto-inherited
  • SyncSettings:
    • List of configured external calendars (provider, status, last_sync)
    • "Add Apple Calendar" → form for CalDAV URL + Apple ID + app-specific password
    • "Add Outlook" → OAuth redirect button
    • Per-provider: sync frequency, sync scope (which event types), "Sync Now" button, "Test Connection" button
    • Conflict notifications displayed as alerts when calendar_sync_conflict WS message received (G4) — shows event title, provider, conflict details, and resolution taken (last-write-wins)
  • Notification display:
    • calendar_reminder WS message → a-notification.warning() with event title + time
    • calendar_invitation WS message → a-notification.info() with inviter name + event title, link to respond (G6)
    • calendar_sync_conflict WS message → a-alert in SyncSettings panel showing conflict details (G4)
    • Tauri mode: also trigger system notification via Tauri notification API
  • Test file: Manual verification + npm run typecheck
  • Test scenarios:
    • Type check passes
    • Add default reminder rule to "会议" type, verify saved
    • Create event of type "会议", verify reminder inherited
    • Add Apple Calendar config, test connection (mock success)
    • Add Outlook config via OAuth (mock redirect)
    • "Sync Now" triggers sync, status updates
    • Reminder WS message displays notification
    • Invitation WS message displays notification with respond link (G6)
    • Conflict WS message displays alert in SyncSettings (G4)
  • Dependencies: U9, U10

Key Flows

  • F1. Agent tool-call creates event

    • Trigger: Agent identifies schedule in ReAct loop
    • Actors: A2 (Agent), A1 (User)
    • Steps: Agent calls CalendarTool.execute(action="create_event", ...) → service writes to DB with source="agent" → WS push calendar_event_created to client → right panel highlights new event → Agent tells user "已创建日程"
    • Covers: R10, R13, R37
  • F2. Post-processing extraction (keyword-gated)

    • Trigger: Conversation turn ends, assistant reply persisted
    • Actors: A2 (post-processing layer), A1 (User)
    • Steps: asyncio.create_task(extractor.extract(...)) → regex keyword scan → if no match, return (zero LLM) → if match, LLM extraction → parse JSON → create events with source="post_extract" → WS push → insert "检测到 N 条日程" hint in chat
    • Covers: R11, R12, R14
  • F3. External calendar sync

    • Trigger: Scheduled sync task (every N minutes per provider config, run by SyncManager) or manual "Sync Now"
    • Actors: A3 (external service), A1 (User)
    • Steps: SyncManager.sync(provider)provider.pull_changes() (fetch remote, match by external_id, create/update local) → provider.push_changes() (push local changes to remote) → conflict detection (last_modified comparison) → last-write-wins → calendar_sync_conflict WS notification to user (G4)
    • Covers: R20, R21, R23
  • F4. Reminder dispatch

    • Trigger: Scheduler loop (every 60s) finds event entering reminder window
    • Actors: A4 (reminder subsystem), A1 (User)
    • Steps: Query events where start_time + offset_minutes in next 60s → check idempotency (delivery exists?) → create ReminderDelivery records → dispatch per channel (client WS / email SMTP / webhook HTTP) → update delivery status → retry on failure (up to 3x)
    • Covers: R25, R26, R27, R29
  • F5. Manual event creation

    • Trigger: User clicks "New" or drag-selects time range in calendar view
    • Actors: A1 (User)
    • Steps: Open EventEditor drawer → fill form (title/time/type/tags/reminders) → save → calendarApi.createEvent() → store updates → event appears in view → if external sync configured, async push to external calendar
    • Covers: R16, R18

Dependencies

New Python dependencies (add to pyproject.toml)

Package Version Used by
python-dateutil>=2.9 latest U1 (RRULE expansion)
icalendar>=6.0 latest U6, U7, U8 (ICS format)
caldav>=1.3 latest U6 (Apple Calendar sync)
aiosmtplib>=3.0 latest U5 (email reminder channel)

New Node dependencies (add to package.json)

Package Used by
@fullcalendar/vue3 U10 (calendar grid)
@fullcalendar/daygrid U10 (month view)
@fullcalendar/timegrid U10 (week/day view)
@fullcalendar/interaction U10 (drag-and-drop)

Existing dependencies reused

  • httpx>=0.27 — Outlook Graph API calls (U7), webhook reminders (U5)
  • aiosqlite>=0.20 — local storage (U1)
  • pydantic>=2.0 — request/response models (U2)
  • fastapi>=0.110 — REST routes (U2)
  • redis>=5.0 — optional cache for sync tokens (U6, U7)

Sequencing

Phase 1 (Foundation):     U1 → U2
Phase 2 (Agent):          U3, U4 (parallel, both depend on U2)
Phase 3 (Reminders):      U5 (depends on U1, U2)
Phase 4 (External sync):  U8 → U6, U7 (U6/U7 parallel, both depend on U8)
Phase 5 (Frontend base):  U9 (depends on U2 API contract)
Phase 6 (Frontend UI):    U10 → U11, U12 (U11/U12 parallel, both depend on U10)

Units within the same phase can be developed in parallel. Phases are sequential — each phase's units depend on the prior phase's output.

Risks and Mitigations

Risk Severity Mitigation
FullCalendar bundle size impacts frontend load Medium Lazy-load calendar components (defineAsyncComponent), only load when tab activated
CalDAV iCloud requires app-specific password (user friction) Medium Document setup steps in UI, provide "How to get app-specific password" link
Outlook OAuth flow complexity in Tauri desktop mode Medium Use system browser for OAuth (not embedded webview), redirect to localhost callback
Post-processing LLM extraction adds latency/cost Medium Keyword gate ensures LLM only called when time keywords present; extraction is async, doesn't block chat
RRULE edge cases (DST, timezone, Feb 29) Low dateutil.rrule handles RFC 5545 edge cases; store all times in UTC, convert at display
Sync conflict data loss High Last-write-wins with conflict notification; never auto-delete, always log
Reminder scheduler misses events if server restarts Medium On startup, scan for events in reminder window that have no delivery records and dispatch them

Scope Boundaries

In scope (this plan)

  • Personal calendar with manual + Agent + post-processing event creation
  • Three view modes (calendar grid, card, list)
  • Apple Calendar (CalDAV), Outlook (Graph API), iCal/ICS bidirectional sync
  • Multi-channel reminder subsystem (client push, email, webhook)
  • Event invitations with accept/decline/tentative
  • Right panel tab + expandable drawer UX

Deferred for later

  • Google Calendar integration (not selected by user, add CalDAV/Google API provider later)
  • Team shared calendars (multi-user co-editing same calendar)
  • Calendar analytics/reports (event statistics, time distribution)
  • Resource booking (meeting rooms, equipment)
  • Mobile native app (current is Web/Tauri only)
  • Calendar subscription (read-only iCal feed URL for others to subscribe)

Outside this product's identity

  • Replacing professional calendar applications (Google Calendar, Notion Calendar) — AgentKit calendar's differentiation is Agent auto-detection, not full-featured calendar competition
  • Project management (task tracking, Gantt charts, dependency management)

Implementation History

  • U1-U12 实现完成 (commits 2ea799f..394d734): 12 个实现单元全部交付104 个单元测试通过ruff cleanfrontend typecheck clean。
  • Intent router 测试修复 (commit 4ea7801): 修复预存测试失败——关键词匹配平局应保持列表顺序而非字母序。
  • 代码走查修复 (commit 3fdee65): 使用 ce-code-review skill 多代理审查,发现 23 个问题2 Critical / 15 Major / 6 Minor全部修复。关键修复包括RRULE 无限循环防护、邀请/事件类型授权校验、SQLite 外键 PRAGMA、DTSTART 时区 bug、同步循环 since 重置、CalDAV 超时、Outlook token 过期检测、Webhook SSRF 防护、ICS 导入 DoS 限制。