@thincoder/core 0.10.2 → 0.10.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/CHANGELOG.md +65 -0
- package/advisor/citations.mjs +40 -4
- package/advisor/compaction.mjs +21 -2
- package/advisor/convergence.mjs +1 -1
- package/advisor/history.mjs +8 -6
- package/advisor/loop.mjs +29 -27
- package/advisor/messages.mjs +5 -4
- package/advisor/run.mjs +5 -1
- package/advisor/timeline.mjs +26 -0
- package/advisor.mjs +4 -2
- package/agent/completion.mjs +3 -1
- package/agent/digest-account.mjs +188 -0
- package/agent/dispatch-run.mjs +29 -12
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +8 -0
- package/agent/record-results.mjs +10 -1
- package/agent/run-stages.mjs +27 -4
- package/agent/run-start.mjs +49 -3
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +60 -12
- package/agent/turn-loop.mjs +31 -2
- package/agent/write-gate.mjs +13 -25
- package/agent-tools/advisor-async.mjs +25 -7
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/advisor.mjs +34 -33
- package/agent-tools/async-settle.mjs +29 -5
- package/agent-tools/batch-lifecycle.mjs +29 -7
- package/agent-tools/batch-paths.mjs +22 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +59 -33
- package/agent-tools/batch.mjs +16 -6
- package/agent-tools/design-token.mjs +76 -16
- package/agent-tools/escalate-async.mjs +4 -2
- package/agent-tools/review-facts.mjs +143 -16
- package/agent-tools/subagent-actions-query.mjs +13 -6
- package/agent-tools/subagent-async.mjs +16 -4
- package/agent-tools/subagent-panel.mjs +121 -37
- package/agent-tools/subagent-spawn.mjs +1 -1
- package/agent-tools/subagent.mjs +5 -3
- package/agent-tools/verify.mjs +5 -0
- package/agent-tools/zero-write-watch.mjs +77 -0
- package/agent.mjs +1 -1
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +30 -0
- package/conventions.mjs +26 -6
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- package/escape.mjs +1 -1
- package/i18n.mjs +5 -0
- package/ledger-cmd.mjs +13 -21
- package/ledger-db.mjs +34 -1
- package/ledger-migrate.mjs +4 -3
- package/ledger-read.mjs +94 -0
- package/ledger-surface.mjs +15 -46
- package/ledger-tools.mjs +148 -85
- package/ledger-variant-notice.mjs +47 -0
- package/ledger.mjs +38 -64
- package/manifest-schema.mjs +19 -7
- package/manifest.mjs +32 -11
- package/mcp/transport-http.mjs +52 -4
- package/mcp/transport-stdio.mjs +33 -8
- package/mcp.mjs +13 -5
- package/memory/code-search.mjs +5 -3
- package/memory/core.mjs +6 -4
- package/memory/docs.mjs +5 -3
- package/memory/schema.mjs +2 -0
- package/package.json +1 -1
- package/prompts/advisor-design.md +3 -2
- package/prompts/advisor-round1.md +1 -0
- package/prompts/advisor-round2.md +8 -3
- package/prompts/advisor-round3.md +7 -2
- package/prompts/common.md +2 -2
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +7 -4
- package/provider/anthropic.mjs +14 -2
- package/provider/core.mjs +10 -4
- package/provider/errors.mjs +23 -0
- package/provider/google.mjs +31 -4
- package/provider/normalize.mjs +57 -0
- package/provider/responses-request.mjs +3 -1
- package/provider/responses.mjs +38 -4
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session.mjs +13 -2
- package/shell-identity.mjs +107 -0
- package/stream-destroy.mjs +62 -0
- package/tool-docs/advisor.md +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/delete.md +7 -5
- package/tool-docs/ledger.md +6 -0
- package/tool-docs/subagent.md +10 -0
- package/tool-docs/verify.md +1 -1
- package/tools/bash.mjs +9 -4
- package/tools/edit-batch.mjs +7 -2
- package/tools/index.mjs +1 -4
- package/tools/patch.mjs +8 -2
- package/tools/write-path.mjs +6 -4
- package/undo-stack.mjs +23 -9
- package/tool-docs/ledger_add.md +0 -1
- package/tool-docs/ledger_close.md +0 -1
- package/tool-docs/ledger_count.md +0 -1
- package/tool-docs/ledger_query.md +0 -1
- package/tool-docs/ledger_update.md +0 -1
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* computePanelBlocks(ctx.state)(ctx.state = agent._tuiState——TUI 装配处接线——单账本);
|
|
10
10
|
* 未挂载(headless/VSC/子代理)→ 现算返 null → 降级/报不可用照旧
|
|
11
11
|
* (T-P5 语义零变)。门控语义零动(F-4)——仅数据源从镜像换现算。
|
|
12
|
+
* **子代理面板批(2026-10-04 · `docs/desktop/design/PANEL-READBACK.md`)——回读源链**:数据源不再单源现算——
|
|
13
|
+
* `readPanelSource(ctx)` = `ctx.readout()`(桌面渲染面实况快照——上报缓存读面)优先 ⇒ `ctx.state` 现算 ⇒
|
|
14
|
+
* 二者皆空报不可用;视图面快照非空 ⇒ `{ source:"renderer", asOf, ageMs, panel[] }`(awaitingDigest 条读时
|
|
15
|
+
* 交叉核池/pending 标 `digested`——与 CLI 面同判据);freeze 门控两源同判(桌面块 `awaitingDigest`/`region` 为
|
|
16
|
+
* 独立字段),键统一规范化(`sub:` 前缀剥除——**有意放宽且两端同宽**;发射键 = 规范化键,relay 文法不吃 `:`)。
|
|
12
17
|
* subagent-actions.mjs 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面。
|
|
13
18
|
*/
|
|
14
19
|
import { getAsyncPool } from "./async-settle.mjs"
|
|
@@ -41,67 +46,129 @@ function consultChildUndigested(agent, key) {
|
|
|
41
46
|
return false
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 回读源链(子代理面板批 · 2026-10-04 · `PANEL-READBACK.md` §2.1/§2.3):渲染面快照
|
|
51
|
+
* (`ctx.readout()`——桌面实况回读上报;`{ blocks, receivedAt }`,块形 = 判别六键 + `role`/`id` 随行)优先
|
|
52
|
+
* ∥ CLI 现算(`computePanelBlocks(ctx.state)`——F-3 单账本)∥ 二者皆空 ⇒ `null`(拒因照旧)。
|
|
53
|
+
* 返回 `{ blocks, readout }`:`readout` 非空 = 桌面快照源(`awaitingDigest` / `region` 为块独立字段)。
|
|
54
|
+
*/
|
|
55
|
+
function readPanelSource(ctx) {
|
|
56
|
+
const snapshot = typeof ctx.readout === "function" ? ctx.readout() : null
|
|
57
|
+
if (snapshot && Array.isArray(snapshot.blocks)) return { blocks: snapshot.blocks, readout: snapshot }
|
|
58
|
+
const snap = computePanelBlocks(ctx.state)
|
|
59
|
+
return Array.isArray(snap) ? { blocks: snap, readout: null } : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 键归一(**有意放宽且两端同宽**——§2.3):`sub:` 前缀剥除后比对(桌面块键 `sub:role#id` ∥ CLI 块键
|
|
63
|
+
* `role#id` 两写法皆接受);命中后比对与发射全用规范化键(relay 前缀文法 `^\w-+#\d+/` 不吃 `:`)。 */
|
|
64
|
+
function normalizePanelKey(key) {
|
|
65
|
+
const k = String(key ?? "")
|
|
66
|
+
return k.startsWith("sub:") ? k.slice("sub:".length) : k
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 两源同判——等待消化:CLI 块 = 现算 status 位;桌面块 = 判别键 `awaitingDigest`。 */
|
|
70
|
+
function awaitingOf(src, block) {
|
|
71
|
+
return src.readout ? block.awaitingDigest === true : block.status === "awaitingDigest"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 活态标签(live 列表 ∥ 拒因文案共用):桌面块把 awaiting 态折进 status 位(与 CLI 现算同形)。 */
|
|
75
|
+
function liveStatusOf(src, block) {
|
|
76
|
+
return awaitingOf(src, block) ? "awaitingDigest" : String(block.status ?? "unknown")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 桌面回读条目(视图面——判别六键 + `role`/`id` 随行;awaitingDigest 条读时交叉核池/pending 标 `digested`
|
|
80
|
+
* —— 判据与 CLI 面同源;consult 子块按消费判据——键经归一后入判)。 */
|
|
81
|
+
function readoutPanelEntry(agent, block) {
|
|
82
|
+
const key = normalizePanelKey(block.key)
|
|
83
|
+
const out = {
|
|
84
|
+
key: block.key,
|
|
85
|
+
role: block.role ?? null,
|
|
86
|
+
id: block.id ?? null,
|
|
87
|
+
status: block.status ?? null,
|
|
88
|
+
frozen: block.frozen === true,
|
|
89
|
+
awaitingDigest: block.awaitingDigest === true,
|
|
90
|
+
region: block.region === "flow" ? "flow" : "activity",
|
|
91
|
+
dom: block.dom === true,
|
|
92
|
+
}
|
|
93
|
+
if (out.awaitingDigest) {
|
|
94
|
+
out.digested = consultChildKey(key)
|
|
95
|
+
? !consultChildUndigested(agent, key)
|
|
96
|
+
: !blockKeyIn(agent._pendingAsyncResults, key)
|
|
97
|
+
&& !blockKeyIn(getAsyncPool(agent, "subagent"), key) && !blockKeyIn(getAsyncPool(agent, "advisor"), key)
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
44
102
|
/**
|
|
45
103
|
* AGENT-LOOP-SUBAGENT.md §6.7.2 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
|
|
46
104
|
* _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
|
|
47
105
|
* (_pendingAsyncResults——D2)无对应条目的块(= 已消化驻留块——报告已入模型上下文
|
|
48
106
|
* ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
|
|
107
|
+
* - 数据源 = **回读源链**(子代理面板批 · 2026-10-04):`ctx.readout`(桌面渲染面快照——块判别键
|
|
108
|
+
* `awaitingDigest`/`region` 独立字段)优先 ∥ `ctx.state` 现算;命中后比对与发射全用**规范化键**
|
|
109
|
+
* (`sub:` 前缀剥除——有意放宽且两端同宽,§2.3)。
|
|
49
110
|
* - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
|
|
50
111
|
* - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
|
|
51
|
-
* -
|
|
112
|
+
* - 无面板(两源皆空)→ 拒绝(headless/VS Code——freeze 不可用——T-P5)
|
|
113
|
+
* - 已入流(桌面快照 `region:"flow"`——已归档)→ 追加拒因(无卡可收;CLI 源无该字段 ⇒ 零改)
|
|
52
114
|
* - consult 子块(#748):会话在跑 ∥ 会话条目在 pending(`childIds` 携该号)→ 拒绝(同判据——与回收 ∥ 补发同源)
|
|
53
|
-
* 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }
|
|
115
|
+
* 错误信息明确(模型可解释 + 自助修正)。返回 `{ ok:true, renderer }` 或 `{ err }`。
|
|
54
116
|
*/
|
|
55
117
|
function panelFreezeGate(ctx, key) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const snap = computePanelBlocks(ctx.state)
|
|
59
|
-
if (!Array.isArray(snap)) {
|
|
118
|
+
const src = readPanelSource(ctx)
|
|
119
|
+
if (src === null) {
|
|
60
120
|
return { err: "panel unavailable — no CLI TUI panel in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
|
|
61
121
|
}
|
|
62
|
-
const
|
|
122
|
+
const want = normalizePanelKey(key)
|
|
123
|
+
const block = src.blocks.find((b) => normalizePanelKey(b.key) === want)
|
|
63
124
|
if (!block) {
|
|
64
|
-
const live =
|
|
125
|
+
const live = src.blocks.map((b) => `${b.key}(${liveStatusOf(src, b)})`).join(", ")
|
|
65
126
|
return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
|
|
66
127
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
128
|
+
// 已入流 = 已归档(桌面快照判别键)——无卡可收(CLI 源无 region 字段 ⇒ 零改)。
|
|
129
|
+
if (src.readout && block.region === "flow") {
|
|
130
|
+
return { err: `block ${want} is already in the conversation flow (region "flow") — it has been archived; there is nothing left to freeze` }
|
|
131
|
+
}
|
|
132
|
+
if (!awaitingOf(src, block)) {
|
|
133
|
+
const status = String(block.status ?? "unknown")
|
|
134
|
+
if (status === "done") {
|
|
135
|
+
return { err: `block ${want} is already done — nothing to freeze; it was (or is about to be) reclaimed into the conversation by the freeze sweep at the turn end / settle (only digested-stuck awaitingDigest blocks need a manual freeze)` }
|
|
70
136
|
}
|
|
71
|
-
if (
|
|
72
|
-
return { err: `block ${
|
|
137
|
+
if (status === "running") {
|
|
138
|
+
return { err: `block ${want} is still running — freeze only reclaims awaitingDigest blocks whose report is already digested; a running block freezes on its own settle (or stop it with action:'cancel' if it is a background async child)` }
|
|
73
139
|
}
|
|
74
|
-
return { err: `block ${
|
|
140
|
+
return { err: `block ${want} is in state ${status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
|
|
75
141
|
}
|
|
76
|
-
if (blockKeyIn(getAsyncPool(ctx.agent, "subagent"),
|
|
77
|
-
return { err: `block ${
|
|
142
|
+
if (blockKeyIn(getAsyncPool(ctx.agent, "subagent"), want) || blockKeyIn(getAsyncPool(ctx.agent, "advisor"), want)) {
|
|
143
|
+
return { err: `block ${want} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
|
|
78
144
|
}
|
|
79
145
|
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
|
|
80
146
|
// 独立族退役)。#748:consult 子块键不经裸键比对(会话条目键 `consult#<sessionId>` 与
|
|
81
147
|
// 子块键可数字撞键)——改判消费判据(会话在跑 ∥ 会话条目在 pending——与回收 ∥ 补发同源)。
|
|
82
|
-
if (!consultChildKey(
|
|
83
|
-
return { err: `block ${
|
|
148
|
+
if (!consultChildKey(want) && blockKeyIn(ctx.agent._pendingAsyncResults, want)) {
|
|
149
|
+
return { err: `block ${want} is still genuinely awaiting digestion — its report is still pending and has NOT reached the model yet; freezing now would break the digestion order (wait for the digest run, which reclaims it automatically)` }
|
|
84
150
|
}
|
|
85
|
-
if (consultChildUndigested(ctx.agent,
|
|
86
|
-
return { err: `block ${
|
|
151
|
+
if (consultChildUndigested(ctx.agent, want)) {
|
|
152
|
+
return { err: `block ${want} belongs to a consultation whose digest has not been consumed — the session is still running, or its session entry is still pending (freeze refused; the block is reclaimed at the consumption window)` }
|
|
87
153
|
}
|
|
88
|
-
return { ok: true }
|
|
154
|
+
return { ok: true, renderer: src.readout !== null }
|
|
89
155
|
}
|
|
90
156
|
|
|
91
157
|
/**
|
|
92
158
|
* AGENT-LOOP-SUBAGENT.md §6.7.2 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
|
|
93
159
|
* freeze 优先):
|
|
94
|
-
* - view
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* 7.2.3.2 #8
|
|
104
|
-
*
|
|
160
|
+
* - view(缺省——返回面板块列表):**回读源链**(子代理面板批 · 2026-10-04):渲染面快照非空 ⇒
|
|
161
|
+
* `{ source:"renderer", asOf, ageMs, panel[] }`(判别六键 + role/id 随行;awaitingDigest 条读时交叉
|
|
162
|
+
* 核池/pending 标 `digested`)∥ 快照空 ⇒ 现链回落——ctx.state(= agent._tuiState——CLI TUI 装配)经
|
|
163
|
+
* computePanelBlocks **读时现算**(F-3 单账本——与用户所见一致),awaitingDigest 条目**读时交叉**
|
|
164
|
+
* _pendingAsyncResults/池 标注 digested(round1 #3——digested:true = 报告已消化但块仍驻留——异常块——
|
|
165
|
+
* freeze 候选;模型可定位解释 UI 怪相)。
|
|
166
|
+
* - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——onToken——TUI routeSubToken
|
|
167
|
+
* 冻结回收 ∥ 桌面经桥 relay ⇒ 渲染面归档机入流 + record——同字面、零新消费者;发射 = 规范化键。
|
|
168
|
+
* 落位复用 sub._freezeAt settle 锚点 splice,无锚点尾推兜底——AGENT-LOOP-ASYNC-POOL.md §6.8 同口径——round1 #2)。
|
|
169
|
+
* 两源皆空(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——7.2.3.2 #8 先例)
|
|
170
|
+
* → view 恒降级池视图(双池经 getAsyncPool + pending 单容器合成)+ no panel 注;freeze 报不可用。
|
|
171
|
+
* CLI-only 完整能力(AC-P4)。
|
|
105
172
|
*/
|
|
106
173
|
export function executePanelAction(args, ctx) {
|
|
107
174
|
const agent = ctx.agent
|
|
@@ -115,17 +182,22 @@ export function executePanelAction(args, ctx) {
|
|
|
115
182
|
}
|
|
116
183
|
const gate = panelFreezeGate(ctx, freezeKey)
|
|
117
184
|
if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
|
|
185
|
+
const panelKey = normalizePanelKey(freezeKey)
|
|
118
186
|
if (!ctx.callbacks?.onToken) {
|
|
119
|
-
return JSON.stringify({ status: "error", error: `panel present but no token relay in this context — the freeze of ${
|
|
187
|
+
return JSON.stringify({ status: "error", error: `panel present but no token relay in this context — the freeze of ${panelKey} cannot reach the TUI` })
|
|
120
188
|
}
|
|
121
189
|
// 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
|
|
122
190
|
// 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
|
|
123
191
|
// freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
|
|
124
|
-
|
|
192
|
+
// 桌面(回读源):同字面经桥 relay 分流 ⇒ 渲染面归档机(归档入流 + record)——零新消费者;
|
|
193
|
+
// 发射键 = 规范化键(`sub:` 前缀不入 relay 文法——§2.3)。
|
|
194
|
+
ctx.callbacks.onToken(`${panelKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
125
195
|
return JSON.stringify({
|
|
126
|
-
key:
|
|
196
|
+
key: panelKey,
|
|
127
197
|
status: "frozen",
|
|
128
|
-
note:
|
|
198
|
+
note: gate.renderer
|
|
199
|
+
? "done freeze event issued — the desktop renderer reclaimed the block into the conversation flow (archived there, record appended)"
|
|
200
|
+
: "done freeze event issued — the TUI reclaimed the block into the conversation (spliced at its settle anchor when one is recorded, else appended at the current stream end — same-rule position)",
|
|
129
201
|
})
|
|
130
202
|
}
|
|
131
203
|
// ── view 面(缺省——readonly)──
|
|
@@ -133,6 +205,17 @@ export function executePanelAction(args, ctx) {
|
|
|
133
205
|
if (args?.view === false) {
|
|
134
206
|
return JSON.stringify({ status: "error", error: "panel has nothing to do — view:false with no freeze key; pass freeze:'role#N' to reclaim a digested-stuck block, or omit view (defaults to true)" })
|
|
135
207
|
}
|
|
208
|
+
// 回读源链(子代理面板批 · 2026-10-04 · `PANEL-READBACK.md` §2.1):渲染面快照非空 ⇒ 实况回读面
|
|
209
|
+
// (判别六键 + role/id 随行 + awaitingDigest 读时交叉;asOf/ageMs 携龄——不设截断,卡留块状态恒定)。
|
|
210
|
+
const readout = typeof ctx.readout === "function" ? ctx.readout() : null
|
|
211
|
+
if (readout && Array.isArray(readout.blocks)) {
|
|
212
|
+
return JSON.stringify({
|
|
213
|
+
source: "renderer",
|
|
214
|
+
asOf: readout.receivedAt ?? null,
|
|
215
|
+
ageMs: typeof readout.receivedAt === "number" ? Math.max(0, Date.now() - readout.receivedAt) : null,
|
|
216
|
+
panel: readout.blocks.map((b) => readoutPanelEntry(agent, b)),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
136
219
|
const snap = computePanelBlocks(ctx.state)
|
|
137
220
|
if (!Array.isArray(snap)) {
|
|
138
221
|
// F-P3 降级:无 TUI state(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
|
|
@@ -174,7 +257,8 @@ export function executePanelAction(args, ctx) {
|
|
|
174
257
|
if (b.status === "running") {
|
|
175
258
|
out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
|
|
176
259
|
} else if (b.status === "awaitingDigest") {
|
|
177
|
-
// 读时交叉(round1 #3):pending/池均无对应 =
|
|
260
|
+
// 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(fallback 面注入即从两者移除;
|
|
261
|
+
// 会话面注入后不随即移除——留容器至销账 / 升级,移除 = `accountDigestRound` 的 splice;§6.31)——
|
|
178
262
|
// 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
|
|
179
263
|
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器参与比对(四族统一)。
|
|
180
264
|
// #748:consult 子块按消费判据(会话在跑 ∥ 会话条目在 pending ⇒ 未消化)——与门控同源。
|
|
@@ -243,7 +243,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
243
243
|
if (role === "eng-coder" || role === "eng-designer") {
|
|
244
244
|
const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
|
|
245
245
|
const refusal = (suffix = "") => new Error(
|
|
246
|
-
`batchDoc is required for role='${role}' — pass the batch record path (docs/batches/<batch>-<topic>.md); spawn refused without it.` + suffix)
|
|
246
|
+
`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)
|
|
247
247
|
if (!given) throw refusal()
|
|
248
248
|
batchDocAbs = resolveBatchReadPath(parent.cwd ?? process.cwd(), given)
|
|
249
249
|
if (!batchDocAbs) throw refusal(" (given path is not a readable file)")
|
package/agent-tools/subagent.mjs
CHANGED
|
@@ -132,14 +132,14 @@ export const subagentTool = {
|
|
|
132
132
|
properties: {
|
|
133
133
|
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design; semantics per action in the tool description." },
|
|
134
134
|
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)." },
|
|
135
|
-
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;
|
|
135
|
+
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)." },
|
|
136
136
|
task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria)." },
|
|
137
137
|
context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
|
|
138
138
|
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)." },
|
|
139
139
|
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)." },
|
|
140
140
|
designToken: { type: "string", description: "Required when role='eng-coder': the token from advisor(type='design') after the review passed — without it, eng-coder cannot modify files." },
|
|
141
141
|
designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the slot to close out (required to pick between several; the gate refuses to guess)." },
|
|
142
|
-
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) — 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." },
|
|
142
|
+
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." },
|
|
143
143
|
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." },
|
|
144
144
|
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." },
|
|
145
145
|
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." },
|
|
@@ -188,7 +188,9 @@ export const subagentTool = {
|
|
|
188
188
|
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
|
|
189
189
|
// (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
|
|
190
190
|
// 手工面板镜像已退役。headless/VSC 无挂载 → 现算返 null → 降级照旧。
|
|
191
|
-
|
|
191
|
+
// 子代理面板批(2026-10-04):另携 ctx.readout(= agent._panelReadout——桌面渲染面实况回读
|
|
192
|
+
// 上报缓存读面;agent-host 装配挂)——核内读源链 readout → state → 降级(PANEL-READBACK.md §2.1)。
|
|
193
|
+
if (action === "panel") return executePanelAction(args, { ...ctx, state: ctx.agent?._tuiState, readout: ctx.agent?._panelReadout })
|
|
192
194
|
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
|
|
193
195
|
// send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
|
|
194
196
|
if (action === "observe") return executeObserveAction(args, ctx)
|
package/agent-tools/verify.mjs
CHANGED
|
@@ -87,6 +87,7 @@ export const verifyTool = {
|
|
|
87
87
|
status: { type: "string", enum: ["passed", "failed", "skipped"], description: "The verification outcome you declare: 'passed' → allowed; 'failed' → blocked; 'skipped' → allowed but requires a summary reason." },
|
|
88
88
|
command: { type: "string", description: "Optional: the verification command you ran (self-reported)." },
|
|
89
89
|
summary: { type: "string", description: "Optional: the result. Required when status='skipped' — a concrete reason (e.g. \"project has no automated tests — verified by manual review\")." },
|
|
90
|
+
basis: { type: "string", description: "Optional: what the conclusion stands on — a judgment source, a read file:line coordinate, or an explicit \"unverified\" (declaration-only: no format is enforced or machine-checked). A passed/skipped declaration without it gets one advisory hint line in the report — never blocking, never changing the gate." },
|
|
90
91
|
},
|
|
91
92
|
required: ["status"],
|
|
92
93
|
},
|
|
@@ -239,12 +240,16 @@ export const verifyTool = {
|
|
|
239
240
|
lines.push("Verification declared passed.")
|
|
240
241
|
if (v.command) lines.push(` command: ${v.command}`)
|
|
241
242
|
if (v.summary) lines.push(` summary: ${v.summary}`)
|
|
243
|
+
if (v.basis) lines.push(` basis: ${v.basis}`)
|
|
244
|
+
else lines.push(" basis: not declared (advisory — does not affect the gate) — note what the conclusion stands on: a judgment source, a read file:line, or an explicit \"unverified\".")
|
|
242
245
|
ctx.agent._verifyPassed = true
|
|
243
246
|
} else if (status === "skipped") {
|
|
244
247
|
const reason = (v.summary ?? "").trim()
|
|
245
248
|
if (reason) {
|
|
246
249
|
lines.push("")
|
|
247
250
|
lines.push(`Verification skipped with reason: ${reason}`)
|
|
251
|
+
if (v.basis) lines.push(` basis: ${v.basis}`)
|
|
252
|
+
else lines.push(" basis: not declared (advisory — does not affect the gate) — note what the conclusion stands on: a judgment source, a read file:line, or an explicit \"unverified\".")
|
|
248
253
|
ctx.agent._verifyPassed = true
|
|
249
254
|
} else {
|
|
250
255
|
lines.push("")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* zero-write-watch.mjs — 零落笔看门狗叶(批 subagent-zero-write-watchdog · 台账 #934)。
|
|
3
|
+
* 设计权威 = `docs/core/design/AGENT-LOOP-UPSTREAM.md` §6.32;需求 = `docs/core/requirements/AGENT-LOOP.md` §4.16。
|
|
4
|
+
*
|
|
5
|
+
* 两族(§6.32.2):子代理族 = eng-coder / eng-designer **连续** N 轮零文件写(采集点邻单点 =
|
|
6
|
+
* `agent/turn-loop.mjs` 轮顶——streak 更新与越阈判定同点);评审族 = 异步评审 **连续** N 轮零评审
|
|
7
|
+
* 文本产出(`advisor/loop.mjs` 循环局部计数 + 可选钩子 `seams.onStallRound`)。
|
|
8
|
+
*
|
|
9
|
+
* 送达 = 复用 §6.27 上行通道(`pushChildUpstream`——note 类:同队列同消费点、零唤醒、零新容器);
|
|
10
|
+
* **只推提醒**——不自动杀 / 不自动转向 / 不代父侧动作(§6.32.10-1)。
|
|
11
|
+
* 防轰炸 = 闩 + 写后重臂(子代理族:`_zeroWriteAlerted`;评审族:单次运行闩——§6.32.3)。
|
|
12
|
+
* 文案单源 = 本档两族文本函数(§6.32.4 逐字——父侧定稿;`from` + 轮数为唯一变量)。
|
|
13
|
+
* 零依赖叶:静态 import 只取 `parent-channel.mjs` 的 `pushChildUpstream`(不引池 / 端面 / 循环)。
|
|
14
|
+
*/
|
|
15
|
+
import { pushChildUpstream } from "./parent-channel.mjs"
|
|
16
|
+
|
|
17
|
+
/** 阈值单源(连续轮数——两族共用;§6.32.2 阈值固定 50,不做配置键)。 */
|
|
18
|
+
export const ZERO_WRITE_ALERT_ROUNDS = 50
|
|
19
|
+
|
|
20
|
+
/** 子代理族触面(用户点名三角色中的两位——有文件写面;explore / plan / consult / 普通 coder / depth-0 不设)。 */
|
|
21
|
+
const WATCH_ROLES = new Set(["eng-coder", "eng-designer"])
|
|
22
|
+
|
|
23
|
+
/** 子代理族文案(§6.32.4 逐字——父侧定稿;`from` = `_upstream.label`,形 `role#id`)。 */
|
|
24
|
+
export function zeroWriteAlertText(from, rounds) {
|
|
25
|
+
return `[zero-write watchdog] ${from}: ${rounds} consecutive rounds with no file write — take a look (subagent action:'status' for the live view).`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 评审族文案(§6.32.4 逐字——父侧定稿;`from` = `advisor#<entry id>`)。 */
|
|
29
|
+
export function advisorAlertText(from, rounds) {
|
|
30
|
+
return `[zero-write watchdog] ${from}: ${rounds} consecutive rounds with no review output — take a look (subagent action:'status' / 'cancel').`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 子代理族判定 + 推送(§6.32.6)——门 = `_role` ∈ 触面 ∧ `_upstream.parent` 在场 ∧
|
|
35
|
+
* `_zeroWriteStreak` **为整数** ≥ 阈值(缺字段 / 非整数 ⇒ 不判——不冒充 `0`)∧ 未闩;
|
|
36
|
+
* 命中 ⇒ `pushChildUpstream({parent, from: _upstream.label, kind: "note", message})` + 置闩。
|
|
37
|
+
* **恒零抛**(兜底捕获兑现——自动源无模型可报错;失败面静默跳过)。
|
|
38
|
+
* @param {object} child 子 agent(eng-coder / eng-designer 形态;`_upstream` 由 spawn 站点装配)。
|
|
39
|
+
* @returns {boolean} 是否命中推送(测试面)。
|
|
40
|
+
*/
|
|
41
|
+
export function maybeZeroWriteAlert(child) {
|
|
42
|
+
try {
|
|
43
|
+
if (!WATCH_ROLES.has(child?._role)) return false
|
|
44
|
+
const upstream = child._upstream
|
|
45
|
+
if (!upstream?.parent) return false
|
|
46
|
+
const streak = child._zeroWriteStreak
|
|
47
|
+
if (!Number.isInteger(streak) || streak < ZERO_WRITE_ALERT_ROUNDS) return false
|
|
48
|
+
if (child._zeroWriteAlerted) return false
|
|
49
|
+
pushChildUpstream({
|
|
50
|
+
parent: upstream.parent,
|
|
51
|
+
from: upstream.label,
|
|
52
|
+
kind: "note",
|
|
53
|
+
message: zeroWriteAlertText(upstream.label, streak),
|
|
54
|
+
})
|
|
55
|
+
child._zeroWriteAlerted = true
|
|
56
|
+
return true
|
|
57
|
+
} catch {
|
|
58
|
+
return false // 恒零抛兑现(§6.32.6——失败面静默跳过)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 评审族推送(组合文案 + push——呼点 = 评审环路钩子 `seams.onStallRound`,单次运行至多一次)。
|
|
64
|
+
* **恒零抛**(实现面兜底捕获兑现——抛错不得上抛为评审 failed 面;§6.32.6)。
|
|
65
|
+
* @param {object} parent 父 agent(接收方——`pushChildUpstream` 载体吸收同款)。
|
|
66
|
+
* @param {string} from 来源标签(`advisor#<entry id>`)。
|
|
67
|
+
* @param {number} rounds 连续无产出轮数。
|
|
68
|
+
* @returns {boolean} 是否推送成功(测试面)。
|
|
69
|
+
*/
|
|
70
|
+
export function pushAdvisorAlert(parent, from, rounds) {
|
|
71
|
+
try {
|
|
72
|
+
pushChildUpstream({ parent, from, kind: "note", message: advisorAlertText(from, rounds) })
|
|
73
|
+
return true
|
|
74
|
+
} catch {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
}
|
package/agent.mjs
CHANGED
|
@@ -100,6 +100,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
100
100
|
// 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
|
|
101
101
|
// 段 + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
|
|
102
102
|
// CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
|
|
103
|
-
await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
|
|
103
|
+
await finalizeAgentTurn(agent, { signal, autoTurn, upstreamTurn, timerTurn, suspDriven, thrownError, depth })
|
|
104
104
|
}
|
|
105
105
|
}
|
package/auto-think.mjs
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { chat } from "./provider/core.mjs"
|
|
18
18
|
import { specForModel } from "./config.mjs"
|
|
19
|
+
// #860(AGENT-LOOP.md §6.1 ∥ §7 D-AL26):off 形族的单一实现源(写面取形 + 有效 off 路径判据)
|
|
20
|
+
import { thinkOffPath, thinkOffShape } from "./think-off.mjs"
|
|
19
21
|
|
|
20
22
|
const CLASSIFY_PROMPT = `Classify this coding task's difficulty: low, medium, or high.
|
|
21
23
|
|
|
@@ -31,6 +33,10 @@ const EFFORT_MAP = {
|
|
|
31
33
|
high: ["max", "max", "xhigh", "max"],
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/** #860(D-AL26):不可关思考的跳过警示 ∕ 分类失败——进程级一次性(按 model ∕ 错误签名去重,防逐轮刷屏)。 */
|
|
37
|
+
const warnedThinkGuard = new Set()
|
|
38
|
+
const warnedClassifyFailure = new Set()
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* Build classifier input from history: the latest real user message (reminders and
|
|
36
42
|
* interrupt injections excluded), plus the previous user message as context when the
|
|
@@ -67,13 +73,25 @@ export async function classifyAndApply(agent, turn) {
|
|
|
67
73
|
const validEfforts = spec.reasoningEffortEnum
|
|
68
74
|
if (!validEfforts) return null // Model doesn't support reasoning effort
|
|
69
75
|
|
|
76
|
+
// #860 守卫(D-AL26):无法关思考的模型(thinkAlwaysOn ∕ 枚举无 none)⇒ 分类调用必然失败
|
|
77
|
+
//(思考吃光小预算 ∕ 400)——不调用;一次可见警示(按 model 去重),语义回退 null。
|
|
78
|
+
if (!thinkOffPath(spec)) {
|
|
79
|
+
if (!warnedThinkGuard.has(agent.provider.model)) {
|
|
80
|
+
warnedThinkGuard.add(agent.provider.model)
|
|
81
|
+
console.warn(`[auto-think] model "${agent.provider.model}" cannot disable thinking — difficulty classification skipped (reasoning effort unchanged)`)
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
const prompt = buildClassifierInput(agent.history)
|
|
71
87
|
if (prompt == null) return null
|
|
72
88
|
|
|
73
89
|
// Classification call: use same provider, minimal tokens, no tools, no streaming
|
|
74
90
|
let level
|
|
75
91
|
try {
|
|
76
|
-
|
|
92
|
+
// #860:关思考形(族别 off 形)+ 小预算 32(原 10 token 被思考吃光)+ reasoningEffort 清空
|
|
93
|
+
//(防父档外溢;effort 族 off 形由载荷门补发 reasoning_effort:"none")。
|
|
94
|
+
const classifierProvider = { ...agent.provider, maxTokens: 32, thinking: thinkOffShape(spec), reasoningEffort: null }
|
|
77
95
|
const response = await chat(classifierProvider, {
|
|
78
96
|
messages: [
|
|
79
97
|
{ role: "system", content: CLASSIFY_PROMPT },
|
|
@@ -101,7 +119,13 @@ export async function classifyAndApply(agent, turn) {
|
|
|
101
119
|
else if (word.startsWith("medium") || word.startsWith("med")) level = "medium"
|
|
102
120
|
else if (word.startsWith("high")) level = "high"
|
|
103
121
|
else return null // Unparseable
|
|
104
|
-
} catch {
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// #860(D-AL26):失败一次可见(按错误签名进程级去重——不再静默);回退 null 语义不变。
|
|
124
|
+
const sig = e?.message ?? String(e)
|
|
125
|
+
if (!warnedClassifyFailure.has(sig)) {
|
|
126
|
+
warnedClassifyFailure.add(sig)
|
|
127
|
+
console.warn(`[auto-think] classification failed: ${sig}`)
|
|
128
|
+
}
|
|
105
129
|
return null // Classification failure → fall back to current setting
|
|
106
130
|
}
|
|
107
131
|
|
package/config-presets.mjs
CHANGED
|
@@ -42,6 +42,14 @@ export const PROVIDER_PRESETS = {
|
|
|
42
42
|
siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32_000, desc: "SiliconFlow (硅基流动)" },
|
|
43
43
|
openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32_000, desc: "OpenRouter" },
|
|
44
44
|
groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32_000, desc: "Groq" },
|
|
45
|
+
// OpenCode Go 订阅网关(opencode.ai/zen 取 sk- key ∥ 无 OAuth)——官方档(go.mdx)双协议混装 ⇒ 拆双预设
|
|
46
|
+
// (PROVIDER.md §6.11):本键 = OpenAI 兼容 `/chat/completions` 侧;`opencode-go-anthropic` = Anthropic
|
|
47
|
+
// `/messages` 侧(同 key——模型按侧选)。字段集 = 最小面:`thinking` / `reasoningEffort` 不设 = 不发
|
|
48
|
+
// (D-13 同口径);`maxTokens` 仅 anthropic 侧设 `65536`(该 transport `max_tokens` 必发——不设即落规格
|
|
49
|
+
// 行值 131072,超渠道口径)。**待验(无 key——未实拉 `/models` ∕ 载荷未实测;接入后按实拉复核)**;
|
|
50
|
+
// `x-opencode-session` 头 = 可选遥测——**不发**(亦不动 `providers[].headers` 面)。
|
|
51
|
+
"opencode-go": { baseURL: "https://opencode.ai/zen/go/v1", model: "glm-5.2", desc: "OpenCode Go subscription (sk- key from opencode.ai/zen; OpenAI-compatible models — MiniMax/Qwen via opencode-go-anthropic)" },
|
|
52
|
+
"opencode-go-anthropic": { baseURL: "https://opencode.ai/zen/go/v1", model: "qwen3.7-max", format: "anthropic", maxTokens: 65536, desc: "OpenCode Go — Anthropic Messages side (same sk- key as opencode-go; MiniMax/Qwen models)" },
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/** Build the stored provider entry from a preset — strip the display field, keep the rest
|
package/config.mjs
CHANGED
|
@@ -188,6 +188,31 @@ function warnConsultModelsFiltered(dropped, path) {
|
|
|
188
188
|
`\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/** #861(AGENT-LOOP-SUBAGENT.md §6.7.1):subagent 模型覆盖加载期清洗——单源纯函数(核 loadConfig ∥ VSC 端壳 raw 读点同消费);非法形态不 throw(`subagentModel` 非法 ⇒ null、`subagentModels` 非对象 ⇒ {}、值非法 ⇒ 剔除该键)。 */
|
|
192
|
+
export function sanitizeSubagentModel(v) {
|
|
193
|
+
const ok = typeof v === "string" && v.trim()
|
|
194
|
+
const dropped = ok || v == null ? [] : [`agent.subagentModel must be a non-empty string ("provider:model" | provider | model) — got ${Array.isArray(v) ? "array" : typeof v} — ignored (inherits the parent provider)`]
|
|
195
|
+
return { value: ok ? v : null, dropped }
|
|
196
|
+
}
|
|
197
|
+
export function sanitizeSubagentModels(v) {
|
|
198
|
+
if (v == null) return { value: {}, dropped: [] }
|
|
199
|
+
if (typeof v !== "object" || Array.isArray(v)) return { value: {}, dropped: [`agent.subagentModels must be an object mapping role → model override — got ${Array.isArray(v) ? "array" : typeof v} — ignored`] }
|
|
200
|
+
const value = {}, dropped = []
|
|
201
|
+
for (const [role, val] of Object.entries(v)) {
|
|
202
|
+
if (typeof val === "string" && val.trim()) { value[role] = val; continue }
|
|
203
|
+
dropped.push(`agent.subagentModels.${role} must be a non-empty string ("provider:model" | provider | model) — got ${Array.isArray(val) ? "array" : typeof val} — dropped`)
|
|
204
|
+
}
|
|
205
|
+
return { value, dropped }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let warnedSubagentModels = false // #861:清洗警告——进程级一次性(同 consultModels 先例)
|
|
209
|
+
function warnSubagentModelsFiltered(dropped, path) {
|
|
210
|
+
if (warnedSubagentModels || dropped.length === 0) return
|
|
211
|
+
warnedSubagentModels = true
|
|
212
|
+
console.warn(`[config] agent.subagentModel/subagentModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
|
|
213
|
+
`${dropped.map((d) => ` - ${d}`).join("\n")}\n Fix: clean it in ${path} — "provider:model" | provider | model.`)
|
|
214
|
+
}
|
|
215
|
+
|
|
191
216
|
/**
|
|
192
217
|
* Find provider by name in providers[].
|
|
193
218
|
* Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
|
|
@@ -311,6 +336,11 @@ export function loadConfig() {
|
|
|
311
336
|
merged.agent.consultModels = cmClean.keep
|
|
312
337
|
if (cmClean.dropped.length) merged.agent.consultModelsFiltered = cmClean.dropped
|
|
313
338
|
|
|
339
|
+
const smClean = sanitizeSubagentModel(merged.agent.subagentModel), smsClean = sanitizeSubagentModels(merged.agent.subagentModels) // #861(§6.7.1):加载期清洗——非法形态不采纳 + 一次性警告(不 throw——同 F-4 范式)
|
|
340
|
+
warnSubagentModelsFiltered([...smClean.dropped, ...smsClean.dropped], path)
|
|
341
|
+
merged.agent.subagentModel = smClean.value
|
|
342
|
+
merged.agent.subagentModels = smsClean.value
|
|
343
|
+
|
|
314
344
|
// Backward compatibility: promote root-level config fields to agent sub-object
|
|
315
345
|
if (config.verifyGuard !== undefined) {
|
|
316
346
|
merged.agent.verifyGuard = config.verifyGuard
|
package/conventions.mjs
CHANGED
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
* code — inside a declared code segment (default: the path segment `src`), or the
|
|
27
27
|
* fallback; doc — documentation extension outside any code segment and not
|
|
28
28
|
* scratch; temp — tmp-* name or .tmp/.temp extension; aux — a default
|
|
29
|
-
* auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp —
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp — F9)
|
|
30
|
+
* that matched no code segment and neither doc nor temp; state — a project state
|
|
31
|
+
* file (`PROJECT-MANIFEST.json` basename ∥ `.thincoder/conventions.json` pair — #941).
|
|
32
|
+
* Precedence: code segment → temp → doc → aux → state → fallback code.
|
|
32
33
|
*
|
|
33
34
|
* Segment matching runs on the PROJECT-ROOT-RELATIVE FACE of a path (D17 · 2026-09-28): the
|
|
34
35
|
* machine layout ABOVE the project root never participates. An absolute path outside the
|
|
@@ -128,18 +129,32 @@ function hasAuxSegment(p, conv) {
|
|
|
128
129
|
return hasSegmentSequence(p, DEFAULT_AUX_PATHS, conv?.root, false)
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
/**
|
|
132
|
+
/** Project state-file markers (data — #941 · PORTABILITY D19): manifest basename (any layer) ∥
|
|
133
|
+
* retired-carrier pair; single sources: `MANIFEST_REL` ∥ `RETIRED_REL_PATH`. */
|
|
134
|
+
const STATE_BASENAME = "project-manifest.json"
|
|
135
|
+
const STATE_PAIR = ".thincoder/conventions.json"
|
|
136
|
+
|
|
137
|
+
/** True when `s` names a project state file (basename any layer ∥ segment pair; case-insensitive — no face, D17). */
|
|
138
|
+
function isStateFile(s) {
|
|
139
|
+
const segs = segmentsOf(s)
|
|
140
|
+
if (segs.length > 0 && lowerSeg(segs[segs.length - 1]) === STATE_BASENAME) return true
|
|
141
|
+
return hasSequenceIn(segs, [STATE_PAIR])
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** "code" | "doc" | "temp" | "aux" | "state" — the single classification decision.
|
|
132
145
|
* Precedence: code segment first (src/** stays product code even when the name
|
|
133
146
|
* looks scratch — the pre-existing unconditional-src rule), then temp, then a
|
|
134
147
|
* documentation extension, then an auxiliary-path segment sequence (test / tests
|
|
135
|
-
* / scripts / .thincoder/tmp — F9),
|
|
136
|
-
*
|
|
148
|
+
* / scripts / .thincoder/tmp — F9), then a project state file (#941), else code
|
|
149
|
+
* (anything not doc/temp/aux/state is product code). Segment checks face-bound (D17);
|
|
150
|
+
* state matches by name marker. */
|
|
137
151
|
export function classifyPath(p, conv) {
|
|
138
152
|
const s = String(p ?? "")
|
|
139
153
|
if (hasCodeSegment(s, conv)) return "code"
|
|
140
154
|
if (TEMP_FILE.test(s)) return "temp"
|
|
141
155
|
if (DOC_FILE.test(s)) return "doc"
|
|
142
156
|
if (hasAuxSegment(s, conv)) return "aux"
|
|
157
|
+
if (isStateFile(s)) return "state"
|
|
143
158
|
return "code"
|
|
144
159
|
}
|
|
145
160
|
|
|
@@ -165,6 +180,11 @@ export function isAuxPath(p, conv) {
|
|
|
165
180
|
return classifyPath(p, conv) === "aux"
|
|
166
181
|
}
|
|
167
182
|
|
|
183
|
+
/** True when the path is a project state file (`classifyPath === "state"` — #941; derived form). */
|
|
184
|
+
export function isStatePath(p, conv) {
|
|
185
|
+
return classifyPath(p, conv) === "state"
|
|
186
|
+
}
|
|
187
|
+
|
|
168
188
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
169
189
|
// 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
|
|
170
190
|
// 转口保名(消费面 / 批内件 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
|
package/declaration.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
|
|
6
6
|
* 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync } from "node:fs"
|
|
9
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
8
|
+
import { existsSync, statSync } from "node:fs"
|
|
9
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
10
10
|
import { DEFAULT_MANIFEST, manifestFilePath, readManifest } from "./manifest.mjs"
|
|
11
11
|
import { logEvent } from "./log.mjs"
|
|
12
12
|
|
|
@@ -140,6 +140,30 @@ export function declaredPublicRoots(cwd) {
|
|
|
140
140
|
return out
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/** 引用解析单点(§6.1 口径 4/5——写门 ∥ 迁移旗同消费,不得二写):① 仓根解析 `resolve(base, part)`
|
|
144
|
+
* 为存在的档 ⇒ 命中;② 声明源前缀解析序:首段命中声明公共仓目录名(消费既有 `declaredPublicRoots`
|
|
145
|
+
* ——同名多仓 ⇒ 声明序首者)⇒ 于该声明根解析余段;未声明 ∥ 声明根缺位 ∥ 目标档缺 ⇒ `{ ok: false, abs }`。
|
|
146
|
+
* @param {string} base 仓根基准(写门口径 2 同一子表达式)
|
|
147
|
+
* @param {string} part task_book 文件部分(首个 `§` 前子串,trim——调用面已判缺位)
|
|
148
|
+
* @returns {{ok: boolean, abs: string}}
|
|
149
|
+
*/
|
|
150
|
+
export function resolveDeclaredRef(base, part) {
|
|
151
|
+
const abs = resolve(base, part)
|
|
152
|
+
if (isFile(abs)) return { ok: true, abs }
|
|
153
|
+
const segs = String(part).replace(/\\/g, "/").split("/").filter(Boolean)
|
|
154
|
+
for (const root of declaredPublicRoots(base)) {
|
|
155
|
+
if (segs[0] !== basename(root)) continue
|
|
156
|
+
if (segs.length > 1) {
|
|
157
|
+
const hit = resolve(root, segs.slice(1).join("/"))
|
|
158
|
+
if (isFile(hit)) return { ok: true, abs: hit }
|
|
159
|
+
}
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
return { ok: false, abs }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
166
|
+
|
|
143
167
|
const _cache = new Map()
|
|
144
168
|
|
|
145
169
|
/** Drop the per-data-file-path cache (test seam — declarations are read once per path). */
|