54 KiB
| 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.py 的 start()/stop() + asyncio.create_task 循环模式,在 app.py lifespan 中启停。Rationale: 不引入新依赖,与现有后台任务模式一致(task_store cleanup loop、session writer loop)。ponytail: 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 同步使用 caldav 库(CalDAV 协议),Outlook 同步直接用 httpx 调用 Microsoft Graph REST API(不引入 msgraph SDK)。Rationale: caldav 库是 Python 生态中唯一成熟的 CalDAV 客户端;msgraph SDK 过重(数百 MB),httpx 已在依赖中,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 1158,DIRECT_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_created、calendar_reminder、calendar_invitation、calendar_sync_conflict 消息类型。Rationale: 避免新建 WS 连接,前端 chat store 已有消息分发机制。
KTD-11. UTC storage + local display timezone strategy. 所有时间字段(start_time、end_time、scheduled_time、last_modified、created_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 arounddateutil.rrule)
- Patterns: Mirror
src/agentkit/documents/db.py(aiosqlite bare-connection,PRAGMA journal_mode=WAL, module-level async functions). Mirrorsrc/agentkit/documents/models.pyfor dataclass DTO pattern.recurrence.pywrapsdateutil.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 withString(36)UUIDs, ISO 8601 UTC timestamps (see KTD-11), indexes onuser_idandstart_time. - RRULE expansion (G1, G9):
recurrence.pyprovidesexpand_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)ReminderSchedulerto 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— callinit_calendar_db(), verify tables existtest_insert_and_get_event_roundtrip— insert event, fetch by id, all fields preserved (includingis_invited)test_list_events_by_user_filtered_by_date_range— insert 3 events, query range covering 2test_update_event_modifies_fields— insert, update title, verifytest_delete_event_removes_record— insert, delete, verify gonetest_event_type_crud— create/list/update/delete event typestest_tag_many_to_many— event with 3 tags, query by tag returns eventtest_reminder_rule_crud— create rule for event, create default rule for typetest_external_config_stores_encrypted_credentials— insert config, verify credentials field is opaquetest_expand_rrule_weekly_count—FREQ=WEEKLY;BYDAY=MO;COUNT=4from Monday → 4 occurrencestest_expand_rrule_daily_range_filter—FREQ=DAILYstarting Jan 1, range Jan 3–Jan 5 → 3 occurrencestest_expand_rrule_until_clause—FREQ=DAILY;UNTIL=20260131→ occurrences stop at Jan 31test_expand_rrule_no_rrule_returns_single—rrule=None→ returns[start_time]onlytest_expand_rrule_all_day_event— all-day event with RRULE, verify date (not datetime) expansiontest_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 —CalendarServiceclass)src/agentkit/server/routes/calendar.py(new —APIRouter(prefix="/calendar", tags=["calendar"]))
- Patterns: Mirror
src/agentkit/documents/service.py(service class with injected dependencies) andsrc/agentkit/server/routes/documents.py(route module with_verify_api_keyorrequire_authenticateddependency, Pydantic request/response models,request.app.state.calendar_serviceaccess). - Service methods:
create_event(),get_event(),list_events()(with RRULE expansion viarecurrence.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 eventGET /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 singlePATCH /api/v1/calendar/events/{id}— updateDELETE /api/v1/calendar/events/{id}— deletePOST /api/v1/calendar/events/{id}/invitations— invite user by email (creates Invitation + pushescalendar_invitationWS to invitee — G6)POST /api/v1/calendar/invitations/{id}/respond— accept/decline/tentativeGET /api/v1/calendar/invitations— list pending invitations for current userGET /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 typesPOST /api/v1/calendar/event-types— create typePATCH /api/v1/calendar/event-types/{id}— update typeGET /api/v1/calendar/tags— list tagsPOST /api/v1/calendar/tags— create tag
- User search implementation (G5/A3):
search_users(q)queriesuserstable (fromsrc/agentkit/server/auth/models.pyUserModel) byusername ILIKE '%q%' OR email ILIKE '%q%', returns top 10 results as[{username, email}]. Does NOT require admin permission — uses standard authenticated user dependency. ExistingGET /api/v1/admin/usersrequires admin and has no username/email search, so this new endpoint is necessary. - Invitation notification (G6): When
create_invitation()is called, after persisting theInvitationrecord, pushcalendar_invitationWS message to the invitee's active session(s) viachat_manager.send_json(). Invitee frontend shows a notification + updates invitation list. - Wiring:
app.pylifespan —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 linkedtest_list_events_filters_by_date_range— 3 events across days, filter returns correct subsettest_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 withFREQ=DAILY;COUNT=3, list range covering 2 days → returns 2 expanded occurrencestest_update_event_partial_fields— PATCH only title, other fields unchangedtest_delete_event_cascades_reminders_and_tags— delete event, verify reminder rules and junction rows removedtest_create_invitation_pushes_ws_to_invitee— create invitation, verifychat_manager.send_json()called withcalendar_invitationtype (G6)test_respond_to_invitation_updates_status— invite, respond "accepted", verify status +responded_atsettest_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 → 401test_search_users_returns_max_10— seed 15 matching users, verify only 10 returnedtest_event_type_default_color_persistence— create type with color, verify roundtriptest_route_create_event_requires_auth— no auth header → 401test_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— extendToolABC, defineinput_schemaas JSON Schema dict,execute(**kwargs)dispatches byactionparameter, 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_dayquery_events— params: start_date, end_date, limitupdate_event— params: event_id, fields to updatedelete_event— params: event_id
- Source marking: All events created via tool set
source="agent"andconversation_idfrom 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 correctlytest_query_events_returns_list— create 2 events, query, verify both returnedtest_update_event_action_modifies_fields— create then update titletest_delete_event_action_removes_record— create then deletetest_invalid_action_returns_error— unknown action →{"success": False, "error": "..."}test_missing_required_field_returns_error— create_event without title → errortest_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 —PostProcessingExtractorclass)
- Patterns: Keyword regex follows the
_TOOL_CONTEXT_RE/ greeting regex pattern insrc/agentkit/chat/request_preprocessor.py. LLM call viaagentkit.llmgateway (reuse existingLLMGateway.complete()oracomplete()). - 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:
- Hook fires after assistant reply persisted in
chat.py(~line 1158 REACT, ~line 971 DIRECT_CHAT) asyncio.create_task(extractor.extract(conversation_text, conversation_id, user_id))- Regex keyword scan — if no match, return immediately (zero LLM cost)
- If match, call LLM gateway with extraction prompt
- Parse JSON response, create events with
source="post_extract" - Push
calendar_event_createdWS message to client
- Hook fires after assistant reply persisted in
- Wiring:
chat.py— aftersm.append_message()for assistant reply, callasyncio.create_task(post_extractor.extract(...)). Extractor instance onapp.state.post_extractor. - Test file:
tests/unit/calendar/test_extraction.py - Test scenarios:
test_keyword_regex_matches_chinese_time_words— "明天下午3点" matches, "继续优化吧" doesn'ttest_keyword_regex_matches_english_time_words— "deadline tomorrow" matchestest_no_keyword_skips_llm_call— mock LLM gateway, verify.complete()never called when no keywordtest_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 createdtest_malformed_llm_response_handled_gracefully— LLM returns invalid JSON, no crash, loggedtest_extracted_events_have_conversation_id— verify conversation_id settest_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 —ReminderSchedulerclass)src/agentkit/calendar/reminders.py(new —ReminderDispatcherclass)
- Patterns: Scheduler follows
src/agentkit/server/task_store.pystart()/stop()+asyncio.create_taskloop pattern. Dispatcher uses strategy pattern — one method per channel. - Scheduler loop:
- Every 60 seconds: query events where
start_time + offset_minutesis within next 60s window - For each match, check if
ReminderDeliveryalready exists (idempotency) - If not, create
ReminderDeliveryrecords (one per channel) and dispatch - Failed deliveries retried up to 3 times with exponential backoff
- Every 60 seconds: query events where
- Dispatcher channels:
client—chat_manager.send_json(session_id, {"type": "calendar_reminder", "data": {...}})email—aiosmtplib.send()with SMTP config fromagentkit.yamlwebhook—httpx.post()to user-configured webhook URL
- Default reminders: When event created, if its
event_type_idhas defaultReminderRules, clone them to the event. - Wiring:
app.pylifespan 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 ittest_scheduler_skips_event_outside_window— event 2 hours away, rule offset -15min, not foundtest_idempotent_delivery_no_duplicate— scheduler runs twice, only one delivery record createdtest_client_channel_sends_ws_message— mock chat_manager, verify send_json calledtest_email_channel_sends_smtp— mock aiosmtplib, verify send called with correct paramstest_webhook_channel_posts_to_url— mock httpx, verify POST calledtest_failed_delivery_retries_up_to_3_times— mock channel to fail, verify 3 attemptstest_default_reminders_inherited_from_event_type— create event with type that has default rules, verify rules clonedtest_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
caldavlibrary, plus SyncManager orchestration. - Files:
src/agentkit/calendar/sync/base.py(new —AbstractSyncProviderinterface:pull_changes(),push_changes(),test_connection())src/agentkit/calendar/sync/caldav_provider.py(new —CalDAVSyncProvider)src/agentkit/calendar/sync/manager.py(new —SyncManagerclass — 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):
SyncManagerorchestrates all providers. Methods:sync_all(user_id)(iterates user'sExternalCalendarConfigs, 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).SyncManageris registered onapp.state.sync_managerand started/stopped inapp.pylifespan. - Sync flow:
pull_changes()—calendar.date_search(start, end)to fetch events in range, match byexternal_id(CalDAV UID), create/update local eventspush_changes()— for local events modified sincelast_sync,calendar.save_event(ical_data)to push to CalDAV- Conflict:
last_modifiedcomparison, last-write-wins, log conflict + pushcalendar_sync_conflictWS notification to user (G4)
- ICS generation: Use
icalendarlibrary to convertCalendarEvent→ iCalendar VEVENT (with RRULE if present). - Config:
ExternalCalendarConfigstores 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 settest_caldav_provider_pull_updates_existing_event— event already exists (matched by external_id), remote modified, local updatedtest_caldav_provider_push_creates_remote_event— local event with no external_id, push creates remote, external_id storedtest_caldav_provider_push_updates_remote_event— local event modified, push updates remotetest_caldav_conflict_last_write_wins— both sides modified, newer last_modified wins, conflict loggedtest_caldav_conflict_sends_ws_notification— conflict detected, verifychat_manager.send_json()called withcalendar_sync_conflicttype (G4)test_caldav_rrule_roundtrip— event with RRULE synced, verify RRULE preserved in both directionstest_caldav_test_connection_success— mock returns calendars, test_connection() returns Truetest_caldav_test_connection_failure— mock raises, test_connection() returns False with error messagetest_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
AbstractSyncProviderinterface. OAuth2 flow — user authorizes via browser, app stores refresh token, auto-refreshes access token. Graph API endpoints:GET /me/calendarView/deltafor incremental pull,POST /me/eventsfor 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:
- User clicks "Connect Outlook" in settings UI
- Redirect to Microsoft login with
Calendars.ReadWritescope - Callback receives authorization code, exchanges for access + refresh token
- Tokens stored in
ExternalCalendarConfig.credentials(encrypted JSON) - On token expiry (401), auto-refresh using refresh token
- Config:
ExternalCalendarConfigstores OAuth client_id, refresh_token, access_token (with expiry). Client ID configured inagentkit.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 createdtest_outlook_provider_pull_delta_incremental— mock returning 1 new + 1 updated, verify only those processedtest_outlook_provider_push_creates_remote_event— local event pushed, remote ID storedtest_outlook_provider_push_updates_remote_event— local modification pushedtest_outlook_token_refresh_on_401— mock 401 response, verify refresh token used, request retriedtest_outlook_conflict_last_write_wins— both sides modified, newer winstest_outlook_rrule_roundtrip— recurring event synced, pattern preservedtest_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
icalendarlibrary. - 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 toCalendarEventdataclass. - 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 eventsGET /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 fieldstest_import_recurring_ics_preserves_rrule— VEVENT with RRULE, verify rrule field settest_import_all_day_event— DTSTART is date not datetime, verify is_all_day=Truetest_import_skips_duplicate_uid— import same ICS twice, second import creates no new eventstest_export_produces_valid_ics— create event, export, parse result with icalendar, verify roundtriptest_export_includes_recurrence— event with rrule, export, verify RRULE in ICStest_export_date_range_filter— 3 events, export range covering 2, verify only 2 in ICStest_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 —useCalendarStoreComposition API)src/agentkit/server/frontend/src/api/types.ts(modify — addcalendar_event_created,calendar_reminder,calendar_invitation,calendar_sync_conflicttoWsServerMessageunion)
- Patterns: API client mirrors
src/agentkit/server/frontend/src/api/documents.ts(extendBaseApiClient, singleton export, types co-located, runtime type guardisCalendarEvent). Store mirrorssrc/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 forcalendar_event_created,calendar_reminder,calendar_invitation,calendar_sync_conflictWS messages). - WS integration:
stores/chat.tshandleWsMessage()— addcase '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 vianpm run typecheck) - Test scenarios:
- Type check:
npm run typecheckpasses with new types test_loadEvents_populates_state— mock API, call loadEvents, verify events ref populatedtest_createEvent_adds_to_state— call createEvent, verify events array updatedtest_handleWsEvent_calendar_event_created— call handleWsEvent with calendar_event_created payload, verify event added to statetest_handleWsEvent_calendar_reminder— call handleWsEvent with reminder payload, verify notification showntest_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
- Type check:
- 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 — addcalendartobottomRightTabs)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/interactiondeps)
- Patterns: Right panel tab follows existing
QuadrantPaneltab pattern (add tobottomRightTabs, add named slot). Drawer followsa-drawerpattern fromSkillDetail.vue(but wider, ~80% viewport). FullCalendar initialized inCalendarGrid.vuewith Vue3 wrapper. - Event source distinction (G3, R15): Each event displays a visual indicator of its origin:
manual— no badge (default)agent— small robot icon (🤖 orRobotOutlined) in event card/grid itempost_extract— small sparkle icon (✨ orThunderboltOutlined) in event card/grid itemEventBadge.vuecomponent renders the appropriate icon based onevent.sourcefield, used in CalendarGrid/CardView/ListView
- Invited event styling (G7, R33): Events with
is_invited=Truerender 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 nativeIntl.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-drawerwithwidth="80%"andplacement="right"- View mode switcher (calendar/card/list) at top
- Contains
CalendarGrid,CardView, orListViewbased 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_eventswith 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 }tobottomRightTabs, 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-drawerwidth 560)src/agentkit/server/frontend/src/components/calendar/InvitationManager.vue(new — invitation list + respond buttons)
- Patterns: Event editor follows
a-drawer+ form pattern fromSkillDetail.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=xxxfrom U2 — G5/A3), invitee view shows accept/decline/tentative buttons. Also displays incoming invitations (received viacalendar_invitationWS 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_invitationWS 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-notificationora-messagecomponent. - 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_conflictWS message received (G4) — shows event title, provider, conflict details, and resolution taken (last-write-wins)
- Notification display:
calendar_reminderWS message →a-notification.warning()with event title + timecalendar_invitationWS message →a-notification.info()with inviter name + event title, link to respond (G6)calendar_sync_conflictWS message →a-alertin 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 withsource="agent"→ WS pushcalendar_event_createdto 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 withsource="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_conflictWS notification to user (G4) - Covers: R20, R21, R23
- Trigger: Scheduled sync task (every N minutes per provider config, run by
-
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_minutesin next 60s → check idempotency (delivery exists?) → createReminderDeliveryrecords → 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
EventEditordrawer → 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)