cs-bots 0.1.1 → 0.7.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +533 -0
  2. package/README.md +65 -10
  3. package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
  4. package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
  5. package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
  6. package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
  7. package/docs/ARCHITECTURE.md +277 -0
  8. package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
  9. package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
  10. package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
  11. package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
  12. package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
  13. package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
  14. package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
  15. package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
  16. package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
  17. package/package.json +4 -2
  18. package/src/admission.js +365 -0
  19. package/src/api.js +4097 -420
  20. package/src/board.js +883 -14
  21. package/src/botprofile.js +190 -3
  22. package/src/client.js +6596 -1082
  23. package/src/cycle.js +516 -0
  24. package/src/delivery-watch.js +255 -0
  25. package/src/dirbrowse.js +20 -0
  26. package/src/discipline.js +1188 -0
  27. package/src/fence.js +142 -0
  28. package/src/index.js +387 -4
  29. package/src/kernel.js +636 -24
  30. package/src/library.js +1224 -11
  31. package/src/memory.js +617 -37
  32. package/src/metrics.js +339 -0
  33. package/src/prompts.js +1318 -0
  34. package/src/reviewer.js +709 -0
  35. package/src/room.js +655 -14
  36. package/src/roster.js +346 -26
  37. package/src/routing/complexity.js +320 -0
  38. package/src/routing/hints.js +707 -0
  39. package/src/scratch.js +477 -0
  40. package/src/settings.js +97 -0
  41. package/src/stage.js +263 -0
  42. package/src/store.js +133 -1
  43. package/src/subscription.js +571 -0
  44. package/src/suggest.js +1203 -0
  45. package/src/teams.js +527 -0
  46. package/src/todo.js +1301 -0
  47. package/src/tools.js +734 -10
  48. package/src/verify.js +356 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * cs-bots 复杂度分类器 —— **纯函数,零 I/O,零宿主依赖**。
