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> = { kind: K; actor: string; stageId: string; object: string | null; known(content: Submission): KnownOf; }; /** 派一次任务,等会话结束,交回调用编号和结局。 */ export async function runModelTask>(s: Study, t: ModelTask): 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), 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>(s: Study, t: ModelTask, material: TaskMaterial, attempt: SubmitAttempt): Promise { 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"}`;