@thincoder/core 0.10.12 → 0.10.13
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/agent/run-stages.mjs +33 -29
- package/agent/setup.mjs +3 -0
- package/agent/turn-loop.mjs +3 -0
- package/agent-tools/batch-lifecycle.mjs +16 -3
- package/agent-tools/batch-skeleton.mjs +9 -1
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent.mjs +1 -0
- package/client-identity.mjs +54 -0
- package/config.mjs +19 -13
- package/context.mjs +5 -5
- package/doc-check/index.mjs +28 -15
- package/doc-check/paragraph.mjs +6 -3
- package/embedding.mjs +2 -0
- package/generate-title.mjs +3 -2
- package/ledger-close-proof.mjs +80 -0
- package/ledger-cmd.mjs +32 -23
- package/ledger-tools.mjs +4 -4
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/prompts/persona-engineering.md +2 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +19 -3
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +10 -4
- package/provider/list-models.mjs +4 -3
- package/provider/responses.mjs +17 -7
- package/session-lifecycle.mjs +3 -0
- package/token-window.mjs +23 -2
package/doc-check/paragraph.mjs
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync } from "node:fs"
|
|
17
17
|
|
|
18
|
+
/** 段判据类名单源(报面与统计同取此名——改报面忘改过滤的字面漂移,2026-10-26 实发过一次)。 */
|
|
19
|
+
export const DOC_CLASS = Object.freeze({ para: "文字太长,难于阅读", line: "行太长" })
|
|
20
|
+
|
|
18
21
|
/** 表格行谓词(markdown 表格行结构性不可折行——不计入段,与锚检查同源)。 */
|
|
19
22
|
export const isTableRow = (l) => /^\s*\|.*\|\s*$/.test(String(l))
|
|
20
23
|
|
|
@@ -62,7 +65,7 @@ export function inCodeSpan(line, idx) {
|
|
|
62
65
|
|
|
63
66
|
/** 段判据(阈值 = 调用方内置常量 `paragraphMax` ∥ `lineMax`;口径逐字 = §3.18 条目 Q):
|
|
64
67
|
* · 文档面:相邻非空正文行合并成一段,段长 = 组成行 `[...line].length` 之和 > `paragraphMax` ⇔ 命中
|
|
65
|
-
* (`clazz: "
|
|
68
|
+
* (`clazz: "文字太长,难于阅读"`,定位 = 段首行号)。正文行 = 非空 ∧ 非表格行 ∧ 非标题 ∧ 非列表项 ∧ 非引用行
|
|
66
69
|
* ∧ 非围栏(标记行与块内整行)∧ 非区带内;其余行类不计入、且把段断开。
|
|
67
70
|
* · 提示词面(`isPromptFace`):逐行裸判——`[...line].length` > `lineMax` ⇔ 命中(`clazz: "行太长"`,
|
|
68
71
|
* 无结构豁免)。
|
|
@@ -79,7 +82,7 @@ export function checkParagraphs({ paragraphMax, lineMax, exemptZones = [], files
|
|
|
79
82
|
if (isPromptFace(f)) {
|
|
80
83
|
text.split("\n").forEach((l, i) => {
|
|
81
84
|
const len = [...l].length
|
|
82
|
-
if (len > lineMax) hits.push({ file: f, line: i + 1, len, clazz:
|
|
85
|
+
if (len > lineMax) hits.push({ file: f, line: i + 1, len, clazz: DOC_CLASS.line })
|
|
83
86
|
})
|
|
84
87
|
continue
|
|
85
88
|
}
|
|
@@ -88,7 +91,7 @@ export function checkParagraphs({ paragraphMax, lineMax, exemptZones = [], files
|
|
|
88
91
|
let para = null // 在写的段 —— { start, len };非正文行 ⇒ 断开
|
|
89
92
|
const lines = text.split("\n")
|
|
90
93
|
const flush = () => {
|
|
91
|
-
if (para !== null && para.len > paragraphMax) hits.push({ file: f, line: para.start, len: para.len, clazz:
|
|
94
|
+
if (para !== null && para.len > paragraphMax) hits.push({ file: f, line: para.start, len: para.len, clazz: DOC_CLASS.para })
|
|
92
95
|
para = null
|
|
93
96
|
}
|
|
94
97
|
lines.forEach((l, i) => {
|
package/embedding.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { RETRYABLE_STATUS } from "./provider/index.mjs"
|
|
9
9
|
import { resolveEnvRefs } from "./env-ref.mjs"
|
|
10
|
+
import { withUserAgent } from "./client-identity.mjs"
|
|
10
11
|
// #859(MEMORY.md §6.3 ∥ §7 D-MEM31):嵌入路径发送前净化孤立 UTF-16 代理——sanitize 单源复用
|
|
11
12
|
import { sanitizeLoneSurrogates } from "./escape.mjs"
|
|
12
13
|
const MAX_RETRIES = 3
|
|
@@ -111,6 +112,7 @@ async function requestWithRetry(embedder, input, signal) {
|
|
|
111
112
|
response = await fetch(`${embedder.baseURL}/embeddings`, {
|
|
112
113
|
method: "POST",
|
|
113
114
|
headers: {
|
|
115
|
+
...withUserAgent(), // 产品标识头(embedding 无定制头字段——PROVIDER.md §6.17)
|
|
114
116
|
"Content-Type": "application/json",
|
|
115
117
|
Authorization: `Bearer ${embedder.apiKey}`,
|
|
116
118
|
},
|
package/generate-title.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { proxyFetch } from "./proxy.mjs"
|
|
12
|
+
import { withUserAgent } from "./client-identity.mjs"
|
|
12
13
|
import { resolveProviderSecrets } from "./env-ref.mjs"
|
|
13
14
|
// D7(2026-09-21 块标题行对齐批——标题链单源):首条真实 user 消息谓词收核单源
|
|
14
15
|
// (`isRealUserMsg`——纯函数零依赖,无环)。
|
|
@@ -97,8 +98,8 @@ export async function generateTitle(userContent, provider) {
|
|
|
97
98
|
body = JSON.stringify(bodyObj)
|
|
98
99
|
extract = (data) => data.choices?.[0]?.message?.content
|
|
99
100
|
}
|
|
100
|
-
//
|
|
101
|
-
const headers = { ...(provider.headers
|
|
101
|
+
// 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17)——定制头在前、内置头在后:内置头胜出;三格式各自内置头。
|
|
102
|
+
const headers = { ...withUserAgent(provider.headers), "Content-Type": "application/json" }
|
|
102
103
|
if (provider.format === "anthropic") {
|
|
103
104
|
headers["x-api-key"] = provider.apiKey
|
|
104
105
|
headers["anthropic-version"] = "2023-06-01"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger-close-proof.mjs — 台账核销机检单源(F-LX4 · 2026-10-26 批 §2;台账 #1807)。
|
|
3
|
+
*
|
|
4
|
+
* 判据(需求档 §② 功能点 11 / AC-M2-34):凡写 `已核销` 的语句,写库前判三件——① 行的 `task_book`
|
|
5
|
+
* 所指批档在盘 ② 该档 §1 状态行 = `已收口` ③ 该批档在 design token 槽的批次归属里有记录;缺一即拒、
|
|
6
|
+
* **一次报全缺项**、行零改。**判据只有目标值一条**——闸挂在写这个值的两处逐行共函数
|
|
7
|
+
* (`ledger-cmd.mjs` 的 `applyRowUpdate` / `applyRowClose`)之前,不判来源态。
|
|
8
|
+
*
|
|
9
|
+
* 住本档的理由(拆分件——先例 = `ledger-tools.mjs` 自 `ledger-cmd.mjs` 拆出):`ledger-cmd.mjs` 收尾
|
|
10
|
+
* 446 行,机检本体 + 三形拒因入册即越单档 500 顾问线(设计 §五「实落超 500 ⇒ 主动拆分」)。两枚指针
|
|
11
|
+
* 值谓词(`filePartOf`——设计档 §6.1 口径 1 单源 ∥ `namesPointer`)随迁本档,`ledger-cmd` 反向 import
|
|
12
|
+
* 取用——方向单向、无静态环。
|
|
13
|
+
*
|
|
14
|
+
* 只读:③ 只读归属旁列(designId → 批档路径),**从不读 token 值那一面**(`_engDesignTokens` /
|
|
15
|
+
* `readEngTokensFromSlot` 不入本档)——拒因文案里只有行 id ∥ `task_book` 值 ∥ 批档路径。
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from "node:fs"
|
|
18
|
+
|
|
19
|
+
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
20
|
+
import { batchPathKey, unionBatches } from "./agent-tools/design-slots.mjs"
|
|
21
|
+
import { isBatchRecord, readBatchStatusLine } from "./agent-tools/batch-skeleton.mjs"
|
|
22
|
+
import { loadManifest } from "./session-slots.mjs"
|
|
23
|
+
|
|
24
|
+
/** 指针 ∥ 批档值 → 文件部分(首个 `§` 前子串,trim——设计档 §6.1 口径 1 单源;`task_book` 与
|
|
25
|
+
* `batch` 同形同口径)。 */
|
|
26
|
+
export const filePartOf = (v) => (v == null ? "" : String(v).split("§")[0].trim())
|
|
27
|
+
|
|
28
|
+
/** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
|
|
29
|
+
export const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
30
|
+
|
|
31
|
+
/** 会话槽号(与 `token-ttl.mjs` `currentSlotNoReadOnly` 同一取值式:`_slot` 优先、未钉槽读 manifest
|
|
32
|
+
* `active`)——只用于 ③ 拒因分辨两种成因,不读任何 token 值面。 */
|
|
33
|
+
function slotNoOf(agent) {
|
|
34
|
+
if (Number.isInteger(agent?._slot)) return agent._slot
|
|
35
|
+
const active = loadManifest(agent?.cwd)?.active
|
|
36
|
+
return Number.isInteger(active) ? active : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** ③ 归属并集一次读入(严格形):`fnName` 在场 ⇒ 读失败即抛(必写 `已核销` 的批量径 ⇒ BEGIN 之前整批拒、
|
|
40
|
+
* 不进 skipped);缺省 ⇒ 返回错误对象(单行径 ∥ 纯字段补丁逐行报 ③)。 */
|
|
41
|
+
export function readCloseOutBatches(agent, fnName) {
|
|
42
|
+
try { return unionBatches(agent, { strict: true }) } catch (e) {
|
|
43
|
+
if (!fnName) return e
|
|
44
|
+
throw new Error(`${fnName}:核销机检未过(整批零行改——会话级读失败,与行无关)\n ③ 读不到 token 槽归属(${e?.message ?? e}):核销被拒、行零改;修好槽文件或权限后重试`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 核销机检(F-LX4 · 需求档 §② 功能点 11 / AC-M2-34):写 `已核销` 前判三件——① `task_book` 所指档在盘
|
|
49
|
+
* ② 该档 §1 = `已收口` ③ 该批档在 design token 槽的批次归属里有记录;**判据只有目标值一条**(两处写
|
|
50
|
+
* 这个值的 SQL 之前同挂),三件各判、**一次报全缺项**、行零改(只判不写)。③ = 值扫(`batchPathKey`
|
|
51
|
+
* 归一),只读归属旁列、从不读 token 值面。`batches` = 批量径预读(`Error` = 预读失败);缺省 ⇒ 此处读。 */
|
|
52
|
+
export function assertCloseOutProof(fnName, { agent, base, row, batches }) {
|
|
53
|
+
const missing = [] // 每项 = 一条拒因行(首字 = 缺项号)
|
|
54
|
+
let abs = null
|
|
55
|
+
if (!namesPointer(row?.task_book)) {
|
|
56
|
+
missing.push(" ① 行无 task_book 指针:追认核销须先挂批档(update 写 task_book),再跑全链")
|
|
57
|
+
} else {
|
|
58
|
+
const hit = resolveDeclaredRef(base, filePartOf(row.task_book))
|
|
59
|
+
if (!hit.ok) missing.push(` ① 批档不在盘(task_book = ${row.task_book}):先补指针,或恢复该档`)
|
|
60
|
+
else {
|
|
61
|
+
abs = hit.abs
|
|
62
|
+
const src = readFileSync(abs, "utf8")
|
|
63
|
+
if (!isBatchRecord(src)) missing.push(` ② 指针所指档不是批档(${abs} 无 §1 段):先把 task_book 改指批档`)
|
|
64
|
+
else {
|
|
65
|
+
const state = readBatchStatusLine(src)
|
|
66
|
+
if (state !== "closed") missing.push(` ② 批档未收口(${abs} §1 = ${state}):该批须先跑全链收口`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (abs !== null) {
|
|
71
|
+
let map = batches instanceof Map || batches instanceof Error ? batches : readCloseOutBatches(agent)
|
|
72
|
+
if (map instanceof Error) missing.push(` ③ 读不到 token 槽归属(${map?.message ?? map}):核销被拒、行零改;修好槽文件或权限后重试`)
|
|
73
|
+
else if (![...map].some(([, b]) => typeof b === "string" && batchPathKey(b) === batchPathKey(abs))) {
|
|
74
|
+
missing.push(slotNoOf(agent) == null
|
|
75
|
+
? " ③ 本会话无槽号(manifest 无 active):核销须在持有该批归属的会话里跑"
|
|
76
|
+
: " ③ 本会话槽内无该批档的归属:先确认是否换了会话——在跑该批设计评审的会话里核销;\n 确无归属 ⇒ 那次评审的 documents 未带该批档,须重跑")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (missing.length > 0) throw new Error(`${fnName}:核销机检未过(行 ${row?.id} 零改)——缺:${missing.map((t) => t.trim()[0]).join(" ")}\n${missing.join("\n")}`)
|
|
80
|
+
}
|
package/ledger-cmd.mjs
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
* 核销标记门(LIGHT-CHANNEL.md §2.5 · 台账 #1780):写入 `已核销` 的三径(close 单行 ∥ close 批量 ∥
|
|
16
16
|
* update 迁移——均经两逐行共函数)判**结果行标题**携 `ROUND_MARKER` ⇒ 拒;判据只认标题标记
|
|
17
17
|
* (不查批档 ∥ 不查评审实跑),撤回(→ 已废弃)不拦。
|
|
18
|
+
* 核销机检(F-LX4 · 2026-10-26 批 §2):同一两逐行共函数在写 `已核销` 前判三件(批档在盘 ∥ §1 =
|
|
19
|
+
* 已收口 ∥ 该批档在 design token 槽的批次归属有记录),缺一即拒、一次报全缺项、行零改;判据只有
|
|
20
|
+
* 目标值一条。本体住拆分件 `ledger-close-proof.mjs`(同 `ledger-tools.mjs` 先例);标记门在前
|
|
21
|
+
* (零成本字符串判先报),两门互不替代。
|
|
18
22
|
*
|
|
19
23
|
* 接线(设计档 §2.2 命令面接线):族出口 = `ledger.mjs`(re-export 两源——KD-M2-3);查询命令 ←
|
|
20
24
|
* tools/index.mjs(assembleBuiltinTools 全角色面);写命令 ← agent/family-tools.mjs(assembleFamilyTools
|
|
@@ -24,6 +28,7 @@
|
|
|
24
28
|
import { resolve } from "node:path"
|
|
25
29
|
import { AGING_DAYS, ALLOWED_MIGRATIONS, EVIDENCE_SOFT_LIMIT, nowIso, openLedger, PENDING_STATUSES, resolveLedgerRoot, ROUND_MARKER } from "./ledger-db.mjs"
|
|
26
30
|
import { resolveDeclaredRef } from "./declaration.mjs"
|
|
31
|
+
import { assertCloseOutProof, filePartOf, namesPointer, readCloseOutBatches } from "./ledger-close-proof.mjs"
|
|
27
32
|
|
|
28
33
|
/** 逐行老化判真(LEDGER.md §13.4 · 2026-10-07 批 ledger-tool):未决四态(`PENDING_STATUSES`)∧
|
|
29
34
|
* 行龄 > `AGING_DAYS` 天;行龄源 = `updated_at ?? created_at`(§5 同源);时间戳缺 / 不可解析
|
|
@@ -36,10 +41,6 @@ function isRowAged(row, now) {
|
|
|
36
41
|
return Number.isFinite(ms) && (now - ms) / 86400000 > AGING_DAYS
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
/** 指针 ∥ 批档值 → 文件部分(首个 `§` 前子串,trim——设计档 §6.1 口径 1 单源;`task_book` 与
|
|
40
|
-
* `batch` 同形同口径)。 */
|
|
41
|
-
const filePartOf = (v) => (v == null ? "" : String(v).split("§")[0].trim())
|
|
42
|
-
|
|
43
44
|
/** 解析基准(设计档 §6.1 口径 2 同一子表达式——与台账库键同源):`resolveLedgerRoot(cwd, boundary)?.root
|
|
44
45
|
* ?? resolve(cwd ?? ".")`。`boundary` = 会话锚(与 `openLedger` 同一枚上界——同一笔动作的判据不能两界)。 */
|
|
45
46
|
const baseOf = (cwd, boundary) => resolveLedgerRoot(cwd, boundary)?.root ?? resolve(cwd ?? ".")
|
|
@@ -136,9 +137,6 @@ export function ledgerEvidenceOversize({ cwd, boundary, task_book = null } = {})
|
|
|
136
137
|
return ledgerCount({ cwd, boundary, task_book }).evidenceOversize
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
/** 本笔指名谓词(设计档 §6.1 口径 3):显式空值(`null` / 空串 / 全空白) ≡ **未指名**——臂② 不触。 */
|
|
140
|
-
const namesPointer = (v) => v != null && String(v).trim() !== ""
|
|
141
|
-
|
|
142
140
|
/** 写门·指针点名(设计档 §6.1 两臂 · AC-M2-10 ∥ AC-M2-29):判**本笔写入面**——两臂之外零判。
|
|
143
141
|
* 臂①(点名非空):结果行 `status ∈ {在途, 待核销}` ∨ 结果行 `trigger = 归批` ⇒ `task_book` 非空
|
|
144
142
|
* (缺 / 空串 / 全空白 ⇒ 拒「必填」);**不判在盘**(列值事实,不随盘面翻转——DDL 咬合 CHECK 为该半幅防线)。
|
|
@@ -214,11 +212,11 @@ function assertEvidenceValue(fnName, evidence) {
|
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
/** 单行落笔·update(**单行形 ∥ 批量形共函数**——设计档 §3.3「逐行语义单源」):迁移表判 → 核销标记门
|
|
217
|
-
* (目标 = `已核销` ∧ 结果标题携标记 ⇒ 拒——判序 = LIGHT-CHANNEL.md §2.5)→
|
|
218
|
-
*
|
|
215
|
+
* (目标 = `已核销` ∧ 结果标题携标记 ⇒ 拒——判序 = LIGHT-CHANNEL.md §2.5)→ **核销机检**(目标 = `已核销`
|
|
216
|
+
* ⇒ `assertCloseOutProof`)→ executor / evidence 值计算(含两拒面)→ 写门 → UPDATE(判序 = §13.9 逐字未变)。`to` = `patch.status ?? 行现态`
|
|
219
217
|
* ——批量形纯字段补丁(不携 `status`)时 `to === row.status` ⇒ 迁移表不判、`status` 零写(§3.3)。
|
|
220
218
|
* `resume_when` = 普通可空字段补丁(`patch.resume_when ?? 行值`——§3.4)。 */
|
|
221
|
-
function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary) {
|
|
219
|
+
function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary, proof = {}) {
|
|
222
220
|
const to = patch.status ?? row.status
|
|
223
221
|
if (to !== row.status && !(ALLOWED_MIGRATIONS[row.status] ?? []).includes(to)) {
|
|
224
222
|
throw new Error(`ledgerUpdate:迁移 ${row.status} → ${to} 不在允许迁移表`)
|
|
@@ -227,6 +225,8 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary) {
|
|
|
227
225
|
if (to === "已核销" && String(patch.title ?? row.title ?? "").includes(ROUND_MARKER)) {
|
|
228
226
|
throw new Error(`ledgerUpdate:行标题仍携「${ROUND_MARKER}」——收尾链未跑完,不许核销(先跑收尾全链,再由父侧清标记)`)
|
|
229
227
|
}
|
|
228
|
+
// 核销机检(F-LX4):目标值 = `已核销` ⇒ 写库前三件校验(批档在盘 ∥ §1 已收口 ∥ 槽内有归属)
|
|
229
|
+
if (to === "已核销") assertCloseOutProof("ledgerUpdate", { agent: proof.agent, base: proof.base ?? baseOf(cwd, boundary), row, batches: proof.batches })
|
|
230
230
|
const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
|
|
231
231
|
const nextEvidence = evidenceValue("ledgerUpdate", row.evidence, patch.evidence, patch.evidenceReplace)
|
|
232
232
|
assertEvidenceValue("ledgerUpdate", patch.evidence)
|
|
@@ -249,27 +249,30 @@ function applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary) {
|
|
|
249
249
|
* 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。
|
|
250
250
|
* `evidence` 传值 = 追加缺省、`patch.evidenceReplace` = 布尔覆盖旗(§13.9——值计算 / 两拒面 = `evidenceValue`
|
|
251
251
|
* / `assertEvidenceValue`;判位 = 本语义段,随 executor 目标值同拍)。**单行形**(`id`)——逐行落笔 = `applyRowUpdate`。 */
|
|
252
|
-
export function ledgerUpdate({ cwd, id, patch, executorSessionId, boundary }) {
|
|
252
|
+
export function ledgerUpdate({ cwd, id, patch, executorSessionId, boundary, agent }) {
|
|
253
253
|
const db = openLedger(cwd, { create: true, boundary })
|
|
254
254
|
try {
|
|
255
255
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
256
256
|
if (!row) throw new Error(`ledgerUpdate:行 ${id} 不存在`)
|
|
257
|
-
const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary)
|
|
257
|
+
const { id: rowId } = applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary, { agent, base: baseOf(cwd, boundary) })
|
|
258
258
|
return { id: rowId }
|
|
259
259
|
} finally { db.close() }
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/** 单行落笔·close(**单行形 ∥ 批量形共函数**——设计档 §3.3「逐行语义单源」):源态判(核销仅三源——
|
|
263
263
|
* 在途不可跳)→ 核销标记门(LIGHT-CHANNEL.md §2.5——`已核销` ∧ 标题携标记 ⇒ 拒;撤回不拦)→
|
|
264
|
-
* 值计算(拒面① 旗独传判于此)→ 追认口 `evidence`
|
|
264
|
+
* **核销机检**(`已核销` ⇒ `assertCloseOutProof`)→ 值计算(拒面① 旗独传判于此)→ 追认口 `evidence`
|
|
265
|
+
* 门 → 拒面② → UPDATE。
|
|
265
266
|
* 撤回(任意态 → 已废弃——含在途直撤)同步 `executor = NULL`(LEDGER.md §3.1 ④);核销两源(勾销 /
|
|
266
267
|
* 追认)`executor` 零触碰(非在途出边)。 */
|
|
267
|
-
function applyRowClose(db, row, status, evidence, evidenceReplace) {
|
|
268
|
+
function applyRowClose(db, row, status, evidence, evidenceReplace, proof = {}) {
|
|
268
269
|
if (status === "已核销" && !["待讨论", "待设计", "待核销"].includes(row.status)) throw new Error(`ledgerClose:核销仅限 待讨论 / 待设计 / 待核销(现态 ${row.status})`)
|
|
269
270
|
// 核销标记门(LIGHT-CHANNEL.md §2.5):勾销 ∥ 追认同判;撤回(→ 已废弃)不入本门
|
|
270
271
|
if (status === "已核销" && String(row.title ?? "").includes(ROUND_MARKER)) {
|
|
271
272
|
throw new Error(`ledgerClose:行标题仍携「${ROUND_MARKER}」——收尾链未跑完,不许核销(先跑收尾全链,再由父侧清标记)`)
|
|
272
273
|
}
|
|
274
|
+
// 核销机检(F-LX4):判序 = 标记门之后(零成本字符串判先报)、值计算之前
|
|
275
|
+
if (status === "已核销") assertCloseOutProof("ledgerClose", { agent: proof.agent, base: proof.base, row, batches: proof.batches })
|
|
273
276
|
const nextEvidence = evidenceValue("ledgerClose", row.evidence, evidence, evidenceReplace)
|
|
274
277
|
if (status === "已核销" && ["待讨论", "待设计"].includes(row.status) && (nextEvidence == null || String(nextEvidence).trim() === "")) throw new Error(`ledgerClose:追认核销须带 evidence(现态 ${row.status})`)
|
|
275
278
|
assertEvidenceValue("ledgerClose", evidence) // 判序:拒面②(新值空白)落追认门之后 → UPDATE
|
|
@@ -283,10 +286,10 @@ function applyRowClose(db, row, status, evidence, evidenceReplace) {
|
|
|
283
286
|
* 可选 `evidence` 参:传值 = 追加缺省(§13.9——旧空直写 / 旧非空拼 `旧|新`)、`evidenceReplace: true`
|
|
284
287
|
* 同传 = 整段替换;勾销 / 追认 / 撤回三径同携(写值落行,内容不判);两拒面(旗独传 @值计算 ∥
|
|
285
288
|
* 新值空白 @追认门后)见 `evidenceValue` / `assertEvidenceValue`。
|
|
286
|
-
* 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → 值计算 →
|
|
287
|
-
* 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3
|
|
288
|
-
*
|
|
289
|
-
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace, boundary }) {
|
|
289
|
+
* 判序(同函数体同层,§13.9):目标集判 → 行取 → 源态判 → **核销机检**(`已核销` 才触)→ 值计算 →
|
|
290
|
+
* 追认门(原样——判**结果值**缺 / 全空白 ⇒ 拒,文案逐字不变)→ 拒面②(新值空白)→ UPDATE(LEDGER.md §3
|
|
291
|
+
* 收口两源)。**单行形**(`id`)——逐行落笔 = `applyRowClose`。 */
|
|
292
|
+
export function ledgerClose({ cwd, id, status, evidence, evidenceReplace, boundary, agent }) {
|
|
290
293
|
if (status !== "已核销" && status !== "已废弃") throw new Error(`ledgerClose:目标态 ${status} ∉ {已核销, 已废弃}`)
|
|
291
294
|
const db = openLedger(cwd, { create: true, boundary })
|
|
292
295
|
try {
|
|
@@ -294,7 +297,7 @@ export function ledgerClose({ cwd, id, status, evidence, evidenceReplace, bounda
|
|
|
294
297
|
try {
|
|
295
298
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
296
299
|
if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
|
|
297
|
-
const out = applyRowClose(db, row, status, evidence, evidenceReplace)
|
|
300
|
+
const out = applyRowClose(db, row, status, evidence, evidenceReplace, { agent, base: baseOf(cwd, boundary) })
|
|
298
301
|
db.exec("COMMIT")
|
|
299
302
|
return out
|
|
300
303
|
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
@@ -330,16 +333,20 @@ function resolveBatchRows(db, base, abs) {
|
|
|
330
333
|
* 并抛(零行改)——故「部分落地」只由逐行门拒产生,「中途失败」不留半批。
|
|
331
334
|
* `BEGIN IMMEDIATE`:写锁一次先把(逐行不升级锁 ⇒ 事务级失败在首行之前即失败)。
|
|
332
335
|
* @returns {{batch: string, status: string|null, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
333
|
-
export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId, boundary }) {
|
|
336
|
+
export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId, boundary, agent }) {
|
|
334
337
|
const db = openLedger(cwd, { create: true, boundary })
|
|
335
338
|
try {
|
|
336
339
|
const base = baseOf(cwd, boundary)
|
|
337
340
|
const abs = assertBatchArg("ledgerUpdate", base, batch)
|
|
341
|
+
// 核销机检 ③:迁移径(结果态 = `已核销`)在 BEGIN 之前一次读入、行间复用(严格读抛 = 会话级 ⇒ 整批拒);
|
|
342
|
+
// 纯字段补丁径也读一次(不抛):闸只对归档行触(存量已知接受),读失败向每行报 ③ 形——不改业务结果
|
|
343
|
+
// (那些行本就被拦),但会以逐行拒因呈现环境问题(父侧已知项,待裁)。
|
|
344
|
+
const proof = { agent, base, batches: patch.status === "已核销" ? readCloseOutBatches(agent, "ledgerUpdate") : patch.status == null ? readCloseOutBatches(agent) : undefined }
|
|
338
345
|
db.exec("BEGIN IMMEDIATE")
|
|
339
346
|
try {
|
|
340
347
|
const applied = [], skipped = []
|
|
341
348
|
for (const row of resolveBatchRows(db, base, abs)) {
|
|
342
|
-
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary).id) }
|
|
349
|
+
try { applied.push(applyRowUpdate(db, cwd, row, patch, executorSessionId, boundary, proof).id) }
|
|
343
350
|
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
344
351
|
}
|
|
345
352
|
db.exec("COMMIT")
|
|
@@ -355,12 +362,14 @@ export function ledgerUpdateBatch({ cwd, batch, patch = {}, executorSessionId, b
|
|
|
355
362
|
* (`evidenceValue` 同源)。回执 `status` 恒 `已核销`(批量形缺省 = 勾销单值;携则守卫已收窄为
|
|
356
363
|
* `{已核销}`——§3.2 `whenPresent`)。事务语义同 `ledgerUpdateBatch`。
|
|
357
364
|
* @returns {{batch: string, status: string, applied: number[], skipped: {id: number, from: string, why: string}[]}} */
|
|
358
|
-
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace, boundary }) {
|
|
365
|
+
export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace, boundary, agent }) {
|
|
359
366
|
const status = "已核销"
|
|
360
367
|
const db = openLedger(cwd, { create: true, boundary })
|
|
361
368
|
try {
|
|
362
369
|
const base = baseOf(cwd, boundary)
|
|
363
370
|
const abs = assertBatchArg("ledgerClose", base, batch)
|
|
371
|
+
// 核销机检 ③:本径恒写 `已核销` ⇒ 归属并集在 BEGIN 之前一次读入(严格读抛 = 会话级 ⇒ 整批拒、不进 skipped)
|
|
372
|
+
const proof = { agent, base, batches: readCloseOutBatches(agent, "ledgerClose") }
|
|
364
373
|
db.exec("BEGIN IMMEDIATE")
|
|
365
374
|
try {
|
|
366
375
|
const applied = [], skipped = []
|
|
@@ -371,7 +380,7 @@ export function ledgerCloseBatch({ cwd, batch, evidence, evidenceReplace, bounda
|
|
|
371
380
|
skipped.push({ id: Number(row.id), from: row.status, why })
|
|
372
381
|
continue
|
|
373
382
|
}
|
|
374
|
-
try { applied.push(applyRowClose(db, row, status, evidence, evidenceReplace).id) }
|
|
383
|
+
try { applied.push(applyRowClose(db, row, status, evidence, evidenceReplace, proof).id) }
|
|
375
384
|
catch (e) { skipped.push({ id: Number(row.id), from: row.status, why: String(e?.message ?? e) }) }
|
|
376
385
|
}
|
|
377
386
|
db.exec("COMMIT")
|
package/ledger-tools.mjs
CHANGED
|
@@ -305,14 +305,14 @@ function makeLedgerTool(actions, { readonly = false, writeFace = false } = {}) {
|
|
|
305
305
|
executorSessionId = getSessionId()
|
|
306
306
|
}
|
|
307
307
|
// 批级批量(§3.3)——`batch` 与 `id` 恰携其一(守卫 P7 已先裁)
|
|
308
|
-
if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId, boundary: anchor }) })
|
|
309
|
-
return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId, boundary: anchor }) })
|
|
308
|
+
if (batch != null) return JSON.stringify({ ...ledgerUpdateBatch({ cwd: target, batch, patch, executorSessionId, boundary: anchor, agent: ctx?.agent }) })
|
|
309
|
+
return JSON.stringify({ ...ledgerUpdate({ cwd: target, id, patch, executorSessionId, boundary: anchor, agent: ctx?.agent }) })
|
|
310
310
|
}
|
|
311
311
|
case "close": {
|
|
312
312
|
const target = startOf(ctx, anchor, rest.cwd, rest.batch, rest.task_book)
|
|
313
313
|
// 批级批量结算(§3.3——勾销径)
|
|
314
|
-
if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor }) })
|
|
315
|
-
return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor }) })
|
|
314
|
+
if (rest.batch != null) return JSON.stringify({ ...ledgerCloseBatch({ cwd: target, batch: rest.batch, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor, agent: ctx?.agent }) })
|
|
315
|
+
return JSON.stringify({ ...ledgerClose({ cwd: target, id: rest.id, status: rest.status, evidence: rest.evidence, evidenceReplace: rest.evidenceReplace, boundary: anchor, agent: ctx?.agent }) })
|
|
316
316
|
}
|
|
317
317
|
case "query": {
|
|
318
318
|
const target = startOf(ctx, anchor, rest.cwd, rest.task_book)
|
package/model-specs.mjs
CHANGED
|
@@ -235,11 +235,12 @@ const MODEL_SPECS = [
|
|
|
235
235
|
["doubao-seed-2-0-lite-260428", { context: 256_000, maxOutput: 131_072, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], multimodal: true }],
|
|
236
236
|
// doubao-seed-2-1 三档(火山方舟 · 2026-09-24 bench 名单批 · 三行同形):thinking / thinkApi / 枚举 = **实测**
|
|
237
237
|
// (裸请求 rc 在场;`reasoning_effort:"none"` → rc=0;七值全 200、乱值 400 泛化 ⇒ **受理级**——等级低于 2.0 行的
|
|
238
|
-
// 「真校验」,同值集不混级);maxOutput
|
|
239
|
-
// context
|
|
240
|
-
|
|
241
|
-
["doubao-seed-2-1-
|
|
242
|
-
["doubao-seed-2-1-
|
|
238
|
+
// 「真校验」,同值集不混级);context / maxOutput = **官方模型列表口径**(2026-10-26 实查:pro / lite 上下文 1024k、
|
|
239
|
+
// turbo 256k;三行最大回答 256k);旧值 context 256_000 / maxOutput 524_288 在档内无任何受理读数取证面,已退场。
|
|
240
|
+
// **视觉面未探**(不声明——如实标级,不冒充实测)。
|
|
241
|
+
["doubao-seed-2-1-pro-260915", { context: 1_024_000, maxOutput: 256_000, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
|
|
242
|
+
["doubao-seed-2-1-turbo-260628", { context: 256_000, maxOutput: 256_000, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
|
|
243
|
+
["doubao-seed-2-1-lite-260915", { context: 1_024_000, maxOutput: 256_000, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
|
|
243
244
|
// step-3.7-flash(StepFun · 2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧交付):**尺寸行**——context 262_144(多源 256K =
|
|
244
245
|
// 网络转述级;渠道未激活——400 "product is not activated");maxOutput 未取到 ⇒ 零口径——取兜底值 32_000(与建行前
|
|
245
246
|
// 生效值同值);行键 = 裸名(渠道名录形 stepfun/step-3.7-flash——命名空间剥离同命中)。
|
package/package.json
CHANGED
|
@@ -246,6 +246,8 @@ The round ledger row's title carries 「收尾链待跑」 — that marker is th
|
|
|
246
246
|
**Cross-session pickup**: at session start (on the same beat as "Re-check deferred batches first") ⇒ scan for unclosed light-channel rounds (the round row unsettled ∥ the batch record not closed out).
|
|
247
247
|
Found ⇒ raise the pickup reminder first, carrying the same four elements (round ∥ pen count ∥ steps owed ∥ one word runs it); the user's one word ⇒ take over and run the full closeout chain.
|
|
248
248
|
Closeout-chain segment map: §2 design formalization · §3 independent review · §4 approval · §6 closeout settlement; the round row settles only after the full chain passes (pen rows may settle with the closeout).
|
|
249
|
+
**Why the closeout chain cannot be skipped**: a light-channel pen changes code only — the design docs keep describing the code as it was before the pen, and no other step in the round pulls them back in sync.
|
|
250
|
+
The chain is that step: the design seat writes the code's current state back into the design docs, an independent review checks it, and the approval closes it; skipped, every later reader follows a description of code that no longer exists.
|
|
249
251
|
|
|
250
252
|
**A pen answers the three questions before it moves**: where the function lives now (a coordinate ∥ a doc section) · what it is called (the existing name, verbatim) · who uses it.
|
|
251
253
|
**The answer joins the pen's §1 entry in one of three shapes**:
|
package/provider/anthropic.mjs
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { specForModel, requestBudget } from "../config.mjs"
|
|
8
8
|
import { proxyFetch } from "../proxy.mjs"
|
|
9
|
+
import { withUserAgent } from "../client-identity.mjs"
|
|
9
10
|
import { requestWithRetry } from "./retry.mjs"
|
|
10
11
|
import { effectiveFetchTimeoutMs, effectiveBodyIdleMs } from "./core.mjs"
|
|
12
|
+
import { assertPromptFits } from "./errors.mjs"
|
|
11
13
|
import { abortError } from "../abort-provenance.mjs"
|
|
12
14
|
|
|
13
15
|
const ANTHROPIC_VERSION = "2023-06-01"
|
|
@@ -153,7 +155,7 @@ export function convertMessages(messages) {
|
|
|
153
155
|
* 2026-08-31 会诊 #6:接入 rateGate/recordRate + 429 Retry-After 单次重试
|
|
154
156
|
* (原实现完全绕过 TPM/RPM 闸门与记账——用户配了 tpm 以为受控实际不受控)。
|
|
155
157
|
* 注:5xx/网络退避重试未与 OpenAI 格式对齐(三 transport 共用那步工作量大,见报告)。 */
|
|
156
|
-
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, parallelToolCalls }) {
|
|
158
|
+
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, parallelToolCalls, logCtx }) {
|
|
157
159
|
// Extract system message(s) — Anthropic uses top-level `system` field
|
|
158
160
|
const systemMessages = []
|
|
159
161
|
const chatMessages = []
|
|
@@ -194,7 +196,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
194
196
|
// 2026-09-01:FETCH_TIMEOUT_MS 常量退役(绝对墙钟废除)——anthropic/responses 经 core.mjs 的
|
|
195
197
|
// effectiveFetchTimeoutMs 共用;响应头阶段 600s 默认,body 阶段 idle 超时。
|
|
196
198
|
const headers = {
|
|
197
|
-
...(provider.headers
|
|
199
|
+
...withUserAgent(provider.headers), // 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17);内置头在后故同名胜出
|
|
198
200
|
"Content-Type": "application/json",
|
|
199
201
|
"x-api-key": provider.apiKey,
|
|
200
202
|
"anthropic-version": ANTHROPIC_VERSION,
|
|
@@ -204,11 +206,13 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
204
206
|
if (signal?.aborted) throw abortError(signal, "provider", "transport-anthropic")
|
|
205
207
|
|
|
206
208
|
// 会诊 #6:TPM/RPM 闸门 + 记账(rate.mjs 与 OpenAI 格式共用同一窗口)
|
|
207
|
-
const { rateGate, recordRate } = await import("./rate.mjs")
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
})
|
|
209
|
+
const { rateGate, recordRate, estimateRequestTokens } = await import("./rate.mjs")
|
|
210
|
+
// §6.17 发送面净判:`estPrompt` = 输入侧**全量**(X 无 `max_tokens` 键 ⇒ 减项恒 0)——取调用方的
|
|
211
|
+
// messages(含 system)而不是 body.messages:anthropic 的 system 是顶层字段(`body.system`),
|
|
212
|
+
// 走 body 会把整块 system 漏在判据外。`estimated`(含 max_tokens)仍供 rateGate / recordRate。
|
|
213
|
+
const estPrompt = estimateRequestTokens({ messages, tools })
|
|
214
|
+
const estimated = estimateRequestTokens(body)
|
|
215
|
+
assertPromptFits(provider, estPrompt, logCtx)
|
|
212
216
|
await rateGate(provider, estimated, onWait, signal)
|
|
213
217
|
|
|
214
218
|
// 2026-08-31:4xx/5xx/网络与 OpenAI 格式统一退避重试链(原仅 429 Retry-After 单次重试,
|
|
@@ -230,6 +234,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
230
234
|
const result = await parseAnthropicStream(response, { onToken, onReasoning, signal })
|
|
231
235
|
recordRate(provider, estimated, result.usage)
|
|
232
236
|
const finished = finishAnthropic(result)
|
|
237
|
+
finished.estimated = estPrompt // §6.17 自校准采样:den 与净判同口径
|
|
233
238
|
// §6.27 告警投递:R7 的畸形参数告警并入既有 `_warnings` 机读线(消费面 agent/run-stages.mjs:44-45)
|
|
234
239
|
finished._warnings = [...(finished._warnings ?? []), ...warnings]
|
|
235
240
|
return finished
|
package/provider/core.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { providerSpec, requestBudget, resolveEnableThinking } from "../config.mj
|
|
|
8
8
|
import { DEFAULT_BODY_IDLE_MS } from "../config-defaults.mjs"
|
|
9
9
|
import { resolveProviderSecrets } from "../env-ref.mjs"
|
|
10
10
|
import { proxyFetch } from "../proxy.mjs"
|
|
11
|
+
import { withUserAgent } from "../client-identity.mjs"
|
|
11
12
|
import { escapeMessages, stripLocalMessageFields } from "../escape.mjs"
|
|
12
13
|
import { logEvent, errText, classifyErr, headText } from "../log.mjs"
|
|
13
14
|
import { abortError, annotateAbort, deathLine } from "../abort-provenance.mjs"
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
estimateRequestTokens, rateGate, recordRate,
|
|
21
22
|
} from "./rate.mjs"
|
|
22
23
|
// 2026-09-05 module-split:错误分类/流规则族迁 provider/errors.mjs(core.mjs 557 > 500 硬限)
|
|
23
|
-
import { parseRetryAfter, isNonRetryableError, betaBaseURL, compileStreamRules, assertProviderModel } from "./errors.mjs"
|
|
24
|
+
import { parseRetryAfter, isNonRetryableError, betaBaseURL, compileStreamRules, assertProviderModel, assertPromptFits, markContextOverflow } from "./errors.mjs"
|
|
24
25
|
// 测试 import 面(provider-stream/stream-rules)——core 曾直接 export 这两个
|
|
25
26
|
export { parseRetryAfter, compileStreamRules } from "./errors.mjs"
|
|
26
27
|
|
|
@@ -157,6 +158,8 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
157
158
|
messages,
|
|
158
159
|
tools: tools?.length ? normalizeTools(tools) : null,
|
|
159
160
|
onToken, onReasoning, onWait, signal, toolChoice,
|
|
161
|
+
// §6.17 豁免面:内部阶段标签(compress / distill / 续写)需传到发送面净判
|
|
162
|
+
logCtx,
|
|
160
163
|
})
|
|
161
164
|
return result
|
|
162
165
|
}
|
|
@@ -167,6 +170,8 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
167
170
|
messages,
|
|
168
171
|
tools: tools?.length ? normalizeTools(tools) : null,
|
|
169
172
|
onToken, onReasoning, onWait, signal, toolChoice,
|
|
173
|
+
// §6.17 豁免面:内部阶段标签(compress / distill / 续写)需传到发送面净判
|
|
174
|
+
logCtx,
|
|
170
175
|
})
|
|
171
176
|
return result
|
|
172
177
|
}
|
|
@@ -180,6 +185,7 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
180
185
|
messages,
|
|
181
186
|
tools,
|
|
182
187
|
onToken, onReasoning, onWait, signal, toolChoice,
|
|
188
|
+
logCtx, // §6.17 豁免面(compress / distill / 续写)
|
|
183
189
|
})
|
|
184
190
|
}
|
|
185
191
|
|
|
@@ -250,10 +256,15 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
250
256
|
if (parallelToolCalls === true) body.parallel_tool_calls = true
|
|
251
257
|
|
|
252
258
|
const estimated = estimateRequestTokens(body)
|
|
259
|
+
// §6.17 发送面净判(第二道):`estPrompt` = 输入侧估算——不减掉 `max_tokens` 会把输出预算白扣一份
|
|
260
|
+
const estPrompt = estimated - (body.max_tokens ?? 0)
|
|
261
|
+
assertPromptFits(provider, estPrompt, logCtx)
|
|
253
262
|
await rateGate(provider, estimated, onWait, signal)
|
|
254
263
|
|
|
255
264
|
const response = await requestWithRetry(provider, body, signal, onWait)
|
|
256
265
|
const result = await readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns, idleMs: effectiveBodyIdleMs(provider) })
|
|
266
|
+
// §6.17 自校准采样:同一个 estPrompt 随 result 返回(den 与净判同口径)
|
|
267
|
+
result.estimated = estPrompt
|
|
257
268
|
recordRate(provider, estimated, result.usage)
|
|
258
269
|
|
|
259
270
|
// Stream rule triggered, user interrupted, or network partial — return immediately.
|
|
@@ -286,6 +297,7 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
286
297
|
}
|
|
287
298
|
|
|
288
299
|
if (!spec.partialMode && !spec.prefixMode) return result
|
|
300
|
+
let continuationRuns = 0
|
|
289
301
|
for (let n = 0; result.finishReason === "length" && result.content && n < MAX_CONTINUATIONS; n++) {
|
|
290
302
|
let continued
|
|
291
303
|
try {
|
|
@@ -312,6 +324,7 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
312
324
|
result.content += continued.content
|
|
313
325
|
result.reasoning += continued.reasoning ?? ""
|
|
314
326
|
mergeRetryToolCalls(result, continued.toolCalls)
|
|
327
|
+
continuationRuns++
|
|
315
328
|
result.finishReason = continued.finishReason
|
|
316
329
|
if (continued.usage) {
|
|
317
330
|
const sum = (k) => (result.usage?.[k] ?? 0) + (continued.usage[k] ?? 0)
|
|
@@ -324,6 +337,9 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
|
|
|
324
337
|
}
|
|
325
338
|
}
|
|
326
339
|
}
|
|
340
|
+
// §6.17 自校准口径:续写链把 `usage.prompt_tokens` 累加了 N 份,而 `estimated` 只有一份输入侧估算
|
|
341
|
+
// ⇒ 这一轮不采样(否则池化比被抬成 ~N 倍,全部估算支虚高、提前压缩)
|
|
342
|
+
if (continuationRuns > 0) result.estimated = undefined
|
|
327
343
|
return result
|
|
328
344
|
}
|
|
329
345
|
|
|
@@ -398,7 +414,7 @@ async function requestWithRetry(provider, body, signal, onWait) {
|
|
|
398
414
|
const opts = {
|
|
399
415
|
method: "POST",
|
|
400
416
|
headers: {
|
|
401
|
-
...(provider.headers
|
|
417
|
+
...withUserAgent(provider.headers), // 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17)
|
|
402
418
|
"Content-Type": "application/json",
|
|
403
419
|
Authorization: `Bearer ${provider.apiKey}`,
|
|
404
420
|
},
|
|
@@ -446,7 +462,7 @@ async function requestWithRetry(provider, body, signal, onWait) {
|
|
|
446
462
|
message += ` [auth diag: baseURL=${host} key=${masked} status=${response.status}]`
|
|
447
463
|
}
|
|
448
464
|
lastStatus = response.status
|
|
449
|
-
if (isNonRetryableError(response.status, text)) throw new Error(message)
|
|
465
|
+
if (isNonRetryableError(response.status, text)) throw markContextOverflow(new Error(message), response.status, text)
|
|
450
466
|
if (response.status === 429) {
|
|
451
467
|
const waitMs = parseRetryAfter(response.headers.get("retry-after"), rateLimitHits)
|
|
452
468
|
rateLimitHits++
|
package/provider/errors.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { RETRYABLE_STATUS, RATE_LIMIT_BACKOFF_MS } from "./rate.mjs"
|
|
11
|
+
import { requestBudget } from "../config.mjs"
|
|
11
12
|
|
|
12
13
|
/** Parse Retry-After: 秒数 or HTTP-date;上限 300s(会诊 #11)— 异常头不得让 CLI 睡数小时。
|
|
13
14
|
* header 缺失/非法时退回指数退避表(rateLimitHits 计数取档)。 */
|
|
@@ -132,3 +133,43 @@ export function classifyResponsesErrorCode(code) {
|
|
|
132
133
|
return null
|
|
133
134
|
}
|
|
134
135
|
}
|
|
136
|
+
|
|
137
|
+
/** 越窗报文特征(§6.17 HTTP 400 分类——三个渠道的逐字形态;词表就这一处)。 */
|
|
138
|
+
const CONTEXT_OVERFLOW_PHRASES = ["maximum context length", "context_length_exceeded", "prompt is too long"]
|
|
139
|
+
|
|
140
|
+
/** 越窗 typed 错误(§6.17):三实数逐字带出(prompt / max_tokens / 窗口,另附 limit);kind / retryable
|
|
141
|
+
* 复用同族既有词表(`classifyResponsesErrorCode("context_length_exceeded")`),零新词。 */
|
|
142
|
+
export function contextOverflowError(provider, prompt, budget, remedy) {
|
|
143
|
+
const cls = classifyResponsesErrorCode("context_length_exceeded")
|
|
144
|
+
const err = new Error(
|
|
145
|
+
`context overflow: prompt ~${prompt} tokens · max_tokens ${budget.maxTokens ?? "not sent"}` +
|
|
146
|
+
` · window ${budget.window} (limit ${budget.limit}) — ${remedy} [model ${provider?.model ?? "unknown"}]`,
|
|
147
|
+
)
|
|
148
|
+
err.kind = cls.kind
|
|
149
|
+
err.retryable = cls.retryable
|
|
150
|
+
return err
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 发送面净判(§6.17 第二道,四个装配面同形一次调用):`estPrompt` = 输入侧估算
|
|
154
|
+
* (= `estimateRequestTokens(X) − (body.max_tokens ?? 0)`,四面同一定义——不减会把输出预算白扣一份)。
|
|
155
|
+
* 内部阶段豁免:`compress` / `distill` / 续写本身就是越线后用来把上下文降下来的那次调用,
|
|
156
|
+
* 判它 = 判掉止血动作本身(标签已存在,零新增机制)。 */
|
|
157
|
+
export function assertPromptFits(provider, estPrompt, logCtx) {
|
|
158
|
+
const stage = logCtx?.stage
|
|
159
|
+
if (stage === "compress" || stage === "distill" || logCtx?.isContinuation === true) return
|
|
160
|
+
const budget = requestBudget(provider)
|
|
161
|
+
if (!(estPrompt > budget.limit)) return
|
|
162
|
+
throw contextOverflowError(provider, estPrompt, budget, "the request was not sent; reduce the conversation (/compact or prune) before retrying")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** HTTP 400 越窗报文 ⇒ typed 错误(§6.17);未命中 ⇒ 原样返回入参错误(不发明分类)。 */
|
|
166
|
+
export function markContextOverflow(error, status, text) {
|
|
167
|
+
if (status !== 400) return error
|
|
168
|
+
const lower = String(text ?? "").toLowerCase()
|
|
169
|
+
if (!CONTEXT_OVERFLOW_PHRASES.some((p) => lower.includes(p))) return error
|
|
170
|
+
const cls = classifyResponsesErrorCode("context_length_exceeded")
|
|
171
|
+
error.kind = cls.kind
|
|
172
|
+
error.retryable = cls.retryable
|
|
173
|
+
if (cls.hint) error.message += `(${cls.hint})`
|
|
174
|
+
return error
|
|
175
|
+
}
|