起点:让 Agent 记住对话里真正需要做的事
我想做一个 Todoist Skill。
它不只是响应“把这件事加入 Todoist”这样的明确命令,还要能在对话过程中识别用户已经作出的承诺:
- “我明天把合同发出去。”
- “下周需要复查这次迁移。”
- “提醒我月底前续费域名。”
如果承诺足够明确,Agent 可以低干扰地创建待办;如果存在关键歧义,再问一个简短的问题。
这个想法看似简单:判断一句话是不是任务,然后调用 Todoist API。真正开始实现后才发现,API 调用反而是最容易的部分。困难集中在三个边界上:
- 谁负责管理凭证?
- 如何让同一个 Skill 在不同 Agent 客户端中运行?
- Agent 可以修改哪些已经创建的任务?
第一版错误:让调用方把 Token 交给 Skill
最初的设计是由 Agent 读取 Token,再通过受保护的通道把它传给 Skill。
这个方案表面上很干净:
- Token 只在内存中出现;
- 不写入命令参数;
- 不打印到终端;
- Skill 不关心 Token 存在哪里。
但它隐藏了一个严重耦合:每个 Agent 客户端都必须提供相同的安全注入能力。
在一个客户端中,这套调用能够工作;换到另一个客户端后,Agent 按照 Skill 的安全规则拒绝继续:
- 不能请求用户在对话里粘贴 Token;
- 不能主动读取凭证;
- 找不到约定的安全注入通道;
- 因此判断当前没有可用的执行器。
后来在明确要求下,第二个客户端也完成了真实任务创建,但这不代表原设计是通用的。它只能证明:在人工介入足够多时,这条路径可以被打通。
这次失败让我重新定义了“公共 Skill”的含义:
公共 Skill 不能只让决策规则跨客户端复用,执行和认证契约也必须跨客户端成立。
重新划分责任:凭证属于执行器
最终采用的结构是:
对话
↓
Agent 识别承诺
↓
Skill 决定是否创建任务,并构造非敏感字段
↓
随 Skill 发布的本地执行器
├── 自行发现并校验用户凭证
└── 调用 Todoist API
各层职责被明确分开:
| 组件 | 负责什么 | 不负责什么 |
|---|---|---|
| Agent | 理解当前对话 | 读取或传递 Token |
| Skill | 判断任务、生成字段、限制操作边界 | 保存凭证、直接拼接 API 请求 |
| 执行器 | 读取凭证、校验参数、调用 Todoist | 判断一句话是否构成承诺 |
| 用户配置 | 一次性保存个人 Token | 每次会话重新输入 Token |
这里有一个容易混淆的细节:并不是让 SKILL.md 自己读取凭证,而是让随 Skill 一起分发的可执行程序负责凭证。
Skill 是决策与行为规范,执行器才是认证边界。
凭证应该由用户配置一次
Todoist 的个人 API Token 属于每个用户自己的账户。公共 Skill 不应该附带统一凭证,也不应该关心使用者具体是谁。
因此,用户只需要在操作系统的私人配置目录中保存一次 Token。之后,Codex、OpenCode 或其他兼容客户端调用同一个执行器时,都不需要再次输入。
执行器在自己的进程内完成以下工作:
- 定位用户配置文件;
- 确认文件属于当前用户;
- 拒绝符号链接;
- 拒绝权限过宽、内容为空、格式异常或体积异常的文件;
- 读取 Token 并完成请求;
- 不把 Token 放入命令参数、环境变量、任务字段或正常输出;
- 不回显可能包含敏感信息的远端错误正文。
Agent 只会执行类似这样的调用:
node <skill-dir>/scripts/todoist-task.mjs \
task add \
--scope "工作" \
--scope "项目A" \
--content "复查同步异常" \
--json
调用方传递的只有任务字段,没有凭证。
需要承认的是:个人 API Token 在服务端仍然可能拥有较广权限。执行器只开放少量命令,可以减少 Agent 的误操作面,但不能把一个广权限 Token 变成真正的服务端细粒度授权。
这是程序边界,不是 OAuth Scope。
“自动捕获”不等于“看到动作就创建任务”
凭证问题解决后,下一步是控制 Agent 的主动性。
最终采用三级置信度:
| 置信度 | 行为 |
|---|---|
| 高 | 自动创建,不中断主要对话 |
| 中 | 询问一个影响任务成立的关键问题 |
| 低 | 静默跳过 |
自动创建必须同时满足几个条件:
- 行动由用户本人负责;
- 事情尚未完成;
- 能写出具体、可执行的任务标题;
- 表达的是承诺、义务、提醒或后续动作;
- 不需要猜测会改变任务含义的重要事实。
以下内容不会被自动捕获:
- 只是设想的可能性;
- 别人的工作;
- 已经完成的事情;
- 引用文本或文档里的行动项;
- 单纯的日历事件;
- Agent 当前正在替用户执行的工作。
尤其是最后一点很重要。
用户说“帮我修复这个测试”时,Agent 应该去修复测试,而不是顺手创建一条“修复测试”的用户待办。否则 Todoist 很快会变成 Agent 工作日志,而不是用户的行动系统。
用可读路径解决多项目归属
公共 Skill 不能假设每个人都有相同的 Todoist 项目结构,也不一定有权限读取项目列表。
但如果所有任务都直接进入 Inbox,多项目协作时很快会失去归属信息。
最后采用了一种可见的标题路径:
私人 / 预约牙医
工作 / 提交工时表
工作 / 项目A / 复查同步异常
待归类 / 续费域名
分隔符固定为 /:斜线左右各一个 ASCII 空格。
这个格式同时兼顾了两件事:
- 对人来说,它比无空格路径更容易阅读;
- 对脚本来说,可以稳定地使用
split(' / ')解析。
路径按“宽范围 → 具体项目 → 行动”排列,最多使用两级归属。它只是标题分类,不会假装已经把任务放进某个 Todoist Project。
这是一种很轻量的命名空间:即使任务仍在 Inbox,也不会失去来源。
修改任务比创建任务更危险
创建接口跑通后,一个自然问题是:既然已经能创建任务,能不能修改标题或完成状态?
技术上当然可以。但如果 Skill 允许 Agent 根据标题搜索历史任务再修改,就会出现新的风险:
- 同名任务可能不止一个;
- 跨会话记忆不可靠;
- 用户粘贴的 ID 或链接无法证明任务来源;
- 搜索结果和 Todoist 内容本身都可能成为不可信输入。
最终的权限边界是:
只允许修改当前会话中由该 Skill 成功创建,并且已经拿到稳定 ID 的任务。
当前会话维护一个临时注册表:
任务 ID + 当前完整标题 + 完成状态
这个注册表有严格限制:
- 只存在于当前对话;
- 不写入磁盘;
- 不跨会话恢复;
- 不接受用户提供的 ID;
- 不从粘贴文本、URL、搜索结果或历史记忆重建;
- 来源无法确认时,拒绝修改。
即使任务在注册表中,修改也必须由用户明确提出。隐式捕获只授权创建新任务,不授权编辑现有数据。
执行器目前只开放两种已有任务操作:
- 替换完整的分类标题;
- 将任务标记为完成。
删除、恢复、移动、搜索、批量修改以及日期、描述、优先级和项目位置调整都不在接口范围内。
这不是因为 Todoist API 做不到,而是因为 Skill 没有足够可靠的上下文去安全地做。
跨客户端验证揭示的真正问题
最终验证分为两层:
- 使用结构校验器和适配器测试确认 Skill 与执行器本身有效;
- 分别在两个 Agent 客户端中创建真实 Todoist 任务,再到网页端确认结果。
第一次跨客户端测试失败,暴露的是认证责任错位;第二次测试成功,才证明两个客户端使用的是同一条自管理执行路径。
这也让我意识到,“兼容多个 Agent”不能只看 SKILL.md 是否能被发现。真正的兼容性至少包括:
- Skill 的触发描述能被理解;
- 执行器不依赖某个客户端专属工具;
- 凭证不需要调用方转运;
- 成功与失败结果有稳定格式;
- 不确定结果不会被自动重试;
- 不同客户端遵守相同的修改边界。
只共享一份提示词,不等于共享一项能力。
名称也是公共接口
Skill 最初使用了一个较长的名字,能准确描述功能,但不利于用户记忆和显式触发。
后来名称收敛为 todoist-capture。
重命名不只是改目录名,还涉及:
- Skill 名称;
- 展示名称;
- 包名称;
- 显式调用名称;
- 凭证目录;
- 文档与示例;
- 旧配置的兼容策略。
新的执行器优先读取新位置,但在新凭证不存在时,可以安全回退到旧位置;如果新位置已经存在但权限或格式有问题,则直接报错,不通过回退掩盖配置错误。
这类细节决定了重命名是平滑迁移,还是一次破坏性升级。
最终得到的五条原则
回看这次迭代,我认为最值得复用的是五条原则:
- 把决策与执行分开。 Skill 判断该不该做,执行器负责可靠地做。
- 让执行器拥有认证责任。 不要要求不同 Agent 客户端分别实现秘密转运协议。
- 公共 Skill 是完整的调用契约。 提示词、执行器、凭证发现、错误语义和安全边界缺一不可。
- 用可见命名空间保留上下文。
范围 / 项目 / 行动同时服务人类阅读和脚本处理。 - 修改权限必须依赖来源证明。 知道任务 ID 不等于有权修改它;会话内的创建结果才是可信来源。
结尾
一开始,我以为这是一个“调用 Todoist API”的小工具。真正做完后,它更像一次 Agent 权限模型的微型实验。
最有价值的设计并不是让 Agent 能创建更多任务,而是明确回答:
- 什么情况下应该创建?
- 谁负责认证?
- 哪些数据可以进入执行层?
- 成功结果如何成为后续操作的依据?
- 一旦离开当前会话,权限应该在哪里终止?
一个好的 Agent Skill,不只是教模型如何完成动作。
它还要告诉模型:什么时候不该行动,以及它的权限究竟到哪里结束。