75d7cffd27
按任务书 12 实现 src/flow:从账本现算下一步的纯函数、主循环、恢复入口、派模型任务和跑正式运行两个共用动作、冻结配置、基线、第零轮三个研究者阶段和入口分配,走到第 1 轮时停下。materials 导出 finalEntries、加 researcherId 和 StudyRecord。 审阅采纳两条:研究编号本机已用过时冻结前报错;docker run 没启动时写完结果就停下,不在本进程里无限起新编号。 eval/ 新增第零轮检验:起研究命令行、回放假模型、检验用 Jev 客户端、六问质量评估、16-real-1 回放、杀进程与两份同时跑。tsc 与 depcruise 也检查 eval/,新增 eval 只经 index.ts 用正式代码的规则。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
6.2 KiB
TypeScript
122 lines
6.2 KiB
TypeScript
import { runSession, type KindOf, type SessionEnding, type SubmitAttempt, type SubmitReceipt } from "../agents/index.ts";
|
||
import { buildMaterial, callId, queryMaterial, researcherId, type TaskMaterial } from "../materials/index.ts";
|
||
import { receive, type Incoming, type KnownOf } from "../submissions/index.ts";
|
||
import type { JevJudgment, Json, Submission } from "../vocab/index.ts";
|
||
import { checkoutVersion } from "../workspace/index.ts";
|
||
import { callPaths, freshDir, workspaceRepo } from "./paths.ts";
|
||
import type { Study } from "./study.ts";
|
||
|
||
// 共用动作一:派一个模型任务(任务书 12 第 2.6 节)。先写"模型调用开始"(材料整份存进去),再开会话,
|
||
// 最后写"模型调用结束",前后顺序由这个入口保证(方法.md 第九部分"先登记、再执行、再登记结果")。
|
||
// 提交收不收只由 submissions 判;形状不合格的由 agents 报上来,这里直接记退回("给 flow")。
|
||
|
||
/** 一个研究者任务:哪种提交、谁、哪个阶段、对象;known 是交上来之后才知道的东西(submissions 的 KnownOf)。 */
|
||
export type ModelTask<K extends KindOf<"researcher">> = {
|
||
kind: K;
|
||
actor: string;
|
||
stageId: string;
|
||
object: string | null;
|
||
known(content: Submission<K>): KnownOf<K>;
|
||
};
|
||
|
||
/** 派一次任务,等会话结束,交回调用编号和结局。 */
|
||
export async function runModelTask<K extends KindOf<"researcher">>(s: Study, t: ModelTask<K>): Promise<{ call: string; ending: SessionEnding }> {
|
||
const { ledger } = s;
|
||
const started = new Set(ledger.records().flatMap((r) => (r.kind === "callStarted" ? [r.content.call] : [])));
|
||
let attempt = 1;
|
||
while (started.has(callId(t.stageId, t.actor, t.kind, t.object, attempt))) attempt++;
|
||
const call = callId(t.stageId, t.actor, t.kind, t.object, attempt);
|
||
const paths = callPaths(s.dir, call);
|
||
const material = buildMaterial({
|
||
kind: t.kind,
|
||
actor: t.actor,
|
||
stageId: t.stageId,
|
||
object: t.object,
|
||
ledger,
|
||
study: s.facts,
|
||
prompts: s.prompts,
|
||
codeRoot: paths.code,
|
||
changes: null,
|
||
artifacts: null,
|
||
});
|
||
const model = researcherModel(s, t.actor);
|
||
// 先登记、后建目录:调用编号只在登记之后才算用掉,被杀在两者之间时下次的编号不会撞上旧目录。
|
||
ledger.append("callStarted", {
|
||
call,
|
||
actor: t.actor,
|
||
stageId: t.stageId,
|
||
model: `${model.provider}/${model.model}(思考档位 ${model.thinking})`,
|
||
materials: material as unknown as Json,
|
||
});
|
||
freshDir(paths.base);
|
||
freshDir(paths.code);
|
||
for (const r of material.readable) checkoutVersion(workspaceRepo(s.dir), r.version, freshDir(r.dir));
|
||
let reply = 0;
|
||
const outcome = await runSession({
|
||
role: "researcher",
|
||
model,
|
||
systemPrompt: s.prompts.roles.researcher.text,
|
||
firstMessage: material.firstMessage,
|
||
workDir: freshDir(paths.work),
|
||
sessionDir: freshDir(paths.sessions),
|
||
kind: t.kind,
|
||
readableDirs: material.readable.map((r) => r.dir),
|
||
query: (ref) => queryMaterial(material, ref),
|
||
submit: (attempt) => submit(s, t, material, attempt as SubmitAttempt<K>),
|
||
usage: (usage) => void ledger.append("callUsage", { call, reply: ++reply, usage }),
|
||
writeNote: (body) => void ledger.append("note", { author: t.actor, body }),
|
||
stop: s.stop,
|
||
});
|
||
// 会话文件就在这次调用的 sessions 目录里,不另记。
|
||
const { wallSeconds, sessionFile: _sessionFile, ...ending } = outcome;
|
||
ledger.append("callEnded", { call, wallSeconds: { status: "measured", value: wallSeconds }, ...ending });
|
||
return { call, ending };
|
||
}
|
||
|
||
/** 研究者用的模型:第 i 位研究者(researcherId(i))用配置里第 i 个。 */
|
||
function researcherModel(s: Study, actor: string) {
|
||
const models = s.frozen.config.models.researchers;
|
||
const i = models.findIndex((_, k) => researcherId(k) === actor);
|
||
if (i < 0) throw new Error(`${actor} 不是这份研究的研究者`);
|
||
return models[i]!;
|
||
}
|
||
|
||
/**
|
||
* 一份提交收不收。这个任务只有一份提交(第零轮每人每阶段一个任务、一份提交):
|
||
* 回给模型的提交编号就是这一份的位置,不分版本;替换时写它,接在最新一版后面。
|
||
* 第一次交以后不写 replaces 又交,或 replaces 写的不是这一份,都退回并说明该怎么写。
|
||
*/
|
||
async function submit<K extends KindOf<"researcher">>(s: Study, t: ModelTask<K>, material: TaskMaterial, attempt: SubmitAttempt<K>): Promise<SubmitReceipt> {
|
||
const { ledger } = s;
|
||
const slot = { stageId: t.stageId, author: t.actor, kind: t.kind, target: t.object, item: null };
|
||
const reject = (submitted: Json, reason: string, jev: JevJudgment[] | null): SubmitReceipt => {
|
||
ledger.append("rejection", { stageId: t.stageId, author: t.actor, kind: t.kind, target: t.object, submitted, reason, jev });
|
||
return { accepted: false, reason };
|
||
};
|
||
if (attempt.shape === "invalid") return reject(attempt.raw, attempt.reason, null);
|
||
const id = receiptId(t);
|
||
const latest = ledger.latestSubmission(slot)?.content.version ?? 0;
|
||
const content = attempt.content as unknown as Json;
|
||
if (attempt.replaces === null && latest > 0) {
|
||
return reject(content, `本阶段你已经交过这一份(提交编号 ${id})。要改就在 replaces 里写 ${id} 整份替换,不要另交`, null);
|
||
}
|
||
if (attempt.replaces !== null && (attempt.replaces !== id || latest === 0)) {
|
||
const which = latest === 0 ? "你本阶段还没有收下的提交,第一次交时不要带 replaces" : `要替换就写 ${id}`;
|
||
return reject(content, `replaces 写的 ${attempt.replaces} 不是你本阶段交过的提交:${which}`, null);
|
||
}
|
||
const verdict = await receive({
|
||
kind: t.kind,
|
||
content: attempt.content,
|
||
checklist: material.checklist,
|
||
known: t.known(attempt.content),
|
||
jevModel: s.frozen.config.models.jev,
|
||
} as Incoming);
|
||
if (!verdict.accepted) return reject(content, verdict.reason, verdict.jev);
|
||
ledger.append("submission", { ...slot, version: latest + 1, content: attempt.content, jev: verdict.jev } as never);
|
||
return { accepted: true, id };
|
||
}
|
||
|
||
/** 回给模型的提交编号:阶段、本人、提交种类、对象。 */
|
||
const receiptId = (t: { stageId: string; actor: string; kind: string; object: string | null }): string =>
|
||
`${t.stageId}.${t.actor}.${t.kind}.${t.object ?? "none"}`;
|