feat: 任务书 07,agents 模块与真实模型测试

src/agents 4 个文件 681 行:runSession 按角色开 Pi 子会话、发工具、核对实际加载内容,
提交参数非法 JSON、必填字段交 null、Pi 形状校验不通过都经提交回调记退回。
Pi 依赖固定 0.85.1;新增 npm run test:pi(glm-5.3 真实会话)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 01:03:19 -04:00
parent a876f0fe9d
commit 3600b5c409
10 changed files with 4529 additions and 15 deletions
+145
View File
@@ -0,0 +1,145 @@
import assert from "node:assert/strict";
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { runSession, type SessionOutcome, type SessionTask, type SubmitAttempt } from "../src/agents/index.ts";
import type { ModelUsage } from "../src/vocab/index.ts";
import { fullConfig } from "./config-helpers.ts";
// agents 的真实模型测试(任务书 07 第六部分),npm run test:pi:要花钱、要联网,不放进 npm test。
// 用本机真实的 ~/.pi/agent(里面有扩展、技能、插件包),模型取测试配置里的 glm-5.3。
// 每次会话的结局、每次回复的用量、墙钟时间都完整打印;会话目录留在临时目录里,路径也打印出来。
const models = fullConfig().models;
const root = mkdtempSync(join(tmpdir(), "inforsi-agents-pi-"));
console.log(`临时目录:${root}`);
const agentDir = join(homedir(), ".pi", "agent");
for (const sub of ["extensions", "skills"]) {
const dir = join(agentDir, sub);
console.log(`本机 ${dir}:${existsSync(dir) ? readdirSync(dir).join(", ") : "(没有)"}`);
}
const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8")) as { packages?: unknown[]; compaction?: unknown };
console.log(`本机 settings.json:packages ${JSON.stringify(settings.packages)};compaction ${JSON.stringify(settings.compaction)}`);
function dirs(name: string): { workDir: string; sessionDir: string } {
const workDir = join(root, name, "work");
const sessionDir = join(root, name, "sessions");
mkdirSync(workDir, { recursive: true });
mkdirSync(sessionDir, { recursive: true });
return { workDir, sessionDir };
}
function show(usage: ModelUsage): string {
const v = (m: ModelUsage["inputTokens"]) => (m.status === "measured" ? String(m.value) : `测不到(${m.reason})`);
return `输入 ${v(usage.inputTokens)},缓存读 ${v(usage.cacheReadTokens)},缓存写 ${v(usage.cacheWriteTokens)},输出 ${v(usage.outputTokens)},价格 ${v(usage.cost)}`;
}
/** 会话文件里助手回复的条数(Pi 的会话文件每行一条 JSON)。 */
function assistantReplies(sessionFile: string): number {
return readFileSync(sessionFile, "utf8")
.split("\n")
.filter((line) => line.trim() !== "")
.map((line) => JSON.parse(line) as { type?: string; message?: { role?: string } })
.filter((entry) => entry.type === "message" && entry.message?.role === "assistant").length;
}
function report(name: string, outcome: SessionOutcome, usages: ModelUsage[], attempts: string[]): void {
console.log(`\n===== ${name} =====`);
console.log(`结局:${JSON.stringify(outcome)}`);
console.log(`墙钟时间:${outcome.wallSeconds.toFixed(1)} 秒`);
attempts.forEach((a, i) => console.log(`提交回调第 ${i + 1} 次:${a}`));
usages.forEach((u, i) => console.log(`第 ${i + 1} 次回复的用量:${show(u)}`));
assert.ok(outcome.sessionFile !== null, "会话文件写出来了");
const replies = assistantReplies(outcome.sessionFile);
console.log(`会话文件里助手回复 ${replies} 条,用量回调 ${usages.length} 次`);
assert.equal(usages.length, replies, "每次回复都交了一份用量");
assert.ok(usages.every((u) => u.inputTokens.status === "measured" && u.outputTokens.status === "measured"), "模型服务每次都报了用量");
}
const stop = new AbortController().signal;
// ---------------------------------------------------------------------------
// 研究者:交一份收尾,第一次故意退回,改了再交,收下
// ---------------------------------------------------------------------------
{
const { workDir, sessionDir } = dirs("researcher");
const readable = join(root, "researcher", "task");
mkdirSync(readable);
writeFileSync(join(readable, "README.md"), "填圆任务:在单位正方形里放 26 个圆,半径之和越大越好。\n");
const usages: ModelUsage[] = [];
const attempts: string[] = [];
const task: SessionTask = {
role: "researcher",
model: models.researchers[0]!,
systemPrompt: "你是一个研究小组里的研究者。你只能用给你的工具读资料和提交;只有调用 research_submit 才算交上了。",
firstMessage: [
"这是一次接口演练。请交一份收尾(research_submit,payload 里只有 body 一个字段)。",
`body 写两三句话,说说这一轮的情况:可以先用 read_source 读一下 ${readable}/README.md 了解任务。`,
"程序如果退回,按退回原因改好再交。被收下以后就结束,不用再做别的。",
].join("\n"),
...{ workDir, sessionDir },
kind: "wrapUp",
readableDirs: [readable],
query: () => null,
writeNote: (body) => attempts.push(`(写笔记 ${body.length} 字)`),
usage: (u) => usages.push(u),
stop,
submit: async (attempt: SubmitAttempt<"wrapUp">) => {
attempts.push(JSON.stringify(attempt));
if (attempt.shape === "invalid") return { accepted: false, reason: attempt.reason };
if (attempts.filter((a) => a.includes('"shape":"valid"')).length === 1) {
return { accepted: false, reason: "收尾的最后一行必须是「下一步:继续在 p1 上试」,原样写上后整份重交" };
}
return { accepted: true, id: "s2" };
},
};
const outcome = await runSession(task);
report("研究者:先退回再收下", outcome, usages, attempts);
assert.equal(outcome.status, "submitted");
assert.ok(attempts.filter((a) => a.includes('"shape":"valid"')).length >= 2, "至少交了两次:一次被退回,一次收下");
}
// ---------------------------------------------------------------------------
// 实验员:在工作目录里写一个文件、跑一条 shell 命令,再交
// ---------------------------------------------------------------------------
{
const { workDir, sessionDir } = dirs("experimenter");
const usages: ModelUsage[] = [];
const attempts: string[] = [];
let checks = 0;
const task: SessionTask = {
role: "experimenter",
model: models.experimenter,
systemPrompt: "你是一个研究小组里的实验员,在自己的工作目录里写代码、跑代码。只有调用 research_submit 才算交上了。",
firstMessage: [
"这是一次接口演练,按顺序做三件事:",
"1. 在工作目录里写一个 hello.py,运行时打印 hello inforsi。",
"2. 用 bash 运行 python3 hello.py,确认输出。",
"3. 用 research_submit 提交:payload 里 alsoOff 交空数组,cannotRun 交 null,concerns 写一句你运行时看到的输出。",
].join("\n"),
...{ workDir, sessionDir },
kind: "ablationImplementation",
query: () => null,
lightCheck: async () => `第 ${++checks} 次轻量检查:通过`,
usage: (u) => usages.push(u),
stop,
submit: async (attempt: SubmitAttempt<"ablationImplementation">) => {
attempts.push(JSON.stringify(attempt));
if (attempt.shape === "invalid") return { accepted: false, reason: attempt.reason };
return existsSync(join(workDir, "hello.py"))
? { accepted: true, id: "s1" }
: { accepted: false, reason: "工作目录里没有 hello.py" };
},
};
const outcome = await runSession(task);
report("实验员:写文件、跑 shell、再交", outcome, usages, attempts);
assert.equal(outcome.status, "submitted");
console.log(`hello.py:${JSON.stringify(readFileSync(join(workDir, "hello.py"), "utf8"))}`);
const log = readFileSync(outcome.sessionFile!, "utf8");
assert.match(log, /"name":"bash"/, "会话里调过 bash");
assert.match(log, /hello inforsi/, "bash 的输出进了会话");
}
console.log("\n全部通过:两次会话的子会话核对都通过(没有加载扩展、技能、提示模板和上下文文件),都交上了,每次回复都有用量。");