@thincoder/core 0.10.10 → 0.10.12
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/CHANGELOG.md +36 -2
- package/advisor/messages.mjs +1 -1
- package/advisor/project-context.mjs +2 -2
- package/agent/completion.mjs +4 -1
- package/agent/family-tools.mjs +3 -3
- package/agent/helpers.mjs +13 -11
- package/agent/light-round.mjs +1 -4
- package/agent/record-results.mjs +6 -1
- package/agent/setup-reminders.mjs +1 -1
- package/agent/unbooked-pen.mjs +62 -0
- package/agent-tools/advisor.mjs +15 -31
- package/agent-tools/audit-block.mjs +16 -62
- package/agent-tools/batch-gate.mjs +74 -66
- package/agent-tools/batch-lifecycle.mjs +66 -9
- package/agent-tools/batch-paths.mjs +10 -8
- package/agent-tools/context.mjs +1 -1
- package/agent-tools/design-slots.mjs +72 -12
- package/agent-tools/design-token.mjs +27 -4
- package/agent-tools/digest-budget.mjs +2 -0
- package/agent-tools/doc-check.mjs +3 -14
- package/agent-tools/manifest.mjs +4 -5
- package/agent-tools/review-facts.mjs +35 -81
- package/agent-tools/settings.mjs +65 -5
- package/agent-tools/spawn-gates.mjs +68 -31
- package/agent-tools/subagent-async.mjs +6 -3
- package/agent-tools/subagent-spawn.mjs +42 -61
- package/agent-tools/subagent.mjs +15 -15
- package/browser/launch.mjs +14 -3
- package/browser/queue.mjs +31 -3
- package/browser/session.mjs +4 -0
- package/config-defaults.mjs +8 -0
- package/config-presets.mjs +1 -1
- package/config.mjs +60 -11
- package/doc-check/anchors.mjs +10 -9
- package/doc-check/index.mjs +27 -23
- package/doc-check/paragraph.mjs +119 -0
- package/fts-text.mjs +4 -1
- package/ledger-cmd.mjs +56 -41
- package/ledger-db.mjs +65 -58
- package/ledger-tools.mjs +60 -23
- package/ledger.mjs +13 -11
- package/manifest-discovery.mjs +58 -34
- package/manifest.mjs +34 -21
- package/model-context.mjs +98 -0
- package/model-specs.mjs +121 -50
- package/package.json +2 -2
- package/permission.mjs +3 -1
- package/prompts/advisor-design.md +16 -0
- package/prompts/common.md +12 -1
- package/prompts/discipline-engineering.md +42 -12
- package/prompts/discipline-normal.md +8 -7
- package/prompts/persona-coder.md +8 -1
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +26 -4
- package/prompts/persona-engineering.md +37 -38
- package/prompts/persona-explore.md +6 -0
- package/prompts/persona-normal.md +6 -0
- package/prompts/persona-plan.md +6 -0
- package/provider/anthropic.mjs +127 -4
- package/provider/core.mjs +16 -6
- package/provider/google.mjs +8 -8
- package/provider/list-models.mjs +37 -5
- package/provider/normalize.mjs +2 -2
- package/provider/responses.mjs +7 -7
- package/provider/sse.mjs +6 -5
- package/proxy-transport.mjs +5 -3
- package/proxy.mjs +2 -1
- package/session-guard.mjs +7 -1
- package/session-lifecycle.mjs +23 -8
- package/session-segments.mjs +1 -0
- package/session-slot-claims.mjs +4 -0
- package/session-slot-write.mjs +27 -1
- package/session-slots-manifest.mjs +25 -4
- package/token-ttl.mjs +78 -4
- package/tool-docs/advisor.md +24 -5
- package/tool-docs/doc_check.md +23 -16
- package/tool-docs/ledger.md +2 -2
- package/tool-docs/subagent.md +41 -16
- package/tool-docs/wait_for.md +15 -6
- package/tools/web-deepseek.mjs +26 -11
- package/doc-check/version.mjs +0 -84
- package/doc-check/width.mjs +0 -87
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* 另在 subagent-run.mjs。subagent.mjs execute 经本文件 import 调用。
|
|
7
7
|
* 2026-09-07:executeConsumeDesignAction 消费执行器(token 链终消费制——与 spawn 侧
|
|
8
8
|
* slot 族同域——ENGINEERING-MODE.md §2.6 F1——removeDesignTokenSlot 自 token-ttl.mjs)。
|
|
9
|
-
* 2026-09-22(台账 #23 · AGENT-LOOP-SUBAGENT.md §6.26
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 2026-09-22(台账 #23 · AGENT-LOOP-SUBAGENT.md §6.26):审计块构造器外提 `./audit-block.mjs`;
|
|
10
|
+
* spawn 级固定机制性指令改走 system 固块(`child._spawnSystemBlock` 单点绑定——消费点 = 核
|
|
11
|
+
* `prepareRun`),`input` 只留任务书。2026-10-24(§6.22):工程角色 `input` = 一行机械任务书行
|
|
12
|
+
* (批档绝对路径 + §2 指向句),任务书本体由子代理自读 §2。
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
import { basename, isAbsolute, relative } from "node:path"
|
|
15
15
|
import {
|
|
16
16
|
createAgent,
|
|
17
17
|
readonlyToolNames, escapeXml, excludeSubagentTools,
|
|
@@ -28,15 +28,16 @@ import {
|
|
|
28
28
|
} from "./subagent-scheduler.mjs"
|
|
29
29
|
// M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):任务书 round 枚举 + 五项结构化参数校验/拼装本体落
|
|
30
30
|
// spawn-gates.mjs(纯谓词零依赖)——本处只加 import 调用。
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
import {
|
|
32
|
+
validateRoundParam, validateTaskBookParams, composeTaskBookBrief,
|
|
33
|
+
isEngineeringRole, rejectEngFreeTextParams,
|
|
34
|
+
} from "./spawn-gates.mjs"
|
|
35
|
+
// 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-3):审计块构造器外提(`buildAuditBlock` 纯函数)
|
|
36
|
+
// ——spawn 档只做「收集 + 绑定」;块文本逐字搬移(D23-4)。
|
|
34
37
|
import { buildAuditBlock } from "./audit-block.mjs"
|
|
35
38
|
// 批次档路径解析单源(BATCH-RECORD.md §4.15 · 台账 #287):门内解析改经叶档(读面非抛形),
|
|
36
39
|
// 与 append / create 同源——门判据仍「参数在 + 路径可读」,错误文案逐字零变。
|
|
37
|
-
import { resolveBatchReadPath
|
|
38
|
-
// LOGGING(LOGGING.md):#309 形三观测留痕走同一事件通道。
|
|
39
|
-
import { logEvent } from "../log.mjs"
|
|
40
|
+
import { resolveBatchReadPath } from "./batch-paths.mjs"
|
|
40
41
|
|
|
41
42
|
/** 任务书播报文本:该角色名集拼装 + 可选 task 自由文本(拼装空 ⇒ 原 task 逐字——旧形零变)。 */
|
|
42
43
|
function taskBriefOf(args, role) {
|
|
@@ -46,31 +47,9 @@ function taskBriefOf(args, role) {
|
|
|
46
47
|
return extra ? `${composed}\n\n${extra}` : composed
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/** #309 任务书 token
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/** #309 形三·task↔batchDoc 观测留痕(§6.29.3——**不阻断**,spawn 照常放行):任务书文本提及
|
|
54
|
-
* **他批存在档** ⇒ `logEvent` 留痕一条(键面 = 两侧基名,零任务书内容);只提绑定档 / 无路径型
|
|
55
|
-
* `.md` 字面 ⇒ 零事件。token 逐条经 `resolveBatchReadPath` 解析(路径单源),命中 = 可读 ∧
|
|
56
|
-
* 落基底内 ∧ ≠ 绑定档(同一档多 token 按解析后绝对路径去重)。 */
|
|
57
|
-
function logBatchDocRefs(parent, role, task, boundAbs) {
|
|
58
|
-
const cwd = parent?.cwd ?? process.cwd()
|
|
59
|
-
const text = typeof task === "string" ? task : ""
|
|
60
|
-
const key = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
|
|
61
|
-
const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——入基底判据覆盖范围随之对齐)
|
|
62
|
-
const seen = new Set()
|
|
63
|
-
for (const m of text.matchAll(MD_PATH_TOKEN)) {
|
|
64
|
-
const token = m[0]
|
|
65
|
-
if (!/[/\\]/.test(token) && !isAbsolute(token)) continue // 裸名(AGENTS.md 形态)不取
|
|
66
|
-
const abs = resolveBatchReadPath(cwd, token)
|
|
67
|
-
if (!abs || key(abs) === key(boundAbs ?? "")) continue // 不可读 / 就是绑定档 ⇒ 零事件
|
|
68
|
-
const inBase = bases.some((b) => { const r = relative(b, abs); return r && !r.startsWith("..") && !isAbsolute(r) })
|
|
69
|
-
if (!inBase || seen.has(key(abs))) continue // 他 .md 引用(设计档 / 需求档等)/ 同档重复
|
|
70
|
-
seen.add(key(abs))
|
|
71
|
-
logEvent("child:batchdoc-ref", { role, batchDocBase: basename(boundAbs ?? ""), refBase: basename(abs) })
|
|
72
|
-
}
|
|
73
|
-
}
|
|
50
|
+
/** 原 #309 任务书 token 观测(`child:batchdoc-ref`)已废止——2026-10-24 用户裁定
|
|
51
|
+
* 「不允许跨批引用」后,工程角色 input 只剩一行机械文本,扫描对象失效;无场景分支不留
|
|
52
|
+
* (AGENT-LOOP-SUBAGENT.md §6.29.3)。 */
|
|
74
53
|
|
|
75
54
|
/**
|
|
76
55
|
* Effective subagent model override for a role (CLI parity shared with VS Code):
|
|
@@ -198,6 +177,8 @@ export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
|
|
|
198
177
|
export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, dependsOn, engAuditAttempt) {
|
|
199
178
|
// Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
|
|
200
179
|
const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
|
|
180
|
+
// 工程角色(任务书 = 批次档 §2,零任务书文本参数)——AGENT-LOOP-SUBAGENT.md §6.22.1。
|
|
181
|
+
const engineeringRole = isEngineeringRole(role)
|
|
201
182
|
|
|
202
183
|
// ── batchDoc gate(ENGINEERING-MODE.md §2.12 + §2.15 D1——FR20 #9 机械面)────────
|
|
203
184
|
// 工程模式下 spawn 工程角色(**eng-coder / eng-designer**——第 2 批扩为角色集合)必传
|
|
@@ -211,15 +192,13 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
211
192
|
// 工程专属、不移植);传则须可读(同评审面判据)⇒ `child._batchDoc` 绑定 §5 记录通道;
|
|
212
193
|
// 未携 ⇒ 不绑定、照常 spawn。
|
|
213
194
|
let batchDocAbs = null
|
|
214
|
-
if (
|
|
195
|
+
if (engineeringRole) {
|
|
215
196
|
const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
|
|
216
197
|
const refusal = (suffix = "") => new Error(
|
|
217
198
|
`batchDoc is required for role='${role}' — pass the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute); spawn refused without it.` + suffix)
|
|
218
199
|
if (!given) throw refusal()
|
|
219
200
|
batchDocAbs = resolveBatchReadPath(parent.cwd ?? process.cwd(), given)
|
|
220
201
|
if (!batchDocAbs) throw refusal(" (given path is not a readable file)")
|
|
221
|
-
// #309 形三:任务书中的他批批次档引用观测留痕(不阻断——放行不受影响)。
|
|
222
|
-
logBatchDocRefs(parent, role, taskBriefOf(args, role), batchDocAbs)
|
|
223
202
|
} else if (role === "coder") {
|
|
224
203
|
const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
|
|
225
204
|
if (given) {
|
|
@@ -230,12 +209,17 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
230
209
|
}
|
|
231
210
|
}
|
|
232
211
|
|
|
233
|
-
// M5 F2(
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
212
|
+
// M5 F2 + §6.22(2026-10-24)任务书门——按角色分流:
|
|
213
|
+
// 工程角色(eng-coder / eng-designer):零任务书文本参数——八个自由文本键命中即拒(键在场即拒)并点名
|
|
214
|
+
// (值全空白也拒);随后 round 枚举(initial|fix)。
|
|
215
|
+
// explore / plan / coder:四项核心 + `scope` 必带(判据三条零改:存在 ∧ string ∧ trim 非空)。
|
|
216
|
+
// 落点 = batchDoc 门之后、token 门之前(门序 = AGENT-LOOP-SUBAGENT.md §6.22.1)。
|
|
217
|
+
if (engineeringRole) {
|
|
218
|
+
rejectEngFreeTextParams(args, role)
|
|
219
|
+
validateRoundParam(args)
|
|
220
|
+
} else {
|
|
221
|
+
validateTaskBookParams(args, role)
|
|
222
|
+
}
|
|
239
223
|
|
|
240
224
|
// eng-coder token gate(TOOLS.md §6.25 ④):判定本体 = gateDesignToken(本档模块尾)——
|
|
241
225
|
// missing / mismatch / expired / invalid 四态各出互异拒句;拒绝集合与旧三条件合取全等。
|
|
@@ -289,7 +273,6 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
289
273
|
// G6(施工②)+ §2.15 B4(第 2 批):工程角色场景即工程纪律——engineering=true 使 setup
|
|
290
274
|
// 场景映射落到 assemblePrompt("eng-coder"/"eng-designer")(persona-{role} + common +
|
|
291
275
|
// discipline-engineering);designer 也必须 engineering=true 才能装配工程纪律槽。
|
|
292
|
-
const engineeringRole = role === "eng-coder" || role === "eng-designer"
|
|
293
276
|
const childConfig = engineeringRole
|
|
294
277
|
? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
|
|
295
278
|
: parent.config
|
|
@@ -338,7 +321,12 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
338
321
|
// 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
|
|
339
322
|
// git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
|
|
340
323
|
// setup.mjs depth===0——D-AG7 范围边界)。
|
|
341
|
-
|
|
324
|
+
// §6.22.2 子代理任务输入:工程角色 `input` = 一行机械任务书行(批档绝对路径 + §2 指向句)——
|
|
325
|
+
// 任务书本体由子代理自读 §2(KD-SC5:运行时不抽取正文注入,避免过期副本 / 脆标题解析);
|
|
326
|
+
// 非工程角色 = 既有拼装(context 前缀 + 任务书参数 + 可选 task——零变)。
|
|
327
|
+
const input = engineeringRole
|
|
328
|
+
? `Task book: §2 of the batch record at ${batchDocAbs} — read it before acting; it is the only task channel for this spawn.`
|
|
329
|
+
: (args.context ? `Context:\n${args.context}\n\nTask:\n${taskBriefOf(args, role)}` : taskBriefOf(args, role))
|
|
342
330
|
// 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-1):**spawn 级固定机制性指令改走 system 固块**——
|
|
343
331
|
// 两处(批次档路径行 + 审计模板块)原随 user 首条下发,压缩把首条并入摘要即丢(长任务中后期
|
|
344
332
|
// 既看不到批次档路径、也看不到审计模板 ⇒ 过度勘察的真病根);固块值在 child 生命周期内恒定
|
|
@@ -347,23 +335,16 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
347
335
|
// 的 system 逐字节同改前——§6.26 缓存契约兼容证明)。
|
|
348
336
|
const spawnBlocks = []
|
|
349
337
|
// §2.11 第 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**下发给工程角色
|
|
350
|
-
// (eng-coder 实现 / eng-designer
|
|
351
|
-
//
|
|
352
|
-
// (Docs involved / Files list / Acceptance criteria)——段匹配不受影响。工程角色恒注入;
|
|
353
|
-
// 携绑定 coder(§4.16)随绑定注入(绑定 coder 要能读到本档)。
|
|
338
|
+
// (eng-coder 实现 / eng-designer 写稿)——任务书本体 = 本档 §2;本行让子代理"拿到本档路径"。
|
|
339
|
+
// 工程角色恒注入;携绑定 coder(§4.16)随绑定注入(绑定 coder 要能读到本档)。
|
|
354
340
|
if (batchBound) spawnBlocks.push(`Batch record (batchDoc): ${batchDocAbs}`)
|
|
355
|
-
// AGENT-LOOP-SUBAGENT.md §6.7.6 D-E2 ③
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// affected-file list / acceptance criteria verbatim, verbose context dropped)
|
|
359
|
-
// — ∪ the mechanically tracked _touchedFiles — NEVER the eng-coder's
|
|
360
|
-
// self-written list: a self-report could omit exactly the out-of-scope file
|
|
361
|
-
// the audit must catch. 块文本(含模板逐字)住 `./audit-block.mjs`(D23-3 外提)。
|
|
341
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.6 D-E2 ③ + §6.22 KD-SC6:审计 spawn 的范围证据 = 机械跟踪的
|
|
342
|
+
// `_touchedFiles` ∪ 批档 §2(审计子代理自读)——**不是** eng-coder 自述(自述可恰好漏掉应抓的
|
|
343
|
+
// 越界档)。块文本(含模板逐字)住 `./audit-block.mjs`(D23-3 外提)。
|
|
362
344
|
if (engAuditAttempt !== null) spawnBlocks.push(buildAuditBlock(ctx))
|
|
363
|
-
// The child's own task input rides the child object:
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
if (role === "eng-coder") child._engTaskInput = input
|
|
345
|
+
// The child's own task input rides the child object: D23-6 —— `_engTaskInput` 语义 = **一行机械
|
|
346
|
+
// 任务书行**(批档绝对路径 + §2 指向句;不含固块)。
|
|
347
|
+
if (engineeringRole) child._engTaskInput = input
|
|
367
348
|
// spawn 固块单点绑定(sync / async 同点——buildSpawnChild 唯一调用点):此后只读 ⇒ 同一
|
|
368
349
|
// child 的任意 run(resume 续跑 / 报告追问重跑)逐字节恒定;消费点 = 核 `prepareRun`。
|
|
369
350
|
if (spawnBlocks.length > 0) child._spawnSystemBlock = spawnBlocks.join("\n\n")
|
package/agent-tools/subagent.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* escalate/cancel/panel)——async 结果仅自动通道送达。
|
|
17
17
|
* 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
|
|
18
18
|
* 调度器全套 → ./subagent-scheduler.mjs——本文件 import 源随之改写。
|
|
19
|
-
* 2026-09-05 模块拆分轮(726 > 500 硬限): spawn 前置 helpers(
|
|
19
|
+
* 2026-09-05 模块拆分轮(726 > 500 硬限): spawn 前置 helpers(subagent 工具描述与门同源面 /
|
|
20
20
|
* effectiveSubagentModel/resolveDesignSlot)+ AGENT-LOOP-SUBAGENT.md §6.9 准入(prepareScheduling)+ child
|
|
21
21
|
* 装配(buildSpawnChild)→ ./subagent-spawn.mjs;async 分支(executeAsyncSpawn)
|
|
22
22
|
* → ./subagent-run.mjs——execute 只保留动作分流 + 装配调用 + 阻塞路径。
|
|
@@ -36,7 +36,7 @@ import { executeStatusAction, executeEscalateAction, executePanelAction, execute
|
|
|
36
36
|
import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
|
|
37
37
|
import { executeDesignSlotsAction } from "./design-slots.mjs"
|
|
38
38
|
import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
39
|
-
import { ROUND_VALUES, composeTaskBookBrief } from "./spawn-gates.mjs"
|
|
39
|
+
import { ROUND_VALUES, composeTaskBookBrief, isEngineeringRole } from "./spawn-gates.mjs"
|
|
40
40
|
// #15 描述外置:描述文本单点 = tool-docs/subagent.md(DESC 单解析面,缺档抛错语义不变)
|
|
41
41
|
import { DESC } from "../tools/shared.mjs"
|
|
42
42
|
|
|
@@ -139,8 +139,8 @@ export const subagentTool = {
|
|
|
139
139
|
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send", "design-slots"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design / design-slots; semantics per action in the tool description." },
|
|
140
140
|
view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
|
|
141
141
|
freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; works on the CLI TUI panel and on the desktop renderer panel (live readback of the blocks as the user sees them)." },
|
|
142
|
-
task: { type: "string", description: "For action:'spawn' (
|
|
143
|
-
context: { type: "string", description: "Optional background
|
|
142
|
+
task: { type: "string", description: "For action:'spawn': normal-mode roles (explore/plan/coder) — OPTIONAL free-text supplement; the brief itself is the four core parameters (goal / knownFacts / acceptance / reportFormat) + scope. Engineering roles (eng-coder / eng-designer) take NO task-book text parameters — their task book is batch record §2 (batchDoc), read by the child itself. For action:'escalate': the brief (goal, constraints, entry files, acceptance criteria)." },
|
|
143
|
+
context: { type: "string", description: "Optional background for normal-mode spawns (explore/plan/coder) only; action:'spawn' only." },
|
|
144
144
|
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
145
145
|
model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
|
|
146
146
|
designToken: { type: "string", description: "REQUIRED for role='eng-coder': the exact token issued by advisor(type='design') after the review passed — copy it verbatim from the design-slots row (the text after designToken=). missing or mismatched = the spawn is refused; an expired token is refused with expired named. Pass it on every eng-coder spawn, fix rounds included (they reuse the same token)." },
|
|
@@ -148,14 +148,13 @@ export const subagentTool = {
|
|
|
148
148
|
designIds: { type: "array", items: { type: "string" }, description: "action:'consume-design' (batch): an explicit non-empty list of design ids to close out — pass exactly ONE selector per call (designId / designIds / expired / olderThanDays)." },
|
|
149
149
|
expired: { type: "boolean", description: "action:'consume-design' (batch): true = close out every expired slot (exactly ONE selector per call)." },
|
|
150
150
|
olderThanDays: { type: "number", description: "action:'consume-design' (batch): close out every slot older than N days (age > N; exactly ONE selector per call)." },
|
|
151
|
-
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
|
|
151
|
+
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. This IS the task book for engineering roles — batch record §2, read by the child itself. Refused when absent or when the path does not resolve to a readable file; the content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
|
|
152
152
|
round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
|
|
153
|
-
goal: { type: "string", description: "REQUIRED for
|
|
154
|
-
knownFacts: { type: "string", description: "REQUIRED for
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
reportFormat: { type: "string", description: "REQUIRED for every spawn role: the delivery-report format the child must produce." },
|
|
153
|
+
goal: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (normal-mode task book): the task book's goal & why — the problem this solves and the cost chain (content only, no label — the runtime composes the labels)." },
|
|
154
|
+
knownFacts: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (normal-mode task book): the facts and file paths the parent already verified — the child must not re-explore them." },
|
|
155
|
+
scope: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (normal-mode task book, the fifth parameter): scope & forbidden scope — what to read / search and what not to touch." },
|
|
156
|
+
acceptance: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (normal-mode task book): acceptance criteria — machine-verifiable (commands / thresholds / assertion counts)." },
|
|
157
|
+
reportFormat: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (normal-mode task book): the delivery-report format the child must produce." },
|
|
159
158
|
async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
|
|
160
159
|
files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
|
|
161
160
|
dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async returns whose outcome this task needs — queues until every dependency settles, then starts. Consumed ids count as satisfied; a cancelled/failed dependency stays queued marked 'dependency cancelled' until you decide (AUTO sessions auto-start). Unknown ids error." },
|
|
@@ -222,10 +221,11 @@ export const subagentTool = {
|
|
|
222
221
|
// Spawn requires a task brief — schema `required` is advisory (multi-action
|
|
223
222
|
// schema), so the mechanical check lives here: an absent task would otherwise
|
|
224
223
|
// flow downstream as `content: undefined` and surface as an obscure error.
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
// 载体判据:**非工程角色** = 结构化参数(拼装后非空)——task 只作补充;两者皆缺 ⇒ 此处先拒。
|
|
225
|
+
// 工程角色 = 无任务书文本参数(任务书 = 批次档 §2):此处不判——自由文本键由
|
|
226
|
+
// buildSpawnChild 的任务书门键在场即拒并点名(空集拼装不得误报「缺任务书」)。
|
|
227
|
+
if (!isEngineeringRole(args.role) && !composeTaskBookBrief(args, args.role) && (typeof args.task !== "string" || !args.task.trim())) {
|
|
228
|
+
throw new Error("subagent action:'spawn' requires the structured task-book parameters for normal-mode roles (four core: goal / knownFacts / acceptance / reportFormat + scope) — a free-text task alone is not the brief. Engineering roles take no task-book text parameters: their task book is batch record §2.")
|
|
229
229
|
}
|
|
230
230
|
// §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
|
|
231
231
|
// default to async for EVERY role (the old role-level default — eng-coder only —
|
package/browser/launch.mjs
CHANGED
|
@@ -31,7 +31,17 @@ export const CANDIDATES = {
|
|
|
31
31
|
|
|
32
32
|
/** 全候选落空(N-BT5)——装配期不剔除工具,首用显式报此句(§2.6)。 */
|
|
33
33
|
export const BROWSER_NOT_FOUND = "no Chromium-based browser found (Edge/Chrome) — install one or set BROWSER_PATH"
|
|
34
|
-
export const START_TIMEOUT_MS =
|
|
34
|
+
export const START_TIMEOUT_MS = 25_000 // 缺省启动窗(重负载 Windows 实测 13–17s,留 ≈8s 余量)
|
|
35
|
+
const START_TIMEOUT_MIN_MS = 2_000 // 下限:防「瞬间判失败」
|
|
36
|
+
const START_TIMEOUT_MAX_MS = 30_000 // 上限:首动作总墙钟 = 启动窗 + 动作预算(§2.9 两段各自有帽——KD-25)
|
|
37
|
+
|
|
38
|
+
/** env 覆盖 `BROWSER_START_TIMEOUT_MS`(§2.4 取值口径 · KD-25):`Number.isFinite(v) && v > 0` 才采纳,
|
|
39
|
+
* 随后夹进 `[2_000, 30_000]`(`"1500"` ⇒ 2000、`"40000"` ⇒ 30000);非数值 ∥ 空串 ∥ `0` ∥ 负值 ⇒ 回落缺省 25s。 */
|
|
40
|
+
export function browserStartTimeoutMs(env = process.env) {
|
|
41
|
+
const wanted = Number(env?.BROWSER_START_TIMEOUT_MS)
|
|
42
|
+
if (!Number.isFinite(wanted) || wanted <= 0) return START_TIMEOUT_MS
|
|
43
|
+
return Math.min(START_TIMEOUT_MAX_MS, Math.max(START_TIMEOUT_MIN_MS, wanted))
|
|
44
|
+
}
|
|
35
45
|
|
|
36
46
|
const defaultPathExists = (p) => existsSync(p)
|
|
37
47
|
function defaultWhich(name) {
|
|
@@ -141,11 +151,12 @@ export function reclaimProfileOrphans(profile, io = {}) {
|
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
/**
|
|
144
|
-
* 启动浏览器(spawn + `DevToolsActivePort` 轮询 ≤
|
|
154
|
+
* 启动浏览器(spawn + `DevToolsActivePort` 轮询 ≤ 启动窗)。
|
|
155
|
+
* 启动窗 = `timeoutMs` 显式入参 > env `BROWSER_START_TIMEOUT_MS` > 缺省 25s(`browserStartTimeoutMs`)。
|
|
145
156
|
* 持久 profile ⇒ 启动前清陈旧端口文件(否则旧值被当本次读数——发现即失效)。
|
|
146
157
|
* @returns {Promise<{child: import("node:child_process").ChildProcess, port: number, profile: string}>}
|
|
147
158
|
*/
|
|
148
|
-
export async function launchBrowser({ exe, headless = true, profile, timeoutMs =
|
|
159
|
+
export async function launchBrowser({ exe, headless = true, profile, timeoutMs = browserStartTimeoutMs(), sleep = (ms) => new Promise((r) => setTimeout(r, ms)), reclaimIo = undefined } = {}) {
|
|
149
160
|
const args = [
|
|
150
161
|
...(headless ? ["--headless=new"] : []),
|
|
151
162
|
"--disable-gpu", "--no-first-run", "--hide-scrollbars",
|
package/browser/queue.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { WAIT_TIMEOUT_DEFAULT, WAIT_TIMEOUT_MAX } from "./actions.mjs"
|
|
|
12
12
|
|
|
13
13
|
/** 动作预算表(§2.9——机检):动作 ⇒ 预算 ms。wait 无固定值(谓词帽 + 余量——`budgetFor` 解算)。 */
|
|
14
14
|
export const ACTION_BUDGETS = {
|
|
15
|
-
navigate: 45_000, //
|
|
15
|
+
navigate: 45_000, // 不含会话自启(自启段有自身帽——§2.4 / KD-26)+ 导航 + 就绪轮询 + 快照
|
|
16
16
|
click: 30_000,
|
|
17
17
|
evaluate: 30_000,
|
|
18
18
|
screenshot: 30_000,
|
|
@@ -58,11 +58,39 @@ export class ActionController {
|
|
|
58
58
|
this._abortHooks = []
|
|
59
59
|
this._abortP = new Promise((resolve) => { this._resolveAbort = resolve })
|
|
60
60
|
this._timer = setTimeout(() => this.abort("timeout"), budgetMs)
|
|
61
|
+
this._pausedAt = null // 冷启段暂停点(null = 计时中——§2.4 冷启预算)
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
/** 剩余预算(ms)——动作内调用帽的下界参照(§2.9
|
|
64
|
+
/** 剩余预算(ms)——动作内调用帽的下界参照(§2.9 层一:动作内调用随控制器);暂停期读暂停点。 */
|
|
64
65
|
remaining() {
|
|
65
|
-
return Math.max(1, this._deadline - Date.now())
|
|
66
|
+
return Math.max(1, this._deadline - (this._pausedAt ?? Date.now()))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 冷启段暂停计量(§2.4 冷启预算 / KD-26):自启窗口内动作预算钟不裁决——清钟、记暂停点。
|
|
70
|
+
* 与 `resumeBudget` 成对;自启段自身由启动窗帽 + 三域 enable 帽界定(session.mjs 开启序)。 */
|
|
71
|
+
pauseBudget() {
|
|
72
|
+
if (this.aborted || this._pausedAt !== null) return
|
|
73
|
+
this._pausedAt = Date.now()
|
|
74
|
+
if (this._timer) { clearTimeout(this._timer); this._timer = null }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 冷启段恢复计量:把暂停时长还给 deadline(经 `extendBy` 重设并重挂)——动作时钟自会话就绪后起算。 */
|
|
78
|
+
resumeBudget() {
|
|
79
|
+
if (this._pausedAt === null) return
|
|
80
|
+
const pausedMs = Math.max(0, Date.now() - this._pausedAt)
|
|
81
|
+
this._pausedAt = null
|
|
82
|
+
this.extendBy(pausedMs)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 冷启段回补(§2.4 冷启预算 / KD-26):把给定时长加回 deadline 并重挂定时器(`ms = 0` 也重挂——
|
|
86
|
+
* 暂停后恢复的零时长路径仍须有活钟)。`budgetMs` 不动(超时句仍报动作本预算);中止后无效果。 */
|
|
87
|
+
extendBy(ms) {
|
|
88
|
+
if (this.aborted) return
|
|
89
|
+
const extra = Number(ms)
|
|
90
|
+
if (!Number.isFinite(extra) || extra < 0) return
|
|
91
|
+
this._deadline += extra
|
|
92
|
+
if (this._timer) clearTimeout(this._timer)
|
|
93
|
+
this._timer = setTimeout(() => this.abort("timeout"), Math.max(1, this._deadline - Date.now()))
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
/** 快速失败(§2.9):abort 之后一切后续调用即刻拒绝(清理路径不得再起命令)。 */
|
package/browser/session.mjs
CHANGED
|
@@ -196,12 +196,16 @@ async function ensureSession(wanted) {
|
|
|
196
196
|
return
|
|
197
197
|
}
|
|
198
198
|
let opened
|
|
199
|
+
const ctl = currentController
|
|
200
|
+
ctl?.pauseBudget() // 冷启段不占动作预算(§2.4 冷启预算 / KD-26):自启窗口内预算钟暂停(自启段自身由启动窗帽 + 三域 enable 帽界定)
|
|
199
201
|
try {
|
|
200
202
|
opened = await runFrame("launch browser", () => (_deps.openBrowser ?? openBrowserReal)({ headless: wanted === undefined ? true : wanted }))
|
|
201
203
|
} catch (e) {
|
|
202
204
|
if (e?.browserAbort) state.reopenNote = true
|
|
205
|
+
ctl?.resumeBudget() // 失败腿同样恢复(自启失败的裁决归启动窗帽与错误本体)
|
|
203
206
|
throw e
|
|
204
207
|
}
|
|
208
|
+
ctl?.resumeBudget() // 动作时钟自会话就绪后起算(自启用时不计入动作预算;排队期照旧计入)
|
|
205
209
|
state.cdp = opened.cdp
|
|
206
210
|
state.child = opened.child ?? null
|
|
207
211
|
state.lockPath = opened.lock ?? null
|
package/config-defaults.mjs
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
import { join } from "node:path"
|
|
13
13
|
import { configDir } from "./config-io.mjs"
|
|
14
14
|
|
|
15
|
+
/** provider 响应体(body)阶段读侧空闲超时的单源缺省:provider 缺省 ∥ 传输层缺省同值同源。
|
|
16
|
+
* 可经 `agent.bodyIdleTimeoutMs` 覆盖;`≤0` / 非有限数回退本值(不禁用——无现场判据)。 */
|
|
17
|
+
export const DEFAULT_BODY_IDLE_MS = 120000
|
|
18
|
+
|
|
15
19
|
export const DEFAULTS = {
|
|
16
20
|
defaultModel: null, // top-level "provider:model" composite — new-session starting point (F-1)
|
|
17
21
|
agent: {
|
|
@@ -20,6 +24,10 @@ export const DEFAULTS = {
|
|
|
20
24
|
subagentModel: null, // default subagent model: "provider:model" | model name (parent provider); null = inherit parent provider
|
|
21
25
|
subagentModels: {}, // per-type override: { explore, plan, coder, "eng-coder" } — priority: subagent tool model arg > this[role] > subagentModel > parent provider
|
|
22
26
|
goalTurns: 200,
|
|
27
|
+
// body 阶段读侧空闲超时(毫秒,TIMEOUT-DIGEST.md §2.1):长生成只要有数据流动就不超时,
|
|
28
|
+
// 连续该值无新 chunk 才断。登记进 DEFAULTS 是为了 `settings list` 可见(判据 A3)——
|
|
29
|
+
// 与 `agent.fetchTimeoutMs`(只被 config 提升、不进本表)不同的一处偏离,理由即此。
|
|
30
|
+
bodyIdleTimeoutMs: DEFAULT_BODY_IDLE_MS,
|
|
23
31
|
// compactThreshold: 键缺省 = auto(CONTEXT-COMPACTION.md §6.17 三档落值)——无默认值:留着默认值
|
|
24
32
|
// 会让每份配置看着都像显式。派生线只进 agent.compactThresholdResolved(内存面,不落盘)。
|
|
25
33
|
verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
|
package/config-presets.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 候选清单字段亦不预置——运行期从 provider 拉取(`GET /models`)。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
/** Built-in provider presets: shared by
|
|
16
|
+
/** Built-in provider presets: shared by the `/model` provider flow, the first-run wizard, and the desktop / VSC provider forms. */
|
|
17
17
|
export const PROVIDER_PRESETS = {
|
|
18
18
|
deepseek: { baseURL: "https://api.deepseek.com", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 384_000, desc: "DeepSeek" },
|
|
19
19
|
kimi: { baseURL: "https://api.moonshot.cn/v1", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
|
package/config.mjs
CHANGED
|
@@ -37,8 +37,10 @@ export { DEFAULTS }
|
|
|
37
37
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
38
38
|
// extract — config.mjs had grown past the 300-line advisory). Re-exported here
|
|
39
39
|
// so the 23 existing importers keep their import paths.
|
|
40
|
-
import { specForModel, providerSpec, specMatch, assistantToolCallMessage } from "./model-specs.mjs"
|
|
41
|
-
export { specForModel, providerSpec, specMatch, assistantToolCallMessage }
|
|
40
|
+
import { specForModel, providerSpec, providerSpecResolution, specMatch, formatContextK, assistantToolCallMessage } from "./model-specs.mjs"
|
|
41
|
+
export { specForModel, providerSpec, providerSpecResolution, specMatch, formatContextK, assistantToolCallMessage }
|
|
42
|
+
// 上下文解析链的取值校验单点(PROVIDER.md §6.15):装载归一与读面共用一个谓词。
|
|
43
|
+
import { positiveInt, warnContextOnce } from "./model-context.mjs"
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
// Window utilization ratio (CONTEXT-COMPACTION.md §6.2 / D-CC30 / §6.17 / D-CC32): auto threshold =
|
|
@@ -79,8 +81,10 @@ export function usableWindow(context, reserve) {
|
|
|
79
81
|
* maxTokens), the stats budget line (agent-tools/context.mjs), the pre-send guard
|
|
80
82
|
* (agent/run-stages.mjs). transport derives from provider.format ("anthropic" | "google" |
|
|
81
83
|
* "responses" | default = OpenAI-compatible). Fields:
|
|
82
|
-
* - window / windowSource: spec.context (
|
|
83
|
-
*
|
|
84
|
+
* - window / windowSource: spec.context (the §6.15 chain — modelContext / legacy context /
|
|
85
|
+
* /models self-report / static table / default — already resolved by providerSpec);
|
|
86
|
+
* source = providerSpecResolution(provider).source — closed set { modelContext |
|
|
87
|
+
* provider-override | list | spec | default }, passed through verbatim (no class merging).
|
|
84
88
|
* - maxTokens / maxTokensSource: the value THIS transport writes into the request body —
|
|
85
89
|
* anthropic always sends it (explicit ?? spec.maxOutput); the other three faces send only an
|
|
86
90
|
* explicit provider.maxTokens, absent ⇒ undefined / "absent". Never a guard criterion
|
|
@@ -90,16 +94,19 @@ export function usableWindow(context, reserve) {
|
|
|
90
94
|
* - threshold: min(usable, ⌊window × COMPACT_RATIO⌋) — the compaction trigger line.
|
|
91
95
|
* - limit: window − reserve — hard prompt ceiling; the guard's ONLY numeric criterion field
|
|
92
96
|
* (always a number; negative = any prompt overflows). */
|
|
93
|
-
export function requestBudget(provider, spec
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
+
export function requestBudget(provider, spec) {
|
|
98
|
+
// windowSource = 五级链的同一次遍历出口(§6.15)——L1 / L3 命中也会如实报来源,不再二值猜。
|
|
99
|
+
// 调用方传 spec(core.mjs 已算过 providerSpec)时窗口值用它,来源仍读 resolution(恒等映射,不并类)。
|
|
100
|
+
const resolution = providerSpecResolution(provider)
|
|
101
|
+
const resolved = spec ?? resolution.spec
|
|
102
|
+
const window = resolved?.context ?? 0
|
|
103
|
+
const windowSource = resolution.source
|
|
97
104
|
const explicitRaw = Number(provider?.maxTokens)
|
|
98
105
|
const explicit = Number.isFinite(explicitRaw) && explicitRaw > 0 ? explicitRaw : undefined
|
|
99
106
|
const anthropicAlwaysSends = provider?.format === "anthropic"
|
|
100
|
-
const maxTokens = explicit ?? (anthropicAlwaysSends ?
|
|
107
|
+
const maxTokens = explicit ?? (anthropicAlwaysSends ? resolved?.maxOutput ?? 0 : undefined)
|
|
101
108
|
const maxTokensSource = explicit != null ? "explicit" : anthropicAlwaysSends ? "spec" : "absent"
|
|
102
|
-
const reserve = maxTokens ??
|
|
109
|
+
const reserve = maxTokens ?? resolved?.maxOutput ?? 0
|
|
103
110
|
const usable = usableWindow(window, reserve)
|
|
104
111
|
return {
|
|
105
112
|
window, windowSource, maxTokens, maxTokensSource, reserve, usable,
|
|
@@ -182,6 +189,20 @@ export function findProvider(providers, name) {
|
|
|
182
189
|
return providers[0] ?? { name: "default", baseURL: "" }
|
|
183
190
|
}
|
|
184
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Promote `agent.advisor` into the runtime view `config.advisor`(decoupled copy——`{ ...config.advisor,
|
|
194
|
+
* ...config.agent.advisor }`)。单一规则:`loadConfig` 装载期与 `settings set` 写面共用(`SETTINGS-TOOL.md` §2.3)。
|
|
195
|
+
* 只住运行面的会话级键(如 `advisor.guard`——`cmd-advisor.mjs` 只写 promoted)在合并中保留;
|
|
196
|
+
* promoted 是副本而不自动跟随 nested,所以每个写点都得同步(`AGENT-PARAMS.md` §2 写面不变式)。
|
|
197
|
+
*/
|
|
198
|
+
export function promoteAgentViews(config) {
|
|
199
|
+
const nested = config.agent?.advisor
|
|
200
|
+
// 非对象段(手写 config.json 的畸形值)不展开——字符串 spread 会产出 0/1/2 垃圾键;
|
|
201
|
+
// 读面本身对非法值「一律回退默认」(AGENT-PARAMS.md §2 / D-P3)
|
|
202
|
+
config.advisor = { ...config.advisor, ...(nested !== null && typeof nested === "object" && !Array.isArray(nested) ? nested : {}) }
|
|
203
|
+
return config
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
/**
|
|
186
207
|
* Load configuration.
|
|
187
208
|
* No env-var overrides — config.json is the single source of truth
|
|
@@ -257,6 +278,30 @@ export function loadConfig() {
|
|
|
257
278
|
delete p.context
|
|
258
279
|
}
|
|
259
280
|
|
|
281
|
+
// providers[].modelContext(按模型的人工声明,K 单位——PROVIDER.md §6.15):值只收正整数,
|
|
282
|
+
// 非法(0 / 负 / 非数 / 非整 / 布尔 / 数字字符串)⇒ 该键视为未声明(落 L2)+ 装载期告警一次
|
|
283
|
+
// (P-13)。数字字符串**不归一**(不同于旧键 context):写面先 parse 再落盘,文件里的字符串是
|
|
284
|
+
// 手改/笔误,静默转数会把「想写哪个单位」藏起来(旧键两起 ×1024 事故的根因)。
|
|
285
|
+
// 读面(manualContextK)仍逐次防御——装载归一不产新键,只清内存镜像(磁盘原文零变)。
|
|
286
|
+
for (const p of merged.providers) {
|
|
287
|
+
if (p.modelContext === undefined) continue
|
|
288
|
+
const rawMC = p.modelContext
|
|
289
|
+
if (!rawMC || typeof rawMC !== "object" || Array.isArray(rawMC)) {
|
|
290
|
+
warnContextOnce(`modelContext-shape\0${p.name ?? ""}`,
|
|
291
|
+
`[config] provider "${p.name}" modelContext must be an object of model ID → positive integer K (e.g. 128 = 128K) — got ${JSON.stringify(rawMC)} — ignored`)
|
|
292
|
+
delete p.modelContext
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
p.modelContext = { ...rawMC }
|
|
296
|
+
for (const [key, value] of Object.entries(p.modelContext)) {
|
|
297
|
+
if (positiveInt(value) !== null) continue
|
|
298
|
+
warnContextOnce(`modelContext-invalid\0${p.name ?? ""}\0${key}`,
|
|
299
|
+
`[config] provider "${p.name}" modelContext["${key}"] must be a positive integer in K units (e.g. 128 = 128K) — got ${JSON.stringify(value)} — ignored, falling through to the provider context / list / model spec value`)
|
|
300
|
+
delete p.modelContext[key]
|
|
301
|
+
}
|
|
302
|
+
if (Object.keys(p.modelContext).length === 0) delete p.modelContext // 不留空对象
|
|
303
|
+
}
|
|
304
|
+
|
|
260
305
|
// F-4 (IKCDMR) consultModels 软失败化——D-S1 范式(defaultModel 无效同族):非法条目
|
|
261
306
|
// 过滤不 throw(去 startup brick——无修复入口的硬崩消);一次性启动警告内嵌修复指引
|
|
262
307
|
// (引导清条目载体——保留 discoverability);过滤记录挂 merged.agent.consultModelsFiltered
|
|
@@ -319,8 +364,12 @@ export function loadConfig() {
|
|
|
319
364
|
// fetch 超时可配置(2026-09-01:agent.fetchTimeoutMs——provider/core.mjs effectiveFetchTimeoutMs 消费)
|
|
320
365
|
merged.provider.fetchTimeoutMs = Number.isFinite(merged.agent?.fetchTimeoutMs) && merged.agent.fetchTimeoutMs > 0
|
|
321
366
|
? merged.agent.fetchTimeoutMs : undefined
|
|
367
|
+
// body 阶段空闲超时同形提升(TIMEOUT-DIGEST.md §2.1 —— provider/core.mjs effectiveBodyIdleMs 消费):
|
|
368
|
+
// 非法 ∥ 缺省 ⇒ undefined ⇒ 回退 DEFAULT_BODY_IDLE_MS(不静默禁用保护)
|
|
369
|
+
merged.provider.bodyIdleTimeoutMs = Number.isFinite(merged.agent?.bodyIdleTimeoutMs) && merged.agent.bodyIdleTimeoutMs > 0
|
|
370
|
+
? merged.agent.bodyIdleTimeoutMs : undefined
|
|
322
371
|
merged.providersList = merged.providers
|
|
323
|
-
merged.advisor
|
|
372
|
+
promoteAgentViews(merged) // agent.advisor → advisor promote for consistent access (decoupled copy)
|
|
324
373
|
|
|
325
374
|
return merged
|
|
326
375
|
}
|
package/doc-check/anchors.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* · 档不在盘 ⇒ `档缺`;带节指示 ∧ 节不在盘 ⇒ `节缺`。
|
|
8
8
|
*
|
|
9
9
|
* 抽取面:逐行扫;fenced 块整块跳过;可执行行(`isExecutableLine`)整行跳过(命令保字面);
|
|
10
|
-
* 提示词面另剥 CANON 操作数(面判据 =
|
|
10
|
+
* 提示词面另剥 CANON 操作数(面判据 = `./paragraph.mjs` 的 `isPromptFace`——七名整串删除后再抽,见 `CANON_OPERANDS`)。
|
|
11
11
|
* 解析序**两级**:① 仓根相对全路径 → ② 引用方档所在目录相对。零命中 ⇒ `档缺`。
|
|
12
12
|
* **裸名不跨目录解析**:跨目录解析须先建全域目录索引——为认出一个裸名而重建索引,等于把全仓扫描请回来;
|
|
13
13
|
* 写全路径后解析零索引,逐档判就是全部(与 D4 实现坐标口径一致)。
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { readFileSync, statSync } from "node:fs"
|
|
19
19
|
import { basename, dirname, resolve } from "node:path"
|
|
20
|
-
import { fenceMark, isExecutableLine } from "./
|
|
20
|
+
import { fenceMark, isExecutableLine, isPromptFace } from "./paragraph.mjs"
|
|
21
21
|
|
|
22
22
|
/** 文档指针 token:`(目录段/)*名.md` + 可选行号坐标尾 + 可选节指示(`§` + 编号)。
|
|
23
23
|
* 左界守卫排除更长 token 的尾段;右界排除后续字母数字(`.md` 之后紧邻 `[\w]` ⇒ 非完整 token)。 */
|
|
@@ -36,8 +36,8 @@ const PLACEHOLDER_STEMS = new Set([
|
|
|
36
36
|
])
|
|
37
37
|
|
|
38
38
|
/** CANON 操作数族(**规范豁免串** · 判据单源 = `docs/core/design/PROMPT-SYSTEM.md` §6.6「判保留面」):
|
|
39
|
-
*
|
|
40
|
-
* 换个目录解析必然 `档缺`。**只对提示词面剥**(面判据 =
|
|
39
|
+
* 七个名字在模型面是**模型要写出来 / 读写的文件名**(行为操作数 · 路径操作数),不是「去读这个档」的指针——
|
|
40
|
+
* 换个目录解析必然 `档缺`。**只对提示词面剥**(面判据 = `isPromptFace`,单源住 `./paragraph.mjs`):`docs/` 面不会因本条多放行一个名——
|
|
41
41
|
* `README.md` / `MANIFEST.md` 在那里仍按指针判(其余四名早已走 `FILENAME_CLASSES` 类名免,非本条引入)。
|
|
42
42
|
* **一处定义、两处消费**:`scripts/prompt-refs-check.mjs` 的 J3 豁免从本档 import 本表与剥除函数(不复制);
|
|
43
43
|
* 反方向零 import——判据链自持 = `docs/core/design/DOC-DISCIPLINE.md` A-DD29 ⑤(`thincoder-core/**` 对
|
|
@@ -45,9 +45,10 @@ const PLACEHOLDER_STEMS = new Set([
|
|
|
45
45
|
export const CANON_OPERANDS = [
|
|
46
46
|
"AGENTS.md", "SKILL.md", "README.md", "MANIFEST.md",
|
|
47
47
|
".thincoder/advisor.md", "project_rules.md",
|
|
48
|
+
"RTM.md",
|
|
48
49
|
]
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
+
/** 操作数剥除:逐行先整串删除七名 CANON 操作数再抽(与 `scripts/prompt-refs-check.mjs` 的 J3 豁免同式)。 */
|
|
51
52
|
export function stripCanonOperands(line) {
|
|
52
53
|
let out = line
|
|
53
54
|
for (const s of CANON_OPERANDS) out = out.split(s).join("")
|
|
@@ -56,9 +57,9 @@ export function stripCanonOperands(line) {
|
|
|
56
57
|
|
|
57
58
|
/** 操作数豁免的**作用面** = 提示词面(`thincoder-core/prompts/**`)——**只此一面**:`docs/` 面同名 token 仍按指针判
|
|
58
59
|
* (`README.md` / `MANIFEST.md` 在那里照报 `档缺`;其余四名走既有的 `FILENAME_CLASSES` 类名免,非本条引入)。
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
const isOperandFace =
|
|
60
|
+
* 这是锚判自己的面判据——与段判据零共享开关:段判据按传入档全量判(提示词面照判——逐行裸判,无面级豁免)。 */
|
|
61
|
+
// 提示词面判据单源 = `./paragraph.mjs` 的 `isPromptFace`(§3.18 ②——形态与旧 `OPERAND_FACE` 逐字同)。
|
|
62
|
+
const isOperandFace = isPromptFace
|
|
62
63
|
|
|
63
64
|
/** 文件存在性(目录 / 读错 ⇒ false)。 */
|
|
64
65
|
function isFile(p) { try { return statSync(p).isFile() } catch { return false } }
|
|
@@ -104,7 +105,7 @@ export function checkAnchors(root, { files = [] } = {}) {
|
|
|
104
105
|
let text = ""
|
|
105
106
|
try { text = readFileSync(f, "utf8") } catch { continue }
|
|
106
107
|
const re = new RegExp(ANCHOR_RE.source, "g") // 每档一份实例(零共享 lastIndex 状态)
|
|
107
|
-
const stripOperands = isOperandFace(f) //
|
|
108
|
+
const stripOperands = isOperandFace(f) // 只有提示词面剥(面判据 = `isPromptFace`)
|
|
108
109
|
let fence = null // 围栏标记(` ∥ ~);null = 不在块内
|
|
109
110
|
text.split("\n").forEach((l, i) => {
|
|
110
111
|
const mark = fenceMark(l)
|