feat(agentscope): add memory system and automation job support
- Add consumer_registry and pipeline_registry for runtime orchestration - Add Visibility schema for message filtering - Add PipelineSpec for agent pipeline configuration - Add automation job models and configuration - Remove memory_prompt.py (consolidated into memory system) - Update runtime components: context_loader, context_service, orchestrator, runner, tasks - Update toolkit: tool_config, tool_middleware, custom tools (calendar, user_lookup) - Add auth_helpers and calendar_domain utilities - Add system_agents.yaml configuration
This commit is contained in:
@@ -1,306 +0,0 @@
|
||||
# 自动化记忆与 Agent 重构设计(v3)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本次重构目标是把现有“router + worker”双阶段执行,收敛为更直接、可控、低 token 成本的执行链路。
|
||||
|
||||
明确目标:
|
||||
|
||||
1. 去除 `router agent`,由 `worker` 直接处理用户消息;
|
||||
2. `worker` 模型从 `deepseek-chat` 切换为 `qwen3.5-30b-a3b`;
|
||||
3. 关闭 `worker` 思考模式(thinking/reasoning off);
|
||||
4. 上下文策略从“今天+昨天”改为“向前回溯直到累计 20 条用户消息”;
|
||||
5. 引入专用 `memory agent`,负责记忆提取,模型为 `qwen3.5-flash`;
|
||||
6. 保持 `/api/v1/agent`、SSE、history 兼容演进,不引入第二运行时。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
1. 不新增独立部署的执行系统;
|
||||
2. 不引入复杂 DSL 编排;
|
||||
3. 不在本阶段改造前端交互形态;
|
||||
4. 不做跨域业务逻辑重写(仅围绕 agent 执行链路和 memory 提取职责)。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
### 3.1 重构后链路
|
||||
|
||||
```text
|
||||
API/Scheduler Trigger
|
||||
-> Context Window Resolver (last 20 user messages)
|
||||
-> Executor Dispatch
|
||||
-> Worker Executor (qwen3.5-30b-a3b, thinking off)
|
||||
-> Memory Executor (qwen3.5-flash, thinking off)
|
||||
-> Tool Authorization
|
||||
-> Persistence + Redis Stream
|
||||
-> SSE/History Consumers
|
||||
```
|
||||
|
||||
### 3.2 核心变化
|
||||
|
||||
1. 删除 router 阶段,缩短调用链路与阶段状态;
|
||||
2. 把“意图识别”内聚到 worker 提示词与执行策略;
|
||||
3. 把“记忆提取”从通用 worker 中剥离为独立 memory executor;
|
||||
4. 上下文装配改为固定规模策略,降低输入 token 波动。
|
||||
|
||||
## 4. 执行角色与职责边界
|
||||
|
||||
### 4.1 Worker Executor(主对话执行器)
|
||||
|
||||
- 模型:`qwen3.5-30b-a3b`
|
||||
- 配置:`thinking=off`
|
||||
- 职责:
|
||||
- 处理用户请求;
|
||||
- 在单次推理中完成意图判断、工具决策、结果回复;
|
||||
- 遵守工具白名单与安全约束。
|
||||
|
||||
### 4.2 Memory Executor(记忆提取执行器)
|
||||
|
||||
- 模型:`qwen3.5-flash`
|
||||
- 配置:`thinking=off`
|
||||
- 职责:
|
||||
- 从对话中提取稳定记忆候选;
|
||||
- 产出结构化记忆写入/遗忘建议;
|
||||
- 不承担通用对话和复杂工具编排。
|
||||
|
||||
### 4.3 工具权限边界
|
||||
|
||||
统一规则:
|
||||
|
||||
`effective_tools = declared_tools ∩ profile_allowlist ∩ system_allowlist`
|
||||
|
||||
- worker 默认可调用通用工具子集;
|
||||
- memory 默认仅允许 `memory_write`、`memory_forget`;
|
||||
- 拒绝调用必须落审计(原因、工具名、请求上下文摘要)。
|
||||
|
||||
## 5. 上下文窗口策略(替代 today_yesterday)
|
||||
|
||||
### 5.1 策略定义
|
||||
|
||||
窗口规则:从当前待处理消息向前回溯,直到累计到 20 条 `role=user` 消息。
|
||||
|
||||
细则:
|
||||
|
||||
1. 计数对象仅为 `role=user`;
|
||||
2. 为保持语义连续,窗口中保留相关 assistant/tool/system 消息;
|
||||
3. 若历史不足 20 条用户消息,返回全部可用历史;
|
||||
4. 当前用户消息默认计入 20 条统计。
|
||||
|
||||
### 5.2 伪代码
|
||||
|
||||
```text
|
||||
collect_context(messages, current_message_id, n=20):
|
||||
included = []
|
||||
user_count = 0
|
||||
|
||||
for msg in reverse(messages up to current_message_id):
|
||||
included.append(msg)
|
||||
if msg.role == "user":
|
||||
user_count += 1
|
||||
if user_count >= n:
|
||||
break
|
||||
|
||||
return reverse(included)
|
||||
```
|
||||
|
||||
### 5.3 预期收益
|
||||
|
||||
1. 输入 token 成本更稳定,不再受日期边界放大;
|
||||
2. router 去除后仍可控住 worker 输入规模;
|
||||
3. 在高频会话下显著降低上下文冗余。
|
||||
|
||||
## 6. 去除 Router 后的意图识别设计
|
||||
|
||||
去掉 router 后,必须在 worker 内完成“识别 + 决策 + 执行”。本节给出可落地方案。
|
||||
|
||||
### 6.1 方案 A:复用 RouterAgentOutput 语义到 Worker Prompt
|
||||
|
||||
做法:把原 router 的标签、判定规则、优先级放进 worker system prompt,让 worker先做内部意图归类,再进入执行。
|
||||
|
||||
优点:
|
||||
|
||||
1. 迁移风险低,行为连续性强;
|
||||
2. 便于快速下线 router;
|
||||
3. 对现有回归样本复用程度高。
|
||||
|
||||
不足:
|
||||
|
||||
1. 提示词偏长;
|
||||
2. 分类与执行耦合,调试颗粒度较粗。
|
||||
|
||||
### 6.2 方案 B:重写 Worker-Native 轻量意图识别提示词
|
||||
|
||||
做法:定义最小标签集(示例:`chat | tool_call | memory_write | memory_forget | reject`)和明确优先级规则,直接服务 worker 执行。
|
||||
|
||||
优点:
|
||||
|
||||
1. prompt 更短,token 更省;
|
||||
2. 更匹配“20 条用户消息窗口”策略;
|
||||
3. 长期维护成本更低。
|
||||
|
||||
不足:
|
||||
|
||||
1. 需要重建回归集与阈值;
|
||||
2. 初期存在行为漂移风险。
|
||||
|
||||
### 6.3 推荐路线:A -> B 两阶段
|
||||
|
||||
1. 第一阶段(稳定迁移):先落地方案 A,确保 router 去除后行为不突变;
|
||||
2. 第二阶段(成本优化):基于线上样本收敛到方案 B,压缩提示词和标签集。
|
||||
|
||||
该路线兼顾了“短期稳定”和“中期降本”。
|
||||
|
||||
## 7. 数据模型与配置约定
|
||||
|
||||
### 7.1 Execution Profile(精简版)
|
||||
|
||||
建议收敛为:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "chat_default",
|
||||
"executor": "worker",
|
||||
"model": {
|
||||
"name": "qwen3.5-30b-a3b",
|
||||
"thinking": "off"
|
||||
},
|
||||
"history_policy": {
|
||||
"mode": "last_n_user_messages",
|
||||
"n": 20,
|
||||
"include_current_user_message": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
memory profile 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "automation_memory_default",
|
||||
"executor": "memory",
|
||||
"model": {
|
||||
"name": "qwen3.5-flash",
|
||||
"thinking": "off"
|
||||
},
|
||||
"history_policy": {
|
||||
"mode": "last_n_user_messages",
|
||||
"n": 20,
|
||||
"include_current_user_message": true
|
||||
},
|
||||
"tool_policy": {
|
||||
"mode": "intersection",
|
||||
"allowlist": ["memory_write", "memory_forget"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 兼容字段策略
|
||||
|
||||
- 历史 `enable_router` 字段保留读取兼容,写入路径不再依赖;
|
||||
- 新任务默认不再产出 router 配置;
|
||||
- 执行路径仅依据 `executor` 与 profile 配置。
|
||||
|
||||
### 7.3 Metadata 扩展建议
|
||||
|
||||
建议标准字段:
|
||||
|
||||
- `origin: "chat" | "automation"`
|
||||
- `executor: "worker" | "memory"`
|
||||
- `execution_profile_name: string`
|
||||
- `hidden_from_user: boolean`
|
||||
|
||||
用于用户可见性隔离与全链路审计。
|
||||
|
||||
## 8. 协议影响与文档更新
|
||||
|
||||
按照“协议先行”原则,先更新 `docs/protocols/`:
|
||||
|
||||
1. `docs/protocols/agent/sse-events.md`
|
||||
- step 枚举从 `router|worker|memory` 收敛为 `worker|memory`;
|
||||
- 对旧客户端声明:`router` 事件可能不再出现。
|
||||
|
||||
2. `docs/protocols/agent/run-agent-input.md`
|
||||
- 增加 `history_policy.mode=last_n_user_messages` 语义与边界规则。
|
||||
|
||||
3. `docs/protocols/agent/api-endpoints.md`
|
||||
- 说明执行阶段由后端 profile 决定;
|
||||
- 不要求前端显式传入 router 或 executor 控制参数。
|
||||
|
||||
## 9. 迁移计划
|
||||
|
||||
### 阶段 1:协议与配置就绪
|
||||
|
||||
1. 完成协议文档更新;
|
||||
2. 增加 profile 新字段和兼容读取逻辑;
|
||||
3. 新建任务默认 profile 切到 worker/memory 双执行器模型。
|
||||
|
||||
### 阶段 2:执行链路切换
|
||||
|
||||
1. 下线 router 运行路径;
|
||||
2. worker 切换 `qwen3.5-30b-a3b` + thinking off;
|
||||
3. 上下文装配切为“20 条用户消息策略”。
|
||||
|
||||
### 阶段 3:memory agent 接管记忆提取
|
||||
|
||||
1. memory executor 切换 `qwen3.5-flash`;
|
||||
2. 自动记忆任务全量走 memory executor;
|
||||
3. 对比提取质量与成本,完成灰度放量。
|
||||
|
||||
### 阶段 4:优化与收敛
|
||||
|
||||
1. worker 意图识别从方案 A 迭代到方案 B;
|
||||
2. 清理 router 相关遗留代码和配置分支;
|
||||
3. 固化观测指标与报警阈值。
|
||||
|
||||
## 10. 测试与验收
|
||||
|
||||
### 10.1 单元测试
|
||||
|
||||
1. `last_n_user_messages` 窗口截取逻辑;
|
||||
2. 工具交集授权逻辑;
|
||||
3. profile 解析与兼容字段读取;
|
||||
4. memory 输出结构校验。
|
||||
|
||||
### 10.2 集成测试
|
||||
|
||||
1. 无 router 情况下 worker 正常执行;
|
||||
2. SSE/history 在 `worker|memory` 阶段下可稳定消费;
|
||||
3. 自动记忆任务完整链路可执行;
|
||||
4. hidden 消息对用户不可见但审计可见。
|
||||
|
||||
### 10.3 验收指标
|
||||
|
||||
P0:
|
||||
|
||||
1. router 下线后核心对话流程零阻断;
|
||||
2. 平均输入 token 相比 today_yesterday 明显下降;
|
||||
3. 工具调用越权率为 0。
|
||||
|
||||
P1:
|
||||
|
||||
1. memory 提取质量不低于现网基线;
|
||||
2. 延迟与成本达到预期区间;
|
||||
3. 协议兼容无前端回归。
|
||||
|
||||
## 11. 风险与回滚
|
||||
|
||||
主要风险:
|
||||
|
||||
1. worker 内聚识别导致误判率短期上升;
|
||||
2. 20 条用户消息窗口在极端长任务中可能信息不足;
|
||||
3. memory 轻量模型在复杂语义下提取质量波动。
|
||||
|
||||
回滚策略:
|
||||
|
||||
1. 保留 profile 级灰度开关,支持按租户/任务类型回切旧模型;
|
||||
2. 上下文策略支持临时扩容(20 -> 25)作为应急参数;
|
||||
3. memory agent 保留质量兜底阈值,低置信度结果不落库。
|
||||
|
||||
---
|
||||
|
||||
本版文档确立了清晰的一次性架构方向:
|
||||
|
||||
- router 从执行链路中移除;
|
||||
- worker 直连用户消息并承担意图识别;
|
||||
- memory 由专用 agent 执行;
|
||||
- 上下文按 20 条用户消息定长回溯,控制 token 成本;
|
||||
- 在兼容现有协议消费方式的前提下完成演进。
|
||||
@@ -1,449 +0,0 @@
|
||||
# Reminder Alert Archival Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Deliver alarm-style reminder popups with cancel/snooze actions, 30s timeout auto-snooze, overlap handling, and archived/gray lifecycle consistency across Android and iOS.
|
||||
|
||||
**Architecture:** Keep scheduling local on device (Flutter local notifications), persist user reminder actions with an app-side outbox for eventual backend sync, and use backend PATCH update for archive status as the source of truth. Add a backend safety net job to auto-archive expired active events so app-terminated scenarios still converge. Implement shared reminder payload and action handler with platform-specific notification configuration (Android full-screen intent, iOS category actions).
|
||||
|
||||
**Tech Stack:** Flutter (`flutter_local_notifications`, existing calendar service), FastAPI schedule-items API, SQLAlchemy service layer, uv/pytest, Dart tests.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update protocol and state semantics first
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/protocols/` (create a new reminder interaction protocol doc or extend existing schedule protocol doc)
|
||||
- Modify: `docs/runtime/runtime-route.md`
|
||||
|
||||
**Step 1: Write failing doc checks (manual checklist as fail-first gate)**
|
||||
|
||||
```text
|
||||
Checklist fails until all are documented:
|
||||
1) cancel action semantics
|
||||
2) snooze +10 minutes semantics
|
||||
3) timeout(30s) = ignore -> snooze
|
||||
4) overlap aggregation semantics
|
||||
5) archive + gray render semantics
|
||||
6) iOS degraded behavior note
|
||||
```
|
||||
|
||||
**Step 2: Run verification of checklist**
|
||||
|
||||
Run: manual review (expect FAIL before edits)
|
||||
|
||||
**Step 3: Write minimal protocol spec**
|
||||
|
||||
Include exact payload keys and action enum:
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "uuid",
|
||||
"title": "string",
|
||||
"startAt": "iso8601",
|
||||
"endAt": "iso8601|null",
|
||||
"timezone": "IANA",
|
||||
"location": "string|null",
|
||||
"notes": "string|null",
|
||||
"color": "#RRGGBB|null",
|
||||
"mode": "single|aggregate",
|
||||
"aggregateIds": ["uuid"]
|
||||
}
|
||||
```
|
||||
|
||||
Actions:
|
||||
- `cancel`: archive target events and stop reminders
|
||||
- `snooze_10m`: reschedule +10m, stop when `now >= endAt`
|
||||
- `timeout_30s`: same as `snooze_10m`
|
||||
|
||||
**Step 4: Verify checklist passes**
|
||||
|
||||
Run: manual review (expect PASS)
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/protocols docs/runtime/runtime-route.md
|
||||
git commit -m "docs: define reminder interaction protocol and lifecycle semantics"
|
||||
```
|
||||
|
||||
### Task 2: Add frontend reminder action models and payload codec
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/models/reminder_payload.dart`
|
||||
- Create: `apps/lib/features/calendar/reminders/models/reminder_action.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/models/reminder_payload_test.dart`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```dart
|
||||
test('round-trips payload with single and aggregate modes', () {
|
||||
final payload = ReminderPayload(...);
|
||||
expect(ReminderPayload.fromJson(payload.toJson()), payload);
|
||||
});
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/models/reminder_payload_test.dart`
|
||||
Expected: FAIL (type/file missing)
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement immutable model + json codec + enum parser.
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/models/reminder_payload_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders/models apps/test/features/calendar/reminders/models/reminder_payload_test.dart
|
||||
git commit -m "feat: add reminder payload and action models"
|
||||
```
|
||||
|
||||
### Task 3: Refactor local notification service for action-capable reminders (Android + iOS)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
- Modify: `apps/lib/main.dart`
|
||||
- Modify: `apps/ios/Runner/AppDelegate.swift`
|
||||
- Test: `apps/test/core/notifications/local_notification_service_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('uses alarm-style Android details with actions and timeout', () async {});
|
||||
test('uses Darwin category actions for cancel/snooze', () async {});
|
||||
test('encodes payload in notification details', () async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests to verify failure**
|
||||
|
||||
Run: `flutter test test/core/notifications/local_notification_service_test.dart`
|
||||
Expected: FAIL
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement:
|
||||
- Android notification actions: `cancel`, `snooze_10m`
|
||||
- `timeoutAfter: 30000`
|
||||
- payload serialization
|
||||
- iOS `DarwinNotificationCategory` + action identifiers
|
||||
- initialize callback registration for action responses
|
||||
|
||||
Also keep existing full-screen alarm setup and exact alarm fallback behavior.
|
||||
|
||||
**Step 4: Run tests to verify pass**
|
||||
|
||||
Run: `flutter test test/core/notifications/local_notification_service_test.dart`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/notifications/local_notification_service.dart apps/lib/main.dart apps/ios/Runner/AppDelegate.swift apps/test/core/notifications/local_notification_service_test.dart
|
||||
git commit -m "feat: support actionable reminder notifications on android and ios"
|
||||
```
|
||||
|
||||
### Task 4: Implement reminder action executor + local outbox for eventual consistency
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_action_executor.dart`
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_outbox_store.dart`
|
||||
- Modify: `apps/lib/features/calendar/data/services/calendar_service.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('cancel archives remotely and cancels local reminders', () async {});
|
||||
test('network failure writes outbox item and keeps local state updated', () async {});
|
||||
test('snooze reschedules +10m and stops after endAt', () async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Rules:
|
||||
- Cancel: local cancel now, enqueue archive API job, best-effort immediate PATCH
|
||||
- Snooze: schedule at `now + 10m`, if `next >= endAt` then archive path
|
||||
- Timeout action uses same path as snooze
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders apps/lib/features/calendar/data/services/calendar_service.dart apps/test/features/calendar/reminders/reminder_action_executor_test.dart
|
||||
git commit -m "feat: add reminder action executor with offline outbox"
|
||||
```
|
||||
|
||||
### Task 5: Add startup reconciliation (replay outbox + rebuild reminders)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/startup/auth_session_bootstrapper.dart`
|
||||
- Modify: `apps/lib/main.dart`
|
||||
- Test: `apps/test/core/startup/auth_session_bootstrapper_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('replays pending reminder actions after login', () async {});
|
||||
test('rebuilds reminders after outbox replay', () async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/core/startup/auth_session_bootstrapper_test.dart`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
In authenticated startup flow:
|
||||
1) replay outbox
|
||||
2) fetch events with overlap semantics (active and not ended)
|
||||
3) rebuild active reminders with compensation scheduling:
|
||||
- `now < remindAt`: schedule at remindAt
|
||||
- `remindAt <= now < endAt`: schedule immediate compensation reminder (e.g. +5s)
|
||||
- `now >= endAt`: archive path
|
||||
4) enforce reminder dedupe key to avoid duplicate reminders after reinstall/restart
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/core/startup/auth_session_bootstrapper_test.dart`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/core/startup/auth_session_bootstrapper.dart apps/lib/main.dart apps/test/core/startup/auth_session_bootstrapper_test.dart
|
||||
git commit -m "feat: replay reminder outbox on startup"
|
||||
```
|
||||
|
||||
### Task 6: Implement overlap strategy (aggregate popup)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/features/calendar/reminders/reminder_overlap_policy.dart`
|
||||
- Modify: `apps/lib/core/notifications/local_notification_service.dart`
|
||||
- Test: `apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
test('groups reminders whose fire time falls into same minute bucket', () {});
|
||||
test('creates aggregate payload with top-3 preview and ids', () {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_overlap_policy_test.dart`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Policy:
|
||||
- same minute bucket => one aggregate popup
|
||||
- actions apply to all members by default
|
||||
- payload includes aggregateIds
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/features/calendar/reminders/reminder_overlap_policy_test.dart`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/reminders/reminder_overlap_policy.dart apps/lib/core/notifications/local_notification_service.dart apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart
|
||||
git commit -m "feat: add overlap aggregation policy for reminders"
|
||||
```
|
||||
|
||||
### Task 7: Render archived events as gray in calendar UI
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/calendar/ui/**` (event color resolution points)
|
||||
- Test: `apps/test/features/calendar/ui/*archived*test.dart` (new if missing)
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```dart
|
||||
testWidgets('archived events use gray token color', (tester) async {});
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `flutter test test/features/calendar/ui`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Rule:
|
||||
- if `status == archived`, force token-based gray (do not mutate persisted `metadata.color`)
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `flutter test test/features/calendar/ui`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/calendar/ui apps/test/features/calendar/ui
|
||||
git commit -m "feat: render archived calendar events in gray"
|
||||
```
|
||||
|
||||
### Task 8: Backend reuse route + add expired-event auto-archive safety job
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/src/v1/schedule_items/service.py` (if needed for stricter status transition)
|
||||
- Modify: `backend/src/v1/schedule_items/repository.py` (range query to overlap query)
|
||||
- Create: `backend/src/jobs/schedule_item_archive_job.py` (or existing worker module path)
|
||||
- Modify: worker scheduler registration file under `backend/src/core/celery/` (actual existing path)
|
||||
- Test: `backend/tests/unit/v1/schedule_items/test_service.py`
|
||||
- Test: `backend/tests/unit/v1/schedule_items/test_repository.py`
|
||||
- Test: `backend/tests/unit/jobs/test_schedule_item_archive_job.py`
|
||||
|
||||
**Step 1: Write failing tests**
|
||||
|
||||
```python
|
||||
def test_patch_status_archived_allowed_for_owner() -> None: ...
|
||||
def test_list_by_overlap_includes_started_but_not_ended_items() -> None: ...
|
||||
def test_archive_job_marks_expired_active_items_archived() -> None: ...
|
||||
```
|
||||
|
||||
**Step 2: Run tests (expect FAIL)**
|
||||
|
||||
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py backend/tests/unit/jobs/test_schedule_item_archive_job.py -q`
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
Implement/verify:
|
||||
- route reuse: PATCH status archived works as-is for authorized user
|
||||
- overlap query for bootstrap: `start_at <= window_end AND (end_at IS NULL OR end_at >= window_start)` and `status=active`
|
||||
- periodic archive job: `end_at < now and status=active -> archived`
|
||||
|
||||
**Step 4: Re-run tests (expect PASS)**
|
||||
|
||||
Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py backend/tests/unit/jobs/test_schedule_item_archive_job.py -q`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src backend/tests
|
||||
git commit -m "feat: add expired schedule auto-archive safety job"
|
||||
```
|
||||
|
||||
### Task 9: End-to-end verification and release notes
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/runtime/runtime-runbook.md`
|
||||
- Modify: `docs/protocols/` reminder doc from Task 1
|
||||
|
||||
**Step 1: Run frontend verification**
|
||||
|
||||
Run:
|
||||
- `flutter analyze`
|
||||
- `flutter test`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 2: Run backend verification**
|
||||
|
||||
Run:
|
||||
- `uv run pytest backend/tests/unit/v1/schedule_items -q`
|
||||
- `uv run pytest backend/tests/unit/jobs/test_schedule_item_archive_job.py -q`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 3: Manual device matrix**
|
||||
|
||||
Android:
|
||||
- app foreground/background/terminated for cancel/snooze/timeout
|
||||
- overlap popup behavior
|
||||
- endAt stop reminder + archive
|
||||
|
||||
iOS:
|
||||
- action button behavior in foreground/background/terminated
|
||||
- timeout -> snooze behavior after relaunch sync
|
||||
- archive sync after offline period
|
||||
|
||||
**Step 4: Document operational caveats**
|
||||
|
||||
Include:
|
||||
- Android full-screen may degrade to heads-up by OEM policy
|
||||
- iOS does not guarantee Android-style full-screen alarm behavior
|
||||
- eventual consistency via outbox + startup replay + backend safety job
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/runtime/runtime-runbook.md docs/protocols
|
||||
git commit -m "docs: add reminder action runbook and platform caveats"
|
||||
```
|
||||
|
||||
## Notes on iOS parity
|
||||
|
||||
- iOS supports actionable local notifications via Darwin categories; implement `cancel` and `snooze_10m` action identifiers with same payload model.
|
||||
- iOS cannot guarantee Android-like forced full-screen alarm takeover; use lock-screen alert + sound + action buttons as equivalent UX.
|
||||
- App-terminated network callback reliability is lower on iOS; therefore outbox + startup replay is mandatory for parity.
|
||||
|
||||
## Data contracts and constraints (added)
|
||||
|
||||
### Reminder payload contract
|
||||
|
||||
```json
|
||||
{
|
||||
"eventId": "uuid",
|
||||
"title": "string",
|
||||
"startAt": "iso8601-with-offset",
|
||||
"endAt": "iso8601-with-offset|null",
|
||||
"timezone": "IANA",
|
||||
"location": "string|null",
|
||||
"notes": "string|null",
|
||||
"color": "#RRGGBB|null",
|
||||
"mode": "single|aggregate",
|
||||
"aggregateIds": ["uuid"]
|
||||
}
|
||||
```
|
||||
|
||||
Constraints:
|
||||
- `eventId` required and valid UUID.
|
||||
- `startAt` must be timezone-aware datetime.
|
||||
- `mode=aggregate` requires `aggregateIds.length >= 2`.
|
||||
- Payload versioning should be explicit if schema evolves.
|
||||
|
||||
### Reminder outbox contract
|
||||
|
||||
```json
|
||||
{
|
||||
"opId": "uuid",
|
||||
"eventId": "uuid",
|
||||
"action": "cancel|snooze_10m|timeout_30s|auto_archive",
|
||||
"targetStatus": "archived|null",
|
||||
"occurredAt": "iso8601-with-offset",
|
||||
"retryCount": 0,
|
||||
"nextRetryAt": "iso8601-with-offset|null",
|
||||
"state": "pending|done|dead",
|
||||
"lastError": "string|null"
|
||||
}
|
||||
```
|
||||
|
||||
Constraints:
|
||||
- Idempotency key: `(eventId, action, occurredAtBucket)`.
|
||||
- Exponential backoff retries with capped max attempts.
|
||||
- `cancel` and `auto_archive` both map to backend `status=archived` PATCH.
|
||||
|
||||
### Uniqueness and dedupe rules
|
||||
|
||||
- Notification identity uses deterministic key per event+cycle: `hash(eventId + cycleStartEpochMinutes + mode)`.
|
||||
- Before scheduling any reminder, cancel existing pending reminders for same dedupe key.
|
||||
- On bootstrap/reinstall, dedupe against local pending requests and outbox state before creating new schedules.
|
||||
- Compensation reminder (`remindAt <= now < endAt`) must generate exactly one immediate reminder per cycle window.
|
||||
|
||||
## Rollback plan
|
||||
|
||||
1) Disable action handling by feature flag while keeping plain reminders.
|
||||
2) Keep backend PATCH status route unchanged (safe rollback path).
|
||||
3) Pause auto-archive job if unexpected archival spikes occur.
|
||||
@@ -1,61 +0,0 @@
|
||||
# UI Schema File Split Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将 `apps/lib/core/schemas/ui_schema.dart` 从超大单文件拆分为同库的多个 `part` 文件,在不改变协议行为的前提下提升可维护性。
|
||||
|
||||
**Architecture:** 保留 `ui_schema.dart` 作为唯一对外入口和 single source of truth;通过 `part` 把 enums、common types、actions、nodes、document、builder 按职责拆分到 `core/schemas/ui_schema/` 子目录。所有类型名、JSON 字段、默认值与工厂方法逻辑保持完全一致,避免协议漂移。
|
||||
|
||||
**Tech Stack:** Dart library/part、Flutter analyze、Flutter test
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 建立拆分骨架并保留外部接口
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/schemas/ui_schema.dart`
|
||||
- Create: `apps/lib/core/schemas/ui_schema/enums.dart`
|
||||
- Create: `apps/lib/core/schemas/ui_schema/common_types.dart`
|
||||
- Create: `apps/lib/core/schemas/ui_schema/actions.dart`
|
||||
- Create: `apps/lib/core/schemas/ui_schema/nodes.dart`
|
||||
- Create: `apps/lib/core/schemas/ui_schema/document.dart`
|
||||
- Create: `apps/lib/core/schemas/ui_schema/builders.dart`
|
||||
|
||||
- [ ] **Step 1: 在主文件添加 `part` 声明并保留文件头注释**
|
||||
- [ ] **Step 1.1: 所有子文件统一使用 `part of '../ui_schema.dart';`,避免子目录相对路径错误**
|
||||
- [ ] **Step 2: 将 enum 定义迁移到 `enums.dart`,语义不变**
|
||||
- [ ] **Step 3: 将基础 DTO 迁移到 `common_types.dart`,语义不变**
|
||||
- [ ] **Step 4: 将 Action 协议与解析逻辑迁移到 `actions.dart`,语义不变**
|
||||
- [ ] **Step 5: 将 UiNode 与各 node 实现迁移到 `nodes.dart`,语义不变**
|
||||
- [ ] **Step 6: 将文档配置/文档模型迁移到 `document.dart`,语义不变**
|
||||
- [ ] **Step 7: 将 `buildSuccessDocument`/`buildErrorDocument` 迁移到 `builders.dart`**
|
||||
|
||||
### Task 2: 进行协议稳定性验证
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/test/core/schemas/ui_schema_test.dart`
|
||||
- Test: `apps/test/features/chat/ui_schema_renderer_test.dart`
|
||||
- Test: `apps/test/features/chat/ui_schema_navigation_test.dart`
|
||||
- Test: `apps/test/features/chat/ag_ui_event_test.dart`
|
||||
|
||||
- [ ] **Step 1: 新增 `ui_schema.dart` 直连回归测试**
|
||||
- 覆盖 enum fallback、`actionSpecFromJson` 分支、`UiNode.fromJson` 分支、Document/builder 默认值、round-trip 稳定性。
|
||||
- [ ] **Step 2: 在 `apps/` 目录运行 analyze,确认 `part` 结构无编译错误**
|
||||
- Run (`apps/`): `flutter analyze`
|
||||
- [ ] **Step 3: 在 `apps/` 目录运行 UI Schema 渲染与导航相关测试**
|
||||
- Run: `flutter test test/features/chat/ui_schema_renderer_test.dart`
|
||||
- Run: `flutter test test/features/chat/ui_schema_navigation_test.dart`
|
||||
- [ ] **Step 4: 在 `apps/` 目录运行 AG-UI 事件模型回归测试**
|
||||
- Run: `flutter test test/features/chat/ag_ui_event_test.dart`
|
||||
- [ ] **Step 5: 在 `apps/` 目录运行新增 schema 回归测试**
|
||||
- Run: `flutter test test/core/schemas/ui_schema_test.dart`
|
||||
|
||||
### Task 3: 完成收尾与风险核对
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/core/schemas/ui_schema.dart`
|
||||
- Modify: `apps/lib/core/schemas/ui_schema/*.dart`
|
||||
|
||||
- [ ] **Step 1: 检查 public API 未变化(类型名/函数名不变)**
|
||||
- [ ] **Step 2: 检查 JSON 键、默认值、fallback 分支未变化**
|
||||
- [ ] **Step 3: 确认 `ui_schema.dart` 行数显著下降并保留 single source 入口定位**
|
||||
Reference in New Issue
Block a user