状态: V1 实现前架构基线已冻结;v1.2 修正 Python 最低版本并补全异步运行时、时间与并发闭环
目标: Windows-first、Local-first、单用户个人音乐 Agent
主要技术栈: Python + TypeScript
设计优先级: 轻量运行、低空闲资源占用、低 Token 成本、清晰插件边界、易维护、功能可独立开发
Music Agent 是桌面个人音乐助手,而不是传统播放器,也不是泛用聊天机器人。
V1 聚焦:
- 通过自然语言或直接控制本地/外部音乐播放;
- 本地曲库与歌单管理;
- 个性化推荐、每日推荐与场景推荐;
- 音乐分析与解释;
- 音乐相关对话;
- 轻量长期个性化;
- 以插件为核心的业务能力扩展。
V1 定位为单设备、Local-first。以下内容不属于 V1:多设备同步、云账号体系、内置本地基础模型、插件市场、复杂工作流引擎、高级编曲/生成、大规模自治 Agent Graph。
V1 使用 Python + TypeScript。
推荐默认分工:
- Python: Core 调度、插件框架、Provider、SQLite/Repository、AI/模型/音频相关业务逻辑;
- TypeScript: 桌面 UI、交互层、视觉状态、前端事件与用户控制。
这是实现阶段的默认建议,而不是要求所有架构对象必须绑定某一种语言。第一方插件应尽量使用项目统一技术栈。Core 可以知道插件的 runtime 类型和兼容性要求。
V1 Core 与第一方 Python 代码的最低版本要求为:
Python >= 3.10
项目级版本约束定义为:
>=3.10
工程约束:
- V1 的验证基线至少覆盖 Python 3.10、3.11、3.12;这三个版本必须运行核心 Contract、Plugin Runtime、异步 Runtime 与 E2E Pipeline 测试;
- 不人为设置
<3.13一类上限。更新的 Python 版本可以使用,但只有进入 dev-harness/CI 验证矩阵后才视为正式验证版本; - Core 与第一方插件不得无条件使用高于 3.10 才出现的语法或标准库 API;确有需要时必须提供兼容实现;
- 第三方
process插件由自身完整runtime.command决定解释器和运行环境,Core 不替插件拼 Python 命令; - 插件可以声明自己的兼容性元数据,但不得提高 Core 的最低 Python 版本要求。
- 重型模型/音频进程按需加载;
- 空闲状态不应无必要常驻分析或模型 worker;
- V1 不内置 LLM 权重;
- 优先事件驱动更新,避免轮询;
- 当任务结构已经明确时,优先确定性本地执行,而不是调用 LLM。
Desktop UI (TypeScript)
│ request ▲ CoreResponse
▼ │
Core Transport / Reply Channel
│
▼
InputGateway
│
▼
RequestCoordinator
│
▼
IntentRouter
│
▼
ContextResolver
│
▼
TaskRouter
│
▼
ExecutionPolicy
│
direct / CoreTaskQueue
+ TaskSupervisor
│
▼
PluginManager
│
┌─────────────────┴─────────────────┐
▼ ▼
Builtin Plugin Process Plugin
│ │
└────────── Unified Contract ───────┘
│
▼
PluginResult
│
▼
ResultValidator
│
▼
StateCoordinator
/ \
▼ ▼
RuntimeStateStore SQLite
\ /
└───────┬───────┘
│ committed result
▼
RequestCoordinator
│ │
│ └─ emit notification Event
▼ │
CoreResponse ▼
│ Lightweight EventBus
▼ │
Core Transport / Reply Channel ▼
│ UI / Core observers
▼
Desktop UI
两套旁路的轻量运行时索引支撑整条请求链:
CapabilityRegistry -> 当前有哪些能力及其契约
ProviderRegistry -> 当前配置并可用的公共/外部 Provider
两者都由配置文件和插件 Manifest 构建为轻量内存快照。正常请求不会反复读取或解析 YAML。
请求流水线保持单向。下游模块不得重新解释上游已经确定的决策。
Input
-> RequestCoordinator
-> IntentRouter
-> ContextResolver
-> TaskRouter
-> ExecutionPolicy
-> Plugin execution
-> Result validation
-> State commit
-> Request completion
-> CoreResponse
-> original caller/UI
V1 输入类型:
UserInput
├── text
├── voice -> ASR -> text path
├── hotkey
└── UI event
SystemTrigger
├── timer
├── startup
├── track_finished
└── plugin/runtime event
物理快捷键/UI 明确命令可以直接映射到已知 Capability,绕过语义 NLP。
负责请求生命周期与澄清状态。
职责:
- 分配
request_id; - 检查并恢复
PendingRequest; - 将澄清回答填回原始请求;
- 协调取消、失败、完成;
- 当一次用户请求明确要求多个 Capability 时,执行小型有序
TaskChain。
PendingRequest 属于 RequestCoordinator,而不是 ContextResolver。
RequestCoordinator 不只负责把请求送入流水线,还负责维护请求生命周期直到终态。accepted 只代表 Core 已经接管任务,绝不代表异步请求已经完成。
统一应用层响应 Envelope:
CoreResponse {
request_id,
status, # accepted | completed | needs_clarification | failed | cancelled | timed_out | rejected | interrupted
terminal, # bool
data,
error,
task_ids[],
clarification # optional
}
核心规则:
PluginResult是 Plugin -> Core 的任务级内部结果;CoreResponse是 Core -> UI/调用方 的请求级结果;CoreResponse(status=accepted, terminal=false)只是 ACK;异步请求必须继续进入终态;needs_clarification对应WAITING_INPUT,同样不是终态;- 只有
completed / failed / cancelled / timed_out / rejected / interrupted属于终态; completed只能在 PluginResult 验证成功、必要状态提交完成、TaskChain 已完成后生成;- UI 通过
request_id查询权威 RequestState,不依赖 EventBus 猜测生命周期;Event 仅用于提示“状态已变化”; - 请求级响应组装必须确定性执行,不为了润色结果隐式调用 LLM。
V1 明确定义请求状态机:
RECEIVED
-> RESOLVING
-> READY
-> RUNNING ----------------------------┐
-> QUEUED -> RUNNING │
-> WAITING_INPUT -> RESOLVING │
▼
COMMITTING
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
COMPLETED FAILED CANCELLED
│
TIMED_OUT / REJECTED /
INTERRUPTED
RequestState 至少记录:
request_id
state
task_ids[]
created_at
updated_at
terminal_at # optional
error # optional
异步请求第一次返回 accepted 后,RequestState 仍继续更新,直到终态。Core 必须提供按 request_id 查询当前状态与最终结果的接口。
同步/快速任务:
request
-> pipeline
-> PluginResult
-> validate
-> StateCoordinator commit
-> RequestState = COMPLETED
-> CoreResponse(completed, terminal=true)
-> caller
任何失败、取消或超时同样必须先提交对应终态,再返回终态 CoreResponse。
耗时任务:
request
-> Task accepted into CoreTaskQueue
-> RequestState = QUEUED/RUNNING
-> CoreResponse(accepted, terminal=false)
-> caller
background lifecycle
-> queue/start/run
-> PluginResult or error/cancel/timeout
-> validate
-> StateCoordinator commit if applicable
-> TaskState terminal
-> RequestState terminal
-> persist terminal outcome
-> emit request/task notification Event
因此异步任务存在两个不同概念:
ACK = Core 已接管任务
Terminal = 任务真正完成/失败/取消/超时/中断
两者不得混淆。
Transport/UI 断开默认不取消已经 accepted 的异步任务。任务继续由 Core 持有;客户端重连后通过 request_id 查询 RequestState/最终结果。只有显式 cancel 请求才触发取消语义。
终态结果必须先写入权威 RequestState/持久记录,再发送通知 Event;即使 Event 丢失或 UI 当时离线,最终结果仍可查询。
TaskChain 中每个 Task 都有独立 task_id,但共享同一个 request_id。只有链中所有必需 Task 完成后 RequestState 才能进入 COMPLETED。
Task 1 terminal success
-> apply ResultBinding
-> materialize Task 2
-> execute Task 2
-> ...
-> final state commit
-> RequestState COMPLETED
任意必需 Task 出现失败、超时、取消或 Binding 失败,链立即停止,并以同一个 request_id 形成明确终态。
IntentRouter 是唯一负责理解“当前用户指令语义”的层。
domain + action + target + entities + constraints + references
示例:
{
"capability_id": "recommendation.recommend",
"domain": "recommendation",
"action": "recommend",
"target": null,
"entities": {},
"constraints": {
"scene": "night_coding",
"energy": "low"
}
}Tier 0 Direct Event
hotkey / exact UI event -> capability
Tier 1 Deterministic Language
aliases / phrase patterns / simple slot extraction
Tier 2 Semantic Resolution
只有确实需要语义理解时才调用 LLM
LLM 只能从允许的 Capability 和字段中选择,不能创造任意 domain/action。
CapabilityRegistry 不是流水线中的顺序阶段,而是旁路只读契约源。
IntentRouter 使用方式:
CapabilityRegistry
│
├─ pre-constraint -> 约束允许的 capability / fields
│
└─ post-validation -> 本地 schema 校验
Post-validation 完全确定性执行,不会进行第二次 LLM 校验。
ContextResolver 不重新解析自然语言,也不重新选择 Capability。
职责只包括:
- 引用绑定;
- 按需上下文补充;
- 一致性/可用性检查。
示例:
current_track -> 具体歌曲
foreground_provider -> 具体播放来源
previous_result -> 仅在明确需要时绑定上一轮结果/推荐
当用户指令本身与上下文强相关且存在歧义时:
current_track与结构化ConversationState是并列第一优先级;- 只有两者不足时,才有限读取
previous_*; - 禁止无限历史搜索;
- V1 ContextResolver 不调用语义 LLM;
- 仍无法解析时返回澄清,而不是继续猜测。
运行时状态可以包括:
- PlaybackState;
- InteractionFocusState;
- TaskState;
- ConversationState;
- EnvironmentState。
持久上下文可以包括:
- Taste Profile;
- 播放历史;
- 长期记忆;
- 推荐历史;
- 分析缓存。
只读取当前 Capability 声明为相关的上下文。
TaskRouter 不再理解语言。它把已解析的请求转换成精准业务执行契约。
核心原则:
TaskDescriptor 必须足够完整,使下游执行层不需要再次解释用户意图。
最小 Envelope:
TaskDescriptor {
task_id,
request_id,
capability_id,
input,
execution_requirements
}
input 由插件定义,并按照 Capability 的 input schema 进行验证。
execution_requirements 只声明下游必须检查的执行条件,例如 Provider/资源是否可用,以及 Capability/插件给出的时间与并发约束提示。真正的绝对 deadline、排队时间和运行时间由 CoreTaskQueue/TaskState 计算与持有。
当原始用户请求明确包含多个操作时,TaskRouter 可以生成一个小型有序 TaskChain。
示例:
"推荐三首然后播放"
Task 1: recommendation.recommend
Task 2: playback.play
RecommendationPlugin 自己不会决定播放任何内容。
多插件协作时,TaskRouter 必须在执行前把依赖关系描述清楚,Core 不允许在执行过程中临时猜测“上一个结果应该塞到哪里”。
V1 使用轻量 ResultBinding:
ResultBinding {
from_task_id,
source_path,
to_task_id,
target_path
}
例如“推荐三首然后播放第一首”:
Task 1: recommendation.recommend
Task 2: playback.play
Binding: task1.data.tracks[0] -> task2.input.track
RequestCoordinator 在 Task 1 获得 ValidatedPluginResult 后应用 Binding,物化 Task 2 的最终 TaskDescriptor,再交给 ExecutionPolicy。Binding 失败时链路停止并返回结构化失败,不允许插件之间直接传递对象。
ExecutionPolicy 刻意保持简单和确定性。
它不得:
- 调用 LLM;
- 重新解释用户语言;
- 修改 Capability;
- 重新设计 Task;
- 搜索无关 Context;
- 创建自治工作流。
它只检查 TaskDescriptor 声明的资源/Provider 条件当前是否满足。
TaskDescriptor
↓
check declared requirements
↓
EXECUTABLE / NOT_EXECUTABLE
如果到这一层仍需要语义猜测,说明上游解析失败,应返回澄清。
缓存查询、算法选择、Analysis/Recommendation 内部调用 LLM、SQLite 查询等业务内部决策全部属于对应插件。
V1 使用轻量、受控的异步任务运行时,而不是 Workflow Engine。CoreTaskQueue 与 TaskSupervisor 一起负责所有后台任务生命周期。
Python Core 默认只有一个主 asyncio 事件循环负责:
- RequestCoordinator;
- TaskQueue 调度;
- Provider 异步 I/O;
- Process Plugin stdin/stdout;
- EventBus 通知;
- Task/Request 状态迁移。
禁止业务模块随意创建裸 threading.Thread、私有事件循环或无限制 Executor。
所有后台 coroutine 必须由 TaskSupervisor 创建并登记。除 TaskSupervisor/Runtime 基础设施外,业务代码禁止直接 fire-and-forget asyncio.create_task(...)。TaskSupervisor 持有 task handle、request/task identity、deadline 与取消入口,确保每个后台任务都可追踪、可等待、可回收。Timer/SystemTrigger 也复用同一个事件循环调度,不为每个定时器创建线程。
运行规则:
async-native I/O
-> 主 asyncio event loop
短时、不可异步化的阻塞库调用
-> Core 统一的 bounded blocking executor
长时间 CPU-bound / 不可安全取消 / 重型模型任务
-> process plugin
V1 不允许每个插件私自维护线程池。共享 blocking executor 必须有固定 max_workers,其具体值属于配置,但不能无限增长。
关键限制:Python 线程不能被安全强杀。因此进入 blocking executor 的操作必须是短时且有界的。长时间工作如果需要可靠超时/取消,必须采用 process runtime。
CoreTaskQueue 必须有界:
max_queue_size
max_running_tasks
V1 只需要两个优先级:
interactive # 用户主动请求
background # timer / scan / refill / maintenance
交互任务优先,但不能无限饿死后台任务。队列已满时必须明确返回 rejected / QUEUE_FULL,禁止无限堆积内存。
Core 同时维护:
- 全局并发上限;
- 每 Plugin/Provider 的并发上限;
- dedup key(适用于重复 SystemTrigger/扫描任务)。
所有时间管理使用 monotonic clock 做运行时判断;wall-clock 只用于日志和持久化时间戳。
每个 Task 至少受以下预算控制:
queue_timeout # 最长等待执行时间
execution_timeout # 真正运行最长时间
Process Runtime 另外拥有:
process_start_timeout
handshake_timeout
cancel_grace_period
process_idle_timeout
shutdown_grace_period
外部请求还可以定义整体 request_deadline。TaskChain 每一步只能消费剩余预算,不能在每个阶段重新获得一整份 timeout。
具体默认秒数属于实现配置,但上述预算类别和语义属于冻结架构。
Provider retry、LLM retry、Process restart 等内部重试必须消费当前 Task 的剩余 execution/request deadline;每次 retry 不得重置完整 timeout。若剩余预算不足以安全开始下一次尝试,直接进入超时/失败终态。
CREATED
-> QUEUED
-> STARTING # process task only when needed
-> RUNNING
-> COMMITTING
-> SUCCEEDED
terminal alternatives:
FAILED
CANCELLED
TIMED_OUT
REJECTED
INTERRUPTED
TaskState 是异步生命周期的权威事实源,EventBus 只广播其变化。
取消必须按执行形态明确处理:
Queued Task
remove/skip from queue
-> CANCELLED
async coroutine / async Provider
cooperative cancel
-> wait cancellation grace
-> CANCELLED or FAILED
blocking executor task
Python 线程不能安全终止。Core 标记 cancellation requested,并丢弃晚到结果;因此这类工作必须短时有界,不能承载重型任务。
process plugin task
send cancel message
-> wait cancel_grace_period
-> terminate process if still running
-> hard kill only as last resort
-> terminal TaskState
任何晚到的 PluginResult 都必须依据 task generation/state 被丢弃,不能把已取消/超时任务重新提交为成功。
V1 每个 Process Plugin 实例默认只允许 1 个 active invocation。这样 stdin/stdout 协议无需在一个 worker 内处理复杂并发复用。
需要并发时,由 CoreTaskQueue 排队。未来如果证明有必要,可以增加多实例 worker pool,但不改变 TaskDescriptor/PluginResult 协议。
- Plugin 不直接写 Core SQLite;
- SQLite 统一通过 Repository/Persistence Gateway;
- 写操作必须串行化(例如单 writer/单 DB worker 或等价机制);
RuntimeStateStore与StateCoordinator的状态提交在 Core 主事件循环上串行化;- 后台线程/Process 返回的结果必须回到 Core loop 后才能修改全局状态。
关闭顺序固定:
stop accepting new work
-> reject/cancel queued work
-> request cooperative cancellation for running work
-> wait shutdown_grace_period
-> shutdown process plugins
-> terminate/kill remaining child processes
-> flush state/persistence
-> exit
V1 不自动恢复应用退出前仍在运行的任务。若持久化记录中存在非终态 Request/Task,下一次启动时统一标记为 INTERRUPTED,由用户或 SystemTrigger 决定是否重新提交。
负责:
- enqueue / bounded queue;
- priority;
- deduplicate;
- concurrency control;
- timeout/deadline;
- cancel;
- task status;
- completion/failure propagation;
- process runtime lifecycle coordination。
明确排除:
NO generic DAG
NO autonomous replanning
NO LangGraph-style orchestration
NO plugin-to-plugin dispatch
NO unmanaged threads
业务功能采用 Plugin-first。
V1 冻结六类插件:
BasePlugin
├── PlaybackPlugin
├── PlaylistPlugin
├── LibraryPlugin
├── RecommendationPlugin
├── AnalysisPlugin
└── ConversationPlugin
Category 层级主要用于开发规范和 Plugin SDK。Python 实现时可使用抽象基类、Protocol 或等价机制;具体语法属于实现设计。
Category = 这个插件属于什么类型
Capability = Core 可以调度的具体操作
示例:
plugin:
id: builtin.recommendation
category: recommendation
capabilities:
- id: recommendation.recommendCore 依据 Capability 调度,而不是依据 Category 写分支。
插件之间禁止直接调用。
非法:
RecommendationPlugin -> PlaybackPlugin
合法:
RecommendationPlugin
-> PluginResult
-> Core
-> next TaskDescriptor
-> PlaybackPlugin
这样可以避免隐式副作用,并保证插件能够独立开发和测试。
UI 属于应用层。桌面窗口、动画、视觉状态和 UI 交互是应用代码能力,不属于业务插件。
Core 只冻结三个跨边界 Envelope:
PluginManifest
TaskDescriptor
PluginResult
必填字段:
api_version: music-agent/v1
plugin:
id: builtin.playback
version: 1.0.0
category: playback
runtime:
type: builtin
capabilities:
- id: playback.pause冻结的最小必填字段:
api_version;plugin.id;plugin.version;plugin.category;runtime.type;- 至少一个
capabilities[].id。
可选、由 Capability/Plugin 自己拥有的字段:
input_schema;output_schema;intentpatterns;contextrequirements;config;config_schema;metadata;extensions。
适用时,缺省 input/output schema 等价于空/默认契约。
Core 协议字段严格校验;插件自定义配置和扩展字段按插件自身 Schema 或插件逻辑校验。
概念上所有插件统一暴露:
initialize
invoke(TaskDescriptor) -> PluginResult
shutdown
具体 Python 类/方法语法属于实现设计。
PluginResult {
task_id,
status,
data,
error,
observations[]
}
data 是 Capability 定义的业务输出。
observations 用于报告可能影响 Core 全局状态的事实。
Core 接受插件输出前,会按照 Capability output schema 验证结果。
V1 只冻结两种 runtime:
builtin
process
Builtin 插件运行在主应用进程内,并通过统一插件框架注册。
轻量第一方能力优先采用 builtin,以避免 IPC 与子进程开销。
每个 process 插件必须声明自己的完整启动命令。Core 不根据插件实现语言拼装命令。
推荐 Manifest:
runtime:
type: process
command:
- python
- main.py
- --stdio
cwd: .command 必须使用 argv 数组,而不是 shell 字符串。
Core 通过 asyncio 子进程能力(或等价异步机制)管理 Process Plugin,不为每个子进程创建独立读取线程。
Core 负责:
read command
-> spawn
-> startup timeout
-> handshake timeout
-> stdin/stdout JSON
-> invoke
-> execution timeout / cancel
-> idle cleanup
-> shutdown
传输固定为:
stdin/stdout JSON Lines
V1 消息类型:
hello
invoke
result
error
cancel
shutdown
progress # optional
重要约束:
stdout只允许协议 JSON;插件日志必须写入stderr,避免破坏消息帧;- 每个消息都必须包含可以关联调用的
request_id/task_id或 invocation id; - 默认每个 Process Plugin 实例同一时间只有一个 active invocation;
- Handshake 至少校验 plugin ID、plugin version、API version;
- 进程启动、握手、调用、取消和关闭分别受对应时间预算控制;
- Process crash 后当前任务进入失败/中断终态;下一次调用可由 PluginManager 按策略重新懒启动;
- 已超时/取消任务的晚到结果必须丢弃。
Process 插件可以懒启动,并在 process_idle_timeout 后退出,非常适合音频/模型密集型 worker。
PluginManager 是基础设施,不负责业务编排。
职责:
configuration CRUD
manifest loading
manifest validation
runtime loading/unloading
plugin lookup
registry refresh
它不负责 Intent 解析、Context 推理、业务规划或插件协作。
Core 使用独立插件管理配置,例如:
version: 1
plugins:
- id: builtin.playback
enabled: true
manifest: ./plugins/playback/manifest.yaml
- id: custom.analysis
enabled: false
manifest: ./plugins/custom-analysis/manifest.yaml职责分离:
Plugin Manifest
= 插件声明自己是谁、会做什么
Plugin management config
= Core 记录安装/引用/启用状态
安装、启用、禁用、更新、删除操作只修改该配置并刷新运行时快照。请求阶段使用内存索引,不重复读取 YAML。
CapabilityRegistry 是紧凑的只读运行时索引。
它回答:
当前系统具备哪些 Capability,它们的契约是什么?
它由已启用插件的 Manifest 派生。
不同模块读取窄视图:
IntentRouter -> routing/schema view
ContextResolver -> context requirement view
TaskRouter -> plugin/handler ownership view
ExecutionPolicy -> execution requirement view
CapabilityRegistry 不执行业务逻辑。
ProviderRegistry 表示已经配置的公共/外部资源 Provider 及其可用性。
V1 最重要的公共 Provider 是 LLM Provider。需要 LLM 的业务插件统一使用 Core 提供的公共接口,不自行维护独立 endpoint/API Key。
V1 支持两种配置方式。
llm:
mode: api
provider: openai
model: model-name
api_key_ref: openai-main适用于使用用户自行购买 API 凭证的商业模型服务。
llm:
mode: endpoint
endpoint: http://127.0.0.1:8000/v1
model: qwen-model-name
api_key_ref: optional-token适用于本地或远端部署的模型服务,例如 OpenAI-compatible endpoint。
公共 Provider 层负责:
- endpoint/SDK transport;
- credentials;
- model name;
- timeout/retry policy;
- provider availability;
- request/response normalization;
- 可选 usage/logging statistics。
Provider 的 timeout/retry 必须接受 Core 当前 Task 的剩余时间预算约束;Provider 不能通过内部重试绕过 Task deadline。
插件拿到的是 Provider Interface,而不是裸 API Key。
ProviderRegistry 也可以管理其他确有必要的公共/外部连接,但 V1 不把它设计成万能 Service Container。
本地播放与外部音乐客户端是平等来源,不冻结永久优先级。
解析规则:
- 用户明确指定来源时,以用户指令为准;
- 未指定时,使用高置信度上下文,例如当前/最近活跃播放器;
- 仍存在冲突时询问用户。
用户明确指定的 Provider 不允许静默 fallback 到其他 Provider。
V1 冻结采用 同步状态提交 + 轻量通知事件。
EventBus 不是事实源。
PluginResult
↓
ResultValidator
↓
StateCoordinator
├── 同步更新 RuntimeStateStore
├── 同步持久化必要 SQLite 记录
└── 状态更新被接受后
↓
emit notification Event
↓
UI / Core observers
Observation
= 插件报告的事实
Event
= Core 完成状态应用后发布的系统确认通知
例如:
PlaybackPlugin
-> observation: playback_started
-> StateCoordinator 更新 current_track/status/history
-> event: playback_state_changed
插件不能直接修改应用全局状态或 UI 状态。
分类保持刻意精简:
- playback started/paused/stopped/track changed;
- playlist created/updated;
- library changed/scanned;
- recommendation generated;
- analysis completed;
- conversation state changed;
- plugin/task status changed。
同样保持粗粒度:
playback_state_changed
playlist_changed
library_changed
recommendation_ready
analysis_ready
conversation_changed
task_status_changed
plugin_status_changed
Event 只用于通知/刷新;发布 Event 时 Core 状态必须已经有效。
同步任务严格顺序:
ValidatedPluginResult
-> StateCoordinator.apply()
-> CommittedResult
-> TaskState SUCCEEDED
-> RequestCoordinator completes request/task-chain
-> RequestState COMPLETED
-> CoreResponse(completed)
-> EventBus notification
异步任务严格顺序:
CoreTaskQueue accepts
-> TaskState QUEUED/RUNNING
-> CoreResponse(accepted, terminal=false)
-> ...background work...
-> ValidatedPluginResult
-> StateCoordinator.apply()
-> terminal TaskState
-> terminal RequestState
-> persist terminal outcome
-> emit task/request notification Event
StateCoordinator 返回结构化 CommittedResult,不能只发 Event。accepted 不是终态;UI 必须能通过 request_id 查询最终 RequestState/结果。
超时/取消时,如果业务状态尚未提交,则禁止应用晚到结果;如果不可逆外部副作用已经发生,则插件必须通过 Observation 报告事实,Core 按实际事实修正 RuntimeState,而不能伪装为“什么都没发生”。
V1 使用 SQLite 作为嵌入式本地持久化。
候选结构化数据包括:
- music library;
- play history;
- playlists;
- recommendation history;
- analysis cache;
- taste profile;
- 经过筛选的 long-term memory;
- agent/application state;
- 异步
RequestState / TaskState的最小生命周期记录与终态结果。
异步生命周期记录不保存 coroutine/thread/process handle,只保存可持久化状态、identity、时间戳、错误和终态结果。应用重启时发现 QUEUED/STARTING/RUNNING/COMMITTING 等非终态记录,统一转换为 INTERRUPTED。
大文件、封面、临时缓存等继续放文件系统。
个性化保持三个概念分离:
Taste Profile -> 用户倾向喜欢什么
Long-term Memory -> 哪些信息值得长期记住
Relationship Model -> 助手应该如何与用户互动
不会把完整聊天记录自动塞进 Prompt,也不会把完整聊天记录直接等同于长期记忆。
音乐分析是业务插件职责,不是 Core 永久子系统。
AnalysisPlugin 内部可以自由组合:
- 公共 LLM Provider;
- 多模态/音频理解模型 endpoint;
- 轻量 DSP/音频元数据代码;
- 本地分析库;
- 插件私有缓存/算法。
Core 只看到类似以下 Capability:
analysis.basic
analysis.explain
analysis.compare
以及对应结构化结果。
此前的 Analysis Hub 概念仍可以存在于某一个 AnalysisPlugin 内部实现中,但它不再是 Core 架构依赖。
RecommendationPlugin 只返回推荐结果。
它不得静默触发播放。
recommendation.recommend
-> RecommendationResult
如果用户明确要求“推荐后播放”,Core 创建有序 TaskChain:
recommendation.recommend
↓ Core 绑定结果
playback.play
所有跨插件协作都遵守同一规则。
ConversationPlugin 处理无法明确映射为确定性领域命令的音乐相关对话。
它可以使用公共 LLM Provider,以及 Core 选择性提供的 Context、Taste、Memory 或分析结果,但不能成为绕过 Capability Routing 的万能后门,也不能直接操作其他插件。
- 业务层 Plugin-first。
- 实现技术栈为 Python + TypeScript。
- 插件契约保持最小且稳定。
- Core 依据 Capability 调度。
- Plugin Category 用于开发结构,不用于 Core 分支调度。
- 插件之间禁止直接调用。
- 跨插件协作必须由 Core 显式编排。
- 推荐不等于播放。
- IntentRouter 独占当前语言理解职责。
- ContextResolver 只做绑定、补充和校验。
- TaskDescriptor 是完整业务执行契约。
- ExecutionPolicy 确定性执行,永不调用 LLM。
- LLM 调用统一通过 Core 公共 Provider。
- Process 插件自行声明完整启动命令。
- Process IPC 使用 stdin/stdout JSON。
- Plugin Manifest 归插件所有;插件管理状态归 Core 配置所有。
- Registry 是轻量运行时快照,不在每次请求时重复解析 YAML。
- 插件输入输出双向契约都必须验证。
- 插件报告 Observation;Core 独占全局状态修改。
- 状态同步提交;Event 只负责轻量通知。
- UI 是应用基础设施,不是插件。
- V1 不使用通用 Workflow/DAG Engine。
- 重型分析/模型 worker 懒加载,并可在空闲后退出。
- 优先 Direct/Local 的确定性执行,再考虑语义 LLM。
- Core 与第一方 Python 代码最低支持 Python 3.10;验证基线至少覆盖 3.10/3.11/3.12。
- 每一个外部请求必须通过
request_id进入可查询生命周期;accepted仅为 ACK,异步任务最终必须进入 completed/failed/cancelled/timed_out/rejected/interrupted 之一。 - TaskChain 的跨任务数据依赖必须由显式 ResultBinding 描述,禁止运行时猜测。
- Core 使用单一主异步事件循环;线程只允许通过受控 bounded executor 使用,重型/不可安全取消工作必须使用 process runtime。
- 所有后台任务必须受有界队列、并发上限、queue/execution timeout 和明确取消语义约束。
- 应用关闭时必须停止接收新任务、回收队列与子进程,并把遗留非终态任务标记为 INTERRUPTED。
- 所有后台 coroutine 必须由 TaskSupervisor 持有,禁止业务代码 fire-and-forget;UI/Transport 断开不隐式取消 accepted 任务。
- Provider/LLM retry 必须消费剩余 deadline,禁止通过重试重置 timeout。
"暂停"
-> FastMatcher
-> playback.pause
-> ContextResolver 绑定当前活跃 provider
-> TaskDescriptor
-> ExecutionPolicy
-> PlaybackPlugin
-> PluginResult(playback_paused)
-> StateCoordinator commit
-> RequestCoordinator
-> CoreResponse(completed)
-> UI
-> playback_state_changed notification
不需要 LLM。
"想听点凌晨城市快醒了但自己还没睡的感觉"
-> IntentRouter Tier 2
-> recommendation.recommend
-> ContextResolver 按需增加 taste/history
-> TaskDescriptor
-> RecommendationPlugin
-> 插件需要时调用 shared LLMProvider
-> RecommendationResult
-> StateCoordinator 持久化 recommendation history
-> RequestCoordinator
-> CoreResponse(completed, recommendations)
-> UI
-> recommendation_ready notification
不会隐式触发播放。
"推荐三首适合写代码的,然后用 Spotify 播放"
-> IntentRouter
-> Core TaskChain
1. recommendation.recommend
2. playback.play
-> RecommendationPlugin
-> validated result
-> Core 将选中的 track 绑定进 Task 2
-> PlaybackPlugin
-> StateCoordinator commit
-> RequestCoordinator completes TaskChain
-> CoreResponse(completed)
-> UI
analysis request
-> TaskDescriptor
-> ExecutionPolicy
-> CoreTaskQueue enqueue
-> TaskState QUEUED
-> CoreResponse(accepted, terminal=false)
-> scheduler acquires global + plugin concurrency permit
-> TaskState STARTING/RUNNING
-> PluginManager lazily starts process if needed
-> process_start_timeout + handshake_timeout
-> stdin JSON invoke
-> stdout JSON progress/result
-> execution_timeout / cancellation supervision
-> output validation
-> StateCoordinator commit
-> TaskState SUCCEEDED (or FAILED/CANCELLED/TIMED_OUT)
-> RequestState terminal
-> persist terminal outcome
-> completion Event(request_id, task_id)
-> UI receives notification and/or queries RequestState
任何 timeout/cancel 后到达的旧 result 都不能覆盖终态。Process 日志写 stderr,stdout 只承载 JSON Lines 协议。
以下内容应在实现/Benchmark 时决定,而不是重新打开大架构讨论:
- Python 具体框架和 package 目录(最低版本已冻结为 >=3.10,首批验证矩阵至少 3.10/3.11/3.12);
- TypeScript 具体桌面/UI 框架;
- Python/TypeScript 在应用 UI/Core 之间的具体边界和 IPC;
- SQLite 表、字段、索引和 migration 工具;
- 具体 ASR/TTS Provider;
- 具体 LLM 厂商、模型和 retry 参数;
- 推荐排序权重;
- Taste/Memory 保留与更新公式;
- Context TTL;
- AnalysisPlugin 内部使用的具体分析库/模型;
- 不同外部音乐客户端的具体控制方式;
- UI 视觉语言、尺寸与动画系统;
- queue/execution/process-start/handshake/cancel-grace/idle/shutdown 等 timeout 的具体默认数值;其类别与语义已经冻结。
这些都属于冻结契约之下的实现选择。
只要实现遵守下列稳定边界,就认为架构已经足够冻结,可以进入开发:
InputGateway
RequestCoordinator
IntentRouter
ContextResolver
TaskRouter
ExecutionPolicy
CoreTaskQueue
TaskSupervisor
PluginManager
CapabilityRegistry
ProviderRegistry
StateCoordinator
RuntimeStateStore
SQLite repositories
Lightweight EventBus
CoreResponse / Reply Channel
ResultBinding
BasePlugin
├── PlaybackPlugin
├── PlaylistPlugin
├── LibraryPlugin
├── RecommendationPlugin
├── AnalysisPlugin
└── ConversationPlugin
下一阶段产物应是 Implementation Plan,而不是继续进行宽泛的架构重构。后续架构修改应由实际实现证据、Benchmark 或原型失败来驱动。
进入业务功能扩展前,dev-harness 必须至少覆盖以下真实纵向链路:
- 同步快路径:
暂停-> Intent -> Context -> TaskDescriptor -> ExecutionPolicy -> PlaybackPlugin(fake/builtin) -> PluginResult -> StateCoordinator ->CoreResponse(completed),并断言状态提交早于响应完成; - 异步 Process 正常路径: request ->
CoreResponse(accepted, terminal=false)-> bounded queue -> process start/handshake -> invoke/result -> state commit -> terminal TaskState/RequestState -> completion Event,且request_id/task_id全程一致; - 队列超时: Task 长时间未获得执行资格 ->
TIMED_OUT,不会再被 worker 执行; - 运行超时: Process/Provider 超过 execution deadline -> cancel -> grace -> terminate/kill(如需要)->
TIMED_OUT,晚到结果不得提交; - 用户取消: queued 与 running 两类任务都能进入
CANCELLED,且不会产生错误的成功状态; - 背压: bounded queue 满时返回
rejected/QUEUE_FULL,内存队列长度不得继续增长; - 应用关闭: shutdown 停止接收新任务,回收队列和子进程;遗留非终态任务在下次启动被标记为
INTERRUPTED; - 并发所有权: Core/Plugin 不得创建未登记裸线程;blocking executor 的 worker 数受配置上限控制;业务代码不得绕过 TaskSupervisor fire-and-forget;
- 断线恢复: accepted 任务在 UI/Transport 断开后继续运行,重连可通过
request_id查询同一终态; - Retry deadline: Provider/LLM retry 不得重置 execution/request deadline;
- TaskChain: 上一步结果只通过显式 ResultBinding 进入下一 Task;任一步超时/取消/失败都正确终止整个 request lifecycle。
兼容性门禁至少在 Python 3.10、3.11、3.12 三个解释器版本运行核心 Contract、Plugin Runtime、Async Runtime 与 E2E 测试。
这些测试用于证明的不只是“组件能运行”,而是:
请求从进入 Core、经过时间/并发控制、插件执行、状态提交,到同步响应或异步终态,形成可查询、可取消、可回收、不会因晚到结果破坏状态的一致闭环。