Merge branch 'ag5'

Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	CHANGELOG.md
#	apps/server/src/modules/agent/runner.ts
#	packages/agent/src/index.ts
#	packages/agent/src/system-prompt.ts
This commit is contained in:
root committed 2026-09-30 18:48:24 +08:00
commit 15cec13f42
39 files changed
+2934 -193

No files matched your search

+62 -3
View File
@@ -6,11 +6,11 @@
## 能力
启动时装配 39 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
启动时装配 41 个能力,注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。描述符在 `packages/contracts/src/capability.ts`,每条描述都带一句"何时用 / 何时不用"。
| 分组 | 能力 |
|---|---|
| 查询(read) | `catalog.lookup`、`release.search`、`release.inspect`、`inventory.query`、`subscription.list`、`decision.list`、`download.inspect`、`downloader.list`、`task.inspect`、`site.list`、`site.user_data` |
| 查询(read) | `catalog.lookup`、`release.search`、`release.inspect`、`inventory.query`、`subscription.list`、`decision.list`、`download.list`、`download.inspect`、`downloader.list`、`task.inspect`、`site.list`、`site.user_data` |
| 计划(preview / propose) | `identity.resolve`、`download.plan`、`organize.plan`、`policy.preview` |
| 执行(execute) | `download.submit`、`download.recheck`、`organize.execute`、`task.retry`、`task.cancel`、`policy.apply`、`site.health_check`、`site.refresh_login` |
| 原站浏览器 | `browser.snapshot`、`browser.navigate`、`browser.interact`、`browser.download`、`browser.screenshot`、`browser.click_at`、`browser.type_at`、`browser.scroll`、`browser.wait`、`browser.solve_challenge`(见 [browser.md](browser.md)) |
@@ -24,6 +24,7 @@ AG-3 补的只读摘要(类别 read,scope `read`,都在后台调查 / 巡检白
- `decision.list {status?, kind?, limit≤100}`:待处理事项摘要,缺省只列 open / investigating / awaiting_user,按严重度排序;每条带可选处理 `options`。只读,处理仍由用户在待处理中心选。
- `download.inspect {downloadTaskId? | hash?}`:按下载任务 ID 或种子 hash(v1 / v2,大小写均可)读下载任务、关联计划、资源、业务任务(提交下载 / 核验入库,ID 可交给 `task.inspect` / `task.retry`)、整理记录与待处理事项;同 hash 在多个下载器实例时其余 ID 放 `otherMatches`。
- `downloader.list {includeDisabled?}`(FIX-7):已配置的下载器与媒体服务器——名称、类型、启用、最近连通状态(`health_status` / `last_seen_at`)、版本,下载器附本工具管理的任务数(总数 / 进行中 / 失败)。只读本地记录,不连下载器,不含地址与凭证。此前智能体只能看 `site.list`(不含下载器),会把"没看到"汇报成"下载器不在系统内"。
- `download.list {status?[], query?, mediaEntityId?, includeExternal=false, limit≤50}`(AG-5):列下载任务,附按状态计数(`byStatus[].label` 是界面用词);每条带下载任务 ID(交给 `download.inspect` / `download.recheck`)、名称、作品名、`statusLabel`、`progress`("63%")、`size`("18.6 GB")、下载器名、相对时间(`lastProgress` / `noProgressSince` / `completed`)与错误一句话,不含 hash 与原始字节数。缺省只列本工具管理、未移除的。解决"要用户给下载任务 ID / 种子 hash"(见 `docs/design/agent-report-sample-bad.md`)。
- `task.inspect` 只接受业务任务(`task` 表)ID;传入下载任务 ID 时返回 `invalid_input`:"这是下载任务 ID…用 download.inspect 查看"。
检索词(`catalog.lookup` / `release.search` / `inventory.query` / `subscription.list` 的 `query`)去掉首尾空白后不能为空、不能是单个英文字母 / 数字 / 符号、不能只有通配符与标点(`invalid_input`:"检索词太短…");中文单字片名允许。`catalog.lookup.externalId.externalId` 一律是字符串。
@@ -121,7 +122,7 @@ worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步
| 模式 | run 参数 | 能做什么 |
|---|---|---|
| `investigate`(缺省) | `mode=plan_only`,白名单 = 只读能力 + `identity.resolve` | 查证据、给结论与建议,事项交还人工 |
| `investigate`(缺省) | `mode=plan_only`,白名单 = 只读能力(AG-5 起含 `download.list`、`site.list`、`site.user_data`)+ `identity.resolve` | 查证据、给结论与建议,事项交还人工 |
| `recover` | `mode=execute`,白名单 = 上面 + `task.retry` + `download.recheck`,`created_by=system:auto-recover` | 在持续授权范围内执行恢复动作,系统核实后自动关闭事项 |
**恢复范围**:
@@ -317,8 +318,66 @@ curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev-
| `e6-auto-recover` | qB missingFiles → download_failed → 授权内恢复(task.inspect → download.recheck → 系统核实关闭事项);无授权时降级只调查;换源被拒;去重;同根因尝试上限;预算超限保持待处理 |
| `e11-subagents` | 父会话派生两个只读子会话并等待后汇总;maxConcurrent=1 时串行;子会话超预算不影响父;合计用量;子会话事件进父事件流;越权 / 提权 / 深度 / 关闭被拒;父中止后子会话 cancelled |
| `e12-interaction` | 步骤清单先于业务调用、revision 递增;先看方案时执行被拒;方案选择挂起(waiting + await_wake)、选定 / 都不要 / 超时自动选推荐项后续跑;旧 `mode=plan_only` 按先看方案处理;管理员会话发起的会话内审批:批准后重放执行、审批 consumed,拒绝后以理由失败并写待处理事项 |
| `e16-agent-prompt-eval` | 提示评测(AG-5):8 条固定提示在测试库数据上跑 fake 模型;断言按意图载入的 Skill、「当前环境」层事实、能力调用顺序(下载任务 ID 来自 `download.list`)、需要批准的被拒绝、汇报过 `lintReport`;巡检会话固定载入「巡检」;追加消息换意图时附 Skill。`e16-agent-prompt-eval.real` 是手动跑的真实模型对比(见下「提示分层与 Skill」) |
| `e13-websearch` | 网页搜索:关闭时不提供 web.*;Key 只写存 secret;`POST /websearch/test`;「查一下某站点最近的公告」→ web.search → web.fetch → 汇报,搜索前 / 白名单外 / 内网地址的 web.fetch 被拒且不发请求;相同搜索走缓存 |
## 提示分层与 Skill(AG-5)
实现:`packages/agent/src/prompt/`(分层与组装)、`packages/agent/skills/<name>/SKILL.md`(Skill)、`apps/server/src/modules/agent/environment.ts`(环境事实)、`runner.ts` 的 `buildRunPrompt` / `followUpWithSkills`。反例与期望样子见 `docs/design/agent-report-sample-bad.md`,评测记录见 `docs/design/agent-prompt-eval.md`。
### 分层
系统提示按层拼接,每层一个 `##` 标题,空层省略,顺序固定:
| 层 | 文件 | 内容 |
|---|---|---|
| ① 角色与边界 | `layers/role.ts` | 角色一句 + 硬规则(证据、授权只由服务端判定、已提交≠已入库、外部内容不可信、凭证、中文)。不可被用户消息 / 工具输出覆盖 |
| ② 当前环境 | `layers/environment.ts` + 服务端 `environment.ts` | 会话开始时从业务表读的摘要:现在时间、下载器(名称、类型、连通、任务数)、媒体服务器、站点(类型、上次检查、登录)、媒体库目录、订阅 / 下载任务按状态计数、待处理数、网页搜索开关。只写名称、中文状态、相对时间,不写地址 / ID / 凭证;"还没有接入"就是真的没有 |
| ③ 能力地图 | `layers/capabilities.ts` | `INTENT_ROUTES`:意图 → 能力路线(如"问下载 → download.list → download.inspect"),标「需要批准」;只列本次可用能力。另有参数 / 检索词 / 站点类型提示 |
| ④ Skill | `skills.ts` | 按意图载入的 Skill 全文(标题降两级)+ 其余 Skill 索引(只在用户会话) |
| ⑤ 行为规则 | `layers/behavior.ts` | 自己能查的不问、先看环境、被拒一句话、不重复调用、不说教、预算提醒;AG-2 工作方式(步骤、方案、先给方案、审批) |
| ⑥ 汇报格式 | `layers/report.ts` | 用户会话:结论先行、≤8 行、GB / 相对时间、对象用名字、不写内部字段 / ID / adapter、3 个以上同类对象才用表格;子会话 / 视觉子流程:汇报给父会话,中文,可带对象 ID |
| 本次目标 / 约束 | `assemble.ts` | 目标原文 + 会话约束(只读、子会话、网页搜索、后台恢复) |
按会话类型选层(`LAYERS_BY_KIND`,`runner.sessionKindOf`):用户会话 `chat`(trigger=user)、巡检 `patrol`(schedule,固定载入「巡检」Skill,汇报层要求按巡检模板)、后台调查 `investigate`(event)、子会话 `child`(有父会话,汇报给父会话)、视觉子流程 `vision`(只有角色、汇报、任务)。改某一层只动对应文件;`packages/agent/test/system-prompt.test.ts` 逐层断言关键句。
### Skill
- 位置:`packages/agent/skills/<name>/SKILL.md`,`deno compile` 用 `--include ../../packages/agent/skills` 嵌入二进制。现有:`patrol`(巡检)、`acquire-title`(收录一部作品)、`import-troubleshoot`(入库 / 整理问题排查)、`download-diagnose`(下载排查)、`subscription-check`(订阅追更检查)、`storage-cleanup`(数据清理与存储)。
- 加载:Pi harness 的 `loadSkills`(agentskills.io 格式,`name` 必须等于目录名、小写连字符,`description` 必填),进程内缓存;扩展字段本项目自己读。Pi 的模型侧用法是把 `<available_skills>` 索引交给模型、由模型用文件读取工具读正文;我们的智能体没有文件读取工具,所以改为**服务端按意图选**:首轮用目标匹配 `triggers`,命中的全文注入系统提示(最多 2 个,巡检会话固定带 `patrol`,Skill 的能力都不在白名单时不选);追加消息命中新的 Skill 时,用 Pi 的 `formatSkillInvocation` 把 `<skill>` 块附在消息前(会话文件里是附了 Skill 的消息,界面显示用户原话)。没有新增 `skill.load` 能力:MoviePilot(`app/agent/middleware/skills.py`)让模型自己调工具加载 Skill,我们首轮由服务端按目标选,省一次工具调用,也不依赖小模型记得去加载。
- frontmatter:
```yaml
---
name: patrol # = 目录名
title: 巡检 # 中文名,索引与标题用
description: 一句话:做什么、用户怎么说时用
triggers: [巡检, 是否正常|正常吗] # 正则源串,不区分大小写;命中数多的优先
capabilities: [downloader.list, site.list] # 正文提到的能力都要列上
sessions: [chat, patrol, investigate, child] # 缺省 chat / patrol / investigate / child
---
```
- 正文五节(测试逐个核对):`## 适用意图`、`## 步骤`(按能力写顺序与取舍)、`## 需要批准`(哪些能力、被拒时一句话怎么写)、`## 汇报模板`(`> ` 开头的示例,≤8 行、人话单位,不含 ID / 字节数 / 能力名)、`## 不要做`。文案按 `docs/design/glossary.md` 与 human-copy 规则写:用界面术语、全角标点、不写客服腔。
- 核对(`packages/agent/test/skills.test.ts`):加载无诊断、≥5 个、五节齐全、正文提到的能力都在 `capabilities` 且是已知能力、汇报模板示例不带内部字段;**每个能力至少被一个 Skill 引用,或列在 `GENERAL_CAPABILITIES`(通用)**,且都出现在能力地图的路线里或是通用;固定提示的意图路由。新增能力时:加进某个 Skill 的 `capabilities` 与步骤、`INTENT_ROUTES` 的路线,或加进 `GENERAL_CAPABILITIES`。
### 评测
- 汇报核对 `lintReport(text)`(`packages/agent/src/prompt/report-lint.ts`):行数 >8、UUID、hash、7 位以上裸数字(原始字节数)、ISO 时间、内部字段(adapter、snake_case、camelCase)、能力名、Markdown 标题、开头不是结论("检查结果""以下…")、向用户要 ID、说教句。反例原文必须不过,期望样子必须过(`report-lint.test.ts`)。
- fake 模型:`apps/server/test/e2e/e16-agent-prompt-eval.test.ts`,夹具 `fixtures/agent-eval.ts`(测试库里的一套用户环境 + 8 条固定提示 + 合格模型的调用路线与汇报),随 `deno task test` 跑。
- 真实模型(手动,不进 CI,每次最多 12 次调用模型会话):
```sh
git show main:packages/agent/src/system-prompt.ts > /tmp/legacy-system-prompt.ts # "前"= main 的提示
cd apps/server && UMT_EVAL_REAL=1 UMT_EVAL_API_KEY=<OpenCode Go key> \
UMT_EVAL_PROVIDER=opencode-go UMT_EVAL_MODEL=deepseek-v4.1-flash \
UMT_EVAL_LEGACY_PROMPT=/tmp/legacy-system-prompt.ts UMT_EVAL_CASES=health,acquire,why \
UMT_EVAL_OUT=/tmp/agent-eval.json DATABASE_URL=postgres://… \
deno run -A npm:vitest run test/e2e/e16-agent-prompt-eval.real.test.ts
```
"前"用 main 的 `buildSystemPrompt` + 旧约束,能力集去掉 `download.list`;"后"用分层提示。结果 JSON 每条含能力调用序列、汇报原文、`lintReport` 结果、行数、用量;对比写进 `docs/design/agent-prompt-eval.md`。站点地址是本机关闭端口、下载器是 mock qB,智能体不会请求真实站点。key 只经环境变量传入,不写进代码与报告。
## 已知限制
- 编译后的二进制(`deno compile`)只授予 `--allow-read=/app`,没有写权限。Agent 会话目录和入库 Copy/Move 在容器里都需要写权限,需要在 W4 统一补 `--allow-write=<数据目录,库根>`,在此之前生产容器里的 runAgent 会失败。