Files
InfoRSI/src/flow/model-task.ts
T
iomgaa 75d7cffd27 feat: flow 第 1 部分(骨架、研究开始、第零轮)与第零轮检验
按任务书 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>
2026-10-03 10:58:52 -04:00

122 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"}`;