@thincoder/core 0.10.1 → 0.10.2
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 +13 -0
- package/config.mjs +12 -8
- package/model-ref.mjs +96 -3
- package/package.json +1 -1
- package/session-lifecycle.mjs +21 -12
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to the core package are documented here.
|
|
4
4
|
Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
|
|
5
5
|
|
|
6
|
+
## [0.10.2] — 2026-10-03
|
|
7
|
+
|
|
8
|
+
> 0.10.1 → 0.10.2(月内 +1——发布时定号)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **统一 provider 解析(#841 · 三端归一)**:核导出 `resolveProviderPlan`(回退链 = 槽渠道(持 key)→ `defaultModel` 渠道 → 首个持 key 渠道;模型面 = 槽模型 → `defaultModel`(同渠道)→ 渠道单值 → `null`;三态 = `ok` ∕ `fallback` ∕ `invalid` + `reason`)与 `resolveChannelModel`(单值转口面)。
|
|
13
|
+
- **`providerState` ∕ `providerStateReason` 两键**:`loadConfig()` 落(`provider` = 解析入选渠道)+ 槽面 key 门(`applySession`——槽无 key ⇒ 跳过且不写槽)。
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **「无效」语义收窄**:仅「无 provider ∥ 无 key」为 `invalid`(不可运行 ⇒ 引导配置);「可运行而无有效 `defaultModel`」为 `fallback`(**可运行 + 必明示**——不再当无效拦下)。
|
|
18
|
+
|
|
6
19
|
## [0.10.1] — 2026-10-03
|
|
7
20
|
|
|
8
21
|
> 0.9.5 → 0.10.1(月切换——十月首发,发布时定号)
|
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 }
|
|
@@ -325,14 +325,18 @@ export function loadConfig() {
|
|
|
325
325
|
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
326
326
|
merged.proxy = normalizeProxy(merged.proxy)
|
|
327
327
|
|
|
328
|
-
// Runtime provider =
|
|
329
|
-
//
|
|
330
|
-
// 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
|
|
331
|
-
// throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
332
|
-
|
|
328
|
+
// Runtime provider = 统一解析(#841——resolveProviderPlan 单源:回退序 + 三态;PROVIDER.md §6.22)。
|
|
329
|
+
// 不可运行(渠表空 ∥ 全表无 key)⇒ {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打
|
|
330
|
+
// _providerInvalid 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
|
|
331
|
+
// findProvider throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
332
|
+
const plan = resolveProviderPlan({ providers: merged.providers, defaultModel: merged.defaultModel })
|
|
333
|
+
merged.provider = plan.provider
|
|
334
|
+
merged.providerState = plan.state // ok ∥ fallback ∥ invalid(三值闭集——KD-841-3 config 级)
|
|
335
|
+
merged.providerStateReason = plan.reason // state≠ok 时非空(语义源;消费者按 state 出词——禁串嗅探)
|
|
336
|
+
// providerInvalidReason 语义不变(provider 不完整时非空——装配校验 validateProvider 消费面照旧)
|
|
333
337
|
merged.providerInvalidReason = merged.provider.name
|
|
334
338
|
? null
|
|
335
|
-
: defaultModelReason(merged.providers, merged.defaultModel)
|
|
339
|
+
: plan.reason ?? defaultModelReason(merged.providers, merged.defaultModel)
|
|
336
340
|
|
|
337
341
|
// Compaction threshold follows the model (provider-level context override honored — providerSpec)
|
|
338
342
|
const explicitThreshold = config.agent?.compactThreshold
|
package/model-ref.mjs
CHANGED
|
@@ -11,15 +11,23 @@
|
|
|
11
11
|
* are gone with activeProvider/activeModel (F-1), so a bare provider or bare model is
|
|
12
12
|
* invalid everywhere (裁定③——裸 provider 拒——显式 p:m)。
|
|
13
13
|
*
|
|
14
|
-
* Runtime model resolution:
|
|
14
|
+
* Runtime model resolution (#841 单源 = resolveProviderPlan——机制全文 PROVIDER.md §6.22):
|
|
15
15
|
* parseModelRef(ref, providers) → { ok, provider, model } | { ok:false, reason }
|
|
16
|
+
* resolveChannelModel(entry, defaultModel)
|
|
17
|
+
* → 渠道模型面单值:defaultModel 属本渠道 ⇒ 其模型段 ∥
|
|
18
|
+
* 渠道单值 `entry.model` ∥ null(VSC 转口面)
|
|
19
|
+
* resolveProviderPlan({ providers, defaultModel, slot })
|
|
20
|
+
* → { state, source, channel, model, provider, reason }
|
|
21
|
+
* 统一回退解析(纯函数 · 零 I/O——三端消费同一函数):
|
|
22
|
+
* 渠道面回退序三步 + 模型面四步 + 三态 ok ∥ fallback ∥ invalid
|
|
16
23
|
* resolveRuntimeProvider(providers, defaultModel)
|
|
17
24
|
* → provider object with `.model` set, or {} when
|
|
18
25
|
* defaultModel is null/invalid (D-S1: callers mark
|
|
19
26
|
* _providerInvalid — never throws)
|
|
20
27
|
* defaultModelReason(providers, defaultModel) → human reason for the D-S1 marker
|
|
21
28
|
*
|
|
22
|
-
* Consumers: config.mjs loadConfig (re-exported as the config hub), make-agent.mjs
|
|
29
|
+
* Consumers: config.mjs loadConfig (re-exported as the config hub), make-agent.mjs,
|
|
30
|
+
* session-lifecycle.mjs applySession(槽面), VSC panel-turn-stages ∥ presets(核转口)。
|
|
23
31
|
*/
|
|
24
32
|
export function parseModelRef(ref, providers) {
|
|
25
33
|
if (typeof ref !== "string" || !ref.trim()) {
|
|
@@ -47,7 +55,9 @@ export function parseModelRef(ref, providers) {
|
|
|
47
55
|
/** Resolve the runtime provider object for config.defaultModel: provider clone carrying
|
|
48
56
|
* `.model` = the parsed concrete model (API/spec consumers read provider.model unchanged).
|
|
49
57
|
* Invalid/missing defaultModel → {} (D-S1 shape — name/model/baseURL absent so
|
|
50
|
-
* validateProvider flags it; TUI/headless surface the reason from defaultModelReason).
|
|
58
|
+
* validateProvider flags it; TUI/headless surface the reason from defaultModelReason).
|
|
59
|
+
* #841 后:运行时取值 = `resolveProviderPlan`(回退序 + 三态);本函数保留为**严格单发**解析面
|
|
60
|
+
* (零回退——解析未过即 `{}`),新码勿调。 */
|
|
51
61
|
export function resolveRuntimeProvider(providers, defaultModel) {
|
|
52
62
|
const r = parseModelRef(defaultModel, providers)
|
|
53
63
|
if (!r.ok) return {}
|
|
@@ -64,3 +74,86 @@ export function defaultModelReason(providers, defaultModel) {
|
|
|
64
74
|
}
|
|
65
75
|
return parseModelRef(defaultModel, list).reason ?? `defaultModel "${defaultModel}" 无效`
|
|
66
76
|
}
|
|
77
|
+
|
|
78
|
+
/** 「持 key」判据(#841——`providers[].apiKey` trim 后非空;env 变量不是密钥源,config.mjs 档头同判)。 */
|
|
79
|
+
function hasKey(entry) {
|
|
80
|
+
return typeof entry?.apiKey === "string" && entry.apiKey.trim() !== ""
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 渠道模型面单值(#841 模型面 ②③——VSC `resolveDefaultModel` 转口面 · 纯函数):
|
|
84
|
+
* ① `defaultModel` 解析通过 ∧ 其渠道 == `entry` ⇒ 其模型段(首冒号分割 · 双段非空 · name 相等);
|
|
85
|
+
* ② 渠道单值 `entry.model`(非空串);③ `null`(合法——模型由运行期 `/models` 候选 ∥ 用户选择决定)。
|
|
86
|
+
* @param {object|null} entry — providers[] 条目(或同形对象)
|
|
87
|
+
* @param {string|null} defaultModel — 顶层复合串
|
|
88
|
+
* @returns {string|null} */
|
|
89
|
+
export function resolveChannelModel(entry, defaultModel) {
|
|
90
|
+
if (entry?.name) {
|
|
91
|
+
const ref = parseModelRef(defaultModel, [entry])
|
|
92
|
+
if (ref.ok) return ref.model
|
|
93
|
+
}
|
|
94
|
+
return typeof entry?.model === "string" && entry.model ? entry.model : null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 运行时渠道/模型统一解析(#841 核单源——机制全文 PROVIDER.md §6.22;纯函数 · 零 I/O)。
|
|
98
|
+
* 三端(CLI ∥ VSC ∥ 桌面)消费同一函数:`loadConfig` 运行时 provider 取值 = 本函数产物(CLI ∥ 桌面
|
|
99
|
+
* 经装配自动同源);VSC 回合面直调(接入面自建链收正)。
|
|
100
|
+
*
|
|
101
|
+
* **渠道面回退序**:① 会话槽渠道(`slot.provider` 在场 ∧ 在册 ∧ 持 key ⇒ `source="slot"`);
|
|
102
|
+
* ② defaultModel 渠道(解析通过 ∧ 该渠道持 key ⇒ `source="defaultModel"`);③ 首个持 key 渠道
|
|
103
|
+
* (按表序 ⇒ `source="registry"`);④ 无 ⇒ `state="invalid"`(渠表空 ∥ 全表无 key)。
|
|
104
|
+
* 槽无 key ⇒ **跳过**(不把不可运行渠道钉进运行态)。
|
|
105
|
+
* **模型面**:① 入选来源 = 槽 ∧ 槽带模型 ⇒ 槽模型;② defaultModel 属入选渠道 ⇒ 其模型段;
|
|
106
|
+
* ③ 入选渠道单值;④ 无 ⇒ `null`(消费者沿既有「model 缺失」处置;明示词形随缺 = 仅渠道名)。
|
|
107
|
+
* **三态(config 级——KD-841-3:槽改写「谁在跑」,不改「默认模型是否有效」)**:`ok` =
|
|
108
|
+
* defaultModel 独立成立(解析通过 ∧ 该渠道持 key)⇒ 零明示;`fallback` =「无有效 defaultModel」类
|
|
109
|
+
* ∧ 全局有可运行渠道 ⇒ 可运行 + 明示必达;`invalid` =「无 provider/key」类 ⇒ 真无效 + 引导配置。
|
|
110
|
+
* `reason` = state≠ok 的语义源(消费者按 `state` 出词——**禁串嗅探**,非契约)。
|
|
111
|
+
* @param {{providers?: object[], defaultModel?: string|null, slot?: {provider?: string|null, model?: string|null}|null}} [input]
|
|
112
|
+
* @returns {{state: "ok"|"fallback"|"invalid", source: "slot"|"defaultModel"|"registry"|null, channel: string|null, model: string|null, provider: object, reason: string|null}} */
|
|
113
|
+
export function resolveProviderPlan({ providers, defaultModel, slot } = {}) {
|
|
114
|
+
const list = Array.isArray(providers) ? providers : []
|
|
115
|
+
const slotName = typeof slot?.provider === "string" && slot.provider ? slot.provider : null
|
|
116
|
+
const slotModel = typeof slot?.model === "string" && slot.model ? slot.model : null
|
|
117
|
+
|
|
118
|
+
// ── 渠道面回退序(三步)──
|
|
119
|
+
let entry = null
|
|
120
|
+
let source = null
|
|
121
|
+
if (slotName) {
|
|
122
|
+
const hit = list.find((p) => p?.name === slotName)
|
|
123
|
+
if (hit && hasKey(hit)) { entry = hit; source = "slot" } // 槽无 key ⇒ 跳过(落下一档)
|
|
124
|
+
}
|
|
125
|
+
const ref = parseModelRef(defaultModel, list)
|
|
126
|
+
if (!entry && ref.ok && hasKey(ref.provider)) { entry = ref.provider; source = "defaultModel" }
|
|
127
|
+
if (!entry) {
|
|
128
|
+
const first = list.find((p) => hasKey(p))
|
|
129
|
+
if (first) { entry = first; source = "registry" }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── 模型面(四步;无入选渠道 ⇒ null)──
|
|
133
|
+
const model = entry ? (source === "slot" && slotModel ? slotModel : resolveChannelModel(entry, defaultModel)) : null
|
|
134
|
+
|
|
135
|
+
// ── 三态(config 级)──
|
|
136
|
+
const dmOk = ref.ok && hasKey(ref.provider)
|
|
137
|
+
const state = !entry ? "invalid" : dmOk ? "ok" : "fallback"
|
|
138
|
+
|
|
139
|
+
// ── reason(语义源 · 逐档)──
|
|
140
|
+
let reason = null
|
|
141
|
+
if (state === "invalid") {
|
|
142
|
+
reason = list.length === 0
|
|
143
|
+
? "未配置任何 provider"
|
|
144
|
+
: "未配置 API 密钥(providers[].apiKey)——请先配置渠道密钥"
|
|
145
|
+
} else if (state === "fallback") {
|
|
146
|
+
reason = ref.ok
|
|
147
|
+
? `defaultModel 渠道 "${ref.provider.name}" 无 API 密钥——已回退到可用渠道`
|
|
148
|
+
: defaultModelReason(list, defaultModel)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
state,
|
|
153
|
+
source,
|
|
154
|
+
channel: entry?.name ?? null,
|
|
155
|
+
model,
|
|
156
|
+
provider: entry ? { ...entry, model } : {},
|
|
157
|
+
reason,
|
|
158
|
+
}
|
|
159
|
+
}
|
package/package.json
CHANGED
package/session-lifecycle.mjs
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync } from "node:fs"
|
|
21
21
|
import { resolveCompactThreshold } from "./config.mjs"
|
|
22
|
+
// 槽面判据单源(#841 KD-841-2:槽渠道 key 门 + 模型面序——PROVIDER.md §6.22)。
|
|
23
|
+
import { resolveProviderPlan } from "./model-ref.mjs"
|
|
22
24
|
import {
|
|
23
25
|
slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
|
|
24
26
|
writeEndMarker, sessionEnd, ownerPid, ownerPids,
|
|
@@ -158,18 +160,25 @@ export function applySession(agent, data, opts = {}) {
|
|
|
158
160
|
} else {
|
|
159
161
|
unbindRecordStore(agent)
|
|
160
162
|
}
|
|
161
|
-
// ── MODEL-MERGE-SESSION 恢复(F-4
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
// ── MODEL-MERGE-SESSION 恢复(F-4——两支;#841:槽面判据单源 = 统一解析第 1 步)──
|
|
164
|
+
// 槽渠道(`activeProvider` 在册 ∧ 持 key)⇒ 槽值设;槽无 key ∥ 不在册 ⇒ 跳过(落 config 链——
|
|
165
|
+
// 不把不可运行渠道钉进运行态;**跳过不写槽**——原槽值保留(D-S3 / KD-841-2))。判定 + 模型面
|
|
166
|
+
// 同源 = `resolveProviderPlan`(PROVIDER.md §6.22——零第二判据)。
|
|
167
|
+
const slotPlan = resolveProviderPlan({
|
|
168
|
+
providers: agent.providers,
|
|
169
|
+
defaultModel: agent.config?.defaultModel,
|
|
170
|
+
slot: { provider: data.activeProvider ?? null, model: data.activeModel ?? null },
|
|
171
|
+
})
|
|
172
|
+
if (slotPlan.source === "slot") {
|
|
173
|
+
const slotProvider = agent.providers?.find((pr) => pr.name === slotPlan.channel)
|
|
174
|
+
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空。
|
|
175
|
+
// 模型面序(#841):槽模型 → `defaultModel` 属本渠道 ⇒ 其模型段 → 渠道默认单值
|
|
176
|
+
// (`providers[].model`);三档皆无 ⇒ `null`(不落 model 键——下游沿既有「model 缺失」处置)。
|
|
177
|
+
// F-2d(MODEL-400-FIX)径保留:空串 / 非串槽模型同样兜(#638 非串 = 未登记)——slotPlan.model
|
|
178
|
+
// 已按同归一出值(单源,无第二判据)。
|
|
168
179
|
const prevName = agent.activeProvider
|
|
169
180
|
const prevModel = agent.activeModel ?? null
|
|
170
|
-
|
|
171
|
-
// 模型——类型脏值不入两状态字段(镜下方阈值重算门 ∕ 档位块 ∕ VSC `panel-turn-stages.mjs:53` 同形门)。
|
|
172
|
-
const slotModel = (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : slotProvider.model
|
|
181
|
+
const slotModel = slotPlan.model
|
|
173
182
|
const switched = prevName !== slotProvider.name || prevModel !== slotModel
|
|
174
183
|
agent.activeProvider = slotProvider.name
|
|
175
184
|
agent.activeModel = slotModel
|
|
@@ -200,8 +209,8 @@ export function applySession(agent, data, opts = {}) {
|
|
|
200
209
|
}
|
|
201
210
|
return switched
|
|
202
211
|
}
|
|
203
|
-
// ②
|
|
204
|
-
//
|
|
212
|
+
// ② 槽无 key ∥ 不在册 → D-S3 保留——静默保持现状(落 config 链:config 统一解析产物已是
|
|
213
|
+
// `agent.provider`;不报错不纠正——两方都无效由调用侧复验 validateProvider 弹重选)
|
|
205
214
|
return false
|
|
206
215
|
}
|
|
207
216
|
|