feat: 接通实现核验修复到正向运行采纳
任务书16第二块:角色分派与每调用轻量检查,提交及代码原子记账,按次数核验修复且耗尽不跑,串行正式运行及恢复引用指标,采纳含null统一公开。451项免费测试通过,全新上下文独立审阅未发现问题;真实研究验收仍待进行。
This commit is contained in:
+11
-2
@@ -1,6 +1,7 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import type { ResearchConfig } from "../config/index.ts";
|
||||
import { forwardNotRunEvent } from "../materials/index.ts";
|
||||
import { Ledger } from "../ledger/index.ts";
|
||||
import { Runner } from "../runner/index.ts";
|
||||
import { fullConfig, readFrozen } from "./frozen.ts";
|
||||
@@ -8,6 +9,7 @@ import { EVENTS, nextStep, type Next } from "./next-step.ts";
|
||||
import { ledgerFile } from "./paths.ts";
|
||||
import { recover } from "./recover.ts";
|
||||
import { baselineRun, saveBaseline } from "./steps/baseline.ts";
|
||||
import { forwardRun, adoptBatch } from "./steps/forward.ts";
|
||||
import { dispatchStage } from "./steps/dispatch.ts";
|
||||
import { dedupe, freezeProtocols, selectBatch } from "./steps/round.ts";
|
||||
import { assignEntries } from "./steps/round-zero.ts";
|
||||
@@ -75,8 +77,9 @@ async function doStep(s: Study, next: Exclude<Next, { do: "freezeConfig" }>): Pr
|
||||
case "saveBaseline":
|
||||
saveBaseline(s);
|
||||
return null;
|
||||
case "baselineRun": {
|
||||
const ran = await baselineRun(s, next.run);
|
||||
case "baselineRun":
|
||||
case "forwardRun": {
|
||||
const ran = next.do === "baselineRun" ? await baselineRun(s, next.run) : await forwardRun(s, next.round, next.candidate, next.run);
|
||||
if (ran === "stopped") return USER_STOPPED;
|
||||
if (ran === "done") return null;
|
||||
return halt(s.ledger, `运行 ${next.run} 没有启动,名额已退还;本进程不重跑,下次启动时起新编号重跑。Docker 的报错:${ran.notStarted}`, [next.run]);
|
||||
@@ -104,6 +107,12 @@ async function doStep(s: Study, next: Exclude<Next, { do: "freezeConfig" }>): Pr
|
||||
case "selectBatch":
|
||||
selectBatch(s, next.round);
|
||||
return null;
|
||||
case "forwardNotRun":
|
||||
s.ledger.append("event", forwardNotRunEvent(next.candidate, next.reason));
|
||||
return null;
|
||||
case "adopt":
|
||||
adoptBatch(s, next.round);
|
||||
return null;
|
||||
case "halt":
|
||||
return halt(s.ledger, next.reason, next.objects);
|
||||
}
|
||||
|
||||
+38
-24
@@ -1,8 +1,9 @@
|
||||
import { runSession, type KindOf, type SessionEnding, type SubmitAttempt, type SubmitReceipt } from "../agents/index.ts";
|
||||
import { runSession, type SessionTask, 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 { receive, type Incoming } from "../submissions/index.ts";
|
||||
import { SUBMISSION_KINDS, type JevJudgment, type Json, type SubmissionKind } from "../vocab/index.ts";
|
||||
import { checkoutVersion } from "../workspace/index.ts";
|
||||
import { prepareSubmission, taskCode, lightChecker } from "./task-code.ts";
|
||||
import { callPaths, freshDir, workspaceRepo } from "./paths.ts";
|
||||
import type { Study } from "./study.ts";
|
||||
|
||||
@@ -10,26 +11,26 @@ import type { Study } from "./study.ts";
|
||||
// 最后写"模型调用结束",前后顺序由这个入口保证(方法.md 第九部分"先登记、再执行、再登记结果")。
|
||||
// 提交收不收只由 submissions 判;形状不合格的由 agents 报上来,这里直接记退回("给 flow")。
|
||||
|
||||
/** 一个研究者任务:哪种提交、谁、哪个阶段、对象。 */
|
||||
export type ModelTask<K extends KindOf<"researcher">> = {
|
||||
/** 一个模型任务:哪种提交、谁、哪个阶段、对象。 */
|
||||
export type ModelTask<K extends SubmissionKind> = {
|
||||
kind: K;
|
||||
actor: string;
|
||||
stageId: string;
|
||||
object: string | null;
|
||||
/** 一个任务里能交几份的种类(提案、挑刺):第 n 份的编号(materials 的 proposalId、critiqueId),收下时分配;只交一份的为 null。 */
|
||||
item: ((n: number) => string) | null;
|
||||
/** 交上来之后才知道的东西(submissions 的 KnownOf)。others 是本任务已收下、而且不是这次要替换的那几份的份数。 */
|
||||
known(content: Submission<K>, others: number): KnownOf<K>;
|
||||
};
|
||||
|
||||
/** 派一次任务,等会话结束,交回调用编号和结局。 */
|
||||
export async function runModelTask<K extends KindOf<"researcher">>(s: Study, t: ModelTask<K>): Promise<{ call: string; ending: SessionEnding }> {
|
||||
export async function runModelTask<K extends SubmissionKind>(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 { changes, startVersion } = taskCode(s, t);
|
||||
const role = SUBMISSION_KINDS[t.kind].role;
|
||||
const material = buildMaterial({
|
||||
kind: t.kind,
|
||||
actor: t.actor,
|
||||
@@ -39,37 +40,44 @@ export async function runModelTask<K extends KindOf<"researcher">>(s: Study, t:
|
||||
study: s.facts,
|
||||
prompts: s.prompts,
|
||||
codeRoot: paths.code,
|
||||
changes: null,
|
||||
changes,
|
||||
artifacts: null,
|
||||
});
|
||||
const model = researcherModel(s, t.actor);
|
||||
const model = role === "researcher" ? researcherModel(s, t.actor) : s.frozen.config.models[role];
|
||||
// 先登记、后建目录:调用编号只在登记之后才算用掉,被杀在两者之间时下次的编号不会撞上旧目录。
|
||||
ledger.append("callStarted", {
|
||||
call,
|
||||
actor: t.actor,
|
||||
stageId: t.stageId,
|
||||
model: `${model.provider}/${model.model}(思考档位 ${model.thinking})`,
|
||||
// SAFETY: TaskMaterial 的全部字段都是可序列化数据,账本在写入时校验 Json。
|
||||
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));
|
||||
const workDir = freshDir(paths.work);
|
||||
if (startVersion !== null) checkoutVersion(workspaceRepo(s.dir), startVersion, workDir);
|
||||
let reply = 0;
|
||||
const outcome = await runSession({
|
||||
role: "researcher",
|
||||
const common = {
|
||||
model,
|
||||
systemPrompt: s.prompts.roles.researcher.text,
|
||||
systemPrompt: s.prompts.roles[role].text,
|
||||
firstMessage: material.firstMessage,
|
||||
workDir: freshDir(paths.work),
|
||||
workDir,
|
||||
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 }),
|
||||
query: (ref: string) => queryMaterial(material, ref),
|
||||
submit: (attempt: SubmitAttempt<K>) => submit(s, t, material, attempt, workDir),
|
||||
usage: (usage: Parameters<SessionTask["usage"]>[0]) => void ledger.append("callUsage", { call, reply: ++reply, usage }),
|
||||
stop: s.stop,
|
||||
});
|
||||
};
|
||||
const readableDirs = material.readable.map((r) => r.dir);
|
||||
const task = role === "experimenter"
|
||||
? { ...common, role, lightCheck: lightChecker(s, call, workDir) }
|
||||
: role === "verifier"
|
||||
? { ...common, role, readableDirs }
|
||||
: { ...common, role, readableDirs, writeNote: (body: string) => void ledger.append("note", { author: t.actor, body }) };
|
||||
const outcome = await runSession(task as SessionTask);
|
||||
// 会话文件就在这次调用的 sessions 目录里,不另记。
|
||||
const { wallSeconds, sessionFile: _sessionFile, ...ending } = outcome;
|
||||
ledger.append("callEnded", { call, wallSeconds: { status: "measured", value: wallSeconds }, ...ending });
|
||||
@@ -85,26 +93,32 @@ function researcherModel(s: Study, actor: string) {
|
||||
}
|
||||
|
||||
/** 一份提交收不收:先定写到哪一份,形状合格的再交给 submissions 判。 */
|
||||
async function submit<K extends KindOf<"researcher">>(s: Study, t: ModelTask<K>, material: TaskMaterial, attempt: SubmitAttempt<K>): Promise<SubmitReceipt> {
|
||||
async function submit<K extends SubmissionKind>(s: Study, t: ModelTask<K>, material: TaskMaterial, attempt: SubmitAttempt<K>, workDir: string): Promise<SubmitReceipt> {
|
||||
const { ledger } = s;
|
||||
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);
|
||||
// SAFETY: agents 已按 vocab 的提交字段校验,提交内容只含 Json 值。
|
||||
const content = attempt.content as unknown as Json;
|
||||
const slot = slotOf(s, t, attempt.replaces);
|
||||
if ("refuse" in slot) return reject(content, slot.refuse, null);
|
||||
const prepared = prepareSubmission(s, t, attempt.content, slot.others, workDir);
|
||||
const verdict = await receive({
|
||||
kind: t.kind,
|
||||
content: attempt.content,
|
||||
checklist: material.checklist,
|
||||
known: t.known(attempt.content, slot.others),
|
||||
known: prepared.known,
|
||||
jevModel: s.frozen.config.models.jev,
|
||||
} as Incoming);
|
||||
if (!verdict.accepted) return reject(content, verdict.reason, verdict.jev);
|
||||
const { stageId, actor: author, kind, object: target } = t;
|
||||
ledger.append("submission", { stageId, author, kind, target, item: slot.item, version: slot.version, content: attempt.content, jev: verdict.jev } as never);
|
||||
// 代码在提交时冻结;提交和代码记录同进同退,恢复不能看到“交了却没快照”的半截状态。
|
||||
ledger.transaction(() => {
|
||||
ledger.append("submission", { stageId, author, kind, target, item: slot.item, version: slot.version, content: attempt.content, jev: verdict.jev } as never);
|
||||
if (prepared.code !== null) ledger.append("code", prepared.code);
|
||||
});
|
||||
return { accepted: true, id: slot.id };
|
||||
}
|
||||
|
||||
@@ -113,7 +127,7 @@ async function submit<K extends KindOf<"researcher">>(s: Study, t: ModelTask<K>,
|
||||
* 提交编号:能交几份的种类就是这一份的编号,只交一份的是"阶段.本人.种类.对象"。
|
||||
* 不写 replaces 是交新的一份,只交一份的种类已经交过就退回并说明该怎么写;写了 replaces 就必须是本任务已收下的某一份。
|
||||
*/
|
||||
function slotOf<K extends KindOf<"researcher">>(
|
||||
function slotOf<K extends SubmissionKind>(
|
||||
s: Study,
|
||||
t: ModelTask<K>,
|
||||
replaces: string | null,
|
||||
|
||||
+89
-13
@@ -1,9 +1,9 @@
|
||||
import type { KindOf } from "../agents/index.ts";
|
||||
import type { RoundStepId, StudyStepId } from "../config/index.ts";
|
||||
import type { Content, LedgerRecord } from "../ledger/index.ts";
|
||||
import { Board, ORIGINAL_BASELINE, researcherId } from "../materials/index.ts";
|
||||
import { Board, experimenterId, verifierId, FORWARD_NOT_RUN, ORIGINAL_BASELINE, researcherId } from "../materials/index.ts";
|
||||
import { compareIds } from "../rules/index.ts";
|
||||
import type { Stage } from "../vocab/index.ts";
|
||||
import { verificationOutcome } from "../submissions/index.ts";
|
||||
import { SUBMISSION_KINDS, type Stage, type SubmissionKind } from "../vocab/index.ts";
|
||||
import { readFrozen, type Frozen } from "./frozen.ts";
|
||||
|
||||
// "下一步做什么"(docs/结构.md 第二部分"几条边界"):步骤的先后只写在这里。
|
||||
@@ -40,8 +40,8 @@ export const EVENTS = {
|
||||
runStatusUnknown: "runStatusUnknown",
|
||||
} as const;
|
||||
|
||||
/** 一个研究者任务:一个人对一个对象(候选、原提案;提案和第零轮的任务没有对象)交一种提交。 */
|
||||
export type TaskRef = { actor: string; kind: KindOf<"researcher">; object: string | null };
|
||||
/** 一个任务:一个人对一个对象交一种提交;提案和第零轮任务没有对象。 */
|
||||
export type TaskRef = { actor: string; kind: SubmissionKind; object: string | null };
|
||||
|
||||
export type Next =
|
||||
| { do: "freezeConfig" }
|
||||
@@ -55,6 +55,9 @@ export type Next =
|
||||
| { do: "dedupe"; round: number }
|
||||
| { do: "freezeProtocols"; round: number }
|
||||
| { do: "selectBatch"; round: number }
|
||||
| { do: "forwardNotRun"; candidate: string; reason: string }
|
||||
| { do: "forwardRun"; round: number; candidate: string; run: string }
|
||||
| { do: "adopt"; round: number }
|
||||
| { do: "halt"; reason: string; objects: string[] };
|
||||
|
||||
export function nextStep(records: readonly LedgerRecord[]): Next {
|
||||
@@ -87,7 +90,7 @@ function studyStepNext(step: Exclude<StudyStepId, "rounds">, view: View, frozen:
|
||||
case "discussion2":
|
||||
case "claim": {
|
||||
const { stage, kind } = ROUND_ZERO_STAGES[step];
|
||||
return stageNext(stageIdOf(0, step), stage, 0, (roster) => roster.map((actor) => ({ actor, kind, object: null })), view, frozen);
|
||||
return stageNext(stageIdOf(0, step), stage, 0, (roster) => roster.map((actor) => ({ actor, kind, object: null })), view, frozen, researchers(frozen));
|
||||
}
|
||||
case "assignEntries":
|
||||
// 分配 → 第 0 轮的入口分配决定(decision,种类 entries)。
|
||||
@@ -99,12 +102,12 @@ function studyStepNext(step: Exclude<StudyStepId, "rounds">, view: View, frozen:
|
||||
|
||||
/**
|
||||
* 一轮接一轮(结构.md 第三部分"每一轮"):第一个没做完的轮次里第一个没做完的步骤。
|
||||
* 每一步做完的记录都带轮次,做完的轮次整轮跳过;现在走到实现一步就停下,所以循环一定会返回。
|
||||
* 每一步做完的记录都带轮次,做完的轮次整轮跳过;现在走到列消融格就停下,所以循环一定会返回。
|
||||
*/
|
||||
function roundsNext(view: View, frozen: Frozen): Next {
|
||||
for (let round = 1; ; round++) {
|
||||
for (const step of frozen.steps.round) {
|
||||
const next = step.status === "skip" ? skipNext(stageIdOf(round, step.id), step.reason, view) : roundStepNext(step.id, round, view, frozen);
|
||||
const next = step.status === "skip" && step.id !== "listCells" ? skipNext(stageIdOf(round, step.id), step.reason, view) : roundStepNext(step.id, round, view, frozen);
|
||||
if (next !== null) return next;
|
||||
}
|
||||
}
|
||||
@@ -122,7 +125,7 @@ function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Fro
|
||||
case "critique":
|
||||
case "response":
|
||||
case "secondPrediction":
|
||||
return stageNext(stageIdOf(round, step), step, round, (roster) => roundTasks(step, round, roster, view.board), view, frozen);
|
||||
return stageNext(stageIdOf(round, step), step, round, (roster) => roundTasks(step, round, roster, view.board), view, frozen, researchers(frozen));
|
||||
case "dedupe":
|
||||
// 判重合并 → 本轮的判重合并决定(decision,种类 dedup)。
|
||||
return view.board.decision("dedup", round) === undefined ? { do: "dedupe", round } : null;
|
||||
@@ -138,10 +141,25 @@ function roundStepNext(step: RoundStepId, round: number, view: View, frozen: Fro
|
||||
case "selectBatch":
|
||||
// 选批 → 本轮的选批决定(decision,种类 batch)。
|
||||
return view.board.decision("batch", round) === undefined ? { do: "selectBatch", round } : null;
|
||||
case "implementation":
|
||||
return { do: "halt", reason: "实现还没写(任务书 16)", objects: [] };
|
||||
case "implementation": {
|
||||
const candidates = view.board.decision("batch", round)!.output.chosen;
|
||||
return assignedStage("implementation", round, "implementation", candidates, view, frozen);
|
||||
}
|
||||
case "verification":
|
||||
case "repair":
|
||||
return verificationNext(round, view, frozen);
|
||||
case "forwardRun":
|
||||
return forwardNext(round, view);
|
||||
case "publish":
|
||||
case "informationAccount":
|
||||
// 不另写记录:整批运行已结束。采纳决定使结果整批公开,信息账从预测和结果现算。
|
||||
return null;
|
||||
case "adopt":
|
||||
return view.board.decision("adopt", round) === undefined ? { do: "adopt", round } : null;
|
||||
case "listCells":
|
||||
return { do: "halt", reason: "消融还没写(任务书 17)", objects: [] };
|
||||
default:
|
||||
throw new Error(`走到了第 ${round} 轮的 ${step}:implementation 一步应当先停下`);
|
||||
throw new Error(`走到了第 ${round} 轮的 ${step}:listCells 一步应当先停下`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,10 +233,11 @@ function stageNext(
|
||||
tasksOf: (roster: readonly string[]) => TaskRef[],
|
||||
view: View,
|
||||
frozen: Frozen,
|
||||
roster: string[],
|
||||
): Next | null {
|
||||
const open = view.board.stages.get(stageId);
|
||||
if (open === undefined) {
|
||||
return { do: "openStage", stageId, stage, round, roster: frozen.config.models.researchers.map((_, i) => researcherId(i)) };
|
||||
return { do: "openStage", stageId, stage, round, roster };
|
||||
}
|
||||
if (open.closed) return null;
|
||||
const todo = tasksOf(open.roster).filter((t) => !view.done(stageId, t));
|
||||
@@ -239,6 +258,63 @@ function stageNext(
|
||||
};
|
||||
}
|
||||
|
||||
/** 研究者阶段的冻结名单。实验员、核验员阶段则按本阶段的任务列名单。 */
|
||||
const researchers = (frozen: Frozen): string[] => frozen.config.models.researchers.map((_, i) => researcherId(i));
|
||||
|
||||
/** 正向的实现、修复、核验共用阶段开关和派发规则,每个候选固定一个人。 */
|
||||
function assignedStage(
|
||||
step: string,
|
||||
round: number,
|
||||
kind: "implementation" | "repair" | "verification",
|
||||
candidates: readonly string[],
|
||||
view: View,
|
||||
frozen: Frozen,
|
||||
): Next | null {
|
||||
const actorOf = kind === "verification" ? verifierId : experimenterId;
|
||||
const tasks = candidates.map((object) => ({ actor: actorOf(object), kind, object }));
|
||||
return stageNext(stageIdOf(round, step), SUBMISSION_KINDS[kind].stage, round, () => tasks, view, frozen, tasks.map((t) => t.actor));
|
||||
}
|
||||
|
||||
/**
|
||||
* ⑨:实现全部关闭后开核验;核验全部关闭后,失败的才修复。每次只重验上次修复的候选。
|
||||
* 第几次由已开关的阶段推出来;次数用完先写不跑事件,不登记运行(方法.md 4.3 ⑨)。
|
||||
*/
|
||||
function verificationNext(round: number, view: View, frozen: Frozen): Next | null {
|
||||
let candidates = view.board.decision("batch", round)!.output.chosen;
|
||||
for (let n = 1; candidates.length > 0; n++) {
|
||||
const verificationId = stageIdOf(round, `verification-${n}`);
|
||||
const verify = assignedStage(`verification-${n}`, round, "verification", candidates, view, frozen);
|
||||
if (verify !== null) return verify;
|
||||
const failed = view.board.published("verification")
|
||||
.filter((s) => s.stageId === verificationId && !verificationOutcome(s.content).passed);
|
||||
if (n > frozen.config.maxRepairs) {
|
||||
for (const s of failed) {
|
||||
if (view.events(FORWARD_NOT_RUN).some((e) => e.objects.includes(s.target!))) continue;
|
||||
return { do: "forwardNotRun", candidate: s.target!, reason: `第 ${n} 次核验仍不通过,修复次数上限 ${frozen.config.maxRepairs} 已用完:${JSON.stringify(verificationOutcome(s.content))};${s.content.body}` };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
candidates = failed.map((s) => s.target!);
|
||||
if (candidates.length === 0) return null;
|
||||
const repair = assignedStage(`repair-${n}`, round, "repair", candidates, view, frozen);
|
||||
if (repair !== null) return repair;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** ⑩:按选批顺序一次只交回一个运行;没启动的下次启动起新编号,不会把半批提前公开。 */
|
||||
function forwardNext(round: number, view: View): Next | null {
|
||||
for (const candidate of view.board.decision("batch", round)!.output.chosen) {
|
||||
if (view.events(FORWARD_NOT_RUN).some((e) => e.objects.includes(candidate))) continue;
|
||||
const runs = view.board.runs().filter((r) => r.plan.purpose === "forward" && r.plan.round === round && r.plan.object === candidate);
|
||||
const last = runs.at(-1);
|
||||
if (last !== undefined && last.result === null) throw new Error(`正向运行 ${last.plan.run} 登记了还没有结果:恢复入口应当已经核对过`);
|
||||
if (last !== undefined && last.result!.process.ending !== "notStarted") continue;
|
||||
return { do: "forwardRun", round, candidate, run: `r${round}-forward-${candidate}-${runs.length + 1}` };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按种类取记录内容的小工具,加上 materials 读账本的那一份(Board)。 */
|
||||
class View {
|
||||
readonly #records: readonly LedgerRecord[];
|
||||
|
||||
+10
-5
@@ -1,4 +1,5 @@
|
||||
import type { Content } from "../ledger/index.ts";
|
||||
import { Board, protocolMetrics } from "../materials/index.ts";
|
||||
import type { HypothesisMetric } from "../vocab/index.ts";
|
||||
import { finishRun } from "./formal-run.ts";
|
||||
import { EVENTS } from "./next-step.ts";
|
||||
@@ -36,13 +37,17 @@ export async function recover(s: Study): Promise<{ halt: string } | "stopped" |
|
||||
// 在跑或已结束:没写"确认启动"的补写(扣次),然后等它、判分、写结果。
|
||||
// 已经确认启动、容器却不见了,真实运行里没出现过,不专门写:runner.wait 会报错,按内部错误停下。
|
||||
if (!started) s.ledger.append("runStarted", { run });
|
||||
if ((await finishRun(s, run, metricsOf(plan.content))) === "stopped") return "stopped";
|
||||
if ((await finishRun(s, run, metricsOf(s, plan.content))) === "stopped") return "stopped";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 一次运行要测的假设专用指标。第 1 部分只有基线运行,它不测假设专用指标;正向、消融运行由第 3、4 部分补。 */
|
||||
function metricsOf(plan: Content<"runPlanned">): HypothesisMetric[] {
|
||||
if (plan.purpose !== "baseline") throw new Error(`运行 ${plan.run} 是 ${plan.purpose} 运行,flow 第 1 部分还不会恢复它`);
|
||||
return [];
|
||||
/** 恢复和第一次运行共用原登记指标定义(引用指标也保留原来的缺失规则)。 */
|
||||
function metricsOf(s: Study, plan: Content<"runPlanned">): HypothesisMetric[] {
|
||||
if (plan.purpose === "baseline") return [];
|
||||
if (plan.purpose === "forward") {
|
||||
const board = new Board(s.ledger.records());
|
||||
return protocolMetrics(board, board.protocol(plan.object!));
|
||||
}
|
||||
throw new Error(`运行 ${plan.run} 是消融运行,消融还没写(任务书 17)`);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { runModelTask, type ModelTask } from "../model-task.ts";
|
||||
import type { Next, TaskRef } from "../next-step.ts";
|
||||
import { halt, USER_STOPPED, type Study, type StudyEnding } from "../study.ts";
|
||||
|
||||
// 派一个研究者阶段里要派的任务(任务书 12 第 2.6 节、任务书 14 第 2.3 节):
|
||||
// 派一个阶段里要派的任务(任务书 12 第 2.6 节、任务书 14 第 2.3 节):
|
||||
// 同一个人的几个任务按先后跑,不同的人同时跑,各自一个会话。
|
||||
|
||||
/**
|
||||
@@ -40,26 +40,10 @@ export async function dispatchStage(s: Study, next: Extract<Next, { do: "dispatc
|
||||
return halt(s.ledger, `阶段 ${next.stageId} 有任务没交上,等这次派发的会话全部结束后停下:${missed.map((m) => `调用 ${m.call},${m.text}`).join(";")}`, missed.map((m) => m.call));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个任务交上来之后才知道的东西和分份编号(submissions 的 KnownOf、任务书 14 第 2.3 节):
|
||||
* 提案是本人本阶段已收下几条,按 proposalId 分份;挑刺按 critiqueId 分份;回应是作者那条原提案;其余没有。
|
||||
*/
|
||||
/** 一个任务的分份编号;实现、核验、修复都只交一份。 */
|
||||
function modelTask(s: Study, stageId: string, t: TaskRef): ModelTask<TaskRef["kind"]> {
|
||||
const base = { kind: t.kind, actor: t.actor, stageId, object: t.object };
|
||||
const board = new Board(s.ledger.records());
|
||||
switch (t.kind) {
|
||||
case "proposal": {
|
||||
const round = board.stage(stageId).round;
|
||||
return { ...base, item: (n) => proposalId(round, t.actor, n), known: (_, others) => ({ accepted: others }) } as ModelTask<"proposal">;
|
||||
}
|
||||
case "critique":
|
||||
return { ...base, item: (n) => critiqueId(t.object!, t.actor, n), known: () => null };
|
||||
case "response": {
|
||||
// 回应的候选只由一条原提案而来(几条合成的交统一)。
|
||||
const original = board.proposal(board.proposalsOf(t.object!)[0]!).content;
|
||||
return { ...base, item: null, known: () => ({ original }) } as ModelTask<"response">;
|
||||
}
|
||||
default:
|
||||
return { ...base, item: null, known: () => null };
|
||||
}
|
||||
const round = new Board(s.ledger.records()).stage(stageId).round;
|
||||
const item = t.kind === "proposal" ? (n: number) => proposalId(round, t.actor, n)
|
||||
: t.kind === "critique" ? (n: number) => critiqueId(t.object!, t.actor, n) : null;
|
||||
return { ...t, stageId, item };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Board, protocolMetrics, validScore } from "../../materials/index.ts";
|
||||
import { adopt } from "../../rules/index.ts";
|
||||
import type { Json } from "../../vocab/index.ts";
|
||||
import { formalRun, type RunOutcome } from "../formal-run.ts";
|
||||
import type { Study } from "../study.ts";
|
||||
|
||||
/** ⑩ 正向运行:用核验过的快照和原登记指标定义,复用正式运行的登记、扣次、判分入口。 */
|
||||
export function forwardRun(s: Study, round: number, candidate: string, run: string): Promise<RunOutcome> {
|
||||
const board = new Board(s.ledger.records());
|
||||
return formalRun(s, { run, purpose: "forward", round, object: candidate }, board.code(candidate).version, protocolMetrics(board, board.protocol(candidate)));
|
||||
}
|
||||
|
||||
/** ⑫ 采纳:只把本批有效实测分数交给 rules(adopt 内部用 adoptionLine);null 也写,整批运行据此公开。 */
|
||||
export function adoptBatch(s: Study, round: number): void {
|
||||
const board = new Board(s.ledger.records());
|
||||
const candidates = board.decision("batch", round)!.output.chosen.flatMap((id) => {
|
||||
const run = board.runs().filter((r) => r.plan.purpose === "forward" && r.plan.round === round && r.plan.object === id).at(-1);
|
||||
const score = run === undefined ? null : validScore(run.result!.judged);
|
||||
return score === null ? [] : [{ id, score }];
|
||||
});
|
||||
const input = {
|
||||
candidates,
|
||||
baseline: board.baselineScore(board.baselineOf(round)),
|
||||
direction: s.frozen.facts.finalMetric.direction,
|
||||
epsilon: s.frozen.facts.epsilon,
|
||||
knownBest: s.frozen.facts.knownBest,
|
||||
};
|
||||
const output = adopt(input);
|
||||
s.ledger.append("decision", {
|
||||
decision: "adopt", round, rule: "adopt", ruleVersion: 1,
|
||||
params: { direction: input.direction, epsilon: input.epsilon, knownBest: input.knownBest }, seed: null,
|
||||
input: input as Json, output: output as Json,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { join } from "node:path";
|
||||
import type { Content } from "../ledger/index.ts";
|
||||
import { codeHunks } from "../jev/index.ts";
|
||||
import { Board, protocolMetrics, type CodeChanges } from "../materials/index.ts";
|
||||
import type { KnownOf } from "../submissions/index.ts";
|
||||
import { lightCheck, protectedChanges, readLightCheck } from "../tasks/index.ts";
|
||||
import type { Json, Submission, SubmissionKind } from "../vocab/index.ts";
|
||||
import { compareVersions, saveVersion } from "../workspace/index.ts";
|
||||
import type { TaskRef } from "./next-step.ts";
|
||||
import { callPaths, containerName, freshDir, workspaceRepo } from "./paths.ts";
|
||||
import type { Study } from "./study.ts";
|
||||
|
||||
/** 实现取本批基线;修复取上次提交。核验、修复的材料差异始终相对本批基线。 */
|
||||
export function taskCode(s: Study, t: TaskRef & { stageId: string }): { changes: CodeChanges | null; startVersion: string | null } {
|
||||
if (t.kind !== "implementation" && t.kind !== "repair" && t.kind !== "verification") return { changes: null, startVersion: null };
|
||||
const board = new Board(s.ledger.records());
|
||||
const baseline = board.code(board.baselineOf(board.stage(t.stageId).round)).version;
|
||||
if (t.kind === "implementation") return { changes: null, startVersion: baseline };
|
||||
const version = board.code(t.object!).version;
|
||||
return { changes: compareVersions(workspaceRepo(s.dir), baseline, version), startVersion: t.kind === "repair" ? version : null };
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交时才知道的东西:提案份数、回应原提案,或实验员当前工作目录的快照和相对本批基线的改动。
|
||||
* 快照写入 workspace 后才问 Jev;收下才把 code 和 submission 在同一个事务写进账本。
|
||||
*/
|
||||
export function prepareSubmission(
|
||||
s: Study,
|
||||
t: TaskRef & { stageId: string },
|
||||
content: Submission<SubmissionKind>,
|
||||
others: number,
|
||||
workDir: string,
|
||||
): { known: KnownOf<SubmissionKind>; code: Content<"code"> | null } {
|
||||
const board = new Board(s.ledger.records());
|
||||
if (t.kind === "proposal") return { known: { accepted: others }, code: null };
|
||||
if (t.kind === "response") return { known: { original: board.proposal(board.proposalsOf(t.object!)[0]!).content }, code: null };
|
||||
if (t.kind !== "implementation" && t.kind !== "repair") return { known: null, code: null };
|
||||
const saved = saveVersion(workspaceRepo(s.dir), workDir, s.frozen.facts.unversionedNames);
|
||||
if (!saved.saved) return { known: { code: saved }, code: null };
|
||||
const baseline = board.code(board.baselineOf(board.stage(t.stageId).round)).version;
|
||||
const changes = compareVersions(workspaceRepo(s.dir), baseline, saved.version);
|
||||
const protocol = board.protocol(t.object!);
|
||||
const impl = content as Submission<"implementation">;
|
||||
return {
|
||||
known: { code: {
|
||||
saved: true,
|
||||
protectedFiles: protectedChanges(s.task, changes.files.map((f) => f.path)),
|
||||
hunks: codeHunks(changes),
|
||||
protocol: { body: protocol.body, metrics: protocolMetrics(board, protocol) },
|
||||
} },
|
||||
code: {
|
||||
object: t.object!, version: saved.version,
|
||||
locations: { factorLocations: impl.factorLocations, safeguardLocations: impl.safeguardLocations, unassigned: impl.unassigned } as Json,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 每次模型调用自己的计次器;会话里记录检查结果,不记正式运行、不扣次数(任务书 16 第 2.2 节)。 */
|
||||
export function lightChecker(s: Study, call: string, workDir: string): () => Promise<string> {
|
||||
let used = 0;
|
||||
const { maxCount, seconds } = s.frozen.config.lightChecks;
|
||||
return async () => {
|
||||
if (used >= maxCount) return `本次调用的轻量检查已用完(上限 ${maxCount} 次),不能再查。`;
|
||||
const n = ++used;
|
||||
const paths = callPaths(s.dir, call);
|
||||
const checkDir = freshDir(join(paths.base, `check-${n}`));
|
||||
const definition = lightCheck(s.task, workDir, checkDir, s.frozen.image, seconds);
|
||||
const name = containerName(s.frozen.config.studyId, `${call}-check-${n}`);
|
||||
const started = await s.runner.start(name, definition, checkDir);
|
||||
if (!started.started) throw new Error(`调用 ${call} 第 ${n} 次轻量检查没有启动:${started.reason}`);
|
||||
const result = await s.runner.wait(name, freshDir(join(paths.base, `check-${n}-logs`)));
|
||||
await s.runner.remove(name);
|
||||
return JSON.stringify({ used, maxCount, seconds, result: readLightCheck(s.task, checkDir, result) });
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChecklistOf } from "../vocab/index.ts";
|
||||
import { Draft, factorLines, KIND_TEXT, offText, registeredMetrics, taskPoints } from "./blocks.ts";
|
||||
import type { ChecklistOf, HypothesisMetric } from "../vocab/index.ts";
|
||||
import { type Draft, factorLines, KIND_TEXT, offText, registeredMetrics, taskPoints } from "./blocks.ts";
|
||||
import type { Board } from "./read.ts";
|
||||
import { fenced, json } from "./format.ts";
|
||||
import { unassignedChanges } from "./locations.ts";
|
||||
import { clauseKey, factorKey, splitCell, type FrozenProtocol, type LocationTable } from "./shapes.ts";
|
||||
@@ -27,9 +28,16 @@ function taskSection(d: Draft, role: string, what: string): void {
|
||||
}
|
||||
|
||||
/** 协议要采集的指标:新登记的全文,引用的取原登记(方法.md 第五部分"引用别人登记过的指标时,沿用原登记的选法")。 */
|
||||
export function protocolMetrics(board: Board, p: FrozenProtocol): HypothesisMetric[] {
|
||||
const registered = registeredMetrics(board);
|
||||
return [...p.metrics.defined, ...p.metrics.reused.map((id) => {
|
||||
const { candidate: _candidate, ...metric } = registered.find((x) => x.id === id)!;
|
||||
return metric;
|
||||
})];
|
||||
}
|
||||
|
||||
function metricSection(d: Draft, p: FrozenProtocol): void {
|
||||
const registered = registeredMetrics(d.board);
|
||||
const all = [...p.metrics.defined, ...p.metrics.reused.map((id) => registered.find((x) => x.id === id)!)];
|
||||
const all = protocolMetrics(d.board, p);
|
||||
d.section("要实现的指标定义", [
|
||||
all.length === 0 ? "(这份协议没有要实现的假设专用指标)" : json(all),
|
||||
"",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// materials:每个任务能看到哪些材料、第一条消息怎么写、查询在材料范围内回答什么(docs/结构.md 第二部分),
|
||||
// 以及账本里只存成 Json 的几样内容的形状和编号规则(用户 2026-10-03 决定 2)。
|
||||
export { protocolMetrics } from "./assigned.ts";
|
||||
export { beliefBook } from "./belief-book.ts";
|
||||
export { finalEntries, type MaterialRecord, type MaterialSection, type ReadableVersion, type TaskMaterial } from "./blocks.ts";
|
||||
export { buildMaterial } from "./build.ts";
|
||||
export { type CodeChanges, type PromptFile, type Prompts, type StudyFacts, type TaskRequest } from "./inputs.ts";
|
||||
export type { CodeChanges, PromptFile, Prompts, StudyFacts, TaskRequest } from "./inputs.ts";
|
||||
export { queryMaterial, readPrompts } from "./prompts.ts";
|
||||
// flow 推进时读账本也用这一份(本批基线、基线分数、留池候选、各候选合并了哪些原提案),不另写一份。
|
||||
export { Board } from "./read.ts";
|
||||
export { Board, validScore } from "./read.ts";
|
||||
export {
|
||||
callId,
|
||||
candidateId,
|
||||
@@ -17,6 +18,10 @@ export {
|
||||
critiqueId,
|
||||
DECISION_KINDS,
|
||||
entryId,
|
||||
experimenterId,
|
||||
verifierId,
|
||||
FORWARD_NOT_RUN,
|
||||
forwardNotRunEvent,
|
||||
explanationId,
|
||||
factorId,
|
||||
factorKey,
|
||||
|
||||
@@ -25,6 +25,9 @@ export const ORIGINAL_BASELINE = "base";
|
||||
|
||||
/** 研究者:第 i 位(从 0 数)是 researcher-(i+1),与参考项目的写法相同,回放时不用改编号。 */
|
||||
export const researcherId = (i: number): string => `researcher-${i + 1}`;
|
||||
/** 每个候选固定一位实验员和核验员,修复、再次核验沿用编号。 */
|
||||
export const experimenterId = (candidate: string): string => `experimenter-${candidate}`;
|
||||
export const verifierId = (candidate: string): string => `verifier-${candidate}`;
|
||||
/** 入口:作者在第零轮交的第 n 个入口,讨论时整份替换的新清单接着往后编(方法.md 4.2)。 */
|
||||
export const entryId = (author: string, n: number): string => `e-${author}-${n}`;
|
||||
/** 提案:第 round 轮作者交的第 n 条,也是提交记录的 item。 */
|
||||
@@ -145,3 +148,7 @@ export type Settlement = {
|
||||
*/
|
||||
export const CELL_NOT_RUN = "cellNotRun";
|
||||
export const cellNotRunEvent = (cell: string, reason: string): Content<"event"> => ({ event: CELL_NOT_RUN, reason, objects: [cell] });
|
||||
|
||||
/** 正向候选核验不通过、修复次数用完而不跑,不登记正式运行、不扣次(方法.md 4.3 ⑨)。 */
|
||||
export const FORWARD_NOT_RUN = "forwardNotRun";
|
||||
export const forwardNotRunEvent = (candidate: string, reason: string): Content<"event"> => ({ event: FORWARD_NOT_RUN, reason, objects: [candidate] });
|
||||
|
||||
Reference in New Issue
Block a user