3
+ *
4
+ * 契约:`design/17-并发感知路由层-接口契约-v1.md` §6。
5
+ *
6
+ * 为什么要有这一层:cs-myfree 的准入流水线只会问「这条请求允许用付费渠道吗」,
7
+ * 它**不知道**这条请求值不值付费。判定必须发生在平台侧(这里),因为只有平台
8
+ * 知道「这是主 Agent 的规划回合还是子智能体的一个窄任务」。于是:
9
+ *
10
+ * 请求信号 → classifyComplexity() → T0..T3 → billingTierFor() → 'free_only' | 'paid_allowed'
11
+ * ↓
12
+ * 旁路注册表 hint(见 ./hints.js)
13
+ *
14
+ * 设计纪律(本文件被 `test/ci-zero-dep.sh` 检查 1 覆盖):
15
+ * 1. **不 import 任何宿主包**(也不 import kernel.js —— 纯函数便于离线表驱动测试)。
16
+ * 2. **全部确定性**:同一输入必得同一输出;不含 Date.now / 随机 / 环境变量。
17
+ * 3. **可审计**:`reasons` 逐条给出「哪条信号把分数推到哪」,不出现「可能/大概」。
18
+ *
19
+ * 接口版本:v1(2026-11-04,与契约 §6 同步)。
20
+ */
21
+
22
+ /** 复杂度档位(契约 §6 冻结)。 */
23
+ export const COMPLEXITY = Object.freeze({ T0: 'T0', T1: 'T1', T2: 'T2', T3: 'T3' })
24
+
25
+ /** 档位升序表(越靠后越复杂)。 */
26
+ export const COMPLEXITY_TIERS = Object.freeze(['T0', 'T1', 'T2', 'T3'])
27
+
28
+ /** 合法 agent 角色(契约 §3 hint 形状)。 */
29
+ const AGENT_ROLES = Object.freeze(['main', 'subagent', 'unknown'])
30
+
31
+ /**
32
+ * 分档阈值(score ∈ [0,1]):`score < 0.15` → T0,`< 0.40` → T1,
33
+ * `< 0.70` → T2,否则 T3。阈值是**常量而非配置**:路由档位一旦可配,
34
+ * 「为什么这条走了付费」就无从复现(可审计性优先于灵活性)。
35
+ */
36
+ const TIER_CUTOFFS = Object.freeze([
37
+ { tier: 'T0', below: 0.15 },
38
+ { tier: 'T1', below: 0.40 },
39
+ { tier: 'T2', below: 0.70 },
40
+ // score >= 0.70 → T3
41
+ ])
42
+
43
+ /**
44
+ * 信号权重表 —— 每条信号贡献多少分,以及触发的门限。
45
+ *
46
+ * 口径说明(为什么是这些数):
47
+ * - `contextTokens`:上下文越长,失败一次越贵(重试成本 × token 量),故权重最高;
48
+ * token 数用「字符数 / 4」粗估(见 ./hints.js 的 estimateContextTokens),
49
+ * 所以门限用**数量级**而不是实测精确值。
50
+ * - `historyDepth`:消息条数反映「已经绕了多少圈」,多轮 = 更可能需要强模型。
51
+ * - `hasToolCalls`:带工具的请求是多步任务的最小证据。
52
+ * - `agentRole`:`main` 是规划/验收方,值得付费;`subagent` 是窄执行者,默认免费。
53
+ */
54
+ const SIGNAL_RULES = Object.freeze({
55
+ contextTokens: [
56
+ { atLeast: 120_000, weight: 0.40, label: '上下文极长' },
57
+ { atLeast: 48_000, weight: 0.28, label: '上下文很长' },
58
+ { atLeast: 16_000, weight: 0.15, label: '上下文偏长' },
59
+ ],
60
+ historyDepth: [
61
+ { atLeast: 40, weight: 0.25, label: '历史轮次很多' },
62
+ { atLeast: 16, weight: 0.15, label: '历史轮次较多' },
63
+ { atLeast: 6, weight: 0.07, label: '历史轮次偏多' },
64
+ ],
65
+ hasToolCalls: { weight: 0.15, label: '请求带工具(多步任务)' },
66
+ agentRole: { main: { weight: 0.12, label: '主 Agent 回合(规划/验收)' } },
67
+ })
68
+
69
+ // ── 归一化小工具 ────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * 把任意值归一成合法档位;非法返回 `null`(**不抛**,分类器永不因脏输入崩)。
73
+ * @param {unknown} value
74
+ * @returns {'T0'|'T1'|'T2'|'T3'|null}
75
+ */
76
+ export function normalizeTier(value) {
77
+ if (typeof value !== 'string') return null
78
+ const v = value.trim().toUpperCase()
79
+ return COMPLEXITY_TIERS.includes(v) ? v : null
80
+ }
81
+
82
+ /**
83
+ * 归一化 agent 角色:除 `'main'` / `'subagent'` 外一律 `'unknown'`(fail-safe 展示口径)。
84
+ * @param {unknown} value
85
+ * @returns {'main'|'subagent'|'unknown'}
86
+ */
87
+ export function normalizeRole(value) {
88
+ if (typeof value !== 'string') return 'unknown'
89
+ const v = value.trim().toLowerCase()
90
+ return AGENT_ROLES.includes(v) ? /** @type {any} */ (v) : 'unknown'
91
+ }
92
+
93
+ /** 有限数字才认;其它(null/NaN/字符串/'12'/Infinity)一律视为「无此信号」。 */
94
+ function finiteOrNull(value) {
95
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
96
+ }
97
+
98
+ // ── 子智能体路由策略(`design/22` 契约 E3/E4)────────────────────────────────
99
+ //
100
+ // 与 cs-myfree 的 `src/config/scope.js` 同一口径(跨仓冻结,改名 = 破坏兼容):
101
+ // auto : 不跟随路由,由 myfree 按可用性自动选路(今天的行为,默认)
102
+ // pinned : 不跟随路由,固定用 `subagentRouting.model`
103
+ // follow : 跟随路由,继承上级会话的授权与选路结果
104
+
105
+ /** 三种策略(顺序 = UI 三选项顺序,契约 E3) */
106
+ export const SUBAGENT_MODES = Object.freeze(['auto', 'pinned', 'follow'])
107
+
108
+ /**
109
+ * 归一化 `subagentRouting.mode`:非法/缺省 ⇒ `null`(= 未配置 ⇒ 走今天的行为)。
110
+ *
111
+ * ⚠️ 刻意**不**回落成 `'auto'`:`null` 与 `'auto'` 在授权判定上等价,但保留 `null`
112
+ * 才能让调用方区分「用户明确选了第一项」与「根本没配置」(诊断/审计要看得出区别)。
113
+ *
114
+ * @param {unknown} value
115
+ * @returns {'auto'|'pinned'|'follow'|null}
116
+ */
117
+ export function normalizeSubagentMode(value) {
118
+ if (typeof value !== 'string') return null
119
+ const v = value.trim().toLowerCase()
120
+ return SUBAGENT_MODES.includes(v) ? /** @type {any} */ (v) : null
121
+ }
122
+
123
+ /**
124
+ * 归一化付费授权标签:只认 `'paid_allowed'` / `'free_only'`,其它 ⇒ `null`(= 未知)。
125
+ * @param {unknown} value
126
+ * @returns {'free_only'|'paid_allowed'|null}
127
+ */
128
+ export function normalizeBillingTier(value) {
129
+ if (typeof value !== 'string') return null
130
+ const v = value.trim().toLowerCase()
131
+ return v === 'paid_allowed' || v === 'free_only' ? /** @type {any} */ (v) : null
132
+ }
133
+
134
+ /** 布尔才认;其它视为无信号。 */
135
+ function boolOrNull(value) {
136
+ return typeof value === 'boolean' ? value : null
137
+ }
138
+
139
+ /** 按阈值表取「最大命中的那一档」权重(表内按 atLeast 降序,便于阅读)。 */
140
+ function weightFor(value, rules) {
141
+ if (value === null) return null
142
+ // 不假设调用方按顺序传入:按 atLeast 降序挑第一个满足的
143
+ const sorted = [...rules].sort((a, b) => b.atLeast - a.atLeast)
144
+ const hit = sorted.find((rule) => value >= rule.atLeast)
145
+ return hit ?? null
146
+ }
147
+
148
+ // ── 分类 ────────────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * 把请求信号分类到 T0–T3。
152
+ *
153
+ * **优先级**:`explicitComplexity` 最高 —— 调用方/任务标注一旦显式给出合法档位,
154
+ * 信号分数**完全不参与**(只作为审计信息保留在 `signals` 里),因为「规划 Agent 已经
155
+ * 决定这是 T3」比任何启发式都更可信。给非法显式值时不静默吞掉:退回信号打分,
156
+ * 并在 `reasons` 里留下 `忽略` 记录(可审计)。
157
+ *
158
+ * @param {{
159
+ * contextTokens?: number, historyDepth?: number, hasToolCalls?: boolean,
160
+ * agentRole?: string, explicitComplexity?: string,
161
+ * }} [signals]
162
+ * @returns {{
163
+ * tier: 'T0'|'T1'|'T2'|'T3',
164
+ * score: number,
165
+ * reasons: string[],
166
+ * signals: { contextTokens: number|null, historyDepth: number|null,
167
+ * hasToolCalls: boolean|null, agentRole: string, explicitComplexity: string|null },
168
+ * }}
169
+ */
170
+ export function classifyComplexity(signals = {}) {
171
+ const input = signals && typeof signals === 'object' ? signals : {}
172
+ const contextTokens = finiteOrNull(input.contextTokens)
173
+ const historyDepth = finiteOrNull(input.historyDepth)
174
+ const hasToolCalls = boolOrNull(input.hasToolCalls)
175
+ const agentRole = normalizeRole(input.agentRole)
176
+ const explicitRaw = input.explicitComplexity
177
+ const explicitTier = normalizeTier(explicitRaw)
178
+
179
+ /** @type {string[]} */
180
+ const reasons = []
181
+
182
+ // ── 优先级 1:显式档位 ────────────────────────────────────────────────────
183
+ if (explicitTier) {
184
+ const idx = COMPLEXITY_TIERS.indexOf(explicitTier)
185
+ reasons.push(
186
+ `显式指定复杂度 ${explicitTier}(来源:任务标注/调用方),已直接定档,忽略信号打分`,
187
+ )
188
+ return {
189
+ tier: explicitTier,
190
+ // 让分数与档位自洽:T0=0 / T1=1/3 / T2=2/3 / T3=1(纯审计用途)
191
+ score: Number((idx / (COMPLEXITY_TIERS.length - 1)).toFixed(3)),
192
+ reasons,
193
+ signals: {
194
+ contextTokens,
195
+ historyDepth,
196
+ hasToolCalls,
197
+ agentRole,
198
+ explicitComplexity: explicitTier,
199
+ },
200
+ }
201
+ }
202
+ if (explicitRaw !== undefined && explicitRaw !== null && explicitRaw !== '') {
203
+ reasons.push(
204
+ `显式复杂度 ${JSON.stringify(explicitRaw)} 非法(合法值:${COMPLEXITY_TIERS.join(' | ')})——已忽略,退回信号打分`,
205
+ )
206
+ }
207
+
208
+ // ── 优先级 2:信号打分 ────────────────────────────────────────────────────
209
+ let score = 0
210
+
211
+ const ctxHit = weightFor(contextTokens, SIGNAL_RULES.contextTokens)
212
+ if (ctxHit) {
213
+ score += ctxHit.weight
214
+ reasons.push(`${ctxHit.label}(contextTokens≈${contextTokens} ≥ ${ctxHit.atLeast}):+${ctxHit.weight}`)
215
+ }
216
+
217
+ const histHit = weightFor(historyDepth, SIGNAL_RULES.historyDepth)
218
+ if (histHit) {
219
+ score += histHit.weight
220
+ reasons.push(`${histHit.label}(historyDepth=${historyDepth} ≥ ${histHit.atLeast}):+${histHit.weight}`)
221
+ }
222
+
223
+ if (hasToolCalls === true) {
224
+ score += SIGNAL_RULES.hasToolCalls.weight
225
+ reasons.push(`${SIGNAL_RULES.hasToolCalls.label}:+${SIGNAL_RULES.hasToolCalls.weight}`)
226
+ }
227
+
228
+ if (agentRole === 'main') {
229
+ score += SIGNAL_RULES.agentRole.main.weight
230
+ reasons.push(`${SIGNAL_RULES.agentRole.main.label}:+${SIGNAL_RULES.agentRole.main.weight}`)
231
+ }
232
+
233
+ score = Math.max(0, Math.min(1, score))
234
+ const rounded = Number(score.toFixed(3))
235
+ const cutoff = TIER_CUTOFFS.find((c) => rounded < c.below)
236
+ const tier = cutoff ? cutoff.tier : 'T3'
237
+
238
+ if (reasons.length === 0) {
239
+ reasons.push('无命中信号(无上下文/tool/角色证据)⇒ 最低档 T0')
240
+ }
241
+ reasons.push(`合计 score=${rounded} ⇒ ${tier}`)
242
+
243
+ return {
244
+ tier,
245
+ score: rounded,
246
+ reasons,
247
+ signals: {
248
+ contextTokens,
249
+ historyDepth,
250
+ hasToolCalls,
251
+ agentRole,
252
+ explicitComplexity: null,
253
+ },
254
+ }
255
+ }
256
+
257
+ // ── 授权标签 / 模型选择 ──────────────────────────────────────────────────────
258
+
259
+ /**
260
+ * 复杂度 + 角色 + 显式诉求 → 付费授权标签(契约 §6 + `design/22` 契约 E4)。
261
+ *
262
+ * **fail-closed 是刻意的**:只有 `role === 'main'`(规划/验收方)或任务显式标了
263
+ * `requiresPaid: true` 才给 `paid_allowed`;其余一律 `free_only`。理由:误判成付费
264
+ * 只会多花钱,误判成免费只是慢一点 —— 两者的代价不对称。
265
+ *
266
+ * `design/22` 契约 E4 的唯一改动(**子智能体分支**):不再一律 `free_only`,
267
+ * 而是按 `subagentRouting.mode` 决定:
268
+ * · `'follow'` ⇒ **继承上级**(`parentBillingTier`);上级未给(无上级/上级也未知)
269
+ * ⇒ 仍落 `free_only`(fail-closed,绝不因为「说是跟随」就凭空放行付费);
270
+ * · `'auto'` / `'pinned'` ⇒ **现状**(`free_only`;pinned 只影响"用哪个模型",
271
+ * 不影响"能不能付费"——授权口径只由 role/requiresPaid/follow 三者决定);
272
+ * · 没有配置(`subagentRouting` 缺省/非法)⇒ 与今天**逐字一致**(零回归)。
273
+ *
274
+ * @param {{ tier?: string, role?: string, requiresPaid?: boolean,
275
+ * parentBillingTier?: string,
276
+ * subagentRouting?: { mode?: string } }} [input]
277
+ * @returns {'free_only'|'paid_allowed'}
278
+ */
279
+ export function billingTierFor(input = {}) {
280
+ const role = normalizeRole(input?.role)
281
+ if (role === 'main') return 'paid_allowed'
282
+ if (input?.requiresPaid === true) return 'paid_allowed'
283
+ if (role === 'subagent') {
284
+ const mode = normalizeSubagentMode(input?.subagentRouting?.mode)
285
+ if (mode === 'follow') {
286
+ const inherited = normalizeBillingTier(
287
+ input?.parentBillingTier ?? input?.subagentRouting?.parentBillingTier,
288
+ )
289
+ if (inherited) return inherited
290
+ }
291
+ }
292
+ return 'free_only'
293
+ }
294
+
295
+ /**
296
+ * 分层模型选择(契约 §6)。
297
+ *
298
+ * - `T0` / `T1`:**只**在 `billing !== 'paid'` 的候选里选(`unknown` 按免费处理,
299
+ * 与 cs-myfree 准入器的成本保守口径一致)。
300
+ * - `T2` / `T3`:**同序**取第一个候选 —— 真正的付费过滤由 cs-myfree 按 hint 的
301
+ * `billing_tier` 决定,平台侧不越权(避免两处授权口径打架)。
302
+ *
303
+ * @param {'T0'|'T1'|'T2'|'T3'} tier
304
+ * @param {Array<{ provider?: string, model?: string, tier?: string, billing?: string }>} [candidates]
305
+ * @returns {object|null} 中选候选(原对象引用),无可选返回 `null`
306
+ */
307
+ export function pickModelForTier(tier, candidates = []) {
308
+ const t = normalizeTier(tier)
309
+ if (!t) return null
310
+ const list = Array.isArray(candidates)
311
+ ? candidates.filter((c) => c && typeof c === 'object' && !Array.isArray(c))
312
+ : []
313
+ if (list.length === 0) return null
314
+ if (t === 'T0' || t === 'T1') {
315
+ return list.find((c) => c.billing !== 'paid') ?? null
316
+ }
317
+ return list[0]
318
+ }
319
+
320
+ export default classifyComplexity