dsh-vibe-math 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +41 -2
- package/README.md +41 -2
- package/cordis.patch.yml +22 -8
- package/docs/AUDIT-CHECKLIST.md +2 -2
- package/docs/final-paper.md +171 -0
- package/docs/release-notes/RELEASE-NOTES-2.6.0.md +268 -0
- package/docs/test-timing.md +8 -4
- package/package.json +5 -3
- package/prompt-corpus-persona/persona-corpus.json +13 -13
- package/prompt-corpus-persona/persona-corpus.md +49 -21
- package/prompt-corpus-v2/formal-verify-v2.json +12 -0
- package/prompt-corpus-v2/formal-verify-v2.md +144 -53
- package/prompt-corpus-v3/formal-verify-v3.json +36 -0
- package/prompt-corpus-v3/formal-verify-v3.md +405 -78
- package/tests/audit-persona-sensitivity.mjs +60 -7
- package/tests/audit-persona-surface.test.mjs +16 -7
- package/tests/e2e-v5-round2.test.mjs +471 -1
- package/tests/formal-verify-v2.test.mjs +3 -0
- package/tests/formal-verify-v3.test.mjs +4 -0
- package/tests/prompt-v5-integrity.test.mjs +9 -3
- package/tests/selfdrive-v5.mjs +55 -4
- package/vibe-math-v2/agent.cordis.yml +14 -4
- package/vibe-math-v2/vibe-math-v2.js +819 -11
- package/vibe-math-v3/agent.cordis.yml +10 -4
- package/vibe-math-v3/vibe-math-v3.js +800 -11
- package/vibe-math-v4/agent.cordis.yml +12 -4
- package/vibe-math-v4/vibe-math-v4.js +793 -8
- package/vibe-math-v5/agent.cordis.yml +8 -4
- package/vibe-math-v5/vibe-math-v5.js +1116 -8
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +70 -1
|
@@ -179,6 +179,12 @@ export function apply(ctx) {
|
|
|
179
179
|
leanCommand: 'lean', // 要执行的 Lean 可执行文件(例:'lake')
|
|
180
180
|
leanArgs: [], // 插在文件名之前的附加参数(例:['env','lean'] 配 leanCommand='lake')
|
|
181
181
|
leanTimeoutMs: 120000, // 单次 Lean 运行超时上限(毫秒,正整数)
|
|
182
|
+
// ---- 最终论文(规格:docs/final-paper.md;v2/v3 为单作者变体)----
|
|
183
|
+
finalPaper: true, // 收口时是否自动撰写最终论文(false 只关自动触发;/vibe paper 仍可用)
|
|
184
|
+
paperFormat: 'both', // both = md + tex | md = 只写 markdown | tex = 只写 latex
|
|
185
|
+
paperLanguage: 'zh', // zh = 中文(ctexart/xelatex 优先)| en = 英文(article/pdflatex 优先)
|
|
186
|
+
paperCompilePdf: true, // 检测到 LaTeX 时是否编译 paper.pdf
|
|
187
|
+
paperLatexCommand: '', // 指定 LaTeX 引擎(空 = 按语言探测 xelatex/latexmk/pdflatex/lualatex/tectonic)
|
|
182
188
|
}
|
|
183
189
|
let params = Object.assign({}, DEFAULT_PARAMS)
|
|
184
190
|
let scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null } // activeCount 由 activeCount() 从 agentRegistry 推导(防漂移,同 v2)
|
|
@@ -299,6 +305,11 @@ export function apply(ctx) {
|
|
|
299
305
|
{ name: 'leanCommand', type: 'string', description: 'Lean 可执行文件(例:lean / lake;配合 leanArgs=[env,lean] 用 lake)', suggestion: 'lean' },
|
|
300
306
|
{ name: 'leanArgs', type: 'string[]', description: '插在 .lean 文件名之前的附加参数', suggestion: [] },
|
|
301
307
|
{ name: 'leanTimeoutMs', type: 'integer', description: '单次 Lean 运行的超时上限(毫秒,非正数回退默认)', suggestion: 120000 },
|
|
308
|
+
{ name: 'finalPaper', type: 'boolean', description: '收口(checkTermination 的完整收口分支:无未解决问题、无待验证对象、无任务/计划/门)时自动撰写最终论文:派遣一名专职「论文撰写」子代理,把已定论命题/解法/方法整理成 Paper/<项目>/{paper.md,paper.tex,paper.meta.json,paper.log.md}。false = 只关自动触发,/vibe paper 手动命令仍可用', suggestion: true },
|
|
309
|
+
{ name: 'paperFormat', type: 'enum', options: ['both', 'md', 'tex'], description: '论文产出格式:both = markdown + latex;md = 只写 paper.md(跳过编译);tex = 只写 paper.tex(tex 才会尝试编译 pdf)', suggestion: 'both' },
|
|
310
|
+
{ name: 'paperLanguage', type: 'enum', options: ['zh', 'en'], description: '论文语言:zh = 中文(LaTeX 用 ctexart,引擎优先 xelatex);en = 英文(article,引擎优先 pdflatex/latexmk)', suggestion: 'zh' },
|
|
311
|
+
{ name: 'paperCompilePdf', type: 'boolean', description: '检测到 LaTeX 时是否编译 paper.pdf(-interaction=nonstopmode 跑两遍;失败先尝试修复:换引擎/去不支持宏包/最小模板)。false 或无 LaTeX 时只保留 tex+md 并记日志(不阻塞定稿)', suggestion: true },
|
|
312
|
+
{ name: 'paperLatexCommand', type: 'string', description: '指定 LaTeX 引擎可执行文件(空 = 按语言探测:中文 xelatex > latexmk > pdflatex > lualatex > tectonic;英文 pdflatex 优先)。解析不到时按"未检测到"降级', suggestion: '' },
|
|
302
313
|
]
|
|
303
314
|
|
|
304
315
|
// ================= fs (adapted to DSH 0.1.1: resolve returns {targetKey, displayPath}) =================
|
|
@@ -389,7 +400,7 @@ export function apply(ctx) {
|
|
|
389
400
|
const intFields = ['maxParallelThreshold', 'solverMaxRounds', 'directionsPerSolver', 'verifierCount', 'debateMaxRounds', 'solverMaxToolCalls', 'verifierMaxToolCalls', 'reportIntervalMs', 'tickIntervalMs', 'activityLogCap', 'maxExplorerRetries', 'planningHorizon', 'planMinIntervalMs', 'plannerMaxFails', 'methodKeepIntervalMs', 'methodKeepEvery', 'projectLockTimeoutMs']
|
|
390
401
|
const numFields = ['promoteValueThreshold']
|
|
391
402
|
const arrayFields = ['solverToolAllow', 'solverToolDeny', 'verifierToolAllow', 'verifierToolDeny', 'leanArgs']
|
|
392
|
-
const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild']
|
|
403
|
+
const boolFields = ['plannerEnabled', 'methodAutoPromote', 'indexAutoRebuild', 'finalPaper', 'paperCompilePdf']
|
|
393
404
|
for (const k of Object.keys(DEFAULT_PARAMS)) {
|
|
394
405
|
if (!(k in obj)) continue
|
|
395
406
|
const v = obj[k]
|
|
@@ -408,6 +419,10 @@ export function apply(ctx) {
|
|
|
408
419
|
else if (k === 'leanCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s || 'lean' }
|
|
409
420
|
else if (k === 'leanTimeoutMs') { const n = Number(v); out[k] = (Number.isFinite(n) && n > 0) ? Math.floor(n) : DEFAULT_PARAMS[k] }
|
|
410
421
|
else if (k === 'solverAllowNetwork' || k === 'verifierAllowNetwork' || k === 'solverAllowScripts' || k === 'verifierAllowScripts') { out[k] = (v === true || v === false || v === '') ? v : DEFAULT_PARAMS[k] }
|
|
422
|
+
// 最终论文(spec v2 §B):布尔/枚举必须**显式归一化**,不允许未知键直通('false' 会保持真值)。
|
|
423
|
+
else if (k === 'paperFormat') { out[k] = (v === 'both' || v === 'md' || v === 'tex') ? v : DEFAULT_PARAMS[k] }
|
|
424
|
+
else if (k === 'paperLanguage') { out[k] = (v === 'zh' || v === 'en') ? v : DEFAULT_PARAMS[k] }
|
|
425
|
+
else if (k === 'paperLatexCommand') { const s = String(v == null ? '' : v).trim(); out[k] = s }
|
|
411
426
|
else { out[k] = v }
|
|
412
427
|
}
|
|
413
428
|
return out
|
|
@@ -3289,13 +3304,493 @@ export function apply(ctx) {
|
|
|
3289
3304
|
return best
|
|
3290
3305
|
}
|
|
3291
3306
|
|
|
3307
|
+
// ==========================================================================================
|
|
3308
|
+
// 最终论文(规格:docs/final-paper.md)—— 会话侧(v3 数据来源:md 知识库)。
|
|
3309
|
+
//
|
|
3310
|
+
// 时序(修订 §A2/§A3):v3 的收口信号是 checkTermination() 的完整收口分支(无未解决问题、
|
|
3311
|
+
// `leftoverVerify` 为空——**这才是真实完整性判据**(§A4)、无真实在途代理、无任务/计划/挂起门)。
|
|
3312
|
+
// 撰写子代理必须在 `scheduler.running=false` 与 `releaseProjectLock()` **之前**派遣
|
|
3313
|
+
// (停止后 scheduleTick() 是空操作;提前释放项目锁后另一会话可能写同一棵树)。派不出去
|
|
3314
|
+
// (宿主激活上限 ACTIVATION_LIMIT_REACHED)→ 排队 + apply 级心跳重试 + 可见告警。
|
|
3315
|
+
// 落盘(§C/§D):md/tex 是文本;**paper.pdf 只能由编译器子进程生成**、插件只 stat 且**永不删除/覆盖**;
|
|
3316
|
+
// paper.meta.json 最后写(原子提交点);幂等 = run id + finalizedAt + 逐产物存在性(不用稳定哈希)。
|
|
3317
|
+
// ==========================================================================================
|
|
3318
|
+
let paperInFlight = '' // 正在撰写的 childId
|
|
3319
|
+
let paperInFlightAt = 0 // 派遣时刻(判定卡死窗口;force 与心跳都据它降级)
|
|
3320
|
+
let paperReaps = 0 // 本 run 内已回收卡死撰写者的次数(自动重派上限)
|
|
3321
|
+
const paperAbandoned = {} // childId -> true:已放弃的撰写者;它事后返回时输出被丢弃并留痕
|
|
3322
|
+
let paperPending = null // 激活上限排队重试:{ tries, retryAt, reason, limit, trigger, opts }
|
|
3323
|
+
const PAPER_RETRY_MS = 5000
|
|
3324
|
+
const PAPER_MAX_RETRIES = 12
|
|
3325
|
+
const PAPER_STALE_MS = 10 * 60 * 1000 // 撰写者卡死窗口:超过它,force 与心跳都会回收并重派(10 分钟)
|
|
3326
|
+
const PAPER_MAX_AUTO_REAPS = 2 // 心跳自动回收+重派的次数上限,之后交给 /vibe paper force
|
|
3327
|
+
const PAPER_LOCK_STALE_MS = 120000
|
|
3328
|
+
function activationLimitFrom(message) { const m = /active child limit:\s*(\d+)/.exec(String(message == null ? '' : message)); return m ? Number(m[1]) : undefined }
|
|
3329
|
+
function paperId(opts) { return paperDirId((opts && opts.id) || currentProject) }
|
|
3330
|
+
function paperDir(id) { return 'Paper/' + paperDirId(id) }
|
|
3331
|
+
function paperAbsDir(id) { return frameworkRoot() + '/' + paperDir(id) }
|
|
3332
|
+
function paperAuthorLine() { return 'Vibe Math V3(单作者:论文撰写子代理)· 项目 ' + currentProject }
|
|
3333
|
+
function paperOpts(opts) {
|
|
3334
|
+
const o = opts || {}
|
|
3335
|
+
const fmt = (o.format === 'md' || o.format === 'tex' || o.format === 'both') ? o.format : ((params.paperFormat === 'md' || params.paperFormat === 'tex') ? params.paperFormat : 'both')
|
|
3336
|
+
const lang = (o.lang === 'en' || o.lang === 'zh') ? o.lang : (params.paperLanguage === 'en' ? 'en' : 'zh')
|
|
3337
|
+
const compilePdf = (o.compilePdf === true || o.compilePdf === false) ? o.compilePdf : (params.paperCompilePdf !== false)
|
|
3338
|
+
return { format: fmt, lang: lang, compilePdf: compilePdf, force: o.force === true, id: o.id }
|
|
3339
|
+
}
|
|
3340
|
+
async function paperReadMeta(id) { const m = await readJson(paperDir(id) + '/paper.meta.json'); return (m && typeof m === 'object' && !Array.isArray(m)) ? m : undefined }
|
|
3341
|
+
async function paperPathExists(rel) { try { const t = await fsTarget(rel); return (await fs.stat(t)) !== undefined } catch (e) { return false } }
|
|
3342
|
+
async function paperAppendLog(id, tag, text) {
|
|
3343
|
+
const rel = paperDir(id) + '/paper.log.md'
|
|
3344
|
+
const prev = await readText(rel)
|
|
3345
|
+
const head = (prev === undefined || !String(prev).trim()) ? (paperLogHead(id).join('\n') + '\n') : String(prev)
|
|
3346
|
+
await writeText(rel, head + paperLogLine(now(), tag, text) + '\n')
|
|
3347
|
+
}
|
|
3348
|
+
/** paper 作用域锁(修订 §A3):v3 在收口时已释放项目锁,用它防止两个会话同时写同一棵 Paper/ 树。 */
|
|
3349
|
+
async function paperLockAcquire(id) {
|
|
3350
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
3351
|
+
const cur = await readJson(rel)
|
|
3352
|
+
if (cur && cur.sessionId && cur.sessionId !== sessionId && (now() - Number(cur.at || 0)) < PAPER_LOCK_STALE_MS) return { ok: false, holder: String(cur.sessionId), at: Number(cur.at || 0) }
|
|
3353
|
+
await writeJson(rel, { sessionId: sessionId, at: now(), project: currentProject })
|
|
3354
|
+
return { ok: true }
|
|
3355
|
+
}
|
|
3356
|
+
async function paperLockRelease(id) {
|
|
3357
|
+
const rel = paperDir(id) + '/paper.lock.json'
|
|
3358
|
+
const cur = await readJson(rel)
|
|
3359
|
+
if (cur && cur.sessionId === sessionId) await writeJson(rel, { sessionId: '', at: now(), released: true })
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* 回收卡死的撰写者(修订 §1:死掉的撰写者绝不能把论文永久卡住)。清状态 + 尽力中断 +
|
|
3363
|
+
* 从 agentRegistry 摘除(否则一直被算作活跃子代理)+ 记进 paperAbandoned(事后返回的输出被明确
|
|
3364
|
+
* 丢弃并留痕)+ 可见告警。
|
|
3365
|
+
*/
|
|
3366
|
+
async function paperReapWriter(id, why) {
|
|
3367
|
+
const cid = paperInFlight
|
|
3368
|
+
paperInFlight = ''; paperInFlightAt = 0
|
|
3369
|
+
if (cid) {
|
|
3370
|
+
try { await interruptChild(cid) } catch (e) { /* best effort */ }
|
|
3371
|
+
delete agentRegistry[cid]
|
|
3372
|
+
paperAbandoned[cid] = true
|
|
3373
|
+
}
|
|
3374
|
+
await paperAppendLog(id, 'reap', '回收卡死的撰写者 ' + (cid || '(未知)') + ':' + why + '(论文不会被永久卡住;force/心跳会继续)')
|
|
3375
|
+
logActivity('paper', '回收卡死的论文撰写子代理 ' + (cid || '(未知)') + ':' + why)
|
|
3376
|
+
console.error('vibe-math-v3: reaped a stalled paper writer (' + (cid || 'unknown') + '): ' + why)
|
|
3377
|
+
reportDirty = true
|
|
3378
|
+
return cid
|
|
3379
|
+
}
|
|
3380
|
+
function paperWriterVerdictNow(force) {
|
|
3381
|
+
return paperWriterVerdict({ inFlight: !!paperInFlight, force: !!force, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, childInRegistry: paperInFlight ? (agentRegistry[paperInFlight] !== undefined) : true })
|
|
3382
|
+
}
|
|
3383
|
+
function paperOneLine(s, cap) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, cap || 400) }
|
|
3384
|
+
/** 证据索引(spec §3.9):只列**确实存在**的文件;排除 Paper/ 自己(避免自我污染)。
|
|
3385
|
+
* formalVerify=off 必须真的是无操作(formal-verify-v3 的语料守卫断言"off 的提示词里零 Lean 文本"),
|
|
3386
|
+
* 所以 off 档不把形式化路径写进材料。 */
|
|
3387
|
+
async function paperEvidenceIndex() {
|
|
3388
|
+
const formalNow = formalOn()
|
|
3389
|
+
const out = []
|
|
3390
|
+
if (formalNow && await paperPathExists('State/formal.json')) out.push('State/formal.json')
|
|
3391
|
+
if (await paperPathExists('State/index.json')) out.push('State/index.json')
|
|
3392
|
+
for (const p of allProblems()) out.push(problemRel(p))
|
|
3393
|
+
for (const p of allPropos()) out.push(propositionRel(p))
|
|
3394
|
+
for (const m of methods.values()) out.push(methodRel(m, false))
|
|
3395
|
+
const dirs = ['Verified/命题', 'Verified/问题', 'Logs/Verification', 'Logs/Plans', 'Progress'].concat(formalNow ? ['Verified/Lean', 'Formal'] : [])
|
|
3396
|
+
for (let i = 0; i < dirs.length; i++) { const files = await listFiles(dirs[i]); for (let j = 0; j < files.length; j++) out.push(dirs[i] + '/' + files[j]) }
|
|
3397
|
+
const seen = {}, uniq = []
|
|
3398
|
+
for (let i = 0; i < out.length; i++) { const x = String(out[i]); if (seen[x] || x.indexOf('Paper/') === 0) continue; seen[x] = true; uniq.push(x) }
|
|
3399
|
+
return uniq.sort()
|
|
3400
|
+
}
|
|
3401
|
+
/** 汇总材料(spec §4 + 修订 §A4):只含既有证据;未决项(含 leftoverVerify 的待验证对象)显式标注。 */
|
|
3402
|
+
async function buildPaperDigest() {
|
|
3403
|
+
const L = []
|
|
3404
|
+
L.push('PRESET: vibe-math-v3 (single-author)')
|
|
3405
|
+
L.push('PROJECT: ' + currentProject)
|
|
3406
|
+
const qs = allProblems()
|
|
3407
|
+
L.push('')
|
|
3408
|
+
L.push('[ORIGINAL PROBLEMS] (Problems/*.md)')
|
|
3409
|
+
if (qs.length === 0) L.push('- (none)')
|
|
3410
|
+
for (let i = 0; i < qs.length; i++) {
|
|
3411
|
+
const q = qs[i]
|
|
3412
|
+
L.push('- id=' + q.id + ' | 状态=' + q.状态 + ' | 优先级=' + q.优先级 + ' | 依赖=' + JSON.stringify(q.依赖 || []) + ' | 来源=' + (q.来源 || '原始') + ' | 陈述=' + paperOneLine(q.陈述, 800))
|
|
3413
|
+
const dirs = getDirState(q.id)
|
|
3414
|
+
if (dirs.length) L.push(' · 方向:' + dirs.map(function (d) { return d.id + ':' + d.status }).join(', '))
|
|
3415
|
+
const sols = q.solutions || []
|
|
3416
|
+
if (sols.length === 0) L.push(' · 解法:无')
|
|
3417
|
+
for (let j = 0; j < sols.length; j++) L.push(' · 解法#' + j + ' 概率=' + sols[j].prob + ' 状态=' + (sols[j].status || '') + ' 证据=Verified/问题/ 与 Logs/Verification/ | ' + paperOneLine(sols[j].text, 1200))
|
|
3418
|
+
}
|
|
3419
|
+
const ps = allPropos()
|
|
3420
|
+
L.push('')
|
|
3421
|
+
L.push('[PROPOSITIONS] (Propos/<分类>/<id>.md)')
|
|
3422
|
+
if (ps.length === 0) L.push('- (none)')
|
|
3423
|
+
for (let i = 0; i < ps.length; i++) {
|
|
3424
|
+
const p = ps[i]
|
|
3425
|
+
const rec = formalOn() ? formalOf(p.id) : null // off 档不把形式化字段写进材料(off 是真无操作)
|
|
3426
|
+
L.push('- id=' + p.id + ' | 概率=' + p.概率 + ' | 状态=' + p.状态 + ' | 优先级=' + p.优先级 + ' | 价值/关键性=' + p.价值关键性 + ' | 来源问题=' + (p.来源问题 || '') + (rec ? (' | 形式化=' + ((rec && rec.status) || 'none') + (rec && rec.proof ? ('(' + rec.proof + ')') : '')) : '') + ' | 陈述=' + paperOneLine(p.陈述, 800))
|
|
3427
|
+
const sides = [['证明', p.proofs || []], ['证伪', p.refutes || []]]
|
|
3428
|
+
for (let k = 0; k < sides.length; k++) { const arr = sides[k][1]; for (let j = 0; j < arr.length; j++) if (arr[j] && arr[j].prob === 1) L.push(' · 已检验通过:' + sides[k][0] + '#' + j + ' | ' + paperOneLine(arr[j].text, 900)) }
|
|
3429
|
+
}
|
|
3430
|
+
L.push('')
|
|
3431
|
+
L.push('[METHODS / ARTEFACTS] (Methods/ + 全局 VibeMath/Methods/)')
|
|
3432
|
+
const ms = Array.from(methods.values()).concat(Array.from(globalMethods.values()))
|
|
3433
|
+
if (ms.length === 0) L.push('- (none)')
|
|
3434
|
+
for (let i = 0; i < ms.length; i++) {
|
|
3435
|
+
const m = ms[i]
|
|
3436
|
+
L.push('- id=' + m.id + ' | 标题=' + m.标题 + ' | 类型=' + m.类型 + ' | 状态=' + m.状态 + ' | 可信断言=' + JSON.stringify(m.可信断言 || []) + ' | 应用次数=' + ((m.applications || []).length) + ' | 核心内容=' + paperOneLine(m.核心内容, 900))
|
|
3437
|
+
}
|
|
3438
|
+
// §A4:v3 的真实完整性判据是 leftoverVerify(buildVerifyCandidates)——把仍未验证的对象列出来。
|
|
3439
|
+
let cands = []
|
|
3440
|
+
try { cands = await buildVerifyCandidates() } catch (e) { /* 尽力而为 */ }
|
|
3441
|
+
L.push('')
|
|
3442
|
+
L.push('[STILL UNVERIFIED — v3 的完整性判据 leftoverVerify(buildVerifyCandidates);必须标注为未决]')
|
|
3443
|
+
if (cands.length === 0) L.push('- (none — 没有待验证对象)')
|
|
3444
|
+
for (let i = 0; i < cands.length; i++) L.push('- 未决:' + cands[i].rId + '(' + cands[i].kind + ',' + paperOneLine(cands[i].概述, 200) + (cands[i].prob === undefined ? '' : (',prob=' + cands[i].prob)) + ')')
|
|
3445
|
+
L.push('')
|
|
3446
|
+
L.push('[UNRESOLVED / REFUTED — 论文里必须显式标注,不得当成已成立的结论]')
|
|
3447
|
+
let any = false
|
|
3448
|
+
for (let i = 0; i < ps.length; i++) {
|
|
3449
|
+
if (!(ps[i].概率 === 1 || ps[i].概率 === 0)) { any = true; L.push('- 未定论:命题 ' + ps[i].id + '(概率=' + ps[i].概率 + ')') }
|
|
3450
|
+
else if (ps[i].概率 === 0) L.push('- 已被否证:命题 ' + ps[i].id)
|
|
3451
|
+
}
|
|
3452
|
+
for (let i = 0; i < qs.length; i++) {
|
|
3453
|
+
const sols = qs[i].solutions || []
|
|
3454
|
+
for (let j = 0; j < sols.length; j++) if (!(sols[j].prob === 1 || sols[j].prob === 0)) { any = true; L.push('- 未定论:问题 ' + qs[i].id + ' 的解法#' + j + '(概率=' + sols[j].prob + ')') }
|
|
3455
|
+
}
|
|
3456
|
+
if (!any && cands.length === 0) L.push('- (none — 所有对象均已定论)')
|
|
3457
|
+
L.push('')
|
|
3458
|
+
L.push('[EVIDENCE INDEX] (only files that exist)')
|
|
3459
|
+
const ev = await paperEvidenceIndex()
|
|
3460
|
+
for (let i = 0; i < ev.length; i++) L.push('- ' + ev[i])
|
|
3461
|
+
return L.join('\n')
|
|
3462
|
+
}
|
|
3463
|
+
/** 「论文撰写」子代理提示词(单作者;与 v2 同一形状/同一骨架)。 */
|
|
3464
|
+
function paperWriterPrompt(digest, o) {
|
|
3465
|
+
const langName = o.lang === 'en' ? 'English' : '中文(Chinese)'
|
|
3466
|
+
return 'You are the DEDICATED PAPER WRITER (single-author mode) of a math research run that has just CONVERGED.\n' +
|
|
3467
|
+
'Write its final paper in ' + langName + ', using ONLY the evidence in the MATERIAL section below.\n\n' +
|
|
3468
|
+
'HARD RULES:\n' +
|
|
3469
|
+
'- NEVER invent content: no new proposition, no new computation, no citation that is not in the MATERIAL.\n' +
|
|
3470
|
+
'- Unresolved or refuted items MUST be explicitly labelled (「未定论」/「已被否证」, or "unresolved"/"refuted" in English); never present them as established.\n' +
|
|
3471
|
+
'- Fixed 9-section skeleton — provide bodies for these EXACT `## ` headings (the framework writes the headings, author/date and the evidence index itself):\n' +
|
|
3472
|
+
PAPER_SKELETON.map(function (s, i) { return ' ' + (i + 1) + '. ' + s.key + ' — ' + s.spec }).join('\n') + '\n' +
|
|
3473
|
+
'- A section with no evidence must be exactly 「' + PAPER_NO_EVIDENCE + '」 (do not pad it).\n' +
|
|
3474
|
+
'- Markdown subset only: `#`/`##`/`###`, `- ` lists, `**bold**`, `*em*`, `` `code` ``, and inline math as `$...$`. No tables, images, footnotes or raw HTML.\n\n' +
|
|
3475
|
+
'OUTPUT CONTRACT — respond with ONLY one ```json code fence, no prose:\n' +
|
|
3476
|
+
'{"title":"<paper title>","abstract":"<original problem + main results>","sections":[{"name":"<one of the 9 headings>","body":"<markdown>"}, ...]}\n\n' +
|
|
3477
|
+
'MATERIAL (evidence only — do not add anything beyond it):\n' + digest
|
|
3478
|
+
}
|
|
3479
|
+
/** LaTeX 检测(spec §5 + 修订 §D):paperLatexCommand 非空时只用它;否则按语言顺序探测。 */
|
|
3480
|
+
async function paperDetectLatex(lang) {
|
|
3481
|
+
const sub = subprocessOf()
|
|
3482
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { available: [], reason: 'no-subprocess' }
|
|
3483
|
+
if (typeof sub.resolveExecutable !== 'function') return { available: [], reason: 'no-resolveExecutable' }
|
|
3484
|
+
const forced = String(params.paperLatexCommand || '').trim()
|
|
3485
|
+
if (forced) {
|
|
3486
|
+
try { const p = await sub.resolveExecutable(forced); return p ? { available: [{ name: forced, path: String(p) }], reason: 'ok', forced: true } : { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
3487
|
+
catch (e) { return { available: [], reason: 'paperLatexCommand not found: ' + forced, forced: true } }
|
|
3488
|
+
}
|
|
3489
|
+
const order = lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH
|
|
3490
|
+
const available = []
|
|
3491
|
+
for (let i = 0; i < order.length; i++) {
|
|
3492
|
+
try { const p = await sub.resolveExecutable(order[i]); if (p) available.push({ name: order[i], path: String(p) }) } catch (e) { /* 未安装 */ }
|
|
3493
|
+
}
|
|
3494
|
+
return { available: available, reason: available.length ? 'ok' : 'none' }
|
|
3495
|
+
}
|
|
3496
|
+
function paperArgvFor(engine) {
|
|
3497
|
+
if (engine.name === 'latexmk') return [engine.path, '-pdf', '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
3498
|
+
if (engine.name === 'tectonic') return [engine.path, '--keep-logs', 'paper.tex']
|
|
3499
|
+
return [engine.path, '-interaction=nonstopmode', '-halt-on-error', 'paper.tex']
|
|
3500
|
+
}
|
|
3501
|
+
/** 一次编译(nonstopmode),带主动超时 terminate。 */
|
|
3502
|
+
async function paperRunOnce(engine, dirAbs, capMs) {
|
|
3503
|
+
const sub = subprocessOf()
|
|
3504
|
+
if (sub === undefined || typeof sub.spawn !== 'function') return { ok: false, exitCode: null, code: 'NO_SUBPROCESS' }
|
|
3505
|
+
let handle
|
|
3506
|
+
try { handle = sub.spawn({ argv: paperArgvFor(engine), cwd: dirAbs, stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: { maxBytes: 64 * 1024 } }, graceMs: capMs }) }
|
|
3507
|
+
catch (e) { return { ok: false, exitCode: null, code: 'SPAWN_FAILED', message: String((e && e.message) || e) } }
|
|
3508
|
+
let timedOut = false, timer = null, outcome
|
|
3509
|
+
try {
|
|
3510
|
+
outcome = await Promise.race([
|
|
3511
|
+
handle.done,
|
|
3512
|
+
new Promise(function (resolve) { timer = setTimeout(function () { timedOut = true; try { if (handle && typeof handle.terminate === 'function') handle.terminate() } catch (e) { /* best effort */ } resolve({ exitCode: null }) }, capMs) }),
|
|
3513
|
+
])
|
|
3514
|
+
} catch (e) { if (timer !== null) clearTimeout(timer); return { ok: false, exitCode: null, code: 'RUN_FAILED', message: String((e && e.message) || e) } }
|
|
3515
|
+
if (timer !== null) clearTimeout(timer)
|
|
3516
|
+
let err = ''
|
|
3517
|
+
try { if (handle.collected && handle.collected.stderr) err = String(handle.collected.stderr.readFrom(0).text || '') } catch (e) { /* best effort */ }
|
|
3518
|
+
return { ok: !timedOut && !!outcome && outcome.exitCode === 0, exitCode: outcome ? outcome.exitCode : null, timedOut: timedOut, stderr: err.slice(-1200) }
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* 编译(spec §5 + 修订 §D/§E):主引擎(nonstopmode,两遍)→ 换引擎 → 去不支持宏包 → 最小模板再试一次;
|
|
3522
|
+
* 然后报告并降级。**永不删除/覆盖已有 pdf**:成功判据 = 本次运行 exit 0 **且** pdf 存在。
|
|
3523
|
+
*/
|
|
3524
|
+
async function paperCompile(id, o, texPrimary, bodyTex) {
|
|
3525
|
+
const relDir = paperDir(id), absDir = paperAbsDir(id)
|
|
3526
|
+
const attempts = []
|
|
3527
|
+
if (!o.compilePdf) return { compile: 'skipped', reason: 'paperCompilePdf=false', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
3528
|
+
if (o.format === 'md') return { compile: 'skipped', reason: 'paperFormat=md(不产出 tex,跳过编译)', engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
3529
|
+
const det = await paperDetectLatex(o.lang)
|
|
3530
|
+
if (det.available.length === 0) {
|
|
3531
|
+
await paperAppendLog(id, 'latex', '未检测到任何 LaTeX 引擎(' + det.reason + ')→ 只保留 paper.tex + paper.md,不编译 pdf(spec §5:不阻塞定稿)')
|
|
3532
|
+
return { compile: 'not-detected', reason: det.reason, engine: null, attempts: attempts, pdfPreserved: await paperPathExists(relDir + '/paper.pdf') }
|
|
3533
|
+
}
|
|
3534
|
+
await paperAppendLog(id, 'latex', '检测到引擎 ' + det.available.map(function (x) { return x.name }).join(', ') + (det.forced ? '(paperLatexCommand 指定)' : ('(顺序 ' + (o.lang === 'en' ? PAPER_ENGINE_ORDER_EN : PAPER_ENGINE_ORDER_ZH).join(' > ') + ')')))
|
|
3535
|
+
const plan = [{ engine: det.available[0], tex: texPrimary, mode: 'primary' }]
|
|
3536
|
+
if (det.available[1]) plan.push({ engine: det.available[1], tex: texPrimary, mode: 'engine-fallback' })
|
|
3537
|
+
plan.push({ engine: det.available[0], tex: paperSanitizeTex(texPrimary), mode: 'drop-unsupported-packages' })
|
|
3538
|
+
plan.push({ engine: det.available[0], tex: paperMinimalTexDoc({ lang: o.lang, bodyTex: bodyTex }), mode: 'minimal-template' })
|
|
3539
|
+
const cap = 120000
|
|
3540
|
+
for (let i = 0; i < plan.length; i++) {
|
|
3541
|
+
const step = plan[i]
|
|
3542
|
+
await writeText(relDir + '/paper.tex', step.tex)
|
|
3543
|
+
const runs = []
|
|
3544
|
+
for (let pass = 0; pass < 2; pass++) runs.push(await paperRunOnce(step.engine, absDir, cap))
|
|
3545
|
+
const last = runs[runs.length - 1]
|
|
3546
|
+
const pdf = !!last.ok && await paperPathExists(relDir + '/paper.pdf')
|
|
3547
|
+
attempts.push({ engine: step.engine.name, mode: step.mode, ok: pdf, exitCode: last.exitCode, timedOut: !!last.timedOut, stderr: last.stderr || '' })
|
|
3548
|
+
await paperAppendLog(id, 'compile', step.mode + ' engine=' + step.engine.name + ' → ' + (pdf ? 'ok(paper.pdf 已生成)' : ('failed(exit=' + String(last.exitCode) + (last.timedOut ? ', timeout' : '') + ')')))
|
|
3549
|
+
if (pdf) return { compile: (i === 0) ? 'ok' : 'repaired', engine: step.engine.name, mode: step.mode, attempts: attempts, pdfPreserved: false }
|
|
3550
|
+
}
|
|
3551
|
+
await writeText(relDir + '/paper.tex', texPrimary)
|
|
3552
|
+
const preserved = await paperPathExists(relDir + '/paper.pdf')
|
|
3553
|
+
await paperAppendLog(id, 'compile', '所有尝试均失败:保留 paper.tex + paper.md' + (preserved ? '(paper.pdf 是**上一次成功编译**留下的,未被覆盖)' : ',未生成 paper.pdf') + '(不阻塞定稿)')
|
|
3554
|
+
logActivity('paper', '论文 pdf 编译失败(已尝试换引擎/去不支持宏包/最小模板),已保留 tex+md:' + relDir)
|
|
3555
|
+
reportDirty = true
|
|
3556
|
+
return { compile: 'failed', engine: det.available[0].name, attempts: attempts, pdfPreserved: preserved }
|
|
3557
|
+
}
|
|
3558
|
+
/** 落盘 md/tex/pdf/meta(meta **最后**写 = 原子提交点;spec §2/§5)。 */
|
|
3559
|
+
async function paperEmitArtifacts(id, o, info) {
|
|
3560
|
+
const relDir = paperDir(id)
|
|
3561
|
+
const dateStr = fmtTime(now()).slice(0, 10)
|
|
3562
|
+
const author = paperAuthorLine()
|
|
3563
|
+
const evidence = info.evidence || (await paperEvidenceIndex())
|
|
3564
|
+
const title = String(info.title || '').trim() || ('研究报告:' + currentProject)
|
|
3565
|
+
const md = paperBuildMarkdown({ title: title, author: author, date: dateStr, abstract: info.abstract, sections: info.sections, evidence: evidence })
|
|
3566
|
+
const artifacts = { md: false, tex: false, pdf: false }
|
|
3567
|
+
let compile = 'skipped', engine = null, attempts = [], pdfPreserved = false
|
|
3568
|
+
if (o.format === 'md' || o.format === 'both') { await writeText(relDir + '/paper.md', md); artifacts.md = true }
|
|
3569
|
+
if (o.format === 'tex' || o.format === 'both') {
|
|
3570
|
+
const bodyTex = paperMdToTexBody(md)
|
|
3571
|
+
const tex = paperTexDoc({ title: title, author: author, date: dateStr, lang: o.lang, bodyTex: bodyTex })
|
|
3572
|
+
await writeText(relDir + '/paper.tex', tex)
|
|
3573
|
+
artifacts.tex = true
|
|
3574
|
+
const c = await paperCompile(id, o, tex, bodyTex)
|
|
3575
|
+
compile = c.compile; engine = c.engine || null; attempts = c.attempts || []
|
|
3576
|
+
pdfPreserved = !!c.pdfPreserved
|
|
3577
|
+
if (compile === 'ok' || compile === 'repaired') artifacts.pdf = await paperPathExists(relDir + '/paper.pdf')
|
|
3578
|
+
}
|
|
3579
|
+
const filled = PAPER_SKELETON.map(function (s) { return s.key }).filter(function (k) { return !!(info.sections && String(info.sections[k] || '').trim()) })
|
|
3580
|
+
const meta = {
|
|
3581
|
+
id: id, dir: relDir, preset: 'vibe-math-v3', singleAuthor: true, project: currentProject,
|
|
3582
|
+
title: title, author: author, date: dateStr, abstract: String(info.abstract || ''),
|
|
3583
|
+
sectionBodies: info.sections || {}, sections: PAPER_SKELETON.map(function (s) { return s.key }), filledSections: filled,
|
|
3584
|
+
raw: String(info.raw == null ? '' : info.raw), notes: Array.isArray(info.notes) ? info.notes : [],
|
|
3585
|
+
trigger: info.trigger || 'manual', runStartedAt: Number(scheduler.startedAt) || 0,
|
|
3586
|
+
params: { finalPaper: params.finalPaper !== false, paperFormat: o.format, paperLanguage: o.lang, paperCompilePdf: !!o.compilePdf, paperLatexCommand: String(params.paperLatexCommand || '') },
|
|
3587
|
+
finalizedAt: now(), finalizedAtText: fmtTime(now()),
|
|
3588
|
+
artifacts: artifacts, compile: compile, compileEngine: engine, compileAttempts: attempts, pdfPreserved: pdfPreserved,
|
|
3589
|
+
material: info.material || null, evidence: evidence,
|
|
3590
|
+
}
|
|
3591
|
+
await writeJson(relDir + '/paper.meta.json', meta)
|
|
3592
|
+
return meta
|
|
3593
|
+
}
|
|
3594
|
+
async function paperMissingArtifacts(id, o) {
|
|
3595
|
+
const relDir = paperDir(id)
|
|
3596
|
+
const need = []
|
|
3597
|
+
if (o.format === 'md' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.md'))) need.push('paper.md') }
|
|
3598
|
+
if (o.format === 'tex' || o.format === 'both') { if (!(await paperPathExists(relDir + '/paper.tex'))) need.push('paper.tex') }
|
|
3599
|
+
return need
|
|
3600
|
+
}
|
|
3601
|
+
/** 派遣(内部):激活上限拒绝 → 排队重试(修订 §A2)。 */
|
|
3602
|
+
async function paperDispatch(trigger, o, id, prevTries) {
|
|
3603
|
+
const relDir = paperDir(id)
|
|
3604
|
+
const digest = await buildPaperDigest()
|
|
3605
|
+
let childId
|
|
3606
|
+
try {
|
|
3607
|
+
childId = await spawnChild('paper-writer:' + id, paperWriterPrompt(digest, o), { role: 'paper', paperId: id, trigger: trigger, lang: o.lang, format: o.format, compilePdf: o.compilePdf })
|
|
3608
|
+
} catch (e) {
|
|
3609
|
+
const message = String((e && e.message) || e)
|
|
3610
|
+
const limit = activationLimitFrom(message)
|
|
3611
|
+
if (limit !== undefined || /ACTIVATION_LIMIT_REACHED|active child limit/i.test(message)) {
|
|
3612
|
+
const tries = Number(prevTries || 0) + 1
|
|
3613
|
+
if (tries > PAPER_MAX_RETRIES) {
|
|
3614
|
+
paperPending = null
|
|
3615
|
+
await paperAppendLog(id, 'dispatch-failed', '宿主激活上限连续拒绝 ' + PAPER_MAX_RETRIES + ' 次,已放弃自动派遣:' + message)
|
|
3616
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限连续拒绝(' + PAPER_MAX_RETRIES + ' 次),已放弃自动派遣(稍后可 /vibe paper force 重试):' + message)
|
|
3617
|
+
console.error('vibe-math-v3: paper writer refused by the host activation limit ' + PAPER_MAX_RETRIES + ' times; giving up (retry later with /vibe paper force) — ' + message)
|
|
3618
|
+
await paperLockRelease(id)
|
|
3619
|
+
return { ok: false, message: 'paper writer refused by the activation limit ' + PAPER_MAX_RETRIES + ' times: ' + message, id: id, dir: relDir, limit: limit }
|
|
3620
|
+
}
|
|
3621
|
+
paperPending = { tries: tries, retryAt: now() + PAPER_RETRY_MS, reason: message, limit: limit, trigger: trigger, opts: { lang: o.lang, format: o.format, compilePdf: o.compilePdf } }
|
|
3622
|
+
if (tries === 1 || tries % 3 === 0) {
|
|
3623
|
+
await paperAppendLog(id, 'dispatch-queued', '宿主激活上限(' + (limit === undefined ? '?' : limit) + ')拒绝派遣(第 ' + tries + ' 次):' + (PAPER_RETRY_MS / 1000) + 's 后重试')
|
|
3624
|
+
logActivity('paper', '论文撰写子代理被宿主激活上限拒绝(第 ' + tries + ' 次),已排队重试:' + message)
|
|
3625
|
+
console.error('vibe-math-v3: paper writer queued after an activation-limit refusal (try ' + tries + '): ' + message)
|
|
3626
|
+
}
|
|
3627
|
+
reportDirty = true
|
|
3628
|
+
return { ok: true, queued: true, reason: 'activation-limit-reached', tries: tries, retryAt: paperPending.retryAt, limit: limit, id: id, dir: relDir }
|
|
3629
|
+
}
|
|
3630
|
+
await paperAppendLog(id, 'dispatch-failed', message)
|
|
3631
|
+
logActivity('paper', '「论文撰写」子代理派遣失败:' + message)
|
|
3632
|
+
await paperLockRelease(id)
|
|
3633
|
+
return { ok: false, message: 'paper writer dispatch failed: ' + message, id: id, dir: relDir }
|
|
3634
|
+
}
|
|
3635
|
+
paperInFlight = childId
|
|
3636
|
+
paperInFlightAt = now()
|
|
3637
|
+
// 只有"非自动重试"的派遣才重置回收计数:auto-retry 必须保留计数,否则卡死重派会无限循环。
|
|
3638
|
+
if (trigger !== 'auto-retry') paperReaps = 0
|
|
3639
|
+
paperPending = null
|
|
3640
|
+
const material = paperMaterialSummary(digest)
|
|
3641
|
+
await paperAppendLog(id, 'dispatch', 'trigger=' + trigger + ' child=' + childId + ' format=' + o.format + ' lang=' + o.lang + ' compilePdf=' + o.compilePdf + (o.force ? ' force=true' : '') + ' run=' + (Number(scheduler.startedAt) || 0) + ' material=' + material.chars + ' chars/' + material.lines + ' lines')
|
|
3642
|
+
logActivity('paper', '已派遣「论文撰写」子代理(' + trigger + ',' + o.format + '/' + o.lang + ')→ ' + relDir)
|
|
3643
|
+
reportDirty = true
|
|
3644
|
+
return { ok: true, dispatched: true, childId: childId, id: id, dir: relDir, trigger: trigger, format: o.format, lang: o.lang, compilePdf: o.compilePdf }
|
|
3645
|
+
}
|
|
3646
|
+
/** 触发入口(spec §2/§3 + 修订 §A2/§A3):自动(收口)与手动命令共用。 */
|
|
3647
|
+
async function maybeWritePaper(trigger, opts) {
|
|
3648
|
+
const o = paperOpts(opts)
|
|
3649
|
+
const id = paperId(opts)
|
|
3650
|
+
const relDir = paperDir(id)
|
|
3651
|
+
const meta = await paperReadMeta(id)
|
|
3652
|
+
const sameRun = !!meta && Number(meta.runStartedAt || 0) === (Number(scheduler.startedAt) || 0)
|
|
3653
|
+
if (!o.force && meta && meta.finalizedAt && sameRun) {
|
|
3654
|
+
const missing = await paperMissingArtifacts(id, o)
|
|
3655
|
+
if (missing.length === 0) {
|
|
3656
|
+
await paperAppendLog(id, 'skip', '幂等:同一 run(' + (Number(scheduler.startedAt) || 0) + ')已有定稿且产物齐全 → 不重复撰写(/vibe paper force 可重写)')
|
|
3657
|
+
return { ok: true, skipped: true, reason: 'already-finalized-this-run', id: id, dir: relDir, finalizedAt: meta.finalizedAt, artifacts: meta.artifacts || {}, compile: meta.compile, note: '同一 run 只写一次;重复触发只补写缺失产物(spec §2)' }
|
|
3658
|
+
}
|
|
3659
|
+
await paperAppendLog(id, 'fill', '幂等:已有定稿但缺少 ' + missing.join(', ') + ' → 用已存的定稿内容补写(不重新派遣作者)')
|
|
3660
|
+
const filled = await paperEmitArtifacts(id, o, { title: meta.title, abstract: meta.abstract, sections: meta.sectionBodies || {}, evidence: meta.evidence, raw: meta.raw, notes: (meta.notes || []).concat(['补写缺失产物:' + missing.join(', ')]), trigger: meta.trigger || trigger, material: meta.material })
|
|
3661
|
+
return { ok: true, skipped: true, reason: 'filled-missing-artifacts', filled: missing, id: id, dir: relDir, finalizedAt: filled.finalizedAt, artifacts: filled.artifacts, compile: filled.compile }
|
|
3662
|
+
}
|
|
3663
|
+
if (paperInFlight) {
|
|
3664
|
+
const verdict = paperWriterVerdictNow(o.force)
|
|
3665
|
+
if (verdict === 'reap') {
|
|
3666
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
3667
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
3668
|
+
} else if (verdict === 'refuse') {
|
|
3669
|
+
const ageMs = now() - Number(paperInFlightAt || 0)
|
|
3670
|
+
await paperAppendLog(id, 'force-refused', '仍有在途撰写者 ' + paperInFlight + '(' + Math.round(ageMs / 1000) + 's):超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟才会被 force 放弃')
|
|
3671
|
+
return { ok: false, reason: 'writer-in-flight', childId: paperInFlight, ageMs: ageMs, staleMs: PAPER_STALE_MS, id: id, dir: relDir, message: '已有在途的论文撰写子代理 ' + paperInFlight + '(已 ' + Math.round(ageMs / 1000) + 's 未返回)。等它返回,或等超过 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟后 force 会自动回收并重派(心跳也会自动回收)。' }
|
|
3672
|
+
} else {
|
|
3673
|
+
return { ok: true, skipped: true, reason: 'writer-in-flight', childId: paperInFlight, ageMs: now() - Number(paperInFlightAt || 0), staleMs: PAPER_STALE_MS, id: id, dir: relDir }
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
if (paperPending) return { ok: true, queued: true, reason: 'retry-queued', retryAt: paperPending.retryAt, tries: paperPending.tries, id: id, dir: relDir }
|
|
3677
|
+
const lock = await paperLockAcquire(id)
|
|
3678
|
+
if (!lock.ok) {
|
|
3679
|
+
await paperAppendLog(id, 'skip', 'paper 锁由会话 ' + lock.holder + ' 持有(未过期)→ 本次不派遣(修订 §A3 的 paper 作用域锁)')
|
|
3680
|
+
return { ok: true, skipped: true, reason: 'locked-by-another-session', holder: lock.holder, id: id, dir: relDir }
|
|
3681
|
+
}
|
|
3682
|
+
return await paperDispatch(trigger, o, id, 0)
|
|
3683
|
+
}
|
|
3684
|
+
function paperRetryDue() { return !!(paperPending && now() >= Number(paperPending.retryAt || 0)) }
|
|
3685
|
+
/**
|
|
3686
|
+
* 心跳(修订 §1):在途 → 续租 paper 锁;**卡死则自动回收**(child 不在注册表,或超过卡死窗口),
|
|
3687
|
+
* 并自动重派(上限 PAPER_MAX_AUTO_REAPS,之后给出可执行提示交给 /vibe paper force);有排队则重试派遣。
|
|
3688
|
+
*/
|
|
3689
|
+
async function runPaperRetry() {
|
|
3690
|
+
const id = paperId()
|
|
3691
|
+
if (paperInFlight) {
|
|
3692
|
+
if (paperWriterVerdictNow(false) === 'reap') {
|
|
3693
|
+
const gone = agentRegistry[paperInFlight] === undefined
|
|
3694
|
+
await paperReapWriter(id, gone ? '子代理已不在 agentRegistry(end 事件丢失 / 宿主丢弃)' : ('超过卡死窗口 ' + Math.round(PAPER_STALE_MS / 60000) + ' 分钟未返回'))
|
|
3695
|
+
const meta = await paperReadMeta(id)
|
|
3696
|
+
if (params.finalPaper !== false && !(meta && meta.finalizedAt) && Number(paperReaps || 0) < PAPER_MAX_AUTO_REAPS) {
|
|
3697
|
+
paperReaps = Number(paperReaps || 0) + 1
|
|
3698
|
+
await paperAppendLog(id, 'retry-after-reap', '第 ' + paperReaps + ' 次自动重派(上限 ' + PAPER_MAX_AUTO_REAPS + ')')
|
|
3699
|
+
await paperDispatch('auto-retry', paperOpts({}), id, 0)
|
|
3700
|
+
} else {
|
|
3701
|
+
await paperAppendLog(id, 'reap-giveup', '自动重派已达上限 ' + PAPER_MAX_AUTO_REAPS + '(或已有定稿):不再自动派遣,请用 /vibe paper force 手动重写')
|
|
3702
|
+
logActivity('paper', '论文撰写子代理连续卡死/已放弃:不再自动重派,可用 /vibe paper force 手动重写')
|
|
3703
|
+
}
|
|
3704
|
+
return
|
|
3705
|
+
}
|
|
3706
|
+
try { await paperLockAcquire(id) } catch (e) { /* 续租失败不致命 */ }
|
|
3707
|
+
return
|
|
3708
|
+
}
|
|
3709
|
+
if (!paperRetryDue()) return
|
|
3710
|
+
const p = paperPending
|
|
3711
|
+
paperPending = null
|
|
3712
|
+
const o = paperOpts(p.opts)
|
|
3713
|
+
await paperDispatch(p.trigger || 'auto-retry', o, id, Number(p.tries || 0))
|
|
3714
|
+
}
|
|
3715
|
+
/** `/vibe paper [lang=zh|en] [format=both|md|tex] [force]`(spec §2/§6)。 */
|
|
3716
|
+
async function paperCommand(args) {
|
|
3717
|
+
const rest = Array.isArray(args) ? args : []
|
|
3718
|
+
const opts = {}
|
|
3719
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3720
|
+
const a = String(rest[i] || '')
|
|
3721
|
+
if (a === 'force') { opts.force = true; continue }
|
|
3722
|
+
const eq = a.indexOf('=')
|
|
3723
|
+
const k = eq > 0 ? a.slice(0, eq).toLowerCase() : ''
|
|
3724
|
+
const v = eq > 0 ? a.slice(eq + 1).toLowerCase() : ''
|
|
3725
|
+
if (k === 'lang') { if (v !== 'zh' && v !== 'en') return { ok: false, message: '/vibe paper lang= 只接受 zh|en(收到 ' + JSON.stringify(a) + ')' }; opts.lang = v; continue }
|
|
3726
|
+
if (k === 'format') { if (v !== 'both' && v !== 'md' && v !== 'tex') return { ok: false, message: '/vibe paper format= 只接受 both|md|tex(收到 ' + JSON.stringify(a) + ')' }; opts.format = v; continue }
|
|
3727
|
+
return { ok: false, message: 'unknown /vibe paper option: ' + a + '(可用 lang=zh|en、format=both|md|tex、force)' }
|
|
3728
|
+
}
|
|
3729
|
+
const r = await maybeWritePaper('manual', opts)
|
|
3730
|
+
if (r.ok === false) return r
|
|
3731
|
+
const autoNote = params.finalPaper === false ? '(自动触发已关闭 finalPaper=false;手动命令仍可用)' : ''
|
|
3732
|
+
const msg = r.dispatched ? ('已派遣「论文撰写」子代理,产物将写入 ' + r.dir + '/')
|
|
3733
|
+
: r.queued ? ('宿主激活上限已满:论文撰写已排队,' + (PAPER_RETRY_MS / 1000) + 's 后自动重试(第 ' + r.tries + ' 次)')
|
|
3734
|
+
: (r.reason === 'already-finalized-this-run' ? '同一 run 已有定稿且产物齐全:不重复撰写(需要重写请用 /vibe paper force)'
|
|
3735
|
+
: (r.reason === 'filled-missing-artifacts' ? ('已用既有定稿补写缺失产物:' + (r.filled || []).join(', ')) : ('未重新派遣:' + (r.reason || ''))))
|
|
3736
|
+
return Object.assign({}, r, { message: msg + autoNote, autoDisabled: params.finalPaper === false })
|
|
3737
|
+
}
|
|
3738
|
+
/** 论文撰写子代理结束(spec §4):解析回复 → 组装 9 节 → 落盘 → 编译 → 记日志 → 释放 paper 锁。 */
|
|
3739
|
+
async function handlePaperWriter(childId, meta, output) {
|
|
3740
|
+
const id = meta.paperId || paperId()
|
|
3741
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0
|
|
3742
|
+
delete paperAbandoned[childId]
|
|
3743
|
+
const o = paperOpts({ lang: meta.lang, format: meta.format, compilePdf: meta.compilePdf })
|
|
3744
|
+
const parsed = parseJson(output)
|
|
3745
|
+
const info = paperSkeletonFromReply(parsed, output)
|
|
3746
|
+
if (!info.title) info.title = paperTitleFromMd(String(output || '')) || ('研究报告:' + currentProject)
|
|
3747
|
+
for (let i = 0; i < info.notes.length; i++) await paperAppendLog(id, 'note', info.notes[i])
|
|
3748
|
+
if (!String(output || '').trim()) await paperAppendLog(id, 'reply', '作者回复为空:只写骨架 + 占位说明(绝不编造内容)')
|
|
3749
|
+
let written
|
|
3750
|
+
try {
|
|
3751
|
+
written = await paperEmitArtifacts(id, o, { title: info.title, abstract: info.abstract, sections: info.sections, raw: String(output || ''), notes: info.notes, trigger: meta.trigger || 'auto', material: paperMaterialSummary(await buildPaperDigest()) })
|
|
3752
|
+
} catch (e) {
|
|
3753
|
+
await paperAppendLog(id, 'finalize-failed', String((e && e.message) || e))
|
|
3754
|
+
logActivity('paper', '论文落盘失败:' + String((e && e.message) || e))
|
|
3755
|
+
await paperLockRelease(id)
|
|
3756
|
+
return
|
|
3757
|
+
}
|
|
3758
|
+
await paperAppendLog(id, 'finalize', '产物 ' + JSON.stringify(written.artifacts) + ' compile=' + written.compile + (written.compileEngine ? (' engine=' + written.compileEngine) : '') + ' run=' + (Number(scheduler.startedAt) || 0))
|
|
3759
|
+
logActivity('paper', '论文(最终稿)已落盘:' + paperDir(id) + '/(' + Object.keys(written.artifacts).filter(function (k) { return written.artifacts[k] }).join('/') + ',编译=' + written.compile + ')')
|
|
3760
|
+
await paperLockRelease(id)
|
|
3761
|
+
reportDirty = true
|
|
3762
|
+
await saveAll()
|
|
3763
|
+
}
|
|
3764
|
+
/** status 里的论文视图(spec §2/§6)。 */
|
|
3765
|
+
async function paperStatusView() {
|
|
3766
|
+
const id = paperId()
|
|
3767
|
+
const meta = await paperReadMeta(id)
|
|
3768
|
+
return {
|
|
3769
|
+
id: id, dir: paperDir(id), inFlight: paperInFlight || null,
|
|
3770
|
+
inFlightSince: paperInFlight ? Number(paperInFlightAt || 0) : null,
|
|
3771
|
+
inFlightAgeMs: paperInFlight ? (now() - Number(paperInFlightAt || 0)) : null,
|
|
3772
|
+
staleMs: PAPER_STALE_MS, reapedThisRun: Number(paperReaps || 0),
|
|
3773
|
+
queued: paperPending ? { tries: paperPending.tries, retryAt: paperPending.retryAt, reason: paperPending.reason, limit: paperPending.limit === undefined ? null : paperPending.limit } : null,
|
|
3774
|
+
autoFinalPaper: params.finalPaper !== false, format: params.paperFormat, language: params.paperLanguage, compilePdf: params.paperCompilePdf !== false,
|
|
3775
|
+
finalizedAt: meta ? meta.finalizedAt : null, artifacts: meta ? (meta.artifacts || {}) : {}, compile: meta ? meta.compile : null,
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3292
3779
|
// ================= child result dispatch =================
|
|
3293
3780
|
async function onChildEnd(info) {
|
|
3294
3781
|
// 子代理结束/中断:自动释放它持有的所有写锁(防锁残留导致文件被永久锁住)
|
|
3295
3782
|
const endedId = String(info.id)
|
|
3296
3783
|
for (const k of Object.keys(fileOwner)) { if (String(fileOwner[k].childId) === endedId) delete fileOwner[k] }
|
|
3297
3784
|
const meta = agentRegistry[info.id]
|
|
3298
|
-
if (meta === undefined)
|
|
3785
|
+
if (meta === undefined) {
|
|
3786
|
+
// 已回收的撰写者事后才返回:输出被丢弃,但必须**留痕**,不能静默消失(修订 §1)。
|
|
3787
|
+
if (paperAbandoned[info.id]) {
|
|
3788
|
+
delete paperAbandoned[info.id]
|
|
3789
|
+
logActivity('paper', '已回收的论文撰写子代理 ' + info.id + ' 事后返回:其输出被丢弃(已重派新的撰写者)')
|
|
3790
|
+
reportDirty = true
|
|
3791
|
+
}
|
|
3792
|
+
return
|
|
3793
|
+
}
|
|
3299
3794
|
const output = blocksToText(info.lastAssistantMessage)
|
|
3300
3795
|
try {
|
|
3301
3796
|
if (meta.role === 'explorer') await handleExplorer(info.id, meta, output)
|
|
@@ -3303,6 +3798,9 @@ export function apply(ctx) {
|
|
|
3303
3798
|
else if (meta.role === 'verifier') await handleVerifier(info.id, meta, output, info.stopReason)
|
|
3304
3799
|
else if (meta.role === 'planner') await handlePlanner(info.id, meta, output)
|
|
3305
3800
|
else if (meta.role === 'method-keeper') await handleMethodKeeper(info.id, meta, output)
|
|
3801
|
+
// 修订 §A2:论文撰写子代理必须有**自己的分支**——未知 role 的产出会被静默丢弃。
|
|
3802
|
+
else if (meta.role === 'paper') await handlePaperWriter(info.id, meta, output)
|
|
3803
|
+
else console.error('vibe-math-v3: subagent ' + info.id + ' ended with an unknown role "' + String(meta.role) + '" — its output is dropped')
|
|
3306
3804
|
} catch (e) { console.error('vibe-math-v3 onChildEnd error: ' + String((e && e.stack) || e)) }
|
|
3307
3805
|
await saveAll()
|
|
3308
3806
|
await maybePromoteMethods()
|
|
@@ -3321,6 +3819,9 @@ export function apply(ctx) {
|
|
|
3321
3819
|
if (Object.keys(agentRegistry).length > 0 || Object.keys(tasks).length > 0) {
|
|
3322
3820
|
logActivity(fresh ? 'start' : 'resume', 'cleared ' + Object.keys(agentRegistry).length + ' agent(s) and ' + Object.keys(tasks).length + ' task(s) (' + (fresh ? 'restart' : 'stale from previous process') + ')')
|
|
3323
3821
|
agentRegistry = {}; tasks = {}
|
|
3822
|
+
// 论文派遣态是本进程内存态:子代理已被中断/丢弃,排队与在途标记必须一起清掉(修订 §A2)。
|
|
3823
|
+
if (paperInFlight || paperPending) logActivity('paper', '重启/跨进程恢复:清空论文撰写态(inFlight=' + (paperInFlight || '-') + (paperPending ? ', queued' : '') + ')')
|
|
3824
|
+
paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
3324
3825
|
}
|
|
3325
3826
|
}
|
|
3326
3827
|
await writeJson('State/process_epoch.json', processEpoch)
|
|
@@ -3376,7 +3877,7 @@ export function apply(ctx) {
|
|
|
3376
3877
|
async function startScheduler(override) { const r = await init(true); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.startedAt = now(); scheduler.gate = null; logActivity('start', 'scheduler started for project ' + currentProject + '(v3:md 知识库 + 规划代理调度 + 方法库)'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler started', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3377
3878
|
async function resumeScheduler(override) { const r = await init(false); if (!r.ok) return r; const lock = await acquireProjectLock(override === true); if (!lock.ok) return lock; scheduler.running = true; scheduler.gate = null; logActivity('resume', 'scheduler resumed'); await saveAll(); await maybeWriteReport(true); scheduleTick(); return { ok: true, message: 'scheduler resumed', project: currentProject, frameworkRoot: frameworkRoot() } }
|
|
3378
3879
|
async function pauseScheduler() { scheduler.running = false; await releaseProjectLock(); logActivity('pause', 'scheduler paused'); await saveAll(); return { ok: true, message: 'scheduler paused' } }
|
|
3379
|
-
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3880
|
+
async function abortScheduler() { scheduler.running = false; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) await interruptChild(ids[i]); agentRegistry = {}; planQueue = []; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null; await releaseProjectLock(); logActivity('abort', 'scheduler aborted, ' + ids.length + ' child(ren) interrupted'); await saveAll(); return { ok: true, message: 'scheduler aborted', interrupted: ids.length } }
|
|
3380
3881
|
async function autoResolvePending() {
|
|
3381
3882
|
const pending = decisionQueue.filter(function (d) { return d.status === 'pending' })
|
|
3382
3883
|
for (let i = 0; i < pending.length; i++) {
|
|
@@ -3413,6 +3914,8 @@ export function apply(ctx) {
|
|
|
3413
3914
|
plannerEnabled: params.plannerEnabled, plannerFails: plannerFails,
|
|
3414
3915
|
formal: formalSummary(),
|
|
3415
3916
|
recentActivity: activityLog.slice(-Math.min(10, Number(params.activityLogCap) || 100)), params: params,
|
|
3917
|
+
// 最终论文的可观测面(spec §2/§6)。
|
|
3918
|
+
paper: await paperStatusView(),
|
|
3416
3919
|
}
|
|
3417
3920
|
}
|
|
3418
3921
|
async function checkTermination() {
|
|
@@ -3431,8 +3934,14 @@ export function apply(ctx) {
|
|
|
3431
3934
|
// H1:在途的 **planner 不算"活跃工作"**。planner 不产出知识、不持有验证任务,只是"问下一步做什么";
|
|
3432
3935
|
// 旧判定把刚派出的 planner 也算成活跃代理,于是本 tick 的停机判定永远被自己派出的 planner 挡掉,
|
|
3433
3936
|
// 空闲时形成"每 tick 一个 planner"的活锁。排除它之后,空闲的那一 tick 就能正常收口停机。
|
|
3434
|
-
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' }).length
|
|
3937
|
+
const realAgents = Object.keys(agentRegistry).filter(function (cid) { const m = agentRegistry[cid]; return m && m.role !== 'planner' && m.role !== 'paper' }).length
|
|
3435
3938
|
if (unsolved.length === 0 && !leftoverVerify && realAgents === 0 && Object.keys(tasks).length === 0 && planQueue.length === 0) {
|
|
3939
|
+
// ★ 最终论文(修订 §A2/§A3):必须在 running=false 与 releaseProjectLock() **之前**派遣——
|
|
3940
|
+
// 停止之后 scheduleTick() 是空操作;提前放锁后另一会话可能开始写同一棵树(paper 锁兜底)。
|
|
3941
|
+
// 触发点就是这个分支本身:`leftoverVerify` 为空才是 v3 的真实完整性判据(§A4)。
|
|
3942
|
+
if (params.finalPaper !== false) {
|
|
3943
|
+
try { await maybeWritePaper('auto') } catch (e) { logActivity('paper', '自动撰写最终论文失败:' + String((e && e.message) || e)) }
|
|
3944
|
+
}
|
|
3436
3945
|
scheduler.running = false
|
|
3437
3946
|
await releaseProjectLock()
|
|
3438
3947
|
logActivity('stop', 'all active problems solved (never-priority excluded) and no active agents/tasks/plans — scheduler stopped (strict termination)')
|
|
@@ -3464,7 +3973,7 @@ export function apply(ctx) {
|
|
|
3464
3973
|
if (!create && !exists) return { ok: false, message: 'project not found: ' + slug }
|
|
3465
3974
|
if (scheduler.running) await abortScheduler()
|
|
3466
3975
|
currentProject = slug; await writeCurrentProject(); await ensureDirs()
|
|
3467
|
-
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }
|
|
3976
|
+
params = Object.assign({}, DEFAULT_PARAMS); scheduler = { running: false, startedAt: 0, lastCheckpoint: 0, gate: null }; agentRegistry = {}; decisionQueue = []; verifierAccuracy = {}; tasks = {}; explorerRetries = {}; activityLog = []; planQueue = []; plannerFails = 0; methodLog = { pendingInventions: [], keepCount: 0, lastKeepAt: 0 }; projectLock = { sessionId: '', at: 0 }; lastReportWrite = 0; lastPushReport = 0; reportDirty = false; lastPlanSummary = null; archivedJ = {}; lastIndexWrite = 0; formalState = { records: {}, todo: [], libRuns: {} }; paperInFlight = ''; paperInFlightAt = 0; paperReaps = 0; paperPending = null
|
|
3468
3977
|
await loadSettings(); await migrateLegacyParams(); await loadState(); await loadKnowledgeBase(); await saveAll()
|
|
3469
3978
|
if (params.indexAutoRebuild) await rebuildIndex()
|
|
3470
3979
|
return { ok: true, project: slug, frameworkRoot: frameworkRoot() }
|
|
@@ -3483,7 +3992,7 @@ export function apply(ctx) {
|
|
|
3483
3992
|
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), async function () { await refreshParams(); return await getStatus() })
|
|
3484
3993
|
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), async function () { await refreshParams(); await maybeWriteReport(true); return await buildReport() })
|
|
3485
3994
|
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), async function (args) { params.mode = args.mode; await saveAll(); await saveSettings(); if (params.mode === 'auto') await autoResolvePending(); return { ok: true, mode: params.mode } })
|
|
3486
|
-
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
|
|
3995
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), async function (args) { params = Object.assign({}, params, sanitizeParams(args)); await saveAll(); await saveSettings(); return { ok: true, params: params } })
|
|
3487
3996
|
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), async function () { await refreshParams(); const list = PARAM_SCHEMA.map(function (p) { const out = Object.assign({}, p); out.current = params[p.name]; out.default = DEFAULT_PARAMS[p.name]; return out }); return { ok: true, parameters: list, saveTo: frameworkRoot() + '/vibe_math_setting.json' } })
|
|
3488
3997
|
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), async function () { return await saveSettings() })
|
|
3489
3998
|
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), async function (args) { return await createTemplate((args && args.where) || 'global') })
|
|
@@ -3745,7 +4254,8 @@ export function apply(ctx) {
|
|
|
3745
4254
|
}
|
|
3746
4255
|
if (cmd === 'decisions') return { ok: true, decisions: decisionQueue.filter(function (d) { return d.status === 'pending' }).map(function (d) { return { id: d.id, node: d.node, context: d.context } }) }
|
|
3747
4256
|
if (cmd === 'agents') { const out = []; const ids = Object.keys(agentRegistry); for (let i = 0; i < ids.length; i++) { const m = agentRegistry[ids[i]]; out.push({ childId: ids[i], role: m.role, qid: m.qid, direction: m.direction, round: m.round }) } return { ok: true, agents: out } }
|
|
3748
|
-
|
|
4257
|
+
if (cmd === 'paper') return await paperCommand(args)
|
|
4258
|
+
return { ok: false, usage: 'start [override] | resume [override] | pause | abort | status | report | mode <auto|manual> | setup | save | template [global|project] | add <id> <desc> | add-proposition <id> <概述> | list-propositions | methods | index | plan | lock | project [list|new <name>|<name>] | decisions | agents | paper [lang=zh|en] [format=both|md|tex] [force]', message: 'unknown /vibe subcommand: ' + (cmd || '(empty)') }
|
|
3749
4259
|
}
|
|
3750
4260
|
|
|
3751
4261
|
// ================= session surface =================
|
|
@@ -3766,6 +4276,10 @@ export function apply(ctx) {
|
|
|
3766
4276
|
handlers: handlers,
|
|
3767
4277
|
refreshProject: async function () { if (rootAgent) currentProject = await readCurrentProject() },
|
|
3768
4278
|
getRunning: function () { return scheduler.running },
|
|
4279
|
+
// apply 级心跳用(修订 §A2):running=false 之后 scheduleTick() 是空操作,论文的排队重试
|
|
4280
|
+
// (激活上限)与 paper 锁续租必须由**独立于调度器**的心跳驱动。
|
|
4281
|
+
paperRetryDue: paperRetryDue,
|
|
4282
|
+
runPaperRetry: runPaperRetry,
|
|
3769
4283
|
// childOwner 裁剪用(审计 L1):这个会话当前仍"可能再发 subagent/end"的 child
|
|
3770
4284
|
// = 在册子代理 + 任何任务正在等的那几个。
|
|
3771
4285
|
referencedChildIds: function () {
|
|
@@ -3804,7 +4318,7 @@ export function apply(ctx) {
|
|
|
3804
4318
|
registerTool('vibe_math_status', TOOL_DESC.vibe_math_status, objParams({}), 'vibe_math_status')
|
|
3805
4319
|
registerTool('vibe_math_report', TOOL_DESC.vibe_math_report, objParams({}), 'vibe_math_report')
|
|
3806
4320
|
registerTool('vibe_math_set_mode', TOOL_DESC.vibe_math_set_mode, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] } }, ['mode']), 'vibe_math_set_mode')
|
|
3807
|
-
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' } }), 'vibe_math_set_params')
|
|
4321
|
+
registerTool('vibe_math_set_params', TOOL_DESC.vibe_math_set_params, objParams({ mode: { type: 'string', enum: ['manual', 'auto'] }, maxParallelThreshold: { type: 'integer' }, solverMaxRounds: { type: 'integer' }, verifierCount: { type: 'integer' }, debateMaxRounds: { type: 'integer' }, verdictMode: { type: 'string', enum: ['flat', 'forced'] }, reportMode: { type: 'string', enum: ['file', 'push', 'both'] }, promoteValueThreshold: { type: 'number' }, priorityAdjust: { type: 'string', enum: ['none', 'deadend-deprioritize', 'survival-map'] }, proposPriorityAdjust: { type: 'string', enum: ['none', 'progress-graded'] }, provider: { type: 'string' }, model: { type: 'string' }, solverPersona: { type: 'string' }, verifierPersona: { type: 'string' }, explorerPersona: { type: 'string' }, plannerPersona: { type: 'string' }, methodKeeperPersona: { type: 'string' }, knowledgeContext: { type: 'string' }, solverToolAllow: { type: 'array', items: { type: 'string' } }, solverToolDeny: { type: 'array', items: { type: 'string' } }, verifierToolAllow: { type: 'array', items: { type: 'string' } }, verifierToolDeny: { type: 'array', items: { type: 'string' } }, solverAllowNetwork: { type: 'boolean' }, verifierAllowNetwork: { type: 'boolean' }, solverAllowScripts: { type: 'boolean' }, verifierAllowScripts: { type: 'boolean' }, solverMaxToolCalls: { type: 'integer' }, verifierMaxToolCalls: { type: 'integer' }, reportIntervalMs: { type: 'integer' }, tickIntervalMs: { type: 'integer' }, activityLogCap: { type: 'integer' }, maxExplorerRetries: { type: 'integer' }, directionsPerSolver: { type: 'integer' }, planningHorizon: { type: 'integer' }, plannerEnabled: { type: 'boolean' }, plannerProvider: { type: 'string' }, plannerModel: { type: 'string' }, planMinIntervalMs: { type: 'integer' }, plannerMaxFails: { type: 'integer' }, methodKeepIntervalMs: { type: 'integer' }, methodKeepEvery: { type: 'integer' }, methodAutoPromote: { type: 'boolean' }, indexAutoRebuild: { type: 'boolean' }, projectLockTimeoutMs: { type: 'integer' }, formalVerify: { type: 'string', enum: ['off', 'encourage', 'require'] }, leanCommand: { type: 'string' }, leanArgs: { type: 'array', items: { type: 'string' } }, leanTimeoutMs: { type: 'integer' }, finalPaper: { type: 'boolean' }, paperFormat: { type: 'string', enum: ['both', 'md', 'tex'] }, paperLanguage: { type: 'string', enum: ['zh', 'en'] }, paperCompilePdf: { type: 'boolean' }, paperLatexCommand: { type: 'string' } }), 'vibe_math_set_params')
|
|
3808
4322
|
registerTool('vibe_math_setup', TOOL_DESC.vibe_math_setup, objParams({}), 'vibe_math_setup')
|
|
3809
4323
|
registerTool('vibe_math_save_settings', TOOL_DESC.vibe_math_save_settings, objParams({}), 'vibe_math_save_settings')
|
|
3810
4324
|
registerTool('vibe_math_template', TOOL_DESC.vibe_math_template, objParams({ where: { type: 'string', enum: ['global', 'project'] } }), 'vibe_math_template')
|
|
@@ -3837,7 +4351,7 @@ export function apply(ctx) {
|
|
|
3837
4351
|
ctx.effect(() => commands.register({
|
|
3838
4352
|
name: 'vibe',
|
|
3839
4353
|
description: 'control the Vibe Math V3 solver (start/pause/projects/setup/save/decisions/agents/methods/index/plan/lock)',
|
|
3840
|
-
input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents]' },
|
|
4354
|
+
input: { hint: '[start [override]|resume [override]|pause|abort|status|report|mode <auto|manual>|setup|save|template [global|project]|add <id> <desc>|add-proposition <id> <概述>|list-propositions|methods|index|plan|lock|project [list|new <name>|<name>]|decisions|agents|paper [lang=zh|en] [format=both|md|tex] [force]]' },
|
|
3841
4355
|
handler: async function (invocation) {
|
|
3842
4356
|
const s = getSession(invocation && invocation.agent)
|
|
3843
4357
|
if (!s) return { kind: 'error', text: JSON.stringify({ ok: false, error: 'no vibe-math session for this agent' }) }
|
|
@@ -3899,7 +4413,12 @@ export function apply(ctx) {
|
|
|
3899
4413
|
ctx.effect(() => {
|
|
3900
4414
|
let beat = 0
|
|
3901
4415
|
return everyMs(1000, function () {
|
|
3902
|
-
for (const s of sessions.values()) {
|
|
4416
|
+
for (const s of sessions.values()) {
|
|
4417
|
+
if (s.getRunning() && !s.tickInFlight && s.tickDue() && s.scheduler.gate === null) s.scheduleTick()
|
|
4418
|
+
// 论文心跳(修订 §A2):**独立于 scheduler.running** —— 收口后 running=false,但被激活上限
|
|
4419
|
+
// 拒绝的撰写派遣仍要重试,在途时还要续租 paper 锁。runPaperRetry() 无事可做时立即返回。
|
|
4420
|
+
if (typeof s.runPaperRetry === 'function') s.runPaperRetry().catch(function (e) { console.error('vibe-math-v3: paper retry failed: ' + String((e && e.message) || e)) })
|
|
4421
|
+
}
|
|
3903
4422
|
// childOwner 裁剪(审计 L1):每 30 拍(约 30s)一次,成本是"会话数 × 映射数"的一次扫描。
|
|
3904
4423
|
if ((++beat % 30) === 0) {
|
|
3905
4424
|
try { pruneChildOwner() } catch (e) { console.error('vibe-math-v3: pruneChildOwner failed: ' + String((e && e.message) || e)) }
|
|
@@ -3921,6 +4440,255 @@ export function apply(ctx) {
|
|
|
3921
4440
|
// to the clock) and `parseProgress` normalises the object it is handed in place (pre-existing).
|
|
3922
4441
|
// Nothing here is used by the plugin at runtime except through `apply()`, and behaviour is
|
|
3923
4442
|
// byte-identical to the previous in-`apply` declarations.
|
|
4443
|
+
// ============================================================================================
|
|
4444
|
+
// 最终论文(规格:docs/final-paper.md)—— **纯函数部分**(module scope;
|
|
4445
|
+
// 与 v2 逐字同构,只有数据来源不同)。会话相关的落盘/派遣/编译在 makeSession 里。
|
|
4446
|
+
// ============================================================================================
|
|
4447
|
+
/** 固定 9 节骨架(spec §3)。key 同时是 md 的 `## ` 标题与 tex 的 `\section{}`。 */
|
|
4448
|
+
const PAPER_SKELETON = [
|
|
4449
|
+
{ key: '摘要', spec: '1 标题、作者、日期、摘要(原问题 + 主要结论)' },
|
|
4450
|
+
{ key: '引言与问题背景', spec: '2 原问题的完整陈述' },
|
|
4451
|
+
{ key: '原问题的完整解法', spec: '3 最终答案 + 完整推理链' },
|
|
4452
|
+
{ key: '已检验通过的命题', spec: '4 逐条列出,含判定为真的估计值与证据来源' },
|
|
4453
|
+
{ key: '已解决的子问题与中间成果', spec: '5' },
|
|
4454
|
+
{ key: '创造或发现的有价值之物', spec: '6 方法、理论、思想、有价值经验、数学理解' },
|
|
4455
|
+
{ key: '规律总结', spec: '7 从上述条目归纳出的可复用规律' },
|
|
4456
|
+
{ key: '讨论、局限与展望', spec: '8' },
|
|
4457
|
+
{ key: '附录:证据与文件索引', spec: '9 Verified/、Logs/、关键卡片路径' },
|
|
4458
|
+
]
|
|
4459
|
+
const PAPER_NO_EVIDENCE = '(本节暂无证据支持的内容——不编造。)'
|
|
4460
|
+
const PAPER_EVIDENCE_HEADING = '证据与文件索引'
|
|
4461
|
+
/** LaTeX 引擎检测顺序(spec §5):中文优先 xelatex,英文优先 pdflatex/latexmk。 */
|
|
4462
|
+
const PAPER_ENGINE_ORDER_ZH = ['xelatex', 'latexmk', 'pdflatex', 'lualatex', 'tectonic']
|
|
4463
|
+
const PAPER_ENGINE_ORDER_EN = ['pdflatex', 'latexmk', 'xelatex', 'lualatex', 'tectonic']
|
|
4464
|
+
/** 只用常见宏包(spec §5);修复阶段会丢弃不在这个名单里的 \usepackage。 */
|
|
4465
|
+
const PAPER_ALLOWED_PACKAGES = ['amsmath', 'amssymb', 'amsthm', 'geometry', 'hyperref', 'longtable', 'booktabs']
|
|
4466
|
+
/** tex 必须转义的 10 个字符(spec §5):\ _ % & # $ { } ~ ^ ——一次替换,避免二次转义。 */
|
|
4467
|
+
const PAPER_TEX_MAP = { '\\': '\\textbackslash{}', '_': '\\_', '%': '\\%', '&': '\\&', '#': '\\#', '$': '\\$', '{': '\\{', '}': '\\}', '~': '\\textasciitilde{}', '^': '\\textasciicircum{}' }
|
|
4468
|
+
function texEscape(s) { return String(s == null ? '' : s).replace(/[\\_%&#${}~^]/g, function (c) { return PAPER_TEX_MAP[c] }) }
|
|
4469
|
+
/** 材料摘要(只作记录;幂等判定用 run id + finalizedAt + 逐产物存在性,修订 §C)。 */
|
|
4470
|
+
function paperMaterialSummary(digest) {
|
|
4471
|
+
const t = String(digest == null ? '' : digest)
|
|
4472
|
+
return { chars: t.length, lines: t.split('\n').length }
|
|
4473
|
+
}
|
|
4474
|
+
/**
|
|
4475
|
+
* 撰写者卡死判定(纯函数,便于守卫覆盖两条分支):
|
|
4476
|
+
* 'none' 正常在途(还没到卡死窗口,且 force 未要求放弃)
|
|
4477
|
+
* 'reap' 必须回收:child 已不在 agentRegistry(end 事件丢失 / 宿主丢弃,它永远不会再回来),
|
|
4478
|
+
* 或已超过卡死窗口——force 与心跳都据此自动降级,论文不能被一个死掉的撰写者永久卡住
|
|
4479
|
+
* 'refuse' force 但还没到窗口:调用方必须给出**可执行**的报错(告诉调用者还要等多久)
|
|
4480
|
+
*/
|
|
4481
|
+
function paperWriterVerdict(o) {
|
|
4482
|
+
if (!(o && o.inFlight)) return 'none'
|
|
4483
|
+
if (o.childInRegistry === false) return 'reap'
|
|
4484
|
+
const age = Number((o && o.ageMs) || 0)
|
|
4485
|
+
const stale = Math.max(1, Number((o && o.staleMs) || 0))
|
|
4486
|
+
if (age >= stale) return 'reap'
|
|
4487
|
+
return (o && o.force) ? 'refuse' : 'none'
|
|
4488
|
+
}
|
|
4489
|
+
/** 行内 md → tex:``code`` / **bold** / *em* 转成命令,其余按 spec §5 转义;`$...$` 原样保留。 */
|
|
4490
|
+
function paperInlineToTex(s) {
|
|
4491
|
+
const t = String(s == null ? '' : s)
|
|
4492
|
+
let out = ''
|
|
4493
|
+
let i = 0
|
|
4494
|
+
while (i < t.length) {
|
|
4495
|
+
const c = t[i]
|
|
4496
|
+
if (c === '$') { const j = t.indexOf('$', i + 1); if (j > i) { out += t.slice(i, j + 1); i = j + 1; continue } }
|
|
4497
|
+
if (c === '`') { const j = t.indexOf('`', i + 1); if (j > i) { out += '\\texttt{' + texEscape(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
4498
|
+
if (c === '*' && t[i + 1] === '*') { const j = t.indexOf('**', i + 2); if (j > i) { out += '\\textbf{' + paperInlineToTex(t.slice(i + 2, j)) + '}'; i = j + 2; continue } }
|
|
4499
|
+
if (c === '*') { const j = t.indexOf('*', i + 1); if (j > i) { out += '\\emph{' + paperInlineToTex(t.slice(i + 1, j)) + '}'; i = j + 1; continue } }
|
|
4500
|
+
out += PAPER_TEX_MAP[c] !== undefined ? PAPER_TEX_MAP[c] : c
|
|
4501
|
+
i++
|
|
4502
|
+
}
|
|
4503
|
+
return out
|
|
4504
|
+
}
|
|
4505
|
+
/** md 正文 → tex 正文(标题/列表/段落;只认 compose 侧约定的最小语法)。 */
|
|
4506
|
+
function paperMdToTexBody(md) {
|
|
4507
|
+
const out = []
|
|
4508
|
+
let inList = false
|
|
4509
|
+
let buf = []
|
|
4510
|
+
const flush = function () { if (buf.length) { out.push(paperInlineToTex(buf.join(' '))); buf = [] } }
|
|
4511
|
+
const closeList = function () { if (inList) { out.push('\\end{itemize}'); inList = false } }
|
|
4512
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
4513
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4514
|
+
const line = lines[i].replace(/\s+$/, '')
|
|
4515
|
+
const h = /^(#{1,4})\s+(.*)$/.exec(line)
|
|
4516
|
+
if (h) {
|
|
4517
|
+
flush(); closeList()
|
|
4518
|
+
const txt = paperInlineToTex(h[2])
|
|
4519
|
+
const lv = h[1].length
|
|
4520
|
+
out.push(lv <= 1 ? ('\\section*{' + txt + '}') : lv === 2 ? ('\\section{' + txt + '}') : lv === 3 ? ('\\subsection{' + txt + '}') : ('\\subsubsection{' + txt + '}'))
|
|
4521
|
+
continue
|
|
4522
|
+
}
|
|
4523
|
+
const li = /^\s*[-*]\s+(.*)$/.exec(line)
|
|
4524
|
+
if (li) { flush(); if (!inList) { out.push('\\begin{itemize}'); inList = true } out.push(' \\item ' + paperInlineToTex(li[1])); continue }
|
|
4525
|
+
if (!line.trim()) { flush(); closeList(); out.push(''); continue }
|
|
4526
|
+
buf.push(line.trim())
|
|
4527
|
+
}
|
|
4528
|
+
flush(); closeList()
|
|
4529
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
4530
|
+
}
|
|
4531
|
+
/** 主模板(spec §5):中文 ctexart / 英文 article,只加常见宏包。 */
|
|
4532
|
+
function paperTexDoc(o) {
|
|
4533
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
4534
|
+
return [
|
|
4535
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
4536
|
+
'\\usepackage[margin=2.5cm]{geometry}',
|
|
4537
|
+
'\\usepackage{amsmath,amssymb,amsthm}',
|
|
4538
|
+
'\\usepackage{hyperref}',
|
|
4539
|
+
'\\usepackage{longtable,booktabs}',
|
|
4540
|
+
'\\title{' + texEscape(o && o.title) + '}',
|
|
4541
|
+
'\\author{' + texEscape(o && o.author) + '}',
|
|
4542
|
+
'\\date{' + texEscape(o && o.date) + '}',
|
|
4543
|
+
'\\begin{document}',
|
|
4544
|
+
'\\maketitle',
|
|
4545
|
+
String((o && o.bodyTex) || ''),
|
|
4546
|
+
'\\end{document}',
|
|
4547
|
+
].join('\n') + '\n'
|
|
4548
|
+
}
|
|
4549
|
+
/** 修复阶段的**最小模板**(spec §5):只留 documentclass + 正文。 */
|
|
4550
|
+
function paperMinimalTexDoc(o) {
|
|
4551
|
+
const lang = (o && o.lang === 'en') ? 'en' : 'zh'
|
|
4552
|
+
return [
|
|
4553
|
+
'\\documentclass[11pt]{' + (lang === 'en' ? 'article' : 'ctexart') + '}',
|
|
4554
|
+
'\\begin{document}',
|
|
4555
|
+
String((o && o.bodyTex) || ''),
|
|
4556
|
+
'\\end{document}',
|
|
4557
|
+
].join('\n') + '\n'
|
|
4558
|
+
}
|
|
4559
|
+
/** 修复阶段:丢弃不在常见宏包名单里的 \usepackage(其余原样保留)。 */
|
|
4560
|
+
function paperSanitizeTex(tex) {
|
|
4561
|
+
const keep = []
|
|
4562
|
+
const lines = String(tex == null ? '' : tex).split('\n')
|
|
4563
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4564
|
+
const m = /^\s*\\usepackage(\[[^\]]*\])?\{([^}]*)\}\s*$/.exec(lines[i])
|
|
4565
|
+
if (m) {
|
|
4566
|
+
const pkgs = String(m[2]).split(',').map(function (x) { return x.trim() }).filter(Boolean)
|
|
4567
|
+
const ok = pkgs.filter(function (p) { return PAPER_ALLOWED_PACKAGES.indexOf(p) >= 0 })
|
|
4568
|
+
if (ok.length === 0) continue
|
|
4569
|
+
keep.push('\\usepackage' + (m[1] || '') + '{' + ok.join(',') + '}')
|
|
4570
|
+
continue
|
|
4571
|
+
}
|
|
4572
|
+
keep.push(lines[i])
|
|
4573
|
+
}
|
|
4574
|
+
return keep.join('\n')
|
|
4575
|
+
}
|
|
4576
|
+
/** 骨架名匹配:允许带序号/前缀,或只写节名的一部分(例如「附录」)。 */
|
|
4577
|
+
function paperSkeletonKey(name) {
|
|
4578
|
+
const n = String(name == null ? '' : name).replace(/^#+\s*/, '').trim()
|
|
4579
|
+
if (!n) return undefined
|
|
4580
|
+
for (const s of PAPER_SKELETON) if (n === s.key) return s.key
|
|
4581
|
+
for (const s of PAPER_SKELETON) if (n.indexOf(s.key) !== -1 || s.key.indexOf(n) !== -1) return s.key
|
|
4582
|
+
return undefined
|
|
4583
|
+
}
|
|
4584
|
+
/** 按 `## ` 把任意 markdown 切成 [{name,text}](无标题时 name='')。 */
|
|
4585
|
+
function paperSplitByHeadings(md) {
|
|
4586
|
+
const out = []
|
|
4587
|
+
let cur = { name: '', text: [] }
|
|
4588
|
+
const lines = String(md == null ? '' : md).split(/\r?\n/)
|
|
4589
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4590
|
+
const h = /^##\s+(.*?)\s*$/.exec(lines[i])
|
|
4591
|
+
if (h) { if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() }); cur = { name: h[1], text: [] }; continue }
|
|
4592
|
+
cur.text.push(lines[i])
|
|
4593
|
+
}
|
|
4594
|
+
if (cur.name || cur.text.length) out.push({ name: cur.name, text: cur.text.join('\n').trim() })
|
|
4595
|
+
return out
|
|
4596
|
+
}
|
|
4597
|
+
/** 把「论文撰写」子代理的回复映射到 9 节骨架上(**绝不编造**:缺的节由调用方填占位说明)。 */
|
|
4598
|
+
function paperSkeletonFromReply(reply, fallbackText) {
|
|
4599
|
+
const sections = {}
|
|
4600
|
+
const notes = []
|
|
4601
|
+
let title = ''
|
|
4602
|
+
let abstract = ''
|
|
4603
|
+
const put = function (key, body) {
|
|
4604
|
+
const b = String(body == null ? '' : body).trim()
|
|
4605
|
+
if (!b) return
|
|
4606
|
+
sections[key] = sections[key] ? (sections[key] + '\n\n' + b) : b
|
|
4607
|
+
}
|
|
4608
|
+
const absorbUnknown = function (name, text) {
|
|
4609
|
+
if (!name && !String(text || '').trim()) return
|
|
4610
|
+
notes.push('作者写了一节「' + (name || '(无名)') + '」,不属于固定 9 节骨架,已并入「讨论、局限与展望」')
|
|
4611
|
+
put('讨论、局限与展望', (name ? ('**' + name + '**\n\n') : '') + text)
|
|
4612
|
+
}
|
|
4613
|
+
const obj = (reply && typeof reply === 'object' && !Array.isArray(reply)) ? reply : null
|
|
4614
|
+
if (obj) {
|
|
4615
|
+
title = String(obj.title || obj.标题 || '').trim()
|
|
4616
|
+
abstract = String(obj.abstract || obj.摘要 || '').trim()
|
|
4617
|
+
const list = Array.isArray(obj.sections) ? obj.sections : (Array.isArray(obj.章节) ? obj.章节 : [])
|
|
4618
|
+
for (let i = 0; i < list.length; i++) {
|
|
4619
|
+
const s = list[i]
|
|
4620
|
+
if (!s) continue
|
|
4621
|
+
const name = String(s.name || s.key || s.heading || s.标题 || '')
|
|
4622
|
+
const body = String(s.body != null ? s.body : (s.content != null ? s.content : (s.内容 != null ? s.内容 : '')))
|
|
4623
|
+
const key = paperSkeletonKey(name)
|
|
4624
|
+
if (key === undefined) absorbUnknown(name.trim(), body)
|
|
4625
|
+
else put(key, body)
|
|
4626
|
+
}
|
|
4627
|
+
const bodyField = String(obj.body || obj.content || obj.md || obj.正文 || '')
|
|
4628
|
+
if (bodyField.trim()) {
|
|
4629
|
+
const parts = paperSplitByHeadings(bodyField)
|
|
4630
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4631
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
4632
|
+
if (key === undefined) { if (!parts[i].name && !abstract && !sections['摘要']) abstract = parts[i].text; else if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
4633
|
+
else put(key, parts[i].text)
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
} else {
|
|
4637
|
+
const parts = paperSplitByHeadings(String(fallbackText == null ? '' : fallbackText))
|
|
4638
|
+
let matched = 0
|
|
4639
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4640
|
+
const key = paperSkeletonKey(parts[i].name)
|
|
4641
|
+
if (key === undefined) { if (parts[i].name) absorbUnknown(parts[i].name, parts[i].text) }
|
|
4642
|
+
else { put(key, parts[i].text); matched++ }
|
|
4643
|
+
}
|
|
4644
|
+
if (matched === 0) {
|
|
4645
|
+
const raw = String(fallbackText == null ? '' : fallbackText).trim()
|
|
4646
|
+
if (raw) { notes.push('作者回复不是契约 JSON,也没有可识别的 `## ` 节标题;其原样正文已记入「讨论、局限与展望」'); put('讨论、局限与展望', raw) }
|
|
4647
|
+
else notes.push('作者回复为空:没有任何内容可写(不得编造)')
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
return { title: title, abstract: abstract, sections: sections, notes: notes }
|
|
4651
|
+
}
|
|
4652
|
+
/** 组装 md(spec §3):9 节固定骨架 + 确定性证据索引;缺证据的节写占位说明。 */
|
|
4653
|
+
function paperBuildMarkdown(o) {
|
|
4654
|
+
const sec = (o && o.sections) || {}
|
|
4655
|
+
const ev = ((o && o.evidence) || []).slice()
|
|
4656
|
+
const L = []
|
|
4657
|
+
const ttl = String((o && o.title) || '').trim() || '研究报告'
|
|
4658
|
+
L.push('# ' + ttl)
|
|
4659
|
+
L.push('')
|
|
4660
|
+
L.push('- 作者:' + String((o && o.author) || ''))
|
|
4661
|
+
L.push('- 日期:' + String((o && o.date) || ''))
|
|
4662
|
+
L.push('')
|
|
4663
|
+
for (let i = 0; i < PAPER_SKELETON.length; i++) {
|
|
4664
|
+
const key = PAPER_SKELETON[i].key
|
|
4665
|
+
L.push('## ' + key)
|
|
4666
|
+
L.push('')
|
|
4667
|
+
let body = String(sec[key] || '').trim()
|
|
4668
|
+
if (key === '摘要') { const ab = String((o && o.abstract) || '').trim(); if (ab) body = body ? (ab + '\n\n' + body) : ab }
|
|
4669
|
+
if (key === '附录:证据与文件索引' && ev.length) {
|
|
4670
|
+
const idx = '### ' + PAPER_EVIDENCE_HEADING + '\n\n' + ev.map(function (x) { return '- `' + String(x) + '`' }).join('\n')
|
|
4671
|
+
body = body ? (body + '\n\n' + idx) : idx
|
|
4672
|
+
}
|
|
4673
|
+
L.push(body || PAPER_NO_EVIDENCE)
|
|
4674
|
+
L.push('')
|
|
4675
|
+
}
|
|
4676
|
+
return L.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
4677
|
+
}
|
|
4678
|
+
/** paper.log.md 的**唯一格式**(v2/v3 逐字一致,spec §4)。 */
|
|
4679
|
+
function paperLogHead(id) {
|
|
4680
|
+
return ['# 论文写作日志|' + String(id), '',
|
|
4681
|
+
'> 单作者流程(v2/v3 同一格式):收口触发 → 派遣「论文撰写」子代理 → 落盘 md/tex → 编译 pdf。', '']
|
|
4682
|
+
}
|
|
4683
|
+
function paperLogLine(at, tag, text) { return '- ' + fmtTime(at) + ' [' + String(tag) + '] ' + String(text == null ? '' : text) }
|
|
4684
|
+
/** 论文目录 id:只允许安全字符(同一把归一化 ⇒ 不可能写出 Paper/ 之外,spec §6 越权用例)。 */
|
|
4685
|
+
function paperDirId(raw) { return (idSafe(String(raw == null ? '' : raw).trim()) || 'run') }
|
|
4686
|
+
/** 从回复里抽出可能存在的标题行(md 非契约路径的兜底)。 */
|
|
4687
|
+
function paperTitleFromMd(md) {
|
|
4688
|
+
const m = /^#\s+(.*?)\s*$/m.exec(String(md == null ? '' : md))
|
|
4689
|
+
return m ? m[1].trim() : ''
|
|
4690
|
+
}
|
|
4691
|
+
|
|
3924
4692
|
export const __testHelpers = {
|
|
3925
4693
|
uuid,
|
|
3926
4694
|
shortId,
|
|
@@ -3948,6 +4716,27 @@ export const __testHelpers = {
|
|
|
3948
4716
|
parseMethodMd,
|
|
3949
4717
|
sanitizeToolFilter,
|
|
3950
4718
|
registeredToolsFromError,
|
|
4719
|
+
// final paper(spec §6 的纯函数守卫面;与 v2 同构)
|
|
4720
|
+
PAPER_SKELETON,
|
|
4721
|
+
PAPER_NO_EVIDENCE,
|
|
4722
|
+
PAPER_ALLOWED_PACKAGES,
|
|
4723
|
+
PAPER_ENGINE_ORDER_ZH,
|
|
4724
|
+
PAPER_ENGINE_ORDER_EN,
|
|
4725
|
+
texEscape,
|
|
4726
|
+
paperMaterialSummary,
|
|
4727
|
+
paperWriterVerdict,
|
|
4728
|
+
paperInlineToTex,
|
|
4729
|
+
paperMdToTexBody,
|
|
4730
|
+
paperTexDoc,
|
|
4731
|
+
paperMinimalTexDoc,
|
|
4732
|
+
paperSanitizeTex,
|
|
4733
|
+
paperSkeletonKey,
|
|
4734
|
+
paperSkeletonFromReply,
|
|
4735
|
+
paperBuildMarkdown,
|
|
4736
|
+
paperLogHead,
|
|
4737
|
+
paperLogLine,
|
|
4738
|
+
paperDirId,
|
|
4739
|
+
paperTitleFromMd,
|
|
3951
4740
|
}
|
|
3952
4741
|
|
|
3953
4742
|
function now() { return Date.now() }
|
|
@@ -3968,7 +4757,7 @@ const TOOL_DESC = {
|
|
|
3968
4757
|
vibe_math_set_mode: 'Switch between manual and auto (preset) mode. Switching to auto auto-resolves any pending manual decisions.',
|
|
3969
4758
|
// set_params 的 schema 现在也收 mode(M10):同一能力既有专用工具 vibe_math_set_mode,也可用这个键;
|
|
3970
4759
|
// 两条注册路径共用这一份描述,`--self-probe` 会证明漂移能被抓到。
|
|
3971
|
-
vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean
|
|
4760
|
+
vibe_math_set_params: 'Update scheduler parameters (partial). Lean 形式化验证:formalVerify = off(默认,不额外要求)| encourage(按实现难度自行决定是否形式化;一旦 Lean 通过,验证转为对 Lean 陈述的「忠实性审查」)| require(同上,且加门禁:对象未达到 Lean 已通过或已记录显式阻塞原因之前,真/假裁定记为未定论、原因 formal-required,并进入 Formal/TODO.md);leanCommand/leanArgs/leanTimeoutMs 控制 Lean 工具链的调用方式。最终论文:finalPaper(默认 true;收口时自动派遣一名「论文撰写」子代理)/ paperFormat = both|md|tex / paperLanguage = zh|en / paperCompilePdf(检测到 LaTeX 时编译 paper.pdf)/ paperLatexCommand(指定引擎,空 = 自动探测 xelatex→latexmk→pdflatex→lualatex→tectonic),产物在 Paper/<项目>/。',
|
|
3972
4761
|
vibe_math_setup: 'Return the interactive parameter schema for guided configuration.',
|
|
3973
4762
|
vibe_math_save_settings: 'Write the current params to vibe_math_setting.json (JSON with comments) as new defaults.',
|
|
3974
4763
|
vibe_math_template: 'Create a fresh vibe_math_setting.json template (with defaults + comments) in the workspace (global) or current project folder.',
|