@thincoder/core 0.10.1 → 0.10.3
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 +46 -0
- package/advisor/citations.mjs +13 -3
- package/advisor/convergence.mjs +1 -1
- package/advisor/messages.mjs +3 -3
- package/agent/dispatch-run.mjs +29 -12
- package/agent/record-results.mjs +6 -1
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +35 -9
- package/agent/turn-loop.mjs +6 -0
- package/agent-tools/advisor-async.mjs +11 -1
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/batch-lifecycle.mjs +3 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +28 -0
- package/agent-tools/batch.mjs +5 -3
- package/agent-tools/design-token.mjs +56 -16
- package/agent-tools/subagent-async.mjs +6 -1
- package/agent-tools/subagent-panel.mjs +119 -36
- package/agent-tools/subagent.mjs +4 -2
- package/agent-tools/verify.mjs +5 -0
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +42 -8
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- 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.mjs +36 -62
- package/manifest-schema.mjs +13 -3
- package/manifest.mjs +27 -8
- 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/model-ref.mjs +96 -3
- 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/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +3 -1
- package/provider/core.mjs +10 -4
- package/provider/google.mjs +18 -2
- package/provider/normalize.mjs +57 -0
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session-lifecycle.mjs +21 -12
- package/session.mjs +13 -2
- package/stream-destroy.mjs +62 -0
- package/tool-docs/verify.md +1 -1
- package/tools/edit-batch.mjs +7 -2
- package/undo-stack.mjs +23 -9
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 沿革:初由 agent-tools/advisor.mjs 迁至 advisor-async.mjs(sync wrapper 与 async
|
|
7
7
|
* settle 共享一套实现——无 wrapper↔runner 环),再由 advisor-async.mjs 迁入本文件。
|
|
8
|
-
* 既有 import 面不变:advisor-async.mjs re-export
|
|
9
|
-
* 的 import 路径原样保留——advisor.mjs 再转发 validateDesignToken 给 spawn
|
|
8
|
+
* 既有 import 面不变:advisor-async.mjs re-export 原 7 个导出(advisor.mjs 与测试
|
|
9
|
+
* 的 import 路径原样保留——advisor.mjs 再转发 validateDesignToken 给 spawn 门禁);
|
|
10
|
+
* 本批 §5.1 新助手(stripDesignTokenEcho / makeDesignTokenPrefixRegex)为新增直连面
|
|
11
|
+
* (设计档 ENG-TOKEN-BINDING.md §5.1——不经 advisor-async re-export)。
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { randomUUID } from "node:crypto"
|
|
@@ -64,15 +66,46 @@ export function makeDesignTokenRegex(token, flags = "") {
|
|
|
64
66
|
)
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** Build a [DESIGN-TOKEN:...] regex matching the token's uuid PREFIX form — the
|
|
70
|
+
* truncated echo (tail missing/inexact, §5.1③; same boundary discipline as the full regex). */
|
|
71
|
+
export function makeDesignTokenPrefixRegex(token, flags = "") {
|
|
72
|
+
const uuid = String(token).split(":")[0]
|
|
73
|
+
const escaped = uuid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
74
|
+
return new RegExp(
|
|
75
|
+
`(?:^|\\s|\`|\\*)\\[DESIGN-TOKEN:\\s*${escaped}[^\\]\\s]*\\s*\\](?:\\s|$|\`|\\*)`,
|
|
76
|
+
flags + "ms"
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** §5.1③ stripping single source: strip every echo form of THIS token — the full
|
|
81
|
+
* [DESIGN-TOKEN:uuid:expiresAt] form, the truncated prefix form, and the bare
|
|
82
|
+
* uuid value (token-unique random ⇒ zero collateral; empty uuid → bare pass
|
|
83
|
+
* skipped — defensive). Other tokens / other uuids pass through untouched. */
|
|
84
|
+
export function stripDesignTokenEcho(text, designToken) {
|
|
85
|
+
if (typeof text !== "string" || !designToken) return text
|
|
86
|
+
const uuid = String(designToken).split(":")[0]
|
|
87
|
+
let out = text.replace(makeDesignTokenRegex(designToken, "g"), "")
|
|
88
|
+
out = out.replace(makeDesignTokenPrefixRegex(designToken, "g"), "")
|
|
89
|
+
if (uuid) out = out.split(uuid).join("")
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** §5.1② constant settlement markers (verbatim — machine-greppable; M1 is
|
|
94
|
+
* emitted even when the review body is empty). */
|
|
95
|
+
const NO_VALID_ECHO_MARK = "未回显有效 token——未签发 (no valid token echo — no design token was issued)"
|
|
96
|
+
const TRUNCATED_ECHO_MARK = "注:回显被截断(uuid 之后缺失或不符)——已按批准结算;全串 token 已签发 (note: truncated echo — the part after the uuid was missing or inexact; settled as approved; the full token was issued)"
|
|
97
|
+
|
|
67
98
|
/**
|
|
68
99
|
* Shared design-review settlement (sync wrapper + async settle): the token echo
|
|
69
|
-
* IS the verdict — the advisor echoes it only on approval. On echo
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* the clean output with the Approved
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
100
|
+
* IS the verdict — the advisor echoes it only on approval. On echo (full string
|
|
101
|
+
* OR truncated — uuid present; §5.1①): slot the token under designId (always the
|
|
102
|
+
* FULL token; + eng-coder gate flag; single-value mirror retired per
|
|
103
|
+
* DESIGN-TOKEN-SETTLEMENT D3) and return the clean output with the Approved
|
|
104
|
+
* suffix (truncated echo: the M2 marker sits before the suffix); the review
|
|
105
|
+
* instance CLOSES (a later review of the same doc-set starts a fresh full
|
|
106
|
+
* review). On non-echo: strip every echo form (single source §5.1③) and return
|
|
107
|
+
* the findings text + the constant M1 marker — slots stay untouched
|
|
108
|
+
* (方案 ②: a failed re-review revokes nothing).
|
|
76
109
|
* @param {Object} [opts] — 第 11 批(A/F11):`opts.incomplete` = 宿主尾族判定 kind
|
|
77
110
|
* (单谓词 `advisorIncompleteMarker`——结算方透传)。非空 ⇒ **一律不签发**(无论评审文本
|
|
78
111
|
* 是否回显 token):剥除全部 token 回显 + 追加未签发提示(原因 + 恢复指引),不写槽、
|
|
@@ -86,16 +119,20 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
|
|
|
86
119
|
// A(F11/ADVISOR-GUARDS.md §1 消费点 1):未完成即不签发——判据与 code 完成守卫同源(单谓词)。
|
|
87
120
|
const incomplete = opts?.incomplete ?? null
|
|
88
121
|
if (incomplete) {
|
|
89
|
-
const stripped = rawResult
|
|
122
|
+
const stripped = stripDesignTokenEcho(rawResult, designToken).trim()
|
|
90
123
|
return {
|
|
91
124
|
passed: false,
|
|
92
125
|
output: `${stripped}\n\n评审未完成——token 未签发 (review incomplete — no design token issued; reason: ${incomplete})\n以更小范围重跑设计评审(逐档 / 逐节拆分,或拆到两次评审),或调大 agent.advisor.timeoutMs 后重试;补充检查未完成的部分不得按已核处理。`.trim(),
|
|
93
126
|
}
|
|
94
127
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
// §5.1① 截断容忍:全串匹配 ∨ 截断形(本 token 的 uuid 值出现——空 uuid 不构成「值」)
|
|
129
|
+
// ⇒ 认 pass;两者皆无 ⇒ fail + 恒定 M1(含正文为空——N7 零静默)。
|
|
130
|
+
const uuid = String(designToken).split(":")[0]
|
|
131
|
+
const fullEcho = makeDesignTokenRegex(designToken).test(rawResult)
|
|
132
|
+
const truncatedEcho = !fullEcho && uuid.length > 0 && rawResult.includes(uuid)
|
|
133
|
+
if (!fullEcho && !truncatedEcho) {
|
|
134
|
+
const stripped = stripDesignTokenEcho(rawResult, designToken).trim()
|
|
135
|
+
return { passed: false, output: [stripped, NO_VALID_ECHO_MARK].filter(Boolean).join("\n\n") }
|
|
99
136
|
}
|
|
100
137
|
// Echoed the token → review passed. Issue it to the parent for eng-coder.
|
|
101
138
|
agent._engDesignTokens ??= new Map()
|
|
@@ -106,12 +143,15 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
|
|
|
106
143
|
// see the sync wrapper's note: unreachable today, kept for parity).
|
|
107
144
|
if (agent._role === "eng-coder") agent._engDesignReviewed = true
|
|
108
145
|
run.open = false // approval closes this doc-set instance — next review is fresh
|
|
109
|
-
|
|
146
|
+
run.priorOutput = null // #863 关闭点轻量化:审批关同拍释放(closed 实例零消费面——续跑仅取 open)
|
|
147
|
+
const clean = stripDesignTokenEcho(rawResult, designToken).trim()
|
|
110
148
|
// F2c: id echo + omission guidance + point-in-time slot snapshot — the
|
|
111
149
|
// same suffix the F2e prior stores strip with (stored for the exact truncation).
|
|
112
150
|
run.approvedSuffix = buildApprovedSuffix(designToken, run.designId, agent._engDesignTokens.size)
|
|
113
151
|
return {
|
|
114
152
|
passed: true,
|
|
115
|
-
|
|
153
|
+
// §5.1② 标记位置不变式:M2 恒居 approvedSuffix 之前、suffix 恒居文末
|
|
154
|
+
// (stripApprovedSuffix 的精确截断依赖此形——尾部追加改动不得破坏)。
|
|
155
|
+
output: [clean, truncatedEcho ? TRUNCATED_ECHO_MARK : null, run.approvedSuffix].filter(Boolean).join("\n\n"),
|
|
116
156
|
}
|
|
117
157
|
}
|
|
@@ -139,7 +139,12 @@ export function runningPoolCount(parent, pool) {
|
|
|
139
139
|
* API keys come from config.json only (env vars are not a key source).
|
|
140
140
|
*/
|
|
141
141
|
export function resolveChildProvider(parent, modelArg) {
|
|
142
|
-
if (
|
|
142
|
+
if (modelArg == null || modelArg === "") return { ...parent.provider } // null ∕ undefined ∕ 空串 = 不覆盖(既有等价语义)
|
|
143
|
+
// #861(AGENT-LOOP-SUBAGENT.md §6.7.1 ②)运行期防御——四消费链汇点(config ∥ tool model 参数 ∥
|
|
144
|
+
// consult ∥ vision-reader):其余非字符串(对象 ∕ 数组 ∕ 数字 ∕ 布尔)明确报错(不裸 TypeError——可自纠)。
|
|
145
|
+
if (typeof modelArg !== "string") {
|
|
146
|
+
throw new Error(`subagent model override must be a string ("provider:model" | provider | model); got ${Array.isArray(modelArg) ? "array" : typeof modelArg}`)
|
|
147
|
+
}
|
|
143
148
|
// "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
|
|
144
149
|
// the literal "default", matched case-insensitively, declares "no override →
|
|
145
150
|
// inherit the default model" — equivalent to null/omission (parent provider
|
|
@@ -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 完整能力)→
|
package/agent-tools/subagent.mjs
CHANGED
|
@@ -132,7 +132,7 @@ 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)." },
|
|
@@ -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("")
|
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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
15
15
|
import { dirname, join } from "node:path"
|
|
16
|
-
import { parseModelRef, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
|
|
16
|
+
import { parseModelRef, resolveChannelModel, resolveProviderPlan, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
|
|
17
17
|
// 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
|
|
18
18
|
import { migrateLegacyModelFields } from "./config-migrate.mjs"
|
|
19
19
|
import { expandHome } from "./expand-home.mjs"
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
_configPath, writeConfigAtomic,
|
|
24
24
|
} from "./config-io.mjs"
|
|
25
25
|
|
|
26
|
-
export { parseModelRef, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
|
|
26
|
+
export { parseModelRef, resolveChannelModel, resolveProviderPlan, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
|
|
27
27
|
export { PROVIDER_PRESETS, presetToEntry, configDir, configPath, _setConfigPathForTest, _resetConfigPathForTest }
|
|
28
28
|
// 核内单一写盘执行体(已迁 `config-io.mjs`)——本档 re-export 保持既有导入面(§2.5 #131)。
|
|
29
29
|
export { writeConfigAtomic }
|
|
@@ -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
|
|
@@ -325,14 +355,18 @@ export function loadConfig() {
|
|
|
325
355
|
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
326
356
|
merged.proxy = normalizeProxy(merged.proxy)
|
|
327
357
|
|
|
328
|
-
// Runtime provider =
|
|
329
|
-
//
|
|
330
|
-
// 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
|
|
331
|
-
// throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
332
|
-
|
|
358
|
+
// Runtime provider = 统一解析(#841——resolveProviderPlan 单源:回退序 + 三态;PROVIDER.md §6.22)。
|
|
359
|
+
// 不可运行(渠表空 ∥ 全表无 key)⇒ {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打
|
|
360
|
+
// _providerInvalid 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
|
|
361
|
+
// findProvider throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
362
|
+
const plan = resolveProviderPlan({ providers: merged.providers, defaultModel: merged.defaultModel })
|
|
363
|
+
merged.provider = plan.provider
|
|
364
|
+
merged.providerState = plan.state // ok ∥ fallback ∥ invalid(三值闭集——KD-841-3 config 级)
|
|
365
|
+
merged.providerStateReason = plan.reason // state≠ok 时非空(语义源;消费者按 state 出词——禁串嗅探)
|
|
366
|
+
// providerInvalidReason 语义不变(provider 不完整时非空——装配校验 validateProvider 消费面照旧)
|
|
333
367
|
merged.providerInvalidReason = merged.provider.name
|
|
334
368
|
? null
|
|
335
|
-
: defaultModelReason(merged.providers, merged.defaultModel)
|
|
369
|
+
: plan.reason ?? defaultModelReason(merged.providers, merged.defaultModel)
|
|
336
370
|
|
|
337
371
|
// Compaction threshold follows the model (provider-level context override honored — providerSpec)
|
|
338
372
|
const explicitThreshold = config.agent?.compactThreshold
|
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). */
|