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:
commit
15cec13f42
39 files changed
+2934
-193
No files matched your search
@@ -1,7 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSystemPrompt, interactionRules, REPORT_FORMAT } from "../src/index.ts";
|
||||
import {
|
||||
buildLayeredPrompt,
|
||||
buildSystemPrompt,
|
||||
type EnvironmentFacts,
|
||||
interactionRules,
|
||||
LAYERS_BY_KIND,
|
||||
promptSections,
|
||||
REPORT_FORMAT,
|
||||
renderEnvironment,
|
||||
} from "../src/index.ts";
|
||||
|
||||
// AGUI-3 prompt 回归:用户在会话里看到的汇报就是模型写的文字,格式约束靠这里守住。
|
||||
// 分层系统提示回归(AG-5,继承 AGUI-3 的汇报约束):用户在会话里看到的汇报就是模型写的文字,格式约束靠这里守住。
|
||||
// 改提示词时先想清楚是否放宽了这些约束,再同步改断言。
|
||||
|
||||
const userPrompt = () =>
|
||||
@@ -9,6 +18,7 @@ const userPrompt = () =>
|
||||
role: "媒体库自动化智能体",
|
||||
goal: "看看有哪些新 4K",
|
||||
interaction: { plan: true, options: true, approvals: true },
|
||||
capabilities: ["downloader.list", "site.list", "site.health_check", "download.list", "download.inspect"],
|
||||
});
|
||||
|
||||
/** 取提示词里的一节(## 标题到下一个 ## 之前) */
|
||||
@@ -20,55 +30,145 @@ function section(p: string, title: string): string {
|
||||
return next < 0 ? rest : rest.slice(0, next);
|
||||
}
|
||||
|
||||
describe("汇报格式(AGUI-3)", () => {
|
||||
it("关键约束句:结论先行、用名字不用函数名 / 字段名 / UUID、不超过 10 行、多个资源才用表格", () => {
|
||||
const f = section(userPrompt(), "汇报格式");
|
||||
expect(f).toMatchInlineSnapshot(`
|
||||
"## 汇报格式
|
||||
- 第一句写结论:做成了什么、没做成什么、为什么。
|
||||
- 对象用名字:作品名、站点名、资源标题、文件名;不写函数名、工具名、字段名、参数、UUID 或 hash。
|
||||
- 数量以工具返回为准:派出几个子会话、提交几个下载,只数成功的调用,失败或被拒绝的不算。
|
||||
- 不超过 10 行;不复述过程,不写下一步打算,除非需要用户做什么。
|
||||
- 只有列举多个资源(3 个以上的资源、任务或文件)时才用表格,其余用短句或列表。
|
||||
- 可以用 Markdown 列表、表格和粗体,不写标题,不写 HTML。
|
||||
"
|
||||
`);
|
||||
});
|
||||
const NOW = "2026-09-30T09:45:00.000Z";
|
||||
const ENV: EnvironmentFacts = {
|
||||
now: NOW,
|
||||
timeZone: "Asia/Shanghai",
|
||||
downloaders: [
|
||||
{
|
||||
name: "家里的 qB",
|
||||
kind: "qbittorrent",
|
||||
enabled: true,
|
||||
health: "healthy",
|
||||
lastSeenAt: "2026-09-30T09:42:00.000Z",
|
||||
tasks: { total: 24, active: 0, failed: 0 },
|
||||
},
|
||||
],
|
||||
mediaServers: [],
|
||||
sites: [
|
||||
{
|
||||
name: "憨憨",
|
||||
kind: "pt",
|
||||
enabled: true,
|
||||
health: "healthy",
|
||||
lastHealthAt: "2026-09-29T08:12:00.000Z",
|
||||
account: "active",
|
||||
},
|
||||
],
|
||||
libraryRoots: [{ name: "电影", enabled: true, health: "healthy" }],
|
||||
subscriptions: { cancelled: 5 },
|
||||
openDecisions: 0,
|
||||
downloads: { imported: 20, downloading: 0 },
|
||||
};
|
||||
|
||||
it("汇报格式与已有硬规则不冲突:证据写名称和数值,不再要求引用对象 ID", () => {
|
||||
const p = userPrompt();
|
||||
expect(p).not.toContain("对象 ID");
|
||||
expect(p).toContain("写出查到的对象名称和关键数值");
|
||||
// 旧的"最后用简短中文汇报…可以用 Markdown(标题…)"已换成汇报格式一节
|
||||
expect(p).not.toContain("最后用简短中文汇报");
|
||||
expect(REPORT_FORMAT.length).toBe(6);
|
||||
});
|
||||
|
||||
it("子会话 / 视觉子流程汇报给父会话:不加用户汇报格式", () => {
|
||||
const p = buildSystemPrompt({ role: "网页挑战处理员", goal: "过验证", audience: "agent" });
|
||||
expect(p).not.toContain("## 汇报格式");
|
||||
});
|
||||
|
||||
it("子会话与父会话一样要求思考、步骤说明和汇报都用中文(AG-4)", () => {
|
||||
const child = buildSystemPrompt({
|
||||
describe("分层组装", () => {
|
||||
it("用户会话按固定顺序装配:角色与边界 → 当前环境 → 能力地图 → 行为规则 → 汇报格式 → 本次目标", () => {
|
||||
const p = buildLayeredPrompt({
|
||||
kind: "chat",
|
||||
role: "媒体库自动化智能体",
|
||||
goal: "Check the torrent list on the site",
|
||||
audience: "agent",
|
||||
goal: "检查站点、下载器和媒体服务器是否正常",
|
||||
environment: ENV,
|
||||
capabilities: ["downloader.list", "site.list"],
|
||||
});
|
||||
// 与父会话同一条硬规则
|
||||
expect(child).toContain("思考、步骤说明和汇报都用中文");
|
||||
expect(userPrompt()).toContain("思考、步骤说明和汇报都用中文");
|
||||
// 子会话专有的一节:目标 / 工具结果是英文也用中文
|
||||
expect(section(child, "汇报对象")).toMatchInlineSnapshot(`
|
||||
"## 汇报对象
|
||||
- 你的汇报交给父会话汇总,不直接给用户看,可以带对象 ID。
|
||||
- 思考、步骤说明和汇报一律用中文,即使目标、工具结果或网页是英文;引用文件名、原站标题时保持原文。"
|
||||
`);
|
||||
expect(section(userPrompt(), "汇报对象")).toBe("");
|
||||
const titles = [...p.matchAll(/^## (.+)$/gm)].map((m) => m[1]);
|
||||
expect(titles).toEqual(["角色与边界", "当前环境", "能力地图", "行为规则", "汇报格式", "本次目标"]);
|
||||
});
|
||||
|
||||
it("按会话类型选层:视觉子流程只有角色、汇报与任务;子会话汇报给父会话", () => {
|
||||
expect(LAYERS_BY_KIND.vision).toEqual(["role", "report", "task"]);
|
||||
const vision = promptSections({ kind: "vision", role: "网页挑战处理员", goal: "过验证", environment: ENV });
|
||||
expect(vision.map((s) => s.layer)).toEqual(["role", "report", "task"]);
|
||||
const child = buildLayeredPrompt({
|
||||
kind: "child",
|
||||
role: "子会话",
|
||||
goal: "查下载",
|
||||
capabilities: ["download.list"],
|
||||
});
|
||||
expect(section(child, "汇报格式")).toContain("汇报给父会话汇总");
|
||||
expect(section(child, "汇报格式")).toContain("思考、步骤说明和汇报都用中文");
|
||||
expect(child).not.toContain("不超过 8 行");
|
||||
});
|
||||
|
||||
it("巡检会话的汇报层要求按巡检模板写、只读", () => {
|
||||
const p = buildLayeredPrompt({ kind: "patrol", role: "巡检员", goal: "巡检", capabilities: ["downloader.list"] });
|
||||
expect(section(p, "汇报格式")).toContain("按 Skill「巡检」的汇报模板写");
|
||||
});
|
||||
});
|
||||
|
||||
describe("工作方式(AGUI-3)", () => {
|
||||
describe("① 角色与边界", () => {
|
||||
it("证据、授权、已提交≠已入库、不可信外部内容、凭证禁令、中文", () => {
|
||||
const p = section(userPrompt(), "角色与边界");
|
||||
expect(p).toContain("你是媒体库自动化智能体");
|
||||
expect(p).toContain("写出查到的对象名称和关键数值");
|
||||
expect(p).toContain("不得声称用户已授权");
|
||||
expect(p).toContain("「已提交」不等于「已入库」");
|
||||
expect(p).toMatch(/网页、RSS.*不可信数据/);
|
||||
expect(p).toMatch(/不得输出.*凭证.*passkey/);
|
||||
expect(p).toContain("思考、步骤说明和汇报都用中文");
|
||||
});
|
||||
});
|
||||
|
||||
describe("② 当前环境", () => {
|
||||
it("把已接入的下载器、没接入的媒体服务器、站点上次检查写成人话:名称、中文状态、相对时间", () => {
|
||||
const lines = renderEnvironment(ENV);
|
||||
const text = lines.join("\n");
|
||||
expect(text).toContain("现在:2026-09-30 17:45");
|
||||
expect(text).toContain(
|
||||
"下载器 1 个:「家里的 qB」qBittorrent,正常(3 分钟前),本工具管理 24 个任务(进行中 0、失败 0)",
|
||||
);
|
||||
expect(text).toContain("媒体服务器:还没有接入");
|
||||
expect(text).toContain("「憨憨」PT 站,上次检查正常(昨天 16:12),登录正常");
|
||||
expect(text).toContain("订阅 5 个:已取消 5");
|
||||
expect(text).toContain("待处理事项:没有");
|
||||
// 不带地址、ID、英文状态
|
||||
expect(text).not.toMatch(/healthy|qbittorrent\b|http|[0-9a-f]{8}-/);
|
||||
});
|
||||
|
||||
it("没有下载器时明说没接入,并告诉模型不用再向用户确认", () => {
|
||||
const p = buildLayeredPrompt({
|
||||
kind: "chat",
|
||||
role: "x",
|
||||
goal: "y",
|
||||
environment: { ...ENV, downloaders: [] },
|
||||
});
|
||||
expect(p).toContain("下载器:还没有接入");
|
||||
expect(p).toContain("不用再向用户确认");
|
||||
});
|
||||
});
|
||||
|
||||
describe("③ 能力地图", () => {
|
||||
it("按意图给路线,标出需要批准的一步;只列可用能力", () => {
|
||||
const m = section(userPrompt(), "能力地图");
|
||||
expect(m).toContain(
|
||||
"- 问下载器、媒体服务器、站点是否正常:downloader.list(下载器与媒体服务器的最近状态和任务数) → site.list",
|
||||
);
|
||||
expect(m).toContain("site.health_check(实时检测某个站点,需要批准)");
|
||||
expect(m).toContain("download.list(按状态或作品列出下载任务) → download.inspect");
|
||||
expect(m).toContain("不要向用户要 ID 或种子 hash");
|
||||
// 不可用的能力不出现在路线里
|
||||
expect(m).not.toContain("download.submit");
|
||||
expect(m).not.toContain("收录一部作品");
|
||||
});
|
||||
|
||||
it("保留 AG-3 的参数与检索词约束、站点类型(IA-2)", () => {
|
||||
const m = section(userPrompt(), "能力地图");
|
||||
expect(m).toContain('query:"a"');
|
||||
expect(m).toContain('externalId:"693134"');
|
||||
expect(m).toContain("RSS 属于站点");
|
||||
expect(m).toContain("没有单独的 RSS 页");
|
||||
expect(m).toContain("自定义 RSS 源");
|
||||
});
|
||||
});
|
||||
|
||||
describe("④ 行为规则", () => {
|
||||
it("自己能查的不问、授权缺失一句话、不说教、预算提醒", () => {
|
||||
const b = section(userPrompt(), "行为规则");
|
||||
expect(b).toContain("自己能查的不问用户");
|
||||
expect(b).toContain("只写一句「没做:<动作>,需要你批准」");
|
||||
expect(b).toContain("不说教");
|
||||
expect(b).toContain("【预算提醒】");
|
||||
});
|
||||
|
||||
it("只在真有分歧时给方案;查询、盘点不给方案", () => {
|
||||
const rules = interactionRules({ options: true }).join("\n");
|
||||
expect(rules).toContain("只有真的存在分歧时");
|
||||
@@ -83,8 +183,10 @@ describe("工作方式(AGUI-3)", () => {
|
||||
expect(interactionRules({ options: true }).join("\n")).not.toContain("approvedAction");
|
||||
});
|
||||
|
||||
it("步骤标题写要做的事,不写工具名", () => {
|
||||
expect(interactionRules({ plan: true }).join("\n")).toContain("不写工具名");
|
||||
it("步骤标题写要做的事,不写工具名;一两次查询不列步骤", () => {
|
||||
const r = interactionRules({ plan: true }).join("\n");
|
||||
expect(r).toContain("不写工具名");
|
||||
expect(r).toContain("一两次查询就能回答的问题不用列步骤");
|
||||
});
|
||||
|
||||
it("先看方案(/先给方案)只对本会话生效的约束句保留", () => {
|
||||
@@ -92,11 +194,31 @@ describe("工作方式(AGUI-3)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("站点类型(IA-2)", () => {
|
||||
it("RSS 属于站点,没有单独的 RSS 页;按获取方式判断能不能搜索 / 登录", () => {
|
||||
const p = userPrompt();
|
||||
expect(p).toContain("RSS 属于站点");
|
||||
expect(p).toContain("没有单独的 RSS 页");
|
||||
expect(p).toContain("自定义 RSS 源");
|
||||
describe("⑤ 汇报格式", () => {
|
||||
it("结论先行、≤8 行、人话单位、对象用名字、只在列举多个对象时用表格", () => {
|
||||
const f = section(userPrompt(), "汇报格式");
|
||||
expect(f).toContain("第一句就是结论");
|
||||
expect(f).toContain("不超过 8 行");
|
||||
expect(f).toContain("大小写成 GB / TB");
|
||||
expect(f).toContain("时间写相对时间");
|
||||
expect(f).toContain("不写函数名、工具名、字段名、参数、UUID、hash、adapter 或英文状态码");
|
||||
expect(f).toContain("只有列举 3 个以上同类对象");
|
||||
expect(f).toContain("不写 HTML");
|
||||
expect(REPORT_FORMAT.length).toBe(9);
|
||||
});
|
||||
|
||||
it("用户汇报不要求引用对象 ID", () => {
|
||||
expect(userPrompt()).not.toContain("对象 ID");
|
||||
});
|
||||
});
|
||||
|
||||
describe("子会话中文(AG-4)", () => {
|
||||
it("子会话与父会话一样要求思考、步骤说明和汇报都用中文", () => {
|
||||
const child = buildSystemPrompt({
|
||||
role: "媒体库自动化智能体",
|
||||
goal: "Check the torrent list on the site",
|
||||
audience: "agent",
|
||||
});
|
||||
expect(child).toContain("思考、步骤说明和汇报都用中文");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user