dsh-vibe-math 2.0.2 → 2.0.4
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.md
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
**一句话流水线**:起始产生 N 个**常驻子代理**(continuable,持久上下文)先各自头脑风暴、产出初始见解/方向 → 此后**所有任务安排由它们互相留言 + 集体开会自主决定**(框架只做消息总线/会议/任务板/产物沉淀,**绝不分配任务**);每个常驻把有价值的产物按**价值程度 / 动机用途计划 / 自身概率估计**沉淀到**自己**的 `Progress/<id>/`、`Propos/<id>/`、`Methods/<id>/`、`Subproblems/<id>/` 库,并**可互相阅读**;验证由它们**自行商议**发起,**仅当全体常驻一致(真或假)**才写入 `Verified/`,否则留库附概率;常驻上下文量达阈值(默认 66%)自动 `/compact`;**仅当全体一致认为原问题已解决**才停止;可随时人工干预/增开/关闭常驻,支持断点续跑。
|
|
41
41
|
|
|
42
42
|
> 说明:V4 去掉 v3 的中央规划器与确定性角色(explorer/solver/verifier/planner/method-keeper),把"研究者"本身作为主体。详见 `vibe-math-v4/实现方案.md`。
|
|
43
|
+
> 保活机制(分级保活 A+B):团伙空闲超 `activityTimeoutMs` 会收到**自驱动** CHECKPOINT(建议继续解决/发消息/提议任务,而非"是否要停止"),唤醒失败会自动重新武装心跳;若团队空闲且**无新产物**超过 `stallAutoMeetingMs`(默认 6 分钟),框架会自动召集一次同步会议让常驻们自行决定下一步——因此小组不会"跑完就永久停滞"(框架只促成、从不指派任务)。
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -97,7 +98,8 @@ dsh plugin --profile <你的 profile> add github:ChongCyrus/Vibe-Mathematics
|
|
|
97
98
|
- **形态依赖**:三个 preset 依赖 DSH 的标准 **agent-preset 机制**(`~/.dsh/.agent-presets/<id>/` + preset picker)与 **bundle patch 机制**(`cordis.patch.yml` 注入安装器)。
|
|
98
99
|
- **宿主插件行**:`agent.cordis.yml` 引用宿主提供的 `@deepseek-ai/dsh-*` 插件行(persona、agent-instructions、tool-bash/pwsh、tool-fs/fs-search、tool-jobs、skill-filesystem、tool-skill、tool-goal、plan-mode、compaction、subagent/workflow、ask-user、todo、web 等,约 21 个唯一包名)。宿主缺行会导致 preset 挂载失败(会话启动时报错)。
|
|
99
100
|
- **宿主服务 API**:预设插件消费 `subagents`(startContinuable / followup / interrupt)、`agents`(roots)、`tools`(register)、`commands`(register)、`fs`(resolve/stat/readText/writeText/listDir)、可选 `subprocess` / `sandboxPolicy`。这些 API 形状随 DSH 版本演进;本项目**已充分测试并确认适配 `dsh-v0.1.2-rc.1`**(`package.json` 的 `dsh.testedVersion`;`minVersion` 为 `0.1.2-rc.1`)。**注意:DSH 0.1.2 起 `subagents.startContinuable` 的 `agentOptions` / `toolFilter` 需要宿主 provider 声明对应 capability**(spawn / fork 进程内 provider 均支持,v4 指定常驻模型/路由与工具权限依赖于此)。
|
|
100
|
-
-
|
|
101
|
+
- **DSH STORE 兼容声明**:`package.json` 的 `dsh.compatibility.dshReleases` 对每个完整 DSH 版本逐项声明 `compatible` / `incompatible` / `unknown`(当前 `0.1.2-alpha.4`、`0.1.2-alpha.5`、`0.1.2-rc.1` 均标 `compatible`);`engines.node` 为 `^22.19.0 || >=24.0.0`(与 DSH 0.1.2 相同)。
|
|
102
|
+
- **运行时自检(能力 + 版本双检)**:安装器(bundle 插件)每次启动时:**① 尽力探测 DSH 版本**(读 `@deepseek-ai/dsh/package.json` 或 `DSH_VERSION` 环境变量;DSH 未通过公开 service/context 暴露版本,故为尽力而为,探测不到就跳过)。若探测到且该版本未被 `dshReleases` 声明为 `compatible`,会给出明确"DSH 版本不匹配,请使用 `dsh-v0.1.2-rc.1`(或 `0.1.2-alpha.4/alpha.5`)"提示;**② 再对上述服务与关键 API 做能力自检**(这是真正的挂载门槛,含 `fs.resolve` 返回形状检测与 subagent `agentOptions`/`toolFilter` capability 检测),不满足时打 warning。preset 挂载失败时先看 DSH 日志里的自检 warning。
|
|
101
103
|
- **升级路径**:DSH 升级后无需重装本包;升级本包用 `dsh plugin update dsh-vibe-math`,重启 DSH 后安装器会自动把 preset 更新到新版本(见上文「安装」说明)。
|
|
102
104
|
|
|
103
105
|
---
|
|
@@ -415,7 +417,8 @@ dsh plugin --profile <你的 profile> add github:ChongCyrus/Vibe-Mathematics
|
|
|
415
417
|
| `compactAfterRounds` | 8 | 常驻每累计 N 轮(未压缩)触发一次软压缩 |
|
|
416
418
|
| `meetingKeepEvery` | 5 | 每积累 N 个新产物自动触发一次同步会议 |
|
|
417
419
|
| `maxParallel` | 3 | 同时唤醒的常驻上限(框架侧并发闸,非指派) |
|
|
418
|
-
| `activityTimeoutMs` | 120000 |
|
|
420
|
+
| `activityTimeoutMs` | 120000 | 空闲心跳间隔(超时才触发**自驱动** CHECKPOINT 唤醒,推动常驻继续推进;唤醒失败会自动重新武装心跳,保证小组永不永久停死) |
|
|
421
|
+
| `stallAutoMeetingMs` | 360000 | **停滞自动同步会议阈值**(分级保活 B):团队空闲且无新产物超过该时长时,框架自动召集一次同步会议,让常驻们自行决定下一步路线/分工(框架只促成,不指派) |
|
|
419
422
|
| `verdictMaxRounds` | 3 | 验证在独立初评后进入辩论的最大轮数 |
|
|
420
423
|
| `provider` / `model` | 空 | **常驻 LLM 路由**(空 = 常驻继承主代理的 provider/model;此前声明未用,v1.4.1 真正接入) |
|
|
421
424
|
| `residentPersona` | 空 | 注入每个常驻提示词开头的人格/要求 |
|
package/installer.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// - same version: no-op (idempotent). Missing files are ALWAYS restored.
|
|
26
26
|
// - force a full refresh at any time: delete the preset dirs and restart DSH.
|
|
27
27
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, unlinkSync, rmdirSync } from 'node:fs'
|
|
28
|
+
import { createRequire } from 'node:module'
|
|
28
29
|
import { createHash } from 'node:crypto'
|
|
29
30
|
import { homedir } from 'node:os'
|
|
30
31
|
import { dirname, join } from 'node:path'
|
|
@@ -75,8 +76,42 @@ function writeState(path, state) {
|
|
|
75
76
|
|
|
76
77
|
// DSH 适配性自检(能力检测,而非版本号——DSH 不向插件暴露版本)。
|
|
77
78
|
// 检查 preset 运行时需要的宿主服务与关键 API 形状是否可用,缺失时打 warning。
|
|
79
|
+
// Best-effort DSH host-version detection. DSH does NOT expose its version through a documented
|
|
80
|
+
// service/context property or a guaranteed env var, so we probe in order: an explicit env var
|
|
81
|
+
// (future-proofing), then the installed @deepseek-ai/dsh package.json. This is layout-dependent
|
|
82
|
+
// (works for a typical global install where @deepseek-ai/dsh is a sibling of this plugin); when it
|
|
83
|
+
// cannot resolve, the capability self-check below is still the authoritative gate.
|
|
84
|
+
const __require = createRequire(import.meta.url)
|
|
85
|
+
function detectDshVersion() {
|
|
86
|
+
try { const v = process.env.DSH_VERSION; if (v && String(v).trim()) return String(v).trim() } catch (e) {}
|
|
87
|
+
try {
|
|
88
|
+
const p = __require.resolve('@deepseek-ai/dsh/package.json')
|
|
89
|
+
const v = (JSON.parse(readFileSync(p, 'utf8')).version || '').trim()
|
|
90
|
+
if (v) return v
|
|
91
|
+
} catch (e) { /* host package not resolvable from here — rely on capability check */ }
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
async function checkHostCapabilities(ctx, logger) {
|
|
79
96
|
const problems = []
|
|
97
|
+
// 1) DSH version compatibility (best-effort, only when the version is detectable).
|
|
98
|
+
// Declared under package.json dsh.compatibility.dshReleases (per the DSH STORE contract):
|
|
99
|
+
// each full DSH release maps to 'compatible' | 'incompatible' | 'unknown'. A version that is
|
|
100
|
+
// absent or 'unknown' is a soft warning; 'incompatible' is a hard "please use X" message.
|
|
101
|
+
let dshRel = {}
|
|
102
|
+
try { const m = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')); dshRel = (m.dsh && m.dsh.compatibility && m.dsh.compatibility.dshReleases) || {} } catch (e) {}
|
|
103
|
+
const supported = Object.keys(dshRel).sort()
|
|
104
|
+
const dshVersion = detectDshVersion()
|
|
105
|
+
if (dshVersion) {
|
|
106
|
+
const status = dshRel[dshVersion]
|
|
107
|
+
if (status === 'incompatible') {
|
|
108
|
+
problems.push('当前 DSH 版本 v' + dshVersion + ' 被本包声明为 incompatible;请使用 ' + supported.join(' / ') + '。')
|
|
109
|
+
} else if (status === undefined || status === 'unknown') {
|
|
110
|
+
problems.push('当前 DSH 版本 v' + dshVersion + ' 尚未被本包声明为兼容(dshReleases 仅声明 ' + supported.join(' / ') + ');建议使用 ' + supported.join(' / ') + ',或将该版本在 dshReleases 中标注后再自行验证。')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 2) capability self-check (the authoritative mounting gate; also covers hosts whose version
|
|
114
|
+
// could not be read). subagents / agents / tools / commands / fs shapes + v4 capabilities.
|
|
80
115
|
const checks = [
|
|
81
116
|
['subagents', ['startContinuable', 'followup', 'interrupt']],
|
|
82
117
|
['agents', ['roots']],
|
|
@@ -120,9 +155,9 @@ async function checkHostCapabilities(ctx, logger) {
|
|
|
120
155
|
}
|
|
121
156
|
} catch (e) { /* 探测失败不致命 */ }
|
|
122
157
|
if (problems.length > 0) {
|
|
123
|
-
logger?.warn?.('[dsh-vibe-math]
|
|
158
|
+
logger?.warn?.('[dsh-vibe-math] 宿主自检:' + problems.length + ' 项不满足(' + problems.join(';') + ')。v2/v3/v4 预设依赖这些宿主服务/API,旧版或未经声明兼容的 DSH 可能无法挂载' + (dshVersion ? '(当前检测到 DSH v' + dshVersion + ',本包适配 ' + (supported.length ? supported.join(' / ') : '(未声明)') + ')' : '') + '。')
|
|
124
159
|
} else {
|
|
125
|
-
logger?.info?.('[dsh-vibe-math]
|
|
160
|
+
logger?.info?.('[dsh-vibe-math] 宿主自检通过:subagents / agents / tools / commands / fs 服务及关键 API 均可用' + (dshVersion ? '(当前 DSH v' + dshVersion + ',本包已声明兼容 ' + supported.join(' / ') + ')' : '') + '。')
|
|
126
161
|
}
|
|
127
162
|
}
|
|
128
163
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vibe-math",
|
|
3
3
|
"description": "Multi-agent mathematical problem-solving & verification frameworks for DeepSeek Harness — THREE agent presets in one install: vibe-math-v2 (probability-driven: qs.json + Propos knowledge base + explorer→solver→review/debate verdict), vibe-math-v3 (THIRD-generation, recommended: paper-style Markdown knowledge base with Problems/Progress/Propos/Methods/Verified + planner-agent scheduling that decides the next N actions + universal theory/method invention library + agents write their own Markdown directly via a per-file write lock), and vibe-math-v4 (FOURTH-generation: persistent self-organizing resident subagents that message & meet to decide all tasks, verify only by unanimous consensus, /compact at a context threshold, and stop only when all agree the problem is solved). Installing this bundle auto-installs all three presets (v1 was removed at v2.0.0).",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.4",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
+
},
|
|
6
9
|
"main": "installer.js",
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./installer.js",
|
|
@@ -54,6 +57,13 @@
|
|
|
54
57
|
},
|
|
55
58
|
"minVersion": "0.1.2-rc.1",
|
|
56
59
|
"testedVersion": "0.1.2-rc.1",
|
|
57
|
-
"compatNote": "依赖宿主提供的 subagents/agents/tools/commands/fs 服务与 @deepseek-ai/dsh-* 插件行。本项目已充分测试并确认适配 dsh-v0.1.2-rc.1(v2/v3/v4;v1 已于 v2.0.0 移除)。注意:DSH 0.1.2 起 subagents.startContinuable 的 agentOptions/toolFilter 需要宿主 provider 声明对应 capability(spawn/fork 进程内 provider 均支持),安装器启动时会做能力自检并在旧版宿主上告警。"
|
|
60
|
+
"compatNote": "依赖宿主提供的 subagents/agents/tools/commands/fs 服务与 @deepseek-ai/dsh-* 插件行。本项目已充分测试并确认适配 dsh-v0.1.2-rc.1(v2/v3/v4;v1 已于 v2.0.0 移除)。注意:DSH 0.1.2 起 subagents.startContinuable 的 agentOptions/toolFilter 需要宿主 provider 声明对应 capability(spawn/fork 进程内 provider 均支持),安装器启动时会做能力自检并在旧版宿主上告警。",
|
|
61
|
+
"compatibility": {
|
|
62
|
+
"dshReleases": {
|
|
63
|
+
"0.1.2-alpha.4": "compatible",
|
|
64
|
+
"0.1.2-alpha.5": "compatible",
|
|
65
|
+
"0.1.2-rc.1": "compatible"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
Main controls (recommended flow: configure FIRST, then start):
|
|
62
62
|
- vibe_v4_configure {project?, problem?, params?} — create/configure the project (name, problem, params) WITHOUT starting a run; set everything here first.
|
|
63
63
|
- vibe_v4_start {problem?, residentCount?, seedDirections?} — begin the run (spawn residents, brainstorm). If problem was configured, omit it.
|
|
64
|
-
- vibe_v4_set {residentCount, compactThreshold, compactAfterRounds, meetingKeepEvery, maxParallel, activityTimeoutMs, verdictMaxRounds, provider, model, residentPersona, toolAllow, toolDeny} — tune params (persisted to the settings file). provider/model override the residents' LLM route (empty = they inherit YOUR model/provider); toolAllow/toolDeny are per-resident tool permissions (empty = they inherit all tools).
|
|
64
|
+
- vibe_v4_set {residentCount, compactThreshold, compactAfterRounds, meetingKeepEvery, maxParallel, activityTimeoutMs, stallAutoMeetingMs, verdictMaxRounds, provider, model, residentPersona, toolAllow, toolDeny} — tune params (persisted to the settings file). provider/model override the residents' LLM route (empty = they inherit YOUR model/provider); toolAllow/toolDeny are per-resident tool permissions (empty = they inherit all tools). stallAutoMeetingMs is the stalled-group auto-sync-meeting threshold (分级保活 B).
|
|
65
65
|
- vibe_v4_resume / vibe_v4_pause / vibe_v4_abort / vibe_v4_status / vibe_v4_report.
|
|
66
66
|
- vibe_v4_message {to|all, content} — inject a message to a resident (human/assistant intervention).
|
|
67
67
|
- vibe_v4_meeting {agenda} — force a meeting.
|
|
@@ -32,6 +32,7 @@ export function apply(ctx) {
|
|
|
32
32
|
residentCount: 4, compactThreshold: 66, compactAfterRounds: 8,
|
|
33
33
|
maxParallel: 3, activityTimeoutMs: 120000, verdictMaxRounds: 3,
|
|
34
34
|
meetingKeepEvery: 5, // 每积累 N 个新产物自动触发一次同步会议
|
|
35
|
+
stallAutoMeetingMs: 360000, // 团队空闲且无新产物的"停滞阈值":超过则自动召集同步会议(分级保活 B)
|
|
35
36
|
// model/provider inheritance: '' = the resident inherits the parent (main assistant)
|
|
36
37
|
// route (provider + model). Set them to override the resident's LLM backend/model.
|
|
37
38
|
provider: '', model: '', residentPersona: '',
|
|
@@ -47,7 +48,7 @@ export function apply(ctx) {
|
|
|
47
48
|
let problemText = '', problemId = 'problem', runId = 'run-' + shortId()
|
|
48
49
|
let meetingState = null, verifyState = null, pendingVerify = null
|
|
49
50
|
let busy = new Set(), wakeKind = new Map(), currentResident = ''
|
|
50
|
-
let lastActivityAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
|
|
51
|
+
let lastActivityAt = now(), lastProgressAt = now(), artifactCount = 0, lastSyncMeetingAt = 0, persistedEpoch = '', heartbeatDisposer = null
|
|
51
52
|
const activityLogCap = 200
|
|
52
53
|
|
|
53
54
|
// ---- utils ----
|
|
@@ -64,6 +65,11 @@ export function apply(ctx) {
|
|
|
64
65
|
function blocksToText(b){ if(!b) return ''; let out=''; for(const x of b){ if(x&&x.type==='text'&&typeof x.text==='string') out+=x.text+'\n' } return out.trim() }
|
|
65
66
|
function logActivity(event,detail){ activityLog.push({at:now(),event,detail:String(detail||'')}); if(activityLog.length>activityLogCap) activityLog.shift() }
|
|
66
67
|
function logDecision(kind,detail){ decisions.push({at:now(),kind,detail:String(detail||'')}) }
|
|
68
|
+
// Record that the project made real progress (new artifact, meeting, verify, task, or a
|
|
69
|
+
// resident speaking to the group). The stall auto-sync meeting (B) fires only when this has
|
|
70
|
+
// NOT advanced for stallAutoMeetingMs, so a group that is genuinely producing keeps working
|
|
71
|
+
// and only a truly stalled group gets a coordination meeting to reboot itself.
|
|
72
|
+
function markProgress(){ lastProgressAt = now(); }
|
|
67
73
|
function pickProvider(){ try { const n=subagents.list?subagents.list():[]; if(n.indexOf('spawn')!==-1) return 'spawn'; if(n.indexOf('fork')!==-1) return 'fork' } catch(e){} return 'spawn' }
|
|
68
74
|
// Per-resident model/provider inheritance: when params.provider / params.model are set,
|
|
69
75
|
// the resident uses that exact route; when left '' the resident inherits the parent's
|
|
@@ -110,10 +116,10 @@ export function apply(ctx) {
|
|
|
110
116
|
await writeJson('State/mailboxes.json', Object.fromEntries(mailboxes))
|
|
111
117
|
await writeJson('State/taskboard.json', taskboard)
|
|
112
118
|
await writeJson('State/decisions.json', decisions)
|
|
113
|
-
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,activityLog,processEpoch,artifactCount})
|
|
119
|
+
await writeJson('State/session.json', {running,autoDone,phase,problemId,problemText,runId,meetings,reports,lastActivityAt,lastProgressAt,activityLog,processEpoch,artifactCount})
|
|
114
120
|
}
|
|
115
121
|
async function loadAll(){
|
|
116
|
-
const s=await readJson('State/session.json'); if(s){ running=!!s.running; autoDone=!!s.autoDone; phase=s.phase||'idle'; problemId=s.problemId||problemId; problemText=s.problemText||problemText; runId=s.runId||runId; meetings=s.meetings||[]; reports=s.reports||[]; lastActivityAt=s.lastActivityAt||now(); activityLog=s.activityLog||activityLog; persistedEpoch=s.processEpoch||''; artifactCount=s.artifactCount||0 }
|
|
122
|
+
const s=await readJson('State/session.json'); if(s){ running=!!s.running; autoDone=!!s.autoDone; phase=s.phase||'idle'; problemId=s.problemId||problemId; problemText=s.problemText||problemText; runId=s.runId||runId; meetings=s.meetings||[]; reports=s.reports||[]; lastActivityAt=s.lastActivityAt||now(); lastProgressAt=s.lastProgressAt||now(); activityLog=s.activityLog||activityLog; persistedEpoch=s.processEpoch||''; artifactCount=s.artifactCount||0 }
|
|
117
123
|
const rm=await readJson('State/residents.json'); if(rm&&typeof rm==='object') residents=new Map(Object.entries(rm))
|
|
118
124
|
const mb=await readJson('State/mailboxes.json'); if(mb&&typeof mb==='object') mailboxes=new Map(Object.entries(mb))
|
|
119
125
|
const tb=await readJson('State/taskboard.json'); if(Array.isArray(tb)) taskboard=tb
|
|
@@ -280,7 +286,7 @@ export function apply(ctx) {
|
|
|
280
286
|
async function recordMethod(rId,o){ const id=o.id||('m-'+shortId()); const lines=['# 方法|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 类型: '+(o.type||'方法'),'- 状态: 经验','- 可信断言: []','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'','## 核心内容',String(o.content||''),'','## 定义与记号',String(o.notation||''),'','## 应用记录','## 改进历史','']; await writeText('Methods/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 方法 '+id); bumpArtifacts(); return {ok:true,id,file:'Methods/'+rId+'/'+id+'.md'} }
|
|
281
287
|
async function recordSubproblem(rId,o){ const id=o.id||('s-'+shortId()); const lines=['# 子问题|'+(o.title||id),'- 标题: '+(o.title||id),'- ID: '+id,'- 状态: 求解中','- 价值程度: '+cl(o.value!=null?o.value:0.5),'- 动机用途计划: '+(o.motivation||''),'- 依赖: []','','## 陈述',String(o.statement||''),'','## 进度','']; await writeText('Subproblems/'+rId+'/'+id+'.md',lines.join('\n')); logActivity('record',rId+' 子问题 '+id); bumpArtifacts(); return {ok:true,id,file:'Subproblems/'+rId+'/'+id+'.md'} }
|
|
282
288
|
// auto-sync meeting: every meetingKeepEvery new artifacts, convene a general coordination meeting
|
|
283
|
-
function bumpArtifacts(){ artifactCount+=1; if(!meetingState && !verifyState && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
|
|
289
|
+
function bumpArtifacts(){ artifactCount+=1; markProgress(); if(!meetingState && !verifyState && Number(params.meetingKeepEvery)>0 && artifactCount % Number(params.meetingKeepEvery)===0){ startMeeting('定期同步:分工/进展/是否需要验证','general',null).catch(()=>{}) } }
|
|
284
290
|
function listResidents(){ return Array.from(residents.values()).map(r=>({id:r.rId,direction:r.direction,status:r.status,rounds:r.rounds,contextPct:r.contextPct,insight:r.insight?r.insight.slice(0,80):''})) }
|
|
285
291
|
// identify WHICH resident is calling a resident-facing tool: match the caller's
|
|
286
292
|
// subagent id to a resident's childId. Fall back to the last-woken resident when
|
|
@@ -290,12 +296,12 @@ export function apply(ctx) {
|
|
|
290
296
|
|
|
291
297
|
// ---- task board (residents propose / claim / complete; framework wakes the claimer) ----
|
|
292
298
|
async function writeTaskboard(){ const lines=['# 任务板','']; for(const t of taskboard){ lines.push('- ['+t.status+'] '+t.title+(t.claimer?('(认领:'+t.claimer+')'):'')+(t.proposer?('(提议:'+t.proposer+')'):'')+(t.description?(':'+t.description):'')) } await writeText('Shared/taskboard.md',lines.join('\n')) }
|
|
293
|
-
async function proposeTask(title,description,proposer){ const id='t-'+shortId(); taskboard.push({id,title:String(title),description:String(description||''),status:'open',proposer:proposer||'',claimer:'',source:''}); await saveTaskboard(); logActivity('task','proposed '+id+'「'+title+'」'); return {ok:true,id} }
|
|
294
|
-
async function claimTask(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false,message:'no such task'}; if(t.status!=='open') return {ok:false,message:'task already '+t.status}; t.status='claimed'; t.claimer=claimer; await saveTaskboard(); logActivity('task',claimer+' claimed '+id);
|
|
299
|
+
async function proposeTask(title,description,proposer){ const id='t-'+shortId(); taskboard.push({id,title:String(title),description:String(description||''),status:'open',proposer:proposer||'',claimer:'',source:''}); await saveTaskboard(); markProgress(); logActivity('task','proposed '+id+'「'+title+'」'); return {ok:true,id} }
|
|
300
|
+
async function claimTask(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false,message:'no such task'}; if(t.status!=='open') return {ok:false,message:'task already '+t.status}; t.status='claimed'; t.claimer=claimer; await saveTaskboard(); markProgress(); logActivity('task',claimer+' claimed '+id);
|
|
295
301
|
// wake the claimer to work on it (framework moves the task, resident decides how)
|
|
296
302
|
const r=residents.get(claimer); if(r && !busy.has(claimer)){ currentResident=claimer; await wakeResident(r, (await normalPrompt(r))+'\n\n[YOU CLAIMED TASK '+id+'] '+t.title+' — '+t.description,'normal'); await saveAll() }
|
|
297
303
|
return {ok:true} }
|
|
298
|
-
async function taskDone(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false}; t.status='done'; t.doneBy=claimer; await saveTaskboard(); await writeTaskboard(); logActivity('task','done '+id); return {ok:true} }
|
|
304
|
+
async function taskDone(id,claimer){ const t=taskboard.find(x=>x.id===id); if(!t) return {ok:false}; t.status='done'; t.doneBy=claimer; await saveTaskboard(); await writeTaskboard(); markProgress(); logActivity('task','done '+id); return {ok:true} }
|
|
299
305
|
async function saveTaskboard(){ await writeJson('State/taskboard.json',taskboard); await writeTaskboard() }
|
|
300
306
|
function listTasks(){ return taskboard.filter(t=>t.status!=='done') }
|
|
301
307
|
async function reportContext(rId,pct){ const r=residents.get(rId); if(r){ r.contextPct=clPct(pct); if(Number(pct)<30) r.needCompact=false; } return {ok:true} }
|
|
@@ -318,7 +324,7 @@ export function apply(ctx) {
|
|
|
318
324
|
if(!busy.has(to)){
|
|
319
325
|
currentResident=r.rId
|
|
320
326
|
await wakeResident(r, (await normalPrompt(r))+'\n\n[NEW MESSAGE from '+from+']\n'+content,'normal')
|
|
321
|
-
await saveAll(); logActivity('message',from+'→'+to); return {ok:true}
|
|
327
|
+
await saveAll(); markProgress(); logActivity('message',from+'→'+to); return {ok:true}
|
|
322
328
|
}
|
|
323
329
|
const mb=mailboxes.get(to)||[]; mb.push({from,at:now(),content}); mailboxes.set(to,mb); await saveAll(); logActivity('message',from+'→'+to+' (queued)'); return {ok:true}
|
|
324
330
|
}
|
|
@@ -336,7 +342,7 @@ export function apply(ctx) {
|
|
|
336
342
|
if(r.rId===from) continue
|
|
337
343
|
const mb=mailboxes.get(r.rId)||[]; mb.push({from,at:now(),content:'[群聊] '+text}); mailboxes.set(r.rId,mb)
|
|
338
344
|
}
|
|
339
|
-
logActivity('relay',from+' → 团队: '+text.slice(0,60)); await saveAll()
|
|
345
|
+
logActivity('relay',from+' → 团队: '+text.slice(0,60)); markProgress(); await saveAll()
|
|
340
346
|
}
|
|
341
347
|
|
|
342
348
|
// ---- meeting ----
|
|
@@ -349,6 +355,7 @@ export function apply(ctx) {
|
|
|
349
355
|
const rot=Math.floor(Math.random()*Math.max(1,ids.length))
|
|
350
356
|
const order=ids.slice(rot).concat(ids.slice(0,rot))
|
|
351
357
|
meetingState={id:'mt-'+shortId(),agenda,type:type||'general',targetId:targetId||null,round:0,asked:[],inputs:{},transcript:[],order}
|
|
358
|
+
markProgress();
|
|
352
359
|
logActivity('meeting','start: '+agenda); await saveAll(); await scheduleNext(); return {ok:true,id:meetingState.id}
|
|
353
360
|
}
|
|
354
361
|
async function continueMeetingRound(){
|
|
@@ -359,7 +366,8 @@ export function apply(ctx) {
|
|
|
359
366
|
const order=meetingState.order||ids
|
|
360
367
|
const id=order.find(x=>meetingState.inputs[x]===undefined && !busy.has(x)); if(!id) return
|
|
361
368
|
const r=residents.get(id)
|
|
362
|
-
await wakeResident(r, meetingPrompt(r,meetingState), 'meeting'); await saveAll()
|
|
369
|
+
const ok = await wakeResident(r, meetingPrompt(r,meetingState), 'meeting'); await saveAll()
|
|
370
|
+
if(!ok) armHeartbeat() // a failed meeting wake must NOT silently hang the meeting
|
|
363
371
|
}
|
|
364
372
|
async function finalizeMeeting(){
|
|
365
373
|
const st=meetingState
|
|
@@ -387,6 +395,7 @@ export function apply(ctx) {
|
|
|
387
395
|
clearHeartbeat()
|
|
388
396
|
pendingVerify=null
|
|
389
397
|
verifyState={targetId:pv.targetId,targetType:pv.targetType,targetOwner:pv.proposer||'',stage:'independent',round:0,asked:[],verdicts:{},transcript:[]}
|
|
398
|
+
markProgress();
|
|
390
399
|
logActivity('verify','debate begin: '+pv.targetId+' ('+pv.targetType+')'); await saveAll(); await scheduleNext()
|
|
391
400
|
}
|
|
392
401
|
async function continueVerifyRound(){
|
|
@@ -395,7 +404,8 @@ export function apply(ctx) {
|
|
|
395
404
|
if(allVoted){ await finalizeVerify(); return }
|
|
396
405
|
const id=ids.find(x=>verifyState.verdicts[x]===undefined && !busy.has(x)); if(!id) return
|
|
397
406
|
const r=residents.get(id)
|
|
398
|
-
await wakeResident(r, verifyPrompt(r,verifyState), verifyState.stage==='independent'?'verif-ind':'verif-deb'); await saveAll()
|
|
407
|
+
const ok = await wakeResident(r, verifyPrompt(r,verifyState), verifyState.stage==='independent'?'verif-ind':'verif-deb'); await saveAll()
|
|
408
|
+
if(!ok) armHeartbeat() // a failed verify wake must NOT silently hang the verification
|
|
399
409
|
}
|
|
400
410
|
async function finalizeVerify(){
|
|
401
411
|
const vs=verifyState; const expected=Array.from(residents.keys()).length
|
|
@@ -460,15 +470,15 @@ export function apply(ctx) {
|
|
|
460
470
|
|
|
461
471
|
// ---- heartbeat / liveness helpers (boundary-A: event-driven + gated heartbeat) ----
|
|
462
472
|
// A checkpoint wake is NOT "keep working forever": it nudges the least-recently-active
|
|
463
|
-
// resident, after an idle timeout, to
|
|
464
|
-
//
|
|
465
|
-
//
|
|
473
|
+
// resident, after an idle timeout, to CONTINUE the work itself (self-drive), and only to
|
|
474
|
+
// propose a meeting/verify/solved when it truly has nothing left. This keeps the group moving
|
|
475
|
+
// on its own (framework never assigns work) but adds convergence pressure instead of letting
|
|
476
|
+
// a stalled group sit idle forever.
|
|
466
477
|
function heartbeatPrompt(r){
|
|
467
478
|
return (params.residentPersona?params.residentPersona+'\n':'')
|
|
468
|
-
+'Resident researcher '+r.rId+' — CHECKPOINT
|
|
469
|
-
+'请用一句话说明你下一步做什么;若你已无产出、或认为问题已解决/接近解决,请**提议开会(propose_meeting)**、**提议验证(propose_verify)**、或**声明 solved=true**,让团队能做出决定——不要产出填充性工作。\n'
|
|
479
|
+
+'Resident researcher '+r.rId+' — CHECKPOINT(团队空闲,请由你们继续自主推进)。当前项目尚未解决(除非你已确认)。团队在等待有人继续:请**继续解决这个问题**——读他人的库对齐、推进某个子问题/引理/方法、尝试一条路线;或向团队发消息(input)、提议任务(propose_task)让大家分工。若你确实认为问题已解决、或已彻底无路可走,才提议开会(propose_meeting)让团队表决/商量、或声明 solved=true。默认立场是:**请推进,而不是停在原地。**\n'
|
|
470
480
|
+'Reply with ONLY a JSON object:\n'
|
|
471
|
-
+'{"summary":"<what you do
|
|
481
|
+
+'{"summary":"<what you will do / what you advanced this round>","input":"<optional: a message to the whole team, or \\"\\">","solved":false,"propose_verify":"<id|null>","propose_meeting":"<agenda|null>","propose_task":"<task title|null>","claim_task":"<id|null>","contextPct":40}'
|
|
472
482
|
}
|
|
473
483
|
function clearHeartbeat(){ if(heartbeatDisposer!==null){ try{ heartbeatDisposer() }catch(e){} heartbeatDisposer=null } }
|
|
474
484
|
function armHeartbeat(){
|
|
@@ -509,14 +519,30 @@ export function apply(ctx) {
|
|
|
509
519
|
// maxParallel: don't start a new wake when the in-flight cap is reached
|
|
510
520
|
const mp=Number(params.maxParallel)||0
|
|
511
521
|
if(mp>0 && busy.size>=mp){ armHeartbeat(); return }
|
|
512
|
-
//
|
|
522
|
+
// B) stall auto-sync meeting (分级保活 B): the group has been idle with NO progress for
|
|
523
|
+
// stallAutoMeetingMs → convene a sync meeting so the residents coordinate their next move
|
|
524
|
+
// (framework convenes & records; residents decide — never assigns work). Only when no
|
|
525
|
+
// meeting/verify/pending work is already active.
|
|
526
|
+
if(phase==='active' && !meetingState && !verifyState && !pendingVerify){
|
|
527
|
+
const stallMs=Number(params.stallAutoMeetingMs)||((Number(params.activityTimeoutMs)||120000)*3)
|
|
528
|
+
if(now()-lastProgressAt>=stallMs){
|
|
529
|
+
await startMeeting('团队较长时间没有新进展。请你们自行讨论:当前问题是否已解决、开放难点是什么、谁负责哪部分、下一步如何推进,并自主决定是否继续。框架只负责转达与记录,不替你们决定。','general',null)
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// A) heartbeat / coordination: wake the least-recently-active resident after an idle timeout
|
|
534
|
+
// to SELF-DRIVE (continue solving / message / propose task / meeting / verify). On a failed
|
|
535
|
+
// wake we re-arm the heartbeat so a single follow-up error NEVER permanently stops the group.
|
|
513
536
|
clearHeartbeat()
|
|
514
537
|
let target=null, oldest=-1
|
|
515
538
|
for(const [,r] of residents){ if(busy.has(r.rId)) continue; const idle=now()-r.lastActiveAt; if(idle>oldest){ oldest=idle; target=r } }
|
|
516
539
|
const atOs=Number(params.activityTimeoutMs)||120000
|
|
517
540
|
if(target && oldest>=atOs){
|
|
518
|
-
|
|
519
|
-
await wakeResident(target, await heartbeatPrompt(target), 'normal')
|
|
541
|
+
let ok=false
|
|
542
|
+
try { ok = await wakeResident(target, await heartbeatPrompt(target), 'normal') } catch(e){ ok=false }
|
|
543
|
+
await saveAll()
|
|
544
|
+
if(!ok) armHeartbeat() // wake failed → re-arm so the group never permanently stops
|
|
545
|
+
return
|
|
520
546
|
}
|
|
521
547
|
// everyone is busy or not idle-enough: arm a heartbeat to re-check later (no infinite spin)
|
|
522
548
|
armHeartbeat()
|
|
@@ -613,7 +639,7 @@ export function apply(ctx) {
|
|
|
613
639
|
}
|
|
614
640
|
function status(){ return { ok:true, running, phase, autoDone, project:currentProject, residentCount:residents.size,
|
|
615
641
|
residents:listResidents(), busy:[...busy], taskboard:taskboard.length,
|
|
616
|
-
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','provider','model','residentPersona','toolAllow','toolDeny'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
642
|
+
params:['residentCount','compactAfterRounds','compactThreshold','maxParallel','activityTimeoutMs','meetingKeepEvery','verdictMaxRounds','stallAutoMeetingMs','provider','model','residentPersona','toolAllow','toolDeny'].map(k=>k+'='+(Array.isArray(params[k])?params[k].join(','):params[k])).join(', ') } }
|
|
617
643
|
function report(){ return { ok:true, running, phase, autoDone, project:currentProject, problem:problemText,
|
|
618
644
|
residents:listResidents(), taskboard:taskboard.filter(t=>t.status!=='done'),
|
|
619
645
|
verify: verifyState?{target:verifyState.targetId,stage:verifyState.stage}:null, meetings:meetings.length,
|
|
@@ -623,7 +649,7 @@ export function apply(ctx) {
|
|
|
623
649
|
// Normalize one parameter value to its intended type so a string from /v4 set or configure
|
|
624
650
|
// becomes the right number/array. Keeps settings.json clean regardless of how it was set.
|
|
625
651
|
function normalizeParam(k, v){
|
|
626
|
-
const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery']
|
|
652
|
+
const INT_KEYS=['residentCount','compactThreshold','compactAfterRounds','maxParallel','activityTimeoutMs','verdictMaxRounds','meetingKeepEvery','stallAutoMeetingMs']
|
|
627
653
|
if(INT_KEYS.includes(k)){ const n=Number(v); return Number.isFinite(n)?n:v }
|
|
628
654
|
if(k==='toolAllow'||k==='toolDeny'){ if(Array.isArray(v)) return v.map(x=>String(x).trim()).filter(Boolean); if(typeof v==='string') return v.split(',').map(x=>x.trim()).filter(Boolean); return [] }
|
|
629
655
|
return v
|
|
@@ -689,7 +715,7 @@ export function apply(ctx) {
|
|
|
689
715
|
// model/provider inheritance: set model/provider to override the residents' LLM route (''=inherit
|
|
690
716
|
// the main assistant's route). toolAllow/toolDeny are per-resident tool permissions (scoped
|
|
691
717
|
// restrict). residentPersona prepends a persona line to every resident prompt.
|
|
692
|
-
registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}}}),(s,a)=>{ s.setParams(a); return {ok:true} })
|
|
718
|
+
registerTool('vibe_v4_set','Set V4 parameters. model/provider override resident LLM route (empty=inherit main); toolAllow/toolDeny restrict resident tools (arrays of tool names); residentPersona adds a persona line.',objParams({residentCount:{type:'integer'},compactAfterRounds:{type:'integer'},compactThreshold:{type:'integer'},meetingKeepEvery:{type:'integer'},maxParallel:{type:'integer'},activityTimeoutMs:{type:'integer'},verdictMaxRounds:{type:'integer'},stallAutoMeetingMs:{type:'integer'},provider:{type:'string'},model:{type:'string'},residentPersona:{type:'string'},toolAllow:{type:'array',items:{type:'string'}},toolDeny:{type:'array',items:{type:'string'}}}),(s,a)=>{ s.setParams(a); return {ok:true} })
|
|
693
719
|
// resident-facing tools: route to the CALLING resident (exec.agent.id === childId);
|
|
694
720
|
// fall back to the last-woken resident when called by the host/assistant.
|
|
695
721
|
registerTool('vibe_v4_send_message','(resident) Send a message to another resident.',objParams({to:{type:'string'},content:{type:'string'}},['to','content']),(s,a,x)=>s.postMessage(s.residentIdOf(x),a.to,a.content))
|
|
@@ -515,6 +515,28 @@ VibeMath/Projects/<project>/
|
|
|
515
515
|
|
|
516
516
|
> 测试:`selfdrive-v4.mjs` 21/21;`e2e-v4-fixes.test.mjs` 36/36(新增 T10 模型/工具权限接线、T11 默认继承、T12 自主发明理论提示、T13 压缩指令不泄漏进 meetings);v3 E2E 100/100、v2 regression 14/14、v2 business 24/24、multisession 25/25 全绿。
|
|
517
517
|
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 25. 分级保活:自驱动心跳 + 停滞自动同步会议(v1.4.2)
|
|
521
|
+
|
|
522
|
+
针对一次真实 run("跑完就停")的诊断:三名常驻第一轮就把问题归约到**同一个硬核引理**并一致给出 `solved=false`——这是**自然的难点停滞**,而非 bug。但框架的活性机制太弱且会"死":
|
|
523
|
+
- 空闲后唯一驱动是心跳(`activityTimeoutMs` 唤醒一个常驻、CHECKPOINT 提示词偏向"是否要停止"),**缺乏推进力**;
|
|
524
|
+
- **唤醒失败会永久停死**(`scheduleNext` 在心跳分支 `await wakeResident(...)` 后无条件 `return`,而 `wakeResident` 捕获异常后返回 false 但**不重新武装心跳**)→ 一旦某次 `subagents.followup` 抛异常,心跳不再武装,小组**永不再被唤醒**;
|
|
525
|
+
- `session.json` 不持久化 `meetingState`(进行中会议只在进程内存),进程重启或会议唤醒失败都可能让会议悬停。
|
|
526
|
+
|
|
527
|
+
### A · 自驱动心跳(修复推进力 + 永不永久停死)
|
|
528
|
+
- `heartbeatPrompt` 改为**自驱动**:不是"是否要停止",而是"**请继续解决这个问题**——读他人库、推进子问题/引理/方法、尝试路线;或向团队发消息(input)、提议任务(propose_task);确实已解决/无路可走才提议开会/声明 solved。默认立场是推进而非停在原地。" 心跳回复 schema 增加 `input`、`propose_task`、`contextPct`,使一轮心跳能产生群聊消息/任务从而带动后续工作。
|
|
529
|
+
- **唤醒失败重新武装心跳**:`scheduleNext` 心跳分支、`continueMeetingRound`、`continueVerifyRound` 在 `wakeResident` 失败(返回 false)时调用 `armHeartbeat()`,保证**任何一次唤醒失败都不会让小组永久停住**(会稍后重试其它常驻)。
|
|
530
|
+
|
|
531
|
+
### B · 停滞自动同步会议(分级保活 B)
|
|
532
|
+
- 新增 `lastProgressAt`(会话状态,随产物/会议/验证/任务/群聊/消息更新,`markProgress()`),并持久化到 `State/session.json`。
|
|
533
|
+
- `scheduleNext` 在无 meeting/verify/pending、`phase==='active'` 且 `now()-lastProgressAt >= stallAutoMeetingMs`(默认 360000=6 分钟,可调)时,自动 `startMeeting('团队较长时间没有新进展。请你们自行讨论…')`——框架**只促成会议、不指派任务**,让常驻们自行决定下一步路线/分工,从而"复活"一个停滞的团队。
|
|
534
|
+
- 这同时覆盖"进程重启后进行中会议丢失"的场景:重启后 `running:true, phase:active`、无会议 → 若持续无新产物,B 会自动再召集一次协调会议。
|
|
535
|
+
|
|
536
|
+
> 行为:有产出的团队会不断更新 `lastProgressAt`,B 不会触发;真正停滞的团队先被 A(自驱动心跳)持续轻推,若仍无新产物则被 B(自动同步会议)召集起来自主重启。全程框架只"促成",从不指派任务。
|
|
537
|
+
>
|
|
538
|
+
> 新增参数:`stallAutoMeetingMs`(`vibe_v4_set`/`status` 可见)。新增测试:T14(停滞自动会议)、T15(自驱动心跳提示词非旧的"说下一步");`selfdrive-v4` 21/21、`e2e-v4-fixes` 39/39。
|
|
539
|
+
|
|
518
540
|
|
|
519
541
|
|
|
520
542
|
|